5
0
mirror of https://github.com/cwinfo/matterbridge.git synced 2024-11-22 21:50:28 +00:00

Update lrstanley/girc vendor (#884)

This commit is contained in:
Wim 2019-09-07 21:35:45 +02:00 committed by GitHub
parent 9327810bbf
commit 1532f6e427
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 420 additions and 101 deletions

2
go.mod
View File

@ -23,7 +23,7 @@ require (
github.com/jtolds/gls v4.2.1+incompatible // indirect github.com/jtolds/gls v4.2.1+incompatible // indirect
github.com/keybase/go-keybase-chat-bot v0.0.0-20190816161829-561f10822eb2 github.com/keybase/go-keybase-chat-bot v0.0.0-20190816161829-561f10822eb2
github.com/labstack/echo/v4 v4.1.6 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/go-slackbot v0.0.0-20180109053408-401027ccfef5 // indirect
github.com/lusis/slack-test v0.0.0-20180109053238-3c758769bfa6 // indirect github.com/lusis/slack-test v0.0.0-20180109053238-3c758769bfa6 // indirect
github.com/matterbridge/Rocket.Chat.Go.SDK v0.0.0-20190210153444-cc9d05784d5d github.com/matterbridge/Rocket.Chat.Go.SDK v0.0.0-20190210153444-cc9d05784d5d

4
go.sum
View File

@ -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/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 h1:heVeuAYtevIQVYkGj6A41dtfT91LrvFG220lavpWhrU=
github.com/labstack/gommon v0.2.9/go.mod h1:E8ZTmW9vw5az5/ZyHWCp0Lw4OH2ecsaBP1C/NKavGG4= 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-20190801035559-4fc93959e1a7 h1:BS9tqL0OCiOGuy/CYYk2gc33fxqaqh5/rhqMKu4tcYA=
github.com/lrstanley/girc v0.0.0-20190210212025-51b8e096d398/go.mod h1:7cRs1SIBfKQ7e3Tam6GKTILSNHzR862JD0JpINaZoJk= 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 h1:AsEBgzv3DhuYHI/GiQh2HxvTP71HCCE9E/tzGUzGdtU=
github.com/lusis/go-slackbot v0.0.0-20180109053408-401027ccfef5/go.mod h1:c2mYKRyMb1BPkO5St0c/ps62L4S0W2NAkaTXj9qEI+0= 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= github.com/lusis/slack-test v0.0.0-20180109053238-3c758769bfa6 h1:iOAVXzZyXtW408TMYejlUPo6BIn92HmOacWtIfNyYns=

View File

@ -93,7 +93,11 @@ func handleConnect(c *Client, e Event) {
} }
time.Sleep(2 * time.Second) 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 // nickCollisionHandler helps prevent the client from having conflicting

View File

@ -5,7 +5,10 @@
package girc package girc
import ( import (
"fmt"
"strconv"
"strings" "strings"
"time"
) )
// Something not in the list? Depending on the type of capability, you can // Something not in the list? Depending on the type of capability, you can
@ -19,13 +22,20 @@ var possibleCap = map[string][]string{
"chghost": nil, "chghost": nil,
"extended-join": nil, "extended-join": nil,
"invite-notify": nil, "invite-notify": nil,
"message-tags": nil,
"msgid": nil,
"multi-prefix": nil, "multi-prefix": nil,
"server-time": nil, "server-time": nil,
"userhost-in-names": 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/message-tags-0.2": nil,
"draft/msgid": 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 // "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. // 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, // 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 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 { for k := range c.Config.SupportedCaps {
out[k] = c.Config.SupportedCaps[k] out[k] = c.Config.SupportedCaps[k]
} }
@ -62,8 +83,8 @@ func possibleCapList(c *Client) map[string][]string {
return out return out
} }
func parseCap(raw string) map[string][]string { func parseCap(raw string) map[string]map[string]string {
out := make(map[string][]string) out := make(map[string]map[string]string)
parts := strings.Split(raw, " ") parts := strings.Split(raw, " ")
var val int var val int
@ -78,7 +99,16 @@ func parseCap(raw string) map[string][]string {
continue 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 return out
@ -88,8 +118,15 @@ func parseCap(raw string) map[string][]string {
// This will lock further registration until we have acknowledged (or denied) // This will lock further registration until we have acknowledged (or denied)
// the capabilities. // the capabilities.
func handleCAP(c *Client, e Event) { func handleCAP(c *Client, e Event) {
if len(e.Params) >= 2 && (e.Params[1] == CAP_NEW || e.Params[1] == CAP_DEL) { c.state.Lock()
c.listCAP() 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 return
} }
@ -101,27 +138,26 @@ func handleCAP(c *Client, e Event) {
} }
possible := possibleCapList(c) possible := possibleCapList(c)
// TODO: test the addition.
if len(e.Params) >= 3 && e.Params[1] == CAP_LS { if len(e.Params) >= 3 && (e.Params[1] == CAP_LS || e.Params[1] == CAP_NEW) {
c.state.Lock()
caps := parseCap(e.Last()) caps := parseCap(e.Last())
for k := range caps { for capName := range caps {
if _, ok := possible[k]; !ok { if _, ok := possible[capName]; !ok {
continue continue
} }
if len(possible[k]) == 0 || len(caps[k]) == 0 { if len(possible[capName]) == 0 || len(caps[capName]) == 0 {
c.state.tmpCap = append(c.state.tmpCap, k) c.state.tmpCap[capName] = caps[capName]
continue continue
} }
var contains bool var contains bool
for i := 0; i < len(caps[k]); i++ {
for j := 0; j < len(possible[k]); j++ { for capAttr := range caps[capName] {
if caps[k][i] == possible[k][j] { for i := 0; i < len(possible[capName]); i++ {
// Assume we have a matching split value. if _, ok := caps[capName][capAttr]; ok {
// Assuming we have a matching attribute for the capability.
contains = true contains = true
goto checkcontains goto checkcontains
} }
@ -133,9 +169,8 @@ func handleCAP(c *Client, e Event) {
continue 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 // Indicates if this is a multi-line LS. (3 args means it's the
// last LS). // last LS).
@ -147,31 +182,113 @@ func handleCAP(c *Client, e Event) {
} }
// Let them know which ones we'd like to enable. // Let them know which ones we'd like to enable.
c.write(&Event{Command: CAP, Params: []string{CAP_REQ, strings.Join(c.state.tmpCap, " ")}}) reqKeys := make([]string, len(c.state.tmpCap))
i := 0
// Re-initialize the tmpCap, so if we get multiple 'CAP LS' requests for k := range c.state.tmpCap {
// due to cap-notify, we can re-evaluate what we can support. reqKeys[i] = k
c.state.Lock() i++
c.state.tmpCap = []string{} }
c.state.Unlock() c.write(&Event{Command: CAP, Params: []string{CAP_REQ, strings.Join(reqKeys, " ")}})
} }
} }
if len(e.Params) == 3 && e.Params[1] == CAP_ACK { if len(e.Params) == 3 && e.Params[1] == CAP_ACK {
c.state.Lock() enabled := strings.Split(e.Last(), " ")
c.state.enabledCap = strings.Split(e.Last(), " ") for _, cap := range enabled {
if val, ok := c.state.tmpCap[cap]; ok {
// Do we need to do sasl auth? c.state.enabledCap[cap] = val
wantsSASL := false } else {
for i := 0; i < len(c.state.enabledCap); i++ { c.state.enabledCap[cap] = nil
if c.state.enabledCap[i] == "sasl" {
wantsSASL = true
break
} }
} }
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 keys 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()}}) c.write(&Event{Command: AUTHENTICATE, Params: []string{c.Config.SASL.Method()}})
// Don't "CAP END", since we want to authenticate. // Don't "CAP END", since we want to authenticate.
return return

View File

@ -38,7 +38,7 @@ const (
prefixTagValue byte = '=' prefixTagValue byte = '='
prefixUserTag byte = '+' prefixUserTag byte = '+'
tagSeparator 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 // 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 // @aaa=bbb;ccc;example.com/ddd=eee
// NOT: // NOT:
// @aaa=bbb;ccc;example.com/ddd=eee :nick!ident@host.com PRIVMSG me :Hello // @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) { func ParseTags(raw string) (t Tags) {
t = make(Tags) t = make(Tags)
@ -79,11 +82,11 @@ func ParseTags(raw string) (t Tags) {
} }
// Check if tag key or decoded value are invalid. // Check if tag key or decoded value are invalid.
if !validTag(parts[i][:hasValue]) || !validTagValue(tagDecoder.Replace(parts[i][hasValue+1:])) { // if !validTag(parts[i][:hasValue]) || !validTagValue(tagDecoder.Replace(parts[i][hasValue+1:])) {
continue // continue
} // }
t[parts[i][:hasValue]] = parts[i][hasValue+1:] t[parts[i][:hasValue]] = tagDecoder.Replace(parts[i][hasValue+1:])
} }
return t return t

View File

@ -12,8 +12,11 @@ import (
"io" "io"
"io/ioutil" "io/ioutil"
"log" "log"
"net"
"os"
"runtime" "runtime"
"sort" "sort"
"strconv"
"strings" "strings"
"sync" "sync"
"time" "time"
@ -95,6 +98,16 @@ type Config struct {
// configuration (e.g. to not force hostname checking). This only has an // configuration (e.g. to not force hostname checking). This only has an
// affect during the dial process. // affect during the dial process.
SSL bool 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 // TLSConfig is an optional user-supplied tls configuration, used during
// socket creation to the server. SSL must be enabled for this to be used. // socket creation to the server. SSL must be enabled for this to be used.
// This only has an affect during the dial process. // 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 // disableTracking disables all channel and user-level tracking. Useful
// for highly embedded scripts with single purposes. This has an exported // 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(). // Client.DisableTracking().
disableTracking bool disableTracking bool
// HandleNickCollide when set, allows the client to handle nick collisions // 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 c.Config.PingDelay = 600 * time.Second
} }
envDebug, _ := strconv.ParseBool(os.Getenv("GIRC_DEBUG"))
if c.Config.Debug == nil { 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 { } 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 = log.New(c.Config.Debug, "debug:", log.Ltime|log.Lshortfile)
c.debug.Print("initializing debugging") c.debug.Print("initializing debugging")
} }
envDisableSTS, _ := strconv.ParseBool((os.Getenv("GIRC_DISABLE_STS")))
if envDisableSTS {
c.Config.DisableSTS = envDisableSTS
}
// Setup the caller. // Setup the caller.
c.Handlers = newCaller(c.debug) c.Handlers = newCaller(c.debug)
// Give ourselves a new state. // Give ourselves a new state.
c.state = &state{} c.state = &state{}
c.state.reset() c.state.reset(true)
// Register builtin handlers. // Register builtin handlers.
c.registerBuiltins() c.registerBuiltins()
@ -323,6 +351,18 @@ func (c *Client) Close() {
c.mu.RUnlock() 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 // 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 response. The string returned contains the trailing text from the
// message. // message.
@ -400,9 +440,21 @@ func (c *Client) DisableTracking() {
c.registerBuiltins() 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 { 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 // 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) name = strings.ToLower(name)
c.state.RLock() c.state.RLock()
for i := 0; i < len(c.state.enabledCap); i++ { for key := range c.state.enabledCap {
if strings.ToLower(c.state.enabledCap[i]) == name { key = strings.ToLower(key)
if key == name {
has = true has = true
break 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)) 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))
}
}
}

View File

@ -58,7 +58,7 @@ type Dialer interface {
} }
// newConn sets up and returns a new connection to the server. // 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 { if err := conf.isValid(); err != nil {
return nil, err 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 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 return nil, err
} }
if conf.SSL { if conf.SSL || sts.enabled() {
var tlsConn net.Conn var tlsConn net.Conn
tlsConn, err = tlsHandshake(conn, conf.TLSConfig, conf.Server, true) tlsConn, err = tlsHandshake(conn, conf.TLSConfig, conf.Server, true)
if err != nil { if err != nil {
if sts.enabled() {
err = &ErrSTSUpgradeFailed{Err: err}
}
if sts.expired() && !conf.DisableSTSFallback {
sts.lastFailed = time.Now()
sts.reset()
}
return nil, err 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 { func (c *Client) internalConnect(mock net.Conn, dialer Dialer) error {
startConn:
// We want to be the only one handling connects/disconnects right now. // We want to be the only one handling connects/disconnects right now.
c.mu.Lock() c.mu.Lock()
@ -253,13 +270,20 @@ func (c *Client) internalConnect(mock net.Conn, dialer Dialer) error {
} }
// Reset the state. // Reset the state.
c.state.reset() c.state.reset(false)
addr := c.server()
if mock == nil { if mock == nil {
// Validate info, and actually make the connection. // Validate info, and actually make the connection.
c.debug.Printf("connecting to %s...", 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, c.Server()) conn, err := newConn(c.Config, dialer, addr, &c.state.sts)
if err != nil { if err != nil {
if _, ok := err.(*ErrSTSUpgradeFailed); ok {
if !c.state.sts.enabled() {
c.RunHandlers(&Event{Command: STS_ERR_FALLBACK})
}
}
c.mu.Unlock() c.mu.Unlock()
return err 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}}) c.write(&Event{Command: USER, Params: []string{c.Config.User, "*", "*", c.Config.Name}})
// Send a virtual event allowing hooks for successful socket connection. // 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. // Wait for the first error.
var result error var result error
select { select {
case <-ctx.Done(): case <-ctx.Done():
c.debug.Print("received request to close, beginning clean up") if !c.state.sts.beginUpgrade {
c.RunHandlers(&Event{Command: CLOSED, Params: []string{c.Server()}}) c.debug.Print("received request to close, beginning clean up")
}
c.RunHandlers(&Event{Command: CLOSED, Params: []string{addr}})
case err := <-errs: case err := <-errs:
c.debug.Print("received error, beginning clean up") c.debug.Printf("received error, beginning cleanup: %v", err)
result = err result = err
} }
@ -336,7 +362,7 @@ func (c *Client) internalConnect(mock net.Conn, dialer Dialer) error {
c.conn.mu.Unlock() c.conn.mu.Unlock()
c.mu.RUnlock() 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. // Once we have our error/result, let all other functions know we're done.
c.debug.Print("waiting for all routines to finish") 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(). // clients, not multiple instances of Connect().
c.mu.Lock() c.mu.Lock()
c.conn = nil 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() c.mu.Unlock()
return result 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 // Send sends an event to the server. Use Client.RunHandlers() if you are
// simply looking to trigger handlers with an event. // simply looking to trigger handlers with an event.
func (c *Client) Send(event *Event) { func (c *Client) Send(event *Event) {
var delay time.Duration
if !c.Config.AllowFlood { 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] != "" && 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]) event.Params[len(event.Params)-1] = Fmt(event.Params[len(event.Params)-1])
} }
<-time.After(delay)
c.write(event) c.write(event)
} }
// write is the lower level function to write an event. It does not have a // write is the lower level function to write an event. It does not have a
// write-delay when sending events. // write-delay when sending events.
func (c *Client) write(event *Event) { 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 c.tx <- event
} }
@ -415,14 +477,10 @@ func (c *Client) write(event *Event) {
func (c *ircConn) rate(chars int) time.Duration { func (c *ircConn) rate(chars int) time.Duration {
_time := time.Second + ((time.Duration(chars) * time.Second) / 100) _time := time.Second + ((time.Duration(chars) * time.Second) / 100)
c.mu.Lock()
if c.writeDelay += _time - time.Now().Sub(c.lastWrite); c.writeDelay < 0 { if c.writeDelay += _time - time.Now().Sub(c.lastWrite); c.writeDelay < 0 {
c.writeDelay = 0 c.writeDelay = 0
} }
c.mu.Unlock()
c.mu.RLock()
defer c.mu.RUnlock()
if c.writeDelay > (8 * time.Second) { if c.writeDelay > (8 * time.Second) {
return _time return _time
} }
@ -445,7 +503,7 @@ func (c *Client) sendLoop(ctx context.Context, errs chan error, wg *sync.WaitGro
c.state.RLock() c.state.RLock()
var in bool var in bool
for i := 0; i < len(c.state.enabledCap); i++ { 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 in = true
break break
} }
@ -457,17 +515,7 @@ func (c *Client) sendLoop(ctx context.Context, errs chan error, wg *sync.WaitGro
} }
} }
// Log the event. c.debugLogEvent(event, false)
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.conn.mu.Lock() c.conn.mu.Lock()
c.conn.lastWrite = time.Now() 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 { if err != nil {
errs <- err errs <- err
wg.Done() wg.Done()

View File

@ -21,13 +21,15 @@ const (
// Emulated event commands used to allow easier hooks into the changing // Emulated event commands used to allow easier hooks into the changing
// state of the client. // state of the client.
const ( const (
UPDATE_STATE = "CLIENT_STATE_UPDATED" // when channel/user state is updated. 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. UPDATE_GENERAL = "CLIENT_GENERAL_UPDATED" // when general state (client nick, server name, etc) is updated.
ALL_EVENTS = "*" // trigger on all events 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 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 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) 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 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. // User/channel prefixes :: RFC1459.
@ -225,6 +227,7 @@ const (
ERR_NOTOPLEVEL = "413" ERR_NOTOPLEVEL = "413"
ERR_WILDTOPLEVEL = "414" ERR_WILDTOPLEVEL = "414"
ERR_BADMASK = "415" ERR_BADMASK = "415"
ERR_INPUTTOOLONG = "417"
ERR_UNKNOWNCOMMAND = "421" ERR_UNKNOWNCOMMAND = "421"
ERR_NOMOTD = "422" ERR_NOMOTD = "422"
ERR_NOADMININFO = "423" ERR_NOADMININFO = "423"
@ -286,6 +289,7 @@ const (
CAP_CHGHOST = "CHGHOST" CAP_CHGHOST = "CHGHOST"
CAP_AWAY = "AWAY" CAP_AWAY = "AWAY"
CAP_ACCOUNT = "ACCOUNT" CAP_ACCOUNT = "ACCOUNT"
CAP_TAGMSG = "TAGMSG"
) )
// Numeric IRC reply mapping for ircv3 :: http://ircv3.net/irc/. // Numeric IRC reply mapping for ircv3 :: http://ircv3.net/irc/.

View File

@ -49,6 +49,7 @@ func ParseEvent(raw string) (e *Event) {
} }
} }
raw = raw[i+1:] raw = raw[i+1:]
i = 0
} }
if raw[0] == messagePrefix { if raw[0] == messagePrefix {
@ -91,7 +92,7 @@ func ParseEvent(raw string) (e *Event) {
if trailerIndex == -1 { if trailerIndex == -1 {
// No trailing argument found, assume the rest is just params. // 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 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 // Check if we need to parse arguments. If so, take everything after the
// command, and right before the trailing prefix, and cut it up. // command, and right before the trailing prefix, and cut it up.
if i > j { 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:]) e.Params = append(e.Params, raw[i+1:])

View File

@ -1 +1,3 @@
module github.com/lrstanley/girc module github.com/lrstanley/girc
go 1.12

View File

@ -431,17 +431,27 @@ func recoverHandlerPanic(client *Client, event *Event, id string, skip int) {
return return
} }
var file string var file, function string
var line int var line int
var ok bool 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{ err := &HandlerError{
Event: *event, Event: *event,
ID: id, ID: id,
File: file, File: file,
Line: line, Line: line,
Func: function,
Panic: perr, Panic: perr,
Stack: debug.Stack(), Stack: debug.Stack(),
callOk: ok, callOk: ok,
@ -460,6 +470,7 @@ type HandlerError struct {
ID string // ID is the CUID of the handler. ID string // ID is the CUID of the handler.
File string // File is the file from where the panic originated. File string // File is the file from where the panic originated.
Line int // Line number where 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(). 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. Stack []byte // Stack is the call stack. Note you may have to skip 1 or 2 due to debug functions.
callOk bool callOk bool

View File

@ -5,6 +5,7 @@
package girc package girc
import ( import (
"fmt"
"sort" "sort"
"sync" "sync"
"time" "time"
@ -22,27 +23,28 @@ type state struct {
// users represents all of users that we're tracking. // users represents all of users that we're tracking.
users map[string]*User users map[string]*User
// enabledCap are the capabilities which are enabled for this connection. // 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 // 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 check. These will get sent once we have received the
// last capability list command from the server. // last capability list command from the server.
tmpCap []string tmpCap map[string]map[string]string
// serverOptions are the standard capabilities and configurations // serverOptions are the standard capabilities and configurations
// supported by the server at connection time. This also includes // supported by the server at connection time. This also includes
// RPL_ISUPPORT entries. // RPL_ISUPPORT entries.
serverOptions map[string]string serverOptions map[string]string
// motd is the servers message of the day. // motd is the servers message of the day.
motd string motd string
}
// notify sends state change notifications so users can update their refs // sts are strict transport security configurations, if specified by the
// when state changes. // server.
func (s *state) notify(c *Client, ntype string) { //
c.RunHandlers(&Event{Command: ntype}) // 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. // reset resets the state back to it's original form.
func (s *state) reset() { func (s *state) reset(initial bool) {
s.Lock() s.Lock()
s.nick = "" s.nick = ""
s.ident = "" s.ident = ""
@ -50,8 +52,13 @@ func (s *state) reset() {
s.channels = make(map[string]*Channel) s.channels = make(map[string]*Channel)
s.users = make(map[string]*User) s.users = make(map[string]*User)
s.serverOptions = make(map[string]string) 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 = "" s.motd = ""
if initial {
s.sts.reset()
}
s.Unlock() 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})
}

2
vendor/modules.txt vendored
View File

@ -91,7 +91,7 @@ github.com/labstack/gommon/color
github.com/labstack/gommon/log github.com/labstack/gommon/log
github.com/labstack/gommon/bytes github.com/labstack/gommon/bytes
github.com/labstack/gommon/random 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/lrstanley/girc
# github.com/magiconair/properties v1.8.0 # github.com/magiconair/properties v1.8.0
github.com/magiconair/properties github.com/magiconair/properties