mirror of
https://github.com/ergochat/ergo.git
synced 2026-05-12 18:38:10 +02:00
* Fix a race condition in persisting channel memberships for always-on clients (the asynchronous write of the client's channel memberships could precede the update to the channel's member list, resulting in the membership not being observed and written) * Ensure always-on state is flushed on shutdown (we were already flushing timestamps, because those writes are heavily debounced, but we were relying on immediate asynchronous writeback for channel memberships and similar state).
1358 lines
49 KiB
Go
1358 lines
49 KiB
Go
// Copyright (c) 2012-2014 Jeremy Latt
|
|
// Copyright (c) 2014-2015 Edmund Huber
|
|
// Copyright (c) 2016-2017 Daniel Oaks <daniel@danieloaks.net>
|
|
// released under the MIT license
|
|
|
|
package irc
|
|
|
|
import (
|
|
"fmt"
|
|
"net"
|
|
"net/http"
|
|
_ "net/http/pprof"
|
|
"os"
|
|
"os/signal"
|
|
"runtime/pprof"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/ergochat/irc-go/ircfmt"
|
|
"github.com/okzk/sdnotify"
|
|
"github.com/tidwall/buntdb"
|
|
|
|
"github.com/ergochat/ergo/irc/bunt"
|
|
"github.com/ergochat/ergo/irc/caps"
|
|
"github.com/ergochat/ergo/irc/connection_limits"
|
|
"github.com/ergochat/ergo/irc/datastore"
|
|
"github.com/ergochat/ergo/irc/flatip"
|
|
"github.com/ergochat/ergo/irc/flock"
|
|
"github.com/ergochat/ergo/irc/history"
|
|
"github.com/ergochat/ergo/irc/logger"
|
|
"github.com/ergochat/ergo/irc/modes"
|
|
"github.com/ergochat/ergo/irc/mysql"
|
|
"github.com/ergochat/ergo/irc/postgresql"
|
|
"github.com/ergochat/ergo/irc/sno"
|
|
"github.com/ergochat/ergo/irc/sqlite"
|
|
"github.com/ergochat/ergo/irc/utils"
|
|
"github.com/ergochat/ergo/irc/webpush"
|
|
)
|
|
|
|
const (
|
|
alwaysOnMaintenanceInterval = 30 * time.Minute
|
|
pushMaintenanceInterval = 24 * time.Hour
|
|
|
|
// common error line to sub values into
|
|
errorMsg = "ERROR :%s\r\n"
|
|
|
|
// we only have standard channels for now. TODO: any updates to this
|
|
// will also need to be reflected in CasefoldChannel
|
|
chanTypes = "#"
|
|
|
|
throttleMessage = "You have attempted to connect too many times within a short duration. Wait a while, and you will be able to connect."
|
|
)
|
|
|
|
var (
|
|
// whitelist of caps to serve on the STS-only listener. In particular,
|
|
// never advertise SASL, to discourage people from sending their passwords:
|
|
stsOnlyCaps = caps.NewSet(caps.STS, caps.MessageTags, caps.ServerTime, caps.Batch, caps.LabeledResponse, caps.EchoMessage, caps.Nope)
|
|
|
|
httpVerbs = utils.SetLiteral("CONNECT", "DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT", "TRACE")
|
|
|
|
unixEpoch = time.Unix(0, 0).UTC()
|
|
year2262Problem = time.Unix(0, 1<<63-1).UTC() // this is the maximum time for which (*time.Time).UnixNano() is well-defined
|
|
)
|
|
|
|
// Server is the main Oragono server.
|
|
type Server struct {
|
|
accepts AcceptManager
|
|
accounts AccountManager
|
|
channels ChannelManager
|
|
clients ClientManager
|
|
config atomic.Pointer[Config]
|
|
configFilename string
|
|
connectionLimiter connection_limits.Limiter
|
|
ctime time.Time
|
|
dlines *DLineManager
|
|
helpIndexManager HelpIndexManager
|
|
klines *KLineManager
|
|
listeners map[string]IRCListener
|
|
logger *logger.Manager
|
|
monitorManager MonitorManager
|
|
name string
|
|
nameCasefolded string
|
|
rehashMutex sync.Mutex // tier 4
|
|
rehashSignal chan os.Signal
|
|
pprofServer *http.Server
|
|
exitSignals chan os.Signal
|
|
tracebackSignal chan os.Signal
|
|
snomasks SnoManager
|
|
store *buntdb.DB
|
|
dstore datastore.Datastore
|
|
mysqlHistoryDB *mysql.MySQL
|
|
postgresHistoryDB *postgresql.PostgreSQL
|
|
sqliteHistoryDB *sqlite.SQLite
|
|
historyDB history.Database
|
|
torLimiter connection_limits.TorLimiter
|
|
whoWas WhoWasList
|
|
stats Stats
|
|
semaphores ServerSemaphores
|
|
flock flock.Flocker
|
|
connIDCounter atomic.Uint64
|
|
defcon atomic.Uint32
|
|
|
|
// API stuff
|
|
apiHandler http.Handler // always initialized
|
|
apiListener *utils.ReloadableListener
|
|
apiServer *http.Server // nil if API is not enabled
|
|
}
|
|
|
|
// NewServer returns a new Oragono server.
|
|
func NewServer(config *Config, logger *logger.Manager) (*Server, error) {
|
|
// sanity check that kernel randomness is available; on modern Linux,
|
|
// this will block until it is, on other platforms it may panic:
|
|
utils.GenerateUUIDv4()
|
|
|
|
// initialize data structures
|
|
server := &Server{
|
|
ctime: time.Now().UTC(),
|
|
listeners: make(map[string]IRCListener),
|
|
logger: logger,
|
|
rehashSignal: make(chan os.Signal, 1),
|
|
exitSignals: make(chan os.Signal, len(utils.ServerExitSignals)),
|
|
tracebackSignal: make(chan os.Signal, len(utils.ServerTracebackSignals)),
|
|
}
|
|
server.defcon.Store(5)
|
|
|
|
server.accepts.Initialize()
|
|
server.clients.Initialize()
|
|
server.semaphores.Initialize()
|
|
server.whoWas.Initialize(config.Limits.WhowasEntries)
|
|
server.monitorManager.Initialize()
|
|
server.snomasks.Initialize()
|
|
|
|
server.apiHandler = newAPIHandler(server)
|
|
|
|
if err := server.applyConfig(config); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Attempt to clean up when receiving these signals.
|
|
signal.Notify(server.exitSignals, utils.ServerExitSignals...)
|
|
signal.Notify(server.rehashSignal, syscall.SIGHUP)
|
|
if len(utils.ServerTracebackSignals) != 0 {
|
|
signal.Notify(server.tracebackSignal, utils.ServerTracebackSignals...)
|
|
}
|
|
|
|
time.AfterFunc(alwaysOnMaintenanceInterval, server.periodicAlwaysOnMaintenance)
|
|
time.AfterFunc(pushMaintenanceInterval, server.periodicPushMaintenance)
|
|
|
|
return server, nil
|
|
}
|
|
|
|
// Shutdown shuts down the server.
|
|
func (server *Server) Shutdown() {
|
|
sdnotify.Stopping()
|
|
server.logger.Info("server", "Stopping server")
|
|
|
|
for _, client := range server.clients.AllClients() {
|
|
client.Notice("Server is shutting down")
|
|
}
|
|
|
|
// flush data associated with always-on clients:
|
|
server.performAlwaysOnMaintenance(true)
|
|
|
|
if err := server.store.Close(); err != nil {
|
|
server.logger.Error("shutdown", "Could not close datastore", err.Error())
|
|
}
|
|
|
|
if err := server.historyDB.Close(); err != nil {
|
|
server.logger.Error("shutdown", "Could not close history database", err.Error())
|
|
}
|
|
server.logger.Info("server", fmt.Sprintf("%s exiting", Ver))
|
|
}
|
|
|
|
// Run starts the server.
|
|
func (server *Server) Run() {
|
|
defer server.Shutdown()
|
|
|
|
for {
|
|
select {
|
|
case <-server.exitSignals:
|
|
return
|
|
case <-server.rehashSignal:
|
|
server.logger.Info("server", "Rehashing due to SIGHUP")
|
|
go server.rehash()
|
|
case <-server.tracebackSignal:
|
|
go server.dumpStacks()
|
|
}
|
|
}
|
|
}
|
|
|
|
func (server *Server) checkBans(config *Config, ipaddr net.IP, checkScripts bool) (banned bool, requireSASL bool, message string) {
|
|
// #671: do not enforce bans against loopback, as a failsafe
|
|
// note that this function is not used for Tor connections (checkTorLimits is used instead)
|
|
if ipaddr.IsLoopback() {
|
|
return
|
|
}
|
|
|
|
if server.Defcon() == 1 {
|
|
if !utils.IPInNets(ipaddr, server.Config().Server.secureNets) {
|
|
return true, false, "New connections to this server are temporarily restricted"
|
|
}
|
|
}
|
|
|
|
flat := flatip.FromNetIP(ipaddr)
|
|
|
|
// check DLINEs
|
|
isBanned, info := server.dlines.CheckIP(flat)
|
|
if isBanned {
|
|
if info.RequireSASL {
|
|
server.logger.Info("connect-ip", "Requiring SASL from client due to d-line", ipaddr.String())
|
|
return false, true, info.BanMessage("You must authenticate with SASL to connect from this IP (%s)")
|
|
} else {
|
|
server.logger.Info("connect-ip", "Client rejected by d-line", ipaddr.String())
|
|
return true, false, info.BanMessage("You are banned from this server (%s)")
|
|
}
|
|
}
|
|
|
|
// check connection limits
|
|
err := server.connectionLimiter.AddClient(flat)
|
|
if err == connection_limits.ErrLimitExceeded {
|
|
// too many connections from one client, tell the client and close the connection
|
|
server.logger.Info("connect-ip", "Client rejected for connection limit", ipaddr.String())
|
|
return true, false, "Too many clients from your network"
|
|
} else if err == connection_limits.ErrThrottleExceeded {
|
|
server.logger.Info("connect-ip", "Client exceeded connection throttle", ipaddr.String())
|
|
return true, false, throttleMessage
|
|
} else if err != nil {
|
|
server.logger.Warning("internal", "unexpected ban result", err.Error())
|
|
}
|
|
|
|
if checkScripts && config.Server.IPCheckScript.Enabled && !config.Server.IPCheckScript.ExemptSASL {
|
|
output, err := CheckIPBan(server.semaphores.IPCheckScript, config.Server.IPCheckScript, ipaddr)
|
|
if err != nil {
|
|
server.logger.Error("internal", "couldn't check IP ban script", ipaddr.String(), err.Error())
|
|
return false, false, ""
|
|
}
|
|
// TODO: currently no way to cache IPAccepted
|
|
if (output.Result == IPBanned || output.Result == IPRequireSASL) && output.CacheSeconds != 0 {
|
|
network, err := flatip.ParseToNormalizedNet(output.CacheNet)
|
|
if err != nil {
|
|
server.logger.Error("internal", "invalid dline net from IP ban script", ipaddr.String(), output.CacheNet)
|
|
} else {
|
|
dlineDuration := time.Duration(output.CacheSeconds) * time.Second
|
|
err := server.dlines.AddNetwork(network, dlineDuration, output.Result == IPRequireSASL, output.BanMessage, "", "")
|
|
if err != nil {
|
|
server.logger.Error("internal", "couldn't set dline from IP ban script", ipaddr.String(), err.Error())
|
|
}
|
|
}
|
|
}
|
|
if output.Result == IPBanned {
|
|
// XXX roll back IP connection/throttling addition for the IP
|
|
server.connectionLimiter.RemoveClient(flat)
|
|
server.logger.Info("connect-ip", "Rejected client due to ip-check-script", ipaddr.String())
|
|
return true, false, output.BanMessage
|
|
} else if output.Result == IPRequireSASL {
|
|
server.logger.Info("connect-ip", "Requiring SASL from client due to ip-check-script", ipaddr.String())
|
|
return false, true, output.BanMessage
|
|
}
|
|
}
|
|
|
|
return false, false, ""
|
|
}
|
|
|
|
func (server *Server) checkTorLimits() (banned bool, message string) {
|
|
switch server.torLimiter.AddClient() {
|
|
case connection_limits.ErrLimitExceeded:
|
|
return true, "Too many clients from the Tor network"
|
|
case connection_limits.ErrThrottleExceeded:
|
|
return true, "Exceeded connection throttle for the Tor network"
|
|
default:
|
|
return false, ""
|
|
}
|
|
}
|
|
|
|
func (server *Server) periodicAlwaysOnMaintenance() {
|
|
defer func() {
|
|
// reschedule whether or not there was a panic
|
|
time.AfterFunc(alwaysOnMaintenanceInterval, server.periodicAlwaysOnMaintenance)
|
|
}()
|
|
|
|
defer server.HandlePanic(nil)
|
|
|
|
server.logger.Info("accounts", "Performing periodic always-on client checks")
|
|
server.performAlwaysOnMaintenance(false)
|
|
}
|
|
|
|
func (server *Server) performAlwaysOnMaintenance(shutdown bool) {
|
|
config := server.Config()
|
|
for _, client := range server.clients.AllClients() {
|
|
if !shutdown && client.IsExpiredAlwaysOn(config) {
|
|
// TODO save the channels list, use it for autojoin if/when they return?
|
|
server.logger.Info("accounts", "Expiring always-on client", client.AccountName())
|
|
client.destroy(nil)
|
|
continue
|
|
}
|
|
|
|
// synchronously flush channel memberships, etc., avoiding a race between
|
|
// immediate but asynchronous writeback of those fields and server shutdown
|
|
if shutdown && client.AlwaysOn() {
|
|
client.Store(0, shutdown)
|
|
}
|
|
|
|
// flush the timestamps (which are not written back immediately, for debouncing
|
|
// reasons), either as periodic maintenance or on shutdown
|
|
if client.shouldFlushTimestamps() {
|
|
account := client.Account()
|
|
server.accounts.saveLastSeen(account, client.copyLastSeen())
|
|
server.accounts.saveReadMarkers(account, client.copyReadMarkers())
|
|
}
|
|
}
|
|
}
|
|
|
|
func (server *Server) periodicPushMaintenance() {
|
|
defer func() {
|
|
// reschedule whether or not there was a panic
|
|
time.AfterFunc(pushMaintenanceInterval, server.periodicPushMaintenance)
|
|
}()
|
|
|
|
defer server.HandlePanic(nil)
|
|
|
|
if server.Config().WebPush.Enabled {
|
|
server.logger.Info("webpush", "Performing periodic push subscription maintenance")
|
|
server.performPushMaintenance()
|
|
} // else: reschedule and check again later, the operator may enable it via rehash
|
|
}
|
|
|
|
func (server *Server) performPushMaintenance() {
|
|
expiration := time.Duration(server.Config().WebPush.Expiration)
|
|
for _, client := range server.clients.AllWithPushSubscriptions() {
|
|
for _, sub := range client.getPushSubscriptions(true) {
|
|
now := time.Now()
|
|
// require both periodic successful push messages and renewal of the subscription via WEBPUSH REGISTER
|
|
if now.Sub(sub.LastSuccess) > expiration || now.Sub(sub.LastRefresh) > expiration {
|
|
server.logger.Debug("webpush", "expiring push subscription for client", client.Nick(), sub.Endpoint)
|
|
client.deletePushSubscription(sub.Endpoint, false)
|
|
} else if now.Sub(sub.LastSuccess) > expiration/2 {
|
|
// we haven't pushed to them recently, make an attempt
|
|
server.logger.Debug("webpush", "pinging push subscription for client", client.Nick(), sub.Endpoint)
|
|
client.sendAndTrackPush(
|
|
sub.Endpoint, sub.Keys,
|
|
pushMessage{
|
|
msg: webpush.PingMessage,
|
|
urgency: webpush.UrgencyNormal,
|
|
},
|
|
false,
|
|
)
|
|
}
|
|
}
|
|
// persist all push subscriptions on the assumption that the timestamps have changed
|
|
client.Store(IncludePushSubscriptions, false)
|
|
}
|
|
}
|
|
|
|
// handles server.ip-check-script.exempt-sasl:
|
|
// run the ip check script at the end of the handshake, only for anonymous connections
|
|
func (server *Server) checkBanScriptExemptSASL(config *Config, session *Session) (outcome AuthOutcome) {
|
|
// TODO add caching for this; see related code in (*server).checkBans;
|
|
// we should probably just put an LRU around this instead of using the DLINE system
|
|
ipaddr := session.IP()
|
|
output, err := CheckIPBan(server.semaphores.IPCheckScript, config.Server.IPCheckScript, ipaddr)
|
|
if err != nil {
|
|
server.logger.Error("internal", "couldn't check IP ban script", ipaddr.String(), err.Error())
|
|
return authSuccess
|
|
}
|
|
if output.Result == IPBanned || output.Result == IPRequireSASL {
|
|
server.logger.Info("connect-ip", session.connID, "Rejecting unauthenticated client due to ip-check-script", ipaddr.String())
|
|
if output.BanMessage != "" {
|
|
session.client.requireSASLMessage = output.BanMessage
|
|
}
|
|
return authFailSaslRequired
|
|
}
|
|
return authSuccess
|
|
}
|
|
|
|
func (server *Server) tryRegister(c *Client, session *Session) (exiting bool) {
|
|
// XXX PROXY or WEBIRC MUST be sent as the first line of the session;
|
|
// if we are here at all that means we have the final value of the IP
|
|
c.finalizeHostname(session)
|
|
|
|
// try to complete registration normally
|
|
// XXX(#1057) username can be filled in by an ident query without the client
|
|
// having sent USER: check for both username and realname to ensure they did
|
|
if c.preregNick == "" || c.username == "" || c.realname == "" || session.capState == caps.NegotiatingState {
|
|
return
|
|
}
|
|
|
|
if c.isSTSOnly {
|
|
server.playSTSBurst(session)
|
|
return true
|
|
}
|
|
|
|
// client MUST send PASS if necessary, or authenticate with SASL if necessary,
|
|
// before completing the other registration commands
|
|
config := server.Config()
|
|
authOutcome := c.isAuthorized(server, config, session, c.requireSASL)
|
|
if authOutcome == authSuccess && c.account == "" &&
|
|
config.Server.IPCheckScript.Enabled && config.Server.IPCheckScript.ExemptSASL {
|
|
authOutcome = server.checkBanScriptExemptSASL(config, session)
|
|
}
|
|
var quitMessage string
|
|
switch authOutcome {
|
|
case authFailPass:
|
|
quitMessage = c.t("Password incorrect")
|
|
c.Send(nil, server.name, ERR_PASSWDMISMATCH, "*", quitMessage)
|
|
case authFailSaslRequired, authFailTorSaslRequired:
|
|
quitMessage = c.requireSASLMessage
|
|
if quitMessage == "" {
|
|
quitMessage = c.t("You must log in with SASL to join this server")
|
|
}
|
|
c.Send(nil, c.server.name, "FAIL", "*", "ACCOUNT_REQUIRED", quitMessage)
|
|
}
|
|
if authOutcome != authSuccess {
|
|
c.Quit(quitMessage, nil)
|
|
return true
|
|
}
|
|
c.requireSASLMessage = ""
|
|
|
|
rb := NewResponseBuffer(session)
|
|
nickError := performNickChange(server, c, c, session, c.preregNick, rb)
|
|
rb.Send(true)
|
|
if nickError != nil {
|
|
c.preregNick = ""
|
|
return false
|
|
}
|
|
|
|
session.cookies = nil // auth is done, allow GC'ing these later
|
|
|
|
if session.client != c {
|
|
// reattached, bail out.
|
|
// we'll play the reg burst later, on the new goroutine associated with
|
|
// (thisSession, otherClient). This is to avoid having to transfer state
|
|
// like nickname, hostname, etc. to show the correct values in the reg burst.
|
|
return false
|
|
}
|
|
|
|
// Apply default user modes (without updating the invisible counter)
|
|
// The number of invisible users will be updated by server.stats.Register
|
|
// if we're using default user mode +i.
|
|
for _, defaultMode := range config.Accounts.defaultUserModes {
|
|
c.SetMode(defaultMode, true)
|
|
}
|
|
|
|
c.applyPreregMetadata(session)
|
|
|
|
c.server.monitorManager.AlertAbout(c.Nick(), c.NickCasefolded(), true, c)
|
|
|
|
// this is not a reattach, so if the client is always-on, this is the first time
|
|
// the Client object was created during the current server uptime. mark dirty in
|
|
// order to persist the realname and the user modes:
|
|
if c.AlwaysOn() {
|
|
c.markDirty(IncludeAllAttrs)
|
|
}
|
|
|
|
// count new user in statistics (before checking KLINEs, see #1303)
|
|
server.stats.Register(c.HasMode(modes.Invisible))
|
|
|
|
// check KLINEs (#671: ignore KLINEs for loopback connections)
|
|
if !session.IP().IsLoopback() || session.isTor {
|
|
isBanned, info := server.klines.CheckMasks(c.AllNickmasks()...)
|
|
if isBanned && !(info.RequireSASL && session.client.Account() != "") {
|
|
c.setKlined()
|
|
c.Quit(info.BanMessage(c.t("You are banned from this server (%s)")), nil)
|
|
server.logger.Info("connect", session.connID, "Client rejected by k-line", c.NickMaskString())
|
|
return true
|
|
}
|
|
}
|
|
|
|
server.playRegistrationBurst(session)
|
|
|
|
if len(config.Channels.AutoJoin) > 0 {
|
|
// only applicable to new clients, not reattaches:
|
|
server.handleAutojoins(session, config.Channels.AutoJoin)
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func (server *Server) playSTSBurst(session *Session) {
|
|
nick := utils.SafeErrorParam(session.client.preregNick)
|
|
session.Send(nil, server.name, RPL_WELCOME, nick, fmt.Sprintf("Welcome to the Internet Relay Network %s", nick))
|
|
session.Send(nil, server.name, RPL_YOURHOST, nick, fmt.Sprintf("Your host is %[1]s, running version %[2]s", server.name, "ergo"))
|
|
session.Send(nil, server.name, RPL_CREATED, nick, fmt.Sprintf("This server was created %s", time.Time{}.Format(time.RFC1123)))
|
|
session.Send(nil, server.name, RPL_MYINFO, nick, server.name, "ergo", "o", "o", "o")
|
|
session.Send(nil, server.name, RPL_ISUPPORT, nick, "CASEMAPPING=ascii", "are supported by this server")
|
|
session.Send(nil, server.name, ERR_NOMOTD, nick, "MOTD is unavailable")
|
|
for _, line := range server.Config().Server.STS.bannerLines {
|
|
session.Send(nil, server.name, "NOTICE", nick, line)
|
|
}
|
|
}
|
|
|
|
func (server *Server) playRegistrationBurst(session *Session) {
|
|
c := session.client
|
|
// continue registration
|
|
d := c.Details()
|
|
server.logger.Info("connect", session.connID, fmt.Sprintf("Client connected [%s] [u:%s] [r:%s]", d.nick, d.username, d.realname))
|
|
server.snomasks.Send(sno.LocalConnects, fmt.Sprintf("Client connected [%s] [u:%s] [h:%s] [ip:%s] [r:%s]", d.nick, d.username, session.rawHostname, session.IP().String(), d.realname))
|
|
if d.account != "" {
|
|
server.sendLoginSnomask(d.nickMask, d.accountName)
|
|
}
|
|
|
|
// send welcome text
|
|
//NOTE(dan): we specifically use the NICK here instead of the nickmask
|
|
// see http://modern.ircdocs.horse/#rplwelcome-001 for details on why we avoid using the nickmask
|
|
config := server.Config()
|
|
session.Send(nil, server.name, RPL_WELCOME, d.nick, fmt.Sprintf(c.t("Welcome to the %s IRC Network %s"), config.Network.Name, d.nick))
|
|
session.Send(nil, server.name, RPL_YOURHOST, d.nick, fmt.Sprintf(c.t("Your host is %[1]s, running version %[2]s"), server.name, Ver))
|
|
session.Send(nil, server.name, RPL_CREATED, d.nick, fmt.Sprintf(c.t("This server was created %s"), server.ctime.Format(time.RFC1123)))
|
|
rplMyInfo1, rplMyInfo2, rplMyInfo3 := modes.RplMyInfo()
|
|
session.Send(nil, server.name, RPL_MYINFO, d.nick, server.name, Ver, rplMyInfo1, rplMyInfo2, rplMyInfo3)
|
|
|
|
rb := NewResponseBuffer(session)
|
|
if !(rb.session.capabilities.Has(caps.ExtendedISupport) && rb.session.isupportSentPrereg) {
|
|
server.RplISupport(c, rb)
|
|
}
|
|
if session.capabilities.Has(caps.Metadata) {
|
|
playMetadataVerbBatch(rb, d.nick, c.ListMetadata())
|
|
}
|
|
if d.account != "" && session.capabilities.Has(caps.Persistence) {
|
|
reportPersistenceStatus(c, rb, false)
|
|
}
|
|
server.Lusers(c, rb)
|
|
server.MOTD(c, rb)
|
|
rb.Send(true)
|
|
|
|
modestring := c.ModeString()
|
|
if modestring != "+" {
|
|
session.Send(nil, server.name, RPL_UMODEIS, d.nick, modestring)
|
|
}
|
|
|
|
c.attemptAutoOper(session)
|
|
|
|
if server.logger.IsLoggingRawIO() {
|
|
session.Send(nil, c.server.name, "NOTICE", d.nick, c.t("This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect."))
|
|
}
|
|
}
|
|
|
|
// RplISupport outputs our ISUPPORT lines to the client. This is used on connection and in VERSION responses.
|
|
func (server *Server) RplISupport(client *Client, rb *ResponseBuffer) {
|
|
server.sendRplISupportLines(client, rb, server.Config().Server.isupport.CachedReply)
|
|
}
|
|
|
|
func (server *Server) sendRplISupportLines(client *Client, rb *ResponseBuffer, lines [][]string) {
|
|
if rb.session.capabilities.Has(caps.ExtendedISupport) {
|
|
batchID := rb.StartNestedBatch(nil, caps.ExtendedISupportBatchType)
|
|
defer rb.EndNestedBatch(batchID)
|
|
}
|
|
finalText := "are supported by this server"
|
|
nick := client.Nick()
|
|
for _, cachedTokenLine := range lines {
|
|
length := len(cachedTokenLine) + 2
|
|
tokenline := make([]string, length)
|
|
tokenline[0] = nick
|
|
copy(tokenline[1:], cachedTokenLine)
|
|
tokenline[length-1] = finalText
|
|
rb.Add(nil, server.name, RPL_ISUPPORT, tokenline...)
|
|
}
|
|
}
|
|
|
|
func (server *Server) Lusers(client *Client, rb *ResponseBuffer) {
|
|
nick := client.Nick()
|
|
config := server.Config()
|
|
var stats StatsValues
|
|
var numChannels int
|
|
if !config.Server.SuppressLusers || client.HasRoleCapabs("ban") {
|
|
stats = server.stats.GetValues()
|
|
numChannels = server.channels.Len()
|
|
}
|
|
|
|
rb.Add(nil, server.name, RPL_LUSERCLIENT, nick, fmt.Sprintf(client.t("There are %[1]d users and %[2]d invisible on %[3]d server(s)"), stats.Total-stats.Invisible, stats.Invisible, 1))
|
|
rb.Add(nil, server.name, RPL_LUSEROP, nick, strconv.Itoa(stats.Operators), client.t("IRC Operators online"))
|
|
rb.Add(nil, server.name, RPL_LUSERUNKNOWN, nick, strconv.Itoa(stats.Unknown), client.t("unregistered connections"))
|
|
rb.Add(nil, server.name, RPL_LUSERCHANNELS, nick, strconv.Itoa(numChannels), client.t("channels formed"))
|
|
rb.Add(nil, server.name, RPL_LUSERME, nick, fmt.Sprintf(client.t("I have %[1]d clients and %[2]d servers"), stats.Total, 0))
|
|
total := strconv.Itoa(stats.Total)
|
|
max := strconv.Itoa(stats.Max)
|
|
rb.Add(nil, server.name, RPL_LOCALUSERS, nick, total, max, fmt.Sprintf(client.t("Current local users %[1]s, max %[2]s"), total, max))
|
|
rb.Add(nil, server.name, RPL_GLOBALUSERS, nick, total, max, fmt.Sprintf(client.t("Current global users %[1]s, max %[2]s"), total, max))
|
|
}
|
|
|
|
// MOTD serves the Message of the Day.
|
|
func (server *Server) MOTD(client *Client, rb *ResponseBuffer) {
|
|
motdLines := server.Config().Server.motdLines
|
|
|
|
if len(motdLines) < 1 {
|
|
rb.Add(nil, server.name, ERR_NOMOTD, client.nick, client.t("MOTD File is missing"))
|
|
return
|
|
}
|
|
|
|
rb.Add(nil, server.name, RPL_MOTDSTART, client.nick, fmt.Sprintf(client.t("- %s Message of the day - "), server.name))
|
|
for _, line := range motdLines {
|
|
rb.Add(nil, server.name, RPL_MOTD, client.nick, line)
|
|
}
|
|
rb.Add(nil, server.name, RPL_ENDOFMOTD, client.nick, client.t("End of MOTD command"))
|
|
}
|
|
|
|
func (server *Server) handleAutojoins(session *Session, channelNames []string) {
|
|
rb := NewResponseBuffer(session)
|
|
for _, chname := range channelNames {
|
|
server.channels.Join(session.client, chname, "", false, rb)
|
|
}
|
|
rb.Send(true)
|
|
}
|
|
|
|
func (client *Client) whoisChannelsNames(target *Client, multiPrefix bool, hasPrivs bool) []string {
|
|
var chstrs []string
|
|
targetInvis := target.HasMode(modes.Invisible)
|
|
for _, channel := range target.Channels() {
|
|
if !hasPrivs && (targetInvis || channel.flags.HasMode(modes.Secret)) && !channel.hasClient(client) {
|
|
// client can't see *this* channel membership
|
|
continue
|
|
}
|
|
chstrs = append(chstrs, channel.ClientPrefixes(target, multiPrefix)+channel.name)
|
|
}
|
|
return chstrs
|
|
}
|
|
|
|
func (client *Client) getWhoisOf(target *Client, hasPrivs bool, rb *ResponseBuffer) {
|
|
oper := client.Oper()
|
|
cnick := client.Nick()
|
|
targetInfo := target.Details()
|
|
rb.Add(nil, client.server.name, RPL_WHOISUSER, cnick, targetInfo.nick, targetInfo.username, targetInfo.hostname, "*", targetInfo.realname)
|
|
tnick := targetInfo.nick
|
|
|
|
whoischannels := client.whoisChannelsNames(target, rb.session.capabilities.Has(caps.MultiPrefix), oper.HasRoleCapab("sajoin"))
|
|
if whoischannels != nil {
|
|
for _, line := range utils.BuildTokenLines(maxLastArgLength, whoischannels, " ") {
|
|
rb.Add(nil, client.server.name, RPL_WHOISCHANNELS, cnick, tnick, line)
|
|
}
|
|
}
|
|
if target.HasMode(modes.Operator) && operStatusVisible(client, target, oper != nil) {
|
|
tOper := target.Oper()
|
|
if tOper != nil {
|
|
rb.Add(nil, client.server.name, RPL_WHOISOPERATOR, cnick, tnick, tOper.WhoisLine)
|
|
}
|
|
}
|
|
if client == target || oper.HasRoleCapab("ban") {
|
|
ip, hostname := target.getWhoisActually()
|
|
rb.Add(nil, client.server.name, RPL_WHOISACTUALLY, cnick, tnick, fmt.Sprintf("%s@%s", targetInfo.username, hostname), utils.IPStringToHostname(ip.String()), client.t("Actual user@host, Actual IP"))
|
|
}
|
|
if client == target || oper.HasRoleCapab("samode") {
|
|
rb.Add(nil, client.server.name, RPL_WHOISMODES, cnick, tnick, fmt.Sprintf(client.t("is using modes +%s"), target.modes.String()))
|
|
}
|
|
if target.HasMode(modes.TLS) {
|
|
rb.Add(nil, client.server.name, RPL_WHOISSECURE, cnick, tnick, client.t("is using a secure connection"))
|
|
}
|
|
if targetInfo.accountName != "*" {
|
|
rb.Add(nil, client.server.name, RPL_WHOISACCOUNT, cnick, tnick, targetInfo.accountName, client.t("is logged in as"))
|
|
}
|
|
if target.HasMode(modes.Bot) {
|
|
rb.Add(nil, client.server.name, RPL_WHOISBOT, cnick, tnick, fmt.Sprintf(ircfmt.Unescape(client.t("is a $bBot$b on %s")), client.server.Config().Network.Name))
|
|
}
|
|
if client == target || oper.HasRoleCapab("ban") {
|
|
for _, session := range target.Sessions() {
|
|
if session.certfp != "" {
|
|
rb.Add(nil, client.server.name, RPL_WHOISCERTFP, cnick, tnick, fmt.Sprintf(client.t("has client certificate fingerprint %s"), session.certfp))
|
|
}
|
|
}
|
|
}
|
|
rb.Add(nil, client.server.name, RPL_WHOISIDLE, cnick, tnick, strconv.FormatUint(target.IdleSeconds(), 10), strconv.FormatInt(target.SignonTime(), 10), client.t("seconds idle, signon time"))
|
|
if away, awayMessage := target.Away(); away {
|
|
rb.Add(nil, client.server.name, RPL_AWAY, cnick, tnick, awayMessage)
|
|
}
|
|
if rb.session.capabilities.Has(caps.Metadata) {
|
|
for key, value := range target.ListMetadata() {
|
|
rb.Add(nil, client.server.name, RPL_WHOISKEYVALUE, cnick, tnick, key, "*", value)
|
|
}
|
|
}
|
|
}
|
|
|
|
// rehash reloads the config and applies the changes from the config file.
|
|
func (server *Server) rehash() error {
|
|
// #1570; this needs its own panic handling because it can be invoked via SIGHUP
|
|
defer server.HandlePanic(nil)
|
|
|
|
server.logger.Info("server", "Attempting rehash")
|
|
|
|
// only let one REHASH go on at a time
|
|
server.rehashMutex.Lock()
|
|
defer server.rehashMutex.Unlock()
|
|
|
|
sdnotify.Reloading()
|
|
defer sdnotify.Ready()
|
|
|
|
config, err := LoadConfig(server.configFilename)
|
|
if err != nil {
|
|
server.logger.Error("server", "failed to load config file", err.Error())
|
|
return err
|
|
}
|
|
|
|
err = server.applyConfig(config)
|
|
if err != nil {
|
|
server.logger.Error("server", "Failed to rehash", err.Error())
|
|
return err
|
|
}
|
|
|
|
server.logger.Info("server", "Rehash completed successfully")
|
|
return nil
|
|
}
|
|
|
|
func (server *Server) applyConfig(config *Config) (err error) {
|
|
oldConfig := server.Config()
|
|
initial := oldConfig == nil
|
|
|
|
if initial {
|
|
server.configFilename = config.Filename
|
|
server.name = config.Server.Name
|
|
server.nameCasefolded = config.Server.nameCasefolded
|
|
globalCasemappingSetting = config.Server.Casemapping
|
|
globalUtf8EnforcementSetting = config.Server.EnforceUtf8
|
|
MaxLineLen = config.Server.MaxLineLen
|
|
RegisterTimeout = config.Server.IdleTimeouts.Registration
|
|
PingTimeout = config.Server.IdleTimeouts.Ping
|
|
DisconnectTimeout = config.Server.IdleTimeouts.Disconnect
|
|
} else {
|
|
// enforce configs that can't be changed after launch:
|
|
if server.name != config.Server.Name {
|
|
return fmt.Errorf("Server name cannot be changed after launching the server, rehash aborted")
|
|
} else if oldConfig.Datastore.Path != config.Datastore.Path {
|
|
return fmt.Errorf("Datastore path cannot be changed after launching the server, rehash aborted")
|
|
} else if globalCasemappingSetting != config.Server.Casemapping {
|
|
return fmt.Errorf("Casemapping cannot be changed after launching the server, rehash aborted")
|
|
} else if globalUtf8EnforcementSetting != config.Server.EnforceUtf8 {
|
|
return fmt.Errorf("UTF-8 enforcement cannot be changed after launching the server, rehash aborted")
|
|
} else if oldConfig.Accounts.Multiclient.AlwaysOn != config.Accounts.Multiclient.AlwaysOn {
|
|
return fmt.Errorf("Default always-on setting cannot be changed after launching the server, rehash aborted")
|
|
} else if oldConfig.Server.Relaymsg.Enabled != config.Server.Relaymsg.Enabled {
|
|
return fmt.Errorf("Cannot enable or disable relaying after launching the server, rehash aborted")
|
|
} else if oldConfig.Server.Relaymsg.Separators != config.Server.Relaymsg.Separators {
|
|
return fmt.Errorf("Cannot change relaying separators after launching the server, rehash aborted")
|
|
} else if oldConfig.Server.IPCheckScript.MaxConcurrency != config.Server.IPCheckScript.MaxConcurrency ||
|
|
oldConfig.Accounts.AuthScript.MaxConcurrency != config.Accounts.AuthScript.MaxConcurrency {
|
|
return fmt.Errorf("Cannot change max-concurrency for scripts after launching the server, rehash aborted")
|
|
} else if oldConfig.Server.OverrideServicesHostname != config.Server.OverrideServicesHostname {
|
|
return fmt.Errorf("Cannot change override-services-hostname after launching the server, rehash aborted")
|
|
} else if !oldConfig.Datastore.MySQL.Enabled && config.Datastore.MySQL.Enabled {
|
|
return fmt.Errorf("Cannot enable MySQL after launching the server, rehash aborted")
|
|
} else if oldConfig.Server.MaxLineLen != config.Server.MaxLineLen {
|
|
return fmt.Errorf("Cannot change max-line-len after launching the server, rehash aborted")
|
|
} else if oldConfig.Server.IdleTimeouts != config.Server.IdleTimeouts {
|
|
return fmt.Errorf("Cannot change idle-timeouts after launching the server, rehash aborted")
|
|
}
|
|
}
|
|
|
|
server.logger.Info("server", "Using config file", server.configFilename)
|
|
|
|
if initial {
|
|
if config.LockFile != "" {
|
|
server.flock, err = flock.TryAcquireFlock(config.LockFile)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to acquire flock on %s: %w",
|
|
config.LockFile, err)
|
|
}
|
|
}
|
|
// the lock is never released until quit; we need to save a pointer
|
|
// to the (*flock.Flock) object so it doesn't get GC'ed, which would
|
|
// close the file and surrender the lock
|
|
}
|
|
|
|
// first, reload config sections for functionality implemented in subpackages:
|
|
wasLoggingRawIO := !initial && server.logger.IsLoggingRawIO()
|
|
err = server.logger.ApplyConfig(config.Logging)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
nowLoggingRawIO := server.logger.IsLoggingRawIO()
|
|
// notify existing clients if raw i/o logging was enabled by a rehash
|
|
sendRawOutputNotice := !wasLoggingRawIO && nowLoggingRawIO
|
|
|
|
server.connectionLimiter.ApplyConfig(&config.Server.IPLimits)
|
|
|
|
tlConf := &config.Server.TorListeners
|
|
server.torLimiter.Configure(tlConf.MaxConnections, tlConf.ThrottleDuration, tlConf.MaxConnectionsPerDuration)
|
|
|
|
// Translations
|
|
server.logger.Debug("server", "Regenerating HELP indexes for new languages")
|
|
server.helpIndexManager.GenerateIndices(config.languageManager)
|
|
|
|
if initial {
|
|
maxIPConc := int(config.Server.IPCheckScript.MaxConcurrency)
|
|
if maxIPConc != 0 {
|
|
server.semaphores.IPCheckScript = utils.NewSemaphore(maxIPConc)
|
|
}
|
|
maxAuthConc := int(config.Accounts.AuthScript.MaxConcurrency)
|
|
if maxAuthConc != 0 {
|
|
server.semaphores.AuthScript = utils.NewSemaphore(maxAuthConc)
|
|
}
|
|
|
|
if err := overrideServicePrefixes(config.Server.OverrideServicesHostname); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if oldConfig != nil {
|
|
// if certain features were enabled by rehash, we need to load the corresponding data
|
|
// from the store
|
|
if !oldConfig.Accounts.NickReservation.Enabled {
|
|
server.accounts.buildNickToAccountIndex(config)
|
|
}
|
|
// resize history buffers as needed
|
|
if config.historyChangedFrom(oldConfig) {
|
|
for _, channel := range server.channels.Channels() {
|
|
channel.resizeHistory(config)
|
|
}
|
|
for _, client := range server.clients.AllClients() {
|
|
client.resizeHistory(config)
|
|
}
|
|
}
|
|
if oldConfig.Accounts.Registration.Throttling != config.Accounts.Registration.Throttling {
|
|
server.accounts.resetRegisterThrottle(config)
|
|
}
|
|
}
|
|
|
|
server.logger.Info("server", "Using datastore", config.Datastore.Path)
|
|
if initial {
|
|
if err := server.loadDatastore(config); err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
if config.Datastore.MySQL.Enabled && server.mysqlHistoryDB != nil {
|
|
if config.Datastore.MySQL != oldConfig.Datastore.MySQL {
|
|
server.mysqlHistoryDB.SetConfig(config.Datastore.MySQL)
|
|
}
|
|
}
|
|
if config.Datastore.PostgreSQL.Enabled && server.postgresHistoryDB != nil {
|
|
if config.Datastore.PostgreSQL != oldConfig.Datastore.PostgreSQL {
|
|
server.postgresHistoryDB.SetConfig(config.Datastore.PostgreSQL)
|
|
}
|
|
}
|
|
if config.Datastore.SQLite.Enabled && server.sqliteHistoryDB != nil {
|
|
if config.Datastore.SQLite != oldConfig.Datastore.SQLite {
|
|
server.sqliteHistoryDB.SetConfig(config.Datastore.SQLite)
|
|
}
|
|
}
|
|
}
|
|
|
|
// now that the datastore is initialized, we can load the cloak secret from it
|
|
// XXX this modifies config after the initial load, which is naughty,
|
|
// but there's no data race because we haven't done SetConfig yet
|
|
cloakSecret, err := LoadCloakSecret(server.dstore)
|
|
if err != nil {
|
|
return fmt.Errorf("Could not load cloak secret: %w", err)
|
|
}
|
|
config.Server.Cloaks.SetSecret(cloakSecret)
|
|
// similarly bring the VAPID keys into the config, which requires regenerating the 005
|
|
if config.WebPush.Enabled {
|
|
config.WebPush.vapidKeys, err = LoadVAPIDKeys(server.dstore)
|
|
if err != nil {
|
|
return fmt.Errorf("Could not load VAPID keys: %w", err)
|
|
}
|
|
if err = config.generateISupport(); err != nil {
|
|
return fmt.Errorf("Could not regenerate cached 005 for VAPID: %w", err)
|
|
}
|
|
}
|
|
|
|
// activate the new config
|
|
server.config.Store(config)
|
|
|
|
// load [dk]-lines, registered users and channels, etc.
|
|
if initial {
|
|
if err := server.loadFromDatastore(config); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// burst new and removed caps
|
|
addedCaps, removedCaps := config.Diff(oldConfig)
|
|
var capBurstSessions []*Session
|
|
added := make(map[caps.Version][]string)
|
|
var removed []string
|
|
|
|
if !addedCaps.Empty() || !removedCaps.Empty() {
|
|
capBurstSessions = server.clients.AllWithCapsNotify()
|
|
|
|
added[caps.Cap301] = addedCaps.Strings(caps.Cap301, config.Server.capValues, 0)
|
|
added[caps.Cap302] = addedCaps.Strings(caps.Cap302, config.Server.capValues, 0)
|
|
// removed never has values, so we leave it as Cap301
|
|
removed = removedCaps.Strings(caps.Cap301, config.Server.capValues, 0)
|
|
}
|
|
|
|
for _, sSession := range capBurstSessions {
|
|
// DEL caps and then send NEW ones so that updated caps get removed/added correctly
|
|
if !removedCaps.Empty() {
|
|
for _, capStr := range removed {
|
|
sSession.Send(nil, server.name, "CAP", sSession.client.Nick(), "DEL", capStr)
|
|
}
|
|
}
|
|
if !addedCaps.Empty() {
|
|
for _, capStr := range added[sSession.capVersion] {
|
|
sSession.Send(nil, server.name, "CAP", sSession.client.Nick(), "NEW", capStr)
|
|
}
|
|
}
|
|
}
|
|
|
|
server.setupPprofListener(config)
|
|
|
|
server.setupAPIListener(config)
|
|
|
|
// set RPL_ISUPPORT
|
|
var newISupportReplies [][]string
|
|
if oldConfig != nil {
|
|
newISupportReplies = oldConfig.Server.isupport.GetDifference(&config.Server.isupport)
|
|
}
|
|
|
|
if len(config.Server.ProxyAllowedFrom) != 0 {
|
|
server.logger.Info("server", "Proxied IPs will be accepted from", strings.Join(config.Server.ProxyAllowedFrom, ", "))
|
|
}
|
|
|
|
// we are now ready to receive connections:
|
|
err = server.setupListeners(config)
|
|
|
|
if initial && err == nil {
|
|
server.logger.Info("server", "Server running")
|
|
sdnotify.Ready()
|
|
}
|
|
|
|
if !initial {
|
|
// send 005 updates (somewhat rare)
|
|
if len(newISupportReplies) != 0 {
|
|
for _, sClient := range server.clients.AllClients() {
|
|
for _, session := range sClient.Sessions() {
|
|
rb := NewResponseBuffer(session)
|
|
server.sendRplISupportLines(sClient, rb, newISupportReplies)
|
|
rb.Send(false)
|
|
}
|
|
}
|
|
}
|
|
|
|
if sendRawOutputNotice {
|
|
for _, sClient := range server.clients.AllClients() {
|
|
sClient.Notice(sClient.t("This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect."))
|
|
}
|
|
}
|
|
}
|
|
|
|
// send other config warnings
|
|
if config.Accounts.RequireSasl.Enabled && config.Accounts.Registration.Enabled {
|
|
server.logger.Warning("server", "Warning: although require-sasl is enabled, users can still register accounts. If your server is not intended to be public, you must set accounts.registration.enabled to false.")
|
|
}
|
|
if config.History.Enabled && config.History.ChathistoryMax == 0 {
|
|
server.logger.Warning("server", "Warning: for history to work correctly, you must set history.chathistory-maxmessages (see default.yaml for a recommendation).")
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
func (server *Server) setupPprofListener(config *Config) {
|
|
pprofListener := config.Debug.PprofListener
|
|
if server.pprofServer != nil {
|
|
if pprofListener == "" || (pprofListener != server.pprofServer.Addr) {
|
|
server.logger.Info("server", "Stopping pprof listener", server.pprofServer.Addr)
|
|
server.pprofServer.Close()
|
|
server.pprofServer = nil
|
|
}
|
|
}
|
|
if pprofListener != "" && server.pprofServer == nil {
|
|
ps := http.Server{
|
|
Addr: pprofListener,
|
|
}
|
|
go func() {
|
|
if err := ps.ListenAndServe(); err != nil {
|
|
server.logger.Error("server", "pprof listener failed", err.Error())
|
|
}
|
|
}()
|
|
server.pprofServer = &ps
|
|
server.logger.Info("server", "Started pprof listener", server.pprofServer.Addr)
|
|
}
|
|
}
|
|
|
|
func (server *Server) setupAPIListener(config *Config) {
|
|
if server.apiServer != nil {
|
|
if !config.API.Enabled || (config.API.Listener != server.apiServer.Addr) {
|
|
server.logger.Info("server", "Stopping API listener", server.apiServer.Addr)
|
|
server.apiServer.Close()
|
|
server.apiListener = nil
|
|
server.apiServer = nil
|
|
}
|
|
}
|
|
if !config.API.Enabled {
|
|
return
|
|
}
|
|
listenerConfig := utils.ListenerConfig{
|
|
TLSConfig: config.API.tlsConfig,
|
|
}
|
|
if server.apiListener != nil {
|
|
server.apiListener.Reload(listenerConfig)
|
|
return
|
|
}
|
|
listener, err := net.Listen("tcp", config.API.Listener)
|
|
if err != nil {
|
|
server.logger.Error("server", "Couldn't create API listener", config.API.Listener, err.Error())
|
|
return
|
|
}
|
|
server.apiListener = utils.NewReloadableListener(listener, listenerConfig)
|
|
server.apiServer = &http.Server{
|
|
Addr: config.API.Listener, // just informational since we created the listener ourselves
|
|
Handler: server.apiHandler,
|
|
ReadTimeout: 10 * time.Second,
|
|
WriteTimeout: 10 * time.Second,
|
|
MaxHeaderBytes: 16384,
|
|
}
|
|
go func(hs *http.Server, listener net.Listener) {
|
|
if err := hs.Serve(listener); err != nil {
|
|
server.logger.Error("server", "API listener failed", err.Error())
|
|
}
|
|
}(server.apiServer, server.apiListener)
|
|
server.logger.Info("server", "Started API listener", server.apiServer.Addr)
|
|
}
|
|
|
|
func (server *Server) loadDatastore(config *Config) error {
|
|
// open the datastore and load server state for which it (rather than config)
|
|
// is the source of truth
|
|
|
|
_, err := os.Stat(config.Datastore.Path)
|
|
if os.IsNotExist(err) {
|
|
server.logger.Warning("server", "database does not exist, creating it", config.Datastore.Path)
|
|
err = initializeDB(config.Datastore.Path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
db, err := OpenDatabase(config)
|
|
if err == nil {
|
|
server.store = db
|
|
server.dstore = bunt.NewBuntdbDatastore(db, server.logger)
|
|
return nil
|
|
} else {
|
|
return fmt.Errorf("Failed to open datastore: %s", err.Error())
|
|
}
|
|
}
|
|
|
|
func (server *Server) loadFromDatastore(config *Config) (err error) {
|
|
// load *lines (from the datastores)
|
|
server.logger.Debug("server", "Loading D/Klines")
|
|
server.loadDLines()
|
|
server.loadKLines()
|
|
|
|
server.channels.Initialize(server, config)
|
|
server.accounts.Initialize(server)
|
|
|
|
if config.Datastore.MySQL.Enabled {
|
|
server.mysqlHistoryDB, err = mysql.NewMySQLDatabase(server.logger, config.Datastore.MySQL)
|
|
if err != nil {
|
|
server.logger.Error("internal", "could not connect to mysql", err.Error())
|
|
return err
|
|
}
|
|
server.historyDB = server.mysqlHistoryDB
|
|
} else if config.Datastore.PostgreSQL.Enabled {
|
|
server.postgresHistoryDB, err = postgresql.NewPostgreSQLDatabase(server.logger, config.Datastore.PostgreSQL)
|
|
if err != nil {
|
|
server.logger.Error("internal", "could not connect to postgresql", err.Error())
|
|
return err
|
|
}
|
|
server.historyDB = server.postgresHistoryDB
|
|
} else if config.Datastore.SQLite.Enabled {
|
|
server.sqliteHistoryDB, err = sqlite.NewSQLiteDatabase(server.logger, config.Datastore.SQLite)
|
|
if err != nil {
|
|
server.logger.Error("internal", "could not open sqlite database", err.Error())
|
|
return err
|
|
}
|
|
server.historyDB = server.sqliteHistoryDB
|
|
} else {
|
|
server.historyDB = history.NewNoopDatabase()
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (server *Server) setupListeners(config *Config) (err error) {
|
|
logListener := func(addr string, config utils.ListenerConfig) {
|
|
server.logger.Info("listeners",
|
|
fmt.Sprintf("now listening on %s, tls=%t, proxy=%t, tor=%t, websocket=%t.", addr, (config.TLSConfig != nil), config.RequireProxy, config.Tor, config.WebSocket),
|
|
)
|
|
}
|
|
|
|
// update or destroy all existing listeners
|
|
for addr := range server.listeners {
|
|
currentListener := server.listeners[addr]
|
|
newConfig, stillConfigured := config.Server.trueListeners[addr]
|
|
|
|
if stillConfigured {
|
|
if reloadErr := currentListener.Reload(newConfig); reloadErr == nil {
|
|
logListener(addr, newConfig)
|
|
} else {
|
|
// stop the listener; we will attempt to replace it below
|
|
currentListener.Stop()
|
|
delete(server.listeners, addr)
|
|
}
|
|
} else {
|
|
currentListener.Stop()
|
|
delete(server.listeners, addr)
|
|
server.logger.Info("listeners", fmt.Sprintf("stopped listening on %s.", addr))
|
|
}
|
|
}
|
|
|
|
publicPlaintextListener := ""
|
|
// create new listeners that were not previously configured,
|
|
// or that couldn't be reloaded above:
|
|
for newAddr, newConfig := range config.Server.trueListeners {
|
|
if strings.HasPrefix(newAddr, ":") && !newConfig.Tor && !newConfig.STSOnly && newConfig.TLSConfig == nil {
|
|
publicPlaintextListener = newAddr
|
|
}
|
|
_, exists := server.listeners[newAddr]
|
|
if !exists {
|
|
// make a new listener
|
|
newListener, newErr := NewListener(server, newAddr, newConfig, config.Server.UnixBindMode)
|
|
if newErr == nil {
|
|
server.listeners[newAddr] = newListener
|
|
logListener(newAddr, newConfig)
|
|
} else {
|
|
server.logger.Error("server", "couldn't listen on", newAddr, newErr.Error())
|
|
err = newErr
|
|
}
|
|
}
|
|
}
|
|
|
|
if publicPlaintextListener != "" {
|
|
server.logger.Warning("listeners", fmt.Sprintf("Warning: your server is configured with public plaintext listener %s. Consider disabling it for improved security and privacy.", publicPlaintextListener))
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// Gets the abstract sequence from which we're going to query history;
|
|
// we may already know the channel we're querying, or we may have
|
|
// to look it up via a string query. This function is responsible for
|
|
// privilege checking.
|
|
func (server *Server) GetHistorySequence(providedChannel *Channel, client *Client, query string) (channel *Channel, sequence history.Sequence, err error) {
|
|
config := server.Config()
|
|
// 4 cases: {persistent, ephemeral} x {normal, conversation}
|
|
// with ephemeral history, target is implicit in the choice of `hist`,
|
|
// and correspondent is "" if we're retrieving a channel or *, and the correspondent's name
|
|
// if we're retrieving a DM conversation ("query buffer"). with persistent history,
|
|
// target is always nonempty, and correspondent is either empty or nonempty as before.
|
|
var status HistoryStatus
|
|
var target, correspondent string
|
|
var hist *history.Buffer
|
|
restriction := HistoryCutoffNone
|
|
channel = providedChannel
|
|
if channel == nil {
|
|
if strings.HasPrefix(query, "#") {
|
|
channel = server.channels.Get(query)
|
|
if channel == nil {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
var joinTimeCutoff time.Time
|
|
if channel != nil {
|
|
if present, cutoff := channel.joinTimeCutoff(client); present {
|
|
joinTimeCutoff = cutoff
|
|
} else {
|
|
err = errInsufficientPrivs
|
|
return
|
|
}
|
|
status, target, restriction = channel.historyStatus(config)
|
|
switch status {
|
|
case HistoryEphemeral:
|
|
hist = &channel.history
|
|
case HistoryPersistent:
|
|
// already set `target`
|
|
default:
|
|
return
|
|
}
|
|
} else {
|
|
status, target = client.historyStatus(config)
|
|
if query != "" {
|
|
correspondent, err = CasefoldName(query)
|
|
if err != nil {
|
|
return
|
|
}
|
|
}
|
|
switch status {
|
|
case HistoryEphemeral:
|
|
hist = &client.history
|
|
case HistoryPersistent:
|
|
// already set `target`, and `correspondent` if necessary
|
|
default:
|
|
return
|
|
}
|
|
}
|
|
|
|
var cutoff time.Time
|
|
// #1593: cutoff is ignored for operators
|
|
if !client.HasRoleCapabs("history") {
|
|
if config.History.Restrictions.ExpireTime != 0 {
|
|
cutoff = time.Now().UTC().Add(-time.Duration(config.History.Restrictions.ExpireTime))
|
|
}
|
|
// #836: registration date cutoff is always enforced for DMs
|
|
// either way, take the later of the two cutoffs
|
|
if restriction == HistoryCutoffRegistrationTime || channel == nil {
|
|
regCutoff := client.historyCutoff()
|
|
if regCutoff.After(cutoff) {
|
|
cutoff = regCutoff
|
|
}
|
|
} else if restriction == HistoryCutoffJoinTime {
|
|
if joinTimeCutoff.After(cutoff) {
|
|
cutoff = joinTimeCutoff
|
|
}
|
|
}
|
|
|
|
// #836 again: grace period is never applied to DMs
|
|
if !cutoff.IsZero() && channel != nil && restriction != HistoryCutoffJoinTime {
|
|
cutoff = cutoff.Add(-time.Duration(config.History.Restrictions.GracePeriod))
|
|
}
|
|
}
|
|
|
|
if hist != nil {
|
|
sequence = hist.MakeSequence(correspondent, cutoff)
|
|
} else if target != "" {
|
|
sequence = server.historyDB.MakeSequence(target, correspondent, cutoff)
|
|
}
|
|
return
|
|
}
|
|
|
|
func (server *Server) ForgetHistory(accountName string) {
|
|
// sanity check
|
|
if accountName == "*" {
|
|
return
|
|
}
|
|
|
|
config := server.Config()
|
|
if !config.History.Enabled {
|
|
return
|
|
}
|
|
|
|
if cfAccount, err := CasefoldName(accountName); err == nil {
|
|
server.historyDB.Forget(cfAccount)
|
|
}
|
|
|
|
persistent := config.History.Persistent
|
|
if persistent.Enabled && persistent.UnregisteredChannels && persistent.RegisteredChannels == PersistentMandatory && persistent.DirectMessages == PersistentMandatory {
|
|
return
|
|
}
|
|
|
|
predicate := func(item *history.Item) bool { return item.AccountName == accountName }
|
|
|
|
for _, channel := range server.channels.Channels() {
|
|
channel.history.Delete(predicate)
|
|
}
|
|
|
|
for _, client := range server.clients.AllClients() {
|
|
client.history.Delete(predicate)
|
|
}
|
|
}
|
|
|
|
// deletes a message. target is a hint about what buffer it's in (not required for
|
|
// persistent history, where all the msgids are indexed together). if accountName
|
|
// is anything other than "*", it must match the recorded AccountName of the message
|
|
func (server *Server) DeleteMessage(target, msgid, accountName string) (err error) {
|
|
config := server.Config()
|
|
var hist *history.Buffer
|
|
|
|
if target != "" {
|
|
if target[0] == '#' {
|
|
channel := server.channels.Get(target)
|
|
if channel != nil {
|
|
if status, _, _ := channel.historyStatus(config); status == HistoryEphemeral {
|
|
hist = &channel.history
|
|
}
|
|
}
|
|
} else {
|
|
client := server.clients.Get(target)
|
|
if client != nil {
|
|
if status, _ := client.historyStatus(config); status == HistoryEphemeral {
|
|
hist = &client.history
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if hist == nil {
|
|
err = server.historyDB.DeleteMsgid(msgid, accountName)
|
|
} else {
|
|
count := hist.Delete(func(item *history.Item) bool {
|
|
return item.Message.Msgid == msgid && (accountName == "*" || item.AccountName == accountName)
|
|
})
|
|
if count == 0 {
|
|
err = history.ErrNotFound
|
|
}
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func (server *Server) UnfoldName(cfname string) (name string) {
|
|
if strings.HasPrefix(cfname, "#") {
|
|
return server.channels.UnfoldName(cfname)
|
|
}
|
|
return server.clients.UnfoldNick(cfname)
|
|
}
|
|
|
|
// generateConnectionID generates a unique string identifier for an incoming connection.
|
|
// this identifier is only used for debug logging.
|
|
func (server *Server) generateConnectionID() string {
|
|
id := server.connIDCounter.Add(1)
|
|
// pad with leading zeroes to a minimum length of 5 hex digits. this enhances greppability;
|
|
// the identifier length will be 6 for the first 1048576 connections, which is less important
|
|
// but makes the log slightly easier to read
|
|
return fmt.Sprintf("s%05x", id)
|
|
}
|
|
|
|
// elistMatcher takes and matches ELIST conditions
|
|
type elistMatcher struct {
|
|
MinClientsActive bool
|
|
MinClients int
|
|
MaxClientsActive bool
|
|
MaxClients int
|
|
}
|
|
|
|
// Matches checks whether the given channel matches our matches.
|
|
func (matcher *elistMatcher) Matches(channel *Channel) bool {
|
|
if matcher.MinClientsActive {
|
|
if len(channel.Members()) < matcher.MinClients {
|
|
return false
|
|
}
|
|
}
|
|
|
|
if matcher.MaxClientsActive {
|
|
if len(channel.Members()) > matcher.MaxClients {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
var (
|
|
infoString1 = strings.Split(`
|
|
__ __ ______ ___ ______ ___
|
|
__/ // /_/ ____/ __ \/ ____/ __ \
|
|
/_ // __/ __/ / /_/ / / __/ / / /
|
|
/_ // __/ /___/ _, _/ /_/ / /_/ /
|
|
/_//_/ /_____/_/ |_|\____/\____/
|
|
|
|
https://ergo.chat/
|
|
https://github.com/ergochat/ergo
|
|
`, "\n")[1:] // XXX: cut off initial blank line
|
|
infoString2 = strings.Split(` Daniel Oakley, DanielOaks, <daniel@danieloaks.net>
|
|
Shivaram Lingamneni, slingamn, <slingamn@cs.stanford.edu>
|
|
`, "\n")
|
|
infoString3 = strings.Split(` Jeremy Latt, jlatt
|
|
Edmund Huber, edmund-huber
|
|
`, "\n")
|
|
)
|
|
|
|
func (server *Server) dumpStacks() {
|
|
if gprof := pprof.Lookup("goroutine"); gprof != nil {
|
|
gprof.WriteTo(os.Stderr, 2)
|
|
} else {
|
|
server.logger.Error("internal", "unable to dump goroutine stacks")
|
|
}
|
|
}
|