Skip to content

Commit 11f62bc

Browse files
committed
can parse curls over tcp
1 parent 2fbb65f commit 11f62bc

5 files changed

Lines changed: 113 additions & 52 deletions

File tree

.gitignore

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1 @@
1-
.DS_Store
21
/idea

cmd/tcplistener/main.go

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,16 @@ func main() {
2828
fmt.Println("error happened", err.Error())
2929
}
3030

31-
fmt.Println("Request line:")
32-
fmt.Println("Method: ", req.RequestLine.Method)
33-
fmt.Println("Http Version: ", req.RequestLine.HttpVersion)
34-
fmt.Println("Target: ", req.RequestLine.RequestTarget)
31+
fmt.Println("Request line")
32+
fmt.Println("- Method: ", req.RequestLine.Method)
33+
fmt.Println("- Http Version: ", req.RequestLine.HttpVersion)
34+
fmt.Println("- Target: ", req.RequestLine.RequestTarget)
3535
fmt.Println("Headers")
3636
for key, val := range req.Headers {
37-
fmt.Println(key, ": ", val)
37+
fmt.Println("- ", key, ": ", val)
3838
}
39+
fmt.Println("Body")
40+
fmt.Println(string(req.Body))
3941
}(conn)
4042
}
4143
}

internal/headers/headers.go

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,19 @@ const crlf = "\r\n"
1111

1212
type Headers map[string]string
1313

14-
// there can be an unlimited amount of whitespace
15-
// before and after the field-value (Header value). However, when parsing a field-name,
16-
// there must be no spaces betwixt the colon and the field-name. In other words,
17-
// these are valid:
18-
19-
// 'Host: localhost:42069'
20-
// ' Host: localhost:42069 '
21-
14+
// Parse parses HTTP header field lines from the given data.
15+
// There can be an unlimited amount of whitespace before and after the field-value (Header value).
16+
// However, when parsing a field-name, there must be no spaces between the colon and the field-name.
17+
// In other words, these are valid:
18+
//
19+
// 'Host: localhost:42069'
20+
// ' Host: localhost:42069 '
21+
//
2222
// But this is not:
23-
24-
// Host : localhost:42069
25-
23+
//
24+
// 'Host : localhost:42069'
25+
//
26+
// - Returns how many bytes consumed, whether parsing is done, and any error encountered.
2627
func (h Headers) Parse(data []byte) (n int, done bool, err error) {
2728
endIdx := strings.Index(string(data), crlf)
2829
if endIdx == -1 {
@@ -60,6 +61,12 @@ func (h Headers) Parse(data []byte) (n int, done bool, err error) {
6061
return endIdx + 2, false, nil
6162
}
6263

64+
func (h Headers) GET(key string) string {
65+
return h[strings.ToLower(key)]
66+
}
67+
68+
// isValidFieldName checks if the given field name contains only valid characters
69+
// according to HTTP specification.
6370
func isValidFieldName(fieldName string) bool {
6471
allowedSpecials := map[rune]bool{
6572
'!': true, '#': true, '$': true, '%': true, '&': true, '\'': true,

internal/request/request.go

Lines changed: 60 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"errors"
66
"fmt"
77
"io"
8+
"strconv"
89
"strings"
910
)
1011

@@ -15,13 +16,15 @@ type RequestState int
1516

1617
const (
1718
RequestStateInitialized RequestState = iota
18-
RequestStateDone
1919
RequestStateParsingHeaders
20+
RequestParsingBody
21+
RequestStateDone
2022
)
2123

2224
type Request struct {
2325
RequestLine RequestLine
2426
Headers headers.Headers
27+
Body []byte
2528
State RequestState
2629
}
2730

@@ -83,39 +86,6 @@ func RequestFromReader(reader io.Reader) (*Request, error) {
8386
return req, nil
8487
}
8588

86-
func parseRequestLine(data string) (*RequestLine, int, error) {
87-
// Look for the CRLF that marks the end of the request line
88-
endIdx := strings.Index(data, crlf)
89-
if endIdx == -1 {
90-
// Not enough data yet; no CRLF found
91-
return nil, 0, nil
92-
}
93-
94-
// Extract the request line (without the trailing CRLF)
95-
reqLine := data[:endIdx]
96-
97-
parts := strings.Split(reqLine, " ")
98-
if len(parts) != 3 {
99-
return nil, endIdx + 2, errors.New("invalid number of parts in request line")
100-
}
101-
// "method" part only contains capital alphabetic characters.
102-
if strings.ToUpper(parts[0]) != parts[0] {
103-
return nil, endIdx + 2, errors.New("http method is not capitalized")
104-
}
105-
106-
httpVersion := strings.Replace(parts[2], "HTTP/", "", 1)
107-
108-
if httpVersion != "1.1" {
109-
return nil, endIdx + 2, errors.New("http/1.1 only supported")
110-
}
111-
112-
return &RequestLine{
113-
Method: parts[0],
114-
HttpVersion: httpVersion,
115-
RequestTarget: parts[1],
116-
}, endIdx + 2, nil
117-
}
118-
11989
func (r *Request) parse(data []byte) (int, error) {
12090
totalBytesParsed := 0
12191
for r.State != RequestStateDone {
@@ -156,10 +126,65 @@ func (r *Request) parseSingle(data []byte) (int, error) {
156126
return 0, err
157127
}
158128
if done {
159-
r.State = RequestStateDone
129+
r.State = RequestParsingBody
160130
}
161131
return numberOfBytes, nil
162132
}
163133

134+
if r.State == RequestParsingBody {
135+
contentLen := r.Headers.GET("Content-Length")
136+
// content-length not present in headers no body
137+
if len(contentLen) == 0 {
138+
r.State = RequestStateDone
139+
return 0, nil
140+
}
141+
contentLenInt, err := strconv.Atoi(contentLen)
142+
if err != nil {
143+
return 0, errors.New("content-length doesn't convert to int")
144+
}
145+
if len(data) > contentLenInt {
146+
return 0, errors.New("body is larger than the content-length")
147+
}
148+
if len(data) == contentLenInt {
149+
r.Body = append(r.Body, data...)
150+
r.State = RequestStateDone
151+
return len(data), nil
152+
}
153+
return 0, nil
154+
}
155+
164156
return 0, errors.New("unknown request status")
165157
}
158+
159+
func parseRequestLine(data string) (*RequestLine, int, error) {
160+
// Look for the CRLF that marks the end of the request line
161+
endIdx := strings.Index(data, crlf)
162+
if endIdx == -1 {
163+
// Not enough data yet; no CRLF found
164+
return nil, 0, nil
165+
}
166+
167+
// Extract the request line (without the trailing CRLF)
168+
reqLine := data[:endIdx]
169+
170+
parts := strings.Split(reqLine, " ")
171+
if len(parts) != 3 {
172+
return nil, endIdx + 2, errors.New("invalid number of parts in request line")
173+
}
174+
// "method" part only contains capital alphabetic characters.
175+
if strings.ToUpper(parts[0]) != parts[0] {
176+
return nil, endIdx + 2, errors.New("http method is not capitalized")
177+
}
178+
179+
httpVersion := strings.Replace(parts[2], "HTTP/", "", 1)
180+
181+
if httpVersion != "1.1" {
182+
return nil, endIdx + 2, errors.New("http/1.1 only supported")
183+
}
184+
185+
return &RequestLine{
186+
Method: parts[0],
187+
HttpVersion: httpVersion,
188+
RequestTarget: parts[1],
189+
}, endIdx + 2, nil
190+
}

internal/request/request_test.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,3 +89,31 @@ func TestHeadersLineParser(t *testing.T) {
8989
_, err = RequestFromReader(reader)
9090
require.Error(t, err)
9191
}
92+
93+
func TestBodyParser(t *testing.T) {
94+
// Test: Standard Body
95+
reader := &chunkReader{
96+
data: "POST /submit HTTP/1.1\r\n" +
97+
"Host: localhost:42069\r\n" +
98+
"Content-Length: 13\r\n" +
99+
"\r\n" +
100+
"hello world!\n",
101+
numBytesPerRead: 3,
102+
}
103+
r, err := RequestFromReader(reader)
104+
require.NoError(t, err)
105+
require.NotNil(t, r)
106+
assert.Equal(t, "hello world!\n", string(r.Body))
107+
108+
// Test: Body shorter than reported content length
109+
reader = &chunkReader{
110+
data: "POST /submit HTTP/1.1\r\n" +
111+
"Host: localhost:42069\r\n" +
112+
"Content-Length: 20\r\n" +
113+
"\r\n" +
114+
"partial content",
115+
numBytesPerRead: 3,
116+
}
117+
_, err = RequestFromReader(reader)
118+
require.Error(t, err)
119+
}

0 commit comments

Comments
 (0)