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)}, + } +}