add NICKSERV SESSIONS command to list sessions

This commit is contained in:
Shivaram Lingamneni 2019-05-08 04:11:54 -04:00
parent b11bf503e7
commit da656c07c8
6 changed files with 122 additions and 22 deletions

View File

@ -94,7 +94,14 @@ type Client struct {
type Session struct { type Session struct {
client *Client client *Client
socket *Socket ctime time.Time
atime time.Time
socket *Socket
realIP net.IP
proxiedIP net.IP
rawHostname string
idletimer IdleTimer idletimer IdleTimer
fakelag Fakelag fakelag Fakelag
@ -104,9 +111,6 @@ type Session struct {
maxlenRest uint32 maxlenRest uint32
capState caps.State capState caps.State
capVersion caps.Version capVersion caps.Version
// TODO track per-connection real IP, proxied IP, and hostname here,
// so we can list attached sessions and their details
} }
// sets the session quit message, if there isn't one already // sets the session quit message, if there isn't one already
@ -187,6 +191,8 @@ func RunNewClient(server *Server, conn clientConn) {
socket: socket, socket: socket,
capVersion: caps.Cap301, capVersion: caps.Cap301,
capState: caps.NoneState, capState: caps.NoneState,
ctime: now,
atime: now,
} }
session.SetMaxlenRest() session.SetMaxlenRest()
client.sessions = []*Session{session} client.sessions = []*Session{session}
@ -197,20 +203,29 @@ func RunNewClient(server *Server, conn clientConn) {
client.certfp, _ = socket.CertFP() client.certfp, _ = socket.CertFP()
} }
remoteAddr := conn.Conn.RemoteAddr()
if conn.IsTor { if conn.IsTor {
client.SetMode(modes.TLS, true) client.SetMode(modes.TLS, true)
client.realIP = utils.IPv4LoopbackAddress session.realIP = utils.AddrToIP(remoteAddr)
client.rawHostname = config.Server.TorListeners.Vhost // cover up details of the tor proxying infrastructure (not a user privacy concern,
// but a hardening measure):
session.proxiedIP = utils.IPv4LoopbackAddress
session.rawHostname = config.Server.TorListeners.Vhost
} else { } else {
remoteAddr := conn.Conn.RemoteAddr() session.realIP = utils.AddrToIP(remoteAddr)
client.realIP = utils.AddrToIP(remoteAddr) // set the hostname for this client (may be overridden later by PROXY or WEBIRC)
// Set the hostname for this client session.rawHostname = utils.LookupHostname(session.realIP.String())
// (may be overridden by a later PROXY command from stunnel) if utils.AddrIsLocal(remoteAddr) {
client.rawHostname = utils.LookupHostname(client.realIP.String()) // treat local connections as secure (may be overridden later by WEBIRC)
client.SetMode(modes.TLS, true)
}
if config.Server.CheckIdent && !utils.AddrIsUnix(remoteAddr) { if config.Server.CheckIdent && !utils.AddrIsUnix(remoteAddr) {
client.doIdentLookup(conn.Conn) client.doIdentLookup(conn.Conn)
} }
} }
client.realIP = session.realIP
client.rawHostname = session.rawHostname
client.proxiedIP = session.proxiedIP
client.run(session) client.run(session)
} }
@ -389,10 +404,13 @@ func (session *Session) playReattachMessages() {
// //
// Active updates when the client was last 'active' (i.e. the user should be sitting in front of their client). // Active updates when the client was last 'active' (i.e. the user should be sitting in front of their client).
func (client *Client) Active() { func (client *Client) Active(session *Session) {
// TODO normalize all times to utc?
now := time.Now()
client.stateMutex.Lock() client.stateMutex.Lock()
defer client.stateMutex.Unlock() defer client.stateMutex.Unlock()
client.atime = time.Now() session.atime = now
client.atime = now
} }
// Ping sends the client a PING message. // Ping sends the client a PING message.

View File

@ -58,8 +58,8 @@ func (cmd *Command) Run(server *Server, client *Client, session *Session, msg ir
session.idletimer.Touch() session.idletimer.Touch()
} }
if !cmd.leaveClientIdle { if client.registered && !cmd.leaveClientIdle {
client.Active() client.Active(session)
} }
return exiting return exiting

View File

@ -73,7 +73,9 @@ func (client *Client) ApplyProxiedIP(session *Session, proxiedIP string, tls boo
client.stateMutex.Lock() client.stateMutex.Lock()
defer client.stateMutex.Unlock() defer client.stateMutex.Unlock()
session.proxiedIP = parsedProxiedIP
client.proxiedIP = parsedProxiedIP client.proxiedIP = parsedProxiedIP
session.rawHostname = rawHostname
client.rawHostname = rawHostname client.rawHostname = rawHostname
// nickmask will be updated when the client completes registration // nickmask will be updated when the client completes registration
// set tls info // set tls info

View File

@ -4,6 +4,7 @@
package irc package irc
import ( import (
"net"
"time" "time"
"github.com/oragono/oragono/irc/isupport" "github.com/oragono/oragono/irc/isupport"
@ -70,6 +71,37 @@ func (client *Client) Sessions() (sessions []*Session) {
return return
} }
type SessionData struct {
ctime time.Time
atime time.Time
ip net.IP
hostname string
}
func (client *Client) AllSessionData(currentSession *Session) (data []SessionData, currentIndex int) {
currentIndex = -1
client.stateMutex.RLock()
defer client.stateMutex.RUnlock()
data = make([]SessionData, len(client.sessions))
for i, session := range client.sessions {
if session == currentSession {
currentIndex = i
}
data[i] = SessionData{
atime: session.atime,
ctime: session.ctime,
hostname: session.rawHostname,
}
if session.proxiedIP != nil {
data[i].ip = session.proxiedIP
} else {
data[i].ip = session.realIP
}
}
return
}
func (client *Client) AddSession(session *Session) (success bool) { func (client *Client) AddSession(session *Session) (success bool) {
client.stateMutex.Lock() client.stateMutex.Lock()
defer client.stateMutex.Unlock() defer client.stateMutex.Unlock()

View File

@ -7,6 +7,8 @@ import (
"fmt" "fmt"
"github.com/goshuirc/irc-go/ircfmt" "github.com/goshuirc/irc-go/ircfmt"
"github.com/oragono/oragono/irc/modes"
) )
// "enabled" callbacks for specific nickserv commands // "enabled" callbacks for specific nickserv commands
@ -26,6 +28,10 @@ func nsEnforceEnabled(config *Config) bool {
return servCmdRequiresNickRes(config) && config.Accounts.NickReservation.AllowCustomEnforcement return servCmdRequiresNickRes(config) && config.Accounts.NickReservation.AllowCustomEnforcement
} }
func servCmdRequiresBouncerEnabled(config *Config) bool {
return config.Accounts.Bouncer.Enabled
}
var ( var (
// ZNC's nickserv module will not detect this unless it is: // ZNC's nickserv module will not detect this unless it is:
// 1. sent with prefix `nickserv` // 1. sent with prefix `nickserv`
@ -142,6 +148,16 @@ an administrator can set use this command to set up user accounts.`,
capabs: []string{"accreg"}, capabs: []string{"accreg"},
minParams: 2, minParams: 2,
}, },
"sessions": {
handler: nsSessionsHandler,
help: `Syntax: $bSESSIONS [nickname]$b
SESSIONS lists information about the sessions currently attached, via
the server's bouncer functionality, to your nickname. An administrator
can use this command to list another user's sessions.`,
helpShort: `$bSESSIONS$b lists the sessions attached to a nickname.`,
enabled: servCmdRequiresBouncerEnabled,
},
"unregister": { "unregister": {
handler: nsUnregisterHandler, handler: nsUnregisterHandler,
help: `Syntax: $bUNREGISTER <username> [code]$b help: `Syntax: $bUNREGISTER <username> [code]$b
@ -569,3 +585,34 @@ func nsEnforceHandler(server *Server, client *Client, command string, params []s
} }
} }
} }
func nsSessionsHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
target := client
if 0 < len(params) {
// same permissions check as RPL_WHOISACTUALLY for now:
if !client.HasMode(modes.Operator) {
nsNotice(rb, client.t("Command restricted"))
return
}
target = server.clients.Get(params[0])
if target == nil {
nsNotice(rb, client.t("No such nick"))
return
}
}
sessionData, currentIndex := target.AllSessionData(rb.session)
nsNotice(rb, fmt.Sprintf(client.t("Nickname %s has %d attached session(s)"), target.Nick(), len(sessionData)))
for i, session := range sessionData {
if currentIndex == i {
nsNotice(rb, fmt.Sprintf(client.t("Session %d (currently attached session):"), i+1))
} else {
nsNotice(rb, fmt.Sprintf(client.t("Session %d:"), i+1))
}
nsNotice(rb, fmt.Sprintf(client.t("IP address: %s"), session.ip.String()))
nsNotice(rb, fmt.Sprintf(client.t("Hostname: %s"), session.hostname))
nsNotice(rb, fmt.Sprintf(client.t("Created at: %s"), session.ctime.Format(IRCv3TimestampFormat)))
nsNotice(rb, fmt.Sprintf(client.t("Last active: %s"), session.atime.Format(IRCv3TimestampFormat)))
}
}

View File

@ -428,17 +428,18 @@ func (server *Server) tryRegister(c *Client, session *Session) {
} }
// continue registration // continue registration
server.logger.Info("localconnect", fmt.Sprintf("Client connected [%s] [u:%s] [r:%s]", c.nick, c.username, c.realname)) d := c.Details()
server.snomasks.Send(sno.LocalConnects, fmt.Sprintf("Client connected [%s] [u:%s] [h:%s] [ip:%s] [r:%s]", c.nick, c.username, c.rawHostname, c.IPString(), c.realname)) server.logger.Info("localconnect", 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, c.RawHostname(), c.IPString(), d.realname))
// send welcome text // send welcome text
//NOTE(dan): we specifically use the NICK here instead of the nickmask //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 // see http://modern.ircdocs.horse/#rplwelcome-001 for details on why we avoid using the nickmask
c.Send(nil, server.name, RPL_WELCOME, c.nick, fmt.Sprintf(c.t("Welcome to the Internet Relay Network %s"), c.nick)) c.Send(nil, server.name, RPL_WELCOME, d.nick, fmt.Sprintf(c.t("Welcome to the Internet Relay Network %s"), d.nick))
c.Send(nil, server.name, RPL_YOURHOST, c.nick, fmt.Sprintf(c.t("Your host is %[1]s, running version %[2]s"), server.name, Ver)) c.Send(nil, server.name, RPL_YOURHOST, d.nick, fmt.Sprintf(c.t("Your host is %[1]s, running version %[2]s"), server.name, Ver))
c.Send(nil, server.name, RPL_CREATED, c.nick, fmt.Sprintf(c.t("This server was created %s"), server.ctime.Format(time.RFC1123))) c.Send(nil, server.name, RPL_CREATED, d.nick, fmt.Sprintf(c.t("This server was created %s"), server.ctime.Format(time.RFC1123)))
//TODO(dan): Look at adding last optional [<channel modes with a parameter>] parameter //TODO(dan): Look at adding last optional [<channel modes with a parameter>] parameter
c.Send(nil, server.name, RPL_MYINFO, c.nick, server.name, Ver, supportedUserModesString, supportedChannelModesString) c.Send(nil, server.name, RPL_MYINFO, d.nick, server.name, Ver, supportedUserModesString, supportedChannelModesString)
rb := NewResponseBuffer(session) rb := NewResponseBuffer(session)
c.RplISupport(rb) c.RplISupport(rb)
@ -447,7 +448,7 @@ func (server *Server) tryRegister(c *Client, session *Session) {
modestring := c.ModeString() modestring := c.ModeString()
if modestring != "+" { if modestring != "+" {
c.Send(nil, c.nickMaskString, RPL_UMODEIS, c.nick, c.ModeString()) c.Send(nil, d.nickMask, RPL_UMODEIS, d.nick, c.ModeString())
} }
if server.logger.IsLoggingRawIO() { if server.logger.IsLoggingRawIO() {
c.Notice(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.")) c.Notice(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."))