Add the tier 3 utility IRC commands (closes #87) #96
No files matched your search
@@ -1951,8 +1951,9 @@ func (hdlr *Handlers) handleQuit(
|
|||||||
http.StatusOK)
|
http.StatusOK)
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleMode handles the MODE command for channels and
|
// handleMode handles the MODE command: a target that starts
|
||||||
// users. Currently supports query-only (no mode changes).
|
// with # is a channel, handled by handleChannelMode, and any
|
||||||
|
// other target a nick, handled by handleUserMode.
|
||||||
func (hdlr *Handlers) handleMode(
|
func (hdlr *Handlers) handleMode(
|
||||||
writer http.ResponseWriter,
|
writer http.ResponseWriter,
|
||||||
request *http.Request,
|
request *http.Request,
|
||||||
|
|||||||
@@ -106,22 +106,20 @@ func (hdlr *Handlers) handleAdmin(
|
|||||||
) {
|
) {
|
||||||
ctx := request.Context()
|
ctx := request.Context()
|
||||||
srvName := hdlr.serverName()
|
srvName := hdlr.serverName()
|
||||||
|
location1, location2, email := hdlr.svc.AdminLines(srvName)
|
||||||
|
|
||||||
hdlr.enqueueNumeric(
|
hdlr.enqueueNumeric(
|
||||||
ctx, clientID, irc.RplAdminMe, nick,
|
ctx, clientID, irc.RplAdminMe, nick,
|
||||||
[]string{srvName}, "Administrative info",
|
[]string{srvName}, "Administrative info",
|
||||||
)
|
)
|
||||||
hdlr.enqueueNumeric(
|
hdlr.enqueueNumeric(
|
||||||
ctx, clientID, irc.RplAdminLoc1, nick, nil,
|
ctx, clientID, irc.RplAdminLoc1, nick, nil, location1,
|
||||||
"neoirc server",
|
|
||||||
)
|
)
|
||||||
hdlr.enqueueNumeric(
|
hdlr.enqueueNumeric(
|
||||||
ctx, clientID, irc.RplAdminLoc2, nick, nil,
|
ctx, clientID, irc.RplAdminLoc2, nick, nil, location2,
|
||||||
"IRC over HTTP",
|
|
||||||
)
|
)
|
||||||
hdlr.enqueueNumeric(
|
hdlr.enqueueNumeric(
|
||||||
ctx, clientID, irc.RplAdminEmail, nick, nil,
|
ctx, clientID, irc.RplAdminEmail, nick, nil, email,
|
||||||
"admin@"+srvName,
|
|
||||||
)
|
)
|
||||||
hdlr.broker.Notify(sessionID)
|
hdlr.broker.Notify(sessionID)
|
||||||
hdlr.respondJSON(writer, request,
|
hdlr.respondJSON(writer, request,
|
||||||
@@ -199,7 +197,7 @@ func (hdlr *Handlers) handleKill(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
reason := "KILLed"
|
reason := ""
|
||||||
if len(lines) > 1 {
|
if len(lines) > 1 {
|
||||||
reason = lines[1]
|
reason = lines[1]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,6 +90,16 @@ func TestUserhost(t *testing.T) {
|
|||||||
t.Errorf("expected alice and bob, got %q", body)
|
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{
|
msgs, _ = sendAndPoll(tserver, token, lastID, map[string]any{
|
||||||
commandKey: irc.CmdUserhost,
|
commandKey: irc.CmdUserhost,
|
||||||
})
|
})
|
||||||
@@ -187,8 +197,8 @@ func TestKill(t *testing.T) {
|
|||||||
commandKey: irc.CmdKill,
|
commandKey: irc.CmdKill,
|
||||||
bodyKey: []string{"victim", "go away"},
|
bodyKey: []string{"victim", "go away"},
|
||||||
})
|
})
|
||||||
if status != http.StatusOK {
|
if status != http.StatusOK || result[statusKey] != "ok" {
|
||||||
t.Fatalf("expected 200, got %d: %v", status, result)
|
t.Fatalf("expected 200 and status ok, got %d: %v", status, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
msgs, _ = tserver.pollMessages(observerToken, observerLastID)
|
msgs, _ = tserver.pollMessages(observerToken, observerLastID)
|
||||||
@@ -283,6 +293,7 @@ func TestUserMode(t *testing.T) {
|
|||||||
{nick, "", "221", "+w"},
|
{nick, "", "221", "+w"},
|
||||||
{strings.ToUpper(nick), "-w", "221", "+"},
|
{strings.ToUpper(nick), "-w", "221", "+"},
|
||||||
{nick, "+z", "501", ""},
|
{nick, "+z", "501", ""},
|
||||||
|
{nick, "+o", "501", ""},
|
||||||
{other, "", "502", ""},
|
{other, "", "502", ""},
|
||||||
{other, "-w", "502", ""},
|
{other, "-w", "502", ""},
|
||||||
} {
|
} {
|
||||||
|
|||||||
@@ -1313,12 +1313,14 @@ func (c *Conn) handleVersion() {
|
|||||||
|
|
||||||
// handleAdmin replies with the server's admin info.
|
// handleAdmin replies with the server's admin info.
|
||||||
func (c *Conn) handleAdmin() {
|
func (c *Conn) handleAdmin() {
|
||||||
|
location1, location2, email := c.svc.AdminLines(c.serverSfx)
|
||||||
|
|
||||||
c.sendNumeric(
|
c.sendNumeric(
|
||||||
irc.RplAdminMe, c.serverSfx, "Administrative info",
|
irc.RplAdminMe, c.serverSfx, "Administrative info",
|
||||||
)
|
)
|
||||||
c.sendNumeric(irc.RplAdminLoc1, "neoirc server")
|
c.sendNumeric(irc.RplAdminLoc1, location1)
|
||||||
c.sendNumeric(irc.RplAdminLoc2, "IRC over HTTP")
|
c.sendNumeric(irc.RplAdminLoc2, location2)
|
||||||
c.sendNumeric(irc.RplAdminEmail, "admin@"+c.serverSfx)
|
c.sendNumeric(irc.RplAdminEmail, email)
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleInfo replies with the server's software info.
|
// handleInfo replies with the server's software info.
|
||||||
@@ -1352,7 +1354,7 @@ func (c *Conn) handleKill(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
reason := "KILLed"
|
reason := ""
|
||||||
if len(msg.Params) > 1 {
|
if len(msg.Params) > 1 {
|
||||||
reason = msg.Params[1]
|
reason = msg.Params[1]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,7 +37,9 @@ func newKillVictim(t *testing.T) (*ircserver.Conn, net.Conn) {
|
|||||||
|
|
||||||
// TestDisconnectDoesNotBlockOnUnresponsiveVictim checks
|
// TestDisconnectDoesNotBlockOnUnresponsiveVictim checks
|
||||||
// that Disconnect returns although the victim never reads,
|
// 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(
|
func TestDisconnectDoesNotBlockOnUnresponsiveVictim(
|
||||||
t *testing.T,
|
t *testing.T,
|
||||||
) {
|
) {
|
||||||
@@ -53,7 +55,7 @@ func TestDisconnectDoesNotBlockOnUnresponsiveVictim(
|
|||||||
|
|
||||||
select {
|
select {
|
||||||
case <-returned:
|
case <-returned:
|
||||||
case <-time.After(testTimeout):
|
case <-time.After(ircserver.KillWriteWindow / 4):
|
||||||
t.Fatal("Disconnect waited for the victim to read")
|
t.Fatal("Disconnect waited for the victim to read")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,6 +56,10 @@ func (s *Server) Listener() net.Listener {
|
|||||||
return s.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
|
// NewTestConn returns a Conn for nick on tcpConn, without a
|
||||||
// server, database or service behind it.
|
// server, database or service behind it.
|
||||||
func NewTestConn(
|
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
|
// UserhostReply returns the RPL_USERHOST text for the first
|
||||||
// five of the given nicks: "nick=+user@host" entries joined
|
// five of the given nicks: "nick=+user@host" entries joined
|
||||||
// by spaces, with * after the nick of an operator and - in
|
// 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
|
// KillUser carries out an operator's KILL: the target's
|
||||||
// channel peers see it quit, its session is deleted, and
|
// 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(
|
func (s *Service) KillUser(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
sessionID int64,
|
sessionID int64,
|
||||||
@@ -231,6 +241,10 @@ func (s *Service) KillUser(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if reason == "" {
|
||||||
|
reason = "KILLed"
|
||||||
|
}
|
||||||
|
|
||||||
quitReason := "Killed (" + nick + " (" + reason + "))"
|
quitReason := "Killed (" + nick + " (" + reason + "))"
|
||||||
|
|
||||||
s.BroadcastQuit(ctx, targetSID, targetNick, quitReason)
|
s.BroadcastQuit(ctx, targetSID, targetNick, quitReason)
|
||||||
|
|||||||
@@ -406,6 +406,16 @@ func TestQueryUserMode(t *testing.T) {
|
|||||||
if modes != "+ow" {
|
if modes != "+ow" {
|
||||||
t.Errorf("expected +ow, got %s", modes)
|
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) {
|
func TestUserhostReply(t *testing.T) {
|
||||||
@@ -453,6 +463,16 @@ func TestUserhostReply(t *testing.T) {
|
|||||||
if strings.Count(reply, "bob=") != 5 {
|
if strings.Count(reply, "bob=") != 5 {
|
||||||
t.Errorf("want 5 entries for 6 nicks, got %q", reply)
|
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
|
// userModes is the stored state of a session's user mode
|
||||||
|
|||||||
Reference in new issue
Block a user