Skip to content

Commit ba39656

Browse files
Adds support for TLS & HTTP/2 (#69)
By default, the server runs over HTTP/1.1. To enable running HTTP/1.1 over TLS, use useHTTPS. To enable HTTP/2 upgrades (will prefer HTTP/2 but still accept HTTP/1.1 over TLS), use useHTTP2. Note that the HTTP/2 protocol is only supported over TLS, so implies using it. Thus, there's no need to call both useHTTPS and useHTTP2; useHTTP2 sets up both TLS and HTTP/2 support.
1 parent efe370b commit ba39656

File tree

9 files changed

+175
-44
lines changed

9 files changed

+175
-44
lines changed

Docs/1_Configuration.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,32 @@ You can load your environment from another location by passing your app the `--e
119119

120120
If you have separate environment variables for different server configurations (i.e. local dev, staging, production), you can pass your program a separate `--env` for each configuration so the right environment is loaded.
121121

122+
## Configuring Your Server
123+
124+
There are a couple of options available for configuring how your server is running. By default, the server runs over `HTTP/1.1`.
125+
126+
### Enable TLS
127+
128+
You can enable running over TLS with `useHTTPS`.
129+
130+
```swift
131+
func boot() throws {
132+
try useHTTPS(key: "/path/to/private-key.pem", cert: "/path/to/cert.pem")
133+
}
134+
```
135+
136+
### Enable HTTP/2
137+
138+
You may also configure your server with `HTTP/2` upgrades (will prefer `HTTP/2` but still accept `HTTP/1.1` over TLS). To do this use `useHTTP2`.
139+
140+
```swift
141+
func boot() throws {
142+
try useHTTP2(key: "/path/to/private-key.pem", cert: "/path/to/cert.pem")
143+
}
144+
```
145+
146+
Note that the `HTTP/2` protocol is only supported over TLS, and so implies using it. Thus, there's no need to call both `useHTTPS` and `useHTTP2`; `useHTTP2` sets up both TLS and `HTTP/2` support.
147+
122148
## Working with Xcode
123149

124150
You can use Xcode to run your project to take advantage of all the great tools built into it; debugging, breakpoints, memory graphs, testing, etc.

Package.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ let package = Package(
1313
dependencies: [
1414
.package(url: "https://github.com/apple/swift-log.git", from: "1.0.0"),
1515
.package(url: "https://github.com/apple/swift-nio.git", from: "2.0.0"),
16+
.package(url: "https://github.com/apple/swift-nio-ssl.git", from: "2.6.0"),
17+
.package(url: "https://github.com/apple/swift-nio-http2.git", from: "1.9.0"),
1618
.package(url: "https://github.com/apple/swift-argument-parser", .upToNextMinor(from: "0.3.0")),
1719
.package(url: "https://github.com/vapor/postgres-nio.git", from: "1.1.0"),
1820
.package(url: "https://github.com/vapor/mysql-nio.git", from: "1.3.0"),
@@ -40,6 +42,8 @@ let package = Package(
4042
.product(name: "MySQLNIO", package: "mysql-nio"),
4143
.product(name: "NIO", package: "swift-nio"),
4244
.product(name: "NIOHTTP1", package: "swift-nio"),
45+
.product(name: "NIOHTTP2", package: "swift-nio-http2"),
46+
.product(name: "NIOSSL", package: "swift-nio-ssl"),
4347
.product(name: "Logging", package: "swift-log"),
4448
.product(name: "Plot", package: "Plot"),
4549
.product(name: "LifecycleNIOCompat", package: "swift-service-lifecycle"),
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import NIOSSL
2+
3+
/// Settings for how this server should talk to clients.
4+
public final class ApplicationConfiguration: Service {
5+
/// Any TLS configuration for serving over HTTPS.
6+
public var tlsConfig: TLSConfiguration?
7+
/// The HTTP protocol versions supported. Defaults to `HTTP/1.1`.
8+
public var httpVersions: [HTTPVersion] = [.http1_1]
9+
}
10+
11+
extension Application {
12+
/// Use HTTPS when serving.
13+
///
14+
/// - Parameters:
15+
/// - key: The path to the private key.
16+
/// - cert: The path of the cert.
17+
/// - Throws: Any errors encountered when accessing the certs.
18+
public func useHTTPS(key: String, cert: String) throws {
19+
let config = Container.resolve(ApplicationConfiguration.self)
20+
config.tlsConfig = TLSConfiguration
21+
.makeServerConfiguration(
22+
certificateChain: try NIOSSLCertificate
23+
.fromPEMFile(cert)
24+
.map { NIOSSLCertificateSource.certificate($0) },
25+
privateKey: .file(key))
26+
}
27+
28+
/// Use HTTPS when serving.
29+
///
30+
/// - Parameter tlsConfig: A raw NIO `TLSConfiguration` to use.
31+
public func useHTTPS(tlsConfig: TLSConfiguration) {
32+
let config = Container.resolve(ApplicationConfiguration.self)
33+
config.tlsConfig = tlsConfig
34+
}
35+
36+
/// Use HTTP/2 when serving, over TLS with the given key and cert.
37+
///
38+
/// - Parameters:
39+
/// - key: The path to the private key.
40+
/// - cert: The path of the cert.
41+
/// - Throws: Any errors encountered when accessing the certs.
42+
public func useHTTP2(key: String, cert: String) throws {
43+
let config = Container.resolve(ApplicationConfiguration.self)
44+
config.httpVersions = [.http2, .http1_1]
45+
try useHTTPS(key: key, cert: cert)
46+
}
47+
48+
/// Use HTTP/2 when serving, over TLS with the given tls config.
49+
///
50+
/// - Parameter tlsConfig: A raw NIO `TLSConfiguration` to use.
51+
public func useHTTP2(tlsConfig: TLSConfiguration) {
52+
let config = Container.resolve(ApplicationConfiguration.self)
53+
config.httpVersions = [.http2, .http1_1]
54+
useHTTPS(tlsConfig: tlsConfig)
55+
}
56+
}

Sources/Alchemy/Application/Application+Launch.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ extension Application {
3939
bootServices()
4040

4141
// Boot the app
42-
boot()
42+
try boot()
4343

4444
// Register the runner
4545
runner.register(lifecycle: lifecycle)

Sources/Alchemy/Application/Application+Services.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ extension Application {
77
Loop.config()
88

99
// Register all services
10+
ApplicationConfiguration.config(default: ApplicationConfiguration())
1011
Router.config(default: Router())
1112
Scheduler.config(default: Scheduler())
1213
NIOThreadPool.config(default: NIOThreadPool(numberOfThreads: System.coreCount))

Sources/Alchemy/Application/Application.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ public protocol Application {
1818
/// environment is loaded and the global `EventLoopGroup` is
1919
/// set. Called on an event loop, so `Loop.current` is
2020
/// available for use if needed.
21-
func boot()
21+
func boot() throws
2222

2323
/// Required empty initializer.
2424
init()

Sources/Alchemy/Commands/HTTPHandler.swift

Lines changed: 16 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ final class HTTPHandler: ChannelInboundHandler {
1919

2020
// Indicates that the TCP connection needs to be closed after a
2121
// response has been sent.
22-
private var closeAfterResponse = true
22+
private var keepAlive = true
2323

2424
/// A temporary local Request that is used to accumulate data
2525
/// into.
@@ -48,7 +48,7 @@ final class HTTPHandler: ChannelInboundHandler {
4848
switch part {
4949
case .head(let requestHead):
5050
// If the part is a `head`, a new Request is received
51-
self.closeAfterResponse = !requestHead.isKeepAlive
51+
keepAlive = requestHead.isKeepAlive
5252

5353
let contentLength: Int
5454

@@ -86,24 +86,25 @@ final class HTTPHandler: ChannelInboundHandler {
8686
self.request = nil
8787

8888
// Writes the response when done
89-
self.writeResponse(response, to: context)
89+
self.writeResponse(version: request.head.version, response: response, to: context)
9090
}
9191
}
9292

9393
/// Writes the `Responder`'s `Response` to a
9494
/// `ChannelHandlerContext`.
9595
///
9696
/// - Parameters:
97+
/// - version: The HTTP version of the connection.
9798
/// - response: The reponse to write to the handler context.
9899
/// - context: The context to write to.
99100
/// - Returns: An future that completes when the response is
100101
/// written.
101102
@discardableResult
102-
private func writeResponse(_ responseFuture: EventLoopFuture<Response>, to context: ChannelHandlerContext) -> EventLoopFuture<Void> {
103-
return responseFuture.flatMap { response in
104-
let responseWriter = HTTPResponseWriter(handler: self, context: context)
103+
private func writeResponse(version: HTTPVersion, response: EventLoopFuture<Response>, to context: ChannelHandlerContext) -> EventLoopFuture<Void> {
104+
return response.flatMap { response in
105+
let responseWriter = HTTPResponseWriter(version: version, handler: self, context: context)
105106
responseWriter.completionPromise.futureResult.whenComplete { _ in
106-
if self.closeAfterResponse {
107+
if !self.keepAlive {
107108
context.close(promise: nil)
108109
}
109110
}
@@ -124,11 +125,11 @@ final class HTTPHandler: ChannelInboundHandler {
124125
/// Used for writing a response to a remote peer with an
125126
/// `HTTPHandler`.
126127
private struct HTTPResponseWriter: ResponseWriter {
127-
/// The HTTP version we're working with.
128-
static private var httpVersion: HTTPVersion { HTTPVersion(major: 1, minor: 1) }
129-
130128
/// A promise to hook into for when the writing is finished.
131129
let completionPromise: EventLoopPromise<Void>
130+
131+
/// The HTTP version we're working with.
132+
private var version: HTTPVersion
132133

133134
/// The handler in which this writer is writing.
134135
private let handler: HTTPHandler
@@ -138,10 +139,12 @@ private struct HTTPResponseWriter: ResponseWriter {
138139

139140
/// Initialize
140141
/// - Parameters:
142+
/// - version: The HTTPVersion of this connection.
141143
/// - handler: The handler in which this response is writing
142144
/// inside.
143145
/// - context: The context to write responses to.
144-
init(handler: HTTPHandler, context: ChannelHandlerContext) {
146+
init(version: HTTPVersion, handler: HTTPHandler, context: ChannelHandlerContext) {
147+
self.version = version
145148
self.handler = handler
146149
self.context = context
147150
self.completionPromise = context.eventLoop.makePromise()
@@ -150,20 +153,15 @@ private struct HTTPResponseWriter: ResponseWriter {
150153
// MARK: ResponseWriter
151154

152155
func writeHead(status: HTTPResponseStatus, _ headers: HTTPHeaders) {
153-
let version = HTTPResponseWriter.httpVersion
154156
let head = HTTPResponseHead(version: version, status: status, headers: headers)
155157
context.write(handler.wrapOutboundOut(.head(head)), promise: nil)
156158
}
157159

158160
func writeBody(_ body: ByteBuffer) {
159-
context.writeAndFlush(
160-
handler.wrapOutboundOut(.body(IOData.byteBuffer(body))),
161-
promise: nil
162-
)
161+
context.writeAndFlush(handler.wrapOutboundOut(.body(IOData.byteBuffer(body))), promise: nil)
163162
}
164163

165164
func writeEnd() {
166-
context.writeAndFlush(handler.wrapOutboundOut(.end(nil)), promise: nil)
167-
completionPromise.succeed(())
165+
context.writeAndFlush(handler.wrapOutboundOut(.end(nil)), promise: completionPromise)
168166
}
169167
}

Sources/Alchemy/Commands/ServeCommand.swift

Lines changed: 68 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import ArgumentParser
22
import NIO
3+
import NIOSSL
4+
import NIOHTTP1
5+
import NIOHTTP2
36

47
/// Command to serve on launched. This is a subcommand of `Launch`.
58
/// The app will route with the singleton `HTTPRouter`.
@@ -68,7 +71,7 @@ extension ServeCommand: Runner {
6871

6972
lifecycle.register(
7073
label: "Serve",
71-
start: .eventLoopFuture { self.start().map { channel = $0 } },
74+
start: .eventLoopFuture { start().map { channel = $0 } },
7275
shutdown: .eventLoopFuture { channel?.close() ?? .new() }
7376
)
7477

@@ -80,25 +83,17 @@ extension ServeCommand: Runner {
8083
}
8184

8285
private func start() -> EventLoopFuture<Channel> {
83-
// Much of this is courtesy of [apple/swift-nio-examples](
84-
// https://github.com/apple/swift-nio-examples/tree/main/http2-server/Sources/http2-server)
85-
func childChannelInitializer(channel: Channel) -> EventLoopFuture<Void> {
86+
func childChannelInitializer(_ channel: Channel) -> EventLoopFuture<Void> {
8687
channel.pipeline
87-
.configureHTTPServerPipeline(withErrorHandling: true)
88-
.flatMap { channel.pipeline.addHandler(HTTPHandler(router: Router.default)) }
88+
.addAnyTLS()
89+
.flatMap { channel.addHTTP() }
8990
}
90-
91+
9192
let serverBootstrap = ServerBootstrap(group: Loop.group)
92-
// Specify backlog and enable SO_REUSEADDR for the server
93-
// itself
9493
.serverChannelOption(ChannelOptions.backlog, value: 256)
9594
.serverChannelOption(ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), SO_REUSEADDR), value: 1)
96-
97-
// Set the handlers that are applied to the accepted
98-
// `Channel`s
99-
.childChannelInitializer(childChannelInitializer(channel:))
100-
101-
// Enable SO_REUSEADDR for the accepted `Channel`s
95+
.childChannelInitializer(childChannelInitializer)
96+
.childChannelOption(ChannelOptions.socket(IPPROTO_TCP, TCP_NODELAY), value: 1)
10297
.childChannelOption(ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), SO_REUSEADDR), value: 1)
10398
.childChannelOption(ChannelOptions.maxMessagesPerRead, value: 1)
10499

@@ -140,3 +135,61 @@ extension SocketAddress {
140135
}
141136
}
142137
}
138+
139+
extension ChannelPipeline {
140+
/// Configures this pipeline with any TLS config in the
141+
/// `ApplicationConfiguration`.
142+
///
143+
/// - Returns: A future that completes when the config completes.
144+
fileprivate func addAnyTLS() -> EventLoopFuture<Void> {
145+
let config = Container.resolve(ApplicationConfiguration.self)
146+
if var tls = config.tlsConfig {
147+
if config.httpVersions.contains(.http2) {
148+
tls.applicationProtocols.append("h2")
149+
}
150+
if config.httpVersions.contains(.http1_1) {
151+
tls.applicationProtocols.append("http/1.1")
152+
}
153+
let sslContext = try! NIOSSLContext(configuration: tls)
154+
let sslHandler = NIOSSLServerHandler(context: sslContext)
155+
return addHandler(sslHandler)
156+
} else {
157+
return .new()
158+
}
159+
}
160+
}
161+
162+
extension Channel {
163+
/// Configures this channel to handle whatever HTTP versions the
164+
/// server should be speaking over.
165+
///
166+
/// - Returns: A future that completes when the config completes.
167+
fileprivate func addHTTP() -> EventLoopFuture<Void> {
168+
let config = Container.resolve(ApplicationConfiguration.self)
169+
if config.httpVersions.contains(.http2) {
170+
return configureHTTP2SecureUpgrade(
171+
h2ChannelConfigurator: { h2Channel in
172+
h2Channel.configureHTTP2Pipeline(
173+
mode: .server,
174+
inboundStreamInitializer: { channel in
175+
channel.pipeline
176+
.addHandlers([
177+
HTTP2FramePayloadToHTTP1ServerCodec(),
178+
HTTPHandler(router: Router.default)
179+
])
180+
})
181+
.voided()
182+
},
183+
http1ChannelConfigurator: { http1Channel in
184+
http1Channel.pipeline
185+
.configureHTTPServerPipeline(withErrorHandling: true)
186+
.flatMap { self.pipeline.addHandler(HTTPHandler(router: Router.default)) }
187+
}
188+
)
189+
} else {
190+
return pipeline
191+
.configureHTTPServerPipeline(withErrorHandling: true)
192+
.flatMap { self.pipeline.addHandler(HTTPHandler(router: Router.default)) }
193+
}
194+
}
195+
}

Sources/Alchemy/HTTP/Response.swift

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,8 @@ public final class Response {
8888
/// - Parameter writer: An abstraction around writing data to a
8989
/// remote peer.
9090
private func defaultWriterClosure(writer: ResponseWriter) {
91-
writer.writeHead(status: self.status, self.headers)
92-
if let body = self.body {
91+
writer.writeHead(status: status, headers)
92+
if let body = body {
9393
writer.writeBody(body.buffer)
9494
}
9595
writer.writeEnd()
@@ -121,10 +121,3 @@ public protocol ResponseWriter {
121121
/// response, when all data has been written.
122122
func writeEnd()
123123
}
124-
125-
extension ResponseWriter {
126-
// Convenience default parameters for `writeHead`.
127-
public func writeHead(status: HTTPResponseStatus = .ok, _ headers: HTTPHeaders = HTTPHeaders()) {
128-
self.writeHead(status: status, headers)
129-
}
130-
}

0 commit comments

Comments
 (0)