|
| 1 | +// This source file is part of the Swift.org open source project |
| 2 | +// |
| 3 | +// Copyright (c) 2014 - 2016 Apple Inc. and the Swift project authors |
| 4 | +// Licensed under Apache License v2.0 with Runtime Library Exception |
| 5 | +// |
| 6 | +// See http://swift.org/LICENSE.txt for license information |
| 7 | +// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors |
| 8 | +// |
| 9 | + |
| 10 | + |
| 11 | +//This is a very rudimentary HTTP server written plainly for testing URLSession. |
| 12 | +//It is not concurrent. It listens on a port, reads once and writes back only once. |
| 13 | +//We can make it better everytime we need more functionality to test different aspects of URLSession. |
| 14 | + |
| 15 | +import Dispatch |
| 16 | + |
| 17 | +#if DEPLOYMENT_RUNTIME_OBJC || os(Linux) |
| 18 | + import Foundation |
| 19 | + import Glibc |
| 20 | +#else |
| 21 | + import CoreFoundation |
| 22 | + import SwiftFoundation |
| 23 | + import Darwin |
| 24 | +#endif |
| 25 | + |
| 26 | +struct _HTTPUtils { |
| 27 | + static let CRLF = "\r\n" |
| 28 | + static let VERSION = "HTTP/1.1" |
| 29 | + static let SPACE = " " |
| 30 | + static let CRLF2 = CRLF + CRLF |
| 31 | + static let EMPTY = "" |
| 32 | +} |
| 33 | + |
| 34 | +class _TCPSocket { |
| 35 | + |
| 36 | + private var listenSocket: Int32! |
| 37 | + private var socketAddress = UnsafeMutablePointer<sockaddr_in>.allocate(capacity: 1) |
| 38 | + private var connectionSocket: Int32! |
| 39 | + |
| 40 | + private func isNotNegative(r: CInt) -> Bool { |
| 41 | + return r != -1 |
| 42 | + } |
| 43 | + |
| 44 | + private func isZero(r: CInt) -> Bool { |
| 45 | + return r == 0 |
| 46 | + } |
| 47 | + |
| 48 | + private func attempt(_ name: String, file: String = #file, line: UInt = #line, valid: (CInt) -> Bool, _ b: @autoclosure () -> CInt) throws -> CInt { |
| 49 | + let r = b() |
| 50 | + guard valid(r) else { throw ServerError(operation: name, errno: r, file: file, line: line) } |
| 51 | + return r |
| 52 | + } |
| 53 | + |
| 54 | + init(port: UInt16) throws { |
| 55 | +#if os(Linux) |
| 56 | + let SOCKSTREAM = Int32(SOCK_STREAM.rawValue) |
| 57 | +#else |
| 58 | + let SOCKSTREAM = SOCK_STREAM |
| 59 | +#endif |
| 60 | + listenSocket = try attempt("socket", valid: isNotNegative, socket(AF_INET, SOCKSTREAM, Int32(IPPROTO_TCP))) |
| 61 | + var on: Int = 1 |
| 62 | + _ = try attempt("setsockopt", valid: isZero, setsockopt(listenSocket, SOL_SOCKET, SO_REUSEADDR, &on, socklen_t(MemoryLayout<Int>.size))) |
| 63 | + let sa = createSockaddr(port) |
| 64 | + socketAddress.initialize(to: sa) |
| 65 | + try socketAddress.withMemoryRebound(to: sockaddr.self, capacity: MemoryLayout<sockaddr>.size, { |
| 66 | + let addr = UnsafePointer<sockaddr>($0) |
| 67 | + _ = try attempt("bind", valid: isZero, bind(listenSocket, addr, socklen_t(MemoryLayout<sockaddr>.size))) |
| 68 | + }) |
| 69 | + } |
| 70 | + |
| 71 | + private func createSockaddr(_ port: UInt16) -> sockaddr_in { |
| 72 | +#if os(Linux) |
| 73 | + return sockaddr_in(sin_family: sa_family_t(AF_INET), sin_port: htons(port), sin_addr: in_addr(s_addr: INADDR_ANY), sin_zero: (0,0,0,0,0,0,0,0)) |
| 74 | +#else |
| 75 | + return sockaddr_in(sin_len: 0, sin_family: sa_family_t(AF_INET), sin_port: CFSwapInt16HostToBig(port), sin_addr: in_addr(s_addr: INADDR_ANY), sin_zero: (0,0,0,0,0,0,0,0)) |
| 76 | +#endif |
| 77 | + } |
| 78 | + func acceptConnection(notify: DispatchSemaphore) throws { |
| 79 | + _ = try attempt("listen", valid: isZero, listen(listenSocket, SOMAXCONN)) |
| 80 | + try socketAddress.withMemoryRebound(to: sockaddr.self, capacity: MemoryLayout<sockaddr>.size, { |
| 81 | + let addr = UnsafeMutablePointer<sockaddr>($0) |
| 82 | + var sockLen = socklen_t(MemoryLayout<sockaddr>.size) |
| 83 | + notify.signal() |
| 84 | + connectionSocket = try attempt("accept", valid: isNotNegative, accept(listenSocket, addr, &sockLen)) |
| 85 | + }) |
| 86 | + } |
| 87 | + |
| 88 | + func readData() throws -> String { |
| 89 | + var buffer = [UInt8](repeating: 0, count: 4096) |
| 90 | + _ = try attempt("read", valid: isNotNegative, CInt(read(connectionSocket, &buffer, 4096))) |
| 91 | + return String(cString: &buffer) |
| 92 | + } |
| 93 | + |
| 94 | + func writeData(data: String) throws { |
| 95 | + var bytes = Array(data.utf8) |
| 96 | + _ = try attempt("write", valid: isNotNegative, CInt(write(connectionSocket, &bytes, data.utf8.count))) |
| 97 | + } |
| 98 | + |
| 99 | + func shutdown() { |
| 100 | + close(connectionSocket) |
| 101 | + close(listenSocket) |
| 102 | + } |
| 103 | +} |
| 104 | + |
| 105 | +class _HTTPServer { |
| 106 | + |
| 107 | + let socket: _TCPSocket |
| 108 | + |
| 109 | + init(port: UInt16) throws { |
| 110 | + socket = try _TCPSocket(port: port) |
| 111 | + } |
| 112 | + |
| 113 | + public class func create(port: UInt16) throws -> _HTTPServer { |
| 114 | + return try _HTTPServer(port: port) |
| 115 | + } |
| 116 | + |
| 117 | + public func listen(notify: DispatchSemaphore) throws { |
| 118 | + try socket.acceptConnection(notify: notify) |
| 119 | + } |
| 120 | + |
| 121 | + public func stop() { |
| 122 | + socket.shutdown() |
| 123 | + } |
| 124 | + |
| 125 | + public func request() throws -> _HTTPRequest { |
| 126 | + return _HTTPRequest(request: try socket.readData()) |
| 127 | + } |
| 128 | + |
| 129 | + public func respond(with response: _HTTPResponse) throws { |
| 130 | + try socket.writeData(data: response.description) |
| 131 | + } |
| 132 | +} |
| 133 | + |
| 134 | +struct _HTTPRequest { |
| 135 | + enum Method : String { |
| 136 | + case GET |
| 137 | + case POST |
| 138 | + case PUT |
| 139 | + } |
| 140 | + let method: Method |
| 141 | + let uri: String |
| 142 | + let body: String |
| 143 | + let headers: [String] |
| 144 | + |
| 145 | + public init(request: String) { |
| 146 | + let lines = request.components(separatedBy: _HTTPUtils.CRLF2)[0].components(separatedBy: _HTTPUtils.CRLF) |
| 147 | + headers = Array(lines[0...lines.count-2]) |
| 148 | + method = Method(rawValue: headers[0].components(separatedBy: " ")[0])! |
| 149 | + uri = headers[0].components(separatedBy: " ")[1] |
| 150 | + body = lines.last! |
| 151 | + } |
| 152 | + |
| 153 | +} |
| 154 | + |
| 155 | +struct _HTTPResponse { |
| 156 | + enum Response : Int { |
| 157 | + case OK = 200 |
| 158 | + } |
| 159 | + private let responseCode: Response |
| 160 | + private let headers: String |
| 161 | + private let body: String |
| 162 | + |
| 163 | + public init(response: Response, headers: String = _HTTPUtils.EMPTY, body: String) { |
| 164 | + self.responseCode = response |
| 165 | + self.headers = headers |
| 166 | + self.body = body |
| 167 | + } |
| 168 | + |
| 169 | + public var description: String { |
| 170 | + let statusLine = _HTTPUtils.VERSION + _HTTPUtils.SPACE + "\(responseCode.rawValue)" + _HTTPUtils.SPACE + "\(responseCode)" |
| 171 | + return statusLine + (headers != _HTTPUtils.EMPTY ? _HTTPUtils.CRLF + headers : _HTTPUtils.EMPTY) + _HTTPUtils.CRLF2 + body |
| 172 | + } |
| 173 | +} |
| 174 | + |
| 175 | +public class TestURLSessionServer { |
| 176 | + let capitals: [String:String] = ["Nepal":"Kathmandu", |
| 177 | + "Peru":"Lima", |
| 178 | + "Italy":"Rome", |
| 179 | + "USA":"Washington, D.C.", |
| 180 | + "country.txt": "A country is a region that is identified as a distinct national entity in political geography"] |
| 181 | + let httpServer: _HTTPServer |
| 182 | + |
| 183 | + public init (port: UInt16) throws { |
| 184 | + httpServer = try _HTTPServer.create(port: port) |
| 185 | + } |
| 186 | + public func start(started: DispatchSemaphore) throws { |
| 187 | + started.signal() |
| 188 | + try httpServer.listen(notify: started) |
| 189 | + } |
| 190 | + |
| 191 | + public func readAndRespond() throws { |
| 192 | + try httpServer.respond(with: process(request: httpServer.request())) |
| 193 | + } |
| 194 | + |
| 195 | + func process(request: _HTTPRequest) -> _HTTPResponse { |
| 196 | + if request.method == .GET { |
| 197 | + return getResponse(uri: request.uri) |
| 198 | + } else { |
| 199 | + fatalError("Unsupported method!") |
| 200 | + } |
| 201 | + } |
| 202 | + |
| 203 | + func getResponse(uri: String) -> _HTTPResponse { |
| 204 | + if uri == "/country.txt" { |
| 205 | + let text = capitals[String(uri.characters.dropFirst())]! |
| 206 | + return _HTTPResponse(response: .OK, headers: "Content-Length: \(text.characters.count)", body: text) |
| 207 | + } |
| 208 | + return _HTTPResponse(response: .OK, body: capitals[String(uri.characters.dropFirst())]!) |
| 209 | + } |
| 210 | + |
| 211 | + func stop() { |
| 212 | + httpServer.stop() |
| 213 | + } |
| 214 | +} |
| 215 | + |
| 216 | +struct ServerError : Error { |
| 217 | + let operation: String |
| 218 | + let errno: CInt |
| 219 | + let file: String |
| 220 | + let line: UInt |
| 221 | + var _code: Int { return Int(errno) } |
| 222 | + var _domain: String { return NSPOSIXErrorDomain } |
| 223 | +} |
| 224 | + |
| 225 | + |
| 226 | +extension ServerError : CustomStringConvertible { |
| 227 | + var description: String { |
| 228 | + let s = String(validatingUTF8: strerror(errno)) ?? "" |
| 229 | + return "\(operation) failed: \(s) (\(_code))" |
| 230 | + } |
| 231 | +} |
0 commit comments