// This source file is part of the Swift.org open source project // // Copyright (c) 2014 - 2016 Apple Inc. and the Swift project authors // Licensed under Apache License v2.0 with Runtime Library Exception // // See http://swift.org/LICENSE.txt for license information // See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors // //This is a very rudimentary HTTP server written plainly for testing URLSession. //It is not concurrent. It listens on a port, reads once and writes back only once. //We can make it better everytime we need more functionality to test different aspects of URLSession. import Dispatch #if canImport(MSVCRT) import MSVCRT import WinSDK #elseif canImport(Darwin) import Darwin #elseif canImport(Glibc) import Glibc #endif #if !os(Windows) typealias SOCKET = Int32 #endif public let globalDispatchQueue = DispatchQueue.global() public let dispatchQueueMake: (String) -> DispatchQueue = { DispatchQueue.init(label: $0) } public let dispatchGroupMake: () -> DispatchGroup = DispatchGroup.init struct _HTTPUtils { static let CRLF = "\r\n" static let VERSION = "HTTP/1.1" static let SPACE = " " static let CRLF2 = CRLF + CRLF static let EMPTY = "" } extension UInt16 { public init(networkByteOrder input: UInt16) { self.init(bigEndian: input) } } class _TCPSocket { #if !os(Windows) private let sendFlags: CInt #endif private var listenSocket: SOCKET! private var socketAddress = UnsafeMutablePointer<sockaddr_in>.allocate(capacity: 1) private var connectionSocket: SOCKET? private func isNotNegative(r: CInt) -> Bool { return r != -1 } private func isZero(r: CInt) -> Bool { return r == 0 } private func attempt<T>(_ name: String, file: String = #file, line: UInt = #line, valid: (T) -> Bool, _ b: @autoclosure () -> T) throws -> T { let r = b() guard valid(r) else { throw ServerError(operation: name, errno: errno, file: file, line: line) } return r } public private(set) var port: UInt16 init(port: UInt16?) throws { #if !os(Windows) #if os(Linux) || os(Android) || os(FreeBSD) sendFlags = CInt(MSG_NOSIGNAL) #else sendFlags = 0 #endif #endif self.port = port ?? 0 #if os(Windows) listenSocket = try attempt("WSASocketW", valid: { $0 != INVALID_SOCKET }, WSASocketW(AF_INET, SOCK_STREAM, IPPROTO_TCP.rawValue, nil, 0, DWORD(WSA_FLAG_OVERLAPPED))) var value: Int8 = 1 _ = try attempt("setsockopt", valid: { $0 == 0 }, setsockopt(listenSocket, SOL_SOCKET, SO_REUSEADDR, &value, Int32(MemoryLayout.size(ofValue: value)))) #else #if os(Linux) && !os(Android) let SOCKSTREAM = Int32(SOCK_STREAM.rawValue) #else let SOCKSTREAM = SOCK_STREAM #endif listenSocket = try attempt("socket", valid: { $0 >= 0 }, socket(AF_INET, SOCKSTREAM, Int32(IPPROTO_TCP))) var on: CInt = 1 _ = try attempt("setsockopt", valid: { $0 == 0 }, setsockopt(listenSocket, SOL_SOCKET, SO_REUSEADDR, &on, socklen_t(MemoryLayout<CInt>.size))) #endif let sa = createSockaddr(port) socketAddress.initialize(to: sa) try socketAddress.withMemoryRebound(to: sockaddr.self, capacity: MemoryLayout<sockaddr>.size, { let addr = UnsafePointer<sockaddr>($0) _ = try attempt("bind", valid: isZero, bind(listenSocket, addr, socklen_t(MemoryLayout<sockaddr>.size))) _ = try attempt("listen", valid: isZero, listen(listenSocket, SOMAXCONN)) }) var actualSA = sockaddr_in() withUnsafeMutablePointer(to: &actualSA) { ptr in ptr.withMemoryRebound(to: sockaddr.self, capacity: 1) { (ptr: UnsafeMutablePointer<sockaddr>) in var len = socklen_t(MemoryLayout<sockaddr>.size) getsockname(listenSocket, ptr, &len) } } self.port = UInt16(networkByteOrder: actualSA.sin_port) } private func createSockaddr(_ port: UInt16?) -> sockaddr_in { // Listen on the loopback address so that OSX doesnt pop up a dialog // asking to accept incoming connections if the firewall is enabled. let addr = UInt32(INADDR_LOOPBACK).bigEndian let netPort = UInt16(bigEndian: port ?? 0) #if os(Android) return sockaddr_in(sin_family: sa_family_t(AF_INET), sin_port: netPort, sin_addr: in_addr(s_addr: addr), __pad: (0,0,0,0,0,0,0,0)) #elseif os(Linux) return sockaddr_in(sin_family: sa_family_t(AF_INET), sin_port: netPort, sin_addr: in_addr(s_addr: addr), sin_zero: (0,0,0,0,0,0,0,0)) #elseif os(Windows) return sockaddr_in(sin_family: ADDRESS_FAMILY(AF_INET), sin_port: USHORT(netPort), sin_addr: IN_ADDR(S_un: in_addr.__Unnamed_union_S_un(S_addr: addr)), sin_zero: (CHAR(0), CHAR(0), CHAR(0), CHAR(0), CHAR(0), CHAR(0), CHAR(0), CHAR(0))) #else return sockaddr_in(sin_len: 0, sin_family: sa_family_t(AF_INET), sin_port: netPort, sin_addr: in_addr(s_addr: addr), sin_zero: (0,0,0,0,0,0,0,0)) #endif } func acceptConnection(notify: ServerSemaphore) throws { try socketAddress.withMemoryRebound(to: sockaddr.self, capacity: MemoryLayout<sockaddr>.size, { let addr = UnsafeMutablePointer<sockaddr>($0) var sockLen = socklen_t(MemoryLayout<sockaddr>.size) #if os(Windows) connectionSocket = try attempt("WSAAccept", valid: { $0 != INVALID_SOCKET }, WSAAccept(listenSocket, addr, &sockLen, nil, 0)) #else connectionSocket = try attempt("accept", valid: { $0 >= 0 }, accept(listenSocket, addr, &sockLen)) #endif #if canImport(Darwin) // Disable SIGPIPEs when writing to closed sockets var on: CInt = 1 if let connectionSocket = connectionSocket { _ = try attempt("setsockopt", valid: isZero, setsockopt(connectionSocket, SOL_SOCKET, SO_NOSIGPIPE, &on, socklen_t(MemoryLayout<CInt>.size))) } #endif }) } func readData() throws -> String { guard let connectionSocket = connectionSocket else { throw InternalServerError.socketAlreadyClosed } var buffer = [CChar](repeating: 0, count: 4096) #if os(Windows) var dwNumberOfBytesRecieved: DWORD = 0; try buffer.withUnsafeMutableBufferPointer { var wsaBuffer: WSABUF = WSABUF(len: ULONG($0.count), buf: $0.baseAddress) var flags: DWORD = 0 _ = try attempt("WSARecv", valid: { $0 != SOCKET_ERROR }, WSARecv(connectionSocket, &wsaBuffer, 1, &dwNumberOfBytesRecieved, &flags, nil, nil)) } #else _ = try attempt("read", valid: { $0 >= 0 }, read(connectionSocket, &buffer, buffer.count)) #endif return String(cString: &buffer) } func writeRawData(_ data: Data) throws { guard let connectionSocket = connectionSocket else { throw InternalServerError.socketAlreadyClosed } #if os(Windows) _ = try data.withUnsafeBytes { var dwNumberOfBytesSent: DWORD = 0 var wsaBuffer: WSABUF = WSABUF(len: ULONG(data.count), buf: UnsafeMutablePointer<CHAR>(mutating: $0.bindMemory(to: CHAR.self).baseAddress)) _ = try attempt("WSASend", valid: { $0 != SOCKET_ERROR }, WSASend(connectionSocket, &wsaBuffer, 1, &dwNumberOfBytesSent, 0, nil, nil)) } #else _ = try data.withUnsafeBytes { ptr in try attempt("send", valid: isNotNegative, CInt(send(connectionSocket, ptr.baseAddress!, data.count, sendFlags))) } #endif } private func _send(_ bytes: [UInt8]) throws -> Int { guard let connectionSocket = connectionSocket else { throw InternalServerError.socketAlreadyClosed } #if os(Windows) return try bytes.withUnsafeBytes { var dwNumberOfBytesSent: DWORD = 0 var wsaBuffer: WSABUF = WSABUF(len: ULONG(bytes.count), buf: UnsafeMutablePointer<CHAR>(mutating: $0.bindMemory(to: CHAR.self).baseAddress)) return try Int(attempt("WSASend", valid: { $0 != SOCKET_ERROR }, WSASend(connectionSocket, &wsaBuffer, 1, &dwNumberOfBytesSent, 0, nil, nil))) } #else return try bytes.withUnsafeBufferPointer { try attempt("send", valid: { $0 >= 0 }, send(connectionSocket, $0.baseAddress, $0.count, sendFlags)) } #endif } func writeData(header: String, bodyData: Data, sendDelay: TimeInterval? = nil, bodyChunks: Int? = nil) throws { _ = try _send(Array(header.utf8)) if let sendDelay = sendDelay, let bodyChunks = bodyChunks { let count = max(1, Int(Double(bodyData.count) / Double(bodyChunks))) for startIndex in stride(from: 0, to: bodyData.count, by: count) { Thread.sleep(forTimeInterval: sendDelay) let endIndex = min(startIndex + count, bodyData.count) try bodyData.withUnsafeBytes { (ptr: UnsafeRawBufferPointer) -> Void in let chunk = UnsafeRawBufferPointer(rebasing: ptr[startIndex..<endIndex]) _ = try _send(Array(chunk.bindMemory(to: UInt8.self))) } } } else { try bodyData.withUnsafeBytes { (ptr: UnsafeRawBufferPointer) -> Void in _ = try _send(Array(ptr.bindMemory(to: UInt8.self))) } } } func closeClient() { if let connectionSocket = self.connectionSocket { #if os(Windows) closesocket(connectionSocket) #else close(connectionSocket) #endif self.connectionSocket = nil } } func shutdownListener() { closeClient() #if os(Windows) shutdown(listenSocket, SD_BOTH) closesocket(listenSocket) #else shutdown(listenSocket, CInt(SHUT_RDWR)) close(listenSocket) #endif } } class _HTTPServer { let socket: _TCPSocket var willReadAgain = false var port: UInt16 { get { return self.socket.port } } init(port: UInt16?) throws { socket = try _TCPSocket(port: port) } public class func create(port: UInt16?) throws -> _HTTPServer { return try _HTTPServer(port: port) } public func listen(notify: ServerSemaphore) throws { try socket.acceptConnection(notify: notify) } public func stop() { if !willReadAgain { socket.closeClient() socket.shutdownListener() } } public func request() throws -> _HTTPRequest { var request = try _HTTPRequest(request: socket.readData()) if Int(request.getHeader(for: "Content-Length") ?? "0") ?? 0 > 0 || (request.getHeader(for: "Transfer-Encoding") ?? "").lowercased() == "chunked" { // According to RFC7230 https://tools.ietf.org/html/rfc7230#section-3 // We receive messageBody after the headers, so we need read from socket minimum 2 times // // HTTP-message structure // // start-line // *( header-field CRLF ) // CRLF // [ message-body ] // We receives '{numofbytes}\r\n{data}\r\n' // TODO read data until the end let substr = try socket.readData().split(separator: "\r\n") if substr.count >= 2 { request.messageBody = String(substr[1]) } } return request } public func respond(with response: _HTTPResponse, startDelay: TimeInterval? = nil, sendDelay: TimeInterval? = nil, bodyChunks: Int? = nil) throws { if let delay = startDelay { Thread.sleep(forTimeInterval: delay) } do { try self.socket.writeData(header: response.header, bodyData: response.bodyData, sendDelay: sendDelay, bodyChunks: bodyChunks) } catch { } } func respondWithBrokenResponses(uri: String) throws { let responseData: Data switch uri { case "/LandOfTheLostCities/Pompeii": /* this is an example of what you get if you connect to an HTTP2 server using HTTP/1.1. Curl interprets that as a HTTP/0.9 simple-response and therefore sends this back as a response body. Go figure! */ responseData = Data([ 0x00, 0x00, 0x18, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x10, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x05, 0x00, 0x00, 0x40, 0x00, 0x00, 0x06, 0x00, 0x00, 0x1f, 0x40, 0x00, 0x00, 0x86, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x48, 0x54, 0x54, 0x50, 0x2f, 0x32, 0x20, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x20, 0x70, 0x72, 0x65, 0x66, 0x61, 0x63, 0x65, 0x20, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x20, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6e, 0x67, 0x20, 0x6f, 0x72, 0x20, 0x63, 0x6f, 0x72, 0x72, 0x75, 0x70, 0x74, 0x2e, 0x20, 0x48, 0x65, 0x78, 0x20, 0x64, 0x75, 0x6d, 0x70, 0x20, 0x66, 0x6f, 0x72, 0x20, 0x72, 0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x64, 0x20, 0x62, 0x79, 0x74, 0x65, 0x73, 0x3a, 0x20, 0x34, 0x37, 0x34, 0x35, 0x35, 0x34, 0x32, 0x30, 0x32, 0x66, 0x33, 0x33, 0x32, 0x66, 0x36, 0x34, 0x36, 0x35, 0x37, 0x36, 0x36, 0x39, 0x36, 0x33, 0x36, 0x35, 0x32, 0x66, 0x33, 0x31, 0x33, 0x32, 0x33, 0x33, 0x33, 0x34, 0x33, 0x35, 0x33, 0x36, 0x33, 0x37, 0x33, 0x38, 0x33, 0x39, 0x33, 0x30]) case "/LandOfTheLostCities/Sodom": /* a technically valid HTTP/0.9 simple-response */ responseData = ("technically, this is a valid HTTP/0.9 " + "simple-response. I know it's odd but CURL supports it " + "still...\r\nFind out more in those URLs:\r\n " + " - https://www.w3.org/Protocols/HTTP/1.0/spec.html#Message-Types\r\n" + " - https://github.com/curl/curl/issues/467\r\n").data(using: .utf8)! case "/LandOfTheLostCities/Gomorrah": /* just broken, hope that's not officially HTTP/0.9 :p */ responseData = "HTTP/1.1\r\n\r\n\r\n".data(using: .utf8)! case "/LandOfTheLostCities/Myndus": responseData = ("HTTP/1.1 200 OK\r\n" + "\r\n" + "this is a body that isn't legal as it's " + "neither chunked encoding nor any Content-Length\r\n").data(using: .utf8)! case "/LandOfTheLostCities/Kameiros": responseData = ("HTTP/1.1 999 Wrong Code\r\n" + "illegal: status code (too large)\r\n" + "\r\n").data(using: .utf8)! case "/LandOfTheLostCities/Dinavar": responseData = ("HTTP/1.1 20 Too Few Digits\r\n" + "illegal: status code (too few digits)\r\n" + "\r\n").data(using: .utf8)! case "/LandOfTheLostCities/Kuhikugu": responseData = ("HTTP/1.1 2000 Too Many Digits\r\n" + "illegal: status code (too many digits)\r\n" + "\r\n").data(using: .utf8)! default: responseData = ("HTTP/1.1 500 Internal Server Error\r\n" + "case-missing-in: TestFoundation/HTTPServer.swift\r\n" + "\r\n").data(using: .utf8)! } try self.socket.writeRawData(responseData) } func respondWithAuthResponse(uri: String, firstRead: Bool) throws { let responseData: Data if firstRead { responseData = ("HTTP/1.1 401 UNAUTHORIZED \r\n" + "Content-Length: 0\r\n" + "WWW-Authenticate: Basic realm=\"Fake Relam\"\r\n" + "Access-Control-Allow-Origin: *\r\n" + "Access-Control-Allow-Credentials: true\r\n" + "Via: 1.1 vegur\r\n" + "Cache-Control: proxy-revalidate\r\n" + "Connection: keep-Alive\r\n" + "\r\n").data(using: .utf8)! } else { responseData = ("HTTP/1.1 200 OK \r\n" + "Content-Length: 37\r\n" + "Content-Type: application/json\r\n" + "Access-Control-Allow-Origin: *\r\n" + "Access-Control-Allow-Credentials: true\r\n" + "Via: 1.1 vegur\r\n" + "Cache-Control: proxy-revalidate\r\n" + "Connection: keep-Alive\r\n" + "\r\n" + "{\"authenticated\":true,\"user\":\"user\"}\n").data(using: .utf8)! } try self.socket.writeRawData(responseData) } func respondWithUnauthorizedHeader() throws{ let responseData = ("HTTP/1.1 401 UNAUTHORIZED \r\n" + "Content-Length: 0\r\n" + "Connection: keep-Alive\r\n" + "\r\n").data(using: .utf8)! try self.socket.writeRawData(responseData) } } struct _HTTPRequest { enum Method : String { case GET case POST case PUT } let method: Method let uri: String let body: String var messageBody: String? let headers: [String] enum Error: Swift.Error { case headerEndNotFound } public init(request: String) throws { let headerEnd = (request as NSString).range(of: _HTTPUtils.CRLF2) guard headerEnd.location != NSNotFound else { throw Error.headerEndNotFound } let header = (request as NSString).substring(to: headerEnd.location) headers = header.components(separatedBy: _HTTPUtils.CRLF) let action = headers[0] method = Method(rawValue: action.components(separatedBy: " ")[0])! uri = action.components(separatedBy: " ")[1] body = (request as NSString).substring(from: headerEnd.location + headerEnd.length) } public func getCommaSeparatedHeaders() -> String { var allHeaders = "" for header in headers { allHeaders += header + "," } return allHeaders } public func getHeader(for key: String) -> String? { let lookup = key.lowercased() for header in headers { let parts = header.components(separatedBy: ":") if parts[0].lowercased() == lookup { return parts[1].trimmingCharacters(in: CharacterSet(charactersIn: " ")) } } return nil } } struct _HTTPResponse { enum Response : Int { case OK = 200 case REDIRECT = 302 case NOTFOUND = 404 } private let responseCode: Response private let headers: String public let bodyData: Data public init(response: Response, headers: String = _HTTPUtils.EMPTY, bodyData: Data) { self.responseCode = response self.headers = headers self.bodyData = bodyData } public init(response: Response, headers: String = _HTTPUtils.EMPTY, body: String) { self.init(response: response, headers: headers, bodyData: body.data(using: .utf8)!) } public var header: String { let statusLine = _HTTPUtils.VERSION + _HTTPUtils.SPACE + "\(responseCode.rawValue)" + _HTTPUtils.SPACE + "\(responseCode)" return statusLine + (headers != _HTTPUtils.EMPTY ? _HTTPUtils.CRLF + headers : _HTTPUtils.EMPTY) + _HTTPUtils.CRLF2 } } public class TestURLSessionServer { let capitals: [String:String] = ["Nepal": "Kathmandu", "Peru": "Lima", "Italy": "Rome", "USA": "Washington, D.C.", "UnitedStates": "USA", "UnitedKingdom": "UK", "UK": "London", "country.txt": "A country is a region that is identified as a distinct national entity in political geography"] let httpServer: _HTTPServer let startDelay: TimeInterval? let sendDelay: TimeInterval? let bodyChunks: Int? var port: UInt16 { get { return self.httpServer.port } } public init (port: UInt16?, startDelay: TimeInterval? = nil, sendDelay: TimeInterval? = nil, bodyChunks: Int? = nil) throws { httpServer = try _HTTPServer.create(port: port) self.startDelay = startDelay self.sendDelay = sendDelay self.bodyChunks = bodyChunks } public func readAndRespond() throws { let req = try httpServer.request() if let value = req.getHeader(for: "x-pause") { if let wait = Double(value), wait > 0 { Thread.sleep(forTimeInterval: wait) } } if req.uri.hasPrefix("/LandOfTheLostCities/") { /* these are all misbehaving servers */ try httpServer.respondWithBrokenResponses(uri: req.uri) } else if req.uri == "/NSString-ISO-8859-1-data.txt" { // Serve this directly as binary data to avoid any String encoding conversions. if let url = testBundle().url(forResource: "NSString-ISO-8859-1-data", withExtension: "txt"), let content = try? Data(contentsOf: url) { var responseData = "HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=ISO-8859-1\r\nContent-Length: \(content.count)\r\n\r\n".data(using: .ascii)! responseData.append(content) try httpServer.socket.writeRawData(responseData) } else { try httpServer.respond(with: _HTTPResponse(response: .NOTFOUND, body: "Not Found")) } } else if req.uri.hasPrefix("/auth") { httpServer.willReadAgain = true try httpServer.respondWithAuthResponse(uri: req.uri, firstRead: true) } else if req.uri.hasPrefix("/unauthorized") { try httpServer.respondWithUnauthorizedHeader() } else { try httpServer.respond(with: process(request: req), startDelay: self.startDelay, sendDelay: self.sendDelay, bodyChunks: self.bodyChunks) } } public func readAndRespondAgain() throws { let req = try httpServer.request() if req.uri.hasPrefix("/auth/") { try httpServer.respondWithAuthResponse(uri: req.uri, firstRead: false) } httpServer.willReadAgain = false } func process(request: _HTTPRequest) -> _HTTPResponse { if request.method == .GET || request.method == .POST || request.method == .PUT { return getResponse(request: request) } else { fatalError("Unsupported method!") } } func getResponse(request: _HTTPRequest) -> _HTTPResponse { let uri = request.uri if uri == "/upload" { let text = "Upload completed!" return _HTTPResponse(response: .OK, headers: "Content-Length: \(text.data(using: .utf8)!.count)", body: text) } if uri == "/country.txt" { let text = capitals[String(uri.dropFirst())]! return _HTTPResponse(response: .OK, headers: "Content-Length: \(text.data(using: .utf8)!.count)", body: text) } if uri == "/requestHeaders" { let text = request.getCommaSeparatedHeaders() return _HTTPResponse(response: .OK, headers: "Content-Length: \(text.data(using: .utf8)!.count)", body: text) } if uri == "/emptyPost" { if request.body.count == 0 && request.getHeader(for: "Content-Type") == nil { return _HTTPResponse(response: .OK, body: "") } return _HTTPResponse(response: .NOTFOUND, body: "") } if uri == "/requestCookies" { let text = request.getCommaSeparatedHeaders() return _HTTPResponse(response: .OK, headers: "Content-Length: \(text.data(using: .utf8)!.count)\r\nSet-Cookie: fr=anjd&232; Max-Age=7776000; path=/\r\nSet-Cookie: nm=sddf&232; Max-Age=7776000; path=/; domain=.swift.org; secure; httponly\r\n", body: text) } if uri == "/setCookies" { let text = request.getCommaSeparatedHeaders() return _HTTPResponse(response: .OK, headers: "Content-Length: \(text.data(using: .utf8)!.count)", body: text) } if uri == "/redirectSetCookies" { return _HTTPResponse(response: .REDIRECT, headers: "Location: /setCookies\r\nSet-Cookie: redirect=true; Max-Age=7776000; path=/", body: "") } if uri == "/UnitedStates" { let value = capitals[String(uri.dropFirst())]! let text = request.getCommaSeparatedHeaders() let host = request.headers[1].components(separatedBy: " ")[1] let ip = host.components(separatedBy: ":")[0] let port = host.components(separatedBy: ":")[1] let newPort = Int(port)! + 1 let newHost = ip + ":" + String(newPort) let httpResponse = _HTTPResponse(response: .REDIRECT, headers: "Location: http://\(newHost + "/" + value)", body: text) return httpResponse } if uri == "/DTDs/PropertyList-1.0.dtd" { let dtd = """ <!ENTITY % plistObject "(array | data | date | dict | real | integer | string | true | false )" > <!ELEMENT plist %plistObject;> <!ATTLIST plist version CDATA "1.0" > <!-- Collections --> <!ELEMENT array (%plistObject;)*> <!ELEMENT dict (key, %plistObject;)*> <!ELEMENT key (#PCDATA)> <!--- Primitive types --> <!ELEMENT string (#PCDATA)> <!ELEMENT data (#PCDATA)> <!-- Contents interpreted as Base-64 encoded --> <!ELEMENT date (#PCDATA)> <!-- Contents should conform to a subset of ISO 8601 (in particular, YYYY '-' MM '-' DD 'T' HH ':' MM ':' SS 'Z'. Smaller units may be omitted with a loss of precision) --> <!-- Numerical primitives --> <!ELEMENT true EMPTY> <!-- Boolean constant true --> <!ELEMENT false EMPTY> <!-- Boolean constant false --> <!ELEMENT real (#PCDATA)> <!-- Contents should represent a floating point number matching ("+" | "-")? d+ ("."d*)? ("E" ("+" | "-") d+)? where d is a digit 0-9. --> <!ELEMENT integer (#PCDATA)> <!-- Contents should represent a (possibly signed) integer number in base 10 --> """ return _HTTPResponse(response: .OK, body: dtd) } if uri == "/UnitedKingdom" { let value = capitals[String(uri.dropFirst())]! let text = request.getCommaSeparatedHeaders() //Response header with only path to the location to redirect. let httpResponse = _HTTPResponse(response: .REDIRECT, headers: "Location: \(value)", body: text) return httpResponse } if uri == "/echo" { return _HTTPResponse(response: .OK, body: request.messageBody ?? request.body) } if uri == "/redirect-with-default-port" { let text = request.getCommaSeparatedHeaders() let host = request.headers[1].components(separatedBy: " ")[1] let ip = host.components(separatedBy: ":")[0] let httpResponse = _HTTPResponse(response: .REDIRECT, headers: "Location: http://\(ip)/redirected-with-default-port", body: text) return httpResponse } if uri == "/gzipped-response" { // This is "Hello World!" gzipped. let helloWorld = Data([0x1f, 0x8b, 0x08, 0x00, 0x6d, 0xca, 0xb2, 0x5c, 0x00, 0x03, 0xf3, 0x48, 0xcd, 0xc9, 0xc9, 0x57, 0x08, 0xcf, 0x2f, 0xca, 0x49, 0x51, 0x04, 0x00, 0xa3, 0x1c, 0x29, 0x1c, 0x0c, 0x00, 0x00, 0x00]) return _HTTPResponse(response: .OK, headers: ["Content-Length: \(helloWorld.count)", "Content-Encoding: gzip"].joined(separator: _HTTPUtils.CRLF), bodyData: helloWorld) } return _HTTPResponse(response: .OK, body: capitals[String(uri.dropFirst())]!) } func stop() { httpServer.stop() } } struct ServerError : Error { let operation: String let errno: CInt let file: String let line: UInt var _code: Int { return Int(errno) } var _domain: String { return NSPOSIXErrorDomain } } extension ServerError : CustomStringConvertible { var description: String { let s = String(validatingUTF8: strerror(errno)) ?? "" return "\(operation) failed: \(s) (\(_code))" } } enum InternalServerError : Error { case socketAlreadyClosed } public class ServerSemaphore { let dispatchSemaphore = DispatchSemaphore(value: 0) public func wait(timeout: DispatchTime) -> DispatchTimeoutResult { return dispatchSemaphore.wait(timeout: timeout) } public func signal() { dispatchSemaphore.signal() } } class LoopbackServerTest : XCTestCase { private static let staticSyncQ = DispatchQueue(label: "org.swift.TestFoundation.HTTPServer.StaticSyncQ") private static var _serverPort: Int = -1 private static let serverReady = ServerSemaphore() private static var _serverActive = false private static var testServer: TestURLSessionServer? = nil static var serverPort: Int { get { return staticSyncQ.sync { _serverPort } } set { staticSyncQ.sync { _serverPort = newValue } } } static var serverActive: Bool { get { return staticSyncQ.sync { _serverActive } } set { staticSyncQ.sync { _serverActive = newValue }} } static func terminateServer() { serverActive = false testServer?.stop() testServer = nil } override class func setUp() { super.setUp() func runServer(with condition: ServerSemaphore, startDelay: TimeInterval? = nil, sendDelay: TimeInterval? = nil, bodyChunks: Int? = nil) throws { let server = try TestURLSessionServer(port: nil, startDelay: startDelay, sendDelay: sendDelay, bodyChunks: bodyChunks) testServer = server serverPort = Int(server.port) serverReady.signal() serverActive = true while serverActive { do { try server.httpServer.listen(notify: condition) try server.readAndRespond() if server.httpServer.willReadAgain { try server.httpServer.listen(notify: condition) try server.readAndRespondAgain() } server.httpServer.socket.closeClient() } catch { } } serverPort = -2 } globalDispatchQueue.async { do { try runServer(with: serverReady) } catch { } } let timeout = DispatchTime(uptimeNanoseconds: DispatchTime.now().uptimeNanoseconds + 2_000_000_000) while serverPort == -1 { guard serverReady.wait(timeout: timeout) == .success else { fatalError("Timedout waiting for server to be ready") } } } override class func tearDown() { super.tearDown() terminateServer() } }