Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- // My small, family IRC server
- //
- // This file won't compile by itself because it's only one file from
- // my larger family server (movie hosting, Asterisk dialplan, Git
- // hosting, personal assistant, etc).
- //
- // Users authenticate via NICK and PASS. The USER is interpreted as a
- // "device" name. That allows each user to connect from multiple
- // devices simultaneously while still appearing as only one nick in
- // channels. Each nick-device combo has a queue of messages which
- // have not yet been delivered. This allows users to disconnect and
- // receive their messages when they reconnect.
- //
- // IRC doesn't have a built in ACK mechanism, so we fake it with PING
- // messages. After each PRIVMSG, we send a PING which identifies the
- // previous PRIVMSG. Since TCP guarantees in-order arrival, if we
- // receive the PONG, we know the client recieved the message too.
- // It's a hack, but it allows messages to be queued when users are
- // offline.
- //
- // This file doesn't include the code for our SMS gateway or our
- // family IRC bot.
- //
- // Copyright 2018 Michael Hendricks
- //
- // Permission is granted to do anything with this work for any purpose
- // with or without fee, and with or without attribution.
- package main
- import (
- "bytes"
- "errors"
- "fmt"
- "io"
- "log"
- "net"
- "runtime/debug"
- "strconv"
- "strings"
- "sync"
- "time"
- "github.com/mndrix/rand"
- )
- // configuration goes here
- const serverHostname = `example.org`
- const ircTimeout = 5 * time.Minute
- var ircAuth = map[string]string{
- "alice": "secret password",
- "bob": "another password",
- "charles": "more secrets",
- "david": "running out of secrets",
- }
- var routeMap = map[string]func(*client, Message) error{
- "JOIN": commandJoin,
- "PASS": commandPass,
- "PING": commandPing,
- "PONG": commandPong,
- "PRIVMSG": commandPrivmsg,
- "WHOIS": commandWhois,
- }
- type IrcServer struct {
- sync.Mutex
- clients Clients // all connected clients
- }
- type NetConn struct {
- conn net.Conn
- buf []byte
- }
- type IrcConn interface {
- Addr() string // connection's address (IP:port, etc)
- Close() error // close connection (noop if already closed)
- Send(Message) error // transmit IRC message to connection
- Recv() (Message, bool) // read next IRC message from connection
- }
- type client struct {
- conn IrcConn
- active time.Time // last time client was active
- device string // name of the device ("phone", "laptop", etc)
- latency time.Duration // latency of most recent ping; -1 if unreachable
- name string // user's real name
- nick string // user's authenticated nickname
- queue []Message // messages sent but not yet acknowledged
- sentPing time.Time // time of the most recent, outgoing ping
- tags map[string]bool // tags set on this client
- }
- type Clients map[*client]bool
- type Message struct {
- command string
- from *client
- id string
- line string
- params []string
- time time.Time
- omitPrefix bool
- }
- var ircServer = &IrcServer{clients: map[*client]bool{}}
- func ircListen() {
- listener, err := net.Listen("tcp", ":6667")
- if err != nil {
- panic(err)
- }
- defer listener.Close()
- log.Printf("IRC listening on %s", listener.Addr())
- go ircBot()
- for {
- conn, err := listener.Accept()
- if err != nil {
- panic(err)
- }
- go newClient(&NetConn{conn: conn}).handle()
- }
- }
- func (srv *IrcServer) Clients() Clients {
- srv.Lock()
- defer srv.Unlock()
- return srv.clients.Keep(func(*client) bool { return true })
- }
- func (srv *IrcServer) addClient(cl *client) {
- srv.Lock()
- defer srv.Unlock()
- // does this user-device combo already have connections?
- cl.queue = nil
- after := cl.conn.Addr()
- remove := make([]*client, 0, len(srv.clients))
- for c := range srv.clients {
- if c.nick == cl.nick && c.device == cl.device {
- before := "(closed)"
- if c.conn != nil {
- before = c.conn.Addr()
- }
- log.Printf("Replacing client %s with %s", before, after)
- cl.queue = append(cl.queue, c.queue...)
- for tag := range c.tags {
- cl.setTag(tag)
- }
- srv.closeClient(c)
- remove = append(remove, c)
- }
- }
- for _, c := range remove {
- delete(srv.clients, c)
- }
- srv.clients[cl] = true
- }
- func (srv *IrcServer) closeClient(cl *client) {
- if err := recover(); err != nil {
- log.Printf("PANIC: %s, %s", err, debug.Stack())
- }
- if cl.conn != nil {
- err := cl.conn.Close()
- if err == nil {
- log.Printf("Server closed connection")
- } else {
- log.Printf("Error closing connection: %s", err)
- }
- cl.conn = nil
- }
- }
- func (conn *NetConn) Addr() string { return conn.conn.RemoteAddr().String() }
- func (conn *NetConn) Close() error { return conn.conn.Close() }
- func (conn *NetConn) Send(msg Message) error {
- if conn.conn == nil {
- return nil
- }
- _, err := conn.conn.Write([]byte(msg.String()))
- return err
- }
- func newClient(conn IrcConn) *client {
- return &client{
- conn: conn,
- tags: make(map[string]bool),
- }
- }
- func (cl *client) next() (msg Message, ok bool) {
- msg, ok = cl.conn.Recv()
- if ok {
- if msg.command != "x-timeout" {
- cl.active = time.Now()
- log.Printf("%s -> %q", cl.logNick(), msg.line)
- }
- if time.Since(cl.sentPing) >= ircTimeout {
- cl.ping(serverHostname)
- }
- }
- return
- }
- func (cl *NetConn) Recv() (msg Message, ok bool) {
- // do we already have a full message in the buffer?
- n := 0
- if msg, n = ParseMessage(cl.buf); n > 0 {
- cl.buf = cl.buf[n:]
- ok = true
- return
- }
- // nope. read more data into the buffer
- cl.conn.SetReadDeadline(time.Now().Add(ircTimeout))
- data := make([]byte, 1024)
- n, err := cl.conn.Read(data)
- if n == 0 && err == nil {
- err = errors.New("no data available to read")
- }
- if neterr, ok := err.(net.Error); ok && neterr.Timeout() {
- return Message{command: "x-timeout"}, true
- }
- switch err {
- case nil:
- cl.buf = append(cl.buf, data[0:n]...)
- return cl.Recv()
- case io.EOF:
- log.Printf("%s -> Client disconnected", cl.Addr())
- default:
- log.Printf("%s -> next(): %s (%#v)", cl.Addr(), err, err)
- }
- return
- }
- func (cl *client) resend(tag string) error {
- // resend messages the client missed while away
- seen := make(map[string]bool, len(cl.queue))
- for _, msg := range cl.queue {
- if !seen[msg.id] && msg.command == "PRIVMSG" && tag == msg.params[0] {
- seen[msg.id] = true
- err := cl.Send(msg)
- if err == nil {
- err = cl.ping(msg.id)
- }
- if err != nil {
- return err
- }
- }
- }
- return nil
- }
- func (cl *client) send(cmd string, args ...string) error {
- return cl.Send(NewMessage(cmd, args...))
- }
- // Send transmits a single message to the client.
- func (cl *client) Send(msg Message) error {
- switch msg.command {
- case "PING", "PRIVMSG":
- cl.queue = append(cl.queue, msg)
- }
- log.Printf("%s <- %q", cl.logNick(), msg.String())
- if cl.conn == nil {
- return nil
- }
- return cl.conn.Send(msg)
- }
- func (cl *client) ping(id string) error {
- cl.sentPing = time.Now()
- return cl.send("PING", id)
- }
- func (cl *client) logNick() string {
- if cl.nick == "" {
- return "?"
- }
- return cl.nick
- }
- func (cl *client) hasTag(tag string) bool {
- _, ok := cl.tags[tag]
- return ok
- }
- func (cl *client) setTag(tag string) {
- cl.tags[tag] = true
- }
- func (cl *client) handle() {
- defer ircServer.closeClient(cl)
- log.Printf("Client connected from %s", cl.conn.Addr())
- cl.sentPing = time.Now() // pretend that we sent a ping
- cl.send("NOTICE", "*", "Hi")
- for {
- msg, ok := cl.next()
- if !ok {
- return
- }
- if fn, ok := routeMap[msg.command]; ok {
- if err := fn(cl, msg); err != nil {
- log.Printf("callback error: %s", err)
- return
- }
- }
- }
- }
- func registrationDone(cl *client) error {
- // announce that registration is done
- sends := []func() error{
- func() error { return cl.send("001", cl.nick, "Welcome") },
- func() error {
- return cl.send(
- "004", cl.nick,
- serverHostname,
- "0.0.1", // version
- "ov", // user modes
- "bklm", // channel modes
- )
- },
- func() error {
- return cl.send(
- "005", cl.nick,
- "CHANTYPES=#",
- "CHANMODES=b,k,l,m",
- "PREFIX=(ov)@+",
- "are supported",
- )
- },
- func() error { return cl.send("422", cl.nick, "No message today") },
- func() error { return cl.resend(cl.nick) },
- func() error { return cl.ping(serverHostname) },
- }
- for _, send := range sends {
- err := send()
- if err != nil {
- return err
- }
- }
- return nil
- }
- func commandPass(cl *client, msg Message) error {
- password := msg.params[0]
- username := ""
- device := ""
- name := ""
- for username == "" || device == "" {
- msg, ok := cl.next()
- if !ok {
- return errors.New("commandPass: trouble reading")
- }
- if msg.command == "NICK" {
- username = msg.params[0]
- } else if msg.command == "USER" {
- device = msg.params[0]
- name = msg.params[3]
- }
- }
- // verify credentials
- correctPassword, ok := ircAuth[username]
- if ok && password == correctPassword {
- cl.nick = username
- cl.setTag(username)
- cl.device = device
- cl.name = name
- ircServer.addClient(cl)
- return registrationDone(cl)
- }
- return cl.send("464", "*", "Wrong username or password")
- }
- func commandPing(cl *client, msg Message) error {
- return cl.send("PONG", serverHostname, msg.params[0])
- }
- func commandPong(cl *client, msg Message) error {
- // keep messages that have not yet been acknowledged
- keep := make([]Message, 0, len(cl.queue))
- id := msg.params[0]
- for _, msg := range cl.queue {
- if msg.id == id {
- // message acknowledged. don't keep it
- } else if msg.command == "PING" && msg.params[0] == id {
- cl.latency = time.Since(msg.time)
- // ping acknowledged. don't keep it
- } else {
- keep = append(keep, msg)
- }
- }
- log.Printf(" acknowledged %d messages", len(cl.queue)-len(keep))
- cl.queue = keep
- log.Printf(" latency %s", cl.latency)
- return nil
- }
- func commandJoin(cl *client, req Message) error {
- channelName := req.params[0]
- if strings.Contains(channelName, ",") {
- for _, name := range strings.Split(channelName, ",") {
- redo := req
- redo.params[0] = name
- err := commandJoin(cl, redo)
- if err != nil {
- return err
- }
- }
- return nil
- }
- if !strings.HasPrefix(channelName, "#") {
- return errors.New("TODO send invalid group error to client")
- }
- // notify channel about the new member
- msg := NewMessage("JOIN", channelName).From(cl)
- members := ircServer.Clients().Tagged(channelName)
- if len(members.Tagged(cl.nick)) == 0 {
- // nick not yet in channel
- members.Send(msg)
- }
- // this client now belongs to this channel
- cl.setTag(channelName)
- cl.Send(msg)
- // provide channel details to the new participant
- nicks := members.UniqueNicks(cl.nick)
- cl.send("332", cl.nick, channelName, "Discuss "+channelName)
- cl.send("353", cl.nick, "=", channelName, strings.Join(nicks, " "))
- cl.send("366", cl.nick, channelName, "End of /NAMES list")
- cl.resend(channelName)
- return nil
- }
- func commandPrivmsg(cl *client, req Message) error {
- target := req.params[0]
- text := req.params[1]
- if text == "" {
- return cl.send("412", cl.nick, "No text to send")
- }
- // who gets a copy of the message?
- recipients := ircServer.Clients().Tagged(target)
- if len(recipients) == 0 {
- if phone, ok := ParsePhone(target); ok && cl.nick == "michael" {
- go SmsSend(req.From(cl), phone, text)
- return nil
- } else {
- return cl.send("401", cl.nick, target, "No such nick/channel")
- }
- }
- recipients = recipients.Except(cl)
- // send the actual message
- res := NewMessage("PRIVMSG", target, text).From(cl)
- err := recipients.Send(res)
- if err != nil {
- return err
- }
- // conclude with a ping to help guess whether the PRIVMSG got through
- for c := range recipients {
- go c.ping(res.id)
- }
- return nil
- }
- func commandWhois(cl *client, msg Message) error {
- nick := msg.params[0]
- found := ircServer.Clients().Tagged(nick).First()
- if found == nil {
- cl.send("401", cl.nick, nick, "No such nick/channel")
- return nil
- }
- // calculate useful details about the user
- idle := fmt.Sprintf("%.0f", time.Since(found.active).Seconds()+1)
- latency := found.latency.Round(10 * time.Millisecond).String()
- if found.latency.Seconds() > 1 {
- latency = found.latency.Round(100 * time.Millisecond).String()
- }
- if found.latency < 0 {
- latency = "unreachable"
- }
- channels := make([]string, 0, len(found.tags))
- for tag := range found.tags {
- if strings.HasPrefix(tag, "#") {
- channels = append(channels, tag)
- }
- }
- // send response
- cl.send("311", cl.nick, nick, found.device, serverHostname, "*", found.name)
- cl.send("312", cl.nick, nick, serverHostname, "ping "+latency)
- cl.send("317", cl.nick, nick, idle, "seconds idle")
- cl.send("319", cl.nick, nick, strings.Join(channels, " "))
- cl.send("318", cl.nick, nick, "End of /WHOIS list")
- // update latency info for target of WHOIS
- if target := ircServer.Clients().Tagged(nick).First(); target != nil {
- target.ping(serverHostname)
- }
- return nil
- }
- func (cls Clients) Keep(f func(*client) bool) Clients {
- out := make(Clients, len(cls))
- for cl := range cls {
- if f(cl) {
- out[cl] = true
- }
- }
- return out
- }
- func (cls Clients) Except(drop *client) Clients {
- return cls.Keep(func(cl *client) bool { return cl != drop })
- }
- func (cls Clients) First() *client {
- for cl := range cls {
- return cl
- }
- return nil
- }
- func (cls Clients) Tagged(tag string) Clients {
- return cls.Keep(func(cl *client) bool { return cl.hasTag(tag) })
- }
- func (recipients Clients) Send(msg Message) (err error) {
- // start all deliveries
- errCh := make(chan error)
- for recipient := range recipients {
- go func(r *client) { errCh <- r.Send(msg) }(recipient)
- }
- // wait for them all to conclude
- outstanding := len(recipients)
- for outstanding > 0 {
- if e := <-errCh; e != nil && err == nil {
- err = e
- }
- outstanding--
- }
- return
- }
- func (cls Clients) UniqueNicks(nicks ...string) []string {
- seen := make(map[string]bool, len(cls)+len(nicks))
- for _, nick := range nicks {
- seen[nick] = true
- }
- for cl := range cls {
- if !seen[cl.nick] {
- nicks = append(nicks, cl.nick)
- seen[cl.nick] = true
- }
- }
- return nicks
- }
- func NewMessage(command string, params ...string) Message {
- return Message{
- id: strconv.FormatInt(rand.Int63(), 36),
- command: command,
- params: params,
- time: time.Now(),
- }
- }
- // returns a message and the number of bytes consumed, or 0 if a full
- // line is not present
- func ParseMessage(data []byte) (msg Message, i int) {
- // extract the first complete line
- i = bytes.Index(data, []byte("\n"))
- if i < 0 {
- i = 0
- return
- }
- data = bytes.TrimRight(data[0:i], "\r")
- msg.line = string(data)
- i++
- // extract the command and other parameters
- words := bytes.Split(data, []byte{' '})
- for n, word := range words {
- if msg.command == "" {
- msg.command = string(word)
- continue
- }
- if len(word) == 0 { // bad client
- continue
- }
- if word[0] == ':' { // reassemble final param
- word = bytes.Join(words[n:], []byte{' '})
- msg.params = append(msg.params, string(word[1:]))
- break
- }
- msg.params = append(msg.params, string(word))
- }
- return
- }
- func (msg Message) String() string {
- parts := make([]string, 0, 2+len(msg.params))
- if msg.command == "PING" || msg.omitPrefix {
- // prefix must be absent
- } else if cl := msg.from; cl == nil {
- parts = append(parts, ":"+serverHostname)
- } else {
- prefix := fmt.Sprintf(":%s!%s@%s", cl.nick, cl.device, serverHostname)
- parts = append(parts, prefix)
- }
- parts = append(parts, msg.command)
- if len(msg.params) > 0 {
- parts = append(parts, msg.params...)
- parts[len(parts)-1] = ":" + parts[len(parts)-1]
- }
- return strings.Join(parts, " ") + "\r\n"
- }
- func (msg Message) From(cl *client) Message {
- msg.from = cl
- return msg
- }
Add Comment
Please, Sign In to add comment