Giter Club home page Giter Club logo

flyingfox's Introduction

Build Codecov Platforms Swift 5.6 License Twitter

Introduction

FlyingFox is a lightweight HTTP server built using Swift Concurrency. The server uses non blocking BSD sockets, handling each connection in a concurrent child Task. When a socket is blocked with no data, tasks are suspended using the shared AsyncSocketPool.

Installation

FlyingFox can be installed by using Swift Package Manager.

Note: FlyingFox requires Swift 5.5 on Xcode 13.2+. It runs on iOS 13+, tvOS 13+, macOS 10.15+ and Linux.

To install using Swift Package Manager, add this to the dependencies: section in your Package.swift file:

.package(url: "https://github.com/swhitty/FlyingFox.git", .upToNextMajor(from: "0.6.0"))

Usage

Start the server by providing a port number:

import FlyingFox

let server = HTTPServer(port: 80)
try await server.start()

The server runs within the the current task. To stop the server, cancel the task:

let task = Task { try await server.start() }
task.cancel()

Note: iOS will hangup the listening socket when the app is suspended in the background. Once the app returns to the foreground, HTTPServer.start() detects this, throwing SocketError.disconnected. The server must then be started once more.

Handlers

Handlers can be added to the server by implementing HTTPHandler:

protocol HTTPHandler {
  func handleRequest(_ request: HTTPRequest) async throws -> HTTPResponse
}

Routes can be added to the server delegating requests to a handler:

await server.appendRoute("/hello", to: handler)

They can also be added to closures:

await server.appendRoute("/hello") { request in
  try await Task.sleep(nanoseconds: 1_000_000_000)
  return HTTPResponse(statusCode: .ok)
}

Incoming requests are routed to the handler of the first matching route.

Handlers can throw HTTPUnhandledError if after inspecting the request, they cannot handle it. The next matching route is then used.

Requests that do not match any handled route receive HTTP 404.

FileHTTPHandler

Requests can be routed to static files with FileHTTPHandler:

await server.appendRoute("GET /mock", to: .file(named: "mock.json"))

FileHTTPHandler will return HTTP 404 if the file does not exist.

ProxyHTTPHandler

Requests can be proxied via a base URL:

await server.appendRoute("GET *", to: .proxy(via: "https://pie.dev"))
// GET /get?fish=chips  ---->  GET https://pie.dev/get?fish=chips

RedirectHTTPHandler

Requests can be redirected to a URL:

await server.appendRoute("GET /fish/*", to: .redirect(to: "https://pie.dev/get"))
// GET /fish/chips  --->  HTTP 301
//                        Location: https://pie.dev/get

WebSocketHTTPHandler

Requests can be routed to a websocket by providing a WSMessageHandler where a pair of AsyncStream<WSMessage> are exchanged:

await server.appendRoute("GET /socket", to: .webSocket(EchoWSMessageHandler()))

protocol WSMessageHandler {
  func makeMessages(for client: AsyncStream<WSMessage>) async throws -> AsyncStream<WSMessage>
}

enum WSMessage {
  case text(String)
  case data(Data)
}

Raw WebSocket frames can also be provided.

RoutedHTTPHandler

Multiple handlers can be grouped with requests and matched against HTTPRoute using RoutedHTTPHandler.

var routes = RoutedHTTPHandler()
routes.appendRoute("GET /fish/chips", to: .file(named: "chips.json"))
routes.appendRoute("GET /fish/mushy_peas", to: .file(named: "mushy_peas.json"))
await server.appendRoute(for: "GET /fish/*", to: routes)

HTTPUnhandledError is thrown when it's unable to handle the request with any of its registered handlers.

Routes

HTTPRoute is designed to be pattern matched against HTTPRequest, allowing requests to be identified by some or all of its properties.

let route = HTTPRoute("/hello/world")

route ~= HTTPRequest(method: .GET, path: "/hello/world") // true
route ~= HTTPRequest(method: .POST, path: "/hello/world") // true
route ~= HTTPRequest(method: .GET, path: "/hello/") // false

Routes are ExpressibleByStringLiteral allowing literals to be automatically converted to HTTPRoute:

let route: HTTPRoute = "/hello/world"

Routes can include a specific method to match against:

let route = HTTPRoute("GET /hello/world")

route ~= HTTPRequest(method: .GET, path: "/hello/world") // true
route ~= HTTPRequest(method: .POST, path: "/hello/world") // false

They can also use wildcards within the path:

let route = HTTPRoute("GET /hello/*/world")

route ~= HTTPRequest(method: .GET, path: "/hello/fish/world") // true
route ~= HTTPRequest(method: .GET, path: "/hello/dog/world") // true
route ~= HTTPRequest(method: .GET, path: "/hello/fish/sea") // false

Trailing wildcards match all trailing path components:

let route = HTTPRoute("/hello/*")

route ~= HTTPRequest(method: .GET, path: "/hello/fish/world") // true
route ~= HTTPRequest(method: .GET, path: "/hello/dog/world") // true
route ~= HTTPRequest(method: .POST, path: "/hello/fish/deep/blue/sea") // true

Specific query items can be matched:

let route = HTTPRoute("/hello?time=morning")

route ~= HTTPRequest(method: .GET, path: "/hello?time=morning") // true
route ~= HTTPRequest(method: .GET, path: "/hello?count=one&time=morning") // true
route ~= HTTPRequest(method: .GET, path: "/hello") // false
route ~= HTTPRequest(method: .GET, path: "/hello?time=afternoon") // false

Query item values can include wildcards:

let route = HTTPRoute("/hello?time=*")

route ~= HTTPRequest(method: .GET, path: "/hello?time=morning") // true
route ~= HTTPRequest(method: .GET, path: "/hello?time=afternoon") // true
route ~= HTTPRequest(method: .GET, path: "/hello") // false

HTTP headers can be matched:

let route = HTTPRoute("*", headers: [.contentType: "application/json"])

route ~= HTTPRequest(headers: [.contentType: "application/json"]) // true
route ~= HTTPRequest(headers: [.contentType: "application/xml"]) // false

Header values can be wildcards:

let route = HTTPRoute("*", headers: [.authorization: "*"])

route ~= HTTPRequest(headers: [.authorization: "abc"]) // true
route ~= HTTPRequest(headers: [.authorization: "xyz"]) // true
route ~= HTTPRequest(headers: [:]) // false

Body patterns can be created to match the request body data:

public protocol HTTPBodyPattern: Sendable {
  func evaluate(_ body: Data) -> Bool
}

Darwin platforms can pattern match a JSON body with an NSPredicate:

let route = HTTPRoute("POST *", body: .json(where: "food == 'fish'"))
{"side": "chips", "food": "fish"}

WebSockets

HTTPResponse can switch the connection to the WebSocket protocol by provding a WSHandler within the response payload.

protocol WSHandler {
  func makeFrames(for client: AsyncThrowingStream<WSFrame, Error>) async throws -> AsyncStream<WSFrame>
}

WSHandler facilitates the exchange of a pair AsyncStream<WSFrame> containing the raw websocket frames sent over the connection. While powerful, it is more convenient to exchange streams of messages via WebSocketHTTPHandler.

SocketAddress

The sockaddr cluster of structures are grouped via conformance to SocketAddress

  • sockaddr_in
  • sockaddr_in6
  • sockaddr_un

This allows HTTPServer to be started with any of these configured addresses:

// only listens on localhost 8080
let server = HTTPServer(address: .loopback(port: 8080))

It can also be used with UNIX-domain addresses, allowing private IPC over a socket:

// only listens on Unix socket "Ants"
let server = HTTPServer(address: .unix(path: "Ants"))

You can then netcat to the socket:

% nc -U Ants

AsyncSocket / PollingSocketPool

Internally, FlyingFox uses standard BSD sockets configured with the flag O_NONBLOCK. When data is unavailable for a socket (EWOULDBLOCK) the task is suspended using the current AsyncSocketPool until data is available:

protocol AsyncSocketPool {
  // Suspends current task until a socket is ready to read and/or write
  func suspendSocket(_ socket: Socket, untilReadyFor events: Socket.Events) async throws
}

PollingSocketPool is currently the only pool available. It uses a continuous loop of poll(2) / Task.yield() to check all sockets awaiting data at a supplied interval. All sockets share the same pool.

Command line app

An example command line app FlyingFoxCLI is available here.

Credits

FlyingFox is primarily the work of Simon Whitty.

(Full list of contributors)

flyingfox's People

Contributors

andrejacobs avatar huwr avatar qmoya avatar stackotter avatar swhitty avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.