]> Cypherpunks.ru repositories - goircd.git/blob - client.go
6e82e1409794db633ba4eca3b27cf7c31994cc9f
[goircd.git] / client.go
1 /*
2 goircd -- minimalistic simple Internet Relay Chat (IRC) server
3 Copyright (C) 2014-2022 Sergey Matveev <stargrave@stargrave.org>
4
5 This program is free software: you can redistribute it and/or modify
6 it under the terms of the GNU General Public License as published by
7 the Free Software Foundation, version 3 of the License.
8
9 This program is distributed in the hope that it will be useful,
10 but WITHOUT ANY WARRANTY; without even the implied warranty of
11 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 GNU General Public License for more details.
13
14 You should have received a copy of the GNU General Public License
15 along with this program.  If not, see <http://www.gnu.org/licenses/>.
16 */
17
18 package main
19
20 import (
21         "bytes"
22         "crypto/sha256"
23         "crypto/subtle"
24         "encoding/hex"
25         "fmt"
26         "io/ioutil"
27         "log"
28         "net"
29         "regexp"
30         "sort"
31         "strings"
32         "sync"
33         "time"
34 )
35
36 const (
37         BufSize   = 1500
38         MaxOutBuf = 128
39 )
40
41 var (
42         CRLF       []byte = []byte{'\x0d', '\x0a'}
43         RENickname        = regexp.MustCompile("^[a-zA-Z0-9-]{1,24}$")
44
45         clients     map[*Client]struct{} = make(map[*Client]struct{})
46         clientsLock sync.RWMutex
47         clientsWG   sync.WaitGroup
48 )
49
50 type Client struct {
51         conn          net.Conn
52         registered    bool
53         nickname      string
54         username      string
55         realname      string
56         password      string
57         away          string
58         recvTimestamp time.Time
59         sendTimestamp time.Time
60         outBuf        chan string
61         alive         bool
62         sync.Mutex
63 }
64
65 func (c *Client) Host() string {
66         if *cloak != "" {
67                 return *cloak
68         }
69         addr := c.conn.RemoteAddr().String()
70         if host, _, err := net.SplitHostPort(addr); err == nil {
71                 addr = host
72         }
73         if domains, err := net.LookupAddr(addr); err == nil {
74                 addr = strings.TrimSuffix(domains[0], ".")
75         }
76         return addr
77 }
78
79 func (c *Client) String() string {
80         return strings.Join([]string{c.nickname, "!", c.username, "@", c.Host()}, "")
81 }
82
83 func NewClient(conn net.Conn, events chan ClientEvent) *Client {
84         c := Client{
85                 conn:          conn,
86                 nickname:      "*",
87                 username:      "",
88                 recvTimestamp: time.Now(),
89                 sendTimestamp: time.Now(),
90                 alive:         true,
91                 outBuf:        make(chan string, MaxOutBuf),
92         }
93         clientsWG.Add(2)
94         go c.MsgSender()
95         go c.Processor(events)
96         return &c
97 }
98
99 func (c *Client) Close() {
100         c.Lock()
101         if c.alive {
102                 close(c.outBuf)
103                 c.alive = false
104         }
105         c.Unlock()
106 }
107
108 func (c *Client) Processor(events chan ClientEvent) {
109         events <- ClientEvent{c, EventNew, ""}
110         if *verbose {
111                 log.Println(c, "connected")
112         }
113         buf := make([]byte, BufSize*2)
114         var n, prev, i int
115         var msg string
116         var err error
117         for {
118                 if prev == BufSize {
119                         log.Println(c, "input buffer size exceeded, kicking him")
120                         break
121                 }
122                 n, err = c.conn.Read(buf[prev:])
123                 if err != nil {
124                         break
125                 }
126                 prev += n
127         CheckMore:
128                 i = bytes.Index(buf[:prev], CRLF)
129                 if i == -1 {
130                         continue
131                 }
132                 if *debug {
133                         log.Println(c, "<-", msg)
134                 }
135                 msg = string(buf[:i])
136                 if *debug {
137                         log.Println(c, "->", msg)
138                 }
139                 events <- ClientEvent{c, EventMsg, msg}
140                 copy(buf, buf[i+2:prev])
141                 prev -= (i + 2)
142                 goto CheckMore
143         }
144         c.Close()
145         if *verbose {
146                 log.Println(c, "disconnected")
147         }
148         events <- ClientEvent{c, EventDel, ""}
149         clientsWG.Done()
150 }
151
152 func (c *Client) MsgSender() {
153         var err error
154         for msg := range c.outBuf {
155                 if *debug {
156                         log.Println(c, "<-", msg)
157                 }
158                 if _, err = c.conn.Write(append([]byte(msg), CRLF...)); err != nil {
159                         if *verbose {
160                                 log.Println(c, "error writing", err)
161                         }
162                         break
163                 }
164         }
165         c.conn.Close()
166         clientsWG.Done()
167 }
168
169 func (c *Client) Msg(text string) {
170         c.Lock()
171         defer c.Unlock()
172         if !c.alive {
173                 return
174         }
175         if len(c.outBuf) == MaxOutBuf {
176                 log.Println(c, "output buffer size exceeded, kicking him")
177                 if c.alive {
178                         close(c.outBuf)
179                         c.alive = false
180                 }
181                 return
182         }
183         c.outBuf <- text
184 }
185
186 func (c *Client) Reply(text string) {
187         c.Msg(":" + *hostname + " " + text)
188 }
189
190 func (c *Client) ReplyParts(code string, text ...string) {
191         parts := []string{code}
192         for _, t := range text {
193                 parts = append(parts, t)
194         }
195         parts[len(parts)-1] = ":" + parts[len(parts)-1]
196         c.Reply(strings.Join(parts, " "))
197 }
198
199 func (c *Client) ReplyNicknamed(code string, text ...string) {
200         c.ReplyParts(code, append([]string{c.nickname}, text...)...)
201 }
202
203 func (c *Client) ReplyNotEnoughParameters(command string) {
204         c.ReplyNicknamed("461", command, "Not enough parameters")
205 }
206
207 func (c *Client) ReplyNoChannel(channel string) {
208         c.ReplyNicknamed("403", channel, "No such channel")
209 }
210
211 func (c *Client) ReplyNoNickChan(channel string) {
212         c.ReplyNicknamed("401", channel, "No such nick/channel")
213 }
214
215 func (c *Client) SendLusers() {
216         lusers := 0
217         clientsLock.RLock()
218         for client := range clients {
219                 if client.registered {
220                         lusers++
221                 }
222         }
223         clientsLock.RUnlock()
224         c.ReplyNicknamed(
225                 "251",
226                 fmt.Sprintf("There are %d users and 0 invisible on 1 servers",
227                         lusers,
228                 ))
229 }
230
231 func (c *Client) SendMotd() {
232         if motd == nil {
233                 c.ReplyNicknamed("422", "MOTD File is missing")
234                 return
235         }
236         motdText, err := ioutil.ReadFile(*motd)
237         if err != nil {
238                 log.Printf("can not read motd file %s: %v", *motd, err)
239                 c.ReplyNicknamed("422", "Error reading MOTD File")
240                 return
241         }
242         c.ReplyNicknamed("375", "- "+*hostname+" Message of the day -")
243         for _, s := range strings.Split(strings.TrimSuffix(string(motdText), "\n"), "\n") {
244                 c.ReplyNicknamed("372", "- "+s)
245         }
246         c.ReplyNicknamed("376", "End of /MOTD command")
247 }
248
249 func (c *Client) Join(cmd string) {
250         args := strings.Split(cmd, " ")
251         rs := strings.Split(args[0], ",")
252         keys := []string{}
253         if len(args) > 1 {
254                 keys = strings.Split(args[1], ",")
255         }
256 RoomCycle:
257         for n, roomName := range rs {
258                 if !RERoom.MatchString(roomName) {
259                         c.ReplyNoChannel(roomName)
260                         continue
261                 }
262                 var key string
263                 if (n < len(keys)) && (keys[n] != "") {
264                         key = keys[n]
265                 }
266                 roomsLock.RLock()
267                 for roomNameExisting, room := range rooms {
268                         if roomName != roomNameExisting {
269                                 continue
270                         }
271                         if (room.key != "") && (room.key != key) {
272                                 c.ReplyNicknamed("475", roomName, "Cannot join channel (+k)")
273                                 roomsLock.RUnlock()
274                                 return
275                         }
276                         room.events <- ClientEvent{c, EventNew, ""}
277                         roomsLock.RUnlock()
278                         continue RoomCycle
279                 }
280                 roomsLock.RUnlock()
281                 roomNew := RoomRegister(roomName)
282                 if *verbose {
283                         log.Println("room", roomName, "created")
284                 }
285                 if key != "" {
286                         roomNew.key = key
287                         roomNew.StateSave()
288                 }
289                 roomNew.events <- ClientEvent{c, EventNew, ""}
290         }
291 }
292
293 func (client *Client) SendWhois(nicknames []string) {
294         var c *Client
295         for _, nickname := range nicknames {
296                 nickname = strings.ToLower(nickname)
297                 clientsLock.RLock()
298                 for c = range clients {
299                         if strings.ToLower(c.nickname) == nickname {
300                                 clientsLock.RUnlock()
301                                 goto Found
302                         }
303                 }
304                 clientsLock.RUnlock()
305                 client.ReplyNoNickChan(nickname)
306                 continue
307         Found:
308                 var host string
309                 if *cloak != "" {
310                         host = *cloak
311                 } else {
312                         host, _, err := net.SplitHostPort(c.conn.RemoteAddr().String())
313                         if err != nil {
314                                 log.Printf("can't parse RemoteAddr %q: %v", host, err)
315                                 host = "Unknown"
316                         }
317                 }
318                 client.ReplyNicknamed("311", c.nickname, c.username, host, "*", c.realname)
319                 client.ReplyNicknamed("312", c.nickname, *hostname, *hostname)
320                 if c.away != "" {
321                         client.ReplyNicknamed("301", c.nickname, c.away)
322                 }
323                 subscriptions := make([]string, 0)
324                 roomsLock.RLock()
325                 for _, room := range rooms {
326                         for subscriber := range room.members {
327                                 if subscriber.nickname == nickname {
328                                         subscriptions = append(subscriptions, room.name)
329                                 }
330                         }
331                 }
332                 roomsLock.RUnlock()
333                 sort.Strings(subscriptions)
334                 client.ReplyNicknamed("319", c.nickname, strings.Join(subscriptions, " "))
335                 client.ReplyNicknamed("318", c.nickname, "End of /WHOIS list")
336         }
337 }
338
339 func (c *Client) SendList(cols []string) {
340         var rs []string
341         if (len(cols) > 1) && (cols[1] != "") {
342                 rs = strings.Split(strings.Split(cols[1], " ")[0], ",")
343         } else {
344                 rs = make([]string, 0)
345                 roomsLock.RLock()
346                 for r := range rooms {
347                         rs = append(rs, r)
348                 }
349                 roomsLock.RUnlock()
350         }
351         sort.Strings(rs)
352         roomsLock.RLock()
353         for _, r := range rs {
354                 if room, found := rooms[r]; found {
355                         c.ReplyNicknamed(
356                                 "322",
357                                 r,
358                                 fmt.Sprintf("%d", len(room.members)),
359                                 room.topic,
360                         )
361                 }
362         }
363         roomsLock.RUnlock()
364         c.ReplyNicknamed("323", "End of /LIST")
365 }
366
367 func (c *Client) Register(cmd string, cols []string) {
368         switch cmd {
369         case "PASS":
370                 if len(cols) == 1 || len(cols[1]) < 1 {
371                         c.ReplyNotEnoughParameters("PASS")
372                         return
373                 }
374                 password := strings.TrimPrefix(cols[1], ":")
375                 c.password = password
376         case "NICK":
377                 if len(cols) == 1 || len(cols[1]) < 1 {
378                         c.ReplyParts("431", "No nickname given")
379                         return
380                 }
381                 nickname := cols[1]
382                 // Compatibility with some clients prepending colons to nickname
383                 nickname = strings.TrimPrefix(nickname, ":")
384                 nickname = strings.ToLower(nickname)
385                 if !RENickname.MatchString(nickname) {
386                         c.ReplyParts("432", "*", cols[1], "Erroneous nickname")
387                         return
388                 }
389                 clientsLock.RLock()
390                 for existingClient := range clients {
391                         if existingClient.nickname == nickname {
392                                 clientsLock.RUnlock()
393                                 c.ReplyParts("433", "*", nickname, "Nickname is already in use")
394                                 return
395                         }
396                 }
397                 clientsLock.RUnlock()
398                 c.nickname = nickname
399         case "USER":
400                 if len(cols) == 1 {
401                         c.ReplyNotEnoughParameters("USER")
402                         return
403                 }
404                 args := strings.SplitN(cols[1], " ", 4)
405                 if len(args) < 4 {
406                         c.ReplyNotEnoughParameters("USER")
407                         return
408                 }
409                 c.username = args[0]
410                 realname := strings.TrimLeft(args[3], ":")
411                 c.realname = realname
412         }
413         if c.nickname != "*" && c.username != "" {
414                 if *passwords != "" {
415                         authenticated := false
416                         if c.password == "" {
417                                 c.ReplyParts("462", "You may not register")
418                                 c.Close()
419                                 return
420                         }
421                         contents, err := ioutil.ReadFile(*passwords)
422                         if err != nil {
423                                 log.Fatalf("can not read passwords file %s: %s", *passwords, err)
424                                 return
425                         }
426                         for n, entry := range strings.Split(string(contents), "\n") {
427                                 if entry == "" || strings.HasPrefix(entry, "#") {
428                                         continue
429                                 }
430                                 cols := strings.Split(entry, ":")
431                                 if len(cols) != 2 {
432                                         log.Fatalf("bad passwords format: %s:%d", *passwords, n)
433                                         continue
434                                 }
435                                 if cols[0] != c.nickname {
436                                         continue
437                                 }
438                                 h := sha256.Sum256([]byte(c.password))
439                                 authenticated = subtle.ConstantTimeCompare(
440                                         []byte(hex.EncodeToString(h[:])),
441                                         []byte(cols[1]),
442                                 ) == 1
443                                 break
444                         }
445                         if !authenticated {
446                                 c.ReplyParts("462", "You may not register")
447                                 c.Close()
448                                 return
449                         }
450                 }
451                 c.registered = true
452                 c.ReplyNicknamed("001", "Hi, welcome to IRC")
453                 c.ReplyNicknamed("002", "Your host is "+*hostname+", running goircd "+Version)
454                 c.ReplyNicknamed("003", "This server was created sometime")
455                 c.ReplyNicknamed("004", *hostname+" goircd o o")
456                 c.SendLusers()
457                 c.SendMotd()
458                 if *verbose {
459                         log.Println(c, "logged in")
460                 }
461         }
462 }