/* 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 Alamofire import Deferred import Shared import SwiftyJSON //public struct PushRemoteError { // static let MissingNecessaryCryptoKeys: Int32 = 101 // static let InvalidURLEndpoint: Int32 = 102 // static let ExpiredURLEndpoint: Int32 = 103 // static let DataPayloadTooLarge: Int32 = 104 // static let EndpointBecameUnavailable: Int32 = 105 // static let InvalidSubscription: Int32 = 106 // static let RouterTypeIsInvalid: Int32 = 108 // static let InvalidAuthentication: Int32 = 109 // static let InvalidCryptoKeysSpecified: Int32 = 110 // static let MissingRequiredHeader: Int32 = 111 // static let InvalidTTLHeaderValue: Int32 = 112 // static let UnknownError: Int32 = 999 //} public let PushClientErrorDomain = "org.mozilla.push.error" private let PushClientUnknownError = NSError(domain: PushClientErrorDomain, code: 999, userInfo: [NSLocalizedDescriptionKey: "Invalid server response"]) private let log = Logger.browserLogger /// Bug 1364403 – This is to be put into the push registration private let apsEnvironment: [String: Any] = [ "mutable-content": 1, "alert": [ "title": " ", "body": " " ], ] public struct PushRemoteError { let code: Int let errno: Int let error: String let message: String? public static func from(json: JSON) -> PushRemoteError? { guard let code = json["code"].int, let errno = json["errno"].int, let error = json["error"].string else { return nil } let message = json["message"].string return PushRemoteError(code: code, errno: errno, error: error, message: message) } } public enum PushClientError: MaybeErrorType { case Remote(PushRemoteError) case Local(Error) public var description: String { switch self { case let .Remote(error): let errorString = error.error let messageString = error.message ?? "" return "" case let .Local(error): return "" } } } public class PushClient { let endpointURL: NSURL let experimentalMode: Bool lazy fileprivate var alamofire: SessionManager = { let ua = UserAgent.fxaUserAgent let configuration = URLSessionConfiguration.ephemeral var defaultHeaders = SessionManager.default.session.configuration.httpAdditionalHeaders ?? [:] defaultHeaders["User-Agent"] = ua configuration.httpAdditionalHeaders = defaultHeaders return SessionManager(configuration: configuration) }() public init(endpointURL: NSURL, experimentalMode: Bool = false) { self.endpointURL = endpointURL self.experimentalMode = experimentalMode } } public extension PushClient { public func register(_ apnsToken: String) -> Deferred> { // POST /v1/{type}/{app_id}/registration let registerURL = endpointURL.appendingPathComponent("registration")! var mutableURLRequest = URLRequest(url: registerURL) mutableURLRequest.httpMethod = HTTPMethod.post.rawValue mutableURLRequest.setValue("application/json", forHTTPHeaderField: "Content-Type") let parameters: [String: Any] if experimentalMode { parameters = [ "token": apnsToken, "aps": apsEnvironment, ] } else { parameters = ["token": apnsToken] } mutableURLRequest.httpBody = JSON(parameters).stringValue()?.utf8EncodedData if experimentalMode { log.info("curl -X POST \(registerURL.absoluteString) --data '\(JSON(parameters).stringValue()!)'") } return send(request: mutableURLRequest) >>== { json in guard let response = PushRegistration.from(json: json) else { return deferMaybe(PushClientError.Local(PushClientUnknownError)) } return deferMaybe(response) } } public func updateUAID(_ apnsToken: String, withRegistration creds: PushRegistration) -> Deferred> { // PUT /v1/{type}/{app_id}/registration/{uaid} let registerURL = endpointURL.appendingPathComponent("registration/\(creds.uaid)")! var mutableURLRequest = URLRequest(url: registerURL) mutableURLRequest.httpMethod = HTTPMethod.put.rawValue mutableURLRequest.addValue("Bearer \(creds.secret)", forHTTPHeaderField: "Authorization") mutableURLRequest.setValue("application/json", forHTTPHeaderField: "Content-Type") let parameters = ["token": apnsToken] mutableURLRequest.httpBody = JSON(parameters).stringValue()?.utf8EncodedData return send(request: mutableURLRequest) >>== { json in return deferMaybe(creds) } } public func unregister(_ creds: PushRegistration) -> Success { // DELETE /v1/{type}/{app_id}/registration/{uaid} let unregisterURL = endpointURL.appendingPathComponent("registration/\(creds.uaid)") var mutableURLRequest = URLRequest(url: unregisterURL!) mutableURLRequest.httpMethod = HTTPMethod.delete.rawValue mutableURLRequest.addValue("Bearer \(creds.secret)", forHTTPHeaderField: "Authorization") return send(request: mutableURLRequest) >>> succeed } } /// Utilities extension PushClient { fileprivate func send(request: URLRequest) -> Deferred> { log.info("\(request.httpMethod!) \(request.url?.absoluteString ?? "nil")") let deferred = Deferred>() alamofire.request(request) .validate(contentType: ["application/json"]) .responseJSON { response in // Don't cancel requests just because our client is deallocated. withExtendedLifetime(self.alamofire) { let result = response.result if let error = result.error { return deferred.fill(Maybe(failure: PushClientError.Local(error))) } guard let data = response.data else { return deferred.fill(Maybe(failure: PushClientError.Local(PushClientUnknownError))) } let json = JSON(data: data) if let remoteError = PushRemoteError.from(json: json) { return deferred.fill(Maybe(failure: PushClientError.Remote(remoteError))) } deferred.fill(Maybe(success: json)) } } return deferred } }