Skip to content

Commit

Permalink
Allow users to import settings by pasting JSON blobs
Browse files Browse the repository at this point in the history
  • Loading branch information
Jon Petersson committed Jan 24, 2024
1 parent 3a5ba4a commit 0c68536
Show file tree
Hide file tree
Showing 14 changed files with 687 additions and 25 deletions.
51 changes: 51 additions & 0 deletions ios/MullvadSettings/IPOverride.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
//
// IPOverride.swift
// MullvadVPN
//
// Created by Jon Petersson on 2024-01-16.
// Copyright © 2024 Mullvad VPN AB. All rights reserved.
//

import Network

public struct RelayOverrides: Codable {
public let overrides: [IPOverride]

private enum CodingKeys: String, CodingKey {
case overrides = "relay_overrides"
}
}

public struct IPOverride: Codable, Equatable {
public let hostname: String
public var ipv4Address: IPv4Address?
public var ipv6Address: IPv6Address?

private enum CodingKeys: String, CodingKey {
case hostname
case ipv4Address = "ipv4_addr_in"
case ipv6Address = "ipv6_addr_in"
}

init(hostname: String, ipv4Address: IPv4Address?, ipv6Address: IPv6Address?) throws {
self.hostname = hostname
self.ipv4Address = ipv4Address
self.ipv6Address = ipv6Address

if self.ipv4Address.isNil && self.ipv6Address.isNil {
throw NSError(domain: "IPOverrideInitDomain", code: NSFormattingError)
}
}

public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)

self.hostname = try container.decode(String.self, forKey: .hostname)
self.ipv4Address = try container.decodeIfPresent(IPv4Address.self, forKey: .ipv4Address)
self.ipv6Address = try container.decodeIfPresent(IPv6Address.self, forKey: .ipv6Address)

if self.ipv4Address.isNil && self.ipv6Address.isNil {
throw NSError(domain: "IPOverrideInitDomain", code: NSFormattingError)
}
}
}
90 changes: 90 additions & 0 deletions ios/MullvadSettings/IPOverrideRepository.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
//
// IPOverrideRepository.swift
// MullvadVPN
//
// Created by Jon Petersson on 2024-01-16.
// Copyright © 2024 Mullvad VPN AB. All rights reserved.
//

import Foundation

public protocol IPOverrideRepositoryProtocol {
func add(_ overrides: [IPOverride])
func fetchAll() -> [IPOverride]
func fetchByHostname(_ hostname: String) -> IPOverride?
func deleteAll()
func parseData(_ data: Data) throws -> [IPOverride]
}

public class IPOverrideRepository: IPOverrideRepositoryProtocol {
public init() {}

public func add(_ overrides: [IPOverride]) {
var storedOverrides = fetchAll()

overrides.forEach { override in
if let existingOverrideIndex = storedOverrides.firstIndex(where: { $0.hostname == override.hostname }) {
var existingOverride = storedOverrides[existingOverrideIndex]

if let ipv4Address = override.ipv4Address {
existingOverride.ipv4Address = ipv4Address
}

if let ipv6Address = override.ipv6Address {
existingOverride.ipv6Address = ipv6Address
}

storedOverrides[existingOverrideIndex] = existingOverride
} else {
storedOverrides.append(override)
}
}

do {
try writeIpOverrides(storedOverrides)
} catch {
print("Could not add override(s): \(overrides) \nError: \(error)")
}
}

public func fetchAll() -> [IPOverride] {
return (try? readIpOverrides()) ?? []
}

public func fetchByHostname(_ hostname: String) -> IPOverride? {
return fetchAll().first { $0.hostname == hostname }
}

public func deleteAll() {
do {
try SettingsManager.store.delete(key: .ipOverrides)
} catch {
print("Could not delete all overrides. \nError: \(error)")
}
}

public func parseData(_ data: Data) throws -> [IPOverride] {
let decoder = JSONDecoder()
let jsonData = try decoder.decode(RelayOverrides.self, from: data)

return jsonData.overrides
}

private func readIpOverrides() throws -> [IPOverride] {
let parser = makeParser()
let data = try SettingsManager.store.read(key: .ipOverrides)

return try parser.parseUnversionedPayload(as: [IPOverride].self, from: data)
}

private func writeIpOverrides(_ overrides: [IPOverride]) throws {
let parser = makeParser()
let data = try parser.produceUnversionedPayload(overrides)

try SettingsManager.store.write(data, for: .ipOverrides)
}

private func makeParser() -> SettingsParser {
SettingsParser(decoder: JSONDecoder(), encoder: JSONEncoder())
}
}
1 change: 1 addition & 0 deletions ios/MullvadSettings/SettingsStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ public enum SettingsKey: String, CaseIterable {
case settings = "Settings"
case deviceState = "DeviceState"
case apiAccessMethods = "ApiAccessMethods"
case ipOverrides = "IPOverrides"
case lastUsedAccount = "LastUsedAccount"
case shouldWipeSettings = "ShouldWipeSettings"
}
Expand Down
28 changes: 28 additions & 0 deletions ios/MullvadVPN.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -507,6 +507,13 @@
7A5869972B32EA4500640D27 /* AppButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A5869962B32EA4500640D27 /* AppButton.swift */; };
7A5869AB2B55527C00640D27 /* IPOverrideCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A5869AA2B55527C00640D27 /* IPOverrideCoordinator.swift */; };
7A5869AD2B5552E200640D27 /* IPOverrideViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A5869AC2B5552E200640D27 /* IPOverrideViewController.swift */; };
7A5869B72B56B41500640D27 /* IPOverrideTextViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A5869B62B56B41500640D27 /* IPOverrideTextViewController.swift */; };
7A5869B92B56E7F000640D27 /* IPOverrideViewControllerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A5869B82B56E7F000640D27 /* IPOverrideViewControllerDelegate.swift */; };
7A5869BC2B56EF3400640D27 /* IPOverrideRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A5869BA2B56EE9500640D27 /* IPOverrideRepository.swift */; };
7A5869BD2B56EF7300640D27 /* IPOverride.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A5869B22B5697AC00640D27 /* IPOverride.swift */; };
7A5869BF2B57D0A100640D27 /* IPOverrideStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A5869BE2B57D0A100640D27 /* IPOverrideStatus.swift */; };
7A5869C12B57D21A00640D27 /* IPOverrideStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A5869C02B57D21A00640D27 /* IPOverrideStatusView.swift */; };
7A5869C32B5820CE00640D27 /* IPOverrideRepositoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A5869C22B5820CE00640D27 /* IPOverrideRepositoryTests.swift */; };
7A6B4F592AB8412E00123853 /* TunnelMonitorTimings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6B4F582AB8412E00123853 /* TunnelMonitorTimings.swift */; };
7A6F2FA52AFA3CB2006D0856 /* AccountExpiryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6F2FA42AFA3CB2006D0856 /* AccountExpiryTests.swift */; };
7A6F2FA72AFBB9AE006D0856 /* AccountExpiry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6F2FA62AFBB9AE006D0856 /* AccountExpiry.swift */; };
Expand Down Expand Up @@ -1673,6 +1680,13 @@
7A5869962B32EA4500640D27 /* AppButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppButton.swift; sourceTree = "<group>"; };
7A5869AA2B55527C00640D27 /* IPOverrideCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IPOverrideCoordinator.swift; sourceTree = "<group>"; };
7A5869AC2B5552E200640D27 /* IPOverrideViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IPOverrideViewController.swift; sourceTree = "<group>"; };
7A5869B22B5697AC00640D27 /* IPOverride.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IPOverride.swift; sourceTree = "<group>"; };
7A5869B62B56B41500640D27 /* IPOverrideTextViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IPOverrideTextViewController.swift; sourceTree = "<group>"; };
7A5869B82B56E7F000640D27 /* IPOverrideViewControllerDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IPOverrideViewControllerDelegate.swift; sourceTree = "<group>"; };
7A5869BA2B56EE9500640D27 /* IPOverrideRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IPOverrideRepository.swift; sourceTree = "<group>"; };
7A5869BE2B57D0A100640D27 /* IPOverrideStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IPOverrideStatus.swift; sourceTree = "<group>"; };
7A5869C02B57D21A00640D27 /* IPOverrideStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IPOverrideStatusView.swift; sourceTree = "<group>"; };
7A5869C22B5820CE00640D27 /* IPOverrideRepositoryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IPOverrideRepositoryTests.swift; sourceTree = "<group>"; };
7A6B4F582AB8412E00123853 /* TunnelMonitorTimings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelMonitorTimings.swift; sourceTree = "<group>"; };
7A6F2FA42AFA3CB2006D0856 /* AccountExpiryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountExpiryTests.swift; sourceTree = "<group>"; };
7A6F2FA62AFBB9AE006D0856 /* AccountExpiry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountExpiry.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -2750,6 +2764,7 @@
58B0A2A4238EE67E00BC001D /* Info.plist */,
A9B6AC192ADE8FBB00F7802A /* InMemorySettingsStore.swift */,
F07BF2572A26112D00042943 /* InputTextFormatterTests.swift */,
7A5869C22B5820CE00640D27 /* IPOverrideRepositoryTests.swift */,
A9B6AC172ADE8F4300F7802A /* MigrationManagerTests.swift */,
58C3FA652A38549D006A450A /* MockFileCache.swift */,
F09D04B42AE93CB6003D4F89 /* OutgoingConnectionProxy+Stub.swift */,
Expand Down Expand Up @@ -2808,6 +2823,8 @@
F0164EBB2B482E430020268D /* AppStorage.swift */,
A92ECC2B2A7803A50052F1B1 /* DeviceState.swift */,
580F8B8528197958002E0998 /* DNSSettings.swift */,
7A5869B22B5697AC00640D27 /* IPOverride.swift */,
7A5869BA2B56EE9500640D27 /* IPOverrideRepository.swift */,
06410DFD292CE18F00AFC18C /* KeychainSettingsStore.swift */,
068CE5732927B7A400A068BB /* Migration.swift */,
A9D96B192A8247C100A5C673 /* MigrationManager.swift */,
Expand Down Expand Up @@ -3286,7 +3303,11 @@
isa = PBXGroup;
children = (
7A5869AA2B55527C00640D27 /* IPOverrideCoordinator.swift */,
7A5869BE2B57D0A100640D27 /* IPOverrideStatus.swift */,
7A5869C02B57D21A00640D27 /* IPOverrideStatusView.swift */,
7A5869B62B56B41500640D27 /* IPOverrideTextViewController.swift */,
7A5869AC2B5552E200640D27 /* IPOverrideViewController.swift */,
7A5869B82B56E7F000640D27 /* IPOverrideViewControllerDelegate.swift */,
);
path = IPOverride;
sourceTree = "<group>";
Expand Down Expand Up @@ -4499,6 +4520,7 @@
A9A5FA402ACB05D90083449F /* DeviceCheckRemoteServiceProtocol.swift in Sources */,
A9A5FA412ACB05D90083449F /* DeviceStateAccessor.swift in Sources */,
A9A5FA422ACB05D90083449F /* DeviceStateAccessorProtocol.swift in Sources */,
7A5869C32B5820CE00640D27 /* IPOverrideRepositoryTests.swift in Sources */,
A9A5FA392ACB05910083449F /* UIColor+Palette.swift in Sources */,
A9A5FA3A2ACB05910083449F /* UIEdgeInsets+Extensions.swift in Sources */,
A9C342C52ACC42130045F00E /* ServerRelaysResponse+Stubs.swift in Sources */,
Expand Down Expand Up @@ -4621,6 +4643,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
7A5869BD2B56EF7300640D27 /* IPOverride.swift in Sources */,
58B2FDEE2AA72098003EB5C6 /* ApplicationConfiguration.swift in Sources */,
58B2FDE52AA71D5C003EB5C6 /* TunnelSettingsV2.swift in Sources */,
A97D30172AE6B5E90045C0E4 /* StoredWgKeyData.swift in Sources */,
Expand All @@ -4644,6 +4667,7 @@
F08827892B3192110020A383 /* AccessMethodRepositoryProtocol.swift in Sources */,
58B2FDE22AA71D5C003EB5C6 /* StoredAccountData.swift in Sources */,
F0D7FF902B31E00B00E0FDE5 /* AccessMethodKind.swift in Sources */,
7A5869BC2B56EF3400640D27 /* IPOverrideRepository.swift in Sources */,
58B2FDE82AA71D5C003EB5C6 /* KeychainSettingsStore.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand Down Expand Up @@ -4956,6 +4980,7 @@
58C8191829FAA2C400DEB1B4 /* NotificationConfiguration.swift in Sources */,
58FF9FE82B07650A00E4C97D /* ButtonCellContentConfiguration.swift in Sources */,
5827B0A82B0F49EF00CCBBA1 /* ProxyConfigurationInteractorProtocol.swift in Sources */,
7A5869B92B56E7F000640D27 /* IPOverrideViewControllerDelegate.swift in Sources */,
586C0D7A2B039CE300E7CDD7 /* ShadowsocksCipherPicker.swift in Sources */,
58EFC76A2AFAC3B800E9F4CB /* ListAccessMethodHeaderView.swift in Sources */,
58EFC7732AFB471500E9F4CB /* AddAccessMethodViewController.swift in Sources */,
Expand Down Expand Up @@ -5002,6 +5027,7 @@
7AF10EB42ADE85BC00C090B9 /* RelayFilterCoordinator.swift in Sources */,
58FB865526E8BF3100F188BC /* StorePaymentManagerError.swift in Sources */,
F09D04B32AE919AC003D4F89 /* OutgoingConnectionProxy.swift in Sources */,
7A5869BF2B57D0A100640D27 /* IPOverrideStatus.swift in Sources */,
58FD5BF42428C67600112C88 /* InAppPurchaseButton.swift in Sources */,
7AF10EB22ADE859200C090B9 /* AlertViewController.swift in Sources */,
587D9676288989DB00CD8F1C /* NSLayoutConstraint+Helpers.swift in Sources */,
Expand All @@ -5026,6 +5052,7 @@
5871167F2910035700D41AAC /* PreferencesInteractor.swift in Sources */,
7A9CCCC22A96302800DD6A34 /* SafariCoordinator.swift in Sources */,
58CEB3082AFD484100E6E088 /* BasicCell.swift in Sources */,
7A5869C12B57D21A00640D27 /* IPOverrideStatusView.swift in Sources */,
58CEB2F52AFD0BB500E6E088 /* TextCellContentConfiguration.swift in Sources */,
58E20771274672CA00DE5D77 /* LaunchViewController.swift in Sources */,
F0E8CC032A4C753B007ED3B4 /* WelcomeViewController.swift in Sources */,
Expand All @@ -5046,6 +5073,7 @@
585B1FF02AB09F97008AD470 /* VPNConnectionProtocol.swift in Sources */,
58C3A4B222456F1B00340BDB /* AccountInputGroupView.swift in Sources */,
F09A297C2A9F8A9B00EA3B6F /* VoucherTextField.swift in Sources */,
7A5869B72B56B41500640D27 /* IPOverrideTextViewController.swift in Sources */,
58ACF64B26553C3F00ACE4B7 /* SettingsSwitchCell.swift in Sources */,
7AF9BE952A40461100DBFEDB /* RelayFilterView.swift in Sources */,
7A09C98129D99215000C2CAC /* String+FuzzyMatch.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,107 @@
//

import MullvadSettings
import MullvadTypes
import Routing
import UIKit

class IPOverrideCoordinator: Coordinator, Presenting, SettingsChildCoordinator {
let navigationController: UINavigationController
let repository: IPOverrideRepositoryProtocol

lazy var ipOverrideViewController: IPOverrideViewController = {
let viewController = IPOverrideViewController(alertPresenter: AlertPresenter(context: self))
viewController.delegate = self
return viewController
}()

var presentationContext: UIViewController {
navigationController
}

init(navigationController: UINavigationController) {
init(navigationController: UINavigationController, repository: IPOverrideRepositoryProtocol) {
self.navigationController = navigationController
self.repository = repository
}

func start(animated: Bool) {
let viewController = IPOverrideViewController(alertPresenter: AlertPresenter(context: self))
navigationController.pushViewController(viewController, animated: animated)
navigationController.pushViewController(ipOverrideViewController, animated: animated)
resetToDefaultStatus()
}

private func showImportTextView() {
let viewController = IPOverrideTextViewController()
let customNavigationController = CustomNavigationController(rootViewController: viewController)

viewController.didFinishEditing = { [weak self] text in
if let data = text.data(using: .utf8) {
self?.handleImport(of: data, context: .text)
} else {
self?.ipOverrideViewController.setStatus(.importFailed(.text))
print("Could not convert string to data: \(text)")
}
}

presentationContext.present(customNavigationController, animated: true)
}

private func showImportFileView() {
let documentPicker = UIDocumentPickerViewController(forOpeningContentTypes: [.json, .text])
documentPicker.delegate = self

presentationContext.present(documentPicker, animated: true)
}

private func resetToDefaultStatus(delay: Duration = .zero) {
DispatchQueue.main.asyncAfter(deadline: .now() + delay.timeInterval) { [weak self] in
if self?.repository.fetchAll().isEmpty == false {
self?.ipOverrideViewController.setStatus(.active)
} else {
self?.ipOverrideViewController.setStatus(.noImports)
}
}
}

private func handleImport(of data: Data, context: IPOverrideStatus.Context) {
do {
let overrides = try repository.parseData(data)

repository.add(overrides)
ipOverrideViewController.setStatus(.importSuccessful(context))
} catch {
ipOverrideViewController.setStatus(.importFailed(context))
print("Error importing ip overrides: \(error)")
}

resetToDefaultStatus(delay: .seconds(10))
}
}

extension IPOverrideCoordinator: IPOverrideViewControllerDelegate {
func controllerShouldShowTextImportView(_ controller: IPOverrideViewController) {
showImportTextView()
}

func controllerShouldShowFileImportView(_ controller: IPOverrideViewController) {
showImportFileView()
}

func controllerShouldClearAllOverrides(_ controller: IPOverrideViewController) {
repository.deleteAll()
resetToDefaultStatus()
}
}

extension IPOverrideCoordinator: UIDocumentPickerDelegate {
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
if let url = urls.first {
do {
let data = try Data(contentsOf: url)
handleImport(of: data, context: .file)
} catch {
ipOverrideViewController.setStatus(.importFailed(.file))
print("Could not convert file at url to data: \(url)")
}
}
}
}
Loading

0 comments on commit 0c68536

Please sign in to comment.