Compare commits

...

17 Commits

Author SHA1 Message Date
dependabot[bot] f695b97682 Bump golang.org/x/term from 0.41.0 to 0.42.0 in the all group (#430)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-14 04:44:17 +02:00
dependabot[bot] 4c8c163558 Bump go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp from 1.42.0 to 1.43.0 (#429)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-09 10:41:08 +02:00
dependabot[bot] 22c79cbabc Bump github.com/bluenviron/gortsplib/v5 from 5.5.0 to 5.5.1 in the all group (#427)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-07 08:35:20 +02:00
dependabot[bot] 2f2abb5c81 Bump github.com/urfave/cli/v3 from 3.7.0 to 3.8.0 in the all group (#426)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-31 08:28:56 +02:00
dependabot[bot] 8047d98736 Bump github.com/bluenviron/gortsplib/v5 from 5.4.0 to 5.5.0 in the all group (#425)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-24 08:01:06 +01:00
dependabot[bot] 0e2bed2b70 Bump google.golang.org/grpc from 1.79.2 to 1.79.3 (#424)
Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.79.2 to 1.79.3.
- [Release notes](https://github.com/grpc/grpc-go/releases)
- [Commits](https://github.com/grpc/grpc-go/compare/v1.79.2...v1.79.3)

---
updated-dependencies:
- dependency-name: google.golang.org/grpc
  dependency-version: 1.79.3
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-19 07:11:06 +01:00
Lawrence Arryl Lopez 8531c006d4 feat: support http tunneled rtsp (#419)
* enhancement: supporting http tunneled rtsp

* refactor: simplify HTTP tunnel support per review feedback

- Extract streamCandidate() for nmap port classification
- Add isCommonHTTPPort() for masscan and nmap fallback
- Move URL building to Stream.String() and Stream.URL()
- Pass Stream directly to attack methods instead of individual args
- Add TLS config for HTTPS tunnel support
- Make auth detection non-fatal for tunneled streams
- Rename HTTPTunnel to UseHTTPTunnel

* - Testing the auth workflow for the tunneled streams is not blocking the rest of the pipeline since I changed the return values to Auth unknown and nil
- added extra ports in the test according to suggestions

* fixing some lint errors

* removing the unused buildrtspurl

* delayed the urlstream call for clarity

removed error messages

refactored the test that used the deprecated buildTRSPurl to use stream.URL and stream.String() methods

* extracting iscommonHTTP port to pkg/ports (package ports)

switching on u.scheme to create proper schemes for http and https

* refactor: replace HTTP tunnel bool with scheme-based detection; enable TLS only for HTTPS-tunneled streams

* chore: simnplify InferTunnelScheme and newRTSPClient

* fix: remove rendundant check in streamCandidate

* fix: typo in parseScheme

* tests: coverage for new schemes

* fix: use RTSP and not RTSPS for HTTPS URLs

* fix: tunneled RTSP scheme handling and auth detection fallback

* ui: render empty credentials as none in summary and TUI

* chore: ignore duplicate-string warning for none literal

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* fix: rtsps probe headers

---------

Co-authored-by: Brendan Le Glaunec <brendan@glaulabs.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-18 06:41:20 +01:00
dependabot[bot] 14dcb74e89 Bump the all group with 2 updates (#422)
Bumps the all group with 2 updates: [github.com/hamba/cmd/v3](https://github.com/hamba/cmd) and [golang.org/x/term](https://github.com/golang/term).


Updates `github.com/hamba/cmd/v3` from 3.1.0 to 3.1.1
- [Release notes](https://github.com/hamba/cmd/releases)
- [Commits](https://github.com/hamba/cmd/compare/v3.1.0...v3.1.1)

Updates `golang.org/x/term` from 0.40.0 to 0.41.0
- [Commits](https://github.com/golang/term/compare/v0.40.0...v0.41.0)

---
updated-dependencies:
- dependency-name: github.com/hamba/cmd/v3
  dependency-version: 3.1.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all
- dependency-name: golang.org/x/term
  dependency-version: 0.41.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-17 06:04:52 +01:00
dependabot[bot] 1700227483 Bump docker/login-action from 3 to 4 in the all group (#420)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-09 22:26:26 +01:00
Brendan Le Glaunec b335f98330 docs: add instructions on what targets to test against (#418) 2026-03-09 14:54:54 +01:00
Brendan Le Glaunec 2e8343526e fix: command/flags to prevent subcommand being required (#411) 2026-03-09 08:18:15 +01:00
Brendan Le Glaunec 0f26f25cb9 docs: remove config section in favor of wiki, add links to wiki (#412) 2026-03-09 07:53:21 +01:00
whiteboxsolutions 21a35a8b48 Merge pull request #409 from Ullaakut/dependabot/go_modules/all-4cffc61c5f
Bump the all group with 2 updates
2026-03-02 22:41:04 -08:00
whiteboxsolutions 0065db672c Merge pull request #408 from Ullaakut/improve-contributing-guide
docs: more precisions in contributing guide
2026-03-02 14:41:04 -08:00
dependabot[bot] ac8a77e539 Bump the all group with 2 updates
Bumps the all group with 2 updates: [github.com/bluenviron/gortsplib/v5](https://github.com/bluenviron/gortsplib) and [github.com/urfave/cli/v3](https://github.com/urfave/cli).


Updates `github.com/bluenviron/gortsplib/v5` from 5.3.2 to 5.4.0
- [Commits](https://github.com/bluenviron/gortsplib/compare/v5.3.2...v5.4.0)

Updates `github.com/urfave/cli/v3` from 3.6.2 to 3.7.0
- [Release notes](https://github.com/urfave/cli/releases)
- [Changelog](https://github.com/urfave/cli/blob/main/docs/CHANGELOG.md)
- [Commits](https://github.com/urfave/cli/compare/v3.6.2...v3.7.0)

---
updated-dependencies:
- dependency-name: github.com/bluenviron/gortsplib/v5
  dependency-version: 5.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all
- dependency-name: github.com/urfave/cli/v3
  dependency-version: 3.7.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-02 21:39:54 +00:00
Brendan Le Glaunec 8956d5bc53 docs: more precisions in contributing guide 2026-03-02 19:57:19 +01:00
Brendan Le Glaunec 40f41c3028 feat: add codeowners, fix typos, fix license badge (#407) 2026-03-02 10:53:51 +01:00
23 changed files with 808 additions and 401 deletions
+2
View File
@@ -0,0 +1,2 @@
*.go @Ullaakut @whiteboxsolutions @nblair2
*.md @Ullaakut @whiteboxsolutions @nblair2
+2 -4
View File
@@ -2,10 +2,8 @@
<!-- A brief description of the change being made with this pull request. --> <!-- A brief description of the change being made with this pull request. -->
<!-- Fixes #
Fixes [#XXX](https://github.com/Ulaakut/cameradar/issues/XXX)
-->
## How did I test it? ## How did I test it?
<!-- A brief description the steps taken to test this pull request. --> <!-- A brief description of the steps taken to test this pull request. -->
+1 -1
View File
@@ -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@v3 uses: docker/login-action@v4
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
+2
View File
@@ -46,6 +46,8 @@ 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$
+69 -1
View File
@@ -14,6 +14,49 @@ Clone the repo and install dependencies using Go modules.
go mod download go mod download
``` ```
### Test against fake targets
Use the following options when you want reproducible local testing.
#### Testing discovery behavior
Use `scanme.nmap.org` to validate discovery-related behavior.
- `scanme.nmap.org` does not expose RTSP or RTSPS ports.
- Target its open ports (for example `22`, `80`, `9929`, `31337`) to test discovery flow, reporting, and scan handling.
Example command:
```bash
cameradar -t scanme.nmap.org -p 22
```
#### Testing RTSP and attack behavior
Use [RTSPAllTheThings](https://github.com/Ullaakut/RTSPAllTheThings) to test RTSP-specific logic and camera attack flows.
- It supports both basic and digest authentication.
- It behaves like a standards-compliant RTSP camera.
> [!CAUTION]
> It is no longer maintained and has limited camera emulation coverage.
Example command:
```bash
docker run --net=host -p 8554:8554 -e RTSP_USERNAME=admin -e RTSP_PASSWORD=12345 -e RTSP_PORT=8554 -e RTSP_AUTHENTICATION_METHOD=digest ullaakut/rtspatt
```
Many real cameras slightly diverge from strict RTSP behavior. For example, some devices allow `DESCRIBE` without authentication, or return `403` and `404` in an order that differs from strict expectations.
Unfortunately, RTSPATT cannot reproduce those behaviors.
#### Prefer real cameras when possible
The most reliable testing method is running against real cameras and real network conditions.
> [!CAUTION]
> Scan only authorized targets and networks.
## Run tests ## Run tests
```bash ```bash
@@ -22,13 +65,37 @@ make test
## Formatting and linting ## Formatting and linting
Run `gofmt` on changed files.
Keep code idiomatic and consistent with existing style. Keep code idiomatic and consistent with existing style.
By default, follow the [Uber Go Style Guide](https://github.com/uber-go/guide) and the guidelines from [Effective Go](https://go.dev/doc/effective_go).
```bash ```bash
make fmt make fmt
``` ```
### Dependency for linting
* golangci-lint
* see current version defined in `.github/workflows/test.yaml` at `jobs.tests.steps.["Run linter"]`
* configured in `.golangci.yml`
```bash
make lint
```
## Commit messages and PR titles
Use [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) for commit messages and pull request titles.
- Use the format: `type: subject`
- Write the subject in imperative mood: `add`, `update`, `remove`, `fix`, `refactor`
- Do not use gerunds in subjects: avoid `adding`, `updating`, `removing`
Examples:
- `feat: add RTSP timeout flag`
- `fix: remove duplicate progress line`
- `docs: update commit message guidelines`
## Reporting issues ## Reporting issues
Use the issue template in [.github/ISSUE_TEMPLATE.md](.github/ISSUE_TEMPLATE.md). Use the issue template in [.github/ISSUE_TEMPLATE.md](.github/ISSUE_TEMPLATE.md).
@@ -43,3 +110,4 @@ Only scan authorized targets.
4. Add or update tests when possible. 4. Add or update tests when possible.
5. Ensure `make test` passes. 5. Ensure `make test` passes.
6. Try to bring as much test coverage as possible with your changes. 6. Try to bring as much test coverage as possible with your changes.
7. Use a Conventional Commit-style PR title with an imperative subject.
+8 -149
View File
@@ -2,7 +2,7 @@
<p align="center"> <p align="center">
<a href="#license"> <a href="#license">
<img src="https://img.shields.io/badge/license-Apache-blue.svg?style=flat" /> <img src="https://img.shields.io/badge/license-MIT-blue.svg?style=flat" />
</a> </a>
<a href="https://hub.docker.com/r/ullaakut/cameradar/"> <a href="https://hub.docker.com/r/ullaakut/cameradar/">
<img src="https://img.shields.io/docker/pulls/ullaakut/cameradar.svg?style=flat" /> <img src="https://img.shields.io/docker/pulls/ullaakut/cameradar.svg?style=flat" />
@@ -47,9 +47,8 @@ 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](#command-line-options) - [Command-line options and environment variables](#command-line-options-and-environment-variables)
- [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)
@@ -75,7 +74,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 [command-line options](#command-line-options). For all options, see [Configuration reference](https://github.com/Ullaakut/cameradar/wiki/Configuration-Reference).
- 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`
@@ -107,7 +106,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 [command-line options](#command-line-options). For available flags, see [Configuration reference](https://github.com/Ullaakut/cameradar/wiki/Configuration-Reference).
## Install on Android (Termux) ## Install on Android (Termux)
@@ -273,117 +272,11 @@ 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.
## Options ## Command-line options and environment variables
### `TARGETS` / `--targets` / `-t` The complete CLI and environment variable reference is maintained in [Configuration reference](https://github.com/Ullaakut/cameradar/wiki/Configuration-Reference).
This variable is required. This includes all supported flags, defaults, accepted values, and env var mapping.
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
@@ -403,41 +296,7 @@ The `cameradar` binary is now in `$GOPATH/bin/cameradar`.
## Frequently asked questions ## Frequently asked questions
> Cameradar does not detect any camera! See [Troubleshooting & FAQ](https://github.com/Ullaakut/cameradar/wiki/Troubleshooting-%26-FAQ)
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
+9 -16
View File
@@ -38,11 +38,10 @@ 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,
@@ -128,19 +127,13 @@ 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,
DefaultCommand: scanCommand.Name, Usage: "Scan targets for RTSP streams",
Flags: flags,
Action: runCameradar,
Commands: []*cli.Command{ Commands: []*cli.Command{
scanCommand,
{ {
Name: "version", Name: "version",
Usage: "Print version information", Usage: "Print version information",
+27 -28
View File
@@ -1,26 +1,26 @@
module github.com/Ullaakut/cameradar/v6 module github.com/Ullaakut/cameradar/v6
go 1.25.3 go 1.25.8
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.3.2 github.com/bluenviron/gortsplib/v5 v5.5.1
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.0 github.com/hamba/cmd/v3 v3.1.1
github.com/stretchr/testify v1.11.1 github.com/stretchr/testify v1.11.1
github.com/urfave/cli/v3 v3.6.2 github.com/urfave/cli/v3 v3.8.0
golang.org/x/term v0.40.0 golang.org/x/term v0.42.0
) )
require ( require (
github.com/VictoriaMetrics/metrics v1.40.1 // indirect github.com/VictoriaMetrics/metrics v1.41.2 // 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.0 // indirect github.com/bluenviron/mediacommon/v2 v2.8.3 // 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,14 +37,15 @@ 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.27.2 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect
github.com/hamba/logger/v2 v2.9.0 // indirect github.com/hamba/logger/v2 v2.9.1 // indirect
github.com/hamba/statter/v2 v2.8.0 // indirect github.com/hamba/statter/v2 v2.8.1 // indirect
github.com/klauspost/compress v1.18.0 // indirect github.com/klauspost/compress v1.18.4 // 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
@@ -53,7 +54,6 @@ 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,22 +71,21 @@ 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.40.0 // indirect go.opentelemetry.io/otel v1.43.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 // indirect
go.opentelemetry.io/otel/exporters/zipkin v1.38.0 // indirect go.opentelemetry.io/otel/metric v1.43.0 // indirect
go.opentelemetry.io/otel/metric v1.40.0 // indirect go.opentelemetry.io/otel/sdk v1.43.0 // indirect
go.opentelemetry.io/otel/sdk v1.40.0 // indirect go.opentelemetry.io/otel/trace v1.43.0 // indirect
go.opentelemetry.io/otel/trace v1.40.0 // indirect go.opentelemetry.io/proto/otlp v1.10.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.50.0 // indirect golang.org/x/net v0.52.0 // indirect
golang.org/x/sys v0.41.0 // indirect golang.org/x/sys v0.43.0 // indirect
golang.org/x/text v0.34.0 // indirect golang.org/x/text v0.35.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250908214217-97024824d090 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect
google.golang.org/grpc v1.75.1 // indirect google.golang.org/grpc v1.80.0 // indirect
google.golang.org/protobuf v1.36.9 // indirect google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )
+64 -66
View File
@@ -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.40.1 h1:FrF5uJRpIVj9fayWcn8xgiI+FYsKGMslzPuOXjdeyR4= github.com/VictoriaMetrics/metrics v1.41.2 h1:pLQ4Mw9TqXFq3ZsZVJkz88JHpjL9LY5NHTY3v2gBNAw=
github.com/VictoriaMetrics/metrics v1.40.1/go.mod h1:XE4uudAAIRaJE614Tl5HMrtoEU6+GDZO4QTnNSsZRuA= github.com/VictoriaMetrics/metrics v1.41.2/go.mod h1:xDM82ULLYCYdFRgQ2JBxi8Uf1+8En1So9YUwlGTOqTc=
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.3.2 h1:eGoOsJzV015A+9xuBPcDYNhqYjogH25zXhMoU1lNeXI= github.com/bluenviron/gortsplib/v5 v5.5.1 h1:jZJb1dLsBMhxW1cFz/CwLhUW5diaqHf/617QyQGx4nY=
github.com/bluenviron/gortsplib/v5 v5.3.2/go.mod h1:x2Pn+7CYoASW4jz8O3Ae1cNTcfOoFMjUCGcafN4qzc8= github.com/bluenviron/gortsplib/v5 v5.5.1/go.mod h1:otcPqR836QZej/EYx7njn8vl4TLj8Ya3QAf+GBwh2cQ=
github.com/bluenviron/mediacommon/v2 v2.8.0 h1:sacjx0Jwdl44awqN5jQhpm7LgVmDKf881hRqL9/fNgQ= github.com/bluenviron/mediacommon/v2 v2.8.3 h1:T6xb7ZK3eBixi/HynzhtGRCEIrazwcmGIeu0WDTVISY=
github.com/bluenviron/mediacommon/v2 v2.8.0/go.mod h1:D63vIFWAgTIo0OLsk9EVKVH4yrs8AKHlNqjzVsBTMwc= github.com/bluenviron/mediacommon/v2 v2.8.3/go.mod h1:CsYjGgzIz8RbloQf4BHR4uReogZsB4PEKWfePVIzJv8=
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,6 +85,8 @@ 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=
@@ -99,19 +101,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.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU= 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/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs= github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c=
github.com/hamba/cmd/v3 v3.1.0 h1:aPartvDscWVC6VrboXC9e/uc0Z5S4ogXqj4yTTyqDmg= github.com/hamba/cmd/v3 v3.1.1 h1:/rJj6bK6ew0zM31I6s2mwtYKSu4BSsd4PxB/dKuwJyA=
github.com/hamba/cmd/v3 v3.1.0/go.mod h1:5kSV/F3sDoN2t4R5Ayb2tRCYfHyVICNW5lUvoFe14FY= github.com/hamba/cmd/v3 v3.1.1/go.mod h1:w1ZhSByZcrL6oB0gkxLeW8wqX+kAbkKf3GiYz/5Kl7I=
github.com/hamba/logger/v2 v2.9.0 h1:gLa4AuoQ17XTBovyIewOK7sALX/sHDJO3kfPUQBUA2o= github.com/hamba/logger/v2 v2.9.1 h1:NRV+6j0SEdGag1DkjWtV/k3JGOFAByx6IEc/nJNpYLs=
github.com/hamba/logger/v2 v2.9.0/go.mod h1:i+ohrYJ5XKaicZAJD+64lsYd3ZqLOjFXzt210lmZ/iQ= github.com/hamba/logger/v2 v2.9.1/go.mod h1:IveSM7xeUVbtmlgXsXoAdNvhQ+JG1CgFMBlKG7hRH/4=
github.com/hamba/statter/v2 v2.8.0 h1:5rLx+e/wODnvtkzpmEQim4hHcWEJbeI+KJuPHTkQCLQ= github.com/hamba/statter/v2 v2.8.1 h1:Y6mEOXPxBLfBvKzb31BjPhtSLyza/ghFu+Kez7t0CaY=
github.com/hamba/statter/v2 v2.8.0/go.mod h1:V3pzf51ZQG5tpVQdbbkoTm3mA5GtxeQ30Yr+GPUa3Is= github.com/hamba/statter/v2 v2.8.1/go.mod h1:DTwNCeix6cqciNDhT8CzzKa5k2nCWPWGjIAru4jRtpA=
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.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
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=
@@ -152,8 +154,6 @@ 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.6.2 h1:lQuqiPrZ1cIz8hz+HcrG0TNZFxU70dPZ3Yl+pSrH9A8= github.com/urfave/cli/v3 v3.8.0 h1:XqKPrm0q4P0q5JpoclYoCAv0/MIvH/jZ2umzuf8pNTI=
github.com/urfave/cli/v3 v3.6.2/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso= github.com/urfave/cli/v3 v3.8.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,60 +216,58 @@ 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.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms= go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g= go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bTWkw0ICGcOLCAI5l6zsD1j20k=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0/go.mod h1:Vl1/iaggsuRlrHf/hfPJPvVag77kKyvrLeD10kpMl+A=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 h1:lwI4Dc5leUqENgGuQImwLo4WnuXFPetmPpkLi2IrX54= 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/go.mod h1:Kz/oCE7z5wuyhPxsXDuaPteSWqjSBD5YaSdbxZYGbGk= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0/go.mod h1:2qXPNBX1OVRC0IwOnfo1ljoid+RD0QK3443EaqVlsOU=
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 h1:3iZJKlCZufyRzPzlQhUIWVmfltrXuGyfjREgGP3UUjc=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0/go.mod h1:kldtb7jDTeol0l3ewcmd8SDvx3EmIE7lyvqbasU3QC4= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0/go.mod h1:/G+nUPfhq2e+qiXMGxMwumDrP5jtzU+mWN7/sjT2rak=
go.opentelemetry.io/otel/exporters/zipkin v1.38.0 h1:0rJ2TmzpHDG+Ib9gPmu3J3cE0zXirumQcKS4wCoZUa0= go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
go.opentelemetry.io/otel/exporters/zipkin v1.38.0/go.mod h1:Su/nq/K5zRjDKKC3Il0xbViE3juWgG3JDoqLumFx5G0= go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g= go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc= go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8= go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE= go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw= go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg= go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw= go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g=
go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk=
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.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
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.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
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.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg= golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY=
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM= golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
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 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA=
google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090/go.mod h1:U8EXRNSd8sUYyDfs/It7KVWodQr+Hf9xtxyxWudSwEw= google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M=
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 h1:m8qni9SQFH0tJc1X0vmnpw/0t+AImlSvp30sEupozUg=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250908214217-97024824d090/go.mod h1:GmFNa4BdJZ2a8G+wCe9Bg3wwThLrJun751XstdJt5Og= google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI= google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM=
google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ= google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4=
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
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=
+31 -39
View File
@@ -291,33 +291,26 @@ 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) {
u, urlStr, err := buildRTSPURL(stream, route, stream.Username, stream.Password) stream.Routes = []string{route}
code, err := a.describeStatus(stream)
if err != nil { if err != nil {
return false, fmt.Errorf("building rtsp url: %w", err) return false, fmt.Errorf("performing describe request at %q: %w", stream, err)
} }
code, err := a.describeStatus(u) a.reporter.Debug(cameradar.StepAttackRoutes, fmt.Sprintf("DESCRIBE %s RTSP/1.0 > %d", stream, code))
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) {
u, urlStr, err := buildRTSPURL(stream, stream.Route(), username, password) stream.Username = username
stream.Password = password
code, err := a.describeStatus(stream)
if err != nil { if err != nil {
return false, fmt.Errorf("building rtsp url: %w", err) return false, fmt.Errorf("performing describe request at %q: %w", stream, err)
} }
code, err := a.describeStatus(u) a.reporter.Debug(cameradar.StepAttackCredentials, fmt.Sprintf("DESCRIBE %s RTSP/1.0 > %d", stream, code))
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
} }
@@ -330,32 +323,27 @@ func (a Attacker) validateStream(ctx context.Context, stream cameradar.Stream, e
return stream, ctx.Err() return stream, ctx.Err()
} }
u, urlStr, err := buildRTSPURL(stream, stream.Route(), stream.Username, stream.Password) client, err := a.newRTSPClient(stream)
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, u, urlStr) desc, res, err := a.describeWithRetry(ctx, client, stream)
if err != nil { if err != nil {
return a.handleDescribeError(stream, urlStr, err) return a.handleDescribeError(stream, err)
} }
a.logDescribeResponse(urlStr, res) a.logDescribeResponse(stream.String(), 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", urlStr) return stream, fmt.Errorf("no media tracks found for %q", stream)
} }
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, urlStr, err) return a.handleSetupError(stream, err)
} }
a.logSetupResponse(urlStr, res) a.logSetupResponse(stream.String(), res)
stream.Available = res != nil && res.StatusCode == base.StatusOK stream.Available = res != nil && res.StatusCode == base.StatusOK
if stream.Available { if stream.Available {
@@ -365,11 +353,15 @@ 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, u *base.URL, urlStr string) (*description.Session, *base.Response, error) { func (a Attacker) describeWithRetry(ctx context.Context, client *gortsplib.Client, stream cameradar.Stream) (*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)
@@ -379,7 +371,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)", urlStr, badStatus.Code)) a.reporter.Debug(cameradar.StepValidateStreams, fmt.Sprintf("DESCRIBE %s RTSP/1.0 > %d (retrying)", stream, badStatus.Code))
select { select {
case <-ctx.Done(): case <-ctx.Done():
return nil, nil, ctx.Err() return nil, nil, ctx.Err()
@@ -391,13 +383,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", urlStr, err) return nil, nil, fmt.Errorf("describe retries exhausted for %q: %w", stream, err)
} }
func (a Attacker) handleDescribeError(stream cameradar.Stream, urlStr string, err error) (cameradar.Stream, error) { func (a Attacker) handleDescribeError(stream cameradar.Stream, 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", urlStr, badStatus.Code)) a.reporter.Debug(cameradar.StepValidateStreams, fmt.Sprintf("DESCRIBE %s RTSP/1.0 > %d", stream, 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,
@@ -407,20 +399,20 @@ func (a Attacker) handleDescribeError(stream cameradar.Stream, urlStr string, er
return stream, nil return stream, nil
} }
a.reporter.Debug(cameradar.StepValidateStreams, fmt.Sprintf("DESCRIBE %s RTSP/1.0 > error: %v", urlStr, err)) a.reporter.Debug(cameradar.StepValidateStreams, fmt.Sprintf("DESCRIBE %s RTSP/1.0 > error: %v", stream, err))
return stream, fmt.Errorf("performing describe request at %q: %w", urlStr, err) return stream, fmt.Errorf("performing describe request at %q: %w", stream, err)
} }
func (a Attacker) handleSetupError(stream cameradar.Stream, urlStr string, err error) (cameradar.Stream, error) { func (a Attacker) handleSetupError(stream cameradar.Stream, 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", urlStr, badStatus.Code)) a.reporter.Debug(cameradar.StepValidateStreams, fmt.Sprintf("SETUP %s RTSP/1.0 > %d", stream, 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", urlStr, err) return stream, fmt.Errorf("performing setup request at %q: %w", stream, err)
} }
func (a Attacker) logDescribeResponse(urlStr string, res *base.Response) { func (a Attacker) logDescribeResponse(urlStr string, res *base.Response) {
+29 -13
View File
@@ -41,28 +41,44 @@ 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, urlStr, err := buildRTSPURL(stream, stream.Route(), "", "") u, err := stream.URL()
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, urlStr) statusCode, headers, err := a.probeDescribeHeaders(ctx, u)
if err != nil { if err != nil {
a.reporter.Debug(cameradar.StepDetectAuth, fmt.Sprintf("DESCRIBE %s RTSP/1.0 > error: %v", urlStr, err)) a.reporter.Debug(cameradar.StepDetectAuth, fmt.Sprintf("DESCRIBE %s RTSP/1.0 > error: %v", u, 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", urlStr, err) return stream, fmt.Errorf("performing describe request at %q: %w", u, err)
} }
a.reporter.Debug(cameradar.StepDetectAuth, fmt.Sprintf("DESCRIBE %s RTSP/1.0 > %d", urlStr, statusCode)) a.reporter.Debug(cameradar.StepDetectAuth, fmt.Sprintf("DESCRIBE %s RTSP/1.0 > %d", u, statusCode))
values := headerValues(headers, "WWW-Authenticate") values := headerValues(headers, "WWW-Authenticate")
switch statusCode { stream.AuthenticationType = authTypeFromStatus(statusCode, values)
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
}
}
+172
View File
@@ -2,7 +2,13 @@ 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"
@@ -78,6 +84,49 @@ 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
@@ -142,6 +191,52 @@ 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()
@@ -193,6 +288,83 @@ 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:
+62 -32
View File
@@ -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,15 +19,46 @@ import (
"github.com/bluenviron/gortsplib/v5/pkg/liberrors" "github.com/bluenviron/gortsplib/v5/pkg/liberrors"
) )
func (a Attacker) newRTSPClient(u *base.URL) (*gortsplib.Client, error) { const (
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
err := client.Start() switch stream.Scheme {
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
} }
@@ -35,8 +66,13 @@ func (a Attacker) newRTSPClient(u *base.URL) (*gortsplib.Client, error) {
return client, nil return client, nil
} }
func (a Attacker) describeStatus(u *base.URL) (base.StatusCode, error) { func (a Attacker) describeStatus(stream cameradar.Stream) (base.StatusCode, error) {
client, err := a.newRTSPClient(u) u, err := stream.URL()
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
} }
@@ -61,9 +97,25 @@ func (a Attacker) describeStatus(u *base.URL) (base.StatusCode, error) {
// //
// 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, urlStr string) (base.StatusCode, base.Header, error) { func (a Attacker) probeDescribeHeaders(ctx context.Context, u *base.URL) (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
} }
@@ -81,7 +133,7 @@ func (a Attacker) probeDescribeHeaders(ctx context.Context, u *base.URL, urlStr
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",
urlStr, u,
u.Host, u.Host,
) )
_, err = conn.Write([]byte(request)) _, err = conn.Write([]byte(request))
@@ -163,25 +215,3 @@ 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
}
+117 -39
View File
@@ -2,85 +2,163 @@ 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 TestBuildRTSPURL(t *testing.T) { func TestStreamURL(t *testing.T) {
stream := cameradar.Stream{
Address: netip.MustParseAddr("192.168.0.10"),
Port: 554,
}
tests := []struct { tests := []struct {
name string name string
route string stream cameradar.Stream
username string wantURL string
password string wantParsedScheme 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",
route: "/", stream: cameradar.Stream{
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",
route: "////", stream: cameradar.Stream{
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",
route: "stream", stream: cameradar.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",
route: "/stream", stream: cameradar.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",
route: "stream/", stream: cameradar.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",
route: " /stream ", stream: cameradar.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",
route: "stream", stream: cameradar.Stream{
username: "admin", Address: netip.MustParseAddr("192.168.0.10"),
password: "admin123", Port: 554,
wantURL: "rtsp://admin:admin123@192.168.0.10:554/stream", Routes: []string{"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",
route: "stream", stream: cameradar.Stream{
password: "pass", Address: netip.MustParseAddr("192.168.0.10"),
wantURL: "rtsp://:pass@192.168.0.10:554/stream", Port: 554,
Routes: []string{"stream"},
Password: "pass",
},
wantURL: "rtsp://:pass@192.168.0.10:554/stream",
}, },
{ {
name: "username only", name: "username only",
route: "stream", stream: cameradar.Stream{
username: "user", Address: netip.MustParseAddr("192.168.0.10"),
wantURL: "rtsp://user:@192.168.0.10:554/stream", Port: 554,
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, err := buildRTSPURL(stream, test.route, test.username, test.password) gotURL := test.stream.String()
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)
}) })
} }
} }
+4
View File
@@ -7,6 +7,7 @@ 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"
) )
@@ -83,9 +84,12 @@ 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,
}) })
} }
} }
+25
View File
@@ -63,6 +63,31 @@ 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"),
+19 -1
View File
@@ -7,6 +7,7 @@ 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"
) )
@@ -67,7 +68,8 @@ func runScan(ctx context.Context, nmap Runner, reporter Reporter) ([]cameradar.S
continue continue
} }
if !strings.Contains(port.Service.Name, "rtsp") { isCandidate := streamCandidate(port.Service.Name, port.ID)
if !isCandidate {
continue continue
} }
@@ -78,10 +80,12 @@ 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,
}) })
} }
} }
@@ -104,3 +108,17 @@ 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
}
+49 -1
View File
@@ -44,8 +44,56 @@ 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 1 RTSP streams", wantProgress: "Found 2 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",
+11 -5
View File
@@ -88,11 +88,9 @@ func formatStream(stream cameradar.Stream) string {
builder.WriteString(" Routes: not found\n") builder.WriteString(" Routes: not found\n")
} }
if stream.CredentialsFound { if stream.CredentialsFound || stream.AuthenticationType == cameradar.AuthNone {
builder.WriteString(" Credentials: ") builder.WriteString(" Credentials: ")
builder.WriteString(stream.Username) builder.WriteString(formatCredentials(stream))
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")
@@ -105,7 +103,7 @@ func formatStream(stream cameradar.Stream) string {
builder.WriteString("no\n") builder.WriteString("no\n")
} }
if stream.RouteFound && stream.CredentialsFound { if stream.RouteFound && (stream.CredentialsFound || stream.AuthenticationType == cameradar.AuthNone) {
builder.WriteString(" RTSP URL: ") builder.WriteString(" RTSP URL: ")
builder.WriteString(formatRTSPURL(stream)) builder.WriteString(formatRTSPURL(stream))
builder.WriteString("\n") builder.WriteString("\n")
@@ -133,6 +131,14 @@ 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:
+24 -1
View File
@@ -73,18 +73,41 @@ 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
View File
@@ -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 = fmt.Sprintf("%s:%s", stream.Username, stream.Password) credentials = formatCredentials(stream)
} }
available := emptyEntry available := emptyEntry
+25
View File
@@ -0,0 +1,25 @@
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 ""
}
+55 -4
View File
@@ -1,7 +1,13 @@
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.
@@ -15,7 +21,7 @@ const (
AuthDigest AuthDigest
) )
// Stream represents a camera's RTSP stream. // Stream represents a camera's stream, typically accessed over RTSP/RTSPS.
type Stream struct { type Stream struct {
Device string `json:"device"` Device string `json:"device"`
Username string `json:"username"` Username string `json:"username"`
@@ -24,13 +30,33 @@ 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 {
@@ -38,3 +64,28 @@ 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())
}