diff --git a/irc/client.go b/irc/client.go index 73c0bfb6..61caa8da 100644 --- a/irc/client.go +++ b/irc/client.go @@ -94,7 +94,14 @@ type Client struct { type Session struct { client *Client - socket *Socket + ctime time.Time + atime time.Time + + socket *Socket + realIP net.IP + proxiedIP net.IP + rawHostname string + idletimer IdleTimer fakelag Fakelag @@ -104,9 +111,6 @@ type Session struct { maxlenRest uint32 capState caps.State 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 @@ -187,6 +191,8 @@ func RunNewClient(server *Server, conn clientConn) { socket: socket, capVersion: caps.Cap301, capState: caps.NoneState, + ctime: now, + atime: now, } session.SetMaxlenRest() client.sessions = []*Session{session} @@ -197,20 +203,29 @@ func RunNewClient(server *Server, conn clientConn) { client.certfp, _ = socket.CertFP() } + remoteAddr := conn.Conn.RemoteAddr() if conn.IsTor { client.SetMode(modes.TLS, true) - client.realIP = utils.IPv4LoopbackAddress - client.rawHostname = config.Server.TorListeners.Vhost + session.realIP = utils.AddrToIP(remoteAddr) + // 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 { - remoteAddr := conn.Conn.RemoteAddr() - client.realIP = utils.AddrToIP(remoteAddr) - // Set the hostname for this client - // (may be overridden by a later PROXY command from stunnel) - client.rawHostname = utils.LookupHostname(client.realIP.String()) + session.realIP = utils.AddrToIP(remoteAddr) + // set the hostname for this client (may be overridden later by PROXY or WEBIRC) + session.rawHostname = utils.LookupHostname(session.realIP.String()) + if utils.AddrIsLocal(remoteAddr) { + // treat local connections as secure (may be overridden later by WEBIRC) + client.SetMode(modes.TLS, true) + } if config.Server.CheckIdent && !utils.AddrIsUnix(remoteAddr) { client.doIdentLookup(conn.Conn) } } + client.realIP = session.realIP + client.rawHostname = session.rawHostname + client.proxiedIP = session.proxiedIP 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). -func (client *Client) Active() { +func (client *Client) Active(session *Session) { + // TODO normalize all times to utc? + now := time.Now() client.stateMutex.Lock() defer client.stateMutex.Unlock() - client.atime = time.Now() + session.atime = now + client.atime = now } // Ping sends the client a PING message. diff --git a/irc/commands.go b/irc/commands.go index e7399acb..e058a3e7 100644 --- a/irc/commands.go +++ b/irc/commands.go @@ -58,8 +58,8 @@ func (cmd *Command) Run(server *Server, client *Client, session *Session, msg ir session.idletimer.Touch() } - if !cmd.leaveClientIdle { - client.Active() + if client.registered && !cmd.leaveClientIdle { + client.Active(session) } return exiting diff --git a/irc/gateways.go b/irc/gateways.go index a3db9582..495dfee0 100644 --- a/irc/gateways.go +++ b/irc/gateways.go @@ -73,7 +73,9 @@ func (client *Client) ApplyProxiedIP(session *Session, proxiedIP string, tls boo client.stateMutex.Lock() defer client.stateMutex.Unlock() + session.proxiedIP = parsedProxiedIP client.proxiedIP = parsedProxiedIP + session.rawHostname = rawHostname client.rawHostname = rawHostname // nickmask will be updated when the client completes registration // set tls info diff --git a/irc/getters.go b/irc/getters.go index ac3e5124..af77a0a8 100644 --- a/irc/getters.go +++ b/irc/getters.go @@ -4,6 +4,7 @@ package irc import ( + "net" "time" "github.com/oragono/oragono/irc/isupport" @@ -70,6 +71,37 @@ func (client *Client) Sessions() (sessions []*Session) { 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) { client.stateMutex.Lock() defer client.stateMutex.Unlock() diff --git a/irc/nickserv.go b/irc/nickserv.go index 74941658..9a69c90f 100644 --- a/irc/nickserv.go +++ b/irc/nickserv.go @@ -7,6 +7,8 @@ import ( "fmt" "github.com/goshuirc/irc-go/ircfmt" + + "github.com/oragono/oragono/irc/modes" ) // "enabled" callbacks for specific nickserv commands @@ -26,6 +28,10 @@ func nsEnforceEnabled(config *Config) bool { return servCmdRequiresNickRes(config) && config.Accounts.NickReservation.AllowCustomEnforcement } +func servCmdRequiresBouncerEnabled(config *Config) bool { + return config.Accounts.Bouncer.Enabled +} + var ( // ZNC's nickserv module will not detect this unless it is: // 1. sent with prefix `nickserv` @@ -142,6 +148,16 @@ an administrator can set use this command to set up user accounts.`, capabs: []string{"accreg"}, 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": { handler: nsUnregisterHandler, help: `Syntax: $bUNREGISTER [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))) + } +} diff --git a/irc/server.go b/irc/server.go index fc628098..fad52417 100644 --- a/irc/server.go +++ b/irc/server.go @@ -428,17 +428,18 @@ func (server *Server) tryRegister(c *Client, session *Session) { } // continue registration - server.logger.Info("localconnect", fmt.Sprintf("Client connected [%s] [u:%s] [r:%s]", c.nick, c.username, c.realname)) - 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)) + d := c.Details() + 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 //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 - 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_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_CREATED, c.nick, fmt.Sprintf(c.t("This server was created %s"), server.ctime.Format(time.RFC1123))) + 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, 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, d.nick, fmt.Sprintf(c.t("This server was created %s"), server.ctime.Format(time.RFC1123))) //TODO(dan): Look at adding last optional [] 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) c.RplISupport(rb) @@ -447,7 +448,7 @@ func (server *Server) tryRegister(c *Client, session *Session) { modestring := c.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() { 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."))