import Foundation #if canImport(Darwin) import Darwin #elseif canImport(Glibc) import Glibc #elseif canImport(Musl) import Musl #endif private let requestReadTimeoutMilliseconds: Int32 = 5000 struct CLILocalHTTPRequest { let method: String let target: String let host: String let path: String let queryItems: [String: String] static func parse(_ data: Data) -> Result { guard let raw = String(data: data, encoding: .utf8), let firstLine = raw.components(separatedBy: "\r\n").first else { return .failure(.invalidRequest) } let parts = firstLine.split(separator: " ") guard parts.count >= 3 else { return .failure(.invalidRequest) } let method = String(parts[0]).uppercased() let target = String(parts[1]) guard target.hasPrefix("/") else { return .failure(.invalidRequest) } let headerResult = Self.parseHeaders(raw) let host: String switch headerResult { case let .success(headers): let hosts = headers.compactMap { name, value in name.lowercased() == "host" ? value : nil } guard let candidate = hosts.first else { return .failure(.missingHost) } guard hosts.count == 1 else { return .failure(.duplicateHost) } guard Self.isAllowedLoopbackHost(candidate) else { return .failure(.disallowedHost) } host = candidate case let .failure(error): return .failure(error) } let components = URLComponents(string: "http://localhost\(target)") let path = components?.path ?? target var queryItems: [String: String] = [:] for item in components?.queryItems ?? [] { if let value = item.value { queryItems[item.name] = value } } return .success(CLILocalHTTPRequest( method: method, target: target, host: host, path: path, queryItems: queryItems)) } private static func parseHeaders(_ raw: String) -> Result<[(String, String)], CLILocalHTTPRequestParseError> { let lines = raw.components(separatedBy: "\r\n") var headers: [(String, String)] = [] for line in lines.dropFirst() { if line.isEmpty { break } guard let separator = line.firstIndex(of: ":") else { return .failure(.invalidRequest) } let name = String(line[.. Bool { let trimmed = host.trimmingCharacters(in: .whitespacesAndNewlines) guard !trimmed.isEmpty, !trimmed.contains(",") else { return false } let hostWithoutPort: String if trimmed.hasPrefix("[") { guard let closingBracket = trimmed.firstIndex(of: "]") else { return false } hostWithoutPort = String(trimmed[...closingBracket]) let remainder = trimmed[trimmed.index(after: closingBracket)...] guard remainder.isEmpty || Self.isValidPortSuffix(String(remainder)) else { return false } } else { let segments = trimmed.split(separator: ":", omittingEmptySubsequences: false) switch segments.count { case 1: hostWithoutPort = String(segments[0]) case 2: guard Self.isValidPort(String(segments[1])) else { return false } hostWithoutPort = String(segments[0]) default: return false } } switch hostWithoutPort.lowercased() { case "127.0.0.1", "localhost", "localhost.", "[::1]": return true default: return false } } private static func isValidPortSuffix(_ raw: String) -> Bool { guard raw.hasPrefix(":") else { return false } return self.isValidPort(String(raw.dropFirst())) } private static func isValidPort(_ raw: String) -> Bool { guard let port = Int(raw), port > 0, port <= Int(UInt16.max) else { return false } return true } } enum CLILocalHTTPRequestParseError: Error, Equatable { case invalidRequest case missingHost case duplicateHost case disallowedHost } enum CLIHTTPStatus { case ok case badRequest case forbidden case notFound case methodNotAllowed case internalServerError case gatewayTimeout var code: Int { switch self { case .ok: 200 case .badRequest: 400 case .forbidden: 403 case .notFound: 404 case .methodNotAllowed: 405 case .internalServerError: 500 case .gatewayTimeout: 504 } } var reason: String { switch self { case .ok: "OK" case .badRequest: "Bad Request" case .forbidden: "Forbidden" case .notFound: "Not Found" case .methodNotAllowed: "Method Not Allowed" case .internalServerError: "Internal Server Error" case .gatewayTimeout: "Gateway Timeout" } } } struct CLILocalHTTPResponse { let status: CLIHTTPStatus let body: Data let contentType: String let usageCacheKeys: [String?]? init( status: CLIHTTPStatus, body: Data, contentType: String = "application/json; charset=utf-8", usageCacheKeys: [String?]? = nil) { self.status = status self.body = body self.contentType = contentType self.usageCacheKeys = usageCacheKeys } var serialized: Data { var headers = "HTTP/1.1 \(self.status.code) \(self.status.reason)\r\n" headers += "Content-Type: \(self.contentType)\r\n" headers += "Content-Length: \(self.body.count)\r\n" headers += "Connection: close\r\n" headers += "\r\n" var data = Data(headers.utf8) data.append(self.body) return data } } final class CLILocalHTTPServer: @unchecked Sendable { typealias Handler = @Sendable (CLILocalHTTPRequest) async -> CLILocalHTTPResponse private let host: String private let port: UInt16 private let handler: Handler private let stateLock = NSLock() private var listeningFD: Int32? private var stopRequested = false init(host: String, port: UInt16, handler: @escaping Handler) { self.host = host self.port = port self.handler = handler } func stop() { self.stateLock.lock() self.stopRequested = true self.stateLock.unlock() } func run(onListening: @Sendable () -> Void = {}) async throws { ignoreSIGPIPE() #if canImport(Darwin) let streamType = SOCK_STREAM #elseif canImport(Glibc) let streamType = Int32(SOCK_STREAM.rawValue) #elseif canImport(Musl) let streamType = Int32(SOCK_STREAM) #endif let serverFD = socket(AF_INET, streamType, 0) guard serverFD >= 0 else { throw POSIXError(POSIXErrorCode(rawValue: errno) ?? .EIO) } var ownsServerFD = true defer { if ownsServerFD { closeSocket(serverFD) } } var reuse: Int32 = 1 setsockopt( serverFD, SOL_SOCKET, SO_REUSEADDR, &reuse, socklen_t(MemoryLayout.size)) var address = sockaddr_in() #if canImport(Darwin) address.sin_len = UInt8(MemoryLayout.size) #endif address.sin_family = sa_family_t(AF_INET) address.sin_port = self.port.bigEndian guard inet_pton(AF_INET, self.host, &address.sin_addr) == 1 else { throw POSIXError(.EADDRNOTAVAIL) } let bound = withUnsafePointer(to: &address) { pointer in pointer.withMemoryRebound(to: sockaddr.self, capacity: 1) { socketAddress in bind(serverFD, socketAddress, socklen_t(MemoryLayout.size)) } } guard bound == 0 else { throw POSIXError(POSIXErrorCode(rawValue: errno) ?? .EIO) } guard listen(serverFD, 16) == 0 else { throw POSIXError(POSIXErrorCode(rawValue: errno) ?? .EIO) } guard self.installListeningFD(serverFD) else { return } ownsServerFD = false defer { if self.releaseListeningFD(serverFD) { closeSocket(serverFD) } } onListening() while !self.isStopRequested { guard waitForReadable(serverFD, timeoutMilliseconds: 250) else { continue } var clientAddress = sockaddr() var clientLength = socklen_t(MemoryLayout.size) let clientFD = accept(serverFD, &clientAddress, &clientLength) guard clientFD >= 0 else { if self.isStopRequested { return } if errno == EINTR || errno == EAGAIN || errno == EWOULDBLOCK || errno == ECONNABORTED { continue } throw POSIXError(POSIXErrorCode(rawValue: errno) ?? .EIO) } let handler = self.handler Task { defer { closeSocket(clientFD) } await handleClient(clientFD, handler: handler) } } } private var isStopRequested: Bool { self.stateLock.lock() let value = self.stopRequested self.stateLock.unlock() return value } private func installListeningFD(_ fd: Int32) -> Bool { self.stateLock.lock() defer { self.stateLock.unlock() } guard !self.stopRequested else { return false } self.listeningFD = fd return true } private func releaseListeningFD(_ fd: Int32) -> Bool { self.stateLock.lock() defer { self.stateLock.unlock() } guard self.listeningFD == fd else { return false } self.listeningFD = nil return true } } private func handleClient( _ clientFD: Int32, handler: @Sendable (CLILocalHTTPRequest) async -> CLILocalHTTPResponse) async { let request: CLILocalHTTPRequest switch readRequest(clientFD) { case let .success(parsedRequest): request = parsedRequest case .failure(.disallowedHost): sendResponse( CLILocalHTTPResponse( status: .forbidden, body: Data(#"{"error":"forbidden host"}"#.utf8)), to: clientFD) return case .failure: sendResponse( CLILocalHTTPResponse( status: .badRequest, body: Data(#"{"error":"invalid request"}"#.utf8)), to: clientFD) return } let response = await handler(request) sendResponse(response, to: clientFD) } private func readRequest(_ fd: Int32) -> Result { var data = Data() var buffer = [UInt8](repeating: 0, count: 4096) let bufferSize = buffer.count var sawHeaderEnd = false while data.count < 16384 { guard waitForReadable(fd, timeoutMilliseconds: requestReadTimeoutMilliseconds) else { return .failure(.invalidRequest) } let count = buffer.withUnsafeMutableBytes { rawBuffer in recv(fd, rawBuffer.baseAddress, bufferSize, 0) } guard count > 0 else { break } data.append(buffer, count: count) if data.range(of: Data("\r\n\r\n".utf8)) != nil { sawHeaderEnd = true break } } guard sawHeaderEnd else { return .failure(.invalidRequest) } return CLILocalHTTPRequest.parse(data) } private func sendResponse(_ response: CLILocalHTTPResponse, to fd: Int32) { let data = response.serialized data.withUnsafeBytes { rawBuffer in guard let base = rawBuffer.baseAddress else { return } var sent = 0 while sent < data.count { let count = send(fd, base.advanced(by: sent), data.count - sent, sendNoSignalFlags()) guard count > 0 else { break } sent += count } } } private func waitForReadable(_ fd: Int32, timeoutMilliseconds: Int32) -> Bool { var pollFD = pollfd(fd: fd, events: Int16(POLLIN), revents: 0) while true { let result = poll(&pollFD, 1, timeoutMilliseconds) if result > 0 { return (pollFD.revents & Int16(POLLIN)) != 0 } if result == -1, errno == EINTR { continue } return false } } private func sendNoSignalFlags() -> Int32 { #if canImport(Darwin) 0 #else Int32(MSG_NOSIGNAL) #endif } private func ignoreSIGPIPE() { #if canImport(Darwin) _ = Darwin.signal(SIGPIPE, SIG_IGN) #elseif canImport(Glibc) _ = Glibc.signal(SIGPIPE, SIG_IGN) #elseif canImport(Musl) _ = Musl.signal(SIGPIPE, SIG_IGN) #endif } private func closeSocket(_ fd: Int32) { #if canImport(Darwin) Darwin.close(fd) #elseif canImport(Glibc) Glibc.close(fd) #elseif canImport(Musl) Musl.close(fd) #endif }