1use serde::{Deserialize, Serialize};
8use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt, BufReader};
9use tokio::net::TcpListener;
10use tokio::sync::mpsc;
11use tracing::{debug, error, trace, warn};
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
15pub enum TelnetCommand {
16 Ping(PingSpec),
18 Traceroute(TracerouteSpec),
20 Dns(DnsSpec),
22 Http(HttpSpec),
24 Tls(TlsSpec),
26 Ntp(NtpSpec),
28 HostTelemetry(HostTelemetrySpec),
31 Status,
33 Stop(u64),
35 Ignored(String),
38 Unknown(String),
40}
41
42#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
44pub enum HostTelemetryKind {
45 Buddyinfo,
46 Rptaddrs,
47}
48
49#[derive(Debug, Clone, Serialize, Deserialize)]
51pub struct HostTelemetrySpec {
52 pub kind: HostTelemetryKind,
53 pub interval: u64,
55 pub msm_id: Option<u32>,
59 pub lowmem: Option<u32>,
61}
62
63#[derive(Debug, Clone, Default, Serialize, Deserialize)]
65pub struct ScheduleSpec {
66 pub interval: u64,
68 pub start_time: i64,
70 pub stop_time: i64,
72}
73
74#[derive(Debug, Clone, Serialize, Deserialize)]
76pub struct PingSpec {
77 pub msm_id: u64,
78 pub target: String,
79 pub af: u8, pub packets: u32,
81 pub size: u16,
82 pub packet_interval: u32, pub spread: Option<u32>, pub schedule: ScheduleSpec,
85}
86
87#[derive(Debug, Clone, Serialize, Deserialize)]
89pub struct TracerouteSpec {
90 pub msm_id: u64,
91 pub target: String,
92 pub af: u8,
93 pub protocol: String, pub paris: Option<u32>,
95 pub first_hop: u8,
96 pub max_hops: u8,
97 pub size: u16,
98 pub spread: Option<u32>,
99 pub schedule: ScheduleSpec,
100}
101
102#[derive(Debug, Clone, Serialize, Deserialize)]
104pub struct DnsSpec {
105 pub msm_id: u64,
106 pub target: String, pub af: u8,
108 pub protocol: String, pub query_type: String,
110 pub query_class: String,
111 pub query_argument: String,
112 pub use_dnssec: bool,
113 pub recursion_desired: bool,
114 pub spread: Option<u32>,
115 pub schedule: ScheduleSpec,
116}
117
118#[derive(Debug, Clone, Serialize, Deserialize)]
120pub struct HttpSpec {
121 pub msm_id: u64,
122 pub url: String,
123 pub method: String,
124 pub af: u8,
125 pub headers: Vec<String>,
126 pub body: Option<String>,
127 pub max_body_size: Option<u32>,
128 pub spread: Option<u32>,
129 pub schedule: ScheduleSpec,
130}
131
132#[derive(Debug, Clone, Serialize, Deserialize)]
134pub struct TlsSpec {
135 pub msm_id: u64,
136 pub target: String,
137 pub port: u16,
138 pub af: u8,
139 pub hostname: Option<String>,
140 pub spread: Option<u32>,
141 pub schedule: ScheduleSpec,
142}
143
144#[derive(Debug, Clone, Serialize, Deserialize)]
146pub struct NtpSpec {
147 pub msm_id: u64,
148 pub target: String,
149 pub af: u8,
150 pub packets: u32,
151 pub spread: Option<u32>,
152 pub schedule: ScheduleSpec,
153}
154
155pub struct TelnetServer {
157 port: u16,
158 probe_id: u32,
159 session_id: std::sync::Arc<tokio::sync::RwLock<Option<String>>>,
160 command_tx: Option<mpsc::Sender<TelnetCommand>>,
161}
162
163const ATLAS_LOGIN: &str = "C_TO_P_TEST_V1";
165const LOGIN_PREFIX: &str = "Atlas probe, see http://atlas.ripe.net/\r\n\r\n";
166const LOGIN_PROMPT: &str = " login: ";
167const PASSWORD_PROMPT: &str = "\r\nPassword: ";
168const RESULT_OK: &str = "OK\r\n\r\n";
169const BAD_PASSWORD: &str = "BAD_PASSWORD\r\n\r\n";
170#[allow(dead_code)]
171const BAD_COMMAND: &str = "BAD_COMMAND\r\n\r\n";
172
173impl TelnetServer {
174 pub fn new(port: u16, probe_id: u32) -> Self {
176 Self {
177 port,
178 probe_id,
179 session_id: std::sync::Arc::new(tokio::sync::RwLock::new(None)),
180 command_tx: None,
181 }
182 }
183
184 pub fn with_channel(port: u16, probe_id: u32, tx: mpsc::Sender<TelnetCommand>) -> Self {
186 Self {
187 port,
188 probe_id,
189 session_id: std::sync::Arc::new(tokio::sync::RwLock::new(None)),
190 command_tx: Some(tx),
191 }
192 }
193
194 pub async fn set_session_id(&self, session_id: String) {
196 debug!("Setting session ID for telnet authentication");
197 *self.session_id.write().await = Some(session_id);
198 }
199
200 pub fn port(&self) -> u16 {
202 self.port
203 }
204
205 pub async fn run(&self) -> anyhow::Result<()> {
207 let addr = format!("127.0.0.1:{}", self.port);
208 let listener = TcpListener::bind(&addr).await?;
209 debug!("Telnet server listening on {}", addr);
210
211 loop {
212 let (socket, remote_addr) = listener.accept().await?;
213 debug!("Accepted telnet connection from {}", remote_addr);
214
215 let command_tx = self.command_tx.clone();
216 let probe_id = self.probe_id;
217 let session_id = self.session_id.clone();
218
219 tokio::spawn(async move {
220 if let Err(e) = handle_connection(socket, command_tx, probe_id, session_id).await {
221 error!("Error handling telnet connection: {}", e);
222 }
223 debug!("Telnet connection from {} closed", remote_addr);
224 });
225 }
226 }
227}
228
229const IAC: u8 = 255; const WILL: u8 = 251;
232const WONT: u8 = 252;
233const DO: u8 = 253;
234const DONT: u8 = 254;
235const SB: u8 = 250; const SE: u8 = 240; const TELOPT_ECHO: u8 = 1;
240const TELOPT_SGA: u8 = 3; const TELOPT_NAWS: u8 = 31; #[derive(Debug, Clone, Copy, PartialEq)]
245enum ConnectionState {
246 AwaitingLoginName,
248 AwaitingPassword,
250 Authenticated,
252}
253
254pub async fn handle_connection(
259 stream: impl AsyncRead + AsyncWrite + Unpin + Send + 'static,
260 command_tx: Option<mpsc::Sender<TelnetCommand>>,
261 probe_id: u32,
262 session_id: std::sync::Arc<tokio::sync::RwLock<Option<String>>>,
263) -> anyhow::Result<()> {
264 let (reader, mut writer) = tokio::io::split(stream);
265
266 let initial_iacs: &[u8] = &[
268 IAC,
269 DO,
270 TELOPT_ECHO, IAC,
272 DO,
273 TELOPT_NAWS, IAC,
275 WILL,
276 TELOPT_ECHO, IAC,
278 WILL,
279 TELOPT_SGA, ];
281
282 if let Err(e) = writer.write_all(initial_iacs).await {
283 warn!("Failed to send telnet negotiation: {}", e);
284 }
285
286 let hostname = hostname::get()
288 .map(|h: std::ffi::OsString| h.to_string_lossy().to_string())
289 .unwrap_or_else(|_| "unknown".to_string());
290
291 let banner = format!(
292 "{}Probe {} ({}){}",
293 LOGIN_PREFIX, probe_id, hostname, LOGIN_PROMPT
294 );
295 if let Err(e) = writer.write_all(banner.as_bytes()).await {
296 warn!("Failed to send login banner: {}", e);
297 }
298 let _ = writer.flush().await;
299
300 trace!("Sent telnet login banner for probe {}", probe_id);
301
302 let mut reader = BufReader::new(reader);
303 let mut raw_buf = [0u8; 4096];
304 let mut state = ConnectionState::AwaitingLoginName;
305 let mut line_buffer = String::new();
306
307 const MAX_LINE_LEN: usize = 256 * 1024; loop {
312 use tokio::io::AsyncReadExt;
313 match reader.read(&mut raw_buf).await {
314 Ok(0) => break, Ok(n) => {
316 let text = filter_telnet_commands(&raw_buf[..n]);
318 if text.is_empty() {
319 continue;
320 }
321
322 if line_buffer.len() + text.len() > MAX_LINE_LEN {
324 error!(
325 "Telnet line buffer exceeded {}KB, disconnecting",
326 MAX_LINE_LEN / 1024
327 );
328 anyhow::bail!("Line buffer overflow");
329 }
330 line_buffer.push_str(&text);
331
332 while let Some(newline_pos) = line_buffer.find(['\n', '\r']) {
334 let line: String = line_buffer.drain(..=newline_pos).collect();
335 let line = line.trim();
336
337 if line.is_empty() {
338 continue;
339 }
340
341 debug!("Telnet state {:?}, received: '{}'", state, line);
342
343 match state {
344 ConnectionState::AwaitingLoginName => {
345 if line == ATLAS_LOGIN {
346 trace!("Atlas login received, requesting password");
348 if let Err(e) = writer.write_all(PASSWORD_PROMPT.as_bytes()).await {
349 warn!("Failed to send password prompt: {}", e);
350 }
351 let _ = writer.flush().await;
352 state = ConnectionState::AwaitingPassword;
353 } else {
354 debug!("Unknown login '{}', closing connection", line);
356 break;
357 }
358 }
359 ConnectionState::AwaitingPassword => {
360 let valid = {
362 let sid = session_id.read().await;
363 sid.as_ref().map(|s| s == line).unwrap_or(false)
364 };
365
366 if valid {
367 debug!("Telnet session authenticated");
368 if let Err(e) = writer.write_all(RESULT_OK.as_bytes()).await {
369 warn!("Failed to send OK: {}", e);
370 }
371 let _ = writer.flush().await;
372 state = ConnectionState::Authenticated;
373 } else {
374 warn!("Bad password received");
375 if let Err(e) = writer.write_all(BAD_PASSWORD.as_bytes()).await {
376 warn!("Failed to send BAD_PASSWORD: {}", e);
377 }
378 let _ = writer.flush().await;
379 break; }
381 }
382 ConnectionState::Authenticated => {
383 trace!("Received command: {}", line);
384
385 let command = parse_command(line);
386
387 let response = match &command {
389 TelnetCommand::Unknown(s) => {
390 format!("ERROR: Unknown command: {}\r\n\r\n", s)
391 }
392 _ => RESULT_OK.to_string(),
393 };
394
395 if let Err(e) = writer.write_all(response.as_bytes()).await {
396 debug!("Failed to send response: {}", e);
399 }
400 let _ = writer.flush().await;
401
402 if let Some(ref tx) = command_tx {
404 if let Err(e) = tx.send(command).await {
405 debug!("Command channel closed (shutdown in progress): {}", e);
407 break; }
409 }
410 }
411 }
412 }
413 }
414 Err(e) => {
415 error!("Error reading from telnet: {}", e);
416 break;
417 }
418 }
419 }
420
421 Ok(())
422}
423
424fn filter_telnet_commands(data: &[u8]) -> String {
426 let mut result = Vec::new();
427 let mut i = 0;
428
429 while i < data.len() {
430 if data[i] == IAC && i + 1 < data.len() {
431 match data[i + 1] {
433 IAC => {
434 result.push(IAC);
436 i += 2;
437 }
438 WILL | WONT | DO | DONT => {
439 if i + 2 < data.len() {
441 debug!(
442 "Telnet: {:?} option {}",
443 match data[i + 1] {
444 WILL => "WILL",
445 WONT => "WONT",
446 DO => "DO",
447 DONT => "DONT",
448 _ => "?",
449 },
450 data[i + 2]
451 );
452 i += 3;
453 } else {
454 i += 2;
455 }
456 }
457 SB => {
458 i += 2;
461 while i + 1 < data.len() {
462 if data[i] == IAC && data[i + 1] == SE {
463 i += 2;
464 break;
465 }
466 i += 1;
467 }
468 }
469 240..=249 => {
470 i += 2;
472 }
473 _ => {
474 i += 2;
476 }
477 }
478 } else {
479 result.push(data[i]);
481 i += 1;
482 }
483 }
484
485 String::from_utf8_lossy(&result).to_string()
486}
487
488pub fn parse_command(cmd: &str) -> TelnetCommand {
492 if cmd.starts_with('{') {
494 if let Ok(spec) = serde_json::from_str::<serde_json::Value>(cmd) {
495 return parse_json_command(&spec);
496 }
497 }
498
499 let parts: Vec<&str> = cmd.split_whitespace().collect();
501 if parts.is_empty() {
502 return TelnetCommand::Unknown(cmd.to_string());
503 }
504
505 match parts[0].to_uppercase().as_str() {
506 "STATUS" => TelnetCommand::Status,
507 "STOP" if parts.len() >= 2 => {
508 if let Ok(msm_id) = parts[1].parse() {
509 TelnetCommand::Stop(msm_id)
510 } else {
511 TelnetCommand::Unknown(cmd.to_string())
512 }
513 }
514 "CRONLINE" => parse_cronline(cmd),
515 "ONEOFF" => {
516 if parts.len() >= 3 {
519 let measurement_parts = &parts[2..]; let fake_cronline =
522 format!("CRONLINE 0 0 0 UNIFORM 0 {}", measurement_parts.join(" "));
523 parse_cronline(&fake_cronline)
524 } else {
525 TelnetCommand::Unknown(cmd.to_string())
526 }
527 }
528 "CRONTAB" => {
529 trace!("CRONTAB command: {}", cmd);
531 TelnetCommand::Ignored(cmd.to_string())
532 }
533 _ => TelnetCommand::Unknown(cmd.to_string()),
534 }
535}
536
537fn parse_cronline(cmd: &str) -> TelnetCommand {
546 let tokens = tokenize_cronline(cmd);
548
549 if tokens.len() < 7 {
550 warn!("CRONLINE too short: {}", cmd);
551 return TelnetCommand::Unknown(cmd.to_string());
552 }
553
554 let interval: u64 = match tokens[1].parse() {
557 Ok(v) => v,
558 Err(_) => {
559 warn!("CRONLINE invalid interval '{}': {}", tokens[1], cmd);
560 return TelnetCommand::Unknown(cmd.to_string());
561 }
562 };
563 let _offset: u64 = tokens[2].parse().unwrap_or(0);
564 let end_time: i64 = tokens[3].parse().unwrap_or(0);
565 let _spread_type = &tokens[4]; let spread: u32 = tokens[5].parse().unwrap_or(0);
567
568 let measurement_cmd = &tokens[6];
570 let measurement_args = &tokens[7..];
571
572 trace!(
573 "CRONLINE: interval={}, end_time={}, spread={}, cmd={}, args={:?}",
574 interval,
575 end_time,
576 spread,
577 measurement_cmd,
578 measurement_args
579 );
580
581 match measurement_cmd.as_str() {
583 "evping" => parse_evping(measurement_args, interval, end_time, spread),
584 "evtraceroute" => parse_evtraceroute(measurement_args, interval, end_time, spread),
585 "evtdig" => parse_evtdig(measurement_args, interval, end_time, spread),
586 "evhttpget" => parse_evhttpget(measurement_args, interval, end_time, spread),
587 "evsslgetcert" => parse_evsslgetcert(measurement_args, interval, end_time, spread),
588 "evntp" => parse_evntp(measurement_args, interval, end_time, spread),
589
590 "buddyinfo" => parse_buddyinfo(measurement_args, interval),
593 "rptaddrs" => parse_rptaddrs(measurement_args, interval),
594
595 "httppost" => {
598 trace!("Ignoring httppost CRONLINE (uploader handles uploads natively)");
599 TelnetCommand::Ignored(cmd.to_string())
600 }
601
602 "condmv" | "dfrm" => {
606 trace!(
607 "Ignoring {} CRONLINE (no on-disk spool to manage)",
608 measurement_cmd
609 );
610 TelnetCommand::Ignored(cmd.to_string())
611 }
612
613 "conntrack" => {
618 trace!("Ignoring conntrack CRONLINE (no reference impl available)");
619 TelnetCommand::Ignored(cmd.to_string())
620 }
621
622 _ => {
623 warn!("Unknown measurement command: {}", measurement_cmd);
624 TelnetCommand::Unknown(cmd.to_string())
625 }
626 }
627}
628
629fn parse_buddyinfo(args: &[String], interval: u64) -> TelnetCommand {
632 let lowmem = args.first().and_then(|s| s.parse::<u32>().ok());
633 TelnetCommand::HostTelemetry(HostTelemetrySpec {
634 kind: HostTelemetryKind::Buddyinfo,
635 interval,
636 msm_id: None,
637 lowmem,
638 })
639}
640
641fn parse_rptaddrs(args: &[String], interval: u64) -> TelnetCommand {
645 let mut msm_id: Option<u32> = None;
646 let mut it = args.iter();
647 while let Some(arg) = it.next() {
648 match arg.as_str() {
649 "-A" => {
650 msm_id = it.next().and_then(|s| s.parse().ok());
651 }
652 "-c" | "-O" => {
653 let _ = it.next();
655 }
656 _ => {}
657 }
658 }
659 TelnetCommand::HostTelemetry(HostTelemetrySpec {
660 kind: HostTelemetryKind::Rptaddrs,
661 interval,
662 msm_id,
663 lowmem: None,
664 })
665}
666
667fn tokenize_cronline(cmd: &str) -> Vec<String> {
669 let mut tokens = Vec::new();
670 let mut current = String::new();
671 let mut in_quotes = false;
672
673 for c in cmd.chars() {
674 match c {
675 '"' => {
676 in_quotes = !in_quotes;
677 }
679 ' ' | '\t' if !in_quotes => {
680 if !current.is_empty() {
681 tokens.push(current.clone());
682 current.clear();
683 }
684 }
685 _ => {
686 current.push(c);
687 }
688 }
689 }
690
691 if !current.is_empty() {
692 tokens.push(current);
693 }
694
695 tokens
696}
697
698fn parse_evping(args: &[String], interval: u64, end_time: i64, spread: u32) -> TelnetCommand {
702 let mut af: u8 = 4;
703 let mut count: u32 = 3;
704 let mut size: u16 = 64;
705 let mut msm_id: u64 = 0;
706 let mut target = String::new();
707
708 let mut i = 0;
709 while i < args.len() {
710 let arg = &args[i];
711 match arg.as_str() {
712 "-4" => af = 4,
713 "-6" => af = 6,
714 "-c" => {
715 if i + 1 < args.len() {
716 count = args[i + 1].parse().unwrap_or(3);
717 i += 1;
718 }
719 }
720 "-s" => {
721 if i + 1 < args.len() {
722 size = args[i + 1].parse().unwrap_or(64);
723 i += 1;
724 }
725 }
726 "-A" => {
727 if i + 1 < args.len() {
728 msm_id = args[i + 1].parse().unwrap_or(0);
729 i += 1;
730 }
731 }
732 "-O" => {
733 if i + 1 < args.len() {
735 i += 1;
736 }
737 }
738 "-I" => {
739 if i + 1 < args.len() {
741 i += 1;
742 }
743 }
744 "-R" => {
745 }
747 _ if !arg.starts_with('-') => {
748 target = arg.clone();
750 }
751 _ => {
752 debug!("evping: ignoring unknown option {}", arg);
753 }
754 }
755 i += 1;
756 }
757
758 if target.is_empty() {
759 warn!("evping: no target specified");
760 return TelnetCommand::Unknown(format!("evping {:?}", args));
761 }
762
763 trace!(
764 "Parsed evping: msm_id={}, target={}, af={}, count={}, size={}, interval={}",
765 msm_id,
766 target,
767 af,
768 count,
769 size,
770 interval
771 );
772
773 TelnetCommand::Ping(PingSpec {
774 msm_id,
775 target,
776 af,
777 packets: count,
778 size,
779 packet_interval: 1000, spread: Some(spread),
781 schedule: ScheduleSpec {
782 interval,
783 start_time: 0, stop_time: end_time,
785 },
786 })
787}
788
789fn parse_evtraceroute(args: &[String], interval: u64, end_time: i64, spread: u32) -> TelnetCommand {
795 let mut af: u8 = 4;
796 let mut protocol = "ICMP".to_string();
797 let mut first_hop: u8 = 1;
798 let mut max_hops: u8 = 32;
799 let mut paris: Option<u32> = None;
800 let mut msm_id: u64 = 0;
801 let mut target = String::new();
802 let mut size: u16 = 40;
803
804 let mut i = 0;
805 while i < args.len() {
806 let arg = &args[i];
807 match arg.as_str() {
808 "-4" => af = 4,
809 "-6" => af = 6,
810 "-I" => protocol = "ICMP".to_string(),
811 "-U" => protocol = "UDP".to_string(),
812 "-T" => protocol = "TCP".to_string(),
813 "-a" => {
814 if i + 1 < args.len() {
816 i += 1;
817 }
818 }
819 "-c" => {
820 if i + 1 < args.len() {
822 i += 1;
823 }
824 }
825 "-w" => {
826 if i + 1 < args.len() {
828 i += 1;
829 }
830 }
831 "-f" => {
832 if i + 1 < args.len() {
833 first_hop = args[i + 1].parse().unwrap_or(1);
834 i += 1;
835 }
836 }
837 "-m" => {
838 if i + 1 < args.len() {
839 max_hops = args[i + 1].parse().unwrap_or(32);
840 i += 1;
841 }
842 }
843 "-p" => {
844 if i + 1 < args.len() {
845 paris = Some(args[i + 1].parse().unwrap_or(0));
846 i += 1;
847 }
848 }
849 "-s" | "-S" => {
850 if i + 1 < args.len() {
851 size = args[i + 1].parse().unwrap_or(40);
852 i += 1;
853 }
854 }
855 "-A" => {
856 if i + 1 < args.len() {
857 msm_id = args[i + 1].parse().unwrap_or(0);
858 i += 1;
859 }
860 }
861 "-O" => {
862 if i + 1 < args.len() {
863 i += 1;
864 }
865 }
866 _ if !arg.starts_with('-') => {
867 target = arg.clone();
868 }
869 _ => {
870 debug!("evtraceroute: ignoring unknown option {}", arg);
871 }
872 }
873 i += 1;
874 }
875
876 if target.is_empty() {
877 warn!("evtraceroute: no target specified");
878 return TelnetCommand::Unknown(format!("evtraceroute {:?}", args));
879 }
880
881 trace!(
882 "Parsed evtraceroute: msm_id={}, target={}, af={}, protocol={}, hops={}-{}, interval={}",
883 msm_id,
884 target,
885 af,
886 protocol,
887 first_hop,
888 max_hops,
889 interval
890 );
891
892 TelnetCommand::Traceroute(TracerouteSpec {
893 msm_id,
894 target,
895 af,
896 protocol,
897 paris,
898 first_hop,
899 max_hops,
900 size,
901 spread: Some(spread),
902 schedule: ScheduleSpec {
903 interval,
904 start_time: 0,
905 stop_time: end_time,
906 },
907 })
908}
909
910fn parse_evtdig(args: &[String], interval: u64, end_time: i64, spread: u32) -> TelnetCommand {
930 let mut af: u8 = 4;
931 let mut protocol = "UDP".to_string();
932 let mut query_type = "A".to_string();
933 let mut query_class = "IN".to_string();
934 let mut use_dnssec = false;
935 let mut recursion_desired = true;
936 let mut msm_id: u64 = 0;
937 let mut target = String::new(); let mut query_argument = String::new(); let mut use_resolver = false; let mut i = 0;
942 while i < args.len() {
943 let arg = &args[i];
944 match arg.as_str() {
945 "-4" => af = 4,
946 "-6" => af = 6,
947 "-p" => {
948 if i + 1 < args.len() {
950 i += 1;
951 }
952 }
953 "--soa" => {
954 query_type = "SOA".to_string();
956 if query_argument.is_empty() {
957 query_argument = ".".to_string();
958 }
959 }
960 "--resolv" => {
961 use_resolver = true;
963 }
964 "-h" => {
965 query_type = "TXT".to_string();
967 query_class = "CH".to_string();
968 query_argument = "hostname.bind".to_string();
969 }
970 "-b" => {
971 query_type = "TXT".to_string();
973 query_class = "CH".to_string();
974 query_argument = "version.bind".to_string();
975 }
976 "-i" => {
977 query_type = "TXT".to_string();
979 query_class = "CH".to_string();
980 query_argument = "id.server".to_string();
981 }
982 "-r" => {
983 use_dnssec = true;
985 }
986 "-d" | "-D" | "+dnssec" => {
987 use_dnssec = true;
989 }
990 "-t" => {
991 protocol = "TCP".to_string();
993 }
994 "--type" | "-type" => {
995 if i + 1 < args.len() {
996 let type_num: u16 = args[i + 1].parse().unwrap_or(1);
998 query_type = match type_num {
999 1 => "A",
1000 2 => "NS",
1001 5 => "CNAME",
1002 6 => "SOA",
1003 12 => "PTR",
1004 15 => "MX",
1005 16 => "TXT",
1006 28 => "AAAA",
1007 33 => "SRV",
1008 43 => "DS",
1009 46 => "RRSIG",
1010 47 => "NSEC",
1011 48 => "DNSKEY",
1012 _ => "A",
1013 }
1014 .to_string();
1015 i += 1;
1016 }
1017 }
1018 "--class" | "-class" => {
1019 if i + 1 < args.len() {
1020 let class_num: u16 = args[i + 1].parse().unwrap_or(1);
1021 query_class = match class_num {
1022 1 => "IN",
1023 3 => "CH",
1024 _ => "IN",
1025 }
1026 .to_string();
1027 i += 1;
1028 }
1029 }
1030 "--query" | "-query" => {
1031 if i + 1 < args.len() {
1032 query_argument = args[i + 1].clone();
1033 i += 1;
1034 }
1035 }
1036 "--a" | "-a" => {
1037 query_type = "A".to_string();
1038 if i + 1 < args.len() && !args[i + 1].starts_with('-') {
1039 query_argument = args[i + 1].clone();
1040 i += 1;
1041 }
1042 }
1043 "--aaaa" | "-aaaa" => {
1044 query_type = "AAAA".to_string();
1045 if i + 1 < args.len() && !args[i + 1].starts_with('-') {
1046 query_argument = args[i + 1].clone();
1047 i += 1;
1048 }
1049 }
1050 "-e" => {
1051 if i + 1 < args.len() {
1053 i += 1;
1054 }
1055 }
1056 "-R" => {
1057 recursion_desired = false;
1059 }
1060 "--retry" => {
1061 if i + 1 < args.len() {
1063 i += 1;
1064 }
1065 }
1066 "--qbuf" => {
1067 }
1069 "-T" => {
1070 protocol = "TCP".to_string();
1072 }
1073 "-A" => {
1074 if i + 1 < args.len() {
1075 msm_id = args[i + 1].parse().unwrap_or(0);
1076 i += 1;
1077 }
1078 }
1079 "-O" => {
1080 if i + 1 < args.len() {
1081 i += 1;
1082 }
1083 }
1084 _ if arg.starts_with('@') => {
1085 target = arg[1..].to_string();
1087 }
1088 _ if arg.starts_with('+') => {
1089 match arg.as_str() {
1091 "+dnssec" | "+do" => use_dnssec = true,
1092 "+tcp" => protocol = "TCP".to_string(),
1093 "+nord" | "+norecurse" => recursion_desired = false,
1094 _ => debug!("evtdig: ignoring option {}", arg),
1095 }
1096 }
1097 _ if arg.starts_with("--") => {
1098 debug!("evtdig: ignoring unknown option {}", arg);
1100 }
1101 _ if !arg.starts_with('-') => {
1102 if target.is_empty() && arg.parse::<std::net::IpAddr>().is_ok() {
1104 target = arg.clone();
1106 } else if query_argument.is_empty() {
1107 query_argument = arg.clone();
1109 }
1110 }
1111 _ => {
1112 debug!("evtdig: ignoring unknown option {}", arg);
1113 }
1114 }
1115 i += 1;
1116 }
1117
1118 if use_resolver && target.is_empty() {
1120 target = if af == 4 {
1123 "8.8.8.8"
1124 } else {
1125 "2001:4860:4860::8888"
1126 }
1127 .to_string();
1128 }
1129
1130 if target.is_empty()
1132 && !query_argument.is_empty()
1133 && query_argument.parse::<std::net::IpAddr>().is_ok()
1134 {
1135 target = query_argument.clone();
1136 query_argument = ".".to_string();
1137 }
1138
1139 if target.is_empty() {
1140 warn!("evtdig: no DNS server specified, args={:?}", args);
1141 return TelnetCommand::Unknown(format!("evtdig {:?}", args));
1142 }
1143
1144 if query_argument.is_empty() {
1145 query_argument = ".".to_string();
1147 }
1148
1149 if query_argument.ends_with('.') && query_argument.len() > 1 {
1151 query_argument = query_argument[..query_argument.len() - 1].to_string();
1152 }
1153
1154 trace!(
1155 "Parsed evtdig: msm_id={}, server={}, query={} {} {}, protocol={}, interval={}",
1156 msm_id,
1157 target,
1158 query_argument,
1159 query_type,
1160 query_class,
1161 protocol,
1162 interval
1163 );
1164
1165 TelnetCommand::Dns(DnsSpec {
1166 msm_id,
1167 target,
1168 af,
1169 protocol,
1170 query_type,
1171 query_class,
1172 query_argument,
1173 use_dnssec,
1174 recursion_desired,
1175 spread: Some(spread),
1176 schedule: ScheduleSpec {
1177 interval,
1178 start_time: 0,
1179 stop_time: end_time,
1180 },
1181 })
1182}
1183
1184fn parse_evhttpget(args: &[String], interval: u64, end_time: i64, spread: u32) -> TelnetCommand {
1189 let mut af: u8 = 4;
1190 let mut msm_id: u64 = 0;
1191 let mut url = String::new();
1192 let mut max_body_size: Option<u32> = None;
1193
1194 let mut i = 0;
1195 while i < args.len() {
1196 let arg = &args[i];
1197 match arg.as_str() {
1198 "-4" => af = 4,
1199 "-6" => af = 6,
1200 "-1" => {
1201 }
1203 "-A" => {
1204 if i + 1 < args.len() {
1205 msm_id = args[i + 1].parse().unwrap_or(0);
1206 i += 1;
1207 }
1208 }
1209 "-O" => {
1210 if i + 1 < args.len() {
1211 i += 1;
1212 }
1213 }
1214 "-M" | "--max-body" => {
1215 if i + 1 < args.len() {
1216 max_body_size = args[i + 1].parse().ok();
1217 i += 1;
1218 }
1219 }
1220 "--store-headers" => {
1221 if i + 1 < args.len() {
1223 i += 1;
1224 }
1225 }
1226 _ if !arg.starts_with('-') => {
1227 url = arg.clone();
1228 }
1229 _ => {
1230 debug!("evhttpget: ignoring unknown option {}", arg);
1231 }
1232 }
1233 i += 1;
1234 }
1235
1236 if url.is_empty() {
1237 warn!("evhttpget: no URL specified");
1238 return TelnetCommand::Unknown(format!("evhttpget {:?}", args));
1239 }
1240
1241 trace!(
1242 "Parsed evhttpget: msm_id={}, url={}, af={}, interval={}",
1243 msm_id,
1244 url,
1245 af,
1246 interval
1247 );
1248
1249 TelnetCommand::Http(HttpSpec {
1250 msm_id,
1251 url,
1252 method: "GET".to_string(),
1253 af,
1254 headers: vec![],
1255 body: None,
1256 max_body_size,
1257 spread: Some(spread),
1258 schedule: ScheduleSpec {
1259 interval,
1260 start_time: 0,
1261 stop_time: end_time,
1262 },
1263 })
1264}
1265
1266fn parse_evsslgetcert(args: &[String], interval: u64, end_time: i64, spread: u32) -> TelnetCommand {
1271 let mut af: u8 = 4;
1272 let mut port: u16 = 443;
1273 let mut msm_id: u64 = 0;
1274 let mut target = String::new();
1275 let mut hostname: Option<String> = None;
1276
1277 let mut i = 0;
1278 while i < args.len() {
1279 let arg = &args[i];
1280 match arg.as_str() {
1281 "-4" => af = 4,
1282 "-6" => af = 6,
1283 "-p" => {
1284 if i + 1 < args.len() {
1285 port = args[i + 1].parse().unwrap_or(443);
1286 i += 1;
1287 }
1288 }
1289 "-A" => {
1290 if i + 1 < args.len() {
1291 msm_id = args[i + 1].parse().unwrap_or(0);
1292 i += 1;
1293 }
1294 }
1295 "-O" => {
1296 if i + 1 < args.len() {
1297 i += 1;
1298 }
1299 }
1300 "-h" | "-H" | "--hostname" => {
1302 if i + 1 < args.len() {
1303 hostname = Some(args[i + 1].clone());
1304 i += 1;
1305 }
1306 }
1307 _ if !arg.starts_with('-') => {
1308 target = arg.clone();
1309 }
1310 _ => {
1311 debug!("evsslgetcert: ignoring unknown option {}", arg);
1312 }
1313 }
1314 i += 1;
1315 }
1316
1317 if target.is_empty() {
1318 warn!("evsslgetcert: no target specified");
1319 return TelnetCommand::Unknown(format!("evsslgetcert {:?}", args));
1320 }
1321
1322 trace!(
1323 "Parsed evsslgetcert: msm_id={}, target={}, hostname={:?}, port={}, af={}, interval={}",
1324 msm_id,
1325 target,
1326 hostname,
1327 port,
1328 af,
1329 interval
1330 );
1331
1332 TelnetCommand::Tls(TlsSpec {
1333 msm_id,
1334 target: target.clone(),
1335 port,
1336 af,
1337 hostname: hostname.or(Some(target)),
1338 spread: Some(spread),
1339 schedule: ScheduleSpec {
1340 interval,
1341 start_time: 0,
1342 stop_time: end_time,
1343 },
1344 })
1345}
1346
1347fn parse_evntp(args: &[String], interval: u64, end_time: i64, spread: u32) -> TelnetCommand {
1351 let mut af: u8 = 4;
1352 let mut packets: u32 = 3;
1353 let mut msm_id: u64 = 0;
1354 let mut target = String::new();
1355
1356 let mut i = 0;
1357 while i < args.len() {
1358 let arg = &args[i];
1359 match arg.as_str() {
1360 "-4" => af = 4,
1361 "-6" => af = 6,
1362 "-c" => {
1363 if i + 1 < args.len() {
1364 packets = args[i + 1].parse().unwrap_or(3);
1365 i += 1;
1366 }
1367 }
1368 "-A" => {
1369 if i + 1 < args.len() {
1370 msm_id = args[i + 1].parse().unwrap_or(0);
1371 i += 1;
1372 }
1373 }
1374 "-O" => {
1375 if i + 1 < args.len() {
1376 i += 1;
1377 }
1378 }
1379 _ if !arg.starts_with('-') => {
1380 target = arg.clone();
1381 }
1382 _ => {
1383 debug!("evntp: ignoring unknown option {}", arg);
1384 }
1385 }
1386 i += 1;
1387 }
1388
1389 if target.is_empty() {
1390 warn!("evntp: no target specified");
1391 return TelnetCommand::Unknown(format!("evntp {:?}", args));
1392 }
1393
1394 trace!(
1395 "Parsed evntp: msm_id={}, target={}, af={}, packets={}, interval={}",
1396 msm_id,
1397 target,
1398 af,
1399 packets,
1400 interval
1401 );
1402
1403 TelnetCommand::Ntp(NtpSpec {
1404 msm_id,
1405 target,
1406 af,
1407 packets,
1408 spread: Some(spread),
1409 schedule: ScheduleSpec {
1410 interval,
1411 start_time: 0,
1412 stop_time: end_time,
1413 },
1414 })
1415}
1416
1417fn parse_schedule(spec: &serde_json::Value) -> ScheduleSpec {
1419 ScheduleSpec {
1420 interval: spec.get("interval").and_then(|v| v.as_u64()).unwrap_or(0),
1421 start_time: spec.get("start_time").and_then(|v| v.as_i64()).unwrap_or(0),
1422 stop_time: spec.get("stop_time").and_then(|v| v.as_i64()).unwrap_or(0),
1423 }
1424}
1425
1426fn parse_json_command(spec: &serde_json::Value) -> TelnetCommand {
1428 let msm_type = spec
1429 .get("type")
1430 .and_then(|v| v.as_str())
1431 .unwrap_or("")
1432 .to_lowercase();
1433
1434 let msm_id = spec.get("msm_id").and_then(|v| v.as_u64()).unwrap_or(0);
1435
1436 let target = spec
1437 .get("target")
1438 .or_else(|| spec.get("dst_name"))
1439 .and_then(|v| v.as_str())
1440 .unwrap_or("")
1441 .to_string();
1442
1443 let af = spec.get("af").and_then(|v| v.as_u64()).unwrap_or(4) as u8;
1444
1445 let spread = spec
1446 .get("spread")
1447 .and_then(|v| v.as_u64())
1448 .map(|v| v as u32);
1449
1450 let schedule = parse_schedule(spec);
1451
1452 match msm_type.as_str() {
1453 "ping" => TelnetCommand::Ping(PingSpec {
1454 msm_id,
1455 target,
1456 af,
1457 packets: spec.get("packets").and_then(|v| v.as_u64()).unwrap_or(3) as u32,
1458 size: spec.get("size").and_then(|v| v.as_u64()).unwrap_or(64) as u16,
1459 packet_interval: spec
1460 .get("packet_interval")
1461 .and_then(|v| v.as_u64())
1462 .unwrap_or(1000) as u32,
1463 spread,
1464 schedule,
1465 }),
1466
1467 "traceroute" => TelnetCommand::Traceroute(TracerouteSpec {
1468 msm_id,
1469 target,
1470 af,
1471 protocol: spec
1472 .get("protocol")
1473 .and_then(|v| v.as_str())
1474 .unwrap_or("ICMP")
1475 .to_string(),
1476 paris: spec.get("paris").and_then(|v| v.as_u64()).map(|v| v as u32),
1477 first_hop: spec.get("first_hop").and_then(|v| v.as_u64()).unwrap_or(1) as u8,
1478 max_hops: spec.get("max_hops").and_then(|v| v.as_u64()).unwrap_or(32) as u8,
1479 size: spec.get("size").and_then(|v| v.as_u64()).unwrap_or(40) as u16,
1480 spread,
1481 schedule,
1482 }),
1483
1484 "dns" => TelnetCommand::Dns(DnsSpec {
1485 msm_id,
1486 target,
1487 af,
1488 protocol: spec
1489 .get("protocol")
1490 .and_then(|v| v.as_str())
1491 .unwrap_or("UDP")
1492 .to_string(),
1493 query_type: spec
1494 .get("query_type")
1495 .and_then(|v| v.as_str())
1496 .unwrap_or("A")
1497 .to_string(),
1498 query_class: spec
1499 .get("query_class")
1500 .and_then(|v| v.as_str())
1501 .unwrap_or("IN")
1502 .to_string(),
1503 query_argument: spec
1504 .get("query_argument")
1505 .and_then(|v| v.as_str())
1506 .unwrap_or("")
1507 .to_string(),
1508 use_dnssec: spec
1509 .get("use_dnssec")
1510 .and_then(|v| v.as_bool())
1511 .unwrap_or(false),
1512 recursion_desired: spec
1513 .get("recursion_desired")
1514 .and_then(|v| v.as_bool())
1515 .unwrap_or(true),
1516 spread,
1517 schedule,
1518 }),
1519
1520 "http" | "https" => TelnetCommand::Http(HttpSpec {
1521 msm_id,
1522 url: spec
1523 .get("url")
1524 .or_else(|| spec.get("target"))
1525 .and_then(|v| v.as_str())
1526 .unwrap_or("")
1527 .to_string(),
1528 method: spec
1529 .get("method")
1530 .and_then(|v| v.as_str())
1531 .unwrap_or("GET")
1532 .to_string(),
1533 af,
1534 headers: spec
1535 .get("headers")
1536 .and_then(|v| v.as_array())
1537 .map(|arr| {
1538 arr.iter()
1539 .filter_map(|v| v.as_str().map(String::from))
1540 .collect()
1541 })
1542 .unwrap_or_default(),
1543 body: spec.get("body").and_then(|v| v.as_str()).map(String::from),
1544 max_body_size: spec
1545 .get("max_body_size")
1546 .and_then(|v| v.as_u64())
1547 .map(|v| v as u32),
1548 spread,
1549 schedule,
1550 }),
1551
1552 "sslcert" | "tls" => TelnetCommand::Tls(TlsSpec {
1553 msm_id,
1554 target: target.clone(),
1555 port: spec.get("port").and_then(|v| v.as_u64()).unwrap_or(443) as u16,
1556 af,
1557 hostname: spec
1558 .get("hostname")
1559 .and_then(|v| v.as_str())
1560 .map(String::from)
1561 .or(Some(target)),
1562 spread,
1563 schedule,
1564 }),
1565
1566 "ntp" => TelnetCommand::Ntp(NtpSpec {
1567 msm_id,
1568 target,
1569 af,
1570 packets: spec.get("packets").and_then(|v| v.as_u64()).unwrap_or(3) as u32,
1571 spread,
1572 schedule,
1573 }),
1574
1575 _ => TelnetCommand::Unknown(format!("Unknown measurement type: {}", msm_type)),
1576 }
1577}
1578
1579#[cfg(test)]
1580mod tests {
1581 use super::*;
1582
1583 #[test]
1584 fn test_parse_ping_command() {
1585 let json = r#"{"type":"ping","msm_id":1001,"target":"8.8.8.8","af":4,"packets":3}"#;
1586 let cmd = parse_command(json);
1587 match cmd {
1588 TelnetCommand::Ping(spec) => {
1589 assert_eq!(spec.msm_id, 1001);
1590 assert_eq!(spec.target, "8.8.8.8");
1591 assert_eq!(spec.packets, 3);
1592 assert_eq!(spec.schedule.interval, 0); }
1594 _ => panic!("Expected Ping command"),
1595 }
1596 }
1597
1598 #[test]
1599 fn test_parse_recurring_ping_command() {
1600 let json = r#"{"type":"ping","msm_id":1001,"target":"8.8.8.8","af":4,"packets":3,"interval":300,"start_time":1700000000,"stop_time":1700086400}"#;
1601 let cmd = parse_command(json);
1602 match cmd {
1603 TelnetCommand::Ping(spec) => {
1604 assert_eq!(spec.msm_id, 1001);
1605 assert_eq!(spec.schedule.interval, 300);
1606 assert_eq!(spec.schedule.start_time, 1700000000);
1607 assert_eq!(spec.schedule.stop_time, 1700086400);
1608 }
1609 _ => panic!("Expected Ping command"),
1610 }
1611 }
1612
1613 #[test]
1614 fn test_parse_dns_command() {
1615 let json = r#"{"type":"dns","msm_id":1002,"target":"9.9.9.9","query_argument":"example.com","query_type":"A"}"#;
1616 let cmd = parse_command(json);
1617 match cmd {
1618 TelnetCommand::Dns(spec) => {
1619 assert_eq!(spec.msm_id, 1002);
1620 assert_eq!(spec.target, "9.9.9.9");
1621 assert_eq!(spec.query_argument, "example.com");
1622 }
1623 _ => panic!("Expected Dns command"),
1624 }
1625 }
1626
1627 #[test]
1628 fn test_parse_status_command() {
1629 let cmd = parse_command("STATUS");
1630 assert!(matches!(cmd, TelnetCommand::Status));
1631 }
1632
1633 #[test]
1634 fn test_parse_stop_command() {
1635 let cmd = parse_command("STOP 12345");
1636 match cmd {
1637 TelnetCommand::Stop(id) => assert_eq!(id, 12345),
1638 _ => panic!("Expected Stop command"),
1639 }
1640 }
1641
1642 #[test]
1645 fn test_tokenize_cronline() {
1646 let tokens = tokenize_cronline(
1647 r#"CRONLINE 240 274 1770761451 UNIFORM 3 evping -4 -c 3 -A "1001" -O /home/atlas/data/new/7 193.0.14.129"#,
1648 );
1649 assert_eq!(tokens[0], "CRONLINE");
1650 assert_eq!(tokens[1], "240");
1651 assert_eq!(tokens[6], "evping");
1652 assert_eq!(tokens[11], "1001"); assert_eq!(tokens[tokens.len() - 1], "193.0.14.129");
1654 }
1655
1656 #[test]
1657 fn test_parse_cronline_evping() {
1658 let cmd = parse_command(
1659 r#"CRONLINE 240 274 1770761451 UNIFORM 3 evping -4 -c 3 -A "1001" -O /home/atlas/data/new/7 193.0.14.129"#,
1660 );
1661 match cmd {
1662 TelnetCommand::Ping(spec) => {
1663 assert_eq!(spec.msm_id, 1001);
1664 assert_eq!(spec.target, "193.0.14.129");
1665 assert_eq!(spec.af, 4);
1666 assert_eq!(spec.packets, 3);
1667 assert_eq!(spec.schedule.interval, 240);
1668 assert_eq!(spec.schedule.stop_time, 1770761451);
1669 assert_eq!(spec.spread, Some(3));
1670 }
1671 _ => panic!("Expected Ping command, got {:?}", cmd),
1672 }
1673 }
1674
1675 #[test]
1676 fn test_parse_cronline_evping_ipv6() {
1677 let cmd = parse_command(
1678 r#"CRONLINE 900 123 1800000000 UNIFORM 5 evping -6 -c 5 -s 128 -A "2001" 2001:4860:4860::8888"#,
1679 );
1680 match cmd {
1681 TelnetCommand::Ping(spec) => {
1682 assert_eq!(spec.msm_id, 2001);
1683 assert_eq!(spec.target, "2001:4860:4860::8888");
1684 assert_eq!(spec.af, 6);
1685 assert_eq!(spec.packets, 5);
1686 assert_eq!(spec.size, 128);
1687 assert_eq!(spec.schedule.interval, 900);
1688 }
1689 _ => panic!("Expected Ping command"),
1690 }
1691 }
1692
1693 #[test]
1694 fn test_parse_cronline_evtraceroute_udp() {
1695 let cmd = parse_command(
1696 r#"CRONLINE 1800 331 1925935851 UNIFORM 3 evtraceroute -4 -U -c 3 -w 1000 -A "5001" -O /home/atlas/data/new/7 193.0.14.129"#,
1697 );
1698 match cmd {
1699 TelnetCommand::Traceroute(spec) => {
1700 assert_eq!(spec.msm_id, 5001);
1701 assert_eq!(spec.target, "193.0.14.129");
1702 assert_eq!(spec.af, 4);
1703 assert_eq!(spec.protocol, "UDP");
1704 assert_eq!(spec.schedule.interval, 1800);
1705 }
1706 _ => panic!("Expected Traceroute command, got {:?}", cmd),
1707 }
1708 }
1709
1710 #[test]
1711 fn test_parse_cronline_evtraceroute_icmp() {
1712 let cmd = parse_command(
1713 r#"CRONLINE 3600 0 0 UNIFORM 10 evtraceroute -4 -I -m 64 -f 1 -A "5002" 8.8.8.8"#,
1714 );
1715 match cmd {
1716 TelnetCommand::Traceroute(spec) => {
1717 assert_eq!(spec.msm_id, 5002);
1718 assert_eq!(spec.target, "8.8.8.8");
1719 assert_eq!(spec.protocol, "ICMP");
1720 assert_eq!(spec.first_hop, 1);
1721 assert_eq!(spec.max_hops, 64);
1722 }
1723 _ => panic!("Expected Traceroute command"),
1724 }
1725 }
1726
1727 #[test]
1728 fn test_parse_cronline_evtdig() {
1729 let cmd = parse_command(
1731 r#"CRONLINE 900 0 0 UNIFORM 5 evtdig -4 --aaaa example.com. -A "6001" @9.9.9.9"#,
1732 );
1733 match cmd {
1734 TelnetCommand::Dns(spec) => {
1735 assert_eq!(spec.msm_id, 6001);
1736 assert_eq!(spec.target, "9.9.9.9");
1737 assert_eq!(spec.query_argument, "example.com"); assert_eq!(spec.query_type, "AAAA");
1739 assert_eq!(spec.af, 4);
1740 }
1741 _ => panic!("Expected Dns command, got {:?}", cmd),
1742 }
1743 }
1744
1745 #[test]
1746 fn test_parse_cronline_evtdig_dnssec() {
1747 let cmd = parse_command(
1749 r#"CRONLINE 1800 0 0 UNIFORM 3 evtdig -4 -d --a dnssec-test.org. -A "6002" @8.8.8.8"#,
1750 );
1751 match cmd {
1752 TelnetCommand::Dns(spec) => {
1753 assert_eq!(spec.msm_id, 6002);
1754 assert!(spec.use_dnssec);
1755 assert_eq!(spec.query_argument, "dnssec-test.org");
1756 }
1757 _ => panic!("Expected Dns command"),
1758 }
1759 }
1760
1761 #[test]
1762 fn test_parse_cronline_evtdig_soa() {
1763 let cmd = parse_command(
1765 r#"CRONLINE 1800 795 1770762780 UNIFORM 3 evtdig -4 --soa . -A "10001" -O /home/atlas/data/new/7 193.0.14.129"#,
1766 );
1767 match cmd {
1768 TelnetCommand::Dns(spec) => {
1769 assert_eq!(spec.msm_id, 10001);
1770 assert_eq!(spec.target, "193.0.14.129");
1771 assert_eq!(spec.query_argument, ".");
1772 assert_eq!(spec.query_type, "SOA");
1773 assert_eq!(spec.af, 4);
1774 assert_eq!(spec.protocol, "UDP");
1775 }
1776 _ => panic!("Expected Dns command, got {:?}", cmd),
1777 }
1778 }
1779
1780 #[test]
1781 fn test_parse_cronline_evtdig_tcp_soa() {
1782 let cmd = parse_command(
1784 r#"CRONLINE 1800 1945 1770762780 UNIFORM 3 evtdig -4 -t --soa . -A "10101" -O /home/atlas/data/new/7 193.0.14.129"#,
1785 );
1786 match cmd {
1787 TelnetCommand::Dns(spec) => {
1788 assert_eq!(spec.msm_id, 10101);
1789 assert_eq!(spec.target, "193.0.14.129");
1790 assert_eq!(spec.query_type, "SOA");
1791 assert_eq!(spec.protocol, "TCP");
1792 }
1793 _ => panic!("Expected Dns command, got {:?}", cmd),
1794 }
1795 }
1796
1797 #[test]
1798 fn test_parse_cronline_evtdig_hostname() {
1799 let cmd = parse_command(
1801 r#"CRONLINE 240 659 1770762780 UNIFORM 3 evtdig -4 -h -A "10301" -O /home/atlas/data/new/7 193.0.14.129"#,
1802 );
1803 match cmd {
1804 TelnetCommand::Dns(spec) => {
1805 assert_eq!(spec.msm_id, 10301);
1806 assert_eq!(spec.target, "193.0.14.129");
1807 assert_eq!(spec.query_argument, "hostname.bind");
1808 assert_eq!(spec.query_type, "TXT");
1809 assert_eq!(spec.query_class, "CH");
1810 }
1811 _ => panic!("Expected Dns command, got {:?}", cmd),
1812 }
1813 }
1814
1815 #[test]
1816 fn test_parse_cronline_evhttpget() {
1817 let cmd = parse_command(
1818 r#"CRONLINE 3600 0 0 UNIFORM 60 evhttpget -4 -A "7001" http://example.com/path"#,
1819 );
1820 match cmd {
1821 TelnetCommand::Http(spec) => {
1822 assert_eq!(spec.msm_id, 7001);
1823 assert_eq!(spec.url, "http://example.com/path");
1824 assert_eq!(spec.method, "GET");
1825 assert_eq!(spec.af, 4);
1826 }
1827 _ => panic!("Expected Http command, got {:?}", cmd),
1828 }
1829 }
1830
1831 #[test]
1832 fn test_parse_cronline_evsslgetcert() {
1833 let cmd = parse_command(
1834 r#"CRONLINE 1800 0 0 UNIFORM 10 evsslgetcert -4 -p 443 -A "8001" example.com"#,
1835 );
1836 match cmd {
1837 TelnetCommand::Tls(spec) => {
1838 assert_eq!(spec.msm_id, 8001);
1839 assert_eq!(spec.target, "example.com");
1840 assert_eq!(spec.port, 443);
1841 assert_eq!(spec.af, 4);
1842 }
1843 _ => panic!("Expected Tls command, got {:?}", cmd),
1844 }
1845 }
1846
1847 #[test]
1848 fn test_parse_cronline_evsslgetcert_custom_port() {
1849 let cmd = parse_command(
1850 r#"CRONLINE 900 0 0 UNIFORM 5 evsslgetcert -6 -p 8443 -A "8002" secure.example.com"#,
1851 );
1852 match cmd {
1853 TelnetCommand::Tls(spec) => {
1854 assert_eq!(spec.msm_id, 8002);
1855 assert_eq!(spec.target, "secure.example.com");
1856 assert_eq!(spec.port, 8443);
1857 assert_eq!(spec.af, 6);
1858 }
1859 _ => panic!("Expected Tls command"),
1860 }
1861 }
1862
1863 #[test]
1864 fn test_parse_cronline_evntp() {
1865 let cmd =
1866 parse_command(r#"CRONLINE 1800 0 0 UNIFORM 3 evntp -4 -c 5 -A "9001" pool.ntp.org"#);
1867 match cmd {
1868 TelnetCommand::Ntp(spec) => {
1869 assert_eq!(spec.msm_id, 9001);
1870 assert_eq!(spec.target, "pool.ntp.org");
1871 assert_eq!(spec.packets, 5);
1872 assert_eq!(spec.af, 4);
1873 }
1874 _ => panic!("Expected Ntp command, got {:?}", cmd),
1875 }
1876 }
1877
1878 #[test]
1879 fn test_parse_cronline_buddyinfo() {
1880 let cmd = parse_command("CRONLINE 600 0 0 UNIFORM 5 buddyinfo 2048");
1881 match cmd {
1882 TelnetCommand::HostTelemetry(spec) => {
1883 assert_eq!(spec.kind, HostTelemetryKind::Buddyinfo);
1884 assert_eq!(spec.interval, 600);
1885 assert_eq!(spec.lowmem, Some(2048));
1886 assert_eq!(spec.msm_id, None);
1887 }
1888 _ => panic!("Expected HostTelemetry/Buddyinfo, got {:?}", cmd),
1889 }
1890 }
1891
1892 #[test]
1893 fn test_parse_cronline_rptaddrs() {
1894 let cmd = parse_command(
1895 r#"CRONLINE 14400 0 0 UNIFORM 30 rptaddrs -A 9104 -c /home/atlas/data/new/v6addr.vol -O /home/atlas/data/new/v6addr.txt"#,
1896 );
1897 match cmd {
1898 TelnetCommand::HostTelemetry(spec) => {
1899 assert_eq!(spec.kind, HostTelemetryKind::Rptaddrs);
1900 assert_eq!(spec.interval, 14400);
1901 assert_eq!(spec.msm_id, Some(9104));
1902 }
1903 _ => panic!("Expected HostTelemetry/Rptaddrs, got {:?}", cmd),
1904 }
1905 }
1906
1907 #[test]
1908 fn test_parse_cronline_httppost_ignored() {
1909 let cmd = parse_command(
1911 "CRONLINE 60 0 0 UNIFORM 5 httppost -O /home/atlas/data/out/foo --post-file foo",
1912 );
1913 assert!(matches!(cmd, TelnetCommand::Ignored(_)));
1914 }
1915
1916 #[test]
1917 fn test_parse_cronline_condmv_ignored() {
1918 let cmd = parse_command(
1919 "CRONLINE 60 0 0 UNIFORM 5 condmv /home/atlas/data/new/foo /home/atlas/data/out/foo",
1920 );
1921 assert!(matches!(cmd, TelnetCommand::Ignored(_)));
1922 }
1923
1924 #[test]
1925 fn test_parse_cronline_conntrack_ignored() {
1926 let cmd = parse_command("CRONLINE 600 0 0 UNIFORM 5 conntrack");
1927 assert!(matches!(cmd, TelnetCommand::Ignored(_)));
1928 }
1929}