feat: add OPER command and oper-only WHOIS client info
- Add OPER command with NEOIRC_OPER_NAME/NEOIRC_OPER_PASSWORD config - Add is_oper column to sessions table - Add RPL_WHOISACTUALLY (338): show client IP/hostname to opers - Add RPL_WHOISOPERATOR (313): show oper status in WHOIS - Add GetOperCount for accurate LUSERS oper count - Fix README schema: add ip/is_oper to sessions, ip/hostname to clients - Add OPER command documentation and numeric references to README - Refactor executeWhois to stay under funlen limit - Add comprehensive tests for OPER auth, oper WHOIS, non-oper WHOIS Closes #81
This commit is contained in:
@@ -469,9 +469,19 @@ func (hdlr *Handlers) deliverLusers(
|
||||
)
|
||||
|
||||
// 252 RPL_LUSEROP
|
||||
operCount, operErr := hdlr.params.Database.
|
||||
GetOperCount(ctx)
|
||||
if operErr != nil {
|
||||
hdlr.log.Error(
|
||||
"lusers oper count", "error", operErr,
|
||||
)
|
||||
|
||||
operCount = 0
|
||||
}
|
||||
|
||||
hdlr.enqueueNumeric(
|
||||
ctx, clientID, irc.RplLuserOp, nick,
|
||||
[]string{"0"},
|
||||
[]string{strconv.FormatInt(operCount, 10)},
|
||||
"operator(s) online",
|
||||
)
|
||||
|
||||
@@ -1002,6 +1012,11 @@ func (hdlr *Handlers) dispatchCommand(
|
||||
hdlr.handleQuit(
|
||||
writer, request, sessionID, nick, body,
|
||||
)
|
||||
case irc.CmdOper:
|
||||
hdlr.handleOper(
|
||||
writer, request,
|
||||
sessionID, clientID, nick, bodyLines,
|
||||
)
|
||||
case irc.CmdMotd, irc.CmdPing:
|
||||
hdlr.dispatchInfoCommand(
|
||||
writer, request,
|
||||
@@ -2562,31 +2577,89 @@ func (hdlr *Handlers) executeWhois(
|
||||
nick, queryNick string,
|
||||
) {
|
||||
ctx := request.Context()
|
||||
srvName := hdlr.serverName()
|
||||
|
||||
targetSID, err := hdlr.params.Database.GetSessionByNick(
|
||||
ctx, queryNick,
|
||||
)
|
||||
if err != nil {
|
||||
hdlr.enqueueNumeric(
|
||||
ctx, clientID, irc.ErrNoSuchNick, nick,
|
||||
[]string{queryNick},
|
||||
"No such nick/channel",
|
||||
hdlr.whoisNotFound(
|
||||
ctx, writer, request,
|
||||
sessionID, clientID, nick, queryNick,
|
||||
)
|
||||
hdlr.enqueueNumeric(
|
||||
ctx, clientID, irc.RplEndOfWhois, nick,
|
||||
[]string{queryNick},
|
||||
"End of /WHOIS list",
|
||||
)
|
||||
hdlr.broker.Notify(sessionID)
|
||||
hdlr.respondJSON(writer, request,
|
||||
map[string]string{"status": "ok"},
|
||||
http.StatusOK)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Look up username and hostname for the target.
|
||||
hdlr.deliverWhoisUser(
|
||||
ctx, clientID, nick, queryNick, targetSID,
|
||||
)
|
||||
|
||||
// 313 RPL_WHOISOPERATOR — show if target is oper.
|
||||
hdlr.deliverWhoisOperator(
|
||||
ctx, clientID, nick, queryNick, targetSID,
|
||||
)
|
||||
|
||||
hdlr.deliverWhoisIdle(
|
||||
ctx, clientID, nick, queryNick, targetSID,
|
||||
)
|
||||
|
||||
hdlr.deliverWhoisChannels(
|
||||
ctx, clientID, nick, queryNick, targetSID,
|
||||
)
|
||||
|
||||
// 338 RPL_WHOISACTUALLY — oper-only.
|
||||
hdlr.deliverWhoisActually(
|
||||
ctx, clientID, nick, queryNick,
|
||||
sessionID, targetSID,
|
||||
)
|
||||
|
||||
hdlr.enqueueNumeric(
|
||||
ctx, clientID, irc.RplEndOfWhois, nick,
|
||||
[]string{queryNick},
|
||||
"End of /WHOIS list",
|
||||
)
|
||||
|
||||
hdlr.broker.Notify(sessionID)
|
||||
hdlr.respondJSON(writer, request,
|
||||
map[string]string{"status": "ok"},
|
||||
http.StatusOK)
|
||||
}
|
||||
|
||||
// whoisNotFound sends the error+end numerics when the
|
||||
// target nick is not found.
|
||||
func (hdlr *Handlers) whoisNotFound(
|
||||
ctx context.Context,
|
||||
writer http.ResponseWriter,
|
||||
request *http.Request,
|
||||
sessionID, clientID int64,
|
||||
nick, queryNick string,
|
||||
) {
|
||||
hdlr.enqueueNumeric(
|
||||
ctx, clientID, irc.ErrNoSuchNick, nick,
|
||||
[]string{queryNick},
|
||||
"No such nick/channel",
|
||||
)
|
||||
hdlr.enqueueNumeric(
|
||||
ctx, clientID, irc.RplEndOfWhois, nick,
|
||||
[]string{queryNick},
|
||||
"End of /WHOIS list",
|
||||
)
|
||||
hdlr.broker.Notify(sessionID)
|
||||
hdlr.respondJSON(writer, request,
|
||||
map[string]string{"status": "ok"},
|
||||
http.StatusOK)
|
||||
}
|
||||
|
||||
// deliverWhoisUser sends RPL_WHOISUSER (311) and
|
||||
// RPL_WHOISSERVER (312).
|
||||
func (hdlr *Handlers) deliverWhoisUser(
|
||||
ctx context.Context,
|
||||
clientID int64,
|
||||
nick, queryNick string,
|
||||
targetSID int64,
|
||||
) {
|
||||
srvName := hdlr.serverName()
|
||||
|
||||
username := queryNick
|
||||
hostname := srvName
|
||||
|
||||
@@ -2602,41 +2675,38 @@ func (hdlr *Handlers) executeWhois(
|
||||
}
|
||||
}
|
||||
|
||||
// 311 RPL_WHOISUSER
|
||||
hdlr.enqueueNumeric(
|
||||
ctx, clientID, irc.RplWhoisUser, nick,
|
||||
[]string{queryNick, username, hostname, "*"},
|
||||
queryNick,
|
||||
)
|
||||
|
||||
// 312 RPL_WHOISSERVER
|
||||
hdlr.enqueueNumeric(
|
||||
ctx, clientID, irc.RplWhoisServer, nick,
|
||||
[]string{queryNick, srvName},
|
||||
"neoirc server",
|
||||
)
|
||||
}
|
||||
|
||||
// 317 RPL_WHOISIDLE
|
||||
hdlr.deliverWhoisIdle(
|
||||
ctx, clientID, nick, queryNick, targetSID,
|
||||
)
|
||||
// deliverWhoisOperator sends RPL_WHOISOPERATOR (313) if
|
||||
// the target has server oper status.
|
||||
func (hdlr *Handlers) deliverWhoisOperator(
|
||||
ctx context.Context,
|
||||
clientID int64,
|
||||
nick, queryNick string,
|
||||
targetSID int64,
|
||||
) {
|
||||
targetIsOper, err := hdlr.params.Database.
|
||||
IsSessionOper(ctx, targetSID)
|
||||
if err != nil || !targetIsOper {
|
||||
return
|
||||
}
|
||||
|
||||
// 319 RPL_WHOISCHANNELS
|
||||
hdlr.deliverWhoisChannels(
|
||||
ctx, clientID, nick, queryNick, targetSID,
|
||||
)
|
||||
|
||||
// 318 RPL_ENDOFWHOIS
|
||||
hdlr.enqueueNumeric(
|
||||
ctx, clientID, irc.RplEndOfWhois, nick,
|
||||
ctx, clientID, irc.RplWhoisOperator, nick,
|
||||
[]string{queryNick},
|
||||
"End of /WHOIS list",
|
||||
"is an IRC operator",
|
||||
)
|
||||
|
||||
hdlr.broker.Notify(sessionID)
|
||||
hdlr.respondJSON(writer, request,
|
||||
map[string]string{"status": "ok"},
|
||||
http.StatusOK)
|
||||
}
|
||||
|
||||
func (hdlr *Handlers) deliverWhoisChannels(
|
||||
@@ -2664,6 +2734,44 @@ func (hdlr *Handlers) deliverWhoisChannels(
|
||||
)
|
||||
}
|
||||
|
||||
// deliverWhoisActually sends RPL_WHOISACTUALLY (338)
|
||||
// with the target's current client IP and hostname, but
|
||||
// only when the querying session has server oper status
|
||||
// (o-line). Non-opers see nothing extra.
|
||||
func (hdlr *Handlers) deliverWhoisActually(
|
||||
ctx context.Context,
|
||||
clientID int64,
|
||||
nick, queryNick string,
|
||||
querierSID, targetSID int64,
|
||||
) {
|
||||
isOper, err := hdlr.params.Database.IsSessionOper(
|
||||
ctx, querierSID,
|
||||
)
|
||||
if err != nil || !isOper {
|
||||
return
|
||||
}
|
||||
|
||||
clientInfo, clErr := hdlr.params.Database.
|
||||
GetLatestClientForSession(ctx, targetSID)
|
||||
if clErr != nil {
|
||||
return
|
||||
}
|
||||
|
||||
actualHost := clientInfo.Hostname
|
||||
if actualHost == "" {
|
||||
actualHost = clientInfo.IP
|
||||
}
|
||||
|
||||
hdlr.enqueueNumeric(
|
||||
ctx, clientID, irc.RplWhoisActually, nick,
|
||||
[]string{
|
||||
queryNick,
|
||||
clientInfo.IP,
|
||||
},
|
||||
"is actually using host "+actualHost,
|
||||
)
|
||||
}
|
||||
|
||||
// handleWho handles the WHO command.
|
||||
func (hdlr *Handlers) handleWho(
|
||||
writer http.ResponseWriter,
|
||||
@@ -3051,6 +3159,74 @@ func (hdlr *Handlers) HandleServerInfo() http.HandlerFunc {
|
||||
|
||||
// handleAway handles the AWAY command. An empty body
|
||||
// clears the away status; a non-empty body sets it.
|
||||
func (hdlr *Handlers) handleOper(
|
||||
writer http.ResponseWriter,
|
||||
request *http.Request,
|
||||
sessionID, clientID int64,
|
||||
nick string,
|
||||
bodyLines func() []string,
|
||||
) {
|
||||
ctx := request.Context()
|
||||
|
||||
lines := bodyLines()
|
||||
if len(lines) < 2 { //nolint:mnd // name + password
|
||||
hdlr.respondIRCError(
|
||||
writer, request, clientID, sessionID,
|
||||
irc.ErrNeedMoreParams, nick,
|
||||
[]string{irc.CmdOper},
|
||||
"Not enough parameters",
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
operName := lines[0]
|
||||
operPass := lines[1]
|
||||
|
||||
cfgName := hdlr.params.Config.OperName
|
||||
cfgPass := hdlr.params.Config.OperPassword
|
||||
|
||||
if cfgName == "" || cfgPass == "" ||
|
||||
operName != cfgName || operPass != cfgPass {
|
||||
hdlr.enqueueNumeric(
|
||||
ctx, clientID, irc.ErrNoOperHost, nick,
|
||||
nil, "No O-lines for your host",
|
||||
)
|
||||
hdlr.broker.Notify(sessionID)
|
||||
hdlr.respondJSON(writer, request,
|
||||
map[string]string{"status": "error"},
|
||||
http.StatusOK)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
err := hdlr.params.Database.SetSessionOper(
|
||||
ctx, sessionID, true,
|
||||
)
|
||||
if err != nil {
|
||||
hdlr.log.Error(
|
||||
"set oper failed", "error", err,
|
||||
)
|
||||
hdlr.respondError(
|
||||
writer, request, "internal error",
|
||||
http.StatusInternalServerError,
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// 381 RPL_YOUREOPER
|
||||
hdlr.enqueueNumeric(
|
||||
ctx, clientID, irc.RplYoureOper, nick,
|
||||
nil, "You are now an IRC operator",
|
||||
)
|
||||
|
||||
hdlr.broker.Notify(sessionID)
|
||||
hdlr.respondJSON(writer, request,
|
||||
map[string]string{"status": "ok"},
|
||||
http.StatusOK)
|
||||
}
|
||||
|
||||
func (hdlr *Handlers) handleAway(
|
||||
writer http.ResponseWriter,
|
||||
request *http.Request,
|
||||
|
||||
Reference in New Issue
Block a user