import Foundation import Network import OSLog struct HTTPRequest: Equatable { var method: String var path: String var headers: [String: String] var body: [UInt8] } struct HTTPResponse { var status: Int var contentType: String var body: [UInt8] static func text(_ status: Int, _ text: String) -> HTTPResponse { HTTPResponse(status: status, contentType: "text/plain; charset=utf-8", body: Array(text.utf8)) } static func json(_ value: some Encodable) -> HTTPResponse { HTTPResponse(status: 200, contentType: "application/json", body: Array((try? JSONEncoder().encode(value)) ?? Data())) } var serialized: [UInt8] { let reason = [200: "OK", 204: "No Content", 400: "Bad Request", 404: "Not Found", 405: "Method Not Allowed", 413: "Payload Too Large", 503: "Service Unavailable"][status] ?? "Error" let head = "HTTP/1.1 \(status) \(reason)\r\nContent-Type: \(contentType)\r\nContent-Length: \(body.count)\r\n" + "Cache-Control: no-store\r\nConnection: close\r\n\r\n" return Array(head.utf8) + body } } /// Incremental HTTP/1.1 request parser (no chunked bodies; the phone page only sends small JSON). struct HTTPRequestParser { enum ParseError: Error { case malformed, tooLarge } static let maxHeader = 8 * 1024 static let maxBody = 64 * 1024 private var buffer: [UInt8] = [] /// Appends bytes; returns the request once it is complete. mutating func append(_ bytes: [UInt8]) throws -> HTTPRequest? { buffer += bytes guard let headerEnd = Self.find([13, 10, 13, 10], in: buffer) else { if buffer.count > Self.maxHeader { throw ParseError.tooLarge } return nil } guard let head = String(bytes: buffer[0..= 0 else { throw ParseError.malformed } guard length <= Self.maxBody else { throw ParseError.tooLarge } let bodyStart = headerEnd + 4 guard buffer.count >= bodyStart + length else { return nil } let path = String(requestLine[1].split(separator: "?", maxSplits: 1).first ?? "/") return HTTPRequest(method: String(requestLine[0]), path: path, headers: headers, body: Array(buffer[bodyStart.. Int? { guard haystack.count >= needle.count else { return nil } for i in 0...(haystack.count - needle.count) where haystack[i] == needle[0] { if Array(haystack[i.. HTTPResponse private let queue = DispatchQueue(label: "studio.zielke.musubi.intercom.http") private let log = Logger(subsystem: "studio.zielke.musubi", category: "intercom") private var listener: NWListener? private let handler: Handler init(handler: @escaping Handler) { self.handler = handler } func start(interface: NWInterface, port: UInt16) async throws { let parameters = NWParameters.tcp parameters.requiredInterface = interface parameters.allowLocalEndpointReuse = true parameters.includePeerToPeer = false let listener = try NWListener(using: parameters, on: NWEndpoint.Port(rawValue: port) ?? .any) let once = OnceFlag() try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in listener.stateUpdateHandler = { [log] state in switch state { case .ready: if once.claim() { continuation.resume() } case .failed(let error): log.error("HTTP failed: \(error.localizedDescription, privacy: .public)") if once.claim() { continuation.resume(throwing: error) } default: break } } listener.newConnectionHandler = { [weak self] connection in self?.serve(connection) } listener.start(queue: queue) } self.listener = listener } func stop() { listener?.cancel() listener = nil } private func serve(_ connection: NWConnection) { connection.start(queue: queue) read(connection, parser: ParserBox()) } private func read(_ connection: NWConnection, parser: ParserBox) { connection.receive(minimumIncompleteLength: 1, maximumLength: 16 * 1024) { [weak self] data, _, isComplete, error in guard let self else { return } do { if let data, let request = try parser.value.append(Array(data)) { // Signalling may block briefly (session setup); keep the HTTP queue free. DispatchQueue.global(qos: .userInitiated).async { self.respond(self.handler(request), on: connection) } return } } catch HTTPRequestParser.ParseError.tooLarge { respond(.text(413, "Zu groß."), on: connection) return } catch { respond(.text(400, "Ungültige Anfrage."), on: connection) return } if isComplete || error != nil { connection.cancel() } else { read(connection, parser: parser) } } } private func respond(_ response: HTTPResponse, on connection: NWConnection) { connection.send(content: Data(response.serialized), completion: .contentProcessed { _ in connection.cancel() }) } } /// Per-connection parser state; only touched on the HTTP queue. private final class ParserBox: @unchecked Sendable { var value = HTTPRequestParser() }