Quellcode durchsuchen

control: enforce JSON content type for config API

tags/v0.9.0
Jan Svabenik vor 1 Monat
Ursprung
Commit
b51a7da522
3 geänderte Dateien mit 54 neuen und 5 gelöschten Zeilen
  1. +2
    -0
      docs/API.md
  2. +21
    -1
      internal/control/control.go
  3. +31
    -4
      internal/control/control_test.go

+ 2
- 0
docs/API.md Datei anzeigen

@@ -144,6 +144,8 @@ The control snapshot (GET /config) only reflects new values once they pass valid


**Request body:** JSON with any subset of patchable fields. **Request body:** JSON with any subset of patchable fields.


**Content-Type:** `application/json` (charset parameters allowed). Requests without it are rejected with 415 Unsupported Media Type.

**Response:** **Response:**
```json ```json
{"ok": true, "live": true} {"ok": true, "live": true}


+ 21
- 1
internal/control/control.go Datei anzeigen

@@ -4,6 +4,7 @@ import (
_ "embed" _ "embed"
"encoding/json" "encoding/json"
"io" "io"
"mime"
"net/http" "net/http"
"strings" "strings"
"sync" "sync"
@@ -50,7 +51,22 @@ type Server struct {
streamSrc *audio.StreamSource // optional, for live audio ingest streamSrc *audio.StreamSource // optional, for live audio ingest
} }


const maxConfigBodyBytes = 64 << 10 // 64 KiB
const (
maxConfigBodyBytes = 64 << 10 // 64 KiB
configContentTypeHeader = "application/json"
)

func isJSONContentType(r *http.Request) bool {
ct := strings.TrimSpace(r.Header.Get("Content-Type"))
if ct == "" {
return false
}
mediaType, _, err := mime.ParseMediaType(ct)
if err != nil {
return false
}
return strings.EqualFold(mediaType, configContentTypeHeader)
}


type ConfigPatch struct { type ConfigPatch struct {
FrequencyMHz *float64 `json:"frequencyMHz,omitempty"` FrequencyMHz *float64 `json:"frequencyMHz,omitempty"`
@@ -299,6 +315,10 @@ func (s *Server) handleConfig(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(cfg) _ = json.NewEncoder(w).Encode(cfg)
case http.MethodPost: case http.MethodPost:
if !isJSONContentType(r) {
http.Error(w, "Content-Type must be application/json", http.StatusUnsupportedMediaType)
return
}
r.Body = http.MaxBytesReader(w, r.Body, maxConfigBodyBytes) r.Body = http.MaxBytesReader(w, r.Body, maxConfigBodyBytes)
var patch ConfigPatch var patch ConfigPatch
if err := json.NewDecoder(r.Body).Decode(&patch); err != nil { if err := json.NewDecoder(r.Body).Decode(&patch); err != nil {


+ 31
- 4
internal/control/control_test.go Datei anzeigen

@@ -127,7 +127,7 @@ func TestConfigPatch(t *testing.T) {
srv := NewServer(cfgpkg.Default()) srv := NewServer(cfgpkg.Default())
body := []byte(`{"toneLeftHz":900,"radioText":"hello world","preEmphasisTauUS":75}`) body := []byte(`{"toneLeftHz":900,"radioText":"hello world","preEmphasisTauUS":75}`)
rec := httptest.NewRecorder() rec := httptest.NewRecorder()
srv.Handler().ServeHTTP(rec, httptest.NewRequest(http.MethodPost, "/config", bytes.NewReader(body)))
srv.Handler().ServeHTTP(rec, newConfigPostRequest(body))
if rec.Code != 200 { if rec.Code != 200 {
t.Fatalf("status: %d body=%s", rec.Code, rec.Body.String()) t.Fatalf("status: %d body=%s", rec.Code, rec.Body.String())
} }
@@ -139,13 +139,34 @@ func TestConfigPatchRejectsOversizeBody(t *testing.T) {
payload := bytes.Repeat([]byte("x"), maxConfigBodyBytes+32) payload := bytes.Repeat([]byte("x"), maxConfigBodyBytes+32)
body := append([]byte(`{"ps":"`), payload...) body := append([]byte(`{"ps":"`), payload...)
body = append(body, []byte(`"}`)...) body = append(body, []byte(`"}`)...)
req := httptest.NewRequest(http.MethodPost, "/config", bytes.NewReader(body))
req := newConfigPostRequest(body)
srv.Handler().ServeHTTP(rec, req) srv.Handler().ServeHTTP(rec, req)
if rec.Code != http.StatusRequestEntityTooLarge { if rec.Code != http.StatusRequestEntityTooLarge {
t.Fatalf("expected 413, got %d response=%q", rec.Code, rec.Body.String()) t.Fatalf("expected 413, got %d response=%q", rec.Code, rec.Body.String())
} }
} }


func TestConfigPatchRejectsMissingContentType(t *testing.T) {
srv := NewServer(cfgpkg.Default())
rec := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPost, "/config", bytes.NewReader([]byte(`{}`)))
srv.Handler().ServeHTTP(rec, req)
if rec.Code != http.StatusUnsupportedMediaType {
t.Fatalf("expected 415 when Content-Type missing, got %d", rec.Code)
}
}

func TestConfigPatchRejectsNonJSONContentType(t *testing.T) {
srv := NewServer(cfgpkg.Default())
rec := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPost, "/config", bytes.NewReader([]byte(`{}`)))
req.Header.Set("Content-Type", "text/plain")
srv.Handler().ServeHTTP(rec, req)
if rec.Code != http.StatusUnsupportedMediaType {
t.Fatalf("expected 415 for non-JSON Content-Type, got %d", rec.Code)
}
}

func TestRuntimeWithoutDriver(t *testing.T) { func TestRuntimeWithoutDriver(t *testing.T) {
srv := NewServer(cfgpkg.Default()) srv := NewServer(cfgpkg.Default())
rec := httptest.NewRecorder() rec := httptest.NewRecorder()
@@ -334,7 +355,7 @@ func TestConfigPatchUpdatesSnapshot(t *testing.T) {


rec := httptest.NewRecorder() rec := httptest.NewRecorder()
body := []byte(`{"outputDrive":1.2}`) body := []byte(`{"outputDrive":1.2}`)
srv.Handler().ServeHTTP(rec, httptest.NewRequest(http.MethodPost, "/config", bytes.NewReader(body)))
srv.Handler().ServeHTTP(rec, newConfigPostRequest(body))
if rec.Code != 200 { if rec.Code != 200 {
t.Fatalf("status: %d", rec.Code) t.Fatalf("status: %d", rec.Code)
} }
@@ -363,7 +384,7 @@ func TestConfigPatchEngineRejectsDoesNotUpdateSnapshot(t *testing.T) {


body := []byte(`{"outputDrive":2.2}`) body := []byte(`{"outputDrive":2.2}`)
rec := httptest.NewRecorder() rec := httptest.NewRecorder()
srv.Handler().ServeHTTP(rec, httptest.NewRequest(http.MethodPost, "/config", bytes.NewReader(body)))
srv.Handler().ServeHTTP(rec, newConfigPostRequest(body))
if rec.Code != http.StatusBadRequest { if rec.Code != http.StatusBadRequest {
t.Fatalf("expected 400, got %d", rec.Code) t.Fatalf("expected 400, got %d", rec.Code)
} }
@@ -379,6 +400,12 @@ func TestConfigPatchEngineRejectsDoesNotUpdateSnapshot(t *testing.T) {
} }
} }


func newConfigPostRequest(body []byte) *http.Request {
req := httptest.NewRequest(http.MethodPost, "/config", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
return req
}

type fakeTXController struct { type fakeTXController struct {
updateErr error updateErr error
resetErr error resetErr error


Laden…
Abbrechen
Speichern