diff options
Diffstat (limited to 'vendor/github.com/lrstanley/girc/cap.go')
-rw-r--r-- | vendor/github.com/lrstanley/girc/cap.go | 191 |
1 files changed, 154 insertions, 37 deletions
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 + } + } + + // 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 } } - c.state.Unlock() - if wantsSASL { + // 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 |