swift-nio/Sources/NIOHTTP1Server/main.swift

580 lines
25 KiB
Swift
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftNIO open source project
//
// Copyright (c) 2017-2021 Apple Inc. and the SwiftNIO project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of SwiftNIO project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//
import NIOCore
import NIOPosix
import NIOHTTP1
extension String {
func chopPrefix(_ prefix: String) -> String? {
if self.unicodeScalars.starts(with: prefix.unicodeScalars) {
return String(self[self.index(self.startIndex, offsetBy: prefix.count)...])
} else {
return nil
}
}
func containsDotDot() -> Bool {
for idx in self.indices {
if self[idx] == "." && idx < self.index(before: self.endIndex) && self[self.index(after: idx)] == "." {
return true
}
}
return false
}
}
private func httpResponseHead(request: HTTPRequestHead, status: HTTPResponseStatus, headers: HTTPHeaders = HTTPHeaders()) -> HTTPResponseHead {
var head = HTTPResponseHead(version: request.version, status: status, headers: headers)
let connectionHeaders: [String] = head.headers[canonicalForm: "connection"].map { $0.lowercased() }
if !connectionHeaders.contains("keep-alive") && !connectionHeaders.contains("close") {
// the user hasn't pre-set either 'keep-alive' or 'close', so we might need to add headers
switch (request.isKeepAlive, request.version.major, request.version.minor) {
case (true, 1, 0):
// HTTP/1.0 and the request has 'Connection: keep-alive', we should mirror that
head.headers.add(name: "Connection", value: "keep-alive")
case (false, 1, let n) where n >= 1:
// HTTP/1.1 (or treated as such) and the request has 'Connection: close', we should mirror that
head.headers.add(name: "Connection", value: "close")
default:
// we should match the default or are dealing with some HTTP that we don't support, let's leave as is
()
}
}
return head
}
private final class HTTPHandler: ChannelInboundHandler {
private enum FileIOMethod {
case sendfile
case nonblockingFileIO
}
public typealias InboundIn = HTTPServerRequestPart
public typealias OutboundOut = HTTPServerResponsePart
private enum State {
case idle
case waitingForRequestBody
case sendingResponse
mutating func requestReceived() {
precondition(self == .idle, "Invalid state for request received: \(self)")
self = .waitingForRequestBody
}
mutating func requestComplete() {
precondition(self == .waitingForRequestBody, "Invalid state for request complete: \(self)")
self = .sendingResponse
}
mutating func responseComplete() {
precondition(self == .sendingResponse, "Invalid state for response complete: \(self)")
self = .idle
}
}
private var buffer: ByteBuffer! = nil
private var keepAlive = false
private var state = State.idle
private let htdocsPath: String
private var infoSavedRequestHead: HTTPRequestHead?
private var infoSavedBodyBytes: Int = 0
private var continuousCount: Int = 0
private var handler: ((ChannelHandlerContext, HTTPServerRequestPart) -> Void)?
private var handlerFuture: EventLoopFuture<Void>?
private let fileIO: NonBlockingFileIO
private let defaultResponse = "Hello World\r\n"
public init(fileIO: NonBlockingFileIO, htdocsPath: String) {
self.htdocsPath = htdocsPath
self.fileIO = fileIO
}
func handleInfo(context: ChannelHandlerContext, request: HTTPServerRequestPart) {
switch request {
case .head(let request):
self.infoSavedRequestHead = request
self.infoSavedBodyBytes = 0
self.keepAlive = request.isKeepAlive
self.state.requestReceived()
case .body(buffer: let buf):
self.infoSavedBodyBytes += buf.readableBytes
case .end:
self.state.requestComplete()
let response = """
HTTP method: \(self.infoSavedRequestHead!.method)\r
URL: \(self.infoSavedRequestHead!.uri)\r
body length: \(self.infoSavedBodyBytes)\r
headers: \(self.infoSavedRequestHead!.headers)\r
client: \(context.remoteAddress?.description ?? "zombie")\r
IO: SwiftNIO Electric Boogaloo™\r\n
"""
self.buffer.clear()
self.buffer.writeString(response)
var headers = HTTPHeaders()
headers.add(name: "Content-Length", value: "\(response.utf8.count)")
context.write(self.wrapOutboundOut(.head(httpResponseHead(request: self.infoSavedRequestHead!, status: .ok, headers: headers))), promise: nil)
context.write(self.wrapOutboundOut(.body(.byteBuffer(self.buffer))), promise: nil)
self.completeResponse(context, trailers: nil, promise: nil)
}
}
func handleEcho(context: ChannelHandlerContext, request: HTTPServerRequestPart) {
self.handleEcho(context: context, request: request, balloonInMemory: false)
}
func handleEcho(context: ChannelHandlerContext, request: HTTPServerRequestPart, balloonInMemory: Bool = false) {
switch request {
case .head(let request):
self.keepAlive = request.isKeepAlive
self.infoSavedRequestHead = request
self.state.requestReceived()
if balloonInMemory {
self.buffer.clear()
} else {
context.writeAndFlush(self.wrapOutboundOut(.head(httpResponseHead(request: request, status: .ok))), promise: nil)
}
case .body(buffer: var buf):
if balloonInMemory {
self.buffer.writeBuffer(&buf)
} else {
context.writeAndFlush(self.wrapOutboundOut(.body(.byteBuffer(buf))), promise: nil)
}
case .end:
self.state.requestComplete()
if balloonInMemory {
var headers = HTTPHeaders()
headers.add(name: "Content-Length", value: "\(self.buffer.readableBytes)")
context.write(self.wrapOutboundOut(.head(httpResponseHead(request: self.infoSavedRequestHead!, status: .ok, headers: headers))), promise: nil)
context.write(self.wrapOutboundOut(.body(.byteBuffer(self.buffer))), promise: nil)
self.completeResponse(context, trailers: nil, promise: nil)
} else {
self.completeResponse(context, trailers: nil, promise: nil)
}
}
}
func handleJustWrite(context: ChannelHandlerContext, request: HTTPServerRequestPart, statusCode: HTTPResponseStatus = .ok, string: String, trailer: (String, String)? = nil, delay: TimeAmount = .nanoseconds(0)) {
switch request {
case .head(let request):
self.keepAlive = request.isKeepAlive
self.state.requestReceived()
context.writeAndFlush(self.wrapOutboundOut(.head(httpResponseHead(request: request, status: statusCode))), promise: nil)
case .body(buffer: _):
()
case .end:
self.state.requestComplete()
context.eventLoop.scheduleTask(in: delay) { () -> Void in
var buf = context.channel.allocator.buffer(capacity: string.utf8.count)
buf.writeString(string)
context.writeAndFlush(self.wrapOutboundOut(.body(.byteBuffer(buf))), promise: nil)
var trailers: HTTPHeaders? = nil
if let trailer = trailer {
trailers = HTTPHeaders()
trailers?.add(name: trailer.0, value: trailer.1)
}
self.completeResponse(context, trailers: trailers, promise: nil)
}
}
}
func handleContinuousWrites(context: ChannelHandlerContext, request: HTTPServerRequestPart) {
switch request {
case .head(let request):
self.keepAlive = request.isKeepAlive
self.continuousCount = 0
self.state.requestReceived()
func doNext() {
self.buffer.clear()
self.continuousCount += 1
self.buffer.writeString("line \(self.continuousCount)\n")
context.writeAndFlush(self.wrapOutboundOut(.body(.byteBuffer(self.buffer)))).map {
context.eventLoop.scheduleTask(in: .milliseconds(400), doNext)
}.whenFailure { (_: Error) in
self.completeResponse(context, trailers: nil, promise: nil)
}
}
context.writeAndFlush(self.wrapOutboundOut(.head(httpResponseHead(request: request, status: .ok))), promise: nil)
doNext()
case .end:
self.state.requestComplete()
default:
break
}
}
func handleMultipleWrites(context: ChannelHandlerContext, request: HTTPServerRequestPart, strings: [String], delay: TimeAmount) {
switch request {
case .head(let request):
self.keepAlive = request.isKeepAlive
self.continuousCount = 0
self.state.requestReceived()
func doNext() {
self.buffer.clear()
self.buffer.writeString(strings[self.continuousCount])
self.continuousCount += 1
context.writeAndFlush(self.wrapOutboundOut(.body(.byteBuffer(self.buffer)))).whenSuccess {
if self.continuousCount < strings.count {
context.eventLoop.scheduleTask(in: delay, doNext)
} else {
self.completeResponse(context, trailers: nil, promise: nil)
}
}
}
context.writeAndFlush(self.wrapOutboundOut(.head(httpResponseHead(request: request, status: .ok))), promise: nil)
doNext()
case .end:
self.state.requestComplete()
default:
break
}
}
func dynamicHandler(request reqHead: HTTPRequestHead) -> ((ChannelHandlerContext, HTTPServerRequestPart) -> Void)? {
if let howLong = reqHead.uri.chopPrefix("/dynamic/write-delay/") {
return { context, req in
self.handleJustWrite(context: context,
request: req, string: self.defaultResponse,
delay: Int64(howLong).map { .milliseconds($0) } ?? .seconds(0))
}
}
switch reqHead.uri {
case "/dynamic/echo":
return self.handleEcho
case "/dynamic/echo_balloon":
return { self.handleEcho(context: $0, request: $1, balloonInMemory: true) }
case "/dynamic/pid":
return { context, req in self.handleJustWrite(context: context, request: req, string: "\(getpid())") }
case "/dynamic/write-delay":
return { context, req in self.handleJustWrite(context: context, request: req, string: self.defaultResponse, delay: .milliseconds(100)) }
case "/dynamic/info":
return self.handleInfo
case "/dynamic/trailers":
return { context, req in self.handleJustWrite(context: context, request: req, string: "\(getpid())\r\n", trailer: ("Trailer-Key", "Trailer-Value")) }
case "/dynamic/continuous":
return self.handleContinuousWrites
case "/dynamic/count-to-ten":
return { self.handleMultipleWrites(context: $0, request: $1, strings: (1...10).map { "\($0)" }, delay: .milliseconds(100)) }
case "/dynamic/client-ip":
return { context, req in self.handleJustWrite(context: context, request: req, string: "\(context.remoteAddress.debugDescription)") }
default:
return { context, req in self.handleJustWrite(context: context, request: req, statusCode: .notFound, string: "not found") }
}
}
private func handleFile(context: ChannelHandlerContext, request: HTTPServerRequestPart, ioMethod: FileIOMethod, path: String) {
self.buffer.clear()
func sendErrorResponse(request: HTTPRequestHead, _ error: Error) {
var body = context.channel.allocator.buffer(capacity: 128)
let response = { () -> HTTPResponseHead in
switch error {
case let e as IOError where e.errnoCode == ENOENT:
body.writeStaticString("IOError (not found)\r\n")
return httpResponseHead(request: request, status: .notFound)
case let e as IOError:
body.writeStaticString("IOError (other)\r\n")
body.writeString(e.description)
body.writeStaticString("\r\n")
return httpResponseHead(request: request, status: .notFound)
default:
body.writeString("\(type(of: error)) error\r\n")
return httpResponseHead(request: request, status: .internalServerError)
}
}()
body.writeString("\(error)")
body.writeStaticString("\r\n")
context.write(self.wrapOutboundOut(.head(response)), promise: nil)
context.write(self.wrapOutboundOut(.body(.byteBuffer(body))), promise: nil)
context.writeAndFlush(self.wrapOutboundOut(.end(nil)), promise: nil)
context.channel.close(promise: nil)
}
func responseHead(request: HTTPRequestHead, fileRegion region: FileRegion) -> HTTPResponseHead {
var response = httpResponseHead(request: request, status: .ok)
response.headers.add(name: "Content-Length", value: "\(region.endIndex)")
response.headers.add(name: "Content-Type", value: "text/plain; charset=utf-8")
return response
}
switch request {
case .head(let request):
self.keepAlive = request.isKeepAlive
self.state.requestReceived()
guard !request.uri.containsDotDot() else {
let response = httpResponseHead(request: request, status: .forbidden)
context.write(self.wrapOutboundOut(.head(response)), promise: nil)
self.completeResponse(context, trailers: nil, promise: nil)
return
}
let path = self.htdocsPath + "/" + path
let fileHandleAndRegion = self.fileIO.openFile(path: path, eventLoop: context.eventLoop)
fileHandleAndRegion.whenFailure {
sendErrorResponse(request: request, $0)
}
fileHandleAndRegion.whenSuccess { (file, region) in
switch ioMethod {
case .nonblockingFileIO:
var responseStarted = false
let response = responseHead(request: request, fileRegion: region)
if region.readableBytes == 0 {
responseStarted = true
context.write(self.wrapOutboundOut(.head(response)), promise: nil)
}
return self.fileIO.readChunked(fileRegion: region,
chunkSize: 32 * 1024,
allocator: context.channel.allocator,
eventLoop: context.eventLoop) { buffer in
if !responseStarted {
responseStarted = true
context.write(self.wrapOutboundOut(.head(response)), promise: nil)
}
return context.writeAndFlush(self.wrapOutboundOut(.body(.byteBuffer(buffer))))
}.flatMap { () -> EventLoopFuture<Void> in
let p = context.eventLoop.makePromise(of: Void.self)
self.completeResponse(context, trailers: nil, promise: p)
return p.futureResult
}.flatMapError { error in
if !responseStarted {
let response = httpResponseHead(request: request, status: .ok)
context.write(self.wrapOutboundOut(.head(response)), promise: nil)
var buffer = context.channel.allocator.buffer(capacity: 100)
buffer.writeString("fail: \(error)")
context.write(self.wrapOutboundOut(.body(.byteBuffer(buffer))), promise: nil)
self.state.responseComplete()
return context.writeAndFlush(self.wrapOutboundOut(.end(nil)))
} else {
return context.close()
}
}.whenComplete { (_: Result<Void, Error>) in
_ = try? file.close()
}
case .sendfile:
let response = responseHead(request: request, fileRegion: region)
context.write(self.wrapOutboundOut(.head(response)), promise: nil)
context.writeAndFlush(self.wrapOutboundOut(.body(.fileRegion(region)))).flatMap {
let p = context.eventLoop.makePromise(of: Void.self)
self.completeResponse(context, trailers: nil, promise: p)
return p.futureResult
}.flatMapError { (_: Error) in
context.close()
}.whenComplete { (_: Result<Void, Error>) in
_ = try? file.close()
}
}
}
case .end:
self.state.requestComplete()
default:
fatalError("oh noes: \(request)")
}
}
private func completeResponse(_ context: ChannelHandlerContext, trailers: HTTPHeaders?, promise: EventLoopPromise<Void>?) {
self.state.responseComplete()
let promise = self.keepAlive ? promise : (promise ?? context.eventLoop.makePromise())
if !self.keepAlive {
promise!.futureResult.whenComplete { (_: Result<Void, Error>) in context.close(promise: nil) }
}
self.handler = nil
context.writeAndFlush(self.wrapOutboundOut(.end(trailers)), promise: promise)
}
func channelRead(context: ChannelHandlerContext, data: NIOAny) {
let reqPart = self.unwrapInboundIn(data)
if let handler = self.handler {
handler(context, reqPart)
return
}
switch reqPart {
case .head(let request):
if request.uri.unicodeScalars.starts(with: "/dynamic".unicodeScalars) {
self.handler = self.dynamicHandler(request: request)
self.handler!(context, reqPart)
return
} else if let path = request.uri.chopPrefix("/sendfile/") {
self.handler = { self.handleFile(context: $0, request: $1, ioMethod: .sendfile, path: path) }
self.handler!(context, reqPart)
return
} else if let path = request.uri.chopPrefix("/fileio/") {
self.handler = { self.handleFile(context: $0, request: $1, ioMethod: .nonblockingFileIO, path: path) }
self.handler!(context, reqPart)
return
}
self.keepAlive = request.isKeepAlive
self.state.requestReceived()
var responseHead = httpResponseHead(request: request, status: HTTPResponseStatus.ok)
self.buffer.clear()
self.buffer.writeString(self.defaultResponse)
responseHead.headers.add(name: "content-length", value: "\(self.buffer!.readableBytes)")
let response = HTTPServerResponsePart.head(responseHead)
context.write(self.wrapOutboundOut(response), promise: nil)
case .body:
break
case .end:
self.state.requestComplete()
let content = HTTPServerResponsePart.body(.byteBuffer(buffer!.slice()))
context.write(self.wrapOutboundOut(content), promise: nil)
self.completeResponse(context, trailers: nil, promise: nil)
}
}
func channelReadComplete(context: ChannelHandlerContext) {
context.flush()
}
func handlerAdded(context: ChannelHandlerContext) {
self.buffer = context.channel.allocator.buffer(capacity: 0)
}
func userInboundEventTriggered(context: ChannelHandlerContext, event: Any) {
switch event {
case let evt as ChannelEvent where evt == ChannelEvent.inputClosed:
// The remote peer half-closed the channel. At this time, any
// outstanding response will now get the channel closed, and
// if we are idle or waiting for a request body to finish we
// will close the channel immediately.
switch self.state {
case .idle, .waitingForRequestBody:
context.close(promise: nil)
case .sendingResponse:
self.keepAlive = false
}
default:
context.fireUserInboundEventTriggered(event)
}
}
}
// First argument is the program path
var arguments = CommandLine.arguments.dropFirst(0) // just to get an ArraySlice<String> from [String]
var allowHalfClosure = true
if arguments.dropFirst().first == .some("--disable-half-closure") {
allowHalfClosure = false
arguments = arguments.dropFirst()
}
let arg1 = arguments.dropFirst().first
let arg2 = arguments.dropFirst(2).first
let arg3 = arguments.dropFirst(3).first
let defaultHost = "::1"
let defaultPort = 8888
let defaultHtdocs = "/dev/null/"
enum BindTo {
case ip(host: String, port: Int)
case unixDomainSocket(path: String)
case stdio
}
let htdocs: String
let bindTarget: BindTo
switch (arg1, arg1.flatMap(Int.init), arg2, arg2.flatMap(Int.init), arg3) {
case (.some(let h), _ , _, .some(let p), let maybeHtdocs):
/* second arg an integer --> host port [htdocs] */
bindTarget = .ip(host: h, port: p)
htdocs = maybeHtdocs ?? defaultHtdocs
case (_, .some(let p), let maybeHtdocs, _, _):
/* first arg an integer --> port [htdocs] */
bindTarget = .ip(host: defaultHost, port: p)
htdocs = maybeHtdocs ?? defaultHtdocs
case (.some(let portString), .none, let maybeHtdocs, .none, .none):
/* couldn't parse as number --> uds-path-or-stdio [htdocs] */
if portString == "-" {
bindTarget = .stdio
} else {
bindTarget = .unixDomainSocket(path: portString)
}
htdocs = maybeHtdocs ?? defaultHtdocs
default:
htdocs = defaultHtdocs
bindTarget = BindTo.ip(host: defaultHost, port: defaultPort)
}
let group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount)
let threadPool = NIOThreadPool(numberOfThreads: 6)
threadPool.start()
func childChannelInitializer(channel: Channel) -> EventLoopFuture<Void> {
return channel.pipeline.configureHTTPServerPipeline(withErrorHandling: true).flatMap {
channel.pipeline.addHandler(HTTPHandler(fileIO: fileIO, htdocsPath: htdocs))
}
}
let fileIO = NonBlockingFileIO(threadPool: threadPool)
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(childChannelInitializer(channel:))
// Enable SO_REUSEADDR for the accepted Channels
.childChannelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1)
.childChannelOption(ChannelOptions.maxMessagesPerRead, value: 1)
.childChannelOption(ChannelOptions.allowRemoteHalfClosure, value: allowHalfClosure)
let pipeBootstrap = NIOPipeBootstrap(group: group)
// Set the handlers that are applied to the accepted Channels
.channelInitializer(childChannelInitializer(channel:))
.channelOption(ChannelOptions.maxMessagesPerRead, value: 1)
.channelOption(ChannelOptions.allowRemoteHalfClosure, value: allowHalfClosure)
defer {
try! group.syncShutdownGracefully()
try! threadPool.syncShutdownGracefully()
}
print("htdocs = \(htdocs)")
let channel = try { () -> Channel in
switch bindTarget {
case .ip(let host, let port):
return try socketBootstrap.bind(host: host, port: port).wait()
case .unixDomainSocket(let path):
return try socketBootstrap.bind(unixDomainSocketPath: path).wait()
case .stdio:
return try pipeBootstrap.withPipes(inputDescriptor: STDIN_FILENO, outputDescriptor: STDOUT_FILENO).wait()
}
}()
let localAddress: String
if case .stdio = bindTarget {
localAddress = "STDIO"
} else {
guard let channelLocalAddress = channel.localAddress else {
fatalError("Address was unable to bind. Please check that the socket was not closed or that the address family was understood.")
}
localAddress = "\(channelLocalAddress)"
}
print("Server started and listening on \(localAddress), htdocs path \(htdocs)")
// This will never unblock as we don't close the ServerChannel
try channel.closeFuture.wait()
print("Server closed")