Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
.DS_Store
/idea
12 changes: 7 additions & 5 deletions cmd/tcplistener/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
29 changes: 18 additions & 11 deletions internal/headers/headers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
Expand Down
102 changes: 66 additions & 36 deletions internal/request/request.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"io"
"strconv"
"strings"
)

Expand All @@ -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
}

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}
28 changes: 28 additions & 0 deletions internal/request/request_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}