Anonymous IRC daemon
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1605 lines
40 KiB

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"
5 years ago
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]",
5 years ago
"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>",
5 years ago
"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>",
5 years ago
"Take ownership of an unfounded channel"},
commandGrant: {"<channel> [account] [permission]",
5 years ago
"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),
5 years ago
"Page -1 shows all matching entries",
"Joins and parts are hidden by default, add 'all' to show them"},
commandAudit: {"<channel> [page]",
5 years ago
"Print channel audit log",
fmt.Sprintf("Results start at page 1, %d per page", logsPerPage),
5 years ago
"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>",
5 years ago
"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 {