2 goircd -- minimalistic simple Internet Relay Chat (IRC) server
3 Copyright (C) 2014-2022 Sergey Matveev <stargrave@stargrave.org>
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.
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.
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/>.
42 CRLF []byte = []byte{'\x0d', '\x0a'}
43 RENickname = regexp.MustCompile("^[a-zA-Z0-9-]{1,24}$")
45 clients map[*Client]struct{} = make(map[*Client]struct{})
46 clientsLock sync.RWMutex
47 clientsWG sync.WaitGroup
58 recvTimestamp time.Time
59 sendTimestamp time.Time
65 func (c *Client) Host() string {
69 addr := c.conn.RemoteAddr().String()
70 if host, _, err := net.SplitHostPort(addr); err == nil {
73 if domains, err := net.LookupAddr(addr); err == nil {
74 addr = strings.TrimSuffix(domains[0], ".")
79 func (c *Client) String() string {
80 return strings.Join([]string{c.nickname, "!", c.username, "@", c.Host()}, "")
83 func NewClient(conn net.Conn, events chan ClientEvent) *Client {
88 recvTimestamp: time.Now(),
89 sendTimestamp: time.Now(),
91 outBuf: make(chan string, MaxOutBuf),
95 go c.Processor(events)
99 func (c *Client) Close() {
108 func (c *Client) Processor(events chan ClientEvent) {
109 events <- ClientEvent{c, EventNew, ""}
111 log.Println(c, "connected")
113 buf := make([]byte, BufSize*2)
119 log.Println(c, "input buffer size exceeded, kicking him")
122 n, err = c.conn.Read(buf[prev:])
128 i = bytes.Index(buf[:prev], CRLF)
133 log.Println(c, "<-", msg)
135 msg = string(buf[:i])
137 log.Println(c, "->", msg)
139 events <- ClientEvent{c, EventMsg, msg}
140 copy(buf, buf[i+2:prev])
146 log.Println(c, "disconnected")
148 events <- ClientEvent{c, EventDel, ""}
152 func (c *Client) MsgSender() {
154 for msg := range c.outBuf {
156 log.Println(c, "<-", msg)
158 if _, err = c.conn.Write(append([]byte(msg), CRLF...)); err != nil {
160 log.Println(c, "error writing", err)
169 func (c *Client) Msg(text string) {
175 if len(c.outBuf) == MaxOutBuf {
176 log.Println(c, "output buffer size exceeded, kicking him")
186 func (c *Client) Reply(text string) {
187 c.Msg(":" + *hostname + " " + text)
190 func (c *Client) ReplyParts(code string, text ...string) {
191 parts := []string{code}
192 for _, t := range text {
193 parts = append(parts, t)
195 parts[len(parts)-1] = ":" + parts[len(parts)-1]
196 c.Reply(strings.Join(parts, " "))
199 func (c *Client) ReplyNicknamed(code string, text ...string) {
200 c.ReplyParts(code, append([]string{c.nickname}, text...)...)
203 func (c *Client) ReplyNotEnoughParameters(command string) {
204 c.ReplyNicknamed("461", command, "Not enough parameters")
207 func (c *Client) ReplyNoChannel(channel string) {
208 c.ReplyNicknamed("403", channel, "No such channel")
211 func (c *Client) ReplyNoNickChan(channel string) {
212 c.ReplyNicknamed("401", channel, "No such nick/channel")
215 func (c *Client) SendLusers() {
218 for client := range clients {
219 if client.registered {
223 clientsLock.RUnlock()
226 fmt.Sprintf("There are %d users and 0 invisible on 1 servers",
231 func (c *Client) SendMotd() {
233 c.ReplyNicknamed("422", "MOTD File is missing")
236 motdText, err := ioutil.ReadFile(*motd)
238 log.Printf("can not read motd file %s: %v", *motd, err)
239 c.ReplyNicknamed("422", "Error reading MOTD File")
242 c.ReplyNicknamed("375", "- "+*hostname+" Message of the day -")
243 for _, s := range strings.Split(strings.TrimSuffix(string(motdText), "\n"), "\n") {
244 c.ReplyNicknamed("372", "- "+s)
246 c.ReplyNicknamed("376", "End of /MOTD command")
249 func (c *Client) Join(cmd string) {
250 args := strings.Split(cmd, " ")
251 rs := strings.Split(args[0], ",")
254 keys = strings.Split(args[1], ",")
257 for n, roomName := range rs {
258 if !RERoom.MatchString(roomName) {
259 c.ReplyNoChannel(roomName)
263 if (n < len(keys)) && (keys[n] != "") {
267 for roomNameExisting, room := range rooms {
268 if roomName != roomNameExisting {
271 if (room.key != "") && (room.key != key) {
272 c.ReplyNicknamed("475", roomName, "Cannot join channel (+k)")
276 room.events <- ClientEvent{c, EventNew, ""}
281 roomNew := RoomRegister(roomName)
283 log.Println("room", roomName, "created")
289 roomNew.events <- ClientEvent{c, EventNew, ""}
293 func (client *Client) SendWhois(nicknames []string) {
295 for _, nickname := range nicknames {
296 nickname = strings.ToLower(nickname)
298 for c = range clients {
299 if strings.ToLower(c.nickname) == nickname {
300 clientsLock.RUnlock()
304 clientsLock.RUnlock()
305 client.ReplyNoNickChan(nickname)
312 host, _, err := net.SplitHostPort(c.conn.RemoteAddr().String())
314 log.Printf("can't parse RemoteAddr %q: %v", host, err)
318 client.ReplyNicknamed("311", c.nickname, c.username, host, "*", c.realname)
319 client.ReplyNicknamed("312", c.nickname, *hostname, *hostname)
321 client.ReplyNicknamed("301", c.nickname, c.away)
323 subscriptions := make([]string, 0)
325 for _, room := range rooms {
326 for subscriber := range room.members {
327 if subscriber.nickname == nickname {
328 subscriptions = append(subscriptions, room.name)
333 sort.Strings(subscriptions)
334 client.ReplyNicknamed("319", c.nickname, strings.Join(subscriptions, " "))
335 client.ReplyNicknamed("318", c.nickname, "End of /WHOIS list")
339 func (c *Client) SendList(cols []string) {
341 if (len(cols) > 1) && (cols[1] != "") {
342 rs = strings.Split(strings.Split(cols[1], " ")[0], ",")
344 rs = make([]string, 0)
346 for r := range rooms {
353 for _, r := range rs {
354 if room, found := rooms[r]; found {
358 fmt.Sprintf("%d", len(room.members)),
364 c.ReplyNicknamed("323", "End of /LIST")
367 func (c *Client) Register(cmd string, cols []string) {
370 if len(cols) == 1 || len(cols[1]) < 1 {
371 c.ReplyNotEnoughParameters("PASS")
374 password := strings.TrimPrefix(cols[1], ":")
375 c.password = password
377 if len(cols) == 1 || len(cols[1]) < 1 {
378 c.ReplyParts("431", "No nickname given")
382 // Compatibility with some clients prepending colons to nickname
383 nickname = strings.TrimPrefix(nickname, ":")
384 nickname = strings.ToLower(nickname)
385 if !RENickname.MatchString(nickname) {
386 c.ReplyParts("432", "*", cols[1], "Erroneous nickname")
390 for existingClient := range clients {
391 if existingClient.nickname == nickname {
392 clientsLock.RUnlock()
393 c.ReplyParts("433", "*", nickname, "Nickname is already in use")
397 clientsLock.RUnlock()
398 c.nickname = nickname
401 c.ReplyNotEnoughParameters("USER")
404 args := strings.SplitN(cols[1], " ", 4)
406 c.ReplyNotEnoughParameters("USER")
410 realname := strings.TrimLeft(args[3], ":")
411 c.realname = realname
413 if c.nickname != "*" && c.username != "" {
414 if *passwords != "" {
415 authenticated := false
416 if c.password == "" {
417 c.ReplyParts("462", "You may not register")
421 contents, err := ioutil.ReadFile(*passwords)
423 log.Fatalf("can not read passwords file %s: %s", *passwords, err)
426 for n, entry := range strings.Split(string(contents), "\n") {
427 if entry == "" || strings.HasPrefix(entry, "#") {
430 cols := strings.Split(entry, ":")
432 log.Fatalf("bad passwords format: %s:%d", *passwords, n)
435 if cols[0] != c.nickname {
438 h := sha256.Sum256([]byte(c.password))
439 authenticated = subtle.ConstantTimeCompare(
440 []byte(hex.EncodeToString(h[:])),
446 c.ReplyParts("462", "You may not register")
452 c.ReplyNicknamed("001", "Hi, welcome to IRC")
453 c.ReplyNicknamed("002", "Your host is "+*hostname+", running goircd "+Version)
454 c.ReplyNicknamed("003", "This server was created sometime")
455 c.ReplyNicknamed("004", *hostname+" goircd o o")
459 log.Println(c, "logged in")