Files
lockdown-iOS-mirror/Client.swift
2024-08-15 23:16:08 +03:00

381 lines
17 KiB
Swift

//
// Client.swift
// Lockdown
//
// Created by Johnny Lin on 7/31/19.
// Copyright © 2019 Confirmed Inc. All rights reserved.
//
import Foundation
import PromiseKit
import SwiftyStoreKit
import CocoaLumberjackSwift
let kApiCodeNoError = 0
let kApiCodeEmailNotConfirmed = 1
let kApiCodeIncorrectLogin = 2
let kApiCodeRequestFieldValidationError = 3
let kApiCodeNoActiveSubscription = 6
let kApiCodeNoSubscriptionInReceipt = 9
let kApiCodeMobileSubscriptionOnly = 38
let kApiCodeEmailAlreadyUsed = 40
let kApiCodeReceiptAlreadyUsed = 48
let kApiCodeInvalidAuth = 401
let kApiCodeTooManyRequests = 999
let kApiCodeSandboxReceiptNotAllowed = 9925
let kApiCodeUnknownError = 99999
let kApiCodeNegativeError = -1
class Client {
// MARK: - CLIENT CALLS
static func signIn(forceRefresh: Bool = false) throws -> Promise<SignIn> {
DDLogInfo("API CALL: signIn")
URLCache.shared.removeAllCachedResponses()
clearCookies()
return getReceipt(forceRefresh: forceRefresh)
.then { receipt -> Promise<(data: Data, response: URLResponse)> in
let parameters:[String : Any] = [
"authtype": "ios",
"authreceipt": receipt,
"lockdown": true
]
return URLSession.shared.dataTask(.promise,
with: try makePostRequest(urlString: mainURL + "/signin",
parameters: parameters))
}
.map { data, response -> SignIn in
try self.validateApiResponse(data: data, response: response)
let resp = response as! HTTPURLResponse // already validated the type in validateApiResponse
DDLogInfo("Got signin response with headers: \(resp.allHeaderFields)")
if (hasValidCookie()) {
return try JSONDecoder().decode(SignIn.self, from: data)
}
else {
throw "No valid cookie received and/or set when trying to sign in"
}
}
}
static func signInWithEmail(email: String, password: String) throws -> Promise<SignIn> {
DDLogInfo("API CALL: test signIn with email")
URLCache.shared.removeAllCachedResponses()
clearCookies()
return firstly { () -> Promise<(data: Data, response: URLResponse)> in
let parameters:[String : Any] = [
"email" : email,
"password" : password,
"lockdown": true
]
return URLSession.shared.dataTask(.promise, with: try makePostRequest(urlString: mainURL + "/signin", parameters: parameters))
}
.map { data, response -> SignIn in
try self.validateApiResponse(data: data, response: response)
let resp = response as! HTTPURLResponse // already validated the type in validateApiResponse
DDLogInfo("Got signin (with email) response with headers: \(resp.allHeaderFields)")
return try JSONDecoder().decode(SignIn.self, from: data)
}
}
static func resendConfirmCode(email: String) throws -> Promise<Bool> {
DDLogInfo("API CALL: resendConfirmCode")
return firstly { () -> Promise<(data: Data, response: URLResponse)> in
let parameters:[String : Any] = [
"email" : email,
"lockdown": true
]
return URLSession.shared.dataTask(.promise, with: try makePostRequest(urlString: mainURL + "/resend-confirm-code", parameters: parameters))
}
.map { data, response -> Bool in
if let httpResponse = response as? HTTPURLResponse {
DDLogInfo("API RESULT: resend-confirm-code: \(httpResponse.statusCode)")
if httpResponse.statusCode < 400 {
return true
}
return false
}
DDLogInfo("API RESULT: error - resend-confirm-code: not HTTPURLResponse")
return false
}
}
static func subscriptionEvent(forceRefresh: Bool = false) throws -> Promise<SubscriptionEvent> {
DDLogInfo("API CALL: subscription-event")
return getReceipt(forceRefresh: forceRefresh)
.then { receipt -> Promise<(data: Data, response: URLResponse)> in
let parameters:[String : Any] = [
"authtype": "ios",
"authreceipt": receipt,
"lockdown": true
]
return URLSession.shared.dataTask(.promise, with: try makePostRequest(urlString: mainURL + "/subscription-event", parameters: parameters))
}
.map { data, response -> SubscriptionEvent in
try self.validateApiResponse(data: data, response: response)
let subscriptionEvent = try JSONDecoder().decode(SubscriptionEvent.self, from: data)
DDLogInfo("API RESULT: subscriptionEvent: \(subscriptionEvent)")
return subscriptionEvent
}
.recover { error -> Promise<SubscriptionEvent> in
DDLogInfo("Recovering from subscription-event error: \(error)")
return .value(SubscriptionEvent(message: "Recovery"))
}
}
static func activeSubscriptions() throws -> Promise<[Subscription]> {
DDLogInfo("API CALL: active-subscriptions")
return firstly {
URLSession.shared.dataTask(.promise, with: try makePostRequest(urlString: mainURL + "/active-subscriptions", parameters: [:]))
}.map { data, response -> [Subscription] in
try self.validateApiResponse(data: data, response: response)
let decoder = JSONDecoder()
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"
decoder.dateDecodingStrategy = .formatted(formatter)
var subscriptions = try decoder.decode([Subscription].self, from: data)
DDLogInfo("API RESULT: active-subscriptions: \(subscriptions)")
// sort subscriptions with highest tier at the top
subscriptions.sort(by: { (sub1: Subscription, sub2: Subscription) -> Bool in
let p1 = Subscription.PlanType.precedence(p: sub1.planType)
let p2 = Subscription.PlanType.precedence(p: sub2.planType)
return p1 <= p2
})
DDLogInfo("API RESULT: sorted-active-subscriptions: \(subscriptions)")
return subscriptions
}
}
// For creating email account only - not signing up with IAP receipt
static func signup(email: String, password: String) throws -> Promise<Signup> {
DDLogInfo("API CALL: signup")
return firstly { () -> Promise<(data: Data, response: URLResponse)> in
let parameters:[String : Any] = [
"email" : email,
"password" : password,
"lockdown": true
]
return URLSession.shared.dataTask(.promise, with: try makePostRequest(urlString: mainURL + "/signup", parameters: parameters))
}
.map { data, response -> Signup in
try self.validateApiResponse(data: data, response: response)
let signup = try JSONDecoder().decode(Signup.self, from: data)
DDLogInfo("API RESULT: signup: \(signup)")
return signup
}
}
static func forgotPassword(email: String) throws -> Promise<Bool> {
DDLogInfo("API CALL: forgot-password")
return firstly { () -> Promise<(data: Data, response: URLResponse)> in
let parameters:[String : Any] = [
"email" : email,
"lockdown": true
]
return URLSession.shared.dataTask(.promise, with: try makePostRequest(urlString: mainURL + "/forgot-password", parameters: parameters))
}
.map { data, response -> Bool in
if let httpResponse = response as? HTTPURLResponse {
DDLogInfo("API RESULT: forgot-password: \(httpResponse.statusCode)")
if httpResponse.statusCode < 400 {
return true
}
if let error = try? JSONDecoder().decode(ApiError.self, from: data) {
throw error
}
throw ApiError(
code: httpResponse.statusCode,
message: "Unknown error"
)
}
DDLogInfo("API RESULT: error - forgot-password: not HTTPURLResponse")
return false
}
}
static func getKey() throws -> Promise<GetKey> {
DDLogInfo("API CALL: getKey")
return firstly { () -> Promise<(data: Data, response: URLResponse)> in
let parameters:[String : Any] = [
"platform" : "ios",
"lockdown": true
]
return URLSession.shared.dataTask(.promise, with: try makePostRequest(urlString: mainURL + "/get-key", parameters: parameters))
}
.map { data, response -> GetKey in
try self.validateApiResponse(data: data, response: response)
let getKey = try JSONDecoder().decode(GetKey.self, from: data)
DDLogInfo("API RESULT: getKey: \(getKey)")
return getKey
}
}
static func getSpeedTestBucket() -> Promise<SpeedTestBucket> {
DDLogInfo("API CALL: download speed test")
return firstly {
URLSession.shared.dataTask(.promise, with: try makeGetRequest(urlString: "\(mainURL)/download-speed-test"))
}
.map { data, response -> SpeedTestBucket in
try self.validateApiResponse(data: data, response: response)
let speedTestBucket = try JSONDecoder().decode(SpeedTestBucket.self, from: data)
DDLogInfo("API RESULT: speedTestBucket: \(speedTestBucket)")
return speedTestBucket
}
}
static func getIP() -> Promise<IP> {
DDLogInfo("API CALL: ip")
URLCache.shared.removeAllCachedResponses()
return firstly {
URLSession.shared.dataTask(.promise, with: try makeGetRequest(urlString: "https://ip.\(mainDomain)/ip"))
}
.map { data, response -> IP in
try self.validateApiResponse(data: data, response: response)
let ip = try JSONDecoder().decode(IP.self, from: data)
DDLogInfo("API RESULT: ip: \(ip)")
return ip
}
}
static func getBlockedDomainTest() -> Promise<Void> {
return firstly {
URLSession.shared.dataTask(.promise, with: try Client.makeGetRequest(urlString: "https://\(testFirewallDomain)"))
}.asVoid()
}
// MARK: - Request Makers
static func makeGetRequest(urlString: String) throws -> URLRequest {
DDLogInfo("makeGetRequest: \(urlString)")
if let url = URL(string: urlString) {
var rq = URLRequest(url: url)
rq.cachePolicy = .reloadIgnoringLocalAndRemoteCacheData
rq.httpMethod = "GET"
rq.addValue("application/json", forHTTPHeaderField: "Accept")
return rq
}
else {
throw "Invalid URL string: \(urlString)"
}
}
static func makePostRequest(urlString: String, parameters: [String: Any]) throws -> URLRequest {
DDLogInfo("makePostRequest: \(urlString)")//", parameters: \(parameters)")
if let url = URL(string: urlString) {
var rq = URLRequest(url: url)
rq.cachePolicy = .reloadIgnoringLocalAndRemoteCacheData
rq.httpMethod = "POST"
rq.addValue("application/json", forHTTPHeaderField: "Content-Type")
rq.addValue("application/json", forHTTPHeaderField: "Accept")
rq.httpBody = try JSONSerialization.data(withJSONObject: parameters, options: [])
return rq
}
else {
throw "Invalid URL string: \(urlString)"
}
}
// MARK: - Util
static func getReceipt(forceRefresh: Bool) -> Promise<String> {
DDLogInfo("fetch and set latest receipt")
return Promise { seal in
SwiftyStoreKit.fetchReceipt(forceRefresh: forceRefresh) { result in
switch result {
case .success(let receiptData):
let receipt = receiptData.base64EncodedString(options: [])
DDLogInfo("fetch latest receipt success base64: \(receipt)")
seal.fulfill(receipt);
case .error(let error):
DDLogError("fetch latest receipt failure: \(error)")
do {
switch error {
case ReceiptError.noReceiptData:
throw "Error refreshing purchases with App Store: No Receipt Data"
case ReceiptError.networkError(let networkError):
throw "Error refreshing purchases with App Store: Network Error - \(networkError.localizedDescription)"
case ReceiptError.noRemoteData:
throw "Error refreshing purchases with App Store: No Remote Data"
case ReceiptError.receiptInvalid(_, let receiptStatus):
throw "Error refreshing purchases with App Store: Invalid Receipt - \(receiptStatus)"
case ReceiptError.requestBodyEncodeError(let error):
throw "Error refreshing purchases with App Store: Encoding Error - \(error.localizedDescription)"
case ReceiptError.jsonDecodeError(_):
throw "Error refreshing purchases with App Store: JSON Decode Error"
}
}
catch {
seal.reject(error)
}
}
}
}
}
static func hasValidCookie() -> Bool {
DDLogInfo("checking for valid cookie")
var hasValidCookie = false
if let cookies = HTTPCookieStorage.shared.cookies {
DDLogInfo("found cookies")
for cookie in cookies {
DDLogInfo("cookie: \(cookie)")
if let timeUntilExpire = cookie.expiresDate?.timeIntervalSinceNow {
DDLogInfo("time until expire: \(timeUntilExpire)")
if cookie.domain.contains(mainDomain) && timeUntilExpire > 120.0 {
DDLogInfo("cookie contains mainDomain and timeuntilexpires > 120")
hasValidCookie = true
}
}
}
}
return hasValidCookie
}
static func clearCookies() {
DDLogInfo("clearing cookies")
var cookiesToDelete:[HTTPCookie] = []
if let cookies = HTTPCookieStorage.shared.cookies {
DDLogInfo("found cookies")
for cookie in cookies {
DDLogInfo("cookie to delete: \(cookie)")
cookiesToDelete.append(cookie)
}
}
for cookie in cookiesToDelete {
DDLogInfo("deleting cookie: \(cookie)")
HTTPCookieStorage.shared.deleteCookie(cookie)
}
}
private static func validateApiResponse(data: Data, response: URLResponse) throws {
DDLogInfo("validating API response")
let dataString = String(data: data, encoding: String.Encoding.utf8)
DDLogInfo("RAW RESULT: \(String(describing: dataString))")
if let resp = response as? HTTPURLResponse {
DDLogInfo("response is HTTPURLResponse: \(resp)")
// see if there's a non-zero code returned
if let apiError = try? JSONDecoder().decode(ApiError.self, from: data) {
if apiError.code == kApiCodeNoError {
DDLogError("zero (non-error) API code received, validated OK: \(apiError)")
return;
}
else {
DDLogError("nonzero API code received, throwing: \(apiError)")
throw apiError;
}
}
// some 4xx/5xx error
else if (resp.statusCode >= 400 || resp.statusCode <= 0) {
DDLogError("response has bad status code \(resp.statusCode)")
throw "response has bad status code \(resp.statusCode)"
}
else {
DDLogInfo("response has good status code (2xx, 3xx) and no error code")
}
}
else {
throw "Invalid URL Response received"
}
}
}