Fix a crash bug at Client.makeURL (#1601)

The crash will happen when you type something unexpected instance URL.

Example
```swift
let server = "mstdn.jp/"

var components = URLComponents()
components.scheme = "https"
components.host = server
components.path = "/api/v1/instance"
components.url! // 💥 error: Execution was interrupted, reason: EXC_BREAKPOINT (code=1, subcode=0x18c986650).
```
This commit is contained in:
Yasura Dodo 2023-10-02 09:31:59 +02:00 committed by GitHub
parent 23a83d69cc
commit e3f7eb31e4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 28 additions and 17 deletions

View file

@ -40,9 +40,14 @@ import Observation
} }
private func connect() { private func connect() {
guard let client else { return } guard let task = try? client?.makeWebSocketTask(
task = client.makeWebSocketTask(endpoint: Streaming.streaming, instanceStreamingURL: instanceStreamingURL) endpoint: Streaming.streaming,
task?.resume() instanceStreamingURL: instanceStreamingURL
) else {
return
}
self.task = task
self.task?.resume()
receiveMessage() receiveMessage()
} }

View file

@ -19,6 +19,10 @@ import SwiftUI
case v1, v2 case v1, v2
} }
public enum ClientError: Error {
case unexpectedRequest
}
public enum OauthError: Error { public enum OauthError: Error {
case missingApp case missingApp
case invalidRedirectURL case invalidRedirectURL
@ -89,8 +93,7 @@ import SwiftUI
private func makeURL(scheme: String = "https", private func makeURL(scheme: String = "https",
endpoint: Endpoint, endpoint: Endpoint,
forceVersion: Version? = nil, forceVersion: Version? = nil,
forceServer: String? = nil) -> URL forceServer: String? = nil) throws -> URL {
{
var components = URLComponents() var components = URLComponents()
components.scheme = scheme components.scheme = scheme
components.host = forceServer ?? server components.host = forceServer ?? server
@ -100,7 +103,10 @@ import SwiftUI
components.path += "/api/\(forceVersion?.rawValue ?? version.rawValue)/\(endpoint.path())" components.path += "/api/\(forceVersion?.rawValue ?? version.rawValue)/\(endpoint.path())"
} }
components.queryItems = endpoint.queryItems() components.queryItems = endpoint.queryItems()
return components.url! guard let url = components.url else {
throw ClientError.unexpectedRequest
}
return url
} }
private func makeURLRequest(url: URL, endpoint: Endpoint, httpMethod: String) -> URLRequest { private func makeURLRequest(url: URL, endpoint: Endpoint, httpMethod: String) -> URLRequest {
@ -124,8 +130,8 @@ import SwiftUI
return request return request
} }
private func makeGet(endpoint: Endpoint) -> URLRequest { private func makeGet(endpoint: Endpoint) throws -> URLRequest {
let url = makeURL(endpoint: endpoint) let url = try makeURL(endpoint: endpoint)
return makeURLRequest(url: url, endpoint: endpoint, httpMethod: "GET") return makeURLRequest(url: url, endpoint: endpoint, httpMethod: "GET")
} }
@ -134,7 +140,7 @@ import SwiftUI
} }
public func getWithLink<Entity: Decodable>(endpoint: Endpoint) async throws -> (Entity, LinkHandler?) { public func getWithLink<Entity: Decodable>(endpoint: Endpoint) async throws -> (Entity, LinkHandler?) {
let (data, httpResponse) = try await urlSession.data(for: makeGet(endpoint: endpoint)) let (data, httpResponse) = try await urlSession.data(for: try makeGet(endpoint: endpoint))
var linkHandler: LinkHandler? var linkHandler: LinkHandler?
if let response = httpResponse as? HTTPURLResponse, if let response = httpResponse as? HTTPURLResponse,
let link = response.allHeaderFields["Link"] as? String let link = response.allHeaderFields["Link"] as? String
@ -150,14 +156,14 @@ import SwiftUI
} }
public func post(endpoint: Endpoint, forceVersion: Version? = nil) async throws -> HTTPURLResponse? { public func post(endpoint: Endpoint, forceVersion: Version? = nil) async throws -> HTTPURLResponse? {
let url = makeURL(endpoint: endpoint, forceVersion: forceVersion) let url = try makeURL(endpoint: endpoint, forceVersion: forceVersion)
let request = makeURLRequest(url: url, endpoint: endpoint, httpMethod: "POST") let request = makeURLRequest(url: url, endpoint: endpoint, httpMethod: "POST")
let (_, httpResponse) = try await urlSession.data(for: request) let (_, httpResponse) = try await urlSession.data(for: request)
return httpResponse as? HTTPURLResponse return httpResponse as? HTTPURLResponse
} }
public func patch(endpoint: Endpoint) async throws -> HTTPURLResponse? { public func patch(endpoint: Endpoint) async throws -> HTTPURLResponse? {
let url = makeURL(endpoint: endpoint) let url = try makeURL(endpoint: endpoint)
let request = makeURLRequest(url: url, endpoint: endpoint, httpMethod: "PATCH") let request = makeURLRequest(url: url, endpoint: endpoint, httpMethod: "PATCH")
let (_, httpResponse) = try await urlSession.data(for: request) let (_, httpResponse) = try await urlSession.data(for: request)
return httpResponse as? HTTPURLResponse return httpResponse as? HTTPURLResponse
@ -168,7 +174,7 @@ import SwiftUI
} }
public func delete(endpoint: Endpoint, forceVersion: Version? = nil) async throws -> HTTPURLResponse? { public func delete(endpoint: Endpoint, forceVersion: Version? = nil) async throws -> HTTPURLResponse? {
let url = makeURL(endpoint: endpoint, forceVersion: forceVersion) let url = try makeURL(endpoint: endpoint, forceVersion: forceVersion)
let request = makeURLRequest(url: url, endpoint: endpoint, httpMethod: "DELETE") let request = makeURLRequest(url: url, endpoint: endpoint, httpMethod: "DELETE")
let (_, httpResponse) = try await urlSession.data(for: request) let (_, httpResponse) = try await urlSession.data(for: request)
return httpResponse as? HTTPURLResponse return httpResponse as? HTTPURLResponse
@ -178,7 +184,7 @@ import SwiftUI
method: String, method: String,
forceVersion: Version? = nil) async throws -> Entity forceVersion: Version? = nil) async throws -> Entity
{ {
let url = makeURL(endpoint: endpoint, forceVersion: forceVersion) let url = try makeURL(endpoint: endpoint, forceVersion: forceVersion)
let request = makeURLRequest(url: url, endpoint: endpoint, httpMethod: method) let request = makeURLRequest(url: url, endpoint: endpoint, httpMethod: method)
let (data, httpResponse) = try await urlSession.data(for: request) let (data, httpResponse) = try await urlSession.data(for: request)
logResponseOnError(httpResponse: httpResponse, data: data) logResponseOnError(httpResponse: httpResponse, data: data)
@ -198,7 +204,7 @@ import SwiftUI
public func oauthURL() async throws -> URL { public func oauthURL() async throws -> URL {
let app: InstanceApp = try await post(endpoint: Apps.registerApp) let app: InstanceApp = try await post(endpoint: Apps.registerApp)
critical.withLock { $0.oauthApp = app } critical.withLock { $0.oauthApp = app }
return makeURL(endpoint: Oauth.authorize(clientId: app.clientId)) return try makeURL(endpoint: Oauth.authorize(clientId: app.clientId))
} }
public func continueOauthFlow(url: URL) async throws -> OauthToken { public func continueOauthFlow(url: URL) async throws -> OauthToken {
@ -217,8 +223,8 @@ import SwiftUI
return token return token
} }
public func makeWebSocketTask(endpoint: Endpoint, instanceStreamingURL: URL?) -> URLSessionWebSocketTask { public func makeWebSocketTask(endpoint: Endpoint, instanceStreamingURL: URL?) throws -> URLSessionWebSocketTask {
let url = makeURL(scheme: "wss", endpoint: endpoint, forceServer: instanceStreamingURL?.host) let url = try makeURL(scheme: "wss", endpoint: endpoint, forceServer: instanceStreamingURL?.host)
var subprotocols: [String] = [] var subprotocols: [String] = []
if let oauthToken = critical.withLock({ $0.oauthToken }) { if let oauthToken = critical.withLock({ $0.oauthToken }) {
subprotocols.append(oauthToken.accessToken) subprotocols.append(oauthToken.accessToken)
@ -233,7 +239,7 @@ import SwiftUI
filename: String, filename: String,
data: Data) async throws -> Entity data: Data) async throws -> Entity
{ {
let url = makeURL(endpoint: endpoint, forceVersion: version) let url = try makeURL(endpoint: endpoint, forceVersion: version)
var request = makeURLRequest(url: url, endpoint: endpoint, httpMethod: method) var request = makeURLRequest(url: url, endpoint: endpoint, httpMethod: method)
let boundary = UUID().uuidString let boundary = UUID().uuidString
request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type") request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")