diff options
Diffstat (limited to 'deprecated-webircgateway/pkg/webircgateway')
13 files changed, 3027 insertions, 0 deletions
diff --git a/deprecated-webircgateway/pkg/webircgateway/client.go b/deprecated-webircgateway/pkg/webircgateway/client.go new file mode 100644 index 0000000..43d3fe7 --- /dev/null +++ b/deprecated-webircgateway/pkg/webircgateway/client.go @@ -0,0 +1,741 @@ +package webircgateway + +import ( + "bufio" + "crypto/tls" + "errors" + "fmt" + "io" + "net" + "runtime/debug" + "strconv" + "strings" + "sync" + "sync/atomic" + "syscall" + "time" + + "golang.org/x/time/rate" + + "github.com/kiwiirc/webircgateway/pkg/dnsbl" + "github.com/kiwiirc/webircgateway/pkg/irc" + "github.com/kiwiirc/webircgateway/pkg/proxy" +) + +const ( + // ClientStateIdle - Client connected and just sat there + ClientStateIdle = "idle" + // ClientStateConnecting - Connecting upstream + ClientStateConnecting = "connecting" + // ClientStateRegistering - Registering to the IRC network + ClientStateRegistering = "registering" + // ClientStateConnected - Connected upstream + ClientStateConnected = "connected" + // ClientStateEnding - Client is ending its connection + ClientStateEnding = "ending" +) + +type ClientSignal [3]string + +// Client - Connecting client struct +type Client struct { + Gateway *Gateway + Id uint64 + State string + EndWG sync.WaitGroup + shuttingDownLock sync.Mutex + shuttingDown bool + SeenQuit bool + Recv chan string + ThrottledRecv *ThrottledStringChannel + upstream io.ReadWriteCloser + UpstreamRecv chan string + UpstreamSend chan string + UpstreamStarted bool + UpstreamConfig *ConfigUpstream + RemoteAddr string + RemoteHostname string + RemotePort int + DestHost string + DestPort int + DestTLS bool + IrcState *irc.State + Encoding string + // Tags get passed upstream via the WEBIRC command + Tags map[string]string + // Captchas may be needed to verify a client + RequiresVerification bool + Verified bool + SentPass bool + // Signals for the transport to make use of (data, connection state, etc) + Signals chan ClientSignal + Features struct { + Messagetags bool + Metadata bool + ExtJwt bool + } + // The specific message-tags CAP that the client has requested if we are wrapping it + RequestedMessageTagsCap string + // Prefix used by the server when sending its own messages + ServerMessagePrefix irc.Mask +} + +var nextClientID uint64 = 1 + +// NewClient - Makes a new client +func NewClient(gateway *Gateway) *Client { + thisID := atomic.AddUint64(&nextClientID, 1) + + recv := make(chan string, 50) + c := &Client{ + Gateway: gateway, + Id: thisID, + State: ClientStateIdle, + Recv: recv, + ThrottledRecv: NewThrottledStringChannel(recv, rate.NewLimiter(rate.Inf, 1)), + UpstreamSend: make(chan string, 50), + UpstreamRecv: make(chan string, 50), + Encoding: "UTF-8", + Signals: make(chan ClientSignal, 50), + Tags: make(map[string]string), + IrcState: irc.NewState(), + UpstreamConfig: &ConfigUpstream{}, + } + + // Auto enable some features by default. They may be disabled later on + c.Features.ExtJwt = true + + c.RequiresVerification = gateway.Config.RequiresVerification + + // Handles data to/from the client and upstreams + go c.clientLineWorker() + + // This Add(1) will be ended once the client starts shutting down in StartShutdown() + c.EndWG.Add(1) + + // Add to the clients maps and wait until everything has been marked + // as completed (several routines add themselves to EndWG so that we can catch + // when they are all completed) + gateway.Clients.Set(strconv.FormatUint(c.Id, 10), c) + go func() { + c.EndWG.Wait() + gateway.Clients.Remove(strconv.FormatUint(c.Id, 10)) + + hook := &HookClientState{ + Client: c, + Connected: false, + } + hook.Dispatch("client.state") + }() + + hook := &HookClientState{ + Client: c, + Connected: true, + } + hook.Dispatch("client.state") + + return c +} + +// Log - Log a line of text with context of this client +func (c *Client) Log(level int, format string, args ...interface{}) { + prefix := fmt.Sprintf("client:%d ", c.Id) + c.Gateway.Log(level, prefix+format, args...) +} + +// TrafficLog - Log out raw IRC traffic +func (c *Client) TrafficLog(isUpstream bool, toGateway bool, traffic string) { + label := "" + if isUpstream && toGateway { + label = "Upstream->" + } else if isUpstream && !toGateway { + label = "->Upstream" + } else if !isUpstream && toGateway { + label = "Client->" + } else if !isUpstream && !toGateway { + label = "->Client" + } + c.Log(1, "Traffic (%s) %s", label, traffic) +} + +func (c *Client) Ready() { + dnsblAction := c.Gateway.Config.DnsblAction + validAction := dnsblAction == "verify" || dnsblAction == "deny" + dnsblTookAction := "" + + if len(c.Gateway.Config.DnsblServers) > 0 && c.RemoteAddr != "" && !c.Verified && validAction { + dnsblTookAction = c.checkDnsBl() + } + + if dnsblTookAction == "" && c.Gateway.Config.RequiresVerification && !c.Verified { + c.SendClientSignal("data", "CAPTCHA NEEDED") + } +} + +func (c *Client) checkDnsBl() (tookAction string) { + dnsResult := dnsbl.Lookup(c.Gateway.Config.DnsblServers, c.RemoteAddr) + if dnsResult.Listed && c.Gateway.Config.DnsblAction == "deny" { + c.SendIrcError("Blocked by DNSBL") + c.SendClientSignal("state", "closed", "dnsbl_listed") + c.StartShutdown("dnsbl") + tookAction = "deny" + } else if dnsResult.Listed && c.Gateway.Config.DnsblAction == "verify" { + c.RequiresVerification = true + c.SendClientSignal("data", "CAPTCHA NEEDED") + tookAction = "verify" + } + + return +} + +func (c *Client) IsShuttingDown() bool { + c.shuttingDownLock.Lock() + defer c.shuttingDownLock.Unlock() + return c.shuttingDown +} + +func (c *Client) StartShutdown(reason string) { + c.shuttingDownLock.Lock() + defer c.shuttingDownLock.Unlock() + + c.Log(1, "StartShutdown(%s) ShuttingDown=%t", reason, c.shuttingDown) + if !c.shuttingDown { + c.shuttingDown = true + c.State = ClientStateEnding + + switch reason { + case "upstream_closed": + c.Log(2, "Upstream closed the connection") + case "err_connecting_upstream": + case "err_no_upstream": + // Error has been logged already + case "client_closed": + c.Log(2, "Client disconnected") + default: + c.Log(2, "Closed: %s", reason) + } + + close(c.Signals) + c.EndWG.Done() + } +} + +func (c *Client) SendClientSignal(signal string, args ...string) { + c.shuttingDownLock.Lock() + defer c.shuttingDownLock.Unlock() + + if !c.shuttingDown { + switch len(args) { + case 0: + c.Signals <- ClientSignal{signal} + case 1: + c.Signals <- ClientSignal{signal, args[0]} + case 2: + c.Signals <- ClientSignal{signal, args[0], args[1]} + } + } +} + +func (c *Client) SendIrcError(message string) { + c.SendClientSignal("data", "ERROR :"+message) +} + +func (c *Client) SendIrcFail(params ...string) { + failMessage := irc.Message{ + Command: "FAIL", + Params: params, + } + c.SendClientSignal("data", failMessage.ToLine()) +} + +func (c *Client) connectUpstream() { + client := c + + c.UpstreamStarted = true + + var upstreamConfig ConfigUpstream + + if client.DestHost == "" { + client.Log(2, "Using configured upstream") + var err error + upstreamConfig, err = c.Gateway.findUpstream() + if err != nil { + client.Log(3, "No upstreams available") + client.SendIrcError("The server has not been configured") + client.StartShutdown("err_no_upstream") + return + } + } else { + if !c.Gateway.isIrcAddressAllowed(client.DestHost) { + client.Log(2, "Server %s is not allowed. Closing connection", client.DestHost) + client.SendIrcError("Not allowed to connect to " + client.DestHost) + client.SendClientSignal("state", "closed", "err_forbidden") + client.StartShutdown("err_no_upstream") + return + } + + client.Log(2, "Using client given upstream") + upstreamConfig = c.configureUpstream() + } + + c.UpstreamConfig = &upstreamConfig + + hook := &HookIrcConnectionPre{ + Client: client, + UpstreamConfig: &upstreamConfig, + } + hook.Dispatch("irc.connection.pre") + if hook.Halt { + client.SendClientSignal("state", "closed", "err_forbidden") + client.StartShutdown("err_connecting_upstream") + return + } + + client.State = ClientStateConnecting + + upstream, upstreamErr := client.makeUpstreamConnection() + if upstreamErr != nil { + // Error handling was already managed in makeUpstreamConnection() + return + } + + client.State = ClientStateRegistering + + client.upstream = upstream + client.readUpstream() + client.writeWebircLines(upstream) + client.maybeSendPass(upstream) + client.SendClientSignal("state", "connected") +} + +func (c *Client) makeUpstreamConnection() (io.ReadWriteCloser, error) { + client := c + upstreamConfig := c.UpstreamConfig + + var connection io.ReadWriteCloser + + if upstreamConfig.Proxy == nil { + // Connect directly to the IRCd + dialer := net.Dialer{} + dialer.Timeout = time.Second * time.Duration(upstreamConfig.Timeout) + + if upstreamConfig.LocalAddr != "" { + parsedIP := net.ParseIP(upstreamConfig.LocalAddr) + if parsedIP != nil { + dialer.LocalAddr = &net.TCPAddr{ + IP: parsedIP, + Port: 0, + } + } else { + client.Log(3, "Failed to parse localaddr for upstream connection \"%s\"", upstreamConfig.LocalAddr) + } + } + + var conn net.Conn + var connErr error + if upstreamConfig.Protocol == "unix" { + conn, connErr = dialer.Dial("unix", upstreamConfig.Hostname) + } else { + upstreamStr := fmt.Sprintf("%s:%d", upstreamConfig.Hostname, upstreamConfig.Port) + conn, connErr = dialer.Dial(upstreamConfig.Protocol, upstreamStr) + } + + if connErr != nil { + client.Log(3, "Error connecting to the upstream IRCd. %s", connErr.Error()) + errString := "" + if errString = typeOfErr(connErr); errString != "" { + errString = "err_" + errString + } + client.SendClientSignal("state", "closed", errString) + client.StartShutdown("err_connecting_upstream") + return nil, errors.New("error connecting upstream") + } + + // Add the ports into the identd before possible TLS handshaking. If we do it after then + // there's a good chance the identd lookup will occur before the handshake has finished + if c.Gateway.Config.Identd { + // Keep track of the upstreams local and remote port numbers + _, lPortStr, _ := net.SplitHostPort(conn.LocalAddr().String()) + client.IrcState.LocalPort, _ = strconv.Atoi(lPortStr) + _, rPortStr, _ := net.SplitHostPort(conn.RemoteAddr().String()) + client.IrcState.RemotePort, _ = strconv.Atoi(rPortStr) + + c.Gateway.identdServ.AddIdent(client.IrcState.LocalPort, client.IrcState.RemotePort, client.IrcState.Username, "") + } + + if upstreamConfig.TLS { + tlsConfig := &tls.Config{InsecureSkipVerify: true} + tlsConn := tls.Client(conn, tlsConfig) + err := tlsConn.Handshake() + if err != nil { + client.Log(3, "Error connecting to the upstream IRCd. %s", err.Error()) + client.SendClientSignal("state", "closed", "err_tls") + client.StartShutdown("err_connecting_upstream") + return nil, errors.New("error connecting upstream") + } + + conn = net.Conn(tlsConn) + } + + connection = conn + } + + if upstreamConfig.Proxy != nil { + // Connect to the IRCd via a proxy + conn := proxy.MakeKiwiProxyConnection() + conn.DestHost = upstreamConfig.Hostname + conn.DestPort = upstreamConfig.Port + conn.DestTLS = upstreamConfig.TLS + conn.Username = upstreamConfig.Proxy.Username + conn.ProxyInterface = upstreamConfig.Proxy.Interface + + dialErr := conn.Dial(fmt.Sprintf( + "%s:%d", + upstreamConfig.Proxy.Hostname, + upstreamConfig.Proxy.Port, + )) + + if dialErr != nil { + errString := "" + if errString = typeOfErr(dialErr); errString != "" { + errString = "err_" + errString + } else { + errString = "err_proxy" + } + client.Log(3, + "Error connecting to the kiwi proxy, %s:%d. %s", + upstreamConfig.Proxy.Hostname, + upstreamConfig.Proxy.Port, + dialErr.Error(), + ) + + client.SendClientSignal("state", "closed", errString) + client.StartShutdown("err_connecting_upstream") + return nil, errors.New("error connecting upstream") + } + + connection = conn + } + + return connection, nil +} + +func (c *Client) writeWebircLines(upstream io.ReadWriteCloser) { + // Send any WEBIRC lines + if c.UpstreamConfig.WebircPassword == "" { + c.Log(1, "No webirc to send") + return + } + + gatewayName := "webircgateway" + if c.Gateway.Config.GatewayName != "" { + gatewayName = c.Gateway.Config.GatewayName + } + if c.UpstreamConfig.GatewayName != "" { + gatewayName = c.UpstreamConfig.GatewayName + } + + webircTags := c.buildWebircTags() + if strings.Contains(webircTags, " ") { + webircTags = ":" + webircTags + } + + clientHostname := c.RemoteHostname + if c.Gateway.Config.ClientHostname != "" { + clientHostname = makeClientReplacements(c.Gateway.Config.ClientHostname, c) + } + + remoteAddr := c.RemoteAddr + // Prefix IPv6 addresses that start with a : so they can be sent as an individual IRC + // parameter. eg. ::1 would not parse correctly as a parameter, while 0::1 will + if strings.HasPrefix(remoteAddr, ":") { + remoteAddr = "0" + remoteAddr + } + + webircLine := fmt.Sprintf( + "WEBIRC %s %s %s %s %s\n", + c.UpstreamConfig.WebircPassword, + gatewayName, + clientHostname, + remoteAddr, + webircTags, + ) + c.Log(1, "->upstream: %s", webircLine) + upstream.Write([]byte(webircLine)) +} + +func (c *Client) maybeSendPass(upstream io.ReadWriteCloser) { + if c.UpstreamConfig.ServerPassword == "" { + return + } + c.SentPass = true + passLine := fmt.Sprintf( + "PASS %s\n", + c.UpstreamConfig.ServerPassword, + ) + c.Log(1, "->upstream: %s", passLine) + upstream.Write([]byte(passLine)) +} + +func (c *Client) processLineToUpstream(data string) { + client := c + upstreamConfig := c.UpstreamConfig + + if strings.HasPrefix(data, "PASS ") && c.SentPass { + // Hijack the PASS command if we already sent a pass command + return + } else if strings.HasPrefix(data, "USER ") { + // Hijack the USER command as we may have some overrides + data = fmt.Sprintf( + "USER %s 0 * :%s", + client.IrcState.Username, + client.IrcState.RealName, + ) + } else if strings.HasPrefix(strings.ToUpper(data), "QUIT ") { + client.SeenQuit = true + } + + message, _ := irc.ParseLine(data) + + hook := &HookIrcLine{ + Client: client, + UpstreamConfig: upstreamConfig, + Line: data, + Message: message, + ToServer: true, + } + hook.Dispatch("irc.line") + if hook.Halt { + return + } + + // Plugins may have modified the data + data = hook.Line + + c.TrafficLog(true, false, data) + data = utf8ToOther(data, client.Encoding) + if data == "" { + client.Log(1, "Failed to encode into '%s'. Dropping data", c.Encoding) + return + } + + if client.upstream != nil { + client.upstream.Write([]byte(data + "\r\n")) + } else { + client.Log(2, "Tried sending data upstream before connected") + } +} + +func (c *Client) handleLineFromUpstream(data string) { + client := c + upstreamConfig := c.UpstreamConfig + + message, _ := irc.ParseLine(data) + + hook := &HookIrcLine{ + Client: client, + UpstreamConfig: upstreamConfig, + Line: data, + Message: message, + ToServer: false, + } + hook.Dispatch("irc.line") + if hook.Halt { + return + } + + // Plugins may have modified the data + data = hook.Line + + if data == "" { + return + } + + data = ensureUtf8(data, client.Encoding) + if data == "" { + client.Log(1, "Failed to decode as 'UTF-8'. Dropping data") + return + } + + data = client.ProcessLineFromUpstream(data) + if data == "" { + return + } + + client.SendClientSignal("data", data) +} + +func typeOfErr(err error) string { + if err == nil { + return "" + } + + if netError, ok := err.(net.Error); ok && netError.Timeout() { + return "timeout" + } + + switch t := err.(type) { + case *proxy.ConnError: + switch t.Type { + case "conn_reset": + return "" + case "conn_refused": + return "refused" + case "not_found": + return "unknown_host" + case "conn_timeout": + return "timeout" + default: + return "" + } + + case *net.OpError: + if t.Op == "dial" { + return "unknown_host" + } else if t.Op == "read" { + return "refused" + } + + case syscall.Errno: + if t == syscall.ECONNREFUSED { + return "refused" + } + } + + return "" +} + +func (c *Client) readUpstream() { + client := c + + // Data from upstream to client + go func() { + reader := bufio.NewReader(client.upstream) + for { + data, err := reader.ReadString('\n') + if err != nil { + break + } + + data = strings.Trim(data, "\n\r") + client.UpstreamRecv <- data + } + + close(client.UpstreamRecv) + client.upstream.Close() + client.upstream = nil + + if client.IrcState.RemotePort > 0 { + c.Gateway.identdServ.RemoveIdent(client.IrcState.LocalPort, client.IrcState.RemotePort, "") + } + }() +} + +// Handle lines sent from the client +func (c *Client) clientLineWorker() { + for { + shouldQuit, _ := c.handleDataLine() + if shouldQuit { + break + } + + } + + c.Log(1, "leaving clientLineWorker") +} + +func (c *Client) handleDataLine() (shouldQuit bool, hadErr bool) { + defer func() { + if err := recover(); err != nil { + c.Log(3, fmt.Sprint("Error handling data ", err)) + fmt.Println("Error handling data", err) + debug.PrintStack() + shouldQuit = false + hadErr = true + } + }() + + // We only want to send data upstream if we have an upstream connection + upstreamSend := c.UpstreamSend + if c.upstream == nil { + upstreamSend = nil + } + + select { + case clientData, ok := <-c.ThrottledRecv.Output: + if !ok { + c.Log(1, "client.Recv closed") + if !c.SeenQuit && c.Gateway.Config.SendQuitOnClientClose != "" && c.State == ClientStateEnding { + c.processLineToUpstream("QUIT :" + c.Gateway.Config.SendQuitOnClientClose) + } + + c.StartShutdown("client_closed") + + if c.upstream != nil { + c.upstream.Close() + } + return true, false + } + c.Log(1, "in c.ThrottledRecv.Output") + c.TrafficLog(false, true, clientData) + + clientLine, err := c.ProcessLineFromClient(clientData) + if err == nil && clientLine != "" { + c.UpstreamSend <- clientLine + } + + case line, ok := <-upstreamSend: + if !ok { + c.Log(1, "client.UpstreamSend closed") + return true, false + } + c.Log(1, "in .UpstreamSend") + c.processLineToUpstream(line) + + case upstreamData, ok := <-c.UpstreamRecv: + if !ok { + c.Log(1, "client.UpstreamRecv closed") + c.SendClientSignal("state", "closed") + c.StartShutdown("upstream_closed") + return true, false + } + c.Log(1, "in .UpstreamRecv") + c.TrafficLog(true, true, upstreamData) + + c.handleLineFromUpstream(upstreamData) + } + + return false, false +} + +// configureUpstream - Generate an upstream configuration from the information set on the client instance +func (c *Client) configureUpstream() ConfigUpstream { + upstreamConfig := ConfigUpstream{} + upstreamConfig.Hostname = c.DestHost + upstreamConfig.Port = c.DestPort + upstreamConfig.TLS = c.DestTLS + upstreamConfig.Timeout = c.Gateway.Config.GatewayTimeout + upstreamConfig.Throttle = c.Gateway.Config.GatewayThrottle + upstreamConfig.WebircPassword = c.Gateway.findWebircPassword(c.DestHost) + upstreamConfig.Protocol = c.Gateway.Config.GatewayProtocol + upstreamConfig.LocalAddr = c.Gateway.Config.GatewayLocalAddr + + return upstreamConfig +} + +func (c *Client) buildWebircTags() string { + str := "" + for key, val := range c.Tags { + if str != "" { + str += " " + } + + if val == "" { + str += key + } else { + str += key + "=" + val + } + } + + return str +} diff --git a/deprecated-webircgateway/pkg/webircgateway/client_command_handlers.go b/deprecated-webircgateway/pkg/webircgateway/client_command_handlers.go new file mode 100644 index 0000000..d5d1fcc --- /dev/null +++ b/deprecated-webircgateway/pkg/webircgateway/client_command_handlers.go @@ -0,0 +1,495 @@ +package webircgateway + +import ( + "errors" + "strconv" + "strings" + "time" + + "github.com/golang-jwt/jwt/v4" + "github.com/kiwiirc/webircgateway/pkg/irc" + "github.com/kiwiirc/webircgateway/pkg/recaptcha" + "golang.org/x/net/html/charset" + "golang.org/x/time/rate" +) + +var MAX_EXTJWT_SIZE = 200 + +/* + * ProcessLineFromUpstream + * Processes and makes any changes to a line of data sent from an upstream + */ +func (c *Client) ProcessLineFromUpstream(data string) string { + client := c + + m, parseErr := irc.ParseLine(data) + if parseErr != nil { + return data + } + + pLen := len(m.Params) + + if pLen > 0 && m.Command == "NICK" && m.Prefix.Nick == c.IrcState.Nick { + client.IrcState.Nick = m.Params[0] + } + if pLen > 0 && m.Command == "001" { + client.IrcState.Nick = m.Params[0] + client.State = ClientStateConnected + client.ServerMessagePrefix = *m.Prefix + + // Throttle writes if configured, but only after registration is complete. Typical IRCd + // behavior is to not throttle registration commands. + client.ThrottledRecv.Limiter = rate.NewLimiter(rate.Limit(client.UpstreamConfig.Throttle), 1) + } + if pLen > 0 && m.Command == "005" { + tokenPairs := m.Params[1 : pLen-1] + iSupport := c.IrcState.ISupport + iSupport.Received = true + iSupport.Tags = m.Tags + iSupport.AddTokens(tokenPairs) + } + if c.IrcState.ISupport.Received && !c.IrcState.ISupport.Injected && m.Command != "005" { + iSupport := c.IrcState.ISupport + iSupport.Injected = true + + msg := irc.NewMessage() + msg.Command = "005" + msg.Prefix = &c.ServerMessagePrefix + msg.Params = append(msg.Params, c.IrcState.Nick) + + if iSupport.HasToken("EXTJWT") { + c.Log(1, "Upstream already supports EXTJWT, disabling feature") + c.Features.ExtJwt = false + } else { + // Add EXTJWT ISupport token + msg.Params = append(msg.Params, "EXTJWT=1") + iSupport.AddToken("EXTJWT=1") + } + + msg.Params = append(msg.Params, "are supported by this server") + if timeTag, ok := c.IrcState.ISupport.Tags["time"]; ok { + msg.Tags["time"] = timeTag + } + if len(msg.Params) > 2 { + // Extra tokens were added, send the line + c.SendClientSignal("data", msg.ToLine()) + } + } + if pLen > 0 && m.Command == "JOIN" && m.Prefix.Nick == c.IrcState.Nick { + channel := irc.NewStateChannel(m.GetParam(0, "")) + c.IrcState.SetChannel(channel) + } + if pLen > 0 && m.Command == "PART" && m.Prefix.Nick == c.IrcState.Nick { + c.IrcState.RemoveChannel(m.GetParam(0, "")) + } + if pLen > 0 && m.Command == "QUIT" && m.Prefix.Nick == c.IrcState.Nick { + c.IrcState.ClearChannels() + } + // :server.com 900 m m!m@irc-3jg.1ab.j4ep8h.IP prawnsalad :You are now logged in as prawnsalad + if pLen > 0 && m.Command == "900" { + c.IrcState.Account = m.GetParam(2, "") + } + // :server.com 901 itsonlybinary itsonlybinary!itsonlybina@user/itsonlybinary :You are now logged out + if m.Command == "901" { + c.IrcState.Account = "" + } + // :prawnsalad!prawn@kiwiirc/prawnsalad MODE #kiwiirc-dev +oo notprawn kiwi-n75 + if pLen > 0 && m.Command == "MODE" { + if strings.HasPrefix(m.GetParam(0, ""), "#") { + channelName := m.GetParam(0, "") + modes := m.GetParam(1, "") + + channel := c.IrcState.GetChannel(channelName) + if channel != nil { + channel = irc.NewStateChannel(channelName) + c.IrcState.SetChannel(channel) + } + + adding := false + paramIdx := 1 + for i := 0; i < len(modes); i++ { + mode := string(modes[i]) + + if mode == "+" { + adding = true + } else if mode == "-" { + adding = false + } else { + paramIdx++ + param := m.GetParam(paramIdx, "") + if strings.EqualFold(param, c.IrcState.Nick) { + if adding { + channel.Modes[mode] = "" + } else { + delete(channel.Modes, mode) + } + } + } + } + } + } + + // If upstream reports that it supports message-tags natively, disable the wrapping of this feature for + // this client + if pLen >= 3 && + strings.ToUpper(m.Command) == "CAP" && + m.GetParamU(1, "") == "LS" { + // The CAPs could be param 2 or 3 depending on if were using multiple lines to list them all. + caps := "" + if pLen >= 4 && m.Params[2] == "*" { + caps = m.GetParamU(3, "") + } else { + caps = m.GetParamU(2, "") + } + + if containsOneOf(caps, []string{"DRAFT/MESSAGE-TAGS-0.2", "MESSAGE-TAGS"}) { + c.Log(1, "Upstream already supports Messagetags, disabling feature") + c.Features.Messagetags = false + } + + // Inject message-tags cap into the last line of IRCd capabilities + if c.Features.Messagetags && m.Params[2] != "*" { + m.Params[2] += " message-tags" + data = m.ToLine() + } + } + + // If we requested message-tags, make sure to include it in the ACK when + // the IRCd sends the ACK through + if m != nil && + client.RequestedMessageTagsCap != "" && + strings.ToUpper(m.Command) == "CAP" && + m.GetParamU(1, "") == "ACK" && + !strings.Contains(m.GetParamU(2, ""), "MESSAGE-TAGS") { + + m.Params[2] += " " + client.RequestedMessageTagsCap + data = m.ToLine() + + client.RequestedMessageTagsCap = "" + } + + if m != nil && client.Features.Messagetags && c.Gateway.messageTags.CanMessageContainClientTags(m) { + // If we have any message tags stored for this message from a previous PRIVMSG sent + // by a client, add them back in + mTags, mTagsExists := c.Gateway.messageTags.GetTagsFromMessage(client, m.Prefix.Nick, m) + if mTagsExists { + for k, v := range mTags.Tags { + m.Tags[k] = v + } + + data = m.ToLine() + } + } + + return data +} + +/* + * ProcessLineFromClient + * Processes and makes any changes to a line of data sent from a client + */ +func (c *Client) ProcessLineFromClient(line string) (string, error) { + message, err := irc.ParseLine(line) + // Just pass any random data upstream + if err != nil { + return line, nil + } + + maybeConnectUpstream := func() { + verified := false + if c.RequiresVerification && !c.Verified { + verified = false + } else { + verified = true + } + + if !c.UpstreamStarted && c.IrcState.Username != "" && c.IrcState.Nick != "" && verified { + c.connectUpstream() + } + } + + if !c.Verified && strings.ToUpper(message.Command) == "CAPTCHA" { + verified := false + if len(message.Params) >= 1 { + captcha := recaptcha.R{ + URL: c.Gateway.Config.ReCaptchaURL, + Secret: c.Gateway.Config.ReCaptchaSecret, + } + + verified = captcha.VerifyResponse(message.Params[0]) + } + + if !verified { + c.SendIrcError("Invalid captcha") + c.SendClientSignal("state", "closed", "bad_captcha") + c.StartShutdown("unverifed") + } else { + c.Verified = true + maybeConnectUpstream() + } + + return "", nil + } + + // NICK <nickname> + if strings.ToUpper(message.Command) == "NICK" && !c.UpstreamStarted { + if len(message.Params) > 0 { + c.IrcState.Nick = message.Params[0] + } + + if !c.UpstreamStarted { + maybeConnectUpstream() + } + } + + // USER <username> <hostname> <servername> <realname> + if strings.ToUpper(message.Command) == "USER" && !c.UpstreamStarted { + if len(message.Params) < 4 { + return line, errors.New("Invalid USER line") + } + + if c.Gateway.Config.ClientUsername != "" { + message.Params[0] = makeClientReplacements(c.Gateway.Config.ClientUsername, c) + } + if c.Gateway.Config.ClientRealname != "" { + message.Params[3] = makeClientReplacements(c.Gateway.Config.ClientRealname, c) + } + + line = message.ToLine() + + c.IrcState.Username = message.Params[0] + c.IrcState.RealName = message.Params[3] + + maybeConnectUpstream() + } + + if strings.ToUpper(message.Command) == "ENCODING" { + if len(message.Params) > 0 { + encoding, _ := charset.Lookup(message.Params[0]) + if encoding == nil { + c.Log(1, "Requested unknown encoding, %s", message.Params[0]) + } else { + c.Encoding = message.Params[0] + c.Log(1, "Set encoding to %s", message.Params[0]) + } + } + + // Don't send the ENCODING command upstream + return "", nil + } + + if strings.ToUpper(message.Command) == "HOST" && !c.UpstreamStarted { + // HOST irc.network.net:6667 + // HOST irc.network.net:+6667 + + if !c.Gateway.Config.Gateway { + return "", nil + } + + if len(message.Params) == 0 { + return "", nil + } + + addr := message.Params[0] + if addr == "" { + c.SendIrcError("Missing host") + c.StartShutdown("missing_host") + return "", nil + } + + // Parse host:+port into the c.dest* vars + portSep := strings.LastIndex(addr, ":") + if portSep == -1 { + c.DestHost = addr + c.DestPort = 6667 + c.DestTLS = false + } else { + c.DestHost = addr[0:portSep] + portParam := addr[portSep+1:] + if len(portParam) > 0 && portParam[0:1] == "+" { + c.DestTLS = true + c.DestPort, err = strconv.Atoi(portParam[1:]) + if err != nil { + c.DestPort = 6697 + } + } else { + c.DestPort, err = strconv.Atoi(portParam[0:]) + if err != nil { + c.DestPort = 6667 + } + } + } + + // Don't send the HOST command upstream + return "", nil + } + + // If the client supports CAP, assume the client also supports parsing MessageTags + // When upstream replies with its CAP listing, we check if message-tags is supported by the IRCd already and if so, + // we disable this feature flag again to use the IRCds native support. + if strings.ToUpper(message.Command) == "CAP" && len(message.Params) > 0 && strings.ToUpper(message.Params[0]) == "LS" { + c.Log(1, "Enabling client Messagetags feature") + c.Features.Messagetags = true + } + + // If we are wrapping the Messagetags feature, make sure the clients REQ message-tags doesn't + // get sent upstream + if c.Features.Messagetags && strings.ToUpper(message.Command) == "CAP" && message.GetParamU(0, "") == "REQ" { + reqCaps := strings.ToLower(message.GetParam(1, "")) + capsThatEnableMessageTags := []string{"message-tags", "account-tag", "server-time", "batch"} + + if strings.Contains(reqCaps, "message-tags") { + // Rebuild the list of requested caps, without message-tags + caps := strings.Split(reqCaps, " ") + newCaps := []string{} + for _, cap := range caps { + if !strings.Contains(strings.ToLower(cap), "message-tags") { + newCaps = append(newCaps, cap) + } else { + c.RequestedMessageTagsCap = cap + } + } + + if len(newCaps) == 0 { + // The only requested CAP was our emulated message-tags + // the server will not be sending an ACK so we need to send our own + c.SendClientSignal("data", "CAP * ACK :"+c.RequestedMessageTagsCap) + return "", nil + } + message.Params[1] = strings.Join(newCaps, " ") + line = message.ToLine() + } else if !containsOneOf(reqCaps, capsThatEnableMessageTags) { + // Didn't request anything that needs message-tags cap so disable it + c.Features.Messagetags = false + } + } + + if c.Features.Messagetags && message.Command == "TAGMSG" { + if len(message.Params) == 0 { + return "", nil + } + + // We can't be 100% sure what this users correct mask is, so just send the nick + message.Prefix.Nick = c.IrcState.Nick + message.Prefix.Hostname = "" + message.Prefix.Username = "" + + thisHost := strings.ToLower(c.UpstreamConfig.Hostname) + target := message.Params[0] + for val := range c.Gateway.Clients.IterBuffered() { + curClient := val.Val.(*Client) + sameHost := strings.ToLower(curClient.UpstreamConfig.Hostname) == thisHost + if !sameHost { + continue + } + + // Only send the message on to either the target nick, or the clients in a set channel + if !strings.EqualFold(target, curClient.IrcState.Nick) && !curClient.IrcState.HasChannel(target) { + continue + } + + curClient.SendClientSignal("data", message.ToLine()) + } + + return "", nil + } + + // Check for any client message tags so that we can store them for replaying to other clients + if c.Features.Messagetags && c.Gateway.messageTags.CanMessageContainClientTags(message) { + c.Gateway.messageTags.AddTagsFromMessage(c, c.IrcState.Nick, message) + // Prevent any client tags heading upstream + for k := range message.Tags { + if len(k) > 0 && k[0] == '+' { + delete(message.Tags, k) + } + } + + line = message.ToLine() + } + + if c.Features.ExtJwt && strings.ToUpper(message.Command) == "EXTJWT" { + tokenTarget := message.GetParam(0, "") + tokenService := message.GetParam(1, "") + + tokenM := irc.Message{} + tokenM.Command = "EXTJWT" + tokenM.Prefix = &c.ServerMessagePrefix + tokenData := jwt.MapClaims{ + "exp": time.Now().UTC().Add(1 * time.Minute).Unix(), + "iss": c.UpstreamConfig.Hostname, + "sub": c.IrcState.Nick, + "account": c.IrcState.Account, + "umodes": []string{}, + + // Channel specific claims + "channel": "", + "joined": 0, + "cmodes": []string{}, + } + + // Use the NetworkCommonAddress if a plugin as assigned one. + // This allows plugins to associate different upstream hosts to the same network + if c.UpstreamConfig.NetworkCommonAddress != "" { + tokenData["iss"] = c.UpstreamConfig.NetworkCommonAddress + } + + if tokenTarget == "" || tokenTarget == "*" { + tokenM.Params = append(tokenM.Params, "*") + } else { + targetChan := c.IrcState.GetChannel(tokenTarget) + if targetChan == nil { + // Channel does not exist in IRC State, send so such channel message + failMessage := irc.Message{ + Command: "403", // ERR_NOSUCHCHANNEL + Prefix: &c.ServerMessagePrefix, + Params: []string{c.IrcState.Nick, tokenTarget, "No such channel"}, + } + c.SendClientSignal("data", failMessage.ToLine()) + return "", nil + } + + tokenM.Params = append(tokenM.Params, tokenTarget) + + tokenData["channel"] = targetChan.Name + tokenData["joined"] = targetChan.Joined.Unix() + + modes := []string{} + for mode := range targetChan.Modes { + modes = append(modes, mode) + } + tokenData["cmodes"] = modes + } + + if tokenService == "" || tokenService == "*" { + tokenM.Params = append(tokenM.Params, "*") + } else { + c.SendIrcFail("EXTJWT", "NO_SUCH_SERVICE", "No such service") + return "", nil + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, tokenData) + tokenSigned, tokenSignedErr := token.SignedString([]byte(c.Gateway.Config.Secret)) + if tokenSignedErr != nil { + c.Log(3, "Error creating JWT token. %s", tokenSignedErr.Error()) + c.SendIrcFail("EXTJWT", "UNKNOWN_ERROR", "Failed to generate token") + return "", nil + } + + // Spit token if it exceeds max length + for len(tokenSigned) > MAX_EXTJWT_SIZE { + tokenSignedPart := tokenSigned[:MAX_EXTJWT_SIZE] + tokenSigned = tokenSigned[MAX_EXTJWT_SIZE:] + + tokenPartM := tokenM + tokenPartM.Params = append(tokenPartM.Params, "*", tokenSignedPart) + c.SendClientSignal("data", tokenPartM.ToLine()) + } + + tokenM.Params = append(tokenM.Params, tokenSigned) + c.SendClientSignal("data", tokenM.ToLine()) + + return "", nil + } + + return line, nil +} diff --git a/deprecated-webircgateway/pkg/webircgateway/config.go b/deprecated-webircgateway/pkg/webircgateway/config.go new file mode 100644 index 0000000..019d955 --- /dev/null +++ b/deprecated-webircgateway/pkg/webircgateway/config.go @@ -0,0 +1,385 @@ +package webircgateway + +import ( + "errors" + "net" + "os" + "os/exec" + "path/filepath" + "strconv" + "strings" + + "github.com/gobwas/glob" + "gopkg.in/ini.v1" +) + +// ConfigUpstream - An upstream config +type ConfigUpstream struct { + // Plugins may assign an arbitary address to an upstream network + NetworkCommonAddress string + Hostname string + Port int + TLS bool + Timeout int + Throttle int + WebircPassword string + ServerPassword string + GatewayName string + Proxy *ConfigProxy + Protocol string + LocalAddr string +} + +// ConfigServer - A web server config +type ConfigServer struct { + LocalAddr string + BindMode os.FileMode + Port int + TLS bool + CertFile string + KeyFile string + LetsEncryptCacheDir string +} + +type ConfigProxy struct { + Type string + Hostname string + Port int + TLS bool + Username string + Interface string +} + +// Config - Config options for the running app +type Config struct { + gateway *Gateway + ConfigFile string + LogLevel int + Gateway bool + GatewayName string + GatewayWhitelist []glob.Glob + GatewayThrottle int + GatewayTimeout int + GatewayWebircPassword map[string]string + GatewayProtocol string + GatewayLocalAddr string + Proxy ConfigServer + Upstreams []ConfigUpstream + Servers []ConfigServer + ServerTransports []string + RemoteOrigins []glob.Glob + ReverseProxies []net.IPNet + Webroot string + ClientRealname string + ClientUsername string + ClientHostname string + Identd bool + RequiresVerification bool + SendQuitOnClientClose string + ReCaptchaURL string + ReCaptchaSecret string + ReCaptchaKey string + Secret string + Plugins []string + DnsblServers []string + // DnsblAction - "deny" = deny the connection. "verify" = require verification + DnsblAction string +} + +func NewConfig(gateway *Gateway) *Config { + return &Config{gateway: gateway} +} + +// ConfigResolvePath - If relative, resolve a path to it's full absolute path relative to the config file +func (c *Config) ResolvePath(path string) string { + // Absolute paths should stay as they are + if path[0:1] == "/" { + return path + } + + resolved := filepath.Dir(c.ConfigFile) + resolved = filepath.Clean(resolved + "/" + path) + return resolved +} + +func (c *Config) SetConfigFile(configFile string) { + // Config paths starting with $ is executed rather than treated as a path + if strings.HasPrefix(configFile, "$ ") { + c.ConfigFile = configFile + } else { + c.ConfigFile, _ = filepath.Abs(configFile) + } +} + +// CurrentConfigFile - Return the full path or command for the config file in use +func (c *Config) CurrentConfigFile() string { + return c.ConfigFile +} + +func (c *Config) Load() error { + var configSrc interface{} + + if strings.HasPrefix(c.ConfigFile, "$ ") { + cmdRawOut, err := exec.Command("sh", "-c", c.ConfigFile[2:]).Output() + if err != nil { + return err + } + + configSrc = cmdRawOut + } else { + configSrc = c.ConfigFile + } + + cfg, err := ini.LoadSources(ini.LoadOptions{AllowBooleanKeys: true, SpaceBeforeInlineComment: true}, configSrc) + if err != nil { + return err + } + + // Clear the existing config + c.Gateway = false + c.GatewayWebircPassword = make(map[string]string) + c.Proxy = ConfigServer{} + c.Upstreams = []ConfigUpstream{} + c.Servers = []ConfigServer{} + c.ServerTransports = []string{} + c.RemoteOrigins = []glob.Glob{} + c.GatewayWhitelist = []glob.Glob{} + c.ReverseProxies = []net.IPNet{} + c.Webroot = "" + c.ReCaptchaURL = "" + c.ReCaptchaSecret = "" + c.ReCaptchaKey = "" + c.RequiresVerification = false + c.Secret = "" + c.SendQuitOnClientClose = "" + c.ClientRealname = "" + c.ClientUsername = "" + c.ClientHostname = "" + c.DnsblServers = []string{} + c.DnsblAction = "" + + for _, section := range cfg.Sections() { + if strings.Index(section.Name(), "DEFAULT") == 0 { + c.LogLevel = section.Key("logLevel").MustInt(3) + if c.LogLevel < 1 || c.LogLevel > 3 { + c.gateway.Log(3, "Config option logLevel must be between 1-3. Setting default value of 3.") + c.LogLevel = 3 + } + + c.Identd = section.Key("identd").MustBool(false) + + c.GatewayName = section.Key("gateway_name").MustString("") + if strings.Contains(c.GatewayName, " ") { + c.gateway.Log(3, "Config option gateway_name must not contain spaces") + c.GatewayName = "" + } + + c.Secret = section.Key("secret").MustString("") + c.SendQuitOnClientClose = section.Key("send_quit_on_client_close").MustString("Connection closed") + } + + if section.Name() == "verify" { + captchaSecret := section.Key("recaptcha_secret").MustString("") + captchaKey := section.Key("recaptcha_key").MustString("") + if captchaSecret != "" && captchaKey != "" { + c.RequiresVerification = section.Key("required").MustBool(false) + c.ReCaptchaSecret = captchaSecret + } + c.ReCaptchaURL = section.Key("recaptcha_url").MustString("https://www.google.com/recaptcha/api/siteverify") + } + + if section.Name() == "dnsbl" { + c.DnsblAction = section.Key("action").MustString("") + } + + if section.Name() == "dnsbl.servers" { + c.DnsblServers = append(c.DnsblServers, section.KeyStrings()...) + } + + if section.Name() == "gateway" { + c.Gateway = section.Key("enabled").MustBool(false) + c.GatewayTimeout = section.Key("timeout").MustInt(10) + c.GatewayThrottle = section.Key("throttle").MustInt(2) + + validProtocols := []string{"tcp", "tcp4", "tcp6"} + c.GatewayProtocol = stringInSliceOrDefault(section.Key("protocol").MustString(""), "tcp", validProtocols) + c.GatewayLocalAddr = section.Key("localaddr").MustString("") + } + + if section.Name() == "gateway.webirc" { + for _, serverAddr := range section.KeyStrings() { + c.GatewayWebircPassword[serverAddr] = section.Key(serverAddr).MustString("") + } + } + + if strings.Index(section.Name(), "clients") == 0 { + c.ClientUsername = section.Key("username").MustString("") + c.ClientRealname = section.Key("realname").MustString("") + c.ClientHostname = section.Key("hostname").MustString("") + } + + if strings.Index(section.Name(), "fileserving") == 0 { + if section.Key("enabled").MustBool(false) { + c.Webroot = section.Key("webroot").MustString("") + } + } + + if strings.Index(section.Name(), "server.") == 0 { + server := ConfigServer{} + server.LocalAddr = confKeyAsString(section.Key("bind"), "127.0.0.1") + rawMode := confKeyAsString(section.Key("bind_mode"), "") + mode, err := strconv.ParseInt(rawMode, 8, 32) + if err != nil { + mode = 0755 + } + server.BindMode = os.FileMode(mode) + server.Port = confKeyAsInt(section.Key("port"), 80) + server.TLS = confKeyAsBool(section.Key("tls"), false) + server.CertFile = confKeyAsString(section.Key("cert"), "") + server.KeyFile = confKeyAsString(section.Key("key"), "") + server.LetsEncryptCacheDir = confKeyAsString(section.Key("letsencrypt_cache"), "") + + if strings.HasSuffix(server.LetsEncryptCacheDir, ".cache") { + return errors.New("Syntax has changed. Please update letsencrypt_cache to a directory path (eg ./cache)") + } + + c.Servers = append(c.Servers, server) + } + + if section.Name() == "proxy" { + server := ConfigServer{} + server.LocalAddr = confKeyAsString(section.Key("bind"), "0.0.0.0") + server.Port = confKeyAsInt(section.Key("port"), 7999) + c.Proxy = server + } + + if strings.Index(section.Name(), "upstream.") == 0 { + upstream := ConfigUpstream{} + + validProtocols := []string{"tcp", "tcp4", "tcp6", "unix"} + upstream.Protocol = stringInSliceOrDefault(section.Key("protocol").MustString(""), "tcp", validProtocols) + + hostname := section.Key("hostname").MustString("127.0.0.1") + if strings.HasPrefix(strings.ToLower(hostname), "unix:") { + upstream.Protocol = "unix" + upstream.Hostname = hostname[5:] + } else { + upstream.Hostname = hostname + upstream.Port = section.Key("port").MustInt(6667) + upstream.TLS = section.Key("tls").MustBool(false) + } + + upstream.Timeout = section.Key("timeout").MustInt(10) + upstream.Throttle = section.Key("throttle").MustInt(2) + upstream.WebircPassword = section.Key("webirc").MustString("") + upstream.ServerPassword = section.Key("serverpassword").MustString("") + upstream.LocalAddr = section.Key("localaddr").MustString("") + + upstream.GatewayName = section.Key("gateway_name").MustString("") + if strings.Contains(upstream.GatewayName, " ") { + c.gateway.Log(3, "Config option gateway_name must not contain spaces") + upstream.GatewayName = "" + } + + upstream.NetworkCommonAddress = section.Key("network_common_address").MustString("") + + c.Upstreams = append(c.Upstreams, upstream) + } + + // "engines" is now legacy naming + if section.Name() == "engines" || section.Name() == "transports" { + for _, transport := range section.KeyStrings() { + c.ServerTransports = append(c.ServerTransports, strings.Trim(transport, "\n")) + } + } + + if strings.Index(section.Name(), "plugins") == 0 { + for _, plugin := range section.KeyStrings() { + c.Plugins = append(c.Plugins, strings.Trim(plugin, "\n")) + } + } + + if strings.Index(section.Name(), "allowed_origins") == 0 { + for _, origin := range section.KeyStrings() { + match, err := glob.Compile(origin) + if err != nil { + c.gateway.Log(3, "Config section allowed_origins has invalid match, "+origin) + continue + } + c.RemoteOrigins = append(c.RemoteOrigins, match) + } + } + + if strings.Index(section.Name(), "gateway.whitelist") == 0 { + for _, origin := range section.KeyStrings() { + match, err := glob.Compile(origin) + if err != nil { + c.gateway.Log(3, "Config section gateway.whitelist has invalid match, "+origin) + continue + } + c.GatewayWhitelist = append(c.GatewayWhitelist, match) + } + } + + if strings.Index(section.Name(), "reverse_proxies") == 0 { + for _, cidrRange := range section.KeyStrings() { + _, validRange, cidrErr := net.ParseCIDR(cidrRange) + if cidrErr != nil { + c.gateway.Log(3, "Config section reverse_proxies has invalid entry, "+cidrRange) + continue + } + c.ReverseProxies = append(c.ReverseProxies, *validRange) + } + } + } + + return nil +} + +func confKeyAsString(key *ini.Key, def string) string { + val := def + + str := key.String() + if len(str) > 1 && str[:1] == "$" { + val = os.Getenv(str[1:]) + } else { + val = key.MustString(def) + } + + return val +} + +func confKeyAsInt(key *ini.Key, def int) int { + val := def + + str := key.String() + if len(str) > 1 && str[:1] == "$" { + envVal := os.Getenv(str[1:]) + envValInt, err := strconv.Atoi(envVal) + if err == nil { + val = envValInt + } + } else { + val = key.MustInt(def) + } + + return val +} + +func confKeyAsBool(key *ini.Key, def bool) bool { + val := def + + str := key.String() + if len(str) > 1 && str[:1] == "$" { + envVal := os.Getenv(str[1:]) + if envVal == "0" || envVal == "false" || envVal == "no" { + val = false + } else { + val = true + } + } else { + val = key.MustBool(def) + } + + return val +} diff --git a/deprecated-webircgateway/pkg/webircgateway/gateway.go b/deprecated-webircgateway/pkg/webircgateway/gateway.go new file mode 100644 index 0000000..47169ef --- /dev/null +++ b/deprecated-webircgateway/pkg/webircgateway/gateway.go @@ -0,0 +1,278 @@ +package webircgateway + +import ( + "crypto/tls" + "encoding/json" + "fmt" + "net" + "net/http" + "os" + "strconv" + "strings" + "sync" + + "errors" + + "github.com/kiwiirc/webircgateway/pkg/identd" + "github.com/kiwiirc/webircgateway/pkg/proxy" + cmap "github.com/orcaman/concurrent-map" +) + +var ( + Version = "-" +) + +type Gateway struct { + Config *Config + HttpRouter *http.ServeMux + LogOutput chan string + messageTags *MessageTagManager + identdServ identd.Server + Clients cmap.ConcurrentMap + Acme *LEManager + Function string + httpSrvs []*http.Server + httpSrvsMu sync.Mutex + closeWg sync.WaitGroup +} + +func NewGateway(function string) *Gateway { + s := &Gateway{} + s.Function = function + s.Config = NewConfig(s) + s.HttpRouter = http.NewServeMux() + s.LogOutput = make(chan string, 5) + s.identdServ = identd.NewIdentdServer() + s.messageTags = NewMessageTagManager() + // Clients hold a map lookup for all the connected clients + s.Clients = cmap.New() + s.Acme = NewLetsEncryptManager(s) + + return s +} + +func (s *Gateway) Log(level int, format string, args ...interface{}) { + if level < s.Config.LogLevel { + return + } + + levels := [...]string{"L_DEBUG", "L_INFO", "L_WARN"} + line := fmt.Sprintf(levels[level-1]+" "+format, args...) + s.LogOutput <- line +} + +func (s *Gateway) Start() { + s.closeWg.Add(1) + + if s.Function == "gateway" { + s.maybeStartStaticFileServer() + s.initHttpRoutes() + s.maybeStartIdentd() + + for _, serverConfig := range s.Config.Servers { + go s.startServer(serverConfig) + } + } + + if s.Function == "proxy" { + proxy.Start(fmt.Sprintf("%s:%d", s.Config.Proxy.LocalAddr, s.Config.Proxy.Port)) + } +} + +func (s *Gateway) Close() { + hook := HookGatewayClosing{} + hook.Dispatch("gateway.closing") + + defer s.closeWg.Done() + + s.httpSrvsMu.Lock() + defer s.httpSrvsMu.Unlock() + + for _, httpSrv := range s.httpSrvs { + httpSrv.Close() + } +} + +func (s *Gateway) WaitClose() { + s.closeWg.Wait() +} + +func (s *Gateway) maybeStartStaticFileServer() { + if s.Config.Webroot != "" { + webroot := s.Config.ResolvePath(s.Config.Webroot) + s.Log(2, "Serving files from %s", webroot) + s.HttpRouter.Handle("/", http.FileServer(http.Dir(webroot))) + } +} + +func (s *Gateway) initHttpRoutes() error { + // Add all the transport routes + engineConfigured := false + for _, transport := range s.Config.ServerTransports { + switch transport { + case "kiwiirc": + t := &TransportKiwiirc{} + t.Init(s) + engineConfigured = true + case "websocket": + t := &TransportWebsocket{} + t.Init(s) + engineConfigured = true + case "sockjs": + t := &TransportSockjs{} + t.Init(s) + engineConfigured = true + default: + s.Log(3, "Invalid server engine: '%s'", transport) + } + } + + if !engineConfigured { + s.Log(3, "No server engines configured") + return errors.New("No server engines configured") + } + + // Add some general server info about this webircgateway instance + s.HttpRouter.HandleFunc("/webirc/info", func(w http.ResponseWriter, r *http.Request) { + out, _ := json.Marshal(map[string]interface{}{ + "name": "webircgateway", + "version": Version, + }) + + w.Write(out) + }) + + s.HttpRouter.HandleFunc("/webirc/_status", func(w http.ResponseWriter, r *http.Request) { + if !isPrivateIP(s.GetRemoteAddressFromRequest(r)) { + w.WriteHeader(403) + return + } + + out := "" + for item := range s.Clients.IterBuffered() { + c := item.Val.(*Client) + line := fmt.Sprintf( + "%s:%d %s %s!%s %s %s", + c.UpstreamConfig.Hostname, + c.UpstreamConfig.Port, + c.State, + c.IrcState.Nick, + c.IrcState.Username, + c.RemoteAddr, + c.RemoteHostname, + ) + + // Allow plugins to add their own status data + hook := HookStatus{} + hook.Client = c + hook.Line = line + hook.Dispatch("status.client") + if !hook.Halt { + out += hook.Line + "\n" + } + + } + + w.Write([]byte(out)) + }) + + return nil +} + +func (s *Gateway) maybeStartIdentd() { + if s.Config.Identd { + err := s.identdServ.Run() + if err != nil { + s.Log(3, "Error starting identd server: %s", err.Error()) + } else { + s.Log(2, "Identd server started") + } + } +} + +func (s *Gateway) startServer(conf ConfigServer) { + addr := fmt.Sprintf("%s:%d", conf.LocalAddr, conf.Port) + + if strings.HasPrefix(strings.ToLower(conf.LocalAddr), "tcp:") { + t := &TransportTcp{} + t.Init(s) + t.Start(conf.LocalAddr[4:] + ":" + strconv.Itoa(conf.Port)) + } else if conf.TLS && conf.LetsEncryptCacheDir == "" { + if conf.CertFile == "" || conf.KeyFile == "" { + s.Log(3, "'cert' and 'key' options must be set for TLS servers") + return + } + + tlsCert := s.Config.ResolvePath(conf.CertFile) + tlsKey := s.Config.ResolvePath(conf.KeyFile) + + s.Log(2, "Listening with TLS on %s", addr) + keyPair, keyPairErr := tls.LoadX509KeyPair(tlsCert, tlsKey) + if keyPairErr != nil { + s.Log(3, "Failed to listen with TLS, certificate error: %s", keyPairErr.Error()) + return + } + srv := &http.Server{ + Addr: addr, + TLSConfig: &tls.Config{ + Certificates: []tls.Certificate{keyPair}, + }, + Handler: s.HttpRouter, + } + s.httpSrvsMu.Lock() + s.httpSrvs = append(s.httpSrvs, srv) + s.httpSrvsMu.Unlock() + + // Don't use HTTP2 since it doesn't support websockets + srv.TLSNextProto = make(map[string]func(*http.Server, *tls.Conn, http.Handler)) + + err := srv.ListenAndServeTLS("", "") + if err != nil && err != http.ErrServerClosed { + s.Log(3, "Failed to listen with TLS: %s", err.Error()) + } + } else if conf.TLS && conf.LetsEncryptCacheDir != "" { + s.Log(2, "Listening with letsencrypt TLS on %s", addr) + leManager := s.Acme.Get(conf.LetsEncryptCacheDir) + srv := &http.Server{ + Addr: addr, + TLSConfig: &tls.Config{ + GetCertificate: leManager.GetCertificate, + }, + Handler: s.HttpRouter, + } + s.httpSrvsMu.Lock() + s.httpSrvs = append(s.httpSrvs, srv) + s.httpSrvsMu.Unlock() + + // Don't use HTTP2 since it doesn't support websockets + srv.TLSNextProto = make(map[string]func(*http.Server, *tls.Conn, http.Handler)) + + err := srv.ListenAndServeTLS("", "") + if err != nil && err != http.ErrServerClosed { + s.Log(3, "Listening with letsencrypt failed: %s", err.Error()) + } + } else if strings.HasPrefix(strings.ToLower(conf.LocalAddr), "unix:") { + socketFile := conf.LocalAddr[5:] + s.Log(2, "Listening on %s", socketFile) + os.Remove(socketFile) + server, serverErr := net.Listen("unix", socketFile) + if serverErr != nil { + s.Log(3, serverErr.Error()) + return + } + os.Chmod(socketFile, conf.BindMode) + http.Serve(server, s.HttpRouter) + } else { + s.Log(2, "Listening on %s", addr) + srv := &http.Server{Addr: addr, Handler: s.HttpRouter} + + s.httpSrvsMu.Lock() + s.httpSrvs = append(s.httpSrvs, srv) + s.httpSrvsMu.Unlock() + + err := srv.ListenAndServe() + if err != nil && err != http.ErrServerClosed { + s.Log(3, err.Error()) + } + } +} diff --git a/deprecated-webircgateway/pkg/webircgateway/gateway_utils.go b/deprecated-webircgateway/pkg/webircgateway/gateway_utils.go new file mode 100644 index 0000000..4b2a38d --- /dev/null +++ b/deprecated-webircgateway/pkg/webircgateway/gateway_utils.go @@ -0,0 +1,133 @@ +package webircgateway + +import ( + "errors" + "math/rand" + "net" + "net/http" + "strings" +) + +var v4LoopbackAddr = net.ParseIP("127.0.0.1") + +func (s *Gateway) NewClient() *Client { + return NewClient(s) +} + +func (s *Gateway) IsClientOriginAllowed(originHeader string) bool { + // Empty list of origins = all origins allowed + if len(s.Config.RemoteOrigins) == 0 { + return true + } + + // No origin header = running on the same page + if originHeader == "" { + return true + } + + foundMatch := false + + for _, originMatch := range s.Config.RemoteOrigins { + if originMatch.Match(originHeader) { + foundMatch = true + break + } + } + + return foundMatch +} + +func (s *Gateway) isIrcAddressAllowed(addr string) bool { + // Empty whitelist = all destinations allowed + if len(s.Config.GatewayWhitelist) == 0 { + return true + } + + foundMatch := false + + for _, addrMatch := range s.Config.GatewayWhitelist { + if addrMatch.Match(addr) { + foundMatch = true + break + } + } + + return foundMatch +} + +func (s *Gateway) findUpstream() (ConfigUpstream, error) { + var ret ConfigUpstream + + if len(s.Config.Upstreams) == 0 { + return ret, errors.New("No upstreams available") + } + + randIdx := rand.Intn(len(s.Config.Upstreams)) + ret = s.Config.Upstreams[randIdx] + + return ret, nil +} + +func (s *Gateway) findWebircPassword(ircHost string) string { + pass, exists := s.Config.GatewayWebircPassword[strings.ToLower(ircHost)] + if !exists { + pass = "" + } + + return pass +} + +func (s *Gateway) GetRemoteAddressFromRequest(req *http.Request) net.IP { + remoteIP := remoteIPFromRequest(req) + + // If the remoteIP is not in a whitelisted reverse proxy range, don't trust + // the headers and use the remoteIP as the users IP + if !s.isTrustedProxy(remoteIP) { + return remoteIP + } + + headerVal := req.Header.Get("x-forwarded-for") + ips := strings.Split(headerVal, ",") + ipStr := strings.Trim(ips[0], " ") + if ipStr != "" { + ip := net.ParseIP(ipStr) + if ip != nil { + remoteIP = ip + } + } + + return remoteIP + +} + +func (s *Gateway) isRequestSecure(req *http.Request) bool { + remoteIP := remoteIPFromRequest(req) + + // If the remoteIP is not in a whitelisted reverse proxy range, don't trust + // the headers and check the request directly + if !s.isTrustedProxy(remoteIP) { + return req.TLS != nil + } + + fwdProto := req.Header.Get("x-forwarded-proto") + return strings.EqualFold(fwdProto, "https") +} + +func (s *Gateway) isTrustedProxy(remoteIP net.IP) bool { + for _, cidrRange := range s.Config.ReverseProxies { + if cidrRange.Contains(remoteIP) { + return true + } + } + return false +} + +func remoteIPFromRequest(req *http.Request) net.IP { + if req.RemoteAddr == "@" { + // remote address is unix socket, treat it as loopback interface + return v4LoopbackAddr + } + + remoteAddr, _, _ := net.SplitHostPort(req.RemoteAddr) + return net.ParseIP(remoteAddr) +} diff --git a/deprecated-webircgateway/pkg/webircgateway/hooks.go b/deprecated-webircgateway/pkg/webircgateway/hooks.go new file mode 100644 index 0000000..1bfd564 --- /dev/null +++ b/deprecated-webircgateway/pkg/webircgateway/hooks.go @@ -0,0 +1,152 @@ +package webircgateway + +import "github.com/kiwiirc/webircgateway/pkg/irc" + +var hooksRegistered map[string][]interface{} + +func init() { + hooksRegistered = make(map[string][]interface{}) +} + +func HookRegister(hookName string, p interface{}) { + _, exists := hooksRegistered[hookName] + if !exists { + hooksRegistered[hookName] = make([]interface{}, 0) + } + + hooksRegistered[hookName] = append(hooksRegistered[hookName], p) +} + +type Hook struct { + ID string + Halt bool +} + +func (h *Hook) getCallbacks(eventType string) []interface{} { + var f []interface{} + f = make([]interface{}, 0) + + callbacks, exists := hooksRegistered[eventType] + if exists { + f = callbacks + } + + return f +} + +/** + * HookIrcConnectionPre + * Dispatched just before an IRC connection is attempted + * Types: irc.connection.pre + */ +type HookIrcConnectionPre struct { + Hook + Client *Client + UpstreamConfig *ConfigUpstream +} + +func (h *HookIrcConnectionPre) Dispatch(eventType string) { + for _, p := range h.getCallbacks(eventType) { + if f, ok := p.(func(*HookIrcConnectionPre)); ok { + f(h) + } + } +} + +/** + * HookIrcLine + * Dispatched when either: + * * A line arrives from the IRCd, before sending to the client + * * A line arrives from the client, before sending to the IRCd + * Types: irc.line + */ +type HookIrcLine struct { + Hook + Client *Client + UpstreamConfig *ConfigUpstream + Line string + Message *irc.Message + ToServer bool +} + +func (h *HookIrcLine) Dispatch(eventType string) { + for _, p := range h.getCallbacks(eventType) { + if f, ok := p.(func(*HookIrcLine)); ok { + f(h) + } + } +} + +/** + * HookClientState + * Dispatched after a client connects or disconnects + * Types: client.state + */ +type HookClientState struct { + Hook + Client *Client + Connected bool +} + +func (h *HookClientState) Dispatch(eventType string) { + for _, p := range h.getCallbacks(eventType) { + if f, ok := p.(func(*HookClientState)); ok { + f(h) + } + } +} + +/** + * HookClientInit + * Dispatched directly after a new Client instance has been created + * Types: client.init + */ +type HookClientInit struct { + Hook + Client *Client + Connected bool +} + +func (h *HookClientInit) Dispatch(eventType string) { + for _, p := range h.getCallbacks(eventType) { + if f, ok := p.(func(*HookClientInit)); ok { + f(h) + } + } +} + +/** + * HookStatus + * Dispatched for each line output of the _status HTTP request + * Types: status.client + */ +type HookStatus struct { + Hook + Client *Client + Line string +} + +func (h *HookStatus) Dispatch(eventType string) { + for _, p := range h.getCallbacks(eventType) { + if f, ok := p.(func(*HookStatus)); ok { + f(h) + } + } +} + +/** + * HookGatewayClosing + * Dispatched when the gateway has been told to shutdown + * Types: gateway.closing + */ +type HookGatewayClosing struct { + Hook +} + +func (h *HookGatewayClosing) Dispatch(eventType string) { + for _, p := range h.getCallbacks(eventType) { + if f, ok := p.(func(*HookGatewayClosing)); ok { + f(h) + } + } +} diff --git a/deprecated-webircgateway/pkg/webircgateway/letsencrypt.go b/deprecated-webircgateway/pkg/webircgateway/letsencrypt.go new file mode 100644 index 0000000..ffa6afe --- /dev/null +++ b/deprecated-webircgateway/pkg/webircgateway/letsencrypt.go @@ -0,0 +1,41 @@ +package webircgateway + +import ( + "context" + "strings" + "sync" + + "golang.org/x/crypto/acme/autocert" +) + +type LEManager struct { + // ensure only one instance of the manager and handler is running + // while allowing multiple listeners to use it + Mutex sync.Mutex + Manager *autocert.Manager + gateway *Gateway +} + +func NewLetsEncryptManager(gateway *Gateway) *LEManager { + return &LEManager{gateway: gateway} +} + +func (le *LEManager) Get(certCacheDir string) *autocert.Manager { + le.Mutex.Lock() + defer le.Mutex.Unlock() + + // Create it if it doesn't already exist + if le.Manager == nil { + le.Manager = &autocert.Manager{ + Prompt: autocert.AcceptTOS, + Cache: autocert.DirCache(strings.TrimRight(certCacheDir, "/")), + HostPolicy: func(ctx context.Context, host string) error { + le.gateway.Log(2, "Automatically requesting a HTTPS certificate for %s", host) + return nil + }, + } + le.gateway.HttpRouter.Handle("/.well-known/", le.Manager.HTTPHandler(nil)) + } + + return le.Manager +} diff --git a/deprecated-webircgateway/pkg/webircgateway/messagetags.go b/deprecated-webircgateway/pkg/webircgateway/messagetags.go new file mode 100644 index 0000000..9715ad6 --- /dev/null +++ b/deprecated-webircgateway/pkg/webircgateway/messagetags.go @@ -0,0 +1,103 @@ +package webircgateway + +import ( + "strings" + "sync" + "time" + + "github.com/OneOfOne/xxhash" + "github.com/kiwiirc/webircgateway/pkg/irc" +) + +type MessageTagManager struct { + Mutex sync.Mutex + knownTags map[uint64]MessageTags + gcTimes map[uint64]time.Time +} +type MessageTags struct { + Tags map[string]string +} + +func NewMessageTagManager() *MessageTagManager { + tm := &MessageTagManager{ + knownTags: make(map[uint64]MessageTags), + gcTimes: make(map[uint64]time.Time), + } + + go tm.RunGarbageCollectionLoop() + return tm +} + +func (tags *MessageTagManager) CanMessageContainClientTags(msg *irc.Message) bool { + return stringInSlice(msg.Command, []string{ + "PRIVMSG", + "NOTICE", + "TAGMSG", + }) +} + +func (tags *MessageTagManager) RunGarbageCollectionLoop() { + for { + tags.Mutex.Lock() + for messageHash, timeCreated := range tags.gcTimes { + if timeCreated.Add(time.Second * 30).After(time.Now()) { + delete(tags.knownTags, messageHash) + } + + } + tags.Mutex.Unlock() + + time.Sleep(time.Second * 30) + } +} + +func (tags *MessageTagManager) AddTagsFromMessage(client *Client, fromNick string, msg *irc.Message) { + if !tags.CanMessageContainClientTags(msg) { + return + } + + clientTags := MessageTags{ + Tags: make(map[string]string), + } + for tagName, tagVal := range msg.Tags { + if len(tagName) > 0 && tagName[0] == '+' { + clientTags.Tags[tagName] = tagVal + } + } + + if len(clientTags.Tags) > 0 { + tags.Mutex.Lock() + msgHash := tags.messageHash(client, fromNick, msg) + tags.knownTags[msgHash] = clientTags + tags.gcTimes[msgHash] = time.Now() + tags.Mutex.Unlock() + } +} + +func (tags *MessageTagManager) GetTagsFromMessage(client *Client, fromNick string, msg *irc.Message) (MessageTags, bool) { + if !tags.CanMessageContainClientTags(msg) { + return MessageTags{}, false + } + + msgHash := tags.messageHash(client, fromNick, msg) + + tags.Mutex.Lock() + defer tags.Mutex.Unlock() + + clientTags, tagsExist := tags.knownTags[msgHash] + if !tagsExist { + return clientTags, false + } + + return clientTags, true +} + +func (tags *MessageTagManager) messageHash(client *Client, fromNick string, msg *irc.Message) uint64 { + h := xxhash.New64() + h.WriteString(strings.ToLower(client.UpstreamConfig.Hostname)) + h.WriteString(strings.ToLower(fromNick)) + // make target case insensitive + h.WriteString(strings.ToLower(msg.GetParam(0, ""))) + h.WriteString(msg.GetParam(1, "")) + return h.Sum64() +} diff --git a/deprecated-webircgateway/pkg/webircgateway/transport_kiwiirc.go b/deprecated-webircgateway/pkg/webircgateway/transport_kiwiirc.go new file mode 100644 index 0000000..e5247e8 --- /dev/null +++ b/deprecated-webircgateway/pkg/webircgateway/transport_kiwiirc.go @@ -0,0 +1,206 @@ +package webircgateway + +import ( + "fmt" + "log" + "net" + "net/http" + "runtime/debug" + "strings" + "sync" + + "github.com/gorilla/websocket" + "github.com/igm/sockjs-go/v3/sockjs" + cmap "github.com/orcaman/concurrent-map" +) + +type TransportKiwiirc struct { + gateway *Gateway +} + +func (t *TransportKiwiirc) Init(g *Gateway) { + t.gateway = g + sockjsOptions := sockjs.DefaultOptions + sockjsOptions.WebsocketUpgrader = &websocket.Upgrader{ + // Origin is checked within the session handler + CheckOrigin: func(_ *http.Request) bool { return true }, + } + handler := sockjs.NewHandler("/webirc/kiwiirc", sockjsOptions, t.sessionHandler) + t.gateway.HttpRouter.Handle("/webirc/kiwiirc/", handler) +} + +func (t *TransportKiwiirc) makeChannel(chanID string, ws sockjs.Session) *TransportKiwiircChannel { + client := t.gateway.NewClient() + + originHeader := strings.ToLower(ws.Request().Header.Get("Origin")) + if !t.gateway.IsClientOriginAllowed(originHeader) { + client.Log(2, "Origin %s not allowed. Closing connection", originHeader) + ws.Close(0, "Origin not allowed") + return nil + } + + client.RemoteAddr = t.gateway.GetRemoteAddressFromRequest(ws.Request()).String() + + clientHostnames, err := net.LookupAddr(client.RemoteAddr) + if err != nil || len(clientHostnames) == 0 { + client.RemoteHostname = client.RemoteAddr + } else { + // FQDNs include a . at the end. Strip it out + potentialHostname := strings.Trim(clientHostnames[0], ".") + + // Must check that the resolved hostname also resolves back to the users IP + addr, err := net.LookupIP(potentialHostname) + if err == nil && len(addr) == 1 && addr[0].String() == client.RemoteAddr { + client.RemoteHostname = potentialHostname + } else { + client.RemoteHostname = client.RemoteAddr + } + } + + if t.gateway.isRequestSecure(ws.Request()) { + client.Tags["secure"] = "" + } + + // This doesn't make sense to have since the remote port may change between requests. Only + // here for testing purposes for now. + _, remoteAddrPort, _ := net.SplitHostPort(ws.Request().RemoteAddr) + client.Tags["remote-port"] = remoteAddrPort + + client.Log(2, "New kiwiirc channel on %s from %s %s", ws.Request().Host, client.RemoteAddr, client.RemoteHostname) + client.Ready() + + channel := &TransportKiwiircChannel{ + Id: chanID, + Client: client, + Conn: ws, + waitForClose: make(chan bool), + Closed: false, + } + + go channel.listenForSignals() + + return channel +} + +func (t *TransportKiwiirc) sessionHandler(session sockjs.Session) { + // Don't let a single users error kill the entire service for everyone + defer func() { + if r := recover(); r != nil { + log.Printf("[ERROR] Recovered from %s\n%s", r, debug.Stack()) + } + }() + + channels := cmap.New() + + // Read from sockjs + go func() { + for { + msg, err := session.Recv() + if err == nil && len(msg) > 0 { + idEnd := strings.Index(msg, " ") + if idEnd == -1 { + // msg is in the form of ":chanId" + chanID := msg[1:] + + c, channelExists := channels.Get(chanID) + if channelExists { + channel := c.(*TransportKiwiircChannel) + channel.close() + } + + if !channelExists { + channel := t.makeChannel(chanID, session) + if channel == nil { + continue + } + channels.Set(chanID, channel) + + // When the channel closes, remove it from the map again + go func() { + <-channel.waitForClose + channel.Client.Log(2, "Removing channel from connection") + channels.Remove(chanID) + }() + } + + session.Send(":" + chanID) + + } else { + // msg is in the form of ":chanId data" + chanID := msg[1:idEnd] + data := msg[idEnd+1:] + + channel, channelExists := channels.Get(chanID) + if channelExists { + c := channel.(*TransportKiwiircChannel) + c.handleIncomingLine(data) + } + } + } else if err != nil { + t.gateway.Log(1, "kiwi connection closed (%s)", err.Error()) + break + } + } + + for channel := range channels.IterBuffered() { + c := channel.Val.(*TransportKiwiircChannel) + c.Closed = true + c.Client.StartShutdown("client_closed") + } + }() +} + +type TransportKiwiircChannel struct { + Conn sockjs.Session + Client *Client + Id string + waitForClose chan bool + ClosedLock sync.Mutex + Closed bool +} + +func (c *TransportKiwiircChannel) listenForSignals() { + for { + signal, ok := <-c.Client.Signals + if !ok { + break + } + c.Client.Log(1, "signal:%s %s", signal[0], signal[1]) + if signal[0] == "state" { + if signal[1] == "connected" { + c.Conn.Send(fmt.Sprintf(":%s control connected", c.Id)) + } else if signal[1] == "closed" { + c.Conn.Send(fmt.Sprintf(":%s control closed %s", c.Id, signal[2])) + } + } + + if signal[0] == "data" { + toSend := strings.Trim(signal[1], "\r\n") + c.Conn.Send(fmt.Sprintf(":%s %s", c.Id, toSend)) + } + } + + c.ClosedLock.Lock() + + c.Closed = true + close(c.Client.Recv) + close(c.waitForClose) + + c.ClosedLock.Unlock() +} + +func (c *TransportKiwiircChannel) handleIncomingLine(line string) { + c.ClosedLock.Lock() + + if !c.Closed { + c.Client.Recv <- line + } + + c.ClosedLock.Unlock() +} + +func (c *TransportKiwiircChannel) close() { + if c.Client.upstream != nil { + c.Client.upstream.Close() + } +} diff --git a/deprecated-webircgateway/pkg/webircgateway/transport_sockjs.go b/deprecated-webircgateway/pkg/webircgateway/transport_sockjs.go new file mode 100644 index 0000000..da4891f --- /dev/null +++ b/deprecated-webircgateway/pkg/webircgateway/transport_sockjs.go @@ -0,0 +1,107 @@ +package webircgateway + +import ( + "net" + "net/http" + "strings" + + "github.com/gorilla/websocket" + "github.com/igm/sockjs-go/v3/sockjs" +) + +type TransportSockjs struct { + gateway *Gateway +} + +func (t *TransportSockjs) Init(g *Gateway) { + t.gateway = g + sockjsOptions := sockjs.DefaultOptions + sockjsOptions.WebsocketUpgrader = &websocket.Upgrader{ + // Origin is checked within the session handler + CheckOrigin: func(_ *http.Request) bool { return true }, + } + sockjsHandler := sockjs.NewHandler("/webirc/sockjs", sockjsOptions, t.sessionHandler) + t.gateway.HttpRouter.Handle("/webirc/sockjs/", sockjsHandler) +} + +func (t *TransportSockjs) sessionHandler(session sockjs.Session) { + client := t.gateway.NewClient() + + originHeader := strings.ToLower(session.Request().Header.Get("Origin")) + if !t.gateway.IsClientOriginAllowed(originHeader) { + client.Log(2, "Origin %s not allowed. Closing connection", originHeader) + session.Close(0, "Origin not allowed") + return + } + + client.RemoteAddr = t.gateway.GetRemoteAddressFromRequest(session.Request()).String() + + clientHostnames, err := net.LookupAddr(client.RemoteAddr) + if err != nil { + client.RemoteHostname = client.RemoteAddr + } else { + // FQDNs include a . at the end. Strip it out + potentialHostname := strings.Trim(clientHostnames[0], ".") + + // Must check that the resolved hostname also resolves back to the users IP + addr, err := net.LookupIP(potentialHostname) + if err == nil && len(addr) == 1 && addr[0].String() == client.RemoteAddr { + client.RemoteHostname = potentialHostname + } else { + client.RemoteHostname = client.RemoteAddr + } + } + + if t.gateway.isRequestSecure(session.Request()) { + client.Tags["secure"] = "" + } + + // This doesn't make sense to have since the remote port may change between requests. Only + // here for testing purposes for now. + _, remoteAddrPort, _ := net.SplitHostPort(session.Request().RemoteAddr) + client.Tags["remote-port"] = remoteAddrPort + + client.Log(2, "New sockjs client on %s from %s %s", session.Request().Host, client.RemoteAddr, client.RemoteHostname) + client.Ready() + + // Read from sockjs + go func() { + for { + msg, err := session.Recv() + if err == nil && len(msg) > 0 { + client.Log(1, "client->: %s", msg) + select { + case client.Recv <- msg: + default: + client.Log(3, "Recv queue full. Dropping data") + // TODO: Should this really just drop the data or close the connection? + } + } else if err != nil { + client.Log(1, "sockjs connection closed (%s)", err.Error()) + break + } else if len(msg) == 0 { + client.Log(1, "Got 0 bytes from websocket") + } + } + + close(client.Recv) + }() + + // Process signals for the client + for { + signal, ok := <-client.Signals + if !ok { + break + } + + if signal[0] == "data" { + line := strings.Trim(signal[1], "\r\n") + client.Log(1, "->ws: %s", line) + session.Send(line) + } + + if signal[0] == "state" && signal[1] == "closed" { + session.Close(0, "Closed") + } + } +} diff --git a/deprecated-webircgateway/pkg/webircgateway/transport_tcp.go b/deprecated-webircgateway/pkg/webircgateway/transport_tcp.go new file mode 100644 index 0000000..b4af7b3 --- /dev/null +++ b/deprecated-webircgateway/pkg/webircgateway/transport_tcp.go @@ -0,0 +1,113 @@ +package webircgateway + +import ( + "bufio" + "net" + "strings" + "sync" +) + +type TransportTcp struct { + gateway *Gateway +} + +func (t *TransportTcp) Init(g *Gateway) { + t.gateway = g +} + +func (t *TransportTcp) Start(lAddr string) { + l, err := net.Listen("tcp", lAddr) + if err != nil { + t.gateway.Log(3, "TCP error listening: "+err.Error()) + return + } + // Close the listener when the application closes. + defer l.Close() + t.gateway.Log(2, "TCP listening on "+lAddr) + for { + // Listen for an incoming connection. + conn, err := l.Accept() + if err != nil { + t.gateway.Log(3, "TCP error accepting: "+err.Error()) + break + } + // Handle connections in a new goroutine. + go t.handleConn(conn) + } +} + +func (t *TransportTcp) handleConn(conn net.Conn) { + client := t.gateway.NewClient() + + client.RemoteAddr = conn.RemoteAddr().String() + + clientHostnames, err := net.LookupAddr(client.RemoteAddr) + if err != nil { + client.RemoteHostname = client.RemoteAddr + } else { + // FQDNs include a . at the end. Strip it out + potentialHostname := strings.Trim(clientHostnames[0], ".") + + // Must check that the resolved hostname also resolves back to the users IP + addr, err := net.LookupIP(potentialHostname) + if err == nil && len(addr) == 1 && addr[0].String() == client.RemoteAddr { + client.RemoteHostname = potentialHostname + } else { + client.RemoteHostname = client.RemoteAddr + } + } + + _, remoteAddrPort, _ := net.SplitHostPort(conn.RemoteAddr().String()) + client.Tags["remote-port"] = remoteAddrPort + + client.Log(2, "New tcp client on %s from %s %s", conn.LocalAddr().String(), client.RemoteAddr, client.RemoteHostname) + client.Ready() + + // We wait until the client send queue has been drained + var sendDrained sync.WaitGroup + sendDrained.Add(1) + + // Read from TCP + go func() { + reader := bufio.NewReader(conn) + for { + data, err := reader.ReadString('\n') + if err == nil { + message := strings.TrimRight(data, "\r\n") + client.Log(1, "client->: %s", message) + select { + case client.Recv <- message: + default: + client.Log(3, "Recv queue full. Dropping data") + // TODO: Should this really just drop the data or close the connection? + } + + } else { + client.Log(1, "TCP connection closed (%s)", err.Error()) + break + + } + } + + close(client.Recv) + }() + + // Process signals for the client + for { + signal, ok := <-client.Signals + if !ok { + sendDrained.Done() + break + } + + if signal[0] == "data" { + //line := strings.Trim(signal[1], "\r\n") + line := signal[1] + "\n" + client.Log(1, "->tcp: %s", signal[1]) + conn.Write([]byte(line)) + } + } + + sendDrained.Wait() + conn.Close() +} diff --git a/deprecated-webircgateway/pkg/webircgateway/transport_websocket.go b/deprecated-webircgateway/pkg/webircgateway/transport_websocket.go new file mode 100644 index 0000000..960718b --- /dev/null +++ b/deprecated-webircgateway/pkg/webircgateway/transport_websocket.go @@ -0,0 +1,126 @@ +package webircgateway + +import ( + "fmt" + "net" + "net/http" + "strings" + "sync" + + "golang.org/x/net/websocket" +) + +type TransportWebsocket struct { + gateway *Gateway + wsServer *websocket.Server +} + +func (t *TransportWebsocket) Init(g *Gateway) { + t.gateway = g + t.wsServer = &websocket.Server{Handler: t.websocketHandler, Handshake: t.checkOrigin} + t.gateway.HttpRouter.Handle("/webirc/websocket/", t.wsServer) +} + +func (t *TransportWebsocket) checkOrigin(config *websocket.Config, req *http.Request) (err error) { + config.Origin, err = websocket.Origin(config, req) + + var origin string + if config.Origin != nil { + origin = config.Origin.String() + } else { + origin = "" + } + + if !t.gateway.IsClientOriginAllowed(origin) { + err = fmt.Errorf("Origin %#v not allowed", origin) + t.gateway.Log(2, "%s. Closing connection", err) + return err + } + + return err +} + +func (t *TransportWebsocket) websocketHandler(ws *websocket.Conn) { + client := t.gateway.NewClient() + + client.RemoteAddr = t.gateway.GetRemoteAddressFromRequest(ws.Request()).String() + + clientHostnames, err := net.LookupAddr(client.RemoteAddr) + if err != nil { + client.RemoteHostname = client.RemoteAddr + } else { + // FQDNs include a . at the end. Strip it out + potentialHostname := strings.Trim(clientHostnames[0], ".") + + // Must check that the resolved hostname also resolves back to the users IP + addr, err := net.LookupIP(potentialHostname) + if err == nil && len(addr) == 1 && addr[0].String() == client.RemoteAddr { + client.RemoteHostname = potentialHostname + } else { + client.RemoteHostname = client.RemoteAddr + } + } + + if t.gateway.isRequestSecure(ws.Request()) { + client.Tags["secure"] = "" + } + + _, remoteAddrPort, _ := net.SplitHostPort(ws.Request().RemoteAddr) + client.Tags["remote-port"] = remoteAddrPort + + client.Log(2, "New websocket client on %s from %s %s", ws.Request().Host, client.RemoteAddr, client.RemoteHostname) + client.Ready() + + // We wait until the client send queue has been drained + var sendDrained sync.WaitGroup + sendDrained.Add(1) + + // Read from websocket + go func() { + for { + r := make([]byte, 1024) + len, err := ws.Read(r) + if err == nil && len > 0 { + message := string(r[:len]) + client.Log(1, "client->: %s", message) + select { + case client.Recv <- message: + default: + client.Log(3, "Recv queue full. Dropping data") + // TODO: Should this really just drop the data or close the connection? + } + + } else if err != nil { + client.Log(1, "Websocket connection closed (%s)", err.Error()) + break + + } else if len == 0 { + client.Log(1, "Got 0 bytes from websocket") + } + } + + close(client.Recv) + }() + + // Process signals for the client + for { + signal, ok := <-client.Signals + if !ok { + sendDrained.Done() + break + } + + if signal[0] == "data" { + line := strings.Trim(signal[1], "\r\n") + client.Log(1, "->ws: %s", line) + ws.Write([]byte(line)) + } + + if signal[0] == "state" && signal[1] == "closed" { + ws.Close() + } + } + + sendDrained.Wait() + ws.Close() +} diff --git a/deprecated-webircgateway/pkg/webircgateway/utils.go b/deprecated-webircgateway/pkg/webircgateway/utils.go new file mode 100644 index 0000000..1fc687a --- /dev/null +++ b/deprecated-webircgateway/pkg/webircgateway/utils.go @@ -0,0 +1,147 @@ +package webircgateway + +import ( + "context" + "fmt" + "net" + "strings" + "unicode/utf8" + + "golang.org/x/net/html/charset" + "golang.org/x/time/rate" +) + +var privateIPBlocks []*net.IPNet + +func init() { + for _, cidr := range []string{ + "127.0.0.0/8", // IPv4 loopback + "10.0.0.0/8", // RFC1918 + "172.16.0.0/12", // RFC1918 + "192.168.0.0/16", // RFC1918 + "::1/128", // IPv6 loopback + "fe80::/10", // IPv6 link-local + } { + _, block, _ := net.ParseCIDR(cidr) + privateIPBlocks = append(privateIPBlocks, block) + } +} + +func isPrivateIP(ip net.IP) bool { + for _, block := range privateIPBlocks { + if block.Contains(ip) { + return true + } + } + return false +} + +// Username / realname / webirc hostname can all have configurable replacements +func makeClientReplacements(format string, client *Client) string { + ret := format + ret = strings.Replace(ret, "%a", client.RemoteAddr, -1) + ret = strings.Replace(ret, "%i", Ipv4ToHex(client.RemoteAddr), -1) + ret = strings.Replace(ret, "%h", client.RemoteHostname, -1) + ret = strings.Replace(ret, "%n", client.IrcState.Nick, -1) + return ret +} + +func Ipv4ToHex(ip string) string { + var ipParts [4]int + fmt.Sscanf(ip, "%d.%d.%d.%d", &ipParts[0], &ipParts[1], &ipParts[2], &ipParts[3]) + ipHex := fmt.Sprintf("%02x%02x%02x%02x", ipParts[0], ipParts[1], ipParts[2], ipParts[3]) + return ipHex +} + +func ensureUtf8(s string, fromEncoding string) string { + if utf8.ValidString(s) { + return s + } + + encoding, encErr := charset.Lookup(fromEncoding) + if encoding == nil { + println("encErr:", encErr) + return "" + } + + d := encoding.NewDecoder() + s2, _ := d.String(s) + return s2 +} + +func utf8ToOther(s string, toEncoding string) string { + if toEncoding == "UTF-8" && utf8.ValidString(s) { + return s + } + + encoding, _ := charset.Lookup(toEncoding) + if encoding == nil { + return "" + } + + e := encoding.NewEncoder() + s2, _ := e.String(s) + return s2 +} + +func containsOneOf(s string, substrs []string) bool { + for _, substr := range substrs { + if strings.Contains(s, substr) { + return true + } + } + + return false +} + +func stringInSlice(s string, slice []string) bool { + for _, v := range slice { + if v == s { + return true + } + } + return false +} + +func stringInSliceOrDefault(s, def string, validStrings []string) string { + if stringInSlice(s, validStrings) { + return s + } + return def +} + +type ThrottledStringChannel struct { + in chan string + Input chan<- string + out chan string + Output <-chan string + *rate.Limiter +} + +func NewThrottledStringChannel(wrappedChan chan string, limiter *rate.Limiter) *ThrottledStringChannel { + out := make(chan string, 50) + + c := &ThrottledStringChannel{ + in: wrappedChan, + Input: wrappedChan, + out: out, + Output: out, + Limiter: limiter, + } + + go c.run() + + return c +} + +func (c *ThrottledStringChannel) run() { + for msg := range c.in { + // start := time.Now() + c.Wait(context.Background()) + c.out <- msg + // elapsed := time.Since(start) + // fmt.Printf("waited %v to send %v\n", elapsed, msg) + } + + close(c.out) +} |
