mirror of
https://github.com/confirmedcode/Lockdown-iOS.git
synced 2025-12-16 12:00:16 +01:00
381 lines
17 KiB
Swift
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"
|
|
}
|
|
}
|
|
}
|