You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1607 lines
40 KiB
Go
1607 lines
40 KiB
Go
package main
|
|
|
|
import (
|
|
"crypto/tls"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"log"
|
|
"math/rand"
|
|
"net"
|
|
"os"
|
|
"reflect"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/BurntSushi/toml"
|
|
"github.com/pkg/errors"
|
|
"golang.org/x/crypto/sha3"
|
|
"gopkg.in/sorcix/irc.v2"
|
|
)
|
|
|
|
const (
|
|
commandHelp = "HELP"
|
|
commandInfo = "INFO"
|
|
|
|
// User commands
|
|
commandRegister = "REGISTER"
|
|
commandIdentify = "IDENTIFY"
|
|
commandToken = "TOKEN"
|
|
commandUsername = "USERNAME"
|
|
commandPassword = "PASSWORD"
|
|
|
|
// User/channel commands
|
|
commandMode = "MODE"
|
|
|
|
// Channel/server commands
|
|
commandFound = "FOUND"
|
|
commandDrop = "DROP"
|
|
commandGrant = "GRANT"
|
|
commandReveal = "REVEAL"
|
|
commandKick = "KICK"
|
|
commandBan = "BAN"
|
|
commandAudit = "AUDIT"
|
|
|
|
// Server admin commands
|
|
commandKill = "KILL"
|
|
commandStats = "STATS"
|
|
commandRehash = "REHASH"
|
|
commandUpgrade = "UPGRADE"
|
|
)
|
|
|
|
var serverCommands = []string{commandKill, commandStats, commandRehash, commandUpgrade}
|
|
|
|
// TODO: Reorder
|
|
const (
|
|
permissionClient = 0
|
|
permissionRegistered = 1
|
|
permissionVIP = 2
|
|
permissionModerator = 3
|
|
permissionAdmin = 4
|
|
permissionSuperAdmin = 5
|
|
)
|
|
|
|
var permissionLabels = map[int]string{
|
|
permissionClient: "Client",
|
|
permissionRegistered: "Registered Client",
|
|
permissionVIP: "VIP",
|
|
permissionModerator: "Moderator",
|
|
permissionAdmin: "Administrator",
|
|
permissionSuperAdmin: "Super Administrator",
|
|
}
|
|
|
|
var allPermissions = "Client, Registered Client, VIP, Moderator, Administrator and Super Administrator"
|
|
|
|
var commandRestrictions = map[int][]string{
|
|
permissionRegistered: {commandToken, commandUsername, commandPassword, commandFound},
|
|
permissionModerator: {commandMode, commandReveal, commandKick, commandBan},
|
|
permissionAdmin: {commandGrant, commandAudit},
|
|
permissionSuperAdmin: {commandDrop, commandKill, commandStats, commandRehash, commandUpgrade}}
|
|
|
|
var helpDuration = "Duration can be 0 to never expire, or e.g. 30m, 1h, 2d, 3w"
|
|
var commandUsage = map[string][]string{
|
|
commandHelp: {"[command|all]",
|
|
"Print usage information regarding a specific command or 'all'",
|
|
"Without a command or 'all', only commands you have permission to use are printed"},
|
|
commandInfo: {"[channel]",
|
|
"When a channel is specified, prints info including whether it is registered",
|
|
"Without a channel, server info is printed"},
|
|
commandRegister: {"<username> <password>",
|
|
"Create an account",
|
|
"Once you've registered, other users may GRANT permissions to you, or ",
|
|
"See IDENTIFY"},
|
|
commandIdentify: {"[username] <password>",
|
|
"Identify to a previously registered account",
|
|
"If username is omitted, it will be replaced with your current nick",
|
|
"Note that you may automatically identify when connecting by specifying a server password of your username and password separated by a colon - Example: admin:hunter2"},
|
|
commandToken: {"<channel>",
|
|
"Returns a token which can be used by channel administrators to grant special access to your account"},
|
|
commandUsername: {"<username> <password> <new username> <confirm new username>",
|
|
"Change your username"},
|
|
commandPassword: {"<username> <password> <new password> <confirm new password>",
|
|
"Change your password"},
|
|
commandFound: {"<channel>",
|
|
"Take ownership of an unfounded channel"},
|
|
commandGrant: {"<channel> [account] [permission]",
|
|
"When an account token isn't specified, all accounts with permissions are listed",
|
|
"Specify an account token and permission level to grant that permission",
|
|
"Specify an account token only to view that account's permission",
|
|
"To remove an account's permissions, set their permission to Client",
|
|
"Permissions: " + allPermissions},
|
|
commandReveal: {"<channel> [page] [all]",
|
|
"Print channel log, allowing KICK/BAN to be used",
|
|
fmt.Sprintf("Results start at page 1, %d per page", logsPerPage),
|
|
"Page -1 shows all matching entries",
|
|
"Joins and parts are hidden by default, add 'all' to show them"},
|
|
commandAudit: {"<channel> [page]",
|
|
"Print channel audit log",
|
|
fmt.Sprintf("Results start at page 1, %d per page", logsPerPage),
|
|
"Page -1 shows all matching entries"},
|
|
commandKick: {"<channel> <5 digit log number> [reason]",
|
|
"Kick a user from a channel"},
|
|
commandBan: {"<channel> <5 digit log number> <duration> [reason]",
|
|
"Kick and ban a user from a channel",
|
|
helpDuration},
|
|
commandDrop: {"<channel> <confirm channel>",
|
|
"Delete all channel data, allowing it to be founded again"},
|
|
commandKill: {"<channel> <5 digit log number> <duration> [reason]",
|
|
"Disconnect and ban a user from the server",
|
|
helpDuration},
|
|
commandStats: {"",
|
|
"Print the current number of clients and channels"},
|
|
commandRehash: {"",
|
|
"Reload the server configuration"},
|
|
commandUpgrade: {"",
|
|
"Upgrade the server without disconnecting clients"},
|
|
}
|
|
|
|
type Config struct {
|
|
MOTD string
|
|
Salt string
|
|
DBDriver string
|
|
DBSource string
|
|
SSLCert string
|
|
SSLKey string
|
|
}
|
|
|
|
type Server struct {
|
|
config *Config
|
|
configfile string
|
|
created int64
|
|
motd []string
|
|
clients *sync.Map
|
|
channels *sync.Map
|
|
|
|
restartplain chan bool
|
|
restartssl chan bool
|
|
|
|
*sync.RWMutex
|
|
}
|
|
|
|
var db = &Database{}
|
|
|
|
func NewServer(configfile string) *Server {
|
|
s := &Server{}
|
|
s.config = &Config{}
|
|
s.configfile = configfile
|
|
s.created = time.Now().Unix()
|
|
s.clients = new(sync.Map)
|
|
s.channels = new(sync.Map)
|
|
|
|
s.restartplain = make(chan bool, 1)
|
|
s.restartssl = make(chan bool, 1)
|
|
s.RWMutex = new(sync.RWMutex)
|
|
|
|
return s
|
|
}
|
|
|
|
func (s *Server) hashPassword(username string, password string) string {
|
|
sha512 := sha3.New512()
|
|
_, err := sha512.Write([]byte(strings.Join([]string{username, s.config.Salt, password}, "-")))
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
|
|
return base64.URLEncoding.EncodeToString(sha512.Sum(nil))
|
|
}
|
|
|
|
func (s *Server) getAnonymousPrefix(i int) *irc.Prefix {
|
|
prefix := prefixAnonymous
|
|
if i > 1 {
|
|
prefix.Name += fmt.Sprintf("%d", i)
|
|
}
|
|
return &prefix
|
|
}
|
|
|
|
func (s *Server) getChannel(channel string) *Channel {
|
|
if ch, ok := s.channels.Load(channel); ok {
|
|
return ch.(*Channel)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *Server) getChannels(client string) map[string]*Channel {
|
|
channels := make(map[string]*Channel)
|
|
s.channels.Range(func(k, v interface{}) bool {
|
|
key := k.(string)
|
|
channel := v.(*Channel)
|
|
if client == "" || s.inChannel(key, client) {
|
|
channels[key] = channel
|
|
}
|
|
|
|
return true
|
|
})
|
|
|
|
return channels
|
|
}
|
|
|
|
func (s *Server) channelCount() int {
|
|
i := 0
|
|
s.channels.Range(func(k, v interface{}) bool {
|
|
i++
|
|
return true
|
|
})
|
|
|
|
return i
|
|
}
|
|
|
|
func (s *Server) getClient(client string) *Client {
|
|
if cl, ok := s.clients.Load(client); ok {
|
|
return cl.(*Client)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *Server) getClients(channel string) map[string]*Client {
|
|
clients := make(map[string]*Client)
|
|
|
|
if channel == "" {
|
|
s.clients.Range(func(k, v interface{}) bool {
|
|
cl := s.getClient(k.(string))
|
|
if cl != nil {
|
|
clients[cl.identifier] = cl
|
|
}
|
|
return true
|
|
})
|
|
return clients
|
|
}
|
|
|
|
ch := s.getChannel(channel)
|
|
if ch == nil {
|
|
return clients
|
|
}
|
|
|
|
ch.clients.Range(func(k, v interface{}) bool {
|
|
cl := s.getClient(k.(string))
|
|
if cl != nil {
|
|
clients[cl.identifier] = cl
|
|
}
|
|
return true
|
|
})
|
|
|
|
return clients
|
|
}
|
|
|
|
func (s *Server) clientCount() int {
|
|
i := 0
|
|
s.clients.Range(func(k, v interface{}) bool {
|
|
i++
|
|
return true
|
|
})
|
|
|
|
return i
|
|
}
|
|
|
|
func (s *Server) revealClient(channel string, identifier string) *Client {
|
|
riphash, raccount := s.revealClientInfo(channel, identifier)
|
|
if riphash == "" && raccount == 0 {
|
|
return nil
|
|
}
|
|
|
|
cls := s.getClients("")
|
|
for _, rcl := range cls {
|
|
if rcl.iphash == riphash || (rcl.account > 0 && rcl.account == raccount) {
|
|
return rcl
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *Server) revealClientInfo(channel string, identifier string) (string, int64) {
|
|
if len(identifier) != 5 {
|
|
return "", 0
|
|
}
|
|
|
|
ch := s.getChannel(channel)
|
|
if ch == nil {
|
|
return "", 0
|
|
}
|
|
|
|
return ch.RevealInfo(identifier)
|
|
}
|
|
|
|
func (s *Server) inChannel(channel string, client string) bool {
|
|
ch := s.getChannel(channel)
|
|
if ch != nil {
|
|
_, ok := ch.clients.Load(client)
|
|
return ok
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func (s *Server) canJoin(c *Client, channel string, key string) (bool, string) {
|
|
dbch, err := db.Channel(channel)
|
|
if err != nil || dbch.Channel == "" {
|
|
return false, "invalid channel"
|
|
}
|
|
ch := s.getChannel(channel)
|
|
if ch == nil {
|
|
return false, "invalid channel"
|
|
} else if banned, reason := c.isBanned(channel); banned {
|
|
if reason != "" {
|
|
reason = fmt.Sprintf(" (%s)", reason)
|
|
}
|
|
return false, "you are banned" + reason
|
|
} else if ch.hasMode("z") && !c.ssl {
|
|
return false, "only clients connected via SSL are allowed"
|
|
}
|
|
|
|
permission := c.globalPermission()
|
|
requiredPermission := permissionClient
|
|
reason := ""
|
|
|
|
if channel[0] == '&' {
|
|
if permission < permissionVIP {
|
|
return false, "restricted channel"
|
|
}
|
|
} else if channel[0] != '#' {
|
|
return false, "invalid channel"
|
|
}
|
|
|
|
if permission < requiredPermission && c.account > 0 {
|
|
chp, err := db.GetPermission(c.account, channel)
|
|
if err != nil && chp.Permission > permission {
|
|
permission = chp.Permission
|
|
}
|
|
}
|
|
|
|
if permission < permissionVIP {
|
|
if ch.hasMode("k") && ch.getMode("k") != key {
|
|
return false, "invalid channel key specified"
|
|
} else if ch.hasMode("l") {
|
|
var l int
|
|
var err error
|
|
l, err = strconv.Atoi(ch.getMode("l"))
|
|
if err != nil {
|
|
l = 0
|
|
}
|
|
|
|
if l > 0 && ch.clientCount() >= l {
|
|
return false, "limited channel, join again in a few moments"
|
|
}
|
|
}
|
|
}
|
|
|
|
if ch.hasMode("r") {
|
|
requiredPermission = permissionRegistered
|
|
reason = "only registered clients are allowed"
|
|
}
|
|
if ch.hasMode("i") {
|
|
requiredPermission = permissionVIP
|
|
reason = "only VIP are allowed"
|
|
}
|
|
|
|
return permission >= requiredPermission, reason
|
|
}
|
|
|
|
func (s *Server) joinChannel(client string, channel string, key string) {
|
|
if s.inChannel(channel, client) {
|
|
return // Already in channel
|
|
}
|
|
|
|
cl := s.getClient(client)
|
|
if cl == nil {
|
|
return
|
|
}
|
|
|
|
ch := s.getChannel(channel)
|
|
if ch == nil {
|
|
ch = NewChannel(channel)
|
|
s.channels.Store(channel, ch)
|
|
} else if canaccess, reason := s.canJoin(cl, channel, key); !canaccess {
|
|
errmsg := fmt.Sprintf("Cannot join %s: %s", channel, reason)
|
|
cl.writeMessage(irc.ERR_INVITEONLYCHAN, []string{channel, errmsg})
|
|
cl.sendNotice(errmsg)
|
|
return
|
|
}
|
|
|
|
ch.clients.Store(client, s.anonCount(channel, client)+1)
|
|
cl.write(cl.getPrefix(), irc.JOIN, []string{channel})
|
|
ch.Log(cl, irc.JOIN, "")
|
|
|
|
s.sendNames(channel, client)
|
|
s.updateClientCount(channel, client, "")
|
|
s.sendTopic(channel, client, false)
|
|
}
|
|
|
|
func (s *Server) partChannel(channel string, client string, reason string) {
|
|
ch := s.getChannel(channel)
|
|
cl := s.getClient(client)
|
|
|
|
if cl == nil || !s.inChannel(channel, client) {
|
|
return
|
|
}
|
|
|
|
cl.write(cl.getPrefix(), irc.PART, []string{channel, reason})
|
|
ch.Log(cl, irc.PART, reason)
|
|
ch.clients.Delete(client)
|
|
|
|
s.updateClientCount(channel, client, reason)
|
|
// TODO: Destroy empty channel
|
|
}
|
|
|
|
func (s *Server) partAllChannels(client string, reason string) {
|
|
for channelname := range s.getChannels(client) {
|
|
s.partChannel(channelname, client, reason)
|
|
}
|
|
}
|
|
|
|
func (s *Server) revealChannelLog(channel string, client string, page int, showAll bool) {
|
|
cl := s.getClient(client)
|
|
if cl == nil {
|
|
return
|
|
}
|
|
|
|
ch := s.getChannel(channel)
|
|
if ch == nil {
|
|
cl.sendError("Unable to reveal, invalid channel specified")
|
|
return
|
|
} else if !ch.HasClient(client) {
|
|
cl.sendError("Unable to reveal, you are not in that channel")
|
|
return
|
|
}
|
|
|
|
r := ch.RevealLog(page, showAll)
|
|
for _, rev := range r {
|
|
cl.sendMessage(rev)
|
|
}
|
|
}
|
|
|
|
func (s *Server) enforceModes(channel string) {
|
|
ch := s.getChannel(channel)
|
|
|
|
if ch != nil && ch.hasMode("z") {
|
|
for client, cl := range s.getClients(channel) {
|
|
if !cl.ssl {
|
|
s.partChannel(channel, client, fmt.Sprintf("You must connect via SSL to join %s", channel))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *Server) anonCount(channel string, client string) int {
|
|
ch := s.getChannel(channel)
|
|
cl := s.getClient(client)
|
|
|
|
if ch == nil || cl == nil {
|
|
return 0
|
|
}
|
|
|
|
ccount := ch.clientCount()
|
|
if (ch.hasMode("c") || cl.hasMode("c")) && ccount >= 2 {
|
|
return 2
|
|
}
|
|
|
|
return ccount
|
|
}
|
|
|
|
func (s *Server) updateClientCount(channel string, client string, reason string) {
|
|
ch := s.getChannel(channel)
|
|
|
|
if ch == nil {
|
|
return
|
|
}
|
|
|
|
var reasonShown bool
|
|
ch.clients.Range(func(k, v interface{}) bool {
|
|
cl := s.getClient(k.(string))
|
|
ccount := v.(int)
|
|
|
|
if cl == nil {
|
|
return true
|
|
} else if client != "" && ch.hasMode("D") && cl.identifier != client {
|
|
return true
|
|
}
|
|
|
|
reasonShown = false
|
|
chancount := s.anonCount(channel, cl.identifier)
|
|
|
|
if ccount < chancount {
|
|
for i := ccount; i < chancount; i++ {
|
|
cl.write(s.getAnonymousPrefix(i), irc.JOIN, []string{channel})
|
|
}
|
|
|
|
ch.clients.Store(cl.identifier, chancount)
|
|
} else if ccount > chancount {
|
|
for i := ccount; i > chancount; i-- {
|
|
pr := ""
|
|
if !reasonShown {
|
|
pr = reason
|
|
}
|
|
|
|
cl.write(s.getAnonymousPrefix(i-1), irc.PART, []string{channel, pr})
|
|
reasonShown = true
|
|
}
|
|
} else {
|
|
return true
|
|
}
|
|
|
|
ch.clients.Store(cl.identifier, chancount)
|
|
|
|
return true
|
|
})
|
|
}
|
|
|
|
func (s *Server) sendNames(channel string, clientname string) {
|
|
if !s.inChannel(channel, clientname) {
|
|
return
|
|
}
|
|
|
|
cl := s.getClient(clientname)
|
|
|
|
if cl == nil {
|
|
return
|
|
}
|
|
|
|
names := []string{}
|
|
if cl.capHostInNames {
|
|
names = append(names, cl.getPrefix().String())
|
|
} else {
|
|
names = append(names, cl.nick)
|
|
}
|
|
|
|
ccount := s.anonCount(channel, clientname)
|
|
for i := 1; i < ccount; i++ {
|
|
if cl.capHostInNames {
|
|
names = append(names, s.getAnonymousPrefix(i).String())
|
|
} else {
|
|
names = append(names, s.getAnonymousPrefix(i).Name)
|
|
}
|
|
}
|
|
|
|
cl.writeMessage(irc.RPL_NAMREPLY, []string{"=", channel, strings.Join(names, " ")})
|
|
cl.writeMessage(irc.RPL_ENDOFNAMES, []string{channel, "End of /NAMES list."})
|
|
}
|
|
|
|
func (s *Server) sendTopic(channel string, client string, changed bool) {
|
|
if !s.inChannel(channel, client) {
|
|
return
|
|
}
|
|
|
|
ch := s.getChannel(channel)
|
|
cl := s.getClient(client)
|
|
|
|
if ch == nil || cl == nil {
|
|
return
|
|
}
|
|
|
|
tprefix := prefixAnonymous
|
|
tcommand := irc.TOPIC
|
|
if !changed {
|
|
tprefix = prefixAnonIRC
|
|
tcommand = irc.RPL_TOPIC
|
|
}
|
|
cl.write(&tprefix, tcommand, []string{channel, ch.topic})
|
|
|
|
if !changed {
|
|
cl.writeMessage(strings.Join([]string{irc.RPL_TOPICWHOTIME, cl.nick, channel, prefixAnonymous.Name, fmt.Sprintf("%d", ch.topictime)}, " "), nil)
|
|
}
|
|
}
|
|
|
|
func (s *Server) handleTopic(channel string, client string, topic string) {
|
|
ch := s.getChannel(channel)
|
|
cl := s.getClient(client)
|
|
|
|
if ch == nil || cl == nil {
|
|
return
|
|
}
|
|
|
|
if !s.inChannel(channel, client) {
|
|
cl.sendNotice("Invalid use of TOPIC")
|
|
return
|
|
}
|
|
|
|
chp, err := db.GetPermission(cl.account, channel)
|
|
if err != nil {
|
|
log.Panicf("%+v", err)
|
|
} else if ch.hasMode("t") && chp.Permission < permissionVIP {
|
|
cl.accessDenied(permissionVIP)
|
|
return
|
|
}
|
|
|
|
ch.topic = topic
|
|
ch.topictime = time.Now().Unix()
|
|
|
|
ch.clients.Range(func(k, v interface{}) bool {
|
|
s.sendTopic(channel, k.(string), true)
|
|
return true
|
|
})
|
|
ch.Log(cl, irc.TOPIC, ch.topic)
|
|
}
|
|
|
|
func (s *Server) handleChannelMode(c *Client, params []string) {
|
|
ch := s.getChannel(params[0])
|
|
if ch == nil || !s.inChannel(params[0], c.identifier) {
|
|
return
|
|
}
|
|
|
|
if len(params) == 1 || params[1] == "" {
|
|
c.writeMessage(strings.Join([]string{irc.RPL_CHANNELMODEIS, c.nick, params[0], ch.printModes(ch.getModes(), nil)}, " "), []string{})
|
|
|
|
// Send channel creation time
|
|
c.writeMessage(strings.Join([]string{"329", c.nick, params[0], fmt.Sprintf("%d", int32(ch.created))}, " "), []string{})
|
|
} else if len(params) > 1 && (params[1] == "" || params[1][0] == '+' || params[1][0] == '-') {
|
|
if !c.canUse(commandMode, params[0]) {
|
|
c.accessDenied(c.permissionRequired(commandMode))
|
|
return
|
|
}
|
|
|
|
lastmodes := make(map[string]string)
|
|
for m, mv := range ch.getModes() {
|
|
lastmodes[m] = mv
|
|
}
|
|
|
|
if params[1][0] == '+' {
|
|
ch.addModes(params[1:])
|
|
} else {
|
|
ch.removeModes(params[1][1:])
|
|
}
|
|
s.enforceModes(params[0])
|
|
|
|
if reflect.DeepEqual(ch.getModes(), lastmodes) {
|
|
return
|
|
}
|
|
|
|
// TODO: Check if local modes were set/unset, only send changes to local client
|
|
addedmodes, removedmodes := ch.diffModes(lastmodes)
|
|
|
|
resendusercount := false
|
|
if _, ok := addedmodes["c"]; ok {
|
|
resendusercount = true
|
|
}
|
|
if _, ok := removedmodes["c"]; ok {
|
|
resendusercount = true
|
|
}
|
|
if _, ok := removedmodes["D"]; ok {
|
|
resendusercount = true
|
|
}
|
|
|
|
if len(addedmodes) == 0 && len(removedmodes) == 0 {
|
|
addedmodes = c.getModes()
|
|
}
|
|
|
|
ch.clients.Range(func(k, v interface{}) bool {
|
|
cl := s.getClient(k.(string))
|
|
if cl != nil {
|
|
cl.write(&prefixAnonymous, irc.MODE, []string{params[0], ch.printModes(addedmodes, removedmodes)})
|
|
}
|
|
|
|
return true
|
|
})
|
|
|
|
if resendusercount {
|
|
s.updateClientCount(params[0], c.identifier, "Enforcing MODEs")
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *Server) handleUserMode(c *Client, params []string) {
|
|
if len(params) == 1 || params[1] == "" {
|
|
c.writeMessage(strings.Join([]string{irc.RPL_UMODEIS, c.nick, c.printModes(c.getModes(), nil)}, " "), []string{})
|
|
return
|
|
}
|
|
|
|
lastmodes := c.getModes()
|
|
|
|
if len(params) > 1 && len(params[1]) > 0 && (params[1][0] == '+' || params[1][0] == '-') {
|
|
if params[1][0] == '+' {
|
|
c.addModes(params[1:])
|
|
} else {
|
|
c.removeModes(params[1][1:])
|
|
}
|
|
}
|
|
|
|
if reflect.DeepEqual(c.modes, lastmodes) {
|
|
return
|
|
}
|
|
|
|
addedmodes, removedmodes := c.diffModes(lastmodes)
|
|
|
|
resendusercount := false
|
|
if _, ok := addedmodes["c"]; ok {
|
|
resendusercount = true
|
|
}
|
|
if _, ok := removedmodes["c"]; ok {
|
|
resendusercount = true
|
|
}
|
|
if _, ok := removedmodes["D"]; ok {
|
|
resendusercount = true
|
|
}
|
|
|
|
if len(addedmodes) == 0 && len(removedmodes) == 0 {
|
|
addedmodes = c.getModes()
|
|
}
|
|
|
|
c.writeMessage(strings.Join([]string{irc.MODE, c.nick}, " "), []string{c.printModes(addedmodes, removedmodes)})
|
|
|
|
if resendusercount {
|
|
for ch := range s.getChannels(c.identifier) {
|
|
s.updateClientCount(ch, c.identifier, "Enforcing MODEs")
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *Server) handleMode(c *Client, params []string) {
|
|
if c == nil {
|
|
return
|
|
}
|
|
|
|
if len(params) == 0 || len(params[0]) == 0 {
|
|
c.sendNotice("Invalid use of MODE")
|
|
return
|
|
}
|
|
|
|
if len(params) > 1 && params[1] == "b" {
|
|
c.writeMessage(irc.RPL_ENDOFBANLIST, []string{params[0], "End of Channel Ban List"})
|
|
return
|
|
}
|
|
|
|
if params[0][0] == '#' || params[0][0] == '&' {
|
|
s.handleChannelMode(c, params)
|
|
} else {
|
|
s.handleUserMode(c, params)
|
|
}
|
|
}
|
|
|
|
func (s *Server) sendUsage(cl *Client, command string) {
|
|
command = strings.ToUpper(command)
|
|
|
|
showAll := false
|
|
if command == "ALL" {
|
|
command = commandHelp
|
|
showAll = true
|
|
}
|
|
|
|
commands := make([]string, 0, len(commandUsage))
|
|
for cmd := range commandUsage {
|
|
commands = append(commands, cmd)
|
|
}
|
|
sort.Strings(commands)
|
|
|
|
var printedLabel bool
|
|
var usage []string
|
|
if command == commandHelp {
|
|
// Print all commands
|
|
var perms []int
|
|
for permission := range permissionLabels {
|
|
perms = append(perms, permission)
|
|
}
|
|
sort.Ints(perms)
|
|
|
|
for i := 0; i < 2; i++ {
|
|
serverLabel := ""
|
|
if i == 1 {
|
|
serverLabel = "Server "
|
|
}
|
|
|
|
for _, permission := range perms {
|
|
printedLabel = false
|
|
for _, cmd := range commands {
|
|
if (i == 0 && containsString(serverCommands, cmd)) || (i == 1 && !containsString(serverCommands, cmd)) || cl.permissionRequired(cmd) != permission {
|
|
continue
|
|
}
|
|
|
|
if !showAll && !cl.canUse(cmd, "") {
|
|
continue
|
|
}
|
|
|
|
if !printedLabel {
|
|
cl.sendNotice(serverLabel + permissionLabels[permission] + " Commands")
|
|
printedLabel = true
|
|
}
|
|
|
|
usage = commandUsage[cmd]
|
|
cl.sendMessage(cmd + " " + usage[0])
|
|
for _, ul := range usage[1:] {
|
|
cl.sendMessage(" " + ul)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
if usage, ok := commandUsage[command]; ok {
|
|
cl.sendMessage(command + " " + usage[0])
|
|
for _, ul := range usage[1:] {
|
|
cl.sendMessage(" " + ul)
|
|
}
|
|
} else {
|
|
cl.sendError("Unknown command specified")
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
func (s *Server) ban(channel string, iphash string, accountid int64, expires int64, reason string) error {
|
|
if channel == "" || expires < 0 {
|
|
return nil
|
|
}
|
|
|
|
b := DBBan{}
|
|
|
|
if iphash != "" {
|
|
b = DBBan{Channel: generateHash(channel), Type: BAN_TYPE_ADDRESS, Target: iphash, Expires: expires}
|
|
err := db.AddBan(b)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if accountid > 0 {
|
|
b = DBBan{Channel: generateHash(channel), Type: BAN_TYPE_ACCOUNT, Target: fmt.Sprintf("%d", accountid), Expires: expires}
|
|
err := db.AddBan(b)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if b.Channel == "" {
|
|
return nil
|
|
}
|
|
|
|
ch := channel
|
|
rs := formatAction("Banned", reason)
|
|
if channel == channelServer {
|
|
ch = ""
|
|
rs = formatAction("Killed", reason)
|
|
}
|
|
cls := s.getClients(ch)
|
|
for _, cl := range cls {
|
|
if cl == nil {
|
|
continue
|
|
}
|
|
|
|
if (iphash != "" && cl.iphash == iphash) || (accountid > 0 && cl.account == accountid) {
|
|
if channel == channelServer {
|
|
cl.writeMessage(irc.KILL, []string{cl.nick, rs})
|
|
s.killClient(cl, rs)
|
|
} else {
|
|
s.partChannel(channel, cl.identifier, rs)
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *Server) handleUserCommand(client string, command string, params []string) {
|
|
cl := s.getClient(client)
|
|
if cl == nil {
|
|
return
|
|
}
|
|
|
|
var err error
|
|
command = strings.ToUpper(command)
|
|
ch := ""
|
|
if len(params) > 0 {
|
|
ch = params[0]
|
|
}
|
|
if !cl.canUse(command, ch) {
|
|
cl.accessDenied(cl.permissionRequired(command))
|
|
return
|
|
}
|
|
|
|
switch command {
|
|
case commandHelp:
|
|
cmd := command
|
|
if len(params) > 0 {
|
|
cmd = params[0]
|
|
}
|
|
s.sendUsage(cl, cmd)
|
|
return
|
|
case commandInfo:
|
|
if len(params) > 0 && len(params[0]) > 0 {
|
|
if !s.inChannel(params[0], client) {
|
|
if canaccess, reason := s.canJoin(cl, params[0], ""); !canaccess {
|
|
cl.sendError("Failed to fetch channel INFO, " + reason)
|
|
return
|
|
}
|
|
}
|
|
|
|
dbch, err := db.Channel(params[0])
|
|
if err != nil {
|
|
cl.sendError("Failed to fetch channel INFO, " + err.Error())
|
|
return
|
|
}
|
|
|
|
chst := "Unfounded"
|
|
if dbch.Channel != "" {
|
|
chst = "Founded"
|
|
}
|
|
|
|
cl.sendMessage(fmt.Sprintf("%s: %s", params[0], chst))
|
|
} else {
|
|
cl.sendMessage("AnonIRCd https://code.rocketnine.space/tslocum/anonircd")
|
|
}
|
|
return
|
|
case commandRegister:
|
|
if len(params) == 0 {
|
|
s.sendUsage(cl, command)
|
|
return
|
|
}
|
|
|
|
// TODO: Only alphanumeric username
|
|
// TODO: allow duplicate usernames, only return error on existing username and password
|
|
case commandIdentify:
|
|
if len(params) == 0 || len(params) > 2 {
|
|
s.sendUsage(cl, command)
|
|
return
|
|
}
|
|
|
|
username := cl.nick
|
|
password := params[0]
|
|
if len(params) == 2 {
|
|
username = params[0]
|
|
password = params[1]
|
|
}
|
|
|
|
authSuccess := cl.identify(username, password)
|
|
if authSuccess {
|
|
cl.sendNotice("Identified successfully")
|
|
if cl.globalPermission() >= permissionVIP {
|
|
s.joinChannel(cl.identifier, channelServer, "")
|
|
}
|
|
|
|
for clch := range s.getChannels(cl.identifier) {
|
|
banned, br := cl.isBanned(clch)
|
|
if banned {
|
|
reason := "Banned"
|
|
if br != "" {
|
|
reason += ": " + br
|
|
}
|
|
s.partChannel(clch, cl.identifier, reason)
|
|
return
|
|
}
|
|
}
|
|
} else {
|
|
cl.sendNotice("Failed to identify, incorrect username/password")
|
|
}
|
|
case commandUsername:
|
|
if cl.account == 0 {
|
|
cl.sendError("You must identify before using that command")
|
|
}
|
|
|
|
if len(params) == 0 || len(params) < 4 {
|
|
s.sendUsage(cl, command)
|
|
return
|
|
}
|
|
|
|
if params[2] != params[3] {
|
|
cl.sendError("Unable to change username, new usernames don't match")
|
|
return
|
|
}
|
|
// TODO: Alphanumeric username
|
|
|
|
accid, err := db.Auth(params[0], params[1])
|
|
if err != nil {
|
|
log.Panicf("%+v", err)
|
|
}
|
|
|
|
if accid == 0 {
|
|
cl.sendError("Unable to change username, incorrect username/password supplied")
|
|
return
|
|
}
|
|
|
|
err = db.SetUsername(accid, params[2], params[1])
|
|
if err != nil {
|
|
log.Panicf("%+v", err)
|
|
}
|
|
cl.sendMessage("Username changed successfully")
|
|
case commandPassword:
|
|
if len(params) == 0 || len(params) < 4 {
|
|
s.sendUsage(cl, command)
|
|
return
|
|
}
|
|
|
|
if params[2] != params[3] {
|
|
cl.sendError("Unable to change password, new passwords don't match")
|
|
return
|
|
}
|
|
|
|
accid, err := db.Auth(params[0], params[1])
|
|
if err != nil {
|
|
log.Panicf("%+v", err)
|
|
}
|
|
|
|
if accid == 0 {
|
|
cl.sendError("Unable to change password, incorrect username/password supplied")
|
|
return
|
|
}
|
|
|
|
err = db.SetPassword(accid, params[0], params[2])
|
|
if err != nil {
|
|
log.Panicf("%+v", err)
|
|
}
|
|
cl.sendMessage("Password changed successfully")
|
|
case commandReveal, commandAudit:
|
|
if len(params) == 0 {
|
|
s.sendUsage(cl, command)
|
|
return
|
|
}
|
|
|
|
ch := s.getChannel(params[0])
|
|
if ch == nil {
|
|
cl.sendError("Unable to reveal, invalid channel specified")
|
|
return
|
|
}
|
|
|
|
page := 1
|
|
all := false
|
|
|
|
if len(params) > 1 {
|
|
page, err = strconv.Atoi(params[1])
|
|
if err != nil || page < -1 || page == 0 {
|
|
if strings.ToLower(params[1]) == "all" {
|
|
page = -1
|
|
all = true
|
|
} else {
|
|
cl.sendError("Unable to reveal, invalid page specified")
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(params) > 2 {
|
|
if strings.ToLower(params[2]) == "all" {
|
|
all = true
|
|
}
|
|
}
|
|
|
|
if command == commandReveal {
|
|
s.revealChannelLog(params[0], cl.identifier, page, all)
|
|
} else {
|
|
// TODO: Audit
|
|
}
|
|
case commandKick:
|
|
if len(params) < 2 {
|
|
s.sendUsage(cl, command)
|
|
return
|
|
}
|
|
|
|
ch := s.getChannel(params[0])
|
|
if ch == nil {
|
|
cl.sendError("Unable to kick, invalid channel specified")
|
|
return
|
|
}
|
|
|
|
rcl := s.revealClient(params[0], params[1])
|
|
if rcl == nil {
|
|
cl.sendError("Unable to kick, client not found or no longer connected")
|
|
return
|
|
}
|
|
|
|
reason := "Kicked"
|
|
if len(params) > 2 {
|
|
reason = fmt.Sprintf("%s: %s", reason, strings.Join(params[2:], " "))
|
|
}
|
|
s.partChannel(ch.identifier, rcl.identifier, reason)
|
|
cl.sendMessage(fmt.Sprintf("Kicked %s %s", params[0], params[1]))
|
|
case commandBan, commandKill:
|
|
if len(params) < 3 {
|
|
s.sendUsage(cl, command)
|
|
return
|
|
}
|
|
|
|
ch := s.getChannel(params[0])
|
|
if ch == nil {
|
|
cl.sendError(fmt.Sprintf("Unable to %s, invalid channel specified", strings.ToLower(command)))
|
|
return
|
|
}
|
|
|
|
rcl := s.revealClient(params[0], params[1])
|
|
if rcl == nil {
|
|
cl.sendError(fmt.Sprintf("Unable to %s, client not found or no longer connected", strings.ToLower(command)))
|
|
return
|
|
}
|
|
|
|
expires := parseDuration(params[2])
|
|
if expires < 0 {
|
|
cl.sendError(fmt.Sprintf("Unable to %s, invalid duration supplied", strings.ToLower(command)))
|
|
return
|
|
} else if expires > 0 {
|
|
expires = time.Now().Unix() + expires
|
|
}
|
|
|
|
reason := ""
|
|
if len(params) > 3 {
|
|
reason = strings.Join(params[3:], " ")
|
|
}
|
|
|
|
bch := ch.identifier
|
|
if command == commandKill {
|
|
bch = channelServer
|
|
}
|
|
err := s.ban(bch, rcl.iphash, rcl.account, expires, reason)
|
|
if err != nil {
|
|
cl.sendError(fmt.Sprintf("Unable to %s, %v", strings.ToLower(command), err))
|
|
return
|
|
}
|
|
|
|
cl.sendMessage(fmt.Sprintf("%sed %s %s", strings.Title(strings.ToLower(command)), params[0], params[1]))
|
|
case commandStats:
|
|
cl.sendMessage(fmt.Sprintf("%d clients in %d channels", s.clientCount(), s.channelCount()))
|
|
case commandRehash:
|
|
|
|
err := s.reload()
|
|
if err != nil {
|
|
cl.sendError(err.Error())
|
|
} else {
|
|
cl.sendMessage("Reloaded configuration")
|
|
}
|
|
case commandUpgrade:
|
|
// TODO
|
|
}
|
|
}
|
|
|
|
func (s *Server) handlePrivmsg(target string, client string, message string) {
|
|
cl := s.getClient(client)
|
|
if cl == nil || len(target) == 0 {
|
|
return
|
|
}
|
|
|
|
if strings.ToLower(target) == "anonirc" {
|
|
params := strings.Split(message, " ")
|
|
if len(params) == 0 || len(params[0]) == 0 {
|
|
return
|
|
}
|
|
|
|
var otherparams []string
|
|
if len(params) > 1 {
|
|
otherparams = params[1:]
|
|
}
|
|
|
|
s.handleUserCommand(client, params[0], otherparams)
|
|
return
|
|
} else if target[0] != '#' && target[0] != '&' {
|
|
cl.writeMessage(irc.ERR_NOSUCHNICK, []string{target, "No such nick/channel"})
|
|
return
|
|
} else if !s.inChannel(target, client) {
|
|
cl.writeMessage(irc.ERR_CANNOTSENDTOCHAN, []string{target, fmt.Sprintf("No external channel messages (%s)", target)})
|
|
return
|
|
}
|
|
|
|
ch := s.getChannel(target)
|
|
if ch == nil {
|
|
return
|
|
} else if ch.hasMode("m") && cl.getPermission(target) < permissionVIP {
|
|
cl.writeMessage(irc.ERR_CANNOTSENDTOCHAN, []string{target, fmt.Sprintf("Channel is moderated, only VIP may speak (%s)", target)})
|
|
return
|
|
}
|
|
|
|
s.updateClientCount(target, "", "")
|
|
ch.clients.Range(func(k, v interface{}) bool {
|
|
chcl := s.getClient(k.(string))
|
|
if chcl != nil && chcl.identifier != client {
|
|
chcl.write(&prefixAnonymous, irc.PRIVMSG, []string{target, message})
|
|
}
|
|
|
|
return true
|
|
})
|
|
ch.Log(cl, "CHAT", message)
|
|
}
|
|
|
|
func (s *Server) handleRead(c *Client) {
|
|
for {
|
|
if c.state == stateTerminating {
|
|
return
|
|
}
|
|
|
|
if _, ok := s.clients.Load(c.identifier); !ok {
|
|
s.killClient(c, "")
|
|
return
|
|
}
|
|
|
|
c.conn.SetReadDeadline(time.Now().Add(300 * time.Second))
|
|
msg, err := c.reader.Decode()
|
|
if c.state == stateTerminating {
|
|
return
|
|
} else if msg == nil || err != nil {
|
|
// Error decoding message, client probably disconnected
|
|
s.killClient(c, "")
|
|
return
|
|
}
|
|
|
|
if debugMode && (verbose || len(msg.Command) < 4 || (msg.Command[0:4] != irc.PING && msg.Command[0:4] != irc.PONG)) {
|
|
log.Printf("%s -> %s", c.identifier, msg)
|
|
}
|
|
|
|
if msg.Command == irc.NICK && c.nick == "*" && len(msg.Params) > 0 && len(msg.Params[0]) > 0 && msg.Params[0] != "" && msg.Params[0] != "*" {
|
|
c.nick = strings.Trim(msg.Params[0], "\"")
|
|
} else if msg.Command == irc.USER && c.user == "" && len(msg.Params) >= 3 && msg.Params[0] != "" && msg.Params[2] != "" {
|
|
c.user = strings.Trim(msg.Params[0], "\"")
|
|
c.host = strings.Trim(msg.Params[2], "\"")
|
|
|
|
c.writeMessage(irc.RPL_WELCOME, []string{"Welcome to AnonIRC " + c.getPrefix().String()})
|
|
c.writeMessage(irc.RPL_YOURHOST, []string{"Your host is AnonIRC, running version AnonIRCd https://code.rocketnine.space/tslocum/anonircd"})
|
|
c.writeMessage(irc.RPL_CREATED, []string{fmt.Sprintf("This server was created %s", time.Unix(s.created, 0).UTC())})
|
|
c.writeMessage(strings.Join([]string{irc.RPL_MYINFO, c.nick, "AnonIRC", "AnonIRCd", clientModes, channelModes, channelModesWithArg}, " "), []string{})
|
|
|
|
for i, motdmsg := range s.motd {
|
|
var motdcode string
|
|
if i == 0 {
|
|
motdcode = irc.RPL_MOTDSTART
|
|
} else if i < len(s.motd)-1 {
|
|
motdcode = irc.RPL_MOTD
|
|
} else {
|
|
motdcode = irc.RPL_ENDOFMOTD
|
|
}
|
|
c.writeMessage(motdcode, []string{" " + motdmsg})
|
|
}
|
|
|
|
s.joinChannel(c.identifier, channelLobby, "")
|
|
if c.globalPermission() >= permissionVIP {
|
|
s.joinChannel(c.identifier, channelServer, "")
|
|
}
|
|
} else if msg.Command == irc.PASS && c.user == "" && len(msg.Params) > 0 && len(msg.Params[0]) > 0 {
|
|
// TODO: Add auth and multiple failed attempts ban
|
|
authSuccess := false
|
|
psplit := strings.SplitN(msg.Params[0], ":", 2)
|
|
if len(psplit) == 2 {
|
|
authSuccess = c.identify(psplit[0], psplit[1])
|
|
}
|
|
|
|
if !authSuccess {
|
|
c.sendPasswordIncorrect()
|
|
s.killClient(c, "")
|
|
}
|
|
} else if msg.Command == irc.CAP && len(msg.Params) > 0 && len(msg.Params[0]) > 0 && msg.Params[0] == irc.CAP_LS {
|
|
c.writeMessage(irc.CAP, []string{irc.CAP_LS, "userhost-in-names"})
|
|
} else if msg.Command == irc.CAP && len(msg.Params) > 0 && len(msg.Params[0]) > 0 && msg.Params[0] == irc.CAP_REQ {
|
|
if strings.Contains(msg.Trailing(), "userhost-in-names") {
|
|
c.capHostInNames = true
|
|
}
|
|
c.writeMessage(irc.CAP, []string{irc.CAP_ACK, msg.Trailing()})
|
|
} else if msg.Command == irc.CAP && len(msg.Params) > 0 && len(msg.Params[0]) > 0 && msg.Params[0] == irc.CAP_LIST {
|
|
caps := []string{}
|
|
if c.capHostInNames {
|
|
caps = append(caps, "userhost-in-names")
|
|
}
|
|
c.writeMessage(irc.CAP, []string{irc.CAP_LIST, strings.Join(caps, " ")})
|
|
} else if msg.Command == irc.PING {
|
|
c.writeMessage(irc.PONG+" AnonIRC", []string{msg.Trailing()})
|
|
} else if c.user == "" {
|
|
// Client must send USER before issuing remaining commands.
|
|
// Ignore command.
|
|
continue
|
|
} else if msg.Command == irc.WHOIS && len(msg.Params) > 0 && len(msg.Params[0]) >= len(prefixAnonymous.Name) && strings.ToLower(msg.Params[0][:len(prefixAnonymous.Name)]) == strings.ToLower(prefixAnonymous.Name) {
|
|
go func() {
|
|
whoisindex := 1
|
|
if len(msg.Params[0]) > len(prefixAnonymous.Name) {
|
|
whoisindex, err = strconv.Atoi(msg.Params[0][len(prefixAnonymous.Name):])
|
|
if err != nil || whoisindex <= 1 {
|
|
return
|
|
}
|
|
}
|
|
|
|
whoisnick := prefixAnonymous.Name
|
|
if whoisindex > 1 {
|
|
whoisnick += strconv.Itoa(whoisindex)
|
|
}
|
|
|
|
easteregg := readOdyssey(whoisindex)
|
|
if easteregg == "" {
|
|
easteregg = "I am the owner of my actions, heir of my actions, actions are the womb (from which I have sprung), actions are my relations, actions are my protection. Whatever actions I do, good or bad, of these I shall become the heir."
|
|
}
|
|
|
|
c.writeMessage(irc.RPL_WHOISUSER, []string{whoisnick, "Anon", "IRC", "*", easteregg})
|
|
c.writeMessage(irc.RPL_ENDOFWHOIS, []string{whoisnick, "End of /WHOIS list."})
|
|
}()
|
|
} else if msg.Command == irc.ISON {
|
|
c.writeMessage(irc.RPL_ISON, []string{""})
|
|
} else if msg.Command == irc.AWAY {
|
|
if len(msg.Params) > 0 {
|
|
c.writeMessage(irc.RPL_NOWAWAY, []string{"You have been marked as being away"})
|
|
} else {
|
|
c.writeMessage(irc.RPL_UNAWAY, []string{"You are no longer marked as being away"})
|
|
}
|
|
} else if msg.Command == irc.LIST {
|
|
chans := make(map[string]int)
|
|
s.channels.Range(func(k, v interface{}) bool {
|
|
key := k.(string)
|
|
ch := v.(*Channel)
|
|
|
|
if key[0] == '&' && c.globalPermission() < permissionVIP {
|
|
return true
|
|
}
|
|
|
|
if ch == nil || ch.hasMode("p") || ch.hasMode("s") {
|
|
return true
|
|
}
|
|
|
|
chans[key] = s.anonCount(key, c.identifier)
|
|
return true
|
|
})
|
|
|
|
c.writeMessage(irc.RPL_LISTSTART, []string{"Channel", "Users Name"})
|
|
for _, pl := range sortMapByValues(chans) {
|
|
ch := s.getChannel(pl.Key)
|
|
|
|
c.writeMessage(irc.RPL_LIST, []string{pl.Key, strconv.Itoa(pl.Value), "[" + ch.printModes(ch.getModes(), nil) + "] " + ch.topic})
|
|
}
|
|
c.writeMessage(irc.RPL_LISTEND, []string{"End of /LIST"})
|
|
} else if msg.Command == irc.JOIN && len(msg.Params) > 0 && len(msg.Params[0]) > 0 {
|
|
key := ""
|
|
if len(msg.Params) > 1 {
|
|
key = msg.Params[1]
|
|
}
|
|
for _, channel := range strings.Split(msg.Params[0], ",") {
|
|
s.joinChannel(c.identifier, channel, key)
|
|
}
|
|
} else if msg.Command == irc.NAMES && len(msg.Params) > 0 && len(msg.Params[0]) > 0 {
|
|
for _, channel := range strings.Split(msg.Params[0], ",") {
|
|
s.sendNames(channel, c.identifier)
|
|
}
|
|
} else if msg.Command == irc.WHO && len(msg.Params) > 0 && len(msg.Params[0]) > 0 {
|
|
var ccount int
|
|
for _, channel := range strings.Split(msg.Params[0], ",") {
|
|
if s.inChannel(channel, c.identifier) {
|
|
ccount = s.anonCount(channel, c.identifier)
|
|
for i := 0; i < ccount; i++ {
|
|
var prfx *irc.Prefix
|
|
if i == 0 {
|
|
prfx = c.getPrefix()
|
|
} else {
|
|
prfx = s.getAnonymousPrefix(i)
|
|
}
|
|
|
|
c.writeMessage(irc.RPL_WHOREPLY, []string{channel, prfx.User, prfx.Host, "AnonIRC", prfx.Name, "H", "0 " + prefixAnonymous.Name})
|
|
}
|
|
c.writeMessage(irc.RPL_ENDOFWHO, []string{channel, "End of /WHO list."})
|
|
}
|
|
}
|
|
} else if msg.Command == irc.MODE {
|
|
s.handleMode(c, msg.Params)
|
|
} else if msg.Command == irc.TOPIC && len(msg.Params) > 0 && len(msg.Params[0]) > 0 {
|
|
if len(msg.Params) == 1 {
|
|
s.sendTopic(msg.Params[0], c.identifier, false)
|
|
} else {
|
|
s.handleTopic(msg.Params[0], c.identifier, strings.Join(msg.Params[1:], " "))
|
|
}
|
|
} else if msg.Command == irc.PRIVMSG && len(msg.Params) > 0 && len(msg.Params[0]) > 0 {
|
|
s.handlePrivmsg(msg.Params[0], c.identifier, msg.Trailing())
|
|
} else if msg.Command == irc.PART && len(msg.Params) > 0 && len(msg.Params[0]) > 0 {
|
|
for _, channel := range strings.Split(msg.Params[0], ",") {
|
|
s.partChannel(channel, c.identifier, "")
|
|
}
|
|
} else if msg.Command == irc.QUIT {
|
|
s.killClient(c, "")
|
|
} else {
|
|
s.handleUserCommand(c.identifier, msg.Command, msg.Params)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *Server) handleWrite(c *Client) {
|
|
werror := false
|
|
for msg := range c.writebuffer {
|
|
if werror {
|
|
// We experienced a write error.
|
|
c.wg.Done()
|
|
continue
|
|
}
|
|
|
|
addnick := false
|
|
if _, err := strconv.Atoi(msg.Command); err == nil {
|
|
addnick = true
|
|
} else if msg.Command == irc.CAP {
|
|
addnick = true
|
|
}
|
|
|
|
if addnick {
|
|
msg.Params = append([]string{c.nick}, msg.Params...)
|
|
}
|
|
|
|
if debugMode && (verbose || len(msg.Command) < 4 || (msg.Command[0:4] != irc.PING && msg.Command[0:4] != irc.PONG)) {
|
|
log.Printf("%s <- %s", c.identifier, msg)
|
|
}
|
|
err := c.writer.Encode(msg)
|
|
if err != nil {
|
|
werror = true
|
|
}
|
|
|
|
c.wg.Done()
|
|
}
|
|
}
|
|
|
|
func (s *Server) handleConnection(conn net.Conn, ssl bool) {
|
|
defer conn.Close()
|
|
var identifier string
|
|
|
|
for {
|
|
identifier = randomIdentifier()
|
|
if _, ok := s.clients.Load(identifier); !ok {
|
|
break
|
|
}
|
|
}
|
|
|
|
c := NewClient(identifier, conn, ssl)
|
|
banned := true
|
|
reason := ""
|
|
if c != nil {
|
|
banned, reason = c.isBanned(channelServer)
|
|
}
|
|
|
|
go s.handleWrite(c)
|
|
if !banned {
|
|
s.clients.Store(c.identifier, c)
|
|
s.handleRead(c) // Block until the connection is closed
|
|
} else {
|
|
c.sendBanned(reason)
|
|
}
|
|
|
|
s.killClient(c, "")
|
|
s.clients.Delete(identifier)
|
|
}
|
|
|
|
func (s *Server) killClient(c *Client, reason string) {
|
|
if c == nil || c.state == stateTerminating {
|
|
return
|
|
}
|
|
c.state = stateTerminating
|
|
|
|
if _, ok := s.clients.Load(c.identifier); ok {
|
|
s.partAllChannels(c.identifier, reason)
|
|
}
|
|
c.wg.Wait()
|
|
close(c.writebuffer)
|
|
c.conn.Close()
|
|
}
|
|
|
|
func (s *Server) listenPlain() {
|
|
for {
|
|
listen, err := net.Listen("tcp", ":6667")
|
|
if err != nil {
|
|
log.Printf("Failed to listen: %v", err)
|
|
time.Sleep(1 * time.Minute)
|
|
continue
|
|
}
|
|
log.Println("Listening on 6667")
|
|
|
|
accept:
|
|
for {
|
|
select {
|
|
case <-s.restartplain:
|
|
break accept
|
|
default:
|
|
conn, err := listen.Accept()
|
|
if err != nil {
|
|
log.Println("Error accepting connection:", err)
|
|
continue
|
|
}
|
|
go s.handleConnection(conn, false)
|
|
}
|
|
}
|
|
listen.Close()
|
|
}
|
|
}
|
|
|
|
func (s *Server) listenSSL() {
|
|
for {
|
|
if s.config.SSLCert == "" {
|
|
time.Sleep(1 * time.Minute)
|
|
return // SSL is disabled
|
|
}
|
|
|
|
cert, err := tls.LoadX509KeyPair(s.config.SSLCert, s.config.SSLKey)
|
|
if err != nil {
|
|
log.Printf("Failed to load SSL certificate: %v", err)
|
|
time.Sleep(1 * time.Minute)
|
|
continue
|
|
}
|
|
|
|
listen, err := tls.Listen("tcp", ":6697", &tls.Config{Certificates: []tls.Certificate{cert}})
|
|
if err != nil {
|
|
log.Printf("Failed to listen: %v", err)
|
|
time.Sleep(1 * time.Minute)
|
|
continue
|
|
}
|
|
log.Println("Listening on +6697")
|
|
|
|
accept:
|
|
for {
|
|
select {
|
|
case <-s.restartssl:
|
|
break accept
|
|
default:
|
|
conn, err := listen.Accept()
|
|
if err != nil {
|
|
log.Println("Error accepting connection:", err)
|
|
continue
|
|
}
|
|
go s.handleConnection(conn, true)
|
|
}
|
|
}
|
|
listen.Close()
|
|
}
|
|
}
|
|
|
|
func (s *Server) pingClients() {
|
|
for {
|
|
s.clients.Range(func(k, v interface{}) bool {
|
|
cl := v.(*Client)
|
|
if cl != nil {
|
|
cl.write(nil, irc.PING, []string{fmt.Sprintf("anonirc%d%d", int32(time.Now().Unix()), rand.Intn(1000))})
|
|
}
|
|
|
|
return true
|
|
})
|
|
time.Sleep(90 * time.Second)
|
|
}
|
|
}
|
|
|
|
func (s *Server) connectDatabase() {
|
|
err := db.Connect(s.config.DBDriver, s.config.DBSource)
|
|
if err != nil {
|
|
log.Panicf("%+v", err)
|
|
}
|
|
}
|
|
|
|
func (s *Server) closeDatabase() {
|
|
err := db.Close()
|
|
if err != nil {
|
|
log.Panicf("%+v", err)
|
|
}
|
|
}
|
|
|
|
func (s *Server) loadConfig() error {
|
|
if s.configfile == "" {
|
|
return errors.New("configuration file must be specified: anonircd -c /home/user/anonircd/anonircd.conf")
|
|
}
|
|
|
|
if _, err := os.Stat(s.configfile); err != nil {
|
|
return errors.New("unable to find configuration file " + s.configfile)
|
|
}
|
|
|
|
oldconfig := &*s.config
|
|
|
|
if _, err := toml.DecodeFile(s.configfile, &s.config); err != nil {
|
|
if oldconfig != nil {
|
|
s.config = oldconfig
|
|
}
|
|
|
|
return errors.New(fmt.Sprintf("Failed to read configuration file %s: %v", s.configfile, err))
|
|
}
|
|
|
|
if s.config.DBDriver == "" || s.config.DBSource == "" {
|
|
if oldconfig != nil {
|
|
s.config = oldconfig
|
|
}
|
|
|
|
return errors.New(fmt.Sprintf("DBDriver and DBSource must be configured in %s\nExample:\n\nDBDriver=\"sqlite\"\nDBSource=\"/home/user/anonircd/anonircd.db\"", s.configfile))
|
|
}
|
|
|
|
motd := defaultMOTD
|
|
if s.config.MOTD != "" {
|
|
motd = s.config.MOTD
|
|
}
|
|
s.motd = strings.Split(strings.TrimRight(motd, " \t\r\n"), "\n")
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *Server) reload() error {
|
|
log.Println("Reloading configuration...")
|
|
|
|
err := s.loadConfig()
|
|
if err != nil {
|
|
log.Println("Failed to reload configuration")
|
|
return errors.Wrap(err, "failed to reload configuration")
|
|
}
|
|
log.Println("Reloaded configuration")
|
|
|
|
s.restartplain <- true
|
|
s.restartssl <- true
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *Server) listen() {
|
|
go s.listenPlain()
|
|
go s.listenSSL()
|
|
|
|
s.pingClients()
|
|
}
|