import Foundation #if canImport(Glibc) import Glibc private let socketStreamType = Int32(SOCK_STREAM.rawValue) private let sendFlags = Int32(MSG_NOSIGNAL) #elseif canImport(Darwin) import Darwin private let socketStreamType = SOCK_STREAM private let sendFlags: Int32 = 0 #endif struct StubResponse { var status: Int = 200 var body: String = "" var headers: [String: String] = [:] } final class Router: @unchecked Sendable { private let lock = NSLock() private var routes: [String: [StubResponse]] = [:] private var handler: ((String) -> StubResponse)? private var recorded: [String] = [] func respond(_ path: String, status: Int = 200, body: String, headers: [String: String] = [:]) { lock.lock() defer { lock.unlock() } routes[path] = [StubResponse(status: status, body: body, headers: headers)] } func respond(_ path: String, sequence: [StubResponse]) { lock.lock() defer { lock.unlock() } routes[path] = sequence } func setHandler(_ handler: @escaping (String) -> StubResponse) { lock.lock() defer { lock.unlock() } self.handler = handler } var calls: [String] { lock.lock() defer { lock.unlock() } return recorded } func count(_ prefix: String) -> Int { calls.filter { $0.hasPrefix(prefix) }.count } func next(_ path: String) -> StubResponse { lock.lock() defer { lock.unlock() } recorded.append(path) if let handler { return handler(path) } guard var queued = routes[path], !queued.isEmpty else { return StubResponse(status: 404, body: #"{"erro":"cep_nao_encontrado"}"#) } if queued.count > 1 { let head = queued.removeFirst() routes[path] = queued return head } return queued[0] } } /// A throwaway HTTP/1.1 server on a random loopback port, built on BSD /// sockets because Foundation ships no server on Linux. /// /// Every response closes its connection, so there is no keep-alive state to /// track and every request is a fresh accept. final class TestHTTPServer: @unchecked Sendable { let router = Router() let port: UInt16 private let listener: Int32 private let stopped = NSLock() private var isStopped = false init() throws { signal(SIGPIPE, SIG_IGN) let opened = try TestHTTPServer.openListener() listener = opened.descriptor port = opened.port let thread = Thread { [weak self] in self?.acceptLoop() } thread.stackSize = 1 << 20 thread.start() } private static func openListener() throws -> (descriptor: Int32, port: UInt16) { let descriptor = socket(AF_INET, socketStreamType, 0) guard descriptor >= 0 else { throw TestServerError.failed("socket() failed with errno \(errno)") } var reuse: Int32 = 1 setsockopt( descriptor, SOL_SOCKET, SO_REUSEADDR, &reuse, socklen_t(MemoryLayout.size)) var address = sockaddr_in() address.sin_family = sa_family_t(AF_INET) address.sin_port = in_port_t(UInt16(0).bigEndian) address.sin_addr = in_addr(s_addr: inet_addr("127.0.0.1")) let bound = withUnsafePointer(to: &address) { pointer -> Int32 in pointer.withMemoryRebound(to: sockaddr.self, capacity: 1) { generic in bind(descriptor, generic, socklen_t(MemoryLayout.size)) } } guard bound == 0 else { close(descriptor) throw TestServerError.failed("bind() failed with errno \(errno)") } guard listen(descriptor, 32) == 0 else { close(descriptor) throw TestServerError.failed("listen() failed with errno \(errno)") } var actual = sockaddr_in() var length = socklen_t(MemoryLayout.size) let named = withUnsafeMutablePointer(to: &actual) { pointer -> Int32 in pointer.withMemoryRebound(to: sockaddr.self, capacity: 1) { generic in getsockname(descriptor, generic, &length) } } guard named == 0 else { close(descriptor) throw TestServerError.failed("getsockname() failed with errno \(errno)") } return (descriptor, UInt16(bigEndian: actual.sin_port)) } var baseURL: URL { URL(string: "http://127.0.0.1:\(port)")! } func stop() { stopped.lock() if isStopped { stopped.unlock() return } isStopped = true stopped.unlock() shutdown(listener, Int32(SHUT_RDWR)) close(listener) } private var shuttingDown: Bool { stopped.lock() defer { stopped.unlock() } return isStopped } private func acceptLoop() { while !shuttingDown { let client = accept(listener, nil, nil) if client < 0 { if errno == EINTR { continue } return } let thread = Thread { [weak self] in self?.handle(client) } thread.stackSize = 1 << 20 thread.start() } } private func handle(_ client: Int32) { defer { close(client) } #if canImport(Darwin) var noSigPipe: Int32 = 1 setsockopt( client, SOL_SOCKET, SO_NOSIGPIPE, &noSigPipe, socklen_t(MemoryLayout.size)) #endif guard let path = readRequestPath(client) else { return } let stub = router.next(path) write(client, response(for: stub)) } private func readRequestPath(_ client: Int32) -> String? { var received: [UInt8] = [] var chunk = [UInt8](repeating: 0, count: 4096) while headerEnd(of: received) == nil { let read = recv(client, &chunk, chunk.count, 0) if read <= 0 { break } received.append(contentsOf: chunk[0.. 1 << 20 { break } } guard headerEnd(of: received) != nil else { return nil } var lineEnd = received.count for index in 0..= 2 else { return nil } return String(parts[1]) } private func headerEnd(of bytes: [UInt8]) -> Int? { guard bytes.count >= 4 else { return nil } for index in 0...(bytes.count - 4) { if bytes[index] == 0x0D, bytes[index + 1] == 0x0A, bytes[index + 2] == 0x0D, bytes[index + 3] == 0x0A { return index } } return nil } private func response(for stub: StubResponse) -> [UInt8] { let body = Array(stub.body.utf8) var head = "HTTP/1.1 \(stub.status) \(reason(stub.status))\r\n" for (name, value) in stub.headers { head += "\(name): \(value)\r\n" } head += "Content-Length: \(body.count)\r\n" head += "Connection: close\r\n\r\n" return Array(head.utf8) + body } private func reason(_ status: Int) -> String { switch status { case 200: return "OK" case 400: return "Bad Request" case 404: return "Not Found" case 429: return "Too Many Requests" case 500: return "Internal Server Error" case 502: return "Bad Gateway" case 503: return "Service Unavailable" case 504: return "Gateway Timeout" default: return "Status" } } private func write(_ client: Int32, _ bytes: [UInt8]) { var offset = 0 while offset < bytes.count { let written = bytes.withUnsafeBufferPointer { buffer -> Int in guard let base = buffer.baseAddress else { return -1 } return send(client, base + offset, bytes.count - offset, sendFlags) } if written <= 0 { return } offset += written } } } enum TestServerError: Error { case failed(String) }