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:
@@ -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