137 lines
5.7 KiB
Go
137 lines
5.7 KiB
Go
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 = `<tds:GetDeviceInformationResponse xmlns:tds="http://www.onvif.org/ver10/device/wsdl"><tds:Manufacturer>HIKVISION</tds:Manufacturer><tds:Model>camera</tds:Model><tds:FirmwareVersion>v1</tds:FirmwareVersion><tds:SerialNumber>serial</tds:SerialNumber></tds:GetDeviceInformationResponse>`
|
|
case strings.Contains(request.Header.Get("Content-Type"), "GetServices"):
|
|
body = `<tds:GetServicesResponse xmlns:tds="http://www.onvif.org/ver10/device/wsdl"><tds:Service><tds:Namespace>http://www.onvif.org/ver10/media/wsdl</tds:Namespace><tds:XAddr>http://192.0.2.10/onvif/Media</tds:XAddr></tds:Service></tds:GetServicesResponse>`
|
|
case strings.Contains(request.Header.Get("Content-Type"), "GetProfiles"):
|
|
body = `<trt:GetProfilesResponse xmlns:trt="http://www.onvif.org/ver10/media/wsdl"><trt:Profiles token="main"><trt:Name>Main</trt:Name><trt:VideoEncoderConfiguration/></trt:Profiles></trt:GetProfilesResponse>`
|
|
case strings.Contains(request.Header.Get("Content-Type"), "GetStreamUri"):
|
|
body = `<trt:GetStreamUriResponse xmlns:trt="http://www.onvif.org/ver10/media/wsdl"><trt:MediaUri><trt:Uri>rtsp://192.0.2.10:554/Streaming/Channels/101?transportmode=unicast&profile=Profile_1</trt:Uri></trt:MediaUri></trt:GetStreamUriResponse>`
|
|
default:
|
|
http.Error(writer, "unexpected action", http.StatusBadRequest)
|
|
return
|
|
}
|
|
writer.Header().Set("Content-Type", "application/soap+xml")
|
|
_, _ = fmt.Fprintf(writer, `<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope"><s:Body>%s</s:Body></s:Envelope>`, 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(`<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope"><s:Body><s:Fault><s:Reason><s:Text>The action requires authorization</s:Text></s:Reason></s:Fault></s:Body></s:Envelope>`))
|
|
}))
|
|
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)
|
|
}
|
|
}
|