mirror of
https://github.com/gesellix/Bose-SoundTouch.git
synced 2026-08-15 07:06:15 +00:00
Replace validateZcBaseURL(zcBaseURL string) with: - validateZcHost(host string) (net.IP, error) — validates literal IP - buildZcBase(ip net.IP, port string) *url.URL — builds URL with literal /zc path The key change: the URL path is now the string literal "/zc" everywhere, never derived from user input. CodeQL's go/request-forgery model traces taint through the Path field of a rebuilt URL; removing that field from the taint chain closes alerts 134, 135, 136. Public API changes: zeroconf.GetInfo(host, port string) zeroconf.PushCredentials(host, port, username, accessToken string) spotify.ZeroConfGetInfo(host, port string) spotify.PushSpotifyCredentials(host, port, username, accessToken string) amazon.PushAmazonCredentials(host, port, username, accessToken string) Callers in handlers/server.go already held host+port separately via net.SplitHostPort; the zcURL construction is removed. Tests updated throughout; TestValidateZcBaseURL renamed to TestValidateZcHost and TestBuildZcBase added for the new helpers. Closes CodeQL alerts 134, 135, 136 (go/request-forgery). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
180 lines
4.8 KiB
Go
180 lines
4.8 KiB
Go
package amazon
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"net"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"github.com/gesellix/bose-soundtouch/pkg/service/zeroconf"
|
|
)
|
|
|
|
func TestPushAmazonCredentials_FullRoundTrip(t *testing.T) {
|
|
speakerPrivate, speakerPublicBytes, err := zeroconf.GenerateDHKeyPair()
|
|
if err != nil {
|
|
t.Fatalf("speaker keygen: %v", err)
|
|
}
|
|
|
|
type received struct {
|
|
username string
|
|
authData string
|
|
authType int
|
|
}
|
|
var got received
|
|
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Query().Get("action") {
|
|
case "getInfo":
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_ = json.NewEncoder(w).Encode(map[string]interface{}{
|
|
"status": 101,
|
|
"statusString": "OK",
|
|
"publicKey": base64.StdEncoding.EncodeToString(speakerPublicBytes),
|
|
})
|
|
|
|
case "addUser":
|
|
if err := r.ParseForm(); err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
blobBytes, err := base64.StdEncoding.DecodeString(r.FormValue("blob"))
|
|
if err != nil {
|
|
http.Error(w, "bad blob base64: "+err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
clientKeyBytes, err := base64.StdEncoding.DecodeString(r.FormValue("clientKey"))
|
|
if err != nil {
|
|
http.Error(w, "bad clientKey base64: "+err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
shared := zeroconf.ComputeSharedSecret(speakerPrivate, clientKeyBytes)
|
|
encKey, macKey := zeroconf.DeriveKeys(shared)
|
|
|
|
plaintext, err := zeroconf.DecryptBlob(encKey, macKey, blobBytes)
|
|
if err != nil {
|
|
http.Error(w, "decrypt failed: "+err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Minimal protobuf parse: field 1 = username, field 4 = authData, field 5 = authType
|
|
i := 0
|
|
for i < len(plaintext) {
|
|
tag := plaintext[i]
|
|
i++
|
|
fieldNum := tag >> 3
|
|
wireType := tag & 0x07
|
|
switch wireType {
|
|
case 0:
|
|
val, n := readVarint(plaintext[i:])
|
|
i += n
|
|
if fieldNum == 5 {
|
|
got.authType = int(val)
|
|
}
|
|
case 2:
|
|
length, n := readVarint(plaintext[i:])
|
|
i += n
|
|
value := plaintext[i : i+int(length)]
|
|
i += int(length)
|
|
switch fieldNum {
|
|
case 1:
|
|
got.username = string(value)
|
|
case 4:
|
|
got.authData = string(value)
|
|
}
|
|
default:
|
|
http.Error(w, "unexpected wire type", http.StatusBadRequest)
|
|
return
|
|
}
|
|
}
|
|
w.WriteHeader(http.StatusOK)
|
|
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
|
|
const wantUsername = "amazonuser@example.com"
|
|
const wantToken = "Atza|access-token"
|
|
|
|
host, port, _ := net.SplitHostPort(srv.Listener.Addr().String())
|
|
if err := PushAmazonCredentials(host, port, wantUsername, wantToken); err != nil {
|
|
t.Fatalf("PushAmazonCredentials: %v", err)
|
|
}
|
|
|
|
if got.username != wantUsername {
|
|
t.Errorf("username = %q, want %q", got.username, wantUsername)
|
|
}
|
|
if got.authData != wantToken {
|
|
t.Errorf("authData = %q, want %q", got.authData, wantToken)
|
|
}
|
|
if uint64(got.authType) != zeroconf.AuthTypeOAuthToken {
|
|
t.Errorf("authType = %d, want %d (AuthTypeOAuthToken)", got.authType, zeroconf.AuthTypeOAuthToken)
|
|
}
|
|
}
|
|
|
|
func TestPushAmazonCredentials_FallbackOnGetInfoFailure(t *testing.T) {
|
|
var receivedForm map[string]string
|
|
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Query().Get("action") {
|
|
case "getInfo":
|
|
http.Error(w, "not supported", http.StatusNotFound)
|
|
case "addUser":
|
|
if err := r.ParseForm(); err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
receivedForm = map[string]string{
|
|
"userName": r.FormValue("userName"),
|
|
"blob": r.FormValue("blob"),
|
|
"clientKey": r.FormValue("clientKey"),
|
|
"tokenType": r.FormValue("tokenType"),
|
|
}
|
|
w.WriteHeader(http.StatusOK)
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
|
|
const wantUsername = "amazonuser@example.com"
|
|
const wantToken = "Atza|raw-access-token"
|
|
|
|
host, port, _ := net.SplitHostPort(srv.Listener.Addr().String())
|
|
if err := PushAmazonCredentials(host, port, wantUsername, wantToken); err != nil {
|
|
t.Fatalf("PushAmazonCredentials: %v", err)
|
|
}
|
|
|
|
if receivedForm == nil {
|
|
t.Fatal("addUser was never called")
|
|
}
|
|
if receivedForm["userName"] != wantUsername {
|
|
t.Errorf("userName = %q, want %q", receivedForm["userName"], wantUsername)
|
|
}
|
|
if receivedForm["blob"] != wantToken {
|
|
t.Errorf("blob = %q, want raw token %q", receivedForm["blob"], wantToken)
|
|
}
|
|
if receivedForm["tokenType"] != "accesstoken" {
|
|
t.Errorf("tokenType = %q, want %q", receivedForm["tokenType"], "accesstoken")
|
|
}
|
|
if receivedForm["clientKey"] != "" {
|
|
t.Errorf("clientKey = %q, want empty for simplified fallback", receivedForm["clientKey"])
|
|
}
|
|
}
|
|
|
|
func readVarint(data []byte) (uint64, int) {
|
|
var val uint64
|
|
for i, b := range data {
|
|
val |= uint64(b&0x7f) << (7 * uint(i))
|
|
if b&0x80 == 0 {
|
|
return val, i + 1
|
|
}
|
|
}
|
|
return 0, len(data)
|
|
}
|