1
1
// src/main.rs
2
2
use clap:: Parser ;
3
3
use futures:: stream:: { self , StreamExt } ;
4
- use serde:: Serialize ;
5
- use std:: time:: Duration ;
4
+ use serde:: { Serialize , Deserialize } ;
5
+ use std:: path:: PathBuf ;
6
+ use std:: fs;
7
+ use std:: io:: { self , BufRead } ;
8
+ use std:: time:: { Duration , SystemTime , UNIX_EPOCH } ;
6
9
use trust_dns_resolver:: config:: { ResolverConfig , ResolverOpts } ;
7
10
use trust_dns_resolver:: TokioAsyncResolver ;
8
11
@@ -13,20 +16,32 @@ use trust_dns_resolver::TokioAsyncResolver;
13
16
version
14
17
) ]
15
18
struct Cli {
16
- /// Domain names to check
17
- #[ arg( required = true ) ]
19
+ /// Domain names to check (optional if reading from stdin)
20
+ #[ arg( required = false ) ]
18
21
domains : Vec < String > ,
19
22
20
23
/// Maximum number of concurrent checks
21
24
#[ arg( short, long, default_value = "10" ) ]
22
25
concurrent : usize ,
23
26
24
- /// Output as JSON
27
+ /// Output as JSON to stdout
25
28
#[ arg( short, long) ]
26
29
json : bool ,
30
+
31
+ /// Save output to JSON file
32
+ #[ arg( long) ]
33
+ output_file : Option < PathBuf > ,
34
+
35
+ /// Include timestamp in output
36
+ #[ arg( short, long) ]
37
+ timestamp : bool ,
38
+
39
+ /// Strip whitespace and empty lines from input
40
+ #[ arg( short, long) ]
41
+ clean : bool ,
27
42
}
28
43
29
- #[ derive( Debug , Serialize ) ]
44
+ #[ derive( Debug , Serialize , Deserialize ) ]
30
45
struct DomainStatus {
31
46
domain : String ,
32
47
registered : bool ,
@@ -37,6 +52,22 @@ struct DomainStatus {
37
52
error : Option < String > ,
38
53
}
39
54
55
+ #[ derive( Debug , Serialize , Deserialize ) ]
56
+ struct CheckResult {
57
+ timestamp : Option < u64 > ,
58
+ check_count : usize ,
59
+ domains : Vec < DomainStatus > ,
60
+ summary : ResultSummary ,
61
+ }
62
+
63
+ #[ derive( Debug , Serialize , Deserialize ) ]
64
+ struct ResultSummary {
65
+ total_checked : usize ,
66
+ registered : usize ,
67
+ unregistered : usize ,
68
+ errors : usize ,
69
+ }
70
+
40
71
struct DomainChecker {
41
72
resolver : TokioAsyncResolver ,
42
73
}
@@ -47,7 +78,10 @@ impl DomainChecker {
47
78
opts. timeout = Duration :: from_secs ( 2 ) ;
48
79
opts. attempts = 2 ;
49
80
50
- let resolver = TokioAsyncResolver :: tokio ( ResolverConfig :: cloudflare ( ) , opts) ;
81
+ let resolver = TokioAsyncResolver :: tokio (
82
+ ResolverConfig :: cloudflare ( ) ,
83
+ opts,
84
+ ) ;
51
85
52
86
Self { resolver }
53
87
}
@@ -68,7 +102,10 @@ impl DomainChecker {
68
102
Ok ( ns_records) => {
69
103
status. has_dns = true ;
70
104
status. registered = true ;
71
- status. nameservers = ns_records. iter ( ) . map ( |record| record. to_string ( ) ) . collect ( ) ;
105
+ status. nameservers = ns_records
106
+ . iter ( )
107
+ . map ( |record| record. to_string ( ) )
108
+ . collect ( ) ;
72
109
}
73
110
Err ( e) => match e. kind ( ) {
74
111
trust_dns_resolver:: error:: ResolveErrorKind :: NoRecordsFound { .. } => { }
@@ -85,7 +122,10 @@ impl DomainChecker {
85
122
Ok ( ips) => {
86
123
status. has_ip = true ;
87
124
status. registered = true ;
88
- status. ip_addresses = ips. iter ( ) . map ( |ip| ip. to_string ( ) ) . collect ( ) ;
125
+ status. ip_addresses = ips
126
+ . iter ( )
127
+ . map ( |ip| ip. to_string ( ) )
128
+ . collect ( ) ;
89
129
}
90
130
Err ( e) => {
91
131
if !status. registered {
@@ -102,11 +142,7 @@ impl DomainChecker {
102
142
status
103
143
}
104
144
105
- async fn check_domains (
106
- & self ,
107
- domains : Vec < String > ,
108
- concurrent_limit : usize ,
109
- ) -> Vec < DomainStatus > {
145
+ async fn check_domains ( & self , domains : Vec < String > , concurrent_limit : usize ) -> Vec < DomainStatus > {
110
146
stream:: iter ( domains)
111
147
. map ( |domain| self . check_domain ( domain) )
112
148
. buffer_unordered ( concurrent_limit)
@@ -115,39 +151,125 @@ impl DomainChecker {
115
151
}
116
152
}
117
153
154
+ fn create_check_result ( domains : Vec < DomainStatus > , include_timestamp : bool ) -> CheckResult {
155
+ let total_checked = domains. len ( ) ;
156
+ let registered = domains. iter ( ) . filter ( |d| d. registered ) . count ( ) ;
157
+ let unregistered = domains. iter ( ) . filter ( |d| !d. registered ) . count ( ) ;
158
+ let errors = domains. iter ( ) . filter ( |d| d. error . is_some ( ) ) . count ( ) ;
159
+
160
+ CheckResult {
161
+ timestamp : if include_timestamp {
162
+ Some ( SystemTime :: now ( )
163
+ . duration_since ( UNIX_EPOCH )
164
+ . unwrap ( )
165
+ . as_secs ( ) )
166
+ } else {
167
+ None
168
+ } ,
169
+ check_count : total_checked,
170
+ domains,
171
+ summary : ResultSummary {
172
+ total_checked,
173
+ registered,
174
+ unregistered,
175
+ errors,
176
+ } ,
177
+ }
178
+ }
179
+
180
+ fn print_text_output ( result : & CheckResult ) {
181
+ if let Some ( timestamp) = result. timestamp {
182
+ println ! ( "\n Timestamp: {}" , timestamp) ;
183
+ }
184
+ println ! ( "\n Summary:" ) ;
185
+ println ! ( " Total Checked: {}" , result. summary. total_checked) ;
186
+ println ! ( " Registered: {}" , result. summary. registered) ;
187
+ println ! ( " Unregistered: {}" , result. summary. unregistered) ;
188
+ println ! ( " Errors: {}" , result. summary. errors) ;
189
+
190
+ println ! ( "\n Detailed Results:" ) ;
191
+ for status in & result. domains {
192
+ println ! ( "\n Domain: {}" , status. domain) ;
193
+ println ! ( "Registered: {}" , status. registered) ;
194
+
195
+ if !status. nameservers . is_empty ( ) {
196
+ println ! ( "Nameservers:" ) ;
197
+ for ns in & status. nameservers {
198
+ println ! ( " - {}" , ns) ;
199
+ }
200
+ }
201
+
202
+ if !status. ip_addresses . is_empty ( ) {
203
+ println ! ( "IP Addresses:" ) ;
204
+ for ip in & status. ip_addresses {
205
+ println ! ( " - {}" , ip) ;
206
+ }
207
+ }
208
+
209
+ if let Some ( error) = & status. error {
210
+ println ! ( "Error: {}" , error) ;
211
+ }
212
+ }
213
+ }
214
+
215
+ fn read_domains_from_stdin ( clean : bool ) -> io:: Result < Vec < String > > {
216
+ let stdin = io:: stdin ( ) ;
217
+ let mut domains = Vec :: new ( ) ;
218
+
219
+ for line in stdin. lock ( ) . lines ( ) {
220
+ let line = line?;
221
+ if clean {
222
+ let trimmed = line. trim ( ) ;
223
+ if !trimmed. is_empty ( ) {
224
+ domains. push ( trimmed. to_string ( ) ) ;
225
+ }
226
+ } else {
227
+ domains. push ( line) ;
228
+ }
229
+ }
230
+
231
+ Ok ( domains)
232
+ }
233
+
118
234
#[ tokio:: main]
119
235
async fn main ( ) -> Result < ( ) , Box < dyn std:: error:: Error > > {
120
236
let cli = Cli :: parse ( ) ;
121
237
let checker = DomainChecker :: new ( ) . await ;
122
238
123
- let results = checker . check_domains ( cli . domains , cli . concurrent ) . await ;
124
-
125
- if cli . json {
126
- println ! ( "{}" , serde_json :: to_string_pretty ( & results ) ? ) ;
239
+ // Get domains from either command line args or stdin
240
+ let domains = if cli . domains . is_empty ( ) {
241
+ // No domains provided as arguments, try reading from stdin
242
+ read_domains_from_stdin ( cli . clean ) ?
127
243
} else {
128
- for status in results {
129
- println ! ( "\n Domain: {}" , status. domain) ;
130
- println ! ( "Registered: {}" , status. registered) ;
131
-
132
- if !status. nameservers . is_empty ( ) {
133
- println ! ( "Nameservers:" ) ;
134
- for ns in status. nameservers {
135
- println ! ( " - {}" , ns) ;
136
- }
137
- }
244
+ cli. domains
245
+ } ;
138
246
139
- if !status. ip_addresses . is_empty ( ) {
140
- println ! ( "IP Addresses:" ) ;
141
- for ip in status. ip_addresses {
142
- println ! ( " - {}" , ip) ;
143
- }
144
- }
247
+ // Verify we have domains to check
248
+ if domains. is_empty ( ) {
249
+ eprintln ! ( "Error: No domains provided. Either specify domains as arguments or pipe them through stdin." ) ;
250
+ std:: process:: exit ( 1 ) ;
251
+ }
145
252
146
- if let Some ( error) = status. error {
147
- println ! ( "Error: {}" , error) ;
148
- }
253
+ let results = checker
254
+ . check_domains ( domains, cli. concurrent )
255
+ . await ;
256
+
257
+ let check_result = create_check_result ( results, cli. timestamp ) ;
258
+
259
+ // Handle output based on flags
260
+ if cli. json || cli. output_file . is_some ( ) {
261
+ let json = serde_json:: to_string_pretty ( & check_result) ?;
262
+
263
+ if cli. json {
264
+ println ! ( "{}" , json) ;
265
+ }
266
+
267
+ if let Some ( path) = cli. output_file {
268
+ fs:: write ( path, json) ?;
149
269
}
270
+ } else {
271
+ print_text_output ( & check_result) ;
150
272
}
151
273
152
274
Ok ( ( ) )
153
- }
275
+ }
0 commit comments