├── examples ├── reverse │ └── reverse.go └── trivial │ └── trivial.go ├── README.md ├── doc.go ├── LICENSE ├── bufio.go ├── reverse_proxy.go ├── response.go ├── serve_http.go ├── server.go ├── http_test.go ├── status.go └── http.go /examples/reverse/reverse.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/evanphx/wildcat" 7 | ) 8 | 9 | type Env int 10 | 11 | func (_ Env) Redirect(hp *wildcat.HTTPParser) (string, string, error) { 12 | return "tcp", os.Getenv("REDIRECT"), nil 13 | } 14 | 15 | func main() { 16 | var e Env 17 | 18 | wildcat.ListenAndServe(":9594", wildcat.NewReverseProxy(e)) 19 | 20 | // h := http.HandlerFunc(static) 21 | 22 | // http.ListenAndServe(":9594", h) 23 | 24 | // wildcat.ListenAndServe(":9594", wildcat.AdaptServeHTTP(h)) 25 | 26 | } 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | wildcat 2 | ======= 3 | 4 | [![GoDoc](https://godoc.org/github.com/evanphx/wildcat?status.svg)](https://godoc.org/github.com/evanphx/wildcat) 5 | 6 | A high performance golang HTTP parser. 7 | 8 | Baseline benchmarking results: 9 | 10 | ``` 11 | zero :: evanphx/wildcat> go test -bench . -benchmem 12 | PASS 13 | BenchmarkParseSimple 50000000 44.4 ns/op 0 B/op 0 allocs/op 14 | BenchmarkNetHTTP 500000 4608 ns/op 4627 B/op 7 allocs/op 15 | BenchmarkParseSimpleHeaders 20000000 106 ns/op 0 B/op 0 allocs/op 16 | BenchmarkParseSimple3Headers 10000000 213 ns/op 0 B/op 0 allocs/op 17 | BenchmarkNetHTTP3 500000 6733 ns/op 5064 B/op 11 allocs/op 18 | ok github.com/evanphx/wildcat 12.665s 19 | 20 | ``` 21 | 22 | NOTE: these are a bit of lie because wildcat doesn't yet do everything that net/http.ReadRequest does. 23 | The numbers are included here only to provide a baseline comparison for future work. 24 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Wildcat provides a HTTP parser that performs zero allocations, in scans a buffer 4 | and tracks slices into the buffer to provide information about the request. 5 | 6 | As a result, it is also quite fast. `go test -bench .` to verify for yourself. 7 | 8 | A couple of cases where it wil perform an allocation: 9 | 10 | 1. When the parser is created, it makes enough space to track 10 headers. If 11 | there end up being more than 10, this is resized up to 20, then 30, etc. 12 | 13 | This can be overriden by using NewSizedHTTPParser to pass in the initial size 14 | to use, eliminated allocations for your use case (you could set this to 1000 15 | for instance). 16 | 17 | 2. If a mime-style multiline header is encountered, wildcat will make a new 18 | buffer to contain the concatination of values. 19 | 20 | NOTE: FindHeader only returns the first header that matches the requested name. 21 | If a request contains multiple values for the same header, use FindAllHeaders. 22 | 23 | */ 24 | package wildcat 25 | -------------------------------------------------------------------------------- /examples/trivial/trivial.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "net" 7 | "net/http" 8 | 9 | "github.com/evanphx/wildcat" 10 | ) 11 | 12 | type X struct{} 13 | 14 | func (x *X) HandleConnection(hp *wildcat.HTTPParser, rest []byte, c net.Conn) { 15 | cl := hp.ContentLength() 16 | 17 | fmt.Printf("host: %s\n", string(hp.Host())) 18 | fmt.Printf("content: %d\n", cl) 19 | 20 | if hp.Post() { 21 | body, err := ioutil.ReadAll(hp.BodyReader(rest, c)) 22 | if err != nil { 23 | panic(err) 24 | } 25 | 26 | fmt.Printf("body: %s\n", body) 27 | } 28 | 29 | resp := wildcat.NewResponse(c) 30 | resp.AddStringHeader("X-Runtime", "8311323") 31 | 32 | resp.WriteStatus(200) 33 | resp.WriteHeaders() 34 | resp.WriteBodyBytes([]byte("hello world\n")) 35 | } 36 | 37 | func static(w http.ResponseWriter, req *http.Request) { 38 | w.Write([]byte("hello world\n")) 39 | } 40 | 41 | func main() { 42 | wildcat.ListenAndServe(":9594", &X{}) 43 | 44 | // h := http.HandlerFunc(static) 45 | 46 | // http.ListenAndServe(":9594", h) 47 | 48 | // wildcat.ListenAndServe(":9594", wildcat.AdaptServeHTTP(h)) 49 | 50 | } 51 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014, Evan Phoenix 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 15 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 18 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 21 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 22 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | 25 | -------------------------------------------------------------------------------- /bufio.go: -------------------------------------------------------------------------------- 1 | package wildcat 2 | 3 | import "io" 4 | 5 | type sizedBodyReader struct { 6 | size int64 7 | rest []byte 8 | c io.ReadCloser 9 | } 10 | 11 | func (br *sizedBodyReader) Read(buf []byte) (int, error) { 12 | if br.size == 0 { 13 | return 0, io.EOF 14 | } 15 | 16 | if br.rest != nil { 17 | if len(buf) < len(br.rest) { 18 | copy(buf, br.rest[:len(buf)]) 19 | 20 | br.rest = br.rest[len(buf):] 21 | br.size -= int64(len(buf)) 22 | return len(buf), nil 23 | } else { 24 | l := len(br.rest) 25 | copy(buf, br.rest) 26 | 27 | br.rest = nil 28 | br.size -= int64(l) 29 | return l, nil 30 | } 31 | } 32 | 33 | n, err := br.c.Read(buf[:br.size]) 34 | if err != nil { 35 | return 0, err 36 | } 37 | 38 | br.size -= int64(n) 39 | return n, nil 40 | } 41 | 42 | func (br *sizedBodyReader) Close() error { 43 | return br.c.Close() 44 | } 45 | 46 | type unsizedBodyReader struct { 47 | rest []byte 48 | c io.ReadCloser 49 | } 50 | 51 | func (br *unsizedBodyReader) Read(buf []byte) (int, error) { 52 | if br.rest != nil { 53 | if len(buf) < len(br.rest) { 54 | copy(buf, br.rest[:len(buf)]) 55 | 56 | br.rest = br.rest[len(buf):] 57 | return len(buf), nil 58 | } else { 59 | l := len(br.rest) 60 | copy(buf, br.rest) 61 | 62 | br.rest = nil 63 | return l, nil 64 | } 65 | } 66 | 67 | return br.c.Read(buf) 68 | } 69 | 70 | func (br *unsizedBodyReader) Close() error { 71 | return br.c.Close() 72 | } 73 | 74 | func BodyReader(size int64, rest []byte, c io.ReadCloser) io.ReadCloser { 75 | switch size { 76 | case 0: 77 | return nil 78 | case -1: 79 | return &unsizedBodyReader{rest, c} 80 | default: 81 | return &sizedBodyReader{size, rest, c} 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /reverse_proxy.go: -------------------------------------------------------------------------------- 1 | package wildcat 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "net" 7 | ) 8 | 9 | type Redirector interface { 10 | Redirect(hp *HTTPParser) (string, string, error) 11 | } 12 | 13 | type ReverseProxy struct { 14 | dir Redirector 15 | } 16 | 17 | func NewReverseProxy(dir Redirector) *ReverseProxy { 18 | return &ReverseProxy{dir} 19 | } 20 | 21 | func (r *ReverseProxy) HandleConnection(hp *HTTPParser, rest []byte, c net.Conn) { 22 | proto, where, err := r.dir.Redirect(hp) 23 | 24 | if err != nil { 25 | r.writeError(c, err) 26 | return 27 | } 28 | 29 | out, err := net.Dial(proto, where) 30 | if err != nil { 31 | r.writeError(c, err) 32 | return 33 | } 34 | 35 | err = r.writeHeader(hp, out) 36 | if err != nil { 37 | r.writeError(c, err) 38 | return 39 | } 40 | 41 | _, err = c.Write(rest) 42 | if err != nil { 43 | return 44 | } 45 | 46 | go io.Copy(c, out) 47 | 48 | io.Copy(out, c) 49 | } 50 | 51 | var ( 52 | cError = []byte("HTTP/1.0 500 Server Error\r\nContent-Length: 0\r\n\r\n") 53 | cSP = []byte(" ") 54 | cHTTP11 = []byte("HTTP/1.1") 55 | ) 56 | 57 | func (r *ReverseProxy) writeError(c net.Conn, err error) { 58 | c.Write(cError) 59 | } 60 | 61 | func (r *ReverseProxy) writeHeader(hp *HTTPParser, c net.Conn) error { 62 | var buf bytes.Buffer 63 | 64 | buf.Write(hp.Method) 65 | buf.Write(cSP) 66 | buf.Write(hp.Path) 67 | buf.Write(cSP) 68 | buf.Write(cHTTP11) 69 | buf.Write(cCRLF) 70 | 71 | for _, h := range hp.Headers { 72 | if h.Name == nil { 73 | continue 74 | } 75 | 76 | buf.Write(h.Name) 77 | buf.Write(cColon) 78 | buf.Write(h.Value) 79 | buf.Write(cCRLF) 80 | } 81 | 82 | buf.Write(cCRLF) 83 | 84 | _, err := c.Write(buf.Bytes()) 85 | 86 | return err 87 | } 88 | -------------------------------------------------------------------------------- /response.go: -------------------------------------------------------------------------------- 1 | package wildcat 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "net" 8 | ) 9 | 10 | type Response struct { 11 | c net.Conn 12 | 13 | headers []header 14 | numHeaders int 15 | } 16 | 17 | func NewResponse(c net.Conn) *Response { 18 | return &Response{ 19 | c: c, 20 | headers: make([]header, 10), 21 | numHeaders: 0, 22 | } 23 | } 24 | 25 | func (r *Response) AddHeader(key, val []byte) { 26 | if r.numHeaders == len(r.headers) { 27 | newHeaders := make([]header, r.numHeaders+10) 28 | copy(newHeaders, r.headers) 29 | r.headers = newHeaders 30 | } 31 | 32 | r.headers[r.numHeaders] = header{key, val} 33 | r.numHeaders++ 34 | } 35 | 36 | func (r *Response) AddStringHeader(key, val string) { 37 | r.AddHeader([]byte(key), []byte(val)) 38 | } 39 | 40 | func (r *Response) WriteStatus(code int) { 41 | status := fmt.Sprintf("HTTP/1.1 %d %s\r\n", code, statusText[code]) 42 | r.c.Write([]byte(status)) 43 | } 44 | 45 | var ( 46 | cColon = []byte(": ") 47 | cCRLF = []byte("\r\n") 48 | cConnClose = []byte("Connection: close\r\n") 49 | ) 50 | 51 | func (r *Response) WriteHeaders() { 52 | var buf bytes.Buffer 53 | 54 | for i := 0; i < r.numHeaders; i++ { 55 | h := &r.headers[i] 56 | buf.Write(h.Name) 57 | buf.Write(cColon) 58 | buf.Write(h.Value) 59 | buf.Write(cCRLF) 60 | } 61 | 62 | r.c.Write(buf.Bytes()) 63 | } 64 | 65 | func (r *Response) WriteBodyBytes(body []byte) { 66 | r.c.Write([]byte(fmt.Sprintf("Content-Length: %d\r\n\r\n", len(body)))) 67 | r.c.Write(body) 68 | } 69 | 70 | func (r *Response) WriteBodyString(body string) { 71 | r.WriteBodyBytes([]byte(body)) 72 | } 73 | 74 | func (r *Response) WriteBodySizedStream(size int, reader io.Reader) { 75 | r.c.Write([]byte(fmt.Sprintf("Content-Length: %d\r\n\r\n", size))) 76 | io.Copy(r.c, reader) 77 | } 78 | 79 | func (r *Response) WriteBodyStream(size int, reader io.Reader) { 80 | r.c.Write(cConnClose) 81 | io.Copy(r.c, reader) 82 | } 83 | -------------------------------------------------------------------------------- /serve_http.go: -------------------------------------------------------------------------------- 1 | package wildcat 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "fmt" 7 | "log" 8 | "net" 9 | "net/http" 10 | "net/url" 11 | "strings" 12 | ) 13 | 14 | type adaptServeHTTP struct { 15 | h http.Handler 16 | } 17 | 18 | func AdaptServeHTTP(h http.Handler) Handler { 19 | return &adaptServeHTTP{h} 20 | } 21 | 22 | func (a *adaptServeHTTP) convertHeader(hp *HTTPParser) http.Header { 23 | header := make(http.Header) 24 | 25 | for _, h := range hp.Headers { 26 | if h.Name == nil { 27 | continue 28 | } 29 | 30 | header[string(h.Name)] = append(header[string(h.Name)], string(h.Value)) 31 | } 32 | 33 | return header 34 | } 35 | 36 | func (a *adaptServeHTTP) HandleConnection(hp *HTTPParser, rest []byte, c net.Conn) { 37 | u, err := url.Parse(fmt.Sprintf("http://%s/%s", string(hp.Host()), string(hp.Path))) 38 | if err != nil { 39 | log.Fatal(err) 40 | return 41 | } 42 | 43 | var protoMajor int 44 | var protoMinor int 45 | 46 | switch string(hp.Version) { 47 | case "HTTP/0.9": 48 | protoMinor = 9 49 | case "HTTP/1.0": 50 | protoMajor = 1 51 | case "HTTP/1.1": 52 | protoMajor = 1 53 | protoMinor = 1 54 | } 55 | 56 | req := http.Request{ 57 | Method: string(hp.Method), 58 | URL: u, 59 | Proto: string(hp.Version), 60 | ProtoMajor: protoMajor, 61 | ProtoMinor: protoMinor, 62 | Header: a.convertHeader(hp), 63 | Body: hp.BodyReader(rest, c), 64 | ContentLength: hp.ContentLength(), 65 | Host: string(hp.Host()), 66 | RequestURI: string(hp.Path), 67 | RemoteAddr: c.RemoteAddr().String(), 68 | } 69 | 70 | w := &responseWriter{c: c} 71 | w.init() 72 | 73 | a.h.ServeHTTP(w, &req) 74 | 75 | c.Close() 76 | } 77 | 78 | type responseWriter struct { 79 | c net.Conn 80 | 81 | code int 82 | header http.Header 83 | 84 | wroteHeader bool 85 | } 86 | 87 | func (r *responseWriter) init() { 88 | r.code = 200 89 | r.header = make(http.Header) 90 | } 91 | 92 | func (r *responseWriter) Header() http.Header { 93 | return r.header 94 | } 95 | 96 | func (r *responseWriter) WriteHeader(code int) { 97 | if r.wroteHeader { 98 | return 99 | } 100 | 101 | var buf bytes.Buffer 102 | 103 | for k, v := range r.header { 104 | buf.WriteString(k) 105 | buf.WriteString(": ") 106 | 107 | if len(v) == 1 { 108 | buf.WriteString(v[0]) 109 | } else { 110 | buf.WriteString(strings.Join(v, ", ")) 111 | } 112 | 113 | buf.WriteString("\r\n") 114 | } 115 | 116 | r.c.Write(buf.Bytes()) 117 | 118 | r.wroteHeader = true 119 | } 120 | 121 | func (r *responseWriter) Write(buf []byte) (int, error) { 122 | r.WriteHeader(r.code) 123 | return r.c.Write(buf) 124 | } 125 | 126 | func (r *responseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { 127 | return r.c, bufio.NewReadWriter(bufio.NewReader(r.c), bufio.NewWriter(r.c)), nil 128 | } 129 | -------------------------------------------------------------------------------- /server.go: -------------------------------------------------------------------------------- 1 | package wildcat 2 | 3 | import ( 4 | "crypto/tls" 5 | "net" 6 | "time" 7 | ) 8 | 9 | type Handler interface { 10 | HandleConnection(parser *HTTPParser, rest []byte, c net.Conn) 11 | } 12 | 13 | type Server struct { 14 | Handler Handler 15 | } 16 | 17 | func (s *Server) ListenAndServe(addr string) error { 18 | l, err := net.Listen("tcp", ":9594") 19 | if err != nil { 20 | return err 21 | } 22 | 23 | return s.Serve(l) 24 | } 25 | 26 | func ListenAndServe(addr string, handler Handler) error { 27 | s := &Server{handler} 28 | 29 | return s.ListenAndServe(addr) 30 | } 31 | 32 | func (s *Server) Serve(l net.Listener) error { 33 | for { 34 | conn, err := l.Accept() 35 | if err != nil { 36 | return err 37 | } 38 | 39 | go s.handle(conn) 40 | } 41 | } 42 | 43 | func (s *Server) handle(c net.Conn) { 44 | buf := make([]byte, OptimalBufferSize) 45 | 46 | hp := NewHTTPParser() 47 | 48 | for { 49 | n, err := c.Read(buf) 50 | if err != nil { 51 | return 52 | } 53 | 54 | res, err := hp.Parse(buf[:n]) 55 | for err == ErrMissingData { 56 | var m int 57 | 58 | m, err = c.Read(buf[n:]) 59 | if err != nil { 60 | return 61 | } 62 | 63 | n += m 64 | 65 | res, err = hp.Parse(buf[:n]) 66 | } 67 | 68 | if err != nil { 69 | panic(err) 70 | } 71 | 72 | s.Handler.HandleConnection(hp, buf[res:n], c) 73 | } 74 | } 75 | 76 | // ListenAndServeTLS listens on the TCP network address srv.Addr and 77 | // then calls Serve to handle requests on incoming TLS connections. 78 | // 79 | // Filenames containing a certificate and matching private key for 80 | // the server must be provided. If the certificate is signed by a 81 | // certificate authority, the certFile should be the concatenation 82 | // of the server's certificate followed by the CA's certificate. 83 | // 84 | // If addr is blank, ":https" is used. 85 | func (srv *Server) ListenAndServeTLS(addr, certFile, keyFile string) error { 86 | if addr == "" { 87 | addr = ":https" 88 | } 89 | 90 | config := &tls.Config{} 91 | config.NextProtos = []string{"http/1.1"} 92 | 93 | var err error 94 | config.Certificates = make([]tls.Certificate, 1) 95 | config.Certificates[0], err = tls.LoadX509KeyPair(certFile, keyFile) 96 | if err != nil { 97 | return err 98 | } 99 | 100 | ln, err := net.Listen("tcp", addr) 101 | if err != nil { 102 | return err 103 | } 104 | 105 | tlsListener := tls.NewListener(tcpKeepAliveListener{ln.(*net.TCPListener)}, config) 106 | return srv.Serve(tlsListener) 107 | } 108 | 109 | func ListenAndServeTLS(addr string, certFile string, keyFile string, handler Handler) error { 110 | server := &Server{Handler: handler} 111 | return server.ListenAndServeTLS(addr, certFile, keyFile) 112 | } 113 | 114 | // tcpKeepAliveListener sets TCP keep-alive timeouts on accepted 115 | // connections. It's used by ListenAndServe and ListenAndServeTLS so 116 | // dead TCP connections (e.g. closing laptop mid-download) eventually 117 | // go away. 118 | type tcpKeepAliveListener struct { 119 | *net.TCPListener 120 | } 121 | 122 | func (ln tcpKeepAliveListener) Accept() (c net.Conn, err error) { 123 | tc, err := ln.AcceptTCP() 124 | if err != nil { 125 | return 126 | } 127 | 128 | tc.SetKeepAlive(true) 129 | tc.SetKeepAlivePeriod(3 * time.Minute) 130 | 131 | return tc, nil 132 | } 133 | -------------------------------------------------------------------------------- /http_test.go: -------------------------------------------------------------------------------- 1 | package wildcat 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "net/http" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | var simple = []byte("GET / HTTP/1.0\r\n\r\n") 14 | 15 | func TestParseSimple(t *testing.T) { 16 | hp := NewHTTPParser() 17 | 18 | n, err := hp.Parse(simple) 19 | require.NoError(t, err) 20 | 21 | assert.Equal(t, n, len(simple)) 22 | 23 | assert.Equal(t, []byte("HTTP/1.0"), hp.Version) 24 | 25 | assert.Equal(t, []byte("/"), hp.Path) 26 | assert.Equal(t, []byte("GET"), hp.Method) 27 | } 28 | 29 | func BenchmarkParseSimple(b *testing.B) { 30 | hp := NewHTTPParser() 31 | 32 | for i := 0; i < b.N; i++ { 33 | hp.Parse(simple) 34 | } 35 | } 36 | 37 | func BenchmarkNetHTTP(b *testing.B) { 38 | for i := 0; i < b.N; i++ { 39 | buf := bufio.NewReader(bytes.NewReader(simple)) 40 | http.ReadRequest(buf) 41 | } 42 | } 43 | 44 | var simpleHeaders = []byte("GET / HTTP/1.0\r\nHost: cookie.com\r\n\r\n") 45 | 46 | func TestParseSimpleHeaders(t *testing.T) { 47 | hp := NewHTTPParser() 48 | 49 | _, err := hp.Parse(simpleHeaders) 50 | require.NoError(t, err) 51 | 52 | assert.Equal(t, []byte("cookie.com"), hp.FindHeader([]byte("Host"))) 53 | } 54 | 55 | func BenchmarkParseSimpleHeaders(b *testing.B) { 56 | hp := NewHTTPParser() 57 | 58 | for i := 0; i < b.N; i++ { 59 | hp.Parse(simpleHeaders) 60 | } 61 | } 62 | 63 | var simple3Headers = []byte("GET / HTTP/1.0\r\nHost: cookie.com\r\nDate: foobar\r\nAccept: these/that\r\n\r\n") 64 | 65 | func TestParseSimple3Headers(t *testing.T) { 66 | hp := NewHTTPParser() 67 | 68 | _, err := hp.Parse(simple3Headers) 69 | require.NoError(t, err) 70 | 71 | assert.Equal(t, []byte("cookie.com"), hp.FindHeader([]byte("Host"))) 72 | assert.Equal(t, []byte("foobar"), hp.FindHeader([]byte("Date"))) 73 | assert.Equal(t, []byte("these/that"), hp.FindHeader([]byte("Accept"))) 74 | } 75 | 76 | func BenchmarkParseSimple3Headers(b *testing.B) { 77 | hp := NewHTTPParser() 78 | 79 | for i := 0; i < b.N; i++ { 80 | hp.Parse(simple3Headers) 81 | } 82 | } 83 | 84 | func BenchmarkNetHTTP3(b *testing.B) { 85 | for i := 0; i < b.N; i++ { 86 | buf := bufio.NewReader(bytes.NewReader(simple3Headers)) 87 | http.ReadRequest(buf) 88 | } 89 | } 90 | 91 | var short = []byte("GET / HT") 92 | 93 | func TestParseMissingData(t *testing.T) { 94 | hp := NewHTTPParser() 95 | 96 | _, err := hp.Parse(short) 97 | 98 | assert.Equal(t, err, ErrMissingData) 99 | } 100 | 101 | var multiline = []byte("GET / HTTP/1.0\r\nHost: cookie.com\r\n more host\r\n\r\n") 102 | 103 | func TestParseMultlineHeader(t *testing.T) { 104 | hp := NewHTTPParser() 105 | 106 | _, err := hp.Parse(multiline) 107 | require.NoError(t, err) 108 | 109 | assert.Equal(t, []byte("cookie.com more host"), hp.FindHeader([]byte("Host"))) 110 | } 111 | 112 | var specialHeaders = []byte("GET / HTTP/1.0\r\nHost: cookie.com\r\nContent-Length: 50\r\n\r\n") 113 | 114 | func TestParseSpecialHeaders(t *testing.T) { 115 | hp := NewHTTPParser() 116 | 117 | _, err := hp.Parse(specialHeaders) 118 | require.NoError(t, err) 119 | 120 | assert.Equal(t, []byte("cookie.com"), hp.Host()) 121 | assert.Equal(t, 50, hp.ContentLength()) 122 | } 123 | 124 | func TestFindHeaderIgnoresCase(t *testing.T) { 125 | hp := NewHTTPParser() 126 | 127 | _, err := hp.Parse(specialHeaders) 128 | require.NoError(t, err) 129 | 130 | assert.Equal(t, []byte("50"), hp.FindHeader([]byte("content-length"))) 131 | } 132 | 133 | var multipleHeaders = []byte("GET / HTTP/1.0\r\nBar: foo\r\nBar: quz\r\n\r\n") 134 | 135 | func TestFindAllHeaders(t *testing.T) { 136 | hp := NewHTTPParser() 137 | 138 | _, err := hp.Parse(multipleHeaders) 139 | require.NoError(t, err) 140 | 141 | bar := []byte("Bar") 142 | 143 | assert.Equal(t, []byte("foo"), hp.FindHeader(bar)) 144 | assert.Equal(t, [][]byte{[]byte("foo"), []byte("quz")}, hp.FindAllHeaders(bar)) 145 | } 146 | -------------------------------------------------------------------------------- /status.go: -------------------------------------------------------------------------------- 1 | // Copyright 2009 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package wildcat 6 | 7 | // HTTP status codes, defined in RFC 2616. 8 | const ( 9 | StatusContinue = 100 10 | StatusSwitchingProtocols = 101 11 | 12 | StatusOK = 200 13 | StatusCreated = 201 14 | StatusAccepted = 202 15 | StatusNonAuthoritativeInfo = 203 16 | StatusNoContent = 204 17 | StatusResetContent = 205 18 | StatusPartialContent = 206 19 | 20 | StatusMultipleChoices = 300 21 | StatusMovedPermanently = 301 22 | StatusFound = 302 23 | StatusSeeOther = 303 24 | StatusNotModified = 304 25 | StatusUseProxy = 305 26 | StatusTemporaryRedirect = 307 27 | 28 | StatusBadRequest = 400 29 | StatusUnauthorized = 401 30 | StatusPaymentRequired = 402 31 | StatusForbidden = 403 32 | StatusNotFound = 404 33 | StatusMethodNotAllowed = 405 34 | StatusNotAcceptable = 406 35 | StatusProxyAuthRequired = 407 36 | StatusRequestTimeout = 408 37 | StatusConflict = 409 38 | StatusGone = 410 39 | StatusLengthRequired = 411 40 | StatusPreconditionFailed = 412 41 | StatusRequestEntityTooLarge = 413 42 | StatusRequestURITooLong = 414 43 | StatusUnsupportedMediaType = 415 44 | StatusRequestedRangeNotSatisfiable = 416 45 | StatusExpectationFailed = 417 46 | StatusTeapot = 418 47 | 48 | StatusInternalServerError = 500 49 | StatusNotImplemented = 501 50 | StatusBadGateway = 502 51 | StatusServiceUnavailable = 503 52 | StatusGatewayTimeout = 504 53 | StatusHTTPVersionNotSupported = 505 54 | 55 | // New HTTP status codes from RFC 6585. Not exported yet in Go 1.1. 56 | // See discussion at https://codereview.appspot.com/7678043/ 57 | statusPreconditionRequired = 428 58 | statusTooManyRequests = 429 59 | statusRequestHeaderFieldsTooLarge = 431 60 | statusNetworkAuthenticationRequired = 511 61 | ) 62 | 63 | var statusText = map[int]string{ 64 | StatusContinue: "Continue", 65 | StatusSwitchingProtocols: "Switching Protocols", 66 | 67 | StatusOK: "OK", 68 | StatusCreated: "Created", 69 | StatusAccepted: "Accepted", 70 | StatusNonAuthoritativeInfo: "Non-Authoritative Information", 71 | StatusNoContent: "No Content", 72 | StatusResetContent: "Reset Content", 73 | StatusPartialContent: "Partial Content", 74 | 75 | StatusMultipleChoices: "Multiple Choices", 76 | StatusMovedPermanently: "Moved Permanently", 77 | StatusFound: "Found", 78 | StatusSeeOther: "See Other", 79 | StatusNotModified: "Not Modified", 80 | StatusUseProxy: "Use Proxy", 81 | StatusTemporaryRedirect: "Temporary Redirect", 82 | 83 | StatusBadRequest: "Bad Request", 84 | StatusUnauthorized: "Unauthorized", 85 | StatusPaymentRequired: "Payment Required", 86 | StatusForbidden: "Forbidden", 87 | StatusNotFound: "Not Found", 88 | StatusMethodNotAllowed: "Method Not Allowed", 89 | StatusNotAcceptable: "Not Acceptable", 90 | StatusProxyAuthRequired: "Proxy Authentication Required", 91 | StatusRequestTimeout: "Request Timeout", 92 | StatusConflict: "Conflict", 93 | StatusGone: "Gone", 94 | StatusLengthRequired: "Length Required", 95 | StatusPreconditionFailed: "Precondition Failed", 96 | StatusRequestEntityTooLarge: "Request Entity Too Large", 97 | StatusRequestURITooLong: "Request URI Too Long", 98 | StatusUnsupportedMediaType: "Unsupported Media Type", 99 | StatusRequestedRangeNotSatisfiable: "Requested Range Not Satisfiable", 100 | StatusExpectationFailed: "Expectation Failed", 101 | StatusTeapot: "I'm a teapot", 102 | 103 | StatusInternalServerError: "Internal Server Error", 104 | StatusNotImplemented: "Not Implemented", 105 | StatusBadGateway: "Bad Gateway", 106 | StatusServiceUnavailable: "Service Unavailable", 107 | StatusGatewayTimeout: "Gateway Timeout", 108 | StatusHTTPVersionNotSupported: "HTTP Version Not Supported", 109 | 110 | statusPreconditionRequired: "Precondition Required", 111 | statusTooManyRequests: "Too Many Requests", 112 | statusRequestHeaderFieldsTooLarge: "Request Header Fields Too Large", 113 | statusNetworkAuthenticationRequired: "Network Authentication Required", 114 | } 115 | 116 | // StatusText returns a text for the HTTP status code. It returns the empty 117 | // string if the code is unknown. 118 | func StatusText(code int) string { 119 | return statusText[code] 120 | } 121 | -------------------------------------------------------------------------------- /http.go: -------------------------------------------------------------------------------- 1 | package wildcat 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "strconv" 7 | 8 | "github.com/vektra/errors" 9 | ) 10 | 11 | const OptimalBufferSize = 1500 12 | 13 | type header struct { 14 | Name []byte 15 | Value []byte 16 | } 17 | 18 | type HTTPParser struct { 19 | Method, Path, Version []byte 20 | 21 | Headers []header 22 | TotalHeaders int 23 | 24 | host []byte 25 | hostRead bool 26 | 27 | contentLength int64 28 | contentLengthRead bool 29 | } 30 | 31 | const DefaultHeaderSlice = 10 32 | 33 | // Create a new parser 34 | func NewHTTPParser() *HTTPParser { 35 | return NewSizedHTTPParser(DefaultHeaderSlice) 36 | } 37 | 38 | // Create a new parser allocating size for size headers 39 | func NewSizedHTTPParser(size int) *HTTPParser { 40 | return &HTTPParser{ 41 | Headers: make([]header, size), 42 | TotalHeaders: size, 43 | contentLength: -1, 44 | } 45 | } 46 | 47 | var ( 48 | ErrBadProto = errors.New("bad protocol") 49 | ErrMissingData = errors.New("missing data") 50 | ErrUnsupported = errors.New("unsupported http feature") 51 | ) 52 | 53 | const ( 54 | eNextHeader int = iota 55 | eNextHeaderN 56 | eHeader 57 | eHeaderValueSpace 58 | eHeaderValue 59 | eHeaderValueN 60 | eMLHeaderStart 61 | eMLHeaderValue 62 | ) 63 | 64 | // Parse the buffer as an HTTP Request. The buffer must contain the entire 65 | // request or Parse will return ErrMissingData for the caller to get more 66 | // data. (this thusly favors getting a completed request in a single Read() 67 | // call). 68 | // 69 | // Returns the number of bytes used by the header (thus where the body begins). 70 | // Also can return ErrUnsupported if an HTTP feature is detected but not supported. 71 | func (hp *HTTPParser) Parse(input []byte) (int, error) { 72 | var headers int 73 | var path int 74 | var ok bool 75 | 76 | total := len(input) 77 | 78 | method: 79 | for i := 0; i < total; i++ { 80 | switch input[i] { 81 | case ' ', '\t': 82 | hp.Method = input[0:i] 83 | ok = true 84 | path = i + 1 85 | break method 86 | } 87 | } 88 | 89 | if !ok { 90 | return 0, ErrMissingData 91 | } 92 | 93 | var version int 94 | 95 | ok = false 96 | 97 | path: 98 | for i := path; i < total; i++ { 99 | switch input[i] { 100 | case ' ', '\t': 101 | ok = true 102 | hp.Path = input[path:i] 103 | version = i + 1 104 | break path 105 | } 106 | } 107 | 108 | if !ok { 109 | return 0, ErrMissingData 110 | } 111 | 112 | var readN bool 113 | 114 | ok = false 115 | loop: 116 | for i := version; i < total; i++ { 117 | c := input[i] 118 | 119 | switch readN { 120 | case false: 121 | switch c { 122 | case '\r': 123 | hp.Version = input[version:i] 124 | readN = true 125 | case '\n': 126 | hp.Version = input[version:i] 127 | headers = i + 1 128 | ok = true 129 | break loop 130 | } 131 | case true: 132 | if c != '\n' { 133 | return 0, errors.Context(ErrBadProto, "missing newline in version") 134 | } 135 | headers = i + 1 136 | ok = true 137 | break loop 138 | } 139 | } 140 | 141 | if !ok { 142 | return 0, ErrMissingData 143 | } 144 | 145 | var h int 146 | 147 | var headerName []byte 148 | 149 | state := eNextHeader 150 | 151 | start := headers 152 | 153 | for i := headers; i < total; i++ { 154 | switch state { 155 | case eNextHeader: 156 | switch input[i] { 157 | case '\r': 158 | state = eNextHeaderN 159 | case '\n': 160 | return i + 1, nil 161 | case ' ', '\t': 162 | state = eMLHeaderStart 163 | default: 164 | start = i 165 | state = eHeader 166 | } 167 | case eNextHeaderN: 168 | if input[i] != '\n' { 169 | return 0, ErrBadProto 170 | } 171 | 172 | return i + 1, nil 173 | case eHeader: 174 | if input[i] == ':' { 175 | headerName = input[start:i] 176 | state = eHeaderValueSpace 177 | } 178 | case eHeaderValueSpace: 179 | switch input[i] { 180 | case ' ', '\t': 181 | continue 182 | } 183 | 184 | start = i 185 | state = eHeaderValue 186 | case eHeaderValue: 187 | switch input[i] { 188 | case '\r': 189 | state = eHeaderValueN 190 | case '\n': 191 | state = eNextHeader 192 | default: 193 | continue 194 | } 195 | 196 | hp.Headers[h] = header{headerName, input[start:i]} 197 | h++ 198 | 199 | if h == hp.TotalHeaders { 200 | newHeaders := make([]header, hp.TotalHeaders+10) 201 | copy(newHeaders, hp.Headers) 202 | hp.Headers = newHeaders 203 | hp.TotalHeaders += 10 204 | } 205 | case eHeaderValueN: 206 | if input[i] != '\n' { 207 | return 0, ErrBadProto 208 | } 209 | state = eNextHeader 210 | 211 | case eMLHeaderStart: 212 | switch input[i] { 213 | case ' ', '\t': 214 | continue 215 | } 216 | 217 | start = i 218 | state = eMLHeaderValue 219 | case eMLHeaderValue: 220 | switch input[i] { 221 | case '\r': 222 | state = eHeaderValueN 223 | case '\n': 224 | state = eNextHeader 225 | default: 226 | continue 227 | } 228 | 229 | cur := hp.Headers[h-1].Value 230 | 231 | newheader := make([]byte, len(cur)+1+(i-start)) 232 | copy(newheader, cur) 233 | copy(newheader[len(cur):], []byte(" ")) 234 | copy(newheader[len(cur)+1:], input[start:i]) 235 | 236 | hp.Headers[h-1].Value = newheader 237 | } 238 | } 239 | 240 | return 0, ErrMissingData 241 | } 242 | 243 | // Return a value of a header matching name. 244 | func (hp *HTTPParser) FindHeader(name []byte) []byte { 245 | for _, header := range hp.Headers { 246 | if bytes.Equal(header.Name, name) { 247 | return header.Value 248 | } 249 | } 250 | 251 | for _, header := range hp.Headers { 252 | if bytes.EqualFold(header.Name, name) { 253 | return header.Value 254 | } 255 | } 256 | 257 | return nil 258 | } 259 | 260 | // Return all values of a header matching name. 261 | func (hp *HTTPParser) FindAllHeaders(name []byte) [][]byte { 262 | var headers [][]byte 263 | 264 | for _, header := range hp.Headers { 265 | if bytes.EqualFold(header.Name, name) { 266 | headers = append(headers, header.Value) 267 | } 268 | } 269 | 270 | return headers 271 | } 272 | 273 | var cHost = []byte("Host") 274 | 275 | // Return the value of the Host header 276 | func (hp *HTTPParser) Host() []byte { 277 | if hp.hostRead { 278 | return hp.host 279 | } 280 | 281 | hp.hostRead = true 282 | hp.host = hp.FindHeader(cHost) 283 | return hp.host 284 | } 285 | 286 | var cContentLength = []byte("Content-Length") 287 | 288 | // Return the value of the Content-Length header. 289 | // A value of -1 indicates the header was not set. 290 | func (hp *HTTPParser) ContentLength() int64 { 291 | if hp.contentLengthRead { 292 | return hp.contentLength 293 | } 294 | 295 | header := hp.FindHeader(cContentLength) 296 | if header != nil { 297 | i, err := strconv.ParseInt(string(header), 10, 0) 298 | if err == nil { 299 | hp.contentLength = i 300 | } 301 | } 302 | 303 | hp.contentLengthRead = true 304 | return hp.contentLength 305 | } 306 | 307 | func (hp *HTTPParser) BodyReader(rest []byte, in io.ReadCloser) io.ReadCloser { 308 | return BodyReader(hp.ContentLength(), rest, in) 309 | } 310 | 311 | var cGet = []byte("GET") 312 | 313 | func (hp *HTTPParser) Get() bool { 314 | return bytes.Equal(hp.Method, cGet) 315 | } 316 | 317 | var cPost = []byte("POST") 318 | 319 | func (hp *HTTPParser) Post() bool { 320 | return bytes.Equal(hp.Method, cPost) 321 | } 322 | --------------------------------------------------------------------------------