Skip to content

Commit 5e30b84

Browse files
committed
Adding proxy server to macOS app
1 parent 7d72fa1 commit 5e30b84

File tree

9 files changed

+404
-80
lines changed

9 files changed

+404
-80
lines changed

OpenHaystack/OpenHaystack.xcodeproj/project.pbxproj

+4
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
objects = {
88

99
/* Begin PBXBuildFile section */
10+
377841A52B929D650083F97A /* APISource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 377841A42B929D650083F97A /* APISource.swift */; };
1011
5A2C9089273425720044407E /* NRF in Resources */ = {isa = PBXBuildFile; fileRef = 5A2C9088273425720044407E /* NRF */; };
1112
5A2C908B2734266A0044407E /* DataToHexExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A2C908A2734266A0044407E /* DataToHexExtension.swift */; };
1213
5A2C908D273429360044407E /* NRFController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A2C908C273429360044407E /* NRFController.swift */; };
@@ -114,6 +115,7 @@
114115
025DFEDB248FED250039C718 /* DecryptReports.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecryptReports.swift; sourceTree = "<group>"; };
115116
0298C0C8248F9506003928FE /* AuthKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AuthKit.framework; path = ../../../../../../../../../../System/Library/PrivateFrameworks/AuthKit.framework; sourceTree = "<group>"; };
116117
116B4EEC24A913AA007BA636 /* SavePanel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SavePanel.swift; sourceTree = "<group>"; };
118+
377841A42B929D650083F97A /* APISource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APISource.swift; sourceTree = "<group>"; };
117119
5A2C9088273425720044407E /* NRF */ = {isa = PBXFileReference; lastKnownFileType = folder; path = NRF; sourceTree = "<group>"; };
118120
5A2C908A2734266A0044407E /* DataToHexExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataToHexExtension.swift; sourceTree = "<group>"; };
119121
5A2C908C273429360044407E /* NRFController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NRFController.swift; sourceTree = "<group>"; };
@@ -372,6 +374,7 @@
372374
children = (
373375
78EC227125DBC8CE0042B775 /* Accessory.swift */,
374376
78286D8B25E5355B00F65511 /* PreviewData.swift */,
377+
377841A42B929D650083F97A /* APISource.swift */,
375378
);
376379
path = Model;
377380
sourceTree = "<group>";
@@ -627,6 +630,7 @@
627630
7899D1E125DE97E200115740 /* IconSelectionView.swift in Sources */,
628631
5A2C908F273429540044407E /* NRFInstallSheet.swift in Sources */,
629632
78EC227725DBDB7E0042B775 /* KeychainController.swift in Sources */,
633+
377841A52B929D650083F97A /* APISource.swift in Sources */,
630634
78D9B80625F7CF60009B9CE8 /* ManageAccessoriesView.swift in Sources */,
631635
78486BEF25DD711E0007ED87 /* PopUpAlertView.swift in Sources */,
632636
78014A2925DC08580089F6D9 /* MicrobitController.swift in Sources */,

OpenHaystack/OpenHaystack/AnisetteDataManager.swift

+2-2
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ public class AnisetteDataManager: NSObject {
5151
}
5252

5353
func requestAnisetteDataAuthKit() -> AppleAccountData? {
54-
let anisetteData = ReportsFetcher().anisetteDataDictionary()
54+
let anisetteData = AnisetteDependentReportsFetcher().anisetteDataDictionary()
5555

5656
let dateFormatter = ISO8601DateFormatter()
5757

@@ -79,7 +79,7 @@ public class AnisetteDataManager: NSObject {
7979
locale: Locale.current,
8080
timeZone: TimeZone.current)
8181

82-
if let spToken = ReportsFetcher().fetchSearchpartyToken() {
82+
if let spToken = AnisetteDependentReportsFetcher().fetchSearchpartyToken() {
8383
accountData.searchPartyToken = spToken
8484
}
8585

OpenHaystack/OpenHaystack/FindMy/FindMyController.swift

+14-20
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,6 @@ class FindMyController: ObservableObject {
1616
@Published var error: Error?
1717
@Published var devices = [FindMyDevice]()
1818

19-
func loadPrivateKeys(from data: Data, with searchPartyToken: Data, completion: @escaping (Error?) -> Void) {
20-
do {
21-
let devices = try PropertyListDecoder().decode([FindMyDevice].self, from: data)
22-
23-
self.devices.append(contentsOf: devices)
24-
self.fetchReports(with: searchPartyToken, completion: completion)
25-
} catch {
26-
self.error = FindMyErrors.decodingPlistFailed(message: String(describing: error))
27-
}
28-
}
29-
3019
func importReports(reports: [FindMyReport], and keys: Data, completion: @escaping () -> Void) throws {
3120
let devices = try PropertyListDecoder().decode([FindMyDevice].self, from: keys)
3221
self.devices = devices
@@ -88,6 +77,14 @@ class FindMyController: ObservableObject {
8877
}
8978

9079
func fetchReports(for accessories: [Accessory], with token: Data, completion: @escaping (Result<[FindMyDevice], Error>) -> Void) {
80+
fetchReports(for: accessories, fetcher: AnisetteDependentReportsFetcher(searchPartyToken: token), completion: completion)
81+
}
82+
83+
func fetchReports(for accessories: [Accessory], with url: URL, authorizationHeader: String?, completion: @escaping (Result<[FindMyDevice], Error>) -> Void) {
84+
fetchReports(for: accessories, fetcher: ExternalReportsFetcher(serverUrl: url, authorizationHeader: authorizationHeader), completion: completion)
85+
}
86+
87+
private func fetchReports(for accessories: [Accessory], fetcher: ReportsFetcher, completion: @escaping (Result<[FindMyDevice], Error>) -> Void) {
9188
let findMyDevices = accessories.compactMap({ acc -> FindMyDevice? in
9289
do {
9390
return try acc.toFindMyDevice()
@@ -99,7 +96,7 @@ class FindMyController: ObservableObject {
9996

10097
self.devices = findMyDevices
10198

102-
self.fetchReports(with: token) { error in
99+
self.fetchReports(from: fetcher) { error in
103100

104101
if let error = error {
105102
completion(.failure(error))
@@ -109,18 +106,15 @@ class FindMyController: ObservableObject {
109106
}
110107
}
111108
}
112-
113-
func fetchReports(with searchPartyToken: Data, completion: @escaping (Error?) -> Void) {
114-
109+
110+
private func fetchReports(from fetcher: ReportsFetcher, completion: @escaping (Error?) -> Void) {
115111
DispatchQueue.global(qos: .background).async { [weak self] in
116112
guard let self = self else {
117113
completion(FindMyErrors.objectReleased)
118114
return
119115
}
120116
let fetchReportGroup = DispatchGroup()
121-
122-
let fetcher = ReportsFetcher()
123-
117+
124118
var devices = self.devices
125119
for deviceIndex in 0..<devices.count {
126120
fetchReportGroup.enter()
@@ -134,8 +128,8 @@ class FindMyController: ObservableObject {
134128
// 21 days
135129
let duration: Double = (24 * 60 * 60) * 21
136130
let startDate = Date() - duration
137-
138-
fetcher.query(forHashes: keyHashes, start: startDate, duration: duration, searchPartyToken: searchPartyToken) { jd in
131+
132+
fetcher.query(forHashes: keyHashes, start: startDate, duration: duration) { jd in
139133
guard let jsonData = jd else {
140134
fetchReportGroup.leave()
141135
return

OpenHaystack/OpenHaystack/HaystackApp/AccessoryController.swift

+54-27
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import SwiftUI
1414

1515
class AccessoryController: ObservableObject {
1616
@Published var accessories: [Accessory]
17+
@AppStorage(APISource.storageKey) var apiSource: APISource = .mailPlugin
18+
1719
var selfObserver: AnyCancellable?
1820
var listElementsObserver = [AnyCancellable]()
1921
let findMyController: FindMyController
@@ -235,39 +237,64 @@ class AccessoryController: ObservableObject {
235237
///
236238
/// - Parameter completion: called when the reports have been succesfully downloaded or the request has failed
237239
func downloadLocationReports(completion: @escaping (Result<Void, OpenHaystackMainView.AlertType>) -> Void) {
238-
AnisetteDataManager.shared.requestAnisetteData { [weak self] result in
239-
guard let self = self else {
240-
completion(.failure(.noReportsFound))
241-
return
242-
}
243-
switch result {
244-
case .failure(_):
245-
completion(.failure(.activatePlugin))
246-
case .success(let accountData):
247-
248-
guard let token = accountData.searchPartyToken,
249-
token.isEmpty == false
250-
else {
251-
completion(.failure(.searchPartyToken))
240+
switch apiSource {
241+
case .mailPlugin:
242+
AnisetteDataManager.shared.requestAnisetteData { [weak self] result in
243+
guard let self = self else {
244+
completion(.failure(.noReportsFound))
252245
return
253246
}
247+
switch result {
248+
case .failure(_):
249+
completion(.failure(.activatePlugin))
250+
case .success(let accountData):
251+
252+
guard let token = accountData.searchPartyToken,
253+
token.isEmpty == false
254+
else {
255+
completion(.failure(.searchPartyToken))
256+
return
257+
}
254258

255-
self.findMyController.fetchReports(for: self.accessories, with: token) { [weak self] result in
256-
switch result {
257-
case .failure(let error):
258-
os_log(.error, "Downloading reports failed %@", error.localizedDescription)
259-
completion(.failure(.downloadingReportsFailed))
260-
case .success(let devices):
261-
let reports = devices.compactMap({ $0.reports }).flatMap({ $0 })
262-
if reports.isEmpty {
263-
completion(.failure(.noReportsFound))
264-
} else {
265-
self?.updateWithDecryptedReports(devices: devices)
266-
completion(.success(()))
259+
self.findMyController.fetchReports(for: self.accessories, with: token) { [weak self] result in
260+
switch result {
261+
case .failure(let error):
262+
os_log(.error, "Downloading reports failed %@", error.localizedDescription)
263+
completion(.failure(.downloadingReportsFailed))
264+
case .success(let devices):
265+
let reports = devices.compactMap({ $0.reports }).flatMap({ $0 })
266+
if reports.isEmpty {
267+
completion(.failure(.noReportsFound))
268+
} else {
269+
self?.updateWithDecryptedReports(devices: devices)
270+
completion(.success(()))
271+
}
267272
}
268273
}
269-
}
270274

275+
}
276+
}
277+
case .reportsServer(let serverOptions):
278+
guard let url = serverOptions.url else {
279+
os_log(.error, "Downloading reports failed, no URL provided")
280+
completion(.failure(.downloadingReportsFailed))
281+
return
282+
}
283+
284+
self.findMyController.fetchReports(for: self.accessories, with: url, authorizationHeader: serverOptions.authorizationHeader) { [weak self] result in
285+
switch result {
286+
case .failure(let error):
287+
os_log(.error, "Downloading reports failed %@", error.localizedDescription)
288+
completion(.failure(.downloadingReportsFailed))
289+
case .success(let devices):
290+
let reports = devices.compactMap({ $0.reports }).flatMap({ $0 })
291+
if reports.isEmpty {
292+
completion(.failure(.noReportsFound))
293+
} else {
294+
self?.updateWithDecryptedReports(devices: devices)
295+
completion(.success(()))
296+
}
297+
}
271298
}
272299
}
273300
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
//
2+
// OpenHaystack – Tracking personal Bluetooth devices via Apple's Find My network
3+
//
4+
// Copyright © 2021 Secure Mobile Networking Lab (SEEMOO)
5+
// Copyright © 2021 The Open Wireless Link Project
6+
//
7+
// SPDX-License-Identifier: AGPL-3.0-only
8+
//
9+
10+
import Foundation
11+
12+
enum APISource {
13+
static let storageKey = "api_source"
14+
struct ServerOptions {
15+
var url: URL?
16+
var authorizationHeader: String?
17+
var isProtected: Bool { authorizationHeader != nil }
18+
}
19+
20+
case mailPlugin
21+
case reportsServer(ServerOptions)
22+
}
23+
24+
extension APISource: Equatable {
25+
static func == (lhs: Self, rhs: Self) -> Bool {
26+
switch (lhs, rhs) {
27+
case (.mailPlugin, .mailPlugin),
28+
(.reportsServer, .reportsServer):
29+
return true
30+
default:
31+
return false
32+
}
33+
}
34+
}
35+
36+
extension APISource: Hashable {
37+
private struct MailPluginHash: Hashable {}
38+
private struct ReportsServerHash: Hashable {}
39+
40+
func hash(into hasher: inout Hasher) {
41+
switch self {
42+
case .mailPlugin:
43+
hasher.combine(MailPluginHash())
44+
case .reportsServer:
45+
hasher.combine(ReportsServerHash())
46+
}
47+
}
48+
}
49+
50+
extension APISource: RawRepresentable {
51+
private static let separator: Character = "|"
52+
private static let mailPluginIdenitifier = "mailPlugin"
53+
private static let reportsServerIdentifier = "reportsServer"
54+
55+
init?(rawValue: String) {
56+
let components = rawValue.split(separator: APISource.separator)
57+
guard let rawType = components.first else { return nil }
58+
switch rawType {
59+
case APISource.mailPluginIdenitifier:
60+
self = .mailPlugin
61+
case APISource.reportsServerIdentifier where components.count == 1:
62+
self = .reportsServer(.init())
63+
case APISource.reportsServerIdentifier where components.count == 2:
64+
self = .reportsServer(.init(url: URL(string: String(components[1]))))
65+
case APISource.reportsServerIdentifier where components.count == 3:
66+
self = .reportsServer(.init(url: URL(string: String(components[1])), authorizationHeader: String(components[2])))
67+
default:
68+
return nil
69+
}
70+
}
71+
72+
var rawValue: String {
73+
switch self {
74+
case .mailPlugin:
75+
return APISource.mailPluginIdenitifier
76+
case .reportsServer(let serverOptions):
77+
var components: [String] = [APISource.reportsServerIdentifier]
78+
guard let url = serverOptions.url else { return components.joined(separator: String(APISource.separator)) }
79+
components.append(url.absoluteString)
80+
if let authorizationHeader = serverOptions.authorizationHeader {
81+
components.append(authorizationHeader)
82+
}
83+
return components.joined(separator: String(APISource.separator))
84+
}
85+
}
86+
}

0 commit comments

Comments
 (0)