• Stars
    star
    576
  • Rank 74,528 (Top 2 %)
  • Language
    Swift
  • License
    MIT License
  • Created almost 8 years ago
  • Updated 9 months ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Super lightweight async HTTP server library in pure Swift runs in iOS / MacOS / Linux

Embassy

Build Status Carthage compatible SwiftPM compatible CocoaPods Swift Version Plaform GitHub license

Super lightweight async HTTP server in pure Swift.

Please read: Embedded web server for iOS UI testing.

See also: Our lightweight web framework Ambassador based on Embassy

Features

  • Swift 4 & 5
  • iOS / tvOS / MacOS / Linux
  • Super lightweight, only 1.5 K of lines
  • Zero third-party dependency
  • Async event loop based HTTP server, makes long-polling, delay and bandwidth throttling all possible
  • HTTP Application based on SWSGI, super flexible
  • IPV6 ready, also supports IPV4 (dual stack)
  • Automatic testing covered

Example

Here's a simple example shows how Embassy works.

let loop = try! SelectorEventLoop(selector: try! KqueueSelector())
let server = DefaultHTTPServer(eventLoop: loop, port: 8080) {
    (
        environ: [String: Any],
        startResponse: ((String, [(String, String)]) -> Void),
        sendBody: ((Data) -> Void)
    ) in
    // Start HTTP response
    startResponse("200 OK", [])
    let pathInfo = environ["PATH_INFO"]! as! String
    sendBody(Data("the path you're visiting is \(pathInfo.debugDescription)".utf8))
    // send EOF
    sendBody(Data())
}

// Start HTTP server to listen on the port
try! server.start()

// Run event loop
loop.runForever()

Then you can visit http://[::1]:8080/foo-bar in the browser and see

the path you're visiting is "/foo-bar"

By default, the server will be bound only to the localhost interface (::1) for security reasons. If you want to access your server over an external network, you'll need to change the bind interface to all addresses:

let server = DefaultHTTPServer(eventLoop: loop, interface: "::", port: 8080)

Async Event Loop

To use the async event loop, you can get it via key embassy.event_loop in environ dictionary and cast it to EventLoop. For example, you can create an SWSGI app which delays sendBody call like this

let app = { (
    environ: [String: Any],
    startResponse: ((String, [(String, String)]) -> Void),
    sendBody: @escaping ((Data) -> Void)
) in
    startResponse("200 OK", [])

    let loop = environ["embassy.event_loop"] as! EventLoop

    loop.call(withDelay: 1) {
        sendBody(Data("hello ".utf8))
    }
    loop.call(withDelay: 2) {
        sendBody(Data("baby ".utf8))
    }
    loop.call(withDelay: 3) {
        sendBody(Data("fin".utf8))
        sendBody(Data())
    }
}

Please notice that functions passed into SWSGI should be only called within the same thread for running the EventLoop, they are all not threadsafe, therefore, you should not use GCD for delaying any call. Instead, there are some methods from EventLoop you can use, and they are all threadsafe

call(callback: (Void) -> Void)

Call given callback as soon as possible in the event loop

call(withDelay: TimeInterval, callback: (Void) -> Void)

Schedule given callback to withDelay seconds then call it in the event loop.

call(atTime: Date, callback: (Void) -> Void)

Schedule given callback to be called at atTime in the event loop. If the given time is in the past or zero, this methods works exactly like call with only callback parameter.

What's SWSGI (Swift Web Server Gateway Interface)?

SWSGI is a hat tip to Python's WSGI (Web Server Gateway Interface). It's a gateway interface enables web applications to talk to HTTP clients without knowing HTTP server implementation details.

It's defined as

public typealias SWSGI = (
    [String: Any],
    @escaping ((String, [(String, String)]) -> Void),
    @escaping ((Data) -> Void)
) -> Void

environ

It's a dictionary contains all necessary information about the request. It basically follows WSGI standard, except wsgi.* keys will be swsgi.* instead. For example:

[
  "SERVER_NAME": "[::1]",
  "SERVER_PROTOCOL" : "HTTP/1.1",
  "SERVER_PORT" : "53479",
  "REQUEST_METHOD": "GET",
  "SCRIPT_NAME" : "",
  "PATH_INFO" : "/",
  "HTTP_HOST": "[::1]:8889",
  "HTTP_USER_AGENT" : "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.102 Safari/537.36",
  "HTTP_ACCEPT_LANGUAGE" : "en-US,en;q=0.8,zh-TW;q=0.6,zh;q=0.4,zh-CN;q=0.2",
  "HTTP_CONNECTION" : "keep-alive",
  "HTTP_ACCEPT" : "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
  "HTTP_ACCEPT_ENCODING" : "gzip, deflate, sdch",
  "swsgi.version" : "0.1",
  "swsgi.input" : (Function),
  "swsgi.error" : "",
  "swsgi.multiprocess" : false,
  "swsgi.multithread" : false,
  "swsgi.url_scheme" : "http",
  "swsgi.run_once" : false
]

To read request from body, you can use swsgi.input, for example

let input = environ["swsgi.input"] as! SWSGIInput
input { data in
    // handle the body data here
}

An empty Data will be passed into the input data handler when EOF reached. Also please notice that, request body won't be read if swsgi.input is not set or set to nil. You can use swsgi.input as bandwidth control, set it to nil when you don't want to receive any data from client.

Some extra Embassy server specific keys are

  • embassy.connection - HTTPConnection object for the request
  • embassy.event_loop - EventLoop object
  • embassy.version - Version of embassy as a String, e.g. 3.0.0

startResponse

Function for starting to send HTTP response header to client, the first argument is status code with message, e.g. "200 OK". The second argument is headers, as a list of key value tuple.

To response HTTP header, you can do

startResponse("200 OK", [("Set-Cookie", "foo=bar")])

startResponse can only be called once per request, extra call will be simply ignored.

sendBody

Function for sending body data to client. You need to call startResponse first in order to call sendBody. If you don't call startResponse first, all calls to sendBody will be ignored. To send data, here you can do

sendBody(Data("hello".utf8))

To end the response data stream simply call sendBody with an empty Data.

sendBody(Data())

Install

CocoaPods

To install with CocoaPod, add Embassy to your Podfile:

pod 'Embassy', '~> 4.1'

Carthage

To install with Carthage, add Embassy to your Cartfile:

github "envoy/Embassy" ~> 4.1

Package Manager

Add it this Embassy repo in Package.swift, like this

import PackageDescription

let package = Package(
    name: "EmbassyExample",
    dependencies: [
        .package(url: "https://github.com/envoy/Embassy.git",
                 from: "4.1.4"),
    ]
)

You can read this example project here.

More Repositories

1

Ambassador

Super lightweight web framework in Swift based on SWSGI
Swift
176
star
2

Engineering

Repo for principles, job bands, coding challenges, and anything else we're ok sharing w/ the world.
102
star
3

eventbrite

eventbrite ruby gem for v3 API.
Ruby
31
star
4

polarwind

Envoy's product component library
JavaScript
21
star
5

gongpi

Rings the gong in the Envoy office every time we make a sale.
Shell
18
star
6

react-native-key-commands

iOS UIKeyCommand native component for React Native
Objective-C
13
star
7

ReactiveAlamofire

Alamofire 3 integration for ReactiveCocoa 4
Swift
11
star
8

BRPtouchPrinterKit

Brother Print SDK for iPhone/iPad
Objective-C
11
star
9

lambda-convert

AWS Lambda powered drop-in replacement for ImageMagick convert command line tool
Ruby
10
star
10

ember-route-constraints

Route navigation constraints for Ember apps.
JavaScript
8
star
11

JSONAPIModel

Simple JSONAPI parser / serializer and data model
Swift
7
star
12

ENVLicenseParser

A library for parsing PDF417 strings
Objective-C
5
star
13

ready-to-reopen

Our employee portal for returning to the office
HTML
5
star
14

tap-clubhouse

Singer tap for Clubhouse
Python
5
star
15

react-native-signature-pad

Signature pad component for React Native
Objective-C
4
star
16

loglevel-file-logger

File logger for loglevel
TypeScript
4
star
17

example-embassy

Embassy example app for Linux
Swift
3
star
18

doorkeeper-device_flow

Ruby
3
star
19

historedis

Create Histograms in Redis
Ruby
2
star
20

react-native-transform-view

TypeScript
2
star
21

express-envoy-auth

Middleware to authenticate an Express application with Envoy
JavaScript
2
star
22

ember-cli-deploy-gcs-index

An ember-cli-deploy plugin to deploy ember-cli's bootstrap index file to Google Cloud Storage.
JavaScript
2
star
23

tap-chargebee

Singer.io tap for extracting data from the Chargebee API
Python
2
star
24

homebrew-tools

Ruby
2
star
25

install-helm-docs

Github Action to install helm docs in CI flow
2
star
26

DoorPi

Ding Dong! Someone is at the front door.
Python
2
star
27

ibeacon-experiment-iphone

Simple iPhone app for ibeacon experiments and investigations
Swift
1
star
28

kt-proj-presetup

The project is a Heroku Custom Buildpack to do the required tasks for Kotlin projects.
Shell
1
star
29

envoy-plugin-example-access-control

This is an example Access Control integration with Envoy's integration builder.
JavaScript
1
star
30

tap-slack

Singer.io tap for extracting data from the Slack Web API
Python
1
star
31

envoy.css

Envoy's custom CSS library
CSS
1
star
32

react-native-ibeacon

React Native iOS native module for iBeacon (only support advertising for now)
Objective-C
1
star
33

LaserDisc

Swift
1
star