diff --git a/go.mod b/go.mod index 080ff55c..27696d28 100644 --- a/go.mod +++ b/go.mod @@ -23,7 +23,7 @@ require ( github.com/jtolds/gls v4.2.1+incompatible // indirect github.com/keybase/go-keybase-chat-bot v0.0.0-20190816161829-561f10822eb2 github.com/labstack/echo/v4 v4.1.6 - github.com/lrstanley/girc v0.0.0-20190210212025-51b8e096d398 + github.com/lrstanley/girc v0.0.0-20190801035559-4fc93959e1a7 github.com/lusis/go-slackbot v0.0.0-20180109053408-401027ccfef5 // indirect github.com/lusis/slack-test v0.0.0-20180109053238-3c758769bfa6 // indirect github.com/matterbridge/Rocket.Chat.Go.SDK v0.0.0-20190210153444-cc9d05784d5d diff --git a/go.sum b/go.sum index 7668b495..d92544c7 100644 --- a/go.sum +++ b/go.sum @@ -110,8 +110,8 @@ github.com/labstack/echo/v4 v4.1.6 h1:WOvLa4T1KzWCRpANwz0HGgWDelXSSGwIKtKBbFdHTv github.com/labstack/echo/v4 v4.1.6/go.mod h1:kU/7PwzgNxZH4das4XNsSpBSOD09XIF5YEPzjpkGnGE= github.com/labstack/gommon v0.2.9 h1:heVeuAYtevIQVYkGj6A41dtfT91LrvFG220lavpWhrU= github.com/labstack/gommon v0.2.9/go.mod h1:E8ZTmW9vw5az5/ZyHWCp0Lw4OH2ecsaBP1C/NKavGG4= -github.com/lrstanley/girc v0.0.0-20190210212025-51b8e096d398 h1:a40kRmhA1p2XFJ6gqXfCExSyuDDCp/U9LA8ZY27u2Lk= -github.com/lrstanley/girc v0.0.0-20190210212025-51b8e096d398/go.mod h1:7cRs1SIBfKQ7e3Tam6GKTILSNHzR862JD0JpINaZoJk= +github.com/lrstanley/girc v0.0.0-20190801035559-4fc93959e1a7 h1:BS9tqL0OCiOGuy/CYYk2gc33fxqaqh5/rhqMKu4tcYA= +github.com/lrstanley/girc v0.0.0-20190801035559-4fc93959e1a7/go.mod h1:liX5MxHPrwgHaKowoLkYGwbXfYABh1jbZ6FpElbGF1I= github.com/lusis/go-slackbot v0.0.0-20180109053408-401027ccfef5 h1:AsEBgzv3DhuYHI/GiQh2HxvTP71HCCE9E/tzGUzGdtU= github.com/lusis/go-slackbot v0.0.0-20180109053408-401027ccfef5/go.mod h1:c2mYKRyMb1BPkO5St0c/ps62L4S0W2NAkaTXj9qEI+0= github.com/lusis/slack-test v0.0.0-20180109053238-3c758769bfa6 h1:iOAVXzZyXtW408TMYejlUPo6BIn92HmOacWtIfNyYns= diff --git a/vendor/github.com/lrstanley/girc/builtin.go b/vendor/github.com/lrstanley/girc/builtin.go index 7f285fda..778a5c68 100644 --- a/vendor/github.com/lrstanley/girc/builtin.go +++ b/vendor/github.com/lrstanley/girc/builtin.go @@ -93,7 +93,11 @@ func handleConnect(c *Client, e Event) { } time.Sleep(2 * time.Second) - c.RunHandlers(&Event{Command: CONNECTED, Params: []string{c.Server()}}) + + c.mu.RLock() + server := c.server() + c.mu.RUnlock() + c.RunHandlers(&Event{Command: CONNECTED, Params: []string{server}}) } // nickCollisionHandler helps prevent the client from having conflicting diff --git a/vendor/github.com/lrstanley/girc/cap.go b/vendor/github.com/lrstanley/girc/cap.go index 5995233f..38ff210c 100644 --- a/vendor/github.com/lrstanley/girc/cap.go +++ b/vendor/github.com/lrstanley/girc/cap.go @@ -5,7 +5,10 @@ package girc import ( + "fmt" + "strconv" "strings" + "time" ) // Something not in the list? Depending on the type of capability, you can @@ -19,13 +22,20 @@ var possibleCap = map[string][]string{ "chghost": nil, "extended-join": nil, "invite-notify": nil, + "message-tags": nil, + "msgid": nil, "multi-prefix": nil, "server-time": nil, "userhost-in-names": nil, + // Supported draft versions, some may be duplicated above, this is for backwards + // compatibility. "draft/message-tags-0.2": nil, "draft/msgid": nil, + // sts, sasl, etc are enabled dynamically/depending on client configuration, + // so aren't included on this list. + // "echo-message" is supported, but it's not enabled by default. This is // to prevent unwanted confusion and utilize less traffic if it's not needed. // echo messages aren't sent to girc.PRIVMSG and girc.NOTICE handlers, @@ -51,6 +61,17 @@ func possibleCapList(c *Client) map[string][]string { out["sasl"] = nil } + if !c.Config.DisableSTS && !c.Config.SSL { + // If fallback supported, and we failed recently, don't try negotiating STS. + // ONLY do this fallback if we're expired (primarily useful during the first + // sts negotation). + if time.Since(c.state.sts.lastFailed) < 5*time.Minute && !c.Config.DisableSTSFallback { + c.debug.Println("skipping strict transport policy negotiation; failed within the last 5 minutes") + } else { + out["sts"] = nil + } + } + for k := range c.Config.SupportedCaps { out[k] = c.Config.SupportedCaps[k] } @@ -62,8 +83,8 @@ func possibleCapList(c *Client) map[string][]string { return out } -func parseCap(raw string) map[string][]string { - out := make(map[string][]string) +func parseCap(raw string) map[string]map[string]string { + out := make(map[string]map[string]string) parts := strings.Split(raw, " ") var val int @@ -78,7 +99,16 @@ func parseCap(raw string) map[string][]string { continue } - out[parts[i][:val]] = strings.Split(parts[i][val+1:], ",") + out[parts[i][:val]] = make(map[string]string) + for _, option := range strings.Split(parts[i][val+1:], ",") { + j := strings.Index(option, "=") + + if j < 0 { + out[parts[i][:val]][option] = "" + } else { + out[parts[i][:val]][option[:j]] = option[j+1 : len(option)] + } + } } return out @@ -88,8 +118,15 @@ func parseCap(raw string) map[string][]string { // This will lock further registration until we have acknowledged (or denied) // the capabilities. func handleCAP(c *Client, e Event) { - if len(e.Params) >= 2 && (e.Params[1] == CAP_NEW || e.Params[1] == CAP_DEL) { - c.listCAP() + c.state.Lock() + defer c.state.Unlock() + + if len(e.Params) >= 2 && e.Params[1] == CAP_DEL { + caps := parseCap(e.Last()) + for cap := range caps { + // TODO: test the deletion. + delete(c.state.enabledCap, cap) + } return } @@ -101,27 +138,26 @@ func handleCAP(c *Client, e Event) { } possible := possibleCapList(c) - - if len(e.Params) >= 3 && e.Params[1] == CAP_LS { - c.state.Lock() - + // TODO: test the addition. + if len(e.Params) >= 3 && (e.Params[1] == CAP_LS || e.Params[1] == CAP_NEW) { caps := parseCap(e.Last()) - for k := range caps { - if _, ok := possible[k]; !ok { + for capName := range caps { + if _, ok := possible[capName]; !ok { continue } - if len(possible[k]) == 0 || len(caps[k]) == 0 { - c.state.tmpCap = append(c.state.tmpCap, k) + if len(possible[capName]) == 0 || len(caps[capName]) == 0 { + c.state.tmpCap[capName] = caps[capName] continue } var contains bool - for i := 0; i < len(caps[k]); i++ { - for j := 0; j < len(possible[k]); j++ { - if caps[k][i] == possible[k][j] { - // Assume we have a matching split value. + + for capAttr := range caps[capName] { + for i := 0; i < len(possible[capName]); i++ { + if _, ok := caps[capName][capAttr]; ok { + // Assuming we have a matching attribute for the capability. contains = true goto checkcontains } @@ -133,9 +169,8 @@ func handleCAP(c *Client, e Event) { continue } - c.state.tmpCap = append(c.state.tmpCap, k) + c.state.tmpCap[capName] = caps[capName] } - c.state.Unlock() // Indicates if this is a multi-line LS. (3 args means it's the // last LS). @@ -147,31 +182,113 @@ func handleCAP(c *Client, e Event) { } // Let them know which ones we'd like to enable. - c.write(&Event{Command: CAP, Params: []string{CAP_REQ, strings.Join(c.state.tmpCap, " ")}}) - - // Re-initialize the tmpCap, so if we get multiple 'CAP LS' requests - // due to cap-notify, we can re-evaluate what we can support. - c.state.Lock() - c.state.tmpCap = []string{} - c.state.Unlock() + reqKeys := make([]string, len(c.state.tmpCap)) + i := 0 + for k := range c.state.tmpCap { + reqKeys[i] = k + i++ + } + c.write(&Event{Command: CAP, Params: []string{CAP_REQ, strings.Join(reqKeys, " ")}}) } } if len(e.Params) == 3 && e.Params[1] == CAP_ACK { - c.state.Lock() - c.state.enabledCap = strings.Split(e.Last(), " ") - - // Do we need to do sasl auth? - wantsSASL := false - for i := 0; i < len(c.state.enabledCap); i++ { - if c.state.enabledCap[i] == "sasl" { - wantsSASL = true - break + enabled := strings.Split(e.Last(), " ") + for _, cap := range enabled { + if val, ok := c.state.tmpCap[cap]; ok { + c.state.enabledCap[cap] = val + } else { + c.state.enabledCap[cap] = nil } } - c.state.Unlock() - if wantsSASL { + // Anything client side that needs to be setup post-capability-acknowledgement, + // should be done here. + + // Handle STS, and only if it's something specifically we enabled (client + // may choose to disable girc automatic STS, and do it themselves). + if sts, sok := c.state.enabledCap["sts"]; sok && !c.Config.DisableSTS { + var isError bool + + // Some things are updated in the policy depending on if the current + // connection is over tls or not. + var hasTLSConnection bool + if tlsState, _ := c.TLSConnectionState(); tlsState != nil { + hasTLSConnection = true + } + + // "This key indicates the port number for making a secure connection. + // This key’s value MUST be a single port number. If the client is not + // already connected securely to the server at the requested hostname, + // it MUST close the insecure connection and reconnect securely on the + // stated port. + // + // To enforce an STS upgrade policy, servers MUST send this key to + // insecurely connected clients. Servers MAY send this key to securely + // connected clients, but it will be ignored." + // + // See: https://ircv3.net/specs/extensions/sts#the-port-key + if !hasTLSConnection { + if port, ok := sts["port"]; ok { + c.state.sts.upgradePort, _ = strconv.Atoi(port) + if c.state.sts.upgradePort < 21 { + isError = true + } + } else { + isError = true + } + } + + // "This key is used on secure connections to indicate how long clients + // MUST continue to use secure connections when connecting to the server + // at the requested hostname. The value of this key MUST be given as a + // single integer which represents the number of seconds until the persistence + // policy expires. + // + // To enforce an STS persistence policy, servers MUST send this key to + // securely connected clients. Servers MAY send this key to all clients, + // but insecurely connected clients MUST ignore it." + // + // See: https://ircv3.net/specs/extensions/sts#the-duration-key + if hasTLSConnection { + if duration, ok := sts["duration"]; ok { + c.state.sts.persistenceDuration, _ = strconv.Atoi(duration) + c.state.sts.persistenceReceived = time.Now() + } else { + isError = true + } + } + + // See: https://ircv3.net/specs/extensions/sts#the-preload-key + if hasTLSConnection { + if preload, ok := sts["preload"]; ok { + c.state.sts.preload, _ = strconv.ParseBool(preload) + } + } + + if isError { + c.rx <- &Event{Command: ERROR, Params: []string{ + fmt.Sprintf("closing connection: strict transport policy provided by server is invalid; possible MITM? config: %#v", sts), + }} + return + } + + // Only upgrade if not already upgraded. + if !hasTLSConnection { + c.state.sts.beginUpgrade = true + + c.RunHandlers(&Event{Command: STS_UPGRADE_INIT}) + c.debug.Println("strict transport security policy provided by server; closing connection to begin upgrade...") + c.Close() + return + } + } + + // Re-initialize the tmpCap, so if we get multiple 'CAP LS' requests + // due to cap-notify, we can re-evaluate what we can support. + c.state.tmpCap = make(map[string]map[string]string) + + if _, ok := c.state.enabledCap["sasl"]; ok && c.Config.SASL != nil { c.write(&Event{Command: AUTHENTICATE, Params: []string{c.Config.SASL.Method()}}) // Don't "CAP END", since we want to authenticate. return diff --git a/vendor/github.com/lrstanley/girc/cap_tags.go b/vendor/github.com/lrstanley/girc/cap_tags.go index aff10f69..42599f3a 100644 --- a/vendor/github.com/lrstanley/girc/cap_tags.go +++ b/vendor/github.com/lrstanley/girc/cap_tags.go @@ -38,7 +38,7 @@ const ( prefixTagValue byte = '=' prefixUserTag byte = '+' tagSeparator byte = ';' - maxTagLength int = 511 // 510 + @ and " " (space), though space usually not included. + maxTagLength int = 4094 // 4094 + @ and " " (space) = 4096, though space usually not included. ) // Tags represents the key-value pairs in IRCv3 message tags. The map contains @@ -55,6 +55,9 @@ type Tags map[string]string // @aaa=bbb;ccc;example.com/ddd=eee // NOT: // @aaa=bbb;ccc;example.com/ddd=eee :nick!ident@host.com PRIVMSG me :Hello +// +// Technically, there is a length limit of 4096, but the server should reject +// tag messages longer than this. func ParseTags(raw string) (t Tags) { t = make(Tags) @@ -79,11 +82,11 @@ func ParseTags(raw string) (t Tags) { } // Check if tag key or decoded value are invalid. - if !validTag(parts[i][:hasValue]) || !validTagValue(tagDecoder.Replace(parts[i][hasValue+1:])) { - continue - } + // if !validTag(parts[i][:hasValue]) || !validTagValue(tagDecoder.Replace(parts[i][hasValue+1:])) { + // continue + // } - t[parts[i][:hasValue]] = parts[i][hasValue+1:] + t[parts[i][:hasValue]] = tagDecoder.Replace(parts[i][hasValue+1:]) } return t diff --git a/vendor/github.com/lrstanley/girc/client.go b/vendor/github.com/lrstanley/girc/client.go index 038cc6a7..f8035755 100644 --- a/vendor/github.com/lrstanley/girc/client.go +++ b/vendor/github.com/lrstanley/girc/client.go @@ -12,8 +12,11 @@ import ( "io" "io/ioutil" "log" + "net" + "os" "runtime" "sort" + "strconv" "strings" "sync" "time" @@ -95,6 +98,16 @@ type Config struct { // configuration (e.g. to not force hostname checking). This only has an // affect during the dial process. SSL bool + // DisableSTS disables the use of automatic STS connection upgrades + // when the server supports STS. STS can also be disabled using the environment + // variable "GIRC_DISABLE_STS=true". As many clients may not propagate options + // like this back to the user, this allows to directly disable such automatic + // functionality. + DisableSTS bool + // DisableSTSFallback disables the "fallback" to a non-tls connection if the + // strict transport policy expires and the first attempt to reconnect back to + // the tls version fails. + DisableSTSFallback bool // TLSConfig is an optional user-supplied tls configuration, used during // socket creation to the server. SSL must be enabled for this to be used. // This only has an affect during the dial process. @@ -146,7 +159,7 @@ type Config struct { // disableTracking disables all channel and user-level tracking. Useful // for highly embedded scripts with single purposes. This has an exported - // method which enables this and ensures prop cleanup, see + // method which enables this and ensures proper cleanup, see // Client.DisableTracking(). disableTracking bool // HandleNickCollide when set, allows the client to handle nick collisions @@ -247,19 +260,34 @@ func New(config Config) *Client { c.Config.PingDelay = 600 * time.Second } + envDebug, _ := strconv.ParseBool(os.Getenv("GIRC_DEBUG")) if c.Config.Debug == nil { - c.debug = log.New(ioutil.Discard, "", 0) + if envDebug { + c.debug = log.New(os.Stderr, "debug:", log.Ltime|log.Lshortfile) + } else { + c.debug = log.New(ioutil.Discard, "", 0) + } } else { + if envDebug { + if c.Config.Debug != os.Stdout && c.Config.Debug != os.Stderr { + c.Config.Debug = io.MultiWriter(os.Stderr, c.Config.Debug) + } + } c.debug = log.New(c.Config.Debug, "debug:", log.Ltime|log.Lshortfile) c.debug.Print("initializing debugging") } + envDisableSTS, _ := strconv.ParseBool((os.Getenv("GIRC_DISABLE_STS"))) + if envDisableSTS { + c.Config.DisableSTS = envDisableSTS + } + // Setup the caller. c.Handlers = newCaller(c.debug) // Give ourselves a new state. c.state = &state{} - c.state.reset() + c.state.reset(true) // Register builtin handlers. c.registerBuiltins() @@ -323,6 +351,18 @@ func (c *Client) Close() { c.mu.RUnlock() } +// Quit sends a QUIT message to the server with a given reason to close the +// connection. Underlying this event being sent, Client.Close() is called as well. +// This is different than just calling Client.Close() in that it provides a reason +// as to why the connection was closed (for bots to tell users the bot is restarting, +// or shutting down, etc). +// +// NOTE: servers may delay showing of QUIT reasons, until you've been connected to +// the server for a certain period of time (e.g. 5 minutes). Keep this in mind. +func (c *Client) Quit(reason string) { + c.Send(&Event{Command: QUIT, Params: []string{reason}}) +} + // ErrEvent is an error returned when the server (or library) sends an ERROR // message response. The string returned contains the trailing text from the // message. @@ -400,9 +440,21 @@ func (c *Client) DisableTracking() { c.registerBuiltins() } -// Server returns the string representation of host+port pair for net.Conn. +// Server returns the string representation of host+port pair for the connection. func (c *Client) Server() string { - return fmt.Sprintf("%s:%d", c.Config.Server, c.Config.Port) + c.state.Lock() + defer c.state.Lock() + + return c.server() +} + +// server returns the string representation of host+port pair for net.Conn, and +// takes into consideration STS. Must lock state mu first! +func (c *Client) server() string { + if c.state.sts.enabled() { + return net.JoinHostPort(c.Config.Server, strconv.Itoa(c.state.sts.upgradePort)) + } + return net.JoinHostPort(c.Config.Server, strconv.Itoa(c.Config.Port)) } // Lifetime returns the amount of time that has passed since the client was @@ -688,8 +740,9 @@ func (c *Client) HasCapability(name string) (has bool) { name = strings.ToLower(name) c.state.RLock() - for i := 0; i < len(c.state.enabledCap); i++ { - if strings.ToLower(c.state.enabledCap[i]) == name { + for key := range c.state.enabledCap { + key = strings.ToLower(key) + if key == name { has = true break } @@ -713,3 +766,25 @@ func (c *Client) panicIfNotTracking() { panic(fmt.Sprintf("%s used when tracking is disabled (caller %s:%d)", fn.Name(), file, line)) } + +func (c *Client) debugLogEvent(e *Event, dropped bool) { + var prefix string + + if dropped { + prefix = "dropping event (disconnected):" + } else { + prefix = ">" + } + + if e.Sensitive { + c.debug.Printf(prefix, " %s ***redacted***", e.Command) + } else { + c.debug.Print(prefix, " ", StripRaw(e.String())) + } + + if c.Config.Out != nil { + if pretty, ok := e.Pretty(); ok { + fmt.Fprintln(c.Config.Out, StripRaw(pretty)) + } + } +} diff --git a/vendor/github.com/lrstanley/girc/conn.go b/vendor/github.com/lrstanley/girc/conn.go index d9ec6319..441c3e71 100644 --- a/vendor/github.com/lrstanley/girc/conn.go +++ b/vendor/github.com/lrstanley/girc/conn.go @@ -58,7 +58,7 @@ type Dialer interface { } // newConn sets up and returns a new connection to the server. -func newConn(conf Config, dialer Dialer, addr string) (*ircConn, error) { +func newConn(conf Config, dialer Dialer, addr string, sts *strictTransport) (*ircConn, error) { if err := conf.isValid(); err != nil { return nil, err } @@ -83,13 +83,29 @@ func newConn(conf Config, dialer Dialer, addr string) (*ircConn, error) { } if conn, err = dialer.Dial("tcp", addr); err != nil { + if sts.enabled() { + err = &ErrSTSUpgradeFailed{Err: err} + } + + if sts.expired() && !conf.DisableSTSFallback { + sts.lastFailed = time.Now() + sts.reset() + } return nil, err } - if conf.SSL { + if conf.SSL || sts.enabled() { var tlsConn net.Conn tlsConn, err = tlsHandshake(conn, conf.TLSConfig, conf.Server, true) if err != nil { + if sts.enabled() { + err = &ErrSTSUpgradeFailed{Err: err} + } + + if sts.expired() && !conf.DisableSTSFallback { + sts.lastFailed = time.Now() + sts.reset() + } return nil, err } @@ -245,6 +261,7 @@ func (c *Client) MockConnect(conn net.Conn) error { } func (c *Client) internalConnect(mock net.Conn, dialer Dialer) error { +startConn: // We want to be the only one handling connects/disconnects right now. c.mu.Lock() @@ -253,13 +270,20 @@ func (c *Client) internalConnect(mock net.Conn, dialer Dialer) error { } // Reset the state. - c.state.reset() + c.state.reset(false) + + addr := c.server() if mock == nil { // Validate info, and actually make the connection. - c.debug.Printf("connecting to %s...", c.Server()) - conn, err := newConn(c.Config, dialer, c.Server()) + c.debug.Printf("connecting to %s... (sts: %v, config-ssl: %v)", addr, c.state.sts.enabled(), c.Config.SSL) + conn, err := newConn(c.Config, dialer, addr, &c.state.sts) if err != nil { + if _, ok := err.(*ErrSTSUpgradeFailed); ok { + if !c.state.sts.enabled() { + c.RunHandlers(&Event{Command: STS_ERR_FALLBACK}) + } + } c.mu.Unlock() return err } @@ -312,16 +336,18 @@ func (c *Client) internalConnect(mock net.Conn, dialer Dialer) error { c.write(&Event{Command: USER, Params: []string{c.Config.User, "*", "*", c.Config.Name}}) // Send a virtual event allowing hooks for successful socket connection. - c.RunHandlers(&Event{Command: INITIALIZED, Params: []string{c.Server()}}) + c.RunHandlers(&Event{Command: INITIALIZED, Params: []string{addr}}) // Wait for the first error. var result error select { case <-ctx.Done(): - c.debug.Print("received request to close, beginning clean up") - c.RunHandlers(&Event{Command: CLOSED, Params: []string{c.Server()}}) + if !c.state.sts.beginUpgrade { + c.debug.Print("received request to close, beginning clean up") + } + c.RunHandlers(&Event{Command: CLOSED, Params: []string{addr}}) case err := <-errs: - c.debug.Print("received error, beginning clean up") + c.debug.Printf("received error, beginning cleanup: %v", err) result = err } @@ -336,7 +362,7 @@ func (c *Client) internalConnect(mock net.Conn, dialer Dialer) error { c.conn.mu.Unlock() c.mu.RUnlock() - c.RunHandlers(&Event{Command: DISCONNECTED, Params: []string{c.Server()}}) + c.RunHandlers(&Event{Command: DISCONNECTED, Params: []string{addr}}) // Once we have our error/result, let all other functions know we're done. c.debug.Print("waiting for all routines to finish") @@ -350,6 +376,18 @@ func (c *Client) internalConnect(mock net.Conn, dialer Dialer) error { // clients, not multiple instances of Connect(). c.mu.Lock() c.conn = nil + + if result == nil { + if c.state.sts.beginUpgrade { + c.state.sts.beginUpgrade = false + c.mu.Unlock() + goto startConn + } + + if c.state.sts.enabled() { + c.state.sts.persistenceReceived = time.Now() + } + } c.mu.Unlock() return result @@ -392,8 +430,23 @@ func (c *Client) readLoop(ctx context.Context, errs chan error, wg *sync.WaitGro // Send sends an event to the server. Use Client.RunHandlers() if you are // simply looking to trigger handlers with an event. func (c *Client) Send(event *Event) { + var delay time.Duration + if !c.Config.AllowFlood { - <-time.After(c.conn.rate(event.Len())) + c.mu.RLock() + + // Drop the event early as we're disconnected, this way we don't have to wait + // the (potentially long) rate limit delay before dropping. + if c.conn == nil { + c.debugLogEvent(event, true) + c.mu.RUnlock() + return + } + + c.conn.mu.Lock() + delay = c.conn.rate(event.Len()) + c.conn.mu.Unlock() + c.mu.RUnlock() } if c.Config.GlobalFormat && len(event.Params) > 0 && event.Params[len(event.Params)-1] != "" && @@ -401,12 +454,21 @@ func (c *Client) Send(event *Event) { event.Params[len(event.Params)-1] = Fmt(event.Params[len(event.Params)-1]) } + <-time.After(delay) c.write(event) } // write is the lower level function to write an event. It does not have a // write-delay when sending events. func (c *Client) write(event *Event) { + c.mu.RLock() + defer c.mu.RUnlock() + + if c.conn == nil { + // Drop the event if disconnected. + c.debugLogEvent(event, true) + return + } c.tx <- event } @@ -415,14 +477,10 @@ func (c *Client) write(event *Event) { func (c *ircConn) rate(chars int) time.Duration { _time := time.Second + ((time.Duration(chars) * time.Second) / 100) - c.mu.Lock() if c.writeDelay += _time - time.Now().Sub(c.lastWrite); c.writeDelay < 0 { c.writeDelay = 0 } - c.mu.Unlock() - c.mu.RLock() - defer c.mu.RUnlock() if c.writeDelay > (8 * time.Second) { return _time } @@ -445,7 +503,7 @@ func (c *Client) sendLoop(ctx context.Context, errs chan error, wg *sync.WaitGro c.state.RLock() var in bool for i := 0; i < len(c.state.enabledCap); i++ { - if c.state.enabledCap[i] == "message-tags" { + if _, ok := c.state.enabledCap["message-tags"]; ok { in = true break } @@ -457,17 +515,7 @@ func (c *Client) sendLoop(ctx context.Context, errs chan error, wg *sync.WaitGro } } - // Log the event. - if event.Sensitive { - c.debug.Printf("> %s ***redacted***", event.Command) - } else { - c.debug.Print("> ", StripRaw(event.String())) - } - if c.Config.Out != nil { - if pretty, ok := event.Pretty(); ok { - fmt.Fprintln(c.Config.Out, StripRaw(pretty)) - } - } + c.debugLogEvent(event, false) c.conn.mu.Lock() c.conn.lastWrite = time.Now() @@ -488,6 +536,12 @@ func (c *Client) sendLoop(ctx context.Context, errs chan error, wg *sync.WaitGro } } + if event.Command == QUIT { + c.Close() + wg.Done() + return + } + if err != nil { errs <- err wg.Done() diff --git a/vendor/github.com/lrstanley/girc/constants.go b/vendor/github.com/lrstanley/girc/constants.go index ddea7d0d..a190ef21 100644 --- a/vendor/github.com/lrstanley/girc/constants.go +++ b/vendor/github.com/lrstanley/girc/constants.go @@ -21,13 +21,15 @@ const ( // Emulated event commands used to allow easier hooks into the changing // state of the client. const ( - UPDATE_STATE = "CLIENT_STATE_UPDATED" // when channel/user state is updated. - UPDATE_GENERAL = "CLIENT_GENERAL_UPDATED" // when general state (client nick, server name, etc) is updated. - ALL_EVENTS = "*" // trigger on all events - CONNECTED = "CLIENT_CONNECTED" // when it's safe to send arbitrary commands (joins, list, who, etc), trailing is host:port - INITIALIZED = "CLIENT_INIT" // verifies successful socket connection, trailing is host:port - DISCONNECTED = "CLIENT_DISCONNECTED" // occurs when we're disconnected from the server (user-requested or not) - CLOSED = "CLIENT_CLOSED" // occurs when Client.Close() has been called + UPDATE_STATE = "CLIENT_STATE_UPDATED" // when channel/user state is updated. + UPDATE_GENERAL = "CLIENT_GENERAL_UPDATED" // when general state (client nick, server name, etc) is updated. + ALL_EVENTS = "*" // trigger on all events + CONNECTED = "CLIENT_CONNECTED" // when it's safe to send arbitrary commands (joins, list, who, etc), trailing is host:port + INITIALIZED = "CLIENT_INIT" // verifies successful socket connection, trailing is host:port + DISCONNECTED = "CLIENT_DISCONNECTED" // occurs when we're disconnected from the server (user-requested or not) + CLOSED = "CLIENT_CLOSED" // occurs when Client.Close() has been called + STS_UPGRADE_INIT = "STS_UPGRADE_INIT" // when an STS upgrade initially happens. + STS_ERR_FALLBACK = "STS_ERR_FALLBACK" // when an STS connection fails and fallbacks are supported. ) // User/channel prefixes :: RFC1459. @@ -225,6 +227,7 @@ const ( ERR_NOTOPLEVEL = "413" ERR_WILDTOPLEVEL = "414" ERR_BADMASK = "415" + ERR_INPUTTOOLONG = "417" ERR_UNKNOWNCOMMAND = "421" ERR_NOMOTD = "422" ERR_NOADMININFO = "423" @@ -286,6 +289,7 @@ const ( CAP_CHGHOST = "CHGHOST" CAP_AWAY = "AWAY" CAP_ACCOUNT = "ACCOUNT" + CAP_TAGMSG = "TAGMSG" ) // Numeric IRC reply mapping for ircv3 :: http://ircv3.net/irc/. diff --git a/vendor/github.com/lrstanley/girc/event.go b/vendor/github.com/lrstanley/girc/event.go index d9d22f26..ef4633f7 100644 --- a/vendor/github.com/lrstanley/girc/event.go +++ b/vendor/github.com/lrstanley/girc/event.go @@ -49,6 +49,7 @@ func ParseEvent(raw string) (e *Event) { } } raw = raw[i+1:] + i = 0 } if raw[0] == messagePrefix { @@ -91,7 +92,7 @@ func ParseEvent(raw string) (e *Event) { if trailerIndex == -1 { // No trailing argument found, assume the rest is just params. - e.Params = strings.Split(raw[j:], string(eventSpace)) + e.Params = strings.Fields(raw[j:]) return e } @@ -114,7 +115,7 @@ func ParseEvent(raw string) (e *Event) { // Check if we need to parse arguments. If so, take everything after the // command, and right before the trailing prefix, and cut it up. if i > j { - e.Params = strings.Split(raw[j:i-1], string(eventSpace)) + e.Params = strings.Fields(raw[j : i-1]) } e.Params = append(e.Params, raw[i+1:]) diff --git a/vendor/github.com/lrstanley/girc/go.mod b/vendor/github.com/lrstanley/girc/go.mod index 57b39ae8..5a4a2aad 100644 --- a/vendor/github.com/lrstanley/girc/go.mod +++ b/vendor/github.com/lrstanley/girc/go.mod @@ -1 +1,3 @@ module github.com/lrstanley/girc + +go 1.12 diff --git a/vendor/github.com/lrstanley/girc/handler.go b/vendor/github.com/lrstanley/girc/handler.go index ec717de6..4832262a 100644 --- a/vendor/github.com/lrstanley/girc/handler.go +++ b/vendor/github.com/lrstanley/girc/handler.go @@ -431,17 +431,27 @@ func recoverHandlerPanic(client *Client, event *Event, id string, skip int) { return } - var file string + var file, function string var line int var ok bool - _, file, line, ok = runtime.Caller(skip) + var pcs [10]uintptr + frames := runtime.CallersFrames(pcs[:runtime.Callers(skip, pcs[:])]) + for { + frame, _ := frames.Next() + file = frame.File + line = frame.Line + function = frame.Function + + break + } err := &HandlerError{ Event: *event, ID: id, File: file, Line: line, + Func: function, Panic: perr, Stack: debug.Stack(), callOk: ok, @@ -460,6 +470,7 @@ type HandlerError struct { ID string // ID is the CUID of the handler. File string // File is the file from where the panic originated. Line int // Line number where panic originated. + Func string // Function name where panic originated. Panic interface{} // Panic is the error that was passed to panic(). Stack []byte // Stack is the call stack. Note you may have to skip 1 or 2 due to debug functions. callOk bool diff --git a/vendor/github.com/lrstanley/girc/state.go b/vendor/github.com/lrstanley/girc/state.go index 0660a686..d9e72981 100644 --- a/vendor/github.com/lrstanley/girc/state.go +++ b/vendor/github.com/lrstanley/girc/state.go @@ -5,6 +5,7 @@ package girc import ( + "fmt" "sort" "sync" "time" @@ -22,27 +23,28 @@ type state struct { // users represents all of users that we're tracking. users map[string]*User // enabledCap are the capabilities which are enabled for this connection. - enabledCap []string + enabledCap map[string]map[string]string // tmpCap are the capabilties which we share with the server during the // last capability check. These will get sent once we have received the // last capability list command from the server. - tmpCap []string + tmpCap map[string]map[string]string // serverOptions are the standard capabilities and configurations // supported by the server at connection time. This also includes // RPL_ISUPPORT entries. serverOptions map[string]string // motd is the servers message of the day. motd string -} -// notify sends state change notifications so users can update their refs -// when state changes. -func (s *state) notify(c *Client, ntype string) { - c.RunHandlers(&Event{Command: ntype}) + // sts are strict transport security configurations, if specified by the + // server. + // + // TODO: ideally, this would be a configurable policy store that the user could + // optionally override (to store STS information on disk, memory, etc). + sts strictTransport } // reset resets the state back to it's original form. -func (s *state) reset() { +func (s *state) reset(initial bool) { s.Lock() s.nick = "" s.ident = "" @@ -50,8 +52,13 @@ func (s *state) reset() { s.channels = make(map[string]*Channel) s.users = make(map[string]*User) s.serverOptions = make(map[string]string) - s.enabledCap = []string{} + s.enabledCap = make(map[string]map[string]string) + s.tmpCap = make(map[string]map[string]string) s.motd = "" + + if initial { + s.sts.reset() + } s.Unlock() } @@ -500,3 +507,44 @@ func (s *state) renameUser(from, to string) { } } } + +type strictTransport struct { + beginUpgrade bool + upgradePort int + persistenceDuration int + persistenceReceived time.Time + preload bool + lastFailed time.Time +} + +func (s *strictTransport) reset() { + s.upgradePort = -1 + s.persistenceDuration = -1 + s.preload = false +} + +func (s *strictTransport) expired() bool { + return int(time.Since(s.persistenceReceived).Seconds()) > s.persistenceDuration +} + +func (s *strictTransport) enabled() bool { + return s.upgradePort > 0 +} + +// ErrSTSUpgradeFailed is an error that occurs when a connection that was attempted +// to be upgraded via a strict transport policy, failed. This does not necessarily +// indicate that STS was to blame, but the underlying connection failed for some +// reason. +type ErrSTSUpgradeFailed struct { + Err error +} + +func (e ErrSTSUpgradeFailed) Error() string { + return fmt.Sprintf("fail to upgrade to secure (sts) connection: %v", e.Err) +} + +// notify sends state change notifications so users can update their refs +// when state changes. +func (s *state) notify(c *Client, ntype string) { + c.RunHandlers(&Event{Command: ntype}) +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 80871d8c..1ec87293 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -91,7 +91,7 @@ github.com/labstack/gommon/color github.com/labstack/gommon/log github.com/labstack/gommon/bytes github.com/labstack/gommon/random -# github.com/lrstanley/girc v0.0.0-20190210212025-51b8e096d398 +# github.com/lrstanley/girc v0.0.0-20190801035559-4fc93959e1a7 github.com/lrstanley/girc # github.com/magiconair/properties v1.8.0 github.com/magiconair/properties