Skip to content

Commit

Permalink
Merge pull request #10 from briancroom/parseBody
Browse files Browse the repository at this point in the history
Parse HTTP message body
  • Loading branch information
kylef committed Dec 16, 2015
2 parents 40b06e7 + c8518c4 commit 5579c42
Show file tree
Hide file tree
Showing 3 changed files with 80 additions and 21 deletions.
83 changes: 62 additions & 21 deletions Sources/HTTPParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import Inquiline
enum HTTPParserError : ErrorType {
case BadSyntax(String)
case BadVersion(String)
case Incomplete
case Internal

func response() -> ResponseType {
Expand All @@ -19,6 +20,8 @@ enum HTTPParserError : ErrorType {
return Response(.BadRequest, contentType: "text/plain", body: "Bad Syntax (\(syntax))")
case let .BadVersion(version):
return Response(.BadRequest, contentType: "text/plain", body: "Bad Version (\(version))")
case .Incomplete:
return Response(.BadRequest, contentType: "text/plain", body: "Incomplete HTTP Request")
case .Internal:
return Response(.InternalServerError, contentType: "text/plain", body: "Internal Server Error")
}
Expand All @@ -32,37 +35,56 @@ class HTTPParser {
self.socket = socket
}

// Read the socket until we find \r\n\r\n
// returning string before and chars after
func readUntil() throws -> (String, [CChar]) {
// Repeatedly read the socket, calling the predicate with the accumulated
// buffer to determine how many bytes to attempt to read next.
// Stops reading and returns the buffer when the predicate returns 0.
func readWhile(predicate: [CChar] -> Int) throws -> [CChar] {
var buffer: [CChar] = []

while true {
if let bytes = try? socket.read(512) {
if bytes.isEmpty {
throw HTTPParserError.Internal
}
let nextSize = predicate(buffer)
guard nextSize > 0 else { break }

buffer += bytes
let bytes = try socket.read(nextSize)
guard !bytes.isEmpty else { break }

let crln: [CChar] = [13, 10, 13, 10]
if let (top, bottom) = buffer.find(crln) {
if let headers = String.fromCString(top + [0]) {
return (headers, bottom)
}
buffer += bytes
}

print("[worker] Failed to decode data from client")
throw HTTPParserError.Internal
}
return buffer
}

// TODO bail if server never sends us \r\n\r\n
// Read the socket until we find \r\n\r\n
// returning string before and chars after
func readHeaders() throws -> (String, [CChar]) {
var findResult: (top: [CChar], bottom: [CChar])?

let crln: [CChar] = [13, 10, 13, 10]
try readWhile({ bytes in
if let (top, bottom) = bytes.find(crln) {
findResult = (top, bottom)
return 0
} else {
return 512
}
})

guard let result = findResult else { throw HTTPParserError.Incomplete }

guard let headers = String.fromCString(result.top + [0]) else {
print("[worker] Failed to decode data from client")
throw HTTPParserError.Internal
}

return (headers, result.bottom)
}

func readBody(maxLength maxLength: Int) throws -> [CChar] {
return try readWhile({ bytes in min(maxLength-bytes.count, 512) })
}

func parse() throws -> RequestType {
// TODO body
let (top, _) = try readUntil()
let (top, startOfBody) = try readHeaders()
var components = top.split("\r\n")
let requestLine = components.removeFirst()
components.removeLast()
Expand All @@ -80,8 +102,15 @@ class HTTPParser {
throw HTTPParserError.BadVersion(version)
}

let headers = parseHeaders(components)
return Request(method: method, path: path, headers: headers)
var request = Request(method: method, path: path, headers: parseHeaders(components))

if let contentLength = request.contentLength {
let remainingContentLength = contentLength - startOfBody.count
let bodyBytes = startOfBody + (try readBody(maxLength: remainingContentLength))
request.body = try parseBody(bodyBytes, contentLength: contentLength)
}

return request
}

func parseHeaders(headers: [String]) -> [Header] {
Expand All @@ -97,6 +126,18 @@ class HTTPParser {
return nil
}
}

func parseBody(bytes: [CChar], contentLength: Int) throws -> String {
guard bytes.count >= contentLength else { throw HTTPParserError.Incomplete }

let trimmedBytes = contentLength<bytes.count ? Array(bytes[0..<contentLength]) : bytes

guard let bodyString = String.fromCString(trimmedBytes + [0]) else {
print("[worker] Failed to decode message body from client")
throw HTTPParserError.Internal
}
return bodyString
}
}


Expand Down
3 changes: 3 additions & 0 deletions Sources/Socket.swift
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,9 @@ class Socket {
func read(bytes: Int) throws -> [CChar] {
let data = Data(capacity: bytes)
let bytes = system_read(descriptor, data.bytes, data.capacity)
guard bytes != -1 else {
throw SocketError()
}
return Array(data.characters[0..<bytes])
}

Expand Down
15 changes: 15 additions & 0 deletions Tests/HTTPParserSpec.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,13 @@ describe("HTTPParser") {
try expect(header.1) == "localhost"
}

$0.it("reads the message body when Content-Length is present") {
outSocket.write("GET / HTTP/1.1\r\nContent-Length: 5\r\n\r\nabcdefgh")

let request = try parser.parse()
try expect(request.body) == "abcde"
}

$0.it("throws an error when the client uses bad HTTP syntax") {
outSocket.write("GET /\r\nHost: localhost\r\n\r\n")
try expect(try parser.parse()).toThrow()
Expand All @@ -62,6 +69,14 @@ describe("HTTPParser") {

try expect(try parser.parse()).toThrow()
}

$0.it("throws an error when the client disconnects before sending the entire message body") {
outSocket.write("GET / HTTP/1.1\r\nContent-Length: 42\r\n\r\nabcdefgh")
outSocket.close()
outSocket = nil

try expect(try parser.parse()).toThrow()
}
}

}

0 comments on commit 5579c42

Please sign in to comment.