Skip to content

Instantly share code, notes, and snippets.

@nickmain
Created April 11, 2016 03:28
Show Gist options
  • Save nickmain/cf0b50e9fb14f36f86e459b77232989b to your computer and use it in GitHub Desktop.
Save nickmain/cf0b50e9fb14f36f86e459b77232989b to your computer and use it in GitHub Desktop.
Minimal Swift HTTP server for unit testing
//: Playground - noun: a place where people can play
import Foundation
class ServerSocket {
private var sockAddrLen = socklen_t(sizeof(sockaddr_in))
private var serverSocket = socket(AF_INET, SOCK_STREAM, 0)
init?(_ port: UInt16) {
var option: UInt32 = 1
setsockopt(serverSocket, SOL_SOCKET, SO_REUSEADDR, &option, 4)
var sockAddr = sockaddr_in()
sockAddr.sin_family = sa_family_t(AF_INET) //IPv4
sockAddr.sin_port = CFSwapInt16(port)
sockAddr.sin_addr = in_addr(s_addr: 0) //bind to any address
guard (withUnsafePointer(&sockAddr) { bind(serverSocket, UnsafePointer($0), sockAddrLen) }) == 0 else { return nil }
guard listen(serverSocket, 5 /*queue*/) == 0 else { return nil }
}
func waitForRequest() -> ClientRequest {
var clientAddr = sockaddr_in()
let incomingSocket = withUnsafeMutablePointer(&clientAddr) {
accept(serverSocket, UnsafeMutablePointer($0), &sockAddrLen)
}
return ClientRequest(incomingSocket)
}
func closeSocket() {
if serverSocket != 0 {
close(serverSocket)
serverSocket = 0
}
}
deinit {
closeSocket()
}
}
class ClientRequest {
private var clientSocket: Int32
private var stream: UnsafeMutablePointer<FILE>
var method = ""
var path = ""
var headers: [String: String] = [:]
var body = ""
private init(_ socket: Int32) {
clientSocket = socket
stream = fdopen(socket, "r+") // open for reading+writing
var buffer = Array<CChar>(count: 1000, repeatedValue: 0)
func readLine() -> String? {
if fgets(&buffer, 1000, stream) != nil {
var line = String.fromCString(&buffer)!
line = line.substringToIndex(line.endIndex.advancedBy(-1)) //chop off the newline
if !line.isEmpty { return line }
}
return nil
}
// read method and path
guard let httpLine = readLine() else { return }
let parts = httpLine.componentsSeparatedByString(" ")
method = parts[0].uppercaseString
path = parts[1]
// read headers up to the empty line
repeat {
guard let line = readLine() else { break }
let elems = line.characters.split(":", maxSplit: 1, allowEmptySlices: true)
let name = String(elems[0]).lowercaseString
let value = String(elems[1]).stringByTrimmingCharactersInSet(NSCharacterSet.whitespaceCharacterSet())
headers.updateValue(value, forKey: name)
} while true
// read body
if let lengthValue = headers["content-length"],
let length = Int(lengthValue) {
var data = Array<CChar>(count: length + 1, repeatedValue: 0)
fread(&data, 1, length, stream)
body = String.fromCString(&data)!
}
}
// Respond with the given status string
func respond(status: String) {
write("HTTP/1.1 \(status)\r\n\r\n")
}
// Respond with the given status string and body
func respond(status: String, content: String, type: String) {
var body = content.cStringUsingEncoding(NSASCIIStringEncoding)!
let length = strlen(&body)
write("HTTP/1.1 \(status)\r\nContent-Type: \(type)\r\nContent-Length: \(length)\r\n\r\n")
write(body)
}
private func write(text: String) {
write(text.cStringUsingEncoding(NSASCIIStringEncoding)!)
}
private func write(chars: [CChar]) {
var data = chars
fwrite(&data, 1, size_t(strlen(&data)), stream)
}
deinit {
fflush(stream)
fclose(stream)
close(clientSocket)
}
}
if let server = ServerSocket(8080) {
let req = server.waitForRequest()
print(req.headers)
req.respond("200 OK", content: "hello world", type: "text/plain")
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment