Add the tier 3 utility IRC commands (closes #87) #96
@@ -1951,8 +1951,9 @@ func (hdlr *Handlers) handleQuit(
|
||||
http.StatusOK)
|
||||
}
|
||||
|
||||
// handleMode handles the MODE command for channels and
|
||||
// users. Currently supports query-only (no mode changes).
|
||||
// handleMode handles the MODE command: a target that starts
|
||||
// with # is a channel, handled by handleChannelMode, and any
|
||||
// other target a nick, handled by handleUserMode.
|
||||
func (hdlr *Handlers) handleMode(
|
||||
writer http.ResponseWriter,
|
||||
request *http.Request,
|
||||
|
||||
@@ -106,22 +106,20 @@ func (hdlr *Handlers) handleAdmin(
|
||||
) {
|
||||
ctx := request.Context()
|
||||
srvName := hdlr.serverName()
|
||||
location1, location2, email := hdlr.svc.AdminLines(srvName)
|
||||
|
||||
hdlr.enqueueNumeric(
|
||||
ctx, clientID, irc.RplAdminMe, nick,
|
||||
[]string{srvName}, "Administrative info",
|
||||
)
|
||||
hdlr.enqueueNumeric(
|
||||
ctx, clientID, irc.RplAdminLoc1, nick, nil,
|
||||
"neoirc server",
|
||||
ctx, clientID, irc.RplAdminLoc1, nick, nil, location1,
|
||||
)
|
||||
hdlr.enqueueNumeric(
|
||||
ctx, clientID, irc.RplAdminLoc2, nick, nil,
|
||||
"IRC over HTTP",
|
||||
ctx, clientID, irc.RplAdminLoc2, nick, nil, location2,
|
||||
)
|
||||
hdlr.enqueueNumeric(
|
||||
ctx, clientID, irc.RplAdminEmail, nick, nil,
|
||||
"admin@"+srvName,
|
||||
ctx, clientID, irc.RplAdminEmail, nick, nil, email,
|
||||
)
|
||||
hdlr.broker.Notify(sessionID)
|
||||
hdlr.respondJSON(writer, request,
|
||||
@@ -199,7 +197,7 @@ func (hdlr *Handlers) handleKill(
|
||||
return
|
||||
}
|
||||
|
||||
reason := "KILLed"
|
||||
reason := ""
|
||||
if len(lines) > 1 {
|
||||
reason = lines[1]
|
||||
}
|
||||
|
||||
@@ -90,6 +90,16 @@ func TestUserhost(t *testing.T) {
|
||||
t.Errorf("expected alice and bob, got %q", body)
|
||||
}
|
||||
|
||||
msgs, lastID = sendAndPoll(tserver, token, lastID, map[string]any{
|
||||
commandKey: irc.CmdUserhost,
|
||||
bodyKey: []string{"ghost"},
|
||||
})
|
||||
|
||||
body = numericBody(t, msgs, "302")
|
||||
if body != "" {
|
||||
t.Errorf("expected an empty reply for ghost, got %q", body)
|
||||
}
|
||||
|
||||
msgs, _ = sendAndPoll(tserver, token, lastID, map[string]any{
|
||||
commandKey: irc.CmdUserhost,
|
||||
})
|
||||
@@ -187,8 +197,8 @@ func TestKill(t *testing.T) {
|
||||
commandKey: irc.CmdKill,
|
||||
bodyKey: []string{"victim", "go away"},
|
||||
})
|
||||
if status != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d: %v", status, result)
|
||||
if status != http.StatusOK || result[statusKey] != "ok" {
|
||||
t.Fatalf("expected 200 and status ok, got %d: %v", status, result)
|
||||
}
|
||||
|
||||
msgs, _ = tserver.pollMessages(observerToken, observerLastID)
|
||||
@@ -283,6 +293,7 @@ func TestUserMode(t *testing.T) {
|
||||
{nick, "", "221", "+w"},
|
||||
{strings.ToUpper(nick), "-w", "221", "+"},
|
||||
{nick, "+z", "501", ""},
|
||||
{nick, "+o", "501", ""},
|
||||
{other, "", "502", ""},
|
||||
{other, "-w", "502", ""},
|
||||
} {
|
||||
|
||||
@@ -1313,12 +1313,14 @@ func (c *Conn) handleVersion() {
|
||||
|
||||
// handleAdmin replies with the server's admin info.
|
||||
func (c *Conn) handleAdmin() {
|
||||
location1, location2, email := c.svc.AdminLines(c.serverSfx)
|
||||
|
||||
c.sendNumeric(
|
||||
irc.RplAdminMe, c.serverSfx, "Administrative info",
|
||||
)
|
||||
c.sendNumeric(irc.RplAdminLoc1, "neoirc server")
|
||||
c.sendNumeric(irc.RplAdminLoc2, "IRC over HTTP")
|
||||
c.sendNumeric(irc.RplAdminEmail, "admin@"+c.serverSfx)
|
||||
c.sendNumeric(irc.RplAdminLoc1, location1)
|
||||
c.sendNumeric(irc.RplAdminLoc2, location2)
|
||||
c.sendNumeric(irc.RplAdminEmail, email)
|
||||
}
|
||||
|
||||
// handleInfo replies with the server's software info.
|
||||
@@ -1352,7 +1354,7 @@ func (c *Conn) handleKill(
|
||||
return
|
||||
}
|
||||
|
||||
reason := "KILLed"
|
||||
reason := ""
|
||||
if len(msg.Params) > 1 {
|
||||
reason = msg.Params[1]
|
||||
}
|
||||
|
||||
@@ -37,7 +37,9 @@ func newKillVictim(t *testing.T) (*ircserver.Conn, net.Conn) {
|
||||
|
||||
// TestDisconnectDoesNotBlockOnUnresponsiveVictim checks
|
||||
// that Disconnect returns although the victim never reads,
|
||||
// so a KILL cannot stall the operator who sent it.
|
||||
// so a KILL cannot stall the operator who sent it. The
|
||||
// limit is well below one KillWriteWindow, so a Disconnect
|
||||
// that waits on any write to the victim fails.
|
||||
func TestDisconnectDoesNotBlockOnUnresponsiveVictim(
|
||||
t *testing.T,
|
||||
) {
|
||||
@@ -53,7 +55,7 @@ func TestDisconnectDoesNotBlockOnUnresponsiveVictim(
|
||||
|
||||
select {
|
||||
case <-returned:
|
||||
case <-time.After(testTimeout):
|
||||
case <-time.After(ircserver.KillWriteWindow / 4):
|
||||
t.Fatal("Disconnect waited for the victim to read")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,6 +56,10 @@ func (s *Server) Listener() net.Listener {
|
||||
return s.listener
|
||||
}
|
||||
|
||||
// KillWriteWindow is how long Disconnect allows each write
|
||||
// to a killed victim.
|
||||
const KillWriteWindow = killWriteWindow
|
||||
|
||||
// NewTestConn returns a Conn for nick on tcpConn, without a
|
||||
// server, database or service behind it.
|
||||
func NewTestConn(
|
||||
|
||||
@@ -119,6 +119,15 @@ func (s *Service) InfoLines() []string {
|
||||
}
|
||||
}
|
||||
|
||||
// AdminLines returns the texts of the RPL_ADMINLOC1,
|
||||
// RPL_ADMINLOC2 and RPL_ADMINEMAIL replies, in that order,
|
||||
// that ADMIN sends on both transports.
|
||||
func (s *Service) AdminLines(
|
||||
serverName string,
|
||||
) (string, string, string) {
|
||||
return "neoirc server", "IRC over HTTP", "admin@" + serverName
|
||||
}
|
||||
|
||||
// UserhostReply returns the RPL_USERHOST text for the first
|
||||
// five of the given nicks: "nick=+user@host" entries joined
|
||||
// by spaces, with * after the nick of an operator and - in
|
||||
@@ -199,7 +208,8 @@ func (s *Service) UnregisterWireConn(
|
||||
|
||||
// KillUser carries out an operator's KILL: the target's
|
||||
// channel peers see it quit, its session is deleted, and
|
||||
// its IRC connection, if it has one, is closed.
|
||||
// its IRC connection, if it has one, is closed. An empty
|
||||
// reason is given as "KILLed".
|
||||
func (s *Service) KillUser(
|
||||
ctx context.Context,
|
||||
sessionID int64,
|
||||
@@ -231,6 +241,10 @@ func (s *Service) KillUser(
|
||||
}
|
||||
}
|
||||
|
||||
if reason == "" {
|
||||
reason = "KILLed"
|
||||
}
|
||||
|
||||
quitReason := "Killed (" + nick + " (" + reason + "))"
|
||||
|
||||
s.BroadcastQuit(ctx, targetSID, targetNick, quitReason)
|
||||
|
||||
@@ -406,6 +406,16 @@ func TestQueryUserMode(t *testing.T) {
|
||||
if modes != "+ow" {
|
||||
t.Errorf("expected +ow, got %s", modes)
|
||||
}
|
||||
|
||||
// A flag that cannot be read is an error, not a flag
|
||||
// that is unset.
|
||||
cancelled, cancel := context.WithCancel(ctx)
|
||||
cancel()
|
||||
|
||||
_, err = env.svc.QueryUserMode(cancelled, sid)
|
||||
if !errors.Is(err, context.Canceled) {
|
||||
t.Errorf("expected the read's error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserhostReply(t *testing.T) {
|
||||
@@ -453,6 +463,16 @@ func TestUserhostReply(t *testing.T) {
|
||||
if strings.Count(reply, "bob=") != 5 {
|
||||
t.Errorf("want 5 entries for 6 nicks, got %q", reply)
|
||||
}
|
||||
|
||||
// A nick that cannot be looked up is an error, not a nick
|
||||
// with no session.
|
||||
cancelled, cancel := context.WithCancel(ctx)
|
||||
cancel()
|
||||
|
||||
_, err = env.svc.UserhostReply(cancelled, []string{"alice"}, "srv")
|
||||
if !errors.Is(err, context.Canceled) {
|
||||
t.Errorf("want the lookup's error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// userModes is the stored state of a session's user mode
|
||||
|
||||
Reference in New Issue
Block a user