]> Cypherpunks.ru repositories - goircd.git/blob - daemon.go
Example lighttpd configuration for logs directory viewing
[goircd.git] / daemon.go
1 /*
2 goircd -- minimalistic simple Internet Relay Chat (IRC) server
3 Copyright (C) 2014-2016 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, either version 3 of the License, or
8 (at your option) any later version.
9
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 GNU General Public License for more details.
14
15 You should have received a copy of the GNU General Public License
16 along with this program.  If not, see <http://www.gnu.org/licenses/>.
17 */
18
19 package main
20
21 import (
22         "fmt"
23         "io/ioutil"
24         "log"
25         "net"
26         "regexp"
27         "sort"
28         "strings"
29         "sync"
30         "time"
31 )
32
33 const (
34         // Max deadline time of client's unresponsiveness
35         PingTimeout = time.Second * 180
36         // Max idle client's time before PING are sent
37         PingThreshold = time.Second * 90
38 )
39
40 var (
41         RENickname = regexp.MustCompile("^[a-zA-Z0-9-]{1,24}$")
42
43         clients    map[*Client]struct{} = make(map[*Client]struct{})
44         clientsM   sync.RWMutex
45         rooms      map[string]*Room = make(map[string]*Room)
46         roomsM     sync.RWMutex
47         roomsGroup sync.WaitGroup
48         roomSinks  map[*Room]chan ClientEvent = make(map[*Room]chan ClientEvent)
49 )
50
51 func SendLusers(client *Client) {
52         lusers := 0
53         clientsM.RLock()
54         for client := range clients {
55                 if client.registered {
56                         lusers++
57                 }
58         }
59         clientsM.RUnlock()
60         client.ReplyNicknamed("251", fmt.Sprintf("There are %d users and 0 invisible on 1 servers", lusers))
61 }
62
63 func SendMotd(client *Client) {
64         if motd == nil {
65                 client.ReplyNicknamed("422", "MOTD File is missing")
66                 return
67         }
68         motdText, err := ioutil.ReadFile(*motd)
69         if err != nil {
70                 log.Printf("Can not read motd file %s: %v", *motd, err)
71                 client.ReplyNicknamed("422", "Error reading MOTD File")
72                 return
73         }
74         client.ReplyNicknamed("375", "- "+*hostname+" Message of the day -")
75         for _, s := range strings.Split(strings.TrimSuffix(string(motdText), "\n"), "\n") {
76                 client.ReplyNicknamed("372", "- "+s)
77         }
78         client.ReplyNicknamed("376", "End of /MOTD command")
79 }
80
81 func SendWhois(client *Client, nicknames []string) {
82         var c *Client
83         var hostPort string
84         var err error
85         var subscriptions []string
86         var room *Room
87         var subscriber *Client
88         for _, nickname := range nicknames {
89                 nickname = strings.ToLower(nickname)
90                 clientsM.RLock()
91                 for c = range clients {
92                         if strings.ToLower(*c.nickname) == nickname {
93                                 clientsM.RUnlock()
94                                 goto Found
95                         }
96                 }
97                 clientsM.RUnlock()
98                 client.ReplyNoNickChan(nickname)
99                 continue
100         Found:
101                 hostPort, _, err = net.SplitHostPort(c.conn.RemoteAddr().String())
102                 if err != nil {
103                         log.Printf("Can't parse RemoteAddr %q: %v", hostPort, err)
104                         hostPort = "Unknown"
105                 }
106                 client.ReplyNicknamed("311", *c.nickname, *c.username, hostPort, "*", *c.realname)
107                 client.ReplyNicknamed("312", *c.nickname, *hostname, *hostname)
108                 if c.away != nil {
109                         client.ReplyNicknamed("301", *c.nickname, *c.away)
110                 }
111                 subscriptions = make([]string, 0)
112                 roomsM.RLock()
113                 for _, room = range rooms {
114                         for subscriber = range room.members {
115                                 if *subscriber.nickname == nickname {
116                                         subscriptions = append(subscriptions, *room.name)
117                                 }
118                         }
119                 }
120                 roomsM.RUnlock()
121                 sort.Strings(subscriptions)
122                 client.ReplyNicknamed("319", *c.nickname, strings.Join(subscriptions, " "))
123                 client.ReplyNicknamed("318", *c.nickname, "End of /WHOIS list")
124         }
125 }
126
127 func SendList(client *Client, cols []string) {
128         var rs []string
129         var r string
130         if (len(cols) > 1) && (cols[1] != "") {
131                 rs = strings.Split(strings.Split(cols[1], " ")[0], ",")
132         } else {
133                 rs = make([]string, 0)
134                 roomsM.RLock()
135                 for r = range rooms {
136                         rs = append(rs, r)
137                 }
138                 roomsM.RUnlock()
139         }
140         sort.Strings(rs)
141         var room *Room
142         var found bool
143         for _, r = range rs {
144                 roomsM.RLock()
145                 if room, found = rooms[r]; found {
146                         client.ReplyNicknamed(
147                                 "322",
148                                 r,
149                                 fmt.Sprintf("%d", len(room.members)),
150                                 *room.topic,
151                         )
152                 }
153                 roomsM.RUnlock()
154         }
155         client.ReplyNicknamed("323", "End of /LIST")
156 }
157
158 // Unregistered client workflow processor. Unregistered client:
159 // * is not PINGed
160 // * only QUIT, NICK and USER commands are processed
161 // * other commands are quietly ignored
162 // When client finishes NICK/USER workflow, then MOTD and LUSERS are send to him.
163 func ClientRegister(client *Client, cmd string, cols []string) {
164         switch cmd {
165         case "PASS":
166                 if len(cols) == 1 || len(cols[1]) < 1 {
167                         client.ReplyNotEnoughParameters("PASS")
168                         return
169                 }
170                 client.password = &cols[1]
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                                                 roomsM.RUnlock()
448                                                 client.ReplyNoChannel(room)
449                                                 continue
450                                         }
451                                 }
452                                 roomsM.RUnlock()
453                         case "PING":
454                                 if len(cols) == 1 {
455                                         client.ReplyNicknamed("409", "No origin specified")
456                                         continue
457                                 }
458                                 client.Reply(fmt.Sprintf("PONG %s :%s", *hostname, cols[1]))
459                         case "PONG":
460                                 continue
461                         case "NOTICE", "PRIVMSG":
462                                 if len(cols) == 1 {
463                                         client.ReplyNicknamed("411", "No recipient given ("+cmd+")")
464                                         continue
465                                 }
466                                 cols = strings.SplitN(cols[1], " ", 2)
467                                 if len(cols) == 1 {
468                                         client.ReplyNicknamed("412", "No text to send")
469                                         continue
470                                 }
471                                 msg := ""
472                                 target := strings.ToLower(cols[0])
473                                 clientsM.RLock()
474                                 for c := range clients {
475                                         if *c.nickname == target {
476                                                 msg = fmt.Sprintf(":%s %s %s %s", client, cmd, *c.nickname, cols[1])
477                                                 c.Msg(msg)
478                                                 if c.away != nil {
479                                                         client.ReplyNicknamed("301", *c.nickname, *c.away)
480                                                 }
481                                                 break
482                                         }
483                                 }
484                                 clientsM.RUnlock()
485                                 if msg != "" {
486                                         continue
487                                 }
488                                 roomsM.RLock()
489                                 if r, found := rooms[target]; found {
490                                         roomSinks[r] <- ClientEvent{
491                                                 client,
492                                                 EventMsg,
493                                                 cmd + " " + strings.TrimLeft(cols[1], ":"),
494                                         }
495                                 } else {
496                                         client.ReplyNoNickChan(target)
497                                 }
498                                 roomsM.RUnlock()
499                         case "TOPIC":
500                                 if len(cols) == 1 {
501                                         client.ReplyNotEnoughParameters("TOPIC")
502                                         continue
503                                 }
504                                 cols = strings.SplitN(cols[1], " ", 2)
505                                 roomsM.RLock()
506                                 r, found := rooms[cols[0]]
507                                 roomsM.RUnlock()
508                                 if !found {
509                                         client.ReplyNoChannel(cols[0])
510                                         continue
511                                 }
512                                 var change string
513                                 if len(cols) > 1 {
514                                         change = cols[1]
515                                 } else {
516                                         change = ""
517                                 }
518                                 roomsM.RLock()
519                                 roomSinks[r] <- ClientEvent{client, EventTopic, change}
520                                 roomsM.RUnlock()
521                         case "WHO":
522                                 if len(cols) == 1 || len(cols[1]) < 1 {
523                                         client.ReplyNotEnoughParameters("WHO")
524                                         continue
525                                 }
526                                 room := strings.Split(cols[1], " ")[0]
527                                 roomsM.RLock()
528                                 if r, found := rooms[room]; found {
529                                         roomSinks[r] <- ClientEvent{client, EventWho, ""}
530                                 } else {
531                                         client.ReplyNoChannel(room)
532                                 }
533                                 roomsM.RUnlock()
534                         case "WHOIS":
535                                 if len(cols) == 1 || len(cols[1]) < 1 {
536                                         client.ReplyNotEnoughParameters("WHOIS")
537                                         continue
538                                 }
539                                 cols := strings.Split(cols[1], " ")
540                                 nicknames := strings.Split(cols[len(cols)-1], ",")
541                                 SendWhois(client, nicknames)
542                         case "ISON":
543                                 if len(cols) == 1 || len(cols[1]) < 1 {
544                                         client.ReplyNotEnoughParameters("ISON")
545                                         continue
546                                 }
547                                 nicksKnown := make(map[string]struct{})
548                                 clientsM.RLock()
549                                 for c := range clients {
550                                         nicksKnown[*c.nickname] = struct{}{}
551                                 }
552                                 clientsM.RUnlock()
553                                 var nicksExists []string
554                                 for _, nickname := range strings.Split(cols[1], " ") {
555                                         if _, exists := nicksKnown[nickname]; exists {
556                                                 nicksExists = append(nicksExists, nickname)
557                                         }
558                                 }
559                                 client.ReplyNicknamed("303", strings.Join(nicksExists, " "))
560                         case "VERSION":
561                                 var debug string
562                                 if *verbose {
563                                         debug = "debug"
564                                 } else {
565                                         debug = ""
566                                 }
567                                 client.ReplyNicknamed("351", fmt.Sprintf("%s.%s %s :", version, debug, *hostname))
568                         default:
569                                 client.ReplyNicknamed("421", cmd, "Unknown command")
570                         }
571                 }
572         }
573 }