summaryrefslogtreecommitdiffstats
path: root/vendor/github.com/lrstanley/girc/conn.go
diff options
context:
space:
mode:
authorWim <wim@42.be>2017-11-08 22:47:18 +0100
committerWim <wim@42.be>2017-11-08 22:47:18 +0100
commite3131541346e49f7f1f789f30c3262421d462458 (patch)
tree8001cd0766f60ad935da8e18de7f7e97aabed1e6 /vendor/github.com/lrstanley/girc/conn.go
parent27e94c438d370c77aa3b2634b615ffafe1828310 (diff)
downloadmatterbridge-msglm-e3131541346e49f7f1f789f30c3262421d462458.tar.gz
matterbridge-msglm-e3131541346e49f7f1f789f30c3262421d462458.tar.bz2
matterbridge-msglm-e3131541346e49f7f1f789f30c3262421d462458.zip
Vendor github.com/lrstanley/girc
Diffstat (limited to 'vendor/github.com/lrstanley/girc/conn.go')
-rw-r--r--vendor/github.com/lrstanley/girc/conn.go566
1 files changed, 566 insertions, 0 deletions
diff --git a/vendor/github.com/lrstanley/girc/conn.go b/vendor/github.com/lrstanley/girc/conn.go
new file mode 100644
index 00000000..a46a5dd7
--- /dev/null
+++ b/vendor/github.com/lrstanley/girc/conn.go
@@ -0,0 +1,566 @@
+// 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 girc
+
+import (
+ "bufio"
+ "context"
+ "crypto/tls"
+ "fmt"
+ "net"
+ "sync"
+ "time"
+)
+
+// Messages are delimited with CR and LF line endings, we're using the last
+// one to split the stream. Both are removed during parsing of the message.
+const delim byte = '\n'
+
+var endline = []byte("\r\n")
+
+// ircConn represents an IRC network protocol connection, it consists of an
+// Encoder and Decoder to manage i/o.
+type ircConn struct {
+ io *bufio.ReadWriter
+ sock net.Conn
+
+ mu sync.RWMutex
+ // lastWrite is used to keep track of when we last wrote to the server.
+ lastWrite time.Time
+ // lastActive is the last time the client was interacting with the server,
+ // excluding a few background commands (PING, PONG, WHO, etc).
+ lastActive time.Time
+ // writeDelay is used to keep track of rate limiting of events sent to
+ // the server.
+ writeDelay time.Duration
+ // connected is true if we're actively connected to a server.
+ connected bool
+ // connTime is the time at which the client has connected to a server.
+ connTime *time.Time
+ // lastPing is the last time that we pinged the server.
+ lastPing time.Time
+ // lastPong is the last successful time that we pinged the server and
+ // received a successful pong back.
+ lastPong time.Time
+ pingDelay time.Duration
+}
+
+// Dialer is an interface implementation of net.Dialer. Use this if you would
+// like to implement your own dialer which the client will use when connecting.
+type Dialer interface {
+ // Dial takes two arguments. Network, which should be similar to "tcp",
+ // "tdp6", "udp", etc -- as well as address, which is the hostname or ip
+ // of the network. Note that network can be ignored if your transport
+ // doesn't take advantage of network types.
+ Dial(network, address string) (net.Conn, error)
+}
+
+// newConn sets up and returns a new connection to the server.
+func newConn(conf Config, dialer Dialer, addr string) (*ircConn, error) {
+ if err := conf.isValid(); err != nil {
+ return nil, err
+ }
+
+ var conn net.Conn
+ var err error
+
+ if dialer == nil {
+ netDialer := &net.Dialer{Timeout: 5 * time.Second}
+
+ if conf.Bind != "" {
+ var local *net.TCPAddr
+ local, err = net.ResolveTCPAddr("tcp", conf.Bind+":0")
+ if err != nil {
+ return nil, err
+ }
+
+ netDialer.LocalAddr = local
+ }
+
+ dialer = netDialer
+ }
+
+ if conn, err = dialer.Dial("tcp", addr); err != nil {
+ return nil, err
+ }
+
+ if conf.SSL {
+ var tlsConn net.Conn
+ tlsConn, err = tlsHandshake(conn, conf.TLSConfig, conf.Server, true)
+ if err != nil {
+ return nil, err
+ }
+
+ conn = tlsConn
+ }
+
+ ctime := time.Now()
+
+ c := &ircConn{
+ sock: conn,
+ connTime: &ctime,
+ connected: true,
+ }
+ c.newReadWriter()
+
+ return c, nil
+}
+
+func newMockConn(conn net.Conn) *ircConn {
+ ctime := time.Now()
+ c := &ircConn{
+ sock: conn,
+ connTime: &ctime,
+ connected: true,
+ }
+ c.newReadWriter()
+
+ return c
+}
+
+// ErrParseEvent is returned when an event cannot be parsed with ParseEvent().
+type ErrParseEvent struct {
+ Line string
+}
+
+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
+ }
+
+ if event = ParseEvent(line); event == nil {
+ return nil, ErrParseEvent{line}
+ }
+
+ return event, nil
+}
+
+func (c *ircConn) encode(event *Event) error {
+ if _, err := c.io.Write(event.Bytes()); err != nil {
+ return err
+ }
+ if _, err := c.io.Write(endline); err != nil {
+ return err
+ }
+
+ return c.io.Flush()
+}
+
+func (c *ircConn) newReadWriter() {
+ c.io = bufio.NewReadWriter(bufio.NewReader(c.sock), bufio.NewWriter(c.sock))
+}
+
+func tlsHandshake(conn net.Conn, conf *tls.Config, server string, validate bool) (net.Conn, error) {
+ if conf == nil {
+ conf = &tls.Config{ServerName: server, InsecureSkipVerify: !validate}
+ }
+
+ tlsConn := tls.Client(conn, conf)
+ return net.Conn(tlsConn), nil
+}
+
+// Close closes the underlying socket.
+func (c *ircConn) Close() error {
+ return c.sock.Close()
+}
+
+// Connect attempts to connect to the given IRC server. Returns only when
+// an error has occurred, or a disconnect was requested with Close(). Connect
+// will only return once all client-based goroutines have been closed to
+// ensure there are no long-running routines becoming backed up.
+//
+// Connect will wait for all non-goroutine handlers to complete on error/quit,
+// however it will not wait for goroutine-based handlers.
+//
+// If this returns nil, this means that the client requested to be closed
+// (e.g. Client.Close()). Connect will panic if called when the last call has
+// not completed.
+func (c *Client) Connect() error {
+ return c.internalConnect(nil, nil)
+}
+
+// DialerConnect allows you to specify your own custom dialer which implements
+// the Dialer interface.
+//
+// An example of using this library would be to take advantage of the
+// golang.org/x/net/proxy library:
+//
+// proxyUrl, _ := proxyURI, err = url.Parse("socks5://1.2.3.4:8888")
+// dialer, _ := proxy.FromURL(proxyURI, &net.Dialer{Timeout: 5 * time.Second})
+// _ := girc.DialerConnect(dialer)
+func (c *Client) DialerConnect(dialer Dialer) error {
+ return c.internalConnect(nil, dialer)
+}
+
+// MockConnect is used to implement mocking with an IRC server. Supply a net.Conn
+// that will be used to spoof the server. A useful way to do this is to so
+// net.Pipe(), pass one end into MockConnect(), and the other end into
+// bufio.NewReader().
+//
+// For example:
+//
+// client := girc.New(girc.Config{
+// Server: "dummy.int",
+// Port: 6667,
+// Nick: "test",
+// User: "test",
+// Name: "Testing123",
+// })
+//
+// in, out := net.Pipe()
+// defer in.Close()
+// defer out.Close()
+// b := bufio.NewReader(in)
+//
+// go func() {
+// if err := client.MockConnect(out); err != nil {
+// panic(err)
+// }
+// }()
+//
+// defer client.Close(false)
+//
+// for {
+// in.SetReadDeadline(time.Now().Add(300 * time.Second))
+// line, err := b.ReadString(byte('\n'))
+// if err != nil {
+// panic(err)
+// }
+//
+// event := girc.ParseEvent(line)
+//
+// if event == nil {
+// continue
+// }
+//
+// // Do stuff with event here.
+// }
+func (c *Client) MockConnect(conn net.Conn) error {
+ return c.internalConnect(conn, nil)
+}
+
+func (c *Client) internalConnect(mock net.Conn, dialer Dialer) error {
+ // We want to be the only one handling connects/disconnects right now.
+ c.mu.Lock()
+
+ if c.conn != nil {
+ panic("use of connect more than once")
+ }
+
+ // Reset the state.
+ c.state.reset()
+
+ 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())
+ if err != nil {
+ c.mu.Unlock()
+ return err
+ }
+
+ c.conn = conn
+ } else {
+ c.conn = newMockConn(mock)
+ }
+
+ 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)
+
+ // Passwords first.
+ if c.Config.ServerPass != "" {
+ c.write(&Event{Command: PASS, Params: []string{c.Config.ServerPass}, Sensitive: true})
+ }
+
+ // List the IRCv3 capabilities, specifically with the max protocol we
+ // support. The IRCv3 specification doesn't directly state if this should
+ // be called directly before registration, or if it should be called
+ // after NICK/USER requests. It looks like non-supporting networks
+ // should ignore this, and some IRCv3 capable networks require this to
+ // occur before NICK/USER registration.
+ c.listCAP()
+
+ // Then nickname.
+ c.write(&Event{Command: NICK, Params: []string{c.Config.Nick}})
+
+ // Then username and realname.
+ if c.Config.Name == "" {
+ c.Config.Name = c.Config.User
+ }
+
+ c.write(&Event{Command: USER, Params: []string{c.Config.User, "*", "*"}, Trailing: c.Config.Name})
+
+ // Send a virtual event allowing hooks for successful socket connection.
+ c.RunHandlers(&Event{Command: INITIALIZED, Trailing: c.Server()})
+
+ // 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: STOPPED, Trailing: c.Server()})
+ case err := <-errs:
+ c.debug.Print("received error, beginning clean up")
+ result = err
+ }
+
+ // Make sure that the connection is closed if not already.
+ c.mu.RLock()
+ if c.stop != nil {
+ c.stop()
+ }
+ c.conn.mu.Lock()
+ c.conn.connected = false
+ _ = c.conn.Close()
+ c.conn.mu.Unlock()
+ c.mu.RUnlock()
+
+ // 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
+ c.mu.Unlock()
+
+ return result
+}
+
+// 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) {
+ c.debug.Print("starting readLoop")
+ defer c.debug.Print("closing readLoop")
+
+ var event *Event
+ var err error
+
+ for {
+ select {
+ case <-ctx.Done():
+ wg.Done()
+ return
+ default:
+ _ = c.conn.sock.SetReadDeadline(time.Now().Add(300 * time.Second))
+ event, err = c.conn.decode()
+ if err != nil {
+ errs <- err
+ wg.Done()
+ return
+ }
+
+ c.rx <- event
+ }
+ }
+}
+
+// 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) {
+ if !c.Config.AllowFlood {
+ <-time.After(c.conn.rate(event.Len()))
+ }
+
+ if c.Config.GlobalFormat && event.Trailing != "" &&
+ (event.Command == PRIVMSG || event.Command == TOPIC || event.Command == NOTICE) {
+ event.Trailing = Fmt(event.Trailing)
+ }
+
+ 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.tx <- event
+}
+
+// rate allows limiting events based on how frequent the event is being sent,
+// as well as how many characters each event has.
+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
+ }
+
+ return 0
+}
+
+func (c *Client) sendLoop(ctx context.Context, errs chan error, wg *sync.WaitGroup) {
+ c.debug.Print("starting sendLoop")
+ defer c.debug.Print("closing sendLoop")
+
+ var err error
+
+ for {
+ select {
+ case event := <-c.tx:
+ // Check if tags exist on the event. If they do, and message-tags
+ // isn't a supported capability, remove them from the event.
+ if event.Tags != nil {
+ c.state.RLock()
+ var in bool
+ for i := 0; i < len(c.state.enabledCap); i++ {
+ if c.state.enabledCap[i] == "message-tags" {
+ in = true
+ break
+ }
+ }
+ c.state.RUnlock()
+
+ if !in {
+ event.Tags = Tags{}
+ }
+ }
+
+ // 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.conn.mu.Lock()
+ c.conn.lastWrite = time.Now()
+
+ if event.Command != PING && event.Command != PONG && event.Command != WHO {
+ c.conn.lastActive = c.conn.lastWrite
+ }
+ c.conn.mu.Unlock()
+
+ // Write the raw line.
+ _, err = c.conn.io.Write(event.Bytes())
+ if err == nil {
+ // And the \r\n.
+ _, err = c.conn.io.Write(endline)
+ if err == nil {
+ // Lastly, flush everything to the socket.
+ err = c.conn.io.Flush()
+ }
+ }
+
+ if err != nil {
+ errs <- err
+ wg.Done()
+ return
+ }
+ case <-ctx.Done():
+ wg.Done()
+ return
+ }
+ }
+}
+
+// ErrTimedOut is returned when we attempt to ping the server, and timed out
+// before receiving a PONG back.
+type ErrTimedOut struct {
+ // TimeSinceSuccess is how long ago we received a successful pong.
+ TimeSinceSuccess time.Duration
+ // LastPong is the time we received our last successful pong.
+ LastPong time.Time
+ // LastPong is the last time we sent a pong request.
+ LastPing time.Time
+ // Delay is the configured delay between how often we send a ping request.
+ Delay time.Duration
+}
+
+func (ErrTimedOut) Error() string { return "timed out during ping to server" }
+
+func (c *Client) pingLoop(ctx context.Context, errs chan error, wg *sync.WaitGroup) {
+ // Don't run the pingLoop if they want to disable it.
+ if c.Config.PingDelay <= 0 {
+ wg.Done()
+ return
+ }
+
+ c.debug.Print("starting pingLoop")
+ defer c.debug.Print("closing pingLoop")
+
+ c.conn.mu.Lock()
+ c.conn.lastPing = time.Now()
+ c.conn.lastPong = time.Now()
+ c.conn.mu.Unlock()
+
+ tick := time.NewTicker(c.Config.PingDelay)
+ defer tick.Stop()
+
+ started := time.Now()
+ past := false
+
+ for {
+ select {
+ case <-tick.C:
+ // Delay during connect to wait for the client to register, otherwise
+ // some ircd's will not respond (e.g. during SASL negotiation).
+ if !past {
+ if time.Since(started) < 30*time.Second {
+ continue
+ }
+
+ past = true
+ }
+
+ c.conn.mu.RLock()
+ if 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.
+ errs <- ErrTimedOut{
+ TimeSinceSuccess: time.Since(c.conn.lastPong),
+ LastPong: c.conn.lastPong,
+ LastPing: c.conn.lastPing,
+ Delay: c.Config.PingDelay,
+ }
+
+ wg.Done()
+ c.conn.mu.RUnlock()
+ return
+ }
+ c.conn.mu.RUnlock()
+
+ c.conn.mu.Lock()
+ c.conn.lastPing = time.Now()
+ c.conn.mu.Unlock()
+
+ c.Cmd.Ping(fmt.Sprintf("%d", time.Now().UnixNano()))
+ case <-ctx.Done():
+ wg.Done()
+ return
+ }
+ }
+}