Files
cameradar/internal/ui/summary.go
T
Lawrence Arryl Lopez 8531c006d4 feat: support http tunneled rtsp (#419)
* enhancement: supporting http tunneled rtsp

* refactor: simplify HTTP tunnel support per review feedback

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

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

* fixing some lint errors

* removing the unused buildrtspurl

* delayed the urlstream call for clarity

removed error messages

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

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

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

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

* chore: simnplify InferTunnelScheme and newRTSPClient

* fix: remove rendundant check in streamCandidate

* fix: typo in parseScheme

* tests: coverage for new schemes

* fix: use RTSP and not RTSPS for HTTPS URLs

* fix: tunneled RTSP scheme handling and auth detection fallback

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

* chore: ignore duplicate-string warning for none literal

* Potential fix for pull request finding

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

* fix: rtsps probe headers

---------

Co-authored-by: Brendan Le Glaunec <brendan@glaulabs.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-18 06:41:20 +01:00

154 lines
4.0 KiB
Go

package ui
import (
"fmt"
"sort"
"strconv"
"strings"
"github.com/Ullaakut/cameradar/v6"
)
// FormatSummary builds a human-readable summary of discovered streams.
func FormatSummary(streams []cameradar.Stream, _ error) string {
accessible, others := partitionStreams(streams)
var builder strings.Builder
builder.WriteString(fmt.Sprintf("Accessible streams: %d\n", len(accessible)))
if len(accessible) == 0 {
builder.WriteString("• None\n")
} else {
for _, stream := range accessible {
builder.WriteString(formatStream(stream))
}
}
if len(others) > 0 {
builder.WriteString("\n")
builder.WriteString(fmt.Sprintf("Other discovered streams: %d\n", len(others)))
for _, stream := range others {
builder.WriteString(formatStream(stream))
}
}
return builder.String()
}
func partitionStreams(streams []cameradar.Stream) ([]cameradar.Stream, []cameradar.Stream) {
var accessible []cameradar.Stream
var others []cameradar.Stream
for _, stream := range streams {
if stream.Available {
accessible = append(accessible, stream)
} else {
others = append(others, stream)
}
}
// Sort streams by address and port.
sort.Slice(accessible, func(i, j int) bool {
if accessible[i].Address.String() == accessible[j].Address.String() {
return accessible[i].Port < accessible[j].Port
}
return accessible[i].Address.String() < accessible[j].Address.String()
})
sort.Slice(others, func(i, j int) bool {
if others[i].Address.String() == others[j].Address.String() {
return others[i].Port < others[j].Port
}
return others[i].Address.String() < others[j].Address.String()
})
return accessible, others
}
func formatStream(stream cameradar.Stream) string {
var builder strings.Builder
builder.WriteString("• ")
builder.WriteString(stream.Address.String())
builder.WriteString(":")
builder.WriteString(strconv.FormatUint(uint64(stream.Port), 10))
if stream.Device != "" {
builder.WriteString(" (")
builder.WriteString(stream.Device)
builder.WriteString(")")
}
builder.WriteString("\n")
builder.WriteString(" Authentication: ")
builder.WriteString(authTypeLabel(stream.AuthenticationType))
builder.WriteString("\n")
if len(stream.Routes) > 0 {
builder.WriteString(" Routes: ")
builder.WriteString(strings.Join(stream.Routes, ", "))
builder.WriteString("\n")
} else {
builder.WriteString(" Routes: not found\n")
}
if stream.CredentialsFound || stream.AuthenticationType == cameradar.AuthNone {
builder.WriteString(" Credentials: ")
builder.WriteString(formatCredentials(stream))
builder.WriteString("\n")
} else {
builder.WriteString(" Credentials: not found\n")
}
builder.WriteString(" Availability: ")
if stream.Available {
builder.WriteString("yes\n")
} else {
builder.WriteString("no\n")
}
if stream.RouteFound && (stream.CredentialsFound || stream.AuthenticationType == cameradar.AuthNone) {
builder.WriteString(" RTSP URL: ")
builder.WriteString(formatRTSPURL(stream))
builder.WriteString("\n")
}
builder.WriteString(" Admin panel: ")
builder.WriteString(formatAdminPanelURL(stream))
builder.WriteString("\n")
return builder.String()
}
func formatRTSPURL(stream cameradar.Stream) string {
path := "/" + strings.TrimLeft(strings.TrimSpace(stream.Route()), "/")
credentials := ""
if stream.Username != "" || stream.Password != "" {
credentials = stream.Username + ":" + stream.Password + "@"
}
return fmt.Sprintf("rtsp://%s%s:%d%s", credentials, stream.Address.String(), stream.Port, path)
}
func formatAdminPanelURL(stream cameradar.Stream) string {
return fmt.Sprintf("http://%s/", stream.Address.String())
}
func formatCredentials(stream cameradar.Stream) string {
if stream.Username == "" && stream.Password == "" {
return "none"
}
return stream.Username + ":" + stream.Password
}
func authTypeLabel(auth cameradar.AuthType) string {
switch auth {
case cameradar.AuthNone:
return "none"
case cameradar.AuthBasic:
return "basic"
case cameradar.AuthDigest:
return "digest"
default:
return fmt.Sprintf("unknown(%d)", auth)
}
}