diff --git a/.gitignore b/.gitignore index 34e5170..ac79464 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1 @@ -.DS_Store /idea \ No newline at end of file diff --git a/cmd/tcplistener/main.go b/cmd/tcplistener/main.go index 1381913..d1b0f70 100644 --- a/cmd/tcplistener/main.go +++ b/cmd/tcplistener/main.go @@ -28,14 +28,16 @@ func main() { fmt.Println("error happened", err.Error()) } - fmt.Println("Request line:") - fmt.Println("Method: ", req.RequestLine.Method) - fmt.Println("Http Version: ", req.RequestLine.HttpVersion) - fmt.Println("Target: ", req.RequestLine.RequestTarget) + fmt.Println("Request line") + fmt.Println("- Method: ", req.RequestLine.Method) + fmt.Println("- Http Version: ", req.RequestLine.HttpVersion) + fmt.Println("- Target: ", req.RequestLine.RequestTarget) fmt.Println("Headers") for key, val := range req.Headers { - fmt.Println(key, ": ", val) + fmt.Println("- ", key, ": ", val) } + fmt.Println("Body") + fmt.Println(string(req.Body)) }(conn) } } diff --git a/internal/headers/headers.go b/internal/headers/headers.go index 6ada13c..67aa1f7 100644 --- a/internal/headers/headers.go +++ b/internal/headers/headers.go @@ -11,18 +11,19 @@ const crlf = "\r\n" type Headers map[string]string -// there can be an unlimited amount of whitespace -// before and after the field-value (Header value). However, when parsing a field-name, -// there must be no spaces betwixt the colon and the field-name. In other words, -// these are valid: - -// 'Host: localhost:42069' -// ' Host: localhost:42069 ' - +// Parse parses HTTP header field lines from the given data. +// There can be an unlimited amount of whitespace before and after the field-value (Header value). +// However, when parsing a field-name, there must be no spaces between the colon and the field-name. +// In other words, these are valid: +// +// 'Host: localhost:42069' +// ' Host: localhost:42069 ' +// // But this is not: - -// Host : localhost:42069 - +// +// 'Host : localhost:42069' +// +// - Returns how many bytes consumed, whether parsing is done, and any error encountered. func (h Headers) Parse(data []byte) (n int, done bool, err error) { endIdx := strings.Index(string(data), crlf) if endIdx == -1 { @@ -60,6 +61,12 @@ func (h Headers) Parse(data []byte) (n int, done bool, err error) { return endIdx + 2, false, nil } +func (h Headers) GET(key string) string { + return h[strings.ToLower(key)] +} + +// isValidFieldName checks if the given field name contains only valid characters +// according to HTTP specification. func isValidFieldName(fieldName string) bool { allowedSpecials := map[rune]bool{ '!': true, '#': true, '$': true, '%': true, '&': true, '\'': true, diff --git a/internal/request/request.go b/internal/request/request.go index 17b0458..54351a0 100644 --- a/internal/request/request.go +++ b/internal/request/request.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "io" + "strconv" "strings" ) @@ -15,13 +16,15 @@ type RequestState int const ( RequestStateInitialized RequestState = iota - RequestStateDone RequestStateParsingHeaders + RequestParsingBody + RequestStateDone ) type Request struct { RequestLine RequestLine Headers headers.Headers + Body []byte State RequestState } @@ -53,7 +56,12 @@ func RequestFromReader(reader io.Reader) (*Request, error) { if err != nil { if errors.Is(err, io.EOF) { - req.State = RequestStateDone + // if the EOF is reached but req isn't done state + // then partial content + if req.State != RequestStateDone { + req.State = RequestStateDone + return req, errors.New("partial content") + } break } return nil, err @@ -83,39 +91,6 @@ func RequestFromReader(reader io.Reader) (*Request, error) { return req, nil } -func parseRequestLine(data string) (*RequestLine, int, error) { - // Look for the CRLF that marks the end of the request line - endIdx := strings.Index(data, crlf) - if endIdx == -1 { - // Not enough data yet; no CRLF found - return nil, 0, nil - } - - // Extract the request line (without the trailing CRLF) - reqLine := data[:endIdx] - - parts := strings.Split(reqLine, " ") - if len(parts) != 3 { - return nil, endIdx + 2, errors.New("invalid number of parts in request line") - } - // "method" part only contains capital alphabetic characters. - if strings.ToUpper(parts[0]) != parts[0] { - return nil, endIdx + 2, errors.New("http method is not capitalized") - } - - httpVersion := strings.Replace(parts[2], "HTTP/", "", 1) - - if httpVersion != "1.1" { - return nil, endIdx + 2, errors.New("http/1.1 only supported") - } - - return &RequestLine{ - Method: parts[0], - HttpVersion: httpVersion, - RequestTarget: parts[1], - }, endIdx + 2, nil -} - func (r *Request) parse(data []byte) (int, error) { totalBytesParsed := 0 for r.State != RequestStateDone { @@ -156,10 +131,65 @@ func (r *Request) parseSingle(data []byte) (int, error) { return 0, err } if done { - r.State = RequestStateDone + r.State = RequestParsingBody } return numberOfBytes, nil } + if r.State == RequestParsingBody { + contentLen := r.Headers.GET("Content-Length") + // content-length not present in headers no body + if len(contentLen) == 0 { + r.State = RequestStateDone + return 0, nil + } + contentLenInt, err := strconv.Atoi(contentLen) + if err != nil { + return 0, errors.New("content-length doesn't convert to int") + } + if len(data) > contentLenInt { + return 0, errors.New("body is larger than the content-length") + } + if len(data) == contentLenInt { + r.Body = append(r.Body, data...) + r.State = RequestStateDone + return len(data), nil + } + return 0, nil + } + return 0, errors.New("unknown request status") } + +func parseRequestLine(data string) (*RequestLine, int, error) { + // Look for the CRLF that marks the end of the request line + endIdx := strings.Index(data, crlf) + if endIdx == -1 { + // Not enough data yet; no CRLF found + return nil, 0, nil + } + + // Extract the request line (without the trailing CRLF) + reqLine := data[:endIdx] + + parts := strings.Split(reqLine, " ") + if len(parts) != 3 { + return nil, endIdx + 2, errors.New("invalid number of parts in request line") + } + // "method" part only contains capital alphabetic characters. + if strings.ToUpper(parts[0]) != parts[0] { + return nil, endIdx + 2, errors.New("http method is not capitalized") + } + + httpVersion := strings.Replace(parts[2], "HTTP/", "", 1) + + if httpVersion != "1.1" { + return nil, endIdx + 2, errors.New("http/1.1 only supported") + } + + return &RequestLine{ + Method: parts[0], + HttpVersion: httpVersion, + RequestTarget: parts[1], + }, endIdx + 2, nil +} diff --git a/internal/request/request_test.go b/internal/request/request_test.go index 766dd87..d364843 100644 --- a/internal/request/request_test.go +++ b/internal/request/request_test.go @@ -89,3 +89,31 @@ func TestHeadersLineParser(t *testing.T) { _, err = RequestFromReader(reader) require.Error(t, err) } + +func TestBodyParser(t *testing.T) { + // Test: Standard Body + reader := &chunkReader{ + data: "POST /submit HTTP/1.1\r\n" + + "Host: localhost:42069\r\n" + + "Content-Length: 13\r\n" + + "\r\n" + + "hello world!\n", + numBytesPerRead: 3, + } + r, err := RequestFromReader(reader) + require.NoError(t, err) + require.NotNil(t, r) + assert.Equal(t, "hello world!\n", string(r.Body)) + + // Test: Body shorter than reported content length + reader = &chunkReader{ + data: "POST /submit HTTP/1.1\r\n" + + "Host: localhost:42069\r\n" + + "Content-Length: 20\r\n" + + "\r\n" + + "partial content", + numBytesPerRead: 3, + } + _, err = RequestFromReader(reader) + require.Error(t, err) +}