mirror of
https://github.com/gesellix/Bose-SoundTouch.git
synced 2026-08-18 16:46:17 +00:00
Commit history from `7204e619decc48df5dee91d18470934b50e389ac` to `f9b5ad3129831086b02bdf20a197ff4e2d098e2d`: https://github.com/gesellix/Bose-SoundTouch-API/compare/7204e619decc48df5dee91d18470934b50e389ac...f9b5ad3129831086b02bdf20a197ff4e2d098e2d * f9b5ad3 - Tobias Gesellchen, 2026-02-07 : Rename module to gesellix/bose-soundtouch-api and update related files * 5b3dbbb - Tobias Gesellchen, 2026-02-07 : docs: translate PLAN.md to English and fix preferredLanguage typo in marge.go * 696b9c9 - Tobias Gesellchen, 2026-02-07 : feat(discovery): fetch serial number from speaker info if missing in discovery and update datastore tests * 8ed78f0 - Tobias Gesellchen, 2026-02-07 : Consolidate proxy and main service on port 8000 and update related tests and UI * 0e3abbb - Tobias Gesellchen, 2026-02-07 : feat(go): lowercase guessed hostnames for URL consistency * ca1091f - Tobias Gesellchen, 2026-02-07 : feat(health): add health endpoint with VCS build information * a432d53 - Tobias Gesellchen, 2026-02-07 : Rename mock token to soundcork-local-token and add documentation * 3b5ee2f - Tobias Gesellchen, 2026-02-07 : Implement Phase 10: Stats API, Device Event Log, and advanced Marge functions * bc96033 - Tobias Gesellchen, 2026-02-06 : chore * c77864b - Tobias Gesellchen, 2026-02-06 : Document Golang header normalization behavior and ensure generic header casing preservation in proxy * a54e7e7 - Tobias Gesellchen, 2026-02-06 : Ensure ETag header preserves casing (uppercase 'T') for case-sensitive devices * 6265fbe - Tobias Gesellchen, 2026-02-06 : update dockerfile to be in sync with go.mod * 5290bad - Tobias Gesellchen, 2026-02-06 : Implement proxy logging settings UI and complete Phase 8 quick wins (ETags, DataStore initialization) * d7aa7f7 - Tobias Gesellchen, 2026-02-06 : Update PLAN.md with recent features and Phase 8 Upstream Parity tasks * c8ae5e2 - Tobias Gesellchen, 2026-02-06 : Enhance Bose SoundTouch migration with proxying, remote services persistence, and improved diagnostics * c53fa00 - Tobias Gesellchen, 2026-02-06 : Implement remote services persistence check and UI improvements for Bose SoundTouch migration * ea5c348 - Tobias Gesellchen, 2026-02-02 : Ignore soundcork-go/data directory and include recent datastore fixes * d162892 - Tobias Gesellchen, 2026-02-02 : Complete Phase 7: Automated Setup & UI refactoring. Implemented programmatic SSH/migration logic, added device discovery endpoints, created Web UI for speaker management, and refactored UI to use external HTML with Go embed. * b528016 - Tobias Gesellchen, 2026-02-01 : Add GitHub workflow to publish Docker image to GHCR and update Dockerfile * 2ee03da - Tobias Gesellchen, 2026-02-01 : Add GitHub Actions workflow for Go CI and update PLAN.md * 439e2a9 - Tobias Gesellchen, 2026-02-01 : Refactor Go implementation: extract handlers and tests into dedicated files, add comprehensive unit and HTTP tests * c0698fb - Tobias Gesellchen, 2026-02-01 : Add Docker telnet example and update IP consistency in documentation * 028a02e - Tobias Gesellchen, 2026-02-01 : Fix older port number in README * 264829d - Tobias Gesellchen, 2026-02-01 : Add setup-speaker.sh and update documentation to match issue #59 * 64306f9 - Tobias Gesellchen, 2026-02-01 : Implement device presets endpoint in Go * b98a602 - Tobias Gesellchen, 2026-02-01 : Implement Phase 4: Datastore and Marge logic in Go * b6e1bc9 - Tobias Gesellchen, 2026-02-01 : Implement Phase 3: BMX Streaming and Service Registry in Go * f1b3dcf - Tobias Gesellchen, 2026-02-01 : Port core models and constants to Go * 9eae655 - Tobias Gesellchen, 2026-02-01 : Implement static file serving for /media in Go * cc73e50 - Tobias Gesellchen, 2026-02-01 : Fix Go service accessibility and improve Docker configuration * e356bdd - Tobias Gesellchen, 2026-02-01 : Initialize Go migration: Phase 1 infrastructure, proxy-first routing, and root endpoint
110 lines
2.7 KiB
Go
110 lines
2.7 KiB
Go
package proxy
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"net/http"
|
|
"net/http/httputil"
|
|
"os"
|
|
"strings"
|
|
)
|
|
|
|
var sensitiveHeaders = []string{
|
|
"Authorization",
|
|
"Cookie",
|
|
"X-Bose-Token",
|
|
}
|
|
|
|
// LoggingProxy wraps a ReverseProxy to provide instrumentation.
|
|
type LoggingProxy struct {
|
|
Proxy *httputil.ReverseProxy
|
|
Redact bool
|
|
LogBody bool
|
|
MaxBodySize int64
|
|
}
|
|
|
|
func NewLoggingProxy(targetURL string, redact bool) *LoggingProxy {
|
|
// targetURL logic should be handled by the caller or we can parse it here
|
|
return &LoggingProxy{
|
|
Redact: redact,
|
|
LogBody: os.Getenv("LOG_PROXY_BODY") == "true",
|
|
MaxBodySize: 1024 * 10, // 10KB default limit for logging
|
|
}
|
|
}
|
|
|
|
func (lp *LoggingProxy) LogRequest(r *http.Request) {
|
|
headers := formatHeaders(r.Header, lp.Redact)
|
|
|
|
bodyStr := "[HIDDEN]"
|
|
if lp.LogBody && shouldLogBody(r.Header.Get("Content-Type")) {
|
|
if r.Body != nil {
|
|
bodyBytes, _ := io.ReadAll(r.Body)
|
|
r.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
|
|
if int64(len(bodyBytes)) > lp.MaxBodySize {
|
|
bodyStr = string(bodyBytes[:lp.MaxBodySize]) + "... [TRUNCATED]"
|
|
} else {
|
|
bodyStr = string(bodyBytes)
|
|
}
|
|
} else {
|
|
bodyStr = "[EMPTY]"
|
|
}
|
|
}
|
|
|
|
log.Printf("[PROXY_REQ] %s %s\n Headers:\n%s\n Body: %s", r.Method, r.URL.String(), headers, bodyStr)
|
|
}
|
|
|
|
func (lp *LoggingProxy) LogResponse(r *http.Response) {
|
|
headers := formatHeaders(r.Header, lp.Redact)
|
|
|
|
bodyStr := "[HIDDEN]"
|
|
if lp.LogBody && shouldLogBody(r.Header.Get("Content-Type")) {
|
|
if r.Body != nil {
|
|
bodyBytes, _ := io.ReadAll(r.Body)
|
|
r.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
|
|
if int64(len(bodyBytes)) > lp.MaxBodySize {
|
|
bodyStr = string(bodyBytes[:lp.MaxBodySize]) + "... [TRUNCATED]"
|
|
} else {
|
|
bodyStr = string(bodyBytes)
|
|
}
|
|
} else {
|
|
bodyStr = "[EMPTY]"
|
|
}
|
|
}
|
|
|
|
log.Printf("[PROXY_RES] %d %s\n Headers:\n%s\n Body: %s", r.StatusCode, r.Request.URL.String(), headers, bodyStr)
|
|
}
|
|
|
|
func formatHeaders(h http.Header, redact bool) string {
|
|
var sb strings.Builder
|
|
// In Go, http.Header is a map[string][]string.
|
|
// Iterating over the map directly allows us to see the actual keys
|
|
// stored in the map, which might not be canonical if set directly.
|
|
for k, vv := range h {
|
|
val := strings.Join(vv, ", ")
|
|
if redact && isSensitive(k) {
|
|
val = "[REDACTED]"
|
|
}
|
|
sb.WriteString(fmt.Sprintf(" %s: %s\n", k, val))
|
|
}
|
|
return strings.TrimSuffix(sb.String(), "\n")
|
|
}
|
|
|
|
func isSensitive(header string) bool {
|
|
for _, h := range sensitiveHeaders {
|
|
if strings.EqualFold(h, header) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func shouldLogBody(contentType string) bool {
|
|
contentType = strings.ToLower(contentType)
|
|
return strings.Contains(contentType, "xml") ||
|
|
strings.Contains(contentType, "json") ||
|
|
strings.Contains(contentType, "text") ||
|
|
contentType == ""
|
|
}
|