Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 629bc7df33 |
@@ -28,7 +28,7 @@ jobs:
|
|||||||
if: steps.install-go.outputs.cache-hit != 'true'
|
if: steps.install-go.outputs.cache-hit != 'true'
|
||||||
|
|
||||||
- name: Log in to Docker Hub
|
- name: Log in to Docker Hub
|
||||||
uses: docker/login-action@v4
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|||||||
@@ -46,8 +46,6 @@ linters:
|
|||||||
exclusions:
|
exclusions:
|
||||||
generated: lax
|
generated: lax
|
||||||
rules:
|
rules:
|
||||||
- path: (.+)\.go$
|
|
||||||
text: 'string `none` has (.+) occurrences, make it a constant'
|
|
||||||
- path: (.+)\.go$
|
- path: (.+)\.go$
|
||||||
text: 'ST1000: at least one file in a package should have a package comment'
|
text: 'ST1000: at least one file in a package should have a package comment'
|
||||||
- path: (.+)\.go$
|
- path: (.+)\.go$
|
||||||
|
|||||||
@@ -47,8 +47,9 @@ Cameradar scans RTSP endpoints on authorized targets, and uses dictionary attack
|
|||||||
- [Security and responsible use](#security-and-responsible-use)
|
- [Security and responsible use](#security-and-responsible-use)
|
||||||
- [Output](#output)
|
- [Output](#output)
|
||||||
- [Check camera access](#check-camera-access)
|
- [Check camera access](#check-camera-access)
|
||||||
- [Command-line options and environment variables](#command-line-options-and-environment-variables)
|
- [Command-line options](#command-line-options)
|
||||||
- [Input file format](#input-file-format)
|
- [Input file format](#input-file-format)
|
||||||
|
- [Environment variables](#environment-variables)
|
||||||
- [Build and contribute](#build-and-contribute)
|
- [Build and contribute](#build-and-contribute)
|
||||||
- [Frequently asked questions](#frequently-asked-questions)
|
- [Frequently asked questions](#frequently-asked-questions)
|
||||||
- [Examples](#examples)
|
- [Examples](#examples)
|
||||||
@@ -74,7 +75,7 @@ docker run --rm -t --net=host ullaakut/cameradar --targets 192.168.100.0/24
|
|||||||
|
|
||||||
This scans ports 554, 5554, and 8554 on the target subnet.
|
This scans ports 554, 5554, and 8554 on the target subnet.
|
||||||
It attempts to enumerate RTSP streams.
|
It attempts to enumerate RTSP streams.
|
||||||
For all options, see [Configuration reference](https://github.com/Ullaakut/cameradar/wiki/Configuration-Reference).
|
For all options, see [command-line options](#command-line-options).
|
||||||
|
|
||||||
- Targets can be CIDRs, IPs, IP ranges or a hostname.
|
- Targets can be CIDRs, IPs, IP ranges or a hostname.
|
||||||
- Subnet: `172.16.100.0/24`
|
- Subnet: `172.16.100.0/24`
|
||||||
@@ -106,7 +107,7 @@ Use this option if Docker is not available or if you want a local build.
|
|||||||
1. `go install github.com/Ullaakut/cameradar/v6/cmd/cameradar@latest`
|
1. `go install github.com/Ullaakut/cameradar/v6/cmd/cameradar@latest`
|
||||||
|
|
||||||
The `cameradar` binary is now in your `$GOPATH/bin`.
|
The `cameradar` binary is now in your `$GOPATH/bin`.
|
||||||
For available flags, see [Configuration reference](https://github.com/Ullaakut/cameradar/wiki/Configuration-Reference).
|
For available flags, see [command-line options](#command-line-options).
|
||||||
|
|
||||||
## Install on Android (Termux)
|
## Install on Android (Termux)
|
||||||
|
|
||||||
@@ -272,11 +273,117 @@ localhost
|
|||||||
When you use `--skip-scan`, Cameradar expands each entry into explicit IP
|
When you use `--skip-scan`, Cameradar expands each entry into explicit IP
|
||||||
addresses before building the target list.
|
addresses before building the target list.
|
||||||
|
|
||||||
## Command-line options and environment variables
|
## Options
|
||||||
|
|
||||||
The complete CLI and environment variable reference is maintained in [Configuration reference](https://github.com/Ullaakut/cameradar/wiki/Configuration-Reference).
|
### `TARGETS` / `--targets` / `-t`
|
||||||
|
|
||||||
This includes all supported flags, defaults, accepted values, and env var mapping.
|
This variable is required.
|
||||||
|
It specifies the target that Cameradar scans and attempts to access.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
* `172.16.100.0/24`
|
||||||
|
* `192.168.1.1`
|
||||||
|
* `localhost`
|
||||||
|
* `192.168.1.140-255`
|
||||||
|
* `192.168.2-3.0-255`
|
||||||
|
|
||||||
|
### `PORTS` / `--ports` / `-p`
|
||||||
|
|
||||||
|
This variable is optional and allows you to specify the ports to scan.
|
||||||
|
|
||||||
|
Default value: `554,5554,8554`
|
||||||
|
|
||||||
|
Change these only if you are sure cameras stream over different ports.
|
||||||
|
Most cameras use these defaults.
|
||||||
|
|
||||||
|
### `CUSTOM_ROUTES` / `--custom-routes` / `-r`
|
||||||
|
|
||||||
|
This option is optional.
|
||||||
|
It replaces the default routes dictionary used for the dictionary attack.
|
||||||
|
|
||||||
|
If unset, Cameradar uses the built-in routes dictionary.
|
||||||
|
|
||||||
|
### `CUSTOM_CREDENTIALS` / `--custom-credentials` / `-c`
|
||||||
|
|
||||||
|
This option is optional.
|
||||||
|
It replaces the default credentials dictionary used for the dictionary attack.
|
||||||
|
|
||||||
|
If unset, Cameradar uses the built-in credentials dictionary.
|
||||||
|
|
||||||
|
### `SCANNER` / `--scanner`
|
||||||
|
|
||||||
|
This optional variable sets the discovery backend.
|
||||||
|
|
||||||
|
* `nmap` includes service discovery and is generally more reliable when you want
|
||||||
|
to specifically identify RTSP services.
|
||||||
|
* `masscan` is generally more efficient for large-scale discovery, but it does
|
||||||
|
not identify services and therefore can be less specific for RTSP.
|
||||||
|
|
||||||
|
Supported values: `nmap`, `masscan`
|
||||||
|
|
||||||
|
Default value: `nmap`
|
||||||
|
|
||||||
|
### `SCAN_SPEED` / `--scan-speed` / `-s`
|
||||||
|
|
||||||
|
This optional variable sets nmap discovery presets for speed or accuracy.
|
||||||
|
Lower it on slow networks and raise it on fast networks.
|
||||||
|
See [nmap timing templates](https://nmap.org/book/man-performance.html).
|
||||||
|
|
||||||
|
This option is ignored when `--scanner masscan` is used.
|
||||||
|
|
||||||
|
Default value: `4`
|
||||||
|
|
||||||
|
### `SKIP_SCAN` / `--skip-scan`
|
||||||
|
|
||||||
|
This optional flag skips network discovery and assumes every target and port
|
||||||
|
pair is an RTSP stream.
|
||||||
|
|
||||||
|
Use it when you already know the RTSP endpoints or when discovery is blocked.
|
||||||
|
For best results, specify only RTSP ports.
|
||||||
|
|
||||||
|
Default value: `false`
|
||||||
|
|
||||||
|
### `ATTACK_INTERVAL` / `--attack-interval` / `-I`
|
||||||
|
|
||||||
|
This optional variable sets a delay between attacks.
|
||||||
|
Increase it for networks that may block brute-force attempts.
|
||||||
|
Default: no delay.
|
||||||
|
|
||||||
|
Default value: `0ms`
|
||||||
|
|
||||||
|
### `TIMEOUT` / `--timeout` / `-T`
|
||||||
|
|
||||||
|
This optional variable sets the timeout for requests sent to the cameras.
|
||||||
|
Increase it for slow networks and decrease it for fast networks.
|
||||||
|
|
||||||
|
Default value: `2000ms`
|
||||||
|
|
||||||
|
### `DEBUG` / `--debug` / `-d`
|
||||||
|
|
||||||
|
This optional variable enables more verbose output.
|
||||||
|
|
||||||
|
It outputs discovery results (`nmap` or `masscan`), cURL requests, and more.
|
||||||
|
|
||||||
|
Default: `false`
|
||||||
|
|
||||||
|
### `UI` / `--ui`
|
||||||
|
|
||||||
|
This option selects the UI mode.
|
||||||
|
|
||||||
|
* `auto` selects `tui` if your terminal is interactive, `plain` otherwise
|
||||||
|
* `tui` shows a fullscreen interface with a progress bar and shows the results in a table
|
||||||
|
* `plain` logs the steps taken by cameradar as plain text and is meant to be used by non-interactive terminals
|
||||||
|
|
||||||
|
Supported values: `auto`, `tui`, `plain`
|
||||||
|
|
||||||
|
Default: `auto`
|
||||||
|
|
||||||
|
### `OUTPUT` / `--output`
|
||||||
|
|
||||||
|
This optional variable writes an M3U playlist of the discovered streams to the given file path.
|
||||||
|
|
||||||
|
Example: `/tmp/cameradar.m3u`
|
||||||
|
|
||||||
## Build and contribute
|
## Build and contribute
|
||||||
|
|
||||||
@@ -296,7 +403,41 @@ The `cameradar` binary is now in `$GOPATH/bin/cameradar`.
|
|||||||
|
|
||||||
## Frequently asked questions
|
## Frequently asked questions
|
||||||
|
|
||||||
See [Troubleshooting & FAQ](https://github.com/Ullaakut/cameradar/wiki/Troubleshooting-%26-FAQ)
|
> Cameradar does not detect any camera!
|
||||||
|
|
||||||
|
This usually means the cameras are not streaming over RTSP.
|
||||||
|
It can also mean the targets are not in your scan range.
|
||||||
|
CCTV cameras are often on private subnets.
|
||||||
|
Use `-t` to set the correct targets.
|
||||||
|
If you still see no results, open an issue with device details.
|
||||||
|
|
||||||
|
> Cameradar detects my cameras, but does not manage to access them!
|
||||||
|
|
||||||
|
The camera configuration may have changed, so defaults do not match.
|
||||||
|
Cameradar uses defaults unless you provide custom dictionaries.
|
||||||
|
Add your credentials and routes, then follow the [configuration](#configuration) section.
|
||||||
|
|
||||||
|
> What happened to the C++ version?
|
||||||
|
|
||||||
|
The 1.1.4 tag contains the legacy C++ implementation.
|
||||||
|
It is slower and less stable than the Go version, so it is not recommended to use.
|
||||||
|
|
||||||
|
> I want to scan my local network or my own machine, and it does not work! What's going on?
|
||||||
|
|
||||||
|
Use `--net=host` when running the Docker image, or use the installed binary.
|
||||||
|
|
||||||
|
> I don't have a camera, but I'd like to try Cameradar!
|
||||||
|
|
||||||
|
Run the following container, then run Cameradar against it:
|
||||||
|
|
||||||
|
`docker run -p 8554:8554 -e RTSP_USERNAME=admin -e RTSP_PASSWORD=12345 -e RTSP_PORT=8554 ullaakut/rtspatt`
|
||||||
|
|
||||||
|
Cameradar should discover the `admin` / `12345` credentials.
|
||||||
|
You can try other default credentials listed in the dictionaries.
|
||||||
|
|
||||||
|
> What authentication types does Cameradar support?
|
||||||
|
|
||||||
|
Cameradar supports both basic and digest authentication.
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
|
|||||||
+16
-9
@@ -38,10 +38,11 @@ var (
|
|||||||
|
|
||||||
var flags = cmd.Flags{
|
var flags = cmd.Flags{
|
||||||
&cli.StringSliceFlag{
|
&cli.StringSliceFlag{
|
||||||
Name: flagTargets,
|
Name: flagTargets,
|
||||||
Usage: "The targets on which to scan for open RTSP streams in a network range format",
|
Usage: "The targets on which to scan for open RTSP streams in a network range format",
|
||||||
Aliases: []string{"t"},
|
Aliases: []string{"t"},
|
||||||
Sources: cli.EnvVars(strcase.ToSNAKE(flagTargets)),
|
Sources: cli.EnvVars(strcase.ToSNAKE(flagTargets)),
|
||||||
|
Required: true,
|
||||||
},
|
},
|
||||||
&cli.StringSliceFlag{
|
&cli.StringSliceFlag{
|
||||||
Name: flagPorts,
|
Name: flagPorts,
|
||||||
@@ -127,13 +128,19 @@ func realMain() (code int) {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
scanCommand := &cli.Command{
|
||||||
|
Name: "scan",
|
||||||
|
Usage: "Scan targets for RTSP streams",
|
||||||
|
Flags: flags,
|
||||||
|
Action: runCameradar,
|
||||||
|
}
|
||||||
|
|
||||||
app := &cli.Command{
|
app := &cli.Command{
|
||||||
Name: "Cameradar",
|
Name: "Cameradar",
|
||||||
Version: version,
|
Version: version,
|
||||||
Usage: "Scan targets for RTSP streams",
|
DefaultCommand: scanCommand.Name,
|
||||||
Flags: flags,
|
|
||||||
Action: runCameradar,
|
|
||||||
Commands: []*cli.Command{
|
Commands: []*cli.Command{
|
||||||
|
scanCommand,
|
||||||
{
|
{
|
||||||
Name: "version",
|
Name: "version",
|
||||||
Usage: "Print version information",
|
Usage: "Print version information",
|
||||||
|
|||||||
@@ -1,26 +1,26 @@
|
|||||||
module github.com/Ullaakut/cameradar/v6
|
module github.com/Ullaakut/cameradar/v6
|
||||||
|
|
||||||
go 1.25.8
|
go 1.25.3
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/Ullaakut/masscan v1.0.0
|
github.com/Ullaakut/masscan v1.0.0
|
||||||
github.com/Ullaakut/nmap/v4 v4.0.0
|
github.com/Ullaakut/nmap/v4 v4.0.0
|
||||||
github.com/bluenviron/gortsplib/v5 v5.5.1
|
github.com/bluenviron/gortsplib/v5 v5.4.0
|
||||||
github.com/charmbracelet/bubbles v1.0.0
|
github.com/charmbracelet/bubbles v1.0.0
|
||||||
github.com/charmbracelet/bubbletea v1.3.10
|
github.com/charmbracelet/bubbletea v1.3.10
|
||||||
github.com/charmbracelet/lipgloss v1.1.0
|
github.com/charmbracelet/lipgloss v1.1.0
|
||||||
github.com/ettle/strcase v0.2.0
|
github.com/ettle/strcase v0.2.0
|
||||||
github.com/hamba/cmd/v3 v3.1.1
|
github.com/hamba/cmd/v3 v3.1.0
|
||||||
github.com/stretchr/testify v1.11.1
|
github.com/stretchr/testify v1.11.1
|
||||||
github.com/urfave/cli/v3 v3.8.0
|
github.com/urfave/cli/v3 v3.7.0
|
||||||
golang.org/x/term v0.42.0
|
golang.org/x/term v0.40.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/VictoriaMetrics/metrics v1.41.2 // indirect
|
github.com/VictoriaMetrics/metrics v1.40.1 // indirect
|
||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||||
github.com/beorn7/perks v1.0.1 // indirect
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
github.com/bluenviron/mediacommon/v2 v2.8.3 // indirect
|
github.com/bluenviron/mediacommon/v2 v2.8.1 // indirect
|
||||||
github.com/cactus/go-statsd-client/v5 v5.1.0 // indirect
|
github.com/cactus/go-statsd-client/v5 v5.1.0 // indirect
|
||||||
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
@@ -37,15 +37,14 @@ require (
|
|||||||
github.com/go-logr/logr v1.4.3 // indirect
|
github.com/go-logr/logr v1.4.3 // indirect
|
||||||
github.com/go-logr/stdr v1.2.2 // indirect
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
github.com/go-stack/stack v1.8.1 // indirect
|
github.com/go-stack/stack v1.8.1 // indirect
|
||||||
github.com/go4org/hashtriemap v0.0.0-20251130024219-545ba229f689 // indirect
|
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/gorilla/websocket v1.5.3 // indirect
|
github.com/gorilla/websocket v1.5.3 // indirect
|
||||||
github.com/grafana/pyroscope-go v1.2.7 // indirect
|
github.com/grafana/pyroscope-go v1.2.7 // indirect
|
||||||
github.com/grafana/pyroscope-go/godeltaprof v0.1.9 // indirect
|
github.com/grafana/pyroscope-go/godeltaprof v0.1.9 // indirect
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect
|
||||||
github.com/hamba/logger/v2 v2.9.1 // indirect
|
github.com/hamba/logger/v2 v2.9.0 // indirect
|
||||||
github.com/hamba/statter/v2 v2.8.1 // indirect
|
github.com/hamba/statter/v2 v2.8.0 // indirect
|
||||||
github.com/klauspost/compress v1.18.4 // indirect
|
github.com/klauspost/compress v1.18.0 // indirect
|
||||||
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
|
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/mattn/go-localereader v0.0.1 // indirect
|
github.com/mattn/go-localereader v0.0.1 // indirect
|
||||||
@@ -54,6 +53,7 @@ require (
|
|||||||
github.com/muesli/cancelreader v0.2.2 // indirect
|
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||||
github.com/muesli/termenv v0.16.0 // indirect
|
github.com/muesli/termenv v0.16.0 // indirect
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||||
|
github.com/openzipkin/zipkin-go v0.4.3 // indirect
|
||||||
github.com/pion/logging v0.2.4 // indirect
|
github.com/pion/logging v0.2.4 // indirect
|
||||||
github.com/pion/randutil v0.1.0 // indirect
|
github.com/pion/randutil v0.1.0 // indirect
|
||||||
github.com/pion/rtcp v1.2.16 // indirect
|
github.com/pion/rtcp v1.2.16 // indirect
|
||||||
@@ -71,21 +71,22 @@ require (
|
|||||||
github.com/valyala/histogram v1.2.0 // indirect
|
github.com/valyala/histogram v1.2.0 // indirect
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||||
go.opentelemetry.io/otel v1.43.0 // indirect
|
go.opentelemetry.io/otel v1.40.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 // indirect
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0 // indirect
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 // indirect
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 // indirect
|
||||||
go.opentelemetry.io/otel/metric v1.43.0 // indirect
|
go.opentelemetry.io/otel/exporters/zipkin v1.38.0 // indirect
|
||||||
go.opentelemetry.io/otel/sdk v1.43.0 // indirect
|
go.opentelemetry.io/otel/metric v1.40.0 // indirect
|
||||||
go.opentelemetry.io/otel/trace v1.43.0 // indirect
|
go.opentelemetry.io/otel/sdk v1.40.0 // indirect
|
||||||
go.opentelemetry.io/proto/otlp v1.10.0 // indirect
|
go.opentelemetry.io/otel/trace v1.40.0 // indirect
|
||||||
|
go.opentelemetry.io/proto/otlp v1.8.0 // indirect
|
||||||
go.yaml.in/yaml/v2 v2.4.2 // indirect
|
go.yaml.in/yaml/v2 v2.4.2 // indirect
|
||||||
golang.org/x/net v0.52.0 // indirect
|
golang.org/x/net v0.51.0 // indirect
|
||||||
golang.org/x/sys v0.43.0 // indirect
|
golang.org/x/sys v0.41.0 // indirect
|
||||||
golang.org/x/text v0.35.0 // indirect
|
golang.org/x/text v0.34.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect
|
google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20250908214217-97024824d090 // indirect
|
||||||
google.golang.org/grpc v1.80.0 // indirect
|
google.golang.org/grpc v1.75.1 // indirect
|
||||||
google.golang.org/protobuf v1.36.11 // indirect
|
google.golang.org/protobuf v1.36.9 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -10,18 +10,18 @@ github.com/Ullaakut/masscan v1.0.0 h1:+YtpxNcIEaB2lMWNy+oDZF+5pP86S7vSzCKMjW6UDD
|
|||||||
github.com/Ullaakut/masscan v1.0.0/go.mod h1:2LQUQ88hmdXZ+JqQTx6RaszuZDRIAwjEoUL+sVXCAe8=
|
github.com/Ullaakut/masscan v1.0.0/go.mod h1:2LQUQ88hmdXZ+JqQTx6RaszuZDRIAwjEoUL+sVXCAe8=
|
||||||
github.com/Ullaakut/nmap/v4 v4.0.0 h1:QwpxX5F+S14ZEvBQKc37xnvpPXcw4vK0rsZkGV4h98s=
|
github.com/Ullaakut/nmap/v4 v4.0.0 h1:QwpxX5F+S14ZEvBQKc37xnvpPXcw4vK0rsZkGV4h98s=
|
||||||
github.com/Ullaakut/nmap/v4 v4.0.0/go.mod h1:B+MtOtHdb+jR9bc11BNwZX1QVHOtsDjfKkXMCZtRzbw=
|
github.com/Ullaakut/nmap/v4 v4.0.0/go.mod h1:B+MtOtHdb+jR9bc11BNwZX1QVHOtsDjfKkXMCZtRzbw=
|
||||||
github.com/VictoriaMetrics/metrics v1.41.2 h1:pLQ4Mw9TqXFq3ZsZVJkz88JHpjL9LY5NHTY3v2gBNAw=
|
github.com/VictoriaMetrics/metrics v1.40.1 h1:FrF5uJRpIVj9fayWcn8xgiI+FYsKGMslzPuOXjdeyR4=
|
||||||
github.com/VictoriaMetrics/metrics v1.41.2/go.mod h1:xDM82ULLYCYdFRgQ2JBxi8Uf1+8En1So9YUwlGTOqTc=
|
github.com/VictoriaMetrics/metrics v1.40.1/go.mod h1:XE4uudAAIRaJE614Tl5HMrtoEU6+GDZO4QTnNSsZRuA=
|
||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||||
github.com/aymanbagabas/go-udiff v0.3.1 h1:LV+qyBQ2pqe0u42ZsUEtPiCaUoqgA9gYRDs3vj1nolY=
|
github.com/aymanbagabas/go-udiff v0.3.1 h1:LV+qyBQ2pqe0u42ZsUEtPiCaUoqgA9gYRDs3vj1nolY=
|
||||||
github.com/aymanbagabas/go-udiff v0.3.1/go.mod h1:G0fsKmG+P6ylD0r6N/KgQD/nWzgfnl8ZBcNLgcbrw8E=
|
github.com/aymanbagabas/go-udiff v0.3.1/go.mod h1:G0fsKmG+P6ylD0r6N/KgQD/nWzgfnl8ZBcNLgcbrw8E=
|
||||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||||
github.com/bluenviron/gortsplib/v5 v5.5.1 h1:jZJb1dLsBMhxW1cFz/CwLhUW5diaqHf/617QyQGx4nY=
|
github.com/bluenviron/gortsplib/v5 v5.4.0 h1:xi9G4NU67+5uNxGZzJP87SwyaWKr+rUAzbIkOE2SQBo=
|
||||||
github.com/bluenviron/gortsplib/v5 v5.5.1/go.mod h1:otcPqR836QZej/EYx7njn8vl4TLj8Ya3QAf+GBwh2cQ=
|
github.com/bluenviron/gortsplib/v5 v5.4.0/go.mod h1:+vGoi2RqF8LA7ktls7nC0JIF3DmOHwj0448kdQGYBEQ=
|
||||||
github.com/bluenviron/mediacommon/v2 v2.8.3 h1:T6xb7ZK3eBixi/HynzhtGRCEIrazwcmGIeu0WDTVISY=
|
github.com/bluenviron/mediacommon/v2 v2.8.1 h1:UfR+AxqpL9fl5+KeT5BGklBfWgKS0OaSA7LsL8eVYS8=
|
||||||
github.com/bluenviron/mediacommon/v2 v2.8.3/go.mod h1:CsYjGgzIz8RbloQf4BHR4uReogZsB4PEKWfePVIzJv8=
|
github.com/bluenviron/mediacommon/v2 v2.8.1/go.mod h1:4AsE74EnTxkHeUs1VMER31fivU0jufZUAepaKFRV1lM=
|
||||||
github.com/cactus/go-statsd-client/v5 v5.1.0 h1:sbbdfIl9PgisjEoXzvXI1lwUKWElngsjJKaZeC021P4=
|
github.com/cactus/go-statsd-client/v5 v5.1.0 h1:sbbdfIl9PgisjEoXzvXI1lwUKWElngsjJKaZeC021P4=
|
||||||
github.com/cactus/go-statsd-client/v5 v5.1.0/go.mod h1:COEvJ1E+/E2L4q6QE5CkjWPi4eeDw9maJBMIuMPBZbY=
|
github.com/cactus/go-statsd-client/v5 v5.1.0/go.mod h1:COEvJ1E+/E2L4q6QE5CkjWPi4eeDw9maJBMIuMPBZbY=
|
||||||
github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
|
github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
|
||||||
@@ -85,8 +85,6 @@ github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
|
|||||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||||
github.com/go-stack/stack v1.8.1 h1:ntEHSVwIt7PNXNpgPmVfMrNhLtgjlmnZha2kOpuRiDw=
|
github.com/go-stack/stack v1.8.1 h1:ntEHSVwIt7PNXNpgPmVfMrNhLtgjlmnZha2kOpuRiDw=
|
||||||
github.com/go-stack/stack v1.8.1/go.mod h1:dcoOX6HbPZSZptuspn9bctJ+N/CnF5gGygcUP3XYfe4=
|
github.com/go-stack/stack v1.8.1/go.mod h1:dcoOX6HbPZSZptuspn9bctJ+N/CnF5gGygcUP3XYfe4=
|
||||||
github.com/go4org/hashtriemap v0.0.0-20251130024219-545ba229f689 h1:0psnKZ+N2IP43/SZC8SKx6OpFJwLmQb9m9QyV9BC2f8=
|
|
||||||
github.com/go4org/hashtriemap v0.0.0-20251130024219-545ba229f689/go.mod h1:OGmRfY/9QEK2P5zCRtmqfbCF283xPkU2dvVA4MvbvpI=
|
|
||||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||||
@@ -101,19 +99,19 @@ github.com/grafana/pyroscope-go v1.2.7 h1:VWBBlqxjyR0Cwk2W6UrE8CdcdD80GOFNutj0Kb
|
|||||||
github.com/grafana/pyroscope-go v1.2.7/go.mod h1:o/bpSLiJYYP6HQtvcoVKiE9s5RiNgjYTj1DhiddP2Pc=
|
github.com/grafana/pyroscope-go v1.2.7/go.mod h1:o/bpSLiJYYP6HQtvcoVKiE9s5RiNgjYTj1DhiddP2Pc=
|
||||||
github.com/grafana/pyroscope-go/godeltaprof v0.1.9 h1:c1Us8i6eSmkW+Ez05d3co8kasnuOY813tbMN8i/a3Og=
|
github.com/grafana/pyroscope-go/godeltaprof v0.1.9 h1:c1Us8i6eSmkW+Ez05d3co8kasnuOY813tbMN8i/a3Og=
|
||||||
github.com/grafana/pyroscope-go/godeltaprof v0.1.9/go.mod h1:2+l7K7twW49Ct4wFluZD3tZ6e0SjanjcUUBPVD/UuGU=
|
github.com/grafana/pyroscope-go/godeltaprof v0.1.9/go.mod h1:2+l7K7twW49Ct4wFluZD3tZ6e0SjanjcUUBPVD/UuGU=
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs=
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU=
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c=
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs=
|
||||||
github.com/hamba/cmd/v3 v3.1.1 h1:/rJj6bK6ew0zM31I6s2mwtYKSu4BSsd4PxB/dKuwJyA=
|
github.com/hamba/cmd/v3 v3.1.0 h1:aPartvDscWVC6VrboXC9e/uc0Z5S4ogXqj4yTTyqDmg=
|
||||||
github.com/hamba/cmd/v3 v3.1.1/go.mod h1:w1ZhSByZcrL6oB0gkxLeW8wqX+kAbkKf3GiYz/5Kl7I=
|
github.com/hamba/cmd/v3 v3.1.0/go.mod h1:5kSV/F3sDoN2t4R5Ayb2tRCYfHyVICNW5lUvoFe14FY=
|
||||||
github.com/hamba/logger/v2 v2.9.1 h1:NRV+6j0SEdGag1DkjWtV/k3JGOFAByx6IEc/nJNpYLs=
|
github.com/hamba/logger/v2 v2.9.0 h1:gLa4AuoQ17XTBovyIewOK7sALX/sHDJO3kfPUQBUA2o=
|
||||||
github.com/hamba/logger/v2 v2.9.1/go.mod h1:IveSM7xeUVbtmlgXsXoAdNvhQ+JG1CgFMBlKG7hRH/4=
|
github.com/hamba/logger/v2 v2.9.0/go.mod h1:i+ohrYJ5XKaicZAJD+64lsYd3ZqLOjFXzt210lmZ/iQ=
|
||||||
github.com/hamba/statter/v2 v2.8.1 h1:Y6mEOXPxBLfBvKzb31BjPhtSLyza/ghFu+Kez7t0CaY=
|
github.com/hamba/statter/v2 v2.8.0 h1:5rLx+e/wODnvtkzpmEQim4hHcWEJbeI+KJuPHTkQCLQ=
|
||||||
github.com/hamba/statter/v2 v2.8.1/go.mod h1:DTwNCeix6cqciNDhT8CzzKa5k2nCWPWGjIAru4jRtpA=
|
github.com/hamba/statter/v2 v2.8.0/go.mod h1:V3pzf51ZQG5tpVQdbbkoTm3mA5GtxeQ30Yr+GPUa3Is=
|
||||||
github.com/hamba/testutils v0.7.0 h1:GQ0RJbz4+aFauvEV5AFgPMOKltl8gWZVbzROS5b9qDc=
|
github.com/hamba/testutils v0.7.0 h1:GQ0RJbz4+aFauvEV5AFgPMOKltl8gWZVbzROS5b9qDc=
|
||||||
github.com/hamba/testutils v0.7.0/go.mod h1:5rw9ZvxgDegvi9j32U5s5LBDrOBhrCu4g53EM03KOF4=
|
github.com/hamba/testutils v0.7.0/go.mod h1:5rw9ZvxgDegvi9j32U5s5LBDrOBhrCu4g53EM03KOF4=
|
||||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||||
github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
|
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||||
github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
@@ -154,6 +152,8 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8
|
|||||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||||
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
|
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
|
||||||
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
|
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
|
||||||
|
github.com/openzipkin/zipkin-go v0.4.3 h1:9EGwpqkgnwdEIJ+Od7QVSEIH+ocmm5nPat0G7sjsSdg=
|
||||||
|
github.com/openzipkin/zipkin-go v0.4.3/go.mod h1:M9wCJZFWCo2RiY+o1eBCEMe0Dp2S5LDHcMZmk3RmK7c=
|
||||||
github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8=
|
github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8=
|
||||||
github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so=
|
github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so=
|
||||||
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
|
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
|
||||||
@@ -202,8 +202,8 @@ github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFA
|
|||||||
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
|
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
|
||||||
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
|
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
|
||||||
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
|
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
|
||||||
github.com/urfave/cli/v3 v3.8.0 h1:XqKPrm0q4P0q5JpoclYoCAv0/MIvH/jZ2umzuf8pNTI=
|
github.com/urfave/cli/v3 v3.7.0 h1:AGSnbUyjtLiM+WJUb4dzXKldl/gL+F8OwmRDtVr6g2U=
|
||||||
github.com/urfave/cli/v3 v3.8.0/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=
|
github.com/urfave/cli/v3 v3.7.0/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=
|
||||||
github.com/valyala/fastrand v1.1.0 h1:f+5HkLW4rsgzdNoleUOB69hyT9IlD2ZQh9GyDMfb5G8=
|
github.com/valyala/fastrand v1.1.0 h1:f+5HkLW4rsgzdNoleUOB69hyT9IlD2ZQh9GyDMfb5G8=
|
||||||
github.com/valyala/fastrand v1.1.0/go.mod h1:HWqCzkrkg6QXT8V2EXWvXCoow7vLwOFN002oeRzjapQ=
|
github.com/valyala/fastrand v1.1.0/go.mod h1:HWqCzkrkg6QXT8V2EXWvXCoow7vLwOFN002oeRzjapQ=
|
||||||
github.com/valyala/histogram v1.2.0 h1:wyYGAZZt3CpwUiIb9AU/Zbllg1llXyrtApRS815OLoQ=
|
github.com/valyala/histogram v1.2.0 h1:wyYGAZZt3CpwUiIb9AU/Zbllg1llXyrtApRS815OLoQ=
|
||||||
@@ -216,58 +216,60 @@ go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ
|
|||||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk=
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk=
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw=
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw=
|
||||||
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
|
go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms=
|
||||||
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
|
go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bTWkw0ICGcOLCAI5l6zsD1j20k=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0/go.mod h1:Vl1/iaggsuRlrHf/hfPJPvVag77kKyvrLeD10kpMl+A=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0 h1:zWWrB1U6nqhS/k6zYB74CjRpuiitRtLLi68VcgmOEto=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 h1:lwI4Dc5leUqENgGuQImwLo4WnuXFPetmPpkLi2IrX54=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0/go.mod h1:2qXPNBX1OVRC0IwOnfo1ljoid+RD0QK3443EaqVlsOU=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0/go.mod h1:Kz/oCE7z5wuyhPxsXDuaPteSWqjSBD5YaSdbxZYGbGk=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 h1:3iZJKlCZufyRzPzlQhUIWVmfltrXuGyfjREgGP3UUjc=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 h1:aTL7F04bJHUlztTsNGJ2l+6he8c+y/b//eR0jjjemT4=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0/go.mod h1:/G+nUPfhq2e+qiXMGxMwumDrP5jtzU+mWN7/sjT2rak=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0/go.mod h1:kldtb7jDTeol0l3ewcmd8SDvx3EmIE7lyvqbasU3QC4=
|
||||||
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
|
go.opentelemetry.io/otel/exporters/zipkin v1.38.0 h1:0rJ2TmzpHDG+Ib9gPmu3J3cE0zXirumQcKS4wCoZUa0=
|
||||||
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
|
go.opentelemetry.io/otel/exporters/zipkin v1.38.0/go.mod h1:Su/nq/K5zRjDKKC3Il0xbViE3juWgG3JDoqLumFx5G0=
|
||||||
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
|
go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g=
|
||||||
go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
|
go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc=
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
|
go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8=
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
|
go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE=
|
||||||
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
|
go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw=
|
||||||
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
|
go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg=
|
||||||
go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g=
|
go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw=
|
||||||
go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk=
|
go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA=
|
||||||
|
go.opentelemetry.io/proto/otlp v1.8.0 h1:fRAZQDcAFHySxpJ1TwlA1cJ4tvcrw7nXl9xWWC8N5CE=
|
||||||
|
go.opentelemetry.io/proto/otlp v1.8.0/go.mod h1:tIeYOeNBU4cvmPqpaji1P+KbB4Oloai8wN4rWzRrFF0=
|
||||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||||
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
|
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
|
||||||
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
|
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
|
||||||
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
|
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||||
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
|
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
|
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
|
||||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
|
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
|
||||||
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
|
golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
|
||||||
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
|
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
|
||||||
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
|
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
|
||||||
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
|
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
|
||||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||||
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
|
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||||
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY=
|
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
|
||||||
golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY=
|
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
|
||||||
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
||||||
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
||||||
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
|
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
|
||||||
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
|
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
|
||||||
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
|
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||||
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
|
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA=
|
google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 h1:d8Nakh1G+ur7+P3GcMjpRDEkoLUcLW2iU92XVqR+XMQ=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M=
|
google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090/go.mod h1:U8EXRNSd8sUYyDfs/It7KVWodQr+Hf9xtxyxWudSwEw=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 h1:m8qni9SQFH0tJc1X0vmnpw/0t+AImlSvp30sEupozUg=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20250908214217-97024824d090 h1:/OQuEa4YWtDt7uQWHd3q3sUMb+QOLQUg1xa8CEsRv5w=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20250908214217-97024824d090/go.mod h1:GmFNa4BdJZ2a8G+wCe9Bg3wwThLrJun751XstdJt5Og=
|
||||||
google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM=
|
google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI=
|
||||||
google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4=
|
google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
|
||||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
|
||||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
|
|||||||
+39
-31
@@ -291,26 +291,33 @@ func (a Attacker) attackRoutesForStream(ctx context.Context, target cameradar.St
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a Attacker) routeAttack(stream cameradar.Stream, route string) (bool, error) {
|
func (a Attacker) routeAttack(stream cameradar.Stream, route string) (bool, error) {
|
||||||
stream.Routes = []string{route}
|
u, urlStr, err := buildRTSPURL(stream, route, stream.Username, stream.Password)
|
||||||
code, err := a.describeStatus(stream)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, fmt.Errorf("performing describe request at %q: %w", stream, err)
|
return false, fmt.Errorf("building rtsp url: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
a.reporter.Debug(cameradar.StepAttackRoutes, fmt.Sprintf("DESCRIBE %s RTSP/1.0 > %d", stream, code))
|
code, err := a.describeStatus(u)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("performing describe request at %q: %w", urlStr, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
a.reporter.Debug(cameradar.StepAttackRoutes, fmt.Sprintf("DESCRIBE %s RTSP/1.0 > %d", urlStr, code))
|
||||||
access := code == base.StatusOK || code == base.StatusUnauthorized || code == base.StatusForbidden
|
access := code == base.StatusOK || code == base.StatusUnauthorized || code == base.StatusForbidden
|
||||||
return access, nil
|
return access, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a Attacker) credAttack(stream cameradar.Stream, username, password string) (bool, error) {
|
func (a Attacker) credAttack(stream cameradar.Stream, username, password string) (bool, error) {
|
||||||
stream.Username = username
|
u, urlStr, err := buildRTSPURL(stream, stream.Route(), username, password)
|
||||||
stream.Password = password
|
|
||||||
code, err := a.describeStatus(stream)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, fmt.Errorf("performing describe request at %q: %w", stream, err)
|
return false, fmt.Errorf("building rtsp url: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
a.reporter.Debug(cameradar.StepAttackCredentials, fmt.Sprintf("DESCRIBE %s RTSP/1.0 > %d", stream, code))
|
code, err := a.describeStatus(u)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("performing describe request at %q: %w", urlStr, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
a.reporter.Debug(cameradar.StepAttackCredentials, fmt.Sprintf("DESCRIBE %s RTSP/1.0 > %d", urlStr, code))
|
||||||
return code == base.StatusOK || code == base.StatusNotFound, nil
|
return code == base.StatusOK || code == base.StatusNotFound, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -323,27 +330,32 @@ func (a Attacker) validateStream(ctx context.Context, stream cameradar.Stream, e
|
|||||||
return stream, ctx.Err()
|
return stream, ctx.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
client, err := a.newRTSPClient(stream)
|
u, urlStr, err := buildRTSPURL(stream, stream.Route(), stream.Username, stream.Password)
|
||||||
|
if err != nil {
|
||||||
|
return stream, fmt.Errorf("building rtsp url: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err := a.newRTSPClient(u)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return stream, fmt.Errorf("starting rtsp client: %w", err)
|
return stream, fmt.Errorf("starting rtsp client: %w", err)
|
||||||
}
|
}
|
||||||
defer client.Close()
|
defer client.Close()
|
||||||
|
|
||||||
desc, res, err := a.describeWithRetry(ctx, client, stream)
|
desc, res, err := a.describeWithRetry(ctx, client, u, urlStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return a.handleDescribeError(stream, err)
|
return a.handleDescribeError(stream, urlStr, err)
|
||||||
}
|
}
|
||||||
a.logDescribeResponse(stream.String(), res)
|
a.logDescribeResponse(urlStr, res)
|
||||||
|
|
||||||
if desc == nil || len(desc.Medias) == 0 {
|
if desc == nil || len(desc.Medias) == 0 {
|
||||||
return stream, fmt.Errorf("no media tracks found for %q", stream)
|
return stream, fmt.Errorf("no media tracks found for %q", urlStr)
|
||||||
}
|
}
|
||||||
|
|
||||||
res, err = client.Setup(desc.BaseURL, desc.Medias[0], 0, 0)
|
res, err = client.Setup(desc.BaseURL, desc.Medias[0], 0, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return a.handleSetupError(stream, err)
|
return a.handleSetupError(stream, urlStr, err)
|
||||||
}
|
}
|
||||||
a.logSetupResponse(stream.String(), res)
|
a.logSetupResponse(urlStr, res)
|
||||||
|
|
||||||
stream.Available = res != nil && res.StatusCode == base.StatusOK
|
stream.Available = res != nil && res.StatusCode == base.StatusOK
|
||||||
if stream.Available {
|
if stream.Available {
|
||||||
@@ -353,15 +365,11 @@ func (a Attacker) validateStream(ctx context.Context, stream cameradar.Stream, e
|
|||||||
return stream, nil
|
return stream, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a Attacker) describeWithRetry(ctx context.Context, client *gortsplib.Client, stream cameradar.Stream) (*description.Session, *base.Response, error) {
|
func (a Attacker) describeWithRetry(ctx context.Context, client *gortsplib.Client, u *base.URL, urlStr string) (*description.Session, *base.Response, error) {
|
||||||
u, err := stream.URL()
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, fmt.Errorf("building rtsp url: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
desc *description.Session
|
desc *description.Session
|
||||||
res *base.Response
|
res *base.Response
|
||||||
|
err error
|
||||||
)
|
)
|
||||||
for range 5 {
|
for range 5 {
|
||||||
desc, res, err = client.Describe(u)
|
desc, res, err = client.Describe(u)
|
||||||
@@ -371,7 +379,7 @@ func (a Attacker) describeWithRetry(ctx context.Context, client *gortsplib.Clien
|
|||||||
|
|
||||||
var badStatus liberrors.ErrClientBadStatusCode
|
var badStatus liberrors.ErrClientBadStatusCode
|
||||||
if errors.As(err, &badStatus) && badStatus.Code == base.StatusServiceUnavailable {
|
if errors.As(err, &badStatus) && badStatus.Code == base.StatusServiceUnavailable {
|
||||||
a.reporter.Debug(cameradar.StepValidateStreams, fmt.Sprintf("DESCRIBE %s RTSP/1.0 > %d (retrying)", stream, badStatus.Code))
|
a.reporter.Debug(cameradar.StepValidateStreams, fmt.Sprintf("DESCRIBE %s RTSP/1.0 > %d (retrying)", urlStr, badStatus.Code))
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return nil, nil, ctx.Err()
|
return nil, nil, ctx.Err()
|
||||||
@@ -383,13 +391,13 @@ func (a Attacker) describeWithRetry(ctx context.Context, client *gortsplib.Clien
|
|||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, nil, fmt.Errorf("describe retries exhausted for %q: %w", stream, err)
|
return nil, nil, fmt.Errorf("describe retries exhausted for %q: %w", urlStr, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a Attacker) handleDescribeError(stream cameradar.Stream, err error) (cameradar.Stream, error) {
|
func (a Attacker) handleDescribeError(stream cameradar.Stream, urlStr string, err error) (cameradar.Stream, error) {
|
||||||
var badStatus liberrors.ErrClientBadStatusCode
|
var badStatus liberrors.ErrClientBadStatusCode
|
||||||
if errors.As(err, &badStatus) && badStatus.Code == base.StatusServiceUnavailable {
|
if errors.As(err, &badStatus) && badStatus.Code == base.StatusServiceUnavailable {
|
||||||
a.reporter.Debug(cameradar.StepValidateStreams, fmt.Sprintf("DESCRIBE %s RTSP/1.0 > %d", stream, badStatus.Code))
|
a.reporter.Debug(cameradar.StepValidateStreams, fmt.Sprintf("DESCRIBE %s RTSP/1.0 > %d", urlStr, badStatus.Code))
|
||||||
a.reporter.Progress(cameradar.StepValidateStreams, fmt.Sprintf("Stream unavailable for %s:%d (RTSP %d)",
|
a.reporter.Progress(cameradar.StepValidateStreams, fmt.Sprintf("Stream unavailable for %s:%d (RTSP %d)",
|
||||||
stream.Address.String(),
|
stream.Address.String(),
|
||||||
stream.Port,
|
stream.Port,
|
||||||
@@ -399,20 +407,20 @@ func (a Attacker) handleDescribeError(stream cameradar.Stream, err error) (camer
|
|||||||
return stream, nil
|
return stream, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
a.reporter.Debug(cameradar.StepValidateStreams, fmt.Sprintf("DESCRIBE %s RTSP/1.0 > error: %v", stream, err))
|
a.reporter.Debug(cameradar.StepValidateStreams, fmt.Sprintf("DESCRIBE %s RTSP/1.0 > error: %v", urlStr, err))
|
||||||
|
|
||||||
return stream, fmt.Errorf("performing describe request at %q: %w", stream, err)
|
return stream, fmt.Errorf("performing describe request at %q: %w", urlStr, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a Attacker) handleSetupError(stream cameradar.Stream, err error) (cameradar.Stream, error) {
|
func (a Attacker) handleSetupError(stream cameradar.Stream, urlStr string, err error) (cameradar.Stream, error) {
|
||||||
var badStatus liberrors.ErrClientBadStatusCode
|
var badStatus liberrors.ErrClientBadStatusCode
|
||||||
if errors.As(err, &badStatus) {
|
if errors.As(err, &badStatus) {
|
||||||
a.reporter.Debug(cameradar.StepValidateStreams, fmt.Sprintf("SETUP %s RTSP/1.0 > %d", stream, badStatus.Code))
|
a.reporter.Debug(cameradar.StepValidateStreams, fmt.Sprintf("SETUP %s RTSP/1.0 > %d", urlStr, badStatus.Code))
|
||||||
stream.Available = badStatus.Code == base.StatusOK
|
stream.Available = badStatus.Code == base.StatusOK
|
||||||
return stream, nil
|
return stream, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return stream, fmt.Errorf("performing setup request at %q: %w", stream, err)
|
return stream, fmt.Errorf("performing setup request at %q: %w", urlStr, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a Attacker) logDescribeResponse(urlStr string, res *base.Response) {
|
func (a Attacker) logDescribeResponse(urlStr string, res *base.Response) {
|
||||||
|
|||||||
@@ -41,44 +41,28 @@ func (a Attacker) detectAuthMethod(ctx context.Context, stream cameradar.Stream)
|
|||||||
if ctx.Err() != nil {
|
if ctx.Err() != nil {
|
||||||
return stream, ctx.Err()
|
return stream, ctx.Err()
|
||||||
}
|
}
|
||||||
u, err := stream.URL()
|
u, urlStr, err := buildRTSPURL(stream, stream.Route(), "", "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return stream, fmt.Errorf("building rtsp url: %w", err)
|
return stream, fmt.Errorf("building rtsp url: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
statusCode, headers, err := a.probeDescribeHeaders(ctx, u)
|
statusCode, headers, err := a.probeDescribeHeaders(ctx, u, urlStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
a.reporter.Debug(cameradar.StepDetectAuth, fmt.Sprintf("DESCRIBE %s RTSP/1.0 > error: %v", u, err))
|
a.reporter.Debug(cameradar.StepDetectAuth, fmt.Sprintf("DESCRIBE %s RTSP/1.0 > error: %v", urlStr, err))
|
||||||
if stream.Scheme == schemeHTTP || stream.Scheme == schemeHTTPS {
|
|
||||||
statusCode, statusErr := a.describeStatus(stream)
|
|
||||||
if statusErr == nil {
|
|
||||||
a.reporter.Debug(cameradar.StepDetectAuth, fmt.Sprintf("DESCRIBE %s RTSP/1.0 > %d (fallback)", u, statusCode))
|
|
||||||
stream.AuthenticationType = authTypeFromStatus(statusCode, nil)
|
|
||||||
return stream, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
stream.AuthenticationType = cameradar.AuthUnknown
|
|
||||||
return stream, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
stream.AuthenticationType = cameradar.AuthUnknown
|
stream.AuthenticationType = cameradar.AuthUnknown
|
||||||
return stream, fmt.Errorf("performing describe request at %q: %w", u, err)
|
return stream, fmt.Errorf("performing describe request at %q: %w", urlStr, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
a.reporter.Debug(cameradar.StepDetectAuth, fmt.Sprintf("DESCRIBE %s RTSP/1.0 > %d", u, statusCode))
|
a.reporter.Debug(cameradar.StepDetectAuth, fmt.Sprintf("DESCRIBE %s RTSP/1.0 > %d", urlStr, statusCode))
|
||||||
values := headerValues(headers, "WWW-Authenticate")
|
values := headerValues(headers, "WWW-Authenticate")
|
||||||
stream.AuthenticationType = authTypeFromStatus(statusCode, values)
|
switch statusCode {
|
||||||
|
case base.StatusOK:
|
||||||
|
stream.AuthenticationType = cameradar.AuthNone
|
||||||
|
case base.StatusUnauthorized:
|
||||||
|
stream.AuthenticationType = authTypeFromHeaders(values)
|
||||||
|
default:
|
||||||
|
stream.AuthenticationType = cameradar.AuthUnknown
|
||||||
|
}
|
||||||
|
|
||||||
return stream, nil
|
return stream, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func authTypeFromStatus(statusCode base.StatusCode, wwwAuthenticate base.HeaderValue) cameradar.AuthType {
|
|
||||||
switch statusCode {
|
|
||||||
case base.StatusOK:
|
|
||||||
return cameradar.AuthNone
|
|
||||||
case base.StatusUnauthorized:
|
|
||||||
return authTypeFromHeaders(wwwAuthenticate)
|
|
||||||
default:
|
|
||||||
return cameradar.AuthUnknown
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -2,13 +2,7 @@ package attack
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"crypto/ecdsa"
|
|
||||||
"crypto/elliptic"
|
|
||||||
"crypto/rand"
|
|
||||||
"crypto/tls"
|
|
||||||
"crypto/x509"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"math/big"
|
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -84,49 +78,6 @@ func TestAuthTypeFromHeaders(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAuthTypeFromStatus(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
statusCode base.StatusCode
|
|
||||||
headers base.HeaderValue
|
|
||||||
wantAuthType cameradar.AuthType
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "status ok means no auth",
|
|
||||||
statusCode: base.StatusOK,
|
|
||||||
wantAuthType: cameradar.AuthNone,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "status unauthorized with basic",
|
|
||||||
statusCode: base.StatusUnauthorized,
|
|
||||||
headers: headers.Authenticate{Method: headers.AuthMethodBasic, Realm: "cam"}.Marshal(),
|
|
||||||
wantAuthType: cameradar.AuthBasic,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "status unauthorized with digest",
|
|
||||||
statusCode: base.StatusUnauthorized,
|
|
||||||
headers: headers.Authenticate{Method: headers.AuthMethodDigest, Realm: "cam", Nonce: "nonce"}.Marshal(),
|
|
||||||
wantAuthType: cameradar.AuthDigest,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "status unauthorized without auth headers",
|
|
||||||
statusCode: base.StatusUnauthorized,
|
|
||||||
wantAuthType: cameradar.AuthUnknown,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "status not found is unknown",
|
|
||||||
statusCode: base.StatusNotFound,
|
|
||||||
wantAuthType: cameradar.AuthUnknown,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, test := range tests {
|
|
||||||
t.Run(test.name, func(t *testing.T) {
|
|
||||||
assert.Equal(t, test.wantAuthType, authTypeFromStatus(test.statusCode, test.headers))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDetectAuthMethod(t *testing.T) {
|
func TestDetectAuthMethod(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
@@ -191,52 +142,6 @@ func TestDetectAuthMethod(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDetectAuthMethod_HTTPTunnel_NonFatal(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
scheme string
|
|
||||||
}{
|
|
||||||
{name: "http tunnel", scheme: "http"},
|
|
||||||
{name: "https tunnel", scheme: "https"},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, test := range tests {
|
|
||||||
t.Run(test.name, func(t *testing.T) {
|
|
||||||
attacker, err := New(testDictionary{}, 0, time.Second, ui.NopReporter{})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
stream := cameradar.Stream{
|
|
||||||
Address: netip.MustParseAddr("127.0.0.1"),
|
|
||||||
Port: 1,
|
|
||||||
Scheme: test.scheme,
|
|
||||||
}
|
|
||||||
|
|
||||||
got, err := attacker.detectAuthMethod(t.Context(), stream)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, cameradar.AuthUnknown, got.AuthenticationType)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDetectAuthMethod_RTSPS(t *testing.T) {
|
|
||||||
addr, port := startRTSPTLSProbeServer(t, base.StatusUnauthorized, base.Header{
|
|
||||||
"WWW-Authenticate": headers.Authenticate{Method: headers.AuthMethodBasic, Realm: "cam"}.Marshal(),
|
|
||||||
})
|
|
||||||
|
|
||||||
attacker, err := New(testDictionary{}, 0, time.Second, ui.NopReporter{})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
stream := cameradar.Stream{
|
|
||||||
Address: addr,
|
|
||||||
Port: port,
|
|
||||||
Scheme: "rtsps",
|
|
||||||
}
|
|
||||||
|
|
||||||
got, err := attacker.detectAuthMethod(t.Context(), stream)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, cameradar.AuthBasic, got.AuthenticationType)
|
|
||||||
}
|
|
||||||
|
|
||||||
func startRTSPProbeServer(t *testing.T, statusCode base.StatusCode, headers base.Header) (netip.Addr, uint16) {
|
func startRTSPProbeServer(t *testing.T, statusCode base.StatusCode, headers base.Header) (netip.Addr, uint16) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
@@ -288,83 +193,6 @@ func startRTSPProbeServer(t *testing.T, statusCode base.StatusCode, headers base
|
|||||||
return netip.MustParseAddr("127.0.0.1"), uint16(tcpAddr.Port)
|
return netip.MustParseAddr("127.0.0.1"), uint16(tcpAddr.Port)
|
||||||
}
|
}
|
||||||
|
|
||||||
func startRTSPTLSProbeServer(t *testing.T, statusCode base.StatusCode, headers base.Header) (netip.Addr, uint16) {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
listener, err := tls.Listen("tcp", "127.0.0.1:0", testTLSConfig(t))
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
t.Cleanup(func() {
|
|
||||||
_ = listener.Close()
|
|
||||||
})
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
conn, err := listener.Accept()
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer conn.Close()
|
|
||||||
|
|
||||||
_ = conn.SetDeadline(time.Now().Add(time.Second))
|
|
||||||
|
|
||||||
reader := bufio.NewReader(conn)
|
|
||||||
for {
|
|
||||||
line, err := reader.ReadString('\n')
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if strings.TrimSpace(line) == "" {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
statusText := statusTextFromCode(statusCode)
|
|
||||||
|
|
||||||
var builder strings.Builder
|
|
||||||
_, _ = fmt.Fprintf(&builder, "RTSP/1.0 %d %s\r\n", statusCode, statusText)
|
|
||||||
builder.WriteString("CSeq: 1\r\n")
|
|
||||||
for key, values := range headers {
|
|
||||||
for _, value := range values {
|
|
||||||
_, _ = fmt.Fprintf(&builder, "%s: %s\r\n", key, value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
builder.WriteString("Content-Length: 0\r\n\r\n")
|
|
||||||
|
|
||||||
_, _ = conn.Write([]byte(builder.String()))
|
|
||||||
}()
|
|
||||||
|
|
||||||
tcpAddr, ok := listener.Addr().(*net.TCPAddr)
|
|
||||||
require.True(t, ok)
|
|
||||||
|
|
||||||
return netip.MustParseAddr("127.0.0.1"), uint16(tcpAddr.Port)
|
|
||||||
}
|
|
||||||
|
|
||||||
func testTLSConfig(t *testing.T) *tls.Config {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
template := &x509.Certificate{
|
|
||||||
SerialNumber: big.NewInt(1),
|
|
||||||
NotBefore: time.Now().Add(-time.Hour),
|
|
||||||
NotAfter: time.Now().Add(time.Hour),
|
|
||||||
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
|
|
||||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
|
||||||
IPAddresses: []net.IP{net.ParseIP("127.0.0.1")},
|
|
||||||
}
|
|
||||||
|
|
||||||
der, err := x509.CreateCertificate(rand.Reader, template, template, &key.PublicKey, key)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
return &tls.Config{
|
|
||||||
Certificates: []tls.Certificate{{
|
|
||||||
Certificate: [][]byte{der},
|
|
||||||
PrivateKey: key,
|
|
||||||
}},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func statusTextFromCode(code base.StatusCode) string {
|
func statusTextFromCode(code base.StatusCode) string {
|
||||||
switch code {
|
switch code {
|
||||||
case base.StatusOK:
|
case base.StatusOK:
|
||||||
|
|||||||
+32
-62
@@ -3,11 +3,11 @@ package attack
|
|||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/textproto"
|
"net/textproto"
|
||||||
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -19,46 +19,15 @@ import (
|
|||||||
"github.com/bluenviron/gortsplib/v5/pkg/liberrors"
|
"github.com/bluenviron/gortsplib/v5/pkg/liberrors"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
func (a Attacker) newRTSPClient(u *base.URL) (*gortsplib.Client, error) {
|
||||||
schemeRTSP = "rtsp"
|
|
||||||
schemeRTSPS = "rtsps"
|
|
||||||
schemeHTTP = "http"
|
|
||||||
schemeHTTPS = "https"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (a Attacker) newRTSPClient(stream cameradar.Stream) (*gortsplib.Client, error) {
|
|
||||||
u, err := stream.URL()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("building rtsp url: %w", err)
|
|
||||||
}
|
|
||||||
if u.Scheme != schemeRTSP && u.Scheme != schemeRTSPS {
|
|
||||||
return nil, fmt.Errorf("unsupported rtsp url scheme: %q", u.Scheme)
|
|
||||||
}
|
|
||||||
|
|
||||||
client := &gortsplib.Client{
|
client := &gortsplib.Client{
|
||||||
ReadTimeout: a.timeout,
|
ReadTimeout: a.timeout,
|
||||||
WriteTimeout: a.timeout,
|
WriteTimeout: a.timeout,
|
||||||
Scheme: u.Scheme,
|
|
||||||
Host: u.Host,
|
|
||||||
}
|
}
|
||||||
|
client.Scheme = u.Scheme
|
||||||
|
client.Host = u.Host
|
||||||
|
|
||||||
switch stream.Scheme {
|
err := client.Start()
|
||||||
case "":
|
|
||||||
// No explicit transport was requested. Use plain RTSP/RTSPS from the URL.
|
|
||||||
case schemeRTSP, schemeRTSPS:
|
|
||||||
// Nothing to do.
|
|
||||||
case schemeHTTP:
|
|
||||||
client.Scheme = schemeRTSP
|
|
||||||
client.Tunnel = gortsplib.TunnelHTTP
|
|
||||||
case schemeHTTPS:
|
|
||||||
client.Scheme = schemeRTSPS
|
|
||||||
client.Tunnel = gortsplib.TunnelHTTP
|
|
||||||
client.TLSConfig = &tls.Config{InsecureSkipVerify: true}
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("unsupported stream transport scheme: %q", stream.Scheme)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = client.Start()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -66,13 +35,8 @@ func (a Attacker) newRTSPClient(stream cameradar.Stream) (*gortsplib.Client, err
|
|||||||
return client, nil
|
return client, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a Attacker) describeStatus(stream cameradar.Stream) (base.StatusCode, error) {
|
func (a Attacker) describeStatus(u *base.URL) (base.StatusCode, error) {
|
||||||
u, err := stream.URL()
|
client, err := a.newRTSPClient(u)
|
||||||
if err != nil {
|
|
||||||
return 0, fmt.Errorf("building rtsp url: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
client, err := a.newRTSPClient(stream)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
@@ -97,25 +61,9 @@ func (a Attacker) describeStatus(stream cameradar.Stream) (base.StatusCode, erro
|
|||||||
//
|
//
|
||||||
// NOTE: We do not use gortsplib here because it does not expose response headers when the status code is 401 Unauthorized,
|
// NOTE: We do not use gortsplib here because it does not expose response headers when the status code is 401 Unauthorized,
|
||||||
// which is exactly what we need in order to detect authentication methods.
|
// which is exactly what we need in order to detect authentication methods.
|
||||||
func (a Attacker) probeDescribeHeaders(ctx context.Context, u *base.URL) (base.StatusCode, base.Header, error) {
|
func (a Attacker) probeDescribeHeaders(ctx context.Context, u *base.URL, urlStr string) (base.StatusCode, base.Header, error) {
|
||||||
dialer := &net.Dialer{Timeout: a.timeout}
|
dialer := &net.Dialer{Timeout: a.timeout}
|
||||||
|
conn, err := dialer.DialContext(ctx, "tcp", u.Host)
|
||||||
var (
|
|
||||||
conn net.Conn
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
switch u.Scheme {
|
|
||||||
case schemeRTSP:
|
|
||||||
conn, err = dialer.DialContext(ctx, "tcp", u.Host)
|
|
||||||
case schemeRTSPS:
|
|
||||||
tlsDialer := &tls.Dialer{
|
|
||||||
NetDialer: dialer,
|
|
||||||
Config: &tls.Config{InsecureSkipVerify: true},
|
|
||||||
}
|
|
||||||
conn, err = tlsDialer.DialContext(ctx, "tcp", u.Host)
|
|
||||||
default:
|
|
||||||
return 0, nil, fmt.Errorf("unsupported rtsp url scheme: %q", u.Scheme)
|
|
||||||
}
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, nil, err
|
return 0, nil, err
|
||||||
}
|
}
|
||||||
@@ -133,7 +81,7 @@ func (a Attacker) probeDescribeHeaders(ctx context.Context, u *base.URL) (base.S
|
|||||||
|
|
||||||
request := fmt.Sprintf(
|
request := fmt.Sprintf(
|
||||||
"DESCRIBE %s RTSP/1.0\r\nCSeq: 1\r\nUser-Agent: cameradar\r\nAccept: application/sdp\r\nHost: %s\r\n\r\n",
|
"DESCRIBE %s RTSP/1.0\r\nCSeq: 1\r\nUser-Agent: cameradar\r\nAccept: application/sdp\r\nHost: %s\r\n\r\n",
|
||||||
u,
|
urlStr,
|
||||||
u.Host,
|
u.Host,
|
||||||
)
|
)
|
||||||
_, err = conn.Write([]byte(request))
|
_, err = conn.Write([]byte(request))
|
||||||
@@ -215,3 +163,25 @@ func headerValues(header base.Header, name string) base.HeaderValue {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func buildRTSPURL(stream cameradar.Stream, route, username, password string) (*base.URL, string, error) {
|
||||||
|
host := net.JoinHostPort(stream.Address.String(), strconv.Itoa(int(stream.Port)))
|
||||||
|
path := "/" + strings.TrimLeft(strings.TrimSpace(route), "/") // Ensure path starts with a single "/"
|
||||||
|
|
||||||
|
u := &url.URL{
|
||||||
|
Scheme: "rtsp",
|
||||||
|
Host: host,
|
||||||
|
Path: path,
|
||||||
|
}
|
||||||
|
if username != "" || password != "" {
|
||||||
|
u.User = url.UserPassword(username, password)
|
||||||
|
}
|
||||||
|
|
||||||
|
urlStr := u.String()
|
||||||
|
parsed, err := base.ParseURL(urlStr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsed, urlStr, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,163 +2,85 @@ package attack
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"net/url"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/Ullaakut/cameradar/v6"
|
"github.com/Ullaakut/cameradar/v6"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestStreamURL(t *testing.T) {
|
func TestBuildRTSPURL(t *testing.T) {
|
||||||
|
stream := cameradar.Stream{
|
||||||
|
Address: netip.MustParseAddr("192.168.0.10"),
|
||||||
|
Port: 554,
|
||||||
|
}
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
stream cameradar.Stream
|
route string
|
||||||
wantURL string
|
username string
|
||||||
wantParsedScheme string
|
password string
|
||||||
|
wantURL string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "empty route",
|
name: "empty route",
|
||||||
stream: cameradar.Stream{
|
|
||||||
Address: netip.MustParseAddr("192.168.0.10"),
|
|
||||||
Port: 554,
|
|
||||||
},
|
|
||||||
wantURL: "rtsp://192.168.0.10:554/",
|
wantURL: "rtsp://192.168.0.10:554/",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "root route",
|
name: "root route",
|
||||||
stream: cameradar.Stream{
|
route: "/",
|
||||||
Address: netip.MustParseAddr("192.168.0.10"),
|
|
||||||
Port: 554,
|
|
||||||
Routes: []string{"/"},
|
|
||||||
},
|
|
||||||
wantURL: "rtsp://192.168.0.10:554/",
|
wantURL: "rtsp://192.168.0.10:554/",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "multiple leading slashes",
|
name: "multiple leading slashes",
|
||||||
stream: cameradar.Stream{
|
route: "////",
|
||||||
Address: netip.MustParseAddr("192.168.0.10"),
|
|
||||||
Port: 554,
|
|
||||||
Routes: []string{"////"},
|
|
||||||
},
|
|
||||||
wantURL: "rtsp://192.168.0.10:554/",
|
wantURL: "rtsp://192.168.0.10:554/",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "route with no leading slash",
|
name: "route with no leading slash",
|
||||||
stream: cameradar.Stream{
|
route: "stream",
|
||||||
Address: netip.MustParseAddr("192.168.0.10"),
|
|
||||||
Port: 554,
|
|
||||||
Routes: []string{"stream"},
|
|
||||||
},
|
|
||||||
wantURL: "rtsp://192.168.0.10:554/stream",
|
wantURL: "rtsp://192.168.0.10:554/stream",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "route with leading slash",
|
name: "route with leading slash",
|
||||||
stream: cameradar.Stream{
|
route: "/stream",
|
||||||
Address: netip.MustParseAddr("192.168.0.10"),
|
|
||||||
Port: 554,
|
|
||||||
Routes: []string{"/stream"},
|
|
||||||
},
|
|
||||||
wantURL: "rtsp://192.168.0.10:554/stream",
|
wantURL: "rtsp://192.168.0.10:554/stream",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "route with trailing slash",
|
name: "route with trailing slash",
|
||||||
stream: cameradar.Stream{
|
route: "stream/",
|
||||||
Address: netip.MustParseAddr("192.168.0.10"),
|
|
||||||
Port: 554,
|
|
||||||
Routes: []string{"stream/"},
|
|
||||||
},
|
|
||||||
wantURL: "rtsp://192.168.0.10:554/stream/",
|
wantURL: "rtsp://192.168.0.10:554/stream/",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "route with spaces",
|
name: "route with spaces",
|
||||||
stream: cameradar.Stream{
|
route: " /stream ",
|
||||||
Address: netip.MustParseAddr("192.168.0.10"),
|
|
||||||
Port: 554,
|
|
||||||
Routes: []string{" /stream "},
|
|
||||||
},
|
|
||||||
wantURL: "rtsp://192.168.0.10:554/stream",
|
wantURL: "rtsp://192.168.0.10:554/stream",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "username and password",
|
name: "username and password",
|
||||||
stream: cameradar.Stream{
|
route: "stream",
|
||||||
Address: netip.MustParseAddr("192.168.0.10"),
|
username: "admin",
|
||||||
Port: 554,
|
password: "admin123",
|
||||||
Routes: []string{"stream"},
|
wantURL: "rtsp://admin:admin123@192.168.0.10:554/stream",
|
||||||
Username: "admin",
|
|
||||||
Password: "admin123",
|
|
||||||
},
|
|
||||||
wantURL: "rtsp://admin:admin123@192.168.0.10:554/stream",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "empty username with password",
|
name: "empty username with password",
|
||||||
stream: cameradar.Stream{
|
route: "stream",
|
||||||
Address: netip.MustParseAddr("192.168.0.10"),
|
password: "pass",
|
||||||
Port: 554,
|
wantURL: "rtsp://:pass@192.168.0.10:554/stream",
|
||||||
Routes: []string{"stream"},
|
|
||||||
Password: "pass",
|
|
||||||
},
|
|
||||||
wantURL: "rtsp://:pass@192.168.0.10:554/stream",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "username only",
|
name: "username only",
|
||||||
stream: cameradar.Stream{
|
route: "stream",
|
||||||
Address: netip.MustParseAddr("192.168.0.10"),
|
username: "user",
|
||||||
Port: 554,
|
wantURL: "rtsp://user:@192.168.0.10:554/stream",
|
||||||
Routes: []string{"stream"},
|
|
||||||
Username: "user",
|
|
||||||
},
|
|
||||||
wantURL: "rtsp://user:@192.168.0.10:554/stream",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "http scheme",
|
|
||||||
stream: cameradar.Stream{
|
|
||||||
Address: netip.MustParseAddr("192.168.0.10"),
|
|
||||||
Port: 554,
|
|
||||||
Routes: []string{"stream"},
|
|
||||||
Scheme: "http",
|
|
||||||
},
|
|
||||||
wantURL: "http://192.168.0.10:554/stream",
|
|
||||||
wantParsedScheme: "rtsp",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "https scheme",
|
|
||||||
stream: cameradar.Stream{
|
|
||||||
Address: netip.MustParseAddr("192.168.0.10"),
|
|
||||||
Port: 554,
|
|
||||||
Routes: []string{"stream"},
|
|
||||||
Scheme: "https",
|
|
||||||
},
|
|
||||||
wantURL: "https://192.168.0.10:554/stream",
|
|
||||||
wantParsedScheme: "rtsps",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "rtsps scheme",
|
|
||||||
stream: cameradar.Stream{
|
|
||||||
Address: netip.MustParseAddr("192.168.0.10"),
|
|
||||||
Port: 554,
|
|
||||||
Routes: []string{"stream"},
|
|
||||||
Scheme: "rtsps",
|
|
||||||
},
|
|
||||||
wantURL: "rtsps://192.168.0.10:554/stream",
|
|
||||||
wantParsedScheme: "rtsps",
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
t.Run(test.name, func(t *testing.T) {
|
t.Run(test.name, func(t *testing.T) {
|
||||||
gotURL := test.stream.String()
|
_, gotURL, err := buildRTSPURL(stream, test.route, test.username, test.password)
|
||||||
|
require.NoError(t, err)
|
||||||
require.Equal(t, test.wantURL, gotURL)
|
require.Equal(t, test.wantURL, gotURL)
|
||||||
|
|
||||||
parsedURL, err := test.stream.URL()
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
expectedURL, err := url.Parse(test.wantURL)
|
|
||||||
require.NoError(t, err)
|
|
||||||
wantParsedScheme := test.wantParsedScheme
|
|
||||||
if wantParsedScheme == "" {
|
|
||||||
wantParsedScheme = expectedURL.Scheme
|
|
||||||
}
|
|
||||||
require.Equal(t, wantParsedScheme, parsedURL.Scheme)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/Ullaakut/cameradar/v6"
|
"github.com/Ullaakut/cameradar/v6"
|
||||||
"github.com/Ullaakut/cameradar/v6/pkg/ports"
|
|
||||||
masscanlib "github.com/Ullaakut/masscan"
|
masscanlib "github.com/Ullaakut/masscan"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -84,12 +83,9 @@ func runScan(ctx context.Context, runner Runner, reporter Reporter) ([]cameradar
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
scheme := ports.InferTunnelScheme(uint16(port.Number), "")
|
|
||||||
|
|
||||||
streams = append(streams, cameradar.Stream{
|
streams = append(streams, cameradar.Stream{
|
||||||
Address: addr,
|
Address: addr,
|
||||||
Port: uint16(port.Number),
|
Port: uint16(port.Number),
|
||||||
Scheme: scheme,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,31 +63,6 @@ func TestRunScan(t *testing.T) {
|
|||||||
},
|
},
|
||||||
wantProgress: []string{"Found 2 RTSP streams"},
|
wantProgress: []string{"Found 2 RTSP streams"},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: "sets scheme for common HTTP ports",
|
|
||||||
result: &masscanlib.Run{
|
|
||||||
Hosts: []masscanlib.Host{
|
|
||||||
{
|
|
||||||
Address: "192.0.2.10",
|
|
||||||
Ports: []masscanlib.Port{
|
|
||||||
{Number: 554, Status: "open"},
|
|
||||||
{Number: 80, Status: "open"},
|
|
||||||
{Number: 443, Status: "open"},
|
|
||||||
{Number: 8080, Status: "open"},
|
|
||||||
{Number: 8443, Status: "open"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
wantStreams: []cameradar.Stream{
|
|
||||||
{Address: netip.MustParseAddr("192.0.2.10"), Port: 554},
|
|
||||||
{Address: netip.MustParseAddr("192.0.2.10"), Port: 80, Scheme: "http"},
|
|
||||||
{Address: netip.MustParseAddr("192.0.2.10"), Port: 443, Scheme: "https"},
|
|
||||||
{Address: netip.MustParseAddr("192.0.2.10"), Port: 8080, Scheme: "http"},
|
|
||||||
{Address: netip.MustParseAddr("192.0.2.10"), Port: 8443, Scheme: "https"},
|
|
||||||
},
|
|
||||||
wantProgress: []string{"Found 5 RTSP streams"},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: "returns error when scan fails",
|
name: "returns error when scan fails",
|
||||||
err: errors.New("scan failed"),
|
err: errors.New("scan failed"),
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/Ullaakut/cameradar/v6"
|
"github.com/Ullaakut/cameradar/v6"
|
||||||
"github.com/Ullaakut/cameradar/v6/pkg/ports"
|
|
||||||
nmaplib "github.com/Ullaakut/nmap/v4"
|
nmaplib "github.com/Ullaakut/nmap/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -68,8 +67,7 @@ func runScan(ctx context.Context, nmap Runner, reporter Reporter) ([]cameradar.S
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
isCandidate := streamCandidate(port.Service.Name, port.ID)
|
if !strings.Contains(port.Service.Name, "rtsp") {
|
||||||
if !isCandidate {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,12 +78,10 @@ func runScan(ctx context.Context, nmap Runner, reporter Reporter) ([]cameradar.S
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
scheme := ports.InferTunnelScheme(port.ID, port.Service.Name)
|
|
||||||
streams = append(streams, cameradar.Stream{
|
streams = append(streams, cameradar.Stream{
|
||||||
Device: port.Service.Product,
|
Device: port.Service.Product,
|
||||||
Address: addr,
|
Address: addr,
|
||||||
Port: port.ID,
|
Port: port.ID,
|
||||||
Scheme: scheme,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -108,17 +104,3 @@ func updateSummary(reporter Reporter, streams []cameradar.Stream) {
|
|||||||
}
|
}
|
||||||
updater.UpdateSummary(streams)
|
updater.UpdateSummary(streams)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extracting the classifying logic to an external function to avoid nesting if loops.
|
|
||||||
func streamCandidate(serviceName string, port uint16) bool {
|
|
||||||
serviceName = strings.ToLower(strings.TrimSpace(serviceName))
|
|
||||||
if strings.Contains(serviceName, "rtsp") {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
if ports.InferTunnelScheme(port, serviceName) != "" {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -44,56 +44,8 @@ func TestScanner_Scan(t *testing.T) {
|
|||||||
Address: netip.MustParseAddr("127.0.0.1"),
|
Address: netip.MustParseAddr("127.0.0.1"),
|
||||||
Port: 8554,
|
Port: 8554,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
Device: "ACME",
|
|
||||||
Address: netip.MustParseAddr("127.0.0.1"),
|
|
||||||
Port: 80,
|
|
||||||
Scheme: "http",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
wantProgress: "Found 2 RTSP streams",
|
wantProgress: "Found 1 RTSP streams",
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "keeps rtsp and http candidates while filtering closed ports",
|
|
||||||
result: buildRun(nmaplib.Host{
|
|
||||||
Addresses: []nmaplib.Address{
|
|
||||||
{Addr: "127.0.0.1"},
|
|
||||||
{Addr: "not-an-ip"},
|
|
||||||
},
|
|
||||||
Ports: []nmaplib.Port{
|
|
||||||
openPort(8554, "rtsp", "ACME"),
|
|
||||||
closedPort(554, "rtsp", "ACME"),
|
|
||||||
openPort(80, "http", "ACME"),
|
|
||||||
openPort(9443, "https", "ACME"),
|
|
||||||
openPort(8443, "", "ACME"),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
wantStreams: []cameradar.Stream{
|
|
||||||
{
|
|
||||||
Device: "ACME",
|
|
||||||
Address: netip.MustParseAddr("127.0.0.1"),
|
|
||||||
Port: 8554,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Device: "ACME",
|
|
||||||
Address: netip.MustParseAddr("127.0.0.1"),
|
|
||||||
Port: 80,
|
|
||||||
Scheme: "http",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Device: "ACME",
|
|
||||||
Address: netip.MustParseAddr("127.0.0.1"),
|
|
||||||
Port: 9443,
|
|
||||||
Scheme: "https",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Device: "ACME",
|
|
||||||
Address: netip.MustParseAddr("127.0.0.1"),
|
|
||||||
Port: 8443,
|
|
||||||
Scheme: "https",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
wantProgress: "Found 4 RTSP streams",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "collects multiple hosts",
|
name: "collects multiple hosts",
|
||||||
|
|||||||
+5
-11
@@ -88,9 +88,11 @@ func formatStream(stream cameradar.Stream) string {
|
|||||||
builder.WriteString(" Routes: not found\n")
|
builder.WriteString(" Routes: not found\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
if stream.CredentialsFound || stream.AuthenticationType == cameradar.AuthNone {
|
if stream.CredentialsFound {
|
||||||
builder.WriteString(" Credentials: ")
|
builder.WriteString(" Credentials: ")
|
||||||
builder.WriteString(formatCredentials(stream))
|
builder.WriteString(stream.Username)
|
||||||
|
builder.WriteString(":")
|
||||||
|
builder.WriteString(stream.Password)
|
||||||
builder.WriteString("\n")
|
builder.WriteString("\n")
|
||||||
} else {
|
} else {
|
||||||
builder.WriteString(" Credentials: not found\n")
|
builder.WriteString(" Credentials: not found\n")
|
||||||
@@ -103,7 +105,7 @@ func formatStream(stream cameradar.Stream) string {
|
|||||||
builder.WriteString("no\n")
|
builder.WriteString("no\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
if stream.RouteFound && (stream.CredentialsFound || stream.AuthenticationType == cameradar.AuthNone) {
|
if stream.RouteFound && stream.CredentialsFound {
|
||||||
builder.WriteString(" RTSP URL: ")
|
builder.WriteString(" RTSP URL: ")
|
||||||
builder.WriteString(formatRTSPURL(stream))
|
builder.WriteString(formatRTSPURL(stream))
|
||||||
builder.WriteString("\n")
|
builder.WriteString("\n")
|
||||||
@@ -131,14 +133,6 @@ func formatAdminPanelURL(stream cameradar.Stream) string {
|
|||||||
return fmt.Sprintf("http://%s/", stream.Address.String())
|
return fmt.Sprintf("http://%s/", stream.Address.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
func formatCredentials(stream cameradar.Stream) string {
|
|
||||||
if stream.Username == "" && stream.Password == "" {
|
|
||||||
return "none"
|
|
||||||
}
|
|
||||||
|
|
||||||
return stream.Username + ":" + stream.Password
|
|
||||||
}
|
|
||||||
|
|
||||||
func authTypeLabel(auth cameradar.AuthType) string {
|
func authTypeLabel(auth cameradar.AuthType) string {
|
||||||
switch auth {
|
switch auth {
|
||||||
case cameradar.AuthNone:
|
case cameradar.AuthNone:
|
||||||
|
|||||||
@@ -73,41 +73,18 @@ func TestFormatSummary(t *testing.T) {
|
|||||||
"Authentication: digest",
|
"Authentication: digest",
|
||||||
"Routes: stream1, stream2",
|
"Routes: stream1, stream2",
|
||||||
"Credentials: user:pass",
|
"Credentials: user:pass",
|
||||||
"Credentials: none",
|
|
||||||
"RTSP URL: rtsp://user:pass@10.0.0.1:8554/stream1",
|
"RTSP URL: rtsp://user:pass@10.0.0.1:8554/stream1",
|
||||||
"Admin panel: http://10.0.0.1/",
|
"Admin panel: http://10.0.0.1/",
|
||||||
"Admin panel: http://10.0.0.2/",
|
"Admin panel: http://10.0.0.2/",
|
||||||
},
|
},
|
||||||
wantNotContains: []string{
|
wantNotContains: []string{
|
||||||
|
"RTSP URL: rtsp://10.0.0.2",
|
||||||
"Error:",
|
"Error:",
|
||||||
},
|
},
|
||||||
orderedPairs: [][2]string{
|
orderedPairs: [][2]string{
|
||||||
{"• 10.0.0.1:8554", "• 10.0.0.2:554"},
|
{"• 10.0.0.1:8554", "• 10.0.0.2:554"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: "empty discovered credentials render as none",
|
|
||||||
streams: []cameradar.Stream{
|
|
||||||
{
|
|
||||||
Address: netip.MustParseAddr("10.0.0.4"),
|
|
||||||
Port: 554,
|
|
||||||
Available: true,
|
|
||||||
RouteFound: true,
|
|
||||||
Routes: []string{"stream"},
|
|
||||||
CredentialsFound: true,
|
|
||||||
AuthenticationType: cameradar.AuthNone,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
wantContains: []string{
|
|
||||||
"Accessible streams: 1",
|
|
||||||
"Credentials: none",
|
|
||||||
"RTSP URL: rtsp://10.0.0.4:554/stream",
|
|
||||||
},
|
|
||||||
wantNotContains: []string{
|
|
||||||
"Credentials: :",
|
|
||||||
"rtsp://:@10.0.0.4:554/stream",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
|
|||||||
+1
-1
@@ -542,7 +542,7 @@ func buildSummaryRows(streams []cameradar.Stream, visibility summaryVisibilitySt
|
|||||||
|
|
||||||
credentials := emptyEntry
|
credentials := emptyEntry
|
||||||
if visibility.showCredentials && stream.CredentialsFound {
|
if visibility.showCredentials && stream.CredentialsFound {
|
||||||
credentials = formatCredentials(stream)
|
credentials = fmt.Sprintf("%s:%s", stream.Username, stream.Password)
|
||||||
}
|
}
|
||||||
|
|
||||||
available := emptyEntry
|
available := emptyEntry
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
package ports
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// InferTunnelScheme returns the likely scheme for a given port and optional service name.
|
|
||||||
func InferTunnelScheme(port uint16, serviceName string) string {
|
|
||||||
if len(serviceName) > 0 {
|
|
||||||
name := strings.ToLower(strings.TrimSpace(serviceName))
|
|
||||||
switch name {
|
|
||||||
case "rtsps", "https", "http":
|
|
||||||
return name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
switch port {
|
|
||||||
case 443, 8443:
|
|
||||||
return "https"
|
|
||||||
case 80, 8080:
|
|
||||||
return "http"
|
|
||||||
}
|
|
||||||
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
@@ -1,13 +1,7 @@
|
|||||||
package cameradar
|
package cameradar
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net"
|
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"net/url"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/bluenviron/gortsplib/v5/pkg/base"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// AuthType represents the RTSP authentication method.
|
// AuthType represents the RTSP authentication method.
|
||||||
@@ -21,7 +15,7 @@ const (
|
|||||||
AuthDigest
|
AuthDigest
|
||||||
)
|
)
|
||||||
|
|
||||||
// Stream represents a camera's stream, typically accessed over RTSP/RTSPS.
|
// Stream represents a camera's RTSP stream.
|
||||||
type Stream struct {
|
type Stream struct {
|
||||||
Device string `json:"device"`
|
Device string `json:"device"`
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
@@ -30,33 +24,13 @@ type Stream struct {
|
|||||||
Address netip.Addr `json:"address" validate:"required"`
|
Address netip.Addr `json:"address" validate:"required"`
|
||||||
Port uint16 `json:"port" validate:"required"`
|
Port uint16 `json:"port" validate:"required"`
|
||||||
|
|
||||||
CredentialsFound bool `json:"credentials_found"`
|
CredentialsFound bool `json:"credentials_found"`
|
||||||
RouteFound bool `json:"route_found"`
|
RouteFound bool `json:"route_found"`
|
||||||
Available bool `json:"available"`
|
Available bool `json:"available"`
|
||||||
Scheme string `json:"scheme"`
|
|
||||||
|
|
||||||
AuthenticationType AuthType `json:"authentication_type"`
|
AuthenticationType AuthType `json:"authentication_type"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s Stream) resolvedScheme() string {
|
|
||||||
scheme := s.Scheme
|
|
||||||
if scheme == "" {
|
|
||||||
return "rtsp"
|
|
||||||
}
|
|
||||||
return scheme
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseScheme(scheme string) string {
|
|
||||||
switch scheme {
|
|
||||||
case "http":
|
|
||||||
return "rtsp"
|
|
||||||
case "https":
|
|
||||||
return "rtsps"
|
|
||||||
default:
|
|
||||||
return scheme
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Route returns this stream's route if there is one.
|
// Route returns this stream's route if there is one.
|
||||||
func (s Stream) Route() string {
|
func (s Stream) Route() string {
|
||||||
if len(s.Routes) > 0 {
|
if len(s.Routes) > 0 {
|
||||||
@@ -64,28 +38,3 @@ func (s Stream) Route() string {
|
|||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// String builds the stream URL using the configured scheme, defaulting to rtsp.
|
|
||||||
func (s Stream) String() string {
|
|
||||||
scheme := s.resolvedScheme()
|
|
||||||
|
|
||||||
host := net.JoinHostPort(s.Address.String(), strconv.Itoa(int(s.Port)))
|
|
||||||
path := "/" + strings.TrimLeft(strings.TrimSpace(s.Route()), "/")
|
|
||||||
|
|
||||||
u := &url.URL{
|
|
||||||
Scheme: scheme,
|
|
||||||
Host: host,
|
|
||||||
Path: path,
|
|
||||||
}
|
|
||||||
if s.Username != "" || s.Password != "" {
|
|
||||||
u.User = url.UserPassword(s.Username, s.Password)
|
|
||||||
}
|
|
||||||
|
|
||||||
return u.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
// URL parses the stream URL into a *base.URL, normalizing http/https to rtsp/rtsps.
|
|
||||||
func (s Stream) URL() (*base.URL, error) {
|
|
||||||
s.Scheme = parseScheme(s.resolvedScheme())
|
|
||||||
return base.ParseURL(s.String())
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user