Apply MAX_MESSAGE_SIZE to the whole request body (closes #250) #253
@@ -188,7 +188,7 @@ func (hdlr *Handlers) handleCreateSession(
|
|||||||
|
|
||||||
var payload createRequest
|
var payload createRequest
|
||||||
|
|
||||||
err := json.NewDecoder(request.Body).Decode(&payload)
|
err := decodeJSON(request, &payload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
hdlr.respondError(
|
hdlr.respondError(
|
||||||
writer, request,
|
writer, request,
|
||||||
@@ -925,7 +925,7 @@ func (hdlr *Handlers) HandleSendCommand() http.HandlerFunc {
|
|||||||
|
|
||||||
var payload commandRequest
|
var payload commandRequest
|
||||||
|
|
||||||
err := json.NewDecoder(request.Body).Decode(&payload)
|
err := decodeJSON(request, &payload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
hdlr.respondError(
|
hdlr.respondError(
|
||||||
writer, request,
|
writer, request,
|
||||||
|
|||||||
@@ -797,6 +797,107 @@ func TestCreateSessionMalformed(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (tserver *testServer) postBody(
|
||||||
|
path, token, body string,
|
||||||
|
) (int, map[string]any) {
|
||||||
|
tserver.t.Helper()
|
||||||
|
|
||||||
|
resp, err := doRequestAuth(
|
||||||
|
tserver.t, http.MethodPost, tserver.url(path), token,
|
||||||
|
strings.NewReader(body),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
tserver.t.Fatalf("post %s: %v", path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() { _ = resp.Body.Close() }()
|
||||||
|
|
||||||
|
var result map[string]any
|
||||||
|
|
||||||
|
_ = json.NewDecoder(resp.Body).Decode(&result)
|
||||||
|
|
||||||
|
return resp.StatusCode, result
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestRequestBodyLimit sends each endpoint a small valid JSON value
|
||||||
|
// followed by other bytes. Only whitespace may follow the value, and
|
||||||
|
// the whole body counts towards MAX_MESSAGE_SIZE.
|
||||||
|
func TestRequestBodyLimit(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
const (
|
||||||
|
maxMessageSize = 4096 // the default MAX_MESSAGE_SIZE
|
||||||
|
nick = "alice_bodylimit"
|
||||||
|
)
|
||||||
|
|
||||||
|
endpoints := []struct {
|
||||||
|
name, path, value string
|
||||||
|
acceptedStatus int
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"create session", apiSession,
|
||||||
|
`{"nick":"bob_bodylimit"}`,
|
||||||
|
http.StatusCreated,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login", "/api/v1/login",
|
||||||
|
`{"nick":"` + nick + `","password":"` + testPassword + `"}`,
|
||||||
|
http.StatusOK,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"post message", apiMessages,
|
||||||
|
`{"command":"PING"}`,
|
||||||
|
http.StatusOK,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, endpoint := range endpoints {
|
||||||
|
t.Run(endpoint.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
tserver := newTestServer(t)
|
||||||
|
token := tserver.createSession(nick)
|
||||||
|
|
||||||
|
tserver.sendCommand(token, map[string]any{
|
||||||
|
commandKey: irc.CmdPass,
|
||||||
|
bodyKey: []string{testPassword},
|
||||||
|
})
|
||||||
|
|
||||||
|
bytesToLimit := maxMessageSize - len(endpoint.value)
|
||||||
|
|
||||||
|
for _, trailing := range []string{
|
||||||
|
strings.Repeat(" ", bytesToLimit+1),
|
||||||
|
strings.Repeat("x", bytesToLimit+1),
|
||||||
|
"x",
|
||||||
|
"{}",
|
||||||
|
} {
|
||||||
|
status, result := tserver.postBody(
|
||||||
|
endpoint.path, token, endpoint.value+trailing,
|
||||||
|
)
|
||||||
|
if status != http.StatusBadRequest ||
|
||||||
|
result["error"] != "invalid request body" {
|
||||||
|
t.Errorf(
|
||||||
|
"%d bytes of %.10q after the value: expected "+
|
||||||
|
"400 invalid request body, got %d: %v",
|
||||||
|
len(trailing), trailing, status, result,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
status, result := tserver.postBody(
|
||||||
|
endpoint.path, token,
|
||||||
|
endpoint.value+strings.Repeat(" ", bytesToLimit),
|
||||||
|
)
|
||||||
|
if status != endpoint.acceptedStatus {
|
||||||
|
t.Errorf(
|
||||||
|
"body of exactly the limit: expected %d, got %d: %v",
|
||||||
|
endpoint.acceptedStatus, status, result,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestAuthNoCookie(t *testing.T) {
|
func TestAuthNoCookie(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -55,7 +54,7 @@ func (hdlr *Handlers) handleLogin(
|
|||||||
|
|
||||||
var payload loginRequest
|
var payload loginRequest
|
||||||
|
|
||||||
err := json.NewDecoder(request.Body).Decode(&payload)
|
err := decodeJSON(request, &payload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
hdlr.respondError(
|
hdlr.respondError(
|
||||||
writer, request,
|
writer, request,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
"io"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
@@ -159,6 +160,18 @@ func (hdlr *Handlers) respondError(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// decodeJSON reads the whole request body and decodes it into dest.
|
||||||
|
// It fails if the body is larger than the http.MaxBytesReader limit,
|
||||||
|
// or holds anything other than one JSON value and whitespace.
|
||||||
|
func decodeJSON(request *http.Request, dest any) error {
|
||||||
|
body, err := io.ReadAll(request.Body)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return json.Unmarshal(body, dest)
|
||||||
|
}
|
||||||
|
|
||||||
func (hdlr *Handlers) idleTimeout() time.Duration {
|
func (hdlr *Handlers) idleTimeout() time.Duration {
|
||||||
raw := hdlr.params.Config.SessionIdleTimeout
|
raw := hdlr.params.Config.SessionIdleTimeout
|
||||||
if raw == "" {
|
if raw == "" {
|
||||||
|
|||||||
Reference in New Issue
Block a user