Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ linters:
- exhaustive
- nlreturn
- gomodguard
- goconst
settings:
Comment on lines 10 to 14
gocritic:
enable-all: true
Expand Down
42 changes: 40 additions & 2 deletions cameras.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,19 @@ import (

// All returns interfaces for every camera.
func (c *Cameras) All() []*Camera {
if c == nil {
return nil
}

return c.cameras
}

// ByNum returns an interface for a single camera.
func (c *Cameras) ByNum(number int) *Camera {
if c == nil {
return nil
}

for _, cam := range c.cameras {
if cam.Number == number {
return cam
Expand All @@ -38,6 +46,10 @@ func (c *Cameras) ByNum(number int) *Camera {

// ByName returns an interface for a single camera, using the name.
func (c *Cameras) ByName(name string) *Camera {
if c == nil {
return nil
}

for _, cam := range c.cameras {
if cam.Name == name {
return cam
Expand All @@ -59,7 +71,7 @@ func (c *Cameras) ByName(name string) *Camera {
// media fragments flush while capture continues. Close() cancels an in-progress capture.
// UseHTTP is not supported (returns ErrHTTPVideoUnsupported).
func (c *Camera) StreamVideo(ops *VidOps, length time.Duration, maxsize int64) (io.ReadCloser, error) {
rtspURL, err := c.makeVideoURL(ops, c.makeRequestParams(ops))
rtspURL, err := c.VideoURL(ops)
if err != nil {
return nil, err
}
Expand All @@ -80,7 +92,7 @@ func (c *Camera) SaveVideo(ops *VidOps, length time.Duration, maxsize int64, out
return ErrPathExists
}

rtspURL, err := c.makeVideoURL(ops, c.makeRequestParams(ops))
rtspURL, err := c.VideoURL(ops)
if err != nil {
return err
}
Expand All @@ -97,6 +109,32 @@ func (c *Camera) SaveVideo(ops *VidOps, length time.Duration, maxsize int64, out
return nil
}

// VideoURL returns the RTSP(S) ++stream URL SaveVideo/StreamVideo would use.
// Credentials are included as userinfo; redact before logging if needed.
func (c *Camera) VideoURL(ops *VidOps) (string, error) {
return c.makeVideoURL(ops, c.makeRequestParams(ops))
}

// RedactedVideoURL is like VideoURL but replaces the password with "REDACTED".
func (c *Camera) RedactedVideoURL(ops *VidOps) (string, error) {
raw, err := c.VideoURL(ops)
if err != nil {
return "", err
}

videoURL, err := url.Parse(raw)
if err != nil {
return "", fmt.Errorf("parsing video URL: %w", err)
}
Comment thread
Copilot marked this conversation as resolved.

if videoURL.User != nil {
user := videoURL.User.Username()
videoURL.User = url.UserPassword(user, "REDACTED")
}

return videoURL.String(), nil
}

func (c *Camera) rtspclipOptions(length time.Duration, maxsize int64) rtspclip.Options {
return rtspclip.Options{
Duration: length,
Expand Down
5 changes: 5 additions & 0 deletions cameras_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,11 @@ func TestMakeVideoURLUserinfoAndCodecs(t *testing.T) {
cam.makeRequestParams(&VidOps{Height: 720}))
require.NoError(t, err)
require.Contains(t, raw, "stream?cameraNum=3&vcodec=h265&acodec=aac&height=720")

redacted, err := cam.RedactedVideoURL(&VidOps{Height: 720, VCodec: "h265", ACodec: aacStr})
require.NoError(t, err)
require.Contains(t, redacted, "rtsps://admin:REDACTED@ss.example:8001/stream?")
require.NotContains(t, redacted, "s3cret")
}

func TestMakeVideoURLPreservesBasePath(t *testing.T) {
Expand Down
3 changes: 1 addition & 2 deletions events.go
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@ func (e *Events) custom(eventType EventType, eventID, cam int, msg string) {
/* INTERFACE HELPER METHODS FOLLOW */

// eventStreamScanner connects to the securityspy event stream and fires events into a channel.
// EventStreamDisconnect is only emitted after a live stream ends, not on failed dials.
//
//nolint:cyclop // but it runs forever!
func (e *Events) eventStreamScanner(ctx context.Context, retryInterval time.Duration) {
Expand All @@ -207,8 +208,6 @@ func (e *Events) eventStreamScanner(ctx context.Context, retryInterval time.Dura

stream, err := e.eventStreamConnect(ctx)
if err != nil {
e.custom(EventStreamDisconnect, -10000, -1, err.Error())

select {
case <-ctx.Done():
return
Expand Down
3 changes: 2 additions & 1 deletion securityspy.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,9 @@ func NewMust(config *server.Config) *Server {
}

// Assign all the sub-interface structs.
secspyServer := &Server{Config: config, Encoder: DefaultEncoder}
secspyServer := &Server{Config: config, Encoder: DefaultEncoder, Info: &ServerInfo{}}
secspyServer.Files = &Files{server: secspyServer}
secspyServer.Cameras = &Cameras{server: secspyServer}
secspyServer.Events = &Events{
server: secspyServer,
eventBinds: make(map[EventType][]func(Event)),
Expand Down
Loading