From 4a3d59dea9ecfc484b4ecfe1b9731f8e95a06b8f Mon Sep 17 00:00:00 2001 From: Nadeen Udantha Date: Fri, 24 Jun 2022 09:35:10 +0530 Subject: [PATCH] boi --- .gitignore | 1 + client.go | 100 +++++++++++++++++++++++++++++++++++ client_test.go | 72 +++++++++++++++++++++++++ go.mod | 5 ++ go.sum | 2 + headers.go | 65 +++++++++++++++++++++++ message.go | 137 ++++++++++++++++++++++++++++++++++++++++++++++++ message_test.go | 57 ++++++++++++++++++++ server.go | 97 ++++++++++++++++++++++++++++++++++ server_test.go | 87 ++++++++++++++++++++++++++++++ 10 files changed, 623 insertions(+) create mode 100644 .gitignore create mode 100644 client.go create mode 100644 client_test.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 headers.go create mode 100644 message.go create mode 100644 message_test.go create mode 100644 server.go create mode 100644 server_test.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..adb36c8 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.exe \ No newline at end of file diff --git a/client.go b/client.go new file mode 100644 index 0000000..7c27657 --- /dev/null +++ b/client.go @@ -0,0 +1,100 @@ +// Copyright (c) 2022 Nadeen Udantha . All rights reserved. + +package boihttp + +import ( + "bufio" + "context" + "io" + "net" + url2 "net/url" +) + +func DoConn(c *bufio.ReadWriter, method string, url string, hdrs Headers, reqbody io.Reader) (res *Message, resbody io.Reader, err error) { + resbody = EmptyBody + if reqbody == nil { + panic("wtf?") + } + u, err := url2.Parse(url) + if err != nil { + return + } + if hdrs == nil { + hdrs = make(Headers) + } + req := &Message{ + Method: method, + Path: u.RequestURI(), + Version: VersionHTTP1_1, + Headers: hdrs, + } + req.Set("Host", u.Host) + req.Set("User-Agent", "boihttp-go/1.0") + err = req.WriteRequest(c.Writer) + if err != nil { + return + } + err = c.Writer.Flush() + if err != nil { + return + } + _, err = c.Writer.ReadFrom(reqbody) + if err != nil { + return + } + res = new(Message) + err = res.ReadResponse(c.Reader) + if err != nil { + return + } + resbody = res.body(c.Reader) + return +} + +func Do(method string, url string, hdrs Headers, reqbody io.Reader, dial DialFunc, ctx context.Context) (res *Message, resbody io.Reader, err error) { + u, err := url2.Parse(url) + if err != nil { + return + } + if u.Scheme != "http" { + panic("wtf?") + } + host, port := u.Hostname(), u.Port() + if port == "" { + port = "80" + } + if dial == nil { + var d net.Dialer + dial = d.DialContext + } + c, err := dial(ctx, "tcp", net.JoinHostPort(host, port)) + if err != nil { + return + } + rw := bufio.NewReadWriter(bufio.NewReader(c), bufio.NewWriter(c)) + res, resbody, err = DoConn(rw, method, url, hdrs, reqbody) + if resbody == EmptyBody || resbody.(*io.LimitedReader).N == 0 { + c.Close() + } + resbody = &closeOnEOF{r: reqbody, c: c} + return +} + +type closeOnEOF struct { + r io.Reader + c net.Conn +} + +func (x *closeOnEOF) Read(p []byte) (n int, err error) { + n, err = x.r.Read(p) + if err == io.EOF { + x.c.Close() + } + return +} + +type DialFunc func(ctx context.Context, network, addr string) (net.Conn, error) + +func Get(url string, hdrs Headers, ctx context.Context) (res *Message, resbody io.Reader, err error) { + return Do("GET", url, hdrs, EmptyBody, nil, ctx) +} diff --git a/client_test.go b/client_test.go new file mode 100644 index 0000000..0390530 --- /dev/null +++ b/client_test.go @@ -0,0 +1,72 @@ +// Copyright (c) 2022 Nadeen Udantha . All rights reserved. + +package boihttp_test + +import ( + "bufio" + "bytes" + "context" + "net" + "testing" + + "git.nadeen.lk/boihttp" + "golang.org/x/net/proxy" +) + +const httpbin_addr = "httpbin.org:80" +const httpbin_url = "http://httpbin.org/anything" + +func TestClientDoConn(t *testing.T) { + c, err := net.Dial("tcp", httpbin_addr) + throw(err) + defer c.Close() + r, w := bufio.NewReaderSize(c, 1024), bufio.NewWriterSize(c, 1024) + res, body, err := boihttp.DoConn(bufio.NewReadWriter(r, w), "GET", httpbin_url, nil, boihttp.EmptyBody) + throw(err) + print(res, body) +} + +func TestClientDoProxy(t *testing.T) { + d, err := proxy.SOCKS5("tcp", "127.0.0.1:1080", nil, proxy.Direct) + throw(err) + res, body, err := boihttp.Do("GET", httpbin_url, nil, boihttp.EmptyBody, d.(proxy.ContextDialer).DialContext, context.Background()) + throw(err) + print(res, body) +} + +func TestClientDoGet(t *testing.T) { + res, body, err := boihttp.Do("GET", httpbin_url, nil, boihttp.EmptyBody, nil, context.Background()) + throw(err) + print(res, body) +} +func TestClientGet(t *testing.T) { + res, body, err := boihttp.Get(httpbin_url, nil, context.Background()) + throw(err) + print(res, body) +} + +func TestClientDoPost(t *testing.T) { + reqbody := []byte("what's up doc?") + hdrs := make(boihttp.Headers) + hdrs.SetContentLength(len(reqbody)) + res, body, err := boihttp.Do("POST", httpbin_url, hdrs, bytes.NewReader(reqbody), nil, context.Background()) + throw(err) + print(res, body) +} + +func TestClientDoMultiple(t *testing.T) { + c, err := net.Dial("tcp", httpbin_addr) + throw(err) + defer c.Close() + r, w := bufio.NewReaderSize(c, 1024), bufio.NewWriterSize(c, 1024) + hdrs := make(boihttp.Headers) + hdrs.SetKeepAlive(true) + for z := 4; z >= 0; z-- { + if z == 0 { + hdrs.SetKeepAlive(false) + } + res, body, err := boihttp.DoConn(bufio.NewReadWriter(r, w), "GET", httpbin_url, hdrs.Clone(), boihttp.EmptyBody) + throw(err) + print(res, body) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..5e25d9f --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module git.nadeen.lk/boihttp + +go 1.17 + +require golang.org/x/net v0.0.0-20220617184016-355a448f1bc9 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..fd14855 --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +golang.org/x/net v0.0.0-20220617184016-355a448f1bc9 h1:Yqz/iviulwKwAREEeUd3nbBFn0XuyJqkoft2IlrvOhc= +golang.org/x/net v0.0.0-20220617184016-355a448f1bc9/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= diff --git a/headers.go b/headers.go new file mode 100644 index 0000000..92e7fd5 --- /dev/null +++ b/headers.go @@ -0,0 +1,65 @@ +// Copyright (c) 2022 Nadeen Udantha . All rights reserved. + +package boihttp + +import ( + "net/textproto" + "strconv" + "strings" +) + +type Headers map[string][]string + +func (h Headers) Add(key, value string) { + textproto.MIMEHeader(h).Add(key, value) +} + +func (h Headers) Get(key string) string { + return textproto.MIMEHeader(h).Get(key) +} + +func (h Headers) Has(key string) bool { + return len(textproto.MIMEHeader(h).Values(key)) > 0 +} + +func (h Headers) Set(key string, value string) { + textproto.MIMEHeader(h).Set(key, value) +} + +func (h Headers) Del(key string) { + textproto.MIMEHeader(h).Del(key) +} + +func (h Headers) Clone() Headers { + x := make(Headers) + for k, v := range h { + y := make([]string, len(v)) + copy(y, v) + x[k] = y + } + return x +} + +func (h Headers) SetKeepAlive(keepalive bool) { + if keepalive { + h.Set("Connection", "keep-alive") + } else { + h.Set("Connection", "close") + } +} + +func (h Headers) KeepAlive() bool { + return strings.ToLower(h.Get("Connection")) == "keep-alive" +} + +func (h Headers) ContentLength() int { + x, err := strconv.Atoi(h.Get("Content-Length")) + if err != nil { + return 0 + } + return x +} + +func (h Headers) SetContentLength(x int) { + h.Set("Content-Length", strconv.Itoa(x)) +} diff --git a/message.go b/message.go new file mode 100644 index 0000000..87394e1 --- /dev/null +++ b/message.go @@ -0,0 +1,137 @@ +// Copyright (c) 2022 Nadeen Udantha . All rights reserved. + +package boihttp + +import ( + "bufio" + "bytes" + "io" + "strconv" +) + +const ( + VersionHTTP1_0 = "HTTP/1.0" + VersionHTTP1_1 = "HTTP/1.1" +) + +// https://datatracker.ietf.org/doc/html/rfc2616#section-4 +type Message struct { + StatusCode int + Version, Method, Path, Status string + Headers +} + +func (m *Message) SetStatus(code int, status string) { + m.Status = status + m.StatusCode = code +} + +var EmptyBody io.Reader = &emptyBody{} + +type emptyBody struct{} + +func (l *emptyBody) Read(p []byte) (n int, err error) { + return 0, io.EOF +} + +func (m *Message) body(r *bufio.Reader) io.Reader { + n := m.ContentLength() + if n == 0 { + return EmptyBody + } + return &io.LimitedReader{R: r, N: int64(n)} +} + +func (m *Message) ReadRequest(r *bufio.Reader) error { + return m.read(r, true) +} + +func (m *Message) ReadResponse(r *bufio.Reader) error { + return m.read(r, false) +} + +func (m *Message) read(r *bufio.Reader, isRequest bool) error { + line, err := readline(r) + if err != nil { + return err + } + parts := bytes.SplitN(line, []byte(" "), 3) + if len(parts) != 3 { + panic("wtf?") + } + if isRequest { // GET / HTTP/1.0 + m.Method, m.Path, m.Version = string(parts[0]), string(parts[1]), string(parts[2]) + } else { // HTTP/1.0 200 Noice + m.StatusCode, err = strconv.Atoi(string(parts[1])) + if err != nil { + return err + } + m.Version, m.Status = string(parts[0]), string(parts[2]) + } + m.Headers = make(Headers) + n := 0 + for { + line, err := readline(r) + if err != nil { + return err + } + if len(line) == 0 { + break + } + parts = bytes.SplitN(line, []byte(": "), 2) + if len(parts) != 2 { + panic("wtf") + } + m.Add(string(parts[0]), string(parts[1])) + n++ + if n >= 64 { + panic("wtf?") + } + } + return nil +} + +func readline(r *bufio.Reader) ([]byte, error) { + line, p, err := r.ReadLine() + if p || len(line) > 8192 { + panic("wtf?") + } + return line, err +} + +const crlf = "\r\n" + +func (m *Message) WriteRequest(r *bufio.Writer) error { + return m.write(r, true) +} + +func (m *Message) WriteResponse(r *bufio.Writer) error { + return m.write(r, false) +} + +func (m *Message) write(w *bufio.Writer, isRequest bool) error { + if isRequest { // GET / HTTP/1.0 + w.WriteString(m.Method) + w.WriteByte(' ') + w.WriteString(m.Path) + w.WriteByte(' ') + w.WriteString(m.Version) + } else { // HTTP/1.0 200 Noice + w.WriteString(m.Version) + w.WriteByte(' ') + w.WriteString(strconv.Itoa(m.StatusCode)) + w.WriteByte(' ') + w.WriteString(m.Status) + } + w.WriteString(crlf) + for k, vs := range m.Headers { + for _, v := range vs { + w.WriteString(k) + w.WriteString(": ") + w.WriteString(v) + w.WriteString(crlf) + } + } + _, err := w.WriteString(crlf) + return err +} diff --git a/message_test.go b/message_test.go new file mode 100644 index 0000000..dbccc82 --- /dev/null +++ b/message_test.go @@ -0,0 +1,57 @@ +// Copyright (c) 2022 Nadeen Udantha . All rights reserved. + +package boihttp_test + +import ( + "bufio" + "fmt" + "io" + "io/ioutil" + "net" + "testing" + "time" + + "git.nadeen.lk/boihttp" +) + +func TestMessage(t *testing.T) { + c, err := net.Dial("tcp", httpbin_addr) + throw(err) + defer c.Close() + r, w := bufio.NewReaderSize(c, 1024), bufio.NewWriterSize(c, 1024) + err = (&boihttp.Message{ + Method: "GET", + Path: "/anything", + Version: boihttp.VersionHTTP1_1, + Headers: boihttp.Headers{ + "Host": []string{"httpbin.org"}, + "Connection": []string{"close"}, + "Boi": []string{"whatsupdoc?"}, + }, + }).WriteRequest(w) + throw(err) + throw(w.Flush()) + var res boihttp.Message + throw(res.ReadResponse(r)) + print(&res, r) +} + +func print(res *boihttp.Message, r io.Reader) { + fmt.Printf("status: %d %s\n", res.StatusCode, res.Status) + fmt.Println("headers:") + for k, vs := range res.Headers { + for _, v := range vs { + fmt.Printf("\t\"%s\": \"%s\"\n", k, v) + } + } + d, err := ioutil.ReadAll(r) + throw(err) + fmt.Printf("body: \"%s\"\n", string(d)) + time.Sleep(time.Second) +} + +func throw(err error) { + if err != nil { + panic(err) + } +} diff --git a/server.go b/server.go new file mode 100644 index 0000000..5799944 --- /dev/null +++ b/server.go @@ -0,0 +1,97 @@ +// Copyright (c) 2022 Nadeen Udantha . All rights reserved. + +package boihttp + +import ( + "bufio" + "context" + "errors" + "fmt" + "io" + "net" + "os" +) + +type Handler func(req *Message, reqbody io.Reader, res *Message) (resbody io.Reader) + +func Listen(addr string, ctx context.Context, handler Handler) (err error) { + var lc net.ListenConfig + l, err := lc.Listen(ctx, "tcp", addr) + if err != nil { + return + } + return Serve(l, handler) +} + +func Serve(l net.Listener, handler Handler) (err error) { + for { + c, err := l.Accept() + if err != nil { + return err + } + go handler2(c, handler) + } +} + +func handler2(c net.Conn, handler Handler) { + defer c.Close() + err := Handle(bufio.NewReadWriter(bufio.NewReader(c), bufio.NewWriter(c)), handler) + if err != nil { + if errors.Is(err, io.EOF) { + return + } + if _, z := err.(net.Error); z { + return + } + fmt.Fprintf(os.Stderr, "boihttp error: %s\n", err) + } +} + +func Handle(c *bufio.ReadWriter, handler Handler) (err error) { + for { + more, err := handle(c, handler) + if err != nil { + return err + } + if !more { + break + } + } + return +} + +func handle(c *bufio.ReadWriter, handler Handler) (more bool, err error) { + req := new(Message) + err = req.ReadRequest(c.Reader) + if err != nil { + return + } + reqbody := req.body(c.Reader) + res := new(Message) + res.Headers = make(Headers) + res.SetKeepAlive(req.KeepAlive() || (req.Version == VersionHTTP1_1 && !req.Has("Connection"))) + res.Version = req.Version + resbody := handler(req, reqbody, res) + _, err = io.Copy(io.Discard, reqbody) + if err != nil { + return + } + more = res.KeepAlive() + err = res.WriteResponse(c.Writer) + if err != nil { + return + } + err = c.Writer.Flush() + if err != nil { + return + } + _, err = c.Writer.ReadFrom(resbody) + if err != nil { + return + } + err = c.Writer.Flush() + if err != nil { + return + } + return +} diff --git a/server_test.go b/server_test.go new file mode 100644 index 0000000..b6beceb --- /dev/null +++ b/server_test.go @@ -0,0 +1,87 @@ +// Copyright (c) 2022 Nadeen Udantha . All rights reserved. + +package boihttp_test + +import ( + "bufio" + "bytes" + "context" + "fmt" + "io" + "io/ioutil" + "math/rand" + "net" + "strconv" + "testing" + + "git.nadeen.lk/boihttp" +) + +func TestServer(t *testing.T) { + l, err := net.Listen("tcp", "localhost:0") + throw(err) + go func() { + err := boihttp.Serve(l, handler) + if err != nil { + fmt.Println(err) + } + }() + url := fmt.Sprintf("http://localhost:%d/", l.Addr().(*net.TCPAddr).Port) + c, err := net.Dial("tcp", l.Addr().String()) + throw(err) + rw := bufio.NewReadWriter(bufio.NewReader(c), bufio.NewWriter(c)) + hdrs := make(boihttp.Headers) + hdrs.Set("Connection", "keep-alive") + for z := 4; z >= 0; z-- { + if z == 0 { + hdrs.Set("Connection", "close") + } + hdrs2 := hdrs.Clone() + x := rand.Int() & 0xffff + xx := strconv.Itoa(x) + hdrs2.Set("x", xx) + hdrs2.SetContentLength(len(xx)) + res, resbody, err := boihttp.DoConn(rw, "POST", url, hdrs2, bytes.NewBufferString(xx)) + throw(err) + y, err := strconv.Atoi(res.Get("y")) + throw(err) + if x*x != y { + panic("wtf?") + } + yy, err := ioutil.ReadAll(resbody) + throw(err) + y, err = strconv.Atoi(string(yy)) + throw(err) + if x*x != y { + panic("wtf?") + } + } +} + +func handler(req *boihttp.Message, reqbody io.Reader, res *boihttp.Message) (resbody io.Reader) { + x, err := strconv.Atoi(req.Get("x")) + throw(err) + xx, err := ioutil.ReadAll(reqbody) + throw(err) + x2, err := strconv.Atoi(string(xx)) + throw(err) + if x != x2 { + panic("wtf") + } + y := strconv.Itoa(x * x) + res.Set("y", y) + res.SetContentLength(len(y)) + return bytes.NewBufferString(y) +} + +func TestServer2(t *testing.T) { + go boihttp.Listen("localhost:8080", context.Background(), func(req *boihttp.Message, reqbody io.Reader, res *boihttp.Message) (resbody io.Reader) { + res.SetStatus(200, "Noice") + res.Set("working", "true") + res.SetContentLength(3) + return bytes.NewBufferString("lol") + }) + res, body, err := boihttp.Get("http://localhost:8080/", nil, context.Background()) + throw(err) + print(res, body) +}