Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions apps/iphone/Yanami/Models/AppModels.swift
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,32 @@ struct ServerProfile: Codable, Equatable, Identifiable {
var requires2FA = false
var authType = AuthType.password
var customHeaders: [CustomHeader] = []
var allowInsecureTLS = false
var createdAt = Date()

enum CodingKeys: String, CodingKey {
case id, name, baseURL, username, password, apiKey, sessionToken, requires2FA
case authType, customHeaders, allowInsecureTLS, createdAt
}

init() {}

init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decodeIfPresent(UUID.self, forKey: .id) ?? UUID()
name = try container.decodeIfPresent(String.self, forKey: .name) ?? "My Komari"
baseURL = try container.decodeIfPresent(String.self, forKey: .baseURL) ?? "https://"
username = try container.decodeIfPresent(String.self, forKey: .username) ?? ""
password = try container.decodeIfPresent(String.self, forKey: .password) ?? ""
apiKey = try container.decodeIfPresent(String.self, forKey: .apiKey) ?? ""
sessionToken = try container.decodeIfPresent(String.self, forKey: .sessionToken) ?? ""
requires2FA = try container.decodeIfPresent(Bool.self, forKey: .requires2FA) ?? false
authType = try container.decodeIfPresent(AuthType.self, forKey: .authType) ?? .password
customHeaders = try container.decodeIfPresent([CustomHeader].self, forKey: .customHeaders) ?? []
allowInsecureTLS = try container.decodeIfPresent(Bool.self, forKey: .allowInsecureTLS) ?? false
createdAt = try container.decodeIfPresent(Date.self, forKey: .createdAt) ?? Date()
}

var normalizedBaseURL: String {
baseURL.trimmingCharacters(in: .whitespacesAndNewlines).trimmedTrailingSlash()
}
Expand Down
53 changes: 45 additions & 8 deletions apps/iphone/Yanami/Services/KomariClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ struct KomariClient {
}

let request = try makeRequest(path: "/api/login", method: "POST", jsonBody: body)
let (data, response) = try await URLSession.shared.data(for: request)
let (data, response) = try await data(for: request)
guard let httpResponse = response as? HTTPURLResponse else {
throw KomariClientError.invalidResponse
}
Expand Down Expand Up @@ -106,7 +106,7 @@ struct KomariClient {
let uuid = try normalizedUUID(uuid)
let encodedUuid = uuid.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? uuid
let request = try makeRequest(path: "/api/recent/\(encodedUuid)", method: "GET", token: token)
let (data, response) = try await URLSession.shared.data(for: request)
let (data, response) = try await data(for: request)
guard let httpResponse = response as? HTTPURLResponse else {
throw KomariClientError.invalidResponse
}
Expand All @@ -125,22 +125,22 @@ struct KomariClient {

func getClients(token: String) async throws -> [ManagedClient] {
let request = try makeRequest(path: "/api/admin/client/list", method: "GET", token: token)
let (data, response) = try await URLSession.shared.data(for: request)
let (data, response) = try await data(for: request)
try validateAdminResponse(data: data, response: response, path: "/api/admin/client/list")
return try decodeAdminPayload([ManagedClient].self, from: data)
}

func deleteClient(token: String, uuid: String) async throws {
let uuid = try normalizedUUID(uuid)
let request = try makeRequest(path: "/api/admin/client/\(uuid)/remove", method: "POST", token: token)
let (data, response) = try await URLSession.shared.data(for: request)
let (data, response) = try await data(for: request)
try validateAdminResponse(data: data, response: response, path: "/api/admin/client/\(uuid)/remove")
}

func getClientToken(token: String, uuid: String) async throws -> String {
let uuid = try normalizedUUID(uuid)
let request = try makeRequest(path: "/api/admin/client/\(uuid)/token", method: "GET", token: token)
let (data, response) = try await URLSession.shared.data(for: request)
let (data, response) = try await data(for: request)
try validateAdminResponse(data: data, response: response, path: "/api/admin/client/\(uuid)/token")
let payload = try decodeAdminPayload([String: String].self, from: data)
return payload["token"] ?? ""
Expand All @@ -150,14 +150,14 @@ struct KomariClient {

func getPingTasks(token: String) async throws -> [AdminPingTask] {
let request = try makeRequest(path: "/api/admin/ping/", method: "GET", token: token)
let (data, response) = try await URLSession.shared.data(for: request)
let (data, response) = try await data(for: request)
try validateAdminResponse(data: data, response: response, path: "/api/admin/ping/")
return try decodeAdminPayload([AdminPingTask].self, from: data)
}

func deletePingTask(token: String, ids: [Int]) async throws {
let request = try makeRequest(path: "/api/admin/ping/delete", method: "POST", jsonBody: ["id": ids], token: token)
let (data, response) = try await URLSession.shared.data(for: request)
let (data, response) = try await data(for: request)
try validateAdminResponse(data: data, response: response, path: "/api/admin/ping/delete")
}

Expand Down Expand Up @@ -297,7 +297,7 @@ struct KomariClient {
body["params"] = params
}
let request = try makeRequest(path: "/api/rpc2", method: "POST", jsonBody: body, token: token)
let (data, response) = try await URLSession.shared.data(for: request)
let (data, response) = try await data(for: request)
guard let httpResponse = response as? HTTPURLResponse else {
throw KomariClientError.invalidResponse
}
Expand Down Expand Up @@ -354,6 +354,16 @@ struct KomariClient {
return request
}

private func data(for request: URLRequest) async throws -> (Data, URLResponse) {
guard profile.allowInsecureTLS else {
return try await URLSession.shared.data(for: request)
}
let delegate = KomariInsecureTLSDelegate(allowedHost: request.url?.host)
let session = URLSession(configuration: .default, delegate: delegate, delegateQueue: nil)
defer { session.finishTasksAndInvalidate() }
return try await session.data(for: request)
}

private func normalizedUUID(_ uuid: String) throws -> String {
let cleaned = uuid.trimmingCharacters(in: .whitespacesAndNewlines)
guard !cleaned.isEmpty else {
Expand Down Expand Up @@ -386,6 +396,33 @@ struct KomariClient {
}()
}

final class KomariInsecureTLSDelegate: NSObject, URLSessionDelegate {
private let allowedHost: String?

init(allowedHost: String?) {
self.allowedHost = allowedHost?.lowercased()
}

func urlSession(
_ session: URLSession,
didReceive challenge: URLAuthenticationChallenge,
completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void
) {
guard challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust,
let serverTrust = challenge.protectionSpace.serverTrust,
isAllowedHost(challenge.protectionSpace.host) else {
completionHandler(.performDefaultHandling, nil)
return
}
completionHandler(.useCredential, URLCredential(trust: serverTrust))
}

private func isAllowedHost(_ host: String) -> Bool {
guard let allowedHost else { return false }
return host.lowercased() == allowedHost
}
}

enum KomariClientError: LocalizedError {
case invalidConfiguration(String)
case invalidResponse
Expand Down
12 changes: 11 additions & 1 deletion apps/iphone/Yanami/Views/NodeDetailView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -635,6 +635,7 @@ final class SshTerminalViewModel: ObservableObject {
private let server: ServerProfile
private let token: String
private var urlSession: URLSession?
private var urlSessionDelegate: URLSessionDelegate?
private var webSocketTask: URLSessionWebSocketTask?
private var heartbeatTimer: Timer?
private var didSendInitialDirectory = false
Expand Down Expand Up @@ -677,7 +678,15 @@ final class SshTerminalViewModel: ObservableObject {
}
request.setValue(buildOrigin(), forHTTPHeaderField: "Origin")

let session = URLSession(configuration: .default)
let session: URLSession
if server.allowInsecureTLS {
let delegate = KomariInsecureTLSDelegate(allowedHost: url.host)
urlSessionDelegate = delegate
session = URLSession(configuration: .default, delegate: delegate, delegateQueue: nil)
} else {
urlSessionDelegate = nil
session = URLSession(configuration: .default)
}
urlSession = session
let task = session.webSocketTask(with: request)
webSocketTask = task
Expand Down Expand Up @@ -856,6 +865,7 @@ final class SshTerminalViewModel: ObservableObject {
webSocketTask = nil
urlSession?.invalidateAndCancel()
urlSession = nil
urlSessionDelegate = nil
didSendInitialDirectory = false
}
}
Expand Down
6 changes: 6 additions & 0 deletions apps/iphone/Yanami/Views/ServerFormView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ struct ServerFormView: View {
.textInputAutocapitalization(.never)
.keyboardType(.URL)
.autocorrectionDisabled()
Toggle("Allow insecure TLS", isOn: $draft.allowInsecureTLS)
if draft.allowInsecureTLS {
Text("Use only for self-signed or private certificates you trust.")
.font(.caption)
.foregroundStyle(.secondary)
}
}

Section("Authentication") {
Expand Down
Loading