Skip to content

Commit

Permalink
Merge pull request #18 from kkrull/parameter-decode-prep
Browse files Browse the repository at this point in the history
Parameter decode prep
  • Loading branch information
kkrull authored Apr 25, 2018
2 parents 7b37ad2 + 2e8979d commit cb322a3
Show file tree
Hide file tree
Showing 17 changed files with 556 additions and 335 deletions.
4 changes: 2 additions & 2 deletions http/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ type blockingConnectionHandler struct {
}

func (handler *blockingConnectionHandler) Handle(requestReader *bufio.Reader, responseWriter io.Writer) {
request, routeErrorResponse := handler.Router.ParseRequest(requestReader)
request, routeErrorResponse := handler.Router.RouteRequest(requestReader)
if routeErrorResponse != nil {
routeErrorResponse.WriteTo(responseWriter)
return
Expand All @@ -35,7 +35,7 @@ func (handler *blockingConnectionHandler) Routes() []Route {
}

type Router interface {
ParseRequest(reader *bufio.Reader) (ok Request, routeError Response)
RouteRequest(reader *bufio.Reader) (ok Request, err Response)
Routes() []Route
}

Expand Down
89 changes: 89 additions & 0 deletions http/parser.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package http

import (
"bufio"
"strings"

"github.com/kkrull/gohttp/msg/clienterror"
)

// Parses an HTTP request message one line at a time.
type LineRequestParser struct{}

func (parser *LineRequestParser) Parse(reader *bufio.Reader) (ok *RequestLine, err Response) {
methodObject := &parseMethodObject{reader: reader}
return methodObject.Parse()
}

type parseMethodObject struct {
reader *bufio.Reader
}

func (parser *parseMethodObject) Parse() (ok *RequestLine, err Response) {
requestLine, err := parser.readCRLFLine()
if err != nil {
return nil, err
}

return parser.doParseRequestLine(requestLine)
}

func (parser *parseMethodObject) doParseRequestLine(requestLine string) (ok *RequestLine, err Response) {
requested, err := parser.parseRequestLine(requestLine)
if err != nil {
return nil, err
}

return parser.doParseHeaders(requested)
}

func (parser *parseMethodObject) doParseHeaders(requested *RequestLine) (ok *RequestLine, err Response) {
err = parser.parseHeaders()
if err != nil {
return nil, err
}

return requested, nil
}

func (parser *parseMethodObject) parseRequestLine(text string) (ok *RequestLine, badRequest Response) {
fields := strings.Split(text, " ")
if len(fields) != 3 {
return nil, &clienterror.BadRequest{DisplayText: "incorrectly formatted or missing request-line"}
}

return &RequestLine{
Method: fields[0],
Target: fields[1],
}, nil
}

func (parser *parseMethodObject) parseHeaders() (badRequest Response) {
isBlankLineBetweenHeadersAndBody := func(line string) bool { return line == "" }

for {
line, err := parser.readCRLFLine()
if err != nil {
return err
} else if isBlankLineBetweenHeadersAndBody(line) {
return nil
}
}
}

func (parser *parseMethodObject) readCRLFLine() (line string, badRequest Response) {
maybeEndsInCR, _ := parser.reader.ReadString('\r')
if len(maybeEndsInCR) == 0 {
return "", &clienterror.BadRequest{DisplayText: "end of input before terminating CRLF"}
} else if !strings.HasSuffix(maybeEndsInCR, "\r") {
return "", &clienterror.BadRequest{DisplayText: "line in request header not ending in CRLF"}
}

nextCharacter, _ := parser.reader.ReadByte()
if nextCharacter != '\n' {
return "", &clienterror.BadRequest{DisplayText: "message header line does not end in LF"}
}

trimmed := strings.TrimSuffix(maybeEndsInCR, "\r")
return trimmed, nil
}
96 changes: 96 additions & 0 deletions http/parser_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package http_test

import (
"bufio"
"bytes"

"github.com/kkrull/gohttp/http"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)

var _ = Describe("LineRequestParser", func() {
Describe("#Parse", func() {
var (
parser http.RequestParser
request *http.RequestLine
err http.Response
)

BeforeEach(func() {
parser = &http.LineRequestParser{}
})

Describe("it returns 400 Bad Request", func() {
It("for a completely blank request", func() {
request, err = parser.Parse(makeReader(""))
Expect(err).To(beABadRequestResponse("line in request header not ending in CRLF"))
})

It("for any line missing CR", func() {
request, err = parser.Parse(makeReader("GET / HTTP/1.1\r\n\n"))
Expect(err).To(beABadRequestResponse("line in request header not ending in CRLF"))
})

It("for any line missing LF", func() {
request, err = parser.Parse(makeReader("GET / HTTP/1.1\r"))
Expect(err).To(beABadRequestResponse("message header line does not end in LF"))
})

It("for a request missing a request-line", func() {
request, err = parser.Parse(makeReader("\r\n"))
Expect(err).To(beABadRequestResponse("incorrectly formatted or missing request-line"))
})

It("for a request missing an ending CRLF", func() {
request, err = parser.Parse(makeReader("GET / HTTP/1.1\r\n"))
Expect(err).To(beABadRequestResponse("line in request header not ending in CRLF"))
})

It("when multiple spaces are separating fields in request-line", func() {
request, err = parser.Parse(makeReader("GET / HTTP/1.1\r\n\r\n"))
Expect(err).To(beABadRequestResponse("incorrectly formatted or missing request-line"))
})

It("when fields in request-line contain spaces", func() {
request, err = parser.Parse(makeReader("GET /a\\ b HTTP/1.1\r\n\r\n"))
Expect(err).To(beABadRequestResponse("incorrectly formatted or missing request-line"))
})

It("when the request starts with whitespace", func() {
request, err = parser.Parse(makeReader(" GET / HTTP/1.1\r\n\r\n"))
Expect(err).To(beABadRequestResponse("incorrectly formatted or missing request-line"))
})
})

Context("given a well-formed request", func() {
var (
reader *bufio.Reader
)

BeforeEach(func() {
buffer := bytes.NewBufferString("GET /foo HTTP/1.1\r\nAccept: */*\r\n\r\n")
reader = bufio.NewReader(buffer)
request, err = parser.Parse(reader)
})

It("returns no error", func() {
Expect(err).To(BeNil())
})

It("reads the entire request until reaching a line with only CRLF", func() {
Expect(reader.Buffered()).To(Equal(0))
})
})

Context("given a well-formed request with query parameters", func() {
BeforeEach(func() {
request, err = parser.Parse(makeReader("GET /foo?one=1 HTTP/1.1\r\n\r\n"))
Expect(err).NotTo(HaveOccurred())
})

XIt("removes the query string from the target")
XIt("passes decoded parameters to the routes")
})
})
})
96 changes: 21 additions & 75 deletions http/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,19 @@ package http

import (
"bufio"
"strings"

"github.com/kkrull/gohttp/msg/clienterror"
"github.com/kkrull/gohttp/msg/servererror"
)

func NewRouter() *RequestLineRouter {
return &RequestLineRouter{
Parser: &LineRequestParser{},
}
}

// Routes requests based solely upon the first line in the request
type RequestLineRouter struct {
Parser RequestParser
routes []Route
}

Expand All @@ -22,98 +28,38 @@ func (router *RequestLineRouter) Routes() []Route {
return routes
}

func (router RequestLineRouter) ParseRequest(reader *bufio.Reader) (ok Request, routeError Response) {
request, err := router.parseRequestLine(reader)
if err != nil {
return nil, err
}

headerError := parseHeaderLines(reader)
if headerError != nil {
return nil, headerError
}

return request, nil
}

func (router RequestLineRouter) parseRequestLine(reader *bufio.Reader) (Request, Response) {
requestLineText, err := readCRLFLine(reader)
func (router RequestLineRouter) RouteRequest(reader *bufio.Reader) (ok Request, err Response) {
requested, err := router.Parser.Parse(reader)
if err != nil {
return nil, err
}

requested, err := parseRequestLine(requestLineText)
if err != nil {
return nil, err
}

if request := router.routeRequest(requested); request != nil {
return request, nil
}

return nil, requested.NotImplemented()
}

func parseHeaderLines(reader *bufio.Reader) Response {
isBlankLineBetweenHeadersAndBody := func(line string) bool { return line == "" }

for {
line, err := readCRLFLine(reader)
if err != nil {
return err
} else if isBlankLineBetweenHeadersAndBody(line) {
return nil
}
}
}

func readCRLFLine(reader *bufio.Reader) (string, Response) {
maybeEndsInCR, _ := reader.ReadString('\r')
if len(maybeEndsInCR) == 0 {
return "", &clienterror.BadRequest{DisplayText: "end of input before terminating CRLF"}
} else if !strings.HasSuffix(maybeEndsInCR, "\r") {
return "", &clienterror.BadRequest{DisplayText: "line in request header not ending in CRLF"}
}

nextCharacter, _ := reader.ReadByte()
if nextCharacter != '\n' {
return "", &clienterror.BadRequest{DisplayText: "message header line does not end in LF"}
}

trimmed := strings.TrimSuffix(maybeEndsInCR, "\r")
return trimmed, nil
return router.routeRequest(requested)
}

func parseRequestLine(text string) (*RequestLine, Response) {
fields := strings.Split(text, " ")
if len(fields) != 3 {
return nil, &clienterror.BadRequest{DisplayText: "incorrectly formatted or missing request-line"}
}

return &RequestLine{
Method: fields[0],
Target: fields[1],
}, nil
}

func (router RequestLineRouter) routeRequest(requested *RequestLine) Request {
func (router RequestLineRouter) routeRequest(requested *RequestLine) (ok Request, notImplemented Response) {
for _, route := range router.routes {
request := route.Route(requested)
if request != nil {
return request
return request, nil
}
}

return nil
return nil, requested.NotImplemented()
}

type RequestParser interface {
Parse(reader *bufio.Reader) (ok *RequestLine, err Response)
}

type Route interface {
Route(requested *RequestLine) Request
}

type RequestLine struct {
Method string
Target string
Method string
Target string
QueryParameters map[string]string
}

func (requestLine *RequestLine) NotImplemented() Response {
Expand Down
Loading

0 comments on commit cb322a3

Please sign in to comment.