|
|
@@ -9,6 +9,7 @@ import ( |
|
|
"net/http" |
|
|
"net/http" |
|
|
"strings" |
|
|
"strings" |
|
|
"sync" |
|
|
"sync" |
|
|
|
|
|
"sync/atomic" |
|
|
|
|
|
|
|
|
"github.com/jan/fm-rds-tx/internal/audio" |
|
|
"github.com/jan/fm-rds-tx/internal/audio" |
|
|
"github.com/jan/fm-rds-tx/internal/config" |
|
|
"github.com/jan/fm-rds-tx/internal/config" |
|
|
@@ -50,6 +51,23 @@ type Server struct { |
|
|
tx TXController |
|
|
tx TXController |
|
|
drv platform.SoapyDriver // optional, for runtime stats |
|
|
drv platform.SoapyDriver // optional, for runtime stats |
|
|
streamSrc *audio.StreamSource // optional, for live audio ingest |
|
|
streamSrc *audio.StreamSource // optional, for live audio ingest |
|
|
|
|
|
audit auditCounters |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
type auditEvent string |
|
|
|
|
|
|
|
|
|
|
|
const ( |
|
|
|
|
|
auditMethodNotAllowed auditEvent = "methodNotAllowed" |
|
|
|
|
|
auditUnsupportedMediaType auditEvent = "unsupportedMediaType" |
|
|
|
|
|
auditBodyTooLarge auditEvent = "bodyTooLarge" |
|
|
|
|
|
auditUnexpectedBody auditEvent = "unexpectedBody" |
|
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
type auditCounters struct { |
|
|
|
|
|
methodNotAllowed uint64 |
|
|
|
|
|
unsupportedMediaType uint64 |
|
|
|
|
|
bodyTooLarge uint64 |
|
|
|
|
|
unexpectedBody uint64 |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
const ( |
|
|
const ( |
|
|
@@ -112,14 +130,37 @@ func hasRequestBody(r *http.Request) bool { |
|
|
return false |
|
|
return false |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
func rejectBody(w http.ResponseWriter, r *http.Request) bool { |
|
|
|
|
|
|
|
|
func (s *Server) rejectBody(w http.ResponseWriter, r *http.Request) bool { |
|
|
if !hasRequestBody(r) { |
|
|
if !hasRequestBody(r) { |
|
|
return true |
|
|
return true |
|
|
} |
|
|
} |
|
|
|
|
|
s.recordAudit(auditUnexpectedBody) |
|
|
http.Error(w, noBodyErrMsg, http.StatusBadRequest) |
|
|
http.Error(w, noBodyErrMsg, http.StatusBadRequest) |
|
|
return false |
|
|
return false |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
func (s *Server) recordAudit(evt auditEvent) { |
|
|
|
|
|
switch evt { |
|
|
|
|
|
case auditMethodNotAllowed: |
|
|
|
|
|
atomic.AddUint64(&s.audit.methodNotAllowed, 1) |
|
|
|
|
|
case auditUnsupportedMediaType: |
|
|
|
|
|
atomic.AddUint64(&s.audit.unsupportedMediaType, 1) |
|
|
|
|
|
case auditBodyTooLarge: |
|
|
|
|
|
atomic.AddUint64(&s.audit.bodyTooLarge, 1) |
|
|
|
|
|
case auditUnexpectedBody: |
|
|
|
|
|
atomic.AddUint64(&s.audit.unexpectedBody, 1) |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
func (s *Server) auditSnapshot() map[string]uint64 { |
|
|
|
|
|
return map[string]uint64{ |
|
|
|
|
|
"methodNotAllowed": atomic.LoadUint64(&s.audit.methodNotAllowed), |
|
|
|
|
|
"unsupportedMediaType": atomic.LoadUint64(&s.audit.unsupportedMediaType), |
|
|
|
|
|
"bodyTooLarge": atomic.LoadUint64(&s.audit.bodyTooLarge), |
|
|
|
|
|
"unexpectedBody": atomic.LoadUint64(&s.audit.unexpectedBody), |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
func isAudioStreamContentType(r *http.Request) bool { |
|
|
func isAudioStreamContentType(r *http.Request) bool { |
|
|
ct := strings.TrimSpace(r.Header.Get("Content-Type")) |
|
|
ct := strings.TrimSpace(r.Header.Get("Content-Type")) |
|
|
if ct == "" { |
|
|
if ct == "" { |
|
|
@@ -239,16 +280,18 @@ func (s *Server) handleRuntime(w http.ResponseWriter, _ *http.Request) { |
|
|
if stream != nil { |
|
|
if stream != nil { |
|
|
result["audioStream"] = stream.Stats() |
|
|
result["audioStream"] = stream.Stats() |
|
|
} |
|
|
} |
|
|
|
|
|
result["controlAudit"] = s.auditSnapshot() |
|
|
w.Header().Set("Content-Type", "application/json") |
|
|
w.Header().Set("Content-Type", "application/json") |
|
|
_ = json.NewEncoder(w).Encode(result) |
|
|
_ = json.NewEncoder(w).Encode(result) |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
func (s *Server) handleRuntimeFaultReset(w http.ResponseWriter, r *http.Request) { |
|
|
func (s *Server) handleRuntimeFaultReset(w http.ResponseWriter, r *http.Request) { |
|
|
if r.Method != http.MethodPost { |
|
|
if r.Method != http.MethodPost { |
|
|
|
|
|
s.recordAudit(auditMethodNotAllowed) |
|
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed) |
|
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed) |
|
|
return |
|
|
return |
|
|
} |
|
|
} |
|
|
if !rejectBody(w, r) { |
|
|
|
|
|
|
|
|
if !s.rejectBody(w, r) { |
|
|
return |
|
|
return |
|
|
} |
|
|
} |
|
|
s.mu.RLock() |
|
|
s.mu.RLock() |
|
|
@@ -272,10 +315,12 @@ func (s *Server) handleRuntimeFaultReset(w http.ResponseWriter, r *http.Request) |
|
|
// ffmpeg ... -f s16le -ar 44100 -ac 2 - | curl -X POST --data-binary @- http://host:8088/audio/stream |
|
|
// ffmpeg ... -f s16le -ar 44100 -ac 2 - | curl -X POST --data-binary @- http://host:8088/audio/stream |
|
|
func (s *Server) handleAudioStream(w http.ResponseWriter, r *http.Request) { |
|
|
func (s *Server) handleAudioStream(w http.ResponseWriter, r *http.Request) { |
|
|
if r.Method != http.MethodPost { |
|
|
if r.Method != http.MethodPost { |
|
|
|
|
|
s.recordAudit(auditMethodNotAllowed) |
|
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed) |
|
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed) |
|
|
return |
|
|
return |
|
|
} |
|
|
} |
|
|
if !isAudioStreamContentType(r) { |
|
|
if !isAudioStreamContentType(r) { |
|
|
|
|
|
s.recordAudit(auditUnsupportedMediaType) |
|
|
http.Error(w, audioStreamContentTypeError, http.StatusUnsupportedMediaType) |
|
|
http.Error(w, audioStreamContentTypeError, http.StatusUnsupportedMediaType) |
|
|
return |
|
|
return |
|
|
} |
|
|
} |
|
|
@@ -304,6 +349,7 @@ func (s *Server) handleAudioStream(w http.ResponseWriter, r *http.Request) { |
|
|
} |
|
|
} |
|
|
var maxErr *http.MaxBytesError |
|
|
var maxErr *http.MaxBytesError |
|
|
if errors.As(err, &maxErr) { |
|
|
if errors.As(err, &maxErr) { |
|
|
|
|
|
s.recordAudit(auditBodyTooLarge) |
|
|
http.Error(w, maxErr.Error(), http.StatusRequestEntityTooLarge) |
|
|
http.Error(w, maxErr.Error(), http.StatusRequestEntityTooLarge) |
|
|
return |
|
|
return |
|
|
} |
|
|
} |
|
|
@@ -322,10 +368,11 @@ func (s *Server) handleAudioStream(w http.ResponseWriter, r *http.Request) { |
|
|
|
|
|
|
|
|
func (s *Server) handleTXStart(w http.ResponseWriter, r *http.Request) { |
|
|
func (s *Server) handleTXStart(w http.ResponseWriter, r *http.Request) { |
|
|
if r.Method != http.MethodPost { |
|
|
if r.Method != http.MethodPost { |
|
|
|
|
|
s.recordAudit(auditMethodNotAllowed) |
|
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed) |
|
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed) |
|
|
return |
|
|
return |
|
|
} |
|
|
} |
|
|
if !rejectBody(w, r) { |
|
|
|
|
|
|
|
|
if !s.rejectBody(w, r) { |
|
|
return |
|
|
return |
|
|
} |
|
|
} |
|
|
s.mu.RLock() |
|
|
s.mu.RLock() |
|
|
@@ -345,10 +392,11 @@ func (s *Server) handleTXStart(w http.ResponseWriter, r *http.Request) { |
|
|
|
|
|
|
|
|
func (s *Server) handleTXStop(w http.ResponseWriter, r *http.Request) { |
|
|
func (s *Server) handleTXStop(w http.ResponseWriter, r *http.Request) { |
|
|
if r.Method != http.MethodPost { |
|
|
if r.Method != http.MethodPost { |
|
|
|
|
|
s.recordAudit(auditMethodNotAllowed) |
|
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed) |
|
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed) |
|
|
return |
|
|
return |
|
|
} |
|
|
} |
|
|
if !rejectBody(w, r) { |
|
|
|
|
|
|
|
|
if !s.rejectBody(w, r) { |
|
|
return |
|
|
return |
|
|
} |
|
|
} |
|
|
s.mu.RLock() |
|
|
s.mu.RLock() |
|
|
@@ -384,6 +432,7 @@ func (s *Server) handleConfig(w http.ResponseWriter, r *http.Request) { |
|
|
_ = json.NewEncoder(w).Encode(cfg) |
|
|
_ = json.NewEncoder(w).Encode(cfg) |
|
|
case http.MethodPost: |
|
|
case http.MethodPost: |
|
|
if !isJSONContentType(r) { |
|
|
if !isJSONContentType(r) { |
|
|
|
|
|
s.recordAudit(auditUnsupportedMediaType) |
|
|
http.Error(w, "Content-Type must be application/json", http.StatusUnsupportedMediaType) |
|
|
http.Error(w, "Content-Type must be application/json", http.StatusUnsupportedMediaType) |
|
|
return |
|
|
return |
|
|
} |
|
|
} |
|
|
@@ -393,6 +442,7 @@ func (s *Server) handleConfig(w http.ResponseWriter, r *http.Request) { |
|
|
statusCode := http.StatusBadRequest |
|
|
statusCode := http.StatusBadRequest |
|
|
if strings.Contains(err.Error(), "http: request body too large") { |
|
|
if strings.Contains(err.Error(), "http: request body too large") { |
|
|
statusCode = http.StatusRequestEntityTooLarge |
|
|
statusCode = http.StatusRequestEntityTooLarge |
|
|
|
|
|
s.recordAudit(auditBodyTooLarge) |
|
|
} |
|
|
} |
|
|
http.Error(w, err.Error(), statusCode) |
|
|
http.Error(w, err.Error(), statusCode) |
|
|
return |
|
|
return |
|
|
|