Update dependencies and remove old matterclient lib (#2067)

This commit is contained in:
Wim
2023-08-05 20:43:19 +02:00
committed by GitHub
parent 9459495484
commit 56e7bd01ca
772 changed files with 139315 additions and 121315 deletions

View File

@@ -1,6 +1,9 @@
# THIS FILE IS GENERATED! DO NOT EDIT! Maintained by Terraform.
#
# editorconfig.org
# editorconfig: https://editorconfig.org/
# actual source: https://github.com/lrstanley/.github/blob/master/terraform/github-common-files/templates/.editorconfig
#
root = true
[*]

View File

@@ -1,11 +1,33 @@
# THIS FILE IS GENERATED! DO NOT EDIT! Maintained by Terraform.
#
# golangci-lint: https://golangci-lint.run/
# false-positives: https://golangci-lint.run/usage/false-positives/
# actual source: https://github.com/lrstanley/.github/blob/master/terraform/github-common-files/templates/.golangci.yml
# modified variant of: https://gist.github.com/maratori/47a4d00457a92aa426dbd48a18776322
#
run:
tests: False
timeout: 3m
issues:
max-per-linter: 0
max-same-issues: 0
# max-same-issues: 0
max-same-issues: 50
exclude-rules:
- source: "(noinspection|TODO)"
linters: [godot]
- source: "//noinspection"
linters: [gocritic]
- path: "_test\\.go"
linters:
- bodyclose
- dupl
- funlen
- goconst
- gosec
- noctx
- wrapcheck
severity:
default-severity: error
@@ -16,17 +38,102 @@ severity:
severity: warning
linters:
disable-all: true
enable:
- asciicheck
- exportloopref
- gci
- gocritic
- gofmt
- misspell
- asasalint # checks for pass []any as any in variadic func(...any)
- asciicheck # checks that your code does not contain non-ASCII identifiers
- bidichk # checks for dangerous unicode character sequences
- bodyclose # checks whether HTTP response body is closed successfully
- cyclop # checks function and package cyclomatic complexity
- dupl # tool for code clone detection
- durationcheck # checks for two durations multiplied together
- errcheck # checking for unchecked errors, these unchecked errors can be critical bugs in some cases
- errorlint # finds code that will cause problems with the error wrapping scheme introduced in Go 1.13
- execinquery # checks query string in Query function which reads your Go src files and warning it finds
- exportloopref # checks for pointers to enclosing loop variables
- forbidigo # forbids identifiers
- funlen # tool for detection of long functions
- gci # controls golang package import order and makes it always deterministic
- gocheckcompilerdirectives # validates go compiler directive comments (//go:)
- gochecknoinits # checks that no init functions are present in Go code
- goconst # finds repeated strings that could be replaced by a constant
- gocritic # provides diagnostics that check for bugs, performance and style issues
- gocyclo # computes and checks the cyclomatic complexity of functions
- godot # checks if comments end in a period
- godox # detects FIXME, TODO and other comment keywords
- goimports # in addition to fixing imports, goimports also formats your code in the same style as gofmt
- gomnd # detects magic numbers
- gomoddirectives # manages the use of 'replace', 'retract', and 'excludes' directives in go.mod
- gomodguard # allow and block lists linter for direct Go module dependencies. This is different from depguard where there are different block types for example version constraints and module recommendations
- goprintffuncname # checks that printf-like functions are named with f at the end
- gosec # inspects source code for security problems
- gosimple # specializes in simplifying a code
- govet # reports suspicious constructs, such as Printf calls whose arguments do not align with the format string
- ineffassign # detects when assignments to existing variables are not used
- loggercheck # checks key value pairs for common logger libraries (kitlog,klog,logr,zap)
- makezero # finds slice declarations with non-zero initial length
- misspell # finds commonly misspelled words
- musttag # enforces field tags in (un)marshaled structs
- nakedret # finds naked returns in functions greater than a specified function length
- nilerr # finds the code that returns nil even if it checks that the error is not nil
- nilnil # checks that there is no simultaneous return of nil error and an invalid value
- noctx # finds sending http request without context.Context
- nosprintfhostport # checks for misuse of Sprintf to construct a host with port in a URL
- predeclared # finds code that shadows one of Go's predeclared identifiers
- promlinter # checks Prometheus metrics naming via promlint
- reassign # checks that package variables are not reassigned
- revive # fast, configurable, extensible, flexible, and beautiful linter for Go, drop-in replacement of golint
- rowserrcheck # checks whether Err of rows is checked successfully
- sqlclosecheck # checks that sql.Rows and sql.Stmt are closed
- staticcheck # is a go vet on steroids, applying a ton of static analysis checks
- stylecheck # is a replacement for golint
- tenv # detects using os.Setenv instead of t.Setenv since Go1.17
- testableexamples # checks if examples are testable (have an expected output)
- tparallel # detects inappropriate usage of t.Parallel() method in your Go test codes
- typecheck # like the front-end of a Go compiler, parses and type-checks Go code
- unconvert # removes unnecessary type conversions
- unparam # reports unused function parameters
- unused # checks for unused constants, variables, functions and types
- usestdlibvars # detects the possibility to use variables/constants from the Go standard library
- wastedassign # finds wasted assignment statements
- whitespace # detects leading and trailing whitespace
# disabled for now:
# - errname # checks that sentinel errors are prefixed with the Err and error types are suffixed with the Error
# - gochecknoglobals # checks that no global variables exist
# - gocognit # computes and checks the cognitive complexity of functions
# - nestif # reports deeply nested if statements
# - nonamedreturns # reports all named returns
# - testpackage # makes you use a separate _test package
linters-settings:
cyclop:
# The maximal code complexity to report.
max-complexity: 30
# The maximal average package complexity.
# If it's higher than 0.0 (float) the check is enabled
package-average: 10.0
errcheck:
# Report about not checking of errors in type assertions: `a := b.(MyStruct)`.
# Such cases aren't reported by default.
check-type-assertions: true
funlen:
# Checks the number of lines in a function.
# If lower than 0, disable the check.
lines: 150
# Checks the number of statements in a function.
# If lower than 0, disable the check.
statements: 75
# gocognit:
# # Minimal code complexity to report.
# min-complexity: 25
gocritic:
disabled-checks:
- whyNoLint
- hugeParam
- ifElseChain
enabled-tags:
@@ -34,5 +141,71 @@ linters-settings:
- opinionated
- performance
- style
# https://go-critic.github.io/overview.
settings:
captLocal:
# Whether to restrict checker to params only.
paramsOnly: false
underef:
# Whether to skip (*x).method() calls where x is a pointer receiver.
skipRecvDeref: false
gomnd:
# Values always ignored: `time.Date`,
# `strconv.FormatInt`, `strconv.FormatUint`, `strconv.FormatFloat`,
# `strconv.ParseInt`, `strconv.ParseUint`, `strconv.ParseFloat`.
ignored-functions:
- os.Chmod
- os.Mkdir
- os.MkdirAll
- os.OpenFile
- os.WriteFile
- prometheus.ExponentialBuckets
- prometheus.ExponentialBucketsRange
- prometheus.LinearBuckets
gomodguard:
blocked:
# List of blocked modules.
modules:
- github.com/golang/protobuf:
recommendations:
- google.golang.org/protobuf
reason: "see https://developers.google.com/protocol-buffers/docs/reference/go/faq#modules"
- github.com/satori/go.uuid:
recommendations:
- github.com/google/uuid
reason: "satori's package is not maintained"
- github.com/gofrs/uuid:
recommendations:
- github.com/google/uuid
reason: "gofrs' package is not go module"
govet:
check-shadowing: true
enable-all: true
# Run `go tool vet help` to see all analyzers.
disable:
- fieldalignment # too strict
settings:
shadow:
# Whether to be strict about shadowing; can be noisy.
strict: true
nakedret:
# Make an issue if func has more lines of code than this setting, and it has naked returns.
max-func-lines: 0
rowserrcheck:
# database/sql is always checked
packages:
- github.com/jmoiron/sqlx
stylecheck:
checks:
- all
- -ST1008 # handled by revive already.
tenv:
# The option `all` will run against whole test files (`_test.go`) regardless of method/function signatures.
# Otherwise, only methods that take `*testing.T`, `*testing.B`, and `testing.TB` as arguments are checked.
all: true

View File

@@ -408,6 +408,48 @@ func handleISUPPORT(c *Client, e Event) {
c.state.serverOptions[name] = val
}
c.state.Unlock()
// Check for max line/nick/user/host lengths here.
c.state.RLock()
maxLineLength := c.state.maxLineLength
c.state.RUnlock()
maxNickLength := defaultNickLength
maxUserLength := defaultUserLength
maxHostLength := defaultHostLength
var ok bool
var tmp int
if tmp, ok = c.GetServerOptionInt("LINELEN"); ok {
maxLineLength = tmp
c.state.Lock()
c.state.maxLineLength = maxTagLength - 2 // -2 for CR-LF.
c.state.Unlock()
}
if tmp, ok = c.GetServerOptionInt("NICKLEN"); ok {
maxNickLength = tmp
}
if tmp, ok = c.GetServerOptionInt("MAXNICKLEN"); ok && tmp > maxNickLength {
maxNickLength = tmp
}
if tmp, ok = c.GetServerOptionInt("USERLEN"); ok && tmp > maxUserLength {
maxUserLength = tmp
}
if tmp, ok = c.GetServerOptionInt("HOSTLEN"); ok && tmp > maxHostLength {
maxHostLength = tmp
}
prefixLen := defaultPrefixPadding + maxNickLength + maxUserLength + maxHostLength
if prefixLen >= maxLineLength {
// Give up and go with defaults.
c.state.notify(c, UPDATE_GENERAL)
return
}
c.state.Lock()
c.state.maxPrefixLength = prefixLen
c.state.Unlock()
c.state.notify(c, UPDATE_GENERAL)
}

View File

@@ -267,9 +267,9 @@ func handleCAP(c *Client, e Event) {
}
if isError {
c.rx <- &Event{Command: ERROR, Params: []string{
c.receive(&Event{Command: ERROR, Params: []string{
fmt.Sprintf("closing connection: strict transport policy provided by server is invalid; possible MITM? config: %#v", sts),
}}
}})
return
}

View File

@@ -95,9 +95,9 @@ func handleSASL(c *Client, e Event) {
// some reason. The SASL spec and IRCv3 spec do not define a clear
// way to abort a SASL exchange, other than to disconnect, or proceed
// with CAP END.
c.rx <- &Event{Command: ERROR, Params: []string{
c.receive(&Event{Command: ERROR, Params: []string{
fmt.Sprintf("closing connection: SASL %s failed: %s", c.Config.SASL.Method(), e.Last()),
}}
}})
return
}
@@ -131,5 +131,5 @@ func handleSASLError(c *Client, e Event) {
// Authentication failed. The SASL spec and IRCv3 spec do not define a
// clear way to abort a SASL exchange, other than to disconnect, or
// proceed with CAP END.
c.rx <- &Event{Command: ERROR, Params: []string{"closing connection: " + e.Last()}}
c.receive(&Event{Command: ERROR, Params: []string{"closing connection: " + e.Last()}})
}

View File

@@ -52,9 +52,12 @@ type Tags map[string]string
// ParseTags parses out the key-value map of tags. raw should only be the tag
// data, not a full message. For example:
// @aaa=bbb;ccc;example.com/ddd=eee
//
// @aaa=bbb;ccc;example.com/ddd=eee
//
// 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.

View File

@@ -155,6 +155,10 @@ type Config struct {
// and the client. If this is set to -1, the client will not attempt to
// send client -> server PING requests.
PingDelay time.Duration
// PingTimeout specifies the duration at which girc will assume
// that the connection to the server has been lost if no PONG
// message has been received in reply to an outstanding PING.
PingTimeout time.Duration
// disableTracking disables all channel and user-level tracking. Useful
// for highly embedded scripts with single purposes. This has an exported
@@ -179,13 +183,13 @@ type Config struct {
// server.
//
// Client expectations:
// - Perform any proxy resolution.
// - Check the reverse DNS and forward DNS match.
// - Check the IP against suitable access controls (ipaccess, dnsbl, etc).
// - Perform any proxy resolution.
// - Check the reverse DNS and forward DNS match.
// - Check the IP against suitable access controls (ipaccess, dnsbl, etc).
//
// More information:
// - https://ircv3.net/specs/extensions/webirc.html
// - https://kiwiirc.com/docs/webirc
// - https://ircv3.net/specs/extensions/webirc.html
// - https://kiwiirc.com/docs/webirc
type WebIRC struct {
// Password that authenticates the WEBIRC command from this client.
Password string
@@ -262,6 +266,10 @@ func New(config Config) *Client {
c.Config.PingDelay = 600 * time.Second
}
if c.Config.PingTimeout == 0 {
c.Config.PingTimeout = 60 * time.Second
}
envDebug, _ := strconv.ParseBool(os.Getenv("GIRC_DEBUG"))
if c.Config.Debug == nil {
if envDebug {
@@ -300,6 +308,23 @@ func New(config Config) *Client {
return c
}
// receive is a wrapper for sending to the Client.rx channel. It will timeout if
// the event can't be sent within 30s.
func (c *Client) receive(e *Event) {
t := time.NewTimer(30 * time.Second)
defer func() {
if !t.Stop() {
<-t.C
}
}()
select {
case c.rx <- e:
case <-t.C:
c.debugLogEvent(e, true)
}
}
// String returns a brief description of the current client state.
func (c *Client) String() string {
connected := c.IsConnected()
@@ -380,7 +405,7 @@ func (e *ErrEvent) Error() string {
return e.Event.Last()
}
func (c *Client) execLoop(ctx context.Context, errs chan error, wg *sync.WaitGroup) {
func (c *Client) execLoop(ctx context.Context) error {
c.debug.Print("starting execLoop")
defer c.debug.Print("closing execLoop")
@@ -403,9 +428,10 @@ func (c *Client) execLoop(ctx context.Context, errs chan error, wg *sync.WaitGro
}
done:
wg.Done()
return
return nil
case event = <-c.rx:
c.RunHandlers(event)
if event != nil && event.Command == ERROR {
// Handles incoming ERROR responses. These are only ever sent
// by the server (with the exception that this library may use
@@ -415,13 +441,9 @@ func (c *Client) execLoop(ctx context.Context, errs chan error, wg *sync.WaitGro
// some reason the server doesn't disconnect the client, or
// if this library is the source of the error, this should
// signal back up to the main connect loop, to disconnect.
errs <- &ErrEvent{Event: event}
// Make sure to not actually exit, so we can let any handlers
// actually handle the ERROR event.
return &ErrEvent{Event: event}
}
c.RunHandlers(event)
}
}
}
@@ -669,8 +691,7 @@ func (c *Client) IsInChannel(channel string) (in bool) {
// during client connection. This is also known as ISUPPORT (or RPL_PROTOCTL).
// Will panic if used when tracking has been disabled. Examples of usage:
//
// nickLen, success := GetServerOption("MAXNICKLEN")
//
// nickLen, success := GetServerOption("MAXNICKLEN")
func (c *Client) GetServerOption(key string) (result string, ok bool) {
c.panicIfNotTracking()
@@ -680,6 +701,42 @@ func (c *Client) GetServerOption(key string) (result string, ok bool) {
return result, ok
}
// GetServerOptionInt retrieves a server capability setting (as an integer) that was
// retrieved during client connection. This is also known as ISUPPORT (or RPL_PROTOCTL).
// Will panic if used when tracking has been disabled. Examples of usage:
//
// nickLen, success := GetServerOption("MAXNICKLEN")
func (c *Client) GetServerOptionInt(key string) (result int, ok bool) {
var data string
var err error
data, ok = c.GetServerOption(key)
if !ok {
return result, ok
}
result, err = strconv.Atoi(data)
if err != nil {
ok = false
}
return result, ok
}
// MaxEventLength returns the maximum supported server length of an event. This is the
// maximum length of the command and arguments, excluding the source/prefix supported
// by the protocol. If state tracking is enabled, this will utilize ISUPPORT/IRCv3
// information to more accurately calculate the maximum supported length (i.e. extended
// length events).
func (c *Client) MaxEventLength() (max int) {
if !c.Config.disableTracking {
c.state.RLock()
max = c.state.maxLineLength - c.state.maxPrefixLength
c.state.RUnlock()
return max
}
return DefaultMaxLineLength - DefaultMaxPrefixLength
}
// NetworkName returns the network identifier. E.g. "EsperNet", "ByteIRC".
// May be empty if the server does not support RPL_ISUPPORT (or RPL_PROTOCTL).
// Will panic if used when tracking has been disabled.
@@ -773,7 +830,7 @@ func (c *Client) debugLogEvent(e *Event, dropped bool) {
var prefix string
if dropped {
prefix = "dropping event (disconnected):"
prefix = "dropping event (disconnected or timeout):"
} else {
prefix = ">"
}

View File

@@ -25,8 +25,8 @@ func (cmd *Commands) Nick(name string) {
// prevent sending extensive JOIN commands.
func (cmd *Commands) Join(channels ...string) {
// We can join multiple channels at once, however we need to ensure that
// we are not exceeding the line length. (see maxLength)
max := maxLength - len(JOIN) - 1
// we are not exceeding the line length (see Client.MaxEventLength()).
max := cmd.c.MaxEventLength() - len(JOIN) - 1
var buffer string
@@ -329,8 +329,8 @@ func (cmd *Commands) List(channels ...string) {
}
// We can LIST multiple channels at once, however we need to ensure that
// we are not exceeding the line length. (see maxLength)
max := maxLength - len(JOIN) - 1
// we are not exceeding the line length (see Client.MaxEventLength()).
max := cmd.c.MaxEventLength() - len(JOIN) - 1
var buffer string

View File

@@ -12,6 +12,8 @@ import (
"net"
"sync"
"time"
"github.com/lrstanley/girc/internal/ctxgroup"
)
// Messages are delimited with CR and LF line endings, we're using the last
@@ -142,17 +144,44 @@ type ErrParseEvent struct {
func (e ErrParseEvent) Error() string { return "unable to parse event: " + e.Line }
func (c *ircConn) decode() (event *Event, err error) {
line, err := c.io.ReadString(delim)
if err != nil {
return nil, err
}
type decodedEvent struct {
event *Event
err error
}
if event = ParseEvent(line); event == nil {
return nil, ErrParseEvent{line}
}
func (c *ircConn) decode() <-chan decodedEvent {
ch := make(chan decodedEvent)
return event, nil
go func() {
defer close(ch)
line, err := c.io.ReadString(delim)
if err != nil {
select {
case ch <- decodedEvent{err: err}:
default:
}
return
}
event := ParseEvent(line)
if event == nil {
select {
case ch <- decodedEvent{err: ErrParseEvent{Line: line}}:
default:
}
return
}
select {
case ch <- decodedEvent{event: event}:
default:
}
}()
return ch
}
func (c *ircConn) encode(event *Event) error {
@@ -291,20 +320,17 @@ startConn:
} else {
c.conn = newMockConn(mock)
}
c.mu.Unlock()
var ctx context.Context
ctx, c.stop = context.WithCancel(context.Background())
c.mu.Unlock()
errs := make(chan error, 4)
var wg sync.WaitGroup
// 4 being the number of goroutines we need to finish when this function
// returns.
wg.Add(4)
go c.execLoop(ctx, errs, &wg)
go c.readLoop(ctx, errs, &wg)
go c.sendLoop(ctx, errs, &wg)
go c.pingLoop(ctx, errs, &wg)
group := ctxgroup.New(ctx)
group.Go(c.execLoop)
group.Go(c.readLoop)
group.Go(c.sendLoop)
group.Go(c.pingLoop)
// Passwords first.
@@ -338,16 +364,15 @@ startConn:
c.RunHandlers(&Event{Command: INITIALIZED, Params: []string{addr}})
// Wait for the first error.
var result error
select {
case <-ctx.Done():
err := group.Wait()
if err != nil {
c.debug.Printf("received error, beginning cleanup: %v", err)
} else {
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.Printf("received error, beginning cleanup: %v", err)
result = err
}
// Make sure that the connection is closed if not already.
@@ -363,20 +388,13 @@ startConn:
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")
// Wait for all goroutines to finish.
wg.Wait()
close(errs)
// This helps ensure that the end user isn't improperly using the client
// more than once. If they want to do this, they should be using multiple
// clients, not multiple instances of Connect().
c.mu.Lock()
c.conn = nil
if result == nil {
if err == nil {
if c.state.sts.beginUpgrade {
c.state.sts.beginUpgrade = false
c.mu.Unlock()
@@ -389,76 +407,85 @@ startConn:
}
c.mu.Unlock()
return result
return err
}
// readLoop sets a timeout of 300 seconds, and then attempts to read from the
// IRC server. If there is an error, it calls Reconnect.
func (c *Client) readLoop(ctx context.Context, errs chan error, wg *sync.WaitGroup) {
func (c *Client) readLoop(ctx context.Context) error {
c.debug.Print("starting readLoop")
defer c.debug.Print("closing readLoop")
var event *Event
var err error
var de decodedEvent
for {
select {
case <-ctx.Done():
wg.Done()
return
return nil
default:
_ = c.conn.sock.SetReadDeadline(time.Now().Add(300 * time.Second))
event, err = c.conn.decode()
if err != nil {
errs <- err
wg.Done()
return
select {
case <-ctx.Done():
return nil
case de = <-c.conn.decode():
}
if de.err != nil {
return de.err
}
// Check if it's an echo-message.
if !c.Config.disableTracking {
event.Echo = (event.Command == PRIVMSG || event.Command == NOTICE) &&
event.Source != nil && event.Source.ID() == c.GetID()
de.event.Echo = (de.event.Command == PRIVMSG || de.event.Command == NOTICE) &&
de.event.Source != nil && de.event.Source.ID() == c.GetID()
}
c.rx <- event
c.receive(de.event)
}
}
}
// Send sends an event to the server. Use Client.RunHandlers() if you are
// simply looking to trigger handlers with an event.
// Send sends an event to the server. Send will split events if the event is longer
// than what the server supports, and is an event that supports splitting. 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 {
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] != "" &&
(event.Command == PRIVMSG || event.Command == TOPIC || event.Command == NOTICE) {
event.Params[len(event.Params)-1] = Fmt(event.Params[len(event.Params)-1])
}
<-time.After(delay)
c.write(event)
var events []*Event
events = event.split(c.MaxEventLength())
for _, e := range events {
if !c.Config.AllowFlood {
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(e, true)
c.mu.RUnlock()
return
}
c.conn.mu.Lock()
delay = c.conn.rate(e.Len())
c.conn.mu.Unlock()
c.mu.RUnlock()
}
<-time.After(delay)
c.write(e)
}
}
// 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. write will timeout after 30s if the event
// can't be sent.
func (c *Client) write(event *Event) {
c.mu.RLock()
defer c.mu.RUnlock()
@@ -468,7 +495,19 @@ func (c *Client) write(event *Event) {
c.debugLogEvent(event, true)
return
}
c.tx <- event
t := time.NewTimer(30 * time.Second)
defer func() {
if !t.Stop() {
<-t.C
}
}()
select {
case c.tx <- event:
case <-t.C:
c.debugLogEvent(event, true)
}
}
// rate allows limiting events based on how frequent the event is being sent,
@@ -487,7 +526,7 @@ func (c *ircConn) rate(chars int) time.Duration {
return 0
}
func (c *Client) sendLoop(ctx context.Context, errs chan error, wg *sync.WaitGroup) {
func (c *Client) sendLoop(ctx context.Context) error {
c.debug.Print("starting sendLoop")
defer c.debug.Print("closing sendLoop")
@@ -537,18 +576,14 @@ func (c *Client) sendLoop(ctx context.Context, errs chan error, wg *sync.WaitGro
if event.Command == QUIT {
c.Close()
wg.Done()
return
return nil
}
if err != nil {
errs <- err
wg.Done()
return
return err
}
case <-ctx.Done():
wg.Done()
return
return nil
}
}
}
@@ -568,11 +603,10 @@ type ErrTimedOut struct {
func (ErrTimedOut) Error() string { return "timed out waiting for a requested PING response" }
func (c *Client) pingLoop(ctx context.Context, errs chan error, wg *sync.WaitGroup) {
func (c *Client) pingLoop(ctx context.Context) error {
// Don't run the pingLoop if they want to disable it.
if c.Config.PingDelay <= 0 {
wg.Done()
return
return nil
}
c.debug.Print("starting pingLoop")
@@ -604,9 +638,8 @@ func (c *Client) pingLoop(ctx context.Context, errs chan error, wg *sync.WaitGro
}
c.conn.mu.RLock()
if pingSent && time.Since(c.conn.lastPong) > c.Config.PingDelay+(60*time.Second) {
// It's 60 seconds over what out ping delay is, connection
// has probably dropped.
if pingSent && time.Since(c.conn.lastPong) > c.Config.PingDelay+c.Config.PingTimeout {
// PingTimeout exceeded, connection has probably dropped.
err := ErrTimedOut{
TimeSinceSuccess: time.Since(c.conn.lastPong),
LastPong: c.conn.lastPong,
@@ -615,9 +648,7 @@ func (c *Client) pingLoop(ctx context.Context, errs chan error, wg *sync.WaitGro
}
c.conn.mu.RUnlock()
errs <- err
wg.Done()
return
return err
}
c.conn.mu.RUnlock()
@@ -628,8 +659,7 @@ func (c *Client) pingLoop(ctx context.Context, errs chan error, wg *sync.WaitGro
c.Cmd.Ping(fmt.Sprintf("%d", time.Now().UnixNano()))
pingSent = true
case <-ctx.Done():
wg.Done()
return
return nil
}
}
}

View File

@@ -13,7 +13,41 @@ import (
const (
eventSpace byte = ' ' // Separator.
maxLength int = 510 // Maximum length is 510 (2 for line endings).
// TODO: if state tracking is enabled, we SHOULD be able to use it's known length.
// Can be overridden by the NICKLEN (or MAXNICKLEN) ISUPPORT parameter. 30 or 31
// are typical values for this parameter advertised by servers today.
defaultNickLength = 30
// The maximum length of <username> may be specified by the USERLEN RPL_ISUPPORT
// parameter. If this length is advertised, the username MUST be silently truncated
// to the given length before being used.
defaultUserLength = 18
// If a looked-up domain name is longer than this length (or overridden by the
// HOSTLEN ISUPPORT parameter), the server SHOULD opt to use the IP address instead,
// so that the hostname is underneath this length.
defaultHostLength = 63
// defaultPrefixPadding defaults the estimated prefix padding length of a given
// event. See also:
// [ ":" ( servername / ( nickname [ [ "!" user ] "@" host ] ) ) SPACE ]
defaultPrefixPadding = 4
)
var (
// DefaultMaxLineLength is the default maximum length for an event. 510 (+2 for line endings)
// is used as a default as this is used by many older implementations.
//
// See also: RFC 2812
// IRC messages are always lines of characters terminated with a CR-LF
// (Carriage Return - Line Feed) pair, and these messages SHALL NOT
// exceed 512 characters in length, counting all characters including
// the trailing CR-LF.
DefaultMaxLineLength = 510
// DefaultMaxPrefixLength defines the default max ":nickname!user@host " length
// that's used to calculate line splitting.
DefaultMaxPrefixLength = defaultPrefixPadding + defaultNickLength + defaultUserLength + defaultHostLength
)
// cutCRFunc is used to trim CR characters from prefixes/messages.
@@ -125,16 +159,16 @@ func ParseEvent(raw string) (e *Event) {
// Event represents an IRC protocol message, see RFC1459 section 2.3.1
//
// <message> :: [':' <prefix> <SPACE>] <command> <params> <crlf>
// <prefix> :: <servername> | <nick> ['!' <user>] ['@' <host>]
// <command> :: <letter>{<letter>} | <number> <number> <number>
// <SPACE> :: ' '{' '}
// <params> :: <SPACE> [':' <trailing> | <middle> <params>]
// <middle> :: <Any *non-empty* sequence of octets not including SPACE or NUL
// or CR or LF, the first of which may not be ':'>
// <trailing> :: <Any, possibly empty, sequence of octets not including NUL or
// CR or LF>
// <crlf> :: CR LF
// <message> :: [':' <prefix> <SPACE>] <command> <params> <crlf>
// <prefix> :: <servername> | <nick> ['!' <user>] ['@' <host>]
// <command> :: <letter>{<letter>} | <number> <number> <number>
// <SPACE> :: ' '{' '}
// <params> :: <SPACE> [':' <trailing> | <middle> <params>]
// <middle> :: <Any *non-empty* sequence of octets not including SPACE or NUL
// or CR or LF, the first of which may not be ':'>
// <trailing> :: <Any, possibly empty, sequence of octets not including NUL or
// CR or LF>
// <crlf> :: CR LF
type Event struct {
// Source is the origin of the event.
Source *Source `json:"source"`
@@ -223,11 +257,80 @@ func (e *Event) Equals(ev *Event) bool {
return true
}
// Len calculates the length of the string representation of event. Note that
// this will return the true length (even if longer than what IRC supports),
// which may be useful if you are trying to check and see if a message is
// too long, to trim it down yourself.
// split will split a potentially large event that is larger than what the server
// supports, into multiple events. split will ignore events that cannot be split, and
// if the event isn't longer than what the server supports, it will just return an array
// with 1 entry, the original event.
func (e *Event) split(maxLength int) []*Event {
if len(e.Params) < 1 || (e.Command != PRIVMSG && e.Command != NOTICE) {
return []*Event{e}
}
// Exclude source, even if it does exist, because the server will likely ignore the
// sent source anyway.
event := e.Copy()
event.Source = nil
if event.LenOpts(false) < maxLength {
return []*Event{e}
}
results := []*Event{}
// Will force the length check to include " :". This will allow us to get the length
// of the commands and necessary prefixes.
text := event.Last()
event.Params[len(event.Params)-1] = ""
cmdLen := event.LenOpts(false)
var ok bool
var ctcp *CTCPEvent
if ok, ctcp = e.IsCTCP(); ok {
if text == "" {
return []*Event{e}
}
text = ctcp.Text
// ctcpDelim's at start and end, and space between command and trailing text.
maxLength -= len(ctcp.Command) + 4
}
// If the command itself is longer than the limit, there is a problem. PRIVMSG should
// be 1->1 per RFC. Just return the original message and let it be the user of the
// libraries problem.
if cmdLen > maxLength {
return []*Event{e}
}
// Split the text into correctly size segments, and make the necessary number of
// events that duplicate the original event.
for _, split := range splitMessage(text, maxLength-cmdLen) {
if ctcp != nil {
split = string(ctcpDelim) + ctcp.Command + string(eventSpace) + split + string(ctcpDelim)
}
clonedEvent := event.Copy()
clonedEvent.Source = e.Source
clonedEvent.Params[len(e.Params)-1] = split
results = append(results, clonedEvent)
}
return results
}
// Len calculates the length of the string representation of event (including tags).
// Note that this will return the true length (even if longer than what IRC supports),
// which may be useful if you are trying to check and see if a message is too long, to
// trim it down yourself.
func (e *Event) Len() (length int) {
return e.LenOpts(true)
}
// LenOpts calculates the length of the string representation of event (with a toggle
// for tags). Note that this will return the true length (even if longer than what IRC
// supports), which may be useful if you are trying to check and see if a message is
// too long, to trim it down yourself.
func (e *Event) LenOpts(includeTags bool) (length int) {
if e.Tags != nil {
// Include tags and trailing space.
length = e.Tags.Len() + 1
@@ -248,7 +351,7 @@ func (e *Event) Len() (length int) {
// If param contains a space or it's empty, it's trailing, so it should be
// prefixed with a colon (:).
if i == len(e.Params)-1 && (strings.Contains(e.Params[i], " ") || strings.HasPrefix(e.Params[i], ":") || e.Params[i] == "") {
if i == len(e.Params)-1 && (strings.Contains(e.Params[i], " ") || e.Params[i] == "" || strings.HasPrefix(e.Params[i], ":")) {
length++
}
}
@@ -259,10 +362,6 @@ func (e *Event) Len() (length int) {
// Bytes returns a []byte representation of event. Strips all newlines and
// carriage returns.
//
// Per RFC2812 section 2.3, messages should not exceed 512 characters in
// length. This method forces that limit by discarding any characters
// exceeding the length limit.
func (e *Event) Bytes() []byte {
buffer := new(bytes.Buffer)
@@ -284,7 +383,7 @@ func (e *Event) Bytes() []byte {
// Space separated list of arguments.
if len(e.Params) > 0 {
for i := 0; i < len(e.Params); i++ {
if i == len(e.Params)-1 && (strings.Contains(e.Params[i], " ") || strings.HasPrefix(e.Params[i], ":") || e.Params[i] == "") {
if i == len(e.Params)-1 && (strings.Contains(e.Params[i], " ") || e.Params[i] == "" || strings.HasPrefix(e.Params[i], ":")) {
buffer.WriteString(string(eventSpace) + string(messagePrefix) + e.Params[i])
continue
}
@@ -292,11 +391,6 @@ func (e *Event) Bytes() []byte {
}
}
// We need the limit the buffer length.
if buffer.Len() > (maxLength) {
buffer.Truncate(maxLength)
}
// If we truncated in the middle of a utf8 character, we need to remove
// the other (now invalid) bytes.
out := bytes.ToValidUTF8(buffer.Bytes(), nil)

View File

@@ -7,13 +7,21 @@ package girc
import (
"bytes"
"fmt"
"net/url"
"regexp"
"strings"
"unicode/utf8"
)
const (
fmtOpenChar = '{'
fmtCloseChar = '}'
fmtOpenChar = '{'
fmtCloseChar = '}'
maxWordSplitLength = 30
)
var (
reCode = regexp.MustCompile(`(\x02|\x1d|\x0f|\x03|\x16|\x1f|\x01)`)
reColor = regexp.MustCompile(`\x03([019]?\d(,[019]?\d)?)`)
)
var fmtColors = map[string]int{
@@ -66,9 +74,9 @@ var fmtCodes = map[string]string{
//
// For example:
//
// client.Message("#channel", Fmt("{red}{b}Hello {red,blue}World{c}"))
// client.Message("#channel", Fmt("{red}{b}Hello {red,blue}World{c}"))
func Fmt(text string) string {
var last = -1
last := -1
for i := 0; i < len(text); i++ {
if text[i] == fmtOpenChar {
last = i
@@ -136,16 +144,12 @@ func TrimFmt(text string) string {
return text
}
// This is really the only fastest way of doing this (marginally better than
// actually trying to parse it manually.)
var reStripColor = regexp.MustCompile(`\x03([019]?\d(,[019]?\d)?)?`)
// StripRaw tries to strip all ASCII format codes that are used for IRC.
// Primarily, foreground/background colors, and other control bytes like
// reset, bold, italic, reverse, etc. This also is done in a specific way
// in order to ensure no truncation of other non-irc formatting.
func StripRaw(text string) string {
text = reStripColor.ReplaceAllString(text, "")
text = reColor.ReplaceAllString(text, "")
for _, code := range fmtCodes {
text = strings.ReplaceAll(text, code, "")
@@ -164,12 +168,12 @@ func StripRaw(text string) string {
// all ASCII printable chars. This function will NOT do that for
// compatibility reasons.
//
// channel = ( "#" / "+" / ( "!" channelid ) / "&" ) chanstring
// [ ":" chanstring ]
// chanstring = 0x01-0x07 / 0x08-0x09 / 0x0B-0x0C / 0x0E-0x1F / 0x21-0x2B
// chanstring = / 0x2D-0x39 / 0x3B-0xFF
// ; any octet except NUL, BELL, CR, LF, " ", "," and ":"
// channelid = 5( 0x41-0x5A / digit ) ; 5( A-Z / 0-9 )
// channel = ( "#" / "+" / ( "!" channelid ) / "&" ) chanstring
// [ ":" chanstring ]
// chanstring = 0x01-0x07 / 0x08-0x09 / 0x0B-0x0C / 0x0E-0x1F / 0x21-0x2B
// chanstring = / 0x2D-0x39 / 0x3B-0xFF
// ; any octet except NUL, BELL, CR, LF, " ", "," and ":"
// channelid = 5( 0x41-0x5A / digit ) ; 5( A-Z / 0-9 )
func IsValidChannel(channel string) bool {
if len(channel) <= 1 || len(channel) > 50 {
return false
@@ -214,10 +218,10 @@ func IsValidChannel(channel string) bool {
// IsValidNick validates an IRC nickname. Note that this does not validate
// IRC nickname length.
//
// nickname = ( letter / special ) *8( letter / digit / special / "-" )
// letter = 0x41-0x5A / 0x61-0x7A
// digit = 0x30-0x39
// special = 0x5B-0x60 / 0x7B-0x7D
// nickname = ( letter / special ) *8( letter / digit / special / "-" )
// letter = 0x41-0x5A / 0x61-0x7A
// digit = 0x30-0x39
// special = 0x5B-0x60 / 0x7B-0x7D
func IsValidNick(nick string) bool {
if nick == "" {
return false
@@ -253,8 +257,9 @@ func IsValidNick(nick string) bool {
// not be supported on all networks. Some limit this to only a single period.
//
// Per RFC:
// user = 1*( %x01-09 / %x0B-0C / %x0E-1F / %x21-3F / %x41-FF )
// ; any octet except NUL, CR, LF, " " and "@"
//
// user = 1*( %x01-09 / %x0B-0C / %x0E-1F / %x21-3F / %x41-FF )
// ; any octet except NUL, CR, LF, " " and "@"
func IsValidUser(name string) bool {
if name == "" {
return false
@@ -350,3 +355,172 @@ func Glob(input, match string) bool {
// Check suffix last.
return trailingGlob || strings.HasSuffix(input, parts[last])
}
// sliceInsert inserts a string into a slice at a specific index, while trying
// to avoid as many allocations as possible.
func sliceInsert(input []string, i int, v ...string) []string {
total := len(input) + len(v)
if total <= cap(input) {
output := input[:total]
copy(output[i+len(v):], input[i:])
copy(output[i:], v)
return output
}
output := make([]string, total)
copy(output, input[:i])
copy(output[i:], v)
copy(output[i+len(v):], input[i:])
return output
}
// splitMessage is a text splitter that takes into consideration a few things:
// - Ensuring the returned text is no longer than maxWidth.
// - Attempting to split at the closest word boundary, while still staying inside
// of the specific maxWidth.
// - if there is no good word boundary for longer words (or e.g. links, raw data, etc)
// that are above maxWordSplitLength characters, split the word into chunks to fit the
//
// maximum width.
func splitMessage(input string, maxWidth int) (output []string) {
input = strings.ToValidUTF8(input, "?")
words := strings.FieldsFunc(strings.TrimSpace(input), func(r rune) bool {
switch r { // Same as unicode.IsSpace, but without ctrl/lf.
case '\t', '\v', '\f', ' ', 0x85, 0xA0:
return true
}
return false
})
output = []string{""}
codes := []string{}
var lastColor string
var match []string
for i := 0; i < len(words); i++ {
j := strings.IndexAny(words[i], "\n\r")
if j == -1 {
continue
}
word := words[i]
words[i] = word[:j]
words = sliceInsert(words, i+1, "", strings.TrimLeft(word[j:], "\n\r"))
}
for _, word := range words {
// Used in place of a single newline.
if word == "" {
// Last line was already empty or already only had control characters.
if output[len(output)-1] == "" || output[len(output)-1] == lastColor+word {
continue
}
output = append(output, strings.Join(codes, "")+lastColor+word)
continue
}
// Keep track of the last used color codes.
match = reColor.FindAllString(word, -1)
if len(match) > 0 {
lastColor = match[len(match)-1]
}
// Find all sequence codes -- this approach isn't perfect (ideally, a lexer
// should be used to track each exact type of code), but it's good enough for
// most cases.
match = reCode.FindAllString(word, -1)
if len(match) > 0 {
for _, m := range match {
// Reset was used, so clear all codes.
if m == fmtCodes["reset"] {
lastColor = ""
codes = []string{}
continue
}
// Check if we already have the code, and if so, remove it (closing).
contains := false
for i := 0; i < len(codes); i++ {
if m == codes[i] {
contains = true
codes = append(codes[:i], codes[i+1:]...)
// If it's a closing color code, reset the last used color
// as well.
if m == fmtCodes["clear"] {
lastColor = ""
}
break
}
}
// Track the new code, unless it's a color clear but we aren't
// tracking a color right now.
if !contains && (lastColor == "" || m != fmtCodes["clear"]) {
codes = append(codes, m)
}
}
}
checkappend:
// Check if we can append, otherwise we must split.
if 1+utf8.RuneCountInString(word)+utf8.RuneCountInString(output[len(output)-1]) < maxWidth {
if output[len(output)-1] != "" {
output[len(output)-1] += " "
}
output[len(output)-1] += word
continue
}
// If the word can fit on a line by itself, check if it's a url. If it is,
// put it on it's own line.
if utf8.RuneCountInString(word+strings.Join(codes, "")+lastColor) < maxWidth {
if _, err := url.Parse(word); err == nil {
output = append(output, strings.Join(codes, "")+lastColor+word)
continue
}
}
// Check to see if we can split by misc symbols, but must be at least a few
// characters long to be split by it.
if j := strings.IndexAny(word, "-+_=|/~:;,."); j > 3 && 1+utf8.RuneCountInString(word[0:j])+utf8.RuneCountInString(output[len(output)-1]) < maxWidth {
if output[len(output)-1] != "" {
output[len(output)-1] += " "
}
output[len(output)-1] += word[0:j]
word = word[j+1:]
goto checkappend
}
// If the word is longer than is acceptable to just put on the next line,
// split it into chunks. Also don't split the word if only a few characters
// left of the word would be on the next line.
if 1+utf8.RuneCountInString(word) > maxWordSplitLength && maxWidth-utf8.RuneCountInString(output[len(output)-1]) > 5 {
left := maxWidth - utf8.RuneCountInString(output[len(output)-1]) - 1 // -1 for the space
if output[len(output)-1] != "" {
output[len(output)-1] += " "
}
output[len(output)-1] += word[0:left]
word = word[left:]
goto checkappend
}
left := maxWidth - utf8.RuneCountInString(output[len(output)-1])
output[len(output)-1] += word[0:left]
output = append(output, strings.Join(codes, "")+lastColor)
word = word[left:]
goto checkappend
}
for i := 0; i < len(output); i++ {
output[i] = strings.ToValidUTF8(output[i], "?")
}
return output
}

View File

@@ -0,0 +1,67 @@
// Copyright (c) Liam Stanley <me@liamstanley.io>. All rights reserved. Use
// of this source code is governed by the MIT license that can be found in
// the LICENSE file.
package ctxgroup
import (
"context"
"sync"
)
// A Group is a collection of goroutines working on subtasks that are part of
// the same overall task.
type Group struct {
ctx context.Context
cancel func()
wg sync.WaitGroup
errOnce sync.Once
err error
}
// New returns a new Group and an associated context derived from ctx.
// Obtain the derived context from calling Group.Context().
//
// The derived context is canceled the first time a function passed to Go
// returns a non-nil error or the first time Wait returns, whichever occurs
// first.
func New(ctx context.Context) *Group {
nctx, cancel := context.WithCancel(ctx)
return &Group{ctx: nctx, cancel: cancel}
}
// Context returns the context for this group. It may be canceled by the first
// function to return a non-nil error.
func (g *Group) Context() context.Context {
return g.ctx
}
// Wait blocks until all function calls from the Go method have returned, then
// returns the first non-nil error (if any) from them.
func (g *Group) Wait() error {
g.wg.Wait()
if g.cancel != nil {
g.cancel()
}
return g.err
}
// Go calls the given function in a new goroutine. The first call to return a
// non-nil error cancels the group; its error will be returned by Wait.
func (g *Group) Go(f func(ctx context.Context) error) {
g.wg.Add(1)
go func() {
defer g.wg.Done()
if err := f(g.ctx); err != nil {
g.errOnce.Do(func() {
g.err = err
if g.cancel != nil {
g.cancel()
}
})
}
}()
}

View File

@@ -118,13 +118,14 @@ func (c *CModes) Get(mode string) (args string, ok bool) {
}
// hasArg checks to see if the mode supports arguments. What ones support this?:
// A = Mode that adds or removes a nick or address to a list. Always has a parameter.
// B = Mode that changes a setting and always has a parameter.
// C = Mode that changes a setting and only has a parameter when set.
// D = Mode that changes a setting and never has a parameter.
// Note: Modes of type A return the list when there is no parameter present.
// Note: Some clients assumes that any mode not listed is of type D.
// Note: Modes in PREFIX are not listed but could be considered type B.
//
// A = Mode that adds or removes a nick or address to a list. Always has a parameter.
// B = Mode that changes a setting and always has a parameter.
// C = Mode that changes a setting and only has a parameter when set.
// D = Mode that changes a setting and never has a parameter.
// Note: Modes of type A return the list when there is no parameter present.
// Note: Some clients assumes that any mode not listed is of type D.
// Note: Modes in PREFIX are not listed but could be considered type B.
func (c *CModes) hasArg(set bool, mode byte) (hasArgs, isSetting bool) {
if len(c.raw) < 1 {
return false, true

View File

@@ -28,10 +28,21 @@ type state struct {
// last capability check. These will get sent once we have received the
// last capability list command from the server.
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
// maxLineLength defines how long before we truncate (or split) messages.
// DefaultMaxLineLength is what is used by default, as this is going to be a common
// standard. However, protocols like IRCv3, or ISUPPORT can override this.
maxLineLength int
// maxPrefixLength defines the estimated prefix length (":nick!user@host ") that
// we can use to calculate line splits.
maxPrefixLength int
// motd is the servers message of the day.
motd string
@@ -51,9 +62,11 @@ func (s *state) reset(initial bool) {
s.host = ""
s.channels = make(map[string]*Channel)
s.users = make(map[string]*User)
s.serverOptions = make(map[string]string)
s.enabledCap = make(map[string]map[string]string)
s.tmpCap = make(map[string]map[string]string)
s.serverOptions = make(map[string]string)
s.maxLineLength = DefaultMaxLineLength
s.maxPrefixLength = DefaultMaxPrefixLength
s.motd = ""
if initial {