wulkanowy-ios/sdk/Sources/Sdk/Sdk.swift

336 lines
14 KiB
Swift

//
// Sdk.swift
//
//
// Created by Tomasz (copied from rrroyal/vulcan) on 14/02/2021.
//
import Foundation
import KeychainAccess
import Combine
import os
import SwiftyJSON
import SwiftUI
@available (iOS 14, macOS 11, watchOS 7, tvOS 14, *)
public class Sdk {
static private let libraryVersion: String = "0.0.1"
private let loggerSubsystem: String = "io.wulkanowy-ios.Sdk"
private var cancellables: Set<AnyCancellable> = []
var firebaseToken: String!
var endpointURL: String!
public let certificate: X509
// MARK: - Init
public init(certificate: X509) {
self.certificate = certificate
}
// MARK: - Public functions
/// Logs in with supplied login data.
/// - Parameters:
/// - token: Login token
/// - symbol: Login symbol
/// - pin: Login PIN
/// - deviceName: Name of the device
/// - completionHandler: Callback
public func login(token: String, symbol: String, pin: String, deviceModel: String, completionHandler: @escaping (Error?) -> Void) {
let logger: Logger = Logger(subsystem: self.loggerSubsystem, category: "Login")
logger.debug("Logging in...")
let endpointPublisher = URLSession.shared.dataTaskPublisher(for: URL(string: "http://komponenty.vulcan.net.pl/UonetPlusMobile/RoutingRules.txt")!)
.mapError { $0 as Error }
// Firebase request
var firebaseRequest: URLRequest = URLRequest(url: URL(string: "https://android.googleapis.com/checkin")!)
firebaseRequest.httpMethod = "POST"
firebaseRequest.setValue("application/json", forHTTPHeaderField: "Content-type")
firebaseRequest.setValue("gzip", forHTTPHeaderField: "Accept-Encoding")
let firebaseRequestBody: [String: Any] = [
"locale": "pl_PL",
"digest": "",
"checkin": [
"iosbuild": [
"model": deviceModel,
"os_version": Self.libraryVersion
],
"last_checkin_msec": 0,
"user_number": 0,
"type": 2
],
"time_zone": TimeZone.current.identifier,
"user_serial_number": 0,
"id": 0,
"logging_id": 0,
"version": 2,
"security_token": 0,
"fragment": 0
]
firebaseRequest.httpBody = try? JSONSerialization.data(withJSONObject: firebaseRequestBody)
let firebasePublisher = URLSession.shared.dataTaskPublisher(for: firebaseRequest)
.receive(on: DispatchQueue.global(qos: .background))
.tryCompactMap { value -> AnyPublisher<Data, Error> in
guard let dictionary: [String: Any] = try? JSONSerialization.jsonObject(with: value.data) as? [String: Any] else {
throw APIError.jsonSerialization
}
var request: URLRequest = URLRequest(url: URL(string: "https://fcmtoken.googleapis.com/register")!)
request.httpMethod = "POST"
request.setValue("AidLogin \(dictionary["android_id"] as? Int ?? 0):\(dictionary["security_token"] as? Int ?? 0)", forHTTPHeaderField: "Authorization")
request.setValue("gzip", forHTTPHeaderField: "Accept-Encoding")
let body: String = "device=\(dictionary["android_id"] as? Int ?? 0)&app=pl.edu.vulcan.hebe&sender=987828170337&X-subtype=987828170337&appid=dLIDwhjvE58&gmp_app_id=1:987828170337:ios:6b65a4ad435fba7f"
request.httpBody = body.data(using: .utf8)
return URLSession.shared.dataTaskPublisher(for: request)
.receive(on: DispatchQueue.global(qos: .background))
.mapError { $0 as Error }
.map { $0.data }
.eraseToAnyPublisher()
}
.flatMap { $0 }
Publishers.Zip(endpointPublisher, firebasePublisher)
.tryMap { (endpoints, firebaseToken) -> (String, String) in
// Find endpointURL
let lines = String(data: endpoints.data, encoding: .utf8)?.split { $0.isNewline }
var endpointURL: String?
lines?.forEach { line in
let items = line.split(separator: ",")
if (token.starts(with: items[0])) {
endpointURL = String(items[1])
return
}
}
guard let finalEndpointURL: String = endpointURL else {
throw APIError.noEndpointURL
}
// Get Firebase token
guard let token: String = String(data: firebaseToken, encoding: .utf8)?.components(separatedBy: "token=").last else {
logger.error("Token empty! Response: \"\(firebaseToken.base64EncodedString(), privacy: .private)\"")
throw APIError.noFirebaseToken
}
return (finalEndpointURL, token)
}
.tryMap { endpointURL, firebaseToken in
try self.registerDevice(endpointURL: endpointURL, firebaseToken: firebaseToken, token: token, symbol: symbol, pin: pin, deviceModel: deviceModel)
.mapError { $0 as Error }
.map { $0.data }
.eraseToAnyPublisher()
}
.flatMap { $0 }
.sink(receiveCompletion: { completion in
switch completion {
case .finished:
break
case .failure(let error):
completionHandler(error)
}
}, receiveValue: { data in
if let response = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let parsedError = self.parseResponse(response) {
completionHandler(parsedError)
} else {
do {
let keychain = Keychain()
let allAccountsCheck: String! = keychain["allAccounts"] ?? "[]"
let receiveValueJSON = try! JSON(data: data)
//parsing allAccounts to array
let data = Data(allAccountsCheck.utf8)
do {
var ids = try JSONSerialization.jsonObject(with: data) as! [Int]
if(ids == [])
{
ids = [0]
} else {
ids.append(ids.last! + 1)
}
keychain["allAccounts"] = "\(ids)"
} catch {
print(error)
}
// Get private key
guard let privateKeyRawData = self.certificate.getPrivateKeyData(format: .DER),
let privateKeyString = String(data: privateKeyRawData, encoding: .utf8)?
.split(separator: "\n")
.dropFirst()
.dropLast()
.joined()
.data(using: .utf8) else {
return
}
let privateKeyStringString = String(decoding: privateKeyString, as: UTF8.self)
let fingerprint = self.certificate.getCertificateFingerprint().lowercased()
let saveAccount = """
{
"actualStudent": "0",
"customUsername": "",
"privateKeyString": "\(privateKeyStringString)",
"fingerprint": "\(fingerprint)",
"deviceModel": "\(deviceModel)",
"account": {
"UserName": "\(receiveValueJSON["Envelope"]["UserName"])",
"RestURL": "\(receiveValueJSON["Envelope"]["RestURL"])",
"UserLogin": "\(receiveValueJSON["Envelope"]["UserLogin"])",
"LoginId": "\(receiveValueJSON["Envelope"]["LoginId"])"
}
}
"""
let ids = keychain["allAccounts"]
let dataIds: Data = Data(ids!.utf8)
let idsArray = try JSONSerialization.jsonObject(with: dataIds) as! [Int]
let id = idsArray.last
keychain["\(id!)"] = "\(saveAccount)"
keychain["actualStudentId"] = "\(id!)"
keychain["actualAccountEmail"] = "\(receiveValueJSON["Envelope"]["UserName"])"
let endpointURL: String = "\(receiveValueJSON["Envelope"]["RestURL"])api/mobile/register/hebe"
let apiResponseRequest = apiRequest(endpointURL: endpointURL, id: "\(id!)")
let session = URLSession.shared
session.dataTask(with: apiResponseRequest) { (data, response, error) in
if let error = error {
// Handle HTTP request error
print(error)
} else if let data = data {
// Handle HTTP request response
let responseBody = String(data: data, encoding: String.Encoding.utf8)
keychain["actualStudentHebe"] = "\(responseBody!)"
} else {
// Handle unexpected error
}
}.resume()
} catch {
print(error)
}
}
completionHandler(nil)
})
.store(in: &cancellables)
}
// MARK: - Private functions
/// Registers the device
/// - Parameters:
/// - endpointURL: API endpoint URL
/// - firebaseToken: FCM token
/// - token: Vulcan token
/// - symbol: Vulcan symbol
/// - pin: Vulcan PIN
/// - deviceModel: Device model
/// - Throws: Error
/// - Returns: URLSession.DataTaskPublisher
private func registerDevice(endpointURL: String, firebaseToken: String, token: String, symbol: String, pin: String, deviceModel: String) throws -> URLSession.DataTaskPublisher {
self.endpointURL = endpointURL
self.firebaseToken = firebaseToken
guard let keyFingerprint = certificate.getPrivateKeyFingerprint(format: .PEM)?.replacingOccurrences(of: ":", with: "").lowercased(),
let keyData = certificate.getPublicKeyData(),
let keyBase64 = String(data: keyData, encoding: .utf8)?
.split(separator: "\n") // Split by newline
.dropFirst() // Drop prefix
.dropLast() // Drop suffix
.joined() // Join
else {
throw APIError.noCertificate
}
// Request
let url = "\(endpointURL)/\(symbol)/api/mobile/register/new"
var request = URLRequest(url: URL(string: url)!)
request.httpMethod = "POST"
let now: Date = Date()
var timestampFormatted: String {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "EEE, dd MMM yyyy HH:mm:ss"
dateFormatter.locale = Locale(identifier: "en_US_POSIX")
return dateFormatter.string(from: now)
}
let keychain = Keychain()
keychain[string: "keyFingerprint"] = keyFingerprint
//Boże, proszę, dlaczego to nie działa, błagam
// Body
let body: [String: Encodable?] = [
"AppName": "DzienniczekPlus 2.0",
"AppVersion": Self.libraryVersion,
"CertificateId": nil,
"Envelope": [
"OS": "iOS",
"PIN": pin,
"Certificate": keyBase64,
"CertificateType": "RSA_PEM",
"DeviceModel": deviceModel,
"SecurityToken": token,
"SelfIdentifier": UUID().uuidString.lowercased(),
"CertificateThumbprint": keyFingerprint
],
"FirebaseToken": firebaseToken,
"API": 1,
"RequestId": UUID().uuidString.lowercased(),
"Timestamp": now.millisecondsSince1970,
"TimestampFormatted": "\(timestampFormatted) GMT"
]
request.httpBody = try? JSONSerialization.data(withJSONObject: body)
request.allHTTPHeaderFields = [
"Content-Type": "application/json",
"Accept-Encoding": "gzip",
"vDeviceModel": deviceModel
]
let signedRequest = try request.signed(with: certificate)
return URLSession.shared.dataTaskPublisher(for: signedRequest)
}
// MARK: - Helper functions
/// Parses the response
/// - Parameter response: Request response
/// - Returns: VulcanKit.APIError?
private func parseResponse(_ response: [String: Any]) -> APIError? {
guard let status = response["Status"] as? [String: Any],
let statusCode = status["Code"] as? Int else {
return nil
}
print("Response status code: \(statusCode)")
switch statusCode {
case 0: return nil
case 200: return APIError.wrongToken
case -1: return APIError.wrongSymbol //Ya, Vulcan returns -1 code
case 203: return APIError.wrongPin
case 205: return APIError.deviceExist
default: return nil
}
}
}