Compare commits
1
Commits
next
...
ca4dadf1eb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ca4dadf1eb |
@@ -643,8 +643,10 @@ Send a message to a channel or user. This is the primary messaging command.
|
|||||||
message echoed back via the queue).
|
message echoed back via the queue).
|
||||||
- If `to` is a bare nick, the message is a DM. The server fans out to the
|
- If `to` is a bare nick, the message is a DM. The server fans out to the
|
||||||
recipient and the sender (so all of the sender's clients see the DM).
|
recipient and the sender (so all of the sender's clients see the DM).
|
||||||
- `body` must be a non-empty array of strings; otherwise the server sends
|
- A `body` that is not an array of strings, or a `meta` that is not an object,
|
||||||
ERR_NOTEXTTOSEND (412). A missing `to` gets ERR_NORECIPIENT (411).
|
gets HTTP 400 `invalid request body`, and nothing is relayed. A missing or
|
||||||
|
empty `body` gets ERR_NOTEXTTOSEND (412). A missing `to` gets ERR_NORECIPIENT
|
||||||
|
(411).
|
||||||
- Only channel members can send to a channel. A non-member, a banned user, and
|
- Only channel members can send to a channel. A non-member, a banned user, and
|
||||||
on a `+m` channel a user with neither `+o` nor `+v`, get ERR_CANNOTSENDTOCHAN
|
on a `+m` channel a user with neither `+o` nor `+v`, get ERR_CANNOTSENDTOCHAN
|
||||||
(404).
|
(404).
|
||||||
@@ -1749,14 +1751,15 @@ command). **Numeric replies**, for success and for errors, are delivered through
|
|||||||
the message queue (see [Numeric Reply Codes](#numeric-reply-codes-s2c-only)); a
|
the message queue (see [Numeric Reply Codes](#numeric-reply-codes-s2c-only)); a
|
||||||
command that has none, such as a successful `PRIVMSG`, is answered by the HTTP
|
command that has none, such as a successful `PRIVMSG`, is answered by the HTTP
|
||||||
response alone, and `PING`'s `PONG` is the HTTP response body. HTTP error codes
|
response alone, and `PING`'s `PONG` is the HTTP response body. HTTP error codes
|
||||||
(4xx/5xx) are reserved for transport-level problems: malformed JSON (400),
|
(4xx/5xx) are reserved for transport-level problems: a malformed request (400),
|
||||||
missing/invalid auth cookies (401), and server errors (500).
|
missing/invalid auth cookies (401), and server errors (500).
|
||||||
|
|
||||||
**HTTP errors (transport-level only):**
|
**HTTP errors (transport-level only):**
|
||||||
|
|
||||||
| Status | Error | When |
|
| Status | Error | When |
|
||||||
| ------ | ---------------------- | ------------------------------------------------------- |
|
| ------ | ---------------------- | ------------------------------------------------------------------------------------------------- |
|
||||||
| 400 | `invalid request body` | Malformed JSON, or a body over `MAX_MESSAGE_SIZE` bytes |
|
| 400 | `invalid request body` | Malformed JSON, or a body over `MAX_MESSAGE_SIZE` bytes |
|
||||||
|
| 400 | `invalid request body` | A `PRIVMSG` or `NOTICE` whose `body` is not an array of strings, or whose `meta` is not an object |
|
||||||
| 400 | `command required` | Empty command |
|
| 400 | `command required` | Empty command |
|
||||||
| 401 | `not registered` | Missing or invalid auth cookie |
|
| 401 | `not registered` | Missing or invalid auth cookie |
|
||||||
| 500 | `internal error` | Server-side failure |
|
| 500 | `internal error` | Server-side failure |
|
||||||
|
|||||||
@@ -1141,6 +1141,16 @@ func (hdlr *Handlers) handlePrivmsg(
|
|||||||
meta json.RawMessage,
|
meta json.RawMessage,
|
||||||
bodyLines func() []string,
|
bodyLines func() []string,
|
||||||
) {
|
) {
|
||||||
|
if !validBodyAndMeta(body, meta) {
|
||||||
|
hdlr.respondError(
|
||||||
|
writer, request,
|
||||||
|
"invalid request body",
|
||||||
|
http.StatusBadRequest,
|
||||||
|
)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if target == "" {
|
if target == "" {
|
||||||
hdlr.enqueueNumeric(
|
hdlr.enqueueNumeric(
|
||||||
request.Context(), clientID,
|
request.Context(), clientID,
|
||||||
@@ -1189,6 +1199,40 @@ func (hdlr *Handlers) handlePrivmsg(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// validBodyAndMeta reports whether body, when present, is an
|
||||||
|
// array of strings and meta, when present, is an object.
|
||||||
|
// Decoding into []string would turn a null line into "", so
|
||||||
|
// each line is decoded as any and checked. A null body or
|
||||||
|
// meta decodes without an error into nil.
|
||||||
|
func validBodyAndMeta(body, meta json.RawMessage) bool {
|
||||||
|
if body != nil {
|
||||||
|
var lines []any
|
||||||
|
|
||||||
|
err := json.Unmarshal(body, &lines)
|
||||||
|
if err != nil || lines == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, line := range lines {
|
||||||
|
_, isString := line.(string)
|
||||||
|
if !isString {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if meta != nil {
|
||||||
|
var fields map[string]any
|
||||||
|
|
||||||
|
err := json.Unmarshal(meta, &fields)
|
||||||
|
if err != nil || fields == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
// respondIRCError enqueues a numeric error reply, notifies
|
// respondIRCError enqueues a numeric error reply, notifies
|
||||||
// the broker, and sends HTTP 200 with {"status":"error"}.
|
// the broker, and sends HTTP 200 with {"status":"error"}.
|
||||||
func (hdlr *Handlers) respondIRCError(
|
func (hdlr *Handlers) respondIRCError(
|
||||||
|
|||||||
@@ -8,10 +8,12 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"maps"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
"reflect"
|
||||||
"slices"
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -1056,6 +1058,132 @@ func TestMessageMissingTo(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestMessageInvalidBodyOrMeta checks that a PRIVMSG or
|
||||||
|
// NOTICE, to a channel or a nick, whose body is not an array
|
||||||
|
// of strings or whose meta is not an object gets 400 and
|
||||||
|
// reaches nobody.
|
||||||
|
func TestMessageInvalidBodyOrMeta(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
fields map[string]any
|
||||||
|
}{
|
||||||
|
{"null line", map[string]any{bodyKey: []any{"hi", nil}}},
|
||||||
|
{"number line", map[string]any{bodyKey: []any{"hi", 1}}},
|
||||||
|
{"object line", map[string]any{bodyKey: []any{map[string]any{}}}},
|
||||||
|
{"null body", map[string]any{bodyKey: nil}},
|
||||||
|
{"object body", map[string]any{bodyKey: map[string]any{}}},
|
||||||
|
{"null meta", map[string]any{bodyKey: []string{"hi"}, metaKey: nil}},
|
||||||
|
{"array meta", map[string]any{bodyKey: []string{"hi"}, metaKey: []string{}}},
|
||||||
|
{"string meta", map[string]any{bodyKey: []string{"hi"}, metaKey: "hi"}},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range cases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
const channel = "#badmsg"
|
||||||
|
|
||||||
|
tserver := newTestServer(t)
|
||||||
|
aliceToken := tserver.createSession("alice_badmsg")
|
||||||
|
bobToken := tserver.createSession("bob_badmsg")
|
||||||
|
|
||||||
|
tserver.sendCommand(aliceToken, map[string]any{
|
||||||
|
commandKey: joinCmd, toKey: channel,
|
||||||
|
})
|
||||||
|
tserver.sendCommand(bobToken, map[string]any{
|
||||||
|
commandKey: joinCmd, toKey: channel,
|
||||||
|
})
|
||||||
|
|
||||||
|
_, bobLastID := tserver.pollMessages(bobToken, 0)
|
||||||
|
|
||||||
|
for _, command := range []string{privmsgCmd, irc.CmdNotice} {
|
||||||
|
for _, target := range []string{channel, "bob_badmsg"} {
|
||||||
|
request := map[string]any{
|
||||||
|
commandKey: command, toKey: target,
|
||||||
|
}
|
||||||
|
maps.Copy(request, tc.fields)
|
||||||
|
|
||||||
|
status, result := tserver.sendCommand(
|
||||||
|
aliceToken, request,
|
||||||
|
)
|
||||||
|
if status != http.StatusBadRequest ||
|
||||||
|
result["error"] != "invalid request body" {
|
||||||
|
t.Errorf(
|
||||||
|
"%s to %s: expected 400 invalid "+
|
||||||
|
"request body, got %d: %v",
|
||||||
|
command, target, status, result,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
msgs, _ := tserver.pollMessages(bobToken, bobLastID)
|
||||||
|
if len(msgs) != 0 {
|
||||||
|
t.Fatalf("expected no messages, bob got %v", msgs)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestMessageRelaysBodyAndMeta checks that a PRIVMSG or
|
||||||
|
// NOTICE, to a channel or a nick, reaches its recipient with
|
||||||
|
// body and meta as sent.
|
||||||
|
func TestMessageRelaysBodyAndMeta(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
const channel = "#relaymsg"
|
||||||
|
|
||||||
|
tserver := newTestServer(t)
|
||||||
|
aliceToken := tserver.createSession("alice_relaymsg")
|
||||||
|
bobToken := tserver.createSession("bob_relaymsg")
|
||||||
|
|
||||||
|
tserver.sendCommand(aliceToken, map[string]any{
|
||||||
|
commandKey: joinCmd, toKey: channel,
|
||||||
|
})
|
||||||
|
tserver.sendCommand(bobToken, map[string]any{
|
||||||
|
commandKey: joinCmd, toKey: channel,
|
||||||
|
})
|
||||||
|
|
||||||
|
_, bobLastID := tserver.pollMessages(bobToken, 0)
|
||||||
|
|
||||||
|
body := []any{"line one", ""}
|
||||||
|
meta := map[string]any{"sig": "abc", "alg": "ed25519"}
|
||||||
|
|
||||||
|
for _, command := range []string{privmsgCmd, irc.CmdNotice} {
|
||||||
|
for _, target := range []string{channel, "bob_relaymsg"} {
|
||||||
|
status, result := tserver.sendCommand(
|
||||||
|
aliceToken,
|
||||||
|
map[string]any{
|
||||||
|
commandKey: command, toKey: target,
|
||||||
|
bodyKey: body, metaKey: meta,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if status != http.StatusOK {
|
||||||
|
t.Fatalf(
|
||||||
|
"%s to %s: expected 200, got %d: %v",
|
||||||
|
command, target, status, result,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
var msgs []map[string]any
|
||||||
|
|
||||||
|
msgs, bobLastID = tserver.pollMessages(
|
||||||
|
bobToken, bobLastID,
|
||||||
|
)
|
||||||
|
if len(msgs) != 1 ||
|
||||||
|
msgs[0][commandKey] != command ||
|
||||||
|
!reflect.DeepEqual(msgs[0][bodyKey], body) ||
|
||||||
|
!reflect.DeepEqual(msgs[0][metaKey], meta) {
|
||||||
|
t.Fatalf(
|
||||||
|
"%s to %s: bob got %v", command, target, msgs,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestNonMemberCannotSend(t *testing.T) {
|
func TestNonMemberCannotSend(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user