Compare commits

..

18 Commits

Author SHA1 Message Date
lunardunno
23eff43575 Open docs url according for Ukrainian language 2024-05-24 11:53:09 +04:00
lunardunno
92d9071b79 Open docs url according for Ukrainian language 2024-05-24 11:35:34 +04:00
Vladyslav Miachkov
8cbc5c6a88 Fix wrong identation 2024-05-20 10:12:54 +03:00
Vladyslav Miachkov
1ada5e7ab9 Refactor error message logic 2024-05-19 14:50:39 +03:00
Vladyslav Miachkov
f7edba7757 Open docs url according to language set 2024-05-19 14:15:23 +03:00
Vladyslav Miachkov
e672a4a471 Change mouse icon on link hover 2024-05-19 14:15:23 +03:00
Vladyslav Miachkov
1bc43a9c06 Add clickable docs url on error 2024-05-19 14:15:23 +03:00
pokamest
53fdf5f70d Merge pull request #811 from amnezia-vpn/feature/api-payload-info
change pretty product name to product type for api payload
2024-05-17 04:57:00 -07:00
vladimir.kuznetsov
871aced1d1 change pretty product name to product type for api payload 2024-05-17 09:40:02 +02:00
Nethius
2254bfc128 added the OS version and application version to the api request payload (#810)
* added the OS version and application version to the api request payload
* added errorStrings for new api error codes
2024-05-16 18:57:51 +01:00
pokamest
b71dcb8dd0 Merge pull request #808 from theLastOfCats/dev
Remove misleading iOS and Android support from IPSec protocol transtation strings
2024-05-16 06:22:43 -07:00
pokamest
33d1518fd2 Request internet permission before connect for iOS (#794)
* Attempt to fix API error 1100
* NSURLSession fake call to exec iOS network settings dialog
* use http://captive.apple.com/generate_204 for requesting internet
permission
* moved MobileUtils to IosController
* replaced callbacks with signal-slots in apiController
2024-05-16 14:19:56 +01:00
Shagit Ziganshin
ee5344a4ea Remove misleading iOS and Android support from IPSec protocol transtation strings.
Signed-off-by: Shagit Ziganshin <3687591+theLastOfCats@users.noreply.github.com>
2024-05-14 01:14:05 +03:00
albexk
abb3c918e3 Android notification and routing (#797)
Android notification and routing
2024-05-12 16:04:14 +01:00
Vladyslav Miachkov
ff348a348c Add checking background service before connect (#716)
checking if the service is running for all platforms
2024-05-10 11:06:04 +01:00
pokamest
d67c378bff Merge pull request #800 from amnezia-vpn/bugfix/ssh-check-connection
pass errorCode by reference in configurators
2024-05-10 03:03:59 -07:00
vladimir.kuznetsov
d85a0439c5 pass errorCode by reference in configurators 2024-05-09 20:56:52 +03:00
Mykola Baibuz
5bd8c33a6d Update Mozilla upstream (#790)
* Update Mozilla upstream
2024-05-08 22:02:02 +01:00
101 changed files with 1646 additions and 948 deletions

View File

@@ -127,7 +127,6 @@ set(HEADERS ${HEADERS}
${CMAKE_CURRENT_LIST_DIR}/core/controllers/vpnConfigurationController.h
${CMAKE_CURRENT_LIST_DIR}/protocols/protocols_defs.h
${CMAKE_CURRENT_LIST_DIR}/protocols/qml_register_protocols.h
${CMAKE_CURRENT_LIST_DIR}/ui/notificationhandler.h
${CMAKE_CURRENT_LIST_DIR}/ui/pages.h
${CMAKE_CURRENT_LIST_DIR}/ui/property_helper.h
${CMAKE_CURRENT_LIST_DIR}/ui/qautostart.h
@@ -152,11 +151,16 @@ include_directories(mozilla/models)
if(NOT IOS)
set(HEADERS ${HEADERS}
${CMAKE_CURRENT_LIST_DIR}/platforms/ios/MobileUtils.h
${CMAKE_CURRENT_LIST_DIR}/platforms/ios/QRCodeReaderBase.h
)
endif()
if(NOT ANDROID)
set(HEADERS ${HEADERS}
${CMAKE_CURRENT_LIST_DIR}/ui/notificationhandler.h
)
endif()
set(SOURCES ${SOURCES}
${CMAKE_CURRENT_LIST_DIR}/migrations.cpp
${CMAKE_CURRENT_LIST_DIR}/amnezia_application.cpp
@@ -168,7 +172,6 @@ set(SOURCES ${SOURCES}
${CMAKE_CURRENT_LIST_DIR}/core/controllers/serverController.cpp
${CMAKE_CURRENT_LIST_DIR}/core/controllers/vpnConfigurationController.cpp
${CMAKE_CURRENT_LIST_DIR}/protocols/protocols_defs.cpp
${CMAKE_CURRENT_LIST_DIR}/ui/notificationhandler.cpp
${CMAKE_CURRENT_LIST_DIR}/ui/qautostart.cpp
${CMAKE_CURRENT_LIST_DIR}/protocols/vpnprotocol.cpp
${CMAKE_CURRENT_LIST_DIR}/core/sshclient.cpp
@@ -189,11 +192,16 @@ endif()
if(NOT IOS)
set(SOURCES ${SOURCES}
${CMAKE_CURRENT_LIST_DIR}/platforms/ios/MobileUtils.cpp
${CMAKE_CURRENT_LIST_DIR}/platforms/ios/QRCodeReaderBase.cpp
)
endif()
if(NOT ANDROID)
set(SOURCES ${SOURCES}
${CMAKE_CURRENT_LIST_DIR}/ui/notificationhandler.cpp
)
endif()
file(GLOB COMMON_FILES_H CONFIGURE_DEPENDS ${CMAKE_CURRENT_LIST_DIR}/*.h)
file(GLOB COMMON_FILES_CPP CONFIGURE_DEPENDS ${CMAKE_CURRENT_LIST_DIR}/*.cpp)

View File

@@ -55,6 +55,7 @@ AmneziaApplication::AmneziaApplication(int &argc, char *argv[], bool allowSecond
#endif
m_settings = std::shared_ptr<Settings>(new Settings);
m_nam = new QNetworkAccessManager(this);
}
AmneziaApplication::~AmneziaApplication()
@@ -142,6 +143,7 @@ void AmneziaApplication::init()
connect(m_settings.get(), &Settings::screenshotsEnabledChanged, [](bool enabled) { AmneziaVPN::toggleScreenshots(enabled); });
#endif
#ifndef Q_OS_ANDROID
m_notificationHandler.reset(NotificationHandler::create(nullptr));
connect(m_vpnConnection.get(), &VpnConnection::connectionStateChanged, m_notificationHandler.get(),
@@ -149,10 +151,11 @@ void AmneziaApplication::init()
connect(m_notificationHandler.get(), &NotificationHandler::raiseRequested, m_pageController.get(), &PageController::raiseMainWindow);
connect(m_notificationHandler.get(), &NotificationHandler::connectRequested, m_connectionController.get(),
&ConnectionController::openConnection);
static_cast<void (ConnectionController::*)()>(&ConnectionController::openConnection));
connect(m_notificationHandler.get(), &NotificationHandler::disconnectRequested, m_connectionController.get(),
&ConnectionController::closeConnection);
connect(this, &AmneziaApplication::translationsUpdated, m_notificationHandler.get(), &NotificationHandler::onTranslationsUpdated);
#endif
m_engine->load(url);
m_systemController->setQmlRoot(m_engine->rootObjects().value(0));
@@ -360,16 +363,22 @@ void AmneziaApplication::initControllers()
new ConnectionController(m_serversModel, m_containersModel, m_clientManagementModel, m_vpnConnection, m_settings));
m_engine->rootContext()->setContextProperty("ConnectionController", m_connectionController.get());
connect(m_connectionController.get(), &ConnectionController::connectionErrorOccurred, this, [this](const QString &errorMessage) {
connect(m_connectionController.get(), qOverload<const QString &>(&ConnectionController::connectionErrorOccurred), this, [this](const QString &errorMessage) {
emit m_pageController->showErrorMessage(errorMessage);
emit m_vpnConnection->connectionStateChanged(Vpn::ConnectionState::Disconnected);
});
connect(m_connectionController.get(), qOverload<ErrorCode>(&ConnectionController::connectionErrorOccurred), this, [this](ErrorCode errorCode) {
emit m_pageController->showErrorMessage(errorCode);
emit m_vpnConnection->connectionStateChanged(Vpn::ConnectionState::Disconnected);
});
connect(m_connectionController.get(), &ConnectionController::connectButtonClicked, m_connectionController.get(),
&ConnectionController::toggleConnection, Qt::QueuedConnection);
connect(this, &AmneziaApplication::translationsUpdated, m_connectionController.get(), &ConnectionController::onTranslationsUpdated);
m_pageController.reset(new PageController(m_serversModel, m_settings));
m_pageController.reset(new PageController(m_serversModel, m_settings, m_languageModel));
m_engine->rootContext()->setContextProperty("PageController", m_pageController.get());
m_installController.reset(new InstallController(m_serversModel, m_containersModel, m_protocolsModel, m_clientManagementModel, m_settings));

View File

@@ -2,6 +2,7 @@
#define AMNEZIA_APPLICATION_H
#include <QCommandLineParser>
#include <QNetworkAccessManager>
#include <QQmlApplicationEngine>
#include <QQmlContext>
#include <QThread>
@@ -26,7 +27,9 @@
#include "ui/models/containers_model.h"
#include "ui/models/languageModel.h"
#include "ui/models/protocols/cloakConfigModel.h"
#include "ui/notificationhandler.h"
#ifndef Q_OS_ANDROID
#include "ui/notificationhandler.h"
#endif
#ifdef Q_OS_WINDOWS
#include "ui/models/protocols/ikev2ConfigModel.h"
#endif
@@ -73,6 +76,7 @@ public:
bool parseCommands();
QQmlApplicationEngine *qmlEngine() const;
QNetworkAccessManager *manager() { return m_nam; }
signals:
void translationsUpdated();
@@ -113,7 +117,9 @@ private:
QSharedPointer<VpnConnection> m_vpnConnection;
QThread m_vpnConnectionThread;
#ifndef Q_OS_ANDROID
QScopedPointer<NotificationHandler> m_notificationHandler;
#endif
QScopedPointer<ConnectionController> m_connectionController;
QScopedPointer<PageController> m_pageController;
@@ -124,6 +130,8 @@ private:
QScopedPointer<SitesController> m_sitesController;
QScopedPointer<SystemController> m_systemController;
QScopedPointer<AppSplitTunnelingController> m_appSplitTunnelingController;
QNetworkAccessManager *m_nam;
};
#endif // AMNEZIA_APPLICATION_H

View File

@@ -3,9 +3,6 @@ package org.amnezia.vpn.protocol.cloak
import android.util.Base64
import net.openvpn.ovpn3.ClientAPI_Config
import org.amnezia.vpn.protocol.openvpn.OpenVpn
import org.amnezia.vpn.protocol.openvpn.OpenVpnConfig
import org.amnezia.vpn.util.net.InetNetwork
import org.amnezia.vpn.util.net.parseInetAddress
import org.json.JSONObject
/**
@@ -54,13 +51,6 @@ class Cloak : OpenVpn() {
return openVpnConfig
}
override fun configPluggableTransport(configBuilder: OpenVpnConfig.Builder, config: JSONObject) {
// exclude remote server ip from vpn routes
val remoteServer = config.getString("hostName")
val remoteServerAddress = InetNetwork(parseInetAddress(remoteServer))
configBuilder.excludeRoute(remoteServerAddress)
}
private fun checkCloakJson(cloakConfigJson: JSONObject): JSONObject {
cloakConfigJson.put("NumConn", 1)
cloakConfigJson.put("ProxyMethod", "openvpn")

View File

@@ -13,7 +13,9 @@ import org.amnezia.vpn.protocol.ProtocolState
import org.amnezia.vpn.protocol.ProtocolState.DISCONNECTED
import org.amnezia.vpn.protocol.Statistics
import org.amnezia.vpn.protocol.VpnStartException
import org.amnezia.vpn.util.net.InetNetwork
import org.amnezia.vpn.util.net.getLocalNetworks
import org.amnezia.vpn.util.net.parseInetAddress
import org.json.JSONObject
/**
@@ -77,6 +79,12 @@ open class OpenVpn : Protocol() {
if (evalConfig.error) {
throw BadConfigException("OpenVPN config parse error: ${evalConfig.message}")
}
// exclude remote server ip from vpn routes
val remoteServer = config.getString("hostName")
val remoteServerAddress = InetNetwork(parseInetAddress(remoteServer))
configBuilder.excludeRoute(remoteServerAddress)
configPluggableTransport(configBuilder, config)
configBuilder.configSplitTunneling(config)
configBuilder.configAppSplitTunneling(config)

View File

@@ -105,15 +105,17 @@ abstract class Protocol {
vpnBuilder.addSearchDomain(it)
}
for (addr in config.routes) {
Log.d(TAG, "addRoute: $addr")
vpnBuilder.addRoute(addr)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
for (addr in config.excludedRoutes) {
Log.d(TAG, "excludeRoute: $addr")
vpnBuilder.excludeRoute(addr)
for ((inetNetwork, include) in config.routes) {
if (include) {
Log.d(TAG, "addRoute: $inetNetwork")
vpnBuilder.addRoute(inetNetwork)
} else {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
Log.d(TAG, "excludeRoute: $inetNetwork")
vpnBuilder.excludeRoute(inetNetwork)
} else {
Log.e(TAG, "Trying to exclude route $inetNetwork on old Android")
}
}
}

View File

@@ -12,8 +12,7 @@ open class ProtocolConfig protected constructor(
val addresses: Set<InetNetwork>,
val dnsServers: Set<InetAddress>,
val searchDomain: String?,
val routes: Set<InetNetwork>,
val excludedRoutes: Set<InetNetwork>,
val routes: Set<Route>,
val includedAddresses: Set<InetNetwork>,
val excludedAddresses: Set<InetNetwork>,
val includedApplications: Set<String>,
@@ -29,7 +28,6 @@ open class ProtocolConfig protected constructor(
builder.dnsServers,
builder.searchDomain,
builder.routes,
builder.excludedRoutes,
builder.includedAddresses,
builder.excludedAddresses,
builder.includedApplications,
@@ -43,8 +41,7 @@ open class ProtocolConfig protected constructor(
open class Builder(blockingMode: Boolean) {
internal val addresses: MutableSet<InetNetwork> = hashSetOf()
internal val dnsServers: MutableSet<InetAddress> = hashSetOf()
internal val routes: MutableSet<InetNetwork> = hashSetOf()
internal val excludedRoutes: MutableSet<InetNetwork> = hashSetOf()
internal val routes: MutableSet<Route> = mutableSetOf()
internal val includedAddresses: MutableSet<InetNetwork> = hashSetOf()
internal val excludedAddresses: MutableSet<InetNetwork> = hashSetOf()
internal val includedApplications: MutableSet<String> = hashSetOf()
@@ -77,13 +74,21 @@ open class ProtocolConfig protected constructor(
fun setSearchDomain(domain: String) = apply { this.searchDomain = domain }
fun addRoute(route: InetNetwork) = apply { this.routes += route }
fun addRoutes(routes: Collection<InetNetwork>) = apply { this.routes += routes }
fun removeRoute(route: InetNetwork) = apply { this.routes.remove(route) }
fun addRoute(route: InetNetwork) = apply { this.routes += Route(route, true) }
fun addRoutes(routes: Collection<InetNetwork>) = apply { this.routes += routes.map { Route(it, true) } }
fun excludeRoute(route: InetNetwork) = apply { this.routes += Route(route, false) }
fun excludeRoutes(routes: Collection<InetNetwork>) = apply { this.routes += routes.map { Route(it, false) } }
fun removeRoute(route: InetNetwork) = apply { this.routes.removeIf { it.inetNetwork == route } }
fun clearRoutes() = apply { this.routes.clear() }
fun excludeRoute(route: InetNetwork) = apply { this.excludedRoutes += route }
fun excludeRoutes(routes: Collection<InetNetwork>) = apply { this.excludedRoutes += routes }
fun prependRoutes(block: Builder.() -> Unit) = apply {
val savedRoutes = mutableListOf<Route>().apply { addAll(routes) }
routes.clear()
block()
routes.addAll(savedRoutes)
}
fun includeAddress(addr: InetNetwork) = apply { this.includedAddresses += addr }
fun includeAddresses(addresses: Collection<InetNetwork>) = apply { this.includedAddresses += addresses }
@@ -117,37 +122,46 @@ open class ProtocolConfig protected constructor(
// remove default routes, if any
removeRoute(InetNetwork("0.0.0.0", 0))
removeRoute(InetNetwork("::", 0))
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
// for older versions of Android, add the default route to the excluded routes
// to correctly build the excluded subnets list later
excludeRoute(InetNetwork("0.0.0.0", 0))
removeRoute(InetNetwork("2000::", 3))
prependRoutes {
addRoutes(includedAddresses)
}
addRoutes(includedAddresses)
} else if (excludedAddresses.isNotEmpty()) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
// default routes are required for split tunneling in newer versions of Android
prependRoutes {
addRoute(InetNetwork("0.0.0.0", 0))
addRoute(InetNetwork("::", 0))
addRoute(InetNetwork("2000::", 3))
excludeRoutes(excludedAddresses)
}
excludeRoutes(excludedAddresses)
}
}
private fun processExcludedRoutes() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU && excludedRoutes.isNotEmpty()) {
// todo: rewrite, taking into account the current routes
// for older versions of Android, build a list of subnets without excluded routes
// and add them to routes
val ipRangeSet = IpRangeSet()
ipRangeSet.remove(IpRange("127.0.0.0", 8))
excludedRoutes.forEach {
ipRangeSet.remove(IpRange(it))
private fun processRoutes() {
// replace ::/0 as it may cause LAN connection issues
val ipv6DefaultRoute = InetNetwork("::", 0)
if (routes.removeIf { it.include && it.inetNetwork == ipv6DefaultRoute }) {
prependRoutes {
addRoute(InetNetwork("2000::", 3))
}
// remove default routes, if any
removeRoute(InetNetwork("0.0.0.0", 0))
removeRoute(InetNetwork("::", 0))
}
// for older versions of Android, build a list of subnets without excluded routes
// and add them to routes
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU && routes.any { !it.include }) {
val ipRangeSet = IpRangeSet()
routes.forEach {
if (it.include) ipRangeSet.add(IpRange(it.inetNetwork))
else ipRangeSet.remove(IpRange(it.inetNetwork))
}
ipRangeSet.remove(IpRange("127.0.0.0", 8))
ipRangeSet.remove(IpRange("::1", 128))
routes.clear()
ipRangeSet.subnets().forEach(::addRoute)
addRoute(InetNetwork("2000::", 3))
}
// filter ipv4 and ipv6 loopback addresses
val ipv6Loopback = InetNetwork("::1", 128)
routes.removeIf {
it.include &&
if (it.inetNetwork.isIpv4) it.inetNetwork.address.address[0] == 127.toByte()
else it.inetNetwork == ipv6Loopback
}
}
@@ -165,7 +179,7 @@ open class ProtocolConfig protected constructor(
protected fun configBuild() {
processSplitTunneling()
processExcludedRoutes()
processRoutes()
validate()
}
@@ -177,3 +191,5 @@ open class ProtocolConfig protected constructor(
Builder(blockingMode).apply(block).build()
}
}
data class Route(val inetNetwork: InetNetwork, val include: Boolean)

View File

@@ -1,12 +1,26 @@
<?xml version='1.0' encoding='utf-8'?>
<resources>
<string name="connecting">Подключение</string>
<string name="disconnecting">Отключение</string>
<string name="cancel">Отмена</string>
<string name="disconnected">Не подключено</string>
<string name="connected">Подключено</string>
<string name="connecting">Подключение…</string>
<string name="disconnecting">Отключение…</string>
<string name="reconnecting">Переподключение…</string>
<string name="connect">Подключиться</string>
<string name="disconnect">Отключиться</string>
<string name="ok">ОК</string>
<string name="cancel">Отмена</string>
<string name="yes">Да</string>
<string name="no">Нет</string>
<string name="vpnGranted">VPN-подключение разрешено</string>
<string name="vpnDenied">VPN-подключение запрещено</string>
<string name="vpnSetupFailed">Ошибка настройки VPN</string>
<string name="vpnSetupFailedMessage">Чтобы подключиться к AmneziaVPN необходимо:\n\n- Разрешить приложению подключаться к сети VPN\n- Отключить функцию \"Постоянная VPN\" для всех остальных VPN-приложений в системных настройках VPN</string>
<string name="openVpnSettings">Открыть настройки VPN</string>
<string name="notificationChannelDescription">Уведомления сервиса AmneziaVPN</string>
<string name="notificationDialogTitle">Сервис AmneziaVPN</string>
<string name="notificationDialogMessage">Показывать статус VPN в строке состояния?</string>
<string name="notificationSettingsDialogTitle">Настройки уведомлений</string>
<string name="notificationSettingsDialogMessage">Для показа уведомлений необходимо включить уведомления в системных настройках</string>
<string name="openNotificationSettings">Открыть настройки уведомлений</string>
</resources>

View File

@@ -1,12 +1,26 @@
<?xml version='1.0' encoding='utf-8'?>
<resources>
<string name="connecting">Connecting</string>
<string name="disconnecting">Disconnecting</string>
<string name="cancel">Cancel</string>
<string name="disconnected">Not connected</string>
<string name="connected">Connected</string>
<string name="connecting">Connecting…</string>
<string name="disconnecting">Disconnecting…</string>
<string name="reconnecting">Reconnecting…</string>
<string name="connect">Connect</string>
<string name="disconnect">Disconnect</string>
<string name="ok">OK</string>
<string name="cancel">Cancel</string>
<string name="yes">Yes</string>
<string name="no">No</string>
<string name="vpnGranted">VPN permission granted</string>
<string name="vpnDenied">VPN permission denied</string>
<string name="vpnSetupFailed">VPN setup error</string>
<string name="vpnSetupFailedMessage">To connect to AmneziaVPN, please do the following:\n\n- Allow the app to set up a VPN connection\n- Disable Always-on VPN for any other VPN app in the VPN system settings</string>
<string name="openVpnSettings">Open VPN settings</string>
<string name="notificationChannelDescription">AmneziaVPN service notification</string>
<string name="notificationDialogTitle">AmneziaVPN service</string>
<string name="notificationDialogMessage">Show the VPN state in the status bar?</string>
<string name="notificationSettingsDialogTitle">Notification settings</string>
<string name="notificationSettingsDialogMessage">To show notifications, you must enable notifications in the system settings</string>
<string name="openNotificationSettings">Open notification settings</string>
</resources>

View File

@@ -1,6 +1,9 @@
package org.amnezia.vpn
import android.Manifest
import android.app.AlertDialog
import android.app.NotificationManager
import android.content.BroadcastReceiver
import android.content.ComponentName
import android.content.Intent
import android.content.Intent.EXTRA_MIME_TYPES
@@ -8,8 +11,8 @@ import android.content.Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY
import android.content.ServiceConnection
import android.content.pm.PackageManager
import android.graphics.Bitmap
import android.net.Uri
import android.net.VpnService
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.IBinder
@@ -21,6 +24,7 @@ import android.view.WindowManager.LayoutParams
import android.webkit.MimeTypeMap
import android.widget.Toast
import androidx.annotation.MainThread
import androidx.annotation.RequiresApi
import androidx.core.content.ContextCompat
import java.io.IOException
import kotlin.LazyThreadSafetyMode.NONE
@@ -38,6 +42,7 @@ import org.amnezia.vpn.protocol.getStatistics
import org.amnezia.vpn.protocol.getStatus
import org.amnezia.vpn.qt.QtAndroidController
import org.amnezia.vpn.util.Log
import org.amnezia.vpn.util.Prefs
import org.qtproject.qt.android.bindings.QtActivity
private const val TAG = "AmneziaActivity"
@@ -46,6 +51,9 @@ const val ACTIVITY_MESSENGER_NAME = "Activity"
private const val CHECK_VPN_PERMISSION_ACTION_CODE = 1
private const val CREATE_FILE_ACTION_CODE = 2
private const val OPEN_FILE_ACTION_CODE = 3
private const val CHECK_NOTIFICATION_PERMISSION_ACTION_CODE = 4
private const val PREFS_NOTIFICATION_PERMISSION_ASKED = "NOTIFICATION_PERMISSION_ASKED"
class AmneziaActivity : QtActivity() {
@@ -54,8 +62,11 @@ class AmneziaActivity : QtActivity() {
private var isWaitingStatus = true
private var isServiceConnected = false
private var isInBoundState = false
private var notificationStateReceiver: BroadcastReceiver? = null
private lateinit var vpnServiceMessenger: IpcMessenger
private var tmpFileContentToSave: String = ""
private val actionResultHandlers = mutableMapOf<Int, ActivityResultHandler>()
private val permissionRequestHandlers = mutableMapOf<Int, PermissionRequestHandler>()
private val vpnServiceEventHandler: Handler by lazy(NONE) {
object : Handler(Looper.getMainLooper()) {
@@ -135,10 +146,6 @@ class AmneziaActivity : QtActivity() {
}
}
private data class CheckVpnPermissionCallbacks(val onSuccess: () -> Unit, val onFail: () -> Unit)
private var checkVpnPermissionCallbacks: CheckVpnPermissionCallbacks? = null
/**
* Activity overloaded methods
*/
@@ -153,9 +160,30 @@ class AmneziaActivity : QtActivity() {
doBindService()
}
)
registerBroadcastReceivers()
intent?.let(::processIntent)
}
private fun registerBroadcastReceivers() {
notificationStateReceiver = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
registerBroadcastReceiver(
arrayOf(
NotificationManager.ACTION_NOTIFICATION_CHANNEL_BLOCK_STATE_CHANGED,
NotificationManager.ACTION_APP_BLOCK_STATE_CHANGED
)
) {
Log.d(
TAG, "Notification state changed: ${it?.action}, blocked = " +
"${it?.getBooleanExtra(NotificationManager.EXTRA_BLOCKED_STATE, false)}"
)
mainScope.launch {
qtInitialized.await()
QtAndroidController.onNotificationStateChanged()
}
}
} else null
}
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
Log.d(TAG, "onNewIntent: $intent")
@@ -193,50 +221,46 @@ class AmneziaActivity : QtActivity() {
override fun onDestroy() {
Log.d(TAG, "Destroy Amnezia activity")
unregisterBroadcastReceiver(notificationStateReceiver)
notificationStateReceiver = null
mainScope.cancel()
super.onDestroy()
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
when (requestCode) {
CREATE_FILE_ACTION_CODE -> {
when (resultCode) {
RESULT_OK -> {
data?.data?.let { uri ->
alterDocument(uri)
}
}
}
Log.d(TAG, "Process activity result, code: ${actionCodeToString(requestCode)}, " +
"resultCode: $resultCode, data: $data")
actionResultHandlers[requestCode]?.let { handler ->
when (resultCode) {
RESULT_OK -> handler.onSuccess(data)
else -> handler.onFail(data)
}
handler.onAny(data)
actionResultHandlers.remove(requestCode)
} ?: super.onActivityResult(requestCode, resultCode, data)
}
OPEN_FILE_ACTION_CODE -> {
when (resultCode) {
RESULT_OK -> data?.data?.toString() ?: ""
else -> ""
}.let { uri ->
QtAndroidController.onFileOpened(uri)
}
private fun startActivityForResult(intent: Intent, requestCode: Int, handler: ActivityResultHandler) {
actionResultHandlers[requestCode] = handler
startActivityForResult(intent, requestCode)
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
Log.d(TAG, "Process permission result, code: ${actionCodeToString(requestCode)}, " +
"permissions: ${permissions.contentToString()}, results: ${grantResults.contentToString()}")
permissionRequestHandlers[requestCode]?.let { handler ->
if (grantResults.isNotEmpty()) {
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) handler.onSuccess()
else handler.onFail()
}
handler.onAny()
permissionRequestHandlers.remove(requestCode)
} ?: super.onRequestPermissionsResult(requestCode, permissions, grantResults)
}
CHECK_VPN_PERMISSION_ACTION_CODE -> {
when (resultCode) {
RESULT_OK -> {
Log.d(TAG, "Vpn permission granted")
Toast.makeText(this, resources.getText(R.string.vpnGranted), Toast.LENGTH_LONG).show()
checkVpnPermissionCallbacks?.run { onSuccess() }
}
else -> {
Log.w(TAG, "Vpn permission denied, resultCode: $resultCode")
showOnVpnPermissionRejectDialog()
checkVpnPermissionCallbacks?.run { onFail() }
}
}
checkVpnPermissionCallbacks = null
}
else -> super.onActivityResult(requestCode, resultCode, data)
}
private fun requestPermission(permission: String, requestCode: Int, handler: PermissionRequestHandler) {
permissionRequestHandlers[requestCode] = handler
requestPermissions(arrayOf(permission), requestCode)
}
/**
@@ -268,22 +292,26 @@ class AmneziaActivity : QtActivity() {
/**
* Methods of starting and stopping VpnService
*/
private fun checkVpnPermissionAndStart(vpnConfig: String) {
checkVpnPermission(
onSuccess = { startVpn(vpnConfig) },
onFail = QtAndroidController::onVpnPermissionRejected
)
}
@MainThread
private fun checkVpnPermission(onSuccess: () -> Unit, onFail: () -> Unit) {
private fun checkVpnPermission(onPermissionGranted: () -> Unit) {
Log.d(TAG, "Check VPN permission")
VpnService.prepare(applicationContext)?.let {
checkVpnPermissionCallbacks = CheckVpnPermissionCallbacks(onSuccess, onFail)
startActivityForResult(it, CHECK_VPN_PERMISSION_ACTION_CODE)
return
}
onSuccess()
VpnService.prepare(applicationContext)?.let { intent ->
startActivityForResult(intent, CHECK_VPN_PERMISSION_ACTION_CODE, ActivityResultHandler(
onSuccess = {
Log.d(TAG, "Vpn permission granted")
Toast.makeText(this@AmneziaActivity, resources.getText(R.string.vpnGranted), Toast.LENGTH_LONG).show()
onPermissionGranted()
},
onFail = {
Log.w(TAG, "Vpn permission denied")
showOnVpnPermissionRejectDialog()
mainScope.launch {
qtInitialized.await()
QtAndroidController.onVpnPermissionRejected()
}
}
))
} ?: onPermissionGranted()
}
private fun showOnVpnPermissionRejectDialog() {
@@ -297,6 +325,44 @@ class AmneziaActivity : QtActivity() {
.show()
}
private fun checkNotificationPermission(onChecked: () -> Unit) {
Log.d(TAG, "Check notification permission")
if (
!isNotificationPermissionGranted() &&
!Prefs.load<Boolean>(PREFS_NOTIFICATION_PERMISSION_ASKED)
) {
showNotificationPermissionDialog(onChecked)
} else {
onChecked()
}
}
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
private fun showNotificationPermissionDialog(onChecked: () -> Unit) {
AlertDialog.Builder(this)
.setTitle(R.string.notificationDialogTitle)
.setMessage(R.string.notificationDialogMessage)
.setNegativeButton(R.string.no) { _, _ ->
Prefs.save(PREFS_NOTIFICATION_PERMISSION_ASKED, true)
onChecked()
}
.setPositiveButton(R.string.yes) { _, _ ->
val saveAsked: () -> Unit = {
Prefs.save(PREFS_NOTIFICATION_PERMISSION_ASKED, true)
}
requestPermission(
Manifest.permission.POST_NOTIFICATIONS,
CHECK_NOTIFICATION_PERMISSION_ACTION_CODE,
PermissionRequestHandler(
onSuccess = saveAsked,
onFail = saveAsked,
onAny = onChecked
)
)
}
.show()
}
@MainThread
private fun startVpn(vpnConfig: String) {
if (isServiceConnected) {
@@ -322,28 +388,21 @@ class AmneziaActivity : QtActivity() {
Intent(this, AmneziaVpnService::class.java).apply {
putExtra(MSG_VPN_CONFIG, vpnConfig)
}.also {
ContextCompat.startForegroundService(this, it)
try {
ContextCompat.startForegroundService(this, it)
} catch (e: SecurityException) {
Log.e(TAG, "Failed to start AmneziaVpnService: $e")
QtAndroidController.onServiceError()
}
}
}
@MainThread
private fun disconnectFromVpn() {
Log.d(TAG, "Disconnect from VPN")
vpnServiceMessenger.send(Action.DISCONNECT)
}
// saving file
private fun alterDocument(uri: Uri) {
try {
contentResolver.openOutputStream(uri)?.use { os ->
os.bufferedWriter().use { it.write(tmpFileContentToSave) }
}
} catch (e: IOException) {
e.printStackTrace()
}
tmpFileContentToSave = ""
}
/**
* Methods called by Qt
*/
@@ -357,7 +416,11 @@ class AmneziaActivity : QtActivity() {
fun start(vpnConfig: String) {
Log.v(TAG, "Start VPN")
mainScope.launch {
checkVpnPermissionAndStart(vpnConfig)
checkVpnPermission {
checkNotificationPermission {
startVpn(vpnConfig)
}
}
}
}
@@ -389,14 +452,26 @@ class AmneziaActivity : QtActivity() {
fun saveFile(fileName: String, data: String) {
Log.d(TAG, "Save file $fileName")
mainScope.launch {
tmpFileContentToSave = data
Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "text/*"
putExtra(Intent.EXTRA_TITLE, fileName)
}.also {
startActivityForResult(it, CREATE_FILE_ACTION_CODE)
startActivityForResult(it, CREATE_FILE_ACTION_CODE, ActivityResultHandler(
onSuccess = {
it?.data?.let { uri ->
Log.d(TAG, "Save file to $uri")
try {
contentResolver.openOutputStream(uri)?.use { os ->
os.bufferedWriter().use { it.write(data) }
}
} catch (e: IOException) {
Log.e(TAG, "Failed to save file $uri: $e")
// todo: send error to Qt
}
}
}
))
}
}
}
@@ -404,42 +479,47 @@ class AmneziaActivity : QtActivity() {
@Suppress("unused")
fun openFile(filter: String?) {
Log.v(TAG, "Open file with filter: $filter")
mainScope.launch {
val mimeTypes = if (!filter.isNullOrEmpty()) {
val extensionRegex = "\\*\\.([a-z0-9]+)".toRegex(IGNORE_CASE)
val mime = MimeTypeMap.getSingleton()
extensionRegex.findAll(filter).map {
it.groups[1]?.value?.let { mime.getMimeTypeFromExtension(it) } ?: "*/*"
}.toSet()
} else emptySet()
val mimeTypes = if (!filter.isNullOrEmpty()) {
val extensionRegex = "\\*\\.([a-z0-9]+)".toRegex(IGNORE_CASE)
val mime = MimeTypeMap.getSingleton()
extensionRegex.findAll(filter).map {
it.groups[1]?.value?.let { mime.getMimeTypeFromExtension(it) } ?: "*/*"
}.toSet()
} else emptySet()
Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
Log.v(TAG, "File mimyType filter: $mimeTypes")
if ("*/*" in mimeTypes) {
type = "*/*"
} else {
when (mimeTypes.size) {
1 -> type = mimeTypes.first()
Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
Log.v(TAG, "File mimyType filter: $mimeTypes")
if ("*/*" in mimeTypes) {
type = "*/*"
} else {
when (mimeTypes.size) {
1 -> type = mimeTypes.first()
in 2..Int.MAX_VALUE -> {
type = "*/*"
putExtra(EXTRA_MIME_TYPES, mimeTypes.toTypedArray())
}
in 2..Int.MAX_VALUE -> {
type = "*/*"
putExtra(EXTRA_MIME_TYPES, mimeTypes.toTypedArray())
else -> type = "*/*"
}
else -> type = "*/*"
}
}.also {
startActivityForResult(it, OPEN_FILE_ACTION_CODE, ActivityResultHandler(
onSuccess = {
val uri = it?.data?.toString() ?: ""
Log.d(TAG, "Open file: $uri")
mainScope.launch {
qtInitialized.await()
QtAndroidController.onFileOpened(uri)
}
}
))
}
}.also {
startActivityForResult(it, OPEN_FILE_ACTION_CODE)
}
}
@Suppress("unused")
fun setNotificationText(title: String, message: String, timerSec: Int) {
Log.v(TAG, "Set notification text")
}
@Suppress("unused")
fun isCameraPresent(): Boolean = applicationContext.packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA)
@@ -514,4 +594,76 @@ class AmneziaActivity : QtActivity() {
Log.v(TAG, "Get app icon")
return AppListProvider.getAppIcon(packageManager, packageName, width, height)
}
@Suppress("unused")
fun isNotificationPermissionGranted(): Boolean = applicationContext.isNotificationPermissionGranted()
@Suppress("unused")
fun requestNotificationPermission() {
val shouldShowPreRequest = shouldShowRequestPermissionRationale(Manifest.permission.POST_NOTIFICATIONS)
requestPermission(
Manifest.permission.POST_NOTIFICATIONS,
CHECK_NOTIFICATION_PERMISSION_ACTION_CODE,
PermissionRequestHandler(
onSuccess = {
mainScope.launch {
Prefs.save(PREFS_NOTIFICATION_PERMISSION_ASKED, true)
vpnServiceMessenger.send(Action.NOTIFICATION_PERMISSION_GRANTED)
qtInitialized.await()
QtAndroidController.onNotificationStateChanged()
}
},
onFail = {
if (!Prefs.load<Boolean>(PREFS_NOTIFICATION_PERMISSION_ASKED)) {
Prefs.save(PREFS_NOTIFICATION_PERMISSION_ASKED, true)
} else {
val shouldShowPostRequest =
shouldShowRequestPermissionRationale(Manifest.permission.POST_NOTIFICATIONS)
if (!shouldShowPreRequest && !shouldShowPostRequest) {
showNotificationSettingsDialog()
}
}
}
)
)
}
private fun showNotificationSettingsDialog() {
AlertDialog.Builder(this)
.setTitle(R.string.notificationSettingsDialogTitle)
.setMessage(R.string.notificationSettingsDialogMessage)
.setNegativeButton(R.string.cancel) { _, _ -> }
.setPositiveButton(R.string.openNotificationSettings) { _, _ ->
startActivity(Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS).apply {
putExtra(Settings.EXTRA_APP_PACKAGE, packageName)
})
}
.show()
}
/**
* Utils methods
*/
companion object {
private fun actionCodeToString(actionCode: Int): String =
when (actionCode) {
CHECK_VPN_PERMISSION_ACTION_CODE -> "CHECK_VPN_PERMISSION"
CREATE_FILE_ACTION_CODE -> "CREATE_FILE"
OPEN_FILE_ACTION_CODE -> "OPEN_FILE"
CHECK_NOTIFICATION_PERMISSION_ACTION_CODE -> "CHECK_NOTIFICATION_PERMISSION"
else -> actionCode.toString()
}
}
}
private class ActivityResultHandler(
val onSuccess: (data: Intent?) -> Unit = {},
val onFail: (data: Intent?) -> Unit = {},
val onAny: (data: Intent?) -> Unit = {}
)
private class PermissionRequestHandler(
val onSuccess: () -> Unit = {},
val onFail: () -> Unit = {},
val onAny: () -> Unit = {}
)

View File

@@ -3,14 +3,11 @@ package org.amnezia.vpn
import androidx.camera.camera2.Camera2Config
import androidx.camera.core.CameraSelector
import androidx.camera.core.CameraXConfig
import androidx.core.app.NotificationChannelCompat.Builder
import androidx.core.app.NotificationManagerCompat
import org.amnezia.vpn.util.Log
import org.amnezia.vpn.util.Prefs
import org.qtproject.qt.android.bindings.QtApplication
private const val TAG = "AmneziaApplication"
const val NOTIFICATION_CHANNEL_ID: String = "org.amnezia.vpn.notification"
class AmneziaApplication : QtApplication(), CameraXConfig.Provider {
@@ -20,7 +17,7 @@ class AmneziaApplication : QtApplication(), CameraXConfig.Provider {
Log.init(this)
VpnStateStore.init(this)
Log.d(TAG, "Create Amnezia application")
createNotificationChannel()
ServiceNotification.createNotificationChannel(this)
}
override fun getCameraXConfig(): CameraXConfig = CameraXConfig.Builder
@@ -28,14 +25,4 @@ class AmneziaApplication : QtApplication(), CameraXConfig.Provider {
.setMinimumLoggingLevel(android.util.Log.ERROR)
.setAvailableCamerasLimiter(CameraSelector.DEFAULT_BACK_CAMERA)
.build()
private fun createNotificationChannel() {
NotificationManagerCompat.from(this).createNotificationChannel(
Builder(NOTIFICATION_CHANNEL_ID, NotificationManagerCompat.IMPORTANCE_LOW)
.setName("AmneziaVPN")
.setDescription("AmneziaVPN service notification")
.setShowBadge(false)
.build()
)
}
}

View File

@@ -0,0 +1,56 @@
package org.amnezia.vpn
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import androidx.core.content.ContextCompat
import androidx.core.content.ContextCompat.RegisterReceiverFlags
import org.amnezia.vpn.protocol.ProtocolState
import org.amnezia.vpn.protocol.ProtocolState.CONNECTED
import org.amnezia.vpn.protocol.ProtocolState.CONNECTING
import org.amnezia.vpn.protocol.ProtocolState.DISCONNECTED
import org.amnezia.vpn.protocol.ProtocolState.DISCONNECTING
import org.amnezia.vpn.protocol.ProtocolState.RECONNECTING
import org.amnezia.vpn.protocol.ProtocolState.UNKNOWN
fun Context.getString(state: ProtocolState): String =
getString(
when (state) {
DISCONNECTED, UNKNOWN -> R.string.disconnected
CONNECTED -> R.string.connected
CONNECTING -> R.string.connecting
DISCONNECTING -> R.string.disconnecting
RECONNECTING -> R.string.reconnecting
}
)
fun Context.registerBroadcastReceiver(
action: String,
@RegisterReceiverFlags flags: Int = ContextCompat.RECEIVER_EXPORTED,
onReceive: (Intent?) -> Unit
): BroadcastReceiver = registerBroadcastReceiver(arrayOf(action), flags, onReceive)
fun Context.registerBroadcastReceiver(
actions: Array<String>,
@RegisterReceiverFlags flags: Int = ContextCompat.RECEIVER_EXPORTED,
onReceive: (Intent?) -> Unit
): BroadcastReceiver =
object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
onReceive(intent)
}
}.also {
ContextCompat.registerReceiver(
this,
it,
IntentFilter().apply {
actions.forEach(::addAction)
},
flags
)
}
fun Context.unregisterBroadcastReceiver(receiver: BroadcastReceiver?) {
receiver?.let { this.unregisterReceiver(it) }
}

View File

@@ -188,11 +188,16 @@ class AmneziaTileService : TileService() {
true
}
private fun startVpnService() =
ContextCompat.startForegroundService(
applicationContext,
Intent(this, AmneziaVpnService::class.java)
)
private fun startVpnService() {
try {
ContextCompat.startForegroundService(
applicationContext,
Intent(this, AmneziaVpnService::class.java)
)
} catch (e: SecurityException) {
Log.e(TAG, "Failed to start AmneziaVpnService: $e")
}
}
private fun connectToVpn() = vpnServiceMessenger.send(Action.CONNECT)
@@ -230,7 +235,7 @@ class AmneziaTileService : TileService() {
val tile = qsTile ?: return
tile.apply {
label = vpnState.serverName ?: DEFAULT_TILE_LABEL
when (vpnState.protocolState) {
when (val protocolState = vpnState.protocolState) {
CONNECTED -> {
state = Tile.STATE_ACTIVE
subtitleCompat = null
@@ -241,14 +246,9 @@ class AmneziaTileService : TileService() {
subtitleCompat = null
}
CONNECTING, RECONNECTING -> {
CONNECTING, DISCONNECTING, RECONNECTING -> {
state = Tile.STATE_UNAVAILABLE
subtitleCompat = resources.getString(R.string.connecting)
}
DISCONNECTING -> {
state = Tile.STATE_UNAVAILABLE
subtitleCompat = resources.getString(R.string.disconnecting)
subtitleCompat = getString(protocolState)
}
}
updateTile()

View File

@@ -2,8 +2,8 @@ package org.amnezia.vpn
import android.app.ActivityManager
import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND_SERVICE
import android.app.Notification
import android.app.PendingIntent
import android.app.NotificationManager
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MANIFEST
@@ -15,10 +15,12 @@ import android.os.IBinder
import android.os.Looper
import android.os.Message
import android.os.Messenger
import android.os.PowerManager
import android.os.Process
import androidx.annotation.MainThread
import androidx.core.app.NotificationCompat
import androidx.core.app.ServiceCompat
import androidx.core.content.ContextCompat
import androidx.core.content.getSystemService
import java.util.concurrent.ConcurrentHashMap
import kotlin.LazyThreadSafetyMode.NONE
import kotlinx.coroutines.CoroutineExceptionHandler
@@ -54,11 +56,14 @@ import org.amnezia.vpn.protocol.wireguard.Wireguard
import org.amnezia.vpn.util.Log
import org.amnezia.vpn.util.Prefs
import org.amnezia.vpn.util.net.NetworkState
import org.amnezia.vpn.util.net.TrafficStats
import org.json.JSONException
import org.json.JSONObject
private const val TAG = "AmneziaVpnService"
const val ACTION_DISCONNECT = "org.amnezia.vpn.action.disconnect"
const val MSG_VPN_CONFIG = "VPN_CONFIG"
const val MSG_ERROR = "ERROR"
const val MSG_SAVE_LOGS = "SAVE_LOGS"
@@ -69,8 +74,8 @@ private const val PREFS_CONFIG_KEY = "LAST_CONF"
private const val PREFS_SERVER_NAME = "LAST_SERVER_NAME"
private const val PREFS_SERVER_INDEX = "LAST_SERVER_INDEX"
private const val PROCESS_NAME = "org.amnezia.vpn:amneziaVpnService"
private const val NOTIFICATION_ID = 1337
private const val STATISTICS_SENDING_TIMEOUT = 1000L
// private const val STATISTICS_SENDING_TIMEOUT = 1000L
private const val TRAFFIC_STATS_UPDATE_TIMEOUT = 1000L
private const val DISCONNECT_TIMEOUT = 5000L
private const val STOP_SERVICE_TIMEOUT = 5000L
@@ -96,8 +101,14 @@ class AmneziaVpnService : VpnService() {
private var connectionJob: Job? = null
private var disconnectionJob: Job? = null
private var statisticsSendingJob: Job? = null
private var trafficStatsUpdateJob: Job? = null
// private var statisticsSendingJob: Job? = null
private lateinit var networkState: NetworkState
private lateinit var trafficStats: TrafficStats
private var disconnectReceiver: BroadcastReceiver? = null
private var notificationStateReceiver: BroadcastReceiver? = null
private var screenOnReceiver: BroadcastReceiver? = null
private var screenOffReceiver: BroadcastReceiver? = null
private val clientMessengers = ConcurrentHashMap<Messenger, IpcMessenger>()
private val isActivityConnected
@@ -131,13 +142,13 @@ class AmneziaVpnService : VpnService() {
val messenger = IpcMessenger(msg.replyTo, clientName)
clientMessengers[msg.replyTo] = messenger
Log.d(TAG, "Messenger client '$clientName' was registered")
if (clientName == ACTIVITY_MESSENGER_NAME && isConnected) launchSendingStatistics()
// if (clientName == ACTIVITY_MESSENGER_NAME && isConnected) launchSendingStatistics()
}
Action.UNREGISTER_CLIENT -> {
clientMessengers.remove(msg.replyTo)?.let {
Log.d(TAG, "Messenger client '${it.name}' was unregistered")
if (it.name == ACTIVITY_MESSENGER_NAME) stopSendingStatistics()
// if (it.name == ACTIVITY_MESSENGER_NAME) stopSendingStatistics()
}
}
@@ -159,6 +170,10 @@ class AmneziaVpnService : VpnService() {
}
}
Action.NOTIFICATION_PERMISSION_GRANTED -> {
enableNotification()
}
Action.SET_SAVE_LOGS -> {
Log.saveLogs = msg.data.getBoolean(MSG_SAVE_LOGS)
}
@@ -181,25 +196,7 @@ class AmneziaVpnService : VpnService() {
else -> 0
}
private val notification: Notification by lazy(NONE) {
NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID)
.setSmallIcon(R.drawable.ic_amnezia_round)
.setShowWhen(false)
.setContentIntent(
PendingIntent.getActivity(
this,
0,
Intent(this, AmneziaActivity::class.java),
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
)
.setOngoing(true)
.setPriority(NotificationCompat.PRIORITY_LOW)
.setCategory(NotificationCompat.CATEGORY_SERVICE)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE)
.build()
}
private val serviceNotification: ServiceNotification by lazy(NONE) { ServiceNotification(this) }
/**
* Service overloaded methods
@@ -212,6 +209,8 @@ class AmneziaVpnService : VpnService() {
loadServerData()
launchProtocolStateHandler()
networkState = NetworkState(this, ::reconnect)
trafficStats = TrafficStats()
registerBroadcastReceivers()
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
@@ -227,7 +226,10 @@ class AmneziaVpnService : VpnService() {
Log.d(TAG, "Start service")
connect(intent?.getStringExtra(MSG_VPN_CONFIG))
}
ServiceCompat.startForeground(this, NOTIFICATION_ID, notification, foregroundServiceTypeCompat)
ServiceCompat.startForeground(
this, NOTIFICATION_ID, serviceNotification.buildNotification(serverName, protocolState.value),
foregroundServiceTypeCompat
)
return START_REDELIVER_INTENT
}
@@ -267,6 +269,7 @@ class AmneziaVpnService : VpnService() {
override fun onDestroy() {
Log.d(TAG, "Destroy service")
unregisterBroadcastReceivers()
runBlocking {
disconnect()
disconnectionJob?.join()
@@ -287,6 +290,63 @@ class AmneziaVpnService : VpnService() {
stopSelf()
}
private fun registerBroadcastReceivers() {
Log.d(TAG, "Register broadcast receivers")
disconnectReceiver = registerBroadcastReceiver(ACTION_DISCONNECT, ContextCompat.RECEIVER_NOT_EXPORTED) {
Log.d(TAG, "Broadcast request received: $ACTION_DISCONNECT")
disconnect()
}
notificationStateReceiver = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
registerBroadcastReceiver(
arrayOf(
NotificationManager.ACTION_NOTIFICATION_CHANNEL_BLOCK_STATE_CHANGED,
NotificationManager.ACTION_APP_BLOCK_STATE_CHANGED
)
) {
val state = it?.getBooleanExtra(NotificationManager.EXTRA_BLOCKED_STATE, false)
Log.d(TAG, "Notification state changed: ${it?.action}, blocked = $state")
if (state == false) {
enableNotification()
} else {
disableNotification()
}
}
} else null
registerScreenStateBroadcastReceivers()
}
private fun registerScreenStateBroadcastReceivers() {
if (serviceNotification.isNotificationEnabled()) {
Log.d(TAG, "Register screen state broadcast receivers")
screenOnReceiver = registerBroadcastReceiver(Intent.ACTION_SCREEN_ON) {
if (isConnected && serviceNotification.isNotificationEnabled()) startTrafficStatsUpdateJob()
}
screenOffReceiver = registerBroadcastReceiver(Intent.ACTION_SCREEN_OFF) {
stopTrafficStatsUpdateJob()
}
}
}
private fun unregisterScreenStateBroadcastReceivers() {
Log.d(TAG, "Unregister screen state broadcast receivers")
unregisterBroadcastReceiver(screenOnReceiver)
unregisterBroadcastReceiver(screenOffReceiver)
screenOnReceiver = null
screenOffReceiver = null
}
private fun unregisterBroadcastReceivers() {
Log.d(TAG, "Unregister broadcast receivers")
unregisterBroadcastReceiver(disconnectReceiver)
unregisterBroadcastReceiver(notificationStateReceiver)
unregisterScreenStateBroadcastReceivers()
disconnectReceiver = null
notificationStateReceiver = null
}
/**
* Methods responsible for processing VPN connection
*/
@@ -295,29 +355,8 @@ class AmneziaVpnService : VpnService() {
// drop first default UNKNOWN state
protocolState.drop(1).collect { protocolState ->
Log.d(TAG, "Protocol state changed: $protocolState")
when (protocolState) {
CONNECTED -> {
networkState.bindNetworkListener()
if (isActivityConnected) launchSendingStatistics()
}
DISCONNECTED -> {
networkState.unbindNetworkListener()
stopSendingStatistics()
if (!isServiceBound) stopService()
}
DISCONNECTING -> {
networkState.unbindNetworkListener()
stopSendingStatistics()
}
RECONNECTING -> {
stopSendingStatistics()
}
CONNECTING, UNKNOWN -> {}
}
serviceNotification.updateNotification(serverName, protocolState)
clientMessengers.send {
ServiceEvent.STATUS_CHANGED.packToMessage {
@@ -326,13 +365,41 @@ class AmneziaVpnService : VpnService() {
}
VpnStateStore.store { VpnState(protocolState, serverName, serverIndex) }
when (protocolState) {
CONNECTED -> {
networkState.bindNetworkListener()
// if (isActivityConnected) launchSendingStatistics()
launchTrafficStatsUpdate()
}
DISCONNECTED -> {
networkState.unbindNetworkListener()
stopTrafficStatsUpdateJob()
// stopSendingStatistics()
if (!isServiceBound) stopService()
}
DISCONNECTING -> {
networkState.unbindNetworkListener()
stopTrafficStatsUpdateJob()
// stopSendingStatistics()
}
RECONNECTING -> {
stopTrafficStatsUpdateJob()
// stopSendingStatistics()
}
CONNECTING, UNKNOWN -> {}
}
}
}
}
@MainThread
/* @MainThread
private fun launchSendingStatistics() {
/* if (isServiceBound && isConnected) {
if (isServiceBound && isConnected) {
statisticsSendingJob = mainScope.launch {
while (true) {
clientMessenger.send {
@@ -343,12 +410,62 @@ class AmneziaVpnService : VpnService() {
delay(STATISTICS_SENDING_TIMEOUT)
}
}
} */
}
}
@MainThread
private fun stopSendingStatistics() {
statisticsSendingJob?.cancel()
} */
@MainThread
private fun enableNotification() {
registerScreenStateBroadcastReceivers()
serviceNotification.updateNotification(serverName, protocolState.value)
launchTrafficStatsUpdate()
}
@MainThread
private fun disableNotification() {
unregisterScreenStateBroadcastReceivers()
stopTrafficStatsUpdateJob()
}
@MainThread
private fun launchTrafficStatsUpdate() {
stopTrafficStatsUpdateJob()
if (isConnected &&
serviceNotification.isNotificationEnabled() &&
getSystemService<PowerManager>()?.isInteractive != false
) {
Log.d(TAG, "Launch traffic stats update")
trafficStats.reset()
startTrafficStatsUpdateJob()
}
}
@MainThread
private fun startTrafficStatsUpdateJob() {
if (trafficStatsUpdateJob == null && trafficStats.isSupported()) {
Log.d(TAG, "Start traffic stats update")
trafficStatsUpdateJob = mainScope.launch {
while (true) {
trafficStats.getSpeed().let { speed ->
if (isConnected) {
serviceNotification.updateSpeed(speed)
}
}
delay(TRAFFIC_STATS_UPDATE_TIMEOUT)
}
}
}
}
@MainThread
private fun stopTrafficStatsUpdateJob() {
Log.d(TAG, "Stop traffic stats update")
trafficStatsUpdateJob?.cancel()
trafficStatsUpdateJob = null
}
@MainThread
@@ -471,6 +588,7 @@ class AmneziaVpnService : VpnService() {
private fun saveServerData(config: JSONObject?) {
serverName = config?.opt("description") as String?
serverIndex = config?.opt("serverIndex") as Int? ?: -1
Log.d(TAG, "Save server data: ($serverIndex, $serverName)")
Prefs.save(PREFS_SERVER_NAME, serverName)
Prefs.save(PREFS_SERVER_INDEX, serverIndex)
}
@@ -478,6 +596,7 @@ class AmneziaVpnService : VpnService() {
private fun loadServerData() {
serverName = Prefs.load<String>(PREFS_SERVER_NAME).ifBlank { null }
if (serverName != null) serverIndex = Prefs.load(PREFS_SERVER_INDEX)
Log.d(TAG, "Load server data: ($serverIndex, $serverName)")
}
private fun checkPermission(): Boolean =
@@ -494,9 +613,8 @@ class AmneziaVpnService : VpnService() {
companion object {
fun isRunning(context: Context): Boolean =
(context.getSystemService(ACTIVITY_SERVICE) as ActivityManager)
.runningAppProcesses.any {
it.processName == PROCESS_NAME && it.importance <= IMPORTANCE_FOREGROUND_SERVICE
}
context.getSystemService<ActivityManager>()!!.runningAppProcesses.any {
it.processName == PROCESS_NAME && it.importance <= IMPORTANCE_FOREGROUND_SERVICE
}
}
}

View File

@@ -32,6 +32,7 @@ enum class Action : IpcMessage {
CONNECT,
DISCONNECT,
REQUEST_STATUS,
NOTIFICATION_PERMISSION_GRANTED,
SET_SAVE_LOGS
}

View File

@@ -1,189 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.amnezia.vpn;
import android.Manifest;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.Manifest.permission;
import android.net.Uri;
import android.os.Build;
import android.util.Log;
import android.webkit.WebView;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.regex.Pattern;
// Gets used by /platforms/android/androidAppListProvider.cpp
public class PackageManagerHelper {
final static String TAG = "PackageManagerHelper";
final static int MIN_CHROME_VERSION = 65;
final static List<String> CHROME_BROWSERS = Arrays.asList(
new String[] {"com.google.android.webview", "com.android.webview", "com.google.chrome"});
private static String getAllAppNames(Context ctx) {
JSONObject output = new JSONObject();
PackageManager pm = ctx.getPackageManager();
List<String> browsers = getBrowserIDs(pm);
List<PackageInfo> packs = pm.getInstalledPackages(PackageManager.GET_PERMISSIONS);
for (int i = 0; i < packs.size(); i++) {
PackageInfo p = packs.get(i);
// Do not add ourselves and System Apps to the list, unless it might be a browser
if ((!isSystemPackage(p,pm) || browsers.contains(p.packageName))
&& !isSelf(p)) {
String appid = p.packageName;
String appName = p.applicationInfo.loadLabel(pm).toString();
try {
output.put(appid, appName);
} catch (JSONException e) {
e.printStackTrace();
}
}
}
return output.toString();
}
private static Drawable getAppIcon(Context ctx, String id) {
try {
return ctx.getPackageManager().getApplicationIcon(id);
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
return new ColorDrawable(Color.TRANSPARENT);
}
private static boolean isSystemPackage(PackageInfo pkgInfo, PackageManager pm) {
if( (pkgInfo.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) == 0){
// no system app
return false;
}
// For Systems Packages there are Cases where we want to add it anyway:
// Has the use Internet permission (otherwise makes no sense)
// Had at least 1 update (this means it's probably on any AppStore)
// Has a a launch activity (has a ui and is not just a system service)
if(!usesInternet(pkgInfo)){
return true;
}
if(!hadUpdate(pkgInfo)){
return true;
}
if(pm.getLaunchIntentForPackage(pkgInfo.packageName) == null){
// If there is no way to launch this from a homescreen, def a sys package
return true;
}
return false;
}
private static boolean isSelf(PackageInfo pkgInfo) {
return pkgInfo.packageName.equals("org.amnezia.vpn")
|| pkgInfo.packageName.equals("org.amnezia.vpn.debug");
}
private static boolean usesInternet(PackageInfo pkgInfo){
if(pkgInfo.requestedPermissions == null){
return false;
}
for(int i=0; i < pkgInfo.requestedPermissions.length; i++) {
String permission = pkgInfo.requestedPermissions[i];
if(Manifest.permission.INTERNET.equals(permission)){
return true;
}
}
return false;
}
private static boolean hadUpdate(PackageInfo pkgInfo){
return pkgInfo.lastUpdateTime > pkgInfo.firstInstallTime;
}
// Returns List of all Packages that can classify themselves as browsers
private static List<String> getBrowserIDs(PackageManager pm) {
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse("https://www.amnezia.org/"));
intent.addCategory(Intent.CATEGORY_BROWSABLE);
// We've tried using PackageManager.MATCH_DEFAULT_ONLY flag and found that browsers that
// are not set as the default browser won't be matched even if they had CATEGORY_DEFAULT set
// in the intent filter
List<ResolveInfo> resolveInfos = pm.queryIntentActivities(intent, PackageManager.MATCH_ALL);
List<String> browsers = new ArrayList<String>();
for (int i = 0; i < resolveInfos.size(); i++) {
ResolveInfo info = resolveInfos.get(i);
String browserID = info.activityInfo.packageName;
browsers.add(browserID);
}
return browsers;
}
// Gets called in AndroidAuthenticationListener;
public static boolean isWebViewSupported(Context ctx) {
Log.v(TAG, "Checking if installed Webview is compatible with FxA");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
// The default Webview is able do to FXA
return true;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
PackageInfo pi = WebView.getCurrentWebViewPackage();
if (CHROME_BROWSERS.contains(pi.packageName)) {
return isSupportedChromeBrowser(pi);
}
return isNotAncientBrowser(pi);
}
// Before O the webview is hardcoded, but we dont know which package it is.
// Check if com.google.android.webview is installed
PackageManager pm = ctx.getPackageManager();
try {
PackageInfo pi = pm.getPackageInfo("com.google.android.webview", 0);
return isSupportedChromeBrowser(pi);
} catch (PackageManager.NameNotFoundException e) {
}
// Otherwise check com.android.webview
try {
PackageInfo pi = pm.getPackageInfo("com.android.webview", 0);
return isSupportedChromeBrowser(pi);
} catch (PackageManager.NameNotFoundException e) {
}
Log.e(TAG, "Android System WebView is not found");
// Giving up :(
return false;
}
private static boolean isSupportedChromeBrowser(PackageInfo pi) {
Log.d(TAG, "Checking Chrome Based Browser: " + pi.packageName);
Log.d(TAG, "version name: " + pi.versionName);
Log.d(TAG, "version code: " + pi.versionCode);
try {
String versionCode = pi.versionName.split(Pattern.quote(" "))[0];
String majorVersion = versionCode.split(Pattern.quote("."))[0];
int version = Integer.parseInt(majorVersion);
return version >= MIN_CHROME_VERSION;
} catch (Exception e) {
Log.e(TAG, "Failed to check Chrome Version Code " + pi.versionName);
return false;
}
}
private static boolean isNotAncientBrowser(PackageInfo pi) {
// Not a google chrome - So the version name is worthless
// Lets just make sure the WebView
// used is not ancient ==> Was updated in at least the last 365 days
Log.d(TAG, "Checking Chrome Based Browser: " + pi.packageName);
Log.d(TAG, "version name: " + pi.versionName);
Log.d(TAG, "version code: " + pi.versionCode);
double oneYearInMillis = 31536000000L;
return pi.lastUpdateTime > (System.currentTimeMillis() - oneYearInMillis);
}
}

View File

@@ -0,0 +1,180 @@
package org.amnezia.vpn
import android.Manifest.permission
import android.annotation.SuppressLint
import android.app.Notification
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import androidx.core.app.NotificationChannelCompat.Builder
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationCompat.Action
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat
import org.amnezia.vpn.protocol.ProtocolState
import org.amnezia.vpn.protocol.ProtocolState.CONNECTED
import org.amnezia.vpn.protocol.ProtocolState.DISCONNECTED
import org.amnezia.vpn.util.Log
import org.amnezia.vpn.util.net.TrafficStats.TrafficData
private const val TAG = "ServiceNotification"
private const val OLD_NOTIFICATION_CHANNEL_ID: String = "org.amnezia.vpn.notification"
private const val NOTIFICATION_CHANNEL_ID: String = "org.amnezia.vpn.notifications"
const val NOTIFICATION_ID = 1337
private const val GET_ACTIVITY_REQUEST_CODE = 0
private const val CONNECT_REQUEST_CODE = 1
private const val DISCONNECT_REQUEST_CODE = 2
class ServiceNotification(private val context: Context) {
private val upDownSymbols = when (Build.BRAND) {
"Infinix" -> '˅' to '˄'
else -> '↓' to '↑'
}
private val notificationManager = NotificationManagerCompat.from(context)
private val notificationBuilder = NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID)
.setShowWhen(false)
.setOngoing(true)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE)
.setCategory(NotificationCompat.CATEGORY_SERVICE)
.setContentIntent(
PendingIntent.getActivity(
context,
GET_ACTIVITY_REQUEST_CODE,
Intent(context, AmneziaActivity::class.java),
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
)
private val zeroSpeed: String = with(TrafficData.ZERO) {
formatSpeedString(rxString, txString)
}
fun buildNotification(serverName: String?, state: ProtocolState): Notification {
val speedString = if (state == CONNECTED) zeroSpeed else null
Log.d(TAG, "Build notification: $serverName, $state")
return notificationBuilder
.setSmallIcon(R.drawable.ic_amnezia_round)
.setContentTitle(serverName ?: "AmneziaVPN")
.setContentText(context.getString(state))
.setSubText(speedString)
.setWhen(System.currentTimeMillis())
.clearActions()
.apply {
getAction(state)?.let {
addAction(it)
}
}
.build()
}
private fun buildNotification(speed: TrafficData): Notification =
notificationBuilder
.setWhen(System.currentTimeMillis())
.setSubText(getSpeedString(speed))
.build()
fun isNotificationEnabled(): Boolean {
if (!context.isNotificationPermissionGranted()) return false
if (!notificationManager.areNotificationsEnabled()) return false
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
return notificationManager.getNotificationChannel(NOTIFICATION_CHANNEL_ID)
?.let { it.importance != NotificationManager.IMPORTANCE_NONE } ?: true
}
return true
}
@SuppressLint("MissingPermission")
fun updateNotification(serverName: String?, state: ProtocolState) {
if (context.isNotificationPermissionGranted()) {
Log.d(TAG, "Update notification: $serverName, $state")
notificationManager.notify(NOTIFICATION_ID, buildNotification(serverName, state))
}
}
@SuppressLint("MissingPermission")
fun updateSpeed(speed: TrafficData) {
if (context.isNotificationPermissionGranted()) {
notificationManager.notify(NOTIFICATION_ID, buildNotification(speed))
}
}
private fun getSpeedString(traffic: TrafficData) =
if (traffic == TrafficData.ZERO) zeroSpeed
else formatSpeedString(traffic.rxString, traffic.txString)
private fun formatSpeedString(rx: String, tx: String) = with(upDownSymbols) { "$first $rx $second $tx" }
private fun getAction(state: ProtocolState): Action? {
return when (state) {
CONNECTED -> {
Action(
0, context.getString(R.string.disconnect),
PendingIntent.getBroadcast(
context,
DISCONNECT_REQUEST_CODE,
Intent(ACTION_DISCONNECT).apply {
setPackage("org.amnezia.vpn")
},
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
)
}
DISCONNECTED -> {
Action(
0, context.getString(R.string.connect),
createServicePendingIntent(
context,
CONNECT_REQUEST_CODE,
Intent(context, AmneziaVpnService::class.java),
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
)
}
else -> null
}
}
private val createServicePendingIntent: (Context, Int, Intent, Int) -> PendingIntent =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
PendingIntent::getForegroundService
} else {
PendingIntent::getService
}
companion object {
fun createNotificationChannel(context: Context) {
with(NotificationManagerCompat.from(context)) {
deleteNotificationChannel(OLD_NOTIFICATION_CHANNEL_ID)
createNotificationChannel(
Builder(NOTIFICATION_CHANNEL_ID, NotificationManagerCompat.IMPORTANCE_DEFAULT)
.setShowBadge(false)
.setSound(null, null)
.setVibrationEnabled(false)
.setLightsEnabled(false)
.setName("AmneziaVPN")
.setDescription(context.resources.getString(R.string.notificationChannelDescription))
.build()
)
}
}
}
}
fun Context.isNotificationPermissionGranted(): Boolean =
Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU ||
ContextCompat.checkSelfPermission(this, permission.POST_NOTIFICATIONS) ==
PackageManager.PERMISSION_GRANTED

View File

@@ -3,9 +3,7 @@ package org.amnezia.vpn
import android.app.AlertDialog
import android.app.KeyguardManager
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.res.Configuration.UI_MODE_NIGHT_MASK
import android.content.res.Configuration.UI_MODE_NIGHT_YES
import android.net.VpnService
@@ -33,11 +31,9 @@ class VpnRequestActivity : ComponentActivity() {
val requestIntent = VpnService.prepare(applicationContext)
if (requestIntent != null) {
if (getSystemService<KeyguardManager>()!!.isKeyguardLocked) {
userPresentReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) =
requestLauncher.launch(requestIntent)
userPresentReceiver = registerBroadcastReceiver(Intent.ACTION_USER_PRESENT) {
requestLauncher.launch(requestIntent)
}
registerReceiver(userPresentReceiver, IntentFilter(Intent.ACTION_USER_PRESENT))
} else {
requestLauncher.launch(requestIntent)
}
@@ -49,9 +45,8 @@ class VpnRequestActivity : ComponentActivity() {
}
override fun onDestroy() {
userPresentReceiver?.let {
unregisterReceiver(it)
}
unregisterBroadcastReceiver(userPresentReceiver)
userPresentReceiver = null
super.onDestroy()
}

View File

@@ -4,6 +4,8 @@ import android.app.Application
import androidx.datastore.core.MultiProcessDataStoreFactory
import androidx.datastore.core.Serializer
import androidx.datastore.dataStoreFile
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.InputStream
import java.io.ObjectInputStream
import java.io.ObjectOutputStream
@@ -59,7 +61,8 @@ private class VpnStateSerializer : Serializer<VpnState> {
override suspend fun readFrom(input: InputStream): VpnState {
return withContext(Dispatchers.IO) {
ObjectInputStream(input).use {
val bios = ByteArrayInputStream(input.readBytes())
ObjectInputStream(bios).use {
it.readObject() as VpnState
}
}
@@ -67,9 +70,11 @@ private class VpnStateSerializer : Serializer<VpnState> {
override suspend fun writeTo(t: VpnState, output: OutputStream) {
withContext(Dispatchers.IO) {
ObjectOutputStream(output).use {
val baos = ByteArrayOutputStream()
ObjectOutputStream(baos).use {
it.writeObject(t)
}
output.write(baos.toByteArray())
}
}
}

View File

@@ -17,6 +17,7 @@ object QtAndroidController {
external fun onServiceError()
external fun onVpnPermissionRejected()
external fun onNotificationStateChanged()
external fun onVpnStateChanged(stateCode: Int)
external fun onStatisticsUpdate(rxBytes: Long, txBytes: Long)

View File

@@ -17,5 +17,7 @@ android {
}
dependencies {
implementation(libs.androidx.core)
implementation(libs.kotlinx.coroutines)
implementation(libs.androidx.security.crypto)
}

View File

@@ -1,16 +1,21 @@
package org.amnezia.vpn.util.net
import java.net.Inet4Address
import java.net.InetAddress
data class InetEndpoint(val address: InetAddress, val port: Int) {
override fun toString(): String = "${address.hostAddress}:$port"
override fun toString(): String = if (address is Inet4Address) {
"${address.ip}:$port"
} else {
"[${address.ip}]:$port"
}
companion object {
fun parse(data: String): InetEndpoint {
val split = data.split(":")
val address = parseInetAddress(split.first())
val port = split.last().toInt()
val i = data.lastIndexOf(':')
val address = parseInetAddress(data.substring(0, i))
val port = data.substring(i + 1).toInt()
return InetEndpoint(address, port)
}
}

View File

@@ -9,7 +9,11 @@ data class InetNetwork(val address: InetAddress, val mask: Int) {
constructor(address: InetAddress) : this(address, address.maxPrefixLength)
override fun toString(): String = "${address.hostAddress}/$mask"
val isIpv4: Boolean = address is Inet4Address
val isIpv6: Boolean
get() = !isIpv4
override fun toString(): String = "${address.ip}/$mask"
companion object {
fun parse(data: String): InetNetwork {

View File

@@ -3,12 +3,17 @@ package org.amnezia.vpn.util.net
import java.net.InetAddress
@OptIn(ExperimentalUnsignedTypes::class)
class IpAddress private constructor(private val address: UByteArray) : Comparable<IpAddress> {
internal class IpAddress private constructor(private val address: UByteArray) : Comparable<IpAddress> {
val size: Int = address.size
val lastIndex: Int = address.lastIndex
val maxMask: Int = size * 8
@OptIn(ExperimentalStdlibApi::class)
val hexFormat: HexFormat by lazy {
HexFormat { number.removeLeadingZeros = true }
}
constructor(inetAddress: InetAddress) : this(inetAddress.address.asUByteArray())
constructor(ipAddress: String) : this(parseInetAddress(ipAddress))
@@ -43,6 +48,8 @@ class IpAddress private constructor(private val address: UByteArray) : Comparabl
return copy
}
fun isMinIp(): Boolean = address.all { it == 0x00u.toUByte() }
fun isMaxIp(): Boolean = address.all { it == 0xffu.toUByte() }
override fun compareTo(other: IpAddress): Int {
@@ -74,12 +81,14 @@ class IpAddress private constructor(private val address: UByteArray) : Comparabl
private fun toIpv6String(): String {
val sb = StringBuilder()
var i = 0
var block: Int
while (i < size) {
sb.append(address[i++].toHexString())
sb.append(address[i++].toHexString())
block = address[i++].toInt() shl 8
block += address[i++].toInt()
sb.append(block.toHexString(hexFormat))
sb.append(':')
}
sb.deleteAt(sb.lastIndex)
return sb.toString()
return convertIpv6ToCanonicalForm(sb.toString())
}
}

View File

@@ -2,14 +2,24 @@ package org.amnezia.vpn.util.net
import java.net.InetAddress
class IpRange(private val start: IpAddress, private val end: IpAddress) : Comparable<IpRange> {
class IpRange internal constructor(
internal val start: IpAddress,
internal val end: IpAddress
) : Comparable<IpRange> {
init {
if (start > end) throw IllegalArgumentException("Start IP: $start is greater then end IP: $end")
if (start.size != end.size) {
throw IllegalArgumentException(
"Unable to create a range between IPv4 and IPv6 addresses (start IP: [$start], end IP: [$end])"
)
}
if (start > end) throw IllegalArgumentException("Start IP: [$start] is greater then end IP: [$end]")
}
private constructor(addresses: Pair<IpAddress, IpAddress>) : this(addresses.first, addresses.second)
internal constructor(ipAddress: IpAddress) : this(ipAddress, ipAddress)
constructor(inetAddress: InetAddress, mask: Int) : this(from(inetAddress, mask))
constructor(address: String, mask: Int) : this(parseInetAddress(address), mask)
@@ -22,6 +32,13 @@ class IpRange(private val start: IpAddress, private val end: IpAddress) : Compar
private fun isIntersect(other: IpRange): Boolean =
(start <= other.end) && (end >= other.start)
operator fun plus(other: IpRange): IpRange? {
if (start > other.end && !start.isMinIp() && start.dec() == other.end) return IpRange(other.start, end)
if (end < other.start && !end.isMaxIp() && end.inc() == other.start) return IpRange(start, other.end)
if (!isIntersect(other)) return null
return IpRange(minOf(start, other.start), maxOf(end, other.end))
}
operator fun minus(other: IpRange): List<IpRange>? {
if (this in other) return emptyList()
if (!isIntersect(other)) return null
@@ -94,9 +111,7 @@ class IpRange(private val start: IpAddress, private val end: IpAddress) : Compar
return result
}
override fun toString(): String {
return "$start - $end"
}
override fun toString(): String = if (start == end) "<$start>" else "<$start - $end>"
companion object {
private fun from(inetAddress: InetAddress, mask: Int): Pair<IpAddress, IpAddress> {

View File

@@ -1,15 +1,35 @@
package org.amnezia.vpn.util.net
class IpRangeSet(ipRange: IpRange = IpRange("0.0.0.0", 0)) {
class IpRangeSet {
private val ranges = sortedSetOf(ipRange)
private val ranges = sortedSetOf<IpRange>()
fun add(ipRange: IpRange) {
val iterator = ranges.iterator()
var rangeToAdd = ipRange
run {
while (iterator.hasNext()) {
val curRange = iterator.next()
if (rangeToAdd.end < curRange.start &&
!rangeToAdd.end.isMaxIp() &&
rangeToAdd.end.inc() != curRange.start) break
(curRange + rangeToAdd)?.let { resultRange ->
if (resultRange == curRange) return@run
iterator.remove()
rangeToAdd = resultRange
}
}
ranges += rangeToAdd
}
}
fun remove(ipRange: IpRange) {
val iterator = ranges.iterator()
val splitRanges = mutableListOf<IpRange>()
while (iterator.hasNext()) {
val range = iterator.next()
(range - ipRange)?.let { resultRanges ->
val curRange = iterator.next()
if (ipRange.end < curRange.start) break
(curRange - ipRange)?.let { resultRanges ->
iterator.remove()
splitRanges += resultRanges
}
@@ -17,10 +37,7 @@ class IpRangeSet(ipRange: IpRange = IpRange("0.0.0.0", 0)) {
ranges += splitRanges
}
fun subnets(): List<InetNetwork> =
ranges.map(IpRange::subnets).flatten()
fun subnets(): List<InetNetwork> = ranges.map(IpRange::subnets).flatten()
override fun toString(): String {
return ranges.toString()
}
override fun toString(): String = ranges.toString()
}

View File

@@ -10,7 +10,9 @@ import android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED
import android.net.NetworkRequest
import android.os.Build
import android.os.Handler
import androidx.core.content.getSystemService
import kotlin.LazyThreadSafetyMode.NONE
import kotlinx.coroutines.delay
import org.amnezia.vpn.util.Log
private const val TAG = "NetworkState"
@@ -28,7 +30,7 @@ class NetworkState(
}
private val connectivityManager: ConnectivityManager by lazy(NONE) {
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
context.getSystemService<ConnectivityManager>()!!
}
private val networkRequest: NetworkRequest by lazy(NONE) {
@@ -80,13 +82,24 @@ class NetworkState(
}
}
fun bindNetworkListener() {
suspend fun bindNetworkListener() {
if (isListenerBound) return
Log.d(TAG, "Bind network listener")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
connectivityManager.registerBestMatchingNetworkCallback(networkRequest, networkCallback, handler)
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
connectivityManager.requestNetwork(networkRequest, networkCallback, handler)
try {
connectivityManager.requestNetwork(networkRequest, networkCallback, handler)
} catch (e: SecurityException) {
Log.e(TAG, "Failed to bind network listener: $e")
// Android 11 bug: https://issuetracker.google.com/issues/175055271
if (e.message?.startsWith("Package android does not belong to") == true) {
delay(1000)
connectivityManager.requestNetwork(networkRequest, networkCallback, handler)
} else {
throw e
}
}
} else {
connectivityManager.requestNetwork(networkRequest, networkCallback)
}

View File

@@ -5,12 +5,14 @@ import android.net.ConnectivityManager
import android.net.InetAddresses
import android.net.NetworkCapabilities
import android.os.Build
import androidx.core.content.getSystemService
import java.lang.reflect.InvocationTargetException
import java.net.Inet4Address
import java.net.Inet6Address
import java.net.InetAddress
fun getLocalNetworks(context: Context, ipv6: Boolean): List<InetNetwork> {
val connectivityManager = context.getSystemService(ConnectivityManager::class.java)
val connectivityManager = context.getSystemService<ConnectivityManager>()!!
connectivityManager.activeNetwork?.let { network ->
val netCapabilities = connectivityManager.getNetworkCapabilities(network)
val linkProperties = connectivityManager.getLinkProperties(network)
@@ -39,8 +41,28 @@ private val parseNumericAddressCompat: (String) -> InetAddress =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
InetAddresses::parseNumericAddress
} else {
val m = InetAddress::class.java.getMethod("parseNumericAddress", String::class.java)
fun(address: String): InetAddress {
return m.invoke(null, address) as InetAddress
try {
val m = InetAddress::class.java.getMethod("parseNumericAddress", String::class.java)
fun(address: String): InetAddress {
try {
return m.invoke(null, address) as InetAddress
} catch (e: InvocationTargetException) {
throw e.cause ?: e
}
}
} catch (_: NoSuchMethodException) {
fun(address: String): InetAddress {
return InetAddress.getByName(address)
}
}
}
internal fun convertIpv6ToCanonicalForm(ipv6: String): String = ipv6
.replace("((?:(?:^|:)0+\\b){2,}):?(?!\\S*\\b\\1:0+\\b)(\\S*)".toRegex(), "::$2")
internal val InetAddress.ip: String
get() = if (this is Inet4Address) {
hostAddress!!
} else {
convertIpv6ToCanonicalForm(hostAddress!!)
}

View File

@@ -0,0 +1,93 @@
package org.amnezia.vpn.util.net
import android.net.TrafficStats
import android.os.Build
import android.os.Process
import android.os.SystemClock
import kotlin.math.roundToLong
private const val BYTE = 1L
private const val KiB = BYTE shl 10
private const val MiB = KiB shl 10
private const val GiB = MiB shl 10
private const val TiB = GiB shl 10
class TrafficStats {
private var lastTrafficData = TrafficData.ZERO
private var lastTimestamp = 0L
private val getTrafficDataCompat: () -> TrafficData =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
val iface = "tun0"
fun(): TrafficData {
return TrafficData(TrafficStats.getRxBytes(iface), TrafficStats.getTxBytes(iface))
}
} else {
val uid = Process.myUid()
fun(): TrafficData {
return TrafficData(TrafficStats.getUidRxBytes(uid), TrafficStats.getUidTxBytes(uid))
}
}
fun reset() {
lastTrafficData = getTrafficDataCompat()
lastTimestamp = SystemClock.elapsedRealtime()
}
fun isSupported(): Boolean =
lastTrafficData.rx != TrafficStats.UNSUPPORTED.toLong() && lastTrafficData.tx != TrafficStats.UNSUPPORTED.toLong()
fun getSpeed(): TrafficData {
val timestamp = SystemClock.elapsedRealtime()
val elapsedSeconds = (timestamp - lastTimestamp) / 1000.0
val trafficData = getTrafficDataCompat()
val speed = trafficData.diff(lastTrafficData, elapsedSeconds)
lastTrafficData = trafficData
lastTimestamp = timestamp
return speed
}
class TrafficData(val rx: Long, val tx: Long) {
private var _rxString: String? = null
val rxString: String
get() {
if (_rxString == null) _rxString = rx.speedToString()
return _rxString ?: throw AssertionError("Set to null by another thread")
}
private var _txString: String? = null
val txString: String
get() {
if (_txString == null) _txString = tx.speedToString()
return _txString ?: throw AssertionError("Set to null by another thread")
}
fun diff(other: TrafficData, elapsedSeconds: Double): TrafficData {
val rx = ((this.rx - other.rx) / elapsedSeconds).round()
val tx = ((this.tx - other.tx) / elapsedSeconds).round()
return if (rx == 0L && tx == 0L) ZERO else TrafficData(rx, tx)
}
private fun Double.round() = if (isNaN()) 0L else roundToLong()
private fun Long.speedToString() =
when {
this < KiB -> formatSize(this, BYTE, "B/s")
this < MiB -> formatSize(this, KiB, "KiB/s")
this < GiB -> formatSize(this, MiB, "MiB/s")
this < TiB -> formatSize(this, GiB, "GiB/s")
else -> formatSize(this, TiB, "TiB/s")
}
private fun formatSize(bytes: Long, divider: Long, unit: String): String {
val s = (bytes.toDouble() / divider * 100).roundToLong() / 100.0
return "${s.toString().removeSuffix(".0")} $unit"
}
companion object {
val ZERO: TrafficData = TrafficData(0L, 0L)
}
}
}

View File

@@ -37,8 +37,8 @@ open class WireguardConfig protected constructor(
open fun appendPeerLine(sb: StringBuilder) = with(sb) {
appendLine("public_key=$publicKeyHex")
routes.forEach { route ->
appendLine("allowed_ip=$route")
routes.filter { it.include }.forEach { route ->
appendLine("allowed_ip=${route.inetNetwork}")
}
appendLine("endpoint=$endpoint")
if (persistentKeepalive != 0)

View File

@@ -26,7 +26,6 @@ link_directories(${CMAKE_CURRENT_SOURCE_DIR}/platforms/android)
set(HEADERS ${HEADERS}
${CMAKE_CURRENT_SOURCE_DIR}/platforms/android/android_controller.h
${CMAKE_CURRENT_SOURCE_DIR}/platforms/android/android_notificationhandler.h
${CMAKE_CURRENT_SOURCE_DIR}/platforms/android/android_utils.h
${CMAKE_CURRENT_SOURCE_DIR}/platforms/android/authResultReceiver.h
${CMAKE_CURRENT_SOURCE_DIR}/protocols/android_vpnprotocol.h
@@ -35,7 +34,6 @@ set(HEADERS ${HEADERS}
set(SOURCES ${SOURCES}
${CMAKE_CURRENT_SOURCE_DIR}/platforms/android/android_controller.cpp
${CMAKE_CURRENT_SOURCE_DIR}/platforms/android/android_notificationhandler.cpp
${CMAKE_CURRENT_SOURCE_DIR}/platforms/android/android_utils.cpp
${CMAKE_CURRENT_SOURCE_DIR}/platforms/android/authResultReceiver.cpp
${CMAKE_CURRENT_SOURCE_DIR}/protocols/android_vpnprotocol.cpp

View File

@@ -46,7 +46,6 @@ 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}/platforms/ios/MobileUtils.mm
)

View File

@@ -9,7 +9,7 @@ AwgConfigurator::AwgConfigurator(std::shared_ptr<Settings> settings, const QShar
}
QString AwgConfigurator::createConfig(const ServerCredentials &credentials, DockerContainer container, const QJsonObject &containerConfig,
ErrorCode errorCode)
ErrorCode &errorCode)
{
QString config = WireguardConfigurator::createConfig(credentials, container, containerConfig, errorCode);

View File

@@ -12,7 +12,7 @@ public:
AwgConfigurator(std::shared_ptr<Settings> settings, const QSharedPointer<ServerController> &serverController, QObject *parent = nullptr);
QString createConfig(const ServerCredentials &credentials, DockerContainer container,
const QJsonObject &containerConfig, ErrorCode errorCode);
const QJsonObject &containerConfig, ErrorCode &errorCode);
};
#endif // AWGCONFIGURATOR_H

View File

@@ -13,7 +13,7 @@ CloakConfigurator::CloakConfigurator(std::shared_ptr<Settings> settings, const Q
}
QString CloakConfigurator::createConfig(const ServerCredentials &credentials, DockerContainer container, const QJsonObject &containerConfig,
ErrorCode errorCode)
ErrorCode &errorCode)
{
QString cloakPublicKey =
m_serverController->getTextFileFromContainer(container, credentials, amnezia::protocols::cloak::ckPublicKeyPath, errorCode);

View File

@@ -14,7 +14,7 @@ public:
CloakConfigurator(std::shared_ptr<Settings> settings, const QSharedPointer<ServerController> &serverController, QObject *parent = nullptr);
QString createConfig(const ServerCredentials &credentials, DockerContainer container,
const QJsonObject &containerConfig, ErrorCode errorCode);
const QJsonObject &containerConfig, ErrorCode &errorCode);
};
#endif // CLOAK_CONFIGURATOR_H

View File

@@ -15,7 +15,7 @@ public:
explicit ConfiguratorBase(std::shared_ptr<Settings> settings, const QSharedPointer<ServerController> &serverController, QObject *parent = nullptr);
virtual QString createConfig(const ServerCredentials &credentials, DockerContainer container,
const QJsonObject &containerConfig, ErrorCode errorCode) = 0;
const QJsonObject &containerConfig, ErrorCode &errorCode) = 0;
virtual QString processConfigWithLocalSettings(const QPair<QString, QString> &dns, const bool isApiConfig,
QString &protocolConfigString);

View File

@@ -20,7 +20,7 @@ Ikev2Configurator::Ikev2Configurator(std::shared_ptr<Settings> settings, const Q
}
Ikev2Configurator::ConnectionData Ikev2Configurator::prepareIkev2Config(const ServerCredentials &credentials, DockerContainer container,
ErrorCode errorCode)
ErrorCode &errorCode)
{
Ikev2Configurator::ConnectionData connData;
connData.host = credentials.hostName;
@@ -55,7 +55,7 @@ Ikev2Configurator::ConnectionData Ikev2Configurator::prepareIkev2Config(const Se
}
QString Ikev2Configurator::createConfig(const ServerCredentials &credentials, DockerContainer container, const QJsonObject &containerConfig,
ErrorCode errorCode)
ErrorCode &errorCode)
{
Q_UNUSED(containerConfig)

View File

@@ -22,14 +22,14 @@ public:
};
QString createConfig(const ServerCredentials &credentials, DockerContainer container,
const QJsonObject &containerConfig, ErrorCode errorCode);
const QJsonObject &containerConfig, ErrorCode &errorCode);
QString genIkev2Config(const ConnectionData &connData);
QString genMobileConfig(const ConnectionData &connData);
QString genStrongSwanConfig(const ConnectionData &connData);
ConnectionData prepareIkev2Config(const ServerCredentials &credentials,
DockerContainer container, ErrorCode errorCode);
DockerContainer container, ErrorCode &errorCode);
};
#endif // IKEV2_CONFIGURATOR_H

View File

@@ -31,7 +31,7 @@ OpenVpnConfigurator::OpenVpnConfigurator(std::shared_ptr<Settings> settings, con
}
OpenVpnConfigurator::ConnectionData OpenVpnConfigurator::prepareOpenVpnConfig(const ServerCredentials &credentials,
DockerContainer container, ErrorCode errorCode)
DockerContainer container, ErrorCode &errorCode)
{
OpenVpnConfigurator::ConnectionData connData = OpenVpnConfigurator::createCertRequest();
connData.host = credentials.hostName;
@@ -72,7 +72,7 @@ OpenVpnConfigurator::ConnectionData OpenVpnConfigurator::prepareOpenVpnConfig(co
}
QString OpenVpnConfigurator::createConfig(const ServerCredentials &credentials, DockerContainer container,
const QJsonObject &containerConfig, ErrorCode errorCode)
const QJsonObject &containerConfig, ErrorCode &errorCode)
{
QString config = m_serverController->replaceVars(amnezia::scriptData(ProtocolScriptType::openvpn_template, container),
m_serverController->genVarsForScript(credentials, container, containerConfig));

View File

@@ -25,7 +25,7 @@ public:
};
QString createConfig(const ServerCredentials &credentials, DockerContainer container,
const QJsonObject &containerConfig, ErrorCode errorCode);
const QJsonObject &containerConfig, ErrorCode &errorCode);
QString processConfigWithLocalSettings(const QPair<QString, QString> &dns, const bool isApiConfig,
QString &protocolConfigString);
@@ -36,7 +36,7 @@ public:
private:
ConnectionData prepareOpenVpnConfig(const ServerCredentials &credentials, DockerContainer container,
ErrorCode errorCode);
ErrorCode &errorCode);
ErrorCode signCert(DockerContainer container, const ServerCredentials &credentials, QString clientId);
};

View File

@@ -14,7 +14,7 @@ ShadowSocksConfigurator::ShadowSocksConfigurator(std::shared_ptr<Settings> setti
}
QString ShadowSocksConfigurator::createConfig(const ServerCredentials &credentials, DockerContainer container,
const QJsonObject &containerConfig, ErrorCode errorCode)
const QJsonObject &containerConfig, ErrorCode &errorCode)
{
QString ssKey =
m_serverController->getTextFileFromContainer(container, credentials, amnezia::protocols::shadowsocks::ssKeyPath, errorCode);

View File

@@ -13,7 +13,7 @@ public:
ShadowSocksConfigurator(std::shared_ptr<Settings> settings, const QSharedPointer<ServerController> &serverController, QObject *parent = nullptr);
QString createConfig(const ServerCredentials &credentials, DockerContainer container,
const QJsonObject &containerConfig, ErrorCode errorCode);
const QJsonObject &containerConfig, ErrorCode &errorCode);
};
#endif // SHADOWSOCKS_CONFIGURATOR_H

View File

@@ -65,7 +65,7 @@ WireguardConfigurator::ConnectionData WireguardConfigurator::genClientKeys()
WireguardConfigurator::ConnectionData WireguardConfigurator::prepareWireguardConfig(const ServerCredentials &credentials,
DockerContainer container,
const QJsonObject &containerConfig, ErrorCode errorCode)
const QJsonObject &containerConfig, ErrorCode &errorCode)
{
WireguardConfigurator::ConnectionData connData = WireguardConfigurator::genClientKeys();
connData.host = credentials.hostName;
@@ -158,7 +158,7 @@ WireguardConfigurator::ConnectionData WireguardConfigurator::prepareWireguardCon
}
QString WireguardConfigurator::createConfig(const ServerCredentials &credentials, DockerContainer container,
const QJsonObject &containerConfig, ErrorCode errorCode)
const QJsonObject &containerConfig, ErrorCode &errorCode)
{
QString scriptData = amnezia::scriptData(m_configTemplate, container);
QString config =

View File

@@ -27,7 +27,7 @@ public:
};
QString createConfig(const ServerCredentials &credentials, DockerContainer container, const QJsonObject &containerConfig,
ErrorCode errorCode);
ErrorCode &errorCode);
QString processConfigWithLocalSettings(const QPair<QString, QString> &dns, const bool isApiConfig, QString &protocolConfigString);
QString processConfigWithExportSettings(const QPair<QString, QString> &dns, const bool isApiConfig, QString &protocolConfigString);
@@ -36,7 +36,7 @@ public:
private:
ConnectionData prepareWireguardConfig(const ServerCredentials &credentials, DockerContainer container,
const QJsonObject &containerConfig, ErrorCode errorCode);
const QJsonObject &containerConfig, ErrorCode &errorCode);
bool m_isAwg;
QString m_serverConfigPath;

View File

@@ -14,7 +14,7 @@ XrayConfigurator::XrayConfigurator(std::shared_ptr<Settings> settings, const QSh
}
QString XrayConfigurator::createConfig(const ServerCredentials &credentials, DockerContainer container, const QJsonObject &containerConfig,
ErrorCode errorCode)
ErrorCode &errorCode)
{
QString config = m_serverController->replaceVars(amnezia::scriptData(ProtocolScriptType::xray_template, container),
m_serverController->genVarsForScript(credentials, container, containerConfig));

View File

@@ -13,7 +13,7 @@ public:
XrayConfigurator(std::shared_ptr<Settings> settings, const QSharedPointer<ServerController> &serverController, QObject *parent = nullptr);
QString createConfig(const ServerCredentials &credentials, DockerContainer container, const QJsonObject &containerConfig,
ErrorCode errorCode);
ErrorCode &errorCode);
};
#endif // XRAY_CONFIGURATOR_H

View File

@@ -5,7 +5,9 @@
#include <QNetworkReply>
#include <QtConcurrent>
#include "amnezia_application.h"
#include "configurators/wireguard_configurator.h"
#include "version.h"
namespace
{
@@ -19,7 +21,10 @@ namespace
constexpr char certificate[] = "certificate";
constexpr char publicKey[] = "public_key";
constexpr char protocol[] = "protocol";
constexpr char uuid[] = "installation_uuid";
constexpr char osVersion[] = "os_version";
constexpr char appVersion[] = "app_version";
}
}
@@ -27,8 +32,7 @@ ApiController::ApiController(QObject *parent) : QObject(parent)
{
}
void ApiController::processApiConfig(const QString &protocol, const ApiController::ApiPayloadData &apiPayloadData,
QString &config)
void ApiController::processApiConfig(const QString &protocol, const ApiController::ApiPayloadData &apiPayloadData, QString &config)
{
if (protocol == configKey::cloak) {
config.replace("<key>", "<key>\n");
@@ -61,53 +65,52 @@ QJsonObject ApiController::fillApiPayload(const QString &protocol, const ApiCont
} else if (protocol == configKey::awg) {
obj[configKey::publicKey] = apiPayloadData.wireGuardClientPubKey;
}
obj[configKey::osVersion] = QSysInfo::productType();
obj[configKey::appVersion] = QString(APP_VERSION);
return obj;
}
ErrorCode ApiController::updateServerConfigFromApi(const QString &installationUuid, QJsonObject &serverConfig)
void ApiController::updateServerConfigFromApi(const QString &installationUuid, const int serverIndex, QJsonObject serverConfig)
{
QFutureWatcher<ErrorCode> watcher;
#ifdef Q_OS_IOS
IosController::Instance()->requestInetAccess();
QThread::msleep(10);
#endif
QFuture<ErrorCode> future = QtConcurrent::run([this, &serverConfig, &installationUuid]() {
auto containerConfig = serverConfig.value(config_key::containers).toArray();
auto containerConfig = serverConfig.value(config_key::containers).toArray();
if (serverConfig.value(config_key::configVersion).toInt()) {
QNetworkAccessManager manager;
if (serverConfig.value(config_key::configVersion).toInt()) {
QNetworkRequest request;
request.setTransferTimeout(7000);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
request.setRawHeader("Authorization", "Api-Key " + serverConfig.value(configKey::accessToken).toString().toUtf8());
QString endpoint = serverConfig.value(configKey::apiEdnpoint).toString();
request.setUrl(endpoint);
QNetworkRequest request;
request.setTransferTimeout(7000);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
request.setRawHeader("Authorization",
"Api-Key " + serverConfig.value(configKey::accessToken).toString().toUtf8());
QString endpoint = serverConfig.value(configKey::apiEdnpoint).toString();
request.setUrl(endpoint);
QString protocol = serverConfig.value(configKey::protocol).toString();
QString protocol = serverConfig.value(configKey::protocol).toString();
ApiPayloadData apiPayloadData = generateApiPayloadData(protocol);
auto apiPayloadData = generateApiPayloadData(protocol);
QJsonObject apiPayload = fillApiPayload(protocol, apiPayloadData);
apiPayload[configKey::uuid] = installationUuid;
auto apiPayload = fillApiPayload(protocol, apiPayloadData);
apiPayload[configKey::uuid] = installationUuid;
QByteArray requestBody = QJsonDocument(apiPayload).toJson();
QByteArray requestBody = QJsonDocument(apiPayload).toJson();
QScopedPointer<QNetworkReply> reply;
reply.reset(manager.post(request, requestBody));
QEventLoop wait;
QObject::connect(reply.get(), &QNetworkReply::finished, &wait, &QEventLoop::quit);
wait.exec();
QNetworkReply *reply = amnApp->manager()->post(request, requestBody); // ??
QObject::connect(reply, &QNetworkReply::finished, [this, reply, protocol, apiPayloadData, serverIndex, serverConfig]() mutable {
if (reply->error() == QNetworkReply::NoError) {
QString contents = QString::fromUtf8(reply->readAll());
auto data = QJsonDocument::fromJson(contents.toUtf8()).object().value(config_key::config).toString();
QString data = QJsonDocument::fromJson(contents.toUtf8()).object().value(config_key::config).toString();
data.replace("vpn://", "");
QByteArray ba = QByteArray::fromBase64(data.toUtf8(),
QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals);
QByteArray ba = QByteArray::fromBase64(data.toUtf8(), QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals);
if (ba.isEmpty()) {
return ErrorCode::ApiConfigDownloadError;
emit errorOccurred(ErrorCode::ApiConfigEmptyError);
return;
}
QByteArray ba_uncompressed = qUncompress(ba);
@@ -119,30 +122,37 @@ ErrorCode ApiController::updateServerConfigFromApi(const QString &installationUu
processApiConfig(protocol, apiPayloadData, configStr);
QJsonObject apiConfig = QJsonDocument::fromJson(configStr.toUtf8()).object();
serverConfig.insert(config_key::dns1, apiConfig.value(config_key::dns1));
serverConfig.insert(config_key::dns2, apiConfig.value(config_key::dns2));
serverConfig.insert(config_key::containers, apiConfig.value(config_key::containers));
serverConfig.insert(config_key::hostName, apiConfig.value(config_key::hostName));
serverConfig[config_key::dns1] = apiConfig.value(config_key::dns1);
serverConfig[config_key::dns2] = apiConfig.value(config_key::dns2);
serverConfig[config_key::containers] = apiConfig.value(config_key::containers);
serverConfig[config_key::hostName] = apiConfig.value(config_key::hostName);
auto defaultContainer = apiConfig.value(config_key::defaultContainer).toString();
serverConfig.insert(config_key::defaultContainer, defaultContainer);
serverConfig[config_key::defaultContainer] = defaultContainer;
emit configUpdated(true, serverConfig, serverIndex);
} else {
QString err = reply->errorString();
qDebug() << QString::fromUtf8(reply->readAll());
qDebug() << reply->error();
qDebug() << err;
qDebug() << reply->attribute(QNetworkRequest::HttpStatusCodeAttribute);
return ErrorCode::ApiConfigDownloadError;
if (reply->error() == QNetworkReply::NetworkError::OperationCanceledError
|| reply->error() == QNetworkReply::NetworkError::TimeoutError) {
emit errorOccurred(ErrorCode::ApiConfigTimeoutError);
} else {
QString err = reply->errorString();
qDebug() << QString::fromUtf8(reply->readAll());
qDebug() << reply->error();
qDebug() << err;
qDebug() << reply->attribute(QNetworkRequest::HttpStatusCodeAttribute);
emit errorOccurred(ErrorCode::ApiConfigDownloadError);
}
}
}
return ErrorCode::NoError;
});
QEventLoop wait;
connect(&watcher, &QFutureWatcher<ErrorCode>::finished, &wait, &QEventLoop::quit);
watcher.setFuture(future);
wait.exec();
reply->deleteLater();
});
return watcher.result();
QObject::connect(reply, &QNetworkReply::errorOccurred,
[this, reply](QNetworkReply::NetworkError error) { qDebug() << reply->errorString() << error; });
connect(reply, &QNetworkReply::sslErrors, [this, reply](const QList<QSslError> &errors) {
qDebug().noquote() << errors;
emit errorOccurred(ErrorCode::ApiConfigSslError);
});
}
}

View File

@@ -5,6 +5,10 @@
#include "configurators/openvpn_configurator.h"
#ifdef Q_OS_IOS
#include "platforms/ios/ios_controller.h"
#endif
class ApiController : public QObject
{
Q_OBJECT
@@ -13,10 +17,15 @@ public:
explicit ApiController(QObject *parent = nullptr);
public slots:
ErrorCode updateServerConfigFromApi(const QString &installationUuid, QJsonObject &serverConfig);
void updateServerConfigFromApi(const QString &installationUuid, const int serverIndex, QJsonObject serverConfig);
signals:
void errorOccurred(ErrorCode errorCode);
void configUpdated(const bool updateConfig, const QJsonObject &config, const int serverIndex);
private:
struct ApiPayloadData {
struct ApiPayloadData
{
OpenVpnConfigurator::ConnectionData certRequest;
QString wireGuardClientPrivKey;

View File

@@ -173,7 +173,7 @@ ErrorCode ServerController::uploadTextFileToContainer(DockerContainer container,
}
QByteArray ServerController::getTextFileFromContainer(DockerContainer container, const ServerCredentials &credentials, const QString &path,
ErrorCode errorCode)
ErrorCode &errorCode)
{
errorCode = ErrorCode::NoError;
@@ -618,7 +618,7 @@ ServerController::Vars ServerController::genVarsForScript(const ServerCredential
return vars;
}
QString ServerController::checkSshConnection(const ServerCredentials &credentials, ErrorCode errorCode)
QString ServerController::checkSshConnection(const ServerCredentials &credentials, ErrorCode &errorCode)
{
QString stdOut;
auto cbReadStdOut = [&](const QString &data, libssh::Client &) {

View File

@@ -36,7 +36,7 @@ public:
const QString &path,
libssh::ScpOverwriteMode overwriteMode = libssh::ScpOverwriteMode::ScpOverwriteExisting);
QByteArray getTextFileFromContainer(DockerContainer container, const ServerCredentials &credentials, const QString &path,
ErrorCode errorCode);
ErrorCode &errorCode);
QString replaceVars(const QString &script, const Vars &vars);
Vars genVarsForScript(const ServerCredentials &credentials, DockerContainer container = DockerContainer::None,
@@ -50,7 +50,7 @@ public:
const std::function<ErrorCode(const QString &, libssh::Client &)> &cbReadStdOut = nullptr,
const std::function<ErrorCode(const QString &, libssh::Client &)> &cbReadStdErr = nullptr);
QString checkSshConnection(const ServerCredentials &credentials, ErrorCode errorCode);
QString checkSshConnection(const ServerCredentials &credentials, ErrorCode &errorCode);
void cancelInstallation();

View File

@@ -77,7 +77,7 @@ ErrorCode VpnConfigurationsController::createProtocolConfigString(const bool isA
QJsonObject VpnConfigurationsController::createVpnConfiguration(const QPair<QString, QString> &dns, const QJsonObject &serverConfig,
const QJsonObject &containerConfig, const DockerContainer container,
ErrorCode errorCode)
ErrorCode &errorCode)
{
QJsonObject vpnConfiguration {};

View File

@@ -21,7 +21,7 @@ public slots:
const DockerContainer container, const QJsonObject &containerConfig, const Proto protocol,
QString &protocolConfigString);
QJsonObject createVpnConfiguration(const QPair<QString, QString> &dns, const QJsonObject &serverConfig,
const QJsonObject &containerConfig, const DockerContainer container, ErrorCode errorCode);
const QJsonObject &containerConfig, const DockerContainer container, ErrorCode &errorCode);
static void updateContainerConfigAfterInstallation(const DockerContainer container, QJsonObject &containerConfig, const QString &stdOut);
signals:

View File

@@ -36,12 +36,17 @@ namespace amnezia
}
};
enum ErrorCode {
namespace error_code_ns
{
Q_NAMESPACE
// TODO: change to enum class
enum ErrorCode {
// General error codes
NoError = 0,
UnknownError = 100,
InternalError = 101,
NotImplementedError = 102,
AmneziaServiceNotRunning = 103,
// Server errors
ServerCheckFailed = 200,
@@ -74,7 +79,7 @@ namespace amnezia
AmneziaServiceConnectionFailed = 603,
ExecutableMissing = 604,
XrayExecutableMissing = 605,
Tun2SockExecutableMissing = 606,
Tun2SockExecutableMissing = 606,
// VPN errors
OpenVpnAdaptersInUseError = 700,
@@ -98,6 +103,9 @@ namespace amnezia
// Api errors
ApiConfigDownloadError = 1100,
ApiConfigAlreadyAdded = 1101,
ApiConfigEmptyError = 1102,
ApiConfigTimeoutError = 1103,
ApiConfigSslError = 1104,
// QFile errors
OpenError = 1200,
@@ -106,7 +114,11 @@ namespace amnezia
UnspecifiedError = 1203,
FatalError = 1204,
AbortError = 1205
};
};
Q_ENUM_NS(ErrorCode)
}
using ErrorCode = error_code_ns::ErrorCode;
} // namespace amnezia

View File

@@ -8,64 +8,68 @@ QString errorString(ErrorCode code) {
switch (code) {
// General error codes
case(NoError): errorMessage = QObject::tr("No error"); break;
case(UnknownError): errorMessage = QObject::tr("Unknown Error"); break;
case(NotImplementedError): errorMessage = QObject::tr("Function not implemented"); break;
case(ErrorCode::NoError): errorMessage = QObject::tr("No error"); break;
case(ErrorCode::UnknownError): errorMessage = QObject::tr("Unknown Error"); break;
case(ErrorCode::NotImplementedError): errorMessage = QObject::tr("Function not implemented"); break;
case(ErrorCode::AmneziaServiceNotRunning): errorMessage = QObject::tr("Background service is not running"); break;
// Server errors
case(ServerCheckFailed): errorMessage = QObject::tr("Server check failed"); break;
case(ServerPortAlreadyAllocatedError): errorMessage = QObject::tr("Server port already used. Check for another software"); break;
case(ServerContainerMissingError): errorMessage = QObject::tr("Server error: Docker container missing"); break;
case(ServerDockerFailedError): errorMessage = QObject::tr("Server error: Docker failed"); break;
case(ServerCancelInstallation): errorMessage = QObject::tr("Installation canceled by user"); break;
case(ServerUserNotInSudo): errorMessage = QObject::tr("The user does not have permission to use sudo"); break;
case(ServerPacketManagerError): errorMessage = QObject::tr("Server error: Packet manager error"); break;
case(ErrorCode::ServerCheckFailed): errorMessage = QObject::tr("Server check failed"); break;
case(ErrorCode::ServerPortAlreadyAllocatedError): errorMessage = QObject::tr("Server port already used. Check for another software"); break;
case(ErrorCode::ServerContainerMissingError): errorMessage = QObject::tr("Server error: Docker container missing"); break;
case(ErrorCode::ServerDockerFailedError): errorMessage = QObject::tr("Server error: Docker failed"); break;
case(ErrorCode::ServerCancelInstallation): errorMessage = QObject::tr("Installation canceled by user"); break;
case(ErrorCode::ServerUserNotInSudo): errorMessage = QObject::tr("The user does not have permission to use sudo"); break;
case(ErrorCode::ServerPacketManagerError): errorMessage = QObject::tr("Server error: Packet manager error"); break;
// Libssh errors
case(SshRequestDeniedError): errorMessage = QObject::tr("Ssh request was denied"); break;
case(SshInterruptedError): errorMessage = QObject::tr("Ssh request was interrupted"); break;
case(SshInternalError): errorMessage = QObject::tr("Ssh internal error"); break;
case(SshPrivateKeyError): errorMessage = QObject::tr("Invalid private key or invalid passphrase entered"); break;
case(SshPrivateKeyFormatError): errorMessage = QObject::tr("The selected private key format is not supported, use openssh ED25519 key types or PEM key types"); break;
case(SshTimeoutError): errorMessage = QObject::tr("Timeout connecting to server"); break;
case(ErrorCode::SshRequestDeniedError): errorMessage = QObject::tr("Ssh request was denied"); break;
case(ErrorCode::SshInterruptedError): errorMessage = QObject::tr("Ssh request was interrupted"); break;
case(ErrorCode::SshInternalError): errorMessage = QObject::tr("Ssh internal error"); break;
case(ErrorCode::SshPrivateKeyError): errorMessage = QObject::tr("Invalid private key or invalid passphrase entered"); break;
case(ErrorCode::SshPrivateKeyFormatError): errorMessage = QObject::tr("The selected private key format is not supported, use openssh ED25519 key types or PEM key types"); break;
case(ErrorCode::SshTimeoutError): errorMessage = QObject::tr("Timeout connecting to server"); break;
// Ssh scp errors
case(SshScpFailureError): errorMessage = QObject::tr("Scp error: Generic failure"); break;
case(ErrorCode::SshScpFailureError): errorMessage = QObject::tr("Scp error: Generic failure"); break;
// Local errors
case (OpenVpnConfigMissing): errorMessage = QObject::tr("OpenVPN config missing"); break;
case (OpenVpnManagementServerError): errorMessage = QObject::tr("OpenVPN management server error"); break;
case (ErrorCode::OpenVpnConfigMissing): errorMessage = QObject::tr("OpenVPN config missing"); break;
case (ErrorCode::OpenVpnManagementServerError): errorMessage = QObject::tr("OpenVPN management server error"); break;
// Distro errors
case (OpenVpnExecutableMissing): errorMessage = QObject::tr("OpenVPN executable missing"); break;
case (ShadowSocksExecutableMissing): errorMessage = QObject::tr("ShadowSocks (ss-local) executable missing"); break;
case (CloakExecutableMissing): errorMessage = QObject::tr("Cloak (ck-client) executable missing"); break;
case (AmneziaServiceConnectionFailed): errorMessage = QObject::tr("Amnezia helper service error"); break;
case (OpenSslFailed): errorMessage = QObject::tr("OpenSSL failed"); break;
case (ErrorCode::OpenVpnExecutableMissing): errorMessage = QObject::tr("OpenVPN executable missing"); break;
case (ErrorCode::ShadowSocksExecutableMissing): errorMessage = QObject::tr("ShadowSocks (ss-local) executable missing"); break;
case (ErrorCode::CloakExecutableMissing): errorMessage = QObject::tr("Cloak (ck-client) executable missing"); break;
case (ErrorCode::AmneziaServiceConnectionFailed): errorMessage = QObject::tr("Amnezia helper service error"); break;
case (ErrorCode::OpenSslFailed): errorMessage = QObject::tr("OpenSSL failed"); break;
// VPN errors
case (OpenVpnAdaptersInUseError): errorMessage = QObject::tr("Can't connect: another VPN connection is active"); break;
case (OpenVpnTapAdapterError): errorMessage = QObject::tr("Can't setup OpenVPN TAP network adapter"); break;
case (AddressPoolError): errorMessage = QObject::tr("VPN pool error: no available addresses"); break;
case (ErrorCode::OpenVpnAdaptersInUseError): errorMessage = QObject::tr("Can't connect: another VPN connection is active"); break;
case (ErrorCode::OpenVpnTapAdapterError): errorMessage = QObject::tr("Can't setup OpenVPN TAP network adapter"); break;
case (ErrorCode::AddressPoolError): errorMessage = QObject::tr("VPN pool error: no available addresses"); break;
case (ImportInvalidConfigError): errorMessage = QObject::tr("The config does not contain any containers and credentials for connecting to the server"); break;
case (ErrorCode::ImportInvalidConfigError): errorMessage = QObject::tr("The config does not contain any containers and credentials for connecting to the server"); break;
// Android errors
case (AndroidError): errorMessage = QObject::tr("VPN connection error"); break;
case (ErrorCode::AndroidError): errorMessage = QObject::tr("VPN connection error"); break;
// Api errors
case (ApiConfigDownloadError): errorMessage = QObject::tr("Error when retrieving configuration from API"); break;
case (ApiConfigAlreadyAdded): errorMessage = QObject::tr("This config has already been added to the application"); break;
case (ErrorCode::ApiConfigDownloadError): errorMessage = QObject::tr("Error when retrieving configuration from API"); break;
case (ErrorCode::ApiConfigAlreadyAdded): errorMessage = QObject::tr("This config has already been added to the application"); break;
case (ErrorCode::ApiConfigEmptyError): errorMessage = QObject::tr("In the response from the server, an empty config was received"); break;
case (ErrorCode::ApiConfigSslError): errorMessage = QObject::tr("SSL error occurred"); break;
case (ErrorCode::ApiConfigTimeoutError): errorMessage = QObject::tr("Server response timeout on api request"); break;
// QFile errors
case(OpenError): errorMessage = QObject::tr("QFile error: The file could not be opened"); break;
case(ReadError): errorMessage = QObject::tr("QFile error: An error occurred when reading from the file"); break;
case(PermissionsError): errorMessage = QObject::tr("QFile error: The file could not be accessed"); break;
case(UnspecifiedError): errorMessage = QObject::tr("QFile error: An unspecified error occurred"); break;
case(FatalError): errorMessage = QObject::tr("QFile error: A fatal error occurred"); break;
case(AbortError): errorMessage = QObject::tr("QFile error: The operation was aborted"); break;
case(ErrorCode::OpenError): errorMessage = QObject::tr("QFile error: The file could not be opened"); break;
case(ErrorCode::ReadError): errorMessage = QObject::tr("QFile error: An error occurred when reading from the file"); break;
case(ErrorCode::PermissionsError): errorMessage = QObject::tr("QFile error: The file could not be accessed"); break;
case(ErrorCode::UnspecifiedError): errorMessage = QObject::tr("QFile error: An unspecified error occurred"); break;
case(ErrorCode::FatalError): errorMessage = QObject::tr("QFile error: A fatal error occurred"); break;
case(ErrorCode::AbortError): errorMessage = QObject::tr("QFile error: The operation was aborted"); break;
case(InternalError):
case(ErrorCode::InternalError):
default:
errorMessage = QObject::tr("Internal error"); break;
}

View File

@@ -51,6 +51,13 @@
<true/>
<key>NSCameraUsageDescription</key>
<string>Amnezia VPN needs access to the camera for reading QR-codes.</string>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<false/>
<key>NSAllowsLocalNetworking</key>
<true/>
</dict>
<key>CFBundleIcons</key>
<dict/>
<key>CFBundleIcons~ipad</key>

View File

@@ -64,6 +64,7 @@ int main(int argc, char *argv[])
qInfo().noquote() << QString("Started %1 version %2 %3").arg(APPLICATION_NAME, APP_VERSION, GIT_COMMIT_HASH);
qInfo().noquote() << QString("%1 (%2)").arg(QSysInfo::prettyProductName(), QSysInfo::currentCpuArchitecture());
qInfo().noquote() << QString("SSL backend: %1").arg(QSslSocket::sslLibraryVersionString());
return app.exec();
}

View File

@@ -93,6 +93,7 @@ bool AndroidController::initialize()
{"onServiceDisconnected", "()V", reinterpret_cast<void *>(onServiceDisconnected)},
{"onServiceError", "()V", reinterpret_cast<void *>(onServiceError)},
{"onVpnPermissionRejected", "()V", reinterpret_cast<void *>(onVpnPermissionRejected)},
{"onNotificationStateChanged", "()V", reinterpret_cast<void *>(onNotificationStateChanged)},
{"onVpnStateChanged", "(I)V", reinterpret_cast<void *>(onVpnStateChanged)},
{"onStatisticsUpdate", "(JJ)V", reinterpret_cast<void *>(onStatisticsUpdate)},
{"onFileOpened", "(Ljava/lang/String;)V", reinterpret_cast<void *>(onFileOpened)},
@@ -135,7 +136,7 @@ ErrorCode AndroidController::start(const QJsonObject &vpnConfig)
callActivityMethod("start", "(Ljava/lang/String;)V",
QJniObject::fromString(config).object<jstring>());
return NoError;
return ErrorCode::NoError;
}
void AndroidController::stop()
@@ -173,14 +174,6 @@ QString AndroidController::openFile(const QString &filter)
return fileName;
}
void AndroidController::setNotificationText(const QString &title, const QString &message, int timerSec)
{
callActivityMethod("setNotificationText", "(Ljava/lang/String;Ljava/lang/String;I)V",
QJniObject::fromString(title).object<jstring>(),
QJniObject::fromString(message).object<jstring>(),
(jint) timerSec);
}
bool AndroidController::isCameraPresent()
{
return callActivityMethod<jboolean>("isCameraPresent", "()Z");
@@ -257,6 +250,16 @@ QPixmap AndroidController::getAppIcon(const QString &package, QSize *size, const
return QPixmap::fromImage(image);
}
bool AndroidController::isNotificationPermissionGranted()
{
return callActivityMethod<jboolean>("isNotificationPermissionGranted", "()Z");
}
void AndroidController::requestNotificationPermission()
{
callActivityMethod("requestNotificationPermission", "()V");
}
// Moving log processing to the Android side
jclass AndroidController::log;
jmethodID AndroidController::logDebug;
@@ -409,6 +412,15 @@ void AndroidController::onVpnPermissionRejected(JNIEnv *env, jobject thiz)
emit AndroidController::instance()->vpnPermissionRejected();
}
// static
void AndroidController::onNotificationStateChanged(JNIEnv *env, jobject thiz)
{
Q_UNUSED(env);
Q_UNUSED(thiz);
emit AndroidController::instance()->notificationStateChanged();
}
// static
void AndroidController::onVpnStateChanged(JNIEnv *env, jobject thiz, jint stateCode)
{

View File

@@ -32,7 +32,6 @@ public:
ErrorCode start(const QJsonObject &vpnConfig);
void stop();
void resetLastServer(int serverIndex);
void setNotificationText(const QString &title, const QString &message, int timerSec);
void saveFile(const QString &fileName, const QString &data);
QString openFile(const QString &filter);
bool isCameraPresent();
@@ -44,6 +43,8 @@ public:
void minimizeApp();
QJsonArray getAppList();
QPixmap getAppIcon(const QString &package, QSize *size, const QSize &requestedSize);
bool isNotificationPermissionGranted();
void requestNotificationPermission();
static bool initLogging();
static void messageHandler(QtMsgType type, const QMessageLogContext &context, const QString &message);
@@ -54,6 +55,7 @@ signals:
void serviceDisconnected();
void serviceError();
void vpnPermissionRejected();
void notificationStateChanged();
void vpnStateChanged(ConnectionState state);
void statisticsUpdated(quint64 rxBytes, quint64 txBytes);
void fileOpened(QString uri);
@@ -81,6 +83,7 @@ private:
static void onServiceDisconnected(JNIEnv *env, jobject thiz);
static void onServiceError(JNIEnv *env, jobject thiz);
static void onVpnPermissionRejected(JNIEnv *env, jobject thiz);
static void onNotificationStateChanged(JNIEnv *env, jobject thiz);
static void onVpnStateChanged(JNIEnv *env, jobject thiz, jint stateCode);
static void onStatisticsUpdate(JNIEnv *env, jobject thiz, jlong rxBytes, jlong txBytes);
static void onConfigImported(JNIEnv *env, jobject thiz, jstring data);

View File

@@ -1,21 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "android_notificationhandler.h"
#include "platforms/android/android_controller.h"
AndroidNotificationHandler::AndroidNotificationHandler(QObject* parent)
: NotificationHandler(parent) {
}
AndroidNotificationHandler::~AndroidNotificationHandler() {
}
void AndroidNotificationHandler::notify(NotificationHandler::Message type,
const QString& title,
const QString& message, int timerMsec) {
Q_UNUSED(type);
qDebug() << "Send notification - " << message;
AndroidController::instance()->setNotificationText(title, message,
timerMsec / 1000);
}

View File

@@ -1,24 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#ifndef ANDROIDNOTIFICATIONHANDLER_H
#define ANDROIDNOTIFICATIONHANDLER_H
#include "ui/notificationhandler.h"
#include <QObject>
class AndroidNotificationHandler final : public NotificationHandler {
Q_DISABLE_COPY_MOVE(AndroidNotificationHandler)
public:
AndroidNotificationHandler(QObject* parent);
~AndroidNotificationHandler();
protected:
void notify(Message type, const QString& title, const QString& message,
int timerMsec) override;
};
#endif // ANDROIDNOTIFICATIONHANDLER_H

View File

@@ -1,15 +0,0 @@
#include "MobileUtils.h"
MobileUtils::MobileUtils(QObject *parent) : QObject(parent)
{
}
bool MobileUtils::shareText(const QStringList &)
{
return false;
}
QString MobileUtils::openFile()
{
return QString();
}

View File

@@ -1,22 +0,0 @@
#ifndef MOBILEUTILS_H
#define MOBILEUTILS_H
#include <QObject>
#include <QStringList>
class MobileUtils : public QObject
{
Q_OBJECT
public:
explicit MobileUtils(QObject *parent = nullptr);
public slots:
bool shareText(const QStringList &filesToSend);
QString openFile();
signals:
void finished();
};
#endif // MOBILEUTILS_H

View File

@@ -1,109 +0,0 @@
#include "MobileUtils.h"
#include <UIKit/UIKit.h>
#include <Security/Security.h>
#include <QEventLoop>
static UIViewController* getViewController() {
NSArray *windows = [[UIApplication sharedApplication]windows];
for (UIWindow *window in windows) {
if (window.isKeyWindow) {
return window.rootViewController;
}
}
return nil;
}
MobileUtils::MobileUtils(QObject *parent) : QObject(parent) {
}
bool MobileUtils::shareText(const QStringList& filesToSend) {
NSMutableArray *sharingItems = [NSMutableArray new];
for (int i = 0; i < filesToSend.size(); i++) {
NSURL *logFileUrl = [[NSURL alloc] initFileURLWithPath:filesToSend[i].toNSString()];
[sharingItems addObject:logFileUrl];
}
UIViewController *qtController = getViewController();
if (!qtController) return;
UIActivityViewController *activityController = [[UIActivityViewController alloc] initWithActivityItems:sharingItems applicationActivities:nil];
__block bool isAccepted = false;
[activityController setCompletionWithItemsHandler:^(NSString *activityType, BOOL completed, NSArray *returnedItems, NSError *activityError) {
isAccepted = completed;
emit finished();
}];
[qtController presentViewController:activityController animated:YES completion:nil];
UIPopoverPresentationController *popController = activityController.popoverPresentationController;
if (popController) {
popController.sourceView = qtController.view;
popController.sourceRect = CGRectMake(100, 100, 100, 100);
}
QEventLoop wait;
QObject::connect(this, &MobileUtils::finished, &wait, &QEventLoop::quit);
wait.exec();
return isAccepted;
}
typedef void (^DocumentPickerClosedCallback)(NSString *path);
@interface DocumentPickerDelegate : NSObject <UIDocumentPickerDelegate>
@property (nonatomic, copy) DocumentPickerClosedCallback documentPickerClosedCallback;
@end
@implementation DocumentPickerDelegate
- (void)documentPicker:(UIDocumentPickerViewController *)controller didPickDocumentsAtURLs:(NSArray<NSURL *> *)urls {
for (NSURL *url in urls) {
if (self.documentPickerClosedCallback) {
self.documentPickerClosedCallback([url path]);
}
}
}
- (void)documentPickerWasCancelled:(UIDocumentPickerViewController *)controller {
if (self.documentPickerClosedCallback) {
self.documentPickerClosedCallback(nil);
}
}
@end
QString MobileUtils::openFile() {
UIDocumentPickerViewController *documentPicker = [[UIDocumentPickerViewController alloc] initWithDocumentTypes:@[@"public.item"] inMode:UIDocumentPickerModeOpen];
DocumentPickerDelegate *documentPickerDelegate = [[DocumentPickerDelegate alloc] init];
documentPicker.delegate = documentPickerDelegate;
UIViewController *qtController = getViewController();
if (!qtController) return;
[qtController presentViewController:documentPicker animated:YES completion:nil];
__block QString filePath;
documentPickerDelegate.documentPickerClosedCallback = ^(NSString *path) {
if (path) {
filePath = QString::fromUtf8(path.UTF8String);
} else {
filePath = QString();
}
emit finished();
};
QEventLoop wait;
QObject::connect(this, &MobileUtils::finished, &wait, &QEventLoop::quit);
wait.exec();
return filePath;
}

View File

@@ -50,12 +50,19 @@ public:
void getBackendLogs(std::function<void(const QString &)> &&callback);
void checkStatus();
bool shareText(const QStringList &filesToSend);
QString openFile();
void requestInetAccess();
signals:
void connectionStateChanged(Vpn::ConnectionState state);
void bytesChanged(quint64 receivedBytes, quint64 sentBytes);
void importConfigFromOutside(const QString);
void importBackupFromOutside(const QString);
void finished();
protected slots:
private:

View File

@@ -6,6 +6,7 @@
#include <QJsonDocument>
#include <QJsonObject>
#include <QThread>
#include <QEventLoop>
#include "../protocols/vpnprotocol.h"
#import "ios_controller_wrapper.h"
@@ -26,6 +27,15 @@ const char* MessageKey::isOnDemand = "is-on-demand";
const char* MessageKey::SplitTunnelType = "SplitTunnelType";
const char* MessageKey::SplitTunnelSites = "SplitTunnelSites";
static UIViewController* getViewController() {
NSArray *windows = [[UIApplication sharedApplication]windows];
for (UIWindow *window in windows) {
if (window.isKeyWindow) {
return window.rootViewController;
}
}
return nil;
}
Vpn::ConnectionState iosStatusToState(NEVPNStatus status) {
switch (status) {
@@ -703,3 +713,86 @@ void IosController::sendVpnExtensionMessage(NSDictionary* message, std::function
}
}
bool IosController::shareText(const QStringList& filesToSend) {
NSMutableArray *sharingItems = [NSMutableArray new];
for (int i = 0; i < filesToSend.size(); i++) {
NSURL *logFileUrl = [[NSURL alloc] initFileURLWithPath:filesToSend[i].toNSString()];
[sharingItems addObject:logFileUrl];
}
UIViewController *qtController = getViewController();
if (!qtController) return;
UIActivityViewController *activityController = [[UIActivityViewController alloc] initWithActivityItems:sharingItems applicationActivities:nil];
__block bool isAccepted = false;
[activityController setCompletionWithItemsHandler:^(NSString *activityType, BOOL completed, NSArray *returnedItems, NSError *activityError) {
isAccepted = completed;
emit finished();
}];
[qtController presentViewController:activityController animated:YES completion:nil];
UIPopoverPresentationController *popController = activityController.popoverPresentationController;
if (popController) {
popController.sourceView = qtController.view;
popController.sourceRect = CGRectMake(100, 100, 100, 100);
}
QEventLoop wait;
QObject::connect(this, &IosController::finished, &wait, &QEventLoop::quit);
wait.exec();
return isAccepted;
}
QString IosController::openFile() {
UIDocumentPickerViewController *documentPicker = [[UIDocumentPickerViewController alloc] initWithDocumentTypes:@[@"public.item"] inMode:UIDocumentPickerModeOpen];
DocumentPickerDelegate *documentPickerDelegate = [[DocumentPickerDelegate alloc] init];
documentPicker.delegate = documentPickerDelegate;
UIViewController *qtController = getViewController();
if (!qtController) return;
[qtController presentViewController:documentPicker animated:YES completion:nil];
__block QString filePath;
documentPickerDelegate.documentPickerClosedCallback = ^(NSString *path) {
if (path) {
filePath = QString::fromUtf8(path.UTF8String);
} else {
filePath = QString();
}
emit finished();
};
QEventLoop wait;
QObject::connect(this, &IosController::finished, &wait, &QEventLoop::quit);
wait.exec();
return filePath;
}
void IosController::requestInetAccess() {
NSURL *url = [NSURL URLWithString:@"http://captive.apple.com/generate_204"];
if (url) {
qDebug() << "IosController::requestInetAccess URL error";
return;
}
NSURLSession *session = [NSURLSession sharedSession];
NSURLSessionDataTask *task = [session dataTaskWithURL:url completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
if (error) {
qDebug() << "IosController::requestInetAccess error:" << error.localizedDescription;
} else {
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
QString responseBody = QString::fromUtf8((const char*)data.bytes, data.length);
qDebug() << "IosController::requestInetAccess server response:" << httpResponse.statusCode << "\n\n" <<responseBody;
}
}];
[task resume];
}

View File

@@ -1,6 +1,8 @@
#import <NetworkExtension/NetworkExtension.h>
#import <NetworkExtension/NETunnelProviderSession.h>
#import <Foundation/Foundation.h>
#include <UIKit/UIKit.h>
#include <Security/Security.h>
class IosController;
@@ -13,3 +15,11 @@ class IosController;
- (void)vpnConfigurationDidChange:(NSNotification *)notification;
@end
typedef void (^DocumentPickerClosedCallback)(NSString *path);
@interface DocumentPickerDelegate : NSObject <UIDocumentPickerDelegate>
@property (nonatomic, copy) DocumentPickerClosedCallback documentPickerClosedCallback;
@end

View File

@@ -24,5 +24,22 @@
// cppController->vpnStatusDidChange(notification);
}
@end
@implementation DocumentPickerDelegate
- (void)documentPicker:(UIDocumentPickerViewController *)controller didPickDocumentsAtURLs:(NSArray<NSURL *> *)urls {
for (NSURL *url in urls) {
if (self.documentPickerClosedCallback) {
self.documentPickerClosedCallback([url path]);
}
}
}
- (void)documentPickerWasCancelled:(UIDocumentPickerViewController *)controller {
if (self.documentPickerClosedCallback) {
self.documentPickerClosedCallback(nil);
}
}
@end

View File

@@ -1,5 +1,4 @@
#include "secure_qsettings.h"
#include "platforms/ios/MobileUtils.h"
#include "QAead.h"
#include "QBlockCipher.h"

View File

@@ -2944,8 +2944,8 @@ For more detailed information, you can
</message>
<message>
<location filename="../containers/containers_defs.cpp" line="115"/>
<source>IKEv2 - Modern stable protocol, a bit faster than others, restores connection after signal loss. It has native support on the latest versions of Android and iOS.</source>
<translation>بروتوكول IKEv2 - بروتوكول مستقر حديث, اسرع بقليل من الباقي, يسترجع الاتصال بعد خسارة الاشارة. لدية يتمتع بدعم أصلي على أحدث إصدارات Android وiOS.</translation>
<source>IKEv2 - Modern stable protocol, a bit faster than others, restores connection after signal loss.</source>
<translation>بروتوكول IKEv2 - بروتوكول مستقر حديث, اسرع بقليل من الباقي, يسترجع الاتصال بعد خسارة الاشارة.</translation>
</message>
<message>
<location filename="../containers/containers_defs.cpp" line="118"/>

View File

@@ -2850,8 +2850,8 @@ While it offers a blend of security, stability, and speed, it&apos;s essential t
</message>
<message>
<location filename="../containers/containers_defs.cpp" line="115"/>
<source>IKEv2 - Modern stable protocol, a bit faster than others, restores connection after signal loss. It has native support on the latest versions of Android and iOS.</source>
<translation>پروتکل IKEv2 پروتکلی پایدار و مدرن که مقداری سریعتر از سایر پروتکلهاست. بعد از قطع سیگنال دوباره اتصال را بازیابی میکند. به صورت پیشفرض بر روی آخرین نسخه دستگاههای اندروید و iOS پیشتیبانی میشود.</translation>
<source>IKEv2 - Modern stable protocol, a bit faster than others, restores connection after signal loss.</source>
<translation>پروتکل IKEv2 پروتکلی پایدار و مدرن که مقداری سریعتر از سایر پروتکلهاست. بعد از قطع سیگنال دوباره اتصال را بازیابی میکند.</translation>
</message>
<message>
<location filename="../containers/containers_defs.cpp" line="118"/>

View File

@@ -2998,8 +2998,8 @@ For more detailed information, you can
</message>
<message>
<location filename="../containers/containers_defs.cpp" line="124"/>
<source>IKEv2 - Modern stable protocol, a bit faster than others, restores connection after signal loss. It has native support on the latest versions of Android and iOS.</source>
<translation>IKEv2 - ि ि , , ि ि ि Android iOS .</translation>
<source>IKEv2 - Modern stable protocol, a bit faster than others, restores connection after signal loss.</source>
<translation>IKEv2 - ि ि , , ि ि ि </translation>
</message>
<message>
<location filename="../containers/containers_defs.cpp" line="127"/>

View File

@@ -2851,8 +2851,8 @@ IKEv2 သည် လုံခြုံရေး၊ တည်ငြိမ်မှ
</message>
<message>
<location filename="../containers/containers_defs.cpp" line="115"/>
<source>IKEv2 - Modern stable protocol, a bit faster than others, restores connection after signal loss. It has native support on the latest versions of Android and iOS.</source>
<translation>IKEv2 - က က signal ကက ကက . Android iOS က က.</translation>
<source>IKEv2 - Modern stable protocol, a bit faster than others, restores connection after signal loss.</source>
<translation>IKEv2 - က က signal ကက ကက .</translation>
</message>
<message>
<location filename="../containers/containers_defs.cpp" line="118"/>

View File

@@ -1088,7 +1088,17 @@ Already installed containers were found on the server. All installed containers
<translation>Язык</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsApplication.qml" line="147"/>
<location filename="../ui/qml/Pages2/PageSettingsApplication.qml" line="218"/>
<source>Enable notifications</source>
<translation>Включить уведомления</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsApplication.qml" line="219"/>
<source>Enable notifications to show the VPN state in the status bar</source>
<translation>Включить уведомления для отображения статуса VPN в строке состояния</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsApplication.qml" line="238"/>
<source>Logging</source>
<translation>Логирование</translation>
</message>
@@ -2987,8 +2997,8 @@ While it offers a blend of security, stability, and speed, it&apos;s essential t
</message>
<message>
<location filename="../containers/containers_defs.cpp" line="121"/>
<source>IKEv2 - Modern stable protocol, a bit faster than others, restores connection after signal loss. It has native support on the latest versions of Android and iOS.</source>
<translation>IKEv2 Современный стабильный протокол, немного быстрее других восстанавливает соединение после потери сигнала. Имеет нативную поддержку последних версиий Android и iOS.</translation>
<source>IKEv2 - Modern stable protocol, a bit faster than others, restores connection after signal loss.</source>
<translation>IKEv2 Современный стабильный протокол, немного быстрее других восстанавливает соединение после потери сигнала.</translation>
</message>
<message>
<location filename="../containers/containers_defs.cpp" line="124"/>

View File

@@ -3191,8 +3191,8 @@ While it offers a blend of security, stability, and speed, it&apos;s essential t
</message>
<message>
<location filename="../containers/containers_defs.cpp" line="124"/>
<source>IKEv2 - Modern stable protocol, a bit faster than others, restores connection after signal loss. It has native support on the latest versions of Android and iOS.</source>
<translation>IKEv2 сучасний стабільний протокол, трішки швидше за інших відновлює підключення. Підтримується в останніх версіях Android и iOS самими операційними системами.</translation>
<source>IKEv2 - Modern stable protocol, a bit faster than others, restores connection after signal loss.</source>
<translation>IKEv2 сучасний стабільний протокол, трішки швидше за інших відновлює підключення.</translation>
</message>
<message>
<location filename="../containers/containers_defs.cpp" line="127"/>

View File

@@ -2947,8 +2947,8 @@ For more detailed information, you can
</message>
<message>
<location filename="../containers/containers_defs.cpp" line="124"/>
<source>IKEv2 - Modern stable protocol, a bit faster than others, restores connection after signal loss. It has native support on the latest versions of Android and iOS.</source>
<translation>IKEv2 - جدید مستحکم پروٹوکول، دوسروں کے مقابلے میں تھوڑا تیز، سگنل ضائع ہونے کے بعد کنکشن بحال کرتا ہے۔ اسے اینڈرائیڈ اور آئی او ایس کے تازہ ترین ورژنز پر مقامی حمایت حاصل ہے۔</translation>
<source>IKEv2 - Modern stable protocol, a bit faster than others, restores connection after signal loss.</source>
<translation>IKEv2 - جدید مستحکم پروٹوکول، دوسروں کے مقابلے میں تھوڑا تیز، سگنل ضائع ہونے کے بعد کنکشن بحال کرتا ہے۔</translation>
</message>
<message>
<location filename="../containers/containers_defs.cpp" line="127"/>

View File

@@ -3057,8 +3057,8 @@ WireGuard非常容易被阻挡因为其独特的数据包签名。与一些
</message>
<message>
<location filename="../containers/containers_defs.cpp" line="115"/>
<source>IKEv2 - Modern stable protocol, a bit faster than others, restores connection after signal loss. It has native support on the latest versions of Android and iOS.</source>
<translation>IKEv2 - Android iOS最新版原生支持</translation>
<source>IKEv2 - Modern stable protocol, a bit faster than others, restores connection after signal loss.</source>
<translation>IKEv2 - </translation>
</message>
<message>
<location filename="../containers/containers_defs.cpp" line="118"/>

View File

@@ -7,9 +7,8 @@
#endif
#include <QtConcurrent>
#include "core/controllers/apiController.h"
#include "core/controllers/vpnConfigurationController.h"
#include "core/errorstrings.h"
#include "version.h"
ConnectionController::ConnectionController(const QSharedPointer<ServersModel> &serversModel,
const QSharedPointer<ContainersModel> &containersModel,
@@ -17,6 +16,7 @@ ConnectionController::ConnectionController(const QSharedPointer<ServersModel> &s
const QSharedPointer<VpnConnection> &vpnConnection, const std::shared_ptr<Settings> &settings,
QObject *parent)
: QObject(parent),
m_apiController(this),
m_serversModel(serversModel),
m_containersModel(containersModel),
m_clientManagementModel(clientManagementModel),
@@ -27,70 +27,34 @@ ConnectionController::ConnectionController(const QSharedPointer<ServersModel> &s
connect(this, &ConnectionController::connectToVpn, m_vpnConnection.get(), &VpnConnection::connectToVpn, Qt::QueuedConnection);
connect(this, &ConnectionController::disconnectFromVpn, m_vpnConnection.get(), &VpnConnection::disconnectFromVpn, Qt::QueuedConnection);
connect(&m_apiController, &ApiController::configUpdated, this,
static_cast<void (ConnectionController::*)(const bool, const QJsonObject &, const int)>(&ConnectionController::openConnection));
connect(&m_apiController, qOverload<ErrorCode>(&ApiController::errorOccurred), this, qOverload<ErrorCode>(&ConnectionController::connectionErrorOccurred));
m_state = Vpn::ConnectionState::Disconnected;
}
void ConnectionController::openConnection()
{
int serverIndex = m_serversModel->getDefaultServerIndex();
auto serverConfig = m_serversModel->getServerConfig(serverIndex);
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
if (!Utils::processIsRunning(Utils::executable(SERVICE_NAME, false), true))
{
emit connectionErrorOccurred(ErrorCode::AmneziaServiceNotRunning);
return;
}
#endif
ErrorCode errorCode = ErrorCode::NoError;
int serverIndex = m_serversModel->getDefaultServerIndex();
QJsonObject serverConfig = m_serversModel->getServerConfig(serverIndex);
emit m_vpnConnection->connectionStateChanged(Vpn::ConnectionState::Preparing);
if (serverConfig.value(config_key::configVersion).toInt()
&& !m_serversModel->data(serverIndex, ServersModel::Roles::HasInstalledContainers).toBool()) {
ApiController apiController;
errorCode = apiController.updateServerConfigFromApi(m_settings->getInstallationUuid(true), serverConfig);
if (errorCode != ErrorCode::NoError) {
emit connectionErrorOccurred(errorString(errorCode));
return;
}
m_serversModel->editServer(serverConfig, serverIndex);
m_apiController.updateServerConfigFromApi(m_settings->getInstallationUuid(true), serverIndex, serverConfig);
} else {
openConnection(false, serverConfig, serverIndex);
}
if (!m_serversModel->data(serverIndex, ServersModel::Roles::HasInstalledContainers).toBool()) {
emit noInstalledContainers();
emit m_vpnConnection->connectionStateChanged(Vpn::ConnectionState::Disconnected);
return;
}
DockerContainer container = qvariant_cast<DockerContainer>(m_serversModel->data(serverIndex, ServersModel::Roles::DefaultContainerRole));
if (!m_containersModel->isSupportedByCurrentPlatform(container)) {
emit connectionErrorOccurred(tr("The selected protocol is not supported on the current platform"));
return;
}
if (container == DockerContainer::None) {
emit connectionErrorOccurred(tr("VPN Protocols is not installed.\n Please install VPN container at first"));
return;
}
qApp->processEvents();
QSharedPointer<ServerController> serverController(new ServerController(m_settings));
VpnConfigurationsController vpnConfigurationController(m_settings, serverController);
QJsonObject containerConfig = m_containersModel->getContainerConfig(container);
ServerCredentials credentials = m_serversModel->getServerCredentials(serverIndex);
errorCode = updateProtocolConfig(container, credentials, containerConfig, serverController);
if (errorCode != ErrorCode::NoError) {
emit connectionErrorOccurred(errorString(errorCode));
return;
}
auto dns = m_serversModel->getDnsPair(serverIndex);
serverConfig = m_serversModel->getServerConfig(serverIndex);
auto vpnConfiguration = vpnConfigurationController.createVpnConfiguration(dns, serverConfig, containerConfig, container, errorCode);
if (errorCode != ErrorCode::NoError) {
emit connectionErrorOccurred(tr("unable to create configuration"));
return;
}
emit connectToVpn(serverIndex, credentials, container, vpnConfiguration);
}
void ConnectionController::closeConnection()
@@ -98,9 +62,9 @@ void ConnectionController::closeConnection()
emit disconnectFromVpn();
}
QString ConnectionController::getLastConnectionError()
ErrorCode ConnectionController::getLastConnectionError()
{
return errorString(m_vpnConnection->lastError());
return m_vpnConnection->lastError();
}
void ConnectionController::onConnectionStateChanged(Vpn::ConnectionState state)
@@ -221,6 +185,53 @@ bool ConnectionController::isProtocolConfigExists(const QJsonObject &containerCo
return true;
}
void ConnectionController::openConnection(const bool updateConfig, const QJsonObject &config, const int serverIndex)
{
// Update config for this server as it was received from API
if (updateConfig) {
m_serversModel->editServer(config, serverIndex);
}
if (!m_serversModel->data(serverIndex, ServersModel::Roles::HasInstalledContainers).toBool()) {
emit noInstalledContainers();
emit m_vpnConnection->connectionStateChanged(Vpn::ConnectionState::Disconnected);
return;
}
DockerContainer container = qvariant_cast<DockerContainer>(m_serversModel->data(serverIndex, ServersModel::Roles::DefaultContainerRole));
if (!m_containersModel->isSupportedByCurrentPlatform(container)) {
emit connectionErrorOccurred(tr("The selected protocol is not supported on the current platform"));
return;
}
if (container == DockerContainer::None) {
emit connectionErrorOccurred(tr("VPN Protocols is not installed.\n Please install VPN container at first"));
return;
}
QSharedPointer<ServerController> serverController(new ServerController(m_settings));
VpnConfigurationsController vpnConfigurationController(m_settings, serverController);
QJsonObject containerConfig = m_containersModel->getContainerConfig(container);
ServerCredentials credentials = m_serversModel->getServerCredentials(serverIndex);
ErrorCode errorCode = updateProtocolConfig(container, credentials, containerConfig, serverController);
if (errorCode != ErrorCode::NoError) {
emit connectionErrorOccurred(errorCode);
return;
}
auto dns = m_serversModel->getDnsPair(serverIndex);
auto vpnConfiguration = vpnConfigurationController.createVpnConfiguration(dns, config, containerConfig, container, errorCode);
if (errorCode != ErrorCode::NoError) {
emit connectionErrorOccurred(tr("unable to create configuration"));
return;
}
emit connectToVpn(serverIndex, credentials, container, vpnConfiguration);
}
ErrorCode ConnectionController::updateProtocolConfig(const DockerContainer container, const ServerCredentials &credentials,
QJsonObject &containerConfig, QSharedPointer<ServerController> serverController)
{

View File

@@ -1,6 +1,7 @@
#ifndef CONNECTIONCONTROLLER_H
#define CONNECTIONCONTROLLER_H
#include "core/controllers/apiController.h"
#include "protocols/vpnprotocol.h"
#include "ui/models/clientManagementModel.h"
#include "ui/models/containers_model.h"
@@ -33,7 +34,7 @@ public slots:
void openConnection();
void closeConnection();
QString getLastConnectionError();
ErrorCode getLastConnectionError();
void onConnectionStateChanged(Vpn::ConnectionState state);
void onCurrentContainerUpdated();
@@ -49,6 +50,7 @@ signals:
void connectionStateChanged();
void connectionErrorOccurred(const QString &errorMessage);
void connectionErrorOccurred(ErrorCode errorCode);
void reconnectWithUpdatedContainer(const QString &message);
void noInstalledContainers();
@@ -60,6 +62,10 @@ private:
Vpn::ConnectionState getCurrentConnectionState();
bool isProtocolConfigExists(const QJsonObject &containerConfig, const DockerContainer container);
void openConnection(const bool updateConfig, const QJsonObject &config, const int serverIndex);
ApiController m_apiController;
QSharedPointer<ServersModel> m_serversModel;
QSharedPointer<ContainersModel> m_containersModel;
QSharedPointer<ClientManagementModel> m_clientManagementModel;

View File

@@ -9,7 +9,6 @@
#include <QStandardPaths>
#include "core/controllers/vpnConfigurationController.h"
#include "core/errorstrings.h"
#include "systemController.h"
#ifdef Q_OS_ANDROID
#include "platforms/android/android_utils.h"
@@ -101,7 +100,7 @@ void ExportController::generateConnectionConfig(const QString &clientName)
errorCode = m_clientManagementModel->appendClient(container, credentials, containerConfig, clientName, serverController);
if (errorCode != ErrorCode::NoError) {
emit exportErrorOccurred(errorString(errorCode));
emit exportErrorOccurred(errorCode);
return;
}
@@ -171,7 +170,7 @@ void ExportController::generateOpenVpnConfig(const QString &clientName)
}
if (errorCode) {
emit exportErrorOccurred(errorString(errorCode));
emit exportErrorOccurred(errorCode);
return;
}
@@ -189,7 +188,7 @@ void ExportController::generateWireGuardConfig(const QString &clientName)
QJsonObject nativeConfig;
ErrorCode errorCode = generateNativeConfig(DockerContainer::WireGuard, clientName, Proto::WireGuard, nativeConfig);
if (errorCode) {
emit exportErrorOccurred(errorString(errorCode));
emit exportErrorOccurred(errorCode);
return;
}
@@ -209,7 +208,7 @@ void ExportController::generateAwgConfig(const QString &clientName)
QJsonObject nativeConfig;
ErrorCode errorCode = generateNativeConfig(DockerContainer::Awg, clientName, Proto::Awg, nativeConfig);
if (errorCode) {
emit exportErrorOccurred(errorString(errorCode));
emit exportErrorOccurred(errorCode);
return;
}
@@ -237,7 +236,7 @@ void ExportController::generateShadowSocksConfig()
}
if (errorCode) {
emit exportErrorOccurred(errorString(errorCode));
emit exportErrorOccurred(errorCode);
return;
}
@@ -263,7 +262,7 @@ void ExportController::generateCloakConfig()
QJsonObject nativeConfig;
ErrorCode errorCode = generateNativeConfig(DockerContainer::Cloak, "", Proto::Cloak, nativeConfig);
if (errorCode) {
emit exportErrorOccurred(errorString(errorCode));
emit exportErrorOccurred(errorCode);
return;
}
@@ -283,7 +282,7 @@ void ExportController::generateXrayConfig()
QJsonObject nativeConfig;
ErrorCode errorCode = generateNativeConfig(DockerContainer::Xray, "", Proto::Xray, nativeConfig);
if (errorCode) {
emit exportErrorOccurred(errorString(errorCode));
emit exportErrorOccurred(errorCode);
return;
}
@@ -320,7 +319,7 @@ void ExportController::updateClientManagementModel(const DockerContainer contain
QSharedPointer<ServerController> serverController(new ServerController(m_settings));
ErrorCode errorCode = m_clientManagementModel->updateModel(container, credentials, serverController);
if (errorCode != ErrorCode::NoError) {
emit exportErrorOccurred(errorString(errorCode));
emit exportErrorOccurred(errorCode);
}
}
@@ -330,7 +329,7 @@ void ExportController::revokeConfig(const int row, const DockerContainer contain
ErrorCode errorCode =
m_clientManagementModel->revokeClient(row, container, credentials, m_serversModel->getProcessedServerIndex(), serverController);
if (errorCode != ErrorCode::NoError) {
emit exportErrorOccurred(errorString(errorCode));
emit exportErrorOccurred(errorCode);
}
}
@@ -339,7 +338,7 @@ void ExportController::renameClient(const int row, const QString &clientName, co
QSharedPointer<ServerController> serverController(new ServerController(m_settings));
ErrorCode errorCode = m_clientManagementModel->renameClient(row, clientName, container, credentials, serverController);
if (errorCode != ErrorCode::NoError) {
emit exportErrorOccurred(errorString(errorCode));
emit exportErrorOccurred(errorCode);
}
}

View File

@@ -49,6 +49,7 @@ public slots:
signals:
void generateConfig(int type);
void exportErrorOccurred(const QString &errorMessage);
void exportErrorOccurred(ErrorCode errorCode);
void exportConfigChanged();

View File

@@ -6,7 +6,6 @@
#include <QRandomGenerator>
#include <QStandardPaths>
#include "core/errorstrings.h"
#ifdef Q_OS_ANDROID
#include "platforms/android/android_controller.h"
#endif
@@ -221,7 +220,7 @@ void ImportController::importConfig()
} else if (m_config.contains(config_key::configVersion)) {
quint16 crc = qChecksum(QJsonDocument(m_config).toJson());
if (m_serversModel->isServerFromApiAlreadyExists(crc)) {
emit importErrorOccurred(errorString(ErrorCode::ApiConfigAlreadyAdded), true);
emit importErrorOccurred(ErrorCode::ApiConfigAlreadyAdded, true);
} else {
m_config.insert(config_key::crc, crc);
@@ -231,7 +230,7 @@ void ImportController::importConfig()
} else {
qDebug() << "Failed to import profile";
qDebug().noquote() << QJsonDocument(m_config).toJson();
emit importErrorOccurred(errorString(ErrorCode::ImportInvalidConfigError), false);
emit importErrorOccurred(ErrorCode::ImportInvalidConfigError, false);
}
m_config = {};
@@ -308,7 +307,7 @@ QJsonObject ImportController::extractWireGuardConfig(const QString &data)
hostName = hostNameAndPortMatch.captured(1);
} else {
qDebug() << "Key parameter 'Endpoint' is missing";
emit importErrorOccurred(errorString(ErrorCode::ImportInvalidConfigError), false);
emit importErrorOccurred(ErrorCode::ImportInvalidConfigError, false);
return QJsonObject();
}
@@ -334,7 +333,7 @@ QJsonObject ImportController::extractWireGuardConfig(const QString &data)
lastConfig[config_key::server_pub_key] = configMap.value("PublicKey");
} else {
qDebug() << "One of the key parameters is missing (PrivateKey, Address, PublicKey)";
emit importErrorOccurred(errorString(ErrorCode::ImportInvalidConfigError), false);
emit importErrorOccurred(ErrorCode::ImportInvalidConfigError, false);
return QJsonObject();
}

View File

@@ -54,6 +54,7 @@ public slots:
signals:
void importFinished();
void importErrorOccurred(const QString &errorMessage, bool goToPageHome);
void importErrorOccurred(ErrorCode errorCode, bool goToPageHome);
void qrDecodingFinished();

View File

@@ -9,7 +9,6 @@
#include "core/controllers/serverController.h"
#include "core/controllers/vpnConfigurationController.h"
#include "core/errorstrings.h"
#include "core/networkUtilities.h"
#include "logger.h"
#include "ui/models/protocols/awgConfigModel.h"
@@ -149,7 +148,7 @@ void InstallController::install(DockerContainer container, int port, TransportPr
QMap<DockerContainer, QJsonObject> installedContainers;
ErrorCode errorCode = getAlreadyInstalledContainers(serverCredentials, serverController, installedContainers);
if (errorCode) {
emit installationErrorOccurred(errorString(errorCode));
emit installationErrorOccurred(errorCode);
return;
}
@@ -158,7 +157,7 @@ void InstallController::install(DockerContainer container, int port, TransportPr
if (!installedContainers.contains(container)) {
errorCode = serverController->setupContainer(serverCredentials, container, config);
if (errorCode) {
emit installationErrorOccurred(errorString(errorCode));
emit installationErrorOccurred(errorCode);
return;
}
@@ -169,7 +168,7 @@ void InstallController::install(DockerContainer container, int port, TransportPr
}
if (errorCode) {
emit installationErrorOccurred(errorString(errorCode));
emit installationErrorOccurred(errorCode);
return;
}
@@ -204,7 +203,7 @@ void InstallController::installServer(const DockerContainer container, const QMa
auto errorCode = vpnConfigurationController.createProtocolConfigForContainer(m_processedServerCredentials, iterator.key(),
containerConfig);
if (errorCode) {
emit installationErrorOccurred(errorString(errorCode));
emit installationErrorOccurred(errorCode);
return;
}
containerConfigs.append(containerConfig);
@@ -212,7 +211,7 @@ void InstallController::installServer(const DockerContainer container, const QMa
errorCode = m_clientManagementModel->appendClient(iterator.key(), serverCredentials, containerConfig,
QString("Admin [%1]").arg(QSysInfo::prettyProductName()), serverController);
if (errorCode) {
emit installationErrorOccurred(errorString(errorCode));
emit installationErrorOccurred(errorCode);
return;
}
} else {
@@ -244,7 +243,7 @@ void InstallController::installContainer(const DockerContainer container, const
auto errorCode =
vpnConfigurationController.createProtocolConfigForContainer(serverCredentials, iterator.key(), containerConfig);
if (errorCode) {
emit installationErrorOccurred(errorString(errorCode));
emit installationErrorOccurred(errorCode);
return;
}
m_serversModel->addContainerConfig(iterator.key(), containerConfig);
@@ -252,7 +251,7 @@ void InstallController::installContainer(const DockerContainer container, const
errorCode = m_clientManagementModel->appendClient(iterator.key(), serverCredentials, containerConfig,
QString("Admin [%1]").arg(QSysInfo::prettyProductName()), serverController);
if (errorCode) {
emit installationErrorOccurred(errorString(errorCode));
emit installationErrorOccurred(errorCode);
return;
}
} else {
@@ -310,7 +309,7 @@ void InstallController::scanServerForInstalledContainers()
auto errorCode =
vpnConfigurationController.createProtocolConfigForContainer(serverCredentials, container, containerConfig);
if (errorCode) {
emit installationErrorOccurred(errorString(errorCode));
emit installationErrorOccurred(errorCode);
return;
}
m_serversModel->addContainerConfig(container, containerConfig);
@@ -319,7 +318,7 @@ void InstallController::scanServerForInstalledContainers()
QString("Admin [%1]").arg(QSysInfo::prettyProductName()),
serverController);
if (errorCode) {
emit installationErrorOccurred(errorString(errorCode));
emit installationErrorOccurred(errorCode);
return;
}
} else {
@@ -334,7 +333,7 @@ void InstallController::scanServerForInstalledContainers()
return;
}
emit installationErrorOccurred(errorString(errorCode));
emit installationErrorOccurred(errorCode);
}
ErrorCode InstallController::getAlreadyInstalledContainers(const ServerCredentials &credentials,
@@ -515,7 +514,7 @@ void InstallController::updateContainer(QJsonObject config)
return;
}
emit installationErrorOccurred(errorString(errorCode));
emit installationErrorOccurred(errorCode);
}
void InstallController::rebootProcessedServer()
@@ -528,7 +527,7 @@ void InstallController::rebootProcessedServer()
if (errorCode == ErrorCode::NoError) {
emit rebootProcessedServerFinished(tr("Server '%1' was rebooted").arg(serverName));
} else {
emit installationErrorOccurred(errorString(errorCode));
emit installationErrorOccurred(errorCode);
}
}
@@ -552,7 +551,7 @@ void InstallController::removeAllContainers()
emit removeAllContainersFinished(tr("All containers from server '%1' have been removed").arg(serverName));
return;
}
emit installationErrorOccurred(errorString(errorCode));
emit installationErrorOccurred(errorCode);
}
void InstallController::removeProcessedContainer()
@@ -570,7 +569,7 @@ void InstallController::removeProcessedContainer()
emit removeProcessedContainerFinished(tr("%1 has been removed from the server '%2'").arg(containerName, serverName));
return;
}
emit installationErrorOccurred(errorString(errorCode));
emit installationErrorOccurred(errorCode);
}
void InstallController::removeApiConfig(const int serverIndex)
@@ -738,7 +737,7 @@ bool InstallController::checkSshConnection(QSharedPointer<ServerController> serv
if (errorCode == ErrorCode::NoError) {
m_processedServerCredentials.secretData = decryptedPrivateKey;
} else {
emit installationErrorOccurred(errorString(errorCode));
emit installationErrorOccurred(errorCode);
return false;
}
}
@@ -747,7 +746,7 @@ bool InstallController::checkSshConnection(QSharedPointer<ServerController> serv
output = serverController->checkSshConnection(m_processedServerCredentials, errorCode);
if (errorCode != ErrorCode::NoError) {
emit installationErrorOccurred(errorString(errorCode));
emit installationErrorOccurred(errorCode);
return false;
} else {
if (output.contains(tr("Please login as the user"))) {

View File

@@ -64,6 +64,7 @@ signals:
void removeProcessedContainerFinished(const QString &finishedMessage);
void installationErrorOccurred(const QString &errorMessage);
void installationErrorOccurred(ErrorCode errorCode);
void serverAlreadyExists(int serverIndex);

View File

@@ -1,4 +1,8 @@
#include "pageController.h"
#include "utils/converter.h"
#include "ui/models/servers_model.h"
#include "ui/models/languageModel.h"
#include "core/errorstrings.h"
#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS)
#include <QGuiApplication>
@@ -16,8 +20,10 @@
#endif
PageController::PageController(const QSharedPointer<ServersModel> &serversModel,
const std::shared_ptr<Settings> &settings, QObject *parent)
: QObject(parent), m_serversModel(serversModel), m_settings(settings)
const std::shared_ptr<Settings> &settings,
const QSharedPointer<LanguageModel> &languageModel,
QObject *parent)
: QObject(parent), m_serversModel(serversModel), m_settings(settings), m_languageModel(languageModel)
{
#ifdef Q_OS_ANDROID
// Change color of navigation and status bar's
@@ -38,6 +44,8 @@ PageController::PageController(const QSharedPointer<ServersModel> &serversModel,
connect(this, &PageController::raiseMainWindow, []() { setDockIconVisible(true); });
connect(this, &PageController::hideMainWindow, []() { setDockIconVisible(false); });
#endif
connect(this, qOverload<ErrorCode>(&PageController::showErrorMessage), this, &PageController::onShowErrorMessage);
m_isTriggeredByConnectButton = false;
}
@@ -161,3 +169,14 @@ int PageController::getDrawerDepth()
{
return m_drawerDepth;
}
void PageController::onShowErrorMessage(ErrorCode errorCode)
{
const auto fullErrorMessage = errorString(errorCode);
const auto errorMessage = fullErrorMessage.mid(fullErrorMessage.indexOf(". ") + 1); // remove ErrorCode %1.
const auto baseUrl = m_languageModel->getDocsEndpoint();
const auto errorUrl = QStringLiteral("%1/troubleshooting/error-codes/#error-%2-%3").arg(baseUrl).arg(static_cast<int>(errorCode)).arg(utils::enumToString(errorCode).toLower());
const auto fullMessage = QStringLiteral("<a href=\"%1\" style=\"color: #FBB26A;\">ErrorCode: %2</a>. %3").arg(errorUrl).arg(static_cast<int>(errorCode)).arg(errorMessage);
emit showErrorMessage(fullMessage);
}

View File

@@ -4,7 +4,9 @@
#include <QObject>
#include <QQmlEngine>
#include "core/defs.h"
#include "ui/models/servers_model.h"
#include "ui/models/languageModel.h"
namespace PageLoader
{
@@ -70,7 +72,7 @@ class PageController : public QObject
Q_OBJECT
public:
explicit PageController(const QSharedPointer<ServersModel> &serversModel, const std::shared_ptr<Settings> &settings,
QObject *parent = nullptr);
const QSharedPointer<LanguageModel> & languageModel,QObject *parent = nullptr);
public slots:
QString getInitialPage();
@@ -93,6 +95,9 @@ public slots:
void setDrawerDepth(const int depth);
int getDrawerDepth();
private slots:
void onShowErrorMessage(amnezia::ErrorCode errorCode);
signals:
void goToPage(PageLoader::PageEnum page, bool slide = true);
void goToStartPage();
@@ -107,6 +112,7 @@ signals:
void restorePageHomeState(bool isContainerInstalled = false);
void replaceStartPage();
void showErrorMessage(amnezia::ErrorCode);
void showErrorMessage(const QString &errorMessage);
void showNotificationMessage(const QString &message);
@@ -131,6 +137,8 @@ private:
std::shared_ptr<Settings> m_settings;
QSharedPointer<LanguageModel> m_languageModel;
bool m_isTriggeredByConnectButton;
int m_drawerDepth = 0;

View File

@@ -30,6 +30,9 @@ SettingsController::SettingsController(const QSharedPointer<ServersModel> &serve
{
m_appVersion = QString("%1 (%2, %3)").arg(QString(APP_VERSION), __DATE__, GIT_COMMIT_HASH);
checkIfNeedDisableLogs();
#ifdef Q_OS_ANDROID
connect(AndroidController::instance(), &AndroidController::notificationStateChanged, this, &SettingsController::onNotificationStateChanged);
#endif
}
void SettingsController::toggleAmneziaDns(bool enable)
@@ -233,3 +236,19 @@ void SettingsController::toggleKillSwitch(bool enable)
{
m_settings->setKillSwitchEnabled(enable);
}
bool SettingsController::isNotificationPermissionGranted()
{
#ifdef Q_OS_ANDROID
return AndroidController::instance()->isNotificationPermissionGranted();
#else
return true;
#endif
}
void SettingsController::requestNotificationPermission()
{
#ifdef Q_OS_ANDROID
AndroidController::instance()->requestNotificationPermission();
#endif
}

View File

@@ -23,6 +23,7 @@ public:
Q_PROPERTY(QString primaryDns READ getPrimaryDns WRITE setPrimaryDns NOTIFY primaryDnsChanged)
Q_PROPERTY(QString secondaryDns READ getSecondaryDns WRITE setSecondaryDns NOTIFY secondaryDnsChanged)
Q_PROPERTY(bool isLoggingEnabled READ isLoggingEnabled WRITE toggleLogging NOTIFY loggingStateChanged)
Q_PROPERTY(bool isNotificationPermissionGranted READ isNotificationPermissionGranted NOTIFY onNotificationStateChanged)
public slots:
void toggleAmneziaDns(bool enable);
@@ -66,6 +67,9 @@ public slots:
bool isKillSwitchEnabled();
void toggleKillSwitch(bool enable);
bool isNotificationPermissionGranted();
void requestNotificationPermission();
signals:
void primaryDnsChanged();
void secondaryDnsChanged();
@@ -83,6 +87,8 @@ signals:
void loggingDisableByWatcher();
void onNotificationStateChanged();
private:
QSharedPointer<ServersModel> m_serversModel;
QSharedPointer<ContainersModel> m_containersModel;

View File

@@ -15,7 +15,7 @@
#endif
#ifdef Q_OS_IOS
#include "platforms/ios/MobileUtils.h"
#include "platforms/ios/ios_controller.h"
#include <CoreFoundation/CoreFoundation.h>
#endif
@@ -46,9 +46,8 @@ void SystemController::saveFile(QString fileName, const QString &data)
#ifdef Q_OS_IOS
QStringList filesToSend;
filesToSend.append(fileUrl.toString());
MobileUtils mobileUtils;
// todo check if save successful
mobileUtils.shareText(filesToSend);
IosController::Instance()->shareText(filesToSend);
return;
#else
QFileInfo fi(fileName);
@@ -67,8 +66,7 @@ QString SystemController::getFileName(const QString &acceptLabel, const QString
#ifdef Q_OS_IOS
MobileUtils mobileUtils;
fileName = mobileUtils.openFile();
fileName = IosController::Instance()->openFile();
if (fileName.isEmpty()) {
return fileName;
}

View File

@@ -90,6 +90,24 @@ int LanguageModel::getCurrentLanguageIndex()
}
}
QString LanguageModel::getDocsEndpoint()
{
QString baseUrl = "https://docs.amnezia.org";
auto locale = m_settings->getAppLanguage();
switch (locale.language()) {
case QLocale::Russian:
baseUrl += "/ru";
break;
case QLocale::Ukrainian:
baseUrl += "/ru";
break;
default:
break;
}
return baseUrl;
}
int LanguageModel::getLineHeightAppend()
{
int langIndex = getCurrentLanguageIndex();

View File

@@ -59,6 +59,7 @@ public slots:
int getCurrentLanguageIndex();
int getLineHeightAppend();
QString getCurrentLanguageName();
QString getDocsEndpoint();
signals:
void updateTranslations(const QLocale &locale);

View File

@@ -7,10 +7,7 @@
#if defined(Q_OS_IOS)
# include "platforms/ios/iosnotificationhandler.h"
#elif defined(Q_OS_ANDROID)
# include "platforms/android/android_notificationhandler.h"
#else
# include "systemtray_notificationhandler.h"
#endif
@@ -18,8 +15,6 @@
NotificationHandler* NotificationHandler::create(QObject* parent) {
#if defined(Q_OS_IOS)
return new IOSNotificationHandler(parent);
#elif defined(Q_OS_ANDROID)
return new AndroidNotificationHandler(parent);
#else
# if defined(Q_OS_LINUX)

View File

@@ -57,7 +57,17 @@ Popup {
horizontalAlignment: Text.AlignLeft
Layout.fillWidth: true
onLinkActivated: function(link) {
Qt.openUrlExternally(link)
}
text: root.text
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.NoButton
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
}
}
Item {

View File

@@ -74,7 +74,8 @@ PageType {
}
}
KeyNavigation.tab: labelWithButtonLanguage.rightButton
KeyNavigation.tab: Qt.platform.os === "android" && !SettingsController.isNotificationPermissionGranted ?
labelWithButtonNotification.rightButton : labelWithButtonLanguage.rightButton
parentFlickable: fl
}
@@ -82,6 +83,27 @@ PageType {
visible: GC.isMobile()
}
LabelWithButtonType {
id: labelWithButtonNotification
visible: Qt.platform.os === "android" && !SettingsController.isNotificationPermissionGranted
Layout.fillWidth: true
text: qsTr("Enable notifications")
descriptionText: qsTr("Enable notifications to show the VPN state in the status bar")
rightImageSource: "qrc:/images/controls/chevron-right.svg"
KeyNavigation.tab: labelWithButtonLanguage.rightButton
parentFlickable: fl
clickedFunction: function() {
SettingsController.requestNotificationPermission()
}
}
DividerType {
visible: Qt.platform.os === "android" && !SettingsController.isNotificationPermissionGranted
}
SwitcherType {
id: switcherAutoStart
visible: !GC.isMobile()
@@ -173,7 +195,6 @@ PageType {
}
}
DividerType {}
LabelWithButtonType {

View File

@@ -76,9 +76,9 @@ PageType {
Connections {
target: InstallController
function onInstallationErrorOccurred(errorMessage) {
function onInstallationErrorOccurred(error) {
PageController.showBusyIndicator(false)
PageController.showErrorMessage(errorMessage)
PageController.showErrorMessage(error)
var currentPageName = stackView.currentItem.objectName
@@ -97,8 +97,8 @@ PageType {
PageController.showBusyIndicator(false)
}
function onImportErrorOccurred(errorMessage, goToPageHome) {
PageController.showErrorMessage(errorMessage)
function onImportErrorOccurred(error, goToPageHome) {
PageController.showErrorMessage(error)
}
}

View File

@@ -102,9 +102,10 @@ PageType {
PageController.showBusyIndicator(false)
}
function onExportErrorOccurred(errorMessage) {
function onExportErrorOccurred(error) {
shareConnectionDrawer.close()
PageController.showErrorMessage(errorMessage)
PageController.showErrorMessage(error)
}
}

View File

@@ -99,9 +99,10 @@ PageType {
Connections {
target: InstallController
function onInstallationErrorOccurred(errorMessage) {
function onInstallationErrorOccurred(error) {
PageController.showBusyIndicator(false)
PageController.showErrorMessage(errorMessage)
PageController.showErrorMessage(error)
var needCloseCurrentPage = false
var currentPageName = tabBarStackView.currentItem.objectName
@@ -146,8 +147,8 @@ PageType {
Connections {
target: ImportController
function onImportErrorOccurred(errorMessage, goToPageHome) {
PageController.showErrorMessage(errorMessage)
function onImportErrorOccurred(error, goToPageHome) {
PageController.showErrorMessage(error)
}
}

View File

@@ -76,7 +76,7 @@ QString Utils::usrExecutable(const QString &baseName)
return ("/usr/bin/" + baseName);
}
bool Utils::processIsRunning(const QString &fileName)
bool Utils::processIsRunning(const QString &fileName, const bool fullFlag)
{
#ifdef Q_OS_WIN
QProcess process;
@@ -107,10 +107,14 @@ bool Utils::processIsRunning(const QString &fileName)
#else
QProcess process;
process.setProcessChannelMode(QProcess::MergedChannels);
process.start("pgrep", QStringList({ fileName }));
process.start("pgrep", QStringList({ fullFlag ? "-f" : "", fileName }));
process.waitForFinished();
if (process.exitStatus() == QProcess::NormalExit) {
return (process.readAll().toUInt() > 0);
if (fullFlag) {
return (process.readLine().toUInt() > 0);
} else {
return (process.readAll().toUInt() > 0);
}
}
return false;
#endif

View File

@@ -22,7 +22,7 @@ public:
static bool createEmptyFile(const QString &path);
static bool initializePath(const QString &path);
static bool processIsRunning(const QString &fileName);
static bool processIsRunning(const QString &fileName, const bool fullFlag = false);
static void killProcessByName(const QString &name);
static QString openVpnExecPath();

Some files were not shown because too many files have changed in this diff Show More