Compare commits

...

34 Commits

Author SHA1 Message Date
whiteboxsolutions 82d063ca8b fix: update go.mod
Co-authored-by: Brendan Le Glaunec <brendan.le-glaunec@epitech.eu>
2026-03-14 14:31:18 -07:00
Jake Daynes 561c19581c doc: update port list in readme 2026-03-13 04:24:13 -07:00
Jake Daynes d7366c970b fix: remove whitespace for linter 2026-03-13 04:23:25 -07:00
Jake Daynes beb59543dc fix: update inline error for linter 2026-03-13 04:21:14 -07:00
Jake Daynes e4ec21e135 fix: fix formatting 2026-03-13 04:16:32 -07:00
Jake Daynes f715379a44 fix: add rtsp/rtsps const 2026-03-13 04:15:26 -07:00
Jake Daynes 8d216370d3 fix: clean for linter 2026-03-13 04:12:48 -07:00
Jake Daynes f3eb09812b fix: update probeDescribeHeaders to support tls dialer & continue if bad credentials in attack 2026-03-13 03:58:00 -07:00
Jake Daynes 75e9b8dc50 fix: set attackRoute to use pointer to persist redirect 2026-03-13 03:42:56 -07:00
Jake Daynes d70d774be6 fix: add tlsConfig to RTSP client to skip self-signed certs 2026-03-13 03:29:18 -07:00
Jake Daynes 62ab02acf0 fix: reorder test case for scan test 2026-03-13 02:55:56 -07:00
Jake Daynes d3a51c18c0 fix: revert credAttack since redirect will already have happened 2026-03-13 02:53:46 -07:00
Jake Daynes 4535a38ad8 fix: update rtsp url builder test to use secure flag 2026-03-13 02:38:02 -07:00
Jake Daynes 9195485b99 fix: update masscan test to handle Secure flag 2026-03-13 02:33:33 -07:00
Jake Daynes 4cc5808883 fix: update nmap scan test to handle Secure flag 2026-03-13 02:32:27 -07:00
Jake Daynes 2d96b9ed8d fix: update skip test to handle Secure flag 2026-03-13 02:23:56 -07:00
Jake Daynes 58e81a7c02 fix: update summary test to handle Secure flag 2026-03-13 02:21:59 -07:00
Jake Daynes a3ab13cbf5 fix: update test runner to include Secure flag 2026-03-13 02:20:01 -07:00
Jake Daynes 2bb748e78f fix: update rtsp url tests 2026-03-13 02:19:21 -07:00
Jake Daynes 5c30baa11e update m3u RTSP URL formatter to handle scheme 2026-03-13 02:01:45 -07:00
Jake Daynes 5912ed5283 update RTSPURL formatter to handle scheme 2026-03-13 01:59:23 -07:00
Jake Daynes 6871ee1c09 update skip stream builder to include Secure state 2026-03-13 01:57:35 -07:00
Jake Daynes 4953b16a08 update masscan scan to include Secure state 2026-03-13 01:56:55 -07:00
Jake Daynes 2865295911 update nmap scan to include Secure state 2026-03-13 01:56:19 -07:00
Jake Daynes 5bb2684d9e update function calls to include context 2026-03-13 01:54:48 -07:00
Jake Daynes 2c548c6b68 update credAttack & routeAttack to support redirect 2026-03-13 01:53:57 -07:00
Jake Daynes f89af3bfd0 add handleRedirect method 2026-03-13 01:50:58 -07:00
Jake Daynes f66c0f6d94 update buildRTSPURL to handle secure flag 2026-03-13 01:45:23 -07:00
Jake Daynes c2c9a6e546 add rtsps ports to default list 2026-03-13 01:43:35 -07:00
Jake Daynes 6d52cf0259 add secure bool 2026-03-13 01:43:08 -07: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
17 changed files with 213 additions and 186 deletions
+1 -1
View File
@@ -28,7 +28,7 @@ jobs:
if: steps.install-go.outputs.cache-hit != 'true'
- name: Log in to Docker Hub
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
+43
View File
@@ -14,6 +14,49 @@ Clone the repo and install dependencies using Go modules.
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
```bash
+8 -149
View File
@@ -47,9 +47,8 @@ Cameradar scans RTSP endpoints on authorized targets, and uses dictionary attack
- [Security and responsible use](#security-and-responsible-use)
- [Output](#output)
- [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)
- [Environment variables](#environment-variables)
- [Build and contribute](#build-and-contribute)
- [Frequently asked questions](#frequently-asked-questions)
- [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.
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.
- 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`
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)
@@ -273,117 +272,11 @@ localhost
When you use `--skip-scan`, Cameradar expands each entry into explicit IP
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.
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`
This includes all supported flags, defaults, accepted values, and env var mapping.
## Build and contribute
@@ -403,41 +296,7 @@ The `cameradar` binary is now in `$GOPATH/bin/cameradar`.
## Frequently asked questions
> Cameradar does not detect any camera!
This usually means the cameras are not streaming over RTSP.
It can also mean the targets are not in your scan range.
CCTV cameras are often on private subnets.
Use `-t` to set the correct targets.
If you still see no results, open an issue with device details.
> Cameradar detects my cameras, but does not manage to access them!
The camera configuration may have changed, so defaults do not match.
Cameradar uses defaults unless you provide custom dictionaries.
Add your credentials and routes, then follow the [configuration](#configuration) section.
> What happened to the C++ version?
The 1.1.4 tag contains the legacy C++ implementation.
It is slower and less stable than the Go version, so it is not recommended to use.
> I want to scan my local network or my own machine, and it does not work! What's going on?
Use `--net=host` when running the Docker image, or use the installed binary.
> I don't have a camera, but I'd like to try Cameradar!
Run the following container, then run Cameradar against it:
`docker run -p 8554:8554 -e RTSP_USERNAME=admin -e RTSP_PASSWORD=12345 -e RTSP_PORT=8554 ullaakut/rtspatt`
Cameradar should discover the `admin` / `12345` credentials.
You can try other default credentials listed in the dictionaries.
> What authentication types does Cameradar support?
Cameradar supports both basic and digest authentication.
See [Troubleshooting & FAQ](https://github.com/Ullaakut/cameradar/wiki/Troubleshooting-%26-FAQ)
## Examples
@@ -449,7 +308,7 @@ Cameradar supports both basic and digest authentication.
`docker run --rm -t --net=host -v /tmp:/tmp ullaakut/cameradar --targets /tmp/test.txt --ports 8554`
> Running cameradar on a subnetwork with custom dictionaries, on ports 554, 5554 and 8554
> Running cameradar on a subnetwork with custom dictionaries, on ports 554, 5554, 8554, 322, and 8322
`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`
+10 -17
View File
@@ -38,18 +38,17 @@ var (
var flags = cmd.Flags{
&cli.StringSliceFlag{
Name: flagTargets,
Usage: "The targets on which to scan for open RTSP streams in a network range format",
Aliases: []string{"t"},
Sources: cli.EnvVars(strcase.ToSNAKE(flagTargets)),
Required: true,
Name: flagTargets,
Usage: "The targets on which to scan for open RTSP streams in a network range format",
Aliases: []string{"t"},
Sources: cli.EnvVars(strcase.ToSNAKE(flagTargets)),
},
&cli.StringSliceFlag{
Name: flagPorts,
Usage: "The ports on which to search for RTSP streams",
Aliases: []string{"p"},
Sources: cli.EnvVars(strcase.ToSNAKE(flagPorts)),
Value: []string{"554", "5554", "8554", "http"},
Value: []string{"554", "5554", "8554", "http", "322", "8322"},
},
&cli.StringFlag{
Name: flagCustomRoutes,
@@ -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{
Name: "Cameradar",
Version: version,
DefaultCommand: scanCommand.Name,
Name: "Cameradar",
Version: version,
Usage: "Scan targets for RTSP streams",
Flags: flags,
Action: runCameradar,
Commands: []*cli.Command{
scanCommand,
{
Name: "version",
Usage: "Print version information",
+20 -6
View File
@@ -224,7 +224,7 @@ func (a Attacker) attackCredentialsForStream(ctx context.Context, target camerad
msg := fmt.Sprintf("credential attempt failed for %s:%d (%s:%s): %v", target.Address.String(), target.Port, username, password, err)
a.reporter.Debug(cameradar.StepAttackCredentials, msg)
return target, nil
continue
}
if ok {
@@ -253,7 +253,7 @@ func (a Attacker) attackRoutesForStream(ctx context.Context, target cameradar.St
if emitProgress {
a.reporter.Progress(cameradar.StepAttackRoutes, cameradar.ProgressTickMessage())
}
ok, err := a.routeAttack(target, dummyRoute)
ok, err := a.routeAttack(ctx, &target, dummyRoute)
if err != nil {
a.reporter.Debug(cameradar.StepAttackRoutes, fmt.Sprintf("route probe failed for %s:%d: %v", target.Address.String(), target.Port, err))
return target, nil
@@ -275,7 +275,7 @@ func (a Attacker) attackRoutesForStream(ctx context.Context, target cameradar.St
if emitProgress {
a.reporter.Progress(cameradar.StepAttackRoutes, cameradar.ProgressTickMessage())
}
ok, err := a.routeAttack(target, route)
ok, err := a.routeAttack(ctx, &target, route)
if err != nil {
a.reporter.Debug(cameradar.StepAttackRoutes, fmt.Sprintf("route attempt failed for %s:%d (%s): %v", target.Address.String(), target.Port, route, err))
return target, nil
@@ -290,18 +290,30 @@ func (a Attacker) attackRoutesForStream(ctx context.Context, target cameradar.St
return target, nil
}
func (a Attacker) routeAttack(stream cameradar.Stream, route string) (bool, error) {
u, urlStr, err := buildRTSPURL(stream, route, stream.Username, stream.Password)
func (a Attacker) routeAttack(ctx context.Context, stream *cameradar.Stream, route string) (bool, error) {
u, urlStr, err := buildRTSPURL(*stream, route, stream.Username, stream.Password)
if err != nil {
return false, fmt.Errorf("building rtsp url: %w", err)
}
code, err := a.describeStatus(u)
code, headers, err := a.probeDescribeHeaders(ctx, u, urlStr)
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))
if code == base.StatusMovedPermanently {
a.handleRedirect(stream, headers)
u, urlStr, err = buildRTSPURL(*stream, route, stream.Username, stream.Password)
if err == nil {
code, _, err = a.probeDescribeHeaders(ctx, u, urlStr)
if err == nil {
a.reporter.Debug(cameradar.StepAttackRoutes, fmt.Sprintf("DESCRIBE %s RTSP/1.0 (redirect followed) > %d", urlStr, code))
}
}
}
access := code == base.StatusOK || code == base.StatusUnauthorized || code == base.StatusForbidden
return access, nil
}
@@ -314,10 +326,12 @@ func (a Attacker) credAttack(stream cameradar.Stream, username, password string)
code, err := a.describeStatus(u)
if err != nil {
a.reporter.Debug(cameradar.StepAttackCredentials, fmt.Sprintf("Error testing %s:%s -> %v", username, password, err))
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
}
+58 -2
View File
@@ -3,9 +3,11 @@ package attack
import (
"bufio"
"context"
"crypto/tls"
"errors"
"fmt"
"net"
"net/netip"
"net/textproto"
"net/url"
"strconv"
@@ -19,10 +21,16 @@ import (
"github.com/bluenviron/gortsplib/v5/pkg/liberrors"
)
const (
rtsp = "rtsp"
rtsps = "rtsps"
)
func (a Attacker) newRTSPClient(u *base.URL) (*gortsplib.Client, error) {
client := &gortsplib.Client{
ReadTimeout: a.timeout,
WriteTimeout: a.timeout,
TLSConfig: &tls.Config{InsecureSkipVerify: true},
}
client.Scheme = u.Scheme
client.Host = u.Host
@@ -63,7 +71,16 @@ func (a Attacker) describeStatus(u *base.URL) (base.StatusCode, error) {
// 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) {
dialer := &net.Dialer{Timeout: a.timeout}
conn, err := dialer.DialContext(ctx, "tcp", u.Host)
var conn net.Conn
var err error
if u.Scheme == rtsps {
tlsDialer := &tls.Dialer{NetDialer: dialer, Config: &tls.Config{InsecureSkipVerify: true}}
conn, err = tlsDialer.DialContext(ctx, "tcp", u.Host)
} else {
conn, err = dialer.DialContext(ctx, "tcp", u.Host)
}
if err != nil {
return 0, nil, err
}
@@ -117,6 +134,40 @@ func (a Attacker) probeDescribeHeaders(ctx context.Context, u *base.URL, urlStr
return base.StatusCode(code), headers, nil
}
func (a Attacker) handleRedirect(stream *cameradar.Stream, resHeaders base.Header) {
locations := headerValues(resHeaders, "Location")
if len(locations) == 0 {
return
}
location, err := url.Parse(locations[0])
if err != nil {
return
}
switch location.Scheme {
case rtsps:
stream.Secure = true
case rtsp:
stream.Secure = false
}
if location.Hostname() != "" {
addr, err := netip.ParseAddr(location.Hostname())
if err == nil {
stream.Address = addr
}
}
if location.Port() != "" {
port, err := strconv.Atoi(location.Port())
if err == nil {
if port >= 0 && port <= 65535 {
stream.Port = uint16(port)
}
}
}
}
func authTypeFromHeaders(values base.HeaderValue) cameradar.AuthType {
if len(values) == 0 {
return cameradar.AuthUnknown
@@ -168,8 +219,13 @@ func buildRTSPURL(stream cameradar.Stream, route, username, password string) (*b
host := net.JoinHostPort(stream.Address.String(), strconv.Itoa(int(stream.Port)))
path := "/" + strings.TrimLeft(strings.TrimSpace(route), "/") // Ensure path starts with a single "/"
scheme := rtsp
if stream.Secure {
scheme = rtsps
}
u := &url.URL{
Scheme: "rtsp",
Scheme: scheme,
Host: host,
Path: path,
}
+18 -1
View File
@@ -19,6 +19,7 @@ func TestBuildRTSPURL(t *testing.T) {
route string
username string
password string
secure bool
wantURL string
}{
{
@@ -74,11 +75,27 @@ func TestBuildRTSPURL(t *testing.T) {
username: "user",
wantURL: "rtsp://user:@192.168.0.10:554/stream",
},
{
name: "secure rtsps scheme without credentials",
route: "stream",
secure: true,
wantURL: "rtsps://192.168.0.10:554/stream",
},
{
name: "secure rtsps scheme with credentials",
route: "stream",
username: "admin",
password: "password",
secure: true,
wantURL: "rtsps://admin:password@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)
s := stream
s.Secure = test.secure
_, gotURL, err := buildRTSPURL(s, test.route, test.username, test.password)
require.NoError(t, err)
require.Equal(t, test.wantURL, gotURL)
})
+6 -1
View File
@@ -119,5 +119,10 @@ func formatRTSPURL(stream cameradar.Stream) string {
credentials = stream.Username + ":" + stream.Password + "@"
}
return "rtsp://" + credentials + stream.Address.String() + ":" + strconv.FormatUint(uint64(stream.Port), 10) + path
scheme := "rtsp"
if stream.Secure {
scheme = "rtsps"
}
return fmt.Sprintf("%s://%s%s:%s%s", scheme, credentials, stream.Address.String(), strconv.FormatUint(uint64(stream.Port), 10), path)
}
+3
View File
@@ -83,9 +83,12 @@ func runScan(ctx context.Context, runner Runner, reporter Reporter) ([]cameradar
continue
}
isSecure := port.Number == 322 || port.Number == 8322
streams = append(streams, cameradar.Stream{
Address: addr,
Port: uint16(port.Number),
Secure: isSecure,
})
}
}
+5 -4
View File
@@ -53,15 +53,16 @@ func TestRunScan(t *testing.T) {
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: "192.0.2.10", Ports: []masscanlib.Port{{Number: 8554, Status: "open"}, {Number: 8322, 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},
{Address: netip.MustParseAddr("192.0.2.10"), Port: 8554, Secure: false},
{Address: netip.MustParseAddr("192.0.2.10"), Port: 8322, Secure: true},
{Address: netip.MustParseAddr("198.51.100.9"), Port: 554, Secure: false},
},
wantProgress: []string{"Found 2 RTSP streams"},
wantProgress: []string{"Found 3 RTSP streams"},
},
{
name: "returns error when scan fails",
+3
View File
@@ -71,6 +71,8 @@ func runScan(ctx context.Context, nmap Runner, reporter Reporter) ([]cameradar.S
continue
}
isSecure := strings.Contains(port.Service.Name, "rtsps") || strings.Contains(port.Service.Name, "ssl") || port.ID == 322 || port.ID == 8322
for _, address := range host.Addresses {
addr, err := netip.ParseAddr(address.Addr)
if err != nil {
@@ -82,6 +84,7 @@ func runScan(ctx context.Context, nmap Runner, reporter Reporter) ([]cameradar.S
Device: port.Service.Product,
Address: addr,
Port: port.ID,
Secure: isSecure,
})
}
}
+17 -1
View File
@@ -54,6 +54,7 @@ func TestScanner_Scan(t *testing.T) {
Addresses: []nmaplib.Address{{Addr: "192.0.2.10"}, {Addr: "192.0.2.11"}},
Ports: []nmaplib.Port{
openPort(8554, "rtsp-alt", "Model A"),
openPort(322, "rtsps", "Model C"),
},
},
nmaplib.Host{
@@ -68,19 +69,34 @@ func TestScanner_Scan(t *testing.T) {
Device: "Model A",
Address: netip.MustParseAddr("192.0.2.10"),
Port: 8554,
Secure: false,
},
{
Device: "Model A",
Address: netip.MustParseAddr("192.0.2.11"),
Port: 8554,
Secure: false,
},
{
Device: "Model C",
Address: netip.MustParseAddr("192.0.2.10"),
Port: 322,
Secure: true,
},
{
Device: "Model C",
Address: netip.MustParseAddr("192.0.2.11"),
Port: 322,
Secure: true,
},
{
Device: "Model B",
Address: netip.MustParseAddr("198.51.100.9"),
Port: 554,
Secure: false,
},
},
wantProgress: "Found 3 RTSP streams",
wantProgress: "Found 5 RTSP streams",
},
{
name: "returns error when scan fails",
+2
View File
@@ -51,9 +51,11 @@ func buildStreamsFromTargets(ctx context.Context, targets, ports []string) ([]ca
streams := make([]cameradar.Stream, 0, len(resolvedTargets)*len(resolvedPorts))
for _, addr := range resolvedTargets {
for _, port := range resolvedPorts {
isSecure := port == 322 || port == 8322
streams = append(streams, cameradar.Stream{
Address: addr,
Port: port,
Secure: isSecure,
})
}
}
+9 -2
View File
@@ -16,7 +16,7 @@ func TestNew_ExpandsTargetsAndPorts(t *testing.T) {
"192.0.2.15",
"192.0.2.10-11",
}
ports := []string{"554", "8554-8555"}
ports := []string{"554", "322", "8554-8555"}
scanner := skip.New(targets, ports)
@@ -32,7 +32,7 @@ func TestNew_ExpandsTargetsAndPorts(t *testing.T) {
netip.MustParseAddr("192.0.2.11"),
netip.MustParseAddr("192.0.2.15"),
}
portsExpected := []uint16{554, 8554, 8555}
portsExpected := []uint16{554, 322, 8554, 8555}
var want []string
for _, addr := range addrs {
@@ -44,6 +44,13 @@ func TestNew_ExpandsTargetsAndPorts(t *testing.T) {
var got []string
for _, stream := range streams {
got = append(got, stream.Address.String()+":"+strconv.Itoa(int(stream.Port)))
if stream.Port == 322 || stream.Port == 8322 {
assert.True(t, stream.Secure)
} else {
assert.False(t, stream.Secure)
}
}
assert.ElementsMatch(t, want, got)
+6 -1
View File
@@ -126,7 +126,12 @@ func formatRTSPURL(stream cameradar.Stream) string {
credentials = stream.Username + ":" + stream.Password + "@"
}
return fmt.Sprintf("rtsp://%s%s:%d%s", credentials, stream.Address.String(), stream.Port, path)
scheme := "rtsp"
if stream.Secure {
scheme = "rtsps"
}
return fmt.Sprintf("%s://%s%s:%d%s", scheme, credentials, stream.Address.String(), stream.Port, path)
}
func formatAdminPanelURL(stream cameradar.Stream) string {
+2 -1
View File
@@ -46,6 +46,7 @@ func TestFormatSummary(t *testing.T) {
Device: "Model A",
Address: netip.MustParseAddr("10.0.0.1"),
Port: 8554,
Secure: true,
Available: true,
Routes: []string{"stream1", "stream2"},
RouteFound: true,
@@ -73,7 +74,7 @@ func TestFormatSummary(t *testing.T) {
"Authentication: digest",
"Routes: stream1, stream2",
"Credentials: user:pass",
"RTSP URL: rtsp://user:pass@10.0.0.1:8554/stream1",
"RTSP URL: rtsps://user:pass@10.0.0.1:8554/stream1",
"Admin panel: http://10.0.0.1/",
"Admin panel: http://10.0.0.2/",
},
+2
View File
@@ -24,6 +24,8 @@ type Stream struct {
Address netip.Addr `json:"address" validate:"required"`
Port uint16 `json:"port" validate:"required"`
Secure bool `json:"secure"`
CredentialsFound bool `json:"credentials_found"`
RouteFound bool `json:"route_found"`
Available bool `json:"available"`