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.

1604 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"
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://github.com/sageru-6ch/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://github.com/sageru-6ch/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 == "" {
return // Client must send USER before issuing remaining commands
} 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, stop writing
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=\"sqlite3\"\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()
}