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:
@@ -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 '',
|
||||
|
||||
Reference in New Issue
Block a user