1#![doc(
203 html_logo_url = "https://raw.githubusercontent.com/bgpkit/assets/main/logos/icon-transparent.png",
204 html_favicon_url = "https://raw.githubusercontent.com/bgpkit/assets/main/logos/favicon.ico"
205)]
206#![allow(unknown_lints)]
207
208mod collector;
209#[cfg(feature = "cli")]
210pub mod config;
211#[cfg(feature = "cli")]
212mod crawler;
213#[cfg(feature = "backend")]
214pub mod db;
215mod error;
216mod item;
217mod peer;
218mod query;
219mod shortcuts;
220#[cfg(feature = "sse")]
221mod sse;
222
223use crate::collector::DEFAULT_COLLECTORS_CONFIG;
224use crate::peer::BrokerPeersResult;
225use crate::query::{BrokerQueryResult, CollectorLatestResult};
226use chrono::{DateTime, NaiveDate, TimeZone, Utc};
227pub use collector::{load_collectors, Collector};
228
229#[cfg(feature = "cli")]
230pub use config::{BrokerConfig, DatabaseTarget};
231#[cfg(feature = "cli")]
232pub use crawler::crawl_collector;
233#[cfg(feature = "cli")]
234pub use db::DatabaseBackend;
235#[cfg(feature = "backend")]
236pub use db::{LocalBrokerDb, PostgresDb, UpdatesMeta, DEFAULT_PAGE_SIZE};
237pub use error::BrokerError;
238pub use item::BrokerItem;
239pub use peer::BrokerPeer;
240pub use query::{QueryParams, SortOrder};
241pub use shortcuts::SnapshotFiles;
242#[cfg(feature = "sse")]
243pub use sse::{BrokerItemSubscription, SseSubscriptionOptions};
244use std::collections::{HashMap, HashSet};
245use std::fmt::Display;
246use std::net::IpAddr;
247use std::path::PathBuf;
248
249const SDK_USER_AGENT: &str = concat!("bgpkit-broker/", env!("CARGO_PKG_VERSION"));
250
251#[derive(Clone)]
255pub struct BgpkitBroker {
256 pub broker_url: String,
257 pub query_params: QueryParams,
258 client: reqwest::blocking::Client,
259 collector_project_map: HashMap<String, String>,
260 accept_invalid_certs: bool,
261 cache_dir: Option<PathBuf>,
262}
263
264impl Default for BgpkitBroker {
265 fn default() -> Self {
266 dotenvy::dotenv().ok();
267 let url = match std::env::var("BGPKIT_BROKER_URL") {
268 Ok(url) => url.trim_end_matches('/').to_string(),
269 Err(_) => "https://api.bgpkit.com/v3/broker".to_string(),
270 };
271
272 let collector_project_map = DEFAULT_COLLECTORS_CONFIG.clone().to_project_map();
273
274 let accept_invalid_certs = read_accept_invalid_certs_from_env();
275 let client = build_blocking_client(accept_invalid_certs);
276
277 Self {
278 broker_url: url,
279 query_params: Default::default(),
280 client,
281 collector_project_map,
282 accept_invalid_certs,
283 cache_dir: None,
284 }
285 }
286}
287
288fn read_accept_invalid_certs_from_env() -> bool {
289 match std::env::var("ONEIO_ACCEPT_INVALID_CERTS") {
290 Ok(t) => {
291 let l = t.to_lowercase();
292 l.starts_with("true") || l.starts_with("y")
293 }
294 Err(_) => false,
295 }
296}
297
298fn build_blocking_client(accept_invalid_certs: bool) -> reqwest::blocking::Client {
299 match reqwest::blocking::ClientBuilder::new()
300 .danger_accept_invalid_certs(accept_invalid_certs)
301 .user_agent(SDK_USER_AGENT)
302 .build()
303 {
304 Ok(c) => c,
305 Err(e) => {
306 panic!("Failed to build HTTP client for broker requests: {}", e);
307 }
308 }
309}
310
311#[cfg(feature = "sse")]
312pub(crate) fn build_async_client(
313 accept_invalid_certs: bool,
314) -> Result<reqwest::Client, BrokerError> {
315 reqwest::ClientBuilder::new()
316 .danger_accept_invalid_certs(accept_invalid_certs)
317 .user_agent(SDK_USER_AGENT)
318 .build()
319 .map_err(BrokerError::NetworkError)
320}
321
322impl BgpkitBroker {
323 pub fn new() -> Self {
336 Self::default()
337 }
338
339 pub fn broker_url<S: Display>(self, url: S) -> Self {
350 let broker_url = url.to_string().trim_end_matches('/').to_string();
351 Self {
352 broker_url,
353 query_params: self.query_params,
354 client: self.client,
355 collector_project_map: self.collector_project_map,
356 accept_invalid_certs: self.accept_invalid_certs,
357 cache_dir: self.cache_dir,
358 }
359 }
360
361 pub fn accept_invalid_certs(self) -> Self {
363 Self {
364 broker_url: self.broker_url,
365 query_params: self.query_params,
366 client: build_blocking_client(true),
367 collector_project_map: self.collector_project_map,
368 accept_invalid_certs: true,
369 cache_dir: self.cache_dir,
370 }
371 }
372
373 #[deprecated(since = "0.7.1", note = "Please use `accept_invalid_certs` instead.")]
375 pub fn disable_ssl_check(self) -> Self {
376 Self::accept_invalid_certs(self)
377 }
378
379 pub fn cache_dir<P: Into<PathBuf>>(mut self, path: P) -> Self {
394 let path = path.into();
395 if !path.exists() {
396 std::fs::create_dir_all(&path).expect("Failed to create cache directory");
397 }
398 self.cache_dir = Some(path);
399 self
400 }
401
402 fn cache_key(&self) -> String {
404 use sha2::{Digest, Sha256};
405
406 let params_str = format!(
407 "{}:{}:{}:{}:{}:{}:{}:{}",
408 self.broker_url,
409 self.query_params.ts_start.as_deref().unwrap_or(""),
410 self.query_params.ts_end.as_deref().unwrap_or(""),
411 self.query_params.collector_id.as_deref().unwrap_or(""),
412 self.query_params.project.as_deref().unwrap_or(""),
413 self.query_params.data_type.as_deref().unwrap_or(""),
414 self.query_params.page,
415 self.query_params.page_size
416 );
417
418 let mut hasher = Sha256::new();
419 hasher.update(params_str.as_bytes());
420 hasher
421 .finalize()
422 .iter()
423 .map(|b| format!("{:02x}", b))
424 .collect::<String>()
425 }
426
427 fn load_cache(&self) -> Option<Vec<BrokerItem>> {
429 let cache_dir = self.cache_dir.as_ref()?;
430 let cache_file = cache_dir.join(self.cache_key()).with_extension("json");
431
432 if !cache_file.exists() {
433 return None;
434 }
435
436 match std::fs::read_to_string(&cache_file) {
437 Ok(contents) => match serde_json::from_str::<Vec<BrokerItem>>(&contents) {
438 Ok(items) => {
439 log::info!("Loaded {} items from cache", items.len());
440 Some(items)
441 }
442 Err(e) => {
443 log::warn!("Failed to deserialize cache file: {}", e);
444 None
445 }
446 },
447 Err(e) => {
448 log::warn!("Failed to read cache file: {}", e);
449 None
450 }
451 }
452 }
453
454 fn save_cache(&self, items: &[BrokerItem]) {
456 let Some(cache_dir) = self.cache_dir.as_ref() else {
457 return;
458 };
459
460 let cache_file = cache_dir.join(self.cache_key()).with_extension("json");
461
462 match serde_json::to_string(items) {
463 Ok(json) => {
464 if let Err(e) = std::fs::write(&cache_file, json) {
465 log::warn!("Failed to write cache file: {}", e);
466 } else {
467 log::info!("Saved {} items to cache", items.len());
468 }
469 }
470 Err(e) => {
471 log::warn!("Failed to serialize items for cache: {}", e);
472 }
473 }
474 }
475
476 fn parse_timestamp(timestamp: &str) -> Result<DateTime<Utc>, BrokerError> {
490 let ts_str = timestamp.trim();
491
492 if let Ok(dt_with_tz) = DateTime::parse_from_rfc3339(ts_str) {
494 return Ok(dt_with_tz.with_timezone(&Utc));
495 }
496
497 if let Ok(naive_dt) = chrono::NaiveDateTime::parse_from_str(ts_str, "%Y-%m-%dT%H:%M:%SZ") {
499 return Ok(Utc.from_utc_datetime(&naive_dt));
500 }
501
502 if let Ok(naive_dt) = chrono::NaiveDateTime::parse_from_str(ts_str, "%Y-%m-%dT%H:%M:%S") {
504 return Ok(Utc.from_utc_datetime(&naive_dt));
505 }
506
507 if let Ok(naive_dt) = chrono::NaiveDateTime::parse_from_str(ts_str, "%Y-%m-%d %H:%M:%S") {
509 return Ok(Utc.from_utc_datetime(&naive_dt));
510 }
511
512 let date_formats = [
514 "%Y-%m-%d", "%Y/%m/%d", "%Y.%m.%d", "%Y%m%d", ];
519
520 for format in &date_formats {
521 if let Ok(date) = NaiveDate::parse_from_str(ts_str, format) {
522 if format == &"%Y%m%d" && ts_str.len() != 8 {
524 continue;
525 }
526 if let Some(naive_datetime) = date.and_hms_opt(0, 0, 0) {
528 return Ok(Utc.from_utc_datetime(&naive_datetime));
529 }
530 }
531 }
532
533 if ts_str.len() >= 9 && ts_str.len() <= 13 && ts_str.chars().all(|c| c.is_ascii_digit()) {
535 if let Ok(timestamp) = ts_str.parse::<i64>() {
536 if let Some(dt) = Utc.timestamp_opt(timestamp, 0).single() {
537 return Ok(dt);
538 }
539 }
540 }
541
542 Err(BrokerError::ConfigurationError(format!(
543 "Invalid timestamp format '{ts_str}'. Supported formats:\n\
544 - Unix timestamp: '1640995200'\n\
545 - RFC3339 with timezone: '2022-01-01T00:00:00+00:00', '2022-01-01T00:00:00Z', '2022-01-01T05:00:00-05:00'\n\
546 - RFC3339 without timezone: '2022-01-01T00:00:00' (assumes UTC)\n\
547 - Date with time: '2022-01-01 00:00:00'\n\
548 - Pure date: '2022-01-01', '2022/01/01', '2022.01.01', '20220101'"
549 )))
550 }
551
552 fn validate_configuration(&self) -> Result<QueryParams, BrokerError> {
557 let mut normalized_params = self.query_params.clone();
559
560 if normalized_params.ts_start.is_none() && normalized_params.ts_end.is_none() {
563 let now = chrono::Utc::now();
564 let thirty_days_ago = now - chrono::Duration::days(30);
565 normalized_params.ts_start =
566 Some(thirty_days_ago.format("%Y-%m-%dT%H:%M:%SZ").to_string());
567 normalized_params.ts_end = Some(now.format("%Y-%m-%dT%H:%M:%SZ").to_string());
568 }
569
570 if let Some(ts) = &self.query_params.ts_start {
571 let parsed_datetime = Self::parse_timestamp(ts)?;
572 normalized_params.ts_start =
573 Some(parsed_datetime.format("%Y-%m-%dT%H:%M:%SZ").to_string());
574 }
575
576 if let Some(ts) = &self.query_params.ts_end {
577 let parsed_datetime = Self::parse_timestamp(ts)?;
578 normalized_params.ts_end =
579 Some(parsed_datetime.format("%Y-%m-%dT%H:%M:%SZ").to_string());
580 }
581
582 if let Some(collector_str) = &self.query_params.collector_id {
584 let collectors: Vec<String> = collector_str
585 .split(',')
586 .map(|s| s.trim())
587 .filter(|s| !s.is_empty())
588 .map(|s| s.to_string())
589 .collect();
590
591 if collectors.is_empty() {
592 return Err(BrokerError::ConfigurationError(
593 "Collector ID cannot be empty".to_string(),
594 ));
595 }
596
597 let mut seen = HashSet::new();
599 let mut deduped = Vec::with_capacity(collectors.len());
600 for c in collectors {
601 if seen.insert(c.clone()) {
602 deduped.push(c);
603 }
604 }
605
606 normalized_params.collector_id = Some(deduped.join(","));
607 }
608
609 if let Some(project_str) = &self.query_params.project {
611 let project_lower = project_str.to_lowercase();
612 match project_lower.as_str() {
613 "rrc" | "riperis" | "ripe_ris" | "routeviews" | "route_views" | "rv" => {
614 }
616 _ => {
617 return Err(BrokerError::ConfigurationError(format!(
618 "Invalid project '{project_str}'. Valid projects are: 'riperis' (aliases: 'rrc', 'ripe_ris') or 'routeviews' (aliases: 'route_views', 'rv')"
619 )));
620 }
621 }
622 }
623
624 if let Some(data_type_str) = &self.query_params.data_type {
626 let data_type_lower = data_type_str.to_lowercase();
627 match data_type_lower.as_str() {
628 "rib" | "ribs" | "r" | "update" | "updates" => {
629 }
631 _ => {
632 return Err(BrokerError::ConfigurationError(format!(
633 "Invalid data type '{data_type_str}'. Valid data types are: 'rib' (aliases: 'ribs', 'r') or 'updates' (alias: 'update')"
634 )));
635 }
636 }
637 }
638
639 if self.query_params.page < 1 {
641 return Err(BrokerError::ConfigurationError(format!(
642 "Invalid page number {}. Page number must be >= 1",
643 self.query_params.page
644 )));
645 }
646
647 if !(1..=100000).contains(&self.query_params.page_size) {
649 return Err(BrokerError::ConfigurationError(format!(
650 "Invalid page size {}. Page size must be between 1 and 100000",
651 self.query_params.page_size
652 )));
653 }
654
655 Ok(normalized_params)
656 }
657
658 pub fn ts_start<S: Display>(self, ts_start: S) -> Self {
691 let mut query_params = self.query_params;
692 query_params.ts_start = Some(ts_start.to_string());
693 Self {
694 broker_url: self.broker_url,
695 query_params,
696 client: self.client,
697 collector_project_map: self.collector_project_map,
698 accept_invalid_certs: self.accept_invalid_certs,
699 cache_dir: self.cache_dir,
700 }
701 }
702
703 pub fn ts_end<S: Display>(self, ts_end: S) -> Self {
728 let mut query_params = self.query_params;
729 query_params.ts_end = Some(ts_end.to_string());
730 Self {
731 broker_url: self.broker_url,
732 client: self.client,
733 query_params,
734 collector_project_map: self.collector_project_map,
735 accept_invalid_certs: self.accept_invalid_certs,
736 cache_dir: self.cache_dir,
737 }
738 }
739
740 pub fn collector_id<S: Display>(self, collector_id: S) -> Self {
759 let mut query_params = self.query_params;
760 query_params.collector_id = Some(collector_id.to_string());
761 Self {
762 client: self.client,
763 broker_url: self.broker_url,
764 query_params,
765 collector_project_map: self.collector_project_map,
766 accept_invalid_certs: self.accept_invalid_certs,
767 cache_dir: self.cache_dir,
768 }
769 }
770
771 pub fn project<S: Display>(self, project: S) -> Self {
785 let mut query_params = self.query_params;
786 query_params.project = Some(project.to_string());
787 Self {
788 client: self.client,
789 broker_url: self.broker_url,
790 query_params,
791 collector_project_map: self.collector_project_map,
792 accept_invalid_certs: self.accept_invalid_certs,
793 cache_dir: self.cache_dir,
794 }
795 }
796
797 pub fn data_type<S: Display>(self, data_type: S) -> Self {
813 let mut query_params = self.query_params;
814 query_params.data_type = Some(data_type.to_string());
815 Self {
816 broker_url: self.broker_url,
817 client: self.client,
818 query_params,
819 collector_project_map: self.collector_project_map,
820 accept_invalid_certs: self.accept_invalid_certs,
821 cache_dir: self.cache_dir,
822 }
823 }
824
825 pub fn page(self, page: i64) -> Self {
837 let mut query_params = self.query_params;
838 query_params.page = page;
839 Self {
840 broker_url: self.broker_url,
841 client: self.client,
842 query_params,
843 collector_project_map: self.collector_project_map,
844 accept_invalid_certs: self.accept_invalid_certs,
845 cache_dir: self.cache_dir,
846 }
847 }
848
849 pub fn page_size(self, page_size: i64) -> Self {
861 let mut query_params = self.query_params;
862 query_params.page_size = page_size;
863 Self {
864 broker_url: self.broker_url,
865 client: self.client,
866 query_params,
867 collector_project_map: self.collector_project_map,
868 accept_invalid_certs: self.accept_invalid_certs,
869 cache_dir: self.cache_dir,
870 }
871 }
872
873 pub fn peers_ip(self, peer_ip: IpAddr) -> Self {
882 let mut query_params = self.query_params;
883 query_params.peers_ip = Some(peer_ip);
884 Self {
885 broker_url: self.broker_url,
886 client: self.client,
887 query_params,
888 collector_project_map: self.collector_project_map,
889 accept_invalid_certs: self.accept_invalid_certs,
890 cache_dir: self.cache_dir,
891 }
892 }
893
894 pub fn peers_asn(self, peer_asn: u32) -> Self {
903 let mut query_params = self.query_params;
904 query_params.peers_asn = Some(peer_asn);
905 Self {
906 broker_url: self.broker_url,
907 client: self.client,
908 query_params,
909 collector_project_map: self.collector_project_map,
910 accept_invalid_certs: self.accept_invalid_certs,
911 cache_dir: self.cache_dir,
912 }
913 }
914
915 pub fn peers_only_full_feed(self, peer_full_feed: bool) -> Self {
924 let mut query_params = self.query_params;
925 query_params.peers_only_full_feed = peer_full_feed;
926 Self {
927 broker_url: self.broker_url,
928 client: self.client,
929 query_params,
930 collector_project_map: self.collector_project_map,
931 accept_invalid_certs: self.accept_invalid_certs,
932 cache_dir: self.cache_dir,
933 }
934 }
935
936 pub fn turn_page(&mut self, page: i64) {
951 self.query_params.page = page;
952 }
953
954 pub fn query_single_page(&self) -> Result<Vec<BrokerItem>, BrokerError> {
964 if let Some(cached_items) = self.load_cache() {
966 return Ok(cached_items);
967 }
968
969 let validated_params = self.validate_configuration()?;
970 let url = format!("{}/search{}", self.broker_url, validated_params);
971 log::info!("sending broker query to {}", url);
972 match self.run_files_query(url.as_str()) {
973 Ok(res) => {
974 self.save_cache(&res.data);
976 Ok(res.data)
977 }
978 Err(e) => Err(e),
979 }
980 }
981
982 pub fn query_total_count(&self) -> Result<i64, BrokerError> {
1006 let validated_params = self.validate_configuration()?;
1007 let url = format!("{}/search{}", self.broker_url, validated_params);
1008 match self.run_files_query(url.as_str()) {
1009 Ok(res) => res.total.ok_or(BrokerError::BrokerError(
1010 "count not found in response".to_string(),
1011 )),
1012 Err(e) => Err(e),
1013 }
1014 }
1015
1016 pub fn health_check(&self) -> Result<(), BrokerError> {
1025 let url = format!("{}/health", self.broker_url.trim_end_matches('/'));
1026 match self.client.get(url.as_str()).send() {
1027 Ok(response) => {
1028 if response.status() == reqwest::StatusCode::OK {
1029 Ok(())
1030 } else {
1031 Err(BrokerError::BrokerError(format!(
1032 "endpoint unhealthy {}",
1033 self.broker_url
1034 )))
1035 }
1036 }
1037 Err(_e) => Err(BrokerError::BrokerError(format!(
1038 "endpoint unhealthy {}",
1039 self.broker_url
1040 ))),
1041 }
1042 }
1043
1044 pub fn query(&self) -> Result<Vec<BrokerItem>, BrokerError> {
1063 let mut p = self.validate_configuration()?;
1064
1065 let mut items = vec![];
1066 loop {
1067 let url = format!("{}/search{}", self.broker_url, p);
1068
1069 let res_items = self.run_files_query(url.as_str())?.data;
1070
1071 let items_count = res_items.len() as i64;
1072
1073 if items_count == 0 {
1074 break;
1076 }
1077
1078 items.extend(res_items);
1079 let cur_page = p.page;
1080 p = p.page(cur_page + 1);
1081
1082 if items_count < p.page_size {
1083 break;
1085 }
1086 }
1087 Ok(items)
1088 }
1089
1090 pub fn latest(&self) -> Result<Vec<BrokerItem>, BrokerError> {
1104 let latest_query_url = format!("{}/latest", self.broker_url);
1105 let mut items = match self.client.get(latest_query_url.as_str()).send() {
1106 Ok(response) => match response.json::<CollectorLatestResult>() {
1107 Ok(result) => result.data,
1108 Err(_) => {
1109 return Err(BrokerError::BrokerError(
1110 "Error parsing response".to_string(),
1111 ));
1112 }
1113 },
1114 Err(e) => {
1115 return Err(BrokerError::BrokerError(format!(
1116 "Unable to connect to the URL ({latest_query_url}): {e}"
1117 )));
1118 }
1119 };
1120
1121 items.retain(|item| {
1122 let mut matches = true;
1123 if let Some(project) = &self.query_params.project {
1124 match project.to_lowercase().as_str() {
1125 "rrc" | "riperis" | "ripe_ris" => {
1126 matches = self
1127 .collector_project_map
1128 .get(&item.collector_id)
1129 .cloned()
1130 .unwrap_or_default()
1131 .as_str()
1132 == "riperis";
1133 }
1134 "routeviews" | "route_views" | "rv" => {
1135 matches = self
1136 .collector_project_map
1137 .get(&item.collector_id)
1138 .cloned()
1139 .unwrap_or_default()
1140 .as_str()
1141 == "routeviews";
1142 }
1143 _ => {}
1144 }
1145 }
1146
1147 if let Some(data_type) = &self.query_params.data_type {
1148 match data_type.to_lowercase().as_str() {
1149 "rib" | "ribs" | "r" if !item.is_rib() => {
1150 matches = false
1152 }
1153 "update" | "updates" if item.is_rib() => {
1154 matches = false
1156 }
1157 _ => {}
1158 }
1159 }
1160
1161 if let Some(collector_id) = &self.query_params.collector_id {
1162 let wanted: HashSet<&str> = collector_id
1163 .split(',')
1164 .map(|s| s.trim())
1165 .filter(|s| !s.is_empty())
1166 .collect();
1167
1168 if !wanted.contains(item.collector_id.as_str()) {
1169 return false;
1170 }
1171 }
1172
1173 matches
1174 });
1175
1176 Ok(items)
1177 }
1178
1179 pub fn get_peers(&self) -> Result<Vec<BrokerPeer>, BrokerError> {
1251 let mut url = format!("{}/peers", self.broker_url);
1252 let mut param_strings = vec![];
1253 if let Some(ip) = &self.query_params.peers_ip {
1254 param_strings.push(format!("ip={ip}"));
1255 }
1256 if let Some(asn) = &self.query_params.peers_asn {
1257 param_strings.push(format!("asn={asn}"));
1258 }
1259 if self.query_params.peers_only_full_feed {
1260 param_strings.push("full_feed=true".to_string());
1261 }
1262 if let Some(collector_id) = &self.query_params.collector_id {
1263 param_strings.push(format!("collector={collector_id}"));
1264 }
1265 if !param_strings.is_empty() {
1266 let param_string = param_strings.join("&");
1267 url = format!("{url}?{param_string}");
1268 }
1269
1270 let peers = match self.client.get(url.as_str()).send() {
1271 Ok(response) => match response.json::<BrokerPeersResult>() {
1272 Ok(result) => result.data,
1273 Err(_) => {
1274 return Err(BrokerError::BrokerError(
1275 "Error parsing response".to_string(),
1276 ));
1277 }
1278 },
1279 Err(e) => {
1280 return Err(BrokerError::BrokerError(format!(
1281 "Unable to connect to the URL ({url}): {e}"
1282 )));
1283 }
1284 };
1285 Ok(peers)
1286 }
1287
1288 fn run_files_query(&self, url: &str) -> Result<BrokerQueryResult, BrokerError> {
1289 log::info!("sending broker query to {}", url);
1290 match self.client.get(url).send() {
1291 Ok(res) => match res.json::<BrokerQueryResult>() {
1292 Ok(res) => {
1293 if let Some(e) = res.error {
1294 Err(BrokerError::BrokerError(e))
1295 } else {
1296 Ok(res)
1297 }
1298 }
1299 Err(e) => {
1300 Err(BrokerError::BrokerError(e.to_string()))
1303 }
1304 },
1305 Err(e) => Err(BrokerError::from(e)),
1306 }
1307 }
1308}
1309
1310pub struct BrokerItemIterator {
1337 broker: BgpkitBroker,
1338 cached_items: Vec<BrokerItem>,
1339 first_run: bool,
1340}
1341
1342impl BrokerItemIterator {
1343 pub fn new(broker: BgpkitBroker) -> BrokerItemIterator {
1344 BrokerItemIterator {
1345 broker,
1346 cached_items: vec![],
1347 first_run: true,
1348 }
1349 }
1350}
1351
1352impl Iterator for BrokerItemIterator {
1353 type Item = BrokerItem;
1354
1355 fn next(&mut self) -> Option<Self::Item> {
1356 if let Some(item) = self.cached_items.pop() {
1358 return Some(item);
1359 }
1360
1361 if self.first_run {
1363 self.first_run = false;
1365 } else {
1366 self.broker.query_params.page += 1;
1368 }
1369
1370 let items = match self.broker.query_single_page() {
1372 Ok(i) => i,
1373 Err(_) => return None,
1374 };
1375
1376 if items.is_empty() {
1377 return None;
1379 } else {
1380 self.cached_items = items;
1382 self.cached_items.reverse();
1383 }
1384
1385 #[allow(clippy::unwrap_used)]
1386 Some(self.cached_items.pop().unwrap())
1387 }
1388}
1389
1390impl IntoIterator for BgpkitBroker {
1391 type Item = BrokerItem;
1392 type IntoIter = BrokerItemIterator;
1393
1394 fn into_iter(self) -> Self::IntoIter {
1395 BrokerItemIterator::new(self)
1396 }
1397}
1398
1399impl IntoIterator for &BgpkitBroker {
1400 type Item = BrokerItem;
1401 type IntoIter = BrokerItemIterator;
1402
1403 fn into_iter(self) -> Self::IntoIter {
1404 BrokerItemIterator::new(self.clone())
1405 }
1406}
1407
1408#[cfg(test)]
1409mod tests {
1410 use super::*;
1411
1412 #[test]
1413 fn test_query() {
1414 let broker = BgpkitBroker::new()
1415 .ts_start("1634693400")
1416 .ts_end("1634693400");
1417 let res = broker.query();
1418 assert!(&res.is_ok());
1419 let data = res.unwrap();
1420 assert!(!data.is_empty());
1421 }
1422
1423 #[test]
1424 fn test_network_error() {
1425 let broker = BgpkitBroker::new().broker_url("https://api.broker.example.com/v2");
1426 let res = broker.query();
1427 assert!(res.is_err());
1429 assert!(matches!(res.err(), Some(BrokerError::NetworkError(_))));
1430 }
1431
1432 #[test]
1433 fn test_broker_error() {
1434 let broker = BgpkitBroker::new().page(-1);
1435 let result = broker.query();
1436 assert!(result.is_err());
1437 assert!(matches!(
1438 result.err(),
1439 Some(BrokerError::ConfigurationError(_))
1440 ));
1441 }
1442
1443 #[test]
1444 fn test_query_all() {
1445 let broker = BgpkitBroker::new()
1446 .ts_start("1634693400")
1447 .ts_end("1634693400")
1448 .page_size(100);
1449 let res = broker.query();
1450 assert!(res.is_ok());
1451 assert!(res.ok().unwrap().len() >= 54);
1452 }
1453
1454 #[test]
1455 fn test_iterator() {
1456 let broker = BgpkitBroker::new()
1457 .ts_start("1634693400")
1458 .ts_end("1634693400");
1459 assert!(broker.into_iter().count() >= 54);
1460 }
1461
1462 #[test]
1463 fn test_filters() {
1464 let broker = BgpkitBroker::new()
1465 .ts_start("1634693400")
1466 .ts_end("1634693400");
1467 let items = broker.query().unwrap();
1468 assert!(items.len() >= 54);
1469
1470 let broker = BgpkitBroker::new()
1471 .ts_start("1634693400")
1472 .ts_end("1634693400")
1473 .collector_id("rrc00");
1474 let items = broker.query().unwrap();
1475 assert_eq!(items.len(), 1);
1476
1477 let broker = BgpkitBroker::new()
1478 .ts_start("1634693400")
1479 .ts_end("1634693400")
1480 .project("riperis");
1481 let items = broker.query().unwrap();
1482 assert_eq!(items.len(), 23);
1483 }
1484
1485 #[test]
1486 fn test_latest() {
1487 let broker = BgpkitBroker::new();
1488 let items = broker.latest().unwrap();
1489 assert!(items.len() >= 125);
1490
1491 let broker = BgpkitBroker::new().project("routeviews".to_string());
1492 let items = broker.latest().unwrap();
1493 assert!(!items.is_empty());
1494 assert!(items
1495 .iter()
1496 .all(|item| !item.collector_id.starts_with("rrc")));
1497
1498 let broker = BgpkitBroker::new().project("riperis".to_string());
1499 let items = broker.latest().unwrap();
1500 assert!(!items.is_empty());
1501 assert!(items
1502 .iter()
1503 .all(|item| item.collector_id.starts_with("rrc")));
1504
1505 let broker = BgpkitBroker::new().data_type("rib".to_string());
1506 let items = broker.latest().unwrap();
1507 assert!(!items.is_empty());
1508 assert!(items.iter().all(|item| item.is_rib()));
1509
1510 let broker = BgpkitBroker::new().data_type("update".to_string());
1511 let items = broker.latest().unwrap();
1512 assert!(!items.is_empty());
1513 assert!(items.iter().all(|item| !item.is_rib()));
1514
1515 let broker = BgpkitBroker::new().collector_id("rrc00".to_string());
1516 let items = broker.latest().unwrap();
1517 assert!(!items.is_empty());
1518 assert!(items
1519 .iter()
1520 .all(|item| item.collector_id.as_str() == "rrc00"));
1521 assert_eq!(items.len(), 2);
1522 }
1523
1524 #[test]
1525 fn test_latest_no_ssl() {
1526 let broker = BgpkitBroker::new().accept_invalid_certs();
1527 let items = broker.latest().unwrap();
1528 assert!(items.len() >= 125);
1529 }
1530
1531 #[test]
1532 fn test_health_check() {
1533 let broker = BgpkitBroker::new();
1534 let res = broker.health_check();
1535 assert!(res.is_ok());
1536 }
1537
1538 #[test]
1539 fn test_peers() {
1540 let broker = BgpkitBroker::new();
1541 let all_peers = broker.get_peers().unwrap();
1542 assert!(!all_peers.is_empty());
1543 let first_peer = all_peers.first().unwrap();
1544 let first_ip = first_peer.ip;
1545 let first_asn = first_peer.asn;
1546
1547 let broker = BgpkitBroker::new().peers_ip(first_ip);
1548 let peers = broker.get_peers().unwrap();
1549 assert!(!peers.is_empty());
1550
1551 let broker = BgpkitBroker::new().peers_asn(first_asn);
1552 let peers = broker.get_peers().unwrap();
1553 assert!(!peers.is_empty());
1554
1555 let broker = BgpkitBroker::new().peers_only_full_feed(true);
1556 let full_feed_peers = broker.get_peers().unwrap();
1557 assert!(!full_feed_peers.is_empty());
1558 assert!(full_feed_peers.len() < all_peers.len());
1559
1560 let broker = BgpkitBroker::new().collector_id("rrc00");
1561 let rrc_peers = broker.get_peers().unwrap();
1562 assert!(!rrc_peers.is_empty());
1563 assert!(rrc_peers.iter().all(|peer| peer.collector == "rrc00"));
1564
1565 let broker = BgpkitBroker::new().collector_id("rrc00,route-views2");
1566 let rrc_rv_peers = broker.get_peers().unwrap();
1567 assert!(!rrc_rv_peers.is_empty());
1568 assert!(rrc_rv_peers
1569 .iter()
1570 .any(|peer| peer.collector == "rrc00" || peer.collector == "route-views2"));
1571
1572 assert!(rrc_rv_peers.len() > rrc_peers.len());
1573 }
1574
1575 #[test]
1576 fn test_timestamp_parsing_unix() {
1577 let broker = BgpkitBroker::new();
1578
1579 let result = broker.clone().ts_start("1640995200");
1581 assert_eq!(result.query_params.ts_start, Some("1640995200".to_string()));
1583
1584 let result = broker.clone().ts_end("1640995200");
1585 assert_eq!(result.query_params.ts_end, Some("1640995200".to_string()));
1586 }
1587
1588 #[test]
1589 fn test_timestamp_parsing_rfc3339() {
1590 let broker = BgpkitBroker::new();
1591
1592 let result = broker.clone().ts_start("2022-01-01T00:00:00Z");
1594 assert_eq!(
1595 result.query_params.ts_start,
1596 Some("2022-01-01T00:00:00Z".to_string())
1597 );
1598
1599 let result = broker.clone().ts_start("2022-01-01T12:30:45");
1601 assert_eq!(
1602 result.query_params.ts_start,
1603 Some("2022-01-01T12:30:45".to_string())
1604 );
1605
1606 let result = broker.clone().ts_end("2022-01-01 12:30:45");
1608 assert_eq!(
1609 result.query_params.ts_end,
1610 Some("2022-01-01 12:30:45".to_string())
1611 );
1612 }
1613
1614 #[test]
1615 fn test_timestamp_parsing_pure_dates() {
1616 let broker = BgpkitBroker::new();
1617
1618 let result = broker.clone().ts_start("2022-01-01");
1620 assert_eq!(result.query_params.ts_start, Some("2022-01-01".to_string()));
1621
1622 let result = broker.clone().ts_start("2022/01/01");
1624 assert_eq!(result.query_params.ts_start, Some("2022/01/01".to_string()));
1625
1626 let result = broker.clone().ts_end("2022.01.01");
1628 assert_eq!(result.query_params.ts_end, Some("2022.01.01".to_string()));
1629
1630 let result = broker.clone().ts_end("20220101");
1632 assert_eq!(result.query_params.ts_end, Some("20220101".to_string()));
1633 }
1634
1635 #[test]
1636 fn test_timestamp_parsing_whitespace() {
1637 let broker = BgpkitBroker::new();
1638
1639 let result = broker.clone().ts_start(" 2022-01-01 ");
1641 assert_eq!(
1642 result.query_params.ts_start,
1643 Some(" 2022-01-01 ".to_string())
1644 );
1645
1646 let result = broker.clone().ts_end("\t1640995200\n");
1647 assert_eq!(
1648 result.query_params.ts_end,
1649 Some("\t1640995200\n".to_string())
1650 );
1651 }
1652
1653 #[test]
1654 fn test_timestamp_parsing_errors() {
1655 let broker = BgpkitBroker::new();
1656
1657 let broker_with_invalid = broker.clone().ts_start("invalid-timestamp");
1659 let result = broker_with_invalid.query();
1660 assert!(result.is_err());
1661 assert!(matches!(
1662 result.err(),
1663 Some(BrokerError::ConfigurationError(_))
1664 ));
1665
1666 let broker_with_invalid = broker.clone().ts_end("2022-13-01");
1668 let result = broker_with_invalid.query();
1669 assert!(result.is_err());
1670 assert!(matches!(
1671 result.err(),
1672 Some(BrokerError::ConfigurationError(_))
1673 ));
1674
1675 let broker_with_invalid = broker.clone().ts_start("20221301");
1677 let result = broker_with_invalid.query();
1678 assert!(result.is_err());
1679 assert!(matches!(
1680 result.err(),
1681 Some(BrokerError::ConfigurationError(_))
1682 ));
1683
1684 let broker_with_invalid = broker.clone().ts_start("2022-01");
1686 let result = broker_with_invalid.query();
1687 assert!(result.is_err());
1688 assert!(matches!(
1689 result.err(),
1690 Some(BrokerError::ConfigurationError(_))
1691 ));
1692 }
1693
1694 #[test]
1695 fn test_parse_timestamp_direct() {
1696 use chrono::{NaiveDate, NaiveDateTime};
1697
1698 let expected_unix = Utc.timestamp_opt(1640995200, 0).single().unwrap();
1702 assert_eq!(
1703 BgpkitBroker::parse_timestamp("1640995200").unwrap(),
1704 expected_unix
1705 );
1706
1707 let expected_rfc3339_z = Utc.from_utc_datetime(
1709 &NaiveDateTime::parse_from_str("2022-01-01T00:00:00", "%Y-%m-%dT%H:%M:%S").unwrap(),
1710 );
1711 assert_eq!(
1712 BgpkitBroker::parse_timestamp("2022-01-01T00:00:00Z").unwrap(),
1713 expected_rfc3339_z
1714 );
1715
1716 let expected_rfc3339_no_z = Utc.from_utc_datetime(
1717 &NaiveDateTime::parse_from_str("2022-01-01T12:30:45", "%Y-%m-%dT%H:%M:%S").unwrap(),
1718 );
1719 assert_eq!(
1720 BgpkitBroker::parse_timestamp("2022-01-01T12:30:45").unwrap(),
1721 expected_rfc3339_no_z
1722 );
1723
1724 let expected_space_format = Utc.from_utc_datetime(
1725 &NaiveDateTime::parse_from_str("2022-01-01 12:30:45", "%Y-%m-%d %H:%M:%S").unwrap(),
1726 );
1727 assert_eq!(
1728 BgpkitBroker::parse_timestamp("2022-01-01 12:30:45").unwrap(),
1729 expected_space_format
1730 );
1731
1732 let expected_date = Utc.from_utc_datetime(
1734 &NaiveDate::from_ymd_opt(2022, 1, 1)
1735 .unwrap()
1736 .and_hms_opt(0, 0, 0)
1737 .unwrap(),
1738 );
1739 assert_eq!(
1740 BgpkitBroker::parse_timestamp("2022-01-01").unwrap(),
1741 expected_date
1742 );
1743 assert_eq!(
1744 BgpkitBroker::parse_timestamp("2022/01/01").unwrap(),
1745 expected_date
1746 );
1747 assert_eq!(
1748 BgpkitBroker::parse_timestamp("2022.01.01").unwrap(),
1749 expected_date
1750 );
1751 assert_eq!(
1752 BgpkitBroker::parse_timestamp("20220101").unwrap(),
1753 expected_date
1754 );
1755
1756 let result_plus_tz = BgpkitBroker::parse_timestamp("2022-01-01T00:00:00+00:00").unwrap();
1758 assert_eq!(result_plus_tz, expected_date);
1759 println!("✓ +00:00 timezone format works");
1760
1761 let result_minus_tz = BgpkitBroker::parse_timestamp("2022-01-01T05:00:00-05:00").unwrap();
1763 let expected_10am = Utc.with_ymd_and_hms(2022, 1, 1, 10, 0, 0).unwrap();
1764 assert_eq!(result_minus_tz, expected_10am);
1765 println!("✓ -05:00 timezone format works (05:00-05:00 = 10:00Z)");
1766
1767 assert!(BgpkitBroker::parse_timestamp("invalid").is_err());
1769 assert!(BgpkitBroker::parse_timestamp("2022-13-01").is_err());
1770 assert!(BgpkitBroker::parse_timestamp("2022-01").is_err());
1771 }
1772
1773 #[test]
1774 fn test_collector_id_validation() {
1775 let broker = BgpkitBroker::new();
1776
1777 let broker_valid = broker.clone().collector_id("rrc00");
1779 let result = broker_valid.validate_configuration();
1780 assert!(result.is_ok());
1781
1782 let broker_valid = broker.clone().collector_id("rrc00,route-views2");
1784 let result = broker_valid.validate_configuration();
1785 assert!(result.is_ok());
1786
1787 let broker_unknown = broker.clone().collector_id("brand-new-collector");
1789 let result = broker_unknown.validate_configuration();
1790 assert!(result.is_ok());
1791
1792 let broker_mixed = broker.clone().collector_id("rrc00,brand-new-collector");
1794 let result = broker_mixed.validate_configuration();
1795 assert!(result.is_ok());
1796
1797 let broker_empty = broker.clone().collector_id(", , ,");
1799 let result = broker_empty.validate_configuration();
1800 assert!(result.is_err());
1801 assert!(matches!(
1802 result.err(),
1803 Some(BrokerError::ConfigurationError(_))
1804 ));
1805 }
1806
1807 #[test]
1808 fn test_project_validation() {
1809 let broker = BgpkitBroker::new();
1810
1811 let broker_valid = broker.clone().project("riperis");
1813 let result = broker_valid.validate_configuration();
1814 assert!(result.is_ok());
1815
1816 let broker_valid = broker.clone().project("routeviews");
1817 let result = broker_valid.validate_configuration();
1818 assert!(result.is_ok());
1819
1820 let broker_valid = broker.clone().project("rrc");
1822 let result = broker_valid.validate_configuration();
1823 assert!(result.is_ok());
1824
1825 let broker_valid = broker.clone().project("rv");
1826 let result = broker_valid.validate_configuration();
1827 assert!(result.is_ok());
1828
1829 let broker_invalid = broker.clone().project("invalid-project");
1831 let result = broker_invalid.validate_configuration();
1832 assert!(result.is_err());
1833 assert!(matches!(
1834 result.err(),
1835 Some(BrokerError::ConfigurationError(_))
1836 ));
1837 }
1838
1839 #[test]
1840 fn test_data_type_validation() {
1841 let broker = BgpkitBroker::new();
1842
1843 let broker_valid = broker.clone().data_type("rib");
1845 let result = broker_valid.validate_configuration();
1846 assert!(result.is_ok());
1847
1848 let broker_valid = broker.clone().data_type("updates");
1849 let result = broker_valid.validate_configuration();
1850 assert!(result.is_ok());
1851
1852 let broker_valid = broker.clone().data_type("ribs");
1854 let result = broker_valid.validate_configuration();
1855 assert!(result.is_ok());
1856
1857 let broker_valid = broker.clone().data_type("update");
1858 let result = broker_valid.validate_configuration();
1859 assert!(result.is_ok());
1860
1861 let broker_invalid = broker.clone().data_type("invalid-type");
1863 let result = broker_invalid.validate_configuration();
1864 assert!(result.is_err());
1865 assert!(matches!(
1866 result.err(),
1867 Some(BrokerError::ConfigurationError(_))
1868 ));
1869 }
1870
1871 #[test]
1872 fn test_page_validation() {
1873 let broker = BgpkitBroker::new();
1874
1875 let broker_valid = broker.clone().page(1);
1877 let result = broker_valid.validate_configuration();
1878 assert!(result.is_ok());
1879
1880 let broker_valid = broker.clone().page(100);
1881 let result = broker_valid.validate_configuration();
1882 assert!(result.is_ok());
1883
1884 let broker_invalid = broker.clone().page(0);
1886 let result = broker_invalid.validate_configuration();
1887 assert!(result.is_err());
1888 assert!(matches!(
1889 result.err(),
1890 Some(BrokerError::ConfigurationError(_))
1891 ));
1892 }
1893
1894 #[test]
1895 fn test_page_size_validation() {
1896 let broker = BgpkitBroker::new();
1897
1898 let broker_valid = broker.clone().page_size(1);
1900 let result = broker_valid.validate_configuration();
1901 assert!(result.is_ok());
1902
1903 let broker_valid = broker.clone().page_size(100);
1904 let result = broker_valid.validate_configuration();
1905 assert!(result.is_ok());
1906
1907 let broker_valid = broker.clone().page_size(100000);
1908 let result = broker_valid.validate_configuration();
1909 assert!(result.is_ok());
1910
1911 let broker_invalid = broker.clone().page_size(0);
1913 let result = broker_invalid.validate_configuration();
1914 assert!(result.is_err());
1915 assert!(matches!(
1916 result.err(),
1917 Some(BrokerError::ConfigurationError(_))
1918 ));
1919
1920 let broker_invalid = broker.clone().page_size(100001);
1921 let result = broker_invalid.validate_configuration();
1922 assert!(result.is_err());
1923 assert!(matches!(
1924 result.err(),
1925 Some(BrokerError::ConfigurationError(_))
1926 ));
1927 }
1928
1929 #[test]
1930 fn test_method_chaining() {
1931 let broker = BgpkitBroker::new()
1932 .ts_start("1634693400")
1933 .ts_end("1634693400")
1934 .collector_id("rrc00")
1935 .project("riperis")
1936 .data_type("rib")
1937 .page(1)
1938 .page_size(10);
1939
1940 assert_eq!(broker.query_params.ts_start, Some("1634693400".to_string()));
1942 assert_eq!(broker.query_params.ts_end, Some("1634693400".to_string()));
1943 assert_eq!(broker.query_params.collector_id, Some("rrc00".to_string()));
1944 assert_eq!(broker.query_params.project, Some("riperis".to_string()));
1945 assert_eq!(broker.query_params.data_type, Some("rib".to_string()));
1946 assert_eq!(broker.query_params.page, 1);
1947 assert_eq!(broker.query_params.page_size, 10);
1948 }
1949}