iOS Wireguard

This commit is contained in:
pokamest 2021-10-23 04:26:47 -07:00
parent 421f665e85
commit 7701efc704
117 changed files with 6577 additions and 0 deletions

View file

@ -0,0 +1,16 @@
/* 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 IOSADJUSTHELPER_H
#define IOSADJUSTHELPER_H
#include <QString>
class IOSAdjustHelper final {
public:
static void initialize();
static void trackEvent(const QString& eventToken);
};
#endif // IOSADJUSTHELPER_H

View file

@ -0,0 +1,37 @@
/* 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 "iosadjusthelper.h"
#include "logger.h"
#include "constants.h"
#import <AdjustSdk/Adjust.h>
namespace {
Logger logger(LOG_IOS, "IOSAdjustHelper");
} // namespace
void IOSAdjustHelper::initialize() {
NSString *adjustToken = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"ADJUST_SDK_TOKEN"];
if(adjustToken.length) {
NSString *environment = Constants::inProduction() ? ADJEnvironmentProduction : ADJEnvironmentSandbox;
ADJConfig *adjustConfig = [ADJConfig configWithAppToken:adjustToken
environment:environment];
[adjustConfig setLogLevel:ADJLogLevelDebug];
[Adjust appDidLaunch:adjustConfig];
}
}
void IOSAdjustHelper::trackEvent(const QString& eventToken) {
NSString *adjustToken = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"ADJUST_SDK_TOKEN"];
if(adjustToken.length) {
ADJEvent *event = [ADJEvent eventWithEventToken:eventToken.toNSString()];
[Adjust trackEvent:event];
}
}

View file

@ -0,0 +1,23 @@
/* 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 IOSAUTHENTICATIONLISTENER_H
#define IOSAUTHENTICATIONLISTENER_H
#include "authenticationlistener.h"
#include <QObject>
class IOSAuthenticationListener final : public AuthenticationListener {
Q_DISABLE_COPY_MOVE(IOSAuthenticationListener)
public:
IOSAuthenticationListener(QObject* parent);
~IOSAuthenticationListener();
void start(const QString& codeChallenge, const QString& codeChallengeMethod,
const QString& emailAddress) override;
};
#endif // IOSAUTHENTICATIONLISTENER_H

View file

@ -0,0 +1,139 @@
/* 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 "iosauthenticationlistener.h"
#include "leakdetector.h"
#include "logger.h"
#include "mozillavpn.h"
#include "qmlengineholder.h"
#include <QApplication>
#include <QUrl>
#include <QUrlQuery>
#include <QtGui/qpa/qplatformnativeinterface.h>
#include <QtGui>
#include <QWindow>
#include <QQmlApplicationEngine>
#import <UIKit/UIKit.h>
#import <AuthenticationServices/ASWebAuthenticationSession.h>
namespace {
Logger logger({LOG_IOS, LOG_MAIN}, "IOSAuthenticationListener");
ASWebAuthenticationSession* session = nullptr;
} // namespace
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000
@interface ContextProvider : NSObject <ASWebAuthenticationPresentationContextProviding> {
UIView* m_view;
}
@end
@implementation ContextProvider
- (id)initWithUIView:(UIView*)uiView {
self = [super init];
if (self) {
m_view = uiView;
}
return self;
}
# pragma mark - ASWebAuthenticationPresentationContextProviding
- (nonnull ASPresentationAnchor)presentationAnchorForWebAuthenticationSession:
(nonnull ASWebAuthenticationSession*)session API_AVAILABLE(ios(13.0)) {
return m_view.window;
}
@end
#endif
IOSAuthenticationListener::IOSAuthenticationListener(QObject* parent)
: AuthenticationListener(parent) {
MVPN_COUNT_CTOR(IOSAuthenticationListener);
}
IOSAuthenticationListener::~IOSAuthenticationListener() {
MVPN_COUNT_DTOR(IOSAuthenticationListener);
}
void IOSAuthenticationListener::start(const QString& codeChallenge,
const QString& codeChallengeMethod,
const QString& emailAddress) {
logger.debug() << "IOSAuthenticationListener initialize";
QUrl url(createAuthenticationUrl(AmneziaVPN::AuthenticationInBrowser, codeChallenge,
codeChallengeMethod, emailAddress));
QUrlQuery query(url.query());
query.addQueryItem("platform", "ios");
url.setQuery(query);
#ifdef QT_DEBUG
logger.debug() << "Authentication URL:" << url.toString();
#endif
if (session) {
[session dealloc];
session = nullptr;
}
session = [[ASWebAuthenticationSession alloc]
initWithURL:url.toNSURL()
callbackURLScheme:@"mozilla-vpn"
completionHandler:^(NSURL* _Nullable callbackURL, NSError* _Nullable error) {
[session dealloc];
session = nullptr;
if (error) {
logger.error() << "Authentication failed:"
<< QString::fromNSString([error localizedDescription]);
logger.error() << "Code:" << [error code];
logger.error() << "Suggestion:"
<< QString::fromNSString([error localizedRecoverySuggestion]);
logger.error() << "Reason:" << QString::fromNSString([error localizedFailureReason]);
if ([error code] == ASWebAuthenticationSessionErrorCodeCanceledLogin) {
emit abortedByUser();
} else {
emit failed(ErrorHandler::RemoteServiceError);
}
return;
}
QUrl callbackUrl = QUrl::fromNSURL(callbackURL);
logger.debug() << "Authentication completed";
Q_ASSERT(callbackUrl.hasQuery());
QUrlQuery callbackUrlQuery(callbackUrl.query());
Q_ASSERT(callbackUrlQuery.hasQueryItem("code"));
QString code = callbackUrlQuery.queryItemValue("code");
emit completed(code);
}];
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000
QObject* rootObject = QmlEngineHolder::instance()->engine()->rootObjects().first();
QWindow* window = qobject_cast<QWindow*>(rootObject);
Q_ASSERT(window);
UIView* view = static_cast<UIView*>(
QGuiApplication::platformNativeInterface()->nativeResourceForWindow("uiview", window));
if (@available(iOS 13, *)) {
session.presentationContextProvider = [[ContextProvider alloc] initWithUIView:view];
}
#endif
if (![session start]) {
[session dealloc];
session = nullptr;
logger.error() << "Authentication failed: session doesn't start.";
emit failed(ErrorHandler::RemoteServiceError);
}
}

View file

@ -0,0 +1,39 @@
/* 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 IOSCONTROLLER_H
#define IOSCONTROLLER_H
#include "controllerimpl.h"
#include <QObject>
class IOSController final : public ControllerImpl {
Q_DISABLE_COPY_MOVE(IOSController)
public:
IOSController();
~IOSController();
void initialize(const Device* device, const Keys* keys) override;
void activate(const QList<Server>& serverList, const Device* device,
const Keys* keys,
const QList<IPAddressRange>& allowedIPAddressRanges,
const QList<QString>& vpnDisabledApps,
const QHostAddress& dnsServer, Reason reason) override;
void deactivate(Reason reason) override;
void checkStatus() override;
void getBackendLogs(std::function<void(const QString&)>&& callback) override;
void cleanupBackendLogs() override;
private:
bool m_checkingStatus = false;
};
#endif // IOSCONTROLLER_H

View file

@ -0,0 +1,240 @@
/* 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 "ioscontroller.h"
#include "Mozilla_VPN-Swift.h"
#include "device.h"
#include "ipaddressrange.h"
#include "keys.h"
#include "leakdetector.h"
#include "logger.h"
#include "mozillavpn.h"
#include "server.h"
#include "settingsholder.h"
#include <QByteArray>
#include <QFile>
#include <QHostAddress>
namespace {
Logger logger({LOG_IOS, LOG_CONTROLLER}, "IOSController");
// Our Swift singleton.
IOSControllerImpl* impl = nullptr;
} // namespace
IOSController::IOSController() {
MVPN_COUNT_CTOR(IOSController);
logger.debug() << "created";
Q_ASSERT(!impl);
}
IOSController::~IOSController() {
MVPN_COUNT_DTOR(IOSController);
logger.debug() << "deallocated";
if (impl) {
[impl dealloc];
impl = nullptr;
}
}
void IOSController::initialize(const Device* device, const Keys* keys) {
Q_ASSERT(!impl);
Q_UNUSED(device);
logger.debug() << "Initializing Swift Controller";
static bool creating = false;
// No nested creation!
Q_ASSERT(creating == false);
creating = true;
QByteArray key = QByteArray::fromBase64(keys->privateKey().toLocal8Bit());
impl = [[IOSControllerImpl alloc] initWithBundleID:@VPN_NE_BUNDLEID
privateKey:key.toNSData()
deviceIpv4Address:device->ipv4Address().toNSString()
deviceIpv6Address:device->ipv6Address().toNSString()
closure:^(ConnectionState state, NSDate* date) {
logger.debug() << "Creation completed with connection state:" << state;
creating = false;
switch (state) {
case ConnectionStateError: {
[impl dealloc];
impl = nullptr;
emit initialized(false, false, QDateTime());
return;
}
case ConnectionStateConnected: {
Q_ASSERT(date);
QDateTime qtDate(QDateTime::fromNSDate(date));
emit initialized(true, true, qtDate);
return;
}
case ConnectionStateDisconnected:
// Just in case we are connecting, let's call disconnect.
[impl disconnect];
emit initialized(true, false, QDateTime());
return;
}
}
callback:^(BOOL a_connected) {
logger.debug() << "State changed: " << a_connected;
if (a_connected) {
emit connected();
return;
}
emit disconnected();
}];
}
void IOSController::activate(const QList<Server>& serverList, const Device* device,
const Keys* keys, const QList<IPAddressRange>& allowedIPAddressRanges,
const QList<QString>& vpnDisabledApps, const QHostAddress& dnsServer,
Reason reason) {
Q_UNUSED(device);
Q_UNUSED(keys);
Q_ASSERT(serverList.length() == 1);
const Server& server = serverList[0];
// This feature is not supported on macos/ios yet.
Q_ASSERT(vpnDisabledApps.isEmpty());
logger.debug() << "IOSController activating" << server.hostname();
if (!impl) {
logger.error() << "Controller not correctly initialized";
emit disconnected();
return;
}
NSMutableArray<VPNIPAddressRange*>* allowedIPAddressRangesNS =
[NSMutableArray<VPNIPAddressRange*> arrayWithCapacity:allowedIPAddressRanges.length()];
for (const IPAddressRange& i : allowedIPAddressRanges) {
VPNIPAddressRange* range =
[[VPNIPAddressRange alloc] initWithAddress:i.ipAddress().toNSString()
networkPrefixLength:i.range()
isIpv6:i.type() == IPAddressRange::IPv6];
[allowedIPAddressRangesNS addObject:[range autorelease]];
}
[impl connectWithDnsServer:dnsServer.toString().toNSString()
serverIpv6Gateway:server.ipv6Gateway().toNSString()
serverPublicKey:server.publicKey().toNSString()
serverIpv4AddrIn:server.ipv4AddrIn().toNSString()
serverPort:server.choosePort()
allowedIPAddressRanges:allowedIPAddressRangesNS
ipv6Enabled:SettingsHolder::instance()->ipv6Enabled()
reason:reason
failureCallback:^() {
logger.error() << "IOSSWiftController - connection failed";
emit disconnected();
}];
}
void IOSController::deactivate(Reason reason) {
logger.debug() << "IOSController deactivated";
if (reason != ReasonNone) {
logger.debug() << "We do not need to disable the VPN for switching or connection check.";
emit disconnected();
return;
}
if (!impl) {
logger.error() << "Controller not correctly initialized";
emit disconnected();
return;
}
[impl disconnect];
}
void IOSController::checkStatus() {
logger.debug() << "Checking status";
if (m_checkingStatus) {
logger.warning() << "We are still waiting for the previous status.";
return;
}
if (!impl) {
logger.error() << "Controller not correctly initialized";
return;
}
m_checkingStatus = true;
[impl checkStatusWithCallback:^(NSString* serverIpv4Gateway, NSString* deviceIpv4Address,
NSString* configString) {
QString config = QString::fromNSString(configString);
m_checkingStatus = false;
if (config.isEmpty()) {
return;
}
uint64_t txBytes = 0;
uint64_t rxBytes = 0;
QStringList lines = config.split("\n");
for (const QString& line : lines) {
if (line.startsWith("tx_bytes=")) {
txBytes = line.split("=")[1].toULongLong();
} else if (line.startsWith("rx_bytes=")) {
rxBytes = line.split("=")[1].toULongLong();
}
if (txBytes && rxBytes) {
break;
}
}
logger.debug() << "ServerIpv4Gateway:" << QString::fromNSString(serverIpv4Gateway)
<< "DeviceIpv4Address:" << QString::fromNSString(deviceIpv4Address)
<< "RxBytes:" << rxBytes << "TxBytes:" << txBytes;
emit statusUpdated(QString::fromNSString(serverIpv4Gateway),
QString::fromNSString(deviceIpv4Address), txBytes, rxBytes);
}];
}
void IOSController::getBackendLogs(std::function<void(const QString&)>&& a_callback) {
std::function<void(const QString&)> callback = std::move(a_callback);
QString groupId(GROUP_ID);
NSURL* groupPath = [[NSFileManager defaultManager]
containerURLForSecurityApplicationGroupIdentifier:groupId.toNSString()];
NSURL* path = [groupPath URLByAppendingPathComponent:@"networkextension.log"];
QFile file(QString::fromNSString([path path]));
if (!file.open(QIODevice::ReadOnly)) {
callback("Network extension log file missing or unreadable.");
return;
}
QByteArray content = file.readAll();
callback(content);
}
void IOSController::cleanupBackendLogs() {
QString groupId(GROUP_ID);
NSURL* groupPath = [[NSFileManager defaultManager]
containerURLForSecurityApplicationGroupIdentifier:groupId.toNSString()];
NSURL* path = [groupPath URLByAppendingPathComponent:@"networkextension.log"];
QFile file(QString::fromNSString([path path]));
file.remove();
}

View file

@ -0,0 +1,289 @@
/* 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/. */
import Foundation
import NetworkExtension
let vpnName = "Mozilla VPN"
var vpnBundleID = "";
@objc class VPNIPAddressRange : NSObject {
public var address: NSString = ""
public var networkPrefixLength: UInt8 = 0
public var isIpv6: Bool = false
@objc init(address: NSString, networkPrefixLength: UInt8, isIpv6: Bool) {
super.init()
self.address = address
self.networkPrefixLength = networkPrefixLength
self.isIpv6 = isIpv6
}
}
public class IOSControllerImpl : NSObject {
private var tunnel: NETunnelProviderManager? = nil
private var stateChangeCallback: ((Bool) -> Void?)? = nil
private var privateKey : PrivateKey? = nil
private var deviceIpv4Address: String? = nil
private var deviceIpv6Address: String? = nil
@objc enum ConnectionState: Int { case Error, Connected, Disconnected }
@objc init(bundleID: String, privateKey: Data, deviceIpv4Address: String, deviceIpv6Address: String, closure: @escaping (ConnectionState, Date?) -> Void, callback: @escaping (Bool) -> Void) {
super.init()
Logger.configureGlobal(tagged: "APP", withFilePath: "")
vpnBundleID = bundleID;
precondition(!vpnBundleID.isEmpty)
stateChangeCallback = callback
self.privateKey = PrivateKey(rawValue: privateKey)
self.deviceIpv4Address = deviceIpv4Address
self.deviceIpv6Address = deviceIpv6Address
NotificationCenter.default.addObserver(self, selector: #selector(self.vpnStatusDidChange(notification:)), name: Notification.Name.NEVPNStatusDidChange, object: nil)
NETunnelProviderManager.loadAllFromPreferences { [weak self] managers, error in
if let error = error {
Logger.global?.log(message: "Loading from preference failed: \(error)")
closure(ConnectionState.Error, nil)
return
}
if self == nil {
Logger.global?.log(message: "We are shutting down.")
return
}
let nsManagers = managers ?? []
Logger.global?.log(message: "We have received \(nsManagers.count) managers.")
let tunnel = nsManagers.first(where: IOSControllerImpl.isOurManager(_:))
if tunnel == nil {
Logger.global?.log(message: "Creating the tunnel")
self!.tunnel = NETunnelProviderManager()
closure(ConnectionState.Disconnected, nil)
return
}
Logger.global?.log(message: "Tunnel already exists")
self!.tunnel = tunnel
if tunnel?.connection.status == .connected {
closure(ConnectionState.Connected, tunnel?.connection.connectedDate)
} else {
closure(ConnectionState.Disconnected, nil)
}
}
}
@objc private func vpnStatusDidChange(notification: Notification) {
guard let session = (notification.object as? NETunnelProviderSession), tunnel?.connection == session else { return }
switch session.status {
case .connected:
Logger.global?.log(message: "STATE CHANGED: connected")
case .connecting:
Logger.global?.log(message: "STATE CHANGED: connecting")
case .disconnected:
Logger.global?.log(message: "STATE CHANGED: disconnected")
case .disconnecting:
Logger.global?.log(message: "STATE CHANGED: disconnecting")
case .invalid:
Logger.global?.log(message: "STATE CHANGED: invalid")
case .reasserting:
Logger.global?.log(message: "STATE CHANGED: reasserting")
default:
Logger.global?.log(message: "STATE CHANGED: unknown status")
}
// We care about "unknown" state changes.
if (session.status != .connected && session.status != .disconnected) {
return
}
stateChangeCallback?(session.status == .connected)
}
private static func isOurManager(_ manager: NETunnelProviderManager) -> Bool {
guard
let proto = manager.protocolConfiguration,
let tunnelProto = proto as? NETunnelProviderProtocol
else {
Logger.global?.log(message: "Ignoring manager because the proto is invalid.")
return false
}
if (tunnelProto.providerBundleIdentifier == nil) {
Logger.global?.log(message: "Ignoring manager because the bundle identifier is null.")
return false
}
if (tunnelProto.providerBundleIdentifier != vpnBundleID) {
Logger.global?.log(message: "Ignoring manager because the bundle identifier doesn't match.")
return false;
}
Logger.global?.log(message: "Found the manager with the correct bundle identifier: \(tunnelProto.providerBundleIdentifier!)")
return true
}
@objc func connect(dnsServer: String, serverIpv6Gateway: String, serverPublicKey: String, serverIpv4AddrIn: String, serverPort: Int, allowedIPAddressRanges: Array<VPNIPAddressRange>, ipv6Enabled: Bool, reason: Int, failureCallback: @escaping () -> Void) {
Logger.global?.log(message: "Connecting")
assert(tunnel != nil)
// Let's remove the previous config if it exists.
(tunnel!.protocolConfiguration as? NETunnelProviderProtocol)?.destroyConfigurationReference()
let keyData = PublicKey(base64Key: serverPublicKey)!
let dnsServerIP = IPv4Address(dnsServer)
let ipv6GatewayIP = IPv6Address(serverIpv6Gateway)
var peerConfiguration = PeerConfiguration(publicKey: keyData)
peerConfiguration.endpoint = Endpoint(from: serverIpv4AddrIn + ":\(serverPort )")
peerConfiguration.allowedIPs = []
allowedIPAddressRanges.forEach {
if (!$0.isIpv6) {
peerConfiguration.allowedIPs.append(IPAddressRange(address: IPv4Address($0.address as String)!, networkPrefixLength: $0.networkPrefixLength))
} else if (ipv6Enabled) {
peerConfiguration.allowedIPs.append(IPAddressRange(address: IPv6Address($0.address as String)!, networkPrefixLength: $0.networkPrefixLength))
}
}
var peerConfigurations: [PeerConfiguration] = []
peerConfigurations.append(peerConfiguration)
var interface = InterfaceConfiguration(privateKey: privateKey!)
if let ipv4Address = IPAddressRange(from: deviceIpv4Address!),
let ipv6Address = IPAddressRange(from: deviceIpv6Address!) {
interface.addresses = [ipv4Address]
if (ipv6Enabled) {
interface.addresses.append(ipv6Address)
}
}
interface.dns = [ DNSServer(address: dnsServerIP!)]
if (ipv6Enabled) {
interface.dns.append(DNSServer(address: ipv6GatewayIP!))
}
let config = TunnelConfiguration(name: vpnName, interface: interface, peers: peerConfigurations)
self.configureTunnel(config: config, reason: reason, failureCallback: failureCallback)
}
func configureTunnel(config: TunnelConfiguration, reason: Int, failureCallback: @escaping () -> Void) {
let proto = NETunnelProviderProtocol(tunnelConfiguration: config)
proto!.providerBundleIdentifier = vpnBundleID
tunnel!.protocolConfiguration = proto
tunnel!.localizedDescription = vpnName
tunnel!.isEnabled = true
tunnel!.saveToPreferences { [unowned self] saveError in
if let error = saveError {
Logger.global?.log(message: "Connect Tunnel Save Error: \(error)")
failureCallback()
return
}
Logger.global?.log(message: "Saving the tunnel succeeded")
self.tunnel!.loadFromPreferences { error in
if let error = error {
Logger.global?.log(message: "Connect Tunnel Load Error: \(error)")
failureCallback()
return
}
Logger.global?.log(message: "Loading the tunnel succeeded")
do {
if (reason == 1 /* ReasonSwitching */) {
let settings = config.asWgQuickConfig()
let settingsData = settings.data(using: .utf8)!
try (self.tunnel!.connection as? NETunnelProviderSession)?
.sendProviderMessage(settingsData) { data in
guard let data = data,
let configString = String(data: data, encoding: .utf8)
else {
Logger.global?.log(message: "Failed to convert response to string")
return
}
}
} else {
try (self.tunnel!.connection as? NETunnelProviderSession)?.startTunnel()
}
} catch let error {
Logger.global?.log(message: "Something went wrong: \(error)")
failureCallback()
return
}
}
}
}
@objc func disconnect() {
Logger.global?.log(message: "Disconnecting")
assert(tunnel != nil)
(tunnel!.connection as? NETunnelProviderSession)?.stopTunnel()
}
@objc func checkStatus(callback: @escaping (String, String, String) -> Void) {
Logger.global?.log(message: "Check status")
assert(tunnel != nil)
let proto = tunnel!.protocolConfiguration as? NETunnelProviderProtocol
if proto == nil {
callback("", "", "")
return
}
let tunnelConfiguration = proto?.asTunnelConfiguration()
if tunnelConfiguration == nil {
callback("", "", "")
return
}
let serverIpv4Gateway = tunnelConfiguration?.interface.dns[0].address
if serverIpv4Gateway == nil {
callback("", "", "")
return
}
let deviceIpv4Address = tunnelConfiguration?.interface.addresses[0].address
if deviceIpv4Address == nil {
callback("", "", "")
return
}
guard let session = tunnel?.connection as? NETunnelProviderSession
else {
callback("", "", "")
return
}
do {
try session.sendProviderMessage(Data([UInt8(0)])) { [callback] data in
guard let data = data,
let configString = String(data: data, encoding: .utf8)
else {
Logger.global?.log(message: "Failed to convert data to string")
callback("", "", "")
return
}
callback("\(serverIpv4Gateway!)", "\(deviceIpv4Address!)", configString)
}
} catch {
Logger.global?.log(message: "Failed to retrieve data from session")
callback("", "", "")
}
}
}

View file

@ -0,0 +1,15 @@
/* 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 IOSDATAMIGRATION_H
#define IOSDATAMIGRATION_H
#include <QString>
class IOSDataMigration final {
public:
static void migrate();
};
#endif // IOSDATAMIGRATION_H

View file

@ -0,0 +1,172 @@
/* 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 "iosdatamigration.h"
#include "device.h"
#include "logger.h"
#include "mozillavpn.h"
#include "user.h"
#include <QByteArray>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonValue>
#import <Foundation/Foundation.h>
namespace {
Logger logger(LOG_IOS, "IOSDataMigration");
void migrateUserDefaultData() {
AmneziaVPN* vpn = AmneziaVPN::instance();
Q_ASSERT(vpn);
NSUserDefaults* sud = [NSUserDefaults standardUserDefaults];
if (!sud) {
return;
}
NSData* userData = [sud dataForKey:@"user"];
if (userData) {
QByteArray json = QByteArray::fromNSData(userData);
if (!json.isEmpty()) {
logger.debug() << "User data to be migrated";
vpn->accountChecked(json);
}
}
NSData* deviceData = [sud dataForKey:@"device"];
if (deviceData) {
QByteArray json = QByteArray::fromNSData(deviceData);
logger.debug() << "Device data to be migrated";
// Nothing has to be done here because the device data is part of the user data.
}
NSData* serversData = [sud dataForKey:@"vpnServers"];
if (serversData) {
QByteArray json = QByteArray::fromNSData(serversData);
if (!json.isEmpty()) {
logger.debug() << "Server list data to be migrated";
// We need to wrap the server list in a object to make it similar to the REST API response.
QJsonDocument serverList = QJsonDocument::fromJson(json);
if (!serverList.isArray()) {
logger.error() << "Server list should be an array!";
return;
}
QJsonObject countriesObj;
countriesObj.insert("countries", QJsonValue(serverList.array()));
QJsonDocument doc;
doc.setObject(countriesObj);
if (!vpn->setServerList(doc.toJson())) {
logger.error() << "Server list cannot be imported";
return;
}
}
}
NSData* selectedCityData = [sud dataForKey:@"selectedCity"];
if (selectedCityData) {
QByteArray json = QByteArray::fromNSData(selectedCityData);
logger.debug() << "SelectedCity data to be migrated" << json;
// Nothing has to be done here because the device data is part of the user data.
QJsonDocument doc = QJsonDocument::fromJson(json);
if (!doc.isObject()) {
logger.error() << "SelectedCity should be an object";
return;
}
QJsonObject obj = doc.object();
QJsonValue code = obj.value("flagCode");
if (!code.isString()) {
logger.error() << "SelectedCity code should be a string";
return;
}
QJsonValue name = obj.value("code");
if (!name.isString()) {
logger.error() << "SelectedCity name should be a string";
return;
}
ServerData serverData;
if (vpn->serverCountryModel()->pickIfExists(code.toString(), name.toString(), serverData)) {
logger.debug() << "ServerCity found";
serverData.writeSettings();
}
}
}
void migrateKeychainData() {
NSData* service = [@"org.mozilla.guardian.credentials" dataUsingEncoding:NSUTF8StringEncoding];
NSMutableDictionary* query = [[NSMutableDictionary alloc] init];
[query setObject:(id)kSecClassGenericPassword forKey:(id)kSecClass];
[query setObject:service forKey:(id)kSecAttrService];
[query setObject:(id)kCFBooleanTrue forKey:(id)kSecReturnData];
[query setObject:(id)kSecMatchLimitOne forKey:(id)kSecMatchLimit];
NSData* dataNS = NULL;
OSStatus status = SecItemCopyMatching((CFDictionaryRef)query, (CFTypeRef*)(void*)&dataNS);
[query release];
if (status != noErr) {
logger.error() << "No credentials found";
return;
}
QByteArray data = QByteArray::fromNSData(dataNS);
logger.debug() << "Credentials:" << logger.sensitive(data);
QJsonDocument json = QJsonDocument::fromJson(data);
if (!json.isObject()) {
logger.error() << "JSON object expected";
return;
}
QJsonObject obj = json.object();
QJsonValue deviceKeyValue = obj.value("deviceKeys");
if (!deviceKeyValue.isObject()) {
logger.error() << "JSON object should have a deviceKeys object";
return;
}
QJsonObject deviceKeyObj = deviceKeyValue.toObject();
QJsonValue publicKey = deviceKeyObj.value("publicKey");
if (!publicKey.isString()) {
logger.error() << "JSON deviceKey object should contain a publicKey value as string";
return;
}
QJsonValue privateKey = deviceKeyObj.value("privateKey");
if (!privateKey.isString()) {
logger.error() << "JSON deviceKey object should contain a privateKey value as string";
return;
}
QJsonValue token = obj.value("verificationToken");
if (!token.isString()) {
logger.error() << "JSON object should contain a verificationToken value s string";
return;
}
AmneziaVPN::instance()->deviceAdded(Device::currentDeviceName(), publicKey.toString(),
privateKey.toString());
AmneziaVPN::instance()->setToken(token.toString());
}
}
// static
void IOSDataMigration::migrate() {
logger.debug() << "IOS Data Migration";
migrateKeychainData();
migrateUserDefaultData();
}

View file

@ -0,0 +1,249 @@
/* 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/. */
// This file contains all the C functions needed by the Wireguard swift code.
#include <stdlib.h>
#include <string.h>
#ifndef NETWORK_EXTENSION
# include "logger.h"
#else
# import <Foundation/Foundation.h>
# import <os/log.h>
#endif
#define MAX_LOG_FILE_SIZE 204800
// Key base64/hex functions
// ------------------------
#define WG_KEY_LEN (32)
#define WG_KEY_LEN_BASE64 (45)
#define WG_KEY_LEN_HEX (65)
#define EXPORT __attribute__((visibility("default")))
extern "C" {
EXPORT void key_to_base64(char base64[WG_KEY_LEN_BASE64], const uint8_t key[WG_KEY_LEN]);
EXPORT bool key_from_base64(uint8_t key[WG_KEY_LEN], const char* base64);
EXPORT void key_to_hex(char hex[WG_KEY_LEN_HEX], const uint8_t key[WG_KEY_LEN]);
EXPORT bool key_from_hex(uint8_t key[WG_KEY_LEN], const char* hex);
EXPORT bool key_eq(const uint8_t key1[WG_KEY_LEN], const uint8_t key2[WG_KEY_LEN]);
EXPORT void write_msg_to_log(const char* tag, const char* msg);
}
EXPORT void key_to_base64(char base64[WG_KEY_LEN_BASE64], const uint8_t key[WG_KEY_LEN]) {
const char range[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
const char padchar = '=';
int padlen = 0;
char* out = base64;
const uint8_t* in = key;
for (int i = 0; i < WG_KEY_LEN;) {
int chunk = 0;
chunk |= int(in[i++]) << 16;
if (i == WG_KEY_LEN) {
padlen = 2;
} else {
chunk |= int(in[i++]) << 8;
if (i == WG_KEY_LEN) {
padlen = 1;
} else {
chunk |= int(in[i++]);
}
}
int j = (chunk & 0x00fc0000) >> 18;
int k = (chunk & 0x0003f000) >> 12;
int l = (chunk & 0x00000fc0) >> 6;
int m = (chunk & 0x0000003f);
*out++ = range[j];
*out++ = range[k];
if (padlen > 1) {
*out++ = padchar;
} else {
*out++ = range[l];
}
if (padlen > 0) {
*out++ = padchar;
} else {
*out++ = range[m];
}
}
base64[WG_KEY_LEN_BASE64 - 1] = 0;
}
EXPORT bool key_from_base64(uint8_t key[WG_KEY_LEN], const char* base64) {
if (strlen(base64) != WG_KEY_LEN_BASE64 - 1 || base64[WG_KEY_LEN_BASE64 - 2] != '=') {
return false;
}
unsigned int buf = 0;
int nbits = 0;
uint8_t* out = key;
int offset = 0;
for (int i = 0; i < WG_KEY_LEN_BASE64; ++i) {
int ch = base64[i];
int d;
if (ch >= 'A' && ch <= 'Z') {
d = ch - 'A';
} else if (ch >= 'a' && ch <= 'z') {
d = ch - 'a' + 26;
} else if (ch >= '0' && ch <= '9') {
d = ch - '0' + 52;
} else if (ch == '+') {
d = 62;
} else if (ch == '/') {
d = 63;
} else {
d = -1;
}
if (d != -1) {
buf = (buf << 6) | d;
nbits += 6;
if (nbits >= 8) {
nbits -= 8;
out[offset++] = buf >> nbits;
buf &= (1 << nbits) - 1;
}
}
}
return true;
}
inline char toHex(uint8_t value) { return "0123456789abcdef"[value & 0xF]; }
inline int fromHex(uint8_t c) {
return ((c >= '0') && (c <= '9'))
? int(c - '0')
: ((c >= 'A') && (c <= 'F')) ? int(c - 'A' + 10)
: ((c >= 'a') && (c <= 'f')) ? int(c - 'a' + 10) : -1;
}
EXPORT void key_to_hex(char hex[WG_KEY_LEN_HEX], const uint8_t key[WG_KEY_LEN]) {
char* hexData = hex;
const unsigned char* data = (const unsigned char*)key;
for (int i = 0, o = 0; i < WG_KEY_LEN; ++i) {
hexData[o++] = toHex(data[i] >> 4);
hexData[o++] = toHex(data[i] & 0xf);
}
hex[WG_KEY_LEN_HEX - 1] = 0;
}
EXPORT bool key_from_hex(uint8_t key[WG_KEY_LEN], const char* hex) {
if (strlen(hex) != WG_KEY_LEN_HEX - 1) {
return false;
}
bool odd_digit = true;
unsigned char* result = (unsigned char*)key + WG_KEY_LEN;
for (int i = WG_KEY_LEN_HEX - 1; i >= 0; --i) {
int tmp = fromHex((unsigned char)(hex[i]));
if (tmp == -1) {
continue;
}
if (odd_digit) {
--result;
*result = tmp;
odd_digit = false;
} else {
*result |= tmp << 4;
odd_digit = true;
}
}
return true;
}
EXPORT bool key_eq(const uint8_t key1[WG_KEY_LEN], const uint8_t key2[WG_KEY_LEN]) {
for (int i = 0; i < WG_KEY_LEN; i++) {
if (key1[i] != key2[i]) {
return false;
}
}
return true;
}
// Logging functions
// -----------------
#ifndef NETWORK_EXTENSION
namespace {
Logger logger(LOG_IOS, "IOSSGlue");
}
#endif
EXPORT void write_msg_to_log(const char* tag, const char* msg) {
#ifndef NETWORK_EXTENSION
logger.debug() << "Swift log - tag:" << tag << "msg: " << msg;
#else
os_log_with_type(OS_LOG_DEFAULT, OS_LOG_TYPE_DEBUG, "tag: %s - msg: %s", tag, msg);
@autoreleasepool {
NSString* groupId = [NSString stringWithUTF8String:GROUP_ID];
NSURL* groupPath =
[[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:groupId];
NSURL* pathUrl = [groupPath URLByAppendingPathComponent:@"networkextension.log"];
NSString* path = [pathUrl path];
if (![[NSFileManager defaultManager] fileExistsAtPath:path]) {
[[NSFileManager defaultManager] createFileAtPath:path contents:nil attributes:nil];
} else {
NSError* error = nil;
NSDictionary* fileAttributes = [[NSFileManager defaultManager] attributesOfItemAtPath:path
error:&error];
if (error) {
return;
}
NSNumber* fileSizeNumber = [fileAttributes objectForKey:NSFileSize];
long long fileSize = [fileSizeNumber longLongValue];
if (fileSize > MAX_LOG_FILE_SIZE) {
[[NSFileManager defaultManager] removeItemAtPath:path error:&error];
[[NSFileManager defaultManager] createFileAtPath:path contents:nil attributes:nil];
}
}
NSError* error = nil;
NSFileHandle* fh = [NSFileHandle fileHandleForWritingToURL:pathUrl error:&error];
if (!fh) {
return;
}
NSString* dateString = [NSDateFormatter localizedStringFromDate:[NSDate date]
dateStyle:NSDateFormatterShortStyle
timeStyle:NSDateFormatterFullStyle];
NSString* str = [NSString stringWithFormat:@" - %s\n", msg];
NSData* data =
[[dateString stringByAppendingString:str] dataUsingEncoding:NSUTF8StringEncoding];
@try {
[fh seekToEndOfFile];
[fh writeData:data];
} @catch (NSException* exception) {
}
[fh closeFile];
}
#endif
}

View file

@ -0,0 +1,30 @@
/* 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 IOSIAPHANDLER_H
#define IOSIAPHANDLER_H
#include "iaphandler.h"
class IOSIAPHandler final : public IAPHandler {
Q_OBJECT
Q_DISABLE_COPY_MOVE(IOSIAPHandler)
public:
explicit IOSIAPHandler(QObject* parent);
~IOSIAPHandler();
public slots:
void productRegistered(void* product);
void processCompletedTransactions(const QStringList& ids);
protected:
void nativeRegisterProducts() override;
void nativeStartSubscription(Product* product) override;
private:
void* m_delegate = nullptr;
};
#endif // IOSIAPHANDLER_H

View file

@ -0,0 +1,369 @@
/* 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 "platforms/ios/iosiaphandler.h"
#include "constants.h"
#include "iosutils.h"
#include "leakdetector.h"
#include "logger.h"
#include "mozillavpn.h"
#include "networkrequest.h"
#include "settingsholder.h"
#include <QCoreApplication>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonValue>
#include <QScopeGuard>
#import <Foundation/Foundation.h>
#import <StoreKit/StoreKit.h>
namespace {
Logger logger(LOG_IAP, "IOSIAPHandler");
} // namespace
@interface IOSIAPHandlerDelegate
: NSObject <SKRequestDelegate, SKProductsRequestDelegate, SKPaymentTransactionObserver> {
IOSIAPHandler* m_handler;
}
@end
@implementation IOSIAPHandlerDelegate
- (id)initWithObject:(IOSIAPHandler*)handler {
self = [super init];
if (self) {
m_handler = handler;
}
return self;
}
- (void)productsRequest:(nonnull SKProductsRequest*)request
didReceiveResponse:(nonnull SKProductsResponse*)response {
logger.debug() << "Registration completed";
if (response.invalidProductIdentifiers) {
NSArray<NSString*>* products = response.invalidProductIdentifiers;
logger.error() << "Registration failure" << [products count];
for (unsigned long i = 0, count = [products count]; i < count; ++i) {
NSString* identifier = [products objectAtIndex:i];
QMetaObject::invokeMethod(m_handler, "unknownProductRegistered", Qt::QueuedConnection,
Q_ARG(QString, QString::fromNSString(identifier)));
}
}
NSArray<SKProduct*>* products = response.products;
if (products) {
logger.debug() << "Products registered" << [products count];
for (unsigned long i = 0, count = [products count]; i < count; ++i) {
SKProduct* product = [[products objectAtIndex:i] retain];
QMetaObject::invokeMethod(m_handler, "productRegistered", Qt::QueuedConnection,
Q_ARG(void*, product));
}
}
QMetaObject::invokeMethod(m_handler, "productsRegistrationCompleted", Qt::QueuedConnection);
[request release];
}
- (void)paymentQueue:(nonnull SKPaymentQueue*)queue
updatedTransactions:(nonnull NSArray<SKPaymentTransaction*>*)transactions {
logger.debug() << "payment queue:" << [transactions count];
QStringList completedTransactionIds;
bool failedTransactions = false;
bool canceledTransactions = false;
bool completedTransactions = false;
for (SKPaymentTransaction* transaction in transactions) {
switch (transaction.transactionState) {
case SKPaymentTransactionStateFailed:
logger.error() << "transaction failed";
if (transaction.error.code == SKErrorPaymentCancelled) {
canceledTransactions = true;
} else {
failedTransactions = true;
}
break;
case SKPaymentTransactionStateRestored:
[[fallthrough]];
case SKPaymentTransactionStatePurchased: {
QString identifier = QString::fromNSString(transaction.transactionIdentifier);
QDateTime date = QDateTime::fromNSDate(transaction.transactionDate);
logger.debug() << "transaction purchased - identifier: " << identifier
<< "- date:" << date.toString();
if (transaction.transactionState == SKPaymentTransactionStateRestored) {
SKPaymentTransaction* originalTransaction = transaction.originalTransaction;
if (originalTransaction) {
QString originalIdentifier =
QString::fromNSString(originalTransaction.transactionIdentifier);
QDateTime originalDate = QDateTime::fromNSDate(originalTransaction.transactionDate);
logger.debug() << "original transaction identifier: " << originalIdentifier
<< "- date:" << originalDate.toString();
}
}
completedTransactions = true;
SettingsHolder* settingsHolder = SettingsHolder::instance();
if (settingsHolder->hasSubscriptionTransaction(identifier)) {
logger.warning() << "This transaction has already been processed. Let's ignore it.";
} else {
completedTransactionIds.append(identifier);
}
break;
}
case SKPaymentTransactionStatePurchasing:
logger.debug() << "transaction purchasing";
break;
case SKPaymentTransactionStateDeferred:
logger.debug() << "transaction deferred";
break;
default:
logger.warning() << "transaction unknwon state";
break;
}
}
if (!completedTransactions && !canceledTransactions && !failedTransactions) {
// Nothing completed, nothing restored, nothing failed. Just purchasing transactions.
return;
}
if (canceledTransactions) {
logger.debug() << "Subscription canceled";
QMetaObject::invokeMethod(m_handler, "stopSubscription", Qt::QueuedConnection);
QMetaObject::invokeMethod(m_handler, "subscriptionCanceled", Qt::QueuedConnection);
} else if (failedTransactions) {
logger.error() << "Subscription failed";
QMetaObject::invokeMethod(m_handler, "stopSubscription", Qt::QueuedConnection);
QMetaObject::invokeMethod(m_handler, "subscriptionCanceled", Qt::QueuedConnection);
} else if (completedTransactionIds.isEmpty()) {
Q_ASSERT(completedTransactions);
logger.debug() << "Subscription completed - but all the transactions are known";
QMetaObject::invokeMethod(m_handler, "stopSubscription", Qt::QueuedConnection);
QMetaObject::invokeMethod(m_handler, "subscriptionCanceled", Qt::QueuedConnection);
} else if (AmneziaVPN::instance()->userAuthenticated()) {
Q_ASSERT(completedTransactions);
logger.debug() << "Subscription completed. Let's start the validation";
QMetaObject::invokeMethod(m_handler, "processCompletedTransactions", Qt::QueuedConnection,
Q_ARG(QStringList, completedTransactionIds));
} else {
Q_ASSERT(completedTransactions);
logger.debug() << "Subscription completed - but the user is not authenticated yet";
QMetaObject::invokeMethod(m_handler, "stopSubscription", Qt::QueuedConnection);
QMetaObject::invokeMethod(m_handler, "subscriptionCanceled", Qt::QueuedConnection);
}
for (SKPaymentTransaction* transaction in transactions) {
switch (transaction.transactionState) {
case SKPaymentTransactionStateFailed:
[[fallthrough]];
case SKPaymentTransactionStateRestored:
[[fallthrough]];
case SKPaymentTransactionStatePurchased:
[queue finishTransaction:transaction];
break;
default:
break;
}
}
}
- (void)requestDidFinish:(SKRequest*)request {
logger.debug() << "Receipt refreshed correctly";
QMetaObject::invokeMethod(m_handler, "stopSubscription", Qt::QueuedConnection);
QMetaObject::invokeMethod(m_handler, "processCompletedTransactions", Qt::QueuedConnection,
Q_ARG(QStringList, QStringList()));
}
- (void)request:(SKRequest*)request didFailWithError:(NSError*)error {
logger.error() << "Failed to refresh the receipt"
<< QString::fromNSString(error.localizedDescription);
QMetaObject::invokeMethod(m_handler, "stopSubscription", Qt::QueuedConnection);
QMetaObject::invokeMethod(m_handler, "subscriptionFailed", Qt::QueuedConnection);
}
@end
IOSIAPHandler::IOSIAPHandler(QObject* parent) : IAPHandler(parent) {
MVPN_COUNT_CTOR(IOSIAPHandler);
m_delegate = [[IOSIAPHandlerDelegate alloc] initWithObject:this];
[[SKPaymentQueue defaultQueue]
addTransactionObserver:static_cast<IOSIAPHandlerDelegate*>(m_delegate)];
}
IOSIAPHandler::~IOSIAPHandler() {
MVPN_COUNT_DTOR(IOSIAPHandler);
IOSIAPHandlerDelegate* delegate = static_cast<IOSIAPHandlerDelegate*>(m_delegate);
[[SKPaymentQueue defaultQueue] removeTransactionObserver:delegate];
[delegate dealloc];
m_delegate = nullptr;
}
void IOSIAPHandler::nativeRegisterProducts() {
NSSet<NSString*>* productIdentifiers = [NSSet<NSString*> set];
for (const Product& product : m_products) {
productIdentifiers = [productIdentifiers setByAddingObject:product.m_name.toNSString()];
}
logger.debug() << "We are about to register" << [productIdentifiers count] << "products";
SKProductsRequest* productsRequest =
[[SKProductsRequest alloc] initWithProductIdentifiers:productIdentifiers];
IOSIAPHandlerDelegate* delegate = static_cast<IOSIAPHandlerDelegate*>(m_delegate);
productsRequest.delegate = delegate;
[productsRequest start];
}
void IOSIAPHandler::nativeStartSubscription(Product* product) {
Q_ASSERT(product->m_extra);
SKProduct* skProduct = static_cast<SKProduct*>(product->m_extra);
SKPayment* payment = [SKPayment paymentWithProduct:skProduct];
[[SKPaymentQueue defaultQueue] addPayment:payment];
}
void IOSIAPHandler::productRegistered(void* a_product) {
SKProduct* product = static_cast<SKProduct*>(a_product);
Q_ASSERT(m_productsRegistrationState == eRegistering);
logger.debug() << "Product registered";
NSString* nsProductIdentifier = [product productIdentifier];
QString productIdentifier = QString::fromNSString(nsProductIdentifier);
Product* productData = findProduct(productIdentifier);
Q_ASSERT(productData);
logger.debug() << "Id:" << productIdentifier;
logger.debug() << "Title:" << QString::fromNSString([product localizedTitle]);
logger.debug() << "Description:" << QString::fromNSString([product localizedDescription]);
QString priceValue;
{
NSNumberFormatter* numberFormatter = [[NSNumberFormatter alloc] init];
[numberFormatter setFormatterBehavior:NSNumberFormatterBehavior10_4];
[numberFormatter setNumberStyle:(NSNumberFormatterStyle)NSNumberFormatterCurrencyStyle];
[numberFormatter setLocale:product.priceLocale];
NSString* price = [numberFormatter stringFromNumber:product.price];
priceValue = QString::fromNSString(price);
[numberFormatter release];
}
logger.debug() << "Price:" << priceValue;
QString monthlyPriceValue;
NSDecimalNumber* monthlyPriceNS = nullptr;
{
NSNumberFormatter* numberFormatter = [[NSNumberFormatter alloc] init];
[numberFormatter setFormatterBehavior:NSNumberFormatterBehavior10_4];
[numberFormatter setNumberStyle:(NSNumberFormatterStyle)NSNumberFormatterCurrencyStyle];
[numberFormatter setLocale:product.priceLocale];
int32_t mounthCount = productTypeToMonthCount(productData->m_type);
Q_ASSERT(mounthCount >= 1);
if (mounthCount == 1) {
monthlyPriceNS = product.price;
} else {
NSDecimalNumber* divider = [[NSDecimalNumber alloc] initWithDouble:(double)mounthCount];
monthlyPriceNS = [product.price decimalNumberByDividingBy:divider];
[divider release];
}
NSString* price = [numberFormatter stringFromNumber:monthlyPriceNS];
monthlyPriceValue = QString::fromNSString(price);
[numberFormatter release];
}
logger.debug() << "Monthly Price:" << monthlyPriceValue;
productData->m_price = priceValue;
productData->m_monthlyPrice = monthlyPriceValue;
productData->m_nonLocalizedMonthlyPrice = [monthlyPriceNS doubleValue];
productData->m_extra = product;
}
void IOSIAPHandler::processCompletedTransactions(const QStringList& ids) {
logger.debug() << "process completed transactions";
if (m_subscriptionState != eActive) {
logger.warning() << "Random transaction to be completed. Let's ignore it";
return;
}
QString receipt = IOSUtils::IAPReceipt();
if (receipt.isEmpty()) {
logger.warning() << "Empty receipt found";
emit subscriptionFailed();
return;
}
NetworkRequest* request = NetworkRequest::createForIOSPurchase(this, receipt);
connect(request, &NetworkRequest::requestFailed,
[this](QNetworkReply::NetworkError error, const QByteArray& data) {
logger.error() << "Purchase request failed" << error;
if (m_subscriptionState != eActive) {
logger.warning() << "We have been canceled in the meantime";
return;
}
stopSubscription();
QJsonDocument json = QJsonDocument::fromJson(data);
if (!json.isObject()) {
AmneziaVPN::instance()->errorHandle(ErrorHandler::toErrorType(error));
emit subscriptionFailed();
return;
}
QJsonObject obj = json.object();
QJsonValue errorValue = obj.value("errno");
if (!errorValue.isDouble()) {
AmneziaVPN::instance()->errorHandle(ErrorHandler::toErrorType(error));
emit subscriptionFailed();
return;
}
int errorNumber = errorValue.toInt();
if (errorNumber != 145) {
AmneziaVPN::instance()->errorHandle(ErrorHandler::toErrorType(error));
emit subscriptionFailed();
return;
}
emit alreadySubscribed();
});
connect(request, &NetworkRequest::requestCompleted, [this, ids](const QByteArray&) {
logger.debug() << "Purchase request completed";
SettingsHolder::instance()->addSubscriptionTransactions(ids);
if (m_subscriptionState != eActive) {
logger.warning() << "We have been canceled in the meantime";
return;
}
stopSubscription();
emit subscriptionCompleted();
});
}

View file

@ -0,0 +1,53 @@
/* 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/. */
import Foundation
import os.log
public class Logger {
static var global: Logger?
var tag: String
init(tagged tag: String) {
self.tag = tag
}
deinit {}
func log(message: String) {
write_msg_to_log(tag, message.trimmingCharacters(in: .newlines))
}
func writeLog(to targetFile: String) -> Bool {
return true;
}
static func configureGlobal(tagged tag: String, withFilePath filePath: String?) {
if Logger.global != nil {
return
}
Logger.global = Logger(tagged: tag)
var appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown version"
if let appBuild = Bundle.main.infoDictionary?["CFBundleVersion"] as? String {
appVersion += " (\(appBuild))"
}
let goBackendVersion = WIREGUARD_GO_VERSION
Logger.global?.log(message: "App version: \(appVersion); Go backend version: \(goBackendVersion)")
}
}
func wg_log(_ type: OSLogType, staticMessage msg: StaticString) {
os_log(msg, log: OSLog.default, type: type)
Logger.global?.log(message: "\(msg)")
}
func wg_log(_ type: OSLogType, message msg: String) {
os_log("%{public}s", log: OSLog.default, type: type, msg)
Logger.global?.log(message: msg)
}

View file

@ -0,0 +1,27 @@
/* 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 IOSNOTIFICATIONHANDLER_H
#define IOSNOTIFICATIONHANDLER_H
#include "notificationhandler.h"
#include <QObject>
class IOSNotificationHandler final : public NotificationHandler {
Q_DISABLE_COPY_MOVE(IOSNotificationHandler)
public:
IOSNotificationHandler(QObject* parent);
~IOSNotificationHandler();
protected:
void notify(const QString& title, const QString& message,
int timerSec) override;
private:
void* m_delegate = nullptr;
};
#endif // IOSNOTIFICATIONHANDLER_H

View file

@ -0,0 +1,87 @@
/* 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 "platforms/ios/iosnotificationhandler.h"
#include "leakdetector.h"
#import <UserNotifications/UserNotifications.h>
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
@interface IOSNotificationDelegate
: UIResponder <UIApplicationDelegate, UNUserNotificationCenterDelegate> {
IOSNotificationHandler* m_iosNotificationHandler;
}
@end
@implementation IOSNotificationDelegate
- (id)initWithObject:(IOSNotificationHandler*)notification {
self = [super init];
if (self) {
m_iosNotificationHandler = notification;
}
return self;
}
- (void)userNotificationCenter:(UNUserNotificationCenter*)center
willPresentNotification:(UNNotification*)notification
withCompletionHandler:
(void (^)(UNNotificationPresentationOptions options))completionHandler {
Q_UNUSED(center)
completionHandler(UNNotificationPresentationOptionAlert);
}
- (void)userNotificationCenter:(UNUserNotificationCenter*)center
didReceiveNotificationResponse:(UNNotificationResponse*)response
withCompletionHandler:(void (^)())completionHandler {
Q_UNUSED(center)
Q_UNUSED(response)
completionHandler();
}
@end
IOSNotificationHandler::IOSNotificationHandler(QObject* parent) : NotificationHandler(parent) {
MVPN_COUNT_CTOR(IOSNotificationHandler);
UNUserNotificationCenter* center = [UNUserNotificationCenter currentNotificationCenter];
[center requestAuthorizationWithOptions:(UNAuthorizationOptionSound | UNAuthorizationOptionAlert |
UNAuthorizationOptionBadge)
completionHandler:^(BOOL granted, NSError* _Nullable error) {
Q_UNUSED(granted);
if (!error) {
m_delegate = [[IOSNotificationDelegate alloc] initWithObject:this];
}
}];
}
IOSNotificationHandler::~IOSNotificationHandler() { MVPN_COUNT_DTOR(IOSNotificationHandler); }
void IOSNotificationHandler::notify(const QString& title, const QString& message, int timerSec) {
if (!m_delegate) {
return;
}
UNMutableNotificationContent* content = [[UNMutableNotificationContent alloc] init];
content.title = title.toNSString();
content.body = message.toNSString();
content.sound = [UNNotificationSound defaultSound];
UNTimeIntervalNotificationTrigger* trigger =
[UNTimeIntervalNotificationTrigger triggerWithTimeInterval:timerSec repeats:NO];
UNNotificationRequest* request = [UNNotificationRequest requestWithIdentifier:@"mozillavpn"
content:content
trigger:trigger];
UNUserNotificationCenter* center = [UNUserNotificationCenter currentNotificationCenter];
center.delegate = id(m_delegate);
[center addNotificationRequest:request
withCompletionHandler:^(NSError* _Nullable error) {
if (error) {
NSLog(@"Local Notification failed");
}
}];
}

View file

@ -0,0 +1,147 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2020 WireGuard LLC. All Rights Reserved.
import Foundation
import NetworkExtension
import os
class PacketTunnelProvider: NEPacketTunnelProvider {
private lazy var adapter: WireGuardAdapter = {
return WireGuardAdapter(with: self) { logLevel, message in
wg_log(logLevel.osLogLevel, message: message)
}
}()
override func startTunnel(options: [String: NSObject]?, completionHandler: @escaping (Error?) -> Void) {
let activationAttemptId = options?["activationAttemptId"] as? String
let errorNotifier = ErrorNotifier(activationAttemptId: activationAttemptId)
Logger.configureGlobal(tagged: "NET", withFilePath: FileManager.logFileURL?.path)
wg_log(.info, message: "Starting tunnel from the " + (activationAttemptId == nil ? "OS directly, rather than the app" : "app"))
guard let tunnelProviderProtocol = self.protocolConfiguration as? NETunnelProviderProtocol,
let tunnelConfiguration = tunnelProviderProtocol.asTunnelConfiguration() else {
errorNotifier.notify(PacketTunnelProviderError.savedProtocolConfigurationIsInvalid)
completionHandler(PacketTunnelProviderError.savedProtocolConfigurationIsInvalid)
return
}
// Start the tunnel
adapter.start(tunnelConfiguration: tunnelConfiguration) { adapterError in
guard let adapterError = adapterError else {
let interfaceName = self.adapter.interfaceName ?? "unknown"
wg_log(.info, message: "Tunnel interface is \(interfaceName)")
completionHandler(nil)
return
}
switch adapterError {
case .cannotLocateTunnelFileDescriptor:
wg_log(.error, staticMessage: "Starting tunnel failed: could not determine file descriptor")
errorNotifier.notify(PacketTunnelProviderError.couldNotDetermineFileDescriptor)
completionHandler(PacketTunnelProviderError.couldNotDetermineFileDescriptor)
case .dnsResolution(let dnsErrors):
let hostnamesWithDnsResolutionFailure = dnsErrors.map { $0.address }
.joined(separator: ", ")
wg_log(.error, message: "DNS resolution failed for the following hostnames: \(hostnamesWithDnsResolutionFailure)")
errorNotifier.notify(PacketTunnelProviderError.dnsResolutionFailure)
completionHandler(PacketTunnelProviderError.dnsResolutionFailure)
case .setNetworkSettings(let error):
wg_log(.error, message: "Starting tunnel failed with setTunnelNetworkSettings returning \(error.localizedDescription)")
errorNotifier.notify(PacketTunnelProviderError.couldNotSetNetworkSettings)
completionHandler(PacketTunnelProviderError.couldNotSetNetworkSettings)
case .startWireGuardBackend(let errorCode):
wg_log(.error, message: "Starting tunnel failed with wgTurnOn returning \(errorCode)")
errorNotifier.notify(PacketTunnelProviderError.couldNotStartBackend)
completionHandler(PacketTunnelProviderError.couldNotStartBackend)
case .invalidState:
// Must never happen
fatalError()
}
}
}
override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) {
wg_log(.info, staticMessage: "Stopping tunnel")
adapter.stop { error in
ErrorNotifier.removeLastErrorFile()
if let error = error {
wg_log(.error, message: "Failed to stop WireGuard adapter: \(error.localizedDescription)")
}
completionHandler()
#if os(macOS)
// HACK: This is a filthy hack to work around Apple bug 32073323 (dup'd by us as 47526107).
// Remove it when they finally fix this upstream and the fix has been rolled out to
// sufficient quantities of users.
exit(0)
#endif
}
}
override func handleAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)? = nil) {
guard let completionHandler = completionHandler else { return }
if messageData.count == 1 && messageData[0] == 0 {
adapter.getRuntimeConfiguration { settings in
var data: Data?
if let settings = settings {
data = settings.data(using: .utf8)!
}
completionHandler(data)
}
} else if messageData.count >= 1 {
// Updates the tunnel configuration and responds with the active configuration
wg_log(.info, message: "Switching tunnel configuration")
guard let configString = String(data: messageData, encoding: .utf8)
else {
completionHandler(nil)
return
}
do {
let tunnelConfiguration = try TunnelConfiguration(fromWgQuickConfig: configString)
adapter.update(tunnelConfiguration: tunnelConfiguration) { error in
if let error = error {
wg_log(.error, message: "Failed to switch tunnel configuration: \(error.localizedDescription)")
completionHandler(nil)
return
}
self.adapter.getRuntimeConfiguration { settings in
var data: Data?
if let settings = settings {
data = settings.data(using: .utf8)!
}
completionHandler(data)
}
}
} catch {
completionHandler(nil)
}
} else {
completionHandler(nil)
}
}
}
extension WireGuardLogLevel {
var osLogLevel: OSLogType {
switch self {
case .verbose:
return .debug
case .error:
return .error
}
}
}

View file

@ -0,0 +1,17 @@
/* 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 IOSUTILS_H
#define IOSUTILS_H
#include <QString>
class IOSUtils final {
public:
static QString computerName();
static QString IAPReceipt();
};
#endif // IOSUTILS_H

View file

@ -0,0 +1,63 @@
/* 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 "iosutils.h"
#include "logger.h"
#include <QDateTime>
#include <QString>
#import <UIKit/UIKit.h>
namespace {
Logger logger(LOG_IOS, "IOSUtils");
}
// static
QString IOSUtils::computerName() {
NSString* name = [[UIDevice currentDevice] name];
return QString::fromNSString(name);
}
// static
QString IOSUtils::IAPReceipt() {
logger.debug() << "Retrieving IAP receipt";
NSURL* receiptURL = [[NSBundle mainBundle] appStoreReceiptURL];
NSData* receipt = [NSData dataWithContentsOfURL:receiptURL];
// All the following is for debug only.
NSString* path = [receiptURL path];
Q_ASSERT(path);
logger.debug() << "Receipt URL:" << QString::fromNSString(path);
NSFileManager* fileManager = [NSFileManager defaultManager];
Q_ASSERT(fileManager);
NSDictionary* fileAttributes = [fileManager attributesOfItemAtPath:path error:NULL];
if (fileAttributes) {
NSNumber* fileSize = [fileAttributes objectForKey:NSFileSize];
if (fileSize) {
logger.debug() << "File size:" << [fileSize unsignedLongLongValue];
}
NSString* fileOwner = [fileAttributes objectForKey:NSFileOwnerAccountName];
if (fileOwner) {
logger.debug() << "Owner:" << QString::fromNSString(fileOwner);
}
NSDate* fileModDate = [fileAttributes objectForKey:NSFileModificationDate];
if (fileModDate) {
logger.debug() << "Modification date:" << QDateTime::fromNSDate(fileModDate).toString();
}
}
if (!receipt) {
return QString();
}
NSString* encodedReceipt = [receipt base64EncodedStringWithOptions:0];
return QString::fromNSString(encodedReceipt);
}