mirror of
https://github.com/amnezia-vpn/amnezia-client.git
synced 2026-11-12 07:23:19 +09:00
Compare commits
15
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a9c0cc654a | ||
|
|
eab4ef3e31 | ||
|
|
bf464f8490 | ||
|
|
ffab8a4e2b | ||
|
|
4142035168 | ||
|
|
8c3e3a58a9 | ||
|
|
7e0748f7b4 | ||
|
|
02e55424ad | ||
|
|
c90107bb7a | ||
|
|
4ae2b32083 | ||
|
|
865880d502 | ||
|
|
7c8613f19a | ||
|
|
9dc7e430a3 | ||
|
|
2850f05be0 | ||
|
|
e2108a28ea |
No files matched your search
@@ -253,6 +253,28 @@ else()
|
||||
qt_finalize_target(${PROJECT})
|
||||
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}
|
||||
DESTINATION ${CMAKE_INSTALL_BINDIR}
|
||||
RUNTIME_DEPENDENCY_SET client_deps
|
||||
|
||||
+161
-23
@@ -15,6 +15,9 @@
|
||||
#include <QEvent>
|
||||
#include <QDir>
|
||||
#include <QSettings>
|
||||
#include <QFileOpenEvent>
|
||||
#include <QUrl>
|
||||
#include <QCoreApplication>
|
||||
#include <QtQuick/QQuickWindow>
|
||||
#include <QWindow>
|
||||
|
||||
@@ -39,6 +42,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")),
|
||||
@@ -71,26 +85,32 @@ AmneziaApplication::AmneziaApplication(int &argc, char *argv[]) : AMNEZIA_BASE_C
|
||||
AmneziaApplication::~AmneziaApplication()
|
||||
{
|
||||
#ifdef AMNEZIA_DESKTOP
|
||||
if (m_vpnConnection && m_vpnConnectionThread.isRunning()) {
|
||||
QMetaObject::invokeMethod(m_vpnConnection.get(), "disconnectSlots", Qt::BlockingQueuedConnection);
|
||||
|
||||
QMetaObject::invokeMethod(m_vpnConnection.get(), "disconnectFromVpn", Qt::BlockingQueuedConnection);
|
||||
if (m_vpnConnection) {
|
||||
m_vpnConnection->disconnectSlots();
|
||||
m_vpnConnection->disconnectFromVpn();
|
||||
}
|
||||
#endif
|
||||
|
||||
m_vpnConnectionThread.requestInterruption();
|
||||
m_vpnConnectionThread.quit();
|
||||
|
||||
if (!m_vpnConnectionThread.wait(3000)) {
|
||||
m_vpnConnectionThread.terminate();
|
||||
m_vpnConnectionThread.wait(500);
|
||||
}
|
||||
|
||||
if (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
|
||||
|
||||
#ifdef Q_OS_ANDROID
|
||||
namespace {
|
||||
static void clearQtCaches()
|
||||
@@ -164,9 +184,6 @@ void AmneziaApplication::init()
|
||||
#endif
|
||||
|
||||
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_engine->addImportPath(QStringLiteral(APP_QML_IMPORT_PATH));
|
||||
@@ -225,6 +242,21 @@ void AmneziaApplication::init()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
|
||||
QTimer::singleShot(0, this, [this]() { m_coreController->checkVpnDeepLinkHandler(); });
|
||||
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()
|
||||
@@ -289,20 +321,126 @@ bool AmneziaApplication::parseCommands()
|
||||
}
|
||||
|
||||
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) && !defined(MACOS_NE)
|
||||
void AmneziaApplication::startLocalServer() {
|
||||
void AmneziaApplication::startLocalServer()
|
||||
{
|
||||
const QString serverName(APP_INSTANCE_NAME);
|
||||
QLocalServer::removeServer(serverName);
|
||||
|
||||
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]() {
|
||||
if (server) {
|
||||
QLocalSocket *clientConnection = server->nextPendingConnection();
|
||||
clientConnection->deleteLater();
|
||||
QObject::connect(server, &QLocalServer::newConnection, this, [this, server]() {
|
||||
QLocalSocket *sock = server->nextPendingConnection();
|
||||
if (!sock) {
|
||||
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;
|
||||
}
|
||||
if (!m_coreController->isVpnDeepLinkEnabled()) {
|
||||
emit m_coreController->pageController()->raiseMainWindow();
|
||||
emit m_coreController->pageController()->showNotificationMessage(
|
||||
tr("Opening vpn:// links is turned off in the settings"));
|
||||
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) {
|
||||
return decoded.mid(idx).trimmed();
|
||||
}
|
||||
if (url.scheme().compare(QLatin1String("vpn"), Qt::CaseInsensitive) == 0) {
|
||||
return decoded.trimmed();
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
#if !defined(MACOS_NE)
|
||||
bool forwardVpnPayloadToPrimaryInstance(const QString &payload)
|
||||
{
|
||||
if (payload.trimmed().isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
QLocalSocket socket;
|
||||
socket.connectToServer(APP_INSTANCE_NAME);
|
||||
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();
|
||||
const QString payload = vpnPayloadFromFileOpenUrl(url);
|
||||
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
|
||||
|
||||
|
||||
@@ -5,13 +5,15 @@
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QQmlApplicationEngine>
|
||||
#include <QQmlContext>
|
||||
#include <QThread>
|
||||
#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS)
|
||||
#include <QGuiApplication>
|
||||
#else
|
||||
#include <QApplication>
|
||||
#endif
|
||||
#include <QClipboard>
|
||||
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
|
||||
#include <QEvent>
|
||||
#endif
|
||||
|
||||
#include "core/controllers/coreController.h"
|
||||
#include "secureQSettings.h"
|
||||
@@ -41,6 +43,7 @@ public:
|
||||
|
||||
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) && !defined(MACOS_NE)
|
||||
void startLocalServer();
|
||||
static void markSecondaryInstanceForDeepLink();
|
||||
#endif
|
||||
|
||||
QQmlApplicationEngine *qmlEngine() const;
|
||||
@@ -67,12 +70,19 @@ private:
|
||||
QCommandLineOption m_optConnect;
|
||||
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;
|
||||
QThread m_vpnConnectionThread;
|
||||
|
||||
QNetworkAccessManager *m_nam;
|
||||
protected:
|
||||
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
|
||||
@@ -249,7 +249,10 @@ class AmneziaActivity : QtActivity() {
|
||||
override fun onNewIntent(intent: Intent?) {
|
||||
super.onNewIntent(intent)
|
||||
Log.v(TAG, "onNewIntent: $intent")
|
||||
intent?.let(::processIntent)
|
||||
intent?.let {
|
||||
setIntent(it)
|
||||
processIntent(it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun processIntent(intent: Intent) {
|
||||
|
||||
@@ -36,6 +36,7 @@ class ImportConfigActivity : ComponentActivity() {
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
super.onNewIntent(intent)
|
||||
Log.v(TAG, "onNewIntent: $intent")
|
||||
setIntent(intent)
|
||||
intent.let(::readConfig)
|
||||
}
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ set(HEADERS ${HEADERS}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ioscontextmenu.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QtAppDelegate.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)
|
||||
|
||||
@@ -47,6 +48,7 @@ set(SOURCES ${SOURCES}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QRCodeReaderBase.mm
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QtAppDelegate.mm
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/AmneziaSceneDelegateHooks.mm
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/AmneziaOpenUrlImport.mm
|
||||
)
|
||||
|
||||
# The context menu helper uses ARC-only constructs (weak references); the
|
||||
|
||||
@@ -18,8 +18,13 @@ set(LIBS ${LIBS}
|
||||
${FW_NETWORK_EXTENSION}
|
||||
)
|
||||
|
||||
set_target_properties(${PROJECT} PROPERTIES
|
||||
set_target_properties(${PROJECT} PROPERTIES
|
||||
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_BUNDLE_VERSION "${CMAKE_PROJECT_VERSION_TWEAK}"
|
||||
)
|
||||
@@ -30,6 +35,7 @@ set(HEADERS ${HEADERS}
|
||||
|
||||
set(SOURCES ${SOURCES}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ui/utils/macosUtil.mm
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ui/utils/vpnUrlHandler_mac.mm
|
||||
)
|
||||
|
||||
|
||||
@@ -39,6 +45,31 @@ get_filename_component(MACOSX_BUNDLE_ICON_FILE "${CLIENT_MACOS_APP_ICNS_PATH}" N
|
||||
set_source_files_properties(${ICON_FILE} PROPERTIES MACOSX_PACKAGE_LOCATION Resources)
|
||||
set(SOURCES ${SOURCES} ${ICON_FILE})
|
||||
|
||||
set(LINK_HANDLER_TARGET AmneziaVPNLinkHandler)
|
||||
set(AMNEZIA_LINK_HANDLER_MAIN_BUNDLE_ID "${BUILD_OSX_APP_IDENTIFIER}")
|
||||
add_executable(${LINK_HANDLER_TARGET} MACOSX_BUNDLE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/macos/linkhandler/main.mm
|
||||
${ICON_FILE}
|
||||
)
|
||||
target_compile_options(${LINK_HANDLER_TARGET} PRIVATE -fobjc-arc)
|
||||
target_link_libraries(${LINK_HANDLER_TARGET} PRIVATE "-framework AppKit")
|
||||
set_target_properties(${LINK_HANDLER_TARGET} PROPERTIES
|
||||
MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_SOURCE_DIR}/macos/linkhandler/Info.plist.in"
|
||||
MACOSX_BUNDLE_GUI_IDENTIFIER "${BUILD_OSX_APP_IDENTIFIER}.linkhandler"
|
||||
MACOSX_BUNDLE_ICON_FILE "${MACOSX_BUNDLE_ICON_FILE}"
|
||||
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}"
|
||||
XCODE_ATTRIBUTE_CODE_SIGNING_ALLOWED "NO"
|
||||
)
|
||||
add_dependencies(${PROJECT} ${LINK_HANDLER_TARGET})
|
||||
set(LINK_HANDLER_TEMPLATE_DIR "$<TARGET_BUNDLE_DIR:${PROJECT}>/Contents/Resources/LinkHandler.template")
|
||||
add_custom_command(TARGET ${LINK_HANDLER_TARGET} POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E rm -rf "${LINK_HANDLER_TEMPLATE_DIR}"
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory "${LINK_HANDLER_TEMPLATE_DIR}"
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory "$<TARGET_BUNDLE_DIR:${LINK_HANDLER_TARGET}>" "${LINK_HANDLER_TEMPLATE_DIR}"
|
||||
COMMENT "Embed vpn:// link handler into ${PROJECT}.app as LinkHandler.template"
|
||||
)
|
||||
|
||||
target_compile_options(${PROJECT} PRIVATE
|
||||
-DGROUP_ID=\"${BUILD_IOS_GROUP_IDENTIFIER}\"
|
||||
-DVPN_NE_BUNDLEID=\"${BUILD_IOS_APP_IDENTIFIER}.network-extension\"
|
||||
|
||||
@@ -46,6 +46,7 @@ set(SOURCES ${SOURCES}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosglue.mm
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QRCodeReaderBase.mm
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QtAppDelegate.mm
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ui/utils/vpnUrlHandler_mac.mm
|
||||
)
|
||||
|
||||
target_include_directories(${PROJECT} PRIVATE
|
||||
@@ -56,7 +57,7 @@ target_include_directories(${PROJECT} PRIVATE
|
||||
|
||||
set_target_properties(${PROJECT} PROPERTIES
|
||||
XCODE_LINK_BUILD_PHASE_MODE KNOWN_LOCATION
|
||||
MACOSX_BUNDLE_INFO_PLIST ${CMAKE_CURRENT_SOURCE_DIR}/macos/app/Info.plist.in
|
||||
MACOSX_BUNDLE_INFO_PLIST ${CMAKE_CURRENT_SOURCE_DIR}/macos/app/Info-NE.plist.in
|
||||
MACOSX_BUNDLE_ICON_FILE "AppIcon"
|
||||
MACOSX_BUNDLE_INFO_STRING "${CLIENT_APPLICATION_NAME}"
|
||||
MACOSX_BUNDLE_BUNDLE_NAME "${CLIENT_APPLICATION_NAME}"
|
||||
|
||||
@@ -57,6 +57,7 @@ set(HEADERS ${HEADERS}
|
||||
${CLIENT_ROOT_DIR}/ui/utils/pages.h
|
||||
${CLIENT_PAGE_ENUM_HEADER}
|
||||
${CLIENT_ROOT_DIR}/ui/utils/qAutoStart.h
|
||||
${CLIENT_ROOT_DIR}/ui/utils/vpnUrlHandler.h
|
||||
${CLIENT_ROOT_DIR}/ui/utils/mtProxyPublicHostInput.h
|
||||
${CLIENT_ROOT_DIR}/core/protocols/vpnProtocol.h
|
||||
${CMAKE_CURRENT_BINARY_DIR}/version.h
|
||||
@@ -144,6 +145,7 @@ set(SOURCES ${SOURCES}
|
||||
${CLIENT_ROOT_DIR}/core/repositories/secureServersRepository.cpp
|
||||
${CLIENT_ROOT_DIR}/core/repositories/secureAppSettingsRepository.cpp
|
||||
${CLIENT_ROOT_DIR}/ui/utils/qAutoStart.cpp
|
||||
${CLIENT_ROOT_DIR}/ui/utils/vpnUrlHandler.cpp
|
||||
${CLIENT_ROOT_DIR}/ui/utils/mtProxyPublicHostInput.cpp
|
||||
${CLIENT_ROOT_DIR}/core/protocols/vpnProtocol.cpp
|
||||
${CLIENT_ROOT_DIR}/core/utils/selfhosted/sshClient.cpp
|
||||
@@ -272,10 +274,12 @@ set(SOURCES ${SOURCES}
|
||||
if(WIN32)
|
||||
set(HEADERS ${HEADERS}
|
||||
${CLIENT_ROOT_DIR}/core/protocols/ikev2VpnProtocolWindows.h
|
||||
${CLIENT_ROOT_DIR}/ui/utils/windowsUtil.h
|
||||
)
|
||||
|
||||
set(SOURCES ${SOURCES}
|
||||
${CLIENT_ROOT_DIR}/core/protocols/ikev2VpnProtocolWindows.cpp
|
||||
${CLIENT_ROOT_DIR}/ui/utils/windowsUtil.cpp
|
||||
)
|
||||
|
||||
set(RESOURCES ${RESOURCES}
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include "logger.h"
|
||||
#include "secureQSettings.h"
|
||||
#include "core/utils/appUiConfig.h"
|
||||
#include "ui/utils/vpnUrlHandler.h"
|
||||
|
||||
#if defined(Q_OS_ANDROID)
|
||||
#include "core/utils/installedAppsImageProvider.h"
|
||||
@@ -324,6 +325,32 @@ void CoreController::checkForAppUpdates()
|
||||
m_updateController->checkForUpdates();
|
||||
}
|
||||
|
||||
void CoreController::checkVpnDeepLinkHandler()
|
||||
{
|
||||
if (!m_appSettingsRepository->isVpnDeepLinkEnabled()) {
|
||||
VpnUrlHandler::unregister();
|
||||
return;
|
||||
}
|
||||
|
||||
switch (VpnUrlHandler::status()) {
|
||||
case VpnUrlHandler::Status::IsDefault:
|
||||
return;
|
||||
case VpnUrlHandler::Status::NotRegistered:
|
||||
VpnUrlHandler::setAsDefault();
|
||||
return;
|
||||
case VpnUrlHandler::Status::OtherHandler:
|
||||
if (!m_appSettingsRepository->isVpnDeepLinkPromptDismissed()) {
|
||||
emit m_pageController->vpnDeepLinkDefaultDrawerRequested();
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
bool CoreController::isVpnDeepLinkEnabled() const
|
||||
{
|
||||
return m_appSettingsRepository->isVpnDeepLinkEnabled();
|
||||
}
|
||||
|
||||
void CoreController::updateTranslator(const QLocale &locale)
|
||||
{
|
||||
if (!m_translator->isEmpty()) {
|
||||
@@ -399,3 +426,19 @@ void CoreController::importConfigFromData(const QString &data)
|
||||
m_importController->importConfig();
|
||||
}
|
||||
}
|
||||
|
||||
void CoreController::openVpnKeyImportPreview(const QString &data)
|
||||
{
|
||||
if (!m_importController || data.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
emit m_pageController->goToPageHome();
|
||||
const bool extracted = m_importController->extractConfigFromData(data);
|
||||
if (extracted) {
|
||||
emit m_pageController->goToPageViewConfig();
|
||||
}
|
||||
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
|
||||
emit m_pageController->raiseMainWindow();
|
||||
#endif
|
||||
}
|
||||
@@ -104,9 +104,13 @@ public:
|
||||
PageController* pageController() const;
|
||||
void setQmlRoot();
|
||||
void checkForAppUpdates();
|
||||
void checkVpnDeepLinkHandler();
|
||||
bool isVpnDeepLinkEnabled() const;
|
||||
|
||||
void openConnectionByIndex(int serverIndex);
|
||||
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);
|
||||
|
||||
signals:
|
||||
|
||||
@@ -439,10 +439,8 @@ void CoreSignalHandlers::initAndroidConnectionHandler()
|
||||
m_coreController->m_connectionController->restoreConnection();
|
||||
});
|
||||
connect(AndroidController::instance(), &AndroidController::importConfigFromOutside, this, [this](QString data) {
|
||||
emit m_coreController->m_pageController->goToPageHome();
|
||||
m_coreController->m_importController->extractConfigFromData(data);
|
||||
m_coreController->openVpnKeyImportPreview(data);
|
||||
data.clear();
|
||||
emit m_coreController->m_pageController->goToPageViewConfig();
|
||||
});
|
||||
#endif
|
||||
}
|
||||
@@ -451,9 +449,7 @@ void CoreSignalHandlers::initIosImportHandler()
|
||||
{
|
||||
#ifdef Q_OS_IOS
|
||||
connect(IosController::Instance(), &IosController::importConfigFromOutside, this, [this](QString data) {
|
||||
emit m_coreController->m_pageController->goToPageHome();
|
||||
m_coreController->m_importController->extractConfigFromData(data);
|
||||
emit m_coreController->m_pageController->goToPageViewConfig();
|
||||
m_coreController->openVpnKeyImportPreview(data);
|
||||
});
|
||||
connect(IosController::Instance(), &IosController::importBackupFromOutside, this, [this](QString filePath) {
|
||||
emit m_coreController->m_pageController->goToPageHome();
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
#include "version.h"
|
||||
#include "ui/utils/qAutoStart.h"
|
||||
#include "ui/utils/vpnUrlHandler.h"
|
||||
#include "logger.h"
|
||||
#include "core/controllers/logsController.h"
|
||||
#if defined(Q_OS_IOS) || defined(MACOS_NE)
|
||||
@@ -140,6 +141,10 @@ ErrorCode SettingsController::restoreAppConfigFromData(const QByteArray &data)
|
||||
autoStart = newConfigData["Conf/autoStart"].toBool();
|
||||
}
|
||||
toggleAutoStart(autoStart);
|
||||
|
||||
if (!m_appSettingsRepository->isVpnDeepLinkEnabled()) {
|
||||
VpnUrlHandler::unregister();
|
||||
}
|
||||
#endif
|
||||
|
||||
#if defined(Q_OS_WINDOWS) || defined(Q_OS_ANDROID)
|
||||
@@ -235,6 +240,42 @@ void SettingsController::toggleStartMinimized(bool enable)
|
||||
m_appSettingsRepository->setStartMinimized(enable);
|
||||
}
|
||||
|
||||
bool SettingsController::isVpnDeepLinkEnabled() const
|
||||
{
|
||||
return m_appSettingsRepository->isVpnDeepLinkEnabled();
|
||||
}
|
||||
|
||||
void SettingsController::toggleVpnDeepLink(bool enable)
|
||||
{
|
||||
m_appSettingsRepository->setVpnDeepLinkEnabled(enable);
|
||||
if (enable) {
|
||||
m_appSettingsRepository->setVpnDeepLinkPromptDismissed(false);
|
||||
VpnUrlHandler::setAsDefault();
|
||||
} else {
|
||||
VpnUrlHandler::unregister();
|
||||
}
|
||||
}
|
||||
|
||||
bool SettingsController::isVpnDeepLinkOverridden() const
|
||||
{
|
||||
return VpnUrlHandler::status() == VpnUrlHandler::Status::OtherHandler;
|
||||
}
|
||||
|
||||
bool SettingsController::makeVpnDeepLinkDefault()
|
||||
{
|
||||
return VpnUrlHandler::setAsDefault();
|
||||
}
|
||||
|
||||
bool SettingsController::isVpnDeepLinkPromptDismissed() const
|
||||
{
|
||||
return m_appSettingsRepository->isVpnDeepLinkPromptDismissed();
|
||||
}
|
||||
|
||||
void SettingsController::dismissVpnDeepLinkPrompt()
|
||||
{
|
||||
m_appSettingsRepository->setVpnDeepLinkPromptDismissed(true);
|
||||
}
|
||||
|
||||
bool SettingsController::isScreenshotsEnabled() const
|
||||
{
|
||||
return m_appSettingsRepository->isScreenshotsEnabled();
|
||||
|
||||
@@ -57,6 +57,13 @@ public:
|
||||
bool isScreenshotsEnabled() const;
|
||||
void toggleScreenshotsEnabled(bool enable);
|
||||
|
||||
bool isVpnDeepLinkEnabled() const;
|
||||
void toggleVpnDeepLink(bool enable);
|
||||
bool isVpnDeepLinkOverridden() const;
|
||||
bool makeVpnDeepLinkDefault();
|
||||
bool isVpnDeepLinkPromptDismissed() const;
|
||||
void dismissVpnDeepLinkPrompt();
|
||||
|
||||
bool isNewsNotificationsEnabled() const;
|
||||
void toggleNewsNotificationsEnabled(bool enable);
|
||||
|
||||
|
||||
@@ -375,6 +375,26 @@ void SecureAppSettingsRepository::setScreenshotsEnabled(bool enabled)
|
||||
emit screenshotsEnabledChanged(enabled);
|
||||
}
|
||||
|
||||
bool SecureAppSettingsRepository::isVpnDeepLinkEnabled() const
|
||||
{
|
||||
return value("Conf/vpnDeepLinkEnabled", true).toBool();
|
||||
}
|
||||
|
||||
void SecureAppSettingsRepository::setVpnDeepLinkEnabled(bool enabled)
|
||||
{
|
||||
setValue("Conf/vpnDeepLinkEnabled", enabled);
|
||||
}
|
||||
|
||||
bool SecureAppSettingsRepository::isVpnDeepLinkPromptDismissed() const
|
||||
{
|
||||
return value("Conf/vpnDeepLinkPromptDismissed", false).toBool();
|
||||
}
|
||||
|
||||
void SecureAppSettingsRepository::setVpnDeepLinkPromptDismissed(bool dismissed)
|
||||
{
|
||||
setValue("Conf/vpnDeepLinkPromptDismissed", dismissed);
|
||||
}
|
||||
|
||||
int SecureAppSettingsRepository::countryListSortMode(const QString &listId) const
|
||||
{
|
||||
return value("Conf/countryListSortMode/" + listId, 0).toInt();
|
||||
|
||||
@@ -77,6 +77,10 @@ public:
|
||||
void setStartMinimized(bool enabled);
|
||||
bool isScreenshotsEnabled() const;
|
||||
void setScreenshotsEnabled(bool enabled);
|
||||
bool isVpnDeepLinkEnabled() const;
|
||||
void setVpnDeepLinkEnabled(bool enabled);
|
||||
bool isVpnDeepLinkPromptDismissed() const;
|
||||
void setVpnDeepLinkPromptDismissed(bool dismissed);
|
||||
|
||||
int countryListSortMode(const QString &listId) const;
|
||||
void setCountryListSortMode(const QString &listId, int mode);
|
||||
|
||||
@@ -107,6 +107,19 @@
|
||||
<string>AppIcon</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleURLName</key>
|
||||
<string>${BUILD_IOS_APP_IDENTIFIER}.vpn-deeplink</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>vpn</string>
|
||||
</array>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
</dict>
|
||||
</array>
|
||||
<key>UTImportedTypeDeclarations</key>
|
||||
<array>
|
||||
<dict>
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleAllowMixedLocalizations</key>
|
||||
<true/>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>${QT_INTERNAL_DOLLAR_VAR}{PRODUCT_NAME}</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>${MACOSX_BUNDLE_EXECUTABLE_NAME}</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>${MACOSX_BUNDLE_ICON_FILE}</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>${MACOSX_BUNDLE_GUI_IDENTIFIER}</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>${MACOSX_BUNDLE_BUNDLE_NAME}</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>${MACOSX_BUNDLE_SHORT_VERSION_STRING}</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>${MACOSX_BUNDLE_BUNDLE_VERSION}</string>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>${MACOSX_BUNDLE_COPYRIGHT}</string>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
<key>LSApplicationCategoryType</key>
|
||||
<string>public.app-category.utilities</string>
|
||||
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>${MACOSX_DEPLOYMENT_TARGET}</string>
|
||||
<key>LSSupportsOpeningDocumentsInPlace</key>
|
||||
<true/>
|
||||
<key>com.wireguard.ios.app_group_id</key>
|
||||
<string>${BUILD_IOS_GROUP_IDENTIFIER}</string>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>${CLIENT_APPLICATION_NAME} needs access to the camera for reading QR-codes.</string>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<false/>
|
||||
<key>NSAllowsLocalNetworking</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>CFBundleIconName</key>
|
||||
<string>AppIcon</string>
|
||||
<key>CFBundleIcons</key>
|
||||
<dict>
|
||||
<key>CFBundlePrimaryIcon</key>
|
||||
<dict>
|
||||
<key>CFBundleIconFiles</key>
|
||||
<array>
|
||||
<string>AppIcon</string>
|
||||
</array>
|
||||
<key>CFBundleIconName</key>
|
||||
<string>AppIcon</string>
|
||||
</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>
|
||||
<array>
|
||||
<dict>
|
||||
<key>UTTypeConformsTo</key>
|
||||
<array>
|
||||
<string>public.data</string>
|
||||
</array>
|
||||
<key>UTTypeDescription</key>
|
||||
<string>${CLIENT_APPLICATION_NAME} config</string>
|
||||
<key>UTTypeIconFiles</key>
|
||||
<array/>
|
||||
<key>UTTypeIdentifier</key>
|
||||
<string>${BUILD_OSX_APP_IDENTIFIER}.amnezia-config</string>
|
||||
<key>UTTypeTagSpecification</key>
|
||||
<dict>
|
||||
<key>public.filename-extension</key>
|
||||
<array>
|
||||
<string>vpn</string>
|
||||
</array>
|
||||
<key>public.mime-type</key>
|
||||
<array>
|
||||
<string>text/plain</string>
|
||||
</array>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>UTTypeConformsTo</key>
|
||||
<array>
|
||||
<string>public.data</string>
|
||||
</array>
|
||||
<key>UTTypeDescription</key>
|
||||
<string>WireGuard config</string>
|
||||
<key>UTTypeIconFiles</key>
|
||||
<array/>
|
||||
<key>UTTypeIdentifier</key>
|
||||
<string>${BUILD_OSX_APP_IDENTIFIER}.wireguard-config</string>
|
||||
<key>UTTypeTagSpecification</key>
|
||||
<dict>
|
||||
<key>public.filename-extension</key>
|
||||
<array>
|
||||
<string>conf</string>
|
||||
<string>cfg</string>
|
||||
</array>
|
||||
<key>public.mime-type</key>
|
||||
<array>
|
||||
<string>text/plain</string>
|
||||
</array>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>UTTypeConformsTo</key>
|
||||
<array>
|
||||
<string>public.data</string>
|
||||
</array>
|
||||
<key>UTTypeDescription</key>
|
||||
<string>OpenVPN config</string>
|
||||
<key>UTTypeIconFiles</key>
|
||||
<array/>
|
||||
<key>UTTypeIdentifier</key>
|
||||
<string>${BUILD_OSX_APP_IDENTIFIER}.openvpn-config</string>
|
||||
<key>UTTypeTagSpecification</key>
|
||||
<dict>
|
||||
<key>public.filename-extension</key>
|
||||
<array>
|
||||
<string>ovpn</string>
|
||||
</array>
|
||||
<key>public.mime-type</key>
|
||||
<array>
|
||||
<string>text/plain</string>
|
||||
</array>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>UTTypeConformsTo</key>
|
||||
<array>
|
||||
<string>public.data</string>
|
||||
</array>
|
||||
<key>UTTypeDescription</key>
|
||||
<string>${CLIENT_APPLICATION_NAME} backup file</string>
|
||||
<key>UTTypeIconFiles</key>
|
||||
<array/>
|
||||
<key>UTTypeIdentifier</key>
|
||||
<string>${BUILD_OSX_APP_IDENTIFIER}.backup-config</string>
|
||||
<key>UTTypeTagSpecification</key>
|
||||
<dict>
|
||||
<key>public.filename-extension</key>
|
||||
<array>
|
||||
<string>backup</string>
|
||||
</array>
|
||||
<key>public.mime-type</key>
|
||||
<array>
|
||||
<string>text/plain</string>
|
||||
</array>
|
||||
</dict>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleDocumentTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>${CLIENT_APPLICATION_NAME} config</string>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Alternate</string>
|
||||
<key>LSItemContentTypes</key>
|
||||
<array>
|
||||
<string>${BUILD_OSX_APP_IDENTIFIER}.amnezia-config</string>
|
||||
<string>${BUILD_OSX_APP_IDENTIFIER}.wireguard-config</string>
|
||||
<string>${BUILD_OSX_APP_IDENTIFIER}.openvpn-config</string>
|
||||
<string>${BUILD_OSX_APP_IDENTIFIER}.backup-config</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>NSExtensions</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>NSExtensionPointIdentifier</key>
|
||||
<string>com.apple.networkextension.packet-tunnel</string>
|
||||
<key>NSExtensionPrincipalClass</key>
|
||||
<string>$(PRODUCT_MODULE_NAME).PacketTunnelProvider</string>
|
||||
</dict>
|
||||
</array>
|
||||
|
||||
${CLIENT_MACOS_EXTRA_PLIST_ENTRIES}
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -10,6 +10,8 @@
|
||||
<string>${QT_INTERNAL_DOLLAR_VAR}{PRODUCT_NAME}</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>${MACOSX_BUNDLE_EXECUTABLE_NAME}</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>${MACOSX_BUNDLE_ICON_FILE}</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>${MACOSX_BUNDLE_GUI_IDENTIFIER}</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>${MACOSX_BUNDLE_EXECUTABLE_NAME}</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>${MACOSX_BUNDLE_GUI_IDENTIFIER}</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>AmneziaVPN</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>AmneziaVPN</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>${MACOSX_BUNDLE_SHORT_VERSION_STRING}</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>${MACOSX_BUNDLE_BUNDLE_VERSION}</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>${MACOSX_BUNDLE_ICON_FILE}</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>${CMAKE_OSX_DEPLOYMENT_TARGET}</string>
|
||||
<key>LSUIElement</key>
|
||||
<true/>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string>NSApplication</string>
|
||||
<key>AmneziaMainBundleIdentifier</key>
|
||||
<string>${AMNEZIA_LINK_HANDLER_MAIN_BUNDLE_ID}</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleURLName</key>
|
||||
<string>${AMNEZIA_LINK_HANDLER_MAIN_BUNDLE_ID}.vpn-deeplink</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>vpn</string>
|
||||
</array>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Viewer</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,106 @@
|
||||
#import <AppKit/AppKit.h>
|
||||
|
||||
namespace {
|
||||
NSString *const kMainBundleIdKey = @"AmneziaMainBundleIdentifier";
|
||||
NSString *const kFallbackMainAppPath = @"/Applications/AmneziaVPN.app";
|
||||
const NSTimeInterval kIdleTimeout = 5.0;
|
||||
|
||||
NSString *mainBundleId()
|
||||
{
|
||||
return [NSBundle.mainBundle objectForInfoDictionaryKey:kMainBundleIdKey];
|
||||
}
|
||||
|
||||
NSURL *mainAppUrl()
|
||||
{
|
||||
NSString *bundleId = mainBundleId();
|
||||
NSArray<NSRunningApplication *> *running =
|
||||
bundleId.length ? [NSRunningApplication runningApplicationsWithBundleIdentifier:bundleId] : @[];
|
||||
if (running.firstObject.bundleURL) {
|
||||
return running.firstObject.bundleURL;
|
||||
}
|
||||
|
||||
NSURL *url = bundleId.length ? [NSWorkspace.sharedWorkspace URLForApplicationWithBundleIdentifier:bundleId] : nil;
|
||||
if (!url && [NSFileManager.defaultManager fileExistsAtPath:kFallbackMainAppPath]) {
|
||||
url = [NSURL fileURLWithPath:kFallbackMainAppPath];
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
void launchMainExecutable(NSURL *appUrl, NSArray<NSURL *> *urls)
|
||||
{
|
||||
NSURL *executable = [NSBundle bundleWithURL:appUrl].executableURL;
|
||||
if (!executable) {
|
||||
return;
|
||||
}
|
||||
NSMutableArray<NSString *> *args = [NSMutableArray array];
|
||||
for (NSURL *url in urls) {
|
||||
[args addObject:url.absoluteString];
|
||||
}
|
||||
[NSTask launchedTaskWithExecutableURL:executable arguments:args error:nil terminationHandler:nil];
|
||||
}
|
||||
} // namespace
|
||||
|
||||
@interface LinkHandlerDelegate : NSObject <NSApplicationDelegate>
|
||||
@end
|
||||
|
||||
@implementation LinkHandlerDelegate
|
||||
|
||||
- (void)applicationDidFinishLaunching:(NSNotification *)notification
|
||||
{
|
||||
[self performSelector:@selector(quit) withObject:nil afterDelay:kIdleTimeout];
|
||||
}
|
||||
|
||||
- (void)application:(NSApplication *)application openURLs:(NSArray<NSURL *> *)urls
|
||||
{
|
||||
NSMutableArray<NSURL *> *vpnUrls = [NSMutableArray array];
|
||||
for (NSURL *url in urls) {
|
||||
if ([url.scheme caseInsensitiveCompare:@"vpn"] == NSOrderedSame) {
|
||||
[vpnUrls addObject:url];
|
||||
}
|
||||
}
|
||||
|
||||
NSURL *appUrl = mainAppUrl();
|
||||
if (!vpnUrls.count || !appUrl) {
|
||||
[self quit];
|
||||
return;
|
||||
}
|
||||
|
||||
NSString *bundleId = [NSBundle bundleWithURL:appUrl].bundleIdentifier ?: mainBundleId();
|
||||
if (@available(macOS 14.0, *)) {
|
||||
if (bundleId.length) {
|
||||
[NSApp yieldActivationToApplicationWithBundleIdentifier:bundleId];
|
||||
}
|
||||
}
|
||||
|
||||
NSWorkspaceOpenConfiguration *configuration = [NSWorkspaceOpenConfiguration configuration];
|
||||
configuration.activates = YES;
|
||||
[NSWorkspace.sharedWorkspace openURLs:vpnUrls
|
||||
withApplicationAtURL:appUrl
|
||||
configuration:configuration
|
||||
completionHandler:^(NSRunningApplication *app, NSError *error) {
|
||||
if (error) {
|
||||
launchMainExecutable(appUrl, vpnUrls);
|
||||
}
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[self quit];
|
||||
});
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)quit
|
||||
{
|
||||
[NSApp terminate:nil];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
int main(int argc, const char *argv[])
|
||||
{
|
||||
@autoreleasepool {
|
||||
NSApplication *application = NSApplication.sharedApplication;
|
||||
LinkHandlerDelegate *delegate = [LinkHandlerDelegate new];
|
||||
application.delegate = delegate;
|
||||
[application run];
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
+34
-7
@@ -1,3 +1,5 @@
|
||||
#include <QByteArray>
|
||||
#include <QCoreApplication>
|
||||
#include <QDebug>
|
||||
#include <QTimer>
|
||||
#include <libssh/libssh.h>
|
||||
@@ -9,6 +11,7 @@
|
||||
#include "core/utils/appUiConfig.h"
|
||||
#include "version.h"
|
||||
|
||||
#include <QLocalSocket>
|
||||
|
||||
// use openssl symbols to prevent linker throwing-off the OpenSSL dependency
|
||||
void anchorOpenSSL() {
|
||||
@@ -24,18 +27,41 @@ void anchorOpenSSL() {
|
||||
#endif
|
||||
|
||||
#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;
|
||||
socket.connectToServer(APP_INSTANCE_NAME);
|
||||
if (socket.waitForConnected(500)) {
|
||||
qWarning() << APPLICATION_NAME << "is already running";
|
||||
return true;
|
||||
if (!socket.waitForConnected(500)) {
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
qWarning() << APPLICATION_NAME << "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
|
||||
|
||||
// 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[])
|
||||
{
|
||||
Migrations migrationsManager;
|
||||
@@ -62,8 +88,9 @@ int main(int argc, char *argv[])
|
||||
});
|
||||
|
||||
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) && !defined(MACOS_NE)
|
||||
if (isAnotherInstanceRunning()) {
|
||||
QTimer::singleShot(1000, &app, [&]() { app.quit(); });
|
||||
const QString vpnFromArgv = findVpnDeepLinkInArguments(QCoreApplication::arguments());
|
||||
if (notifyRunningInstanceOrExit(app, vpnFromArgv)) {
|
||||
AmneziaApplication::markSecondaryInstanceForDeepLink();
|
||||
return app.exec();
|
||||
}
|
||||
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 <objc/runtime.h>
|
||||
#include <dispatch/dispatch.h>
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QFile>
|
||||
#include <QString>
|
||||
|
||||
#include "ios_controller.h"
|
||||
#import "AmneziaOpenUrlImport.h"
|
||||
|
||||
using SceneOpenURLContexts = void (*)(id, SEL, UIScene *, NSSet<UIOpenURLContext *> *);
|
||||
|
||||
@@ -14,29 +9,7 @@ static SceneOpenURLContexts g_originalSceneOpenURLContexts = nullptr;
|
||||
|
||||
static void amnezia_handleURL(NSURL *url)
|
||||
{
|
||||
if (!url || !url.isFileURL) {
|
||||
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));
|
||||
});
|
||||
AmneziaHandleOpenUrl(url);
|
||||
}
|
||||
|
||||
static void amnezia_scene_openURLContexts(id self, SEL _cmd, UIScene *scene, NSSet<UIOpenURLContext *> *contexts)
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
#import "QtAppDelegate.h"
|
||||
#import "ios_controller.h"
|
||||
|
||||
#include <QFile>
|
||||
|
||||
#import "AmneziaOpenUrlImport.h"
|
||||
|
||||
@implementation QIOSApplicationDelegate (AmneziaVPNDelegate)
|
||||
#if !MACOS_NE
|
||||
@@ -11,6 +8,10 @@
|
||||
[application setMinimumBackgroundFetchInterval: UIApplicationBackgroundFetchIntervalMinimum];
|
||||
// Override point for customization after application launch.
|
||||
NSLog(@"Application didFinishLaunchingWithOptions");
|
||||
NSURL *launchUrl = launchOptions[UIApplicationLaunchOptionsURLKey];
|
||||
if (launchUrl) {
|
||||
AmneziaHandleOpenUrl(launchUrl);
|
||||
}
|
||||
return YES;
|
||||
}
|
||||
|
||||
@@ -35,24 +36,11 @@
|
||||
- (BOOL)application:(UIApplication *)app
|
||||
openURL:(NSURL *)url
|
||||
options:(NSDictionary<UIApplicationOpenURLOptionsKey, id> *)options {
|
||||
if (url.fileURL) {
|
||||
QString filePath(url.path.UTF8String);
|
||||
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));
|
||||
}
|
||||
});
|
||||
NSLog(@"Application openURL: %@", url);
|
||||
AmneziaHandleOpenUrl(url);
|
||||
|
||||
NSString *scheme = url.scheme ? [url.scheme lowercaseString] : @"";
|
||||
if ([scheme isEqualToString:@"vpn"] || url.fileURL) {
|
||||
return YES;
|
||||
}
|
||||
return NO;
|
||||
|
||||
@@ -144,6 +144,20 @@ private slots:
|
||||
QVERIFY2(m_coreController->m_appSettingsRepository->isStartMinimized() == !initialStartMinimized, "Start minimized state should be available in SecureAppSettingsRepository");
|
||||
}
|
||||
|
||||
void testVpnDeepLinkSignals() {
|
||||
QSignalSpy vpnDeepLinkChangedSpy(m_coreController->m_settingsUiController, &SettingsUiController::vpnDeepLinkChanged);
|
||||
|
||||
m_coreController->m_appSettingsRepository->setVpnDeepLinkPromptDismissed(false);
|
||||
m_coreController->m_settingsUiController->dismissVpnDeepLinkPrompt();
|
||||
QVERIFY2(m_coreController->m_settingsController->isVpnDeepLinkPromptDismissed(), "Prompt dismissal should be stored");
|
||||
QVERIFY2(m_coreController->m_appSettingsRepository->isVpnDeepLinkPromptDismissed(), "Prompt dismissal should be available in SecureAppSettingsRepository");
|
||||
|
||||
m_coreController->m_settingsUiController->refreshVpnDeepLinkStatus();
|
||||
QVERIFY2(vpnDeepLinkChangedSpy.count() == 1, "vpnDeepLinkChanged signal should be emitted on refresh");
|
||||
QVERIFY2(m_coreController->m_settingsUiController->isVpnDeepLinkEnabled() == m_coreController->m_appSettingsRepository->isVpnDeepLinkEnabled(),
|
||||
"Deep link state should be available in SettingsUiController");
|
||||
}
|
||||
|
||||
void testAutoConnectSignals() {
|
||||
bool initialAutoConnect = m_coreController->m_settingsController->isAutoConnectEnabled();
|
||||
|
||||
|
||||
@@ -8,6 +8,10 @@
|
||||
#include "core/utils/api/apiUtils.h"
|
||||
#include "core/utils/qrCodeUtils.h"
|
||||
#include "ui/controllers/systemController.h"
|
||||
#ifdef Q_OS_IOS
|
||||
#include "platforms/ios/ios_controller.h"
|
||||
#include <QThread>
|
||||
#endif
|
||||
#include "version.h"
|
||||
#include <QClipboard>
|
||||
#include <QDebug>
|
||||
|
||||
@@ -20,6 +20,9 @@
|
||||
#if defined Q_OS_MAC
|
||||
#include "ui/utils/macosUtil.h"
|
||||
#endif
|
||||
#ifdef Q_OS_WIN
|
||||
#include "ui/utils/windowsUtil.h"
|
||||
#endif
|
||||
|
||||
PageController::PageController(ServersController* serversController, SettingsController* settingsController,
|
||||
QObject *parent)
|
||||
@@ -55,6 +58,10 @@ PageController::PageController(ServersController* serversController, SettingsCon
|
||||
});
|
||||
#endif
|
||||
|
||||
#if defined Q_OS_MACX || defined Q_OS_WIN
|
||||
connect(this, &PageController::raiseMainWindow, this, []() { activateApplication(); }, Qt::QueuedConnection);
|
||||
#endif
|
||||
|
||||
connect(this, qOverload<ErrorCode>(&PageController::showErrorMessage), this, &PageController::onShowErrorMessage);
|
||||
|
||||
m_isTriggeredByConnectButton = false;
|
||||
|
||||
@@ -84,6 +84,7 @@ signals:
|
||||
void passphraseRequestDrawerClosed(QString passphrase);
|
||||
|
||||
void unsupportedConnectDrawerRequested();
|
||||
void vpnDeepLinkDefaultDrawerRequested();
|
||||
|
||||
void escapePressed();
|
||||
|
||||
|
||||
@@ -135,6 +135,7 @@ void SettingsUiController::restoreAppConfigFromData(const QByteArray &data)
|
||||
emit restoreBackupFinished();
|
||||
emit autoStartChanged();
|
||||
emit startMinimizedChanged();
|
||||
emit vpnDeepLinkChanged();
|
||||
|
||||
if (m_settingsController->unsupportedFormatConfigsSkippedCount() > 0) {
|
||||
emit errorOccurred(ErrorCode::RestoreBackupUnsupportedConfigsSkipped);
|
||||
@@ -154,6 +155,7 @@ void SettingsUiController::clearSettings()
|
||||
m_settingsController->clearSettings();
|
||||
emit autoStartChanged();
|
||||
emit startMinimizedChanged();
|
||||
emit vpnDeepLinkChanged();
|
||||
emit resetLanguageToSystem();
|
||||
|
||||
emit changeSettingsFinished(tr("All settings have been reset to default values"));
|
||||
@@ -196,6 +198,40 @@ void SettingsUiController::toggleStartMinimized(bool enable)
|
||||
emit startMinimizedChanged();
|
||||
}
|
||||
|
||||
bool SettingsUiController::isVpnDeepLinkEnabled()
|
||||
{
|
||||
return m_settingsController->isVpnDeepLinkEnabled();
|
||||
}
|
||||
|
||||
void SettingsUiController::toggleVpnDeepLink(bool enable)
|
||||
{
|
||||
m_settingsController->toggleVpnDeepLink(enable);
|
||||
emit vpnDeepLinkChanged();
|
||||
}
|
||||
|
||||
bool SettingsUiController::isVpnDeepLinkOverridden()
|
||||
{
|
||||
return m_settingsController->isVpnDeepLinkOverridden();
|
||||
}
|
||||
|
||||
void SettingsUiController::makeVpnDeepLinkDefault()
|
||||
{
|
||||
if (!m_settingsController->makeVpnDeepLinkDefault()) {
|
||||
emit changeSettingsFinished(tr("Select AmneziaVPN for vpn:// links in the system settings"));
|
||||
}
|
||||
emit vpnDeepLinkChanged();
|
||||
}
|
||||
|
||||
void SettingsUiController::dismissVpnDeepLinkPrompt()
|
||||
{
|
||||
m_settingsController->dismissVpnDeepLinkPrompt();
|
||||
}
|
||||
|
||||
void SettingsUiController::refreshVpnDeepLinkStatus()
|
||||
{
|
||||
emit vpnDeepLinkChanged();
|
||||
}
|
||||
|
||||
bool SettingsUiController::isScreenshotsEnabled()
|
||||
{
|
||||
return m_settingsController->isScreenshotsEnabled();
|
||||
|
||||
@@ -31,6 +31,8 @@ public:
|
||||
Q_PROPERTY(bool isHomeAdLabelVisible READ isHomeAdLabelVisible NOTIFY isHomeAdLabelVisibleChanged)
|
||||
Q_PROPERTY(bool autoStartEnabled READ isAutoStartEnabled NOTIFY autoStartChanged)
|
||||
Q_PROPERTY(bool startMinimized READ isStartMinimizedEnabled NOTIFY startMinimizedChanged)
|
||||
Q_PROPERTY(bool vpnDeepLinkEnabled READ isVpnDeepLinkEnabled NOTIFY vpnDeepLinkChanged)
|
||||
Q_PROPERTY(bool vpnDeepLinkOverridden READ isVpnDeepLinkOverridden NOTIFY vpnDeepLinkChanged)
|
||||
|
||||
public slots:
|
||||
void toggleAmneziaDns(bool enable);
|
||||
@@ -65,6 +67,13 @@ public slots:
|
||||
bool isStartMinimizedEnabled();
|
||||
void toggleStartMinimized(bool enable);
|
||||
|
||||
bool isVpnDeepLinkEnabled();
|
||||
void toggleVpnDeepLink(bool enable);
|
||||
bool isVpnDeepLinkOverridden();
|
||||
void makeVpnDeepLinkDefault();
|
||||
void dismissVpnDeepLinkPrompt();
|
||||
void refreshVpnDeepLinkStatus();
|
||||
|
||||
bool isNewsNotificationsEnabled();
|
||||
void toggleNewsNotificationsEnabled(bool enable);
|
||||
|
||||
@@ -135,6 +144,7 @@ signals:
|
||||
void isHomeAdLabelVisibleChanged(bool visible);
|
||||
void autoStartChanged();
|
||||
void startMinimizedChanged();
|
||||
void vpnDeepLinkChanged();
|
||||
|
||||
private:
|
||||
SettingsController* m_settingsController;
|
||||
|
||||
@@ -13,6 +13,13 @@ import "../Components"
|
||||
PageType {
|
||||
id: root
|
||||
|
||||
Component.onCompleted: SettingsController.refreshVpnDeepLinkStatus()
|
||||
onVisibleChanged: {
|
||||
if (visible) {
|
||||
SettingsController.refreshVpnDeepLinkStatus()
|
||||
}
|
||||
}
|
||||
|
||||
FlickableType {
|
||||
id: flickable
|
||||
|
||||
@@ -260,6 +267,51 @@ PageType {
|
||||
}
|
||||
}
|
||||
|
||||
ListItemType {
|
||||
id: switcherVpnDeepLinkRow
|
||||
|
||||
Layout.fillWidth: true
|
||||
|
||||
visible: !GC.isMobile()
|
||||
|
||||
text: qsTr("Open vpn:// links")
|
||||
subtitle: qsTr("Import a key when clicking a vpn:// link in the browser")
|
||||
|
||||
onClicked: {
|
||||
switcherVpnDeepLink.toggle()
|
||||
switcherVpnDeepLink.toggled()
|
||||
}
|
||||
|
||||
SwitchType {
|
||||
id: switcherVpnDeepLink
|
||||
|
||||
focusPolicy: Qt.NoFocus
|
||||
|
||||
checked: SettingsController.vpnDeepLinkEnabled
|
||||
onToggled: function() {
|
||||
if (checked !== SettingsController.vpnDeepLinkEnabled) {
|
||||
SettingsController.toggleVpnDeepLink(checked)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ListItemType {
|
||||
id: vpnDeepLinkOverrideRow
|
||||
|
||||
Layout.fillWidth: true
|
||||
|
||||
visible: !GC.isMobile() && SettingsController.vpnDeepLinkEnabled && SettingsController.vpnDeepLinkOverridden
|
||||
|
||||
text: qsTr("Override alias settings for vpn:// links")
|
||||
subtitle: qsTr("vpn:// links are currently opened by another application")
|
||||
trailingIconSource: "qrc:/images/controls/chevron-right.svg"
|
||||
|
||||
onClicked: {
|
||||
SettingsController.makeVpnDeepLinkDefault()
|
||||
}
|
||||
}
|
||||
|
||||
ListItemType {
|
||||
id: labelWithButtonReset
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ PageType {
|
||||
NavigationBarType {
|
||||
Layout.fillWidth: true
|
||||
|
||||
title: qsTr("New connection")
|
||||
title: qsTr("Add this connection?")
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
@@ -186,7 +186,7 @@ PageType {
|
||||
|
||||
Layout.fillWidth: true
|
||||
|
||||
text: qsTr("Connect")
|
||||
text: qsTr("Add")
|
||||
|
||||
onClicked: {
|
||||
if (cloakingCheckBox.checked) {
|
||||
|
||||
@@ -260,6 +260,10 @@ Window {
|
||||
function onUnsupportedConnectDrawerRequested() {
|
||||
root.showUnsupportedConnectDrawer()
|
||||
}
|
||||
|
||||
function onVpnDeepLinkDefaultDrawerRequested() {
|
||||
root.showVpnDeepLinkDefaultDrawer()
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
@@ -335,6 +339,22 @@ Window {
|
||||
showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction)
|
||||
}
|
||||
|
||||
function showVpnDeepLinkDefaultDrawer() {
|
||||
let headerText = qsTr("Make AmneziaVPN the default app for vpn:// links?")
|
||||
let descriptionText = qsTr("vpn:// links are currently opened by another application. You can change this later in Settings → Application.")
|
||||
let yesButtonText = qsTr("Make default")
|
||||
let noButtonText = qsTr("No")
|
||||
|
||||
let yesButtonFunction = function() {
|
||||
SettingsController.makeVpnDeepLinkDefault()
|
||||
}
|
||||
let noButtonFunction = function() {
|
||||
SettingsController.dismissVpnDeepLinkPrompt()
|
||||
}
|
||||
|
||||
showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction)
|
||||
}
|
||||
|
||||
function showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction) {
|
||||
questionDrawer.headerText = headerText
|
||||
questionDrawer.descriptionText = descriptionText
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <QWidget>
|
||||
|
||||
void setDockIconVisible(bool visible);
|
||||
void activateApplication();
|
||||
void fixWidget(QWidget *widget);
|
||||
#endif
|
||||
|
||||
|
||||
@@ -36,6 +36,15 @@ void setDockIconVisible(bool visible)
|
||||
}
|
||||
}
|
||||
|
||||
void activateApplication()
|
||||
{
|
||||
if (@available(macOS 14.0, *)) {
|
||||
[NSApp activate];
|
||||
} else {
|
||||
[NSApp activateIgnoringOtherApps:YES];
|
||||
}
|
||||
}
|
||||
|
||||
// this Objective-c class is used to override the action of system close button and zoom button
|
||||
// https://stackoverflow.com/questions/27643659/setting-c-function-as-selector-for-nsbutton-produces-no-results
|
||||
@interface ButtonPasser : NSObject {
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
#include "vpnUrlHandler.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QProcess>
|
||||
#include <QSettings>
|
||||
#include <QStandardPaths>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
|
||||
#if defined(Q_OS_WIN)
|
||||
#include <QDesktopServices>
|
||||
#include <QUrl>
|
||||
|
||||
#include <windows.h>
|
||||
#include <shlwapi.h>
|
||||
|
||||
namespace {
|
||||
const QString kClassesKey = QStringLiteral("HKEY_CURRENT_USER\\Software\\Classes\\vpn");
|
||||
const QString kCommandKey = QStringLiteral("HKEY_CURRENT_USER\\Software\\Classes\\vpn\\shell\\open\\command");
|
||||
const QString kUrlName = QStringLiteral("URL:AmneziaVPN");
|
||||
|
||||
QString appExe()
|
||||
{
|
||||
return QDir::toNativeSeparators(QCoreApplication::applicationFilePath());
|
||||
}
|
||||
|
||||
QString openCommand()
|
||||
{
|
||||
return QStringLiteral("\"%1\" \"%2\"").arg(appExe(), QStringLiteral("%1"));
|
||||
}
|
||||
|
||||
QString effectiveHandlerExe()
|
||||
{
|
||||
wchar_t buf[MAX_PATH * 2] = {};
|
||||
DWORD size = ARRAYSIZE(buf);
|
||||
const HRESULT hr = AssocQueryStringW(ASSOCF_IS_PROTOCOL, ASSOCSTR_EXECUTABLE, L"vpn", nullptr, buf, &size);
|
||||
if (FAILED(hr)) {
|
||||
return {};
|
||||
}
|
||||
return QString::fromWCharArray(buf);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
VpnUrlHandler::Status VpnUrlHandler::status()
|
||||
{
|
||||
const QString handler = effectiveHandlerExe();
|
||||
if (handler.isEmpty()) {
|
||||
return Status::NotRegistered;
|
||||
}
|
||||
|
||||
if (QFileInfo(handler).canonicalFilePath().compare(QFileInfo(appExe()).canonicalFilePath(), Qt::CaseInsensitive) != 0) {
|
||||
return Status::OtherHandler;
|
||||
}
|
||||
|
||||
QSettings vpnKey(kClassesKey, QSettings::NativeFormat);
|
||||
if (!vpnKey.contains(QStringLiteral("URL Protocol"))) {
|
||||
return Status::OtherHandler;
|
||||
}
|
||||
return Status::IsDefault;
|
||||
}
|
||||
|
||||
bool VpnUrlHandler::setAsDefault()
|
||||
{
|
||||
QSettings vpnKey(kClassesKey, QSettings::NativeFormat);
|
||||
vpnKey.setValue(QStringLiteral("."), kUrlName);
|
||||
vpnKey.setValue(QStringLiteral("URL Protocol"), QString());
|
||||
vpnKey.sync();
|
||||
|
||||
QSettings cmdKey(kCommandKey, QSettings::NativeFormat);
|
||||
cmdKey.setValue(QStringLiteral("."), openCommand());
|
||||
cmdKey.sync();
|
||||
|
||||
if (status() == Status::IsDefault) {
|
||||
return true;
|
||||
}
|
||||
|
||||
QDesktopServices::openUrl(QUrl(QStringLiteral("ms-settings:defaultapps")));
|
||||
return false;
|
||||
}
|
||||
|
||||
void VpnUrlHandler::unregister()
|
||||
{
|
||||
QSettings cmdKey(kCommandKey, QSettings::NativeFormat);
|
||||
if (cmdKey.value(QStringLiteral(".")).toString().compare(openCommand(), Qt::CaseInsensitive) != 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
QSettings classesKey(QStringLiteral("HKEY_CURRENT_USER\\Software\\Classes"), QSettings::NativeFormat);
|
||||
classesKey.remove(QStringLiteral("vpn"));
|
||||
}
|
||||
|
||||
#elif defined(Q_OS_MACX)
|
||||
|
||||
#elif defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID)
|
||||
|
||||
namespace {
|
||||
const QString kDesktopFile = QStringLiteral("AmneziaVPN.desktop");
|
||||
const QString kMimeType = QStringLiteral("x-scheme-handler/vpn");
|
||||
|
||||
bool runXdgMime(const QStringList &args, QString *output = nullptr)
|
||||
{
|
||||
QProcess process;
|
||||
process.start(QStringLiteral("xdg-mime"), args);
|
||||
if (!process.waitForFinished(3000) || process.exitStatus() != QProcess::NormalExit || process.exitCode() != 0) {
|
||||
return false;
|
||||
}
|
||||
if (output) {
|
||||
*output = QString::fromLocal8Bit(process.readAllStandardOutput()).trimmed();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
VpnUrlHandler::Status VpnUrlHandler::status()
|
||||
{
|
||||
if (QStandardPaths::findExecutable(QStringLiteral("xdg-mime")).isEmpty()) {
|
||||
return Status::IsDefault;
|
||||
}
|
||||
|
||||
QString handler;
|
||||
if (!runXdgMime({ QStringLiteral("query"), QStringLiteral("default"), kMimeType }, &handler) || handler.isEmpty()) {
|
||||
return Status::NotRegistered;
|
||||
}
|
||||
return handler == kDesktopFile ? Status::IsDefault : Status::OtherHandler;
|
||||
}
|
||||
|
||||
bool VpnUrlHandler::setAsDefault()
|
||||
{
|
||||
return runXdgMime({ QStringLiteral("default"), kDesktopFile, kMimeType }) && status() == Status::IsDefault;
|
||||
}
|
||||
|
||||
void VpnUrlHandler::unregister()
|
||||
{
|
||||
const QString path = QStandardPaths::writableLocation(QStandardPaths::ConfigLocation) + QStringLiteral("/mimeapps.list");
|
||||
QFile file(path);
|
||||
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||
return;
|
||||
}
|
||||
const QStringList lines = QString::fromUtf8(file.readAll()).split(QLatin1Char('\n'));
|
||||
file.close();
|
||||
|
||||
const QString prefix = kMimeType + QLatin1Char('=');
|
||||
QStringList result;
|
||||
bool changed = false;
|
||||
for (const QString &line : lines) {
|
||||
if (!line.startsWith(prefix)) {
|
||||
result << line;
|
||||
continue;
|
||||
}
|
||||
QStringList apps = line.mid(prefix.size()).split(QLatin1Char(';'), Qt::SkipEmptyParts);
|
||||
if (apps.removeAll(kDesktopFile) == 0) {
|
||||
result << line;
|
||||
continue;
|
||||
}
|
||||
changed = true;
|
||||
if (!apps.isEmpty()) {
|
||||
result << prefix + apps.join(QLatin1Char(';')) + QLatin1Char(';');
|
||||
}
|
||||
}
|
||||
|
||||
if (changed && file.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text)) {
|
||||
file.write(result.join(QLatin1Char('\n')).toUtf8());
|
||||
}
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
VpnUrlHandler::Status VpnUrlHandler::status()
|
||||
{
|
||||
return Status::IsDefault;
|
||||
}
|
||||
|
||||
bool VpnUrlHandler::setAsDefault()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
void VpnUrlHandler::unregister()
|
||||
{
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,20 @@
|
||||
#ifndef VPNURLHANDLER_H
|
||||
#define VPNURLHANDLER_H
|
||||
|
||||
class VpnUrlHandler
|
||||
{
|
||||
public:
|
||||
enum class Status {
|
||||
IsDefault,
|
||||
NotRegistered,
|
||||
OtherHandler
|
||||
};
|
||||
|
||||
static Status status();
|
||||
|
||||
static bool setAsDefault();
|
||||
|
||||
static void unregister();
|
||||
};
|
||||
|
||||
#endif // VPNURLHANDLER_H
|
||||
@@ -0,0 +1,225 @@
|
||||
#include "vpnUrlHandler.h"
|
||||
|
||||
#import <AppKit/AppKit.h>
|
||||
#import <CoreServices/CoreServices.h>
|
||||
|
||||
namespace {
|
||||
NSURL *probeUrl()
|
||||
{
|
||||
return [NSURL URLWithString:@"vpn://probe"];
|
||||
}
|
||||
} // namespace
|
||||
|
||||
#ifdef MACOS_NE
|
||||
|
||||
VpnUrlHandler::Status VpnUrlHandler::status()
|
||||
{
|
||||
@autoreleasepool {
|
||||
NSURL *handler = [[NSWorkspace sharedWorkspace] URLForApplicationToOpenURL:probeUrl()];
|
||||
if (!handler) {
|
||||
return Status::NotRegistered;
|
||||
}
|
||||
NSURL *ours = [[NSBundle mainBundle] bundleURL];
|
||||
return [handler.URLByStandardizingPath isEqual:ours.URLByStandardizingPath] ? Status::IsDefault
|
||||
: Status::OtherHandler;
|
||||
}
|
||||
}
|
||||
|
||||
bool VpnUrlHandler::setAsDefault()
|
||||
{
|
||||
@autoreleasepool {
|
||||
[[NSWorkspace sharedWorkspace] setDefaultApplicationAtURL:[[NSBundle mainBundle] bundleURL]
|
||||
toOpenURLsWithScheme:@"vpn"
|
||||
completionHandler:^(NSError *error) {
|
||||
}];
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
void VpnUrlHandler::unregister()
|
||||
{
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
namespace {
|
||||
NSString *const kLsRegister =
|
||||
@"/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister";
|
||||
|
||||
NSURL *templateUrl()
|
||||
{
|
||||
return [[[NSBundle mainBundle] resourceURL] URLByAppendingPathComponent:@"LinkHandler.template" isDirectory:YES];
|
||||
}
|
||||
|
||||
NSString *helperBundleId()
|
||||
{
|
||||
return [NSBundle bundleWithURL:templateUrl()].bundleIdentifier;
|
||||
}
|
||||
|
||||
NSURL *dataFolderUrl()
|
||||
{
|
||||
NSURL *appSupport = [[NSFileManager defaultManager] URLsForDirectory:NSApplicationSupportDirectory
|
||||
inDomains:NSUserDomainMask].firstObject;
|
||||
return [appSupport URLByAppendingPathComponent:@"AmneziaVPN" isDirectory:YES];
|
||||
}
|
||||
|
||||
NSURL *copyUrl()
|
||||
{
|
||||
return [[dataFolderUrl() URLByAppendingPathComponent:@"LinkHandler" isDirectory:YES]
|
||||
URLByAppendingPathComponent:@"AmneziaVPN.app"
|
||||
isDirectory:YES];
|
||||
}
|
||||
|
||||
bool sameFile(NSURL *a, NSURL *b)
|
||||
{
|
||||
return [a.URLByResolvingSymlinksInPath.path isEqualToString:b.URLByResolvingSymlinksInPath.path];
|
||||
}
|
||||
|
||||
bool isOurHelper(NSURL *app)
|
||||
{
|
||||
NSString *helperId = helperBundleId();
|
||||
return sameFile(app, copyUrl())
|
||||
|| (helperId.length && [[NSBundle bundleWithURL:app].bundleIdentifier isEqualToString:helperId]);
|
||||
}
|
||||
|
||||
bool runTool(NSString *path, NSArray<NSString *> *args)
|
||||
{
|
||||
NSTask *task = [NSTask launchedTaskWithExecutableURL:[NSURL fileURLWithPath:path]
|
||||
arguments:args
|
||||
error:nil
|
||||
terminationHandler:nil];
|
||||
if (!task) {
|
||||
return false;
|
||||
}
|
||||
[task waitUntilExit];
|
||||
return task.terminationStatus == 0;
|
||||
}
|
||||
|
||||
bool sameContents(NSURL *a, NSURL *b, NSString *relativePath)
|
||||
{
|
||||
NSData *left = [NSData dataWithContentsOfURL:[a URLByAppendingPathComponent:relativePath]];
|
||||
NSData *right = [NSData dataWithContentsOfURL:[b URLByAppendingPathComponent:relativePath]];
|
||||
if (!left || !right) {
|
||||
return !left && !right;
|
||||
}
|
||||
return [left isEqualToData:right];
|
||||
}
|
||||
|
||||
bool isCopyUpToDate()
|
||||
{
|
||||
NSURL *tmpl = templateUrl();
|
||||
NSURL *copy = copyUrl();
|
||||
NSString *executable = [[NSBundle bundleWithURL:tmpl] objectForInfoDictionaryKey:@"CFBundleExecutable"];
|
||||
if (!executable.length) {
|
||||
return false;
|
||||
}
|
||||
NSString *executablePath = [@"Contents/MacOS" stringByAppendingPathComponent:executable];
|
||||
return [[NSFileManager defaultManager] fileExistsAtPath:[copy URLByAppendingPathComponent:executablePath].path]
|
||||
&& sameContents(tmpl, copy, @"Contents/Info.plist")
|
||||
&& sameContents(tmpl, copy, executablePath)
|
||||
&& sameContents(tmpl, copy, @"Contents/_CodeSignature/CodeResources");
|
||||
}
|
||||
|
||||
void removeHelper(NSURL *app)
|
||||
{
|
||||
runTool(kLsRegister, @[ @"-u", app.path ]);
|
||||
|
||||
NSString *folder = [dataFolderUrl().URLByResolvingSymlinksInPath.path stringByAppendingString:@"/"];
|
||||
if (![app.URLByResolvingSymlinksInPath.path hasPrefix:folder]
|
||||
|| ![[NSFileManager defaultManager] fileExistsAtPath:app.path]) {
|
||||
return;
|
||||
}
|
||||
[[NSFileManager defaultManager] removeItemAtURL:app error:nil];
|
||||
}
|
||||
|
||||
void removeHelpers(NSURL *keep)
|
||||
{
|
||||
NSMutableArray<NSURL *> *apps =
|
||||
[[[NSWorkspace sharedWorkspace] URLsForApplicationsToOpenURL:probeUrl()] mutableCopy] ?: [NSMutableArray array];
|
||||
[apps addObject:copyUrl()];
|
||||
for (NSURL *app in apps) {
|
||||
if (keep && sameFile(app, keep)) {
|
||||
continue;
|
||||
}
|
||||
if (isOurHelper(app) && [[NSFileManager defaultManager] fileExistsAtPath:app.path]) {
|
||||
removeHelper(app);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool ensureCopy()
|
||||
{
|
||||
NSURL *tmpl = templateUrl();
|
||||
if (![[NSFileManager defaultManager] fileExistsAtPath:tmpl.path]) {
|
||||
return false;
|
||||
}
|
||||
if (isCopyUpToDate()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
removeHelpers(nil);
|
||||
NSURL *copy = copyUrl();
|
||||
if (![[NSFileManager defaultManager] createDirectoryAtURL:copy.URLByDeletingLastPathComponent
|
||||
withIntermediateDirectories:YES
|
||||
attributes:nil
|
||||
error:nil]) {
|
||||
return false;
|
||||
}
|
||||
return runTool(@"/usr/bin/ditto", @[ tmpl.path, copy.path ]);
|
||||
}
|
||||
|
||||
VpnUrlHandler::Status evaluateStatus(NSURL *handler)
|
||||
{
|
||||
if (!handler) {
|
||||
return VpnUrlHandler::Status::NotRegistered;
|
||||
}
|
||||
if (sameFile(handler, copyUrl())) {
|
||||
return isCopyUpToDate() ? VpnUrlHandler::Status::IsDefault : VpnUrlHandler::Status::NotRegistered;
|
||||
}
|
||||
if (isOurHelper(handler)) {
|
||||
return VpnUrlHandler::Status::NotRegistered;
|
||||
}
|
||||
NSString *handlerId = [NSBundle bundleWithURL:handler].bundleIdentifier;
|
||||
if ([handlerId isEqualToString:[NSBundle mainBundle].bundleIdentifier]) {
|
||||
return VpnUrlHandler::Status::NotRegistered;
|
||||
}
|
||||
return VpnUrlHandler::Status::OtherHandler;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
VpnUrlHandler::Status VpnUrlHandler::status()
|
||||
{
|
||||
@autoreleasepool {
|
||||
return evaluateStatus([[NSWorkspace sharedWorkspace] URLForApplicationToOpenURL:probeUrl()]);
|
||||
}
|
||||
}
|
||||
|
||||
bool VpnUrlHandler::setAsDefault()
|
||||
{
|
||||
@autoreleasepool {
|
||||
if (!ensureCopy()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
NSURL *copy = copyUrl();
|
||||
removeHelpers(copy);
|
||||
if (LSRegisterURL((__bridge CFURLRef)copy, true) != noErr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
[[NSWorkspace sharedWorkspace] setDefaultApplicationAtURL:copy
|
||||
toOpenURLsWithScheme:@"vpn"
|
||||
completionHandler:^(NSError *error) {
|
||||
}];
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
void VpnUrlHandler::unregister()
|
||||
{
|
||||
@autoreleasepool {
|
||||
removeHelpers(nil);
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,46 @@
|
||||
#include "windowsUtil.h"
|
||||
|
||||
#include <QGuiApplication>
|
||||
#include <QWindow>
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
void activateApplication()
|
||||
{
|
||||
QWindow *window = nullptr;
|
||||
for (QWindow *candidate : QGuiApplication::topLevelWindows()) {
|
||||
if (candidate->objectName() == QLatin1String("mainWindow")) {
|
||||
window = candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!window) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (window->visibility() == QWindow::Hidden) {
|
||||
window->show();
|
||||
}
|
||||
|
||||
const HWND hwnd = reinterpret_cast<HWND>(window->winId());
|
||||
if (!hwnd) {
|
||||
return;
|
||||
}
|
||||
if (IsIconic(hwnd)) {
|
||||
ShowWindow(hwnd, SW_RESTORE);
|
||||
}
|
||||
|
||||
const HWND foreground = GetForegroundWindow();
|
||||
if (foreground == hwnd) {
|
||||
return;
|
||||
}
|
||||
|
||||
const DWORD foregroundThread = foreground ? GetWindowThreadProcessId(foreground, nullptr) : 0;
|
||||
const DWORD currentThread = GetCurrentThreadId();
|
||||
const bool attached = foregroundThread && foregroundThread != currentThread
|
||||
&& AttachThreadInput(currentThread, foregroundThread, TRUE);
|
||||
SetForegroundWindow(hwnd);
|
||||
if (attached) {
|
||||
AttachThreadInput(currentThread, foregroundThread, FALSE);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
#ifndef WINDOWSUTIL_H
|
||||
#define WINDOWSUTIL_H
|
||||
|
||||
void activateApplication();
|
||||
|
||||
#endif // WINDOWSUTIL_H
|
||||
@@ -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()
|
||||
@@ -40,7 +40,12 @@ if(APPLE)
|
||||
set(other_execs ${execs})
|
||||
list(FILTER other_execs EXCLUDE REGEX [[AmneziaVPN$|AmneziaVPN-service$]])
|
||||
|
||||
list(APPEND files "${frameworks}" "${dylibs}" "${other_execs}" "${service_exec}" "${client_exec}" "${bundle}")
|
||||
set(link_handler "${bundle}/Contents/Resources/LinkHandler.template")
|
||||
if(NOT EXISTS "${link_handler}")
|
||||
set(link_handler "")
|
||||
endif()
|
||||
|
||||
list(APPEND files "${frameworks}" "${dylibs}" "${other_execs}" "${service_exec}" "${link_handler}" "${client_exec}" "${bundle}")
|
||||
|
||||
if (files AND CODESIGN_SIGNATURE)
|
||||
include(${CMAKE_CURRENT_LIST_DIR}/util/codesign.cmake)
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
#!/usr/bin/env xdg-open
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=AmneziaVPN
|
||||
Version=1.0
|
||||
Comment=Client of your self-hosted VPN
|
||||
Exec=AmneziaVPN
|
||||
Exec=/opt/AmneziaVPN/bin/AmneziaVPN %u
|
||||
Icon=/usr/share/pixmaps/AmneziaVPN.png
|
||||
Categories=Network;Qt;Security;
|
||||
MimeType=x-scheme-handler/vpn;
|
||||
Terminal=false
|
||||
StartupWMClass=AmneziaVPN
|
||||
@@ -30,6 +30,13 @@ if sudo systemctl is-active --quiet $APP_NAME; then
|
||||
sudo rm -rf /etc/systemd/system/$APP_NAME.service >> $LOG_FILE
|
||||
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 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 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
|
||||
|
||||
if command -v steamos-readonly &> /dev/null; then
|
||||
|
||||
@@ -40,7 +40,7 @@ fi
|
||||
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"
|
||||
|
||||
# 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
|
||||
log "Group $SERVICE_GROUP already exists"
|
||||
else
|
||||
@@ -54,6 +54,21 @@ run_cmd chmod -R a-w "$APP_PATH/"
|
||||
run_cmd chown -R root "$APP_PATH/"
|
||||
run_cmd 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"
|
||||
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