/* goircd -- minimalistic simple Internet Relay Chat (IRC) server Copyright (C) 2014-2020 Sergey Matveev This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 3 of the License. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package main import ( "bytes" "crypto/sha256" "crypto/subtle" "encoding/hex" "fmt" "io/ioutil" "log" "net" "regexp" "sort" "strings" "sync" "time" ) const ( BufSize = 1500 MaxOutBuf = 128 ) var ( CRLF []byte = []byte{'\x0d', '\x0a'} RENickname = regexp.MustCompile("^[a-zA-Z0-9-]{1,24}$") clients map[*Client]struct{} = make(map[*Client]struct{}) clientsLock sync.RWMutex clientsWG sync.WaitGroup ) type Client struct { conn net.Conn registered bool nickname string username string realname string password string away string recvTimestamp time.Time sendTimestamp time.Time outBuf chan string alive bool sync.Mutex } func (c *Client) Host() string { if *cloak != "" { return *cloak } addr := c.conn.RemoteAddr().String() if host, _, err := net.SplitHostPort(addr); err == nil { addr = host } if domains, err := net.LookupAddr(addr); err == nil { addr = strings.TrimSuffix(domains[0], ".") } return addr } func (c *Client) String() string { return strings.Join([]string{c.nickname, "!", c.username, "@", c.Host()}, "") } func NewClient(conn net.Conn, events chan ClientEvent) *Client { c := Client{ conn: conn, nickname: "*", username: "", recvTimestamp: time.Now(), sendTimestamp: time.Now(), alive: true, outBuf: make(chan string, MaxOutBuf), } clientsWG.Add(2) go c.MsgSender() go c.Processor(events) return &c } func (c *Client) Close() { c.Lock() if c.alive { close(c.outBuf) c.alive = false } c.Unlock() } func (c *Client) Processor(events chan ClientEvent) { events <- ClientEvent{c, EventNew, ""} if *verbose { log.Println(c, "connected") } buf := make([]byte, BufSize*2) var n, prev, i int var msg string var err error for { if prev == BufSize { log.Println(c, "input buffer size exceeded, kicking him") break } n, err = c.conn.Read(buf[prev:]) if err != nil { break } prev += n CheckMore: i = bytes.Index(buf[:prev], CRLF) if i == -1 { continue } if *debug { log.Println(c, "<-", msg) } msg = string(buf[:i]) if *debug { log.Println(c, "->", msg) } events <- ClientEvent{c, EventMsg, msg} copy(buf, buf[i+2:prev]) prev -= (i + 2) goto CheckMore } c.Close() if *verbose { log.Println(c, "disconnected") } events <- ClientEvent{c, EventDel, ""} clientsWG.Done() } func (c *Client) MsgSender() { var err error for msg := range c.outBuf { if *debug { log.Println(c, "<-", msg) } if _, err = c.conn.Write(append([]byte(msg), CRLF...)); err != nil { if *verbose { log.Println(c, "error writing", err) } break } } c.conn.Close() clientsWG.Done() } func (c *Client) Msg(text string) { c.Lock() defer c.Unlock() if !c.alive { return } if len(c.outBuf) == MaxOutBuf { log.Println(c, "output buffer size exceeded, kicking him") if c.alive { close(c.outBuf) c.alive = false } return } c.outBuf <- text } func (c *Client) Reply(text string) { c.Msg(":" + *hostname + " " + text) } func (c *Client) ReplyParts(code string, text ...string) { parts := []string{code} for _, t := range text { parts = append(parts, t) } parts[len(parts)-1] = ":" + parts[len(parts)-1] c.Reply(strings.Join(parts, " ")) } func (c *Client) ReplyNicknamed(code string, text ...string) { c.ReplyParts(code, append([]string{c.nickname}, text...)...) } func (c *Client) ReplyNotEnoughParameters(command string) { c.ReplyNicknamed("461", command, "Not enough parameters") } func (c *Client) ReplyNoChannel(channel string) { c.ReplyNicknamed("403", channel, "No such channel") } func (c *Client) ReplyNoNickChan(channel string) { c.ReplyNicknamed("401", channel, "No such nick/channel") } func (c *Client) SendLusers() { lusers := 0 clientsLock.RLock() for client := range clients { if client.registered { lusers++ } } clientsLock.RUnlock() c.ReplyNicknamed( "251", fmt.Sprintf("There are %d users and 0 invisible on 1 servers", lusers, )) } func (c *Client) SendMotd() { if motd == nil { c.ReplyNicknamed("422", "MOTD File is missing") return } motdText, err := ioutil.ReadFile(*motd) if err != nil { log.Printf("can not read motd file %s: %v", *motd, err) c.ReplyNicknamed("422", "Error reading MOTD File") return } c.ReplyNicknamed("375", "- "+*hostname+" Message of the day -") for _, s := range strings.Split(strings.TrimSuffix(string(motdText), "\n"), "\n") { c.ReplyNicknamed("372", "- "+s) } c.ReplyNicknamed("376", "End of /MOTD command") } func (c *Client) Join(cmd string) { args := strings.Split(cmd, " ") rs := strings.Split(args[0], ",") keys := []string{} if len(args) > 1 { keys = strings.Split(args[1], ",") } RoomCycle: for n, roomName := range rs { if !RERoom.MatchString(roomName) { c.ReplyNoChannel(roomName) continue } var key string if (n < len(keys)) && (keys[n] != "") { key = keys[n] } roomsLock.RLock() for roomNameExisting, room := range rooms { if roomName != roomNameExisting { continue } if (room.key != "") && (room.key != key) { c.ReplyNicknamed("475", roomName, "Cannot join channel (+k)") roomsLock.RUnlock() return } room.events <- ClientEvent{c, EventNew, ""} roomsLock.RUnlock() continue RoomCycle } roomsLock.RUnlock() roomNew := RoomRegister(roomName) if *verbose { log.Println("room", roomName, "created") } if key != "" { roomNew.key = key roomNew.StateSave() } roomNew.events <- ClientEvent{c, EventNew, ""} } } func (client *Client) SendWhois(nicknames []string) { var c *Client for _, nickname := range nicknames { nickname = strings.ToLower(nickname) clientsLock.RLock() for c = range clients { if strings.ToLower(c.nickname) == nickname { clientsLock.RUnlock() goto Found } } clientsLock.RUnlock() client.ReplyNoNickChan(nickname) continue Found: var host string if *cloak != "" { host = *cloak } else { host, _, err := net.SplitHostPort(c.conn.RemoteAddr().String()) if err != nil { log.Printf("can't parse RemoteAddr %q: %v", host, err) host = "Unknown" } } client.ReplyNicknamed("311", c.nickname, c.username, host, "*", c.realname) client.ReplyNicknamed("312", c.nickname, *hostname, *hostname) if c.away != "" { client.ReplyNicknamed("301", c.nickname, c.away) } subscriptions := make([]string, 0) roomsLock.RLock() for _, room := range rooms { for subscriber := range room.members { if subscriber.nickname == nickname { subscriptions = append(subscriptions, room.name) } } } roomsLock.RUnlock() sort.Strings(subscriptions) client.ReplyNicknamed("319", c.nickname, strings.Join(subscriptions, " ")) client.ReplyNicknamed("318", c.nickname, "End of /WHOIS list") } } func (c *Client) SendList(cols []string) { var rs []string if (len(cols) > 1) && (cols[1] != "") { rs = strings.Split(strings.Split(cols[1], " ")[0], ",") } else { rs = make([]string, 0) roomsLock.RLock() for r := range rooms { rs = append(rs, r) } roomsLock.RUnlock() } sort.Strings(rs) roomsLock.RLock() for _, r := range rs { if room, found := rooms[r]; found { c.ReplyNicknamed( "322", r, fmt.Sprintf("%d", len(room.members)), room.topic, ) } } roomsLock.RUnlock() c.ReplyNicknamed("323", "End of /LIST") } func (c *Client) Register(cmd string, cols []string) { switch cmd { case "PASS": if len(cols) == 1 || len(cols[1]) < 1 { c.ReplyNotEnoughParameters("PASS") return } password := strings.TrimPrefix(cols[1], ":") c.password = password case "NICK": if len(cols) == 1 || len(cols[1]) < 1 { c.ReplyParts("431", "No nickname given") return } nickname := cols[1] // Compatibility with some clients prepending colons to nickname nickname = strings.TrimPrefix(nickname, ":") nickname = strings.ToLower(nickname) if !RENickname.MatchString(nickname) { c.ReplyParts("432", "*", cols[1], "Erroneous nickname") return } clientsLock.RLock() for existingClient := range clients { if existingClient.nickname == nickname { clientsLock.RUnlock() c.ReplyParts("433", "*", nickname, "Nickname is already in use") return } } clientsLock.RUnlock() c.nickname = nickname case "USER": if len(cols) == 1 { c.ReplyNotEnoughParameters("USER") return } args := strings.SplitN(cols[1], " ", 4) if len(args) < 4 { c.ReplyNotEnoughParameters("USER") return } c.username = args[0] realname := strings.TrimLeft(args[3], ":") c.realname = realname } if c.nickname != "*" && c.username != "" { if *passwords != "" { authenticated := false if c.password == "" { c.ReplyParts("462", "You may not register") c.Close() return } contents, err := ioutil.ReadFile(*passwords) if err != nil { log.Fatalf("can not read passwords file %s: %s", *passwords, err) return } for n, entry := range strings.Split(string(contents), "\n") { if entry == "" || strings.HasPrefix(entry, "#") { continue } cols := strings.Split(entry, ":") if len(cols) != 2 { log.Fatalf("bad passwords format: %s:%d", *passwords, n) continue } if cols[0] != c.nickname { continue } h := sha256.Sum256([]byte(c.password)) authenticated = subtle.ConstantTimeCompare( []byte(hex.EncodeToString(h[:])), []byte(cols[1]), ) == 1 break } if !authenticated { c.ReplyParts("462", "You may not register") c.Close() return } } c.registered = true c.ReplyNicknamed("001", "Hi, welcome to IRC") c.ReplyNicknamed("002", "Your host is "+*hostname+", running goircd "+Version) c.ReplyNicknamed("003", "This server was created sometime") c.ReplyNicknamed("004", *hostname+" goircd o o") c.SendLusers() c.SendMotd() if *verbose { log.Println(c, "logged in") } } }