├── .gitignore ├── .gitmodules ├── .swift-version ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── Package.swift ├── README.md ├── Sources ├── Curassow │ ├── Address.swift │ ├── Arbiter.swift │ ├── AsyncronousWorker.swift │ ├── Configuration.swift │ ├── Curassow.swift │ ├── Data.swift │ ├── DispatchWorker.swift │ ├── HTTPParser.swift │ ├── Logger.swift │ ├── Signals.swift │ ├── Socket.swift │ ├── SynchronousWorker.swift │ └── Worker.swift └── example │ ├── example.apib │ └── main.swift ├── Tests ├── CurassowTests │ ├── AddressSpec.swift │ ├── ConfigurationSpec.swift │ ├── HTTPParserSpec.swift │ └── XCTests.swift └── LinuxMain.swift └── docs ├── Makefile ├── _templates └── sidebar_intro.html ├── architecture.md ├── conf.py ├── configuration.md ├── deployment.html ├── index.rst └── signal-handling.md /.gitignore: -------------------------------------------------------------------------------- 1 | .build/ 2 | Packages/ 3 | run-tests 4 | run-integration-tests 5 | example/example 6 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "Tests/Integration/NestTestSuite"] 2 | path = Tests/Integration/NestTestSuite 3 | url = https://github.com/nestproject/NestTestSuite 4 | -------------------------------------------------------------------------------- /.swift-version: -------------------------------------------------------------------------------- 1 | 3.1 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | os: 2 | - linux 3 | - osx 4 | language: node_js 5 | node_js: 8 6 | sudo: required 7 | dist: trusty 8 | osx_image: xcode9.3 9 | env: 10 | - SWIFT_VERSION=3.0.2 11 | - SWIFT_VERSION=3.1 12 | - SWIFT_VERSION=4.0.3 13 | install: 14 | - eval "$(curl -sL https://swiftenv.fuller.li/install.sh)" 15 | - npm install --global --no-optional dredd 16 | script: 17 | - swift test 18 | - make test 19 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog for Curassow 2 | 3 | ## 0.6.1 4 | 5 | Added support for Swift 3.1. 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Kyle Fuller 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 15 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 18 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 21 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 22 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: build 2 | build: 3 | swift build 4 | 5 | test-sync: build 6 | dredd --server '.build/debug/example' Sources/example/example.apib http://localhost:8000 7 | 8 | test-dispatch: build 9 | dredd --server '.build/debug/example --worker-type dispatch --bind 0.0.0.0:9000' Sources/example/example.apib http://localhost:9000 10 | 11 | test: test-sync test-dispatch 12 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | import PackageDescription 2 | 3 | 4 | let package = Package( 5 | name: "Curassow", 6 | targets: [ 7 | Target(name: "example", dependencies: ["Curassow"]), 8 | ], 9 | dependencies: [ 10 | .Package(url: "https://github.com/nestproject/Nest.git", majorVersion: 0, minor: 4), 11 | .Package(url: "https://github.com/nestproject/Inquiline.git", majorVersion: 0, minor: 4), 12 | .Package(url: "https://github.com/kylef/Commander.git", majorVersion: 0, minor: 6), 13 | .Package(url: "https://github.com/kylef/fd.git", majorVersion: 0, minor: 2), 14 | .Package(url: "https://github.com/kylef/Spectre.git", majorVersion: 0, minor: 7), 15 | ] 16 | ) 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Curassow 2 | 3 | [![Build Status](https://travis-ci.org/kylef/Curassow.svg?branch=master)](https://travis-ci.org/kylef/Curassow) 4 | 5 | Curassow is a Swift [Nest](https://github.com/nestproject/Nest) 6 | HTTP Server. It uses the pre-fork worker model and it's similar to Python's 7 | Gunicorn and Ruby's Unicorn. 8 | 9 | It exposes a [Nest-compatible interface](https://github.com/nestproject/Nest) 10 | for your application, allowing you to use Curassow with any Nest compatible 11 | web frameworks of your choice. 12 | 13 | ## Documentation 14 | 15 | Full documentation can be found on the Curassow website: 16 | https://curassow.fuller.li 17 | 18 | ## Usage 19 | 20 | To use Curassow, you will need to install it via the Swift Package Manager, 21 | you can add it to the list of dependencies in your `Package.swift`: 22 | 23 | ```swift 24 | import PackageDescription 25 | 26 | let package = Package( 27 | name: "HelloWorld", 28 | dependencies: [ 29 | .Package(url: "https://github.com/kylef/Curassow.git", majorVersion: 0, minor: 6), 30 | ] 31 | ) 32 | ``` 33 | 34 | Afterwards you can place your web application implementation in `Sources` 35 | and add the runner inside `main.swift` which exposes a command line tool to 36 | run your web application: 37 | 38 | ```swift 39 | import Curassow 40 | import Inquiline 41 | 42 | 43 | serve { request in 44 | return Response(.ok, contentType: "text/plain", body: "Hello World") 45 | } 46 | ``` 47 | 48 | Then build and run your application: 49 | 50 | ```shell 51 | $ swift build --configuration release 52 | ``` 53 | 54 | ### Example Application 55 | 56 | You can find a [hello world example](https://github.com/kylef/Curassow-example-helloworld) application that uses Curassow. 57 | 58 | ## License 59 | 60 | Curassow is licensed under the BSD license. See [LICENSE](LICENSE) for more 61 | info. 62 | -------------------------------------------------------------------------------- /Sources/Curassow/Address.swift: -------------------------------------------------------------------------------- 1 | #if os(Linux) 2 | import Glibc 3 | #else 4 | import Darwin.C 5 | #endif 6 | 7 | import Commander 8 | 9 | 10 | public enum Address : Equatable, CustomStringConvertible { 11 | case ip(hostname: String, port: UInt16) 12 | case unix(path: String) 13 | 14 | func socket(_ backlog: Int32) throws -> Socket { 15 | switch self { 16 | case let .ip(hostname, port): 17 | let socket = try Socket() 18 | try socket.bind(hostname, port: port) 19 | try socket.listen(backlog) 20 | socket.blocking = false 21 | return socket 22 | case let .unix(path): 23 | // Delete old file if exists 24 | unlink(path) 25 | 26 | let socket = try Socket(family: AF_UNIX) 27 | try socket.bind(path) 28 | try socket.listen(backlog) 29 | socket.blocking = false 30 | return socket 31 | } 32 | } 33 | 34 | public var description: String { 35 | switch self { 36 | case let .ip(hostname, port): 37 | return "\(hostname):\(port)" 38 | case let .unix(path): 39 | return "unix:\(path)" 40 | } 41 | } 42 | } 43 | 44 | 45 | public func == (lhs: Address, rhs: Address) -> Bool { 46 | switch (lhs, rhs) { 47 | case let (.ip(lhsHostname, lhsPort), .ip(rhsHostname, rhsPort)): 48 | return lhsHostname == rhsHostname && lhsPort == rhsPort 49 | case let (.unix(lhsPath), .unix(rhsPath)): 50 | return lhsPath == rhsPath 51 | default: 52 | return false 53 | } 54 | } 55 | 56 | 57 | extension Address : ArgumentConvertible { 58 | public init(parser: ArgumentParser) throws { 59 | if let value = parser.shift() { 60 | if value.hasPrefix("unix:") { 61 | let prefixEnd = value.index(value.startIndex, offsetBy: 5) 62 | self = .unix(path: value[prefixEnd ..< value.endIndex]) 63 | } else { 64 | let components = value.characters.split(separator: ":").map(String.init) 65 | if components.count != 2 { 66 | throw ArgumentError.invalidType(value: value, type: "hostname and port separated by `:`.", argument: nil) 67 | } 68 | 69 | if let port = UInt16(components[1]) { 70 | self = .ip(hostname: components[0], port: port) 71 | } else { 72 | throw ArgumentError.invalidType(value: components[1], type: "number", argument: "port") 73 | } 74 | } 75 | } else { 76 | throw ArgumentError.missingValue(argument: nil) 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Sources/Curassow/Arbiter.swift: -------------------------------------------------------------------------------- 1 | #if os(Linux) 2 | import Glibc 3 | private let system_fork = Glibc.fork 4 | #else 5 | import Darwin.C 6 | @_silgen_name("fork") private func system_fork() -> Int32 7 | #endif 8 | 9 | import fd 10 | import Nest 11 | 12 | 13 | /// Arbiter maintains the worker processes 14 | public final class Arbiter { 15 | let configuration: Configuration 16 | let logger = Logger() 17 | var listeners: [Socket] = [] 18 | var workers: [pid_t: WorkerProcess] = [:] 19 | 20 | var numberOfWorkers: Int 21 | 22 | let application: (RequestType) -> ResponseType 23 | 24 | var signalHandler: SignalHandler! 25 | 26 | public init(configuration: Configuration, workers: Int, application: @escaping Application) { 27 | self.configuration = configuration 28 | self.numberOfWorkers = workers 29 | self.application = application 30 | } 31 | 32 | func createSockets() throws { 33 | for address in configuration.addresses { 34 | listeners.append(try address.socket(configuration.backlog)) 35 | logger.info("Listening at http://\(address) (\(getpid()))") 36 | } 37 | } 38 | 39 | func registerSignals() throws { 40 | signalHandler = try SignalHandler() 41 | signalHandler.register(.interrupt, handleINT) 42 | signalHandler.register(.quit, handleQUIT) 43 | signalHandler.register(.terminate, handleTerminate) 44 | signalHandler.register(.ttin, handleTTIN) 45 | signalHandler.register(.ttou, handleTTOU) 46 | signalHandler.register(.child, handleChild) 47 | sharedHandler = signalHandler 48 | SignalHandler.registerSignals() 49 | } 50 | 51 | var running = false 52 | 53 | // Main run loop for the master process 54 | public func run(daemonize: Bool = false) throws -> Never { 55 | running = true 56 | 57 | try registerSignals() 58 | try createSockets() 59 | 60 | if daemonize { 61 | let devnull = open("/dev/null", O_RDWR) 62 | if devnull == -1 { 63 | throw SocketError() 64 | } 65 | 66 | if system_fork() != 0 { 67 | exit(0) 68 | } 69 | 70 | setsid() 71 | 72 | for descriptor in Int32(0).. Never { 103 | stop() 104 | logger.info("Shutting down") 105 | exit(exitStatus) 106 | } 107 | 108 | /// Sleep, waiting for stuff to happen on our signal pipe 109 | func sleep() { 110 | let timeout: timeval 111 | 112 | if configuration.timeout > 0 { 113 | timeout = timeval(tv_sec: configuration.timeout, tv_usec: 0) 114 | } else { 115 | timeout = timeval(tv_sec: 30, tv_usec: 0) 116 | } 117 | 118 | let fds = [Socket(descriptor: signalHandler.pipe.reader.fileNumber)] 119 | let result = try? select(reads: fds, writes: [Socket](), errors: [Socket](), timeout: timeout) 120 | let read = result?.reads ?? [] 121 | 122 | if !read.isEmpty { 123 | do { 124 | while try signalHandler.pipe.reader.read(1).count > 0 {} 125 | } catch {} 126 | } 127 | } 128 | 129 | // MARK: Handle Signals 130 | 131 | func handleINT() { 132 | stop(false) 133 | } 134 | 135 | func handleQUIT() { 136 | stop(false) 137 | } 138 | 139 | func handleTerminate() { 140 | running = false 141 | } 142 | 143 | /// Increases the amount of workers by one 144 | func handleTTIN() { 145 | numberOfWorkers += 1 146 | manageWorkers() 147 | } 148 | 149 | /// Decreases the amount of workers by one 150 | func handleTTOU() { 151 | if numberOfWorkers > 1 { 152 | numberOfWorkers -= 1 153 | manageWorkers() 154 | } 155 | } 156 | 157 | func handleChild() { 158 | while true { 159 | var stat: Int32 = 0 160 | let pid = waitpid(-1, &stat, WNOHANG) 161 | if pid == -1 { 162 | break 163 | } 164 | 165 | workers.removeValue(forKey: pid) 166 | } 167 | 168 | manageWorkers() 169 | } 170 | 171 | // MARK: Worker 172 | 173 | // Maintain number of workers by spawning or killing as required. 174 | func manageWorkers() { 175 | spawnWorkers() 176 | murderExcessWorkers() 177 | } 178 | 179 | // Spawn workers until we have enough 180 | func spawnWorkers() { 181 | let neededWorkers = numberOfWorkers - workers.count 182 | if neededWorkers > 0 { 183 | for _ in 0..= configuration.timeout { 200 | if worker.aborted { 201 | if kill(pid, SIGKILL) == ESRCH { 202 | workers.removeValue(forKey: pid) 203 | } 204 | } else { 205 | worker.aborted = true 206 | 207 | logger.critical("Worker timeout (pid: \(pid))") 208 | 209 | if kill(pid, SIGABRT) == ESRCH { 210 | workers.removeValue(forKey: pid) 211 | } 212 | } 213 | } 214 | } 215 | } 216 | 217 | // Murder unused workers, oldest first 218 | func murderExcessWorkers() { 219 | let killCount = workers.count - numberOfWorkers 220 | if killCount > 0 { 221 | for _ in 0.. Int { 13 | let value = getenv(key) 14 | if value != nil { 15 | if let stringValue = String(validatingUTF8: value!), let intValue = Int(stringValue) { 16 | return intValue 17 | } 18 | } 19 | 20 | return `default` 21 | } 22 | 23 | 24 | struct ServeError : Error, CustomStringConvertible { 25 | let description: String 26 | 27 | init(_ description: String) { 28 | self.description = description 29 | } 30 | } 31 | 32 | 33 | public func serve(_ closure: @escaping (RequestType) -> ResponseType) -> Never { 34 | let port = UInt16(getIntEnv("PORT", default: 8000)) 35 | let workers = getIntEnv("WEB_CONCURRENCY", default: 1) 36 | 37 | command( 38 | Option("worker-type", "sync"), 39 | Option("workers", workers, description: "The number of processes for handling requests."), 40 | VariadicOption("bind", [Address.ip(hostname: "0.0.0.0", port: port)], description: "The address to bind sockets."), 41 | Option("timeout", 30, description: "Amount of seconds to wait on a worker without activity before killing and restarting the worker."), 42 | Flag("daemon", description: "Detaches the server from the controlling terminal and enter the background.") 43 | ) { workerType, workers, addresses, timeout, daemonize in 44 | var configuration = Configuration() 45 | configuration.addresses = addresses 46 | configuration.timeout = timeout 47 | 48 | if workerType == "synchronous" || workerType == "sync" { 49 | let arbiter = Arbiter(configuration: configuration, workers: workers, application: closure) 50 | try arbiter.run(daemonize: daemonize) 51 | } else if workerType == "dispatch" || workerType == "gcd" { 52 | let arbiter = Arbiter(configuration: configuration, workers: workers, application: closure) 53 | try arbiter.run(daemonize: daemonize) 54 | } else { 55 | throw ArgumentError.invalidType(value: workerType, type: "worker type", argument: "worker-type") 56 | } 57 | }.run() 58 | } 59 | -------------------------------------------------------------------------------- /Sources/Curassow/Data.swift: -------------------------------------------------------------------------------- 1 | #if os(Linux) 2 | import Glibc 3 | #else 4 | import Darwin.C 5 | #endif 6 | 7 | class Data { 8 | let bytes: UnsafeMutableRawPointer 9 | let capacity: Int 10 | 11 | init(capacity: Int) { 12 | self.bytes = UnsafeMutableRawPointer(malloc(capacity + 1)) 13 | // bytes = UnsafeMutablePointer(malloc(capacity + 1)) 14 | self.capacity = capacity 15 | } 16 | 17 | deinit { 18 | free(bytes) 19 | } 20 | 21 | var characters: [CChar] { 22 | var data = [CChar](repeating: 0, count: capacity) 23 | memcpy(&data, bytes, data.count) 24 | return data 25 | } 26 | 27 | var string: String? { 28 | let pointer = bytes.bindMemory(to: CChar.self, capacity: capacity) 29 | return String(cString: pointer) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Sources/Curassow/DispatchWorker.swift: -------------------------------------------------------------------------------- 1 | #if os(Linux) 2 | import Glibc 3 | #else 4 | import Darwin.C 5 | #endif 6 | 7 | import Dispatch 8 | import fd 9 | import Nest 10 | import Inquiline 11 | 12 | 13 | final public class DispatchWorker : WorkerType { 14 | let configuration: Configuration 15 | let logger: Logger 16 | let listeners: [Socket] 17 | let notify: (Void) -> Void 18 | let application: (RequestType) -> ResponseType 19 | 20 | public init(configuration: Configuration, logger: Logger, listeners: [Listener], notify: @escaping (Void) -> Void, application: @escaping Application) { 21 | self.logger = logger 22 | self.listeners = listeners.map { Socket(descriptor: $0.fileNumber) } 23 | self.configuration = configuration 24 | self.notify = notify 25 | self.application = application 26 | } 27 | 28 | public func run() { 29 | logger.info("Booting worker process with pid: \(getpid())") 30 | 31 | let timerSource = configureTimer() 32 | timerSource.resume() 33 | 34 | listeners.forEach(registerSocketHandler) 35 | 36 | // Gracefully shutdown 37 | signal(SIGTERM, SIG_IGN) 38 | let terminateSignal = DispatchSource.makeSignalSource(signal: SIGTERM, queue: DispatchQueue.main) 39 | terminateSignal.setEventHandler { [unowned self] in 40 | self.exitWorker() 41 | } 42 | terminateSignal.resume() 43 | 44 | // Quick shutdown 45 | signal(SIGQUIT, SIG_IGN) 46 | let quitSignal = DispatchSource.makeSignalSource(signal: SIGQUIT, queue: DispatchQueue.main) 47 | quitSignal.setEventHandler { [unowned self] in 48 | self.exitWorker() 49 | } 50 | quitSignal.resume() 51 | 52 | signal(SIGINT, SIG_IGN) 53 | let interruptSignal = DispatchSource.makeSignalSource(signal: SIGINT, queue: DispatchQueue.main) 54 | interruptSignal.setEventHandler { [unowned self] in 55 | self.exitWorker() 56 | } 57 | interruptSignal.resume() 58 | 59 | dispatchMain() 60 | } 61 | 62 | func exitWorker() { 63 | logger.info("Worker exiting (pid: \(getpid()))") 64 | exit(0) 65 | } 66 | 67 | func registerSocketHandler(socket: Socket) { 68 | socket.consume { [unowned self] (source, socket) in 69 | if let connection = try? socket.accept() { 70 | let clientSocket = Socket(descriptor: connection.fileNumber) 71 | // TODO: Handle socket asyncronously, use GCD to observe data 72 | 73 | clientSocket.blocking = true 74 | self.handle(client: clientSocket) 75 | } 76 | } 77 | } 78 | 79 | func configureTimer() -> DispatchSourceTimer { 80 | let timer = DispatchSource.makeTimerSource() 81 | timer.scheduleRepeating(deadline: .now(), interval: .seconds(configuration.timeout / 2)) 82 | 83 | timer.setEventHandler { [unowned self] in 84 | self.notify() 85 | } 86 | 87 | return timer 88 | } 89 | 90 | func handle(client: Socket) { 91 | let parser = HTTPParser(reader: client) 92 | 93 | let response: ResponseType 94 | 95 | do { 96 | let request = try parser.parse() 97 | response = application(request) 98 | print("[worker] \(request.method) \(request.path) - \(response.statusLine)") 99 | } catch let error as HTTPParserError { 100 | response = error.response() 101 | } catch { 102 | print("[worker] Unknown error: \(error)") 103 | response = Response(.internalServerError, contentType: "text/plain", content: "Internal Server Error") 104 | } 105 | 106 | sendResponse(client, response: response) 107 | 108 | client.shutdown() 109 | _ = try? client.close() 110 | } 111 | } 112 | 113 | 114 | extension Socket { 115 | func consume(closure: @escaping (DispatchSourceRead, Socket) -> ()) { 116 | let source = DispatchSource.makeReadSource(fileDescriptor: fileNumber) 117 | 118 | source.setEventHandler { [unowned self] in 119 | closure(source, self) 120 | } 121 | 122 | source.setCancelHandler { [unowned self] in 123 | _ = try? self.close() 124 | } 125 | 126 | source.resume() 127 | } 128 | /* 129 | func consumeData(closure: (Socket, Data) -> ()) { 130 | consume { source, socket in 131 | let estimated = Int(dispatch_source_get_data(source)) 132 | let data = self.read(estimated) 133 | closure(socket, data) 134 | } 135 | } 136 | */ 137 | } 138 | -------------------------------------------------------------------------------- /Sources/Curassow/HTTPParser.swift: -------------------------------------------------------------------------------- 1 | #if os(Linux) 2 | import Glibc 3 | #else 4 | import Darwin.C 5 | #endif 6 | 7 | import Nest 8 | import Inquiline 9 | 10 | 11 | enum HTTPParserError : Error { 12 | case badSyntax(String) 13 | case badVersion(String) 14 | case incomplete 15 | case `internal` 16 | 17 | func response() -> ResponseType { 18 | func error(_ status: Status, message: String) -> ResponseType { 19 | return Response(status, contentType: "text/plain", content: message) 20 | } 21 | 22 | switch self { 23 | case let .badSyntax(syntax): 24 | return error(.badRequest, message: "Bad Syntax (\(syntax))") 25 | case let .badVersion(version): 26 | return error(.badRequest, message: "Bad Version (\(version))") 27 | case .incomplete: 28 | return error(.badRequest, message: "Incomplete HTTP Request") 29 | case .internal: 30 | return error(.internalServerError, message: "Internal Server Error") 31 | } 32 | } 33 | } 34 | 35 | 36 | class HTTPParser { 37 | let reader: Unreader 38 | 39 | init(reader: Readable) { 40 | self.reader = Unreader(reader: reader) 41 | } 42 | 43 | func readUntil(_ bytes: [Int8]) throws -> [Int8] { 44 | if bytes.isEmpty { 45 | return [] 46 | } 47 | 48 | var buffer: [Int8] = [] 49 | while true { 50 | let read = try reader.read(8192) 51 | if read.isEmpty { 52 | return [] 53 | } 54 | 55 | buffer += read 56 | if let (top, bottom) = buffer.find(bytes) { 57 | reader.unread(bottom) 58 | return top 59 | } 60 | } 61 | } 62 | 63 | // Read the socket until we find \r\n\r\n 64 | func readHeaders() throws -> String { 65 | let crln: [CChar] = [13, 10, 13, 10] 66 | let buffer = try readUntil(crln) 67 | 68 | if buffer.isEmpty { 69 | throw HTTPParserError.incomplete 70 | } 71 | 72 | if let headers = String(validatingUTF8: (buffer + [0])) { 73 | return headers 74 | } 75 | 76 | print("[worker] Failed to decode data from client") 77 | throw HTTPParserError.internal 78 | } 79 | 80 | func parse() throws -> RequestType { 81 | let top = try readHeaders() 82 | var components = top.split(separator: "\r\n") 83 | let requestLine = components.removeFirst() 84 | components.removeLast() 85 | let requestComponents = requestLine.split(separator: " ") 86 | if requestComponents.count != 3 { 87 | throw HTTPParserError.badSyntax(requestLine) 88 | } 89 | 90 | let method = requestComponents[0] 91 | let path = requestComponents[1] 92 | let version = requestComponents[2] 93 | 94 | if !version.hasPrefix("HTTP/1") { 95 | throw HTTPParserError.badVersion(version) 96 | } 97 | 98 | let headers = parseHeaders(components) 99 | let contentSize = headers.filter { $0.0.lowercased() == "content-length" }.flatMap { Int($0.1) }.first 100 | let payload = ReaderPayload(reader: reader, contentSize: contentSize) 101 | return Request(method: method, path: path, headers: headers, content: payload) 102 | } 103 | 104 | func parseHeaders(_ headers: [String]) -> [Header] { 105 | return headers.map { $0.split(separator: ":", maxSeparator: 1) }.flatMap { 106 | if $0.count == 2 { 107 | let key = $0[0] 108 | var value = $0[1] 109 | 110 | if value.hasPrefix(" ") { 111 | value.remove(at: value.startIndex) 112 | return (key, value) 113 | } 114 | 115 | return (key, value) 116 | } 117 | 118 | return nil 119 | } 120 | } 121 | } 122 | 123 | 124 | extension Collection where Iterator.Element == CChar { 125 | fileprivate func find(_ characters: [CChar]) -> ([CChar], [CChar])? { 126 | var lhs: [CChar] = [] 127 | var rhs = Array(self) 128 | 129 | while !rhs.isEmpty { 130 | let character = rhs.remove(at: 0) 131 | lhs.append(character) 132 | if lhs.hasSuffix(characters) { 133 | return (lhs, rhs) 134 | } 135 | } 136 | 137 | return nil 138 | } 139 | 140 | fileprivate func hasSuffix(_ characters: [CChar]) -> Bool { 141 | let chars = Array(self) 142 | if chars.count >= characters.count { 143 | let index = chars.count - characters.count 144 | return Array(chars[index.. [String] { 154 | let scanner = Scanner(self) 155 | var components: [String] = [] 156 | var scans = 0 157 | 158 | while !scanner.isEmpty && scans <= maxSeparator { 159 | components.append(scanner.scan(until: separator)) 160 | scans += 1 161 | } 162 | 163 | return components 164 | } 165 | } 166 | 167 | 168 | fileprivate class Scanner { 169 | var content: String 170 | 171 | init(_ content: String) { 172 | self.content = content 173 | } 174 | 175 | var isEmpty: Bool { 176 | return content.characters.count == 0 177 | } 178 | 179 | func scan(until: String) -> String { 180 | if until.isEmpty { 181 | return "" 182 | } 183 | 184 | var characters: [Character] = [] 185 | 186 | while !content.isEmpty { 187 | let character = content.characters.first! 188 | content = String(content.characters.dropFirst()) 189 | 190 | characters.append(character) 191 | 192 | if content.hasPrefix(until) { 193 | let index = content.characters.index(content.characters.startIndex, offsetBy: until.characters.count) 194 | content = String(content.characters[index.. Bool { 205 | let characters = utf16 206 | let prefixCharacters = prefix.utf16 207 | let start = characters.startIndex 208 | let prefixStart = prefixCharacters.startIndex 209 | 210 | if characters.count < prefixCharacters.count { 211 | return false 212 | } 213 | 214 | for idx in 0.. [UInt8]? { 240 | if !buffer.isEmpty { 241 | if let remainingSize = remainingSize { 242 | self.remainingSize = remainingSize - self.buffer.count 243 | } 244 | 245 | let buffer = self.buffer 246 | self.buffer = [] 247 | return buffer 248 | } 249 | 250 | if let remainingSize = remainingSize, remainingSize <= 0 { 251 | return nil 252 | } 253 | 254 | let size = min(remainingSize ?? bufferSize, bufferSize) 255 | if let bytes = try? reader.read(size) { 256 | if let remainingSize = remainingSize { 257 | self.remainingSize = remainingSize - bytes.count 258 | } 259 | 260 | return bytes.map { UInt8($0) } 261 | } 262 | 263 | return nil 264 | } 265 | 266 | func toPayload() -> PayloadType { 267 | return self 268 | } 269 | } 270 | -------------------------------------------------------------------------------- /Sources/Curassow/Logger.swift: -------------------------------------------------------------------------------- 1 | #if os(Linux) 2 | import Glibc 3 | #else 4 | import Darwin.C 5 | #endif 6 | 7 | 8 | public final class Logger { 9 | func currentTime() -> String { 10 | var t = time(nil) 11 | let tm = localtime(&t) 12 | var buffer = [Int8](repeating: 0, count: 64) 13 | strftime(&buffer, 64, "%Y-%m-%d %T %z", tm) 14 | return String(cString: buffer) 15 | } 16 | 17 | public func info(_ message: String) { 18 | print("[\(currentTime())] [\(getpid())] [INFO] \(message)") 19 | } 20 | 21 | public func critical(_ message: String) { 22 | print("[\(currentTime())] [\(getpid())] [CRITICAL] \(message)") 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/Curassow/Signals.swift: -------------------------------------------------------------------------------- 1 | #if os(Linux) 2 | import Glibc 3 | #else 4 | import Darwin.C 5 | #endif 6 | 7 | import fd 8 | 9 | var sharedHandler: SignalHandler? 10 | 11 | class SignalHandler { 12 | enum Signal { 13 | case interrupt 14 | case quit 15 | case ttin 16 | case ttou 17 | case terminate 18 | case child 19 | } 20 | 21 | class func registerSignals() { 22 | signal(SIGTERM) { _ in sharedHandler?.handle(.terminate) } 23 | signal(SIGINT) { _ in sharedHandler?.handle(.interrupt) } 24 | signal(SIGQUIT) { _ in sharedHandler?.handle(.quit) } 25 | signal(SIGTTIN) { _ in sharedHandler?.handle(.ttin) } 26 | signal(SIGTTOU) { _ in sharedHandler?.handle(.ttou) } 27 | signal(SIGCHLD) { _ in sharedHandler?.handle(.child) } 28 | } 29 | 30 | class func reset() { 31 | signal(SIGTERM, SIG_DFL) 32 | signal(SIGINT, SIG_DFL) 33 | signal(SIGQUIT, SIG_DFL) 34 | signal(SIGTTIN, SIG_DFL) 35 | signal(SIGTTOU, SIG_DFL) 36 | signal(SIGCHLD, SIG_DFL) 37 | } 38 | 39 | var pipe: (reader: ReadableFileDescriptor, writer: WritableFileDescriptor) 40 | var signalQueue: [Signal] = [] 41 | 42 | init() throws { 43 | pipe = try fd.pipe() 44 | pipe.reader.closeOnExec = true 45 | pipe.reader.blocking = false 46 | pipe.writer.closeOnExec = true 47 | pipe.writer.blocking = false 48 | } 49 | 50 | // Wake up the process by writing to the pipe 51 | func wakeup() { 52 | _ = try? pipe.writer.write([46]) 53 | } 54 | 55 | func handle(_ signal: Signal) { 56 | signalQueue.append(signal) 57 | wakeup() 58 | } 59 | 60 | var callbacks: [Signal: () -> ()] = [:] 61 | func register(_ signal: Signal, _ callback: @escaping () -> ()) { 62 | callbacks[signal] = callback 63 | } 64 | 65 | func process() -> Bool { 66 | let result = !signalQueue.isEmpty 67 | 68 | if !signalQueue.isEmpty { 69 | if let handler = callbacks[signalQueue.removeFirst()] { 70 | handler() 71 | } 72 | } 73 | 74 | return result 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Sources/Curassow/Socket.swift: -------------------------------------------------------------------------------- 1 | #if os(Linux) 2 | import Glibc 3 | 4 | private let sock_stream = Int32(SOCK_STREAM.rawValue) 5 | 6 | private let system_accept = Glibc.accept 7 | private let system_bind = Glibc.bind 8 | private let system_listen = Glibc.listen 9 | private let system_send = Glibc.send 10 | private let system_write = Glibc.write 11 | private let system_shutdown = Glibc.shutdown 12 | private let system_select = Glibc.select 13 | private let system_pipe = Glibc.pipe 14 | #else 15 | import Darwin.C 16 | 17 | private let sock_stream = SOCK_STREAM 18 | 19 | private let system_accept = Darwin.accept 20 | private let system_bind = Darwin.bind 21 | private let system_listen = Darwin.listen 22 | private let system_send = Darwin.send 23 | private let system_write = Darwin.write 24 | private let system_shutdown = Darwin.shutdown 25 | private let system_select = Darwin.select 26 | private let system_pipe = Darwin.pipe 27 | #endif 28 | 29 | import fd 30 | 31 | 32 | struct SocketError : Error, CustomStringConvertible { 33 | let function: String 34 | let number: Int32 35 | 36 | init(function: String = #function) { 37 | self.function = function 38 | self.number = errno 39 | } 40 | 41 | var description: String { 42 | return "Socket.\(function) failed [\(number)]" 43 | } 44 | } 45 | 46 | 47 | protocol Readable { 48 | func read(_ bytes: Int) throws -> [Int8] 49 | } 50 | 51 | 52 | class Unreader : Readable { 53 | let reader: Readable 54 | var buffer: [Int8] = [] 55 | 56 | init(reader: Readable) { 57 | self.reader = reader 58 | } 59 | 60 | func read(_ bytes: Int) throws -> [Int8] { 61 | if !buffer.isEmpty { 62 | let buffer = self.buffer 63 | self.buffer = [] 64 | return buffer 65 | } 66 | 67 | return try reader.read(bytes) 68 | } 69 | 70 | func unread(_ buffer: [Int8]) { 71 | self.buffer += buffer 72 | } 73 | } 74 | 75 | 76 | /// Represents a TCP AF_INET/AF_UNIX socket 77 | final public class Socket : Readable, FileDescriptor, ReadableFileDescriptor, Listener, Connection { 78 | typealias Port = UInt16 79 | 80 | public let fileNumber: FileNumber 81 | 82 | class func pipe() throws -> (read: Socket, write: Socket) { 83 | var fds: [Int32] = [0, 0] 84 | if system_pipe(&fds) == -1 { 85 | throw SocketError() 86 | } 87 | return (Socket(descriptor: fds[0]), Socket(descriptor: fds[1])) 88 | } 89 | 90 | init(family: Int32 = AF_INET) throws { 91 | #if os(Linux) 92 | fileNumber = socket(family, sock_stream, 0) 93 | #else 94 | fileNumber = socket(family, sock_stream, family == AF_UNIX ? 0 : IPPROTO_TCP) 95 | #endif 96 | assert(fileNumber > 0) 97 | 98 | var value: Int32 = 1; 99 | guard setsockopt(fileNumber, SOL_SOCKET, SO_REUSEADDR, &value, socklen_t(MemoryLayout.size)) != -1 else { 100 | throw SocketError(function: "setsockopt()") 101 | } 102 | 103 | #if !os(Linux) 104 | guard setsockopt(fileNumber, SOL_SOCKET, SO_NOSIGPIPE, &value, socklen_t(MemoryLayout.size)) != -1 else { 105 | throw SocketError(function: "setsockopt()") 106 | } 107 | #endif 108 | } 109 | 110 | init(descriptor: FileNumber) { 111 | self.fileNumber = descriptor 112 | } 113 | 114 | func listen(_ backlog: Int32) throws { 115 | if system_listen(fileNumber, backlog) == -1 { 116 | throw SocketError() 117 | } 118 | } 119 | 120 | func bind(_ address: String, port: Port) throws { 121 | var addr = sockaddr_in() 122 | addr.sin_family = sa_family_t(AF_INET) 123 | addr.sin_port = in_port_t(htons(in_port_t(port))) 124 | addr.sin_addr = in_addr(s_addr: address.withCString { inet_addr($0) }) 125 | addr.sin_zero = (0, 0, 0, 0, 0, 0, 0, 0) 126 | 127 | let len = socklen_t(UInt8(MemoryLayout.size)) 128 | 129 | try withUnsafePointer(to: &addr) { 130 | try $0.withMemoryRebound(to: sockaddr.self, capacity: 1) { 131 | guard system_bind(fileNumber, $0, len) != -1 else { 132 | throw SocketError() 133 | } 134 | } 135 | } 136 | } 137 | 138 | func bind(_ path: String) throws { 139 | var addr = sockaddr_un() 140 | addr.sun_family = sa_family_t(AF_UNIX) 141 | 142 | let lengthOfPath = path.withCString { Int(strlen($0)) } 143 | 144 | guard lengthOfPath < MemoryLayout.size(ofValue: addr.sun_path) else { 145 | throw SocketError() 146 | } 147 | 148 | _ = withUnsafeMutablePointer(to: &addr.sun_path.0) { ptr in 149 | path.withCString { 150 | strncpy(ptr, $0, lengthOfPath) 151 | } 152 | } 153 | 154 | try withUnsafePointer(to: &addr) { 155 | try $0.withMemoryRebound(to: sockaddr.self, capacity: 1) { 156 | guard system_bind(fileNumber, $0, UInt32(MemoryLayout.stride)) != -1 else { 157 | throw SocketError() 158 | } 159 | } 160 | } 161 | } 162 | 163 | public func accept() throws -> Connection { 164 | let descriptor = system_accept(fileNumber, nil, nil) 165 | if descriptor == -1 { 166 | throw SocketError() 167 | } 168 | return Socket(descriptor: descriptor) 169 | } 170 | 171 | func shutdown() { 172 | _ = system_shutdown(fileNumber, Int32(SHUT_RDWR)) 173 | } 174 | 175 | func send(_ output: String) { 176 | output.withCString { bytes in 177 | #if os(Linux) 178 | let flags = Int32(MSG_NOSIGNAL) 179 | #else 180 | let flags = Int32(0) 181 | #endif 182 | _ = system_send(fileNumber, bytes, Int(strlen(bytes)), flags) 183 | } 184 | } 185 | 186 | func send(_ bytes: [UInt8]) { 187 | #if os(Linux) 188 | let flags = Int32(MSG_NOSIGNAL) 189 | #else 190 | let flags = Int32(0) 191 | #endif 192 | _ = system_send(fileNumber, bytes, bytes.count, flags) 193 | } 194 | 195 | func write(_ output: String) { 196 | _ = output.withCString { bytes in 197 | system_write(fileNumber, bytes, Int(strlen(bytes))) 198 | } 199 | } 200 | 201 | /// Returns whether the socket is set to non-blocking or blocking 202 | var blocking: Bool { 203 | get { 204 | let flags = fcntl(fileNumber, F_GETFL, 0) 205 | return flags & O_NONBLOCK == 0 206 | } 207 | 208 | set { 209 | let flags = fcntl(fileNumber, F_GETFL, 0) 210 | let newFlags: Int32 211 | 212 | if newValue { 213 | newFlags = flags & ~O_NONBLOCK 214 | } else { 215 | newFlags = flags | O_NONBLOCK 216 | } 217 | 218 | let _ = fcntl(fileNumber, F_SETFL, newFlags) 219 | } 220 | } 221 | 222 | /// Returns whether the socket is has the FD_CLOEXEC flag set 223 | var closeOnExec: Bool { 224 | get { 225 | let flags = fcntl(fileNumber, F_GETFL, 0) 226 | return flags & FD_CLOEXEC == 1 227 | } 228 | 229 | set { 230 | let flags = fcntl(fileNumber, F_GETFL, 0) 231 | let newFlags: Int32 232 | 233 | if newValue { 234 | newFlags = flags ^ FD_CLOEXEC 235 | } else { 236 | newFlags = flags | FD_CLOEXEC 237 | } 238 | 239 | let _ = fcntl(fileNumber, F_SETFL, newFlags) 240 | } 241 | } 242 | 243 | fileprivate func htons(_ value: CUnsignedShort) -> CUnsignedShort { 244 | return (value << 8) + (value >> 8) 245 | } 246 | } 247 | 248 | extension FileDescriptor { 249 | /// Returns whether the socket is set to non-blocking or blocking 250 | var blocking: Bool { 251 | get { 252 | let flags = fcntl(fileNumber, F_GETFL, 0) 253 | return flags & O_NONBLOCK == 0 254 | } 255 | 256 | set { 257 | let flags = fcntl(fileNumber, F_GETFL, 0) 258 | let newFlags: Int32 259 | 260 | if newValue { 261 | newFlags = flags & ~O_NONBLOCK 262 | } else { 263 | newFlags = flags | O_NONBLOCK 264 | } 265 | 266 | let _ = fcntl(fileNumber, F_SETFL, newFlags) 267 | } 268 | } 269 | 270 | /// Returns whether the socket is has the FD_CLOEXEC flag set 271 | var closeOnExec: Bool { 272 | get { 273 | let flags = fcntl(fileNumber, F_GETFL, 0) 274 | return flags & FD_CLOEXEC == 1 275 | } 276 | 277 | set { 278 | let flags = fcntl(fileNumber, F_GETFL, 0) 279 | let newFlags: Int32 280 | 281 | if newValue { 282 | newFlags = flags ^ FD_CLOEXEC 283 | } else { 284 | newFlags = flags | FD_CLOEXEC 285 | } 286 | 287 | let _ = fcntl(fileNumber, F_SETFL, newFlags) 288 | } 289 | } 290 | } 291 | -------------------------------------------------------------------------------- /Sources/Curassow/SynchronousWorker.swift: -------------------------------------------------------------------------------- 1 | #if os(Linux) 2 | import Glibc 3 | #else 4 | import Darwin.C 5 | #endif 6 | 7 | import fd 8 | import Nest 9 | import Inquiline 10 | 11 | 12 | public final class SynchronousWorker : WorkerType { 13 | let configuration: Configuration 14 | let logger: Logger 15 | let listeners: [Socket] 16 | 17 | var timeout: Int { 18 | return configuration.timeout / 2 19 | } 20 | 21 | let notify: (Void) -> Void 22 | 23 | let application: (RequestType) -> ResponseType 24 | var isAlive: Bool = false 25 | let parentPid: pid_t 26 | 27 | public init(configuration: Configuration, logger: Logger, listeners: [Listener], notify: @escaping (Void) -> Void, application: @escaping Application) { 28 | self.parentPid = getpid() 29 | self.logger = logger 30 | self.listeners = listeners.map { Socket(descriptor: $0.fileNumber) } 31 | self.configuration = configuration 32 | self.notify = notify 33 | self.application = application 34 | } 35 | 36 | func registerSignals() throws { 37 | let signals = try SignalHandler() 38 | signals.register(.interrupt, handleQuit) 39 | signals.register(.quit, handleQuit) 40 | signals.register(.terminate, handleTerminate) 41 | sharedHandler = signals 42 | SignalHandler.registerSignals() 43 | } 44 | 45 | public func run() { 46 | logger.info("Booting worker process with pid: \(getpid())") 47 | 48 | do { 49 | try registerSignals() 50 | } catch { 51 | logger.info("Failed to boot \(error)") 52 | return 53 | } 54 | isAlive = true 55 | 56 | listeners.forEach { $0.blocking = false } 57 | 58 | if listeners.count == 1 { 59 | runOne(listeners.first!) 60 | } else { 61 | runMultiple(listeners) 62 | } 63 | } 64 | 65 | func isParentAlive() -> Bool { 66 | if getppid() != parentPid { 67 | logger.info("Parent changed, shutting down") 68 | return false 69 | } 70 | 71 | return true 72 | } 73 | 74 | func runOne(_ listener: Socket) { 75 | while isAlive { 76 | _ = sharedHandler?.process() 77 | notify() 78 | accept(listener) 79 | 80 | if !isParentAlive() { 81 | return 82 | } 83 | 84 | _ = wait() 85 | } 86 | } 87 | 88 | func runMultiple(_ listeners: [Socket]) { 89 | while isAlive { 90 | _ = sharedHandler?.process() 91 | notify() 92 | 93 | let sockets = wait().filter { 94 | $0.fileNumber != sharedHandler!.pipe.reader.fileNumber 95 | } 96 | 97 | sockets.forEach(accept) 98 | 99 | if !isParentAlive() { 100 | return 101 | } 102 | } 103 | } 104 | 105 | // MARK: Signal Handling 106 | 107 | func handleQuit() { 108 | isAlive = false 109 | } 110 | 111 | func handleTerminate() { 112 | isAlive = false 113 | } 114 | 115 | func wait() -> [Socket] { 116 | let timeout: timeval 117 | 118 | if self.timeout > 0 { 119 | timeout = timeval(tv_sec: self.timeout, tv_usec: 0) 120 | } else { 121 | timeout = timeval(tv_sec: 120, tv_usec: 0) 122 | } 123 | 124 | let socks = listeners + [Socket(descriptor: sharedHandler!.pipe.reader.fileNumber)] 125 | let result = try? fd.select(reads: socks, writes: [Socket](), errors: [Socket](), timeout: timeout) 126 | return result?.reads ?? [] 127 | } 128 | 129 | func accept(_ listener: Socket) { 130 | if let connection = try? listener.accept() { 131 | let client = Socket(descriptor: connection.fileNumber) 132 | client.blocking = true 133 | handle(client) 134 | } 135 | } 136 | 137 | func handle(_ client: Socket) { 138 | let parser = HTTPParser(reader: client) 139 | 140 | let response: ResponseType 141 | 142 | do { 143 | let request = try parser.parse() 144 | response = application(request) 145 | print("[worker] \(request.method) \(request.path) - \(response.statusLine)") 146 | } catch let error as HTTPParserError { 147 | response = error.response() 148 | } catch { 149 | print("[worker] Unknown error: \(error)") 150 | response = Response(.internalServerError, contentType: "text/plain", content: "Internal Server Error") 151 | } 152 | 153 | sendResponse(client, response: response) 154 | 155 | client.shutdown() 156 | _ = try? client.close() 157 | } 158 | } 159 | 160 | 161 | func sendResponse(_ client: Socket, response: ResponseType) { 162 | client.send("HTTP/1.1 \(response.statusLine)\r\n") 163 | client.send("Connection: close\r\n") 164 | 165 | for (key, value) in response.headers { 166 | if key != "Connection" { 167 | client.send("\(key): \(value)\r\n") 168 | } 169 | } 170 | 171 | client.send("\r\n") 172 | 173 | if let body = response.body { 174 | var body = body 175 | while let bytes = body.next() { 176 | client.send(bytes) 177 | } 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /Sources/Curassow/Worker.swift: -------------------------------------------------------------------------------- 1 | #if os(Linux) 2 | import Glibc 3 | #else 4 | import Darwin.C 5 | #endif 6 | 7 | import fd 8 | import Nest 9 | import Inquiline 10 | 11 | 12 | public typealias Application = (RequestType) -> ResponseType 13 | 14 | 15 | public protocol WorkerType { 16 | /*** Initialises the worker 17 | - Parameters: 18 | - configuration 19 | - logger 20 | - listeners 21 | - notify: A notify callback, this should be retained and invoked to notify the arbiter of your existance to prevent timeouts 22 | - application: The users Nest application 23 | 24 | NOTE: This is invoked from the master process 25 | */ 26 | init(configuration: Configuration, logger: Logger, listeners: [Listener], notify: @escaping (Void) -> Void, application: @escaping Application) 27 | 28 | /*** Runs the worker 29 | The implementation should start listening for requests on the listeners, 30 | and invoke the notify callback before `configuration.timeout` happens. 31 | 32 | NOTE: This is invoked from the workers fork 33 | **/ 34 | func run() 35 | } 36 | 37 | 38 | // Represents a worker 39 | final class WorkerProcess { 40 | /// Indicates when the worker has been aborted, used by the arbiter 41 | var aborted: Bool = false 42 | let temp = WorkerTemp() 43 | 44 | func notify() { 45 | temp.notify() 46 | } 47 | } 48 | 49 | 50 | func getenv(_ key: String, default: String) -> String { 51 | let result = getenv(key) 52 | if result != nil { 53 | if let value = String(validatingUTF8: result!) { 54 | return value 55 | } 56 | } 57 | 58 | return `default` 59 | } 60 | 61 | 62 | class WorkerTemp { 63 | let descriptor: Int32 64 | var state: mode_t = 0 65 | 66 | init() { 67 | 68 | var tempdir = getenv("TMPDIR", default: "/tmp/") 69 | #if !os(Linux) 70 | if !tempdir.hasSuffix("/") { 71 | tempdir += "/" 72 | } 73 | #endif 74 | 75 | let template = "\(tempdir)/curassow.XXXXXXXX" 76 | var templateChars = Array(template.utf8).map { Int8($0) } + [0] 77 | descriptor = withUnsafeMutablePointer(to: &templateChars[0]) { buffer -> Int32 in 78 | return mkstemp(buffer) 79 | } 80 | 81 | if descriptor == -1 { 82 | fatalError("mkstemp(\(template)) failed") 83 | } 84 | 85 | // Find the filename 86 | #if os(Linux) 87 | let filename = Data(capacity: Int(PATH_MAX)) 88 | let pointer = filename.bytes.bindMemory(to: CChar.self, capacity: filename.capacity) 89 | let size = readlink("/proc/self/fd/\(descriptor)", pointer, filename.capacity) 90 | #else 91 | let filename = Data(capacity: Int(MAXPATHLEN)) 92 | if fcntl(descriptor, F_GETPATH, filename.bytes) == -1 { 93 | fatalError("fcntl failed") 94 | } 95 | #endif 96 | 97 | // Unlink, so once last close is done, it gets deleted 98 | unlink(filename.string!) 99 | } 100 | 101 | deinit { 102 | close(descriptor) 103 | } 104 | 105 | func notify() { 106 | if state == 1 { 107 | state = 0 108 | } else { 109 | state = 1 110 | } 111 | 112 | fchmod(descriptor, state) 113 | } 114 | 115 | var lastUpdate: timespec { 116 | var stats = stat() 117 | fstat(descriptor, &stats) 118 | #if os(Linux) 119 | return stats.st_ctim 120 | #else 121 | return stats.st_ctimespec 122 | #endif 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /Sources/example/example.apib: -------------------------------------------------------------------------------- 1 | # GET / 2 | 3 | + Response 200 (text/plain) 4 | 5 | Hello World 6 | -------------------------------------------------------------------------------- /Sources/example/main.swift: -------------------------------------------------------------------------------- 1 | import Curassow 2 | import Inquiline 3 | 4 | 5 | serve { _ in 6 | Response(.ok, contentType: "text/plain", content: "Hello World\n") 7 | } 8 | -------------------------------------------------------------------------------- /Tests/CurassowTests/AddressSpec.swift: -------------------------------------------------------------------------------- 1 | import Spectre 2 | import Commander 3 | import Curassow 4 | 5 | 6 | func testAddress() { 7 | describe("Address") { 8 | $0.describe("CustomStringConvertible") { 9 | $0.it("shows hostname:port description for IP addresses") { 10 | let address = Address.ip(hostname: "127.0.0.1", port: 80) 11 | 12 | try expect(address.description) == "127.0.0.1:80" 13 | } 14 | 15 | $0.it("shows description for UNIX addresses") { 16 | let address = Address.unix(path: "/tmp/curassow") 17 | 18 | try expect(address.description) == "unix:/tmp/curassow" 19 | } 20 | } 21 | 22 | $0.describe("ArgumentConvertible") { 23 | $0.it("can be converted from a host:port") { 24 | let parser = ArgumentParser(arguments: ["127.0.0.1:80"]) 25 | let address = try Address(parser: parser) 26 | 27 | try expect(address) == .ip(hostname: "127.0.0.1", port: 80) 28 | } 29 | 30 | $0.it("can be converted from a host:port") { 31 | let parser = ArgumentParser(arguments: ["unix:/tmp/curassow"]) 32 | let address = try Address(parser: parser) 33 | 34 | try expect(address) == .unix(path: "/tmp/curassow") 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Tests/CurassowTests/ConfigurationSpec.swift: -------------------------------------------------------------------------------- 1 | import Spectre 2 | import Curassow 3 | 4 | 5 | func testConfiguration() { 6 | describe("Configuration") { 7 | let configuration = Configuration() 8 | 9 | $0.it("doesn't have any default addresses") { 10 | try expect(configuration.addresses.count) == 0 11 | } 12 | 13 | $0.it("defaults the timeout to 30 seconds") { 14 | try expect(configuration.timeout) == 30 15 | } 16 | 17 | $0.it("defaults the backlog to 2048 connections") { 18 | try expect(configuration.backlog) == 2048 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Tests/CurassowTests/HTTPParserSpec.swift: -------------------------------------------------------------------------------- 1 | #if os(Linux) 2 | import Glibc 3 | #else 4 | import Darwin 5 | #endif 6 | 7 | import Spectre 8 | import Nest 9 | @testable import Curassow 10 | 11 | 12 | extension PayloadType { 13 | /// Collects every single byte in the payload until returns nil 14 | mutating func collect() -> [UInt8] { 15 | var buffer: [UInt8] = [] 16 | 17 | while true { 18 | if let bytes = next() { 19 | buffer += bytes 20 | } else { 21 | break 22 | } 23 | } 24 | 25 | return buffer 26 | } 27 | } 28 | 29 | 30 | func testHTTPParser() { 31 | describe("HTTPParser") { 32 | var parser: HTTPParser! 33 | var inSocket: Socket! 34 | var outSocket: Socket! 35 | 36 | $0.before { 37 | var descriptors: [Int32] = [0, 0] 38 | if pipe(&descriptors) == -1 { 39 | fatalError("HTTPParser pipe: \(errno)") 40 | } 41 | 42 | inSocket = Socket(descriptor: descriptors[0]) 43 | outSocket = Socket(descriptor: descriptors[1]) 44 | 45 | parser = HTTPParser(reader: inSocket) 46 | } 47 | 48 | $0.after { 49 | try! inSocket?.close() 50 | try! outSocket?.close() 51 | } 52 | 53 | $0.it("can parse a HTTP request") { 54 | outSocket.write("GET / HTTP/1.1\r\nHost: localhost\r\n\r\n") 55 | 56 | let request = try parser.parse() 57 | try expect(request.method) == "GET" 58 | try expect(request.path) == "/" 59 | try expect(request.headers.count) == 1 60 | 61 | let header = request.headers[0] 62 | try expect(header.0) == "Host" 63 | try expect(header.1) == "localhost" 64 | } 65 | 66 | $0.it("provides the message body until Content-Length value") { 67 | outSocket.write("GET / HTTP/1.1\r\nContent-Length: 5\r\n\r\nabcdefgh") 68 | 69 | var request = try parser.parse() 70 | 71 | let body = request.body!.collect() 72 | try expect(body.count) >= 5 73 | try expect(body[0]) == 97 74 | try expect(body[1]) == 98 75 | try expect(body[2]) == 99 76 | try expect(body[3]) == 100 77 | try expect(body[4]) == 101 78 | } 79 | 80 | $0.it("provides the message body until Content-Length value with buffer size + 1 content length") { 81 | let bufferSize = 8192 82 | outSocket.write("GET / HTTP/1.1\r\nContent-Length: \(bufferSize + 1)\r\n\r\n" + String(repeating: "a", count: bufferSize + 1)) 83 | 84 | var request = try parser.parse() 85 | 86 | let body = request.body!.collect() 87 | try expect(body.count) == bufferSize + 1 88 | } 89 | 90 | $0.it("throws an error when the client uses bad HTTP syntax") { 91 | outSocket.write("GET /\r\nHost: localhost\r\n\r\n") 92 | try expect(try parser.parse()).toThrow() 93 | } 94 | 95 | $0.it("throws an error when the client uses a bad HTTP version") { 96 | outSocket.write("GET / HTTP/5.0\r\nHost: localhost\r\n\r\n") 97 | try expect(try parser.parse()).toThrow() 98 | } 99 | 100 | $0.it("throws an error when the client disconnects before sending an HTTP request") { 101 | try outSocket.close() 102 | outSocket = nil 103 | 104 | try expect(try parser.parse()).toThrow() 105 | } 106 | 107 | $0.it("throws an error when the client disconnects before sending the entire message body") { 108 | outSocket.write("GET / HTTP/1.1\r\nContent-Length: 42\r\n") 109 | try outSocket.close() 110 | outSocket = nil 111 | 112 | try expect(try parser.parse()).toThrow() 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /Tests/CurassowTests/XCTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | 4 | class CurassowTests: XCTestCase { 5 | func testCurassow() { 6 | testAddress() 7 | testConfiguration() 8 | testHTTPParser() 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | @testable import CurassowTests 2 | 3 | testConfiguration() 4 | testHTTPParser() 5 | testAddress() 6 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help 23 | help: 24 | @echo "Please use \`make ' where is one of" 25 | @echo " html to make standalone HTML files" 26 | @echo " dirhtml to make HTML files named index.html in directories" 27 | @echo " singlehtml to make a single large HTML file" 28 | @echo " pickle to make pickle files" 29 | @echo " json to make JSON files" 30 | @echo " htmlhelp to make HTML files and a HTML help project" 31 | @echo " qthelp to make HTML files and a qthelp project" 32 | @echo " applehelp to make an Apple Help Book" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 36 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 38 | @echo " text to make text files" 39 | @echo " man to make manual pages" 40 | @echo " texinfo to make Texinfo files" 41 | @echo " info to make Texinfo files and run them through makeinfo" 42 | @echo " gettext to make PO message catalogs" 43 | @echo " changes to make an overview of all changed/added/deprecated items" 44 | @echo " xml to make Docutils-native XML files" 45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 46 | @echo " linkcheck to check all external links for integrity" 47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 48 | @echo " coverage to run coverage check of the documentation (if enabled)" 49 | 50 | .PHONY: clean 51 | clean: 52 | rm -rf $(BUILDDIR)/* 53 | 54 | .PHONY: html 55 | html: 56 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 57 | @echo 58 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 59 | 60 | .PHONY: dirhtml 61 | dirhtml: 62 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 63 | @echo 64 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 65 | 66 | .PHONY: singlehtml 67 | singlehtml: 68 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 69 | @echo 70 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 71 | 72 | .PHONY: pickle 73 | pickle: 74 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 75 | @echo 76 | @echo "Build finished; now you can process the pickle files." 77 | 78 | .PHONY: json 79 | json: 80 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 81 | @echo 82 | @echo "Build finished; now you can process the JSON files." 83 | 84 | .PHONY: htmlhelp 85 | htmlhelp: 86 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 87 | @echo 88 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 89 | ".hhp project file in $(BUILDDIR)/htmlhelp." 90 | 91 | .PHONY: qthelp 92 | qthelp: 93 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 94 | @echo 95 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 96 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 97 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Curassow.qhcp" 98 | @echo "To view the help file:" 99 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Curassow.qhc" 100 | 101 | .PHONY: applehelp 102 | applehelp: 103 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp 104 | @echo 105 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." 106 | @echo "N.B. You won't be able to view it unless you put it in" \ 107 | "~/Library/Documentation/Help or install it in your application" \ 108 | "bundle." 109 | 110 | .PHONY: devhelp 111 | devhelp: 112 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 113 | @echo 114 | @echo "Build finished." 115 | @echo "To view the help file:" 116 | @echo "# mkdir -p $$HOME/.local/share/devhelp/Curassow" 117 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Curassow" 118 | @echo "# devhelp" 119 | 120 | .PHONY: epub 121 | epub: 122 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 123 | @echo 124 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 125 | 126 | .PHONY: latex 127 | latex: 128 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 129 | @echo 130 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 131 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 132 | "(use \`make latexpdf' here to do that automatically)." 133 | 134 | .PHONY: latexpdf 135 | latexpdf: 136 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 137 | @echo "Running LaTeX files through pdflatex..." 138 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 139 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 140 | 141 | .PHONY: latexpdfja 142 | latexpdfja: 143 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 144 | @echo "Running LaTeX files through platex and dvipdfmx..." 145 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 146 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 147 | 148 | .PHONY: text 149 | text: 150 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 151 | @echo 152 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 153 | 154 | .PHONY: man 155 | man: 156 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 157 | @echo 158 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 159 | 160 | .PHONY: texinfo 161 | texinfo: 162 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 163 | @echo 164 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 165 | @echo "Run \`make' in that directory to run these through makeinfo" \ 166 | "(use \`make info' here to do that automatically)." 167 | 168 | .PHONY: info 169 | info: 170 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 171 | @echo "Running Texinfo files through makeinfo..." 172 | make -C $(BUILDDIR)/texinfo info 173 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 174 | 175 | .PHONY: gettext 176 | gettext: 177 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 178 | @echo 179 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 180 | 181 | .PHONY: changes 182 | changes: 183 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 184 | @echo 185 | @echo "The overview file is in $(BUILDDIR)/changes." 186 | 187 | .PHONY: linkcheck 188 | linkcheck: 189 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 190 | @echo 191 | @echo "Link check complete; look for any errors in the above output " \ 192 | "or in $(BUILDDIR)/linkcheck/output.txt." 193 | 194 | .PHONY: doctest 195 | doctest: 196 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 197 | @echo "Testing of doctests in the sources finished, look at the " \ 198 | "results in $(BUILDDIR)/doctest/output.txt." 199 | 200 | .PHONY: coverage 201 | coverage: 202 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage 203 | @echo "Testing of coverage in the sources finished, look at the " \ 204 | "results in $(BUILDDIR)/coverage/python.txt." 205 | 206 | .PHONY: xml 207 | xml: 208 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 209 | @echo 210 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 211 | 212 | .PHONY: pseudoxml 213 | pseudoxml: 214 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 215 | @echo 216 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 217 | -------------------------------------------------------------------------------- /docs/_templates/sidebar_intro.html: -------------------------------------------------------------------------------- 1 |

Curassow

2 | 3 |

4 | 8 |

9 | 10 |

Swift HTTP server using the pre-fork worker model.

11 | 12 | 24 | 25 |

Other Projects

26 | 27 |

More Kyle Fuller projects:

28 | 34 | -------------------------------------------------------------------------------- /docs/architecture.md: -------------------------------------------------------------------------------- 1 | # Curassow Architecture 2 | 3 | This document outlines the architecture and design of the Curassow HTTP server. 4 | 5 | Curassow uses the pre-fork worker model, which means that HTTP requests are 6 | handled independently inside child worker processes. These worker processes are 7 | automatically handled by Curassow, for example if a request causes a crash it 8 | will be isolated to the single worker process which (which by default is 9 | configured to handle a single request at a time) which means that if a request 10 | causes a crash, it is isolated to that single request and does not cause 11 | cascading failures. We leave balancing between the worker processes to the 12 | kernel. Similar to the design of 13 | [unicorn](https://bogomips.org/unicorn/DESIGN.html). 14 | 15 | ## Arbiter 16 | 17 | The arbiter is the master process that manages the children worker processes. 18 | It has a simple loop that listens for signals sent to the master process and 19 | handles these signals. It manages worker processes, detects when a worker has 20 | timed out or has crashed and recovers from these failures. 21 | 22 | ### Signals 23 | 24 | The arbiter will watch for system signals and perform actions when it receives 25 | them. 26 | 27 | #### SIGQUIT and SIGINT 28 | 29 | The quit and interrupt signals can be used to quickly shutdown Curassow. 30 | 31 | #### SIGTERM 32 | 33 | The termination signal can be used to gracefully shutdown Curassow. The arbiter 34 | will wait for worker processes to finish handling their current requests or 35 | gracefully timeout. 36 | 37 | #### TTIN 38 | 39 | Increment the amount of worker processes by one. 40 | 41 | #### TTOU 42 | 43 | Decrement the amount of worker processes by one. 44 | 45 | ## Worker 46 | 47 | The worker process is responsible for handling HTTP requests. 48 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import sys 4 | import os 5 | 6 | from recommonmark.parser import CommonMarkParser 7 | 8 | # -- General configuration ------------------------------------------------ 9 | 10 | # If your documentation needs a minimal Sphinx version, state it here. 11 | #needs_sphinx = '1.0' 12 | 13 | # Add any Sphinx extension module names here, as strings. They can be 14 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 15 | # ones. 16 | extensions = [] 17 | 18 | # Add any paths that contain templates here, relative to this directory. 19 | templates_path = ['_templates'] 20 | 21 | source_parsers = { 22 | '.md': CommonMarkParser, 23 | } 24 | source_suffix = ['.rst', '.md'] 25 | 26 | # The encoding of source files. 27 | #source_encoding = 'utf-8-sig' 28 | 29 | # The master toctree document. 30 | master_doc = 'index' 31 | 32 | # General information about the project. 33 | project = u'Curassow' 34 | copyright = u'2016, Kyle Fuller' 35 | author = u'Kyle Fuller' 36 | 37 | # The version info for the project you're documenting, acts as replacement for 38 | # |version| and |release|, also used in various other places throughout the 39 | # built documents. 40 | # 41 | # The short X.Y version. 42 | version = u'0.5' 43 | # The full version, including alpha/beta/rc tags. 44 | release = u'0.5.0' 45 | 46 | # The language for content autogenerated by Sphinx. Refer to documentation 47 | # for a list of supported languages. 48 | # 49 | # This is also used if you do content translation via gettext catalogs. 50 | # Usually you set "language" from the command line for these cases. 51 | language = None 52 | 53 | # There are two options for replacing |today|: either, you set today to some 54 | # non-false value, then it is used: 55 | #today = '' 56 | # Else, today_fmt is used as the format for a strftime call. 57 | #today_fmt = '%B %d, %Y' 58 | 59 | # List of patterns, relative to source directory, that match files and 60 | # directories to ignore when looking for source files. 61 | exclude_patterns = ['_build'] 62 | 63 | # The reST default role (used for this markup: `text`) to use for all 64 | # documents. 65 | #default_role = None 66 | 67 | # If true, '()' will be appended to :func: etc. cross-reference text. 68 | #add_function_parentheses = True 69 | 70 | # If true, the current module name will be prepended to all description 71 | # unit titles (such as .. function::). 72 | #add_module_names = True 73 | 74 | # If true, sectionauthor and moduleauthor directives will be shown in the 75 | # output. They are ignored by default. 76 | #show_authors = False 77 | 78 | # The name of the Pygments (syntax highlighting) style to use. 79 | pygments_style = 'sphinx' 80 | 81 | # A list of ignored prefixes for module index sorting. 82 | #modindex_common_prefix = [] 83 | 84 | # If true, keep warnings as "system message" paragraphs in the built documents. 85 | #keep_warnings = False 86 | 87 | # If true, `todo` and `todoList` produce output, else they produce nothing. 88 | todo_include_todos = False 89 | 90 | 91 | # -- Options for HTML output ---------------------------------------------- 92 | 93 | # The theme to use for HTML and HTML Help pages. See the documentation for 94 | # a list of builtin themes. 95 | html_theme = 'alabaster' 96 | 97 | # Theme options are theme-specific and customize the look and feel of a theme 98 | # further. For a list of options available for each theme, see the 99 | # documentation. 100 | #html_theme_options = {} 101 | 102 | # Add any paths that contain custom themes here, relative to this directory. 103 | #html_theme_path = [] 104 | 105 | # The name for this set of Sphinx documents. If None, it defaults to 106 | # " v documentation". 107 | #html_title = None 108 | 109 | # A shorter title for the navigation bar. Default is the same as html_title. 110 | #html_short_title = None 111 | 112 | # The name of an image file (relative to this directory) to place at the top 113 | # of the sidebar. 114 | #html_logo = None 115 | 116 | # The name of an image file (relative to this directory) to use as a favicon of 117 | # the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 118 | # pixels large. 119 | #html_favicon = None 120 | 121 | # Add any paths that contain custom static files (such as style sheets) here, 122 | # relative to this directory. They are copied after the builtin static files, 123 | # so a file named "default.css" will overwrite the builtin "default.css". 124 | html_static_path = ['_static'] 125 | 126 | # Add any extra paths that contain custom files (such as robots.txt or 127 | # .htaccess) here, relative to this directory. These files are copied 128 | # directly to the root of the documentation. 129 | #html_extra_path = [] 130 | 131 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 132 | # using the given strftime format. 133 | #html_last_updated_fmt = '%b %d, %Y' 134 | 135 | # If true, SmartyPants will be used to convert quotes and dashes to 136 | # typographically correct entities. 137 | #html_use_smartypants = True 138 | 139 | html_sidebars = { 140 | 'index': ['sidebar_intro.html', 'searchbox.html'], 141 | '**': ['sidebar_intro.html', 'localtoc.html', 'relations.html', 'searchbox.html'], 142 | } 143 | 144 | # Additional templates that should be rendered to pages, maps page names to 145 | # template names. 146 | #html_additional_pages = {} 147 | 148 | # If false, no module index is generated. 149 | #html_domain_indices = True 150 | 151 | # If false, no index is generated. 152 | #html_use_index = True 153 | 154 | # If true, the index is split into individual pages for each letter. 155 | #html_split_index = False 156 | 157 | html_show_sourcelink = True 158 | html_show_sphinx = False 159 | html_show_copyright = True 160 | 161 | # If true, an OpenSearch description file will be output, and all pages will 162 | # contain a tag referring to it. The value of this option must be the 163 | # base URL from which the finished HTML is served. 164 | #html_use_opensearch = '' 165 | 166 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 167 | #html_file_suffix = None 168 | 169 | # Language to be used for generating the HTML full-text search index. 170 | # Sphinx supports the following languages: 171 | # 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' 172 | # 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr' 173 | #html_search_language = 'en' 174 | 175 | # A dictionary with options for the search language support, empty by default. 176 | # Now only 'ja' uses this config value 177 | #html_search_options = {'type': 'default'} 178 | 179 | # The name of a javascript file (relative to the configuration directory) that 180 | # implements a search results scorer. If empty, the default will be used. 181 | #html_search_scorer = 'scorer.js' 182 | 183 | # Output file base name for HTML help builder. 184 | htmlhelp_basename = 'Curassowdoc' 185 | 186 | # -- Options for LaTeX output --------------------------------------------- 187 | 188 | latex_elements = { 189 | # The paper size ('letterpaper' or 'a4paper'). 190 | #'papersize': 'letterpaper', 191 | 192 | # The font size ('10pt', '11pt' or '12pt'). 193 | #'pointsize': '10pt', 194 | 195 | # Additional stuff for the LaTeX preamble. 196 | #'preamble': '', 197 | 198 | # Latex figure (float) alignment 199 | #'figure_align': 'htbp', 200 | } 201 | 202 | # Grouping the document tree into LaTeX files. List of tuples 203 | # (source start file, target name, title, 204 | # author, documentclass [howto, manual, or own class]). 205 | latex_documents = [ 206 | (master_doc, 'Curassow.tex', u'Curassow Documentation', 207 | u'Kyle Fuller', 'manual'), 208 | ] 209 | 210 | # The name of an image file (relative to this directory) to place at the top of 211 | # the title page. 212 | #latex_logo = None 213 | 214 | # For "manual" documents, if this is true, then toplevel headings are parts, 215 | # not chapters. 216 | #latex_use_parts = False 217 | 218 | # If true, show page references after internal links. 219 | #latex_show_pagerefs = False 220 | 221 | # If true, show URL addresses after external links. 222 | #latex_show_urls = False 223 | 224 | # Documents to append as an appendix to all manuals. 225 | #latex_appendices = [] 226 | 227 | # If false, no module index is generated. 228 | #latex_domain_indices = True 229 | 230 | 231 | # -- Options for manual page output --------------------------------------- 232 | 233 | # One entry per manual page. List of tuples 234 | # (source start file, name, description, authors, manual section). 235 | man_pages = [ 236 | (master_doc, 'curassow', u'Curassow Documentation', 237 | [author], 1) 238 | ] 239 | 240 | # If true, show URL addresses after external links. 241 | #man_show_urls = False 242 | 243 | 244 | # -- Options for Texinfo output ------------------------------------------- 245 | 246 | # Grouping the document tree into Texinfo files. List of tuples 247 | # (source start file, target name, title, author, 248 | # dir menu entry, description, category) 249 | texinfo_documents = [ 250 | (master_doc, 'Curassow', u'Curassow Documentation', 251 | author, 'Curassow', 'One line description of project.', 252 | 'Miscellaneous'), 253 | ] 254 | 255 | # Documents to append as an appendix to all manuals. 256 | #texinfo_appendices = [] 257 | 258 | # If false, no module index is generated. 259 | #texinfo_domain_indices = True 260 | 261 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 262 | #texinfo_show_urls = 'footnote' 263 | 264 | # If true, do not generate a @detailmenu in the "Top" node's menu. 265 | #texinfo_no_detailmenu = False 266 | -------------------------------------------------------------------------------- /docs/configuration.md: -------------------------------------------------------------------------------- 1 | # Configuration 2 | 3 | Curassow provides you with an API, and a command line interface for configuration. 4 | 5 | ## Server Socket 6 | 7 | ### bind 8 | 9 | The socket to bind to. This may either be in the form `HOST:PORT` or 10 | `unix:PATH` where any valid IP is acceptable for `HOST`. 11 | 12 | ```shell 13 | $ curassow --bind 127.0.0.1:9000 14 | [INFO] Listening at http://127.0.0.1:9000 (940) 15 | ``` 16 | 17 | ```shell 18 | $ curassow --bind unix:/tmp/helloworld.sock 19 | [INFO] Listening at unix:/tmp/helloworld.sock (940) 20 | ``` 21 | 22 | ## Worker Processes 23 | 24 | ### workers 25 | 26 | The number of worker processes for handling requests. 27 | 28 | ```shell 29 | $ curassow --workers 3 30 | [INFO] Listening at http://0.0.0.0:8000 (940) 31 | [INFO] Booting worker process with pid: 941 32 | [INFO] Booting worker process with pid: 942 33 | [INFO] Booting worker process with pid: 943 34 | ``` 35 | 36 | By default, the value of the environment variable `WEB_CONCURRENCY` will be 37 | used. If the environment variable is not set, `1` will be the default. 38 | 39 | ### worker-type 40 | 41 | The type of worker to use. This defaults to `sync`. Currently the only 42 | supported value is `sync`, there may be an async and gcd worker in the future. 43 | 44 | ``` 45 | $ curassow --worker-type sync 46 | ``` 47 | 48 | 49 | ## timeout 50 | 51 | By default, Curassow will kill and restart workers after 30 seconds if it 52 | hasn't responded to the master process. 53 | 54 | ``` 55 | $ curassow --timeout 30 56 | ``` 57 | 58 | You can set the timeout to `0` to disable worker timeout handling. 59 | 60 | ## Server 61 | 62 | ### daemon 63 | 64 | Daemonize the Curassow process. Detaches the server from the controlling 65 | terminal and enters the background. 66 | 67 | ```shell 68 | $ curassow --daemon 69 | ``` 70 | -------------------------------------------------------------------------------- /docs/deployment.html: -------------------------------------------------------------------------------- 1 | # Deployment 2 | 3 | When it comes to deploying your Curassow server to production, it's highly 4 | recommended to run Curassow behind a HTTP proxy such as 5 | [nginx](http://nginx.org/). 6 | 7 | It's important to use a proxy server that can buffer slow clients when using 8 | Curassow. Without buffering slow clients, Curassow will be easily be 9 | susceptible to denial-of-service attacks. 10 | 11 | **NOTE**: *Platforms such as Heroku already sit your HTTP server behind a proxy 12 | so this does not apply on these types of platforms.* 13 | 14 | ## nginx 15 | 16 | We highly recommend [nginx](http://nginx.org/), you can find 17 | an example configuration below. 18 | 19 | ```nginx 20 | worker_processes 1; 21 | 22 | events { 23 | # Increase for higher clients 24 | worker_connections 1024; 25 | 26 | # Switch 'on' if Nginx's worker processes is more than one. 27 | accept_mutex off; 28 | 29 | # If you're on Linux 30 | # use epoll; 31 | 32 | #If you're on FreeBSD or OS X 33 | # use kqueue; 34 | } 35 | 36 | http { 37 | upstream curassow { 38 | # When serving via UNIX domain socket. Change location to your socket 39 | # server unix:/tmp/curassow.sock fail_timeout=0; 40 | 41 | # When serving via IP. Change address and port for your Curassow server 42 | # server 127.0.0.1:8000 fail_timeout=0; 43 | } 44 | 45 | server { 46 | listen 80; 47 | 48 | # On Linux, instead use: 49 | # listen 80 deferred; 50 | 51 | # On FreeBSD instead use: 52 | # listen 80 accept_filter=httpready; 53 | 54 | client_max_body_size 1G; 55 | 56 | # Change to your host(s) 57 | server_name curassow.com www.curassow.com; 58 | 59 | keepalive_timeout 5; 60 | 61 | # Path to look for static resources and assets 62 | root /location/of/static/resources; 63 | 64 | location / { 65 | # Let's check for a static file locally before forwarding to Curassow. 66 | try_files $uri @proxy_to_app; 67 | } 68 | 69 | location @proxy_to_app { 70 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 71 | proxy_set_header Host $http_host; 72 | proxy_set_header X-Forwarded-Proto $scheme; 73 | 74 | proxy_redirect off; 75 | proxy_pass http://curassow; 76 | } 77 | } 78 | } 79 | ``` 80 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Curassow 2 | ======== 3 | 4 | Curassow is a Swift Nest_ HTTP Server. It uses the pre-fork worker model 5 | and it's similar to Python's Gunicorn and Ruby's Unicorn. 6 | 7 | .. _Nest: https://github.com/nestproject/Nest 8 | 9 | It exposes a Nest-compatible interface for your application, allowing 10 | you to use Curassow with any Nest compatible web frameworks of your choice. 11 | 12 | Quick Start 13 | ----------- 14 | 15 | To use Curassow, you will need to install it via the Swift Package Manager, 16 | you can add it to the list of dependencies in your `Package.swift`: 17 | 18 | .. code-block:: swift 19 | 20 | import PackageDescription 21 | 22 | 23 | let package = Package( 24 | name: "HelloWorld", 25 | dependencies: [ 26 | .Package(url: "https://github.com/kylef/Curassow.git", majorVersion: 0, minor: 6), 27 | ] 28 | ) 29 | 30 | Afterwards you can place your web application implementation in `Sources` 31 | and add the runner inside `main.swift` which exposes a command line tool to 32 | run your web application: 33 | 34 | .. code-block:: swift 35 | 36 | import Curassow 37 | import Inquiline 38 | 39 | 40 | serve { request in 41 | return Response(.ok, contentType: "text/plain", content: "Hello World") 42 | } 43 | 44 | Then build and run your application: 45 | 46 | .. code-block:: shell 47 | 48 | $ swift build --configuration release 49 | $ ./.build/release/HelloWorld 50 | 51 | Check out the `Hello World example `_ application. 52 | 53 | Contents 54 | -------- 55 | 56 | .. toctree:: 57 | :maxdepth: 2 58 | 59 | configuration 60 | signal-handling 61 | deployment 62 | architecture 63 | -------------------------------------------------------------------------------- /docs/signal-handling.md: -------------------------------------------------------------------------------- 1 | # Signal Handling 2 | 3 | The master and worker processes will respond to the following signals as 4 | documented. 5 | 6 | ## Master Process 7 | 8 | - `QUIT` and `INT` - Quickly shutdown. 9 | - `TERM` - Gracefully shutdown, this will wait for the workers to finish their 10 | current requests and gracefully timeout. 11 | - `TTIN` - Increases the worker count by one. 12 | - `TTOU` - Decreases the worker count by one. 13 | 14 | ### Example 15 | 16 | TTIN and TTOU signals can be sent to the master to increase or decrease the number of workers. 17 | 18 | To increase the worker count by one, where $PID is the PID of the master process. 19 | 20 | ``` 21 | $ kill -TTIN $PID 22 | ``` 23 | 24 | To decrease the worker count by one: 25 | 26 | ``` 27 | $ kill -TTOU $PID 28 | ``` 29 | 30 | ## Worker Process 31 | 32 | You may send signals directly to a worker process. 33 | 34 | - `QUIT` and `INT` - Quickly shutdown. 35 | - `TERM` - Gracefully shutdown. 36 | --------------------------------------------------------------------------------