diff options
| author | Mistivia <i@mistivia.com> | 2025-11-02 15:29:28 +0800 |
|---|---|---|
| committer | Mistivia <i@mistivia.com> | 2025-11-02 15:29:28 +0800 |
| commit | 9f42c2d5f911cb4e215d7873221e642ce7df4d61 (patch) | |
| tree | 6dac90a889a7402a9556d3d1bcc5cb53cdb9f123 /webircgateway/pkg | |
| parent | fb2d9de539b660a261af19b1cbcceb7ee7980cb1 (diff) | |
deprecate webircdateway and ngircd
Diffstat (limited to 'webircgateway/pkg')
22 files changed, 0 insertions, 4070 deletions
diff --git a/webircgateway/pkg/dnsbl/dnsbl.go b/webircgateway/pkg/dnsbl/dnsbl.go deleted file mode 100644 index 318102e..0000000 --- a/webircgateway/pkg/dnsbl/dnsbl.go +++ /dev/null @@ -1,121 +0,0 @@ -package dnsbl - -import ( - "encoding/hex" - "fmt" - "net" - "strings" -) - -type ResultList struct { - Listed bool - Results []Result -} - -/* -Result holds the individual IP lookup results for each RBL search -*/ -type Result struct { - // Blacklist is the DNSBL server that gave this result - Blacklist string - // Address is the IP address that was searched - Address string - // Listed indicates whether or not the IP was on the RBL - Listed bool - // RBL lists sometimes add extra information as a TXT record - // if any info is present, it will be stored here. - Text string - // Error represents any error that was encountered (DNS timeout, host not - // found, etc.) if any - Error bool - // ErrorType is the type of error encountered if any - ErrorType error -} - -/* -Convert an IP to a hostname ready for a dnsbl lookup -127.0.0.1 becomes 1.0.0.127 -1234:1234:1234:1234:1234:1234:1234:1234 becomes 4.3.2.1.4.3.2.1.4.3.2.1.4.3.2.1.4.3.2.1.4.3.2.1.4.3.2.1.4.3.2.1 -*/ -func toDnsBlHostname(ip net.IP) string { - if ip.To4() != nil { - // IPv4 - // Reverse the complete octects - splitAddress := strings.Split(ip.String(), ".") - for i, j := 0, len(splitAddress)-1; i < len(splitAddress)/2; i, j = i+1, j-1 { - splitAddress[i], splitAddress[j] = splitAddress[j], splitAddress[i] - } - - return strings.Join(splitAddress, ".") - } - - // IPv6 - // Remove all : from a full expanded address, then reverse all the hex characters - ipv6Str := expandIPv6(ip) - addrHexStr := strings.ReplaceAll(ipv6Str, ":", "") - chars := []rune(addrHexStr) - for i, j := 0, len(chars)-1; i < j; i, j = i+1, j-1 { - chars[i], chars[j] = chars[j], chars[i] - } - - return strings.Join(strings.Split(string(chars), ""), ".") -} - -func expandIPv6(ip net.IP) string { - dst := make([]byte, hex.EncodedLen(len(ip))) - _ = hex.Encode(dst, ip) - return string(dst[0:4]) + ":" + - string(dst[4:8]) + ":" + - string(dst[8:12]) + ":" + - string(dst[12:16]) + ":" + - string(dst[16:20]) + ":" + - string(dst[20:24]) + ":" + - string(dst[24:28]) + ":" + - string(dst[28:]) -} - -func query(rbl string, host string, r *Result) { - r.Listed = false - - lookup := fmt.Sprintf("%s.%s", host, rbl) - res, err := net.LookupHost(lookup) - - if len(res) > 0 { - r.Listed = true - txt, _ := net.LookupTXT(lookup) - if len(txt) > 0 { - r.Text = txt[0] - } - } - if err != nil { - r.Error = true - r.ErrorType = err - } - - return -} - -func Lookup(dnsblList []string, targetHost string) (r ResultList) { - ip, err := net.LookupIP(targetHost) - if err != nil { - return - } - - for _, dnsbl := range dnsblList { - for _, addr := range ip { - res := Result{} - res.Blacklist = dnsbl - res.Address = addr.String() - - addr := toDnsBlHostname(addr) - query(dnsbl, addr, &res) - r.Results = append(r.Results, res) - - if res.Listed { - r.Listed = true - } - } - } - - return -} diff --git a/webircgateway/pkg/identd/identd.go b/webircgateway/pkg/identd/identd.go deleted file mode 100644 index 9a84d76..0000000 --- a/webircgateway/pkg/identd/identd.go +++ /dev/null @@ -1,86 +0,0 @@ -package identd - -import ( - "fmt" - "net" - "net/textproto" - "strings" - "sync" -) - -// Server - An IdentD server -type Server struct { - Entries map[string]string - EntriesLock sync.Mutex -} - -// NewIdentdServer - Create a new IdentdServer instance -func NewIdentdServer() Server { - return Server{ - Entries: make(map[string]string), - } -} - -// AddIdent - Add an ident to be looked up -func (i *Server) AddIdent(localPort, remotePort int, ident string, iface string) { - i.EntriesLock.Lock() - i.Entries[fmt.Sprintf("%d-%d", localPort, remotePort)] = ident - i.EntriesLock.Unlock() -} - -// RemoveIdent - Remove an ident from being looked up -func (i *Server) RemoveIdent(localPort, remotePort int, iface string) { - i.EntriesLock.Lock() - delete(i.Entries, fmt.Sprintf("%d-%d", localPort, remotePort)) - i.EntriesLock.Unlock() -} - -// Run - Start listening for ident lookups -func (i *Server) Run() error { - serv, err := net.Listen("tcp", ":113") - if err != nil { - return err - } - - go i.ListenForRequests(&serv) - return nil -} - -// ListenForRequests - Listen on a net.Listener for ident lookups -func (i *Server) ListenForRequests(serverSocket *net.Listener) { - for { - serv := *serverSocket - client, err := serv.Accept() - if err != nil { - break - } - - go func(conn net.Conn) { - tc := textproto.NewConn(conn) - - line, err := tc.ReadLine() - if err != nil { - conn.Close() - return - } - - // Remove all spaces, some servers like to send "%d , %d" but the spec examples use "%d, %d" - line = strings.ReplaceAll(line, " ", "") - - var localPort, remotePort int - fmt.Sscanf(line, "%d,%d", &localPort, &remotePort) - if localPort > 0 && remotePort > 0 { - i.EntriesLock.Lock() - ident, ok := i.Entries[fmt.Sprintf("%d-%d", localPort, remotePort)] - i.EntriesLock.Unlock() - if !ok { - fmt.Fprintf(conn, "%d, %d : ERROR : NO-USER\r\n", localPort, remotePort) - } else { - fmt.Fprintf(conn, "%d, %d : USERID : UNIX : %s\r\n", localPort, remotePort, ident) - } - } - - conn.Close() - }(client) - } -} diff --git a/webircgateway/pkg/identd/rpcclient.go b/webircgateway/pkg/identd/rpcclient.go deleted file mode 100644 index 37aec3e..0000000 --- a/webircgateway/pkg/identd/rpcclient.go +++ /dev/null @@ -1,59 +0,0 @@ -package identd - -import "net" -import "fmt" -import "time" - -func MakeRpcClient(appName string) *RpcClient { - return &RpcClient{AppName: appName} -} - -type RpcClient struct { - AppName string - Conn *net.Conn -} - -func (rpc *RpcClient) ConnectAndReconnect(serverAddress string) { - for { - if rpc.Conn == nil { - println("Connecting to identd RPC...") - rpc.Connect(serverAddress) - } - - time.Sleep(time.Second * 3) - } -} - -func (rpc *RpcClient) Connect(serverAddress string) error { - conn, err := net.Dial("tcp", serverAddress) - if err != nil { - return err - } - - rpc.Conn = &conn - rpc.Write("id " + rpc.AppName) - - return nil -} - -func (rpc *RpcClient) Write(line string) error { - if rpc.Conn == nil { - return fmt.Errorf("not connected") - } - - conn := *rpc.Conn - _, err := conn.Write([]byte(line + "\n")) - if err != nil { - rpc.Conn = nil - conn.Close() - } - return err -} - -func (rpc *RpcClient) AddIdent(lport int, rport int, username string, iface string) { - rpc.Write(fmt.Sprintf("add %s %d %d %s", username, lport, rport, iface)) -} - -func (rpc *RpcClient) RemoveIdent(lport int, rport int, username string, iface string) { - rpc.Write(fmt.Sprintf("del %d %d %s", lport, rport, iface)) -} diff --git a/webircgateway/pkg/irc/isupport.go b/webircgateway/pkg/irc/isupport.go deleted file mode 100644 index fdb7bee..0000000 --- a/webircgateway/pkg/irc/isupport.go +++ /dev/null @@ -1,56 +0,0 @@ -package irc - -import ( - "strings" - "sync" -) - -type ISupport struct { - Received bool - Injected bool - Tags map[string]string - tokens map[string]string - tokensMutex sync.RWMutex -} - -func (m *ISupport) ClearTokens() { - m.tokensMutex.Lock() - m.tokens = make(map[string]string) - m.tokensMutex.Unlock() -} - -func (m *ISupport) AddToken(tokenPair string) { - m.tokensMutex.Lock() - m.addToken(tokenPair) - m.tokensMutex.Unlock() -} - -func (m *ISupport) AddTokens(tokenPairs []string) { - m.tokensMutex.Lock() - for _, tp := range tokenPairs { - m.addToken(tp) - } - m.tokensMutex.Unlock() -} - -func (m *ISupport) HasToken(key string) (ok bool) { - m.tokensMutex.RLock() - _, ok = m.tokens[strings.ToUpper(key)] - m.tokensMutex.RUnlock() - return -} - -func (m *ISupport) GetToken(key string) (val string) { - m.tokensMutex.RLock() - val = m.tokens[strings.ToUpper(key)] - m.tokensMutex.RUnlock() - return -} - -func (m *ISupport) addToken(tokenPair string) { - kv := strings.Split(tokenPair, "=") - if len(kv) == 1 { - kv = append(kv, "") - } - m.tokens[strings.ToUpper(kv[0])] = kv[1] -} diff --git a/webircgateway/pkg/irc/message.go b/webircgateway/pkg/irc/message.go deleted file mode 100644 index 18477d6..0000000 --- a/webircgateway/pkg/irc/message.go +++ /dev/null @@ -1,217 +0,0 @@ -package irc - -import ( - "errors" - "strings" -) - -type Mask struct { - Nick string - Username string - Hostname string - Mask string -} -type Message struct { - Raw string - Tags map[string]string - Prefix *Mask - Command string - Params []string -} - -func NewMessage() *Message { - return &Message{ - Tags: make(map[string]string), - Prefix: &Mask{}, - } -} - -// GetParam - Get a param value, returning a default value if it doesn't exist -func (m *Message) GetParam(idx int, def string) string { - if idx < 0 || idx > len(m.Params)-1 { - return def - } - - return m.Params[idx] -} - -// GetParamU - Get a param value in uppercase, returning a default value if it doesn't exist -func (m *Message) GetParamU(idx int, def string) string { - return strings.ToUpper(m.GetParam(idx, def)) -} - -// ToLine - Convert the Message struct to its raw IRC line -func (m *Message) ToLine() string { - line := "" - - if len(m.Tags) > 0 { - line += "@" - tagCount := 0 - for tagName, tagVal := range m.Tags { - tagCount++ - line += tagName - if tagVal != "" { - line += "=" + tagVal - } - if tagCount < len(m.Tags) { - line += ";" - } - } - } - - if m.Prefix != nil && (m.Prefix.Nick != "" || m.Prefix.Username != "" || m.Prefix.Hostname != "") { - prefix := "" - - if m.Prefix.Nick != "" { - prefix += m.Prefix.Nick - } - - if m.Prefix.Username != "" && m.Prefix.Nick != "" { - prefix += "!" + m.Prefix.Username - } else if m.Prefix.Username != "" { - prefix += m.Prefix.Username - } - - if m.Prefix.Hostname != "" && prefix != "" { - prefix += "@" + m.Prefix.Username - } else if m.Prefix.Hostname != "" { - prefix += m.Prefix.Hostname - } - - if line != "" { - line += " :" + prefix - } else { - line += ":" + prefix - } - } - - if line != "" { - line += " " + m.Command - } else { - line += m.Command - } - - paramLen := len(m.Params) - for idx, param := range m.Params { - if idx == paramLen-1 && (strings.Contains(param, " ") || strings.HasPrefix(param, ":")) { - line += " :" + param - } else { - line += " " + param - } - } - - return line -} - -func createMask(maskStr string) *Mask { - mask := &Mask{ - Mask: maskStr, - } - - usernameStart := strings.Index(maskStr, "!") - hostStart := strings.Index(maskStr, "@") - - if usernameStart == -1 && hostStart == -1 { - mask.Nick = maskStr - } else if usernameStart > -1 && hostStart > -1 { - mask.Nick = maskStr[0:usernameStart] - mask.Username = maskStr[usernameStart+1 : hostStart] - mask.Hostname = maskStr[hostStart+1:] - } else if usernameStart > -1 && hostStart == -1 { - mask.Nick = maskStr[0:usernameStart] - mask.Username = maskStr[usernameStart+1:] - } else if usernameStart == -1 && hostStart > -1 { - mask.Username = maskStr[0:hostStart] - mask.Hostname = maskStr[hostStart+1:] - } - - return mask -} - -// ParseLine - Turn a raw IRC line into a message -func ParseLine(input string) (*Message, error) { - line := strings.Trim(input, "\r\n") - - message := NewMessage() - message.Raw = line - - token := "" - rest := "" - - token, rest = nextToken(line, false) - if token == "" { - return message, errors.New("Empty line") - } - - // Tags. Starts with "@" - if token[0] == 64 { - tagsRaw := token[1:] - tags := strings.Split(tagsRaw, ";") - for _, tag := range tags { - parts := strings.Split(tag, "=") - if len(parts) > 0 && parts[0] == "" { - continue - } - - if len(parts) == 1 { - message.Tags[parts[0]] = "" - } else { - message.Tags[parts[0]] = parts[1] - } - } - - token, rest = nextToken(rest, false) - } - - // Prefix. Starts with ":" - if token != "" && token[0] == 58 { - message.Prefix = createMask(token[1:]) - token, rest = nextToken(rest, false) - } else { - message.Prefix = createMask("") - } - - // Command - if token == "" { - return message, errors.New("Missing command") - } - - message.Command = token - - // Params - for { - token, rest = nextToken(rest, true) - if token == "" { - break - } - - message.Params = append(message.Params, token) - } - - return message, nil -} - -func nextToken(s string, allowTrailing bool) (string, string) { - s = strings.TrimLeft(s, " ") - - if len(s) == 0 { - return "", "" - } - - // The last token (trailing) start with : - if allowTrailing && s[0] == 58 { - return s[1:], "" - } - - token := "" - spaceIdx := strings.Index(s, " ") - if spaceIdx > -1 { - token = s[:spaceIdx] - s = s[spaceIdx+1:] - } else { - token = s - s = "" - } - - return token, s -} diff --git a/webircgateway/pkg/irc/state.go b/webircgateway/pkg/irc/state.go deleted file mode 100644 index 69480fc..0000000 --- a/webircgateway/pkg/irc/state.go +++ /dev/null @@ -1,79 +0,0 @@ -package irc - -import ( - "strings" - "sync" - "time" -) - -type State struct { - LocalPort int - RemotePort int - Username string - Nick string - RealName string - Password string - Account string - Modes map[string]string - - channelsMutex sync.Mutex - Channels map[string]*StateChannel - ISupport *ISupport -} - -type StateChannel struct { - Name string - Modes map[string]string - Joined time.Time -} - -func NewState() *State { - return &State{ - Channels: make(map[string]*StateChannel), - ISupport: &ISupport{ - tokens: make(map[string]string), - }, - } -} - -func NewStateChannel(name string) *StateChannel { - return &StateChannel{ - Name: name, - Modes: make(map[string]string), - Joined: time.Now(), - } -} - -func (m *State) HasChannel(name string) (ok bool) { - m.channelsMutex.Lock() - _, ok = m.Channels[strings.ToLower(name)] - m.channelsMutex.Unlock() - return -} - -func (m *State) GetChannel(name string) (channel *StateChannel) { - m.channelsMutex.Lock() - channel = m.Channels[strings.ToLower(name)] - m.channelsMutex.Unlock() - return -} - -func (m *State) SetChannel(channel *StateChannel) { - m.channelsMutex.Lock() - m.Channels[strings.ToLower(channel.Name)] = channel - m.channelsMutex.Unlock() -} - -func (m *State) RemoveChannel(name string) { - m.channelsMutex.Lock() - delete(m.Channels, strings.ToLower(name)) - m.channelsMutex.Unlock() -} - -func (m *State) ClearChannels() { - m.channelsMutex.Lock() - for i := range m.Channels { - delete(m.Channels, i) - } - m.channelsMutex.Unlock() -} diff --git a/webircgateway/pkg/proxy/proxy.go b/webircgateway/pkg/proxy/proxy.go deleted file mode 100644 index c332f40..0000000 --- a/webircgateway/pkg/proxy/proxy.go +++ /dev/null @@ -1,129 +0,0 @@ -package proxy - -import ( - "encoding/json" - "errors" - "io" - "net" -) - -type KiwiProxyState int - -const KiwiProxyStateClosed KiwiProxyState = 0 -const KiwiProxyStateConnecting KiwiProxyState = 1 -const KiwiProxyStateHandshaking KiwiProxyState = 2 -const KiwiProxyStateConnected KiwiProxyState = 3 - -type ConnError struct { - Msg string - Type string -} - -func (err *ConnError) Error() string { - return err.Msg -} - -type KiwiProxyConnection struct { - Username string - ProxyInterface string - DestHost string - DestPort int - DestTLS bool - State KiwiProxyState - Conn *net.Conn -} - -func MakeKiwiProxyConnection() *KiwiProxyConnection { - return &KiwiProxyConnection{ - State: KiwiProxyStateClosed, - } -} - -func (c *KiwiProxyConnection) Close() error { - if c.State == KiwiProxyStateClosed { - return errors.New("Connection already closed") - } - - return (*c.Conn).Close() -} - -func (c *KiwiProxyConnection) Dial(proxyServerAddr string) error { - if c.State != KiwiProxyStateClosed { - return errors.New("Connection in closed state") - } - - c.State = KiwiProxyStateConnecting - - conn, err := net.Dial("tcp", proxyServerAddr) - if err != nil { - return err - } - - c.Conn = &conn - c.State = KiwiProxyStateHandshaking - - meta, _ := json.Marshal(map[string]interface{}{ - "username": c.Username, - "interface": c.ProxyInterface, - "host": c.DestHost, - "port": c.DestPort, - "ssl": c.DestTLS, - }) - - (*c.Conn).Write(append(meta, byte('\n'))) - - buf := make([]byte, 1024) - bufLen, readErr := (*c.Conn).Read(buf) - if readErr != nil { - (*c.Conn).Close() - c.State = KiwiProxyStateClosed - return readErr - } - - response := string(buf) - if bufLen > 0 && response[0] == '1' { - c.State = KiwiProxyStateConnected - } else { - (*c.Conn).Close() - c.State = KiwiProxyStateClosed - - if bufLen == 0 { - return errors.New("The proxy could not connect to the destination") - } - - switch response[0] { - case '0': - return errors.New("The proxy could not connect to the destination") - case '2': - return &ConnError{Msg: "Connection reset", Type: "conn_reset"} - case '3': - return &ConnError{Msg: "Connection refused", Type: "conn_refused"} - case '4': - return &ConnError{Msg: "Host not found", Type: "not_found"} - case '5': - return &ConnError{Msg: "Connection timed out", Type: "conn_timeout"} - } - } - - return nil -} - -func (c *KiwiProxyConnection) Read(b []byte) (n int, err error) { - if c.State == KiwiProxyStateConnecting || c.State == KiwiProxyStateHandshaking { - return 0, nil - } else if c.State == KiwiProxyStateClosed { - return 0, io.EOF - } else { - return (*c.Conn).Read(b) - } -} - -func (c *KiwiProxyConnection) Write(b []byte) (n int, err error) { - if c.State == KiwiProxyStateConnecting || c.State == KiwiProxyStateHandshaking { - return 0, nil - } else if c.State == KiwiProxyStateClosed { - return 0, io.EOF - } else { - return (*c.Conn).Write(b) - } -} diff --git a/webircgateway/pkg/proxy/server.go b/webircgateway/pkg/proxy/server.go deleted file mode 100644 index 7e3f62f..0000000 --- a/webircgateway/pkg/proxy/server.go +++ /dev/null @@ -1,237 +0,0 @@ -package proxy - -import ( - "bufio" - "crypto/tls" - "encoding/json" - "fmt" - "io" - "log" - "net" - "strconv" - "sync" - "syscall" - "time" - - "github.com/kiwiirc/webircgateway/pkg/identd" -) - -const ( - ResponseError = "0" - ResponseOK = "1" - ResponseReset = "2" - ResponseRefused = "3" - ResponseUnknownHost = "4" - ResponseTimeout = "5" -) - -var identdRpc *identd.RpcClient -var Server net.Listener - -type HandshakeMeta struct { - Host string `json:"host"` - Port int `json:"port"` - TLS bool `json:"ssl"` - Username string `json:"username"` - Interface string `json:"interface"` -} - -func MakeClient(conn net.Conn) *Client { - return &Client{ - Client: conn, - } -} - -type Client struct { - Client net.Conn - Upstream net.Conn - UpstreamAddr *net.TCPAddr - Username string - BindAddr *net.TCPAddr - TLS bool -} - -func (c *Client) Run() { - var err error - - err = c.Handshake() - if err != nil { - log.Println(err.Error()) - return - } - - err = c.ConnectUpstream() - if err != nil { - log.Println(err.Error()) - return - } - - c.Pipe() -} - -func (c *Client) Handshake() error { - // Read the first line - it should be JSON - reader := bufio.NewReader(c.Client) - line, readErr := reader.ReadBytes('\n') - if readErr != nil { - return readErr - } - - var meta = HandshakeMeta{ - Username: "user", - Port: 6667, - Interface: "0.0.0.0", - } - unmarshalErr := json.Unmarshal(line, &meta) - if unmarshalErr != nil { - c.Client.Write([]byte(ResponseError)) - return unmarshalErr - } - - if meta.Host == "" || meta.Port == 0 || meta.Username == "" || meta.Interface == "" { - c.Client.Write([]byte(ResponseError)) - return fmt.Errorf("missing args") - } - - c.Username = meta.Username - c.TLS = meta.TLS - - bindAddr, bindAddrErr := net.ResolveTCPAddr("tcp", meta.Interface+":") - if bindAddrErr != nil { - c.Client.Write([]byte(ResponseError)) - return fmt.Errorf("interface: " + bindAddrErr.Error()) - } - c.BindAddr = bindAddr - - hostStr := net.JoinHostPort(meta.Host, strconv.Itoa(meta.Port)) - addr, addrErr := net.ResolveTCPAddr("tcp", hostStr) - if addrErr != nil { - c.Client.Write([]byte(ResponseUnknownHost)) - return fmt.Errorf("remote host: " + addrErr.Error()) - } - c.UpstreamAddr = addr - - return nil -} - -func (c *Client) ConnectUpstream() error { - dialer := &net.Dialer{} - dialer.LocalAddr = c.BindAddr - dialer.Timeout = time.Second * 10 - - conn, err := dialer.Dial("tcp", c.UpstreamAddr.String()) - if err != nil { - response := "" - errType := typeOfErr(err) - switch errType { - case "timeout": - response = ResponseTimeout - case "unknown_host": - response = ResponseUnknownHost - case "refused": - response = ResponseRefused - } - - c.Client.Write([]byte(response)) - return err - } - - if identdRpc != nil { - lAddr, lPortStr, _ := net.SplitHostPort(conn.LocalAddr().String()) - lPort, _ := strconv.Atoi(lPortStr) - identdRpc.AddIdent(lPort, c.UpstreamAddr.Port, c.Username, lAddr) - } - - if c.TLS { - tlsConfig := &tls.Config{InsecureSkipVerify: true} - tlsConn := tls.Client(conn, tlsConfig) - err := tlsConn.Handshake() - if err != nil { - conn.Close() - c.Client.Write([]byte(ResponseReset)) - return err - } - - conn = net.Conn(tlsConn) - } - - c.Upstream = conn - c.Client.Write([]byte(ResponseOK)) - return nil -} - -func (c *Client) Pipe() { - wg := sync.WaitGroup{} - wg.Add(2) - - go func() { - io.Copy(c.Client, c.Upstream) - c.Client.Close() - wg.Done() - }() - - go func() { - io.Copy(c.Upstream, c.Client) - c.Upstream.Close() - wg.Done() - }() - - wg.Wait() - - if identdRpc != nil { - lAddr, lPortStr, _ := net.SplitHostPort(c.Upstream.LocalAddr().String()) - lPort, _ := strconv.Atoi(lPortStr) - identdRpc.RemoveIdent(lPort, c.UpstreamAddr.Port, c.Username, lAddr) - } -} - -func Start(laddr string) { - srv, err := net.Listen("tcp", laddr) - if err != nil { - log.Fatal(err.Error()) - } - - // Expose the server - Server = srv - log.Printf("Kiwi proxy listening on %s", srv.Addr().String()) - - identdRpc = identd.MakeRpcClient("kiwiproxy" + laddr) - go identdRpc.ConnectAndReconnect("127.0.0.1:1133") - - for { - conn, err := srv.Accept() - if err != nil { - log.Print(err.Error()) - break - } - - c := MakeClient(conn) - go c.Run() - } -} - -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 *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 "" -} diff --git a/webircgateway/pkg/recaptcha/recaptcha.go b/webircgateway/pkg/recaptcha/recaptcha.go deleted file mode 100644 index 2d602fc..0000000 --- a/webircgateway/pkg/recaptcha/recaptcha.go +++ /dev/null @@ -1,59 +0,0 @@ -// Google re-captcha package tweaked from http://github.com/haisum/recaptcha - -package recaptcha - -import ( - "encoding/json" - "io/ioutil" - "net/http" - "net/url" - "time" -) - -// R type represents an object of Recaptcha and has public property Secret, -// which is secret obtained from google recaptcha tool admin interface -type R struct { - URL string - Secret string - lastError []string -} - -// Struct for parsing json in google's response -type googleResponse struct { - Success bool - ErrorCodes []string `json:"error-codes"` -} - -// VerifyResponse is a method similar to `Verify`; but doesn't parse the form for you. Useful if -// you're receiving the data as a JSON object from a javascript app or similar. -func (r *R) VerifyResponse(response string) bool { - r.lastError = make([]string, 1) - client := &http.Client{Timeout: 20 * time.Second} - resp, err := client.PostForm(r.URL, - url.Values{"secret": {r.Secret}, "response": {response}}) - if err != nil { - r.lastError = append(r.lastError, err.Error()) - return false - } - defer resp.Body.Close() - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - r.lastError = append(r.lastError, err.Error()) - return false - } - gr := new(googleResponse) - err = json.Unmarshal(body, gr) - if err != nil { - r.lastError = append(r.lastError, err.Error()) - return false - } - if !gr.Success { - r.lastError = append(r.lastError, gr.ErrorCodes...) - } - return gr.Success -} - -// LastError returns errors occurred in last re-captcha validation attempt -func (r R) LastError() []string { - return r.lastError -} diff --git a/webircgateway/pkg/webircgateway/client.go b/webircgateway/pkg/webircgateway/client.go deleted file mode 100644 index 43d3fe7..0000000 --- a/webircgateway/pkg/webircgateway/client.go +++ /dev/null @@ -1,741 +0,0 @@ -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/webircgateway/pkg/webircgateway/client_command_handlers.go b/webircgateway/pkg/webircgateway/client_command_handlers.go deleted file mode 100644 index d5d1fcc..0000000 --- a/webircgateway/pkg/webircgateway/client_command_handlers.go +++ /dev/null @@ -1,495 +0,0 @@ -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/webircgateway/pkg/webircgateway/config.go b/webircgateway/pkg/webircgateway/config.go deleted file mode 100644 index 019d955..0000000 --- a/webircgateway/pkg/webircgateway/config.go +++ /dev/null @@ -1,385 +0,0 @@ -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/webircgateway/pkg/webircgateway/gateway.go b/webircgateway/pkg/webircgateway/gateway.go deleted file mode 100644 index 47169ef..0000000 --- a/webircgateway/pkg/webircgateway/gateway.go +++ /dev/null @@ -1,278 +0,0 @@ -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/webircgateway/pkg/webircgateway/gateway_utils.go b/webircgateway/pkg/webircgateway/gateway_utils.go deleted file mode 100644 index 4b2a38d..0000000 --- a/webircgateway/pkg/webircgateway/gateway_utils.go +++ /dev/null @@ -1,133 +0,0 @@ -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/webircgateway/pkg/webircgateway/hooks.go b/webircgateway/pkg/webircgateway/hooks.go deleted file mode 100644 index 1bfd564..0000000 --- a/webircgateway/pkg/webircgateway/hooks.go +++ /dev/null @@ -1,152 +0,0 @@ -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/webircgateway/pkg/webircgateway/letsencrypt.go b/webircgateway/pkg/webircgateway/letsencrypt.go deleted file mode 100644 index ffa6afe..0000000 --- a/webircgateway/pkg/webircgateway/letsencrypt.go +++ /dev/null @@ -1,41 +0,0 @@ -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/webircgateway/pkg/webircgateway/messagetags.go b/webircgateway/pkg/webircgateway/messagetags.go deleted file mode 100644 index 9715ad6..0000000 --- a/webircgateway/pkg/webircgateway/messagetags.go +++ /dev/null @@ -1,103 +0,0 @@ -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/webircgateway/pkg/webircgateway/transport_kiwiirc.go b/webircgateway/pkg/webircgateway/transport_kiwiirc.go deleted file mode 100644 index e5247e8..0000000 --- a/webircgateway/pkg/webircgateway/transport_kiwiirc.go +++ /dev/null @@ -1,206 +0,0 @@ -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/webircgateway/pkg/webircgateway/transport_sockjs.go b/webircgateway/pkg/webircgateway/transport_sockjs.go deleted file mode 100644 index da4891f..0000000 --- a/webircgateway/pkg/webircgateway/transport_sockjs.go +++ /dev/null @@ -1,107 +0,0 @@ -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/webircgateway/pkg/webircgateway/transport_tcp.go b/webircgateway/pkg/webircgateway/transport_tcp.go deleted file mode 100644 index b4af7b3..0000000 --- a/webircgateway/pkg/webircgateway/transport_tcp.go +++ /dev/null @@ -1,113 +0,0 @@ -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/webircgateway/pkg/webircgateway/transport_websocket.go b/webircgateway/pkg/webircgateway/transport_websocket.go deleted file mode 100644 index 960718b..0000000 --- a/webircgateway/pkg/webircgateway/transport_websocket.go +++ /dev/null @@ -1,126 +0,0 @@ -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/webircgateway/pkg/webircgateway/utils.go b/webircgateway/pkg/webircgateway/utils.go deleted file mode 100644 index 1fc687a..0000000 --- a/webircgateway/pkg/webircgateway/utils.go +++ /dev/null @@ -1,147 +0,0 @@ -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) -} |
