Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 20 additions & 3 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ let package = Package(
),
.package(url: "https://github.com/apple/swift-distributed-tracing.git", from: "1.4.1"),
.package(url: "https://github.com/apple/swift-certificates.git", from: "1.19.1"),
.package(url: "https://github.com/apple/swift-log.git", from: "1.13.2"),
.package(url: "https://github.com/apple/swift-log.git", from: "1.14.0"),
.package(url: "https://github.com/apple/swift-nio.git", from: "2.100.0"),
.package(url: "https://github.com/apple/swift-nio-ssl.git", from: "2.37.0"),
.package(url: "https://github.com/apple/swift-nio-extras.git", from: "1.34.1"),
Expand All @@ -63,10 +63,27 @@ let package = Package(
.package(url: "https://github.com/swift-server/swift-service-lifecycle.git", from: "2.11.0"),
],
targets: [
.target(
name: "ExampleSupport",
dependencies: [
.product(name: "Tracing", package: "swift-distributed-tracing")
],
swiftSettings: extraSettings
),
.executableTarget(
name: "RequestHandlerExample",
dependencies: [
"ExampleSupport",
.product(name: "Instrumentation", package: "swift-distributed-tracing"),
.product(name: "Logging", package: "swift-log"),
"NIOHTTPServer",
],
swiftSettings: extraSettings
),
.executableTarget(
name: "Example",
name: "ConnectionHandlerExample",
dependencies: [
.product(name: "Tracing", package: "swift-distributed-tracing"),
"ExampleSupport",
.product(name: "Instrumentation", package: "swift-distributed-tracing"),
.product(name: "Logging", package: "swift-log"),
"NIOHTTPServer",
Expand Down
97 changes: 97 additions & 0 deletions Sources/ConnectionHandlerExample/ConnectionHandlerExample.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift HTTP Server open source project
//
// Copyright (c) 2026 Apple Inc. and the Swift HTTP Server project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of Swift HTTP Server project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

import BasicContainers
import Crypto
import ExampleSupport
import Foundation
import Instrumentation
import Logging
import NIOHTTPServer
import X509

@main
@available(anyAppleOS 26.0, *)
struct ConnectionHandlerExample {
static func main() async throws {
try await serve()
}

@concurrent
static func serve() async throws {
InstrumentationSystem.bootstrap(LogTracer())

var rootLogger = Logger(label: "ConnectionHandlerExample")
rootLogger.logLevel = .trace
try await withLogger(rootLogger) { rootLogger in
let privateKey = P256.Signing.PrivateKey()
let server = NIOHTTPServer(
logger: rootLogger,
configuration: try .init(
bindTarget: .hostAndPort(host: "127.0.0.1", port: 12346),
supportedHTTPVersions: [.http1_1, .http2(config: .init())],
transportSecurity: .tls(
credentials: .inMemory(
certificateChain: [
try Certificate(
version: .v3,
serialNumber: .init(bytes: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]),
publicKey: .init(privateKey.publicKey),
notValidBefore: Date.now.addingTimeInterval(-60),
notValidAfter: Date.now.addingTimeInterval(60 * 60),
issuer: DistinguishedName(),
subject: DistinguishedName(),
signatureAlgorithm: .ecdsaWithSHA256,
extensions: .init(),
issuerPrivateKey: Certificate.PrivateKey(privateKey)
)
],
privateKey: Certificate.PrivateKey(privateKey)
)
)
)
)

// A connection handler runs once per accepted connection and drives the
// request loop via `connection.handleRequests`. Both the connection
// handler and the request handler are inline closures.
try await server.serve { connection, context in
var connection = Optional(connection)
try await withLogger(mergingMetadata: [
"peer": .string(context.remoteAddress.map { "\($0)" } ?? "unknown"),
"http": .string(context.httpVersion.rawValue),
]) { connectionLogger in
connectionLogger.info("connection accepted")
defer { connectionLogger.info("connection closed") }

await connection.take()!.handleRequests { request, _, _, responseSender in
var responseSender = Optional(responseSender)
try await withLogger(mergingMetadata: [
"path": .string(request.path ?? "")
]) { requestLogger in
requestLogger.info("request received")
defer { requestLogger.info("request completed") }

var body = UniqueArray<UInt8>(copying: "Well, hello!".utf8)
try await responseSender.take()!.sendAndFinish(
HTTPResponse(status: .ok),
buffer: &body
)
}
}
}
}
}
}
}
69 changes: 0 additions & 69 deletions Sources/Example/Example.swift

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,14 @@
//
//===----------------------------------------------------------------------===//

import Tracing
package import Tracing

struct LogTracer: Tracer {
typealias Span = NoOpSpan
package struct LogTracer: Tracer {
package typealias Span = NoOpSpan

init() {}
package init() {}

func startAnySpan<Instant: TracerInstant>(
package func startAnySpan<Instant: TracerInstant>(
_ operationName: String,
context: @autoclosure () -> ServiceContext,
ofKind kind: SpanKind,
Expand All @@ -32,16 +32,20 @@ struct LogTracer: Tracer {
return NoOpSpan(context: context())
}

func forceFlush() {
package func forceFlush() {
print("Flushing")
}

func inject<Carrier, Inject>(_ context: ServiceContext, into carrier: inout Carrier, using injector: Inject)
package func inject<Carrier, Inject>(
_ context: ServiceContext,
into carrier: inout Carrier,
using injector: Inject
)
where Inject: Injector, Carrier == Inject.Carrier {
// no-op
}

func extract<Carrier, Extract>(
package func extract<Carrier, Extract>(
_ carrier: Carrier,
into context: inout ServiceContext,
using extractor: Extract
Expand All @@ -50,47 +54,43 @@ struct LogTracer: Tracer {
// no-op
}

struct NoOpSpan: Tracing.Span {
let context: ServiceContext
var isRecording: Bool {
package struct NoOpSpan: Tracing.Span {
package let context: ServiceContext
package var isRecording: Bool {
false
}

var operationName: String {
get {
"noop"
}
package var operationName: String {
get { "noop" }
nonmutating set {
// ignore
}
}

init(context: ServiceContext) {
package init(context: ServiceContext) {
self.context = context
}

func setStatus(_ status: SpanStatus) {}
package func setStatus(_ status: SpanStatus) {}

func addLink(_ link: SpanLink) {}
package func addLink(_ link: SpanLink) {}

func addEvent(_ event: SpanEvent) {}
package func addEvent(_ event: SpanEvent) {}

func recordError<Instant: TracerInstant>(
package func recordError<Instant: TracerInstant>(
_ error: any Error,
attributes: SpanAttributes,
at instant: @autoclosure () -> Instant
) {}

var attributes: SpanAttributes {
get {
[:]
}
package var attributes: SpanAttributes {
get { [:] }
nonmutating set {
// ignore
}
}

func end<Instant: TracerInstant>(at instant: @autoclosure () -> Instant) {
package func end<Instant: TracerInstant>(at instant: @autoclosure () -> Instant) {
print("Ending span")
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,7 @@ extension NIOHTTPServerConfiguration.TransportSecurity {
/// ``CertificateVerificationResult/certificateVerified(_:)`` from the callback if verification succeeds,
/// optionally including the validated certificate chain you derived. Returning the validated certificate
/// chain allows ``NIOHTTPServer`` to provide access to it in the request handler through
/// ``NIOHTTPServer/ConnectionContext/peerCertificateChain``, accessed via the task-local
/// ``NIOHTTPServer/connectionContext`` property. Otherwise, return
/// ``NIOHTTPServer/RequestContext/peerCertificateChain``. Otherwise, return
/// ``CertificateVerificationResult/failed(_:)`` if verification fails.
/// - certificateVerification: The client certificate verification behavior. Defaults to
/// ``CertificateVerificationMode/noHostnameVerification``.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift HTTP Server open source project
//
// Copyright (c) 2026 Apple Inc. and the Swift HTTP Server project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of Swift HTTP Server project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

public import HTTPAPIs
public import X509

@available(anyAppleOS 26.0, *)
extension HTTPServerCapability {
/// A request-context capability exposing connection-scoped peer and local addresses.
///
/// Servers whose request context conforms to this capability surface the
/// peer's address and the local address the connection is bound to. Both
/// are reported best-effort: implementations may return `nil` when the
/// underlying transport cannot report an address.
public protocol ConnectionInfo: RequestContext {
Comment thread
FranzBusch marked this conversation as resolved.
/// The peer's address, when known.
var remoteAddress: NIOHTTPServer.SocketAddress? { get }

/// The local address the connection is bound to, when known.
var localAddress: NIOHTTPServer.SocketAddress? { get }
}

/// A request-context capability exposing the validated peer certificate chain.
///
/// Servers whose request context conforms to this capability surface the
/// peer's mTLS-validated certificate chain (when applicable). Implementations
/// return `nil` if mTLS isn't configured, or if no validated chain was
/// derived.
public protocol PeerCertificate: RequestContext {
Comment thread
FranzBusch marked this conversation as resolved.
/// The peer's validated certificate chain, when available.
var peerCertificateChain: X509.ValidatedCertificateChain? { get async throws }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this async and throws? That seems very odd

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was already its signature - I'm just introducing this capability. Look at NIOHTTPServer+ConnectionContext.swift:ConnectionContext/peerCertificateChain.
Based on how we're getting it, it has to be async throws.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is highly unfortunate since it should be synchronously present. Let's file an issue to track this since in the HTTPAPIs we should not make the property async or throws

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why should it be synchronously present and not throw?

This is currently async because we're are calling get() on an ELF. We could just block if we need it to be sync.

We can't get around the throws unless we try?, but is hiding the errors desired?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because at the point where we hand the connection to the user we already have the TLS connection established. So we should be able to get the peer cert before we hand the connection to the user

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure. We can probably get it via sync operations. I'll create a follow up issue.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#97

}
}
Loading
Loading