]> Cypherpunks.ru repositories - goircd.git/blob - client.go
e2bc5b212ad22d61b08cab8930211c5d9d3b69d5
[goircd.git] / client.go
1 /*
2 goircd -- minimalistic simple Internet Relay Chat (IRC) server
3 Copyright (C) 2014-2023 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         "log"
27         "net"
28         "os"
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 := append([]string{code}, text...)
192         parts[len(parts)-1] = ":" + parts[len(parts)-1]
193         c.Reply(strings.Join(parts, " "))
194 }
195
196 func (c *Client) ReplyNicknamed(code string, text ...string) {
197         c.ReplyParts(code, append([]string{c.nickname}, text...)...)
198 }
199
200 func (c *Client) ReplyNotEnoughParameters(command string) {
201         c.ReplyNicknamed("461", command, "Not enough parameters")
202 }
203
204 func (c *Client) ReplyNoChannel(channel string) {
205         c.ReplyNicknamed("403", channel, "No such channel")
206 }
207
208 func (c *Client) ReplyNoNickChan(channel string) {
209         c.ReplyNicknamed("401", channel, "No such nick/channel")
210 }
211
212 func (c *Client) SendLusers() {
213         lusers := 0
214         clientsLock.RLock()
215         for client := range clients {
216                 if client.registered {
217                         lusers++
218                 }
219         }
220         clientsLock.RUnlock()
221         c.ReplyNicknamed(
222                 "251",
223                 fmt.Sprintf("There are %d users and 0 invisible on 1 servers",
224                         lusers,
225                 ))
226 }
227
228 func (c *Client) SendMotd() {
229         if motd == nil {
230                 c.ReplyNicknamed("422", "MOTD File is missing")
231                 return
232         }
233         motdText, err := os.ReadFile(*motd)
234         if err != nil {
235                 log.Printf("can not read motd file %s: %v", *motd, err)
236                 c.ReplyNicknamed("422", "Error reading MOTD File")
237                 return
238         }
239         c.ReplyNicknamed("375", "- "+*hostname+" Message of the day -")
240         for _, s := range strings.Split(strings.TrimSuffix(string(motdText), "\n"), "\n") {
241                 c.ReplyNicknamed("372", "- "+s)
242         }
243         c.ReplyNicknamed("376", "End of /MOTD command")
244 }
245
246 func (c *Client) Join(cmd string) {
247         args := strings.Split(cmd, " ")
248         rs := strings.Split(args[0], ",")
249         keys := []string{}
250         if len(args) > 1 {
251                 keys = strings.Split(args[1], ",")
252         }
253 RoomCycle:
254         for n, roomName := range rs {
255                 if !RERoom.MatchString(roomName) {
256                         c.ReplyNoChannel(roomName)
257                         continue
258                 }
259                 var key string
260                 if (n < len(keys)) && (keys[n] != "") {
261                         key = keys[n]
262                 }
263                 roomsLock.RLock()
264                 for roomNameExisting, room := range rooms {
265                         if roomName != roomNameExisting {
266                                 continue
267                         }
268                         if (room.key != "") && (room.key != key) {
269                                 c.ReplyNicknamed("475", roomName, "Cannot join channel (+k)")
270                                 roomsLock.RUnlock()
271                                 return
272                         }
273                         room.events <- ClientEvent{c, EventNew, ""}
274                         roomsLock.RUnlock()
275                         continue RoomCycle
276                 }
277                 roomsLock.RUnlock()
278                 roomNew := RoomRegister(roomName)
279                 if *verbose {
280                         log.Println("room", roomName, "created")
281                 }
282                 if key != "" {
283                         roomNew.key = key
284                         roomNew.StateSave()
285                 }
286                 roomNew.events <- ClientEvent{c, EventNew, ""}
287         }
288 }
289
290 func (client *Client) SendWhois(nicknames []string) {
291         var c *Client
292         for _, nickname := range nicknames {
293                 nickname = strings.ToLower(nickname)
294                 clientsLock.RLock()
295                 for c = range clients {
296                         if strings.ToLower(c.nickname) == nickname {
297                                 clientsLock.RUnlock()
298                                 goto Found
299                         }
300                 }
301                 clientsLock.RUnlock()
302                 client.ReplyNoNickChan(nickname)
303                 continue
304         Found:
305                 var host string
306                 if *cloak != "" {
307                         host = *cloak
308                 } else {
309                         host, _, err := net.SplitHostPort(c.conn.RemoteAddr().String())
310                         if err != nil {
311                                 log.Printf("can't parse RemoteAddr %q: %v", host, err)
312                                 host = "Unknown"
313                         }
314                 }
315                 client.ReplyNicknamed("311", c.nickname, c.username, host, "*", c.realname)
316                 client.ReplyNicknamed("312", c.nickname, *hostname, *hostname)
317                 if c.away != "" {
318                         client.ReplyNicknamed("301", c.nickname, c.away)
319                 }
320                 subscriptions := make([]string, 0)
321                 roomsLock.RLock()
322                 for _, room := range rooms {
323                         for subscriber := range room.members {
324                                 if subscriber.nickname == nickname {
325                                         subscriptions = append(subscriptions, room.name)
326                                 }
327                         }
328                 }
329                 roomsLock.RUnlock()
330                 sort.Strings(subscriptions)
331                 client.ReplyNicknamed("319", c.nickname, strings.Join(subscriptions, " "))
332                 client.ReplyNicknamed("318", c.nickname, "End of /WHOIS list")
333         }
334 }
335
336 func (c *Client) SendList(cols []string) {
337         var rs []string
338         if (len(cols) > 1) && (cols[1] != "") {
339                 rs = strings.Split(strings.Split(cols[1], " ")[0], ",")
340         } else {
341                 rs = make([]string, 0)
342                 roomsLock.RLock()
343                 for r := range rooms {
344                         rs = append(rs, r)
345                 }
346                 roomsLock.RUnlock()
347         }
348         sort.Strings(rs)
349         roomsLock.RLock()
350         for _, r := range rs {
351                 if room, found := rooms[r]; found {
352                         c.ReplyNicknamed(
353                                 "322",
354                                 r,
355                                 fmt.Sprintf("%d", len(room.members)),
356                                 room.topic,
357                         )
358                 }
359         }
360         roomsLock.RUnlock()
361         c.ReplyNicknamed("323", "End of /LIST")
362 }
363
364 func (c *Client) Register(cmd string, cols []string) {
365         switch cmd {
366         case "PASS":
367                 if len(cols) == 1 || len(cols[1]) < 1 {
368                         c.ReplyNotEnoughParameters("PASS")
369                         return
370                 }
371                 password := strings.TrimPrefix(cols[1], ":")
372                 c.password = password
373         case "NICK":
374                 if len(cols) == 1 || len(cols[1]) < 1 {
375                         c.ReplyParts("431", "No nickname given")
376                         return
377                 }
378                 nickname := cols[1]
379                 // Compatibility with some clients prepending colons to nickname
380                 nickname = strings.TrimPrefix(nickname, ":")
381                 nickname = strings.ToLower(nickname)
382                 if !RENickname.MatchString(nickname) {
383                         c.ReplyParts("432", "*", cols[1], "Erroneous nickname")
384                         return
385                 }
386                 clientsLock.RLock()
387                 for existingClient := range clients {
388                         if existingClient.nickname == nickname {
389                                 clientsLock.RUnlock()
390                                 c.ReplyParts("433", "*", nickname, "Nickname is already in use")
391                                 return
392                         }
393                 }
394                 clientsLock.RUnlock()
395                 c.nickname = nickname
396         case "USER":
397                 if len(cols) == 1 {
398                         c.ReplyNotEnoughParameters("USER")
399                         return
400                 }
401                 args := strings.SplitN(cols[1], " ", 4)
402                 if len(args) < 4 {
403                         c.ReplyNotEnoughParameters("USER")
404                         return
405                 }
406                 c.username = args[0]
407                 realname := strings.TrimLeft(args[3], ":")
408                 c.realname = realname
409         }
410         if c.nickname != "*" && c.username != "" {
411                 if *passwords != "" {
412                         authenticated := false
413                         if c.password == "" {
414                                 c.ReplyParts("462", "You may not register")
415                                 c.Close()
416                                 return
417                         }
418                         contents, err := os.ReadFile(*passwords)
419                         if err != nil {
420                                 log.Fatalf("can not read passwords file %s: %s", *passwords, err)
421                                 return
422                         }
423                         for n, entry := range strings.Split(string(contents), "\n") {
424                                 if entry == "" || strings.HasPrefix(entry, "#") {
425                                         continue
426                                 }
427                                 cols := strings.Split(entry, ":")
428                                 if len(cols) != 2 {
429                                         log.Fatalf("bad passwords format: %s:%d", *passwords, n)
430                                         continue
431                                 }
432                                 if cols[0] != c.nickname {
433                                         continue
434                                 }
435                                 h := sha256.Sum256([]byte(c.password))
436                                 authenticated = subtle.ConstantTimeCompare(
437                                         []byte(hex.EncodeToString(h[:])),
438                                         []byte(cols[1]),
439                                 ) == 1
440                                 break
441                         }
442                         if !authenticated {
443                                 c.ReplyParts("462", "You may not register")
444                                 c.Close()
445                                 return
446                         }
447                 }
448                 c.registered = true
449                 c.ReplyNicknamed("001", "Hi, welcome to IRC")
450                 c.ReplyNicknamed("002", "Your host is "+*hostname+", running goircd "+Version)
451                 c.ReplyNicknamed("003", "This server was created sometime")
452                 c.ReplyNicknamed("004", *hostname+" goircd o o")
453                 c.SendLusers()
454                 c.SendMotd()
455                 if *verbose {
456                         log.Println(c, "logged in")
457                 }
458         }
459 }