Skip to content

Commit b97f725

Browse files
author
Jon Petersson
committed
Allow users to import settings by pasting JSON blobs
1 parent 3a5ba4a commit b97f725

14 files changed

+694
-25
lines changed

ios/MullvadSettings/IPOverride.swift

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
//
2+
// IPOverride.swift
3+
// MullvadVPN
4+
//
5+
// Created by Jon Petersson on 2024-01-16.
6+
// Copyright © 2024 Mullvad VPN AB. All rights reserved.
7+
//
8+
9+
import Network
10+
11+
public struct RelayOverrides: Codable {
12+
public let overrides: [IPOverride]
13+
14+
private enum CodingKeys: String, CodingKey {
15+
case overrides = "relay_overrides"
16+
}
17+
}
18+
19+
public struct IPOverride: Codable, Equatable {
20+
public let hostname: String
21+
public var ipv4Address: IPv4Address?
22+
public var ipv6Address: IPv6Address?
23+
24+
private enum CodingKeys: String, CodingKey {
25+
case hostname
26+
case ipv4Address = "ipv4_addr_in"
27+
case ipv6Address = "ipv6_addr_in"
28+
}
29+
30+
init(hostname: String, ipv4Address: IPv4Address?, ipv6Address: IPv6Address?) throws {
31+
self.hostname = hostname
32+
self.ipv4Address = ipv4Address
33+
self.ipv6Address = ipv6Address
34+
35+
if self.ipv4Address.isNil && self.ipv6Address.isNil {
36+
throw NSError(domain: "IPOverrideInitDomain", code: NSFormattingError)
37+
}
38+
}
39+
40+
public init(from decoder: Decoder) throws {
41+
let container = try decoder.container(keyedBy: CodingKeys.self)
42+
43+
self.hostname = try container.decode(String.self, forKey: .hostname)
44+
self.ipv4Address = try container.decodeIfPresent(IPv4Address.self, forKey: .ipv4Address)
45+
self.ipv6Address = try container.decodeIfPresent(IPv6Address.self, forKey: .ipv6Address)
46+
47+
if self.ipv4Address.isNil && self.ipv6Address.isNil {
48+
throw NSError(domain: "IPOverrideInitDomain", code: NSFormattingError)
49+
}
50+
}
51+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
//
2+
// IPOverrideRepository.swift
3+
// MullvadVPN
4+
//
5+
// Created by Jon Petersson on 2024-01-16.
6+
// Copyright © 2024 Mullvad VPN AB. All rights reserved.
7+
//
8+
9+
import Foundation
10+
11+
public protocol IPOverrideRepositoryProtocol {
12+
func add(_ overrides: [IPOverride])
13+
func fetchAll() -> [IPOverride]
14+
func fetchByHostname(_ hostname: String) -> IPOverride?
15+
func deleteAll()
16+
func parseData(_ data: Data) throws -> [IPOverride]
17+
}
18+
19+
public class IPOverrideRepository: IPOverrideRepositoryProtocol {
20+
public init() {}
21+
22+
public func add(_ overrides: [IPOverride]) {
23+
var storedOverrides = fetchAll()
24+
25+
overrides.forEach { override in
26+
if let existingOverrideIndex = storedOverrides.firstIndex(where: { $0.hostname == override.hostname }) {
27+
var existingOverride = storedOverrides[existingOverrideIndex]
28+
29+
if let ipv4Address = override.ipv4Address {
30+
existingOverride.ipv4Address = ipv4Address
31+
}
32+
33+
if let ipv6Address = override.ipv6Address {
34+
existingOverride.ipv6Address = ipv6Address
35+
}
36+
37+
storedOverrides[existingOverrideIndex] = existingOverride
38+
} else {
39+
storedOverrides.append(override)
40+
}
41+
}
42+
43+
do {
44+
try writeIpOverrides(storedOverrides)
45+
} catch {
46+
print("Could not add override(s): \(overrides) \nError: \(error)")
47+
}
48+
}
49+
50+
public func fetchAll() -> [IPOverride] {
51+
return (try? readIpOverrides()) ?? []
52+
}
53+
54+
public func fetchByHostname(_ hostname: String) -> IPOverride? {
55+
return fetchAll().first { $0.hostname == hostname }
56+
}
57+
58+
public func deleteAll() {
59+
do {
60+
try SettingsManager.store.delete(key: .ipOverrides)
61+
} catch {
62+
print("Could not delete all overrides. \nError: \(error)")
63+
}
64+
}
65+
66+
public func parseData(_ data: Data) throws -> [IPOverride] {
67+
let decoder = JSONDecoder()
68+
let jsonData = try decoder.decode(RelayOverrides.self, from: data)
69+
70+
return jsonData.overrides
71+
}
72+
73+
private func readIpOverrides() throws -> [IPOverride] {
74+
let parser = makeParser()
75+
let data = try SettingsManager.store.read(key: .ipOverrides)
76+
77+
return try parser.parseUnversionedPayload(as: [IPOverride].self, from: data)
78+
}
79+
80+
private func writeIpOverrides(_ overrides: [IPOverride]) throws {
81+
let parser = makeParser()
82+
let data = try parser.produceUnversionedPayload(overrides)
83+
84+
try SettingsManager.store.write(data, for: .ipOverrides)
85+
}
86+
87+
private func makeParser() -> SettingsParser {
88+
SettingsParser(decoder: JSONDecoder(), encoder: JSONEncoder())
89+
}
90+
}

ios/MullvadSettings/SettingsStore.swift

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ public enum SettingsKey: String, CaseIterable {
1212
case settings = "Settings"
1313
case deviceState = "DeviceState"
1414
case apiAccessMethods = "ApiAccessMethods"
15+
case ipOverrides = "IPOverrides"
1516
case lastUsedAccount = "LastUsedAccount"
1617
case shouldWipeSettings = "ShouldWipeSettings"
1718
}

ios/MullvadVPN.xcodeproj/project.pbxproj

+28
Original file line numberDiff line numberDiff line change
@@ -507,6 +507,13 @@
507507
7A5869972B32EA4500640D27 /* AppButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A5869962B32EA4500640D27 /* AppButton.swift */; };
508508
7A5869AB2B55527C00640D27 /* IPOverrideCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A5869AA2B55527C00640D27 /* IPOverrideCoordinator.swift */; };
509509
7A5869AD2B5552E200640D27 /* IPOverrideViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A5869AC2B5552E200640D27 /* IPOverrideViewController.swift */; };
510+
7A5869B72B56B41500640D27 /* IPOverrideTextViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A5869B62B56B41500640D27 /* IPOverrideTextViewController.swift */; };
511+
7A5869B92B56E7F000640D27 /* IPOverrideViewControllerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A5869B82B56E7F000640D27 /* IPOverrideViewControllerDelegate.swift */; };
512+
7A5869BC2B56EF3400640D27 /* IPOverrideRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A5869BA2B56EE9500640D27 /* IPOverrideRepository.swift */; };
513+
7A5869BD2B56EF7300640D27 /* IPOverride.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A5869B22B5697AC00640D27 /* IPOverride.swift */; };
514+
7A5869BF2B57D0A100640D27 /* IPOverrideStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A5869BE2B57D0A100640D27 /* IPOverrideStatus.swift */; };
515+
7A5869C12B57D21A00640D27 /* IPOverrideStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A5869C02B57D21A00640D27 /* IPOverrideStatusView.swift */; };
516+
7A5869C32B5820CE00640D27 /* IPOverrideRepositoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A5869C22B5820CE00640D27 /* IPOverrideRepositoryTests.swift */; };
510517
7A6B4F592AB8412E00123853 /* TunnelMonitorTimings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6B4F582AB8412E00123853 /* TunnelMonitorTimings.swift */; };
511518
7A6F2FA52AFA3CB2006D0856 /* AccountExpiryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6F2FA42AFA3CB2006D0856 /* AccountExpiryTests.swift */; };
512519
7A6F2FA72AFBB9AE006D0856 /* AccountExpiry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6F2FA62AFBB9AE006D0856 /* AccountExpiry.swift */; };
@@ -1673,6 +1680,13 @@
16731680
7A5869962B32EA4500640D27 /* AppButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppButton.swift; sourceTree = "<group>"; };
16741681
7A5869AA2B55527C00640D27 /* IPOverrideCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IPOverrideCoordinator.swift; sourceTree = "<group>"; };
16751682
7A5869AC2B5552E200640D27 /* IPOverrideViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IPOverrideViewController.swift; sourceTree = "<group>"; };
1683+
7A5869B22B5697AC00640D27 /* IPOverride.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IPOverride.swift; sourceTree = "<group>"; };
1684+
7A5869B62B56B41500640D27 /* IPOverrideTextViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IPOverrideTextViewController.swift; sourceTree = "<group>"; };
1685+
7A5869B82B56E7F000640D27 /* IPOverrideViewControllerDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IPOverrideViewControllerDelegate.swift; sourceTree = "<group>"; };
1686+
7A5869BA2B56EE9500640D27 /* IPOverrideRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IPOverrideRepository.swift; sourceTree = "<group>"; };
1687+
7A5869BE2B57D0A100640D27 /* IPOverrideStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IPOverrideStatus.swift; sourceTree = "<group>"; };
1688+
7A5869C02B57D21A00640D27 /* IPOverrideStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IPOverrideStatusView.swift; sourceTree = "<group>"; };
1689+
7A5869C22B5820CE00640D27 /* IPOverrideRepositoryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IPOverrideRepositoryTests.swift; sourceTree = "<group>"; };
16761690
7A6B4F582AB8412E00123853 /* TunnelMonitorTimings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelMonitorTimings.swift; sourceTree = "<group>"; };
16771691
7A6F2FA42AFA3CB2006D0856 /* AccountExpiryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountExpiryTests.swift; sourceTree = "<group>"; };
16781692
7A6F2FA62AFBB9AE006D0856 /* AccountExpiry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountExpiry.swift; sourceTree = "<group>"; };
@@ -2750,6 +2764,7 @@
27502764
58B0A2A4238EE67E00BC001D /* Info.plist */,
27512765
A9B6AC192ADE8FBB00F7802A /* InMemorySettingsStore.swift */,
27522766
F07BF2572A26112D00042943 /* InputTextFormatterTests.swift */,
2767+
7A5869C22B5820CE00640D27 /* IPOverrideRepositoryTests.swift */,
27532768
A9B6AC172ADE8F4300F7802A /* MigrationManagerTests.swift */,
27542769
58C3FA652A38549D006A450A /* MockFileCache.swift */,
27552770
F09D04B42AE93CB6003D4F89 /* OutgoingConnectionProxy+Stub.swift */,
@@ -2808,6 +2823,8 @@
28082823
F0164EBB2B482E430020268D /* AppStorage.swift */,
28092824
A92ECC2B2A7803A50052F1B1 /* DeviceState.swift */,
28102825
580F8B8528197958002E0998 /* DNSSettings.swift */,
2826+
7A5869B22B5697AC00640D27 /* IPOverride.swift */,
2827+
7A5869BA2B56EE9500640D27 /* IPOverrideRepository.swift */,
28112828
06410DFD292CE18F00AFC18C /* KeychainSettingsStore.swift */,
28122829
068CE5732927B7A400A068BB /* Migration.swift */,
28132830
A9D96B192A8247C100A5C673 /* MigrationManager.swift */,
@@ -3286,7 +3303,11 @@
32863303
isa = PBXGroup;
32873304
children = (
32883305
7A5869AA2B55527C00640D27 /* IPOverrideCoordinator.swift */,
3306+
7A5869BE2B57D0A100640D27 /* IPOverrideStatus.swift */,
3307+
7A5869C02B57D21A00640D27 /* IPOverrideStatusView.swift */,
3308+
7A5869B62B56B41500640D27 /* IPOverrideTextViewController.swift */,
32893309
7A5869AC2B5552E200640D27 /* IPOverrideViewController.swift */,
3310+
7A5869B82B56E7F000640D27 /* IPOverrideViewControllerDelegate.swift */,
32903311
);
32913312
path = IPOverride;
32923313
sourceTree = "<group>";
@@ -4499,6 +4520,7 @@
44994520
A9A5FA402ACB05D90083449F /* DeviceCheckRemoteServiceProtocol.swift in Sources */,
45004521
A9A5FA412ACB05D90083449F /* DeviceStateAccessor.swift in Sources */,
45014522
A9A5FA422ACB05D90083449F /* DeviceStateAccessorProtocol.swift in Sources */,
4523+
7A5869C32B5820CE00640D27 /* IPOverrideRepositoryTests.swift in Sources */,
45024524
A9A5FA392ACB05910083449F /* UIColor+Palette.swift in Sources */,
45034525
A9A5FA3A2ACB05910083449F /* UIEdgeInsets+Extensions.swift in Sources */,
45044526
A9C342C52ACC42130045F00E /* ServerRelaysResponse+Stubs.swift in Sources */,
@@ -4621,6 +4643,7 @@
46214643
isa = PBXSourcesBuildPhase;
46224644
buildActionMask = 2147483647;
46234645
files = (
4646+
7A5869BD2B56EF7300640D27 /* IPOverride.swift in Sources */,
46244647
58B2FDEE2AA72098003EB5C6 /* ApplicationConfiguration.swift in Sources */,
46254648
58B2FDE52AA71D5C003EB5C6 /* TunnelSettingsV2.swift in Sources */,
46264649
A97D30172AE6B5E90045C0E4 /* StoredWgKeyData.swift in Sources */,
@@ -4644,6 +4667,7 @@
46444667
F08827892B3192110020A383 /* AccessMethodRepositoryProtocol.swift in Sources */,
46454668
58B2FDE22AA71D5C003EB5C6 /* StoredAccountData.swift in Sources */,
46464669
F0D7FF902B31E00B00E0FDE5 /* AccessMethodKind.swift in Sources */,
4670+
7A5869BC2B56EF3400640D27 /* IPOverrideRepository.swift in Sources */,
46474671
58B2FDE82AA71D5C003EB5C6 /* KeychainSettingsStore.swift in Sources */,
46484672
);
46494673
runOnlyForDeploymentPostprocessing = 0;
@@ -4956,6 +4980,7 @@
49564980
58C8191829FAA2C400DEB1B4 /* NotificationConfiguration.swift in Sources */,
49574981
58FF9FE82B07650A00E4C97D /* ButtonCellContentConfiguration.swift in Sources */,
49584982
5827B0A82B0F49EF00CCBBA1 /* ProxyConfigurationInteractorProtocol.swift in Sources */,
4983+
7A5869B92B56E7F000640D27 /* IPOverrideViewControllerDelegate.swift in Sources */,
49594984
586C0D7A2B039CE300E7CDD7 /* ShadowsocksCipherPicker.swift in Sources */,
49604985
58EFC76A2AFAC3B800E9F4CB /* ListAccessMethodHeaderView.swift in Sources */,
49614986
58EFC7732AFB471500E9F4CB /* AddAccessMethodViewController.swift in Sources */,
@@ -5002,6 +5027,7 @@
50025027
7AF10EB42ADE85BC00C090B9 /* RelayFilterCoordinator.swift in Sources */,
50035028
58FB865526E8BF3100F188BC /* StorePaymentManagerError.swift in Sources */,
50045029
F09D04B32AE919AC003D4F89 /* OutgoingConnectionProxy.swift in Sources */,
5030+
7A5869BF2B57D0A100640D27 /* IPOverrideStatus.swift in Sources */,
50055031
58FD5BF42428C67600112C88 /* InAppPurchaseButton.swift in Sources */,
50065032
7AF10EB22ADE859200C090B9 /* AlertViewController.swift in Sources */,
50075033
587D9676288989DB00CD8F1C /* NSLayoutConstraint+Helpers.swift in Sources */,
@@ -5026,6 +5052,7 @@
50265052
5871167F2910035700D41AAC /* PreferencesInteractor.swift in Sources */,
50275053
7A9CCCC22A96302800DD6A34 /* SafariCoordinator.swift in Sources */,
50285054
58CEB3082AFD484100E6E088 /* BasicCell.swift in Sources */,
5055+
7A5869C12B57D21A00640D27 /* IPOverrideStatusView.swift in Sources */,
50295056
58CEB2F52AFD0BB500E6E088 /* TextCellContentConfiguration.swift in Sources */,
50305057
58E20771274672CA00DE5D77 /* LaunchViewController.swift in Sources */,
50315058
F0E8CC032A4C753B007ED3B4 /* WelcomeViewController.swift in Sources */,
@@ -5046,6 +5073,7 @@
50465073
585B1FF02AB09F97008AD470 /* VPNConnectionProtocol.swift in Sources */,
50475074
58C3A4B222456F1B00340BDB /* AccountInputGroupView.swift in Sources */,
50485075
F09A297C2A9F8A9B00EA3B6F /* VoucherTextField.swift in Sources */,
5076+
7A5869B72B56B41500640D27 /* IPOverrideTextViewController.swift in Sources */,
50495077
58ACF64B26553C3F00ACE4B7 /* SettingsSwitchCell.swift in Sources */,
50505078
7AF9BE952A40461100DBFEDB /* RelayFilterView.swift in Sources */,
50515079
7A09C98129D99215000C2CAC /* String+FuzzyMatch.swift in Sources */,

ios/MullvadVPN/Coordinators/Settings/IPOverride/IPOverrideCoordinator.swift

+88-3
Original file line numberDiff line numberDiff line change
@@ -7,22 +7,107 @@
77
//
88

99
import MullvadSettings
10+
import MullvadTypes
1011
import Routing
1112
import UIKit
1213

1314
class IPOverrideCoordinator: Coordinator, Presenting, SettingsChildCoordinator {
1415
let navigationController: UINavigationController
16+
let repository: IPOverrideRepositoryProtocol
17+
18+
lazy var ipOverrideViewController: IPOverrideViewController = {
19+
let viewController = IPOverrideViewController(alertPresenter: AlertPresenter(context: self))
20+
viewController.delegate = self
21+
return viewController
22+
}()
1523

1624
var presentationContext: UIViewController {
1725
navigationController
1826
}
1927

20-
init(navigationController: UINavigationController) {
28+
init(navigationController: UINavigationController, repository: IPOverrideRepositoryProtocol) {
2129
self.navigationController = navigationController
30+
self.repository = repository
2231
}
2332

2433
func start(animated: Bool) {
25-
let viewController = IPOverrideViewController(alertPresenter: AlertPresenter(context: self))
26-
navigationController.pushViewController(viewController, animated: animated)
34+
navigationController.pushViewController(ipOverrideViewController, animated: animated)
35+
resetToDefaultStatus()
36+
}
37+
38+
private func showImportTextView() {
39+
let viewController = IPOverrideTextViewController()
40+
let customNavigationController = CustomNavigationController(rootViewController: viewController)
41+
42+
viewController.didFinishEditing = { [weak self] text in
43+
if let data = text.data(using: .utf8) {
44+
self?.handleImport(of: data, context: .text)
45+
} else {
46+
self?.ipOverrideViewController.setStatus(.importFailed(.text))
47+
print("Could not convert string to data: \(text)")
48+
}
49+
}
50+
51+
presentationContext.present(customNavigationController, animated: true)
52+
}
53+
54+
private func showImportFileView() {
55+
let documentPicker = UIDocumentPickerViewController(forOpeningContentTypes: [.json, .text])
56+
documentPicker.delegate = self
57+
58+
presentationContext.present(documentPicker, animated: true)
59+
}
60+
61+
private func resetToDefaultStatus(delay: Duration = .zero) {
62+
DispatchQueue.main.asyncAfter(deadline: .now() + delay.timeInterval) { [weak self] in
63+
if self?.repository.fetchAll().isEmpty == false {
64+
self?.ipOverrideViewController.setStatus(.active)
65+
} else {
66+
self?.ipOverrideViewController.setStatus(.noImports)
67+
}
68+
}
69+
}
70+
71+
private func handleImport(of data: Data, context: IPOverrideStatus.Context) {
72+
do {
73+
let overrides = try repository.parseData(data)
74+
75+
repository.add(overrides)
76+
ipOverrideViewController.setStatus(.importSuccessful(context))
77+
} catch {
78+
ipOverrideViewController.setStatus(.importFailed(context))
79+
print("Error importing ip overrides: \(error)")
80+
}
81+
82+
resetToDefaultStatus(delay: .seconds(10))
83+
}
84+
}
85+
86+
extension IPOverrideCoordinator: IPOverrideViewControllerDelegate {
87+
func controllerShouldShowTextImportView(_ controller: IPOverrideViewController) {
88+
showImportTextView()
89+
}
90+
91+
func controllerShouldShowFileImportView(_ controller: IPOverrideViewController) {
92+
showImportFileView()
93+
}
94+
95+
func controllerShouldClearAllOverrides(_ controller: IPOverrideViewController) {
96+
repository.deleteAll()
97+
resetToDefaultStatus()
98+
}
99+
}
100+
101+
extension IPOverrideCoordinator: UIDocumentPickerDelegate {
102+
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
103+
if let url = urls.first {
104+
do {
105+
let data = try Data(contentsOf: url)
106+
handleImport(of: data, context: .file)
107+
} catch {
108+
ipOverrideViewController.setStatus(.importFailed(.file))
109+
print("Could not convert file at url to data: \(url)")
110+
}
111+
}
27112
}
28113
}

0 commit comments

Comments
 (0)