package onvif import ( "context" "fmt" "net/http" "net/http/httptest" "net/url" "strings" "testing" ) type staticCredentials struct { value Credentials err error } func (s staticCredentials) Resolve(string) (Credentials, error) { return s.value, s.err } func TestHTTPAdapterDiscoversMediaAndRewritesNATStream(t *testing.T) { t.Parallel() server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { if !strings.Contains(request.Header.Get("Content-Type"), "action=") { t.Fatal("SOAP action is required") } body := "" switch { case strings.Contains(request.Header.Get("Content-Type"), "GetDeviceInformation"): body = `HIKVISIONcamerav1serial` case strings.Contains(request.Header.Get("Content-Type"), "GetServices"): body = `http://www.onvif.org/ver10/media/wsdlhttp://192.0.2.10/onvif/Media` case strings.Contains(request.Header.Get("Content-Type"), "GetProfiles"): body = `Main` case strings.Contains(request.Header.Get("Content-Type"), "GetStreamUri"): body = `rtsp://192.0.2.10:554/Streaming/Channels/101?transportmode=unicast&profile=Profile_1` default: http.Error(writer, "unexpected action", http.StatusBadRequest) return } writer.Header().Set("Content-Type", "application/soap+xml") _, _ = fmt.Fprintf(writer, `%s`, body) })) defer server.Close() credentials := Credentials{ ONVIFUsername: "onvif-user", ONVIFPassword: "onvif-password", RTSPUsername: "rtsp-user", RTSPPassword: "rtsp-password", } adapter := NewHTTPAdapter(staticCredentials{value: credentials}, server.Client(), HTTPOptions{ RTSPRewriteHost: "127.0.0.1", RTSPRewritePort: 10554, StripRTSPQuery: true, }) result, err := adapter.Probe(context.Background(), Target{ EndpointRef: server.URL + "/onvif/device_service", CredentialRef: "env://camera", }) if err != nil { t.Fatal(err) } if result.Manufacturer != "HIKVISION" || result.Model != "camera" || len(result.Profiles) != 1 { t.Fatalf("unexpected probe result: %+v", result) } stream, err := url.Parse(result.StreamURI) if err != nil { t.Fatal(err) } if stream.Host != "127.0.0.1:10554" || stream.Path != "/Streaming/Channels/101" || stream.RawQuery != "" { t.Fatalf("unexpected rewritten stream address: host=%s path=%s", stream.Host, stream.Path) } password, _ := stream.User.Password() if stream.User.Username() != credentials.RTSPUsername || password != credentials.RTSPPassword { t.Fatal("RTSP credentials were not injected") } } func TestHTTPAdapterPreservesRTSPQueryByDefault(t *testing.T) { t.Parallel() adapter := NewHTTPAdapter(staticCredentials{}, nil, HTTPOptions{}) streamURI, err := adapter.rewriteStreamURI( &url.URL{Scheme: "http", Host: "camera.example:8008"}, "rtsp://192.0.2.10:554/live?profile=main", Credentials{RTSPUsername: "user", RTSPPassword: "secret"}, ) if err != nil { t.Fatal(err) } stream, err := url.Parse(streamURI) if err != nil { t.Fatal(err) } if stream.RawQuery != "profile=main" { t.Fatalf("RTSP query was unexpectedly changed: %q", stream.RawQuery) } } func TestHTTPAdapterMapsAuthorizationFaultWithoutLeakingSecret(t *testing.T) { t.Parallel() server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, _ *http.Request) { writer.WriteHeader(http.StatusBadRequest) _, _ = writer.Write([]byte(`The action requires authorization`)) })) defer server.Close() secret := "not-for-errors" adapter := NewHTTPAdapter(staticCredentials{value: Credentials{ ONVIFUsername: "user", ONVIFPassword: secret, RTSPUsername: "user", RTSPPassword: secret, }}, server.Client(), HTTPOptions{}) _, err := adapter.Probe(context.Background(), Target{EndpointRef: server.URL, CredentialRef: "env://camera"}) if CodeOf(err) != ErrorAuthentication || strings.Contains(err.Error(), secret) { t.Fatalf("expected redacted authentication error, got %v", err) } } func TestEnvCredentialsAndDirectRTSPRouting(t *testing.T) { t.Parallel() values := map[string]string{ "SENSE_CREDENTIAL_CAMERA_ONVIF_USERNAME": "onvif", "SENSE_CREDENTIAL_CAMERA_ONVIF_PASSWORD": "onvif-secret", "SENSE_CREDENTIAL_CAMERA_RTSP_USERNAME": "rtsp", "SENSE_CREDENTIAL_CAMERA_RTSP_PASSWORD": "rtsp-secret", } provider := EnvCredentials{LookupEnv: func(name string) (string, bool) { value, ok := values[name] return value, ok }} resolved, err := provider.Resolve("env://camera") if err != nil || resolved.RTSPUsername != "rtsp" { t.Fatalf("resolve credentials: %+v err=%v", resolved, err) } router := NewRouter(UnavailableAdapter{}, provider) result, err := router.Probe(context.Background(), Target{ EndpointRef: "rtsp://127.0.0.1:8555/synthetic-1", }) if err != nil || result.StreamURI != "rtsp://127.0.0.1:8555/synthetic-1" { t.Fatalf("route direct RTSP: %+v err=%v", result, err) } }