Update lrstanley/girc vendor (#884)
This commit is contained in:
parent
9327810bbf
commit
1532f6e427
2
go.mod
2
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
|
||||
|
|
4
go.sum
4
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=
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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/.
|
||||
|
|
|
@ -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:])
|
||||
|
|
|
@ -1 +1,3 @@
|
|||
module github.com/lrstanley/girc
|
||||
|
||||
go 1.12
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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})
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue