This commit is contained in:
Nadeen Udantha 2022-06-24 09:35:10 +05:30
commit 4a3d59dea9
10 changed files with 623 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
*.exe

100
client.go Normal file
View File

@ -0,0 +1,100 @@
// Copyright (c) 2022 Nadeen Udantha <me@nadeen.lk>. 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)
}

72
client_test.go Normal file
View File

@ -0,0 +1,72 @@
// Copyright (c) 2022 Nadeen Udantha <me@nadeen.lk>. 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)
}
}

5
go.mod Normal file
View File

@ -0,0 +1,5 @@
module git.nadeen.lk/boihttp
go 1.17
require golang.org/x/net v0.0.0-20220617184016-355a448f1bc9 // indirect

2
go.sum Normal file
View File

@ -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=

65
headers.go Normal file
View File

@ -0,0 +1,65 @@
// Copyright (c) 2022 Nadeen Udantha <me@nadeen.lk>. 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))
}

137
message.go Normal file
View File

@ -0,0 +1,137 @@
// Copyright (c) 2022 Nadeen Udantha <me@nadeen.lk>. 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
}

57
message_test.go Normal file
View File

@ -0,0 +1,57 @@
// Copyright (c) 2022 Nadeen Udantha <me@nadeen.lk>. 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)
}
}

97
server.go Normal file
View File

@ -0,0 +1,97 @@
// Copyright (c) 2022 Nadeen Udantha <me@nadeen.lk>. 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
}

87
server_test.go Normal file
View File

@ -0,0 +1,87 @@
// Copyright (c) 2022 Nadeen Udantha <me@nadeen.lk>. 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)
}