diff options
Diffstat (limited to 'deprecated-webircgateway/pkg/webircgateway/client.go')
| -rw-r--r-- | deprecated-webircgateway/pkg/webircgateway/client.go | 741 |
1 files changed, 741 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 +} |
