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