aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorShulhan <ms@kilabit.info>2019-03-20 23:33:28 +0700
committerShulhan <ms@kilabit.info>2019-03-20 23:33:28 +0700
commit6740da2369bf6324ddc0f7303ea2dd4b008fbfe4 (patch)
tree265c648ad653cf4a1b53a820ed93e26d7fc8fc4e
parent9eda1ba207d2a603d383b3fe14ae7739847ddb2d (diff)
downloadpakakeh.go-6740da2369bf6324ddc0f7303ea2dd4b008fbfe4.tar.xz
http: add function to parse HTTP header only
In WebSocket handshake, we only need to parse the HTTP header only. Due to TCP buffering, a server can send an HTTP handshake response (with header only) and the WebSocket wire frame to client in single read. If we use standard http.ReadResponse, we can't get the rest of packet (the initial frame). To fix this issue we create our own HTTP parser, which limited to parsing header only without any validation.
-rw-r--r--lib/http/response.go128
-rw-r--r--lib/http/response_test.go186
2 files changed, 314 insertions, 0 deletions
diff --git a/lib/http/response.go b/lib/http/response.go
new file mode 100644
index 00000000..afd4ad77
--- /dev/null
+++ b/lib/http/response.go
@@ -0,0 +1,128 @@
+// Copyright 2019, Shulhan <ms@kilabit.info>. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package http
+
+import (
+ "bytes"
+ "fmt"
+ "net/http"
+ "strconv"
+
+ libio "github.com/shuLhan/share/lib/io"
+)
+
+//
+// ParseResponseHeader parse HTTP response header and return it as standard
+// HTTP Response with unreaded packet.
+//
+func ParseResponseHeader(raw []byte) (resp *http.Response, rest []byte, err error) {
+ if len(raw) == 0 {
+ return nil, nil, nil
+ }
+ // The minimum HTTP response without header is 16 bytes:
+ // "HTTP/X.X" SP 3DIGITS CRLF CRLF
+ if len(raw) < 16 {
+ return nil, raw, fmt.Errorf("http: invalid response header length")
+ }
+ // The HTTP-name is case sensitive: "HTTP".
+ if !bytes.Equal(raw[:4], []byte("HTTP")) {
+ return nil, raw, fmt.Errorf("http: invalid protocol name '%s'", raw[:4])
+ }
+ if raw[4] != '/' {
+ return nil, raw, fmt.Errorf("http: invalid protocol separator '%c'", raw[4])
+ }
+ if raw[6] != '.' {
+ return nil, raw, fmt.Errorf("http: invalid version separator '%c'", raw[6])
+ }
+ ilf := bytes.Index(raw, []byte{'\n'})
+ if ilf < 0 || raw[ilf-1] != '\r' {
+ return nil, raw, fmt.Errorf("http: missing CRLF on status line")
+ }
+
+ resp = &http.Response{
+ Proto: string(raw[:8]),
+ ProtoMajor: int(raw[5] - 48),
+ ProtoMinor: int(raw[7] - 48),
+ Status: string(raw[9 : ilf-1]),
+ }
+
+ if resp.ProtoMajor <= 0 || resp.ProtoMajor > 2 {
+ return nil, raw, fmt.Errorf("http: invalid major version '%c'", raw[5])
+ }
+ if resp.ProtoMinor < 0 || resp.ProtoMinor > 1 {
+ return nil, raw, fmt.Errorf("http: invalid minor version '%c'", raw[7])
+ }
+
+ resp.StatusCode, err = strconv.Atoi(string(raw[9:12]))
+ if err != nil {
+ return nil, raw, fmt.Errorf("http: status code: " + err.Error())
+ }
+ if resp.StatusCode < 100 || resp.StatusCode >= 600 {
+ return nil, raw, fmt.Errorf("http: invalid status code '%s'", raw[9:12])
+ }
+
+ rest = raw[ilf+1:]
+
+ resp.Header, rest, err = parseHeaders(rest)
+ if err != nil {
+ return nil, raw, err
+ }
+
+ return resp, rest, nil
+}
+
+func parseHeaders(raw []byte) (header http.Header, rest []byte, err error) {
+ spaces := []byte{'\r', '\n', '\v', '\t', ' '}
+ delims := []byte{'"', '(', ')', ',', '/', ':', ';', '<', '=', '>',
+ '?', '@', '[', '\\', ']', '{', '}'}
+ lf := []byte{'\n'}
+
+ rest = raw
+ reader := &libio.Reader{
+ V: raw,
+ }
+
+ header = make(http.Header)
+
+ // Loop until we found an empty line with CRLF.
+ for len(rest) > 0 {
+ switch len(rest) {
+ case 1:
+ return nil, rest, fmt.Errorf("http: missing CRLF at the end, found \"%s\"", rest)
+ default:
+ if rest[0] == '\r' && rest[1] == '\n' {
+ rest = rest[2:]
+ return header, rest, nil
+ }
+ }
+
+ // Get the field name.
+ tok, isTerm, c := reader.ReadUntil(spaces, delims)
+ if !isTerm || c != ':' {
+ return nil, nil, fmt.Errorf("http: missing field separator at line \"%s\"", rest)
+ }
+
+ key := string(tok)
+
+ tok, isTerm, _ = reader.ReadUntil(nil, lf)
+ if !isTerm {
+ return nil, nil, fmt.Errorf("http: missing CRLF at the end of field line")
+ }
+ if reader.V[reader.X-2] != '\r' {
+ return nil, nil, fmt.Errorf("http: missing CR at the end of line")
+ }
+
+ tok = bytes.TrimSpace(tok[:len(tok)-1])
+ if len(tok) == 0 {
+ return nil, nil, fmt.Errorf("http: key '%s' have empty value", key)
+ }
+
+ header.Add(key, string(tok))
+
+ rest = reader.V[reader.X:]
+ }
+
+ return header, rest, nil
+}
diff --git a/lib/http/response_test.go b/lib/http/response_test.go
new file mode 100644
index 00000000..ffd57388
--- /dev/null
+++ b/lib/http/response_test.go
@@ -0,0 +1,186 @@
+// Copyright 2019, Shulhan <ms@kilabit.info>. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package http
+
+import (
+ "net/http"
+ "testing"
+
+ "github.com/shuLhan/share/lib/test"
+)
+
+func TestParseResponseHeader(t *testing.T) {
+ cases := []struct {
+ desc string
+ raw []byte
+ expResp *http.Response
+ expRest []byte
+ expErr string
+ }{{
+ desc: "With empty input",
+ }, {
+ desc: "With invalid length",
+ raw: []byte("HTTP/1.1 101\r\n"),
+ expErr: "http: invalid response header length",
+ }, {
+ desc: "With lower case HTTP name",
+ raw: []byte("Http/1.1 101\r\n\r\n"),
+ expErr: "http: invalid protocol name 'Http'",
+ }, {
+ desc: "With invalid protocol separator",
+ raw: []byte("HTTP /1.1 101\r\n\r\n"),
+ expErr: "http: invalid protocol separator ' '",
+ }, {
+ desc: "With invalid version separator",
+ raw: []byte("HTTP/1 .1 101\r\n\r\n"),
+ expErr: "http: invalid version separator ' '",
+ }, {
+ desc: "With missing CR",
+ raw: []byte("HTTP/1.1 101\nHeader: Value\n"),
+ expErr: "http: missing CRLF on status line",
+ }, {
+ desc: "With invalid major version",
+ raw: []byte("HTTP/0.1 101\r\n\r\n"),
+ expErr: "http: invalid major version '0'",
+ }, {
+ desc: "With invalid major version (2)",
+ raw: []byte("HTTP/a.1 101\r\n\r\n"),
+ expErr: "http: invalid major version 'a'",
+ }, {
+ desc: "With invalid minor version",
+ raw: []byte("HTTP/1. 101\r\n\r\n"),
+ expErr: "http: invalid minor version ' '",
+ }, {
+ desc: "With invalid minor version (2)",
+ raw: []byte("HTTP/1.a 101\r\n\r\n"),
+ expErr: "http: invalid minor version 'a'",
+ }, {
+ desc: "With invalid status code",
+ raw: []byte("HTTP/1.1 999\r\n\r\n"),
+ expErr: "http: invalid status code '999'",
+ }, {
+ desc: "With invalid status code (2)",
+ raw: []byte("HTTP/1.1 10a\r\n\r\n"),
+ expErr: `http: status code: strconv.Atoi: parsing "10a": invalid syntax`,
+ }, {
+ desc: "Without CRLF",
+ raw: []byte("HTTP/1.1 101 Switching protocol\r\n\n"),
+ expErr: "http: missing CRLF at the end, found \"\n\"",
+ }, {
+ desc: "Without CRLF (2)",
+ raw: []byte("HTTP/1.1 101 Switching protocol\r\nFi"),
+ expErr: "http: missing field separator at line \"Fi\"",
+ }, {
+ desc: "With valid status line",
+ raw: []byte("HTTP/1.1 101 Switching protocol\r\n\r\n"),
+ expResp: &http.Response{
+ Status: "101 Switching protocol",
+ StatusCode: 101,
+ Proto: "HTTP/1.1",
+ ProtoMajor: 1,
+ ProtoMinor: 1,
+ Header: http.Header{},
+ },
+ expRest: []byte{},
+ }, {
+ desc: "With valid status line and a header",
+ raw: []byte("HTTP/1.1 101 Switching protocol\r\nKey: Value\r\n\r\n"),
+ expResp: &http.Response{
+ Status: "101 Switching protocol",
+ StatusCode: 101,
+ Proto: "HTTP/1.1",
+ ProtoMajor: 1,
+ ProtoMinor: 1,
+ Header: http.Header{
+ "Key": []string{
+ "Value",
+ },
+ },
+ },
+ expRest: []byte{},
+ }}
+
+ for _, c := range cases {
+ t.Log(c.desc)
+
+ got, rest, err := ParseResponseHeader(c.raw)
+ if err != nil {
+ test.Assert(t, "error", c.expErr, err.Error(), true)
+ continue
+ }
+
+ test.Assert(t, "http.Response", c.expResp, got, true)
+ test.Assert(t, "rest", c.expRest, rest, true)
+ }
+}
+
+func TestParseHeaders(t *testing.T) {
+ cases := []struct {
+ desc string
+ raw []byte
+ exp http.Header
+ expErr string
+ expRest []byte
+ }{{
+ desc: "With empty input",
+ exp: make(http.Header),
+ }, {
+ desc: "With single byte as input",
+ raw: []byte{'x'},
+ expErr: `http: missing CRLF at the end, found "x"`,
+ }, {
+ desc: "Without CRLF at the end",
+ raw: []byte("xx"),
+ expErr: `http: missing field separator at line "xx"`,
+ }, {
+ desc: "Without field separator",
+ raw: []byte("key value\r\n"),
+ expErr: "http: missing field separator at line \"key value\r\n\"",
+ }, {
+ desc: "Without line feed",
+ raw: []byte("key:value"),
+ expErr: "http: missing CRLF at the end of field line",
+ }, {
+ desc: "Without carriage return",
+ raw: []byte("key:value\n"),
+ expErr: "http: missing CR at the end of line",
+ }, {
+ desc: "Without field value",
+ raw: []byte("key:\r\n"),
+ expErr: "http: key 'key' have empty value",
+ }, {
+ desc: "With valid field",
+ raw: []byte("key:value\r\n\r\n"),
+ exp: http.Header{
+ "Key": []string{
+ "value",
+ },
+ },
+ expRest: []byte{},
+ }, {
+ desc: "With valid field (2)",
+ raw: []byte("key:value\r\nkey: another value \r\n\r\nbody"),
+ exp: http.Header{
+ "Key": []string{
+ "value",
+ "another value",
+ },
+ },
+ expRest: []byte("body"),
+ }}
+
+ for _, c := range cases {
+ t.Log(c.desc)
+
+ header, rest, err := parseHeaders(c.raw)
+ if err != nil {
+ test.Assert(t, "error", c.expErr, err.Error(), true)
+ continue
+ }
+
+ test.Assert(t, "header", c.exp, header, true)
+ test.Assert(t, "rest", c.expRest, rest, true)
+ }
+}