mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-07-09 16:00:59 +00:00
Fix mobile onboarding URL handoff sanitization
Some checks are pending
Canonical Governance / governance (push) Waiting to run
Build and Test / Secret Scan (push) Waiting to run
Build and Test / Frontend & Backend (push) Waiting to run
Helm CI / Lint and Render Chart (push) Waiting to run
Core E2E Tests / Playwright Core E2E (push) Waiting to run
Some checks are pending
Canonical Governance / governance (push) Waiting to run
Build and Test / Secret Scan (push) Waiting to run
Build and Test / Frontend & Backend (push) Waiting to run
Helm CI / Lint and Render Chart (push) Waiting to run
Core E2E Tests / Playwright Core E2E (push) Waiting to run
This commit is contained in:
parent
e1cddca2d1
commit
57139c65c5
10 changed files with 156 additions and 11 deletions
|
|
@ -8997,6 +8997,45 @@ func TestContract_OnboardingQRResponseJSONSnapshot(t *testing.T) {
|
|||
assertJSONSnapshot(t, got, want)
|
||||
}
|
||||
|
||||
func TestContract_OnboardingQRResponseOmitsEmptyInstanceURLJSONSnapshot(t *testing.T) {
|
||||
payload := onboardingQRResponse{
|
||||
Schema: onboardingSchemaVersion,
|
||||
InstanceID: "relay_abc123",
|
||||
Relay: onboardingRelayDetails{
|
||||
Enabled: true,
|
||||
URL: "wss://relay.example.test/ws/app",
|
||||
},
|
||||
AuthToken: "token-123",
|
||||
DeepLink: "pulse://connect?schema=pulse-mobile-onboarding-v1&instance_id=relay_abc123&relay_url=wss%3A%2F%2Frelay.example.test%2Fws%2Fapp&auth_token=token-123",
|
||||
Diagnostics: []onboardingDiagnostic{
|
||||
{
|
||||
Code: "instance_url_not_https",
|
||||
Severity: "warning",
|
||||
Field: "instance_url",
|
||||
Expected: "https://...",
|
||||
Received: "http://pulse.local.test",
|
||||
Message: "Pulse web link is not included in this pairing code because the resolved Pulse URL is not HTTPS. Pairing can continue, but opening Pulse web from the phone requires an HTTPS Pulse URL.",
|
||||
},
|
||||
},
|
||||
}.normalizeCollections()
|
||||
|
||||
got, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal onboarding qr response without instance_url: %v", err)
|
||||
}
|
||||
|
||||
const want = `{
|
||||
"schema":"pulse-mobile-onboarding-v1",
|
||||
"instance_id":"relay_abc123",
|
||||
"relay":{"enabled":true,"url":"wss://relay.example.test/ws/app"},
|
||||
"auth_token":"token-123",
|
||||
"deep_link":"pulse://connect?schema=pulse-mobile-onboarding-v1\u0026instance_id=relay_abc123\u0026relay_url=wss%3A%2F%2Frelay.example.test%2Fws%2Fapp\u0026auth_token=token-123",
|
||||
"diagnostics":[{"code":"instance_url_not_https","severity":"warning","message":"Pulse web link is not included in this pairing code because the resolved Pulse URL is not HTTPS. Pairing can continue, but opening Pulse web from the phone requires an HTTPS Pulse URL.","field":"instance_url","expected":"https://...","received":"http://pulse.local.test"}]
|
||||
}`
|
||||
|
||||
assertJSONSnapshot(t, got, want)
|
||||
}
|
||||
|
||||
func TestContract_OnboardingNotReadyResponseJSONSnapshot(t *testing.T) {
|
||||
payload := onboardingNotReadyResponse{
|
||||
Code: onboardingNotReadyCode,
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ type onboardingRelayDetails struct {
|
|||
|
||||
type onboardingQRResponse struct {
|
||||
Schema string `json:"schema"`
|
||||
InstanceURL string `json:"instance_url"`
|
||||
InstanceURL string `json:"instance_url,omitempty"`
|
||||
InstanceID string `json:"instance_id,omitempty"`
|
||||
Relay onboardingRelayDetails `json:"relay"`
|
||||
AuthToken string `json:"auth_token"`
|
||||
|
|
@ -288,7 +288,8 @@ func (r *Router) buildOnboardingPayload(req *http.Request, relayCfg *relay.Confi
|
|||
|
||||
payload := emptyOnboardingQRResponse()
|
||||
payload.Schema = onboardingSchemaVersion
|
||||
payload.InstanceURL = strings.TrimSpace(r.resolvePublicURL(req))
|
||||
instanceURL, instanceURLDiagnostic := normalizeOnboardingInstanceURL(r.resolvePublicURL(req))
|
||||
payload.InstanceURL = instanceURL
|
||||
payload.InstanceID = r.currentRelayInstanceID()
|
||||
payload.Relay = onboardingRelayDetails{
|
||||
Enabled: relayCfg.Enabled,
|
||||
|
|
@ -299,6 +300,9 @@ func (r *Router) buildOnboardingPayload(req *http.Request, relayCfg *relay.Confi
|
|||
payload.AuthToken = authToken
|
||||
|
||||
diagnostics := make([]onboardingDiagnostic, 0, 4)
|
||||
if instanceURLDiagnostic != nil {
|
||||
diagnostics = append(diagnostics, *instanceURLDiagnostic)
|
||||
}
|
||||
if !payload.Relay.Enabled {
|
||||
diagnostics = append(diagnostics, onboardingDiagnostic{
|
||||
Code: "relay_disabled",
|
||||
|
|
@ -329,6 +333,27 @@ func (r *Router) buildOnboardingPayload(req *http.Request, relayCfg *relay.Confi
|
|||
return payload, diagnostics
|
||||
}
|
||||
|
||||
func normalizeOnboardingInstanceURL(raw string) (string, *onboardingDiagnostic) {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
parsed, err := url.Parse(raw)
|
||||
if err == nil && strings.EqualFold(parsed.Scheme, "https") && strings.TrimSpace(parsed.Host) != "" {
|
||||
return raw, nil
|
||||
}
|
||||
|
||||
return "", &onboardingDiagnostic{
|
||||
Code: "instance_url_not_https",
|
||||
Severity: "warning",
|
||||
Field: "instance_url",
|
||||
Expected: "https://...",
|
||||
Received: raw,
|
||||
Message: "Pulse web link is not included in this pairing code because the resolved Pulse URL is not HTTPS. Pairing can continue, but opening Pulse web from the phone requires an HTTPS Pulse URL.",
|
||||
}
|
||||
}
|
||||
|
||||
func writeOnboardingNotReady(w http.ResponseWriter, diagnostics []onboardingDiagnostic) {
|
||||
response := onboardingNotReadyResponse{
|
||||
Code: onboardingNotReadyCode,
|
||||
|
|
@ -456,7 +481,9 @@ func hasOnboardingError(diagnostics []onboardingDiagnostic) bool {
|
|||
func buildOnboardingDeepLink(payload onboardingQRResponse) string {
|
||||
query := url.Values{}
|
||||
query.Set("schema", payload.Schema)
|
||||
query.Set("instance_url", payload.InstanceURL)
|
||||
if payload.InstanceURL != "" {
|
||||
query.Set("instance_url", payload.InstanceURL)
|
||||
}
|
||||
if payload.InstanceID != "" {
|
||||
query.Set("instance_id", payload.InstanceID)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,6 +56,45 @@ func TestOnboardingQRPayloadStructure(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestOnboardingQROmitsNonHTTPSInstanceURL(t *testing.T) {
|
||||
router, rawToken, _ := newOnboardingContractRouter(t)
|
||||
router.config.PublicURL = ""
|
||||
router.config.AgentConnectURL = ""
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "http://pulse.local.test/api/onboarding/qr", nil)
|
||||
req.Header.Set("X-API-Token", rawToken)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
router.handleGetOnboardingQR(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("expected status 200, got %d: %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
|
||||
body := rec.Body.String()
|
||||
var raw map[string]json.RawMessage
|
||||
if err := json.Unmarshal([]byte(body), &raw); err != nil {
|
||||
t.Fatalf("decode raw QR response: %v", err)
|
||||
}
|
||||
if _, ok := raw["instance_url"]; ok {
|
||||
t.Fatalf("expected unsafe top-level instance_url to be omitted from QR response, got %s", body)
|
||||
}
|
||||
|
||||
var payload onboardingQRResponse
|
||||
if err := json.NewDecoder(strings.NewReader(body)).Decode(&payload); err != nil {
|
||||
t.Fatalf("decode QR response: %v", err)
|
||||
}
|
||||
if payload.InstanceURL != "" {
|
||||
t.Fatalf("expected empty instance_url, got %q", payload.InstanceURL)
|
||||
}
|
||||
if strings.Contains(payload.DeepLink, "instance_url") {
|
||||
t.Fatalf("expected unsafe instance_url to be omitted from deep link, got %q", payload.DeepLink)
|
||||
}
|
||||
if !diagnosticCodePresent(payload.Diagnostics, "instance_url_not_https") {
|
||||
t.Fatalf("expected instance_url_not_https diagnostic, got %#v", payload.Diagnostics)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOnboardingQRRejectsIncompleteRelayRegistration(t *testing.T) {
|
||||
router, rawToken, _ := newOnboardingContractRouter(t)
|
||||
router.relayClient = nil
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue