]> Cypherpunks.ru repositories - goircd.git/blob - daemon.go
Raise copyright years
[goircd.git] / daemon.go
1 /*
2 goircd -- minimalistic simple Internet Relay Chat (IRC) server
3 Copyright (C) 2014-2020 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         "fmt"
22         "io/ioutil"
23         "log"
24         "net"
25         "regexp"
26         "sort"
27         "strings"
28         "sync"
29         "time"
30 )
31
32 const (
33         // Max deadline time of client's unresponsiveness
34         PingTimeout = time.Second * 180
35         // Max idle client's time before PING are sent
36         PingThreshold = time.Second * 90
37 )
38
39 var (
40         RENickname = regexp.MustCompile("^[a-zA-Z0-9-]{1,24}$")
41
42         clients    map[*Client]struct{} = make(map[*Client]struct{})
43         clientsM   sync.RWMutex
44         rooms      map[string]*Room = make(map[string]*Room)
45         roomsM     sync.RWMutex
46         roomsGroup sync.WaitGroup
47         roomSinks  map[*Room]chan ClientEvent = make(map[*Room]chan ClientEvent)
48 )
49
50 func SendLusers(client *Client) {
51         lusers := 0
52         clientsM.RLock()
53         for client := range clients {
54                 if client.registered {
55                         lusers++
56                 }
57         }
58         clientsM.RUnlock()
59         client.ReplyNicknamed("251", fmt.Sprintf("There are %d users and 0 invisible on 1 servers", lusers))
60 }
61
62 func SendMotd(client *Client) {
63         if motd == nil {
64                 client.ReplyNicknamed("422", "MOTD File is missing")
65                 return
66         }
67         motdText, err := ioutil.ReadFile(*motd)
68         if err != nil {
69                 log.Printf("Can not read motd file %s: %v", *motd, err)
70                 client.ReplyNicknamed("422", "Error reading MOTD File")
71                 return
72         }
73         client.ReplyNicknamed("375", "- "+*hostname+" Message of the day -")
74         for _, s := range strings.Split(strings.TrimSuffix(string(motdText), "\n"), "\n") {
75                 client.ReplyNicknamed("372", "- "+s)
76         }
77         client.ReplyNicknamed("376", "End of /MOTD command")
78 }
79
80 func SendWhois(client *Client, nicknames []string) {
81         var c *Client
82         var hostPort string
83         var err error
84         var subscriptions []string
85         var room *Room
86         var subscriber *Client
87         for _, nickname := range nicknames {
88                 nickname = strings.ToLower(nickname)
89                 clientsM.RLock()
90                 for c = range clients {
91                         if strings.ToLower(*c.nickname) == nickname {
92                                 clientsM.RUnlock()
93                                 goto Found
94                         }
95                 }
96                 clientsM.RUnlock()
97                 client.ReplyNoNickChan(nickname)
98                 continue
99         Found:
100                 hostPort, _, err = net.SplitHostPort(c.conn.RemoteAddr().String())
101                 if err != nil {
102                         log.Printf("Can't parse RemoteAddr %q: %v", hostPort, err)
103                         hostPort = "Unknown"
104                 }
105                 client.ReplyNicknamed("311", *c.nickname, *c.username, hostPort, "*", *c.realname)
106                 client.ReplyNicknamed("312", *c.nickname, *hostname, *hostname)
107                 if c.away != nil {
108                         client.ReplyNicknamed("301", *c.nickname, *c.away)
109                 }
110                 subscriptions = make([]string, 0)
111                 roomsM.RLock()
112                 for _, room = range rooms {
113                         for subscriber = range room.members {
114                                 if *subscriber.nickname == nickname {
115                                         subscriptions = append(subscriptions, *room.name)
116                                 }
117                         }
118                 }
119                 roomsM.RUnlock()
120                 sort.Strings(subscriptions)
121                 client.ReplyNicknamed("319", *c.nickname, strings.Join(subscriptions, " "))
122                 client.ReplyNicknamed("318", *c.nickname, "End of /WHOIS list")
123         }
124 }
125
126 func SendList(client *Client, cols []string) {
127         var rs []string
128         var r string
129         if (len(cols) > 1) && (cols[1] != "") {
130                 rs = strings.Split(strings.Split(cols[1], " ")[0], ",")
131         } else {
132                 rs = make([]string, 0)
133                 roomsM.RLock()
134                 for r = range rooms {
135                         rs = append(rs, r)
136                 }
137                 roomsM.RUnlock()
138         }
139         sort.Strings(rs)
140         var room *Room
141         var found bool
142         for _, r = range rs {
143                 roomsM.RLock()
144                 if room, found = rooms[r]; found {
145                         client.ReplyNicknamed(
146                                 "322",
147                                 r,
148                                 fmt.Sprintf("%d", len(room.members)),
149                                 *room.topic,
150                         )
151                 }
152                 roomsM.RUnlock()
153         }
154         client.ReplyNicknamed("323", "End of /LIST")
155 }
156
157 // Unregistered client workflow processor. Unregistered client:
158 // * is not PINGed
159 // * only QUIT, NICK and USER commands are processed
160 // * other commands are quietly ignored
161 // When client finishes NICK/USER workflow, then MOTD and LUSERS are send to him.
162 func ClientRegister(client *Client, cmd string, cols []string) {
163         switch cmd {
164         case "PASS":
165                 if len(cols) == 1 || len(cols[1]) < 1 {
166                         client.ReplyNotEnoughParameters("PASS")
167                         return
168                 }
169                 password := strings.TrimPrefix(cols[1], ":")
170                 client.password = &password
171         case "NICK":
172                 if len(cols) == 1 || len(cols[1]) < 1 {
173                         client.ReplyParts("431", "No nickname given")
174                         return
175                 }
176                 nickname := cols[1]
177                 // Compatibility with some clients prepending colons to nickname
178                 nickname = strings.TrimPrefix(nickname, ":")
179                 nickname = strings.ToLower(nickname)
180                 clientsM.RLock()
181                 for existingClient := range clients {
182                         if *existingClient.nickname == nickname {
183                                 clientsM.RUnlock()
184                                 client.ReplyParts("433", "*", nickname, "Nickname is already in use")
185                                 return
186                         }
187                 }
188                 clientsM.RUnlock()
189                 if !RENickname.MatchString(nickname) {
190                         client.ReplyParts("432", "*", cols[1], "Erroneous nickname")
191                         return
192                 }
193                 client.nickname = &nickname
194         case "USER":
195                 if len(cols) == 1 {
196                         client.ReplyNotEnoughParameters("USER")
197                         return
198                 }
199                 args := strings.SplitN(cols[1], " ", 4)
200                 if len(args) < 4 {
201                         client.ReplyNotEnoughParameters("USER")
202                         return
203                 }
204                 client.username = &args[0]
205                 realname := strings.TrimLeft(args[3], ":")
206                 client.realname = &realname
207         }
208         if *client.nickname != "*" && *client.username != "" {
209                 if passwords != nil && *passwords != "" {
210                         if client.password == nil {
211                                 client.ReplyParts("462", "You may not register")
212                                 client.Close()
213                                 return
214                         }
215                         contents, err := ioutil.ReadFile(*passwords)
216                         if err != nil {
217                                 log.Fatalf("Can no read passwords file %s: %s", *passwords, err)
218                                 return
219                         }
220                         for _, entry := range strings.Split(string(contents), "\n") {
221                                 if entry == "" {
222                                         continue
223                                 }
224                                 if lp := strings.Split(entry, ":"); lp[0] == *client.nickname && lp[1] != *client.password {
225                                         client.ReplyParts("462", "You may not register")
226                                         client.Close()
227                                         return
228                                 }
229                         }
230                 }
231                 client.registered = true
232                 client.ReplyNicknamed("001", "Hi, welcome to IRC")
233                 client.ReplyNicknamed("002", "Your host is "+*hostname+", running goircd "+version)
234                 client.ReplyNicknamed("003", "This server was created sometime")
235                 client.ReplyNicknamed("004", *hostname+" goircd o o")
236                 SendLusers(client)
237                 SendMotd(client)
238                 log.Println(client, "logged in")
239         }
240 }
241
242 // Register new room in Daemon. Create an object, events sink, save pointers
243 // to corresponding daemon's places and start room's processor goroutine.
244 func RoomRegister(name string) (*Room, chan ClientEvent) {
245         roomNew := NewRoom(name)
246         roomSink := make(chan ClientEvent)
247         roomsM.Lock()
248         rooms[name] = roomNew
249         roomSinks[roomNew] = roomSink
250         roomsM.Unlock()
251         go roomNew.Processor(roomSink)
252         roomsGroup.Add(1)
253         return roomNew, roomSink
254 }
255
256 func HandlerJoin(client *Client, cmd string) {
257         args := strings.Split(cmd, " ")
258         rs := strings.Split(args[0], ",")
259         var keys []string
260         if len(args) > 1 {
261                 keys = strings.Split(args[1], ",")
262         } else {
263                 keys = make([]string, 0)
264         }
265         var roomExisting *Room
266         var roomSink chan ClientEvent
267         var roomNew *Room
268         for n, room := range rs {
269                 if !RoomNameValid(room) {
270                         client.ReplyNoChannel(room)
271                         continue
272                 }
273                 var key string
274                 if (n < len(keys)) && (keys[n] != "") {
275                         key = keys[n]
276                 } else {
277                         key = ""
278                 }
279                 roomsM.RLock()
280                 for roomExisting, roomSink = range roomSinks {
281                         if room == *roomExisting.name {
282                                 roomsM.RUnlock()
283                                 if (*roomExisting.key != "") && (*roomExisting.key != key) {
284                                         goto Denied
285                                 }
286                                 roomSink <- ClientEvent{client, EventNew, ""}
287                                 goto Joined
288                         }
289                 }
290                 roomsM.RUnlock()
291                 roomNew, roomSink = RoomRegister(room)
292                 log.Println("Room", roomNew, "created")
293                 if key != "" {
294                         roomNew.key = &key
295                         roomNew.StateSave()
296                 }
297                 roomSink <- ClientEvent{client, EventNew, ""}
298                 continue
299         Denied:
300                 client.ReplyNicknamed("475", room, "Cannot join channel (+k) - bad key")
301         Joined:
302         }
303 }
304
305 func Processor(events chan ClientEvent, finished chan struct{}) {
306         var now time.Time
307         go func() {
308                 for {
309                         time.Sleep(10 * time.Second)
310                         events <- ClientEvent{eventType: EventTick}
311                 }
312         }()
313         for event := range events {
314                 now = time.Now()
315                 client := event.client
316                 switch event.eventType {
317                 case EventTick:
318                         clientsM.RLock()
319                         for c := range clients {
320                                 if c.recvTimestamp.Add(PingTimeout).Before(now) {
321                                         log.Println(c, "ping timeout")
322                                         c.Close()
323                                         continue
324                                 }
325                                 if c.sendTimestamp.Add(PingThreshold).Before(now) {
326                                         if c.registered {
327                                                 c.Msg("PING :" + *hostname)
328                                                 c.sendTimestamp = time.Now()
329                                         } else {
330                                                 log.Println(c, "ping timeout")
331                                                 c.Close()
332                                         }
333                                 }
334                         }
335                         clientsM.RUnlock()
336                         roomsM.Lock()
337                         for rn, r := range rooms {
338                                 if *statedir == "" && len(r.members) == 0 {
339                                         log.Println(rn, "emptied room")
340                                         delete(rooms, rn)
341                                         close(roomSinks[r])
342                                         delete(roomSinks, r)
343                                 }
344                         }
345                         roomsM.Unlock()
346                 case EventTerm:
347                         roomsM.RLock()
348                         for _, sink := range roomSinks {
349                                 sink <- ClientEvent{eventType: EventTerm}
350                         }
351                         roomsM.RUnlock()
352                         roomsGroup.Wait()
353                         close(finished)
354                         return
355                 case EventNew:
356                         clientsM.Lock()
357                         clients[client] = struct{}{}
358                         clientsM.Unlock()
359                 case EventDel:
360                         clientsM.Lock()
361                         delete(clients, client)
362                         clientsM.Unlock()
363                         roomsM.RLock()
364                         for _, roomSink := range roomSinks {
365                                 roomSink <- event
366                         }
367                         roomsM.RUnlock()
368                 case EventMsg:
369                         cols := strings.SplitN(event.text, " ", 2)
370                         cmd := strings.ToUpper(cols[0])
371                         if *verbose {
372                                 log.Println(client, "command", cmd)
373                         }
374                         if cmd == "QUIT" {
375                                 log.Println(client, "quit")
376                                 client.Close()
377                                 continue
378                         }
379                         if !client.registered {
380                                 ClientRegister(client, cmd, cols)
381                                 continue
382                         }
383                         if client != nil {
384                                 client.recvTimestamp = now
385                         }
386                         switch cmd {
387                         case "AWAY":
388                                 if len(cols) == 1 {
389                                         client.away = nil
390                                         client.ReplyNicknamed("305", "You are no longer marked as being away")
391                                         continue
392                                 }
393                                 msg := strings.TrimLeft(cols[1], ":")
394                                 client.away = &msg
395                                 client.ReplyNicknamed("306", "You have been marked as being away")
396                         case "JOIN":
397                                 if len(cols) == 1 || len(cols[1]) < 1 {
398                                         client.ReplyNotEnoughParameters("JOIN")
399                                         continue
400                                 }
401                                 HandlerJoin(client, cols[1])
402                         case "LIST":
403                                 SendList(client, cols)
404                         case "LUSERS":
405                                 SendLusers(client)
406                         case "MODE":
407                                 if len(cols) == 1 || len(cols[1]) < 1 {
408                                         client.ReplyNotEnoughParameters("MODE")
409                                         continue
410                                 }
411                                 cols = strings.SplitN(cols[1], " ", 2)
412                                 if cols[0] == *client.username {
413                                         if len(cols) == 1 {
414                                                 client.Msg("221 " + *client.nickname + " +")
415                                         } else {
416                                                 client.ReplyNicknamed("501", "Unknown MODE flag")
417                                         }
418                                         continue
419                                 }
420                                 room := cols[0]
421                                 roomsM.RLock()
422                                 r, found := rooms[room]
423                                 if !found {
424                                         client.ReplyNoChannel(room)
425                                         roomsM.RUnlock()
426                                         continue
427                                 }
428                                 if len(cols) == 1 {
429                                         roomSinks[r] <- ClientEvent{client, EventMode, ""}
430                                 } else {
431                                         roomSinks[r] <- ClientEvent{client, EventMode, cols[1]}
432                                 }
433                                 roomsM.RUnlock()
434                         case "MOTD":
435                                 SendMotd(client)
436                         case "PART":
437                                 if len(cols) == 1 || len(cols[1]) < 1 {
438                                         client.ReplyNotEnoughParameters("PART")
439                                         continue
440                                 }
441                                 rs := strings.Split(cols[1], " ")[0]
442                                 roomsM.RLock()
443                                 for _, room := range strings.Split(rs, ",") {
444                                         if r, found := rooms[room]; found {
445                                                 roomSinks[r] <- ClientEvent{client, EventDel, ""}
446                                         } else {
447                                                 client.ReplyNoChannel(room)
448                                         }
449                                 }
450                                 roomsM.RUnlock()
451                         case "PING":
452                                 if len(cols) == 1 {
453                                         client.ReplyNicknamed("409", "No origin specified")
454                                         continue
455                                 }
456                                 client.Reply(fmt.Sprintf("PONG %s :%s", *hostname, cols[1]))
457                         case "PONG":
458                                 continue
459                         case "NOTICE", "PRIVMSG":
460                                 if len(cols) == 1 {
461                                         client.ReplyNicknamed("411", "No recipient given ("+cmd+")")
462                                         continue
463                                 }
464                                 cols = strings.SplitN(cols[1], " ", 2)
465                                 if len(cols) == 1 {
466                                         client.ReplyNicknamed("412", "No text to send")
467                                         continue
468                                 }
469                                 msg := ""
470                                 target := strings.ToLower(cols[0])
471                                 clientsM.RLock()
472                                 for c := range clients {
473                                         if *c.nickname == target {
474                                                 msg = fmt.Sprintf(":%s %s %s %s", client, cmd, *c.nickname, cols[1])
475                                                 c.Msg(msg)
476                                                 if c.away != nil {
477                                                         client.ReplyNicknamed("301", *c.nickname, *c.away)
478                                                 }
479                                                 break
480                                         }
481                                 }
482                                 clientsM.RUnlock()
483                                 if msg != "" {
484                                         continue
485                                 }
486                                 roomsM.RLock()
487                                 if r, found := rooms[target]; found {
488                                         roomSinks[r] <- ClientEvent{
489                                                 client,
490                                                 EventMsg,
491                                                 cmd + " " + strings.TrimLeft(cols[1], ":"),
492                                         }
493                                 } else {
494                                         client.ReplyNoNickChan(target)
495                                 }
496                                 roomsM.RUnlock()
497                         case "TOPIC":
498                                 if len(cols) == 1 {
499                                         client.ReplyNotEnoughParameters("TOPIC")
500                                         continue
501                                 }
502                                 cols = strings.SplitN(cols[1], " ", 2)
503                                 roomsM.RLock()
504                                 r, found := rooms[cols[0]]
505                                 roomsM.RUnlock()
506                                 if !found {
507                                         client.ReplyNoChannel(cols[0])
508                                         continue
509                                 }
510                                 var change string
511                                 if len(cols) > 1 {
512                                         change = cols[1]
513                                 } else {
514                                         change = ""
515                                 }
516                                 roomsM.RLock()
517                                 roomSinks[r] <- ClientEvent{client, EventTopic, change}
518                                 roomsM.RUnlock()
519                         case "WHO":
520                                 if len(cols) == 1 || len(cols[1]) < 1 {
521                                         client.ReplyNotEnoughParameters("WHO")
522                                         continue
523                                 }
524                                 room := strings.Split(cols[1], " ")[0]
525                                 roomsM.RLock()
526                                 if r, found := rooms[room]; found {
527                                         roomSinks[r] <- ClientEvent{client, EventWho, ""}
528                                 } else {
529                                         client.ReplyNoChannel(room)
530                                 }
531                                 roomsM.RUnlock()
532                         case "WHOIS":
533                                 if len(cols) == 1 || len(cols[1]) < 1 {
534                                         client.ReplyNotEnoughParameters("WHOIS")
535                                         continue
536                                 }
537                                 cols := strings.Split(cols[1], " ")
538                                 nicknames := strings.Split(cols[len(cols)-1], ",")
539                                 SendWhois(client, nicknames)
540                         case "ISON":
541                                 if len(cols) == 1 || len(cols[1]) < 1 {
542                                         client.ReplyNotEnoughParameters("ISON")
543                                         continue
544                                 }
545                                 nicksKnown := make(map[string]struct{})
546                                 clientsM.RLock()
547                                 for c := range clients {
548                                         nicksKnown[*c.nickname] = struct{}{}
549                                 }
550                                 clientsM.RUnlock()
551                                 var nicksExists []string
552                                 for _, nickname := range strings.Split(cols[1], " ") {
553                                         if _, exists := nicksKnown[nickname]; exists {
554                                                 nicksExists = append(nicksExists, nickname)
555                                         }
556                                 }
557                                 client.ReplyNicknamed("303", strings.Join(nicksExists, " "))
558                         case "VERSION":
559                                 var debug string
560                                 if *verbose {
561                                         debug = "debug"
562                                 } else {
563                                         debug = ""
564                                 }
565                                 client.ReplyNicknamed("351", fmt.Sprintf("%s.%s %s :", version, debug, *hostname))
566                         default:
567                                 client.ReplyNicknamed("421", cmd, "Unknown command")
568                         }
569                 }
570         }
571 }