ptrstream

- endless stream of rdns
git clone git://git.acid.vegas/ptrstream.git
Log | Files | Refs | Archive | README | LICENSE

commit 2f4438c213caa3b4b5c136109ce8b0816564dbf2
parent 2669371ef4a818087a020461c764a5eaf0661e6a
Author: acidvegas <acid.vegas@acid.vegas>
Date: Sun, 5 Jan 2025 04:31:09 -0500

added elapsed timed and ttl colors based on the values

Diffstat:
M.screens/preview.gif | 0
MREADME.md | 1+
Mptrstream.go | 141+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------

3 files changed, 107 insertions(+), 35 deletions(-)

diff --git a/.screens/preview.gif b/.screens/preview.gif
Binary files differ.
diff --git a/README.md b/README.md
@@ -27,6 +27,7 @@ go install github.com/acidvegas/ptrstream@latest
 | `-c`     | `int`    | `100`   | Concurrency level                          |
 | `-debug` | `bool`   | `false` | Show unsuccessful lookups                  |
 | `-dns`   | `string` |         | File containing DNS servers                |
+| `-l`     | `bool`   | `false` | Loop continuously after completion         |
 | `-o`     | `string` |         | Path to NDJSON output file                 |
 | `-r`     | `int`    | `2`     | Number of retries for failed lookups       |
 | `-s`     | `int`    | `0`     | Seed for IP generation *(0 for random)*    |
diff --git a/ptrstream.go b/ptrstream.go
@@ -33,6 +33,7 @@ type Config struct {
 	mu            sync.Mutex
 	lastDNSUpdate time.Time
 	updateMu      sync.Mutex
+	loop          bool
 }
 
 type Stats struct {
@@ -40,6 +41,7 @@ type Stats struct {
 	total         uint64
 	lastProcessed uint64
 	lastCheckTime time.Time
+	startTime     time.Time
 	success       uint64
 	failed        uint64
 	cnames        uint64
@@ -272,7 +274,7 @@ func colorizeIPInPtr(ptr, ip string) string {
 
 	matches := re.FindAllStringIndex(ptr, -1)
 	if matches == nil {
-		return "[green]" + ptr
+		return "[white]" + ptr
 	}
 
 	var result strings.Builder
@@ -280,7 +282,7 @@ func colorizeIPInPtr(ptr, ip string) string {
 
 	for _, match := range matches {
 		if match[0] > lastEnd {
-			result.WriteString("[green]")
+			result.WriteString("[white]")
 			result.WriteString(ptr[lastEnd:match[0]])
 		}
 		result.WriteString("[aqua]")
@@ -289,7 +291,7 @@ func colorizeIPInPtr(ptr, ip string) string {
 	}
 
 	if lastEnd < len(ptr) {
-		result.WriteString("[green]")
+		result.WriteString("[white]")
 		result.WriteString(ptr[lastEnd:])
 	}
 
@@ -395,19 +397,19 @@ func worker(jobs <-chan string, wg *sync.WaitGroup, cfg *Config, stats *Stats, t
 
 		var line string
 		if len(cfg.dnsServers) > 0 {
-			line = fmt.Sprintf("[gray]%s [gray]│[-] [purple]%15s[-] [gray]│[-] [yellow]%-15s[-] [gray]│[-] %-5s [gray]│[-] [white]%-6d[-] [gray]│[-] %s\n",
+			line = fmt.Sprintf("[gray]%s [gray]│[-] [purple]%15s[-] [gray]│[-] [aqua]%-15s[-] [gray]│[-] %-5s [gray]│[-] %s [gray]│[-] %s\n",
 				timeStr,
 				ip,
 				response.Server,
 				recordTypeColor,
-				response.TTL,
+				colorizeTTL(response.TTL),
 				colorizeIPInPtr(ptr, ip))
 		} else {
-			line = fmt.Sprintf("[gray]%s [gray]│[-] [purple]%15s[-] [gray]│[-] %-5s [gray]│[-] [white]%-6d[-] [gray]│[-] %s\n",
+			line = fmt.Sprintf("[gray]%s [gray]│[-] [purple]%15s[-] [gray]│[-] %-5s [gray]│[-] %s [gray]│[-] %s\n",
 				timeStr,
 				ip,
 				recordTypeColor,
-				response.TTL,
+				colorizeTTL(response.TTL),
 				colorizeIPInPtr(ptr, ip))
 		}
 
@@ -493,6 +495,7 @@ func main() {
 	outputPath := flag.String("o", "", "Path to NDJSON output file")
 	seed := flag.Int64("s", 0, "Seed for IP generation (0 for random)")
 	shard := flag.String("shard", "", "Shard specification (e.g., 1/4 for first shard of 4)")
+	loop := flag.Bool("l", false, "Loop continuously after completion")
 	flag.Parse()
 
 	shardNum, totalShards, err := parseShardArg(*shard)
@@ -524,6 +527,7 @@ func main() {
 		debug:         *debug,
 		dnsServers:    servers,
 		lastDNSUpdate: time.Now(),
+		loop:          *loop,
 	}
 
 	if *outputPath != "" {
@@ -554,11 +558,12 @@ func main() {
 	flex := tview.NewFlex().
 		SetDirection(tview.FlexRow).
 		AddItem(textView, 0, 1, false).
-		AddItem(progress, 3, 0, false)
+		AddItem(progress, 4, 0, false)
 
 	stats := &Stats{
 		total:         1 << 32,
 		lastCheckTime: time.Now(),
+		startTime:     time.Now(),
 	}
 
 	go func() {
@@ -601,24 +606,24 @@ func main() {
 						return
 					}
 
-					statsText := fmt.Sprintf(" [aqua]Count:[:-] [white]%s [gray]│[-] [aqua]Progress:[:-] [darkgray]%7.2f%%[-] [gray]│[-] [aqua]Rate:[:-] %s [gray]│[-] [aqua]CNAMEs:[:-] [yellow]%s[-] [gray]│[-] [aqua]Successful:[:-] [green]✓%s [-][darkgray](%5.1f%%)[-] [gray]│[-] [aqua]Failed:[:-] [red]✗%s [-][darkgray](%5.1f%%)[-] ",
+					// First line: stats
+					statsLine := fmt.Sprintf(" [aqua]Elapsed:[:-] [white]%s [gray]│[-] [aqua]Count:[:-] [white]%s [gray]│[-] [aqua]Progress:[:-] [darkgray]%7.2f%%[-] [gray]│[-] [aqua]Rate:[:-] %s [gray]│[-] [aqua]CNAMEs:[:-] [yellow]%s [-][darkgray](%5.1f%%)[-] [gray]│[-] [aqua]Successful:[:-] [green]✓%s [-][darkgray](%5.1f%%)[-] [gray]│[-] [aqua]Failed:[:-] [red]✗%s [-][darkgray](%5.1f%%)[-]\n",
+						formatDuration(time.Since(stats.startTime)),
 						formatNumber(processed),
 						percent,
 						colorizeSpeed(avgSpeed),
 						formatNumber(atomic.LoadUint64(&stats.cnames)),
+						float64(atomic.LoadUint64(&stats.cnames))/float64(processed)*100,
 						formatNumber(success),
 						float64(success)/float64(processed)*100,
 						formatNumber(failed),
 						float64(failed)/float64(processed)*100)
 
-					textWidth := visibleLength(statsText)
-					barWidth := width - textWidth - 2 // -2 for the [] characters
-
-					// Ensure barWidth is at least 1
+					// Second line: progress bar
+					barWidth := width - 3 // -3 for the [] and space
 					if barWidth < 1 {
-						// If there's not enough space, just show the stats without the progress bar
 						progress.Clear()
-						fmt.Fprint(progress, statsText)
+						fmt.Fprint(progress, statsLine)
 						return
 					}
 
@@ -627,15 +632,13 @@ func main() {
 						filled = barWidth
 					}
 
-					bar := strings.Builder{}
-					bar.WriteString(statsText)
-					bar.WriteString("[")
-					bar.WriteString(strings.Repeat("█", filled))
-					bar.WriteString(strings.Repeat("░", barWidth-filled))
-					bar.WriteString("]")
+					barLine := fmt.Sprintf(" [%s%s]",
+						strings.Repeat("█", filled),
+						strings.Repeat("░", barWidth-filled))
 
+					// Combine both lines with explicit newline
 					progress.Clear()
-					fmt.Fprint(progress, bar.String())
+					fmt.Fprintf(progress, "%s%s", statsLine, barLine)
 				})
 			}
 
@@ -643,14 +646,27 @@ func main() {
 		}
 	}()
 
-	stream, err := golcg.IPStream("0.0.0.0/0", shardNum, totalShards, int(*seed), nil)
-	if err != nil {
-		fmt.Printf("Error creating IP stream: %v\n", err)
-		return
-	}
-
 	jobs := make(chan string, cfg.concurrency)
 
+	go func() {
+		for {
+			stream, err := golcg.IPStream("0.0.0.0/0", shardNum, totalShards, int(*seed), nil)
+			if err != nil {
+				fmt.Printf("Error creating IP stream: %v\n", err)
+				return
+			}
+
+			for ip := range stream {
+				jobs <- ip
+			}
+
+			if !cfg.loop {
+				break
+			}
+		}
+		close(jobs)
+	}()
+
 	var wg sync.WaitGroup
 	for i := 0; i < cfg.concurrency; i++ {
 		wg.Add(1)
@@ -658,13 +674,6 @@ func main() {
 	}
 
 	go func() {
-		for ip := range stream {
-			jobs <- ip
-		}
-		close(jobs)
-	}()
-
-	go func() {
 		wg.Wait()
 		app.Stop()
 	}()
@@ -744,3 +753,65 @@ func writeNDJSON(cfg *Config, timestamp time.Time, ip, server, ptr, recordType, 
 		cfg.mu.Unlock()
 	}
 }
+
+func formatDuration(d time.Duration) string {
+	d = d.Round(time.Second)
+
+	days := d / (24 * time.Hour)
+	d -= days * 24 * time.Hour
+
+	hours := d / time.Hour
+	d -= hours * time.Hour
+
+	minutes := d / time.Minute
+	d -= minutes * time.Minute
+
+	seconds := d / time.Second
+
+	var result string
+
+	if days > 0 {
+		if hours > 0 && minutes > 0 {
+			result = fmt.Sprintf("%dd %dh %dm", days, hours, minutes)
+		} else if hours > 0 {
+			result = fmt.Sprintf("%dd %dh", days, hours)
+		} else {
+			result = fmt.Sprintf("%dd", days)
+		}
+	} else if hours > 0 {
+		if minutes > 0 {
+			result = fmt.Sprintf("%dh %dm", hours, minutes)
+		} else {
+			result = fmt.Sprintf("%dh", hours)
+		}
+	} else if minutes > 0 {
+		if seconds > 0 {
+			result = fmt.Sprintf("%dm %ds", minutes, seconds)
+		} else {
+			result = fmt.Sprintf("%dm", minutes)
+		}
+	} else {
+		result = fmt.Sprintf("%ds", seconds)
+	}
+
+	// Pad to exactly 14 characters
+	for len(result) < 14 {
+		result = " " + result
+	}
+	return result
+}
+
+func colorizeTTL(ttl uint32) string {
+	switch {
+	case ttl >= 86400: // 1 day or more
+		return fmt.Sprintf("[#00FF00::b]%-6d[-]", ttl) // Bright green with bold
+	case ttl >= 3600: // 1 hour or more
+		return fmt.Sprintf("[yellow]%-6d[-]", ttl)
+	case ttl >= 300: // 5 minutes or more
+		return fmt.Sprintf("[orange]%-6d[-]", ttl)
+	case ttl >= 60: // 1 minute or more
+		return fmt.Sprintf("[red]%-6d[-]", ttl)
+	default: // Less than 60 seconds
+		return fmt.Sprintf("[gray]%-6d[-]", ttl)
+	}
+}