feat: v6 rewrite
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
package nmap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"strings"
|
||||
|
||||
"github.com/Ullaakut/cameradar/v6"
|
||||
nmaplib "github.com/Ullaakut/nmap/v4"
|
||||
)
|
||||
|
||||
// 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 an nmap scan.
|
||||
type Runner interface {
|
||||
Run(ctx context.Context) (*nmaplib.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 terminal and scan speed.
|
||||
func New(scanSpeed int16, targets, ports []string, reporter Reporter) (*Scanner, error) {
|
||||
runner, err := nmaplib.NewScanner(
|
||||
nmaplib.WithTargets(targets...),
|
||||
nmaplib.WithPorts(ports...),
|
||||
nmaplib.WithServiceInfo(),
|
||||
nmaplib.WithTimingTemplate(nmaplib.Timing(scanSpeed)),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("creating nmap 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, nmap Runner, reporter Reporter) ([]cameradar.Stream, error) {
|
||||
results, err := nmap.Run(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("scanning network: %w", err)
|
||||
}
|
||||
|
||||
for _, warning := range results.Warnings() {
|
||||
reporter.Debug(cameradar.StepScan, "nmap warning: "+warning)
|
||||
}
|
||||
|
||||
var streams []cameradar.Stream
|
||||
for _, host := range results.Hosts {
|
||||
for _, port := range host.Ports {
|
||||
if port.Status() != "open" {
|
||||
continue
|
||||
}
|
||||
|
||||
if !strings.Contains(port.Service.Name, "rtsp") {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, address := range host.Addresses {
|
||||
addr, err := netip.ParseAddr(address.Addr)
|
||||
if err != nil {
|
||||
reporter.Progress(cameradar.StepScan, fmt.Sprintf("Skipping invalid address %q: %v", address.Addr, err))
|
||||
continue
|
||||
}
|
||||
|
||||
streams = append(streams, cameradar.Stream{
|
||||
Device: port.Service.Product,
|
||||
Address: addr,
|
||||
Port: port.ID,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,187 @@
|
||||
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,
|
||||
},
|
||||
},
|
||||
wantProgress: "Found 1 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,
|
||||
},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user