From 8129c961feaba5a33d3633abb03b9e913506b79a Mon Sep 17 00:00:00 2001 From: "C. Torres" Date: Thu, 5 Dec 2024 12:34:04 -0300 Subject: [PATCH] Implement first prototype Implement the 1st working prototype. At this stage the program is able to listen and accept TCP connections, and parse incoming HTTP requests for websocket upgrades. Deviant requests are treated as errors and reported back to the caller as such. Signed-off-by: C. Torres --- log.go | 19 +++++++++ main.go | 79 ++++++++++++++++++++++++++++++++++++ wshttp/wshttp.go | 103 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 201 insertions(+) create mode 100644 log.go create mode 100644 main.go create mode 100644 wshttp/wshttp.go diff --git a/log.go b/log.go new file mode 100644 index 0000000..41642b7 --- /dev/null +++ b/log.go @@ -0,0 +1,19 @@ +package main + +import ( + "fmt" + "log" + "os" +) + +const ( + // sep holds white space used as log type separator. + sep = " " +) + +var ( + // logInfo is used to log stable runtime information. + logInfo = log.New(os.Stdout, fmt.Sprintf("%s%s", "info", sep), 0) + // logErr is used to log errors. + logErr = log.New(os.Stdout, fmt.Sprintf("%s%s", "error", sep), 0) +) diff --git a/main.go b/main.go new file mode 100644 index 0000000..5f90dc5 --- /dev/null +++ b/main.go @@ -0,0 +1,79 @@ +package main + +import ( + "bufio" + "fmt" + "net" + "net/http" + wshttp "wsserv/wshttp" + + "github.com/gorilla/websocket" +) + +const ( + flagHost = "host" + flagPort = "port" + + defHost = "localhost" + defPort = 8070 + defBufSiz = 256 +) + +var ( + port int = 8070 + stream bool +) + +func acceptTcpCon(addr string) (net.Conn, error) { + var ( + err error + l net.Listener + ) + + if l, err = net.Listen("tcp", addr); err != nil { + return nil, err + } + return l.Accept() +} + +func acceptWsCon(tCon net.Conn) *websocket.Conn { + var ( + err error + req *http.Request + con *websocket.Conn + + r = bufio.NewReader(tCon) + rw = wshttp.NewResponse(tCon) + ) + + for con == nil { + if req, err = http.ReadRequest(r); err != nil { + http.Error(rw, fmt.Sprintf("error: unable to read request: %v", err), http.StatusBadRequest) + continue + } + + if con, err = websocket.Upgrade(rw, req, nil, defBufSiz, defBufSiz); err != nil { + http.Error(rw, fmt.Sprintf("error: unable to create websocket connection: %v", err), http.StatusBadRequest) + } + } + + return con +} + +func main() { + var ( + err error + tCon net.Conn + con *websocket.Conn + + listenAddr = fmt.Sprintf("%s:%d", defHost, port) + ) + + logInfo.Printf("listening on '%s'", listenAddr) + if tCon, err = acceptTcpCon(listenAddr); err != nil { + panic(err) + } + con = acceptWsCon(tCon) + + fmt.Println(con.WriteMessage(websocket.TextMessage, []byte("hellooooooo\n"))) +} diff --git a/wshttp/wshttp.go b/wshttp/wshttp.go new file mode 100644 index 0000000..397de0f --- /dev/null +++ b/wshttp/wshttp.go @@ -0,0 +1,103 @@ +// Package wshttp contains a basic implementation of the interfaces +// 'http.ResponseWriter' and 'http.Hijacker' centered on websocket usage, +// specially the handshake response. +package wshttp + +import ( + "bufio" + "bytes" + "fmt" + "io" + "net" + "net/http" + "net/http/httputil" +) + +// Response implements 'http.ResponseWriter' and 'http.Hijacker'. +type Response struct { + Res http.Response + Con net.Conn + hasStatus bool + hijacked bool +} + +// Header implements 'http.ResponseWriter' strictly for websocket connections. +func (r *Response) Header() http.Header { + if r == nil { + return nil + } + if r.Res.Header == nil { + r.Res.Header = make(map[string][]string) + } + return r.Res.Header +} + +// WriteHeader implements 'http.ResponseWriter' strictly for websocket +// connections. +func (r *Response) WriteHeader(code int) { + if r == nil { + return + } + + if r.hasStatus { + return + } + r.hasStatus = true + + r.Res.StatusCode = code + r.Res.Status = http.StatusText(code) +} + +// Write implements 'http.ResponseWriter' strictly for websocket connections. +func (r *Response) Write(data []byte) (int, error) { + if r == nil { + return 0, fmt.Errorf("nil response") + } + if r.hijacked { + return 0, http.ErrHijacked + } + + if !r.hasStatus { + r.Res.StatusCode = http.StatusOK + r.Res.Status = http.StatusText(http.StatusOK) + } + + if r.Res.Body == nil { + r.Res.Body = io.NopCloser(bytes.NewBuffer(data)) + } + var dump, err = httputil.DumpResponse(&r.Res, true) + if err != nil { + return 0, err + } + + return r.Con.Write(dump) +} + +// Hijack implements 'http.Hijacker'. The response body is drained, as it's not +// used in websocket handshakes. +func (r *Response) Hijack() (net.Conn, *bufio.ReadWriter, error) { + if r == nil { + return nil, nil, fmt.Errorf("nil response") + } + if r.hijacked { + return nil, nil, http.ErrHijacked + } + var err error + + r.hijacked = true + if r.Res.Body != nil { + if _, err = io.Copy(io.Discard, r.Res.Body); err != nil { + return nil, nil, fmt.Errorf("unable to drain response body: %v", err) + } + } + + return r.Con, bufio.NewReadWriter(bufio.NewReader(r.Con), bufio.NewWriter(r.Con)), nil +} + +// NewResponse instantiates an HTTP response for websocket handshake requests. +func NewResponse(con net.Conn) *Response { + return &Response{ + Con: con, + Res: http.Response{Header: make(map[string][]string)}, + } +}