mirror of
https://github.com/amnezia-vpn/amnezia-client.git
synced 2026-06-21 02:01:03 +07:00
fixed send vpn:// close app
This commit is contained in:
@@ -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
|
||||
$<TARGET_BUNDLE_DIR:${PROJECT}>
|
||||
COMMENT "lsregister: register $<TARGET_BUNDLE_DIR:${PROJECT}> with Launch Services"
|
||||
)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
install(TARGETS ${PROJECT}
|
||||
DESTINATION ${CMAKE_INSTALL_BINDIR}
|
||||
COMPONENT AmneziaVPN
|
||||
|
||||
@@ -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<QFileOpenEvent *>(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
|
||||
|
||||
@@ -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<VpnConnection> m_vpnConnection;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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.
|
||||
@@ -0,0 +1,143 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Amnezia — тест vpn:// из браузера</title>
|
||||
<style>
|
||||
body { font-family: system-ui, sans-serif; max-width: 48rem; margin: 2rem auto; padding: 0 1rem; line-height: 1.5; }
|
||||
code { background: #eee; padding: 0.1rem 0.35rem; border-radius: 4px; }
|
||||
.warn { background: #fff3cd; padding: 1rem; border-radius: 8px; margin: 1rem 0; }
|
||||
label { display: block; font-weight: 600; margin-top: 1rem; }
|
||||
textarea {
|
||||
width: 100%; min-height: 6rem; box-sizing: border-box; font-family: ui-monospace, monospace;
|
||||
font-size: 0.85rem; padding: 0.5rem; border: 1px solid #ccc; border-radius: 6px; resize: vertical;
|
||||
}
|
||||
.row { display: flex; flex-wrap: wrap; gap: 0.5rem; margin: 0.75rem 0; align-items: center; }
|
||||
a.button, button {
|
||||
display: inline-block; padding: 0.55rem 1rem;
|
||||
background: #1a73e8; color: #fff; text-decoration: none; border-radius: 6px; border: none; font-size: 0.95rem; cursor: pointer;
|
||||
}
|
||||
button.secondary { background: #333; }
|
||||
button.muted { background: #666; }
|
||||
#log {
|
||||
margin-top: 1rem; padding: 0.75rem; background: #1e1e1e; color: #d4d4d4; border-radius: 6px;
|
||||
font-family: ui-monospace, monospace; font-size: 0.75rem; max-height: 14rem; overflow-y: auto; white-space: pre-wrap; word-break: break-all;
|
||||
}
|
||||
#log .line { border-bottom: 1px solid #333; padding: 0.25rem 0; }
|
||||
#log .time { color: #6a9955; margin-right: 0.5rem; }
|
||||
.hint { color: #555; font-size: 0.9rem; margin-top: 0.25rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Тест deep link <code>vpn://</code></h1>
|
||||
|
||||
<div class="warn">
|
||||
Откройте эту страницу по <strong>HTTPS</strong> (см. <code>docs/deeplink/README.md</code>).
|
||||
Клик по ссылке / кнопке должен быть <strong>жестом пользователя</strong> (как у <code>tg://</code> с https-страницы).
|
||||
</div>
|
||||
|
||||
<label for="vpnInput">Строка ключа (целиком <code>vpn://…</code> или только хвост после <code>vpn://</code>)</label>
|
||||
<textarea id="vpnInput" spellcheck="false" placeholder="vpn://AAAA_… или AAAA_…"></textarea>
|
||||
<p class="hint">Если строка не начинается с <code>vpn://</code>, префикс будет добавлен автоматически.</p>
|
||||
|
||||
<div class="row">
|
||||
<a class="button" id="vpnLink" href="#">Открыть (ссылка <a>)</a>
|
||||
<button type="button" id="vpnBtn">Открыть (кнопка → location)</button>
|
||||
<button type="button" class="secondary" id="copyBtn">Копировать URL</button>
|
||||
<button type="button" class="muted" id="clearLogBtn">Очистить лог</button>
|
||||
</div>
|
||||
|
||||
<p>Ожидание: Chrome спросит разрешение на схему <code>vpn</code> → AmneziaVPN → экран предпросмотра импорта.</p>
|
||||
|
||||
<label>Лог (на странице; дублируется в консоль DevTools)</label>
|
||||
<div id="log" aria-live="polite"></div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
var input = document.getElementById("vpnInput");
|
||||
var link = document.getElementById("vpnLink");
|
||||
var logEl = document.getElementById("log");
|
||||
|
||||
function ts() {
|
||||
return new Date().toISOString();
|
||||
}
|
||||
|
||||
function log(msg) {
|
||||
var line = document.createElement("div");
|
||||
line.className = "line";
|
||||
var t = document.createElement("span");
|
||||
t.className = "time";
|
||||
t.textContent = ts();
|
||||
line.appendChild(t);
|
||||
line.appendChild(document.createTextNode(msg));
|
||||
logEl.insertBefore(line, logEl.firstChild);
|
||||
try { console.log(ts(), msg); } catch (e) {}
|
||||
}
|
||||
|
||||
function buildHref() {
|
||||
var raw = (input.value || "").trim();
|
||||
if (!raw) {
|
||||
log("Пустое поле — вставьте ключ.");
|
||||
return "";
|
||||
}
|
||||
var href = raw.indexOf("vpn://") === 0 ? raw : "vpn://" + raw;
|
||||
log("Собран URL, длина символов: " + href.length);
|
||||
return href;
|
||||
}
|
||||
|
||||
function syncLinkHref() {
|
||||
var href = buildHref();
|
||||
if (href) {
|
||||
link.setAttribute("href", href);
|
||||
log("href ссылки обновлён (первые 80 символов): " + href.slice(0, 80) + (href.length > 80 ? "…" : ""));
|
||||
} else {
|
||||
link.setAttribute("href", "#");
|
||||
}
|
||||
}
|
||||
|
||||
input.addEventListener("input", function () {
|
||||
syncLinkHref();
|
||||
});
|
||||
|
||||
link.addEventListener("click", function (ev) {
|
||||
var href = link.getAttribute("href");
|
||||
log("Клик по ссылке, href.length=" + (href ? href.length : 0));
|
||||
if (!href || href === "#") {
|
||||
ev.preventDefault();
|
||||
log("Отмена: нет валидного href.");
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById("vpnBtn").addEventListener("click", function () {
|
||||
var href = buildHref();
|
||||
if (!href) return;
|
||||
log("Кнопка: переход window.location.href, длина=" + href.length);
|
||||
window.location.href = href;
|
||||
});
|
||||
|
||||
document.getElementById("copyBtn").addEventListener("click", function () {
|
||||
var href = buildHref();
|
||||
if (!href) return;
|
||||
navigator.clipboard.writeText(href).then(function () {
|
||||
log("Скопировано в буфер обмена.");
|
||||
}).catch(function (err) {
|
||||
log("Копирование не удалось: " + (err && err.message ? err.message : String(err)));
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById("clearLogBtn").addEventListener("click", function () {
|
||||
logEl.textContent = "";
|
||||
log("Лог очищен.");
|
||||
});
|
||||
|
||||
window.addEventListener("pagehide", function () {
|
||||
log("pagehide (страница уходит / навигация)");
|
||||
});
|
||||
|
||||
log("Страница загружена. Вставьте ключ и нажмите ссылку или кнопку.");
|
||||
syncLinkHref();
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user