mirror of
https://github.com/kerberos-io/onvif.git
synced 2026-08-23 15:08:33 +00:00
fix(examples): secure credential handling and Errors-arm bug
Two findings from the resource/security and API reviews of the
streamtest example.
Credentials
-----------
* loadPassword resolves the camera password in order:
1. ONVIF_PASSWORD environment variable (recommended).
2. -password-file <path> (newline trimmed).
3. Interactive prompt when nothing else is set.
* -password flag still works but now logs a WARNING that the value
leaks into shell history and process listings. Documented as
'INSECURE' in the flag help.
* Updated package godoc with a Credentials section.
Errors-arm bug
--------------
* Previous code: case e := <-s.Errors() with no ok check. When the
Stream closed, this arm would spin on a closed channel printing
'<nil>' forever (until ctx-done elsewhere unblocked it). Mirror
the Events arm's ok pattern.
* Switched the error-log branch to inspect the typed errors added
in the previous commit: ErrRecreateFailed gets a louder 'camera
may be offline' log line; ErrPullFailed is a quieter
'will retry' since the loop handles transient pull errors
automatically.
Also prints '[after-reconnect]' on events carrying that flag so the
operator can see when the stream silently recovered a dropped
subscription — confirms the new observability surface is useful at
the CLI level.
This commit is contained in:
@@ -3,24 +3,36 @@
|
||||
// classifier against real-camera topics; not intended as a production
|
||||
// tool.
|
||||
//
|
||||
// Example:
|
||||
// # Usage
|
||||
//
|
||||
// go run ./examples/event/stream \
|
||||
// -xaddr 192.168.1.10 \
|
||||
// -username root -password admin \
|
||||
// -username root \
|
||||
// -duration 60s
|
||||
//
|
||||
// The xaddr is the camera's host or host:port (the library appends
|
||||
// /onvif/device_service); pass with no protocol prefix.
|
||||
// # Credentials
|
||||
//
|
||||
// The camera password is read, in order of preference:
|
||||
//
|
||||
// 1. The ONVIF_PASSWORD environment variable.
|
||||
// 2. A file pointed at by -password-file (newline stripped).
|
||||
// 3. Interactive prompt when stdin is a tty.
|
||||
//
|
||||
// -password is also accepted but DISCOURAGED — it leaks the credential
|
||||
// into shell history and the system process listing. Use only for
|
||||
// throwaway dev cameras.
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
@@ -31,14 +43,15 @@ import (
|
||||
func main() {
|
||||
xaddr := flag.String("xaddr", "", "camera host or host:port (required)")
|
||||
username := flag.String("username", "", "ONVIF user (required)")
|
||||
password := flag.String("password", "", "ONVIF password (required)")
|
||||
insecurePassword := flag.String("password", "", "INSECURE — leaks into shell history; prefer ONVIF_PASSWORD env or -password-file")
|
||||
passwordFile := flag.String("password-file", "", "read password from this file (newline trimmed)")
|
||||
deviceID := flag.String("device-id", "", "logical name printed with each event (default: xaddr)")
|
||||
filter := flag.String("filter", "", "raw ONVIF ConcreteSet topic filter (empty = all topics, works on AXIS)")
|
||||
pullTimeout := flag.Duration("pull-timeout", 5*time.Second, "server-side wait per PullMessages call")
|
||||
duration := flag.Duration("duration", 0, "stop after this long (0 = run until Ctrl-C)")
|
||||
flag.Parse()
|
||||
|
||||
if *xaddr == "" || *username == "" || *password == "" {
|
||||
if *xaddr == "" || *username == "" {
|
||||
flag.Usage()
|
||||
os.Exit(2)
|
||||
}
|
||||
@@ -46,10 +59,15 @@ func main() {
|
||||
*deviceID = *xaddr
|
||||
}
|
||||
|
||||
password, err := loadPassword(*insecurePassword, *passwordFile)
|
||||
if err != nil {
|
||||
log.Fatalf("password: %v", err)
|
||||
}
|
||||
|
||||
dev, err := onvif.NewDevice(onvif.DeviceParams{
|
||||
Xaddr: *xaddr,
|
||||
Username: *username,
|
||||
Password: *password,
|
||||
Password: password,
|
||||
AuthMode: onvif.UsernameTokenAuth,
|
||||
})
|
||||
if err != nil {
|
||||
@@ -79,7 +97,11 @@ func main() {
|
||||
if err != nil {
|
||||
log.Fatalf("open stream: %v", err)
|
||||
}
|
||||
defer s.Close()
|
||||
defer func() {
|
||||
if err := s.Close(); err != nil {
|
||||
log.Printf("stream close: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
log.Printf("streaming from %s (device-id=%s, filter=%q)", *xaddr, *deviceID, *filter)
|
||||
for {
|
||||
@@ -93,6 +115,9 @@ func main() {
|
||||
}
|
||||
fmt.Printf("%s kind=%-15s state=%-9s op=%-12s topic=%s",
|
||||
ev.Timestamp.Format(time.RFC3339), ev.Kind, ev.State, ev.Operation, ev.Topic)
|
||||
if ev.AfterReconnect {
|
||||
fmt.Print(" [after-reconnect]")
|
||||
}
|
||||
if len(ev.Source) > 0 {
|
||||
fmt.Printf(" source=%v", ev.Source)
|
||||
}
|
||||
@@ -100,8 +125,52 @@ func main() {
|
||||
fmt.Printf(" data=%v", ev.Data)
|
||||
}
|
||||
fmt.Println()
|
||||
case e := <-s.Errors():
|
||||
log.Printf("stream error: %v", e)
|
||||
case e, ok := <-s.Errors():
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var pull stream.ErrPullFailed
|
||||
var recreate stream.ErrRecreateFailed
|
||||
switch {
|
||||
case errors.As(e, &recreate):
|
||||
log.Printf("RECREATE failed: %v (camera may be offline)", recreate.Err)
|
||||
case errors.As(e, &pull):
|
||||
log.Printf("pull error (will retry): %v", pull.Err)
|
||||
default:
|
||||
log.Printf("stream error: %v", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// loadPassword resolves the camera password from the environment first
|
||||
// (ONVIF_PASSWORD), then -password-file, then an interactive prompt as
|
||||
// a last resort. The insecure -password flag is honoured only if
|
||||
// nothing else is set, and a warning is logged.
|
||||
func loadPassword(insecure, file string) (string, error) {
|
||||
if env := os.Getenv("ONVIF_PASSWORD"); env != "" {
|
||||
return env, nil
|
||||
}
|
||||
if file != "" {
|
||||
b, err := os.ReadFile(file)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("read %s: %w", file, err)
|
||||
}
|
||||
return strings.TrimRight(string(b), "\r\n"), nil
|
||||
}
|
||||
if insecure != "" {
|
||||
log.Print("WARNING: -password leaks into shell history and process listings; prefer ONVIF_PASSWORD env or -password-file")
|
||||
return insecure, nil
|
||||
}
|
||||
// Interactive prompt — works when stdin is a tty. We use a plain
|
||||
// reader (rather than golang.org/x/term hidden input) to keep
|
||||
// this example dependency-free; in production, callers should
|
||||
// integrate term.ReadPassword.
|
||||
fmt.Fprint(os.Stderr, "ONVIF password (visible): ")
|
||||
r := bufio.NewReader(os.Stdin)
|
||||
line, err := r.ReadString('\n')
|
||||
if err != nil {
|
||||
return "", errors.New("no password supplied (set ONVIF_PASSWORD, -password-file, or pipe input)")
|
||||
}
|
||||
return strings.TrimRight(line, "\r\n"), nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user