diff --git a/ergonomadic.conf b/ergonomadic.conf index fd08218f..7225b98b 100644 --- a/ergonomadic.conf +++ b/ergonomadic.conf @@ -3,6 +3,7 @@ name = "irc.example.com" ; required, usually a hostname database = "ergonomadic.db" ; path relative to this file listen = "localhost:6667" ; see `net.Listen` for examples listen = "[::1]:6667" ; multiple `listen`s are allowed. +wslisten = ":8080" ; websocket listen log = "debug" ; error, warn, info, debug motd = "motd.txt" ; path relative to this file password = "JDJhJDA0JHJzVFFlNXdOUXNhLmtkSGRUQVVEVHVYWXRKUmdNQ3FKVTRrczRSMTlSWGRPZHRSMVRzQmtt" ; 'test' diff --git a/irc/config.go b/irc/config.go index 5a3c2e44..f3db3ea9 100644 --- a/irc/config.go +++ b/irc/config.go @@ -23,6 +23,7 @@ type Config struct { PassConfig Database string Listen []string + Wslisten string Log string MOTD string Name string diff --git a/irc/server.go b/irc/server.go index f21812dd..33fa2fb7 100644 --- a/irc/server.go +++ b/irc/server.go @@ -6,6 +6,7 @@ import ( "fmt" "log" "net" + "net/http" "os" "os/signal" "strings" @@ -72,6 +73,10 @@ func NewServer(config *Config) *Server { server.listen(addr) } + if config.Server.Wslisten != "" { + server.wslisten(config.Server.Wslisten) + } + signal.Notify(server.signals, SERVER_SIGNALS...) return server @@ -203,6 +208,41 @@ func (s *Server) listen(addr string) { }() } +// +// websocket listen goroutine +// + +func (s *Server) wslisten(addr string) { + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + if r.Method != "GET" { + Log.error.Printf("%s method not allowed", s) + return + } + + // We don't have any subprotocols, so if someone attempts to `new + // WebSocket(server, "subprotocol")` they'll break here, instead of + // getting the default, ambiguous, response from gorilla. + if v, ok := r.Header["Sec-Websocket-Protocol"]; ok { + http.Error(w, fmt.Sprintf("WebSocket subprocotols (e.g. %s) not supported", v), 400) + } + + ws, err := upgrader.Upgrade(w, r, nil) + if err != nil { + Log.error.Printf("%s websocket upgrade error: %s", s, err) + return + } + + s.newConns <- WSContainer{ws} + }) + go func() { + Log.info.Printf("%s listening on %s", s, addr) + err := http.ListenAndServe(addr, nil) + if err != nil { + Log.error.Printf("%s listenAndServe error: %s", s, err) + } + }() +} + // // server functionality // diff --git a/irc/websocket.go b/irc/websocket.go new file mode 100644 index 00000000..72a1c1c8 --- /dev/null +++ b/irc/websocket.go @@ -0,0 +1,47 @@ +package irc + +import ( + "github.com/gorilla/websocket" + "net/http" + "time" +) + +var upgrader = websocket.Upgrader{ + ReadBufferSize: 1024, + WriteBufferSize: 1024, + // If a WS session contains sensitive information, and you choose to use + // cookies for authentication (during the HTTP(S) upgrade request), then + // you should check that Origin is a domain under your control. If it + // isn't, then it is possible for users of your site, visiting a naughty + // Origin, to have a WS opened using their credentials. See + // http://www.christian-schneider.net/CrossSiteWebSocketHijacking.html#main. + // We don't care about Origin because the (IRC) authentication is contained + // in the WS stream -- the WS session is not privileged when it is opened. + CheckOrigin: func(r *http.Request) bool { return true }, +} + +type WSContainer struct { + *websocket.Conn +} + +func (this WSContainer) Read(msg []byte) (int, error) { + ty, bytes, err := this.ReadMessage() + if ty == websocket.TextMessage { + n := copy(msg, []byte(string(bytes)+CRLF+CRLF)) + return n, err + } + // Binary, and other kinds of messages, are thrown away. + return 0, nil +} + +func (this WSContainer) Write(msg []byte) (int, error) { + err := this.WriteMessage(websocket.TextMessage, msg) + return len(msg), err +} + +func (this WSContainer) SetDeadline(t time.Time) error { + if err := this.SetWriteDeadline(t); err != nil { + return err + } + return this.SetReadDeadline(t) +}