3
0
mirror of https://github.com/ergochat/ergo.git synced 2024-12-22 18:52:41 +01:00

Add NickServ "CLIENTS LIST" and "CLIENTS LOGOUT".

CLIENTS LIST shows information about clients attached to a nick.
CLIENTS LOGOUT allows individual (or all) sessions to be logged out.

SESSIONS is now an alias for CLIENTS LIST.

Fixes #1072.
This commit is contained in:
Alex Jaspersen 2020-09-19 10:19:41 -07:00
parent 536c21a874
commit ca2132ff09
3 changed files with 123 additions and 22 deletions

View File

@ -90,6 +90,7 @@ type Client struct {
lastSeen map[string]time.Time // maps device ID (including "") to time of last received command
lastSeenLastWrite time.Time // last time `lastSeen` was written to the datastore
loginThrottle connection_limits.GenericThrottle
nextSessionID int64 // Incremented when a new session is established
nick string
nickCasefolded string
nickMaskCasefolded string
@ -150,6 +151,7 @@ type Session struct {
idleTimer *time.Timer
pingSent bool // we sent PING to a putatively idle connection and we're waiting for PONG
sessionID int64
socket *Socket
realIP net.IP
proxiedIP net.IP
@ -353,6 +355,7 @@ func (server *Server) RunClient(conn IRCConn) {
realIP: realIP,
proxiedIP: proxiedIP,
requireSASL: requireSASL,
nextSessionID: 1,
}
if requireSASL {
client.requireSASLMessage = banMsg
@ -420,6 +423,8 @@ func (server *Server) AddAlwaysOnClient(account ClientAccount, chnames []string,
alwaysOn: true,
realname: realname,
nextSessionID: 1,
}
client.SetMode(modes.TLS, true)

View File

@ -65,12 +65,13 @@ func (client *Client) GetSessionByResumeID(resumeID string) (result *Session) {
}
type SessionData struct {
ctime time.Time
atime time.Time
ip net.IP
hostname string
certfp string
deviceID string
ctime time.Time
atime time.Time
ip net.IP
hostname string
certfp string
deviceID string
sessionID int64
}
func (client *Client) AllSessionData(currentSession *Session) (data []SessionData, currentIndex int) {
@ -84,11 +85,12 @@ func (client *Client) AllSessionData(currentSession *Session) (data []SessionDat
currentIndex = i
}
data[i] = SessionData{
atime: session.lastActive,
ctime: session.ctime,
hostname: session.rawHostname,
certfp: session.certfp,
deviceID: session.deviceID,
atime: session.lastActive,
ctime: session.ctime,
hostname: session.rawHostname,
certfp: session.certfp,
deviceID: session.deviceID,
sessionID: session.sessionID,
}
if session.proxiedIP != nil {
data[i].ip = session.proxiedIP
@ -109,6 +111,8 @@ func (client *Client) AddSession(session *Session) (success bool, numSessions in
}
// success, attach the new session to the client
session.client = client
session.sessionID = client.nextSessionID
client.nextSessionID++
newSessions := make([]*Session, len(client.sessions)+1)
copy(newSessions, client.sessions)
newSessions[len(newSessions)-1] = session

View File

@ -43,6 +43,23 @@ const nickservHelp = `NickServ lets you register, log in to, and manage an accou
var (
nickservCommands = map[string]*serviceCommand{
"clients": {
handler: nsClientsHandler,
help: `Syntax: $bCLIENTS LIST [nickname]$b
CLIENTS LIST shows information about the clients currently attached, via
the server's multiclient functionality, to your nickname. An administrator
can use this command to list another user's clients.
Syntax: $bCLIENTS LOGOUT [nickname] [client_id/all]$b
CLIENTS LOGOUT detaches a single client, or all other clients currently
attached, via the server's multiclient functionality, to your nickname. An
administrator can use this command to logout another user's clients.`,
helpShort: `$bCLIENTS$b can list and logout the sessions attached to a nickname.`,
enabled: servCmdRequiresBouncerEnabled,
minParams: 1,
},
"drop": {
handler: nsDropHandler,
help: `Syntax: $bDROP [nickname]$b
@ -150,14 +167,13 @@ an administrator can set use this command to set up user accounts.`,
minParams: 1,
},
"sessions": {
handler: nsSessionsHandler,
hidden: true,
handler: nsClientsHandler,
help: `Syntax: $bSESSIONS [nickname]$b
SESSIONS lists information about the sessions currently attached, via
the server's multiclient 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,
SESSIONS is an alias for $bCLIENTS LIST$b. See the help entry for $bCLIENTS$b
for more information.`,
enabled: servCmdRequiresBouncerEnabled,
},
"unregister": {
handler: nsUnregisterHandler,
@ -1065,9 +1081,30 @@ func nsEnforceHandler(server *Server, client *Client, command string, params []s
}
}
func nsSessionsHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
target := client
func nsClientsHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
var verb string
if command == "sessions" {
// Legacy "SESSIONS" command is an alias for CLIENTS LIST.
verb = "list"
} else if len(params) > 0 {
verb = strings.ToLower(params[0])
params = params[1:]
}
switch verb {
case "list":
nsClientsListHandler(server, client, params, rb)
case "logout":
nsClientsLogoutHandler(server, client, params, rb)
default:
nsNotice(rb, client.t("Invalid parameters"))
return
}
}
func nsClientsListHandler(server *Server, client *Client, params []string, rb *ResponseBuffer) {
target := client
if 0 < len(params) {
target = server.clients.Get(params[0])
if target == nil {
@ -1082,12 +1119,12 @@ func nsSessionsHandler(server *Server, client *Client, command string, params []
}
sessionData, currentIndex := target.AllSessionData(rb.session)
nsNotice(rb, fmt.Sprintf(client.t("Nickname %[1]s has %[2]d attached session(s)"), target.Nick(), len(sessionData)))
nsNotice(rb, fmt.Sprintf(client.t("Nickname %[1]s has %[2]d attached clients(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))
nsNotice(rb, fmt.Sprintf(client.t("Client %d (currently attached client):"), session.sessionID))
} else {
nsNotice(rb, fmt.Sprintf(client.t("Session %d:"), i+1))
nsNotice(rb, fmt.Sprintf(client.t("Client %d:"), session.sessionID))
}
if session.deviceID != "" {
nsNotice(rb, fmt.Sprintf(client.t("Device ID: %s"), session.deviceID))
@ -1102,6 +1139,61 @@ func nsSessionsHandler(server *Server, client *Client, command string, params []
}
}
func nsClientsLogoutHandler(server *Server, client *Client, params []string, rb *ResponseBuffer) {
if len(params) < 1 {
nsNotice(rb, client.t("Missing client ID to logout (or \"all\")"))
return
}
target := client
if len(params) >= 2 {
// CLIENTS LOGOUT [nickname] [client ID]
target = server.clients.Get(params[0])
if target == nil {
nsNotice(rb, client.t("No such nick"))
return
}
// User must have "local_kill" privileges to logout other user sessions.
if target != client {
oper := client.Oper()
if oper == nil || !oper.Class.Capabilities.Has("local_kill") {
nsNotice(rb, client.t("Insufficient oper privs"))
return
}
}
params = params[1:]
}
var sessionToDestroy *Session // target.destroy(nil) will logout all sessions
if strings.ToLower(params[0]) != "all" {
sessionID, err := strconv.ParseInt(params[0], 10, 64)
if err != nil {
nsNotice(rb, client.t("Client ID to logout should be an integer (or \"all\")"))
return
}
// Find the client ID that the user requested to logout.
sessions := target.Sessions()
for _, session := range sessions {
if session.sessionID == sessionID {
sessionToDestroy = session
}
}
if sessionToDestroy == nil {
nsNotice(rb, client.t("Specified client ID does not exist"))
return
}
}
target.destroy(sessionToDestroy)
if (sessionToDestroy != nil && rb.session != sessionToDestroy) || client != target {
if sessionToDestroy != nil {
nsNotice(rb, client.t("Successfully logged out session"))
} else {
nsNotice(rb, client.t("Successfully logged out all sessions"))
}
}
}
func nsCertHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
verb := strings.ToLower(params[0])
params = params[1:]