1use crate::database::{MonocleDatabase, RpkiAspaRecord, RpkiRoaRecord, RpkiValidationState};
7use crate::server::handler::{WsContext, WsError, WsMethod, WsRequest, WsResult};
8use crate::server::op_sink::WsOpSink;
9use async_trait::async_trait;
10use chrono::NaiveDate;
11use serde::{Deserialize, Serialize};
12use std::sync::Arc;
13
14#[derive(Debug, Clone, Deserialize, Serialize)]
20pub struct RpkiValidateParams {
21 pub prefix: String,
23
24 pub asn: u32,
26}
27
28#[derive(Debug, Clone, Serialize)]
30#[serde(rename_all = "lowercase")]
31pub enum ValidationState {
32 Valid,
33 Invalid,
34 NotFound,
35}
36
37impl From<crate::database::RpkiValidationState> for ValidationState {
38 fn from(state: crate::database::RpkiValidationState) -> Self {
39 match state {
40 crate::database::RpkiValidationState::Valid => ValidationState::Valid,
41 crate::database::RpkiValidationState::Invalid => ValidationState::Invalid,
42 crate::database::RpkiValidationState::NotFound => ValidationState::NotFound,
43 }
44 }
45}
46
47#[derive(Debug, Clone, Serialize)]
49pub struct ValidationDetails {
50 pub prefix: String,
52
53 pub asn: u32,
55
56 pub state: ValidationState,
58
59 #[serde(skip_serializing_if = "Option::is_none")]
61 pub reason: Option<String>,
62}
63
64#[derive(Debug, Clone, Serialize)]
66pub struct CoveringRoa {
67 pub prefix: String,
69
70 pub max_length: u8,
72
73 pub origin_asn: u32,
75
76 pub ta: String,
78}
79
80#[derive(Debug, Clone, Serialize)]
82pub struct RpkiValidateResponse {
83 pub validation: ValidationDetails,
85
86 pub covering_roas: Vec<CoveringRoa>,
88}
89
90pub struct RpkiValidateHandler;
92
93#[async_trait]
94impl WsMethod for RpkiValidateHandler {
95 const METHOD: &'static str = "rpki.validate";
96 const IS_STREAMING: bool = false;
97
98 type Params = RpkiValidateParams;
99
100 fn validate(params: &Self::Params) -> WsResult<()> {
101 params
103 .prefix
104 .parse::<ipnet::IpNet>()
105 .map_err(|_| WsError::invalid_params(format!("Invalid prefix: {}", params.prefix)))?;
106 Ok(())
107 }
108
109 async fn handle(
110 ctx: Arc<WsContext>,
111 _req: WsRequest,
112 params: Self::Params,
113 sink: WsOpSink,
114 ) -> WsResult<()> {
115 let response = {
119 let db = MonocleDatabase::open_in_dir(&ctx.data_dir).map_err(|e| {
121 WsError::operation_failed(format!("Failed to open database: {}", e))
122 })?;
123
124 let rpki_repo = db.rpki();
125
126 if rpki_repo.is_empty() {
128 return Err(WsError::not_initialized("RPKI"));
129 }
130
131 let (state, covering) = rpki_repo
133 .validate(¶ms.prefix, params.asn)
134 .map_err(|e| WsError::operation_failed(e.to_string()))?;
135
136 let (state, reason) = match state {
138 RpkiValidationState::Valid => (
139 ValidationState::Valid,
140 Some("ROA exists with matching ASN and valid prefix length".to_string()),
141 ),
142 RpkiValidationState::Invalid => {
143 (ValidationState::Invalid, Some("Invalid".to_string()))
144 }
145 RpkiValidationState::NotFound => (
146 ValidationState::NotFound,
147 Some("No covering ROA found".to_string()),
148 ),
149 };
150
151 let covering_roas: Vec<CoveringRoa> = covering
152 .into_iter()
153 .map(|r: RpkiRoaRecord| CoveringRoa {
154 prefix: r.prefix,
155 max_length: r.max_length,
156 origin_asn: r.origin_asn,
157 ta: r.ta,
158 })
159 .collect();
160
161 RpkiValidateResponse {
162 validation: ValidationDetails {
163 prefix: params.prefix,
164 asn: params.asn,
165 state,
166 reason,
167 },
168 covering_roas,
169 }
170 };
171
172 sink.send_result(response)
173 .await
174 .map_err(|e| WsError::internal(e.to_string()))?;
175
176 Ok(())
177 }
178}
179
180#[derive(Debug, Clone, Default, Deserialize, Serialize)]
186pub struct RpkiRoasParams {
187 #[serde(default)]
189 pub asn: Option<u32>,
190
191 #[serde(default)]
193 pub prefix: Option<String>,
194
195 #[serde(default)]
197 pub date: Option<String>,
198
199 #[serde(default)]
201 pub source: Option<String>,
202}
203
204#[derive(Debug, Clone, Serialize)]
206pub struct RoaEntry {
207 pub prefix: String,
209
210 pub max_length: u8,
212
213 pub origin_asn: u32,
215
216 pub ta: String,
218}
219
220impl From<RpkiRoaRecord> for RoaEntry {
221 fn from(record: RpkiRoaRecord) -> Self {
222 Self {
223 prefix: record.prefix,
224 max_length: record.max_length,
225 origin_asn: record.origin_asn,
226 ta: record.ta,
227 }
228 }
229}
230
231#[derive(Debug, Clone, Serialize)]
233pub struct RpkiRoasResponse {
234 pub roas: Vec<RoaEntry>,
236
237 pub count: usize,
239}
240
241pub struct RpkiRoasHandler;
243
244#[async_trait]
245impl WsMethod for RpkiRoasHandler {
246 const METHOD: &'static str = "rpki.roas";
247 const IS_STREAMING: bool = false;
248
249 type Params = RpkiRoasParams;
250
251 fn validate(params: &Self::Params) -> WsResult<()> {
252 if let Some(ref prefix) = params.prefix {
254 prefix
255 .parse::<ipnet::IpNet>()
256 .map_err(|_| WsError::invalid_params(format!("Invalid prefix: {}", prefix)))?;
257 }
258
259 if let Some(ref date) = params.date {
261 NaiveDate::parse_from_str(date, "%Y-%m-%d").map_err(|_| {
262 WsError::invalid_params(format!("Invalid date format: {}. Use YYYY-MM-DD", date))
263 })?;
264 }
265
266 if let Some(ref source) = params.source {
268 match source.to_lowercase().as_str() {
269 "cloudflare" | "ripe" | "rpkiviews" => {}
270 _ => {
271 return Err(WsError::invalid_params(format!(
272 "Invalid source: {}. Use cloudflare, ripe, or rpkiviews",
273 source
274 )));
275 }
276 }
277 }
278
279 Ok(())
280 }
281
282 async fn handle(
283 ctx: Arc<WsContext>,
284 _req: WsRequest,
285 params: Self::Params,
286 sink: WsOpSink,
287 ) -> WsResult<()> {
288 let response = {
291 let db = MonocleDatabase::open_in_dir(&ctx.data_dir).map_err(|e| {
293 WsError::operation_failed(format!("Failed to open database: {}", e))
294 })?;
295
296 let rpki_repo = db.rpki();
297
298 if rpki_repo.is_empty() {
300 return Err(WsError::not_initialized("RPKI"));
301 }
302
303 if params.date.is_some() {
307 return Err(WsError::invalid_params(
308 "Historical date filtering is not supported in DB-first mode yet",
309 ));
310 }
311
312 let prefix_filter: Option<ipnet::IpNet> = match params.prefix.as_deref() {
314 Some(p) => Some(
315 p.parse::<ipnet::IpNet>()
316 .map_err(|_| WsError::invalid_params(format!("Invalid prefix: {}", p)))?,
317 ),
318 None => None,
319 };
320
321 let mut roas = rpki_repo
324 .get_all_roas()
325 .map_err(|e| WsError::operation_failed(e.to_string()))?;
326
327 if let Some(asn) = params.asn {
328 roas.retain(|r| r.origin_asn == asn);
329 }
330 if let Some(prefix) = prefix_filter {
331 roas.retain(|r| r.prefix == prefix.to_string());
332 }
333
334 let count = roas.len();
335 let roa_entries: Vec<RoaEntry> = roas.into_iter().map(RoaEntry::from).collect();
336
337 RpkiRoasResponse {
338 roas: roa_entries,
339 count,
340 }
341 };
342
343 sink.send_result(response)
344 .await
345 .map_err(|e| WsError::internal(e.to_string()))?;
346
347 Ok(())
348 }
349}
350
351#[derive(Debug, Clone, Default, Deserialize, Serialize)]
357pub struct RpkiAspasParams {
358 #[serde(default)]
360 pub customer_asn: Option<u32>,
361
362 #[serde(default)]
364 pub provider_asn: Option<u32>,
365
366 #[serde(default)]
368 pub date: Option<String>,
369
370 #[serde(default)]
372 pub source: Option<String>,
373}
374
375#[derive(Debug, Clone, Serialize)]
377pub struct AspaEntry {
378 pub customer_asn: u32,
380
381 pub provider_asns: Vec<u32>,
383}
384
385impl From<RpkiAspaRecord> for AspaEntry {
386 fn from(record: RpkiAspaRecord) -> Self {
387 Self {
388 customer_asn: record.customer_asn,
389 provider_asns: record.provider_asns,
390 }
391 }
392}
393
394#[derive(Debug, Clone, Serialize)]
396pub struct RpkiAspasResponse {
397 pub aspas: Vec<AspaEntry>,
399
400 pub count: usize,
402}
403
404pub struct RpkiAspasHandler;
406
407#[async_trait]
408impl WsMethod for RpkiAspasHandler {
409 const METHOD: &'static str = "rpki.aspas";
410 const IS_STREAMING: bool = false;
411
412 type Params = RpkiAspasParams;
413
414 fn validate(params: &Self::Params) -> WsResult<()> {
415 if let Some(ref date) = params.date {
417 NaiveDate::parse_from_str(date, "%Y-%m-%d").map_err(|_| {
418 WsError::invalid_params(format!("Invalid date format: {}. Use YYYY-MM-DD", date))
419 })?;
420 }
421
422 if let Some(ref source) = params.source {
424 match source.to_lowercase().as_str() {
425 "cloudflare" | "ripe" | "rpkiviews" => {}
426 _ => {
427 return Err(WsError::invalid_params(format!(
428 "Invalid source: {}. Use cloudflare, ripe, or rpkiviews",
429 source
430 )));
431 }
432 }
433 }
434
435 Ok(())
436 }
437
438 async fn handle(
439 ctx: Arc<WsContext>,
440 _req: WsRequest,
441 params: Self::Params,
442 sink: WsOpSink,
443 ) -> WsResult<()> {
444 let response = {
447 let db = MonocleDatabase::open_in_dir(&ctx.data_dir).map_err(|e| {
449 WsError::operation_failed(format!("Failed to open database: {}", e))
450 })?;
451
452 let rpki_repo = db.rpki();
453
454 if rpki_repo.is_empty() {
456 return Err(WsError::not_initialized("RPKI"));
457 }
458
459 if params.date.is_some() {
463 return Err(WsError::invalid_params(
464 "Historical date filtering is not supported in DB-first mode yet",
465 ));
466 }
467
468 let mut aspas = rpki_repo
470 .get_all_aspas()
471 .map_err(|e| WsError::operation_failed(e.to_string()))?;
472
473 if let Some(customer) = params.customer_asn {
474 aspas.retain(|a| a.customer_asn == customer);
475 }
476 if let Some(provider) = params.provider_asn {
477 aspas.retain(|a| a.provider_asns.contains(&provider));
478 }
479
480 let count = aspas.len();
481 let aspa_entries: Vec<AspaEntry> = aspas.into_iter().map(AspaEntry::from).collect();
482
483 RpkiAspasResponse {
484 aspas: aspa_entries,
485 count,
486 }
487 };
488
489 sink.send_result(response)
490 .await
491 .map_err(|e| WsError::internal(e.to_string()))?;
492
493 Ok(())
494 }
495}
496
497#[cfg(test)]
502mod tests {
503 use super::*;
504
505 #[test]
506 fn test_rpki_validate_params_deserialization() {
507 let json = r#"{"prefix": "1.1.1.0/24", "asn": 13335}"#;
508 let params: RpkiValidateParams = serde_json::from_str(json).unwrap();
509 assert_eq!(params.prefix, "1.1.1.0/24");
510 assert_eq!(params.asn, 13335);
511 }
512
513 #[test]
514 fn test_rpki_validate_params_validation() {
515 let params = RpkiValidateParams {
517 prefix: "1.1.1.0/24".to_string(),
518 asn: 13335,
519 };
520 assert!(RpkiValidateHandler::validate(¶ms).is_ok());
521
522 let params = RpkiValidateParams {
524 prefix: "not-a-prefix".to_string(),
525 asn: 13335,
526 };
527 assert!(RpkiValidateHandler::validate(¶ms).is_err());
528 }
529
530 #[test]
531 fn test_rpki_roas_params_default() {
532 let params = RpkiRoasParams::default();
533 assert!(params.asn.is_none());
534 assert!(params.prefix.is_none());
535 assert!(params.date.is_none());
536 assert!(params.source.is_none());
537 }
538
539 #[test]
540 fn test_rpki_roas_params_deserialization() {
541 let json = r#"{"asn": 13335, "source": "cloudflare"}"#;
542 let params: RpkiRoasParams = serde_json::from_str(json).unwrap();
543 assert_eq!(params.asn, Some(13335));
544 assert_eq!(params.source, Some("cloudflare".to_string()));
545 }
546
547 #[test]
548 fn test_rpki_roas_params_validation() {
549 let params = RpkiRoasParams {
551 asn: Some(13335),
552 prefix: Some("1.1.1.0/24".to_string()),
553 date: Some("2024-01-01".to_string()),
554 source: Some("cloudflare".to_string()),
555 };
556 assert!(RpkiRoasHandler::validate(¶ms).is_ok());
557
558 let params = RpkiRoasParams {
560 prefix: Some("invalid".to_string()),
561 ..Default::default()
562 };
563 assert!(RpkiRoasHandler::validate(¶ms).is_err());
564
565 let params = RpkiRoasParams {
567 date: Some("not-a-date".to_string()),
568 ..Default::default()
569 };
570 assert!(RpkiRoasHandler::validate(¶ms).is_err());
571
572 let params = RpkiRoasParams {
574 source: Some("invalid-source".to_string()),
575 ..Default::default()
576 };
577 assert!(RpkiRoasHandler::validate(¶ms).is_err());
578 }
579
580 #[test]
581 fn test_rpki_aspas_params_default() {
582 let params = RpkiAspasParams::default();
583 assert!(params.customer_asn.is_none());
584 assert!(params.provider_asn.is_none());
585 }
586
587 #[test]
588 fn test_rpki_aspas_params_deserialization() {
589 let json = r#"{"customer_asn": 13335}"#;
590 let params: RpkiAspasParams = serde_json::from_str(json).unwrap();
591 assert_eq!(params.customer_asn, Some(13335));
592 assert!(params.provider_asn.is_none());
593 }
594
595 #[test]
596 fn test_validation_state_serialization() {
597 let state = ValidationState::Valid;
598 let json = serde_json::to_string(&state).unwrap();
599 assert_eq!(json, "\"valid\"");
600
601 let state = ValidationState::Invalid;
602 let json = serde_json::to_string(&state).unwrap();
603 assert_eq!(json, "\"invalid\"");
604
605 let state = ValidationState::NotFound;
606 let json = serde_json::to_string(&state).unwrap();
607 assert_eq!(json, "\"notfound\"");
608 }
609
610 #[test]
611 fn test_rpki_validate_response_serialization() {
612 let response = RpkiValidateResponse {
613 validation: ValidationDetails {
614 prefix: "1.1.1.0/24".to_string(),
615 asn: 13335,
616 state: ValidationState::Valid,
617 reason: Some("ROA exists".to_string()),
618 },
619 covering_roas: vec![CoveringRoa {
620 prefix: "1.1.1.0/24".to_string(),
621 max_length: 24,
622 origin_asn: 13335,
623 ta: "APNIC".to_string(),
624 }],
625 };
626 let json = serde_json::to_string(&response).unwrap();
627 assert!(json.contains("\"state\":\"valid\""));
628 assert!(json.contains("\"prefix\":\"1.1.1.0/24\""));
629 assert!(json.contains("\"asn\":13335"));
630 }
631}