mirror of
https://github.com/jeremyd/ergo.git
synced 2026-07-15 04:08:10 -07:00
WebSocket support
This commit is contained in:
+6
-3
@@ -50,9 +50,10 @@ type TLSListenConfig struct {
|
||||
|
||||
// This is the YAML-deserializable type of the value of the `Server.Listeners` map
|
||||
type listenerConfigBlock struct {
|
||||
TLS TLSListenConfig
|
||||
Tor bool
|
||||
STSOnly bool `yaml:"sts-only"`
|
||||
TLS TLSListenConfig
|
||||
Tor bool
|
||||
STSOnly bool `yaml:"sts-only"`
|
||||
WebSocket bool
|
||||
}
|
||||
|
||||
// listenerConfig is the config governing a particular listener (bound address),
|
||||
@@ -62,6 +63,7 @@ type listenerConfig struct {
|
||||
Tor bool
|
||||
STSOnly bool
|
||||
ProxyBeforeTLS bool
|
||||
WebSocket bool
|
||||
}
|
||||
|
||||
type PersistentStatus uint
|
||||
@@ -778,6 +780,7 @@ func (conf *Config) prepareListeners() (err error) {
|
||||
lconf.TLSConfig = tlsConfig
|
||||
lconf.ProxyBeforeTLS = block.TLS.Proxy
|
||||
}
|
||||
lconf.WebSocket = block.WebSocket
|
||||
conf.Server.trueListeners[addr] = lconf
|
||||
}
|
||||
return nil
|
||||
|
||||
+97
-2
@@ -29,6 +29,7 @@ import (
|
||||
"github.com/oragono/oragono/irc/modes"
|
||||
"github.com/oragono/oragono/irc/mysql"
|
||||
"github.com/oragono/oragono/irc/sno"
|
||||
"github.com/oragono/oragono/irc/utils"
|
||||
"github.com/tidwall/buntdb"
|
||||
)
|
||||
|
||||
@@ -57,6 +58,8 @@ type ListenerWrapper struct {
|
||||
// protects atomic update of config and shouldStop:
|
||||
sync.Mutex // tier 1
|
||||
listener net.Listener
|
||||
// optional WebSocket endpoint
|
||||
httpServer *http.Server
|
||||
config listenerConfig
|
||||
shouldStop bool
|
||||
}
|
||||
@@ -226,9 +229,101 @@ func (server *Server) checkTorLimits() (banned bool, message string) {
|
||||
|
||||
// createListener starts a given listener.
|
||||
func (server *Server) createListener(addr string, conf listenerConfig, bindMode os.FileMode) (*ListenerWrapper, error) {
|
||||
// make listener
|
||||
if conf.WebSocket {
|
||||
return server.createWSListener(addr, conf)
|
||||
}
|
||||
return server.createNetListener(addr, conf, bindMode)
|
||||
}
|
||||
|
||||
func (server *Server) isTrusted(ip string) bool {
|
||||
netIP := net.ParseIP(ip)
|
||||
return utils.IPInNets(netIP, server.Config().Server.proxyAllowedFromNets)
|
||||
}
|
||||
|
||||
func (server *Server) followHTTPForwards(addr string, forwards string) string {
|
||||
if !server.isTrusted(addr) {
|
||||
return addr
|
||||
}
|
||||
|
||||
forwardIPs := strings.Split(forwards, ",")
|
||||
|
||||
// Iterate backwards to have the inner-most proxy first.
|
||||
for i := len(forwardIPs) - 1; i >= 0; i-- {
|
||||
// Using i so that addr points to the last item after the end of the loop.
|
||||
addr = forwardIPs[i]
|
||||
|
||||
if !server.isTrusted(addr) {
|
||||
return addr
|
||||
}
|
||||
}
|
||||
|
||||
// All IPs are trusted? weird. Let's take the last one and call it a day.
|
||||
return addr
|
||||
}
|
||||
|
||||
// createWSListener starts a given WebSocket listener.
|
||||
func (server *Server) createWSListener(addr string, conf listenerConfig) (*ListenerWrapper, error) {
|
||||
var listener net.Listener
|
||||
var err error
|
||||
|
||||
handler := func(w http.ResponseWriter, r *http.Request) {
|
||||
remoteAddr := r.RemoteAddr
|
||||
if header, ok := r.Header["X-Forwarded-For"]; ok {
|
||||
remoteAddr = server.followHTTPForwards(remoteAddr, header[len(header)-1])
|
||||
}
|
||||
|
||||
conn, err := wsUpgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
server.logger.Error("internal", "upgrade error", addr, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
newConn := clientConn{
|
||||
Conn: WSContainer{conn},
|
||||
Config: conf,
|
||||
}
|
||||
|
||||
server.RunClient(newConn, "")
|
||||
}
|
||||
endpoint := http.Server{
|
||||
Addr: addr,
|
||||
Handler: http.HandlerFunc(handler),
|
||||
ReadTimeout: 10 * time.Second,
|
||||
WriteTimeout: 10 * time.Second,
|
||||
MaxHeaderBytes: 1 << 20,
|
||||
}
|
||||
if conf.TLSConfig != nil {
|
||||
listener, err = tls.Listen("tcp", addr, conf.TLSConfig)
|
||||
} else {
|
||||
listener, err = net.Listen("tcp", addr)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// throw our details to the server so we can be modified/killed later
|
||||
wrapper := ListenerWrapper{
|
||||
listener: listener,
|
||||
httpServer: &endpoint,
|
||||
config: conf,
|
||||
shouldStop: false,
|
||||
}
|
||||
|
||||
go func() {
|
||||
err := endpoint.Serve(listener)
|
||||
if err != nil {
|
||||
server.logger.Error("internal", "Failed to start WebSocket listener on", addr)
|
||||
}
|
||||
}()
|
||||
|
||||
return &wrapper, nil
|
||||
}
|
||||
|
||||
// createNetListener starts a given unix or TCP listener.
|
||||
func (server *Server) createNetListener(addr string, conf listenerConfig, bindMode os.FileMode) (*ListenerWrapper, error) {
|
||||
var listener net.Listener
|
||||
var err error
|
||||
|
||||
addr = strings.TrimPrefix(addr, "unix:")
|
||||
if strings.HasPrefix(addr, "/") {
|
||||
// https://stackoverflow.com/a/34881585
|
||||
@@ -815,7 +910,7 @@ func (server *Server) loadDatastore(config *Config) error {
|
||||
func (server *Server) setupListeners(config *Config) (err error) {
|
||||
logListener := func(addr string, config listenerConfig) {
|
||||
server.logger.Info("listeners",
|
||||
fmt.Sprintf("now listening on %s, tls=%t, tlsproxy=%t, tor=%t.", addr, (config.TLSConfig != nil), config.ProxyBeforeTLS, config.Tor),
|
||||
fmt.Sprintf("now listening on %s, tls=%t, tlsproxy=%t, tor=%t, websocket=%t.", addr, (config.TLSConfig != nil), config.ProxyBeforeTLS, config.Tor, config.WebSocket),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
package irc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"github.com/gorilla/websocket"
|
||||
"net/http"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
var wsUpgrader = websocket.Upgrader{
|
||||
ReadBufferSize: 2 * 1024,
|
||||
WriteBufferSize: 2 * 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 },
|
||||
}
|
||||
|
||||
// WSContainer wraps a WebSocket connection so that it implements net.Conn
|
||||
// entirely.
|
||||
type WSContainer struct {
|
||||
*websocket.Conn
|
||||
}
|
||||
|
||||
func (ws WSContainer) Read(b []byte) (n int, err error) {
|
||||
var messageType int
|
||||
var bytes []byte
|
||||
|
||||
for {
|
||||
messageType, bytes, err = ws.ReadMessage()
|
||||
if messageType == websocket.TextMessage {
|
||||
n = copy(b, bytes)
|
||||
return
|
||||
}
|
||||
if len(bytes) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
// Throw other kind of messages away.
|
||||
}
|
||||
// We don't want to return (0, nil) here because that would mean the
|
||||
// connection is closed (Read calls must block until data is received).
|
||||
}
|
||||
|
||||
func (ws WSContainer) Write(b []byte) (n int, err error) {
|
||||
if !utf8.Valid(b) {
|
||||
return 0, errors.New("outgoing WebSocket message isn't valid UTF-8")
|
||||
}
|
||||
|
||||
b = bytes.TrimSuffix(b, []byte("\r\n"))
|
||||
n = len(b)
|
||||
err = ws.WriteMessage(websocket.TextMessage, b)
|
||||
return
|
||||
}
|
||||
|
||||
// SetDeadline is part of the net.Conn interface.
|
||||
func (ws WSContainer) SetDeadline(t time.Time) (err error) {
|
||||
err = ws.SetWriteDeadline(t)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = ws.SetReadDeadline(t)
|
||||
return
|
||||
}
|
||||
Reference in New Issue
Block a user