8531c006d4
* enhancement: supporting http tunneled rtsp * refactor: simplify HTTP tunnel support per review feedback - Extract streamCandidate() for nmap port classification - Add isCommonHTTPPort() for masscan and nmap fallback - Move URL building to Stream.String() and Stream.URL() - Pass Stream directly to attack methods instead of individual args - Add TLS config for HTTPS tunnel support - Make auth detection non-fatal for tunneled streams - Rename HTTPTunnel to UseHTTPTunnel * - Testing the auth workflow for the tunneled streams is not blocking the rest of the pipeline since I changed the return values to Auth unknown and nil - added extra ports in the test according to suggestions * fixing some lint errors * removing the unused buildrtspurl * delayed the urlstream call for clarity removed error messages refactored the test that used the deprecated buildTRSPurl to use stream.URL and stream.String() methods * extracting iscommonHTTP port to pkg/ports (package ports) switching on u.scheme to create proper schemes for http and https * refactor: replace HTTP tunnel bool with scheme-based detection; enable TLS only for HTTPS-tunneled streams * chore: simnplify InferTunnelScheme and newRTSPClient * fix: remove rendundant check in streamCandidate * fix: typo in parseScheme * tests: coverage for new schemes * fix: use RTSP and not RTSPS for HTTPS URLs * fix: tunneled RTSP scheme handling and auth detection fallback * ui: render empty credentials as none in summary and TUI * chore: ignore duplicate-string warning for none literal * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * fix: rtsps probe headers --------- Co-authored-by: Brendan Le Glaunec <brendan@glaulabs.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
236 lines
5.3 KiB
Go
236 lines
5.3 KiB
Go
package nmap
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"net/netip"
|
|
"sync"
|
|
"testing"
|
|
|
|
"github.com/Ullaakut/cameradar/v6"
|
|
nmaplib "github.com/Ullaakut/nmap/v4"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestScanner_Scan(t *testing.T) {
|
|
ctx := context.WithValue(t.Context(), contextKey("trace"), "scan")
|
|
|
|
tests := []struct {
|
|
name string
|
|
result *nmaplib.Run
|
|
err error
|
|
wantStreams []cameradar.Stream
|
|
wantDebug []string
|
|
wantProgress string
|
|
wantErrContains string
|
|
}{
|
|
{
|
|
name: "filters non-rtsp and closed ports",
|
|
result: buildRun(nmaplib.Host{
|
|
Addresses: []nmaplib.Address{
|
|
{Addr: "127.0.0.1"},
|
|
{Addr: "not-an-ip"},
|
|
},
|
|
Ports: []nmaplib.Port{
|
|
openPort(8554, "rtsp", "ACME"),
|
|
closedPort(554, "rtsp", "ACME"),
|
|
openPort(80, "http", "ACME"),
|
|
},
|
|
}),
|
|
wantStreams: []cameradar.Stream{
|
|
{
|
|
Device: "ACME",
|
|
Address: netip.MustParseAddr("127.0.0.1"),
|
|
Port: 8554,
|
|
},
|
|
{
|
|
Device: "ACME",
|
|
Address: netip.MustParseAddr("127.0.0.1"),
|
|
Port: 80,
|
|
Scheme: "http",
|
|
},
|
|
},
|
|
wantProgress: "Found 2 RTSP streams",
|
|
},
|
|
{
|
|
name: "keeps rtsp and http candidates while filtering closed ports",
|
|
result: buildRun(nmaplib.Host{
|
|
Addresses: []nmaplib.Address{
|
|
{Addr: "127.0.0.1"},
|
|
{Addr: "not-an-ip"},
|
|
},
|
|
Ports: []nmaplib.Port{
|
|
openPort(8554, "rtsp", "ACME"),
|
|
closedPort(554, "rtsp", "ACME"),
|
|
openPort(80, "http", "ACME"),
|
|
openPort(9443, "https", "ACME"),
|
|
openPort(8443, "", "ACME"),
|
|
},
|
|
}),
|
|
wantStreams: []cameradar.Stream{
|
|
{
|
|
Device: "ACME",
|
|
Address: netip.MustParseAddr("127.0.0.1"),
|
|
Port: 8554,
|
|
},
|
|
{
|
|
Device: "ACME",
|
|
Address: netip.MustParseAddr("127.0.0.1"),
|
|
Port: 80,
|
|
Scheme: "http",
|
|
},
|
|
{
|
|
Device: "ACME",
|
|
Address: netip.MustParseAddr("127.0.0.1"),
|
|
Port: 9443,
|
|
Scheme: "https",
|
|
},
|
|
{
|
|
Device: "ACME",
|
|
Address: netip.MustParseAddr("127.0.0.1"),
|
|
Port: 8443,
|
|
Scheme: "https",
|
|
},
|
|
},
|
|
wantProgress: "Found 4 RTSP streams",
|
|
},
|
|
{
|
|
name: "collects multiple hosts",
|
|
result: buildRun(
|
|
nmaplib.Host{
|
|
Addresses: []nmaplib.Address{{Addr: "192.0.2.10"}, {Addr: "192.0.2.11"}},
|
|
Ports: []nmaplib.Port{
|
|
openPort(8554, "rtsp-alt", "Model A"),
|
|
},
|
|
},
|
|
nmaplib.Host{
|
|
Addresses: []nmaplib.Address{{Addr: "198.51.100.9"}},
|
|
Ports: []nmaplib.Port{
|
|
openPort(554, "rtsp", "Model B"),
|
|
},
|
|
},
|
|
),
|
|
wantStreams: []cameradar.Stream{
|
|
{
|
|
Device: "Model A",
|
|
Address: netip.MustParseAddr("192.0.2.10"),
|
|
Port: 8554,
|
|
},
|
|
{
|
|
Device: "Model A",
|
|
Address: netip.MustParseAddr("192.0.2.11"),
|
|
Port: 8554,
|
|
},
|
|
{
|
|
Device: "Model B",
|
|
Address: netip.MustParseAddr("198.51.100.9"),
|
|
Port: 554,
|
|
},
|
|
},
|
|
wantProgress: "Found 3 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{}
|
|
|
|
scanner, err := New(4, []string{"192.0.2.1"}, []string{"554", "8554"}, reporter)
|
|
require.NoError(t, err)
|
|
|
|
scanner.runner = fakeRunner{result: test.result, err: test.err}
|
|
|
|
streams, err := scanner.Scan(ctx)
|
|
|
|
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)
|
|
assert.Contains(t, reporter.progress, test.wantProgress)
|
|
})
|
|
}
|
|
}
|
|
|
|
type contextKey string
|
|
|
|
type fakeRunner struct {
|
|
result *nmaplib.Run
|
|
err error
|
|
}
|
|
|
|
func (f fakeRunner) Run(context.Context) (*nmaplib.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() {}
|
|
|
|
func buildRun(hosts ...nmaplib.Host) *nmaplib.Run {
|
|
return &nmaplib.Run{Hosts: hosts}
|
|
}
|
|
|
|
func openPort(id uint16, serviceName, product string) nmaplib.Port {
|
|
return nmaplib.Port{
|
|
ID: id,
|
|
State: nmaplib.State{
|
|
State: string(nmaplib.Open),
|
|
},
|
|
Service: nmaplib.Service{
|
|
Name: serviceName,
|
|
Product: product,
|
|
},
|
|
}
|
|
}
|
|
|
|
func closedPort(id uint16, serviceName, product string) nmaplib.Port {
|
|
return nmaplib.Port{
|
|
ID: id,
|
|
State: nmaplib.State{
|
|
State: string(nmaplib.Closed),
|
|
},
|
|
Service: nmaplib.Service{
|
|
Name: serviceName,
|
|
Product: product,
|
|
},
|
|
}
|
|
}
|