Przeglądaj źródła

control: enforce JSON content type for config API

tags/v0.9.0
Jan Svabenik 1 miesiąc temu
rodzic
commit
b51a7da522
3 zmienionych plików z 54 dodań i 5 usunięć
  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 Wyświetl plik

@@ -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.

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

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


+ 21
- 1
internal/control/control.go Wyświetl plik

@@ -4,6 +4,7 @@ import (
_ "embed"
"encoding/json"
"io"
"mime"
"net/http"
"strings"
"sync"
@@ -50,7 +51,22 @@ type Server struct {
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 {
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")
_ = json.NewEncoder(w).Encode(cfg)
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)
var patch ConfigPatch
if err := json.NewDecoder(r.Body).Decode(&patch); err != nil {


+ 31
- 4
internal/control/control_test.go Wyświetl plik

@@ -127,7 +127,7 @@ func TestConfigPatch(t *testing.T) {
srv := NewServer(cfgpkg.Default())
body := []byte(`{"toneLeftHz":900,"radioText":"hello world","preEmphasisTauUS":75}`)
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 {
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)
body := append([]byte(`{"ps":"`), payload...)
body = append(body, []byte(`"}`)...)
req := httptest.NewRequest(http.MethodPost, "/config", bytes.NewReader(body))
req := newConfigPostRequest(body)
srv.Handler().ServeHTTP(rec, req)
if rec.Code != http.StatusRequestEntityTooLarge {
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) {
srv := NewServer(cfgpkg.Default())
rec := httptest.NewRecorder()
@@ -334,7 +355,7 @@ func TestConfigPatchUpdatesSnapshot(t *testing.T) {

rec := httptest.NewRecorder()
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 {
t.Fatalf("status: %d", rec.Code)
}
@@ -363,7 +384,7 @@ func TestConfigPatchEngineRejectsDoesNotUpdateSnapshot(t *testing.T) {

body := []byte(`{"outputDrive":2.2}`)
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 {
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 {
updateErr error
resetErr error


Ładowanie…
Anuluj
Zapisz