Skip to content

Instantly share code, notes, and snippets.

@stephancasas
Created March 29, 2024 16:28
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save stephancasas/f7d2d6cf19077a539539cc06bce77be4 to your computer and use it in GitHub Desktop.
Save stephancasas/f7d2d6cf19077a539539cc06bce77be4 to your computer and use it in GitHub Desktop.
A low-level HTTP server offering a SwiftUI-observable lifecycle.
//
// DumbHTTPServer.swift
// DumbHTTPServer
//
// Created by Stephan Casas on 3/29/24.
//
import SwiftUI;
import Combine;
class DumbHTTPServer: ObservableObject {
@Published var isListening: Bool = false;
@Published var responseStatus: HTTPStatusCode = .ok;
@Published var responseBody: String = "OK";
@Published var requestString: String = "";
@Published var port: Int;
private let backlog: Int32;
private let requestBufferSize: Int;
private var serverThread: Thread? = nil;
private var subscriptions = [AnyCancellable]();
static let defaultPort: Int = 4444;
init(port: Int, backlog: Int32 = 10, requestBufferSize: Int = 1024) {
self.port = port;
self.backlog = backlog;
self.requestBufferSize = requestBufferSize;
self.$isListening.sink(receiveValue: { [weak self] in
self?.onIsListeningDidChange($0)
}).store(in: &self.subscriptions);
self.$port
.debounce(for: .milliseconds(500), scheduler: RunLoop.main)
.sink(receiveValue: { [weak self] in
self?.onPortNumberDidChange($0);
}).store(in: &self.subscriptions);
}
private func onPortNumberDidChange(_ newValue: Int) {
if !self.isListening { return }
self.hangup();
self.listen();
}
private func onIsListeningDidChange(_ newValue: Bool) {
guard newValue else {
return self.hangup();
}
self.listen();
}
private func listen() {
let listen_fd = socket(AF_INET, SOCK_STREAM, 0);
let servaddr = sockaddr_in(
sin_len: 0,
sin_family: UInt8(AF_INET),
sin_port: _OSSwapInt16(.init(self.port == 0 ? Self.defaultPort : self.port)),
sin_addr: .init(s_addr: _OSSwapInt32(INADDR_ANY)),
sin_zero: (0, 0, 0, 0, 0, 0, 0, 0));
withUnsafePointer(to: unsafeBitCast(servaddr, to: sockaddr.self), {
let _ = bind(listen_fd, $0, UInt32(MemoryLayout.size(ofValue: servaddr)));
});
Darwin.listen(listen_fd, self.backlog);
self.serverThread = .init(block: { [weak self] in
let requestBufferSize = self?.requestBufferSize ?? 1024;
var requestBuffer = [UInt8].init(repeating: 0, count: requestBufferSize);
while true {
let connection = accept(listen_fd, nil, nil);
requestBuffer.withContiguousMutableStorageIfAvailable({
$0.initialize(repeating: 0);
read(connection, $0.baseAddress, requestBufferSize);
});
let responseString = self?.responseString ?? "";
responseString.utf8CString.withUnsafeBytes({
let _ = write(connection, $0.baseAddress, responseString.utf8.count);
});
close(connection);
guard self != nil else { break }
DispatchQueue.main.async(execute: {
self?.requestString = .init(cString: requestBuffer);
})
}
});
self.serverThread?.start();
}
private func hangup() {
self.serverThread?.cancel();
}
private var responseString: String {
"""
HTTP/1.1 \(self.responseStatus)
Content-Type: text/plain; charset=utf-8
Connection: close
Content-Length: \(self.responseBody.count)
\(self.responseBody)
"""
}
deinit {
self.serverThread?.cancel();
}
}
//
// HTTPStatusCode.swift
// DumbHTTPServer
//
// Created by Stephan Casas on 3/29/24.
//
import Foundation
enum HTTPStatusCode: Int, Error, CaseIterable {
enum ResponseType {
case informational, success, redirection, clientError, serverError, undefined;
}
case `continue` = 100;
case switchingProtocols = 101;
case processing = 102;
case ok = 200;
case created = 201;
case accepted = 202;
case nonAuthoritativeInformation = 203;
case noContent = 204;
case resetContent = 205;
case partialContent = 206;
case multiStatus = 207;
case alreadyReported = 208;
case IMUsed = 226;
case multipleChoices = 300;
case movedPermanently = 301;
case found = 302;
case seeOther = 303;
case notModified = 304;
case useProxy = 305;
case switchProxy = 306;
case temporaryRedirect = 307;
case permanentRedirect = 308;
case badRequest = 400;
case unauthorized = 401;
case paymentRequired = 402;
case forbidden = 403;
case notFound = 404;
case methodNotAllowed = 405;
case notAcceptable = 406;
case proxyAuthenticationRequired = 407;
case requestTimeout = 408;
case conflict = 409;
case gone = 410;
case lengthRequired = 411;
case preconditionFailed = 412;
case payloadTooLarge = 413;
case URITooLong = 414;
case unsupportedMediaType = 415;
case rangeNotSatisfiable = 416;
case expectationFailed = 417;
case teapot = 418;
case misdirectedRequest = 421;
case unprocessableEntity = 422;
case locked = 423;
case failedDependency = 424;
case upgradeRequired = 426;
case preconditionRequired = 428;
case tooManyRequests = 429;
case requestHeaderFieldsTooLarge = 431;
case noResponse = 444;
case unavailableForLegalReasons = 451;
case SSLCertificateError = 495;
case SSLCertificateRequired = 496;
case HTTPRequestSentToHTTPSPort = 497;
case clientClosedRequest = 499;
case internalServerError = 500;
case notImplemented = 501;
case badGateway = 502;
case serviceUnavailable = 503;
case gatewayTimeout = 504;
case HTTPVersionNotSupported = 505;
case variantAlsoNegotiates = 506;
case insufficientStorage = 507;
case loopDetected = 508;
case notExtended = 510;
case networkAuthenticationRequired = 511;
var responseType: ResponseType {
switch self.rawValue {
case 100..<200:
return .informational
case 200..<300:
return .success
case 300..<400:
return .redirection
case 400..<500:
return .clientError
case 500..<600:
return .serverError
default:
return .undefined
}
}
}
extension HTTPURLResponse {
var status: HTTPStatusCode? {
return HTTPStatusCode(rawValue: statusCode)
}
}
extension HTTPStatusCode: CustomStringConvertible {
var description: String {
"\(self.rawValue) \(self.disposition)"
}
var disposition: String {
switch self {
case .continue:
"Continue"
case .switchingProtocols:
"Switching Protocols"
case .processing:
"Processing"
case .ok:
"OK"
case .created:
"Created"
case .accepted:
"Accepted"
case .nonAuthoritativeInformation:
"Non-Authoritative Information"
case .noContent:
"No Content"
case .resetContent:
"Reset Content"
case .partialContent:
"Partial Content"
case .multiStatus:
"Multi-Status"
case .alreadyReported:
"Already Reported"
case .IMUsed:
"IM Used"
case .multipleChoices:
"Multiple Choices"
case .movedPermanently:
"Moved Permanently"
case .found:
"Found"
case .seeOther:
"See Other"
case .notModified:
"Not Modified"
case .useProxy:
"Use Proxy"
case .switchProxy:
"Switch Proxy"
case .temporaryRedirect:
"Temporary Redirect"
case .permanentRedirect:
"Permanent Redirect"
case .badRequest:
"Bad Request"
case .unauthorized:
"Unauthorized"
case .paymentRequired:
"Payment Required"
case .forbidden:
"Forbidden"
case .notFound:
"Not Found"
case .methodNotAllowed:
"Method Not Allowed"
case .notAcceptable:
"Not Acceptable"
case .proxyAuthenticationRequired:
"Proxy Authentication Required"
case .requestTimeout:
"Request Timeout"
case .conflict:
"Conflict"
case .gone:
"Gone"
case .lengthRequired:
"Length Required"
case .preconditionFailed:
"Precondition Failed"
case .payloadTooLarge:
"Payload Too Large"
case .URITooLong:
"URI Too Long"
case .unsupportedMediaType:
"Unsupported Media Type"
case .rangeNotSatisfiable:
"Range Not Satisfiable"
case .expectationFailed:
"Expectation Failed"
case .teapot:
"I'm a Teapot"
case .misdirectedRequest:
"Misdirected Request"
case .unprocessableEntity:
"Unprocessable Entity"
case .locked:
"Locked"
case .failedDependency:
"Failed Dependency"
case .upgradeRequired:
"Upgrade Required"
case .preconditionRequired:
"Precondition Required"
case .tooManyRequests:
"Too Many Requests"
case .requestHeaderFieldsTooLarge:
"Request Header Fields Too Large"
case .noResponse:
"No Response"
case .unavailableForLegalReasons:
"Unavailable for Legal Reasons"
case .SSLCertificateError:
"SSL Certificate Error"
case .SSLCertificateRequired:
"SSL Certificate Required"
case .HTTPRequestSentToHTTPSPort:
"HTTP Request Sent to HTTPS Port"
case .clientClosedRequest:
"Client Closed Request"
case .internalServerError:
"Internal Server Error"
case .notImplemented:
"Not Implemented"
case .badGateway:
"Bad Gateway"
case .serviceUnavailable:
"Service Unavailable"
case .gatewayTimeout:
"Gateway Timeout"
case .HTTPVersionNotSupported:
"HTTP Version Not Supported"
case .variantAlsoNegotiates:
"Variant Also Negotiates"
case .insufficientStorage:
"Insufficient Storage"
case .loopDetected:
"Loop Detected"
case .notExtended:
"Not Extended"
case .networkAuthenticationRequired:
"Network Authentication Required"
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment