mirror of
https://github.com/amnezia-vpn/amnezia-client.git
synced 2026-06-22 02:01:08 +07:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7e0748f7b4 | |||
| 02e55424ad | |||
| c90107bb7a | |||
| 4ae2b32083 | |||
| 865880d502 | |||
| 7c8613f19a | |||
| 9dc7e430a3 | |||
| 2850f05be0 | |||
| e2108a28ea |
@@ -214,6 +214,28 @@ else()
|
|||||||
qt_finalize_target(${PROJECT})
|
qt_finalize_target(${PROJECT})
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
if(IS_REGISTER_VPN_URL)
|
||||||
|
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"
|
||||||
|
)
|
||||||
|
elseif(WIN32)
|
||||||
|
add_custom_command(
|
||||||
|
TARGET ${PROJECT} POST_BUILD
|
||||||
|
COMMAND ${CMAKE_COMMAND}
|
||||||
|
-DEXE_PATH=$<TARGET_FILE:${PROJECT}>
|
||||||
|
-P "${CMAKE_SOURCE_DIR}/cmake/register_vpn_url_win.cmake"
|
||||||
|
COMMENT "HKCU: register vpn\\shell\\open\\command -> $<TARGET_FILE:${PROJECT}>"
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
|
||||||
install(TARGETS ${PROJECT}
|
install(TARGETS ${PROJECT}
|
||||||
DESTINATION ${CMAKE_INSTALL_BINDIR}
|
DESTINATION ${CMAKE_INSTALL_BINDIR}
|
||||||
COMPONENT AmneziaVPN
|
COMPONENT AmneziaVPN
|
||||||
|
|||||||
+185
-24
@@ -15,6 +15,9 @@
|
|||||||
#include <QEvent>
|
#include <QEvent>
|
||||||
#include <QDir>
|
#include <QDir>
|
||||||
#include <QSettings>
|
#include <QSettings>
|
||||||
|
#include <QFileOpenEvent>
|
||||||
|
#include <QUrl>
|
||||||
|
#include <QCoreApplication>
|
||||||
#include <QtQuick/QQuickWindow>
|
#include <QtQuick/QQuickWindow>
|
||||||
#include <QWindow>
|
#include <QWindow>
|
||||||
|
|
||||||
@@ -29,6 +32,17 @@
|
|||||||
|
|
||||||
bool AmneziaApplication::m_forceQuit = false;
|
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),
|
AmneziaApplication::AmneziaApplication(int &argc, char *argv[]) : AMNEZIA_BASE_CLASS(argc, argv),
|
||||||
m_optAutostart({QStringLiteral("a"), QStringLiteral("autostart")}, QStringLiteral("System autostart")),
|
m_optAutostart({QStringLiteral("a"), QStringLiteral("autostart")}, QStringLiteral("System autostart")),
|
||||||
m_optCleanup ({QStringLiteral("c"), QStringLiteral("cleanup")}, QStringLiteral("Cleanup logs")),
|
m_optCleanup ({QStringLiteral("c"), QStringLiteral("cleanup")}, QStringLiteral("Cleanup logs")),
|
||||||
@@ -61,26 +75,55 @@ AmneziaApplication::AmneziaApplication(int &argc, char *argv[]) : AMNEZIA_BASE_C
|
|||||||
AmneziaApplication::~AmneziaApplication()
|
AmneziaApplication::~AmneziaApplication()
|
||||||
{
|
{
|
||||||
#ifdef AMNEZIA_DESKTOP
|
#ifdef AMNEZIA_DESKTOP
|
||||||
if (m_vpnConnection && m_vpnConnectionThread.isRunning()) {
|
if (m_vpnConnection) {
|
||||||
QMetaObject::invokeMethod(m_vpnConnection.get(), "disconnectSlots", Qt::BlockingQueuedConnection);
|
m_vpnConnection->disconnectSlots();
|
||||||
|
m_vpnConnection->disconnectFromVpn();
|
||||||
QMetaObject::invokeMethod(m_vpnConnection.get(), "disconnectFromVpn", Qt::BlockingQueuedConnection);
|
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
m_vpnConnectionThread.requestInterruption();
|
|
||||||
m_vpnConnectionThread.quit();
|
|
||||||
|
|
||||||
if (!m_vpnConnectionThread.wait(3000)) {
|
|
||||||
m_vpnConnectionThread.terminate();
|
|
||||||
m_vpnConnectionThread.wait(500);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (m_engine) {
|
if (m_engine) {
|
||||||
delete m_engine;
|
delete m_engine;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
|
||||||
|
namespace {
|
||||||
|
QString vpnUrlFromArguments(const QStringList &args)
|
||||||
|
{
|
||||||
|
for (const QString &arg : args) {
|
||||||
|
const QString t = arg.trimmed();
|
||||||
|
if (t.startsWith(QLatin1String("vpn://"), Qt::CaseInsensitive)) {
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if defined(Q_OS_WIN) && !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
|
||||||
|
namespace {
|
||||||
|
void registerWindowsVpnUrlSchemeIfNeeded()
|
||||||
|
{
|
||||||
|
QSettings flag(ORGANIZATION_NAME, APPLICATION_NAME);
|
||||||
|
if (flag.value(QStringLiteral("protocolHandler/vpnRegistered")).toBool()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString exe = QDir::toNativeSeparators(QCoreApplication::applicationFilePath());
|
||||||
|
|
||||||
|
QSettings vpnKey(QStringLiteral("HKEY_CURRENT_USER\\Software\\Classes\\vpn"), QSettings::NativeFormat);
|
||||||
|
vpnKey.setValue(QStringLiteral("."), QStringLiteral("URL:AmneziaVPN"));
|
||||||
|
vpnKey.setValue(QStringLiteral("URL Protocol"), QString());
|
||||||
|
|
||||||
|
QSettings cmdKey(QStringLiteral("HKEY_CURRENT_USER\\Software\\Classes\\vpn\\shell\\open\\command"), QSettings::NativeFormat);
|
||||||
|
cmdKey.setValue(QStringLiteral("."), QStringLiteral("\"%1\" \"%2\"").arg(exe, QStringLiteral("%1")));
|
||||||
|
|
||||||
|
flag.setValue(QStringLiteral("protocolHandler/vpnRegistered"), true);
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
#endif
|
||||||
|
|
||||||
#ifdef Q_OS_ANDROID
|
#ifdef Q_OS_ANDROID
|
||||||
namespace {
|
namespace {
|
||||||
static void clearQtCaches()
|
static void clearQtCaches()
|
||||||
@@ -133,9 +176,6 @@ void AmneziaApplication::init()
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
m_vpnConnection.reset(new VpnConnection(nullptr, nullptr));
|
m_vpnConnection.reset(new VpnConnection(nullptr, nullptr));
|
||||||
m_vpnConnection->moveToThread(&m_vpnConnectionThread);
|
|
||||||
m_vpnConnectionThread.start();
|
|
||||||
|
|
||||||
m_coreController.reset(new CoreController(m_vpnConnection, m_settings, m_engine));
|
m_coreController.reset(new CoreController(m_vpnConnection, m_settings, m_engine));
|
||||||
|
|
||||||
m_engine->addImportPath("qrc:/ui/qml/Modules/");
|
m_engine->addImportPath("qrc:/ui/qml/Modules/");
|
||||||
@@ -190,6 +230,23 @@ void AmneziaApplication::init()
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
|
||||||
|
# ifdef Q_OS_WIN
|
||||||
|
registerWindowsVpnUrlSchemeIfNeeded();
|
||||||
|
# endif
|
||||||
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
void AmneziaApplication::registerTypes()
|
void AmneziaApplication::registerTypes()
|
||||||
@@ -250,20 +307,124 @@ bool AmneziaApplication::parseCommands()
|
|||||||
}
|
}
|
||||||
|
|
||||||
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) && !defined(MACOS_NE)
|
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) && !defined(MACOS_NE)
|
||||||
void AmneziaApplication::startLocalServer() {
|
void AmneziaApplication::startLocalServer()
|
||||||
const QString serverName("AmneziaVPNInstance");
|
{
|
||||||
|
const QString serverName(QStringLiteral("AmneziaVPNInstance"));
|
||||||
QLocalServer::removeServer(serverName);
|
QLocalServer::removeServer(serverName);
|
||||||
|
|
||||||
QLocalServer *server = new QLocalServer(this);
|
QLocalServer *server = new QLocalServer(this);
|
||||||
server->listen(serverName);
|
if (!server->listen(serverName)) {
|
||||||
|
qWarning() << "QLocalServer::listen failed:" << server->errorString();
|
||||||
|
}
|
||||||
|
|
||||||
QObject::connect(server, &QLocalServer::newConnection, this, [server, this]() {
|
QObject::connect(server, &QLocalServer::newConnection, this, [this, server]() {
|
||||||
if (server) {
|
QLocalSocket *sock = server->nextPendingConnection();
|
||||||
QLocalSocket *clientConnection = server->nextPendingConnection();
|
if (!sock) {
|
||||||
clientConnection->deleteLater();
|
return;
|
||||||
}
|
}
|
||||||
emit m_coreController->pageController()->raiseMainWindow(); //TODO
|
|
||||||
});
|
QString vpnPayload;
|
||||||
|
if (sock->waitForReadyRead(3000)) {
|
||||||
|
const QByteArray buf = sock->readAll();
|
||||||
|
static const QByteArray prefix = QByteArrayLiteral("VPN\n");
|
||||||
|
if (buf.startsWith(prefix)) {
|
||||||
|
vpnPayload = QString::fromUtf8(buf.mid(prefix.size())).trimmed();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sock->deleteLater();
|
||||||
|
|
||||||
|
if (!vpnPayload.isEmpty()) {
|
||||||
|
QTimer::singleShot(0, this, [this, vpnPayload]() { deliverVpnDeepLink(vpnPayload); });
|
||||||
|
}
|
||||||
|
|
||||||
|
QTimer::singleShot(0, this, [this]() {
|
||||||
|
if (m_coreController && m_coreController->pageController()) {
|
||||||
|
emit m_coreController->pageController()->raiseMainWindow();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, Qt::QueuedConnection);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
|
||||||
|
void AmneziaApplication::deliverVpnDeepLink(const QString &payload)
|
||||||
|
{
|
||||||
|
if (!m_coreController) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const QString trimmed = payload.trimmed();
|
||||||
|
if (trimmed.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_coreController->openVpnKeyImportPreview(trimmed);
|
||||||
|
}
|
||||||
|
|
||||||
|
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()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
QLocalSocket socket;
|
||||||
|
socket.connectToServer(QStringLiteral("AmneziaVPNInstance"));
|
||||||
|
if (!socket.waitForConnected(800)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const QByteArray msg = QByteArrayLiteral("VPN\n") + payload.toUtf8() + '\n';
|
||||||
|
socket.write(msg);
|
||||||
|
socket.waitForBytesWritten(3000);
|
||||||
|
socket.flush();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
bool AmneziaApplication::event(QEvent *event)
|
||||||
|
{
|
||||||
|
if (event->type() == QEvent::FileOpen) {
|
||||||
|
auto *foe = static_cast<QFileOpenEvent *>(event);
|
||||||
|
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)
|
||||||
|
// 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: secondary instance could not reach primary (socket)";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return AMNEZIA_BASE_CLASS::event(event);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|||||||
@@ -5,13 +5,15 @@
|
|||||||
#include <QNetworkAccessManager>
|
#include <QNetworkAccessManager>
|
||||||
#include <QQmlApplicationEngine>
|
#include <QQmlApplicationEngine>
|
||||||
#include <QQmlContext>
|
#include <QQmlContext>
|
||||||
#include <QThread>
|
|
||||||
#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS)
|
#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS)
|
||||||
#include <QGuiApplication>
|
#include <QGuiApplication>
|
||||||
#else
|
#else
|
||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
#endif
|
#endif
|
||||||
#include <QClipboard>
|
#include <QClipboard>
|
||||||
|
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
|
||||||
|
#include <QEvent>
|
||||||
|
#endif
|
||||||
|
|
||||||
#include "core/controllers/coreController.h"
|
#include "core/controllers/coreController.h"
|
||||||
#include "secureQSettings.h"
|
#include "secureQSettings.h"
|
||||||
@@ -41,6 +43,7 @@ public:
|
|||||||
|
|
||||||
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) && !defined(MACOS_NE)
|
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) && !defined(MACOS_NE)
|
||||||
void startLocalServer();
|
void startLocalServer();
|
||||||
|
static void markSecondaryInstanceForDeepLink();
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
QQmlApplicationEngine *qmlEngine() const;
|
QQmlApplicationEngine *qmlEngine() const;
|
||||||
@@ -67,12 +70,19 @@ private:
|
|||||||
QCommandLineOption m_optConnect;
|
QCommandLineOption m_optConnect;
|
||||||
QCommandLineOption m_optImport;
|
QCommandLineOption m_optImport;
|
||||||
|
|
||||||
|
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
|
||||||
|
void deliverVpnDeepLink(const QString &payload);
|
||||||
|
QString m_pendingVpnDeepLink;
|
||||||
|
#endif
|
||||||
|
|
||||||
QSharedPointer<VpnConnection> m_vpnConnection;
|
QSharedPointer<VpnConnection> m_vpnConnection;
|
||||||
QThread m_vpnConnectionThread;
|
|
||||||
|
|
||||||
QNetworkAccessManager *m_nam;
|
QNetworkAccessManager *m_nam;
|
||||||
protected:
|
protected:
|
||||||
bool eventFilter(QObject *watched, QEvent *event) override;
|
bool eventFilter(QObject *watched, QEvent *event) override;
|
||||||
|
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
|
||||||
|
bool event(QEvent *event) override;
|
||||||
|
#endif
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // AMNEZIA_APPLICATION_H
|
#endif // AMNEZIA_APPLICATION_H
|
||||||
|
|||||||
@@ -243,7 +243,10 @@ class AmneziaActivity : QtActivity() {
|
|||||||
override fun onNewIntent(intent: Intent?) {
|
override fun onNewIntent(intent: Intent?) {
|
||||||
super.onNewIntent(intent)
|
super.onNewIntent(intent)
|
||||||
Log.v(TAG, "onNewIntent: $intent")
|
Log.v(TAG, "onNewIntent: $intent")
|
||||||
intent?.let(::processIntent)
|
intent?.let {
|
||||||
|
setIntent(it)
|
||||||
|
processIntent(it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun processIntent(intent: Intent) {
|
private fun processIntent(intent: Intent) {
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ class ImportConfigActivity : ComponentActivity() {
|
|||||||
override fun onNewIntent(intent: Intent) {
|
override fun onNewIntent(intent: Intent) {
|
||||||
super.onNewIntent(intent)
|
super.onNewIntent(intent)
|
||||||
Log.v(TAG, "onNewIntent: $intent")
|
Log.v(TAG, "onNewIntent: $intent")
|
||||||
|
setIntent(intent)
|
||||||
intent.let(::readConfig)
|
intent.let(::readConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ set(HEADERS ${HEADERS}
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QtAppDelegate.h
|
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QtAppDelegate.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/StoreKitController.h
|
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/StoreKitController.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QtAppDelegate-C-Interface.h
|
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QtAppDelegate-C-Interface.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/AmneziaOpenUrlImport.h
|
||||||
)
|
)
|
||||||
set_source_files_properties(${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ios_controller.h PROPERTIES OBJECTIVE_CPP_HEADER TRUE)
|
set_source_files_properties(${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ios_controller.h PROPERTIES OBJECTIVE_CPP_HEADER TRUE)
|
||||||
|
|
||||||
@@ -47,6 +48,7 @@ set(SOURCES ${SOURCES}
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QtAppDelegate.mm
|
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QtAppDelegate.mm
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/StoreKitController.mm
|
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/StoreKitController.mm
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/AmneziaSceneDelegateHooks.mm
|
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/AmneziaSceneDelegateHooks.mm
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/AmneziaOpenUrlImport.mm
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,11 @@ set(LIBS ${LIBS}
|
|||||||
|
|
||||||
set_target_properties(${PROJECT} PROPERTIES
|
set_target_properties(${PROJECT} PROPERTIES
|
||||||
MACOSX_BUNDLE TRUE
|
MACOSX_BUNDLE TRUE
|
||||||
|
MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_SOURCE_DIR}/macos/app/Info.plist.in"
|
||||||
|
MACOSX_BUNDLE_GUI_IDENTIFIER "${BUILD_OSX_APP_IDENTIFIER}"
|
||||||
|
MACOSX_BUNDLE_BUNDLE_NAME "AmneziaVPN"
|
||||||
|
MACOSX_BUNDLE_COPYRIGHT ""
|
||||||
|
MACOSX_BUNDLE_ICON_FILE "app.icns"
|
||||||
MACOSX_BUNDLE_SHORT_VERSION_STRING "${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}.${CMAKE_PROJECT_VERSION_PATCH}"
|
MACOSX_BUNDLE_SHORT_VERSION_STRING "${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}.${CMAKE_PROJECT_VERSION_PATCH}"
|
||||||
MACOSX_BUNDLE_BUNDLE_VERSION "${CMAKE_PROJECT_VERSION_TWEAK}"
|
MACOSX_BUNDLE_BUNDLE_VERSION "${CMAKE_PROJECT_VERSION_TWEAK}"
|
||||||
)
|
)
|
||||||
@@ -35,7 +40,6 @@ set(SOURCES ${SOURCES}
|
|||||||
|
|
||||||
|
|
||||||
set(ICON_FILE ${CMAKE_CURRENT_SOURCE_DIR}/images/app.icns)
|
set(ICON_FILE ${CMAKE_CURRENT_SOURCE_DIR}/images/app.icns)
|
||||||
set(MACOSX_BUNDLE_ICON_FILE app.icns)
|
|
||||||
set_source_files_properties(${ICON_FILE} PROPERTIES MACOSX_PACKAGE_LOCATION Resources)
|
set_source_files_properties(${ICON_FILE} PROPERTIES MACOSX_PACKAGE_LOCATION Resources)
|
||||||
set(SOURCES ${SOURCES} ${ICON_FILE})
|
set(SOURCES ${SOURCES} ${ICON_FILE})
|
||||||
|
|
||||||
|
|||||||
@@ -360,3 +360,19 @@ void CoreController::importConfigFromData(const QString &data)
|
|||||||
m_importController->importConfig();
|
m_importController->importConfig();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void CoreController::openVpnKeyImportPreview(const QString &data)
|
||||||
|
{
|
||||||
|
if (!m_importController || data.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
emit m_pageController->goToPageHome();
|
||||||
|
if (!m_importController->extractConfigFromData(data)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
emit m_pageController->goToPageViewConfig();
|
||||||
|
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
|
||||||
|
emit m_pageController->raiseMainWindow();
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|||||||
@@ -119,6 +119,8 @@ public:
|
|||||||
|
|
||||||
void openConnectionByIndex(int serverIndex);
|
void openConnectionByIndex(int serverIndex);
|
||||||
void importConfigFromData(const QString &data);
|
void importConfigFromData(const QString &data);
|
||||||
|
/** Navigate home, parse key, open preview (same path as mobile deep link / share). */
|
||||||
|
void openVpnKeyImportPreview(const QString &data);
|
||||||
void updateTranslator(const QLocale &locale);
|
void updateTranslator(const QLocale &locale);
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
|
|||||||
@@ -377,10 +377,8 @@ void CoreSignalHandlers::initAndroidConnectionHandler()
|
|||||||
m_coreController->m_connectionController->restoreConnection();
|
m_coreController->m_connectionController->restoreConnection();
|
||||||
});
|
});
|
||||||
connect(AndroidController::instance(), &AndroidController::importConfigFromOutside, this, [this](QString data) {
|
connect(AndroidController::instance(), &AndroidController::importConfigFromOutside, this, [this](QString data) {
|
||||||
emit m_coreController->m_pageController->goToPageHome();
|
m_coreController->openVpnKeyImportPreview(data);
|
||||||
m_coreController->m_importController->extractConfigFromData(data);
|
|
||||||
data.clear();
|
data.clear();
|
||||||
emit m_coreController->m_pageController->goToPageViewConfig();
|
|
||||||
});
|
});
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
@@ -389,9 +387,7 @@ void CoreSignalHandlers::initIosImportHandler()
|
|||||||
{
|
{
|
||||||
#ifdef Q_OS_IOS
|
#ifdef Q_OS_IOS
|
||||||
connect(IosController::Instance(), &IosController::importConfigFromOutside, this, [this](QString data) {
|
connect(IosController::Instance(), &IosController::importConfigFromOutside, this, [this](QString data) {
|
||||||
emit m_coreController->m_pageController->goToPageHome();
|
m_coreController->openVpnKeyImportPreview(data);
|
||||||
m_coreController->m_importController->extractConfigFromData(data);
|
|
||||||
emit m_coreController->m_pageController->goToPageViewConfig();
|
|
||||||
});
|
});
|
||||||
connect(IosController::Instance(), &IosController::importBackupFromOutside, this, [this](QString filePath) {
|
connect(IosController::Instance(), &IosController::importBackupFromOutside, this, [this](QString filePath) {
|
||||||
emit m_coreController->m_pageController->goToPageHome();
|
emit m_coreController->m_pageController->goToPageHome();
|
||||||
|
|||||||
@@ -86,6 +86,19 @@
|
|||||||
<dict/>
|
<dict/>
|
||||||
<key>CFBundleIcons~ipad</key>
|
<key>CFBundleIcons~ipad</key>
|
||||||
<dict/>
|
<dict/>
|
||||||
|
<key>CFBundleURLTypes</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleURLName</key>
|
||||||
|
<string>org.amnezia.AmneziaVPN.vpn-deeplink</string>
|
||||||
|
<key>CFBundleURLSchemes</key>
|
||||||
|
<array>
|
||||||
|
<string>vpn</string>
|
||||||
|
</array>
|
||||||
|
<key>CFBundleTypeRole</key>
|
||||||
|
<string>Editor</string>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
<key>UTImportedTypeDeclarations</key>
|
<key>UTImportedTypeDeclarations</key>
|
||||||
<array>
|
<array>
|
||||||
<dict>
|
<dict>
|
||||||
|
|||||||
@@ -10,6 +10,8 @@
|
|||||||
<string>${QT_INTERNAL_DOLLAR_VAR}{PRODUCT_NAME}</string>
|
<string>${QT_INTERNAL_DOLLAR_VAR}{PRODUCT_NAME}</string>
|
||||||
<key>CFBundleExecutable</key>
|
<key>CFBundleExecutable</key>
|
||||||
<string>${MACOSX_BUNDLE_EXECUTABLE_NAME}</string>
|
<string>${MACOSX_BUNDLE_EXECUTABLE_NAME}</string>
|
||||||
|
<key>CFBundleIconFile</key>
|
||||||
|
<string>${MACOSX_BUNDLE_ICON_FILE}</string>
|
||||||
<key>CFBundleIdentifier</key>
|
<key>CFBundleIdentifier</key>
|
||||||
<string>${MACOSX_BUNDLE_GUI_IDENTIFIER}</string>
|
<string>${MACOSX_BUNDLE_GUI_IDENTIFIER}</string>
|
||||||
<key>CFBundleInfoDictionaryVersion</key>
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
@@ -46,6 +48,19 @@
|
|||||||
</dict>
|
</dict>
|
||||||
<key>CFBundleIcons</key>
|
<key>CFBundleIcons</key>
|
||||||
<dict/>
|
<dict/>
|
||||||
|
<key>CFBundleURLTypes</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleURLName</key>
|
||||||
|
<string>${MACOSX_BUNDLE_GUI_IDENTIFIER}.vpn-deeplink</string>
|
||||||
|
<key>CFBundleURLSchemes</key>
|
||||||
|
<array>
|
||||||
|
<string>vpn</string>
|
||||||
|
</array>
|
||||||
|
<key>CFBundleTypeRole</key>
|
||||||
|
<string>Editor</string>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
<key>UTImportedTypeDeclarations</key>
|
<key>UTImportedTypeDeclarations</key>
|
||||||
<array>
|
<array>
|
||||||
<dict>
|
<dict>
|
||||||
|
|||||||
+35
-9
@@ -1,3 +1,5 @@
|
|||||||
|
#include <QByteArray>
|
||||||
|
#include <QCoreApplication>
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
|
|
||||||
@@ -6,7 +8,7 @@
|
|||||||
#include "core/utils/migrations.h"
|
#include "core/utils/migrations.h"
|
||||||
#include "version.h"
|
#include "version.h"
|
||||||
|
|
||||||
#include <QTimer>
|
#include <QLocalSocket>
|
||||||
|
|
||||||
#ifdef Q_OS_WIN
|
#ifdef Q_OS_WIN
|
||||||
#include "Windows.h"
|
#include "Windows.h"
|
||||||
@@ -17,18 +19,41 @@
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) && !defined(MACOS_NE)
|
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) && !defined(MACOS_NE)
|
||||||
bool isAnotherInstanceRunning()
|
namespace {
|
||||||
|
QString findVpnDeepLinkInArguments(const QStringList &args)
|
||||||
|
{
|
||||||
|
for (const QString &arg : args) {
|
||||||
|
const QString t = arg.trimmed();
|
||||||
|
if (t.startsWith(QLatin1String("vpn://"), Qt::CaseInsensitive)) {
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
bool notifyRunningInstanceOrExit(AmneziaApplication &app, const QString &vpnPayload)
|
||||||
{
|
{
|
||||||
QLocalSocket socket;
|
QLocalSocket socket;
|
||||||
socket.connectToServer("AmneziaVPNInstance");
|
socket.connectToServer(QStringLiteral("AmneziaVPNInstance"));
|
||||||
if (socket.waitForConnected(500)) {
|
if (!socket.waitForConnected(500)) {
|
||||||
qWarning() << "AmneziaVPN is already running";
|
return false;
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
return false;
|
qWarning() << "AmneziaVPN is already running";
|
||||||
|
if (!vpnPayload.isEmpty()) {
|
||||||
|
const QByteArray msg = QByteArrayLiteral("VPN\n") + vpnPayload.toUtf8() + '\n';
|
||||||
|
socket.write(msg);
|
||||||
|
socket.waitForBytesWritten(3000);
|
||||||
|
}
|
||||||
|
socket.flush();
|
||||||
|
QTimer::singleShot(1000, &app, [&app]() { app.quit(); });
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
} // namespace
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
// Desktop (non-NE): single-instance IPC forwards vpn:// to the running process. MACOS_NE has no IPC here;
|
||||||
|
// deep links use argv / QFileOpenEvent after registration in the app bundle Info.plist.
|
||||||
|
|
||||||
int main(int argc, char *argv[])
|
int main(int argc, char *argv[])
|
||||||
{
|
{
|
||||||
Migrations migrationsManager;
|
Migrations migrationsManager;
|
||||||
@@ -48,8 +73,9 @@ int main(int argc, char *argv[])
|
|||||||
OsSignalHandler::setup();
|
OsSignalHandler::setup();
|
||||||
|
|
||||||
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) && !defined(MACOS_NE)
|
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) && !defined(MACOS_NE)
|
||||||
if (isAnotherInstanceRunning()) {
|
const QString vpnFromArgv = findVpnDeepLinkInArguments(QCoreApplication::arguments());
|
||||||
QTimer::singleShot(1000, &app, [&]() { app.quit(); });
|
if (notifyRunningInstanceOrExit(app, vpnFromArgv)) {
|
||||||
|
AmneziaApplication::markSecondaryInstanceForDeepLink();
|
||||||
return app.exec();
|
return app.exec();
|
||||||
}
|
}
|
||||||
app.startLocalServer();
|
app.startLocalServer();
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/** Handles custom scheme vpn:// (full absoluteString) and file URLs for config / backup import. */
|
||||||
|
void AmneziaHandleOpenUrl(NSURL *url);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
#import "AmneziaOpenUrlImport.h"
|
||||||
|
|
||||||
|
#include "ios_controller.h"
|
||||||
|
|
||||||
|
#include <QFile>
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
#include <dispatch/dispatch.h>
|
||||||
|
|
||||||
|
void AmneziaHandleOpenUrl(NSURL *url)
|
||||||
|
{
|
||||||
|
if (!url) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
NSString *scheme = url.scheme ? [url.scheme lowercaseString] : @"";
|
||||||
|
if ([scheme isEqualToString:@"vpn"]) {
|
||||||
|
NSString *absolute = url.absoluteString;
|
||||||
|
if (absolute.length == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
||||||
|
IosController::Instance()->importConfigFromOutside(QString::fromUtf8([absolute UTF8String]));
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!url.isFileURL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString filePath = QString::fromUtf8([url.path UTF8String]);
|
||||||
|
if (filePath.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
||||||
|
if (filePath.contains(QLatin1String("backup"))) {
|
||||||
|
IosController::Instance()->importBackupFromOutside(filePath);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QFile file(filePath);
|
||||||
|
if (!file.open(QIODevice::ReadOnly)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QByteArray data = file.readAll();
|
||||||
|
IosController::Instance()->importConfigFromOutside(QString::fromUtf8(data));
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,12 +1,7 @@
|
|||||||
#import <UIKit/UIKit.h>
|
#import <UIKit/UIKit.h>
|
||||||
#import <objc/runtime.h>
|
#import <objc/runtime.h>
|
||||||
#include <dispatch/dispatch.h>
|
|
||||||
|
|
||||||
#include <QByteArray>
|
#import "AmneziaOpenUrlImport.h"
|
||||||
#include <QFile>
|
|
||||||
#include <QString>
|
|
||||||
|
|
||||||
#include "ios_controller.h"
|
|
||||||
|
|
||||||
using SceneOpenURLContexts = void (*)(id, SEL, UIScene *, NSSet<UIOpenURLContext *> *);
|
using SceneOpenURLContexts = void (*)(id, SEL, UIScene *, NSSet<UIOpenURLContext *> *);
|
||||||
|
|
||||||
@@ -14,29 +9,7 @@ static SceneOpenURLContexts g_originalSceneOpenURLContexts = nullptr;
|
|||||||
|
|
||||||
static void amnezia_handleURL(NSURL *url)
|
static void amnezia_handleURL(NSURL *url)
|
||||||
{
|
{
|
||||||
if (!url || !url.isFileURL) {
|
AmneziaHandleOpenUrl(url);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString filePath(url.path.UTF8String);
|
|
||||||
if (filePath.isEmpty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
|
||||||
if (filePath.contains("backup")) {
|
|
||||||
IosController::Instance()->importBackupFromOutside(filePath);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
QFile file(filePath);
|
|
||||||
if (!file.open(QIODevice::ReadOnly)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const QByteArray data = file.readAll();
|
|
||||||
IosController::Instance()->importConfigFromOutside(QString::fromUtf8(data));
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static void amnezia_scene_openURLContexts(id self, SEL _cmd, UIScene *scene, NSSet<UIOpenURLContext *> *contexts)
|
static void amnezia_scene_openURLContexts(id self, SEL _cmd, UIScene *scene, NSSet<UIOpenURLContext *> *contexts)
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
#import "QtAppDelegate.h"
|
#import "QtAppDelegate.h"
|
||||||
#import "ios_controller.h"
|
#import "AmneziaOpenUrlImport.h"
|
||||||
|
|
||||||
#include <QFile>
|
|
||||||
|
|
||||||
|
|
||||||
@implementation QIOSApplicationDelegate (AmneziaVPNDelegate)
|
@implementation QIOSApplicationDelegate (AmneziaVPNDelegate)
|
||||||
#if !MACOS_NE
|
#if !MACOS_NE
|
||||||
@@ -11,6 +8,10 @@
|
|||||||
[application setMinimumBackgroundFetchInterval: UIApplicationBackgroundFetchIntervalMinimum];
|
[application setMinimumBackgroundFetchInterval: UIApplicationBackgroundFetchIntervalMinimum];
|
||||||
// Override point for customization after application launch.
|
// Override point for customization after application launch.
|
||||||
NSLog(@"Application didFinishLaunchingWithOptions");
|
NSLog(@"Application didFinishLaunchingWithOptions");
|
||||||
|
NSURL *launchUrl = launchOptions[UIApplicationLaunchOptionsURLKey];
|
||||||
|
if (launchUrl) {
|
||||||
|
AmneziaHandleOpenUrl(launchUrl);
|
||||||
|
}
|
||||||
return YES;
|
return YES;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,24 +36,11 @@
|
|||||||
- (BOOL)application:(UIApplication *)app
|
- (BOOL)application:(UIApplication *)app
|
||||||
openURL:(NSURL *)url
|
openURL:(NSURL *)url
|
||||||
options:(NSDictionary<UIApplicationOpenURLOptionsKey, id> *)options {
|
options:(NSDictionary<UIApplicationOpenURLOptionsKey, id> *)options {
|
||||||
if (url.fileURL) {
|
NSLog(@"Application openURL: %@", url);
|
||||||
QString filePath(url.path.UTF8String);
|
AmneziaHandleOpenUrl(url);
|
||||||
if (filePath.isEmpty()) return NO;
|
|
||||||
|
|
||||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
|
|
||||||
NSLog(@"Application openURL: %@", url);
|
|
||||||
|
|
||||||
if (filePath.contains("backup")) {
|
|
||||||
IosController::Instance()->importBackupFromOutside(filePath);
|
|
||||||
} else {
|
|
||||||
QFile file(filePath);
|
|
||||||
bool isOpenFile = file.open(QIODevice::ReadOnly);
|
|
||||||
QByteArray data = file.readAll();
|
|
||||||
|
|
||||||
IosController::Instance()->importConfigFromOutside(QString(data));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
NSString *scheme = url.scheme ? [url.scheme lowercaseString] : @"";
|
||||||
|
if ([scheme isEqualToString:@"vpn"] || url.fileURL) {
|
||||||
return YES;
|
return YES;
|
||||||
}
|
}
|
||||||
return NO;
|
return NO;
|
||||||
|
|||||||
@@ -8,6 +8,10 @@
|
|||||||
#include "core/utils/api/apiUtils.h"
|
#include "core/utils/api/apiUtils.h"
|
||||||
#include "core/utils/qrCodeUtils.h"
|
#include "core/utils/qrCodeUtils.h"
|
||||||
#include "ui/controllers/systemController.h"
|
#include "ui/controllers/systemController.h"
|
||||||
|
#ifdef Q_OS_IOS
|
||||||
|
#include "platforms/ios/ios_controller.h"
|
||||||
|
#include <QThread>
|
||||||
|
#endif
|
||||||
#include "version.h"
|
#include "version.h"
|
||||||
#include <QClipboard>
|
#include <QClipboard>
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ PageType {
|
|||||||
Layout.leftMargin: 16
|
Layout.leftMargin: 16
|
||||||
Layout.rightMargin: 16
|
Layout.rightMargin: 16
|
||||||
|
|
||||||
headerText: qsTr("New connection")
|
headerText: qsTr("Add this connection?")
|
||||||
}
|
}
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
@@ -204,7 +204,7 @@ PageType {
|
|||||||
Layout.rightMargin: 16
|
Layout.rightMargin: 16
|
||||||
Layout.leftMargin: 16
|
Layout.leftMargin: 16
|
||||||
|
|
||||||
text: qsTr("Connect")
|
text: qsTr("Add")
|
||||||
clickedFunc: function() {
|
clickedFunc: function() {
|
||||||
const headerItem = listView.headerItem;
|
const headerItem = listView.headerItem;
|
||||||
if (!headerItem) {
|
if (!headerItem) {
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
# POST_BUILD helper: HKCU\Software\Classes\vpn\shell\open\command -> "app.exe" "%1"
|
||||||
|
# Invoke: cmake -DEXE_PATH="..." -P register_vpn_url_win.cmake
|
||||||
|
if(NOT DEFINED EXE_PATH OR EXE_PATH STREQUAL "")
|
||||||
|
message(FATAL_ERROR "register_vpn_url_win.cmake: EXE_PATH is empty")
|
||||||
|
endif()
|
||||||
|
if(NOT EXISTS "${EXE_PATH}")
|
||||||
|
message(WARNING "register_vpn_url_win.cmake: EXE not found (POST_BUILD order?): ${EXE_PATH}")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
file(TO_NATIVE_PATH "${EXE_PATH}" _exe_native)
|
||||||
|
string(REPLACE "/" "\\" _exe_native "${_exe_native}")
|
||||||
|
|
||||||
|
# reg.exe: "C:\path\app.exe" "%1" — %1 is the full vpn://... string
|
||||||
|
set(_reg_dval "\"${_exe_native}\" \"%1\"")
|
||||||
|
|
||||||
|
execute_process(
|
||||||
|
COMMAND reg ADD "HKCU\\Software\\Classes\\vpn\\shell\\open\\command"
|
||||||
|
/ve /t REG_SZ /d "${_reg_dval}" /f
|
||||||
|
RESULT_VARIABLE _reg_res
|
||||||
|
ERROR_VARIABLE _reg_err
|
||||||
|
)
|
||||||
|
if(NOT _reg_res EQUAL 0)
|
||||||
|
message(WARNING "register_vpn_url_win.cmake: reg ADD failed (code ${_reg_res}): ${_reg_err}")
|
||||||
|
else()
|
||||||
|
message(STATUS "vpn:// HKCU shell\\open\\command = ${_reg_dval}")
|
||||||
|
endif()
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
#!/usr/bin/env xdg-open
|
|
||||||
[Desktop Entry]
|
[Desktop Entry]
|
||||||
Type=Application
|
Type=Application
|
||||||
Name=AmneziaVPN
|
Name=AmneziaVPN
|
||||||
Version=1.0
|
Version=1.0
|
||||||
Comment=Client of your self-hosted VPN
|
Comment=Client of your self-hosted VPN
|
||||||
Exec=AmneziaVPN
|
Exec=/opt/AmneziaVPN/bin/AmneziaVPN %u
|
||||||
Icon=/usr/share/pixmaps/AmneziaVPN.png
|
Icon=/usr/share/pixmaps/AmneziaVPN.png
|
||||||
Categories=Network;Qt;Security;
|
Categories=Network;Qt;Security;
|
||||||
|
MimeType=x-scheme-handler/vpn;
|
||||||
Terminal=false
|
Terminal=false
|
||||||
|
|||||||
@@ -30,6 +30,13 @@ if sudo systemctl is-active --quiet $APP_NAME; then
|
|||||||
sudo rm -rf /etc/systemd/system/$APP_NAME.service >> $LOG_FILE
|
sudo rm -rf /etc/systemd/system/$APP_NAME.service >> $LOG_FILE
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Absolute Exec= in .desktop: Firefox/portal invoke handlers with a minimal PATH.
|
||||||
|
DESKTOP_IN_APP="$APP_PATH/$APP_NAME.desktop"
|
||||||
|
if [ -f "$DESKTOP_IN_APP" ]; then
|
||||||
|
sudo sed -i "s|^Exec=.*|Exec=$APP_PATH/bin/$APP_NAME %u|" "$DESKTOP_IN_APP" >> $LOG_FILE 2>&1 || true
|
||||||
|
sudo sed -i '1{/^#!\/usr\/bin\/env xdg-open$/d;}' "$DESKTOP_IN_APP" >> $LOG_FILE 2>&1 || true
|
||||||
|
fi
|
||||||
|
|
||||||
sudo chmod -R a-w $APP_PATH/
|
sudo chmod -R a-w $APP_PATH/
|
||||||
|
|
||||||
sudo cp $APP_PATH/$APP_NAME.service /etc/systemd/system/ >> $LOG_FILE
|
sudo cp $APP_PATH/$APP_NAME.service /etc/systemd/system/ >> $LOG_FILE
|
||||||
@@ -44,6 +51,35 @@ sudo cp $APP_PATH/$APP_NAME.desktop /usr/share/applications/ >> $LOG_FILE
|
|||||||
sudo cp $APP_PATH/$APP_NAME.png /usr/share/pixmaps/ >> $LOG_FILE
|
sudo cp $APP_PATH/$APP_NAME.png /usr/share/pixmaps/ >> $LOG_FILE
|
||||||
sudo chmod 555 /usr/share/applications/$APP_NAME.desktop >> $LOG_FILE
|
sudo chmod 555 /usr/share/applications/$APP_NAME.desktop >> $LOG_FILE
|
||||||
|
|
||||||
|
if command -v update-desktop-database &> /dev/null; then
|
||||||
|
sudo update-desktop-database /usr/share/applications >> $LOG_FILE 2>&1 || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
register_vpn_scheme_for_user() {
|
||||||
|
local user="$1"
|
||||||
|
if [ -z "$user" ] || [ "$user" = "root" ]; then
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
if ! command -v xdg-mime &> /dev/null; then
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
local home
|
||||||
|
home=$(getent passwd "$user" | cut -d: -f6)
|
||||||
|
if [ -z "$home" ] || [ ! -d "$home" ]; then
|
||||||
|
echo "skip xdg-mime for $user: no home" >> $LOG_FILE
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
echo "xdg-mime default for user $user" >> $LOG_FILE
|
||||||
|
sudo -u "$user" env HOME="$home" \
|
||||||
|
xdg-mime default "$APP_NAME.desktop" x-scheme-handler/vpn >> $LOG_FILE 2>&1 || true
|
||||||
|
}
|
||||||
|
|
||||||
|
if [ -n "$SUDO_USER" ] && [ "$SUDO_USER" != "root" ]; then
|
||||||
|
register_vpn_scheme_for_user "$SUDO_USER"
|
||||||
|
elif [ -n "$USER" ] && [ "$USER" != "root" ]; then
|
||||||
|
register_vpn_scheme_for_user "$USER"
|
||||||
|
fi
|
||||||
|
|
||||||
echo "user desktop creation loop ended" >> $LOG_FILE
|
echo "user desktop creation loop ended" >> $LOG_FILE
|
||||||
|
|
||||||
if command -v steamos-readonly &> /dev/null; then
|
if command -v steamos-readonly &> /dev/null; then
|
||||||
|
|||||||
@@ -35,12 +35,10 @@ fi
|
|||||||
run_cmd launchctl bootout system "$LAUNCH_DAEMONS_PLIST_NAME" || run_cmd launchctl unload "$LAUNCH_DAEMONS_PLIST_NAME"
|
run_cmd launchctl bootout system "$LAUNCH_DAEMONS_PLIST_NAME" || run_cmd launchctl unload "$LAUNCH_DAEMONS_PLIST_NAME"
|
||||||
run_cmd rm -f "$LAUNCH_DAEMONS_PLIST_NAME"
|
run_cmd rm -f "$LAUNCH_DAEMONS_PLIST_NAME"
|
||||||
|
|
||||||
# Add separate group for xray filtering
|
# Add separate group for xray filtering (do not exit the script if the group already exists)
|
||||||
if dscl . -read "/Groups/$SERVICE_GROUP" >/dev/null 2>&1; then
|
if dscl . -read "/Groups/$SERVICE_GROUP" >/dev/null 2>&1; then
|
||||||
log "Group $SERVICE_GROUP already exists"
|
log "Group $SERVICE_GROUP already exists"
|
||||||
return 0
|
|
||||||
else
|
else
|
||||||
local next_gid
|
|
||||||
next_gid=$(dscl . -list /Groups PrimaryGroupID 2>/dev/null | awk '{print $2}' | sort -n | awk '$1>=500{g=$1} END{print (g?g+1:501)}')
|
next_gid=$(dscl . -list /Groups PrimaryGroupID 2>/dev/null | awk '{print $2}' | sort -n | awk '$1>=500{g=$1} END{print (g?g+1:501)}')
|
||||||
run_cmd dscl . -create "/Groups/$SERVICE_GROUP"
|
run_cmd dscl . -create "/Groups/$SERVICE_GROUP"
|
||||||
run_cmd dscl . -create "/Groups/$SERVICE_GROUP" PrimaryGroupID "$next_gid"
|
run_cmd dscl . -create "/Groups/$SERVICE_GROUP" PrimaryGroupID "$next_gid"
|
||||||
@@ -51,6 +49,21 @@ run_cmd sudo chmod -R a-w "$APP_PATH/"
|
|||||||
run_cmd sudo chown -R root "$APP_PATH/"
|
run_cmd sudo chown -R root "$APP_PATH/"
|
||||||
run_cmd sudo chgrp -R wheel "$APP_PATH/"
|
run_cmd sudo chgrp -R wheel "$APP_PATH/"
|
||||||
|
|
||||||
|
# Refresh Launch Services so CFBundleURLTypes (e.g. vpn://) is picked up without a manual lsregister.
|
||||||
|
LSREGISTER="/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister"
|
||||||
|
if [ -d "$APP_PATH" ]; then
|
||||||
|
log "Launch Services: lsregister -f -R $APP_PATH"
|
||||||
|
run_cmd "$LSREGISTER" -f -R "$APP_PATH" || true
|
||||||
|
INFO_PLIST="$APP_PATH/Contents/Info.plist"
|
||||||
|
if [ -f "$INFO_PLIST" ] && plutil -p "$INFO_PLIST" 2>/dev/null | grep -q 'CFBundleURLTypes'; then
|
||||||
|
log "Info.plist: CFBundleURLTypes present (vpn:// can be registered with Launch Services)"
|
||||||
|
else
|
||||||
|
log "ERROR: Info.plist has no CFBundleURLTypes — open vpn:// will fail (-10814). Fix the app bundle Info.plist at build time; lsregister cannot invent URL schemes."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
log "WARN: $APP_PATH missing, skipping lsregister"
|
||||||
|
fi
|
||||||
|
|
||||||
log "Requesting ${APP_NAME} to quit gracefully"
|
log "Requesting ${APP_NAME} to quit gracefully"
|
||||||
run_cmd osascript -e 'tell application "AmneziaVPN" to quit' || true
|
run_cmd osascript -e 'tell application "AmneziaVPN" to quit' || true
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,124 @@
|
|||||||
|
<!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" 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("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