Skip to content

Commit

Permalink
HTTPResponsivenessServer (#95)
Browse files Browse the repository at this point in the history
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
ehaydenr authored Jan 27, 2025
1 parent 737599b commit 4bd02d1
Show file tree
Hide file tree
Showing 2 changed files with 303 additions and 0 deletions.
48 changes: 48 additions & 0 deletions http-responsiveness-server/Package.swift
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 http-responsiveness-server/Sources/HTTPResponsivenessServer/main.swift
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()
}
}

0 comments on commit 4bd02d1

Please sign in to comment.