|
@@ -19,6 +19,7 @@ struct EmptyAPIData: Decodable { }
|
|
|
enum APIError: LocalizedError {
|
|
enum APIError: LocalizedError {
|
|
|
case invalidConfiguration
|
|
case invalidConfiguration
|
|
|
case invalidResponse
|
|
case invalidResponse
|
|
|
|
|
+ case secureConnection
|
|
|
case unauthorized
|
|
case unauthorized
|
|
|
case server(code: Int, message: String)
|
|
case server(code: Int, message: String)
|
|
|
case transport(String)
|
|
case transport(String)
|
|
@@ -29,6 +30,7 @@ enum APIError: LocalizedError {
|
|
|
switch self {
|
|
switch self {
|
|
|
case .invalidConfiguration: return "服务地址配置无效"
|
|
case .invalidConfiguration: return "服务地址配置无效"
|
|
|
case .invalidResponse: return "服务器返回了无效响应"
|
|
case .invalidResponse: return "服务器返回了无效响应"
|
|
|
|
|
+ case .secureConnection: return "无法安全连接登录服务器,请检查服务器 HTTPS 证书配置"
|
|
|
case .unauthorized: return "登录状态已失效,请重新登录"
|
|
case .unauthorized: return "登录状态已失效,请重新登录"
|
|
|
case .server(_, let message): return message
|
|
case .server(_, let message): return message
|
|
|
case .transport(let message): return "网络连接失败:\(message)"
|
|
case .transport(let message): return "网络连接失败:\(message)"
|
|
@@ -116,14 +118,16 @@ actor APIClient {
|
|
|
tokenStore: TokenStore = .shared
|
|
tokenStore: TokenStore = .shared
|
|
|
) {
|
|
) {
|
|
|
let configured = baseURL ?? (Bundle.main.object(forInfoDictionaryKey: "APIBaseURL") as? String).flatMap(URL.init(string:))
|
|
let configured = baseURL ?? (Bundle.main.object(forInfoDictionaryKey: "APIBaseURL") as? String).flatMap(URL.init(string:))
|
|
|
- self.baseURL = configured ?? URL(string: "https://celestia-trace.ccdw.life/v1")!
|
|
|
|
|
|
|
+ self.baseURL = configured ?? URL(string: "https://api.ccdw.life/celestia-trace/v1")!
|
|
|
if let session {
|
|
if let session {
|
|
|
self.session = session
|
|
self.session = session
|
|
|
} else {
|
|
} else {
|
|
|
let configuration = URLSessionConfiguration.default
|
|
let configuration = URLSessionConfiguration.default
|
|
|
- configuration.waitsForConnectivity = true
|
|
|
|
|
- configuration.timeoutIntervalForRequest = 30
|
|
|
|
|
- configuration.timeoutIntervalForResource = 300
|
|
|
|
|
|
|
+ // Authentication is user-initiated. Fail promptly when the device is
|
|
|
|
|
+ // offline or TLS negotiation fails instead of leaving the UI waiting.
|
|
|
|
|
+ configuration.waitsForConnectivity = false
|
|
|
|
|
+ configuration.timeoutIntervalForRequest = 15
|
|
|
|
|
+ configuration.timeoutIntervalForResource = 60
|
|
|
self.session = URLSession(configuration: configuration)
|
|
self.session = URLSession(configuration: configuration)
|
|
|
}
|
|
}
|
|
|
self.tokenStore = tokenStore
|
|
self.tokenStore = tokenStore
|
|
@@ -213,6 +217,8 @@ actor APIClient {
|
|
|
let response: URLResponse
|
|
let response: URLResponse
|
|
|
do {
|
|
do {
|
|
|
(data, response) = try await session.data(for: request)
|
|
(data, response) = try await session.data(for: request)
|
|
|
|
|
+ } catch let error as URLError where Self.isSecureConnectionError(error.code) {
|
|
|
|
|
+ throw APIError.secureConnection
|
|
|
} catch {
|
|
} catch {
|
|
|
throw APIError.transport(error.localizedDescription)
|
|
throw APIError.transport(error.localizedDescription)
|
|
|
}
|
|
}
|
|
@@ -368,4 +374,20 @@ actor APIClient {
|
|
|
}
|
|
}
|
|
|
return decoder
|
|
return decoder
|
|
|
}()
|
|
}()
|
|
|
|
|
+
|
|
|
|
|
+ private static func isSecureConnectionError(_ code: URLError.Code) -> Bool {
|
|
|
|
|
+ switch code {
|
|
|
|
|
+ case .secureConnectionFailed,
|
|
|
|
|
+ .serverCertificateHasBadDate,
|
|
|
|
|
+ .serverCertificateUntrusted,
|
|
|
|
|
+ .serverCertificateHasUnknownRoot,
|
|
|
|
|
+ .serverCertificateNotYetValid,
|
|
|
|
|
+ .clientCertificateRejected,
|
|
|
|
|
+ .clientCertificateRequired,
|
|
|
|
|
+ .appTransportSecurityRequiresSecureConnection:
|
|
|
|
|
+ return true
|
|
|
|
|
+ default:
|
|
|
|
|
+ return false
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|