forked from swiftlang/swift-corelibs-foundation
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathHTTPServer.swift
788 lines (685 loc) · 31.2 KB
/
HTTPServer.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
// 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()
}
}