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