Skip to content

Commit 60a5c48

Browse files
authored
Merge pull request #3 from arkahood/tcp-parser
can parse curls over tcp
2 parents 2fbb65f + d5d87f6 commit 60a5c48

5 files changed

Lines changed: 119 additions & 53 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: 66 additions & 36 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

@@ -53,7 +56,12 @@ func RequestFromReader(reader io.Reader) (*Request, error) {
5356

5457
if err != nil {
5558
if errors.Is(err, io.EOF) {
56-
req.State = RequestStateDone
59+
// if the EOF is reached but req isn't done state
60+
// then partial content
61+
if req.State != RequestStateDone {
62+
req.State = RequestStateDone
63+
return req, errors.New("partial content")
64+
}
5765
break
5866
}
5967
return nil, err
@@ -83,39 +91,6 @@ func RequestFromReader(reader io.Reader) (*Request, error) {
8391
return req, nil
8492
}
8593

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-
11994
func (r *Request) parse(data []byte) (int, error) {
12095
totalBytesParsed := 0
12196
for r.State != RequestStateDone {
@@ -156,10 +131,65 @@ func (r *Request) parseSingle(data []byte) (int, error) {
156131
return 0, err
157132
}
158133
if done {
159-
r.State = RequestStateDone
134+
r.State = RequestParsingBody
160135
}
161136
return numberOfBytes, nil
162137
}
163138

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

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)