-
Notifications
You must be signed in to change notification settings - Fork 63
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add executable counterpart for apple/swift-nio-extras#242 ### Motivation: We added the request handling logic in swift-nio-extras, we should have an actual executable target too for each usage. ### Modifications: Added new nio-http-responsiveness-server target ### Result: We now have an executable target for NIOHTTPResponsivenessServer
- Loading branch information
Showing
2 changed files
with
303 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
// swift-tools-version: 5.10 | ||
// The swift-tools-version declares the minimum version of Swift required to build this package. | ||
|
||
import PackageDescription | ||
|
||
let package = Package( | ||
name: "nio-http-responsiveness-server", | ||
platforms: [ | ||
.macOS(.v14) | ||
], | ||
products: [ | ||
.executable(name: "HTTPResponsivenessServer", targets: ["HTTPResponsivenessServer"]) | ||
], | ||
dependencies: [ | ||
.package(url: "https://github.com/apple/swift-nio.git", from: "2.79.0"), | ||
.package(url: "https://github.com/apple/swift-nio-http2.git", from: "1.35.0"), | ||
.package(url: "https://github.com/apple/swift-nio-ssl.git", from: "2.27.0"), | ||
.package( | ||
url: "https://github.com/apple/swift-nio-extras.git", | ||
revision: "4804de1953c14ce71cfca47a03fb4581a6b3301c" | ||
), | ||
.package(url: "https://github.com/apple/swift-http-types.git", from: "1.1.0"), | ||
.package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.4.0"), | ||
.package(url: "https://github.com/swift-extras/swift-extras-json.git", from: "0.6.0"), | ||
.package(url: "https://github.com/apple/swift-nio-transport-services.git", from: "1.23.0"), | ||
], | ||
targets: [ | ||
.executableTarget( | ||
name: "HTTPResponsivenessServer", | ||
dependencies: [ | ||
.product(name: "NIOCore", package: "swift-nio"), | ||
.product(name: "NIOPosix", package: "swift-nio"), | ||
.product(name: "NIOHTTP1", package: "swift-nio"), | ||
.product(name: "NIOHTTP2", package: "swift-nio-http2"), | ||
.product(name: "NIOSSL", package: "swift-nio-ssl"), | ||
.product(name: "ArgumentParser", package: "swift-argument-parser"), | ||
.product(name: "NIOHTTPTypesHTTP2", package: "swift-nio-extras"), | ||
.product(name: "NIOHTTPTypesHTTP1", package: "swift-nio-extras"), | ||
.product(name: "NIOHTTPResponsiveness", package: "swift-nio-extras"), | ||
.product(name: "ExtrasJSON", package: "swift-extras-json"), | ||
.product(name: "NIOTransportServices", package: "swift-nio-transport-services"), | ||
], | ||
swiftSettings: [ | ||
.enableExperimentalFeature("StrictConcurrency") | ||
] | ||
) | ||
] | ||
) |
255 changes: 255 additions & 0 deletions
255
http-responsiveness-server/Sources/HTTPResponsivenessServer/main.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,255 @@ | ||
import ArgumentParser | ||
import ExtrasJSON | ||
import NIOCore | ||
import NIOHTTP1 | ||
import NIOHTTP2 | ||
import NIOHTTPResponsiveness | ||
import NIOHTTPTypesHTTP1 | ||
import NIOHTTPTypesHTTP2 | ||
import NIOPosix | ||
import NIOSSL | ||
import NIOTLS | ||
import NIOTransportServices | ||
|
||
enum ChannelInitializeError: Error { | ||
case unrecognizedPort(Int?) | ||
} | ||
|
||
func configureCommonHTTPTypesServerPipeline( | ||
_ channel: Channel, | ||
_ configurator: @Sendable @escaping (Channel) -> EventLoopFuture<Void> | ||
) -> EventLoopFuture<Void> { | ||
channel.configureHTTP2SecureUpgrade( | ||
h2ChannelConfigurator: { channel in | ||
channel.configureHTTP2Pipeline(mode: .server) { streamChannel in | ||
do { | ||
try streamChannel.pipeline.syncOperations.addHandler( | ||
HTTP2FramePayloadToHTTPServerCodec()) | ||
} catch { | ||
return streamChannel.eventLoop.makeFailedFuture(error) | ||
} | ||
return configurator(streamChannel) | ||
}.map { _ in () } | ||
}, | ||
http1ChannelConfigurator: { channel in | ||
channel.pipeline.configureHTTPServerPipeline().flatMap { _ in | ||
do { | ||
try channel.pipeline.syncOperations.addHandler( | ||
HTTP1ToHTTPServerCodec(secure: true)) | ||
} catch { | ||
return channel.eventLoop.makeFailedFuture(error) | ||
} | ||
return configurator(channel) | ||
} | ||
} | ||
) | ||
} | ||
|
||
func channelInitializer( | ||
channel: Channel, | ||
tls: ([Int], NIOSSLContext, ByteBuffer)?, | ||
insecure: ([Int], ByteBuffer)?, | ||
isNIOTS: Bool = false | ||
) -> EventLoopFuture<Void> { | ||
// Handle TLS case | ||
var port = channel.localAddress?.port | ||
if port == nil && isNIOTS { | ||
port = insecure?.0.first | ||
} | ||
|
||
if let (ports, sslContext, config) = tls, let port, | ||
ports.contains(port) | ||
{ | ||
let handler = NIOSSLServerHandler(context: sslContext) | ||
do { | ||
try channel.pipeline.syncOperations.addHandler(handler) | ||
} catch { | ||
return channel.eventLoop.makeFailedFuture(error) | ||
} | ||
return configureCommonHTTPTypesServerPipeline(channel) { channel in | ||
channel.eventLoop.makeCompletedFuture { | ||
try channel.pipeline.syncOperations.addHandler( | ||
SimpleResponsivenessRequestMux(responsivenessConfigBuffer: config) | ||
) | ||
} | ||
} | ||
} | ||
|
||
// Handle insecure case | ||
if let (ports, config) = insecure, let port, ports.contains(port) { | ||
return channel.pipeline.configureHTTPServerPipeline().flatMapThrowing { | ||
let mux = SimpleResponsivenessRequestMux(responsivenessConfigBuffer: config) | ||
return try channel.pipeline.syncOperations.addHandlers([ | ||
HTTP1ToHTTPServerCodec(secure: false), | ||
mux, | ||
]) | ||
} | ||
} | ||
|
||
// We're getting traffic on a port we didn't expect. Fail the connection | ||
return channel.eventLoop.makeFailedFuture( | ||
ChannelInitializeError.unrecognizedPort(channel.localAddress?.port) | ||
) | ||
} | ||
|
||
enum RunError: Error { | ||
case inputError(String) | ||
} | ||
|
||
func responsivenessConfigBuffer(scheme: String, host: String, port: Int) throws -> ByteBuffer { | ||
let cfg = ResponsivenessConfig( | ||
version: 1, | ||
urls: ResponsivenessConfigURLs(scheme: scheme, authority: "\(host):\(port)") | ||
) | ||
let encoded = try XJSONEncoder().encode(cfg) | ||
return ByteBuffer(bytes: encoded) | ||
} | ||
|
||
@main | ||
private struct HTTPResponsivenessServer: ParsableCommand { | ||
@Option(help: "Which host to bind to") | ||
var host: String | ||
|
||
@Option(help: "Which port to bind to for encrypted connections") | ||
var port: Int? = nil | ||
|
||
@Option(help: "Which port to bind to for unencrypted connections") | ||
var insecurePort: Int? = nil | ||
|
||
@Option(help: "path to PEM encoded certificate") | ||
var certificatePath: String? | ||
|
||
@Option(help: "path to PEM encoded private key") | ||
var privateKeyPath: String? | ||
|
||
@Flag( | ||
name: .customLong("nw"), | ||
help: "Use Network framework instead of NIOSSL. Disables TLS support.") | ||
var useNetwork: Bool = false | ||
|
||
@Option(help: "override how many threads to use") | ||
var threads: Int? = nil | ||
|
||
func run() throws { | ||
if port == nil && insecurePort == nil { | ||
throw RunError.inputError("must provide either port or insecurePort") | ||
} | ||
|
||
if useNetwork && port != nil { | ||
throw RunError.inputError("Network.framework backend doesn't support TLS") | ||
} | ||
|
||
let tls = try port.map { port in | ||
guard let certificatePath = certificatePath, let privateKeyPath = privateKeyPath else { | ||
throw RunError.inputError("must provide TLS keypair") | ||
} | ||
|
||
let secureResponsivenessConfig = try responsivenessConfigBuffer( | ||
scheme: "https", | ||
host: host, | ||
port: port | ||
) | ||
|
||
let certificate = try NIOSSLCertificate(file: certificatePath, format: .pem) | ||
let privateKey = try NIOSSLPrivateKey(file: privateKeyPath, format: .pem) | ||
var sslConfiguration = TLSConfiguration.makeServerConfiguration( | ||
certificateChain: [.certificate(certificate)], | ||
privateKey: .privateKey(privateKey) | ||
) | ||
sslConfiguration.applicationProtocols = ["h2", "http/1.1"] | ||
let sslContext = try NIOSSLContext(configuration: sslConfiguration) | ||
return ([port], sslContext, secureResponsivenessConfig) | ||
} | ||
|
||
let insecure = try insecurePort.map { port in | ||
let config = try responsivenessConfigBuffer(scheme: "http", host: host, port: port) | ||
return ([port], config) | ||
} | ||
|
||
let secureChannelBootstrap: EventLoopFuture<Channel>? | ||
let insecureChannelBootstrap: EventLoopFuture<Channel>? | ||
|
||
if useNetwork { | ||
#if canImport(Network) | ||
let socketBootstrap = NIOTSListenerBootstrap( | ||
group: NIOTSEventLoopGroup(loopCount: threads ?? 1) | ||
) | ||
// Enable SO_REUSEADDR for the server itself | ||
.serverChannelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1) | ||
|
||
// Set the handlers that are applied to the accepted Channels | ||
.childChannelInitializer({ [useNetwork] channel in | ||
channelInitializer( | ||
channel: channel, | ||
tls: tls, | ||
insecure: insecure, | ||
isNIOTS: useNetwork | ||
) | ||
}) | ||
|
||
// Enable SO_REUSEADDR for the accepted Channels | ||
.childChannelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1) | ||
.childChannelOption(ChannelOptions.tcpOption(.tcp_nodelay), value: 1) | ||
.childChannelOption( | ||
ChannelOptions.writeBufferWaterMark, | ||
value: .init(low: 100 * 16384, high: 100 * 100 * 16384) | ||
) | ||
|
||
// Split this out as a prior step because we want to initiate both binds at once without waiting on either one of them | ||
secureChannelBootstrap = nil | ||
insecureChannelBootstrap = insecurePort.map { | ||
socketBootstrap.bind(host: host, port: $0) | ||
} | ||
#else | ||
throw RunError.inputError("No Network.framework support on Linux") | ||
#endif | ||
} else { | ||
let group = MultiThreadedEventLoopGroup( | ||
numberOfThreads: threads ?? NIOSingletons.groupLoopCountSuggestion) | ||
let socketBootstrap = ServerBootstrap(group: group) | ||
// Specify backlog and enable SO_REUSEADDR for the server itself | ||
.serverChannelOption(ChannelOptions.backlog, value: 256) | ||
.serverChannelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1) | ||
|
||
// Set the handlers that are applied to the accepted Channels | ||
.childChannelInitializer({ channel in | ||
channelInitializer( | ||
channel: channel, | ||
tls: tls, | ||
insecure: insecure | ||
) | ||
}) | ||
|
||
// Enable SO_REUSEADDR for the accepted Channels | ||
.childChannelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1) | ||
.childChannelOption(ChannelOptions.tcpOption(.tcp_nodelay), value: 1) | ||
.childChannelOption( | ||
ChannelOptions.socketOption(.init(rawValue: SO_SNDBUF)), value: 10 * 1024 * 1024 | ||
) | ||
.childChannelOption( | ||
ChannelOptions.writeBufferWaterMark, | ||
value: .init(low: 100 * 16384, high: 100 * 100 * 16384) | ||
) | ||
|
||
// Split this out as a prior step because we want to initiate both binds at once without waiting on either one of them | ||
secureChannelBootstrap = port.map { socketBootstrap.bind(host: host, port: $0) } | ||
insecureChannelBootstrap = insecurePort.map { | ||
socketBootstrap.bind(host: host, port: $0) | ||
} | ||
} | ||
|
||
let secureChannel = try secureChannelBootstrap.map { | ||
let out = try $0.wait() | ||
print("Listening on https://\(host):\(port!)") | ||
return out | ||
} | ||
let insecureChannel = try insecureChannelBootstrap.map { | ||
let out = try $0.wait() | ||
print("Listening on http://\(host):\(insecurePort!)") | ||
return out | ||
} | ||
|
||
let _ = try secureChannel?.closeFuture.wait() | ||
let _ = try insecureChannel?.closeFuture.wait() | ||
} | ||
} |