Skip to content

Commit

Permalink
Merge pull request joewalnes#276 from asergeyev/gorilla
Browse files Browse the repository at this point in the history
Gorilla support in Websocketd
  • Loading branch information
asergeyev committed Dec 14, 2017
2 parents f128e07 + b2b6022 commit 729c67f
Show file tree
Hide file tree
Showing 18 changed files with 133 additions and 113 deletions.
5 changes: 5 additions & 0 deletions CHANGES
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
Version 0.3.0 (??, 2017)

* Migration of underlying websocket server to Gorilla Websocket lib.
* Binaries build code switched to 1.9.2

Version 0.2.12 (Feb 17, 2016)

* Update of underlying go standard libraries change how SSL works. SSL3 is no longer supported.
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ from disk using a `file://` URL.
More Features
-------------

* Very simple install. Just [download](https://github.com/joewalnes/websocketd/wiki/Download-and-install) the single executable for Linux, Mac or Windows and run it. No dependencies, no installers, no package managers, no external libraries. Suitable for development and production servers.
* Very simple install. Just [download](https://github.com/joewalnes/websocketd/wiki/Download-and-install) the single executable for Linux, Mac or Windows and run it. Minimal dependencies, no installers, no package managers, no external libraries. Suitable for development and production servers.
* Server side scripts can access details about the WebSocket HTTP request (e.g. remote host, query parameters, cookies, path, etc) via standard [CGI environment variables](https://github.com/joewalnes/websocketd/wiki/Environment-variables).
* As well as serving websocket daemons it also includes a static file server and classic CGI server for convenience.
* Command line help available via `websocketd --help`.
Expand Down
1 change: 1 addition & 0 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ func parseCommandLine() *Config {
config.DevConsole = *devConsoleFlag
config.StartupTime = time.Now()
config.ServerSoftware = fmt.Sprintf("websocketd/%s", Version())
config.HandshakeTimeout = time.Millisecond * 1500 // only default for now

if len(os.Args) == 1 {
fmt.Printf("Command line arguments are missing.\n")
Expand Down
2 changes: 1 addition & 1 deletion examples/windows-jscript/count.cmd
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
@echo off
cscript /nologo count.js
cscript /nologo %0\..\count.js
2 changes: 1 addition & 1 deletion examples/windows-jscript/dump-env.cmd
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
@echo off
cscript /nologo dump-env.js
cscript /nologo %0\..\dump-env.js
2 changes: 1 addition & 1 deletion examples/windows-jscript/greeter.cmd
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
@echo off
cscript /nologo greeter.js
cscript /nologo %0\..\greeter.js
2 changes: 1 addition & 1 deletion examples/windows-vbscript/count.cmd
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
@echo off
cscript /nologo count.vbs
cscript /nologo %0\..\count.vbs
2 changes: 1 addition & 1 deletion examples/windows-vbscript/dump-env.cmd
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
@echo off
cscript /nologo dump-env.vbs
cscript /nologo %0\..\dump-env.vbs
2 changes: 1 addition & 1 deletion examples/windows-vbscript/greeter.cmd
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
@echo off
cscript /nologo greeter.vbs
cscript /nologo %0\..\greeter.vbs
23 changes: 12 additions & 11 deletions help.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,31 +33,32 @@ Options:
--port=PORT HTTP port to listen on.
--address=ADDRESS Address to bind to (multiple options allowed)
Use square brackets to specify IPv6 address.
Use square brackets to specify IPv6 address.
Default: "" (all)
--sameorigin={true,false} Restrict (HTTP 403) protocol upgrades if the
Origin header does not match to requested HTTP
Origin header does not match to requested HTTP
Host. Default: false.
--origin=host[:port][,host[:port]...]
Restrict (HTTP 403) protocol upgrades if the
Origin header does not match to one of the host
and port combinations listed. If the port is not
specified, any port number will match.
specified, any port number will match.
Default: "" (allow any origin)
--ssl Listen for HTTPS socket instead of HTTP.
--ssl Listen for HTTPS socket instead of HTTP.
--sslcert=FILE All three options must be used or all of
--sslkey=FILE them should be omitted.
--sslkey=FILE them should be omitted.
--redirport=PORT Open alternative port and redirect HTTP traffic
from it to canonical address (mostly useful
for HTTPS-only configurations to redirect HTTP
traffic)
--passenv VAR[,VAR...] Lists environment variables allowed to be
passed to executed scripts.
passed to executed scripts. Does not work for
Windows since all the variables are kept there.
--binary={true,false} Switches communication to binary, process reads
send to browser as blobs and all reads from the
Expand All @@ -76,16 +77,16 @@ Options:
--cgidir=DIR Serve CGI scripts in this directory over HTTP.
--maxforks=N Limit number of processes that websocketd is
--maxforks=N Limit number of processes that websocketd is
able to execute with WS and CGI handlers.
When maxforks reached the server will be
rejecting requests that require executing
When maxforks reached the server will be
rejecting requests that require executing
another process (unlimited when 0 or negative).
Default: 0
--closems=milliseconds Specifies additional time process needs to gracefully
finish before websocketd will send termination signals
to it. Default: 0 (signals sent after 100ms, 250ms,
finish before websocketd will send termination signals
to it. Default: 0 (signals sent after 100ms, 250ms,
and 500ms of waiting)
--header="..." Set custom HTTP header to each answer. For
Expand Down
2 changes: 2 additions & 0 deletions libwebsocketd/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ type Config struct {
ServerSoftware string // Value to pass to SERVER_SOFTWARE environment variable (e.g. websocketd/1.2.3).
CloseMs uint // Milliseconds to start sending signals

HandshakeTimeout time.Duration // time to finish handshake (default 1500ms)

// settings
Binary bool // Use binary communication (send data in chunks they are read from process)
ReverseLookup bool // Perform reverse DNS lookups on hostnames (useful, but slower).
Expand Down
20 changes: 10 additions & 10 deletions libwebsocketd/endpoint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,30 +19,30 @@ var eol_answers = []string{

func TestTrimEOL(t *testing.T) {
for n := 0; n < len(eol_tests); n++ {
answ := trimEOL(eol_tests[n])
if answ != eol_answers[n] {
answ := trimEOL([]byte(eol_tests[n]))
if string(answ) != eol_answers[n] {
t.Errorf("Answer '%s' did not match predicted '%s'", answ, eol_answers[n])
}
}
}

func BenchmarkTrimEOL(b *testing.B) {
for n := 0; n < b.N; n++ {
trimEOL(eol_tests[n%len(eol_tests)])
trimEOL([]byte(eol_tests[n%len(eol_tests)]))
}
}

type TestEndpoint struct {
limit int
prefix string
c chan string
c chan []byte
result []string
}

func (e *TestEndpoint) StartReading() {
go func() {
for i := 0; i < e.limit; i++ {
e.c <- e.prefix + strconv.Itoa(i)
e.c <- []byte(e.prefix + strconv.Itoa(i))
}
time.Sleep(time.Millisecond) // should be enough for smaller channel to catch up with long one
close(e.c)
Expand All @@ -52,18 +52,18 @@ func (e *TestEndpoint) StartReading() {
func (e *TestEndpoint) Terminate() {
}

func (e *TestEndpoint) Output() chan string {
func (e *TestEndpoint) Output() chan []byte {
return e.c
}

func (e *TestEndpoint) Send(msg string) bool {
e.result = append(e.result, msg)
func (e *TestEndpoint) Send(msg []byte) bool {
e.result = append(e.result, string(msg))
return true
}

func TestEndpointPipe(t *testing.T) {
one := &TestEndpoint{2, "one:", make(chan string), make([]string, 0)}
two := &TestEndpoint{4, "two:", make(chan string), make([]string, 0)}
one := &TestEndpoint{2, "one:", make(chan []byte), make([]string, 0)}
two := &TestEndpoint{4, "two:", make(chan []byte), make([]string, 0)}
PipeEndpoints(one, two)
if len(one.result) != 4 || len(two.result) != 2 {
t.Errorf("Invalid lengths, should be 4 and 2: %v %v", one.result, two.result)
Expand Down
9 changes: 1 addition & 8 deletions libwebsocketd/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (
"strings"
"time"

"golang.org/x/net/websocket"
"github.com/gorilla/websocket"
)

var ScriptNotFoundError = errors.New("script not found")
Expand Down Expand Up @@ -57,13 +57,6 @@ func NewWebsocketdHandler(s *WebsocketdServer, req *http.Request, log *LogScope)
return wsh, nil
}

// wshandler returns function that executes code with given log context
func (wsh *WebsocketdHandler) wshandler(log *LogScope) websocket.Handler {
return websocket.Handler(func(ws *websocket.Conn) {
wsh.accept(ws, log)
})
}

func (wsh *WebsocketdHandler) accept(ws *websocket.Conn, log *LogScope) {
defer ws.Close()

Expand Down
73 changes: 41 additions & 32 deletions libwebsocketd/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ package libwebsocketd
import (
"errors"
"fmt"
"golang.org/x/net/websocket"
"net"
"net/http"
"net/http/cgi"
Expand All @@ -19,6 +18,8 @@ import (
"path/filepath"
"regexp"
"strings"

"github.com/gorilla/websocket"
)

var ForkNotAllowedError = errors.New("too many forks active")
Expand All @@ -42,20 +43,6 @@ func NewWebsocketdServer(config *Config, log *LogScope, maxforks int) *Websocket
return mux
}

// wshandshake returns closure to verify websocket origin header according to configured rules
func (h *WebsocketdServer) wshandshake(log *LogScope) func(*websocket.Config, *http.Request) error {
return func(wsconf *websocket.Config, req *http.Request) error {
if len(h.Config.Headers)+len(h.Config.HeadersWs) > 0 {
if wsconf.Header == nil {
wsconf.Header = http.Header(make(map[string][]string))
}
pushHeaders(wsconf.Header, h.Config.Headers)
pushHeaders(wsconf.Header, h.Config.HeadersWs)
}
return checkOrigin(wsconf, req, h.Config, log)
}
}

func splitMimeHeader(s string) (string, string) {
p := strings.IndexByte(s, ':')
if p < 0 {
Expand All @@ -79,11 +66,9 @@ func pushHeaders(h http.Header, hdrs []string) {

// ServeHTTP muxes between WebSocket handler, CGI handler, DevConsole, Static HTML or 404.
func (h *WebsocketdServer) ServeHTTP(w http.ResponseWriter, req *http.Request) {

log := h.Log.NewLevel(h.Log.LogFunc)
log.Associate("url", h.TellURL("http", req.Host, req.RequestURI))

pushHeaders(w.Header(), h.Config.Headers)
if h.Config.CommandName != "" || h.Config.UsingScriptDir {
hdrs := req.Header
upgradeRe := regexp.MustCompile("(?i)(^|[,\\s])Upgrade($|[,\\s])")
Expand All @@ -92,6 +77,7 @@ func (h *WebsocketdServer) ServeHTTP(w http.ResponseWriter, req *http.Request) {
if h.noteForkCreated() == nil {
defer h.noteForkCompled()

// start figuring out if we even need to upgrade
handler, err := NewWebsocketdHandler(h, req, log)
if err != nil {
if err == ScriptNotFoundError {
Expand All @@ -104,12 +90,32 @@ func (h *WebsocketdServer) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}

// Now we are ready for connection upgrade dance...
wsServer := &websocket.Server{
Handshake: h.wshandshake(log),
Handler: handler.wshandler(log),
var headers http.Header
if len(h.Config.Headers)+len(h.Config.HeadersWs) > 0 {
headers = http.Header(make(map[string][]string))
pushHeaders(headers, h.Config.Headers)
pushHeaders(headers, h.Config.HeadersWs)
}

upgrader := &websocket.Upgrader{
HandshakeTimeout: h.Config.HandshakeTimeout,
CheckOrigin: func(r *http.Request) bool {
// backporting previous checkorigin for use in gorilla/websocket for now
err := checkOrigin(req, h.Config, log)
return err == nil
},
}
wsServer.ServeHTTP(w, req)
conn, err := upgrader.Upgrade(w, req, headers)
if err != nil {
log.Access("session", "Unable to Upgrade: %s", err)
http.Error(w, "500 Internal Error", 500)
return
}

// old func was used in x/net/websocket style, we reuse it here for gorilla/websocket
handler.accept(conn, log)
return

} else {
log.Error("http", "Max of possible forks already active, upgrade rejected")
http.Error(w, "429 Too Many Requests", 429)
Expand Down Expand Up @@ -225,31 +231,34 @@ func (h *WebsocketdServer) noteForkCompled() {
return
}

func checkOrigin(wsconf *websocket.Config, req *http.Request, config *Config, log *LogScope) (err error) {
func checkOrigin(req *http.Request, config *Config, log *LogScope) (err error) {
// CONVERT GORILLA:
// this is origin checking function, it's called from wshandshake which is from ServeHTTP main handler
// should be trivial to reuse in gorilla's upgrader.CheckOrigin function.
// Only difference is to parse request and fetching passed Origin header out of it instead of using
// pre-parsed wsconf.Origin

// check for origin to be correct in future
// handshaker triggers answering with 403 if error was returned
// We keep behavior of original handshaker that populates this field
origin := req.Header.Get("Origin")
if origin == "" || (origin == "null" && config.AllowOrigins == nil) {
// we don't want to trust string "null" if there is any
// enforcements are active
req.Header.Set("Origin", "file:")
origin = "file:"
}

wsconf.Origin, err = websocket.Origin(wsconf, req)
if err == nil && wsconf.Origin == nil {
log.Access("session", "rejected null origin")
return fmt.Errorf("null origin not allowed")
}
originParsed, err := url.ParseRequestURI(origin)
if err != nil {
log.Access("session", "Origin parsing error: %s", err)
return err
}
log.Associate("origin", wsconf.Origin.String())

log.Associate("origin", originParsed.String())

// If some origin restrictions are present:
if config.SameOrigin || config.AllowOrigins != nil {
originServer, originPort, err := tellHostPort(wsconf.Origin.Host, wsconf.Origin.Scheme == "https")
originServer, originPort, err := tellHostPort(originParsed.Host, originParsed.Scheme == "https")
if err != nil {
log.Access("session", "Origin hostname parsing error: %s", err)
return err
Expand All @@ -274,7 +283,7 @@ func checkOrigin(wsconf *websocket.Config, req *http.Request, config *Config, lo
if err != nil {
continue // pass bad URLs in origin list
}
if allowedURL.Scheme != wsconf.Origin.Scheme {
if allowedURL.Scheme != originParsed.Scheme {
continue // mismatch
}
allowed = allowed[pos+3:]
Expand Down
Loading

0 comments on commit 729c67f

Please sign in to comment.