// 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 } } }