Skip to content

Instantly share code, notes, and snippets.

@blunderbusq
Created March 24, 2022 01:20
Show Gist options
  • Save blunderbusq/3a25d4d17d7bdb8e7d1df484f4096b66 to your computer and use it in GitHub Desktop.
Save blunderbusq/3a25d4d17d7bdb8e7d1df484f4096b66 to your computer and use it in GitHub Desktop.
A stand-alone HTTP server that fetches anisette data from the locally-running AltServer mail plug-in and returns it as a JSON object
// An HTTP server that connects to the AltServer mail plugin
// on the local host and serves the anisette data in JSON form.
//
// Requires macOS 10.12+ and Swift 5.5+
//
// Server usage: swift aniserve.swift -p <port:6969> -d <endpoint:"anisette">
//
// Client usage: curl http://localhost:6969/anisette
//
import Foundation
import Dispatch
let args = CommandLine.arguments.dropFirst()
var flags: [String: String] = [:]
for i in stride(from: args.startIndex, to: args.endIndex - 1, by: 2) {
flags[args[i]] = args[i+1]
}
// the port is specified with the "-p" flag
let port = flags["-p"].flatMap(UInt16.init) ?? 6969
// the default timeout (in seconds) is specified with the "-t" flag
let timeout = flags["-t"].flatMap(Int.init) ?? 2
// the endpoint is specified with the "-d" flag
let dir = flags["-d"] ?? "anisette"
do {
if #available(macOS 10.13, *) {
let server = HttpServer()
server.router.register("GET", path: dir, handler: handleAnisette)
try server.start(port, priority: .default)
print("Anisette proxy server started at: http//localhost:\(try server.port())/\(dir). Awaiting connections.")
// RunLoop.current.run(mode: .default, before: Date.distantFuture)
dispatchMain()
}
} catch {
print("Server start error: \(error)")
}
// MARK: AltServer communication
@available(macOS 10.13, *)
private func handleAnisette(request: HttpRequest, respond: @escaping HttpResponder) {
// the notification center that acts as the communication bridge to the mail plugin
let center = DistributedNotificationCenter.default()
var responded = false
let reqid = UUID().uuidString
let observer = center.addObserver(forName: Notification.Name("com.rileytestut.AltServer.AnisetteDataResponse"), object: nil, queue: .main) { note in
guard let requestUUID = note.userInfo?["requestUUID"] as? String,
requestUUID == reqid else {
// not our request…
return
}
responded = true // cancel the timeout
if let anisetteData = note.userInfo?["anisetteData"] as? Data,
let anisette = try? NSKeyedUnarchiver.unarchivedObject(ofClass: AnisetteData.self, from: anisetteData) {
// we've successfully deserialized the anisette data;
// encode the dictionary as JSON and return it
respond(HttpResponse.ok(.json(anisette.dictionaryRepresentation)))
} else {
respond(HttpResponse.internalServerError(.json(["error": "Anisette data coult not be unarchived"])))
}
}
// post the fetch request to the mail plug-in running in Mail.app
center.postNotificationName(Notification.Name("com.rileytestut.AltServer.FetchAnisetteData"), object: nil, userInfo: ["requestUUID": reqid], options: .deliverImmediately)
// start a timeout countdown
DispatchQueue.global(qos: .utility).asyncAfter(deadline: .now() + .seconds(timeout)) {
if responded == false {
respond(HttpResponse.internalServerError(.json(["error": "Timeout waiting for response. Is the AltServer mail plug-in installed, activated, and running?"])))
}
center.removeObserver(observer)
}
}
@available(macOS 10.12, *)
@objc(ALTAnisetteData) final class AnisetteData : NSObject, NSSecureCoding {
static var supportsSecureCoding = true
var machineID: String
var oneTimePassword: String
var localUserID: String
var routingInfo: UInt64
var deviceUniqueIdentifier: String
var deviceSerialNumber: String
var deviceDescription: String
var date: Date
var locale: Locale
var timeZone: TimeZone
required init?(coder aDecoder: NSCoder) {
guard
let machineID = aDecoder.decodeObject(of: [NSString.self], forKey: "machineID") as? NSString,
let oneTimePassword = aDecoder.decodeObject(of: [NSString.self], forKey: "oneTimePassword") as? NSString,
let localUserID = aDecoder.decodeObject(of: [NSString.self], forKey: "localUserID") as? NSString,
let routingInfo = aDecoder.decodeObject(of: [NSNumber.self], forKey: "routingInfo") as? NSNumber,
let deviceUniqueIdentifier = aDecoder.decodeObject(of: [NSString.self], forKey: "deviceUniqueIdentifier") as? NSString,
let deviceSerialNumber = aDecoder.decodeObject(of: [NSString.self], forKey: "deviceSerialNumber") as? NSString,
let deviceDescription = aDecoder.decodeObject(of: [NSString.self], forKey: "deviceDescription") as? NSString,
let date = aDecoder.decodeObject(of: [NSDate.self], forKey: "date") as? NSDate,
let locale = aDecoder.decodeObject(of: [NSLocale.self], forKey: "locale") as? NSLocale,
let timeZone = aDecoder.decodeObject(of: [NSTimeZone.self], forKey: "timeZone") as? NSTimeZone
else {
return nil
}
self.machineID = machineID as String
self.oneTimePassword = oneTimePassword as String
self.localUserID = localUserID as String
self.routingInfo = routingInfo.uint64Value
self.deviceUniqueIdentifier = deviceUniqueIdentifier as String
self.deviceSerialNumber = deviceSerialNumber as String
self.deviceDescription = deviceDescription as String
self.date = date as Date
self.locale = locale as Locale
self.timeZone = timeZone as TimeZone
}
func encode(with aCoder: NSCoder) {
}
/// The values that will be serialized to JSON in the response
var dictionaryRepresentation: NSDictionary {
[
"X-Apple-I-Client-Time": ISO8601DateFormatter().string(from: self.date),
"X-Apple-I-MD": self.oneTimePassword,
"X-Apple-I-MD-LU": self.localUserID,
"X-Apple-I-MD-M": self.machineID,
"X-Apple-I-MD-RINFO": self.routingInfo,
"X-Apple-I-SRL-NO": self.deviceSerialNumber,
"X-Apple-I-TimeZone": self.timeZone.abbreviation() ?? "",
"X-Apple-Locale": self.locale.identifier,
"X-MMe-Client-Info": self.deviceDescription,
"X-Mme-Device-Id": self.deviceUniqueIdentifier,
]
}
}
// MARK: Swifter
// The following code is a stripped-down, single-file version of
// Swifter (https://github.com/httpswift/swifter)
// modified to support asynchronous responses.
//
// Released under the BSD-3-Clause License
// Copyright © 2016 Damian Kołakowski
/// The callback for responding to an HTTP request
public typealias HttpResponder = (HttpResponse) -> ()
public typealias HttpHandler = (HttpRequest, @escaping HttpResponder) -> ()
open class Socket: Hashable, Equatable {
let socketFileDescriptor: Int32
private var shutdown = false
public init(socketFileDescriptor: Int32) {
self.socketFileDescriptor = socketFileDescriptor
}
deinit {
close()
}
public func hash(into hasher: inout Hasher) {
hasher.combine(self.socketFileDescriptor)
}
public func close() {
if shutdown {
return
}
shutdown = true
Socket.close(self.socketFileDescriptor)
}
public func port() throws -> in_port_t {
var addr = sockaddr_in()
return try withUnsafePointer(to: &addr) { pointer in
var len = socklen_t(MemoryLayout<sockaddr_in>.size)
if getsockname(socketFileDescriptor, UnsafeMutablePointer(OpaquePointer(pointer)), &len) != 0 {
throw SocketError.getSockNameFailed(Errno.description())
}
let sin_port = pointer.pointee.sin_port
#if os(Linux)
return ntohs(sin_port)
#else
return Int(OSHostByteOrder()) != OSLittleEndian ? sin_port.littleEndian : sin_port.bigEndian
#endif
}
}
public func isIPv4() throws -> Bool {
var addr = sockaddr_in()
return try withUnsafePointer(to: &addr) { pointer in
var len = socklen_t(MemoryLayout<sockaddr_in>.size)
if getsockname(socketFileDescriptor, UnsafeMutablePointer(OpaquePointer(pointer)), &len) != 0 {
throw SocketError.getSockNameFailed(Errno.description())
}
return Int32(pointer.pointee.sin_family) == AF_INET
}
}
public func writeUTF8(_ string: String) throws {
try writeUInt8(ArraySlice(string.utf8))
}
public func writeUInt8(_ data: [UInt8]) throws {
try writeUInt8(ArraySlice(data))
}
public func writeUInt8(_ data: ArraySlice<UInt8>) throws {
try data.withUnsafeBufferPointer {
try writeBuffer($0.baseAddress!, length: data.count)
}
}
public func writeData(_ data: NSData) throws {
try writeBuffer(data.bytes, length: data.length)
}
public func writeData(_ data: Data) throws {
#if compiler(>=5.0)
try data.withUnsafeBytes { (body: UnsafeRawBufferPointer) -> Void in
if let baseAddress = body.baseAddress, body.count > 0 {
let pointer = baseAddress.assumingMemoryBound(to: UInt8.self)
try self.writeBuffer(pointer, length: data.count)
}
}
#else
try data.withUnsafeBytes { (pointer: UnsafePointer<UInt8>) -> Void in
try self.writeBuffer(pointer, length: data.count)
}
#endif
}
private func writeBuffer(_ pointer: UnsafeRawPointer, length: Int) throws {
var sent = 0
while sent < length {
#if os(Linux)
let result = send(self.socketFileDescriptor, pointer + sent, Int(length - sent), Int32(MSG_NOSIGNAL))
#else
let result = write(self.socketFileDescriptor, pointer + sent, Int(length - sent))
#endif
if result <= 0 {
throw SocketError.writeFailed(Errno.description())
}
sent += result
}
}
/// Read a single byte off the socket. This method is optimized for reading
/// a single byte. For reading multiple bytes, use read(length:), which will
/// pre-allocate heap space and read directly into it.
///
/// - Returns: A single byte
/// - Throws: SocketError.recvFailed if unable to read from the socket
open func read() throws -> UInt8 {
var byte: UInt8 = 0
#if os(Linux)
let count = Glibc.read(self.socketFileDescriptor as Int32, &byte, 1)
#else
let count = Darwin.read(self.socketFileDescriptor as Int32, &byte, 1)
#endif
guard count > 0 else {
throw SocketError.recvFailed(Errno.description())
}
return byte
}
/// Read up to `length` bytes from this socket
///
/// - Parameter length: The maximum bytes to read
/// - Returns: A buffer containing the bytes read
/// - Throws: SocketError.recvFailed if unable to read bytes from the socket
open func read(length: Int) throws -> [UInt8] {
return try [UInt8](unsafeUninitializedCapacity: length) { buffer, bytesRead in
bytesRead = try read(into: &buffer, length: length)
}
}
static let kBufferLength = 1024
/// Read up to `length` bytes from this socket into an existing buffer
///
/// - Parameter into: The buffer to read into (must be at least length bytes in size)
/// - Parameter length: The maximum bytes to read
/// - Returns: The number of bytes read
/// - Throws: SocketError.recvFailed if unable to read bytes from the socket
func read(into buffer: inout UnsafeMutableBufferPointer<UInt8>, length: Int) throws -> Int {
var offset = 0
guard let baseAddress = buffer.baseAddress else { return 0 }
while offset < length {
// Compute next read length in bytes. The bytes read is never more than kBufferLength at once.
let readLength = offset + Socket.kBufferLength < length ? Socket.kBufferLength : length - offset
#if os(Linux)
let bytesRead = Glibc.read(self.socketFileDescriptor as Int32, baseAddress + offset, readLength)
#else
let bytesRead = Darwin.read(self.socketFileDescriptor as Int32, baseAddress + offset, readLength)
#endif
guard bytesRead > 0 else {
throw SocketError.recvFailed(Errno.description())
}
offset += bytesRead
}
return offset
}
private static let CR: UInt8 = 13
private static let NL: UInt8 = 10
public func readLine() throws -> String {
var characters: String = ""
var index: UInt8 = 0
repeat {
index = try self.read()
if index > Socket.CR { characters.append(Character(UnicodeScalar(index))) }
} while index != Socket.NL
return characters
}
public func peername() throws -> String {
var addr = sockaddr(), len: socklen_t = socklen_t(MemoryLayout<sockaddr>.size)
if getpeername(self.socketFileDescriptor, &addr, &len) != 0 {
throw SocketError.getPeerNameFailed(Errno.description())
}
var hostBuffer = [CChar](repeating: 0, count: Int(NI_MAXHOST))
if getnameinfo(&addr, len, &hostBuffer, socklen_t(hostBuffer.count), nil, 0, NI_NUMERICHOST) != 0 {
throw SocketError.getNameInfoFailed(Errno.description())
}
return String(cString: hostBuffer)
}
public class func setNoSigPipe(_ socket: Int32) {
#if os(Linux)
// There is no SO_NOSIGPIPE in Linux (nor some other systems). You can instead use the MSG_NOSIGNAL flag when calling send(),
// or use signal(SIGPIPE, SIG_IGN) to make your entire application ignore SIGPIPE.
#else
// Prevents crashes when blocking calls are pending and the app is paused ( via Home button ).
var no_sig_pipe: Int32 = 1
setsockopt(socket, SOL_SOCKET, SO_NOSIGPIPE, &no_sig_pipe, socklen_t(MemoryLayout<Int32>.size))
#endif
}
public class func close(_ socket: Int32) {
#if os(Linux)
_ = Glibc.close(socket)
#else
_ = Darwin.close(socket)
#endif
}
}
extension Socket {
/// - Parameters:
/// - listenAddress: String representation of the address the socket should accept
/// connections from. It should be in IPv4 format if forceIPv4 == true,
/// otherwise - in IPv6.
public class func tcpSocketForListen(_ port: in_port_t, _ forceIPv4: Bool = false, _ maxPendingConnection: Int32 = SOMAXCONN, _ listenAddress: String? = nil) throws -> Socket {
#if os(Linux)
let socketFileDescriptor = socket(forceIPv4 ? AF_INET : AF_INET6, Int32(SOCK_STREAM.rawValue), 0)
#else
let socketFileDescriptor = socket(forceIPv4 ? AF_INET : AF_INET6, SOCK_STREAM, 0)
#endif
if socketFileDescriptor == -1 {
throw SocketError.socketCreationFailed(Errno.description())
}
var value: Int32 = 1
if setsockopt(socketFileDescriptor, SOL_SOCKET, SO_REUSEADDR, &value, socklen_t(MemoryLayout<Int32>.size)) == -1 {
let details = Errno.description()
Socket.close(socketFileDescriptor)
throw SocketError.socketSettingReUseAddrFailed(details)
}
Socket.setNoSigPipe(socketFileDescriptor)
var bindResult: Int32 = -1
if forceIPv4 {
#if os(Linux)
var addr = sockaddr_in(
sin_family: sa_family_t(AF_INET),
sin_port: port.bigEndian,
sin_addr: in_addr(s_addr: in_addr_t(0)),
sin_zero: (0, 0, 0, 0, 0, 0, 0, 0))
#else
var addr = sockaddr_in(
sin_len: UInt8(MemoryLayout<sockaddr_in>.stride),
sin_family: UInt8(AF_INET),
sin_port: port.bigEndian,
sin_addr: in_addr(s_addr: in_addr_t(0)),
sin_zero: (0, 0, 0, 0, 0, 0, 0, 0))
#endif
if let address = listenAddress {
if address.withCString({ cstring in inet_pton(AF_INET, cstring, &addr.sin_addr) }) == 1 {
// print("\(address) is converted to \(addr.sin_addr).")
} else {
// print("\(address) is not converted.")
}
}
bindResult = withUnsafePointer(to: &addr) {
bind(socketFileDescriptor, UnsafePointer<sockaddr>(OpaquePointer($0)), socklen_t(MemoryLayout<sockaddr_in>.size))
}
} else {
#if os(Linux)
var addr = sockaddr_in6(
sin6_family: sa_family_t(AF_INET6),
sin6_port: port.bigEndian,
sin6_flowinfo: 0,
sin6_addr: in6addr_any,
sin6_scope_id: 0)
#else
var addr = sockaddr_in6(
sin6_len: UInt8(MemoryLayout<sockaddr_in6>.stride),
sin6_family: UInt8(AF_INET6),
sin6_port: port.bigEndian,
sin6_flowinfo: 0,
sin6_addr: in6addr_any,
sin6_scope_id: 0)
#endif
if let address = listenAddress {
if address.withCString({ cstring in inet_pton(AF_INET6, cstring, &addr.sin6_addr) }) == 1 {
//print("\(address) is converted to \(addr.sin6_addr).")
} else {
//print("\(address) is not converted.")
}
}
bindResult = withUnsafePointer(to: &addr) {
bind(socketFileDescriptor, UnsafePointer<sockaddr>(OpaquePointer($0)), socklen_t(MemoryLayout<sockaddr_in6>.size))
}
}
if bindResult == -1 {
let details = Errno.description()
Socket.close(socketFileDescriptor)
throw SocketError.bindFailed(details)
}
if listen(socketFileDescriptor, maxPendingConnection) == -1 {
let details = Errno.description()
Socket.close(socketFileDescriptor)
throw SocketError.listenFailed(details)
}
return Socket(socketFileDescriptor: socketFileDescriptor)
}
public func acceptClientSocket() throws -> Socket {
var addr = sockaddr()
var len: socklen_t = 0
let clientSocket = accept(self.socketFileDescriptor, &addr, &len)
if clientSocket == -1 {
throw SocketError.acceptFailed(Errno.description())
}
Socket.setNoSigPipe(clientSocket)
return Socket(socketFileDescriptor: clientSocket)
}
}
public func == (socket1: Socket, socket2: Socket) -> Bool {
return socket1.socketFileDescriptor == socket2.socketFileDescriptor
}
public enum SocketError: Error {
case socketCreationFailed(String)
case socketSettingReUseAddrFailed(String)
case bindFailed(String)
case listenFailed(String)
case writeFailed(String)
case getPeerNameFailed(String)
case convertingPeerNameFailed
case getNameInfoFailed(String)
case acceptFailed(String)
case recvFailed(String)
case getSockNameFailed(String)
}
public protocol HttpServerIODelegate: AnyObject {
func socketConnectionReceived(_ socket: Socket)
}
enum HttpParserError: Error, Equatable {
case invalidStatusLine(String)
case negativeContentLength
}
public class HttpParser {
public init() { }
public func readHttpRequest(_ socket: Socket) throws -> HttpRequest {
let statusLine = try socket.readLine()
let statusLineTokens = statusLine.components(separatedBy: " ")
if statusLineTokens.count < 3 {
throw HttpParserError.invalidStatusLine(statusLine)
}
let request = HttpRequest()
request.method = statusLineTokens[0]
let encodedPath = statusLineTokens[1].addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? statusLineTokens[1]
let urlComponents = URLComponents(string: encodedPath)
request.path = urlComponents?.path ?? ""
request.queryParams = urlComponents?.queryItems?.map { ($0.name, $0.value ?? "") } ?? []
request.headers = try readHeaders(socket)
if let contentLength = request.headers["content-length"], let contentLengthValue = Int(contentLength) {
// Prevent a buffer overflow and runtime error trying to create an `UnsafeMutableBufferPointer` with
// a negative length
guard contentLengthValue >= 0 else {
throw HttpParserError.negativeContentLength
}
request.body = try readBody(socket, size: contentLengthValue)
}
return request
}
private func readBody(_ socket: Socket, size: Int) throws -> [UInt8] {
return try socket.read(length: size)
}
private func readHeaders(_ socket: Socket) throws -> [String: String] {
var headers = [String: String]()
while case let headerLine = try socket.readLine(), !headerLine.isEmpty {
let headerTokens = headerLine.split(separator: ":", maxSplits: 1, omittingEmptySubsequences: true).map(String.init)
if let name = headerTokens.first, let value = headerTokens.last {
headers[name.lowercased()] = value.trimmingCharacters(in: .whitespaces)
}
}
return headers
}
}
open class HttpServerIO {
public weak var delegate: HttpServerIODelegate?
private var socket = Socket(socketFileDescriptor: -1)
private var sockets = Set<Socket>()
public enum HttpServerIOState: Int32 {
case starting
case running
case stopping
case stopped
}
private var stateValue: Int32 = HttpServerIOState.stopped.rawValue
public private(set) var state: HttpServerIOState {
get {
return HttpServerIOState(rawValue: stateValue)!
}
set(state) {
#if !os(Linux)
OSAtomicCompareAndSwapInt(self.state.rawValue, state.rawValue, &stateValue)
#else
self.stateValue = state.rawValue
#endif
}
}
public var operating: Bool { return self.state == .running }
/// String representation of the IPv4 address to receive requests from.
/// It's only used when the server is started with `forceIPv4` option set to true.
/// Otherwise, `listenAddressIPv6` will be used.
public var listenAddressIPv4: String?
/// String representation of the IPv6 address to receive requests from.
/// It's only used when the server is started with `forceIPv4` option set to false.
/// Otherwise, `listenAddressIPv4` will be used.
public var listenAddressIPv6: String?
private let queue = DispatchQueue(label: "swifter.httpserverio.clientsockets")
public func port() throws -> Int {
return Int(try socket.port())
}
public func isIPv4() throws -> Bool {
return try socket.isIPv4()
}
deinit {
stop()
}
public func start(_ port: in_port_t = 8080, forceIPv4: Bool = false, priority: DispatchQoS.QoSClass = DispatchQoS.QoSClass.background) throws {
guard !self.operating else { return }
stop()
self.state = .starting
let address = forceIPv4 ? listenAddressIPv4 : listenAddressIPv6
self.socket = try Socket.tcpSocketForListen(port, forceIPv4, SOMAXCONN, address)
self.state = .running
DispatchQueue.global(qos: priority).async { [weak self] in
guard let strongSelf = self else { return }
guard strongSelf.operating else { return }
while let socket = try? strongSelf.socket.acceptClientSocket() {
DispatchQueue.global(qos: priority).async { [weak self] in
guard let strongSelf = self else { return }
guard strongSelf.operating else { return }
strongSelf.queue.async {
strongSelf.sockets.insert(socket)
}
strongSelf.handleConnection(socket)
strongSelf.queue.async {
strongSelf.sockets.remove(socket)
}
}
}
strongSelf.stop()
}
}
public func stop() {
guard self.operating else { return }
self.state = .stopping
// Shutdown connected peers because they can live in 'keep-alive' or 'websocket' loops.
for socket in self.sockets {
socket.close()
}
self.queue.sync {
self.sockets.removeAll(keepingCapacity: true)
}
socket.close()
self.state = .stopped
}
open func dispatch(_ request: HttpRequest) -> ([String: String], HttpHandler) {
return ([:], { _, responder in responder(HttpResponse.notFound(nil)) })
}
private func handleConnection(_ socket: Socket) {
let parser = HttpParser()
if self.operating, let request = try? parser.readHttpRequest(socket) {
let request = request
request.address = try? socket.peername()
let (params, handler) = self.dispatch(request)
request.params = params
handler(request) { response in
do {
if self.operating {
try self.respond(socket, response: response)
}
} catch {
print("Failed to send response: \(error)")
}
socket.close()
}
}
}
private struct InnerWriteContext: HttpResponseBodyWriter {
let socket: Socket
// func write(_ file: String.File) throws {
// try socket.writeFile(file)
// }
func write(_ data: [UInt8]) throws {
try write(ArraySlice(data))
}
func write(_ data: ArraySlice<UInt8>) throws {
try socket.writeUInt8(data)
}
func write(_ data: NSData) throws {
try socket.writeData(data)
}
func write(_ data: Data) throws {
try socket.writeData(data)
}
}
private func respond(_ socket: Socket, response: HttpResponse) throws {
guard self.operating else { return }
// Some web-socket clients (like Jetfire) expects to have header section in a single packet.
// We can't promise that but make sure we invoke "write" only once for response header section.
var responseHeader = String()
responseHeader.append("HTTP/1.1 \(response.statusCode) \(response.reasonPhrase)\r\n")
let content = response.content()
if content.length >= 0 {
responseHeader.append("Content-Length: \(content.length)\r\n")
}
for (name, value) in response.headers() {
responseHeader.append("\(name): \(value)\r\n")
}
responseHeader.append("\r\n")
try socket.writeUTF8(responseHeader)
if let writeClosure = content.write {
let context = InnerWriteContext(socket: socket)
try writeClosure(context)
}
}
}
open class HttpServer: HttpServerIO {
let router = HttpRouter()
public override init() {
self.DELETE = MethodRoute(method: "DELETE", router: router)
self.PATCH = MethodRoute(method: "PATCH", router: router)
self.HEAD = MethodRoute(method: "HEAD", router: router)
self.POST = MethodRoute(method: "POST", router: router)
self.GET = MethodRoute(method: "GET", router: router)
self.PUT = MethodRoute(method: "PUT", router: router)
self.delete = MethodRoute(method: "DELETE", router: router)
self.patch = MethodRoute(method: "PATCH", router: router)
self.head = MethodRoute(method: "HEAD", router: router)
self.post = MethodRoute(method: "POST", router: router)
self.get = MethodRoute(method: "GET", router: router)
self.put = MethodRoute(method: "PUT", router: router)
}
public var DELETE, PATCH, HEAD, POST, GET, PUT: MethodRoute
public var delete, patch, head, post, get, put: MethodRoute
public subscript(path: String) -> HttpHandler? {
get { return nil }
set {
router.register(nil, path: path, handler: newValue)
}
}
public var routes: [String] {
return router.routes()
}
public var notFoundHandler: HttpHandler?
override open func dispatch(_ request: HttpRequest) -> ([String: String], HttpHandler) {
if let result = router.route(request.method, path: request.path) {
return result
}
if let notFoundHandler = self.notFoundHandler {
return ([:], notFoundHandler)
}
return super.dispatch(request)
}
public struct MethodRoute {
public let method: String
public let router: HttpRouter
public subscript(path: String) -> ((HttpRequest, HttpResponder) -> ())? {
get { return nil }
set {
router.register(method, path: path, handler: newValue)
}
}
}
}
public class HttpRequest {
public var path: String = ""
public var queryParams: [(String, String)] = []
public var method: String = ""
public var headers: [String: String] = [:]
public var body: [UInt8] = []
public var address: String? = ""
public var params: [String: String] = [:]
public init() {}
public func hasTokenForHeader(_ headerName: String, token: String) -> Bool {
guard let headerValue = headers[headerName] else {
return false
}
return headerValue.components(separatedBy: ",").filter({ $0.trimmingCharacters(in: .whitespaces).lowercased() == token }).count > 0
}
public func parseUrlencodedForm() -> [(String, String)] {
guard let contentTypeHeader = headers["content-type"] else {
return []
}
let contentTypeHeaderTokens = contentTypeHeader.components(separatedBy: ";").map { $0.trimmingCharacters(in: .whitespaces) }
guard let contentType = contentTypeHeaderTokens.first, contentType == "application/x-www-form-urlencoded" else {
return []
}
guard let utf8String = String(bytes: body, encoding: .utf8) else {
// Consider to throw an exception here (examine the encoding from headers).
return []
}
return utf8String.components(separatedBy: "&").map { param -> (String, String) in
let tokens = param.components(separatedBy: "=")
if let name = tokens.first?.removingPercentEncoding, let value = tokens.last?.removingPercentEncoding, tokens.count == 2 {
return (name.replacingOccurrences(of: "+", with: " "),
value.replacingOccurrences(of: "+", with: " "))
}
return ("", "")
}
}
}
public enum SerializationError: Error {
case invalidObject
case notSupported
}
public protocol HttpResponseBodyWriter {
// func write(_ file: String.File) throws
func write(_ data: [UInt8]) throws
func write(_ data: ArraySlice<UInt8>) throws
func write(_ data: NSData) throws
func write(_ data: Data) throws
}
public enum HttpResponseBody {
case json(Any)
case html(String)
case htmlBody(String)
case text(String)
case data(Data, contentType: String? = nil)
case custom(Any, (Any) throws -> String)
func content() -> (Int, ((HttpResponseBodyWriter) throws -> Void)?) {
do {
switch self {
case .json(let object):
guard JSONSerialization.isValidJSONObject(object) else {
throw SerializationError.invalidObject
}
let data: Data
if #available(macOS 10.15, *), { true }() {
data = try JSONSerialization.data(withJSONObject: object, options: [.sortedKeys, .withoutEscapingSlashes])
} else if #available(macOS 10.13, *), { true }() {
data = try JSONSerialization.data(withJSONObject: object, options: [.sortedKeys])
} else {
data = try JSONSerialization.data(withJSONObject: object, options: [])
}
return (data.count, {
try $0.write(data)
})
case .text(let body):
let data = [UInt8](body.utf8)
return (data.count, {
try $0.write(data)
})
case .html(let html):
let data = [UInt8](html.utf8)
return (data.count, {
try $0.write(data)
})
case .htmlBody(let body):
let serialized = "<html><meta charset=\"UTF-8\"><body>\(body)</body></html>"
let data = [UInt8](serialized.utf8)
return (data.count, {
try $0.write(data)
})
case .data(let data, _):
return (data.count, {
try $0.write(data)
})
case .custom(let object, let closure):
let serialized = try closure(object)
let data = [UInt8](serialized.utf8)
return (data.count, {
try $0.write(data)
})
}
} catch {
let data = [UInt8]("Serialization error: \(error)".utf8)
return (data.count, {
try $0.write(data)
})
}
}
}
public enum HttpResponse {
case ok(HttpResponseBody, [String: String] = [:]), created, accepted
case movedPermanently(String)
case movedTemporarily(String)
case badRequest(HttpResponseBody?), unauthorized(HttpResponseBody?), forbidden(HttpResponseBody?), notFound(HttpResponseBody? = nil), notAcceptable(HttpResponseBody?), tooManyRequests(HttpResponseBody?), internalServerError(HttpResponseBody?)
case raw(Int, String, [String: String]?, ((HttpResponseBodyWriter) throws -> Void)? )
public var statusCode: Int {
switch self {
case .ok : return 200
case .created : return 201
case .accepted : return 202
case .movedPermanently : return 301
case .movedTemporarily : return 307
case .badRequest : return 400
case .unauthorized : return 401
case .forbidden : return 403
case .notFound : return 404
case .notAcceptable : return 406
case .tooManyRequests : return 429
case .internalServerError : return 500
case .raw(let code, _, _, _) : return code
}
}
public var reasonPhrase: String {
switch self {
case .ok : return "OK"
case .created : return "Created"
case .accepted : return "Accepted"
case .movedPermanently : return "Moved Permanently"
case .movedTemporarily : return "Moved Temporarily"
case .badRequest : return "Bad Request"
case .unauthorized : return "Unauthorized"
case .forbidden : return "Forbidden"
case .notFound : return "Not Found"
case .notAcceptable : return "Not Acceptable"
case .tooManyRequests : return "Too Many Requests"
case .internalServerError : return "Internal Server Error"
case .raw(_, let phrase, _, _) : return phrase
}
}
public func headers() -> [String: String] {
var headers = ["Server": "Swiftlet"]
switch self {
case .ok(let body, let customHeaders):
for (key, value) in customHeaders {
headers.updateValue(value, forKey: key)
}
switch body {
case .json: headers["Content-Type"] = "application/json"
case .html, .htmlBody: headers["Content-Type"] = "text/html"
case .text: headers["Content-Type"] = "text/plain"
case .data(_, let contentType): headers["Content-Type"] = contentType
default:break
}
case .movedPermanently(let location):
headers["Location"] = location
case .movedTemporarily(let location):
headers["Location"] = location
case .raw(_, _, let rawHeaders, _):
if let rawHeaders = rawHeaders {
for (key, value) in rawHeaders {
headers.updateValue(value, forKey: key)
}
}
default:break
}
return headers
}
func content() -> (length: Int, write: ((HttpResponseBodyWriter) throws -> Void)?) {
switch self {
case .ok(let body, _) : return body.content()
case .badRequest(let body), .unauthorized(let body), .forbidden(let body), .notFound(let body), .tooManyRequests(let body), .internalServerError(let body) : return body?.content() ?? (-1, nil)
case .raw(_, _, _, let writer) : return (-1, writer)
default : return (-1, nil)
}
}
}
open class HttpRouter {
public init() {}
private class Node {
/// The children nodes that form the route
var nodes = [String: Node]()
/// Define whether or not this node is the end of a route
var isEndOfRoute: Bool = false
/// The closure to handle the route
var handler: HttpHandler?
}
private var rootNode = Node()
/// The Queue to handle the thread safe access to the routes
private let queue = DispatchQueue(label: "swifter.httpserverio.httprouter")
public func routes() -> [String] {
var routes = [String]()
for (_, child) in rootNode.nodes {
routes.append(contentsOf: routesForNode(child))
}
return routes
}
private func routesForNode(_ node: Node, prefix: String = "") -> [String] {
var result = [String]()
if node.handler != nil {
result.append(prefix)
}
for (key, child) in node.nodes {
result.append(contentsOf: routesForNode(child, prefix: prefix + "/" + key))
}
return result
}
public func register(_ method: String?, path: String, handler: HttpHandler?) {
var pathSegments = stripQuery(path).split("/")
if let method = method {
pathSegments.insert(method, at: 0)
} else {
pathSegments.insert("*", at: 0)
}
var pathSegmentsGenerator = pathSegments.makeIterator()
inflate(&rootNode, generator: &pathSegmentsGenerator).handler = handler
}
public func route(_ method: String?, path: String) -> ([String: String], HttpHandler)? {
return queue.sync {
if let method = method {
let pathSegments = (method + "/" + stripQuery(path)).split("/")
var pathSegmentsGenerator = pathSegments.makeIterator()
var params = [String: String]()
if let handler = findHandler(&rootNode, params: &params, generator: &pathSegmentsGenerator) {
return (params, handler)
}
}
let pathSegments = ("*/" + stripQuery(path)).split("/")
var pathSegmentsGenerator = pathSegments.makeIterator()
var params = [String: String]()
if let handler = findHandler(&rootNode, params: &params, generator: &pathSegmentsGenerator) {
return (params, handler)
}
return nil
}
}
private func inflate(_ node: inout Node, generator: inout IndexingIterator<[String]>) -> Node {
var currentNode = node
while let pathSegment = generator.next() {
if let nextNode = currentNode.nodes[pathSegment] {
currentNode = nextNode
} else {
currentNode.nodes[pathSegment] = Node()
currentNode = currentNode.nodes[pathSegment]!
}
}
currentNode.isEndOfRoute = true
return currentNode
}
private func findHandler(_ node: inout Node, params: inout [String: String], generator: inout IndexingIterator<[String]>) -> HttpHandler? {
var matchedRoutes = [Node]()
let pattern = generator.map { $0 }
let numberOfElements = pattern.count
findHandler(&node, params: &params, pattern: pattern, matchedNodes: &matchedRoutes, index: 0, count: numberOfElements)
return matchedRoutes.first?.handler
}
/// Find the handlers for a specified route
///
/// - Parameters:
/// - node: The root node of the tree representing all the routes
/// - params: The parameters of the match
/// - pattern: The pattern or route to find in the routes tree
/// - matchedNodes: An array with the nodes matching the route
/// - index: The index of current position in the generator
/// - count: The number of elements if the route to match
private func findHandler(_ node: inout Node, params: inout [String: String], pattern: [String], matchedNodes: inout [Node], index: Int, count: Int) {
if index < count, let pathToken = pattern[index].removingPercentEncoding {
var currentIndex = index + 1
let variableNodes = node.nodes.filter { $0.0.first == ":" }
if let variableNode = variableNodes.first {
if currentIndex == count && variableNode.1.isEndOfRoute {
// if it's the last element of the pattern and it's a variable, stop the search and
// append a tail as a value for the variable.
let tail = pattern[currentIndex..<count].joined(separator: "/")
if tail.count > 0 {
params[variableNode.0] = pathToken + "/" + tail
} else {
params[variableNode.0] = pathToken
}
matchedNodes.append(variableNode.value)
return
}
params[variableNode.0] = pathToken
findHandler(&node.nodes[variableNode.0]!, params: &params, pattern: pattern, matchedNodes: &matchedNodes, index: currentIndex, count: count)
}
if var node = node.nodes[pathToken] {
findHandler(&node, params: &params, pattern: pattern, matchedNodes: &matchedNodes, index: currentIndex, count: count)
}
if var node = node.nodes["*"] {
findHandler(&node, params: &params, pattern: pattern, matchedNodes: &matchedNodes, index: currentIndex, count: count)
}
if let startStarNode = node.nodes["**"] {
if startStarNode.isEndOfRoute {
// ** at the end of a route works as a catch-all
matchedNodes.append(startStarNode)
return
}
let startStarNodeKeys = startStarNode.nodes.keys
currentIndex += 1
while currentIndex < count, let pathToken = pattern[currentIndex].removingPercentEncoding {
currentIndex += 1
if startStarNodeKeys.contains(pathToken) {
findHandler(&startStarNode.nodes[pathToken]!, params: &params, pattern: pattern, matchedNodes: &matchedNodes, index: currentIndex, count: count)
}
}
}
}
if node.isEndOfRoute && index == count {
// if it's the last element and the path to match is done then it's a pattern matching
matchedNodes.append(node)
return
}
}
private func stripQuery(_ path: String) -> String {
if let path = path.components(separatedBy: "?").first {
return path
}
return path
}
}
public class Errno {
public class func description() -> String {
return String(cString: strerror(errno))
}
}
extension String {
func split(_ separator: Character) -> [String] {
return self.split { $0 == separator }.map(String.init)
}
}
extension String {
public func unquote() -> String {
var scalars = self.unicodeScalars
if scalars.first == "\"" && scalars.last == "\"" && scalars.count >= 2 {
scalars.removeFirst()
scalars.removeLast()
return String(scalars)
}
return self
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment