From 428daac5b3f1337799650964510e85d734f4b51c Mon Sep 17 00:00:00 2001
From: Ryan ZHAO <>
Date: Tue, 21 Jul 2020 14:51:15 +1000
Subject: [PATCH] use onion routing for file server requests

---
 .../src/Loki/API/FileServerAPI.swift          | 106 +++++++++++-------
 .../OnionRequestAPI+Encryption.swift          |  21 ++--
 .../API/Onion Requests/OnionRequestAPI.swift  |  47 +++++++-
 3 files changed, 125 insertions(+), 49 deletions(-)

diff --git a/SignalServiceKit/src/Loki/API/FileServerAPI.swift b/SignalServiceKit/src/Loki/API/FileServerAPI.swift
index 1472c916a..a0772c3bc 100644
--- a/SignalServiceKit/src/Loki/API/FileServerAPI.swift
+++ b/SignalServiceKit/src/Loki/API/FileServerAPI.swift
@@ -44,52 +44,69 @@ public final class FileServerAPI : DotNetAPI {
     public static func getDeviceLinks(associatedWith hexEncodedPublicKeys: Set<String>) -> Promise<Set<DeviceLink>> {
         let hexEncodedPublicKeysDescription = "[ \(hexEncodedPublicKeys.joined(separator: ", ")) ]"
         print("[Loki] Getting device links for: \(hexEncodedPublicKeysDescription).")
+        
+        func handleRawResponseForDeviceLinks(rawResponse: JSON, data: [JSON]) -> Set<DeviceLink> {
+            return Set(data.flatMap { data -> [DeviceLink] in
+                guard let annotations = data["annotations"] as? [JSON], !annotations.isEmpty else { return [] }
+                guard let annotation = annotations.first(where: { $0["type"] as? String == deviceLinkType }),
+                    let value = annotation["value"] as? JSON, let rawDeviceLinks = value["authorisations"] as? [JSON],
+                    let hexEncodedPublicKey = data["username"] as? String else {
+                    print("[Loki] Couldn't parse device links from: \(rawResponse).")
+                    return []
+                }
+                return rawDeviceLinks.compactMap { rawDeviceLink in
+                    guard let masterHexEncodedPublicKey = rawDeviceLink["primaryDevicePubKey"] as? String, let slaveHexEncodedPublicKey = rawDeviceLink["secondaryDevicePubKey"] as? String,
+                        let base64EncodedSlaveSignature = rawDeviceLink["requestSignature"] as? String else {
+                        print("[Loki] Couldn't parse device link for user: \(hexEncodedPublicKey) from: \(rawResponse).")
+                        return nil
+                    }
+                    let masterSignature: Data?
+                    if let base64EncodedMasterSignature = rawDeviceLink["grantSignature"] as? String {
+                        masterSignature = Data(base64Encoded: base64EncodedMasterSignature)
+                    } else {
+                        masterSignature = nil
+                    }
+                    let slaveSignature = Data(base64Encoded: base64EncodedSlaveSignature)
+                    let master = DeviceLink.Device(hexEncodedPublicKey: masterHexEncodedPublicKey, signature: masterSignature)
+                    let slave = DeviceLink.Device(hexEncodedPublicKey: slaveHexEncodedPublicKey, signature: slaveSignature)
+                    let deviceLink = DeviceLink(between: master, and: slave)
+                    if let masterSignature = masterSignature {
+                        guard DeviceLinkingUtilities.hasValidMasterSignature(deviceLink) else {
+                            print("[Loki] Received a device link with an invalid master signature.")
+                            return nil
+                        }
+                    }
+                    guard DeviceLinkingUtilities.hasValidSlaveSignature(deviceLink) else {
+                        print("[Loki] Received a device link with an invalid slave signature.")
+                        return nil
+                    }
+                    return deviceLink
+                }
+            })
+        }
+        
         return getAuthToken(for: server).then2 { token -> Promise<Set<DeviceLink>> in
             let queryParameters = "ids=\(hexEncodedPublicKeys.map { "@\($0)" }.joined(separator: ","))&include_user_annotations=1"
             let url = URL(string: "\(server)/users?\(queryParameters)")!
             let request = TSRequest(url: url)
+            if (useOnionRequests) {
+                return OnionRequestAPI.sendOnionRequestFileServerDest(request, server: server, using: fileServerPublicKey).map2 { rawResponse -> Set<DeviceLink> in
+                    guard let data = rawResponse["data"] as? [JSON] else {
+                        print("[Loki] Couldn't parse device links for users: \(hexEncodedPublicKeys) from: \(rawResponse).")
+                        throw DotNetAPIError.parsingFailed
+                    }
+                    return handleRawResponseForDeviceLinks(rawResponse: rawResponse, data: data)
+                }.map2 { deviceLinks in
+                    storage.setDeviceLinks(deviceLinks)
+                    return deviceLinks
+                }
+            }
             return LokiFileServerProxy(for: server).perform(request, withCompletionQueue: DispatchQueue.global(qos: .default)).map2 { rawResponse -> Set<DeviceLink> in
                 guard let json = rawResponse as? JSON, let data = json["data"] as? [JSON] else {
                     print("[Loki] Couldn't parse device links for users: \(hexEncodedPublicKeys) from: \(rawResponse).")
                     throw DotNetAPIError.parsingFailed
                 }
-                return Set(data.flatMap { data -> [DeviceLink] in
-                    guard let annotations = data["annotations"] as? [JSON], !annotations.isEmpty else { return [] }
-                    guard let annotation = annotations.first(where: { $0["type"] as? String == deviceLinkType }),
-                        let value = annotation["value"] as? JSON, let rawDeviceLinks = value["authorisations"] as? [JSON],
-                        let hexEncodedPublicKey = data["username"] as? String else {
-                        print("[Loki] Couldn't parse device links from: \(rawResponse).")
-                        return []
-                    }
-                    return rawDeviceLinks.compactMap { rawDeviceLink in
-                        guard let masterHexEncodedPublicKey = rawDeviceLink["primaryDevicePubKey"] as? String, let slaveHexEncodedPublicKey = rawDeviceLink["secondaryDevicePubKey"] as? String,
-                            let base64EncodedSlaveSignature = rawDeviceLink["requestSignature"] as? String else {
-                            print("[Loki] Couldn't parse device link for user: \(hexEncodedPublicKey) from: \(rawResponse).")
-                            return nil
-                        }
-                        let masterSignature: Data?
-                        if let base64EncodedMasterSignature = rawDeviceLink["grantSignature"] as? String {
-                            masterSignature = Data(base64Encoded: base64EncodedMasterSignature)
-                        } else {
-                            masterSignature = nil
-                        }
-                        let slaveSignature = Data(base64Encoded: base64EncodedSlaveSignature)
-                        let master = DeviceLink.Device(hexEncodedPublicKey: masterHexEncodedPublicKey, signature: masterSignature)
-                        let slave = DeviceLink.Device(hexEncodedPublicKey: slaveHexEncodedPublicKey, signature: slaveSignature)
-                        let deviceLink = DeviceLink(between: master, and: slave)
-                        if let masterSignature = masterSignature {
-                            guard DeviceLinkingUtilities.hasValidMasterSignature(deviceLink) else {
-                                print("[Loki] Received a device link with an invalid master signature.")
-                                return nil
-                            }
-                        }
-                        guard DeviceLinkingUtilities.hasValidSlaveSignature(deviceLink) else {
-                            print("[Loki] Received a device link with an invalid slave signature.")
-                            return nil
-                        }
-                        return deviceLink
-                    }
-                })
+                return handleRawResponseForDeviceLinks(rawResponse: json, data: data)
             }.map2 { deviceLinks in
                 storage.setDeviceLinks(deviceLinks)
                 return deviceLinks
@@ -109,7 +126,10 @@ public final class FileServerAPI : DotNetAPI {
             let request = TSRequest(url: url, method: "PATCH", parameters: parameters)
             request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ]
             return attempt(maxRetryCount: 8, recoveringOn: SnodeAPI.workQueue) {
-                LokiFileServerProxy(for: server).perform(request).map2 { _ in }
+                if (useOnionRequests) {
+                    return OnionRequestAPI.sendOnionRequestFileServerDest(request, server: server, using: fileServerPublicKey).map2 { _ in }
+                }
+                return LokiFileServerProxy(for: server).perform(request).map2 { _ in }
             }.handlingInvalidAuthTokenIfNeeded(for: server).recover2 { error in
                 print("Couldn't update device links due to error: \(error).")
                 throw error
@@ -161,6 +181,16 @@ public final class FileServerAPI : DotNetAPI {
             print("[Loki] Couldn't upload profile picture due to error: \(error).")
             return Promise(error: error)
         }
+        if (useOnionRequests) {
+            return OnionRequestAPI.sendOnionRequestFileServerDest(request, server: server, using: fileServerPublicKey).map2 { json in
+                guard let data = json["data"] as? JSON, let downloadURL = data["url"] as? String else {
+                    print("[Loki] Couldn't parse profile picture from: \(json).")
+                    throw DotNetAPIError.parsingFailed
+                }
+                UserDefaults.standard[.lastProfilePictureUpload] = Date()
+                return downloadURL
+            }
+        }
         return LokiFileServerProxy(for: server).performLokiFileServerNSURLRequest(request as NSURLRequest).map2 { responseObject in
             guard let json = responseObject as? JSON, let data = json["data"] as? JSON, let downloadURL = data["url"] as? String else {
                 print("[Loki] Couldn't parse profile picture from: \(responseObject).")
diff --git a/SignalServiceKit/src/Loki/API/Onion Requests/OnionRequestAPI+Encryption.swift b/SignalServiceKit/src/Loki/API/Onion Requests/OnionRequestAPI+Encryption.swift
index 6bb38b658..12e1b6aa0 100644
--- a/SignalServiceKit/src/Loki/API/Onion Requests/OnionRequestAPI+Encryption.swift	
+++ b/SignalServiceKit/src/Loki/API/Onion Requests/OnionRequestAPI+Encryption.swift	
@@ -35,14 +35,21 @@ extension OnionRequestAPI {
         let (promise, seal) = Promise<EncryptionResult>.pending()
         DispatchQueue.global(qos: .userInitiated).async {
             do {
+                // The wrapper is not needed when it is a file server onion request
                 guard JSONSerialization.isValidJSONObject(payload) else { return seal.reject(HTTP.Error.invalidJSON) }
-                let payloadAsData = try JSONSerialization.data(withJSONObject: payload, options: [ .fragmentsAllowed ])
-                let payloadAsString = String(data: payloadAsData, encoding: .utf8)! // Snodes only accept this as a string
-                let wrapper: JSON = [ "body" : payloadAsString, "headers" : "" ]
-                guard JSONSerialization.isValidJSONObject(wrapper) else { return seal.reject(HTTP.Error.invalidJSON) }
-                let plaintext = try JSONSerialization.data(withJSONObject: wrapper, options: [ .fragmentsAllowed ])
-                let result = try encrypt(plaintext, using: x25519Key)
-                seal.fulfill(result)
+                if let destination = destination["destination"] {
+                    let payloadAsData = try JSONSerialization.data(withJSONObject: payload, options: [ .fragmentsAllowed ])
+                    let payloadAsString = String(data: payloadAsData, encoding: .utf8)! // Snodes only accept this as a string
+                    let wrapper: JSON = [ "body" : payloadAsString, "headers" : "" ]
+                    guard JSONSerialization.isValidJSONObject(wrapper) else { return seal.reject(HTTP.Error.invalidJSON) }
+                    let plaintext = try JSONSerialization.data(withJSONObject: wrapper, options: [ .fragmentsAllowed ])
+                    let result = try encrypt(plaintext, using: x25519Key)
+                    seal.fulfill(result)
+                } else {
+                    let plaintext = try JSONSerialization.data(withJSONObject: payload, options: [ .fragmentsAllowed ])
+                    let result = try encrypt(plaintext, using: x25519Key)
+                    seal.fulfill(result)
+                }
             } catch (let error) {
                 seal.reject(error)
             }
diff --git a/SignalServiceKit/src/Loki/API/Onion Requests/OnionRequestAPI.swift b/SignalServiceKit/src/Loki/API/Onion Requests/OnionRequestAPI.swift
index 2423e7830..7c15554f4 100644
--- a/SignalServiceKit/src/Loki/API/Onion Requests/OnionRequestAPI.swift	
+++ b/SignalServiceKit/src/Loki/API/Onion Requests/OnionRequestAPI.swift	
@@ -198,7 +198,7 @@ public enum OnionRequestAPI {
                 // Recursively encrypt the layers of the onion (again in reverse order)
                 encryptionResult = r
                 var path = path
-                var destination: JSON = [:]
+                var destination = dest
                 func addLayer() -> Promise<EncryptionResult> {
                     if path.isEmpty {
                         return Promise<EncryptionResult> { $0.fulfill(encryptionResult) }
@@ -218,6 +218,17 @@ public enum OnionRequestAPI {
     }
 
     // MARK: Internal API
+    internal static func getCanonicalHeaders(for request: NSURLRequest) -> [String:Any] {
+        guard let headers = request.allHTTPHeaderFields else { return [:] }
+        return headers.mapValues { value in
+            switch value.lowercased() {
+            case "true": return true
+            case "false": return false
+            default: return value
+            }
+        }
+    }
+    
     /// Sends an onion request to `snode`. Builds new paths as needed.
     internal static func sendOnionRequestSnodeDest(invoking method: Snode.Method, on snode: Snode, with parameters: JSON, associatedWith publicKey: String) -> Promise<JSON> {
         let payload: JSON = [ "method" : method.rawValue, "params" : parameters ]
@@ -230,11 +241,39 @@ public enum OnionRequestAPI {
     }
     
     /// Sends an onion request to `file server`. Builds new paths as needed.
-    internal static func sendOnionRequestLsrpcDest(to host: String, with payload: JSON, using x25519Key: String, associatedWith publicKey: String) -> Promise<JSON> {
-        let destination: JSON = [ "host"    : host,
+    internal static func sendOnionRequestFileServerDest(_ request: NSURLRequest, server: String, using x25519Key: String) -> Promise<JSON> {
+        var headers = getCanonicalHeaders(for: request)
+        let urlAsString = request.url!.absoluteString
+        let serverURLEndIndex = urlAsString.range(of: server)!.upperBound
+        let endpointStartIndex = urlAsString.index(after: serverURLEndIndex)
+        let endpoint = String(urlAsString[endpointStartIndex..<urlAsString.endIndex])
+        let parametersAsString: String
+        if let tsRequest = request as? TSRequest {
+            headers["Content-Type"] = "application/json"
+            let parametersAsData = try! JSONSerialization.data(withJSONObject: tsRequest.parameters, options: [ .fragmentsAllowed ])
+            parametersAsString = !tsRequest.parameters.isEmpty ? String(bytes: parametersAsData, encoding: .utf8)! : "null"
+        } else {
+            headers["Content-Type"] = request.allHTTPHeaderFields!["Content-Type"]
+            if let parametersAsInputStream = request.httpBodyStream, let parametersAsData = try? Data(from: parametersAsInputStream) {
+                parametersAsString = "{ \"fileUpload\" : \"\(String(data: parametersAsData.base64EncodedData(), encoding: .utf8) ?? "null")\" }"
+            } else {
+                parametersAsString = "null"
+            }
+        }
+        let payload: JSON = [
+            "body" : parametersAsString,
+            "endpoint": endpoint,
+            "method" : request.httpMethod,
+            "headers" : headers
+        ]
+        let destination: JSON = [ "host"    : request.url?.host,
                                   "target"  : "/loki/v1/lsrpc",
                                   "method"  : "POST"]
-        let promise = sendOnionRequest(on: nil, with: payload, to: destination, using: x25519Key, associatedWith: publicKey)
+        let promise = sendOnionRequest(on: nil, with: payload, to: destination, using: x25519Key, associatedWith: getUserHexEncodedPublicKey())
+        promise.recover2{ error -> Promise<JSON> in
+            // TODO: File Server API handle Error
+            throw error
+        }
         return promise
     }