158 lines
5.7 KiB
Go
158 lines
5.7 KiB
Go
package onvif
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"strings"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestProfilesDiscoversMediaServiceAndUsesDigest(t *testing.T) {
|
|
var digestRequests atomic.Int32
|
|
var server *httptest.Server
|
|
server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/onvif/device_service":
|
|
fmt.Fprintf(w, `<Envelope><Body><GetCapabilitiesResponse><Capabilities><Media><XAddr>%s/onvif/media_service</XAddr></Media></Capabilities></GetCapabilitiesResponse></Body></Envelope>`, server.URL)
|
|
case "/onvif/media_service":
|
|
authorization := r.Header.Get("Authorization")
|
|
if !strings.HasPrefix(authorization, "Digest ") {
|
|
w.Header().Set("WWW-Authenticate", `Digest realm="camera", nonce="nonce-1", algorithm=MD5, qop="auth"`)
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
return
|
|
}
|
|
digestRequests.Add(1)
|
|
if strings.Contains(readRequestBody(t, r), "GetProfiles") {
|
|
fmt.Fprint(w, `<Envelope><Body><GetProfilesResponse><Profiles token="main"><Name>Main</Name><VideoEncoderConfiguration><Encoding>H264</Encoding><Resolution><Width>1920</Width><Height>1080</Height></Resolution></VideoEncoderConfiguration></Profiles></GetProfilesResponse></Body></Envelope>`)
|
|
return
|
|
}
|
|
fmt.Fprint(w, `<Envelope><Body><GetStreamUriResponse><MediaUri><Uri>rtsp://camera.invalid/live</Uri></MediaUri></GetStreamUriResponse></Body></Envelope>`)
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
}))
|
|
defer server.Close()
|
|
|
|
profiles, err := NewHTTPClient(2*time.Second).Profiles(context.Background(), server.URL+"/onvif/device_service", Credential{Username: "operator", Password: "secret"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
serverURL, _ := url.Parse(server.URL)
|
|
if len(profiles) != 1 || profiles[0].Width != 1920 || profiles[0].StreamURI != "rtsp://"+serverURL.Hostname()+"/live" {
|
|
t.Fatalf("profiles=%#v", profiles)
|
|
}
|
|
if digestRequests.Load() != 2 {
|
|
t.Fatalf("digest requests=%d", digestRequests.Load())
|
|
}
|
|
}
|
|
|
|
func TestNormalizeServiceEndpoint(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
device string
|
|
advertised string
|
|
want string
|
|
wantError bool
|
|
}{
|
|
{name: "same host keeps media port", device: "http://camera.local:80/device", advertised: "http://camera.local:8000/media", want: "http://camera.local:8000/media"},
|
|
{name: "different host uses authorized origin", device: "http://192.0.2.10:8080/device", advertised: "http://unusable.local:9000/media?profile=1", want: "http://192.0.2.10:8080/media?profile=1"},
|
|
{name: "reject credentials", device: "http://camera.local/device", advertised: "http://user:pass@camera.local/media", wantError: true},
|
|
{name: "reject scheme", device: "http://camera.local/device", advertised: "ftp://camera.local/media", wantError: true},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
got, err := normalizeServiceEndpoint(test.device, test.advertised)
|
|
if test.wantError {
|
|
if err == nil {
|
|
t.Fatalf("got=%q", got)
|
|
}
|
|
return
|
|
}
|
|
if err != nil || got != test.want {
|
|
t.Fatalf("got=%q err=%v", got, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNormalizeStreamURI(t *testing.T) {
|
|
got, err := normalizeStreamURI("http://192.0.2.10:80/onvif/device_service", "rtsp://unusable.local:8554/live/main?channel=1")
|
|
if err != nil || got != "rtsp://192.0.2.10:8554/live/main?channel=1" {
|
|
t.Fatalf("got=%q err=%v", got, err)
|
|
}
|
|
if _, err := normalizeStreamURI("http://camera.local/onvif", "rtsp://user:pass@camera.local/live"); err == nil {
|
|
t.Fatal("credential stream URI accepted")
|
|
}
|
|
if _, err := normalizeStreamURI("http://camera.local/onvif", "http://camera.local/live"); err == nil {
|
|
t.Fatal("non-RTSP URI accepted")
|
|
}
|
|
}
|
|
|
|
func TestRejectUnsupportedDigestChallenge(t *testing.T) {
|
|
for _, challenge := range []string{
|
|
`Digest realm="camera", nonce="n", algorithm=SHA-512, qop="auth"`,
|
|
`Digest realm="camera", nonce="n", algorithm=MD5, qop="auth-int"`,
|
|
`Digest realm="camera"`,
|
|
} {
|
|
if _, err := parseDigestChallenge([]string{challenge}); err == nil {
|
|
t.Fatalf("challenge accepted: %s", challenge)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestValidateEndpointRejectsCredentials(t *testing.T) {
|
|
if _, err := validateEndpoint("http://user:pass@camera.invalid/onvif"); err == nil {
|
|
t.Fatal("credential endpoint accepted")
|
|
}
|
|
}
|
|
|
|
func TestSOAPDoesNotFollowRedirect(t *testing.T) {
|
|
redirectTargetCalled := false
|
|
target := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
|
|
redirectTargetCalled = true
|
|
}))
|
|
defer target.Close()
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
http.Redirect(w, r, target.URL, http.StatusFound)
|
|
}))
|
|
defer server.Close()
|
|
|
|
_, err := NewHTTPClient(time.Second).soap(context.Background(), server.URL, Credential{Username: "operator", Password: "secret"}, "<Envelope />")
|
|
if err == nil || redirectTargetCalled {
|
|
t.Fatalf("err=%v redirect_target_called=%v", err, redirectTargetCalled)
|
|
}
|
|
}
|
|
|
|
func readRequestBody(t *testing.T, r *http.Request) string {
|
|
t.Helper()
|
|
defer r.Body.Close()
|
|
data, err := io.ReadAll(r.Body)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return string(data)
|
|
}
|
|
|
|
func TestDigestAuthorizationUsesRequestURI(t *testing.T) {
|
|
header, err := digestAuthorization(http.MethodPost, "/media?profile=1", Credential{Username: "operator", Password: "secret"}, digestChallenge{realm: "camera", nonce: "n", algorithm: "SHA-256", qop: "auth"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
parsed, err := parseAuthParameters(strings.TrimPrefix(header, "Digest "))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if parsed["uri"] != "/media?profile=1" || parsed["username"] != "operator" || parsed["response"] == "" {
|
|
t.Fatalf("invalid digest fields: %#v", parsed)
|
|
}
|
|
if _, err := url.Parse(parsed["uri"]); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|