From f37320439cd57e46ec70bb84678cf4af7ac86bf1 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 | 86 +++++++++++++++++++++++++++++++++++++++ ws/stdin.go | 41 +++++++++++++++++++ wshttp/wshttp.go | 103 +++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 249 insertions(+) create mode 100644 log.go create mode 100644 main.go create mode 100644 ws/stdin.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..dfcddfb --- /dev/null +++ b/main.go @@ -0,0 +1,86 @@ +package main + +import ( + "bufio" + "fmt" + "net" + "net/http" + "wssrv/ws" + "wssrv/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 + eMsg string + + r = bufio.NewReader(tCon) + rw = wshttp.NewResponse(tCon) + ) + + for con == nil { + if req, err = http.ReadRequest(r); err != nil { + eMsg = fmt.Sprintf("unable to read request: %v", err) + logErr.Print(eMsg) + http.Error(rw, fmt.Sprintf("error: %s", eMsg), http.StatusBadRequest) + continue + } + + if con, err = websocket.Upgrade(rw, req, nil, defBufSiz, defBufSiz); err != nil { + eMsg = fmt.Sprintf("unable to create websocket connection: %v", err) + logErr.Print(eMsg) + http.Error(rw, fmt.Sprintf("error: %s", eMsg), 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) + defer con.Close() + + fmt.Println(ws.SendStdinLines(con)) +} diff --git a/ws/stdin.go b/ws/stdin.go new file mode 100644 index 0000000..a69b52d --- /dev/null +++ b/ws/stdin.go @@ -0,0 +1,41 @@ +package ws + +import ( + "bufio" + "fmt" + "io" + "os" + + "github.com/gorilla/websocket" +) + +// SendStdinLines reads lines from the Stdin buffer, sends them in the +// websocket and reads a reply. +func SendStdinLines(con *websocket.Conn) error { + var ( + err error + l, msg []byte + + inp = bufio.NewReader(os.Stdin) + ) + + for err != io.EOF { + fmt.Print("> ") + if l, err = inp.ReadBytes('\n'); err != nil && err != io.EOF { + return fmt.Errorf("unable to read from stdin: %v", err) + } + if len(l) != 0 { + l = l[:len(l)-1] + } + + if err = con.WriteMessage(websocket.TextMessage, l); err != nil { + return fmt.Errorf("unable to write on websocket on '%s': %v", con.RemoteAddr(), err) + } + + if _, msg, err = con.ReadMessage(); err != nil { + return fmt.Errorf("unable to read from websocket on '%s': %v", con.RemoteAddr(), err) + } + fmt.Printf("(%s): %s\n", con.RemoteAddr(), msg) + } + return nil +} 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)}, + } +}