-
-
Notifications
You must be signed in to change notification settings - Fork 1.4k
/
HTTPServer.swift
635 lines (558 loc) Β· 24.7 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
import NIOCore
import NIOExtras
import NIOHTTP1
import NIOHTTP2
import NIOHTTPCompression
import NIOSSL
import Logging
import NIOPosix
import NIOConcurrencyHelpers
public enum HTTPVersionMajor: Equatable, Hashable, Sendable {
case one
case two
}
public final class HTTPServer: Server, Sendable {
/// Engine server config struct.
///
/// let serverConfig = HTTPServerConfig.default(port: 8123)
/// services.register(serverConfig)
///
public struct Configuration: Sendable {
public static let defaultHostname = "127.0.0.1"
public static let defaultPort = 8080
/// Address the server will bind to. Configuring an address using a hostname with a nil host or port will use the default hostname or port respectively.
public var address: BindAddress
/// Host name the server will bind to.
public var hostname: String {
get {
switch address {
case .hostname(let hostname, _):
return hostname ?? Self.defaultHostname
default:
return Self.defaultHostname
}
}
set {
switch address {
case .hostname(_, let port):
address = .hostname(newValue, port: port)
default:
address = .hostname(newValue, port: nil)
}
}
}
/// Port the server will bind to.
public var port: Int {
get {
switch address {
case .hostname(_, let port):
return port ?? Self.defaultPort
default:
return Self.defaultPort
}
}
set {
switch address {
case .hostname(let hostname, _):
address = .hostname(hostname, port: newValue)
default:
address = .hostname(nil, port: newValue)
}
}
}
/// Listen backlog.
public var backlog: Int
/// When `true`, can prevent errors re-binding to a socket after successive server restarts.
public var reuseAddress: Bool
/// When `true`, OS will attempt to minimize TCP packet delay.
public var tcpNoDelay: Bool
/// Response compression configuration.
public var responseCompression: CompressionConfiguration
/// Supported HTTP compression options.
public struct CompressionConfiguration: Sendable {
/// Disables compression. This is the default.
public static var disabled: Self {
.init(storage: .disabled)
}
/// Enables compression with default configuration.
public static var enabled: Self {
.enabled(initialByteBufferCapacity: 1024)
}
/// Enables compression with custom configuration.
public static func enabled(
initialByteBufferCapacity: Int
) -> Self {
.init(storage: .enabled(
initialByteBufferCapacity: initialByteBufferCapacity
))
}
enum Storage {
case disabled
case enabled(initialByteBufferCapacity: Int)
}
var storage: Storage
}
/// Request decompression configuration.
public var requestDecompression: DecompressionConfiguration
/// Supported HTTP decompression options.
public struct DecompressionConfiguration: Sendable {
/// Disables decompression. This is the default option.
public static var disabled: Self {
.init(storage: .disabled)
}
/// Enables decompression with default configuration.
public static var enabled: Self {
.enabled(limit: .ratio(10))
}
/// Enables decompression with custom configuration.
public static func enabled(
limit: NIOHTTPDecompression.DecompressionLimit
) -> Self {
.init(storage: .enabled(limit: limit))
}
enum Storage {
case disabled
case enabled(limit: NIOHTTPDecompression.DecompressionLimit)
}
var storage: Storage
}
/// When `true`, HTTP server will support pipelined requests.
public var supportPipelining: Bool
public var supportVersions: Set<HTTPVersionMajor>
public var tlsConfiguration: TLSConfiguration?
/// If set, this name will be serialized as the `Server` header in outgoing responses.
public var serverName: String?
/// When `true`, report http metrics through `swift-metrics`
public var reportMetrics: Bool
/// Any uncaught server or responder errors will go here.
public var logger: Logger
/// A time limit to complete a graceful shutdown
public var shutdownTimeout: TimeAmount
/// An optional callback that will be called instead of using swift-nio-ssl's regular certificate verification logic.
/// This is the same as `NIOSSLCustomVerificationCallback` but just marked as `Sendable`
@preconcurrency
public var customCertificateVerifyCallback: (@Sendable ([NIOSSLCertificate], EventLoopPromise<NIOSSLVerificationResult>) -> Void)?
public init(
hostname: String = Self.defaultHostname,
port: Int = Self.defaultPort,
backlog: Int = 256,
reuseAddress: Bool = true,
tcpNoDelay: Bool = true,
responseCompression: CompressionConfiguration = .disabled,
requestDecompression: DecompressionConfiguration = .disabled,
supportPipelining: Bool = true,
supportVersions: Set<HTTPVersionMajor>? = nil,
tlsConfiguration: TLSConfiguration? = nil,
serverName: String? = nil,
reportMetrics: Bool = true,
logger: Logger? = nil,
shutdownTimeout: TimeAmount = .seconds(10)
) {
self.init(
address: .hostname(hostname, port: port),
backlog: backlog,
reuseAddress: reuseAddress,
tcpNoDelay: tcpNoDelay,
responseCompression: responseCompression,
requestDecompression: requestDecompression,
supportPipelining: supportPipelining,
supportVersions: supportVersions,
tlsConfiguration: tlsConfiguration,
serverName: serverName,
reportMetrics: reportMetrics,
logger: logger,
shutdownTimeout: shutdownTimeout
)
}
public init(
address: BindAddress,
backlog: Int = 256,
reuseAddress: Bool = true,
tcpNoDelay: Bool = true,
responseCompression: CompressionConfiguration = .disabled,
requestDecompression: DecompressionConfiguration = .disabled,
supportPipelining: Bool = true,
supportVersions: Set<HTTPVersionMajor>? = nil,
tlsConfiguration: TLSConfiguration? = nil,
serverName: String? = nil,
reportMetrics: Bool = true,
logger: Logger? = nil,
shutdownTimeout: TimeAmount = .seconds(10)
) {
self.address = address
self.backlog = backlog
self.reuseAddress = reuseAddress
self.tcpNoDelay = tcpNoDelay
self.responseCompression = responseCompression
self.requestDecompression = requestDecompression
self.supportPipelining = supportPipelining
if let supportVersions = supportVersions {
self.supportVersions = supportVersions
} else {
self.supportVersions = tlsConfiguration == nil ? [.one] : [.one, .two]
}
self.tlsConfiguration = tlsConfiguration
self.serverName = serverName
self.reportMetrics = reportMetrics
self.logger = logger ?? Logger(label: "codes.vapor.http-server")
self.shutdownTimeout = shutdownTimeout
self.customCertificateVerifyCallback = nil
}
}
public var onShutdown: EventLoopFuture<Void> {
guard let connection = self.connection.withLockedValue({ $0 }) else {
fatalError("Server has not started yet")
}
return connection.channel.closeFuture
}
/// The configuration for the HTTP server.
///
/// Many properties of the configuration may be changed both before and after the server has been started.
///
/// However, a warning will be logged and the configuration will be discarded if an option could not be
/// changed after the server has started. These include the following properties, which are only read
/// once when the server starts:
/// - ``Configuration-swift.struct/address``
/// - ``Configuration-swift.struct/hostname``
/// - ``Configuration-swift.struct/port``
/// - ``Configuration-swift.struct/backlog``
/// - ``Configuration-swift.struct/reuseAddress``
/// - ``Configuration-swift.struct/tcpNoDelay``
public var configuration: Configuration {
get { _configuration.withLockedValue { $0 } }
set {
let oldValue = _configuration.withLockedValue { $0 }
let canBeUpdatedDynamically =
oldValue.address == newValue.address
&& oldValue.backlog == newValue.backlog
&& oldValue.reuseAddress == newValue.reuseAddress
&& oldValue.tcpNoDelay == newValue.tcpNoDelay
guard canBeUpdatedDynamically || !didStart.withLockedValue({ $0 }) else {
oldValue.logger.warning("Cannot modify server configuration after server has been started.")
return
}
self.application.storage[Application.HTTP.Server.ConfigurationKey.self] = newValue
_configuration.withLockedValue { $0 = newValue }
}
}
private let responder: Responder
private let _configuration: NIOLockedValueBox<Configuration>
private let eventLoopGroup: EventLoopGroup
private let connection: NIOLockedValueBox<HTTPServerConnection?>
private let didShutdown: NIOLockedValueBox<Bool>
private let didStart: NIOLockedValueBox<Bool>
private let application: Application
public init(
application: Application,
responder: Responder,
configuration: Configuration,
on eventLoopGroup: EventLoopGroup = MultiThreadedEventLoopGroup.singleton
) {
self.application = application
self.responder = responder
self._configuration = .init(configuration)
self.eventLoopGroup = eventLoopGroup
self.didStart = .init(false)
self.didShutdown = .init(false)
self.connection = .init(nil)
}
public func start(address: BindAddress?) throws {
var configuration = self.configuration
switch address {
case .none:
/// Use the configuration as is.
break
case .hostname(let hostname, let port):
/// Override the hostname, port, neither, or both.
configuration.address = .hostname(hostname ?? configuration.hostname, port: port ?? configuration.port)
case .unixDomainSocket:
/// Override the socket path.
configuration.address = address!
}
/// Print starting message.
let scheme = configuration.tlsConfiguration == nil ? "http" : "https"
let addressDescription: String
switch configuration.address {
case .hostname(let hostname, let port):
addressDescription = "\(scheme)://\(hostname ?? configuration.hostname):\(port ?? configuration.port)"
case .unixDomainSocket(let socketPath):
addressDescription = "\(scheme)+unix: \(socketPath)"
}
self.configuration.logger.notice("Server starting on \(addressDescription)")
/// Start the actual `HTTPServer`.
try self.connection.withLockedValue {
$0 = try HTTPServerConnection.start(
application: self.application,
server: self,
responder: self.responder,
configuration: configuration,
on: self.eventLoopGroup
).wait()
}
self.configuration = configuration
self.didStart.withLockedValue { $0 = true }
}
public func shutdown() {
guard let connection = self.connection.withLockedValue({ $0 }) else {
return
}
self.configuration.logger.debug("Requesting HTTP server shutdown")
do {
try connection.close(timeout: self.configuration.shutdownTimeout).wait()
} catch {
self.configuration.logger.error("Could not stop HTTP server: \(error)")
}
self.configuration.logger.debug("HTTP server shutting down")
self.didShutdown.withLockedValue { $0 = true }
}
public var localAddress: SocketAddress? {
return self.connection.withLockedValue({ $0 })?.channel.localAddress
}
deinit {
let started = self.didStart.withLockedValue { $0 }
let shutdown = self.didShutdown.withLockedValue { $0 }
assert(!started || shutdown, "HTTPServer did not shutdown before deinitializing")
}
}
private final class HTTPServerConnection: Sendable {
let channel: Channel
let quiesce: ServerQuiescingHelper
static func start(
application: Application,
server: HTTPServer,
responder: Responder,
configuration: HTTPServer.Configuration,
on eventLoopGroup: EventLoopGroup
) -> EventLoopFuture<HTTPServerConnection> {
let quiesce = ServerQuiescingHelper(group: eventLoopGroup)
let bootstrap = ServerBootstrap(group: eventLoopGroup)
/// Specify backlog and enable `SO_REUSEADDR` for the server itself.
.serverChannelOption(ChannelOptions.backlog, value: Int32(configuration.backlog))
.serverChannelOption(ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), SO_REUSEADDR), value: configuration.reuseAddress ? SocketOptionValue(1) : SocketOptionValue(0))
/// Set handlers that are applied to the Server's channel.
.serverChannelInitializer { channel in
channel.pipeline.addHandler(quiesce.makeServerChannelHandler(channel: channel))
}
/// Set the handlers that are applied to the accepted Channels.
.childChannelInitializer { [unowned application, unowned server] channel in
/// Copy the most up-to-date configuration.
let configuration = server.configuration
/// Add TLS handlers if configured.
if var tlsConfiguration = configuration.tlsConfiguration {
/// Prioritize http/2 if supported.
if configuration.supportVersions.contains(.two) {
tlsConfiguration.applicationProtocols.append("h2")
}
if configuration.supportVersions.contains(.one) {
tlsConfiguration.applicationProtocols.append("http/1.1")
}
let sslContext: NIOSSLContext
let tlsHandler: NIOSSLServerHandler
do {
sslContext = try NIOSSLContext(configuration: tlsConfiguration)
tlsHandler = NIOSSLServerHandler(context: sslContext, customVerifyCallback: configuration.customCertificateVerifyCallback)
} catch {
configuration.logger.error("Could not configure TLS: \(error)")
return channel.close(mode: .all)
}
return channel.pipeline.addHandler(tlsHandler).flatMap { _ in
channel.configureHTTP2SecureUpgrade(h2ChannelConfigurator: { channel in
channel.configureHTTP2Pipeline(
mode: .server,
inboundStreamInitializer: { channel in
return channel.pipeline.addVaporHTTP2Handlers(
application: application,
responder: responder,
configuration: configuration
)
}
).map { _ in }
}, http1ChannelConfigurator: { channel in
return channel.pipeline.addVaporHTTP1Handlers(
application: application,
responder: responder,
configuration: configuration
)
})
}
} else {
guard !configuration.supportVersions.contains(.two) else {
fatalError("Plaintext HTTP/2 (h2c) not yet supported.")
}
return channel.pipeline.addVaporHTTP1Handlers(
application: application,
responder: responder,
configuration: configuration
)
}
}
/// Enable `TCP_NODELAY` and `SO_REUSEADDR` for the accepted Channels.
.childChannelOption(ChannelOptions.socket(IPPROTO_TCP, TCP_NODELAY), value: configuration.tcpNoDelay ? SocketOptionValue(1) : SocketOptionValue(0))
.childChannelOption(ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), SO_REUSEADDR), value: configuration.reuseAddress ? SocketOptionValue(1) : SocketOptionValue(0))
.childChannelOption(ChannelOptions.maxMessagesPerRead, value: 1)
let channel: EventLoopFuture<Channel>
switch configuration.address {
case .hostname:
channel = bootstrap.bind(host: configuration.hostname, port: configuration.port)
case .unixDomainSocket(let socketPath):
channel = bootstrap.bind(unixDomainSocketPath: socketPath)
}
return channel.map { channel in
return .init(channel: channel, quiesce: quiesce)
}.flatMapErrorThrowing { error -> HTTPServerConnection in
quiesce.initiateShutdown(promise: nil)
throw error
}
}
init(channel: Channel, quiesce: ServerQuiescingHelper) {
self.channel = channel
self.quiesce = quiesce
}
func close(timeout: TimeAmount) -> EventLoopFuture<Void> {
let promise = self.channel.eventLoop.makePromise(of: Void.self)
self.channel.eventLoop.scheduleTask(in: timeout) {
promise.fail(Abort(.internalServerError, reason: "Server stop took too long."))
}
self.quiesce.initiateShutdown(promise: promise)
return promise.futureResult
}
var onClose: EventLoopFuture<Void> {
self.channel.closeFuture
}
deinit {
assert(!self.channel.isActive, "HTTPServerConnection deinitialized without calling shutdown()")
}
}
extension HTTPResponseHead {
/// Determines if the head is purely informational. If a head is informational another head will follow this
/// head eventually.
///
/// This is also from SwiftNIO
var isInformational: Bool {
100 <= self.status.code && self.status.code < 200 && self.status.code != 101
}
}
extension ChannelPipeline {
func addVaporHTTP2Handlers(
application: Application,
responder: Responder,
configuration: HTTPServer.Configuration
) -> EventLoopFuture<Void> {
/// Create server pipeline array.
var handlers: [ChannelHandler] = []
let http2 = HTTP2FramePayloadToHTTP1ServerCodec()
handlers.append(http2)
/// Add response compressor if configured.
switch configuration.responseCompression.storage {
case .enabled(let initialByteBufferCapacity):
let responseCompressionHandler = HTTPResponseCompressor(
initialByteBufferCapacity: initialByteBufferCapacity
)
handlers.append(responseCompressionHandler)
case .disabled:
break
}
/// Add request decompressor if configured.
switch configuration.requestDecompression.storage {
case .enabled(let limit):
let requestDecompressionHandler = NIOHTTPRequestDecompressor(
limit: limit
)
handlers.append(requestDecompressionHandler)
case .disabled:
break
}
/// Add NIO β HTTP request decoder.
let serverReqDecoder = HTTPServerRequestDecoder(
application: application
)
handlers.append(serverReqDecoder)
/// Add NIO β HTTP response encoder.
let serverResEncoder = HTTPServerResponseEncoder(
serverHeader: configuration.serverName,
dateCache: .eventLoop(self.eventLoop)
)
handlers.append(serverResEncoder)
/// Add server request β response delegate.
let handler = HTTPServerHandler(responder: responder, logger: application.logger)
handlers.append(handler)
return self.addHandlers(handlers).flatMap {
/// Close the connection in case of any errors.
self.addHandler(NIOCloseOnErrorHandler())
}
}
func addVaporHTTP1Handlers(
application: Application,
responder: Responder,
configuration: HTTPServer.Configuration
) -> EventLoopFuture<Void> {
/// Create server pipeline array.
var handlers: [RemovableChannelHandler] = []
/// Configure HTTP/1:
/// Add http parsing and serializing.
let httpResEncoder = HTTPResponseEncoder()
let httpReqDecoder = ByteToMessageHandler(HTTPRequestDecoder(
leftOverBytesStrategy: .forwardBytes
))
handlers += [httpResEncoder, httpReqDecoder]
/// Add pipelining support if configured.
if configuration.supportPipelining {
let pipelineHandler = HTTPServerPipelineHandler()
handlers.append(pipelineHandler)
}
/// Add response compressor if configured.
switch configuration.responseCompression.storage {
case .enabled(let initialByteBufferCapacity):
let responseCompressionHandler = HTTPResponseCompressor(
initialByteBufferCapacity: initialByteBufferCapacity
)
handlers.append(responseCompressionHandler)
case .disabled:
break
}
/// Add request decompressor if configured.
switch configuration.requestDecompression.storage {
case .enabled(let limit):
let requestDecompressionHandler = NIOHTTPRequestDecompressor(
limit: limit
)
handlers.append(requestDecompressionHandler)
case .disabled:
break
}
/// Add NIO β HTTP response encoder.
let serverResEncoder = HTTPServerResponseEncoder(
serverHeader: configuration.serverName,
dateCache: .eventLoop(self.eventLoop)
)
handlers.append(serverResEncoder)
/// Add NIO β HTTP request decoder.
let serverReqDecoder = HTTPServerRequestDecoder(
application: application
)
handlers.append(serverReqDecoder)
/// Add server request β response delegate.
let handler = HTTPServerHandler(responder: responder, logger: application.logger)
/// Add HTTP upgrade handler.
let upgrader = HTTPServerUpgradeHandler(
httpRequestDecoder: httpReqDecoder,
httpHandlers: handlers + [handler]
)
handlers.append(upgrader)
handlers.append(handler)
return self.addHandlers(handlers).flatMap {
/// Close the connection in case of any errors.
self.addHandler(NIOCloseOnErrorHandler())
}
}
}
// MARK: Helper function for constructing NIOSSLServerHandler.
extension NIOSSLServerHandler {
convenience init(context: NIOSSLContext, customVerifyCallback: NIOSSLCustomVerificationCallback?) {
if let callback = customVerifyCallback {
self.init(context: context, customVerificationCallback: callback)
} else {
self.init(context: context)
}
}
}