From 865880d50281818bcdb23eba8fa93b04d7c10cb2 Mon Sep 17 00:00:00 2001 From: dranik Date: Thu, 14 May 2026 16:28:31 +0300 Subject: [PATCH] fixed send vpn:// close app --- client/CMakeLists.txt | 14 +++ client/amneziaApplication.cpp | 60 ++++++++--- client/amneziaApplication.h | 2 + client/main.cpp | 1 + tools/deeplink/README.md | 20 ++++ tools/deeplink/vpn-deeplink-demo.html | 143 ++++++++++++++++++++++++++ 6 files changed, 227 insertions(+), 13 deletions(-) create mode 100644 tools/deeplink/README.md create mode 100644 tools/deeplink/vpn-deeplink-demo.html diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index 2f138157d..486c4d230 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -214,6 +214,20 @@ else() qt_finalize_target(${PROJECT}) endif() +if(IS_LSREGISTER_MACOS) + if(APPLE AND NOT IOS AND CMAKE_HOST_SYSTEM_NAME STREQUAL "Darwin") + add_custom_command( + TARGET ${PROJECT} POST_BUILD + COMMAND + /System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister + -f + -R + $ + COMMENT "lsregister: register $ with Launch Services" + ) + endif() +endif() + install(TARGETS ${PROJECT} DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT AmneziaVPN diff --git a/client/amneziaApplication.cpp b/client/amneziaApplication.cpp index 8af526aa3..01e81a9ba 100644 --- a/client/amneziaApplication.cpp +++ b/client/amneziaApplication.cpp @@ -32,6 +32,17 @@ bool AmneziaApplication::m_forceQuit = false; +#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) && !defined(MACOS_NE) +namespace { +bool g_secondaryInstanceForDeepLink = false; +} + +void AmneziaApplication::markSecondaryInstanceForDeepLink() +{ + g_secondaryInstanceForDeepLink = true; +} +#endif + AmneziaApplication::AmneziaApplication(int &argc, char *argv[]) : AMNEZIA_BASE_CLASS(argc, argv), m_optAutostart({QStringLiteral("a"), QStringLiteral("autostart")}, QStringLiteral("System autostart")), m_optCleanup ({QStringLiteral("c"), QStringLiteral("cleanup")}, QStringLiteral("Cleanup logs")), @@ -239,9 +250,14 @@ void AmneziaApplication::init() if (!m_parser.isSet(m_optImport)) { const QString vpnArg = vpnUrlFromArguments(QCoreApplication::arguments()); if (!vpnArg.isEmpty()) { + m_pendingVpnDeepLink.clear(); QTimer::singleShot(0, this, [this, vpnArg]() { deliverVpnDeepLink(vpnArg); }); } } + if (!m_pendingVpnDeepLink.isEmpty()) { + const QString pending = std::move(m_pendingVpnDeepLink); + QTimer::singleShot(0, this, [this, pending]() { deliverVpnDeepLink(pending); }); + } #endif } @@ -355,8 +371,22 @@ void AmneziaApplication::deliverVpnDeepLink(const QString &payload) m_coreController->openVpnKeyImportPreview(trimmed); } -#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) && !defined(MACOS_NE) -namespace { +QString vpnPayloadFromFileOpenUrl(const QUrl &url) +{ + const QString decoded = url.toString(QUrl::PrettyDecoded); + const int idx = decoded.indexOf(QLatin1String("vpn://"), 0, Qt::CaseInsensitive); + if (idx >= 0) { + qDebug() << "vpn://" << decoded; + return decoded.mid(idx).trimmed(); + } + if (url.scheme().compare(QLatin1String("vpn"), Qt::CaseInsensitive) == 0) { + qDebug() << "vpn://" << decoded; + return decoded.trimmed(); + } + return {}; +} + +#if !defined(MACOS_NE) bool forwardVpnPayloadToPrimaryInstance(const QString &payload) { if (payload.trimmed().isEmpty()) { @@ -373,35 +403,39 @@ bool forwardVpnPayloadToPrimaryInstance(const QString &payload) socket.flush(); return true; } -} // namespace #endif bool AmneziaApplication::event(QEvent *event) { -#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) if (event->type() == QEvent::FileOpen) { auto *foe = static_cast(event); - const QUrl url = foe->url(); - if (url.scheme().compare(QLatin1String("vpn"), Qt::CaseInsensitive) == 0) { - const QString payload = url.toString(QUrl::PrettyDecoded); + const QUrl &url = foe->url(); + qDebug() << "url:" << url; + const QString payload = vpnPayloadFromFileOpenUrl(url); + qDebug() << "payload" << payload; + if (!payload.isEmpty()) { + if (m_coreController) { + QTimer::singleShot(0, this, [this, payload]() { deliverVpnDeepLink(payload); }); + return true; + } #if !defined(MACOS_NE) - // Secondary instance: main() exits before init(), so m_coreController is null; browsers often - // pass the URL only via QFileOpenEvent (not argv). Forward to the running primary process. - if (!m_coreController) { + // True secondary process (main skipped init): forward to the primary instance. + if (g_secondaryInstanceForDeepLink) { if (forwardVpnPayloadToPrimaryInstance(payload)) { qInfo().noquote() << "Forwarded vpn deep link to primary instance, bytes:" << payload.size(); QTimer::singleShot(0, qApp, &QCoreApplication::quit); return true; } - qWarning() << "vpn FileOpen: no CoreController and could not reach primary instance (socket)"; + qWarning() << "vpn FileOpen: secondary instance could not reach primary (socket)"; return true; } #endif - QTimer::singleShot(0, this, [this, payload]() { deliverVpnDeepLink(payload); }); + // Cold start: FileOpen can arrive while init() is still running (CoreController not ready yet). + // Do not forward to our own local server — queue and flush at end of init(). + m_pendingVpnDeepLink = payload; return true; } } -#endif return AMNEZIA_BASE_CLASS::event(event); } #endif diff --git a/client/amneziaApplication.h b/client/amneziaApplication.h index 9220b665f..fed7e8ac1 100644 --- a/client/amneziaApplication.h +++ b/client/amneziaApplication.h @@ -44,6 +44,7 @@ public: #if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) && !defined(MACOS_NE) void startLocalServer(); + static void markSecondaryInstanceForDeepLink(); #endif QQmlApplicationEngine *qmlEngine() const; @@ -72,6 +73,7 @@ private: #if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) void deliverVpnDeepLink(const QString &payload); + QString m_pendingVpnDeepLink; #endif QSharedPointer m_vpnConnection; diff --git a/client/main.cpp b/client/main.cpp index 3239b01b9..c87069e76 100644 --- a/client/main.cpp +++ b/client/main.cpp @@ -75,6 +75,7 @@ int main(int argc, char *argv[]) #if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) && !defined(MACOS_NE) const QString vpnFromArgv = findVpnDeepLinkInArguments(QCoreApplication::arguments()); if (notifyRunningInstanceOrExit(app, vpnFromArgv)) { + AmneziaApplication::markSecondaryInstanceForDeepLink(); return app.exec(); } app.startLocalServer(); diff --git a/tools/deeplink/README.md b/tools/deeplink/README.md new file mode 100644 index 000000000..aff52413d --- /dev/null +++ b/tools/deeplink/README.md @@ -0,0 +1,20 @@ +# Демо: `vpn://` из браузера (как `tg://`) + +Браузер показывает диалог «разрешить сайту открывать ссылки **vpn** через приложение» только если: + +1. Страница открыта по **HTTPS** (или `localhost` в части конфигураций; для надёжного сценария как у `tlgrm.ru` — **настоящий TLS**). +2. В ОС зарегистрирован обработчик схемы **`vpn`** → AmneziaVPN (см. [AH-355-deep-link-approval-and-operations.md](../../docs/plans/AH-355-deep-link-approval-and-operations.md)). +3. Переход на `vpn://` сделан **жестом пользователя** (клик по ссылке / кнопке), а не только автозапуск при загрузке страницы (политики браузера могут блокировать). + +## Файлы + +- [vpn-deeplink-demo.html](vpn-deeplink-demo.html) — поле для вставки/редактирования `vpn://…`, кнопки открытия и **лог на странице** (+ дублирование в консоль браузера). Открывайте **по HTTPS**. + +## Windows: регистрация `vpn` + +Один раз запустите установленный AmneziaVPN — при первом запуске клиент записывает обработчик в реестр пользователя (`HKCU\Software\Classes\vpn`). Без этого шага Браузер может не предложить Amnezia. + +## Проверка конкурента за схему `vpn` + +- **Windows:** «Параметры приложений по умолчанию» → протоколы / сопоставления URI, или `regedit` → `HKEY_CURRENT_USER\Software\Classes\vpn`. +- **macOS:** при конфликте система откроет не то приложение; проверьте «Открыть с помощью» для тестового `vpn://` в Safari. diff --git a/tools/deeplink/vpn-deeplink-demo.html b/tools/deeplink/vpn-deeplink-demo.html new file mode 100644 index 000000000..602f8ebab --- /dev/null +++ b/tools/deeplink/vpn-deeplink-demo.html @@ -0,0 +1,143 @@ + + + + + + Amnezia — тест vpn:// из браузера + + + +

Тест deep link vpn://

+ +
+ Откройте эту страницу по HTTPS (см. docs/deeplink/README.md). + Клик по ссылке / кнопке должен быть жестом пользователя (как у tg:// с https-страницы). +
+ + + +

Если строка не начинается с vpn://, префикс будет добавлен автоматически.

+ +
+ Открыть (ссылка <a>) + + + +
+ +

Ожидание: Chrome спросит разрешение на схему vpn → AmneziaVPN → экран предпросмотра импорта.

+ + +
+ + + +