Skip to content

Commit

Permalink
Merge branch 'main' into Fix-3163
Browse files Browse the repository at this point in the history
  • Loading branch information
challfry committed Apr 25, 2024
2 parents 53268fc + 4c80aab commit e894a97
Show file tree
Hide file tree
Showing 34 changed files with 1,438 additions and 175 deletions.
2 changes: 1 addition & 1 deletion .github/contributing.md
Expand Up @@ -84,7 +84,7 @@ The release title should be concise description. For example:
- ✅ Add case-insensitive routing
- ✅ Fix `routes` command symbol usage

The release titles should use sentence capitialization and not be too verbose. They should also use present tense.
The release titles should use sentence capitalization and not be too verbose. They should also use present tense.

- ❌ Fix `routes` Command Symbol Usage
- ❌ Add new method on RouteBuilder called `caseInsensitive` which can be used to enable case-insensitive routing
Expand Down
2 changes: 1 addition & 1 deletion .github/maintainers.md
Expand Up @@ -6,7 +6,7 @@ maintainers will determine if you are a good fit and either accept and merge the
to be taken in order to be accepted.

## Perks
- Special "Maintainer" role in Discord with purple highlght.
- Special "Maintainer" role in Discord with purple highlight.
- Write access to maintained repo (including approve and merge permissions).
- Invite to "Maintainers" team on GitHub.
- Opportunity to actively participate in deciding on new features in package.
Expand Down
4 changes: 3 additions & 1 deletion Package.swift
Expand Up @@ -34,7 +34,7 @@ let package = Package(
.package(url: "https://github.com/swift-server/swift-backtrace.git", from: "1.1.1"),

// Event-driven network application framework for high performance protocol servers & clients, non-blocking.
.package(url: "https://github.com/apple/swift-nio.git", from: "2.62.0"),
.package(url: "https://github.com/apple/swift-nio.git", from: "2.63.0"),

// Bindings to OpenSSL-compatible libraries for TLS support in SwiftNIO
.package(url: "https://github.com/apple/swift-nio-ssl.git", from: "2.8.0"),
Expand Down Expand Up @@ -92,6 +92,8 @@ let package = Package(
.product(name: "WebSocketKit", package: "websocket-kit"),
.product(name: "MultipartKit", package: "multipart-kit"),
.product(name: "Atomics", package: "swift-atomics"),

.product(name: "_NIOFileSystem", package: "swift-nio"),
]),

// Development
Expand Down
4 changes: 3 additions & 1 deletion Package@swift-5.9.swift
Expand Up @@ -31,7 +31,7 @@ let package = Package(
.package(url: "https://github.com/vapor/routing-kit.git", from: "4.9.0"),

// Event-driven network application framework for high performance protocol servers & clients, non-blocking.
.package(url: "https://github.com/apple/swift-nio.git", from: "2.62.0"),
.package(url: "https://github.com/apple/swift-nio.git", from: "2.63.0"),

// Bindings to OpenSSL-compatible libraries for TLS support in SwiftNIO
.package(url: "https://github.com/apple/swift-nio-ssl.git", from: "2.8.0"),
Expand Down Expand Up @@ -90,6 +90,8 @@ let package = Package(
.product(name: "WebSocketKit", package: "websocket-kit"),
.product(name: "MultipartKit", package: "multipart-kit"),
.product(name: "Atomics", package: "swift-atomics"),

.product(name: "_NIOFileSystem", package: "swift-nio"),
],
swiftSettings: [.enableExperimentalFeature("StrictConcurrency=complete")]
),
Expand Down
2 changes: 1 addition & 1 deletion Sources/Vapor/Authentication/AuthenticationCache.swift
Expand Up @@ -104,7 +104,7 @@ extension Request.Authentication {
// multiple places. But given how Vapor and its users use Authentication this should almost never
// occur and it was decided the trade-off was acceptable
// As the name implies, the usage of this is unsafe because it disables the sendable checking of the
// compiler and does not add any synchronisation.
// compiler and does not add any synchronization.
@usableFromInline
internal struct UnsafeAuthenticationBox<A>: @unchecked Sendable {
@usableFromInline
Expand Down
6 changes: 5 additions & 1 deletion Sources/Vapor/Commands/ServeCommand.swift
Expand Up @@ -85,14 +85,18 @@ public final class ServeCommand: Command, Sendable {
// setup signal sources for shutdown
let signalQueue = DispatchQueue(label: "codes.vapor.server.shutdown")
func makeSignalSource(_ code: Int32) {
#if canImport(Darwin)
/// https://github.com/swift-server/swift-service-lifecycle/blob/main/Sources/UnixSignals/UnixSignalsSequence.swift#L77-L82
signal(code, SIG_IGN)
#endif

let source = DispatchSource.makeSignalSource(signal: code, queue: signalQueue)
source.setEventHandler {
print() // clear ^C
promise.succeed(())
}
source.resume()
box.signalSources.append(source)
signal(code, SIG_IGN)
}
makeSignalSource(SIGTERM)
makeSignalSource(SIGINT)
Expand Down
50 changes: 0 additions & 50 deletions Sources/Vapor/Concurrency/FileIO+Concurrency.swift

This file was deleted.

4 changes: 2 additions & 2 deletions Sources/Vapor/Concurrency/RequestBody+Concurrency.swift
Expand Up @@ -120,7 +120,7 @@ extension Request.Body: AsyncSequence {
/// Generates an `AsyncIterator` to stream the body’s content as
/// `ByteBuffer` sequences. This implementation supports backpressure using
/// `NIOAsyncSequenceProducerBackPressureStrategies`
/// - Returns: `AsyncIterator` containing the `Requeset.Body` as a
/// - Returns: `AsyncIterator` containing the `Request.Body` as a
/// `ByteBuffer` sequence
public func makeAsyncIterator() -> AsyncIterator {
let delegate = AsyncSequenceDelegate(eventLoop: request.eventLoop)
Expand Down Expand Up @@ -159,7 +159,7 @@ extension Request.Body: AsyncSequence {
// return the future that we will fulfill eventually.
return promise.futureResult
case .produceMore:
// We can produce more immidately. Return a succeeded future.
// We can produce more immediately. Return a succeeded future.
return request.eventLoop.makeSucceededVoidFuture()
}
case .error(let error):
Expand Down
4 changes: 2 additions & 2 deletions Sources/Vapor/Content/ContentCoders.swift
Expand Up @@ -27,7 +27,7 @@ public protocol ContentEncoder {

/// Conform a type to this protocol to make it usable for decoding data via Vapor's ``ContentConfiguration`` system.
public protocol ContentDecoder {
/// Legacy "decode object" method. The provided ``NIOCore/ByteBuffer`` should be decoded as a vaule of the given
/// Legacy "decode object" method. The provided ``NIOCore/ByteBuffer`` should be decoded as a value of the given
/// type, optionally guided by the provided ``NIOHTTP1/HTTPHeaders``.
///
/// Most decoders should implement this method by simply forwarding it to the decoder userInfo-aware version below,
Expand All @@ -36,7 +36,7 @@ public protocol ContentDecoder {
func decode<D>(_ decodable: D.Type, from body: ByteBuffer, headers: HTTPHeaders) throws -> D
where D: Decodable

/// "Decode object" method. The provided ``NIOCore/ByteBuffer`` should be decoded as a vaule of the given type,
/// "Decode object" method. The provided ``NIOCore/ByteBuffer`` should be decoded as a value of the given type,
/// optionally guided by the provided ``NIOHTTP1/HTTPHeaders``. The provided ``userInfo`` dictionary must be
/// forwarded to the underlying ``Swift/Decoder`` used to perform the decoding operation.
///
Expand Down
2 changes: 1 addition & 1 deletion Sources/Vapor/Content/PlaintextEncoder.swift
Expand Up @@ -98,7 +98,7 @@ private final class _PlaintextEncoder: Encoder, SingleValueEncodingContainer {
}
}

/// This ridiculosity is a workaround for the inability of encoders to throw errors in various places. It's still better than fatalError()ing.
/// This ridiculously is a workaround for the inability of encoders to throw errors in various places. It's still better than fatalError()ing.
struct FailureEncoder<K: CodingKey>: Encoder, KeyedEncodingContainerProtocol, UnkeyedEncodingContainer, SingleValueEncodingContainer {
let codingPath = [CodingKey](), userInfo = [CodingUserInfoKey: Any](), count = 0
var error: EncodingError { .invalidValue((), .init(codingPath: [], debugDescription: "Plaintext encoding does not support nesting.")) }
Expand Down
4 changes: 2 additions & 2 deletions Sources/Vapor/Deprecations/DotEnvFile+load.swift
Expand Up @@ -19,7 +19,7 @@ extension DotEnvFile {
public static func load(
for environment: Environment = .development,
on eventLoopGroupProvider: Application.EventLoopGroupProvider = .singleton,
logger: Logger = Logger(label: "dot-env-loggger")
logger: Logger = Logger(label: "dot-env-logger")
) {
let threadPool = NIOThreadPool(numberOfThreads: 1)
threadPool.start()
Expand Down Expand Up @@ -51,7 +51,7 @@ extension DotEnvFile {
public static func load(
path: String,
on eventLoopGroupProvider: Application.EventLoopGroupProvider = .singleton,
logger: Logger = Logger(label: "dot-env-loggger")
logger: Logger = Logger(label: "dot-env-logger")
) {
let threadPool = NIOThreadPool(numberOfThreads: 1)
threadPool.start()
Expand Down
4 changes: 2 additions & 2 deletions Sources/Vapor/HTTP/Headers/HTTPCookies.swift
Expand Up @@ -193,7 +193,7 @@ public struct HTTPCookies: ExpressibleByDictionaryLiteral, Sendable {

// MARK: Methods

/// Seriaizes an `HTTPCookie` to a `String`.
/// Serializes an `HTTPCookie` to a `String`.
public func serialize(name: String) -> String {
var serialized = "\(name)=\(self.string)"

Expand Down Expand Up @@ -264,7 +264,7 @@ public struct HTTPCookies: ExpressibleByDictionaryLiteral, Sendable {

// MARK: Serialize

/// Seriaizes the `Cookies` for a `Request`
/// Serializes the `Cookies` for a `Request`
var cookieHeader: String? {
guard !self.cookies.isEmpty else {
return nil
Expand Down
6 changes: 3 additions & 3 deletions Sources/Vapor/HTTP/Headers/HTTPHeaders+ContentRange.swift
Expand Up @@ -5,7 +5,7 @@ extension HTTPHeaders {

/// The unit in which `ContentRange`s and `Range`s are specified. This is usually `bytes`.
/// See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Range
public enum RangeUnit: Equatable {
public enum RangeUnit: Sendable, Equatable {
case bytes
case custom(value: String)

Expand All @@ -21,7 +21,7 @@ extension HTTPHeaders {

/// Represents the HTTP `Range` request header.
/// See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Range
public struct Range: Equatable {
public struct Range: Sendable, Equatable {
public let unit: RangeUnit
public let ranges: [HTTPHeaders.Range.Value]

Expand Down Expand Up @@ -134,7 +134,7 @@ extension HTTPHeaders.Range {
/// Represents one value of the `Range` request header.
///
/// See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Range
public enum Value: Equatable {
public enum Value: Sendable, Equatable {
///Integer with single trailing dash, e.g. `25-`
case start(value: Int)
///Integer with single leading dash, e.g. `-25`
Expand Down
28 changes: 25 additions & 3 deletions Sources/Vapor/HTTP/Server/HTTPServer.swift
Expand Up @@ -119,7 +119,7 @@ public final class HTTPServer: Server, Sendable {

/// Enables decompression with default configuration.
public static var enabled: Self {
.enabled(limit: .ratio(10))
.enabled(limit: .ratio(25))
}

/// Enables decompression with custom configuration.
Expand Down Expand Up @@ -168,7 +168,7 @@ public final class HTTPServer: Server, Sendable {
reuseAddress: Bool = true,
tcpNoDelay: Bool = true,
responseCompression: CompressionConfiguration = .disabled,
requestDecompression: DecompressionConfiguration = .disabled,
requestDecompression: DecompressionConfiguration = .enabled,
supportPipelining: Bool = true,
supportVersions: Set<HTTPVersionMajor>? = nil,
tlsConfiguration: TLSConfiguration? = nil,
Expand Down Expand Up @@ -200,7 +200,7 @@ public final class HTTPServer: Server, Sendable {
reuseAddress: Bool = true,
tcpNoDelay: Bool = true,
responseCompression: CompressionConfiguration = .disabled,
requestDecompression: DecompressionConfiguration = .disabled,
requestDecompression: DecompressionConfiguration = .enabled,
supportPipelining: Bool = true,
supportVersions: Set<HTTPVersionMajor>? = nil,
tlsConfiguration: TLSConfiguration? = nil,
Expand Down Expand Up @@ -503,6 +503,28 @@ extension ChannelPipeline {
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
Expand Down
21 changes: 16 additions & 5 deletions Sources/Vapor/Middleware/FileMiddleware.swift
Expand Up @@ -9,7 +9,8 @@ public final class FileMiddleware: Middleware {
private let publicDirectory: String
private let defaultFile: String?
private let directoryAction: DirectoryAction

private let advancedETagComparison: Bool

public struct BundleSetupError: Equatable, Error {

/// The description of this error.
Expand All @@ -22,17 +23,28 @@ public final class FileMiddleware: Middleware {
public static let publicDirectoryIsNotAFolder: Self = .init(description: "Cannot find any actual folder for the given Public Directory")
}

struct ETagHashes: StorageKey {
public typealias Value = [String: FileHash]

public struct FileHash {
let lastModified: Date
let digestHex: String
}
}

/// Creates a new `FileMiddleware`.
///
/// - parameters:
/// - publicDirectory: The public directory to serve files from.
/// - defaultFile: The name of the default file to look for and serve if a request hits any public directory. Starting with `/` implies
/// an absolute path from the public directory root. If `nil`, no default files are served.
/// - directoryAction: Determines the action to take when the request doesn't have a trailing slash but matches a directory.
public init(publicDirectory: String, defaultFile: String? = nil, directoryAction: DirectoryAction = .none) {
/// - advancedETagComparison: The method used when ETags are generated. If true, a byte-by-byte hash is created (and cached), otherwise a simple comparison based on the file's last modified date and size.
public init(publicDirectory: String, defaultFile: String? = nil, directoryAction: DirectoryAction = .none, advancedETagComparison: Bool = false) {
self.publicDirectory = publicDirectory.addTrailingSlash()
self.defaultFile = defaultFile
self.directoryAction = directoryAction
self.advancedETagComparison = advancedETagComparison
}

public func respond(to request: Request, chainingTo next: Responder) -> EventLoopFuture<Response> {
Expand Down Expand Up @@ -88,10 +100,9 @@ public final class FileMiddleware: Middleware {
return next.respond(to: request)
}
}

// stream the file
let res = request.fileio.streamFile(at: absPath)
return request.eventLoop.makeSucceededFuture(res)
return request.fileio.streamFile(at: absPath, advancedETagComparison: advancedETagComparison)
}

/// Creates a new `FileMiddleware` for a server contained in an Xcode Project.
Expand Down
2 changes: 1 addition & 1 deletion Sources/Vapor/Request/Request.swift
Expand Up @@ -57,7 +57,7 @@ public final class Request: CustomStringConvertible, Sendable {
/// A unique ID for the request.
///
/// The request identifier is set to value of the `X-Request-Id` header when present, or to a
/// uniquelly generated value otherwise.
/// uniquely generated value otherwise.
public let id: String

// MARK: Metadata
Expand Down
26 changes: 0 additions & 26 deletions Sources/Vapor/Responder/DefaultResponder.swift
Expand Up @@ -39,26 +39,6 @@ internal struct DefaultResponder: Responder {
}
}

// If the route isn't explicitly a HEAD route,
// and it's made up solely of .constant components,
// register a HEAD route with the same path
if route.method == .GET &&
route.path.allSatisfy({ component in
if case .constant(_) = component { return true }
return false
}) {
let headRoute = Route(
method: .HEAD,
path: route.path,
responder: middleware.makeResponder(chainingTo: HeadResponder()),
requestType: route.requestType,
responseType: route.responseType)

let headCachedRoute = CachedRoute(route: headRoute, responder: middleware.makeResponder(chainingTo: HeadResponder()))

router.register(headCachedRoute, at: [.constant(HTTPMethod.HEAD.string)] + path)
}

router.register(cached, at: [.constant(route.method.string)] + path)
}
self.router = router
Expand Down Expand Up @@ -155,12 +135,6 @@ internal struct DefaultResponder: Responder {
}
}

private struct HeadResponder: Responder {
func respond(to request: Request) -> EventLoopFuture<Response> {
request.eventLoop.makeSucceededFuture(.init(status: .ok))
}
}

private struct NotFoundResponder: Responder {
func respond(to request: Request) -> EventLoopFuture<Response> {
request.eventLoop.makeFailedFuture(RouteNotFound())
Expand Down

0 comments on commit e894a97

Please sign in to comment.