fix: rigorous atomic user mode parser and fix router race in server
Some checks failed
check / check (push) Failing after 23s
Some checks failed
check / check (push) Failing after 23s
Mode parser (internal/service/service.go): - Reject strings without leading + or - (e.g. "xw", "w", "") with ERR_UMODEUNKNOWNFLAG instead of silently treating them as "-". - Support multi-sign transitions: +w-o, -w+o, +o-w+w, -x+y, +y-x. The active sign flips each time + or - is seen; subsequent letters apply with the active sign. - Atomic from caller's perspective: parse the whole string to a list of ops first, reject the whole request on any unknown mode char, and only then apply ops to the DB. Partial application of +w before rejecting +o is gone. - HTTP and IRC still share the same ApplyUserMode entry point. Router race (internal/server/server.go): - The fx OnStart hook previously spawned serve() in a goroutine that called SetupRoutes asynchronously, while ServeHTTP delegated to srv.router. Test harnesses (httptest wrapping srv as Handler) raced against SetupRoutes writing srv.router vs ServeHTTP reading it, producing the race detector failures in CI on main. - SetupRoutes is now called synchronously inside OnStart before the serve goroutine starts, so srv.router is fully initialized before any request can reach ServeHTTP. Tests (internal/service/service_test.go): - Replaced the per-mode tests with a single table-driven TestApplyUserMode that asserts both the returned mode string and the persisted DB state (oper/wallops) for each case, including the malformed and multi-sign cases above. The +wz case seeds wallops=true to prove the whole string is rejected and +w is not partially applied.
This commit is contained in:
@@ -814,28 +814,34 @@ func (s *Service) QueryUserMode(
|
||||
return modes
|
||||
}
|
||||
|
||||
// ApplyUserMode parses a mode string character by
|
||||
// character (e.g. "+wo", "-w") and applies each mode
|
||||
// change to the session. Returns the resulting mode string
|
||||
// after all changes, or an IRCError on failure.
|
||||
// userModeOp is a single parsed user-mode change collected
|
||||
// by parseUserModeString before any DB writes happen.
|
||||
type userModeOp struct {
|
||||
char rune
|
||||
adding bool
|
||||
}
|
||||
|
||||
// ApplyUserMode parses an IRC user-mode string and applies
|
||||
// the resulting changes atomically. It supports multiple
|
||||
// sign transitions (e.g. "+w-o", "-w+o", "+o-w+w") and
|
||||
// rejects malformed input (empty string, no leading sign,
|
||||
// bare sign with no mode letters, unknown mode letters,
|
||||
// +o which must be set via OPER) with an IRCError. On
|
||||
// failure, no persistent change is made. On success, the
|
||||
// resulting mode string is returned.
|
||||
func (s *Service) ApplyUserMode(
|
||||
ctx context.Context,
|
||||
sessionID int64,
|
||||
modeStr string,
|
||||
) (string, error) {
|
||||
if len(modeStr) < 2 { //nolint:mnd // +/- prefix + ≥1 char
|
||||
return "", &IRCError{
|
||||
Code: irc.ErrUmodeUnknownFlag,
|
||||
Params: nil,
|
||||
Message: "Unknown MODE flag",
|
||||
}
|
||||
ops, err := parseUserModeString(modeStr)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
adding := modeStr[0] == '+'
|
||||
|
||||
for _, ch := range modeStr[1:] {
|
||||
for _, op := range ops {
|
||||
if err := s.applySingleUserMode(
|
||||
ctx, sessionID, ch, adding,
|
||||
ctx, sessionID, op.char, op.adding,
|
||||
); err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -844,7 +850,79 @@ func (s *Service) ApplyUserMode(
|
||||
return s.QueryUserMode(ctx, sessionID), nil
|
||||
}
|
||||
|
||||
// applySingleUserMode applies one user mode character.
|
||||
// parseUserModeString validates and parses a user-mode
|
||||
// string into a list of operations. The string must begin
|
||||
// with '+' or '-'; subsequent '+' / '-' characters flip the
|
||||
// active sign, and letters between them are applied with
|
||||
// the current sign. Every letter must be a recognized user
|
||||
// mode for this server, and '+o' is never allowed via MODE
|
||||
// (use OPER to become operator). If any character is
|
||||
// invalid, no operations are returned and an IRCError with
|
||||
// ERR_UMODEUNKNOWNFLAG (501) is returned.
|
||||
func parseUserModeString(
|
||||
modeStr string,
|
||||
) ([]userModeOp, error) {
|
||||
unknownFlag := &IRCError{
|
||||
Code: irc.ErrUmodeUnknownFlag,
|
||||
Params: nil,
|
||||
Message: "Unknown MODE flag",
|
||||
}
|
||||
|
||||
if modeStr == "" {
|
||||
return nil, unknownFlag
|
||||
}
|
||||
|
||||
first := modeStr[0]
|
||||
if first != '+' && first != '-' {
|
||||
return nil, unknownFlag
|
||||
}
|
||||
|
||||
ops := make([]userModeOp, 0, len(modeStr)-1)
|
||||
adding := true
|
||||
|
||||
for _, ch := range modeStr {
|
||||
switch ch {
|
||||
case '+':
|
||||
adding = true
|
||||
case '-':
|
||||
adding = false
|
||||
default:
|
||||
if !isKnownUserModeChar(ch) {
|
||||
return nil, unknownFlag
|
||||
}
|
||||
|
||||
if ch == 'o' && adding {
|
||||
return nil, unknownFlag
|
||||
}
|
||||
|
||||
ops = append(ops, userModeOp{
|
||||
char: ch, adding: adding,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if len(ops) == 0 {
|
||||
return nil, unknownFlag
|
||||
}
|
||||
|
||||
return ops, nil
|
||||
}
|
||||
|
||||
// isKnownUserModeChar reports whether the character is a
|
||||
// recognized user mode letter.
|
||||
func isKnownUserModeChar(ch rune) bool {
|
||||
switch ch {
|
||||
case 'w', 'o':
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// applySingleUserMode applies one already-validated user
|
||||
// mode character to the session. parseUserModeString must
|
||||
// have validated the character and sign before this runs;
|
||||
// the default branch here is defence-in-depth only.
|
||||
func (s *Service) applySingleUserMode(
|
||||
ctx context.Context,
|
||||
sessionID int64,
|
||||
@@ -864,7 +942,6 @@ func (s *Service) applySingleUserMode(
|
||||
return fmt.Errorf("set wallops: %w", err)
|
||||
}
|
||||
case 'o':
|
||||
// +o cannot be set via MODE; use OPER command.
|
||||
if adding {
|
||||
return &IRCError{
|
||||
Code: irc.ErrUmodeUnknownFlag,
|
||||
|
||||
Reference in New Issue
Block a user