]> Cypherpunks.ru repositories - goircd.git/blobdiff - client.go
Many fixes and additions
[goircd.git] / client.go
index 2c8c0ad6721fb04a828029329a0ab607d74beef3..6a21ab0ce7d415edb05bf3f062f4546e010c23e4 100644 (file)
--- a/client.go
+++ b/client.go
@@ -19,8 +19,15 @@ package main
 
 import (
        "bytes"
+       "crypto/sha256"
+       "crypto/subtle"
+       "encoding/hex"
+       "fmt"
+       "io/ioutil"
        "log"
        "net"
+       "regexp"
+       "sort"
        "strings"
        "sync"
        "time"
@@ -28,29 +35,37 @@ import (
 
 const (
        BufSize   = 1500
-       MaxOutBuf = 1 << 12
+       MaxOutBuf = 128
 )
 
 var (
-       CRLF []byte = []byte{'\x0d', '\x0a'}
+       CRLF       []byte = []byte{'\x0d', '\x0a'}
+       RENickname        = regexp.MustCompile("^[a-zA-Z0-9-]{1,24}$")
+
+       clients     map[*Client]struct{} = make(map[*Client]struct{})
+       clientsLock sync.RWMutex
+       clientsWG   sync.WaitGroup
 )
 
 type Client struct {
        conn          net.Conn
        registered    bool
-       nickname      *string
-       username      *string
-       realname      *string
-       password      *string
-       away          *string
+       nickname      string
+       username      string
+       realname      string
+       password      string
+       away          string
        recvTimestamp time.Time
        sendTimestamp time.Time
-       outBuf        chan *string
+       outBuf        chan string
        alive         bool
        sync.Mutex
 }
 
 func (c *Client) Host() string {
+       if *cloak != "" {
+               return *cloak
+       }
        addr := c.conn.RemoteAddr().String()
        if host, _, err := net.SplitHostPort(addr); err == nil {
                addr = host
@@ -62,48 +77,42 @@ func (c *Client) Host() string {
 }
 
 func (c *Client) String() string {
-       return *c.nickname + "!" + *c.username + "@" + c.Host()
+       return strings.Join([]string{c.nickname, "!", c.username, "@", c.Host()}, "")
 }
 
-func NewClient(conn net.Conn) *Client {
-       nickname := "*"
-       username := ""
+func NewClient(conn net.Conn, events chan ClientEvent) *Client {
        c := Client{
                conn:          conn,
-               nickname:      &nickname,
-               username:      &username,
+               nickname:      "*",
+               username:      "",
                recvTimestamp: time.Now(),
                sendTimestamp: time.Now(),
                alive:         true,
-               outBuf:        make(chan *string, MaxOutBuf),
+               outBuf:        make(chan string, MaxOutBuf),
        }
+       clientsWG.Add(2)
        go c.MsgSender()
+       go c.Processor(events)
        return &c
 }
 
-func (c *Client) SetDead() {
-       c.outBuf <- nil
-       c.alive = false
-}
-
 func (c *Client) Close() {
        c.Lock()
        if c.alive {
-               c.SetDead()
+               close(c.outBuf)
+               c.alive = false
        }
        c.Unlock()
 }
 
-// Client processor blockingly reads everything remote client sends,
-// splits messages by CRLF and send them to Daemon gorouting for processing
-// it futher. Also it can signalize that client is unavailable (disconnected).
-func (c *Client) Processor(sink chan ClientEvent) {
-       sink <- ClientEvent{c, EventNew, ""}
-       log.Println(c, "New client")
+func (c *Client) Processor(events chan ClientEvent) {
+       events <- ClientEvent{c, EventNew, ""}
+       if *verbose {
+               log.Println(c, "connected")
+       }
        buf := make([]byte, BufSize*2)
-       var n int
-       var prev int
-       var i int
+       var n, prev, i int
+       var msg string
        var err error
        for {
                if prev == BufSize {
@@ -120,26 +129,43 @@ func (c *Client) Processor(sink chan ClientEvent) {
                if i == -1 {
                        continue
                }
-               sink <- ClientEvent{c, EventMsg, string(buf[:i])}
+               if *debug {
+                       log.Println(c, "<-", msg)
+               }
+               msg = string(buf[:i])
+               if *debug {
+                       log.Println(c, "->", msg)
+               }
+               events <- ClientEvent{c, EventMsg, msg}
                copy(buf, buf[i+2:prev])
                prev -= (i + 2)
                goto CheckMore
        }
        c.Close()
-       sink <- ClientEvent{c, EventDel, ""}
+       if *verbose {
+               log.Println(c, "disconnected")
+       }
+       events <- ClientEvent{c, EventDel, ""}
+       clientsWG.Done()
 }
 
 func (c *Client) MsgSender() {
+       var err error
        for msg := range c.outBuf {
-               if msg == nil {
-                       c.conn.Close()
+               if *debug {
+                       log.Println(c, "<-", msg)
+               }
+               if _, err = c.conn.Write(append([]byte(msg), CRLF...)); err != nil {
+                       if *verbose {
+                               log.Println(c, "error writing", err)
+                       }
                        break
                }
-               c.conn.Write(append([]byte(*msg), CRLF...))
        }
+       c.conn.Close()
+       clientsWG.Done()
 }
 
-// Send message as is with CRLF appended.
 func (c *Client) Msg(text string) {
        c.Lock()
        defer c.Unlock()
@@ -149,20 +175,18 @@ func (c *Client) Msg(text string) {
        if len(c.outBuf) == MaxOutBuf {
                log.Println(c, "output buffer size exceeded, kicking him")
                if c.alive {
-                       c.SetDead()
+                       close(c.outBuf)
+                       c.alive = false
                }
                return
        }
-       c.outBuf <- &text
+       c.outBuf <- text
 }
 
-// Send message from server. It has ": servername" prefix.
 func (c *Client) Reply(text string) {
        c.Msg(":" + *hostname + " " + text)
 }
 
-// Send server message, concatenating all provided text parts and
-// prefix the last one with ":".
 func (c *Client) ReplyParts(code string, text ...string) {
        parts := []string{code}
        for _, t := range text {
@@ -172,18 +196,14 @@ func (c *Client) ReplyParts(code string, text ...string) {
        c.Reply(strings.Join(parts, " "))
 }
 
-// Send nicknamed server message. After servername it always has target
-// client's nickname. The last part is prefixed with ":".
 func (c *Client) ReplyNicknamed(code string, text ...string) {
-       c.ReplyParts(code, append([]string{*c.nickname}, text...)...)
+       c.ReplyParts(code, append([]string{c.nickname}, text...)...)
 }
 
-// Reply "461 not enough parameters" error for given command.
 func (c *Client) ReplyNotEnoughParameters(command string) {
        c.ReplyNicknamed("461", command, "Not enough parameters")
 }
 
-// Reply "403 no such channel" error for specified channel.
 func (c *Client) ReplyNoChannel(channel string) {
        c.ReplyNicknamed("403", channel, "No such channel")
 }
@@ -191,3 +211,252 @@ func (c *Client) ReplyNoChannel(channel string) {
 func (c *Client) ReplyNoNickChan(channel string) {
        c.ReplyNicknamed("401", channel, "No such nick/channel")
 }
+
+func (c *Client) SendLusers() {
+       lusers := 0
+       clientsLock.RLock()
+       for client := range clients {
+               if client.registered {
+                       lusers++
+               }
+       }
+       clientsLock.RUnlock()
+       c.ReplyNicknamed(
+               "251",
+               fmt.Sprintf("There are %d users and 0 invisible on 1 servers",
+                       lusers,
+               ))
+}
+
+func (c *Client) SendMotd() {
+       if motd == nil {
+               c.ReplyNicknamed("422", "MOTD File is missing")
+               return
+       }
+       motdText, err := ioutil.ReadFile(*motd)
+       if err != nil {
+               log.Printf("can not read motd file %s: %v", *motd, err)
+               c.ReplyNicknamed("422", "Error reading MOTD File")
+               return
+       }
+       c.ReplyNicknamed("375", "- "+*hostname+" Message of the day -")
+       for _, s := range strings.Split(strings.TrimSuffix(string(motdText), "\n"), "\n") {
+               c.ReplyNicknamed("372", "- "+s)
+       }
+       c.ReplyNicknamed("376", "End of /MOTD command")
+}
+
+func (c *Client) Join(cmd string) {
+       args := strings.Split(cmd, " ")
+       rs := strings.Split(args[0], ",")
+       keys := []string{}
+       if len(args) > 1 {
+               keys = strings.Split(args[1], ",")
+       }
+RoomCycle:
+       for n, roomName := range rs {
+               if !RERoom.MatchString(roomName) {
+                       c.ReplyNoChannel(roomName)
+                       continue
+               }
+               var key string
+               if (n < len(keys)) && (keys[n] != "") {
+                       key = keys[n]
+               }
+               roomsLock.RLock()
+               for roomNameExisting, room := range rooms {
+                       if roomName != roomNameExisting {
+                               continue
+                       }
+                       if (room.key != "") && (room.key != key) {
+                               c.ReplyNicknamed("475", roomName, "Cannot join channel (+k)")
+                               roomsLock.RUnlock()
+                               return
+                       }
+                       room.events <- ClientEvent{c, EventNew, ""}
+                       roomsLock.RUnlock()
+                       continue RoomCycle
+               }
+               roomsLock.RUnlock()
+               roomNew := RoomRegister(roomName)
+               if *verbose {
+                       log.Println("room", roomName, "created")
+               }
+               if key != "" {
+                       roomNew.key = key
+                       roomNew.StateSave()
+               }
+               roomNew.events <- ClientEvent{c, EventNew, ""}
+       }
+}
+
+func (client *Client) SendWhois(nicknames []string) {
+       var c *Client
+       for _, nickname := range nicknames {
+               nickname = strings.ToLower(nickname)
+               clientsLock.RLock()
+               for c = range clients {
+                       if strings.ToLower(c.nickname) == nickname {
+                               clientsLock.RUnlock()
+                               goto Found
+                       }
+               }
+               clientsLock.RUnlock()
+               client.ReplyNoNickChan(nickname)
+               continue
+       Found:
+               var host string
+               if *cloak != "" {
+                       host = *cloak
+               } else {
+                       host, _, err := net.SplitHostPort(c.conn.RemoteAddr().String())
+                       if err != nil {
+                               log.Printf("can't parse RemoteAddr %q: %v", host, err)
+                               host = "Unknown"
+                       }
+               }
+               client.ReplyNicknamed("311", c.nickname, c.username, host, "*", c.realname)
+               client.ReplyNicknamed("312", c.nickname, *hostname, *hostname)
+               if c.away != "" {
+                       client.ReplyNicknamed("301", c.nickname, c.away)
+               }
+               subscriptions := make([]string, 0)
+               roomsLock.RLock()
+               for _, room := range rooms {
+                       for subscriber := range room.members {
+                               if subscriber.nickname == nickname {
+                                       subscriptions = append(subscriptions, room.name)
+                               }
+                       }
+               }
+               roomsLock.RUnlock()
+               sort.Strings(subscriptions)
+               client.ReplyNicknamed("319", c.nickname, strings.Join(subscriptions, " "))
+               client.ReplyNicknamed("318", c.nickname, "End of /WHOIS list")
+       }
+}
+
+func (c *Client) SendList(cols []string) {
+       var rs []string
+       if (len(cols) > 1) && (cols[1] != "") {
+               rs = strings.Split(strings.Split(cols[1], " ")[0], ",")
+       } else {
+               rs = make([]string, 0)
+               roomsLock.RLock()
+               for r := range rooms {
+                       rs = append(rs, r)
+               }
+               roomsLock.RUnlock()
+       }
+       sort.Strings(rs)
+       roomsLock.RLock()
+       for _, r := range rs {
+               if room, found := rooms[r]; found {
+                       c.ReplyNicknamed(
+                               "322",
+                               r,
+                               fmt.Sprintf("%d", len(room.members)),
+                               room.topic,
+                       )
+               }
+       }
+       roomsLock.RUnlock()
+       c.ReplyNicknamed("323", "End of /LIST")
+}
+
+func (c *Client) Register(cmd string, cols []string) {
+       switch cmd {
+       case "PASS":
+               if len(cols) == 1 || len(cols[1]) < 1 {
+                       c.ReplyNotEnoughParameters("PASS")
+                       return
+               }
+               password := strings.TrimPrefix(cols[1], ":")
+               c.password = password
+       case "NICK":
+               if len(cols) == 1 || len(cols[1]) < 1 {
+                       c.ReplyParts("431", "No nickname given")
+                       return
+               }
+               nickname := cols[1]
+               // Compatibility with some clients prepending colons to nickname
+               nickname = strings.TrimPrefix(nickname, ":")
+               nickname = strings.ToLower(nickname)
+               if !RENickname.MatchString(nickname) {
+                       c.ReplyParts("432", "*", cols[1], "Erroneous nickname")
+                       return
+               }
+               clientsLock.RLock()
+               for existingClient := range clients {
+                       if existingClient.nickname == nickname {
+                               clientsLock.RUnlock()
+                               c.ReplyParts("433", "*", nickname, "Nickname is already in use")
+                               return
+                       }
+               }
+               clientsLock.RUnlock()
+               c.nickname = nickname
+       case "USER":
+               if len(cols) == 1 {
+                       c.ReplyNotEnoughParameters("USER")
+                       return
+               }
+               args := strings.SplitN(cols[1], " ", 4)
+               if len(args) < 4 {
+                       c.ReplyNotEnoughParameters("USER")
+                       return
+               }
+               c.username = args[0]
+               realname := strings.TrimLeft(args[3], ":")
+               c.realname = realname
+       }
+       if c.nickname != "*" && c.username != "" {
+               if *passwords != "" {
+                       authenticated := false
+                       if c.password == "" {
+                               c.ReplyParts("462", "You may not register")
+                               c.Close()
+                               return
+                       }
+                       contents, err := ioutil.ReadFile(*passwords)
+                       if err != nil {
+                               log.Fatalf("can not read passwords file %s: %s", *passwords, err)
+                               return
+                       }
+                       for n, entry := range strings.Split(string(contents), "\n") {
+                               if entry == "" || strings.HasPrefix(entry, "#") {
+                                       continue
+                               }
+                               cols := strings.Split(entry, ":")
+                               if len(cols) != 2 {
+                                       log.Fatalf("bad passwords format: %s:%d", *passwords, n)
+                                       continue
+                               }
+                               if cols[0] != c.nickname {
+                                       continue
+                               }
+                               h := sha256.Sum256([]byte(c.password))
+                               authenticated = subtle.ConstantTimeCompare(
+                                       []byte(hex.EncodeToString(h[:])),
+                                       []byte(cols[1]),
+                               ) == 1
+                               break
+                       }
+                       if !authenticated {
+                               c.ReplyParts("462", "You may not register")
+                               c.Close()
+                               return
+                       }
+               }
+               c.registered = true
+               c.ReplyNicknamed("001", "Hi, welcome to IRC")
+               c.ReplyNicknamed("002", "Your host is "+*hostname+", running goircd "+Version)
+               c.ReplyNicknamed("003", "This server was created sometime")
+               c.ReplyNicknamed("004", *hostname+" goircd o o")
+               c.SendLusers()
+               c.SendMotd()
+               if *verbose {
+                       log.Println(c, "logged in")
+               }
+       }
+}