feat(ios): add native app foundation and unsigned ipa build
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
import Foundation
|
||||
|
||||
public actor InstanceSessionStore {
|
||||
private var cookies: [UUID: (origin: URL, cookie: String)] = [:]
|
||||
|
||||
public init() {}
|
||||
|
||||
public func cookie(for id: UUID, origin: URL) -> String? {
|
||||
guard let value = cookies[id], value.origin == origin else { return nil }
|
||||
return value.cookie
|
||||
}
|
||||
|
||||
public func set(_ id: UUID, origin: URL, cookie: String) {
|
||||
cookies[id] = (origin, cookie)
|
||||
}
|
||||
|
||||
public func clear(_ id: UUID) {
|
||||
cookies.removeValue(forKey: id)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import Foundation
|
||||
|
||||
public struct UpstreamResponse: Equatable, Sendable {
|
||||
public let status: Int
|
||||
public let data: Data
|
||||
public let setCookie: String?
|
||||
|
||||
public init(status: Int, data: Data, setCookie: String? = nil) {
|
||||
self.status = status
|
||||
self.data = data
|
||||
self.setCookie = setCookie
|
||||
}
|
||||
}
|
||||
|
||||
public struct SimAdminClient: Sendable {
|
||||
private let session: URLSession
|
||||
private let maximumBytes: Int
|
||||
|
||||
public init(session: URLSession = .shared, maximumBytes: Int = 262_144) {
|
||||
self.session = session
|
||||
self.maximumBytes = maximumBytes
|
||||
}
|
||||
|
||||
public func send(
|
||||
_ route: UpstreamRoute,
|
||||
baseURL: URL,
|
||||
cookie: String?
|
||||
) async throws -> UpstreamResponse {
|
||||
guard baseURL.scheme == "http" || baseURL.scheme == "https",
|
||||
baseURL.path.isEmpty || baseURL.path == "/"
|
||||
else {
|
||||
throw UpstreamError.invalidRequest
|
||||
}
|
||||
guard let url = URL(string: baseURL.absoluteString + route.path) else {
|
||||
throw UpstreamError.invalidRequest
|
||||
}
|
||||
var request = URLRequest(url: url, timeoutInterval: 30)
|
||||
request.httpMethod = route.method
|
||||
request.httpBody = route.body
|
||||
request.setValue("application/json", forHTTPHeaderField: "accept")
|
||||
if route.body != nil {
|
||||
request.setValue("application/json", forHTTPHeaderField: "content-type")
|
||||
}
|
||||
if let cookie {
|
||||
request.setValue(cookie, forHTTPHeaderField: "cookie")
|
||||
}
|
||||
|
||||
do {
|
||||
let (data, response) = try await session.data(for: request)
|
||||
guard let http = response as? HTTPURLResponse, data.count <= maximumBytes else {
|
||||
throw UpstreamError.invalidResponse
|
||||
}
|
||||
switch http.statusCode {
|
||||
case 200...299:
|
||||
break
|
||||
case 401: throw UpstreamError.unauthorized
|
||||
case 403: throw UpstreamError.forbidden
|
||||
case 404, 405: throw UpstreamError.notSupported
|
||||
default: throw UpstreamError.transportFailed
|
||||
}
|
||||
let setCookie = http.allHeaderFields["Set-Cookie"] as? String
|
||||
return UpstreamResponse(status: http.statusCode, data: data, setCookie: setCookie)
|
||||
} catch let error as UpstreamError {
|
||||
throw error
|
||||
} catch let error as URLError where error.code == .timedOut {
|
||||
throw UpstreamError.timeout
|
||||
} catch {
|
||||
throw UpstreamError.transportFailed
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import Foundation
|
||||
|
||||
public enum UpstreamRoute: Equatable, Sendable {
|
||||
case health
|
||||
case device
|
||||
case sim
|
||||
case network
|
||||
case signalStrength
|
||||
case connectivity
|
||||
case smsList(limit: Int, offset: Int, direction: String?)
|
||||
case smsSend(phoneNumber: String, content: String)
|
||||
case smsBatchDelete(ids: [Int])
|
||||
case authLogin(password: String)
|
||||
case authLogout
|
||||
|
||||
public var method: String {
|
||||
switch self {
|
||||
case .health, .device, .sim, .network, .signalStrength, .connectivity, .smsList:
|
||||
return "GET"
|
||||
case .smsSend, .smsBatchDelete, .authLogin, .authLogout:
|
||||
return "POST"
|
||||
}
|
||||
}
|
||||
|
||||
public var path: String {
|
||||
switch self {
|
||||
case .health: return "/api/health"
|
||||
case .device: return "/api/device"
|
||||
case .sim: return "/api/sim"
|
||||
case .network: return "/api/network"
|
||||
case .signalStrength: return "/api/network/signal-strength"
|
||||
case .connectivity: return "/api/connectivity"
|
||||
case .smsList(let limit, let offset, let direction):
|
||||
var items = ["limit=\(limit)", "offset=\(offset)"]
|
||||
if let direction {
|
||||
items.append("direction=\(direction)")
|
||||
}
|
||||
return "/api/sms/list?\(items.joined(separator: "&"))"
|
||||
case .smsSend: return "/api/sms/send"
|
||||
case .smsBatchDelete: return "/api/sms/batch-delete"
|
||||
case .authLogin: return "/api/auth/login"
|
||||
case .authLogout: return "/api/auth/logout"
|
||||
}
|
||||
}
|
||||
|
||||
public var body: Data? {
|
||||
switch self {
|
||||
case .smsSend(let phoneNumber, let content):
|
||||
return try? JSONSerialization.data(
|
||||
withJSONObject: ["phone_number": phoneNumber, "content": content]
|
||||
)
|
||||
case .smsBatchDelete(let ids):
|
||||
return try? JSONSerialization.data(withJSONObject: ["ids": ids])
|
||||
case .authLogin(let password):
|
||||
return try? JSONSerialization.data(withJSONObject: ["password": password])
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum UpstreamError: Error, Equatable {
|
||||
case invalidRequest
|
||||
case unauthorized
|
||||
case forbidden
|
||||
case notSupported
|
||||
case timeout
|
||||
case transportFailed
|
||||
case invalidResponse
|
||||
}
|
||||
Reference in New Issue
Block a user