feat: add OPER command and oper-only WHOIS client info
Some checks failed
check / check (push) Failing after 1m52s
Some checks failed
check / check (push) Failing after 1m52s
- 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:
@@ -46,6 +46,8 @@ type Config struct {
|
||||
FederationKey string
|
||||
SessionIdleTimeout string
|
||||
HashcashBits int
|
||||
OperName string
|
||||
OperPassword string
|
||||
params *Params
|
||||
log *slog.Logger
|
||||
}
|
||||
@@ -78,6 +80,8 @@ func New(
|
||||
viper.SetDefault("FEDERATION_KEY", "")
|
||||
viper.SetDefault("SESSION_IDLE_TIMEOUT", "720h")
|
||||
viper.SetDefault("NEOIRC_HASHCASH_BITS", "20")
|
||||
viper.SetDefault("NEOIRC_OPER_NAME", "")
|
||||
viper.SetDefault("NEOIRC_OPER_PASSWORD", "")
|
||||
|
||||
err := viper.ReadInConfig()
|
||||
if err != nil {
|
||||
@@ -104,6 +108,8 @@ func New(
|
||||
FederationKey: viper.GetString("FEDERATION_KEY"),
|
||||
SessionIdleTimeout: viper.GetString("SESSION_IDLE_TIMEOUT"),
|
||||
HashcashBits: viper.GetInt("NEOIRC_HASHCASH_BITS"),
|
||||
OperName: viper.GetString("NEOIRC_OPER_NAME"),
|
||||
OperPassword: viper.GetString("NEOIRC_OPER_PASSWORD"),
|
||||
log: log,
|
||||
params: ¶ms,
|
||||
}
|
||||
|
||||
@@ -298,6 +298,75 @@ func (database *Database) GetClientHostInfo(
|
||||
return &info, nil
|
||||
}
|
||||
|
||||
// SetSessionOper sets the is_oper flag on a session.
|
||||
func (database *Database) SetSessionOper(
|
||||
ctx context.Context,
|
||||
sessionID int64,
|
||||
isOper bool,
|
||||
) error {
|
||||
val := 0
|
||||
if isOper {
|
||||
val = 1
|
||||
}
|
||||
|
||||
_, err := database.conn.ExecContext(
|
||||
ctx,
|
||||
`UPDATE sessions SET is_oper = ? WHERE id = ?`,
|
||||
val, sessionID,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("set session oper: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsSessionOper returns whether the session has oper
|
||||
// status.
|
||||
func (database *Database) IsSessionOper(
|
||||
ctx context.Context,
|
||||
sessionID int64,
|
||||
) (bool, error) {
|
||||
var isOper int
|
||||
|
||||
err := database.conn.QueryRowContext(
|
||||
ctx,
|
||||
`SELECT is_oper FROM sessions WHERE id = ?`,
|
||||
sessionID,
|
||||
).Scan(&isOper)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf(
|
||||
"check session oper: %w", err,
|
||||
)
|
||||
}
|
||||
|
||||
return isOper != 0, nil
|
||||
}
|
||||
|
||||
// GetLatestClientForSession returns the IP and hostname
|
||||
// of the most recently created client for a session.
|
||||
func (database *Database) GetLatestClientForSession(
|
||||
ctx context.Context,
|
||||
sessionID int64,
|
||||
) (*ClientHostInfo, error) {
|
||||
var info ClientHostInfo
|
||||
|
||||
err := database.conn.QueryRowContext(
|
||||
ctx,
|
||||
`SELECT ip, hostname FROM clients
|
||||
WHERE session_id = ?
|
||||
ORDER BY created_at DESC LIMIT 1`,
|
||||
sessionID,
|
||||
).Scan(&info.IP, &info.Hostname)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(
|
||||
"get latest client for session: %w", err,
|
||||
)
|
||||
}
|
||||
|
||||
return &info, nil
|
||||
}
|
||||
|
||||
// GetChannelByName returns the channel ID for a name.
|
||||
func (database *Database) GetChannelByName(
|
||||
ctx context.Context,
|
||||
@@ -951,6 +1020,26 @@ func (database *Database) GetUserCount(
|
||||
return count, nil
|
||||
}
|
||||
|
||||
// GetOperCount returns the number of sessions with oper
|
||||
// status.
|
||||
func (database *Database) GetOperCount(
|
||||
ctx context.Context,
|
||||
) (int64, error) {
|
||||
var count int64
|
||||
|
||||
err := database.conn.QueryRowContext(
|
||||
ctx,
|
||||
"SELECT COUNT(*) FROM sessions WHERE is_oper = 1",
|
||||
).Scan(&count)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf(
|
||||
"get oper count: %w", err,
|
||||
)
|
||||
}
|
||||
|
||||
return count, nil
|
||||
}
|
||||
|
||||
// ClientCountForSession returns the number of clients
|
||||
// belonging to a session.
|
||||
func (database *Database) ClientCountForSession(
|
||||
|
||||
@@ -887,3 +887,133 @@ func TestEnqueueToClient(t *testing.T) {
|
||||
t.Fatalf("expected 1, got %d", len(msgs))
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetAndCheckSessionOper(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
database := setupTestDB(t)
|
||||
ctx := t.Context()
|
||||
|
||||
sessionID, _, _, err := database.CreateSession(
|
||||
ctx, "opernick", "", "", "",
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Initially not oper.
|
||||
isOper, err := database.IsSessionOper(ctx, sessionID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if isOper {
|
||||
t.Fatal("expected session not to be oper")
|
||||
}
|
||||
|
||||
// Set oper.
|
||||
err = database.SetSessionOper(ctx, sessionID, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
isOper, err = database.IsSessionOper(ctx, sessionID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if !isOper {
|
||||
t.Fatal("expected session to be oper")
|
||||
}
|
||||
|
||||
// Unset oper.
|
||||
err = database.SetSessionOper(ctx, sessionID, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
isOper, err = database.IsSessionOper(ctx, sessionID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if isOper {
|
||||
t.Fatal("expected session not to be oper")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetLatestClientForSession(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
database := setupTestDB(t)
|
||||
ctx := t.Context()
|
||||
|
||||
sessionID, _, _, err := database.CreateSession(
|
||||
ctx, "clientnick", "", "", "10.0.0.1",
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
clientInfo, err := database.GetLatestClientForSession(
|
||||
ctx, sessionID,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if clientInfo.IP != "10.0.0.1" {
|
||||
t.Fatalf(
|
||||
"expected IP 10.0.0.1, got %s",
|
||||
clientInfo.IP,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetOperCount(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
database := setupTestDB(t)
|
||||
ctx := t.Context()
|
||||
|
||||
// Create two sessions.
|
||||
sid1, _, _, err := database.CreateSession(
|
||||
ctx, "user1", "", "", "",
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
sid2, _, _, err := database.CreateSession(
|
||||
ctx, "user2", "", "", "",
|
||||
)
|
||||
_ = sid2
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Initially zero opers.
|
||||
count, err := database.GetOperCount(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if count != 0 {
|
||||
t.Fatalf("expected 0 opers, got %d", count)
|
||||
}
|
||||
|
||||
// Set one as oper.
|
||||
err = database.SetSessionOper(ctx, sid1, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
count, err = database.GetOperCount(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if count != 1 {
|
||||
t.Fatalf("expected 1 oper, got %d", count)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ CREATE TABLE IF NOT EXISTS sessions (
|
||||
username TEXT NOT NULL DEFAULT '',
|
||||
hostname TEXT NOT NULL DEFAULT '',
|
||||
ip TEXT NOT NULL DEFAULT '',
|
||||
is_oper INTEGER NOT NULL DEFAULT 0,
|
||||
password_hash TEXT NOT NULL DEFAULT '',
|
||||
signing_key TEXT NOT NULL DEFAULT '',
|
||||
away_message TEXT NOT NULL DEFAULT '',
|
||||
|
||||
@@ -460,9 +460,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",
|
||||
)
|
||||
|
||||
@@ -992,6 +1002,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,
|
||||
@@ -2198,31 +2213,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
|
||||
|
||||
@@ -2238,41 +2311,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(
|
||||
@@ -2300,6 +2370,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,
|
||||
@@ -2687,6 +2795,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,
|
||||
|
||||
@@ -2532,3 +2532,315 @@ func assertNamesHostmask(
|
||||
targetNick, msgs,
|
||||
)
|
||||
}
|
||||
|
||||
const testOperName = "admin"
|
||||
const testOperPassword = "secretpass"
|
||||
|
||||
// newTestServerWithOper creates a test server with oper
|
||||
// credentials configured (admin / secretpass).
|
||||
func newTestServerWithOper(
|
||||
t *testing.T,
|
||||
) *testServer {
|
||||
t.Helper()
|
||||
|
||||
dbPath := filepath.Join(
|
||||
t.TempDir(), "test.db",
|
||||
)
|
||||
|
||||
dbURL := "file:" + dbPath +
|
||||
"?_journal_mode=WAL&_busy_timeout=5000"
|
||||
|
||||
var srv *server.Server
|
||||
|
||||
app := fxtest.New(t,
|
||||
fx.Provide(
|
||||
newTestGlobals,
|
||||
logger.New,
|
||||
func(
|
||||
lifecycle fx.Lifecycle,
|
||||
globs *globals.Globals,
|
||||
log *logger.Logger,
|
||||
) (*config.Config, error) {
|
||||
cfg, err := config.New(
|
||||
lifecycle, config.Params{ //nolint:exhaustruct
|
||||
Globals: globs, Logger: log,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(
|
||||
"test config: %w", err,
|
||||
)
|
||||
}
|
||||
|
||||
cfg.DBURL = dbURL
|
||||
cfg.Port = 0
|
||||
cfg.HashcashBits = 0
|
||||
cfg.OperName = testOperName
|
||||
cfg.OperPassword = testOperPassword
|
||||
|
||||
return cfg, nil
|
||||
},
|
||||
newTestDB,
|
||||
stats.New,
|
||||
newTestHealthcheck,
|
||||
newTestMiddleware,
|
||||
newTestHandlers,
|
||||
newTestServerFx,
|
||||
),
|
||||
fx.Populate(&srv),
|
||||
)
|
||||
|
||||
const startupDelay = 100 * time.Millisecond
|
||||
|
||||
app.RequireStart()
|
||||
time.Sleep(startupDelay)
|
||||
|
||||
httpSrv := httptest.NewServer(srv)
|
||||
|
||||
t.Cleanup(func() {
|
||||
httpSrv.Close()
|
||||
app.RequireStop()
|
||||
})
|
||||
|
||||
return &testServer{
|
||||
httpServer: httpSrv,
|
||||
t: t,
|
||||
fxApp: app,
|
||||
}
|
||||
}
|
||||
|
||||
func TestOperCommandSuccess(t *testing.T) {
|
||||
tserver := newTestServerWithOper(t)
|
||||
|
||||
token := tserver.createSession("operuser")
|
||||
_, lastID := tserver.pollMessages(token, 0)
|
||||
|
||||
// Send OPER command.
|
||||
tserver.sendCommand(token, map[string]any{
|
||||
commandKey: "OPER",
|
||||
bodyKey: []string{testOperName, testOperPassword},
|
||||
})
|
||||
|
||||
msgs, _ := tserver.pollMessages(token, lastID)
|
||||
|
||||
// Expect 381 RPL_YOUREOPER.
|
||||
if !findNumeric(msgs, "381") {
|
||||
t.Fatalf(
|
||||
"expected RPL_YOUREOPER (381), got %v",
|
||||
msgs,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOperCommandFailure(t *testing.T) {
|
||||
tserver := newTestServerWithOper(t)
|
||||
|
||||
token := tserver.createSession("badoper")
|
||||
_, lastID := tserver.pollMessages(token, 0)
|
||||
|
||||
// Send OPER with wrong password.
|
||||
tserver.sendCommand(token, map[string]any{
|
||||
commandKey: "OPER",
|
||||
bodyKey: []string{testOperName, "wrongpass"},
|
||||
})
|
||||
|
||||
msgs, _ := tserver.pollMessages(token, lastID)
|
||||
|
||||
// Expect 491 ERR_NOOPERHOST.
|
||||
if !findNumeric(msgs, "491") {
|
||||
t.Fatalf(
|
||||
"expected ERR_NOOPERHOST (491), got %v",
|
||||
msgs,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOperCommandNeedMoreParams(t *testing.T) {
|
||||
tserver := newTestServerWithOper(t)
|
||||
|
||||
token := tserver.createSession("shortoper")
|
||||
_, lastID := tserver.pollMessages(token, 0)
|
||||
|
||||
// Send OPER with only one parameter.
|
||||
tserver.sendCommand(token, map[string]any{
|
||||
commandKey: "OPER",
|
||||
bodyKey: []string{testOperName},
|
||||
})
|
||||
|
||||
msgs, _ := tserver.pollMessages(token, lastID)
|
||||
|
||||
// Expect 461 ERR_NEEDMOREPARAMS.
|
||||
if !findNumeric(msgs, "461") {
|
||||
t.Fatalf(
|
||||
"expected ERR_NEEDMOREPARAMS (461), got %v",
|
||||
msgs,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOperWhoisShowsClientInfo(t *testing.T) {
|
||||
tserver := newTestServerWithOper(t)
|
||||
|
||||
// Create a target user.
|
||||
_ = tserver.createSession("target")
|
||||
|
||||
// Create an oper user.
|
||||
operToken := tserver.createSession("theoper")
|
||||
_, lastID := tserver.pollMessages(operToken, 0)
|
||||
|
||||
// Authenticate as oper.
|
||||
tserver.sendCommand(operToken, map[string]any{
|
||||
commandKey: "OPER",
|
||||
bodyKey: []string{testOperName, testOperPassword},
|
||||
})
|
||||
|
||||
var msgs []map[string]any
|
||||
|
||||
msgs, lastID = tserver.pollMessages(operToken, lastID)
|
||||
|
||||
if !findNumeric(msgs, "381") {
|
||||
t.Fatalf(
|
||||
"expected RPL_YOUREOPER (381), got %v",
|
||||
msgs,
|
||||
)
|
||||
}
|
||||
|
||||
// Now WHOIS the target.
|
||||
tserver.sendCommand(operToken, map[string]any{
|
||||
commandKey: "WHOIS",
|
||||
toKey: "target",
|
||||
})
|
||||
|
||||
msgs, _ = tserver.pollMessages(operToken, lastID)
|
||||
|
||||
// Expect 338 RPL_WHOISACTUALLY with client IP.
|
||||
whoisActually := findNumericWithParams(msgs, "338")
|
||||
if whoisActually == nil {
|
||||
t.Fatalf(
|
||||
"expected RPL_WHOISACTUALLY (338) for "+
|
||||
"oper WHOIS, got %v",
|
||||
msgs,
|
||||
)
|
||||
}
|
||||
|
||||
params := getNumericParams(whoisActually)
|
||||
if len(params) < 2 {
|
||||
t.Fatalf(
|
||||
"expected at least 2 params in 338, "+
|
||||
"got %v",
|
||||
params,
|
||||
)
|
||||
}
|
||||
|
||||
// First param should be the target nick.
|
||||
if params[0] != "target" {
|
||||
t.Fatalf(
|
||||
"expected first param 'target', got %s",
|
||||
params[0],
|
||||
)
|
||||
}
|
||||
|
||||
// Second param should be a non-empty IP.
|
||||
if params[1] == "" {
|
||||
t.Fatal("expected non-empty IP in 338 params")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNonOperWhoisHidesClientInfo(t *testing.T) {
|
||||
tserver := newTestServerWithOper(t)
|
||||
|
||||
// Create a target user.
|
||||
_ = tserver.createSession("hidden")
|
||||
|
||||
// Create a regular (non-oper) user.
|
||||
regToken := tserver.createSession("regular")
|
||||
_, lastID := tserver.pollMessages(regToken, 0)
|
||||
|
||||
// WHOIS the target without oper status.
|
||||
tserver.sendCommand(regToken, map[string]any{
|
||||
commandKey: "WHOIS",
|
||||
toKey: "hidden",
|
||||
})
|
||||
|
||||
msgs, _ := tserver.pollMessages(regToken, lastID)
|
||||
|
||||
// Should NOT see 338 RPL_WHOISACTUALLY.
|
||||
if findNumeric(msgs, "338") {
|
||||
t.Fatalf(
|
||||
"non-oper should not see "+
|
||||
"RPL_WHOISACTUALLY (338), got %v",
|
||||
msgs,
|
||||
)
|
||||
}
|
||||
|
||||
// But should see 311 RPL_WHOISUSER (normal WHOIS).
|
||||
if !findNumeric(msgs, "311") {
|
||||
t.Fatalf(
|
||||
"expected RPL_WHOISUSER (311), got %v",
|
||||
msgs,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWhoisShowsOperatorStatus(t *testing.T) {
|
||||
tserver := newTestServerWithOper(t)
|
||||
|
||||
// Create oper user and authenticate.
|
||||
operToken := tserver.createSession("iamoper")
|
||||
_, lastID := tserver.pollMessages(operToken, 0)
|
||||
|
||||
tserver.sendCommand(operToken, map[string]any{
|
||||
commandKey: "OPER",
|
||||
bodyKey: []string{testOperName, testOperPassword},
|
||||
})
|
||||
|
||||
msgs, _ := tserver.pollMessages(operToken, lastID)
|
||||
|
||||
if !findNumeric(msgs, "381") {
|
||||
t.Fatalf("expected 381, got %v", msgs)
|
||||
}
|
||||
|
||||
// Another user does WHOIS on the oper.
|
||||
queryToken := tserver.createSession("asker")
|
||||
_, queryLastID := tserver.pollMessages(queryToken, 0)
|
||||
|
||||
tserver.sendCommand(queryToken, map[string]any{
|
||||
commandKey: "WHOIS",
|
||||
toKey: "iamoper",
|
||||
})
|
||||
|
||||
msgs, _ = tserver.pollMessages(queryToken, queryLastID)
|
||||
|
||||
// Should see 313 RPL_WHOISOPERATOR.
|
||||
if !findNumeric(msgs, "313") {
|
||||
t.Fatalf(
|
||||
"expected RPL_WHOISOPERATOR (313) in "+
|
||||
"WHOIS of oper, got %v",
|
||||
msgs,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOperNoOlineConfigured(t *testing.T) {
|
||||
// Standard test server has no oper configured.
|
||||
tserver := newTestServer(t)
|
||||
|
||||
token := tserver.createSession("nooline")
|
||||
_, lastID := tserver.pollMessages(token, 0)
|
||||
|
||||
tserver.sendCommand(token, map[string]any{
|
||||
commandKey: "OPER",
|
||||
bodyKey: []string{testOperName, "password"},
|
||||
})
|
||||
|
||||
msgs, _ := tserver.pollMessages(token, lastID)
|
||||
|
||||
// Should get 491 since no o-line is configured.
|
||||
if !findNumeric(msgs, "491") {
|
||||
t.Fatalf(
|
||||
"expected ERR_NOOPERHOST (491) when no "+
|
||||
"o-line configured, got %v",
|
||||
msgs,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user