Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c37d584aa2 | |||
| fd0d948c16 | |||
| 7bd7460b5b | |||
| 69f4fb418a | |||
| 18ffb7af61 | |||
| c11e3217ea | |||
| d16443109a | |||
| f93f9c9780 | |||
| 55d11e2887 |
@@ -1,57 +0,0 @@
|
||||
Before filing, search open and closed issues and check the FAQ in the README.
|
||||
If this is a security issue, do not post details in a public issue.
|
||||
Do not include IPs or any other information that can identify a vulnerable network in your issue.
|
||||
|
||||
## Issue type
|
||||
|
||||
- [ ] Bug report
|
||||
- [ ] Feature request
|
||||
- [ ] Documentation issue
|
||||
- [ ] Question
|
||||
|
||||
## Context
|
||||
|
||||
### Install method
|
||||
|
||||
- [ ] Docker image `ullaakut/cameradar`
|
||||
- [ ] Custom Docker build
|
||||
- [ ] Pre-compiled binary
|
||||
- [ ] Custom binary build
|
||||
- [ ] Not sure
|
||||
|
||||
### Version
|
||||
|
||||
- [ ] Release tag: <tag>
|
||||
- [ ] Latest commit on `master`
|
||||
- [ ] Fork: <fork URL>
|
||||
- [ ] Commit: <hash>
|
||||
|
||||
## Environment
|
||||
|
||||
- OS: <Windows | macOS | Linux | Other>
|
||||
- OS version: <version>
|
||||
- Architecture: <arch>
|
||||
|
||||
## Description
|
||||
|
||||
### Expected behavior
|
||||
|
||||
<expected behavior>
|
||||
|
||||
### Actual behavior
|
||||
|
||||
<actual behavior>
|
||||
|
||||
### Steps to reproduce
|
||||
|
||||
1. <step>
|
||||
2. <step>
|
||||
3. <step>
|
||||
|
||||
### Logs
|
||||
|
||||
If this is a CLI or Docker issue, run with debug logs and paste output.
|
||||
|
||||
```text
|
||||
<logs>
|
||||
```
|
||||
@@ -0,0 +1,103 @@
|
||||
name: Bug report
|
||||
description: Create a report to help Cameradar improve
|
||||
labels:
|
||||
- needs-triage
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Please make sure your problem is not already addressed in another issue.
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Description
|
||||
description: Please give a clear and concise description of the bug.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: version
|
||||
attributes:
|
||||
label: Cameradar version
|
||||
description: Output of `cameradar version`
|
||||
render: bash
|
||||
placeholder: |
|
||||
Version: v6.0.2-SNAPSHOT-c11e321
|
||||
Commit: c11e3217ea0b1ea9e45d0da4c072e07775bde68c
|
||||
Build date: 2026-02-03T10:02:30Z
|
||||
Nmap: 7.94SVN
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: env
|
||||
attributes:
|
||||
label: Environment
|
||||
description: How do you run cameradar?
|
||||
options:
|
||||
- "`ullaakut/cameradar` docker image"
|
||||
- Precompiled binary from GitHub releases
|
||||
- Custom docker image
|
||||
- Custom binary build
|
||||
default: 0
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: os
|
||||
attributes:
|
||||
label: Operating system
|
||||
description: Operating system where you run cameradar.
|
||||
render: bash
|
||||
placeholder: |
|
||||
- OS: <Windows | macOS | Linux | Other>
|
||||
- OS version: <version>
|
||||
- Architecture: <arch>
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
id: cmd
|
||||
attributes:
|
||||
label: Command
|
||||
description: The command that you ran and all of its arguments. Make sure to redact any sensitive information. Make sure to run your command in debug mode.
|
||||
placeholder: |
|
||||
E.g. `docker run --net=host -it ullaakut/cameradar -t localhost --debug`
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: output
|
||||
attributes:
|
||||
label: Output logs
|
||||
description: Output of the command you ran, including any error messages. Make sure to redact any sensitive information.
|
||||
placeholder: |
|
||||
2026-02-03T09:33:24Z [INFO] Startup: Running cameradar version 6.0.2-SNAPSHOT-75bf524, commit 75bf524
|
||||
2026-02-03T09:33:24Z [INFO] Startup: targets: localhost
|
||||
2026-02-03T09:33:24Z [INFO] Startup: ports: 554, 5554, 8554, http
|
||||
...
|
||||
Accessible streams: 1
|
||||
• 127.0.0.1:8554 (GStreamer rtspd)
|
||||
Authentication: digest
|
||||
Routes: live.sdp
|
||||
Credentials: admin:12345
|
||||
Availability: yes
|
||||
RTSP URL: rtsp://admin:12345@127.0.0.1:8554/live.sdp
|
||||
Admin panel: http://127.0.0.1/
|
||||
- type: textarea
|
||||
id: expected
|
||||
attributes:
|
||||
label: Expected behavior
|
||||
description: What is the expected behavior?
|
||||
placeholder: |
|
||||
E.g. "Cameradar should have been able to find the camera's RTSP stream using the provided credentials."
|
||||
- type: textarea
|
||||
id: additional
|
||||
attributes:
|
||||
label: Additional Info
|
||||
description: Additional info you want to provide such as system info, target info, network conditions etc.
|
||||
validations:
|
||||
required: false
|
||||
- type: checkboxes
|
||||
id: terms
|
||||
attributes:
|
||||
label: Code of Conduct
|
||||
description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/Ullaakut/cameradar/blob/master/CODE_OF_CONDUCT.md).
|
||||
options:
|
||||
- label: I agree to follow this project's Code of Conduct
|
||||
required: true
|
||||
@@ -0,0 +1,5 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Cameradar Community discussion board
|
||||
url: https://github.com/Ullaakut/cameradar/discussions
|
||||
about: Please ask and answer questions here.
|
||||
@@ -0,0 +1,31 @@
|
||||
name: Feature request
|
||||
description: Propose a feature or enhancement to help Cameradar improve
|
||||
labels:
|
||||
- needs-triage
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Please make sure your request is not already proposed in another issue.
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Description
|
||||
description: Please give a clear and concise description of the feature request.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: additional
|
||||
attributes:
|
||||
label: Additional Info
|
||||
description: Additional info you want to provide.
|
||||
validations:
|
||||
required: false
|
||||
- type: checkboxes
|
||||
id: terms
|
||||
attributes:
|
||||
label: Code of Conduct
|
||||
description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/Ullaakut/cameradar/blob/master/CODE_OF_CONDUCT.md).
|
||||
options:
|
||||
- label: I agree to follow this project's Code of Conduct
|
||||
required: true
|
||||
@@ -208,34 +208,6 @@ These instructions are based on:
|
||||
- Handle errors at the appropriate level
|
||||
- Consider using structured errors for better debugging
|
||||
|
||||
## API Design
|
||||
|
||||
### HTTP Handlers
|
||||
|
||||
- Use `http.HandlerFunc` for simple handlers
|
||||
- Implement `http.Handler` for handlers that need state
|
||||
- Use middleware for cross-cutting concerns
|
||||
- Set appropriate status codes and headers
|
||||
- Handle errors gracefully and return appropriate error responses
|
||||
- Use `github.com/go-chi/chi/v5` for its `mux` with pattern-based routing and method matching
|
||||
|
||||
### JSON APIs
|
||||
|
||||
- Use struct tags to control JSON marshaling
|
||||
- Validate input data
|
||||
- Use pointers for optional fields
|
||||
- Consider using `json.RawMessage` for delayed parsing
|
||||
- Handle JSON errors appropriately
|
||||
|
||||
### HTTP Clients
|
||||
|
||||
- Keep the client struct focused on configuration and dependencies only (e.g., base URL, `*http.Client`, auth, default headers). It must not store per-request state
|
||||
- Do not store or cache `*http.Request` inside the client struct, and do not persist request-specific state across calls; instead, construct a fresh request per method invocation
|
||||
- Methods should accept `context.Context` and input parameters, assemble the `*http.Request` locally (or via a short-lived builder/helper created per call), then call `c.httpClient.Do(req)`
|
||||
- If request-building logic is reused, factor it into unexported helper functions or a per-call builder type; never keep `http.Request` (URL params, body, headers) as fields on the long-lived client
|
||||
- Ensure the underlying `*http.Client` is configured (timeouts, transport) and is safe for concurrent use; avoid mutating `Transport` after first use
|
||||
- Always set headers on the request instance you’re sending, and close response bodies (`defer resp.Body.Close()`), handling errors appropriately
|
||||
|
||||
## Performance Optimization
|
||||
|
||||
### Memory Management
|
||||
@@ -350,18 +322,15 @@ These instructions are based on:
|
||||
|
||||
### Essential Tools
|
||||
|
||||
- `go fmt`: Format code
|
||||
- `go vet`: Find suspicious constructs
|
||||
- `golangci-lint`: Additional linting
|
||||
- `go test`: Run tests
|
||||
- `make fmt`: Format code
|
||||
- `make lint`: Additional linting
|
||||
- `make test`: Run tests
|
||||
- `go mod`: Manage dependencies
|
||||
- `go generate`: Code generation
|
||||
|
||||
### Development Practices
|
||||
|
||||
- Run tests before committing
|
||||
- Run linter before committing
|
||||
- Run `make sqlc`, `make openapi-gen` and `make readme-gen` before committing if you touched related files
|
||||
- Run tests before committing (`make test`)
|
||||
- Run linter before committing (`make lint`)
|
||||
- Keep commits focused and atomic
|
||||
- Write meaningful commit messages
|
||||
- Review diffs before committing
|
||||
|
||||
@@ -13,13 +13,13 @@ jobs:
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install Go
|
||||
id: install-go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: 'go.mod'
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ jobs:
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
# Go Test looks at `mtime` for caching. `git clone` messes with this. Set it consistently to last commit time.
|
||||
- name: Restore file modification time
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
|
||||
- name: Install Go
|
||||
id: install-go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: 'go.mod'
|
||||
cache-dependency-path: |
|
||||
@@ -40,7 +40,7 @@ jobs:
|
||||
if: steps.install-go.outputs.cache-hit != 'true'
|
||||
|
||||
- name: Run Linter
|
||||
uses: golangci/golangci-lint-action@v8
|
||||
uses: golangci/golangci-lint-action@v9
|
||||
with:
|
||||
version: v2.7.2
|
||||
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
nationality, personal appearance, race, religion, or sexual identity
|
||||
and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||
diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
* Demonstrating empathy and kindness toward other people
|
||||
* Being respectful of differing opinions, viewpoints, and experiences
|
||||
* Giving and gracefully accepting constructive feedback
|
||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
* Focusing on what is best not just for us as individuals, but for the
|
||||
overall community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
* The use of sexualized language or imagery, and sexual attention or
|
||||
advances of any kind
|
||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or email
|
||||
address, without their explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Community leaders are responsible for clarifying and enforcing our standards of
|
||||
acceptable behavior and will take appropriate and fair corrective action in
|
||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||
or harmful.
|
||||
|
||||
Community leaders have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||
decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces, and also applies when
|
||||
an individual is officially representing the community in public spaces.
|
||||
Examples of representing our community include using an official e-mail address,
|
||||
posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the community leaders responsible for enforcement at
|
||||
`contact+cameradar@glaulabs.com`.
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in determining
|
||||
the consequences for any action they deem in violation of this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||
unprofessional or unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from community leaders, providing
|
||||
clarity around the nature of the violation and an explanation of why the
|
||||
behavior was inappropriate. A public apology may be requested.
|
||||
|
||||
### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series
|
||||
of actions.
|
||||
|
||||
**Consequence**: A warning with consequences for continued behavior. No
|
||||
interaction with the people involved, including unsolicited interaction with
|
||||
those enforcing the Code of Conduct, for a specified period of time. This
|
||||
includes avoiding interactions in community spaces as well as external channels
|
||||
like social media. Violating these terms may lead to a temporary or
|
||||
permanent ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
**Community Impact**: A serious violation of community standards, including
|
||||
sustained inappropriate behavior.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or public
|
||||
communication with the community for a specified period of time. No public or
|
||||
private interaction with the people involved, including unsolicited interaction
|
||||
with those enforcing the Code of Conduct, is allowed during this period.
|
||||
Violating these terms may lead to a permanent ban.
|
||||
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within
|
||||
the community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||
version 2.0, available at
|
||||
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
||||
|
||||
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
||||
enforcement ladder](https://github.com/mozilla/diversity).
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
https://www.contributor-covenant.org/faq. Translations are available at
|
||||
https://www.contributor-covenant.org/translations.
|
||||
+4
-1
@@ -2,7 +2,10 @@ FROM alpine
|
||||
|
||||
RUN apk --update add --no-cache nmap \
|
||||
nmap-nselibs \
|
||||
nmap-scripts
|
||||
nmap-scripts \
|
||||
masscan \
|
||||
libpcap \
|
||||
libpcap-dev
|
||||
|
||||
WORKDIR /app/cameradar
|
||||
|
||||
|
||||
@@ -189,12 +189,12 @@ docker run --rm -t --net=host \
|
||||
### Skip discovery with `--skip-scan`
|
||||
|
||||
If you already know the RTSP endpoints, you can skip discovery and treat each
|
||||
target and port as a stream candidate. This mode does not run nmap and can be
|
||||
target and port as a stream candidate. This mode does not run discovery and can be
|
||||
useful on restricted networks or when you want to attack a known inventory.
|
||||
|
||||
Skipping discovery means:
|
||||
|
||||
- Cameradar does not run nmap and does not detect device models.
|
||||
- Cameradar does not run discovery and does not detect device models.
|
||||
- Targets resolve to IP addresses. Hostnames resolve via DNS.
|
||||
- CIDR blocks and IPv4 ranges expand to every address in the range.
|
||||
- Large ranges create many targets, so use them carefully.
|
||||
@@ -212,6 +212,30 @@ docker run --rm -t --net=host \
|
||||
In this example, Cameradar attempts dictionary attacks against
|
||||
ports 554 and 8554 of `192.168.1.10`.
|
||||
|
||||
### Choose the discovery scanner with `--scanner`
|
||||
|
||||
Cameradar supports two discovery backends:
|
||||
|
||||
- `nmap` (default)
|
||||
- `masscan`
|
||||
|
||||
Use `nmap` when you want more reliable RTSP discovery: it performs service
|
||||
identification and can better distinguish RTSP from other open ports.
|
||||
|
||||
Use `masscan` when scanning very large networks: it is generally faster and
|
||||
more efficient at scale, but it does not provide service discovery.
|
||||
|
||||
```bash
|
||||
docker run --rm -t --net=host \
|
||||
ullaakut/cameradar \
|
||||
--scanner masscan \
|
||||
--ports "554,8554" \
|
||||
--targets 192.168.1.0/24
|
||||
```
|
||||
|
||||
> [!WARNING]
|
||||
> `--scan-speed` only applies to the `nmap` scanner.
|
||||
|
||||
## Security and responsible use
|
||||
|
||||
Cameradar is a penetration testing tool.
|
||||
@@ -287,12 +311,27 @@ 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`
|
||||
@@ -324,7 +363,7 @@ Default value: `2000ms`
|
||||
|
||||
This optional variable enables more verbose output.
|
||||
|
||||
It outputs nmap results, cURL requests, and more.
|
||||
It outputs discovery results (`nmap` or `masscan`), cURL requests, and more.
|
||||
|
||||
Default: `false`
|
||||
|
||||
@@ -414,6 +453,10 @@ Cameradar supports both basic and digest authentication.
|
||||
|
||||
`docker run --rm -t --net=host -v /tmp:/tmp ullaakut/cameradar --targets 192.168.0.0/24 --custom-credentials "/tmp/dictionaries/credentials.json" --custom-routes "/tmp/dictionaries/routes" --ports 554,5554,8554`
|
||||
|
||||
> Running cameradar with masscan discovery
|
||||
|
||||
`docker run --rm -t --net=host ullaakut/cameradar --scanner masscan --targets 192.168.0.0/24 --ports 554,8554`
|
||||
|
||||
## License
|
||||
|
||||
Copyright 2026 Ullaakut
|
||||
|
||||
@@ -79,6 +79,7 @@ func runCameradar(ctx context.Context, cmd *cli.Command) error {
|
||||
routesPath,
|
||||
credsPath,
|
||||
outputPath,
|
||||
cmd.String(flagScanner),
|
||||
cmd.Int16(flagScanSpeed),
|
||||
cmd.Duration(flagAttackInterval),
|
||||
cmd.Duration(flagTimeout),
|
||||
@@ -97,6 +98,7 @@ func runCameradar(ctx context.Context, cmd *cli.Command) error {
|
||||
Targets: targets,
|
||||
Ports: ports,
|
||||
ScanSpeed: cmd.Int16(flagScanSpeed),
|
||||
Scanner: cmd.String(flagScanner),
|
||||
}
|
||||
var scanner cameradar.StreamScanner
|
||||
scanner, err = scan.New(config, reporter)
|
||||
@@ -141,6 +143,7 @@ func buildStartupOptions(
|
||||
routesPath string,
|
||||
credsPath string,
|
||||
outputPath string,
|
||||
scanner string,
|
||||
scanSpeed int16,
|
||||
attackInterval time.Duration,
|
||||
timeout time.Duration,
|
||||
@@ -153,6 +156,7 @@ func buildStartupOptions(
|
||||
"ports: " + strings.Join(ports, ", "),
|
||||
"custom-routes: " + fallbackValue(routesPath, "builtin"),
|
||||
"custom-credentials: " + fallbackValue(credsPath, "builtin"),
|
||||
"scanner: " + fallbackValue(scanner, "nmap"),
|
||||
"scan-speed: " + strconv.FormatInt(int64(scanSpeed), 10),
|
||||
"skip-scan: " + strconv.FormatBool(skipScan),
|
||||
"attack-interval: " + attackInterval.String(),
|
||||
|
||||
+25
-4
@@ -20,6 +20,7 @@ const (
|
||||
flagPorts = "ports"
|
||||
flagCustomRoutes = "custom-routes"
|
||||
flagCustomCredentials = "custom-credentials"
|
||||
flagScanner = "scanner"
|
||||
flagScanSpeed = "scan-speed"
|
||||
flagAttackInterval = "attack-interval"
|
||||
flagTimeout = "timeout"
|
||||
@@ -62,6 +63,12 @@ var flags = cmd.Flags{
|
||||
Aliases: []string{"c"},
|
||||
Sources: cli.EnvVars(strcase.ToSNAKE(flagCustomCredentials)),
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: flagScanner,
|
||||
Usage: "Discovery scanner backend: nmap or masscan",
|
||||
Sources: cli.EnvVars(strcase.ToSNAKE(flagScanner)),
|
||||
Value: "nmap",
|
||||
},
|
||||
&cli.Int16Flag{
|
||||
Name: flagScanSpeed,
|
||||
Usage: "The nmap speed preset to use for scanning (lower is stealthier)",
|
||||
@@ -121,11 +128,25 @@ func realMain() (code int) {
|
||||
}
|
||||
}()
|
||||
|
||||
scanCommand := &cli.Command{
|
||||
Name: "scan",
|
||||
Usage: "Scan targets for RTSP streams",
|
||||
Flags: flags,
|
||||
Action: runCameradar,
|
||||
}
|
||||
|
||||
app := &cli.Command{
|
||||
Name: "Cameradar",
|
||||
Version: version,
|
||||
Flags: flags,
|
||||
Action: runCameradar,
|
||||
Name: "Cameradar",
|
||||
Version: version,
|
||||
DefaultCommand: scanCommand.Name,
|
||||
Commands: []*cli.Command{
|
||||
scanCommand,
|
||||
{
|
||||
Name: "version",
|
||||
Usage: "Print version information",
|
||||
Action: printVersion,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/Ullaakut/cameradar/v6/internal/ui"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
func printVersion(ctx context.Context, _ *cli.Command) error {
|
||||
buildInfo := ui.BuildInfo{Version: version, Commit: commit, Date: date}
|
||||
nmapVersion := getNmapVersion(ctx)
|
||||
_, err := fmt.Fprintf(
|
||||
os.Stdout,
|
||||
"Version:\t%s\nCommit:\t\t%s\nBuild date:\t%s\nNmap:\t\t%s\n",
|
||||
buildInfo.DisplayVersion(),
|
||||
buildInfo.ShortCommit(),
|
||||
buildInfo.Date,
|
||||
nmapVersion,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
const unknownVersion = "unknown"
|
||||
|
||||
func getNmapVersion(ctx context.Context) string {
|
||||
output, err := exec.CommandContext(ctx, "nmap", "--version").Output()
|
||||
if err != nil {
|
||||
return unknownVersion
|
||||
}
|
||||
|
||||
lines := strings.SplitN(string(output), "\n", 2)
|
||||
firstLine := strings.TrimSpace(lines[0])
|
||||
const prefix = "Nmap version "
|
||||
if !strings.HasPrefix(firstLine, prefix) {
|
||||
return unknownVersion
|
||||
}
|
||||
|
||||
versionPart := strings.TrimSpace(strings.TrimPrefix(firstLine, prefix))
|
||||
fields := strings.Fields(versionPart)
|
||||
if len(fields) == 0 {
|
||||
return unknownVersion
|
||||
}
|
||||
return fields[0]
|
||||
}
|
||||
@@ -1,27 +1,28 @@
|
||||
module github.com/Ullaakut/cameradar/v6
|
||||
|
||||
go 1.25.0
|
||||
go 1.25.3
|
||||
|
||||
require (
|
||||
github.com/Ullaakut/nmap/v4 v4.0.0-20260127164606-833e3208bd52
|
||||
github.com/bluenviron/gortsplib/v5 v5.2.2
|
||||
github.com/Ullaakut/masscan v1.0.0
|
||||
github.com/Ullaakut/nmap/v4 v4.0.0
|
||||
github.com/bluenviron/gortsplib/v5 v5.3.0
|
||||
github.com/charmbracelet/bubbles v0.21.0
|
||||
github.com/charmbracelet/bubbletea v1.3.10
|
||||
github.com/charmbracelet/lipgloss v1.1.0
|
||||
github.com/ettle/strcase v0.2.0
|
||||
github.com/hamba/cmd/v3 v3.0.0
|
||||
github.com/hamba/cmd/v3 v3.1.0
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/urfave/cli/v3 v3.3.8
|
||||
github.com/urfave/cli/v3 v3.4.1
|
||||
golang.org/x/term v0.39.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/VictoriaMetrics/metrics v1.38.0 // indirect
|
||||
github.com/VictoriaMetrics/metrics v1.40.1 // indirect
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/bluenviron/mediacommon/v2 v2.6.0 // indirect
|
||||
github.com/bluenviron/mediacommon/v2 v2.7.0 // indirect
|
||||
github.com/cactus/go-statsd-client/v5 v5.1.0 // indirect
|
||||
github.com/cenkalti/backoff/v5 v5.0.2 // indirect
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
|
||||
github.com/charmbracelet/harmonica v0.2.0 // indirect
|
||||
@@ -35,11 +36,11 @@ require (
|
||||
github.com/go-stack/stack v1.8.1 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.3 // indirect
|
||||
github.com/grafana/pyroscope-go v1.2.2 // indirect
|
||||
github.com/grafana/pyroscope-go/godeltaprof v0.1.8 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 // indirect
|
||||
github.com/hamba/logger/v2 v2.8.0 // indirect
|
||||
github.com/hamba/statter/v2 v2.7.0 // indirect
|
||||
github.com/grafana/pyroscope-go v1.2.7 // indirect
|
||||
github.com/grafana/pyroscope-go/godeltaprof v0.1.9 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect
|
||||
github.com/hamba/logger/v2 v2.9.0 // indirect
|
||||
github.com/hamba/statter/v2 v2.8.0 // indirect
|
||||
github.com/klauspost/compress v1.18.0 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
@@ -53,35 +54,36 @@ require (
|
||||
github.com/pion/logging v0.2.4 // indirect
|
||||
github.com/pion/randutil v0.1.0 // indirect
|
||||
github.com/pion/rtcp v1.2.16 // indirect
|
||||
github.com/pion/rtp v1.9.0 // indirect
|
||||
github.com/pion/rtp v1.10.0 // indirect
|
||||
github.com/pion/sdp/v3 v3.0.17 // indirect
|
||||
github.com/pion/srtp/v3 v3.0.9 // indirect
|
||||
github.com/pion/transport/v3 v3.1.1 // indirect
|
||||
github.com/pion/srtp/v3 v3.0.10 // indirect
|
||||
github.com/pion/transport/v4 v4.0.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/client_golang v1.22.0 // indirect
|
||||
github.com/prometheus/client_golang v1.23.2 // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/prometheus/common v0.65.0 // indirect
|
||||
github.com/prometheus/common v0.66.1 // indirect
|
||||
github.com/prometheus/procfs v0.17.0 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/valyala/fastrand v1.1.0 // indirect
|
||||
github.com/valyala/histogram v1.2.0 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.opentelemetry.io/otel v1.37.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.37.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/zipkin v1.37.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.37.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.37.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.37.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.7.0 // indirect
|
||||
golang.org/x/net v0.48.0 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/otel v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/zipkin v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.38.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.8.0 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.2 // indirect
|
||||
golang.org/x/net v0.49.0 // indirect
|
||||
golang.org/x/sys v0.40.0 // indirect
|
||||
golang.org/x/text v0.32.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect
|
||||
google.golang.org/grpc v1.73.0 // indirect
|
||||
google.golang.org/protobuf v1.36.6 // indirect
|
||||
golang.org/x/text v0.33.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250908214217-97024824d090 // indirect
|
||||
google.golang.org/grpc v1.75.1 // indirect
|
||||
google.golang.org/protobuf v1.36.9 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
@@ -6,26 +6,28 @@ github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migc
|
||||
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
|
||||
github.com/Microsoft/hcsshim v0.11.4 h1:68vKo2VN8DE9AdN4tnkWnmdhqdbpUFM8OF3Airm7fz8=
|
||||
github.com/Microsoft/hcsshim v0.11.4/go.mod h1:smjE4dvqPX9Zldna+t5FG3rnoHhaB7QYxPRqGcpAD9w=
|
||||
github.com/Ullaakut/nmap/v4 v4.0.0-20260127164606-833e3208bd52 h1:7o/BZmbn5jJvwBoQqHxLe+UHBz1DD8yx5oWdjOJC76Q=
|
||||
github.com/Ullaakut/nmap/v4 v4.0.0-20260127164606-833e3208bd52/go.mod h1:B+MtOtHdb+jR9bc11BNwZX1QVHOtsDjfKkXMCZtRzbw=
|
||||
github.com/VictoriaMetrics/metrics v1.38.0 h1:1d0dRgVH8Nnu8dKMfisKefPC3q7gqf3/odyO0quAvyA=
|
||||
github.com/VictoriaMetrics/metrics v1.38.0/go.mod h1:r7hveu6xMdUACXvB8TYdAj8WEsKzWB0EkpJN+RDtOf8=
|
||||
github.com/Ullaakut/masscan v1.0.0 h1:+YtpxNcIEaB2lMWNy+oDZF+5pP86S7vSzCKMjW6UDDA=
|
||||
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/go.mod h1:B+MtOtHdb+jR9bc11BNwZX1QVHOtsDjfKkXMCZtRzbw=
|
||||
github.com/VictoriaMetrics/metrics v1.40.1 h1:FrF5uJRpIVj9fayWcn8xgiI+FYsKGMslzPuOXjdeyR4=
|
||||
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/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||
github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8=
|
||||
github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA=
|
||||
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/bluenviron/gortsplib/v5 v5.2.2 h1:5q2viB8PGxWOSXNhVvj8buyr1wighLbHqRZ0U7MLM3o=
|
||||
github.com/bluenviron/gortsplib/v5 v5.2.2/go.mod h1:xkVBOAnR4fzaerPN650CBb7N+zUUsj7PI2HiY1TP7Co=
|
||||
github.com/bluenviron/mediacommon/v2 v2.6.0 h1:wZAPXwv7V78Cx2x7cToYIHOLToHl6APcvHbdQT+gOkg=
|
||||
github.com/bluenviron/mediacommon/v2 v2.6.0/go.mod h1:5V15TiOfeaNVmZPVuOqAwqQSWyvMV86/dijDKu5q9Zs=
|
||||
github.com/bluenviron/gortsplib/v5 v5.3.0 h1:uVuCjYTiSnru9ZNF9+DdNjtQgL1Mv6TKlXjdcz/U5ic=
|
||||
github.com/bluenviron/gortsplib/v5 v5.3.0/go.mod h1:0005vOF5SUy6uKqOD+vp11nDYi1y3AzM+ood9DBzCbM=
|
||||
github.com/bluenviron/mediacommon/v2 v2.7.0 h1:XPj8UQu8iZuytwaeiQvqyDrBmo7VdV2+/ND5zPdgbCY=
|
||||
github.com/bluenviron/mediacommon/v2 v2.7.0/go.mod h1:5V15TiOfeaNVmZPVuOqAwqQSWyvMV86/dijDKu5q9Zs=
|
||||
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/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/cenkalti/backoff/v5 v5.0.2 h1:rIfFVxEf1QsI7E1ZHfp/B4DF/6QBAUhmgkxc0H7Zss8=
|
||||
github.com/cenkalti/backoff/v5 v5.0.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
||||
github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
|
||||
github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
|
||||
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs=
|
||||
@@ -87,18 +89,18 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/grafana/pyroscope-go v1.2.2 h1:uvKCyZMD724RkaCEMrSTC38Yn7AnFe8S2wiAIYdDPCE=
|
||||
github.com/grafana/pyroscope-go v1.2.2/go.mod h1:zzT9QXQAp2Iz2ZdS216UiV8y9uXJYQiGE1q8v1FyhqU=
|
||||
github.com/grafana/pyroscope-go/godeltaprof v0.1.8 h1:iwOtYXeeVSAeYefJNaxDytgjKtUuKQbJqgAIjlnicKg=
|
||||
github.com/grafana/pyroscope-go/godeltaprof v0.1.8/go.mod h1:2+l7K7twW49Ct4wFluZD3tZ6e0SjanjcUUBPVD/UuGU=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 h1:X5VWvz21y3gzm9Nw/kaUeku/1+uBhcekkmy4IkffJww=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90=
|
||||
github.com/hamba/cmd/v3 v3.0.0 h1:YBMRgCCLajyHO68mEM0m5GLTUYDDwosTVp76+eDvsPE=
|
||||
github.com/hamba/cmd/v3 v3.0.0/go.mod h1:66LglrgdSkqPXhnxXKzDNXHkXsHYo0qiJnravEBmHII=
|
||||
github.com/hamba/logger/v2 v2.8.0 h1:0JJnEhVW4sHGn4/9fPP0LsZXD2ytG+NrnrXCdM8/vmg=
|
||||
github.com/hamba/logger/v2 v2.8.0/go.mod h1:V58KZPAmDEWi14dOZjbKDPFkdyvpGwxXtLzLkVTNBic=
|
||||
github.com/hamba/statter/v2 v2.7.0 h1:9CnjJ5PcxOzIVJSAFSJm0lnUUBjTo3psV9nn+yZ1cMM=
|
||||
github.com/hamba/statter/v2 v2.7.0/go.mod h1:SJPj0HCM+z7GxnoG+YBgN87SP0GVJ5YPjqHINrgqFYE=
|
||||
github.com/grafana/pyroscope-go v1.2.7 h1:VWBBlqxjyR0Cwk2W6UrE8CdcdD80GOFNutj0Kb1T8ac=
|
||||
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/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.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs=
|
||||
github.com/hamba/cmd/v3 v3.1.0 h1:aPartvDscWVC6VrboXC9e/uc0Z5S4ogXqj4yTTyqDmg=
|
||||
github.com/hamba/cmd/v3 v3.1.0/go.mod h1:5kSV/F3sDoN2t4R5Ayb2tRCYfHyVICNW5lUvoFe14FY=
|
||||
github.com/hamba/logger/v2 v2.9.0 h1:gLa4AuoQ17XTBovyIewOK7sALX/sHDJO3kfPUQBUA2o=
|
||||
github.com/hamba/logger/v2 v2.9.0/go.mod h1:i+ohrYJ5XKaicZAJD+64lsYd3ZqLOjFXzt210lmZ/iQ=
|
||||
github.com/hamba/statter/v2 v2.8.0 h1:5rLx+e/wODnvtkzpmEQim4hHcWEJbeI+KJuPHTkQCLQ=
|
||||
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/go.mod h1:5rw9ZvxgDegvi9j32U5s5LBDrOBhrCu4g53EM03KOF4=
|
||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
@@ -152,33 +154,33 @@ github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
|
||||
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
|
||||
github.com/pion/rtcp v1.2.16 h1:fk1B1dNW4hsI78XUCljZJlC4kZOPk67mNRuQ0fcEkSo=
|
||||
github.com/pion/rtcp v1.2.16/go.mod h1:/as7VKfYbs5NIb4h6muQ35kQF/J0ZVNz2Z3xKoCBYOo=
|
||||
github.com/pion/rtp v1.9.0 h1:NL2nGZPXhjnTQGRgsDZRv0ZTo0Or5fkjCy9o9PtBHBU=
|
||||
github.com/pion/rtp v1.9.0/go.mod h1:rF5nS1GqbR7H/TCpKwylzeq6yDM+MM6k+On5EgeThEM=
|
||||
github.com/pion/rtp v1.10.0 h1:XN/xca4ho6ZEcijpdF2VGFbwuHUfiIMf3ew8eAAE43w=
|
||||
github.com/pion/rtp v1.10.0/go.mod h1:rF5nS1GqbR7H/TCpKwylzeq6yDM+MM6k+On5EgeThEM=
|
||||
github.com/pion/sdp/v3 v3.0.17 h1:9SfLAW/fF1XC8yRqQ3iWGzxkySxup4k4V7yN8Fs8nuo=
|
||||
github.com/pion/sdp/v3 v3.0.17/go.mod h1:9tyKzznud3qiweZcD86kS0ff1pGYB3VX+Bcsmkx6IXo=
|
||||
github.com/pion/srtp/v3 v3.0.9 h1:lRGF4G61xxj+m/YluB3ZnBpiALSri2lTzba0kGZMrQY=
|
||||
github.com/pion/srtp/v3 v3.0.9/go.mod h1:E+AuWd7Ug2Fp5u38MKnhduvpVkveXJX6J4Lq4rxUYt8=
|
||||
github.com/pion/transport/v3 v3.1.1 h1:Tr684+fnnKlhPceU+ICdrw6KKkTms+5qHMgw6bIkYOM=
|
||||
github.com/pion/transport/v3 v3.1.1/go.mod h1:+c2eewC5WJQHiAA46fkMMzoYZSuGzA/7E2FPrOYHctQ=
|
||||
github.com/pion/srtp/v3 v3.0.10 h1:tFirkpBb3XccP5VEXLi50GqXhv5SKPxqrdlhDCJlZrQ=
|
||||
github.com/pion/srtp/v3 v3.0.10/go.mod h1:3mOTIB0cq9qlbn59V4ozvv9ClW/BSEbRp4cY0VtaR7M=
|
||||
github.com/pion/transport/v4 v4.0.1 h1:sdROELU6BZ63Ab7FrOLn13M6YdJLY20wldXW2Cu2k8o=
|
||||
github.com/pion/transport/v4 v4.0.1/go.mod h1:nEuEA4AD5lPdcIegQDpVLgNoDGreqM/YqmEx3ovP4jM=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
|
||||
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
|
||||
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
|
||||
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
|
||||
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||
github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE=
|
||||
github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
|
||||
github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs=
|
||||
github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
|
||||
github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0=
|
||||
github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/shirou/gopsutil/v3 v3.23.12 h1:z90NtUkp3bMtmICZKpC4+WaknU1eXtp5vtbQ11DgpE4=
|
||||
github.com/shirou/gopsutil/v3 v3.23.12/go.mod h1:1FrWgea594Jp7qmjHUUPlJDTPgcsb9mGnXDxavtikzM=
|
||||
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
|
||||
@@ -195,8 +197,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/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
|
||||
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
|
||||
github.com/urfave/cli/v3 v3.3.8 h1:BzolUExliMdet9NlJ/u4m5vHSotJ3PzEqSAZ1oPMa/E=
|
||||
github.com/urfave/cli/v3 v3.3.8/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo=
|
||||
github.com/urfave/cli/v3 v3.4.1 h1:1M9UOCy5bLmGnuu1yn3t3CB4rG79Rtoxuv1sPhnm6qM=
|
||||
github.com/urfave/cli/v3 v3.4.1/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo=
|
||||
github.com/valyala/fastrand v1.1.0 h1:f+5HkLW4rsgzdNoleUOB69hyT9IlD2ZQh9GyDMfb5G8=
|
||||
github.com/valyala/fastrand v1.1.0/go.mod h1:HWqCzkrkg6QXT8V2EXWvXCoow7vLwOFN002oeRzjapQ=
|
||||
github.com/valyala/histogram v1.2.0 h1:wyYGAZZt3CpwUiIb9AU/Zbllg1llXyrtApRS815OLoQ=
|
||||
@@ -205,40 +207,42 @@ github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavM
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||
github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw=
|
||||
github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
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/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw=
|
||||
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
|
||||
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 h1:Ahq7pZmv87yiyn3jeFz/LekZmPLLdKejuO3NcK9MssM=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0/go.mod h1:MJTqhM0im3mRLw1i8uGHnCvUEeS7VwRyxlLC78PA18M=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0 h1:EtFWSnwW9hGObjkIdmlnWSydO+Qs8OwzfzXLUPg4xOc=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0/go.mod h1:QjUEoiGCPkvFZ/MjK6ZZfNOS6mfVEVKYE99dFhuN2LI=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.37.0 h1:bDMKF3RUSxshZ5OjOTi8rsHGaPKsAt76FaqgvIUySLc=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.37.0/go.mod h1:dDT67G/IkA46Mr2l9Uj7HsQVwsjASyV9SjGofsiUZDA=
|
||||
go.opentelemetry.io/otel/exporters/zipkin v1.37.0 h1:Z2apuaRnHEjzDAkpbWNPiksz1R0/FCIrJSjiMA43zwI=
|
||||
go.opentelemetry.io/otel/exporters/zipkin v1.37.0/go.mod h1:ofGu/7fG+bpmjZoiPUUmYDJ4vXWxMT57HmGoegx49uw=
|
||||
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
|
||||
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
|
||||
go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
|
||||
go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=
|
||||
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
|
||||
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
|
||||
go.opentelemetry.io/proto/otlp v1.7.0 h1:jX1VolD6nHuFzOYso2E73H85i92Mv8JQYk0K9vz09os=
|
||||
go.opentelemetry.io/proto/otlp v1.7.0/go.mod h1:fSKjH6YJ7HDlwzltzyMj036AJ3ejJLCgCSHGj4efDDo=
|
||||
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
|
||||
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 h1:lwI4Dc5leUqENgGuQImwLo4WnuXFPetmPpkLi2IrX54=
|
||||
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.38.0 h1:aTL7F04bJHUlztTsNGJ2l+6he8c+y/b//eR0jjjemT4=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0/go.mod h1:kldtb7jDTeol0l3ewcmd8SDvx3EmIE7lyvqbasU3QC4=
|
||||
go.opentelemetry.io/otel/exporters/zipkin v1.38.0 h1:0rJ2TmzpHDG+Ib9gPmu3J3cE0zXirumQcKS4wCoZUa0=
|
||||
go.opentelemetry.io/otel/exporters/zipkin v1.38.0/go.mod h1:Su/nq/K5zRjDKKC3Il0xbViE3juWgG3JDoqLumFx5G0=
|
||||
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
|
||||
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
|
||||
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
|
||||
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
|
||||
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
|
||||
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
|
||||
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/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
|
||||
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
|
||||
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=
|
||||
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
|
||||
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
|
||||
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E=
|
||||
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
|
||||
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
|
||||
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
|
||||
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
|
||||
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
|
||||
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
|
||||
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
|
||||
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
|
||||
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
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=
|
||||
@@ -247,18 +251,20 @@ golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
||||
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=
|
||||
golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww=
|
||||
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
||||
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
||||
golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
|
||||
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 h1:oWVWY3NzT7KJppx2UKhKmzPq4SRe0LdCijVRwvGeikY=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
|
||||
google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok=
|
||||
google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc=
|
||||
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
|
||||
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
|
||||
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
|
||||
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
|
||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||
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-20250908214217-97024824d090/go.mod h1:U8EXRNSd8sUYyDfs/It7KVWodQr+Hf9xtxyxWudSwEw=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250908214217-97024824d090 h1:/OQuEa4YWtDt7uQWHd3q3sUMb+QOLQUg1xa8CEsRv5w=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250908214217-97024824d090/go.mod h1:GmFNa4BdJZ2a8G+wCe9Bg3wwThLrJun751XstdJt5Og=
|
||||
google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI=
|
||||
google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
|
||||
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
|
||||
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 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 2.0 MiB After Width: | Height: | Size: 1.5 MiB |
@@ -166,10 +166,7 @@ func headerValues(header base.Header, name string) base.HeaderValue {
|
||||
|
||||
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.TrimSpace(route)
|
||||
if path != "" && !strings.HasPrefix(path, "/") {
|
||||
path = "/" + path
|
||||
}
|
||||
path := "/" + strings.TrimLeft(strings.TrimSpace(route), "/") // Ensure path starts with a single "/"
|
||||
|
||||
u := &url.URL{
|
||||
Scheme: "rtsp",
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
package attack
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
"testing"
|
||||
|
||||
"github.com/Ullaakut/cameradar/v6"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestBuildRTSPURL(t *testing.T) {
|
||||
stream := cameradar.Stream{
|
||||
Address: netip.MustParseAddr("192.168.0.10"),
|
||||
Port: 554,
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
route string
|
||||
username string
|
||||
password string
|
||||
wantURL string
|
||||
}{
|
||||
{
|
||||
name: "empty route",
|
||||
wantURL: "rtsp://192.168.0.10:554/",
|
||||
},
|
||||
{
|
||||
name: "root route",
|
||||
route: "/",
|
||||
wantURL: "rtsp://192.168.0.10:554/",
|
||||
},
|
||||
{
|
||||
name: "multiple leading slashes",
|
||||
route: "////",
|
||||
wantURL: "rtsp://192.168.0.10:554/",
|
||||
},
|
||||
{
|
||||
name: "route with no leading slash",
|
||||
route: "stream",
|
||||
wantURL: "rtsp://192.168.0.10:554/stream",
|
||||
},
|
||||
{
|
||||
name: "route with leading slash",
|
||||
route: "/stream",
|
||||
wantURL: "rtsp://192.168.0.10:554/stream",
|
||||
},
|
||||
{
|
||||
name: "route with trailing slash",
|
||||
route: "stream/",
|
||||
wantURL: "rtsp://192.168.0.10:554/stream/",
|
||||
},
|
||||
{
|
||||
name: "route with spaces",
|
||||
route: " /stream ",
|
||||
wantURL: "rtsp://192.168.0.10:554/stream",
|
||||
},
|
||||
{
|
||||
name: "username and password",
|
||||
route: "stream",
|
||||
username: "admin",
|
||||
password: "admin123",
|
||||
wantURL: "rtsp://admin:admin123@192.168.0.10:554/stream",
|
||||
},
|
||||
{
|
||||
name: "empty username with password",
|
||||
route: "stream",
|
||||
password: "pass",
|
||||
wantURL: "rtsp://:pass@192.168.0.10:554/stream",
|
||||
},
|
||||
{
|
||||
name: "username only",
|
||||
route: "stream",
|
||||
username: "user",
|
||||
wantURL: "rtsp://user:@192.168.0.10:554/stream",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
_, gotURL, err := buildRTSPURL(stream, test.route, test.username, test.password)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, test.wantURL, gotURL)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -112,10 +112,7 @@ func formatStreamLabel(stream cameradar.Stream) string {
|
||||
}
|
||||
|
||||
func formatRTSPURL(stream cameradar.Stream) string {
|
||||
path := strings.TrimSpace(stream.Route())
|
||||
if path != "" && !strings.HasPrefix(path, "/") {
|
||||
path = "/" + path
|
||||
}
|
||||
path := "/" + strings.TrimLeft(strings.TrimSpace(stream.Route()), "/")
|
||||
|
||||
credentials := ""
|
||||
if stream.CredentialsFound && (stream.Username != "" || stream.Password != "") {
|
||||
|
||||
@@ -1,17 +1,28 @@
|
||||
package scan
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/Ullaakut/cameradar/v6"
|
||||
"github.com/Ullaakut/cameradar/v6/internal/scan/masscan"
|
||||
"github.com/Ullaakut/cameradar/v6/internal/scan/nmap"
|
||||
"github.com/Ullaakut/cameradar/v6/internal/scan/skip"
|
||||
)
|
||||
|
||||
// Supported discovery backends.
|
||||
const (
|
||||
ScannerNmap = "nmap"
|
||||
ScannerMasscan = "masscan"
|
||||
)
|
||||
|
||||
// Config configures how Cameradar discovers RTSP streams.
|
||||
type Config struct {
|
||||
SkipScan bool
|
||||
Targets []string
|
||||
Ports []string
|
||||
ScanSpeed int16
|
||||
Scanner string
|
||||
}
|
||||
|
||||
// Reporter reports scan progress and debug information.
|
||||
@@ -31,5 +42,17 @@ func New(config Config, reporter Reporter) (cameradar.StreamScanner, error) {
|
||||
return skip.New(expandedTargets, config.Ports), nil
|
||||
}
|
||||
|
||||
return nmap.New(config.ScanSpeed, expandedTargets, config.Ports, reporter)
|
||||
scanner := strings.ToLower(strings.TrimSpace(config.Scanner))
|
||||
if scanner == "" {
|
||||
scanner = ScannerNmap
|
||||
}
|
||||
|
||||
switch scanner {
|
||||
case ScannerNmap:
|
||||
return nmap.New(config.ScanSpeed, expandedTargets, config.Ports, reporter)
|
||||
case ScannerMasscan:
|
||||
return masscan.New(expandedTargets, config.Ports, reporter)
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported scanner %q", scanner)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,3 +64,31 @@ func TestNew_SkipScanPropagatesErrors(t *testing.T) {
|
||||
require.Error(t, err)
|
||||
assert.ErrorContains(t, err, "invalid port range")
|
||||
}
|
||||
|
||||
func TestNew_UnsupportedScanner(t *testing.T) {
|
||||
config := scan.Config{
|
||||
Targets: []string{"192.0.2.1"},
|
||||
Ports: []string{"554"},
|
||||
Scanner: "unsupported",
|
||||
}
|
||||
|
||||
_, err := scan.New(config, nil)
|
||||
require.Error(t, err)
|
||||
assert.ErrorContains(t, err, "unsupported scanner")
|
||||
}
|
||||
|
||||
func TestNew_SkipScanIgnoresUnsupportedScanner(t *testing.T) {
|
||||
config := scan.Config{
|
||||
SkipScan: true,
|
||||
Targets: []string{"192.0.2.1"},
|
||||
Ports: []string{"554"},
|
||||
Scanner: "unsupported",
|
||||
}
|
||||
|
||||
scanner, err := scan.New(config, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
streams, err := scanner.Scan(t.Context())
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []cameradar.Stream{{Address: netip.MustParseAddr("192.0.2.1"), Port: 554}}, streams)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
package masscan
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"strings"
|
||||
|
||||
"github.com/Ullaakut/cameradar/v6"
|
||||
masscanlib "github.com/Ullaakut/masscan"
|
||||
)
|
||||
|
||||
// Reporter reports scan progress and debug information.
|
||||
type Reporter interface {
|
||||
Debug(step cameradar.Step, message string)
|
||||
Progress(step cameradar.Step, message string)
|
||||
}
|
||||
|
||||
// Runner is something that can run a masscan scan.
|
||||
type Runner interface {
|
||||
Run(ctx context.Context) (*masscanlib.Run, error)
|
||||
}
|
||||
|
||||
// Scanner scans targets and ports for RTSP streams.
|
||||
type Scanner struct {
|
||||
runner Runner
|
||||
reporter Reporter
|
||||
}
|
||||
|
||||
// New returns a Scanner configured with the provided targets and ports.
|
||||
func New(targets, ports []string, reporter Reporter) (*Scanner, error) {
|
||||
runner, err := masscanlib.NewScanner(
|
||||
masscanlib.WithTargets(targets...),
|
||||
masscanlib.WithPorts(ports...),
|
||||
masscanlib.WithOpenOnly(),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("creating masscan scanner: %w", err)
|
||||
}
|
||||
|
||||
return &Scanner{
|
||||
runner: runner,
|
||||
reporter: reporter,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Scan discovers RTSP streams on the configured targets and ports.
|
||||
func (s *Scanner) Scan(ctx context.Context) ([]cameradar.Stream, error) {
|
||||
return runScan(ctx, s.runner, s.reporter)
|
||||
}
|
||||
|
||||
func runScan(ctx context.Context, runner Runner, reporter Reporter) ([]cameradar.Stream, error) {
|
||||
results, err := runner.Run(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("scanning network: %w", err)
|
||||
}
|
||||
|
||||
for _, warning := range results.Warnings() {
|
||||
reporter.Debug(cameradar.StepScan, "masscan warning: "+warning)
|
||||
}
|
||||
|
||||
var streams []cameradar.Stream
|
||||
for _, host := range results.Hosts {
|
||||
address := strings.TrimSpace(host.Address)
|
||||
if address == "" {
|
||||
reporter.Progress(cameradar.StepScan, "Skipping host with empty address")
|
||||
continue
|
||||
}
|
||||
|
||||
addr, err := netip.ParseAddr(address)
|
||||
if err != nil {
|
||||
reporter.Progress(cameradar.StepScan, fmt.Sprintf("Skipping invalid address %q: %v", host.Address, err))
|
||||
continue
|
||||
}
|
||||
|
||||
for _, port := range host.Ports {
|
||||
if port.Status != "open" {
|
||||
continue
|
||||
}
|
||||
|
||||
if port.Number <= 0 || port.Number > 65535 {
|
||||
reporter.Progress(cameradar.StepScan, fmt.Sprintf("Skipping invalid port %d on %s", port.Number, host.Address))
|
||||
continue
|
||||
}
|
||||
|
||||
streams = append(streams, cameradar.Stream{
|
||||
Address: addr,
|
||||
Port: uint16(port.Number),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
reporter.Progress(cameradar.StepScan, fmt.Sprintf("Found %d RTSP streams", len(streams)))
|
||||
updateSummary(reporter, streams)
|
||||
|
||||
return streams, nil
|
||||
}
|
||||
|
||||
type summaryUpdater interface {
|
||||
UpdateSummary(streams []cameradar.Stream)
|
||||
}
|
||||
|
||||
func updateSummary(reporter Reporter, streams []cameradar.Stream) {
|
||||
updater, ok := reporter.(summaryUpdater)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
updater.UpdateSummary(streams)
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
package masscan
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/netip"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/Ullaakut/cameradar/v6"
|
||||
masscanlib "github.com/Ullaakut/masscan"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestRunScan(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
result *masscanlib.Run
|
||||
err error
|
||||
wantStreams []cameradar.Stream
|
||||
wantDebug []string
|
||||
wantProgress []string
|
||||
wantErrContains string
|
||||
}{
|
||||
{
|
||||
name: "filters invalid addresses, closed and invalid ports",
|
||||
result: &masscanlib.Run{
|
||||
Hosts: []masscanlib.Host{
|
||||
{
|
||||
Address: "192.0.2.10",
|
||||
Ports: []masscanlib.Port{
|
||||
{Number: 554, Status: "open"},
|
||||
{Number: 8554, Status: "closed"},
|
||||
{Number: 0, Status: "open"},
|
||||
},
|
||||
},
|
||||
{Address: "not-an-ip", Ports: []masscanlib.Port{{Number: 8554, Status: "open"}}},
|
||||
{Address: "", Ports: []masscanlib.Port{{Number: 8554, Status: "open"}}},
|
||||
},
|
||||
},
|
||||
wantStreams: []cameradar.Stream{
|
||||
{Address: netip.MustParseAddr("192.0.2.10"), Port: 554},
|
||||
},
|
||||
wantProgress: []string{
|
||||
"Skipping invalid port 0 on 192.0.2.10",
|
||||
"Skipping invalid address \"not-an-ip\": ParseAddr(\"not-an-ip\"): unable to parse IP",
|
||||
"Skipping host with empty address",
|
||||
"Found 1 RTSP streams",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "collects streams from multiple hosts",
|
||||
result: &masscanlib.Run{
|
||||
Hosts: []masscanlib.Host{
|
||||
{Address: "192.0.2.10", Ports: []masscanlib.Port{{Number: 8554, Status: "open"}}},
|
||||
{Address: "198.51.100.9", Ports: []masscanlib.Port{{Number: 554, Status: "open"}}},
|
||||
},
|
||||
},
|
||||
wantStreams: []cameradar.Stream{
|
||||
{Address: netip.MustParseAddr("192.0.2.10"), Port: 8554},
|
||||
{Address: netip.MustParseAddr("198.51.100.9"), Port: 554},
|
||||
},
|
||||
wantProgress: []string{"Found 2 RTSP streams"},
|
||||
},
|
||||
{
|
||||
name: "returns error when scan fails",
|
||||
err: errors.New("scan failed"),
|
||||
wantErrContains: "scanning network",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
reporter := &recordingReporter{}
|
||||
|
||||
streams, err := runScan(t.Context(), fakeRunner{result: test.result, err: test.err}, reporter)
|
||||
|
||||
if test.wantErrContains != "" {
|
||||
require.Error(t, err)
|
||||
assert.ErrorContains(t, err, test.wantErrContains)
|
||||
assert.Empty(t, streams)
|
||||
assert.Empty(t, reporter.progress)
|
||||
assert.Equal(t, test.wantDebug, reporter.debug)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, test.wantStreams, streams)
|
||||
assert.Equal(t, test.wantDebug, reporter.debug)
|
||||
for _, progress := range test.wantProgress {
|
||||
assert.Contains(t, reporter.progress, progress)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type fakeRunner struct {
|
||||
result *masscanlib.Run
|
||||
err error
|
||||
}
|
||||
|
||||
func (f fakeRunner) Run(context.Context) (*masscanlib.Run, error) {
|
||||
return f.result, f.err
|
||||
}
|
||||
|
||||
type recordingReporter struct {
|
||||
mu sync.Mutex
|
||||
debug []string
|
||||
progress []string
|
||||
}
|
||||
|
||||
func (r *recordingReporter) Start(cameradar.Step, string) {}
|
||||
|
||||
func (r *recordingReporter) Done(cameradar.Step, string) {}
|
||||
|
||||
func (r *recordingReporter) Progress(_ cameradar.Step, message string) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
r.progress = append(r.progress, message)
|
||||
}
|
||||
|
||||
func (r *recordingReporter) Debug(_ cameradar.Step, message string) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
r.debug = append(r.debug, message)
|
||||
}
|
||||
|
||||
func (r *recordingReporter) Error(cameradar.Step, error) {}
|
||||
|
||||
func (r *recordingReporter) Summary([]cameradar.Stream, error) {}
|
||||
|
||||
func (r *recordingReporter) Close() {}
|
||||
+133
-38
@@ -7,6 +7,7 @@ import (
|
||||
"github.com/Ullaakut/cameradar/v6"
|
||||
"github.com/charmbracelet/bubbles/progress"
|
||||
"github.com/charmbracelet/bubbles/spinner"
|
||||
"github.com/charmbracelet/bubbles/table"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
@@ -14,7 +15,6 @@ type modelState struct {
|
||||
steps []cameradar.Step
|
||||
status map[cameradar.Step]state
|
||||
logs []logMsg
|
||||
summary []summaryTable
|
||||
summaryStreams []cameradar.Stream
|
||||
summaryFinal bool
|
||||
buildInfo BuildInfo
|
||||
@@ -23,6 +23,7 @@ type modelState struct {
|
||||
spinner spinner.Model
|
||||
progress progress.Model
|
||||
width int
|
||||
height int
|
||||
quitting bool
|
||||
progressTotals map[cameradar.Step]int
|
||||
progressCounts map[cameradar.Step]int
|
||||
@@ -82,7 +83,6 @@ func (m *modelState) handleStepMsg(msg stepMsg) {
|
||||
markStepComplete(m, msg.step)
|
||||
queueProgressUpdate(m)
|
||||
}
|
||||
m.summary = buildSummaryTables(m.summaryStreams, m.width, m.status, m.summaryFinal)
|
||||
}
|
||||
|
||||
func (m *modelState) handleLogMsg(msg logMsg) {
|
||||
@@ -92,7 +92,6 @@ func (m *modelState) handleLogMsg(msg logMsg) {
|
||||
func (m *modelState) handleSummaryMsg(msg summaryMsg) {
|
||||
m.summaryStreams = msg.streams
|
||||
m.summaryFinal = msg.final
|
||||
m.summary = buildSummaryTables(msg.streams, m.width, m.status, msg.final)
|
||||
if msg.final {
|
||||
m.status[cameradar.StepSummary] = stateDone
|
||||
markStepComplete(m, cameradar.StepSummary)
|
||||
@@ -134,55 +133,151 @@ func (m *modelState) handleSpinnerMsg(msg spinner.TickMsg) []tea.Cmd {
|
||||
|
||||
func (m *modelState) handleWindowSizeMsg(msg tea.WindowSizeMsg) {
|
||||
m.width = msg.Width
|
||||
m.height = msg.Height
|
||||
m.progress.Width = progressWidth(msg.Width)
|
||||
m.summary = buildSummaryTables(m.summaryStreams, m.width, m.status, m.summaryFinal)
|
||||
}
|
||||
|
||||
func (m *modelState) View() string {
|
||||
var builder strings.Builder
|
||||
builder.WriteString(sectionStyle.Render(m.buildInfo.TUIHeader()))
|
||||
builder.WriteString("\n")
|
||||
builder.WriteString(renderProgress(m))
|
||||
builder.WriteString("\n")
|
||||
header := sectionStyle.Render(m.buildInfo.TUIHeader())
|
||||
headerLines := splitLines(header)
|
||||
builder.WriteString(strings.Join(headerLines, "\n"))
|
||||
builder.WriteString("\n\n")
|
||||
|
||||
spinnerView := m.spinner.View()
|
||||
for _, step := range m.steps {
|
||||
builder.WriteString(renderStep(step, m.status[step], spinnerView))
|
||||
builder.WriteString("\n")
|
||||
}
|
||||
stepsLines := m.renderSteps()
|
||||
builder.WriteString(strings.Join(stepsLines, "\n"))
|
||||
builder.WriteString("\n\n")
|
||||
|
||||
builder.WriteString("\n")
|
||||
summaryHeight, logsHeight := m.layoutHeights(len(headerLines), len(stepsLines))
|
||||
logsLines := m.renderLogs(logsHeight)
|
||||
builder.WriteString(sectionStyle.Render("Logs"))
|
||||
builder.WriteString("\n")
|
||||
if len(m.logs) == 0 {
|
||||
builder.WriteString(dimStyle.Render("No events yet."))
|
||||
builder.WriteString("\n")
|
||||
} else {
|
||||
for _, entry := range m.logs {
|
||||
builder.WriteString(renderLog(entry))
|
||||
builder.WriteString("\n")
|
||||
}
|
||||
}
|
||||
builder.WriteString(strings.Join(logsLines, "\n"))
|
||||
builder.WriteString("\n\n")
|
||||
|
||||
rowsToShow := max(1, summaryHeight-2)
|
||||
summaryTitle := renderSummaryTitle(m.summaryStreams)
|
||||
summaryTables := buildSummaryTables(m.summaryStreams, m.width, m.status, rowsToShow)
|
||||
builder.WriteString(sectionStyle.Render(summaryTitle))
|
||||
builder.WriteString("\n")
|
||||
builder.WriteString(sectionStyle.Render("Summary"))
|
||||
builder.WriteString("\n")
|
||||
for i, summary := range m.summary {
|
||||
if summary.title != "" {
|
||||
builder.WriteString(subsectionStyle.Render(summary.title))
|
||||
builder.WriteString("\n")
|
||||
}
|
||||
if summary.emptyMessage != "" {
|
||||
builder.WriteString(dimStyle.Render(summary.emptyMessage))
|
||||
builder.WriteString("\n")
|
||||
} else {
|
||||
builder.WriteString(summaryTableStyle.Render(summary.table.View()))
|
||||
builder.WriteString("\n")
|
||||
}
|
||||
if i < len(m.summary)-1 {
|
||||
for i, summary := range summaryTables {
|
||||
builder.WriteString(summaryTableStyle.Render(summary.table.View()))
|
||||
if i < len(summaryTables)-1 {
|
||||
builder.WriteString("\n")
|
||||
}
|
||||
}
|
||||
|
||||
return builder.String()
|
||||
}
|
||||
|
||||
func (m *modelState) FinalView() string {
|
||||
var builder strings.Builder
|
||||
header := sectionStyle.Render(m.buildInfo.TUIHeader())
|
||||
headerLines := splitLines(header)
|
||||
builder.WriteString(strings.Join(headerLines, "\n"))
|
||||
builder.WriteString("\n\n")
|
||||
|
||||
stepsLines := m.renderSteps()
|
||||
builder.WriteString(strings.Join(stepsLines, "\n"))
|
||||
builder.WriteString("\n\n")
|
||||
|
||||
builder.WriteString(sectionStyle.Render("Logs"))
|
||||
builder.WriteString("\n")
|
||||
logLines := m.renderLogsAll()
|
||||
if len(logLines) == 0 {
|
||||
builder.WriteString(dimStyle.Render("No events yet."))
|
||||
} else {
|
||||
builder.WriteString(strings.Join(logLines, "\n"))
|
||||
}
|
||||
builder.WriteString("\n\n")
|
||||
|
||||
summaryTitle := renderSummaryTitle(m.summaryStreams)
|
||||
visibility := summaryVisibility(summaryStatusAllDone())
|
||||
accessible, others := partitionStreams(m.summaryStreams)
|
||||
rows := append(buildSummaryRows(accessible, visibility), buildSummaryRows(others, visibility)...)
|
||||
if len(rows) == 0 {
|
||||
rows = []table.Row{emptySummaryRow()}
|
||||
}
|
||||
columns := summaryColumns(m.width, rows)
|
||||
builder.WriteString(sectionStyle.Render(summaryTitle))
|
||||
builder.WriteString("\n")
|
||||
builder.WriteString(renderSummaryTablePlain(columns, rows))
|
||||
return builder.String()
|
||||
}
|
||||
|
||||
func (m *modelState) renderSteps() []string {
|
||||
lines := []string{sectionStyle.Render("Steps"), renderProgress(m)}
|
||||
spinnerView := m.spinner.View()
|
||||
for _, step := range m.steps {
|
||||
lines = append(lines, renderStep(step, m.status[step], spinnerView))
|
||||
}
|
||||
return lines
|
||||
}
|
||||
|
||||
func (m *modelState) renderLogs(height int) []string {
|
||||
if height <= 0 {
|
||||
return nil
|
||||
}
|
||||
if len(m.logs) == 0 {
|
||||
lines := []string{dimStyle.Render("No events yet.")}
|
||||
return padLines(lines, height)
|
||||
}
|
||||
|
||||
start := 0
|
||||
if len(m.logs) > height {
|
||||
start = len(m.logs) - height
|
||||
}
|
||||
lines := make([]string, 0, min(height, len(m.logs)))
|
||||
for _, entry := range m.logs[start:] {
|
||||
lines = append(lines, renderLog(entry))
|
||||
}
|
||||
return padLines(lines, height)
|
||||
}
|
||||
|
||||
func (m *modelState) renderLogsAll() []string {
|
||||
if len(m.logs) == 0 {
|
||||
return nil
|
||||
}
|
||||
lines := make([]string, 0, len(m.logs))
|
||||
for _, entry := range m.logs {
|
||||
lines = append(lines, renderLog(entry))
|
||||
}
|
||||
return lines
|
||||
}
|
||||
|
||||
func (m *modelState) layoutHeights(headerLines, stepsLines int) (summaryHeight, logsHeight int) {
|
||||
if m.height <= 0 {
|
||||
return summaryMinHeight, len(m.logs)
|
||||
}
|
||||
|
||||
reserved := headerLines + 1 + stepsLines + 1 + 1 + 1
|
||||
remaining := m.height - reserved
|
||||
remaining = max(0, remaining)
|
||||
|
||||
switch {
|
||||
case remaining < summaryMinHeight:
|
||||
summaryHeight = max(3, remaining)
|
||||
case remaining > summaryMaxHeight:
|
||||
summaryHeight = summaryMaxHeight
|
||||
default:
|
||||
summaryHeight = remaining
|
||||
}
|
||||
|
||||
logsHeight = max(0, remaining-summaryHeight)
|
||||
|
||||
return summaryHeight, logsHeight
|
||||
}
|
||||
|
||||
func padLines(lines []string, height int) []string {
|
||||
if height <= 0 {
|
||||
return lines
|
||||
}
|
||||
for len(lines) < height {
|
||||
lines = append(lines, "")
|
||||
}
|
||||
return lines
|
||||
}
|
||||
|
||||
func splitLines(value string) []string {
|
||||
return strings.Split(value, "\n")
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import "github.com/charmbracelet/lipgloss"
|
||||
|
||||
var (
|
||||
sectionStyle = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("63"))
|
||||
subsectionStyle = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("111"))
|
||||
infoStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("252"))
|
||||
debugStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("244"))
|
||||
successStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("42"))
|
||||
|
||||
@@ -119,10 +119,7 @@ func formatStream(stream cameradar.Stream) string {
|
||||
}
|
||||
|
||||
func formatRTSPURL(stream cameradar.Stream) string {
|
||||
path := stream.Route()
|
||||
if path != "" && !strings.HasPrefix(path, "/") {
|
||||
path = "/" + path
|
||||
}
|
||||
path := "/" + strings.TrimLeft(strings.TrimSpace(stream.Route()), "/")
|
||||
|
||||
credentials := ""
|
||||
if stream.Username != "" || stream.Password != "" {
|
||||
|
||||
+169
-16
@@ -59,17 +59,23 @@ type summaryMsg struct {
|
||||
}
|
||||
|
||||
type summaryTable struct {
|
||||
title string
|
||||
table table.Model
|
||||
emptyMessage string
|
||||
table table.Model
|
||||
}
|
||||
|
||||
const (
|
||||
summaryMinHeight = 8
|
||||
summaryMaxHeight = 10
|
||||
summaryColumnCount = 8
|
||||
)
|
||||
|
||||
// TUIReporter renders a Bubble Tea based UI.
|
||||
type TUIReporter struct {
|
||||
program *tea.Program
|
||||
debug bool
|
||||
once sync.Once
|
||||
closed chan struct{}
|
||||
mu sync.Mutex
|
||||
last []cameradar.Stream
|
||||
}
|
||||
|
||||
// NewTUIReporter creates a new Bubble Tea reporter.
|
||||
@@ -96,7 +102,6 @@ func NewTUIReporter(debug bool, out io.Writer, buildInfo BuildInfo, cancel conte
|
||||
progressTotals: make(map[cameradar.Step]int),
|
||||
progressCounts: make(map[cameradar.Step]int),
|
||||
}
|
||||
initial.summary = buildSummaryTables(nil, initial.width, initial.status, false)
|
||||
|
||||
p := tea.NewProgram(initial, tea.WithInputTTY(), tea.WithOutput(out), tea.WithAltScreen())
|
||||
reporter := &TUIReporter{program: p, debug: debug, closed: make(chan struct{})}
|
||||
@@ -110,7 +115,19 @@ func NewTUIReporter(debug bool, out io.Writer, buildInfo BuildInfo, cancel conte
|
||||
}
|
||||
|
||||
if rendered, ok := model.(*modelState); ok {
|
||||
_, _ = fmt.Fprintln(out, rendered.View())
|
||||
output := rendered.FinalView()
|
||||
if len(rendered.summaryStreams) == 0 {
|
||||
fallback := reporter.snapshotSummary()
|
||||
if len(fallback) > 0 {
|
||||
tmp := &modelState{
|
||||
summaryStreams: fallback,
|
||||
width: rendered.width,
|
||||
status: summaryStatusAllDone(),
|
||||
}
|
||||
output = tmp.FinalView()
|
||||
}
|
||||
}
|
||||
_, _ = fmt.Fprintln(out, output)
|
||||
}
|
||||
close(reporter.closed)
|
||||
}()
|
||||
@@ -165,12 +182,28 @@ func (r *TUIReporter) Error(step cameradar.Step, err error) {
|
||||
|
||||
// Summary implements Reporter.
|
||||
func (r *TUIReporter) Summary(streams []cameradar.Stream, _ error) {
|
||||
r.send(summaryMsg{streams: copyStreams(streams), final: true})
|
||||
cloned := copyStreams(streams)
|
||||
r.recordSummary(cloned)
|
||||
r.send(summaryMsg{streams: cloned, final: true})
|
||||
}
|
||||
|
||||
// UpdateSummary updates the summary section with partial results.
|
||||
func (r *TUIReporter) UpdateSummary(streams []cameradar.Stream) {
|
||||
r.send(summaryMsg{streams: copyStreams(streams), final: false})
|
||||
cloned := copyStreams(streams)
|
||||
r.recordSummary(cloned)
|
||||
r.send(summaryMsg{streams: cloned, final: false})
|
||||
}
|
||||
|
||||
func (r *TUIReporter) recordSummary(streams []cameradar.Stream) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
r.last = streams
|
||||
}
|
||||
|
||||
func (r *TUIReporter) snapshotSummary() []cameradar.Stream {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
return copyStreams(r.last)
|
||||
}
|
||||
|
||||
// Close implements Reporter.
|
||||
@@ -346,33 +379,153 @@ func progressWidth(width int) int {
|
||||
return 36
|
||||
}
|
||||
|
||||
func buildSummaryTables(streams []cameradar.Stream, width int, status map[cameradar.Step]state, final bool) []summaryTable {
|
||||
func buildSummaryTables(streams []cameradar.Stream, width int, status map[cameradar.Step]state, maxRows int) []summaryTable {
|
||||
visibility := summaryVisibility(status)
|
||||
accessible, others := partitionStreams(streams)
|
||||
rows := append(buildSummaryRows(accessible, visibility), buildSummaryRows(others, visibility)...)
|
||||
if len(rows) == 0 {
|
||||
message := "Waiting for results..."
|
||||
if final {
|
||||
message = "No streams discovered."
|
||||
}
|
||||
return []summaryTable{{title: "Streams", emptyMessage: message}}
|
||||
rows = []table.Row{emptySummaryRow()}
|
||||
}
|
||||
|
||||
if maxRows > 0 {
|
||||
switch {
|
||||
case len(rows) > maxRows:
|
||||
if maxRows == 1 {
|
||||
rows = []table.Row{summaryOverflowRow(len(rows))}
|
||||
} else {
|
||||
visibleRows := maxRows - 1
|
||||
hidden := len(rows) - visibleRows
|
||||
rows = append(rows[:visibleRows], summaryOverflowRow(hidden))
|
||||
}
|
||||
case len(rows) < maxRows:
|
||||
rows = padSummaryRows(rows, maxRows)
|
||||
}
|
||||
}
|
||||
|
||||
title := fmt.Sprintf("Streams (%d accessible / %d total)", len(accessible), len(streams))
|
||||
columns := summaryColumns(width, rows)
|
||||
model := table.New(
|
||||
table.WithColumns(columns),
|
||||
table.WithRows(rows),
|
||||
table.WithFocused(false),
|
||||
table.WithHeight(len(rows)+1),
|
||||
table.WithHeight(len(rows)),
|
||||
)
|
||||
model.SetStyles(summaryTableStyles())
|
||||
|
||||
return []summaryTable{{title: title, table: model}}
|
||||
return []summaryTable{{table: model}}
|
||||
}
|
||||
|
||||
func renderSummaryTitle(streams []cameradar.Stream) string {
|
||||
accessible, _ := partitionStreams(streams)
|
||||
return fmt.Sprintf("Summary - Streams (%d accessible / %d total)", len(accessible), len(streams))
|
||||
}
|
||||
|
||||
func summaryStatusAllDone() map[cameradar.Step]state {
|
||||
status := make(map[cameradar.Step]state)
|
||||
for _, step := range cameradar.Steps() {
|
||||
status[step] = stateDone
|
||||
}
|
||||
return status
|
||||
}
|
||||
|
||||
const emptyEntry = "—"
|
||||
|
||||
func emptySummaryRow() table.Row {
|
||||
row := make(table.Row, summaryColumnCount)
|
||||
for i := range row {
|
||||
row[i] = emptyEntry
|
||||
}
|
||||
return row
|
||||
}
|
||||
|
||||
func padSummaryRows(rows []table.Row, maxRows int) []table.Row {
|
||||
for len(rows) < maxRows {
|
||||
rows = append(rows, emptySummaryRow())
|
||||
}
|
||||
return rows
|
||||
}
|
||||
|
||||
func summaryOverflowRow(hidden int) table.Row {
|
||||
row := emptySummaryRow()
|
||||
if hidden <= 0 {
|
||||
return row
|
||||
}
|
||||
label := "\u2026 1 more stream"
|
||||
if hidden > 1 {
|
||||
label = fmt.Sprintf("\u2026 %d more streams", hidden)
|
||||
}
|
||||
row[0] = label
|
||||
return row
|
||||
}
|
||||
|
||||
func renderSummaryTablePlain(columns []table.Column, rows []table.Row) string {
|
||||
colWidths := make([]int, len(columns))
|
||||
for i, col := range columns {
|
||||
colWidths[i] = max(col.Width, len([]rune(col.Title)))
|
||||
}
|
||||
|
||||
var builder strings.Builder
|
||||
builder.WriteString(renderSummaryBorder("┌", "┬", "┐", colWidths))
|
||||
builder.WriteString("\n")
|
||||
builder.WriteString(renderSummaryRow(columnTitles(columns), colWidths))
|
||||
builder.WriteString("\n")
|
||||
builder.WriteString(renderSummaryBorder("├", "┼", "┤", colWidths))
|
||||
for _, row := range rows {
|
||||
builder.WriteString("\n")
|
||||
builder.WriteString(renderSummaryRow(row, colWidths))
|
||||
}
|
||||
builder.WriteString("\n")
|
||||
builder.WriteString(renderSummaryBorder("└", "┴", "┘", colWidths))
|
||||
return builder.String()
|
||||
}
|
||||
|
||||
func renderSummaryBorder(left, middle, right string, widths []int) string {
|
||||
parts := make([]string, 0, len(widths))
|
||||
for _, width := range widths {
|
||||
parts = append(parts, strings.Repeat("─", width+2))
|
||||
}
|
||||
return left + strings.Join(parts, middle) + right
|
||||
}
|
||||
|
||||
func renderSummaryRow(cells []string, widths []int) string {
|
||||
var builder strings.Builder
|
||||
builder.WriteString("│")
|
||||
for i, width := range widths {
|
||||
value := ""
|
||||
if i < len(cells) {
|
||||
value = cells[i]
|
||||
}
|
||||
builder.WriteString(" ")
|
||||
builder.WriteString(padAndTrim(value, width))
|
||||
builder.WriteString(" │")
|
||||
}
|
||||
return builder.String()
|
||||
}
|
||||
|
||||
func padAndTrim(value string, width int) string {
|
||||
if width <= 0 {
|
||||
return ""
|
||||
}
|
||||
runes := []rune(value)
|
||||
if len(runes) > width {
|
||||
return string(runes[:width])
|
||||
}
|
||||
if len(runes) < width {
|
||||
return string(runes) + strings.Repeat(" ", width-len(runes))
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func columnTitles(columns []table.Column) []string {
|
||||
if len(columns) == 0 {
|
||||
return nil
|
||||
}
|
||||
titles := make([]string, len(columns))
|
||||
for i, col := range columns {
|
||||
titles[i] = col.Title
|
||||
}
|
||||
return titles
|
||||
}
|
||||
|
||||
func buildSummaryRows(streams []cameradar.Stream, visibility summaryVisibilityState) []table.Row {
|
||||
rows := make([]table.Row, 0, len(streams))
|
||||
for _, stream := range streams {
|
||||
|
||||
Reference in New Issue
Block a user