home » torr/wssrv.git
Author C. Torres <torr.c@mailgw.com> 2024-12-05 15:34:04
Committer C. Torres <torr.c@mailgw.com> 2024-12-05 15:34:04
Commit f373204 (patch)
Tree 16b7e7e
Parent(s)

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 <torr.c@mailgw.com>


commits diff: 3bf7406..f373204
4 files changed, 249 insertions, 0 deletionsdownload


Diffstat
-rw-r--r-- log.go 19
-rw-r--r-- main.go 86
-rw-r--r-- ws/stdin.go 41
-rw-r--r-- wshttp/wshttp.go 103

Diff options
View
Side
Whitespace
Context lines
Inter-hunk lines
+19/-0 A   log.go
index 0000000..41642b7
old size: 0B - new size: 356B
new file mode: -rw-r--r--
@@ -0,0 +1,19 @@
1 + package main
2 +
3 + import (
4 + "fmt"
5 + "log"
6 + "os"
7 + )
8 +
9 + const (
10 + // sep holds white space used as log type separator.
11 + sep = " "
12 + )
13 +
14 + var (
15 + // logInfo is used to log stable runtime information.
16 + logInfo = log.New(os.Stdout, fmt.Sprintf("%s%s", "info", sep), 0)
17 + // logErr is used to log errors.
18 + logErr = log.New(os.Stdout, fmt.Sprintf("%s%s", "error", sep), 0)
19 + )

+86/-0 A   main.go
index 0000000..dfcddfb
old size: 0B - new size: 1K
new file mode: -rw-r--r--
@@ -0,0 +1,86 @@
1 + package main
2 +
3 + import (
4 + "bufio"
5 + "fmt"
6 + "net"
7 + "net/http"
8 + "wssrv/ws"
9 + "wssrv/wshttp"
10 +
11 + "github.com/gorilla/websocket"
12 + )
13 +
14 + const (
15 + flagHost = "host"
16 + flagPort = "port"
17 +
18 + defHost = "localhost"
19 + defPort = 8070
20 + defBufSiz = 256
21 + )
22 +
23 + var (
24 + port int = 8070
25 + stream bool
26 + )
27 +
28 + func acceptTcpCon(addr string) (net.Conn, error) {
29 + var (
30 + err error
31 + l net.Listener
32 + )
33 +
34 + if l, err = net.Listen("tcp", addr); err != nil {
35 + return nil, err
36 + }
37 + return l.Accept()
38 + }
39 +
40 + func acceptWsCon(tCon net.Conn) *websocket.Conn {
41 + var (
42 + err error
43 + req *http.Request
44 + con *websocket.Conn
45 + eMsg string
46 +
47 + r = bufio.NewReader(tCon)
48 + rw = wshttp.NewResponse(tCon)
49 + )
50 +
51 + for con == nil {
52 + if req, err = http.ReadRequest(r); err != nil {
53 + eMsg = fmt.Sprintf("unable to read request: %v", err)
54 + logErr.Print(eMsg)
55 + http.Error(rw, fmt.Sprintf("error: %s", eMsg), http.StatusBadRequest)
56 + continue
57 + }
58 +
59 + if con, err = websocket.Upgrade(rw, req, nil, defBufSiz, defBufSiz); err != nil {
60 + eMsg = fmt.Sprintf("unable to create websocket connection: %v", err)
61 + logErr.Print(eMsg)
62 + http.Error(rw, fmt.Sprintf("error: %s", eMsg), http.StatusBadRequest)
63 + }
64 + }
65 +
66 + return con
67 + }
68 +
69 + func main() {
70 + var (
71 + err error
72 + tCon net.Conn
73 + con *websocket.Conn
74 +
75 + listenAddr = fmt.Sprintf("%s:%d", defHost, port)
76 + )
77 +
78 + logInfo.Printf("listening on '%s'", listenAddr)
79 + if tCon, err = acceptTcpCon(listenAddr); err != nil {
80 + panic(err)
81 + }
82 + con = acceptWsCon(tCon)
83 + defer con.Close()
84 +
85 + fmt.Println(ws.SendStdinLines(con))
86 + }

+41/-0 A   ws/stdin.go
index 0000000..a69b52d
old size: 0B - new size: 898B
new file mode: -rw-r--r--
@@ -0,0 +1,41 @@
1 + package ws
2 +
3 + import (
4 + "bufio"
5 + "fmt"
6 + "io"
7 + "os"
8 +
9 + "github.com/gorilla/websocket"
10 + )
11 +
12 + // SendStdinLines reads lines from the Stdin buffer, sends them in the
13 + // websocket and reads a reply.
14 + func SendStdinLines(con *websocket.Conn) error {
15 + var (
16 + err error
17 + l, msg []byte
18 +
19 + inp = bufio.NewReader(os.Stdin)
20 + )
21 +
22 + for err != io.EOF {
23 + fmt.Print("> ")
24 + if l, err = inp.ReadBytes('\n'); err != nil && err != io.EOF {
25 + return fmt.Errorf("unable to read from stdin: %v", err)
26 + }
27 + if len(l) != 0 {
28 + l = l[:len(l)-1]
29 + }
30 +
31 + if err = con.WriteMessage(websocket.TextMessage, l); err != nil {
32 + return fmt.Errorf("unable to write on websocket on '%s': %v", con.RemoteAddr(), err)
33 + }
34 +
35 + if _, msg, err = con.ReadMessage(); err != nil {
36 + return fmt.Errorf("unable to read from websocket on '%s': %v", con.RemoteAddr(), err)
37 + }
38 + fmt.Printf("(%s): %s\n", con.RemoteAddr(), msg)
39 + }
40 + return nil
41 + }

+103/-0 A   wshttp/wshttp.go
index 0000000..397de0f
old size: 0B - new size: 2K
new file mode: -rw-r--r--
@@ -0,0 +1,103 @@
1 + // Package wshttp contains a basic implementation of the interfaces
2 + // 'http.ResponseWriter' and 'http.Hijacker' centered on websocket usage,
3 + // specially the handshake response.
4 + package wshttp
5 +
6 + import (
7 + "bufio"
8 + "bytes"
9 + "fmt"
10 + "io"
11 + "net"
12 + "net/http"
13 + "net/http/httputil"
14 + )
15 +
16 + // Response implements 'http.ResponseWriter' and 'http.Hijacker'.
17 + type Response struct {
18 + Res http.Response
19 + Con net.Conn
20 + hasStatus bool
21 + hijacked bool
22 + }
23 +
24 + // Header implements 'http.ResponseWriter' strictly for websocket connections.
25 + func (r *Response) Header() http.Header {
26 + if r == nil {
27 + return nil
28 + }
29 + if r.Res.Header == nil {
30 + r.Res.Header = make(map[string][]string)
31 + }
32 + return r.Res.Header
33 + }
34 +
35 + // WriteHeader implements 'http.ResponseWriter' strictly for websocket
36 + // connections.
37 + func (r *Response) WriteHeader(code int) {
38 + if r == nil {
39 + return
40 + }
41 +
42 + if r.hasStatus {
43 + return
44 + }
45 + r.hasStatus = true
46 +
47 + r.Res.StatusCode = code
48 + r.Res.Status = http.StatusText(code)
49 + }
50 +
51 + // Write implements 'http.ResponseWriter' strictly for websocket connections.
52 + func (r *Response) Write(data []byte) (int, error) {
53 + if r == nil {
54 + return 0, fmt.Errorf("nil response")
55 + }
56 + if r.hijacked {
57 + return 0, http.ErrHijacked
58 + }
59 +
60 + if !r.hasStatus {
61 + r.Res.StatusCode = http.StatusOK
62 + r.Res.Status = http.StatusText(http.StatusOK)
63 + }
64 +
65 + if r.Res.Body == nil {
66 + r.Res.Body = io.NopCloser(bytes.NewBuffer(data))
67 + }
68 + var dump, err = httputil.DumpResponse(&r.Res, true)
69 + if err != nil {
70 + return 0, err
71 + }
72 +
73 + return r.Con.Write(dump)
74 + }
75 +
76 + // Hijack implements 'http.Hijacker'. The response body is drained, as it's not
77 + // used in websocket handshakes.
78 + func (r *Response) Hijack() (net.Conn, *bufio.ReadWriter, error) {
79 + if r == nil {
80 + return nil, nil, fmt.Errorf("nil response")
81 + }
82 + if r.hijacked {
83 + return nil, nil, http.ErrHijacked
84 + }
85 + var err error
86 +
87 + r.hijacked = true
88 + if r.Res.Body != nil {
89 + if _, err = io.Copy(io.Discard, r.Res.Body); err != nil {
90 + return nil, nil, fmt.Errorf("unable to drain response body: %v", err)
91 + }
92 + }
93 +
94 + return r.Con, bufio.NewReadWriter(bufio.NewReader(r.Con), bufio.NewWriter(r.Con)), nil
95 + }
96 +
97 + // NewResponse instantiates an HTTP response for websocket handshake requests.
98 + func NewResponse(con net.Conn) *Response {
99 + return &Response{
100 + Con: con,
101 + Res: http.Response{Header: make(map[string][]string)},
102 + }
103 + }