Created
March 24, 2022 01:20
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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: ¶ms, generator: &pathSegmentsGenerator) { | |
return (params, handler) | |
} | |
} | |
let pathSegments = ("*/" + stripQuery(path)).split("/") | |
var pathSegmentsGenerator = pathSegments.makeIterator() | |
var params = [String: String]() | |
if let handler = findHandler(&rootNode, params: ¶ms, 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: ¶ms, 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: ¶ms, pattern: pattern, matchedNodes: &matchedNodes, index: currentIndex, count: count) | |
} | |
if var node = node.nodes[pathToken] { | |
findHandler(&node, params: ¶ms, pattern: pattern, matchedNodes: &matchedNodes, index: currentIndex, count: count) | |
} | |
if var node = node.nodes["*"] { | |
findHandler(&node, params: ¶ms, 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: ¶ms, 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