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.

Revisions

  1. nickmain created this gist Apr 11, 2016.
    125 changes: 125 additions & 0 deletions MiniHTTPServer.swift
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,125 @@
    //: 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")
    }