monocle/server/handlers/
rpki.rs

1//! RPKI handlers for RPKI validation and lookup operations
2//!
3//! This module provides handlers for RPKI-related methods like `rpki.validate`,
4//! `rpki.roas`, and `rpki.aspas`.
5
6use 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// =============================================================================
15// rpki.validate
16// =============================================================================
17
18/// Parameters for rpki.validate
19#[derive(Debug, Clone, Deserialize, Serialize)]
20pub struct RpkiValidateParams {
21    /// IP prefix to validate (e.g., "1.1.1.0/24")
22    pub prefix: String,
23
24    /// AS number to validate
25    pub asn: u32,
26}
27
28/// Validation state for a prefix-ASN pair
29#[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/// Validation result details
48#[derive(Debug, Clone, Serialize)]
49pub struct ValidationDetails {
50    /// The validated prefix
51    pub prefix: String,
52
53    /// The validated ASN
54    pub asn: u32,
55
56    /// Validation state
57    pub state: ValidationState,
58
59    /// Human-readable reason
60    #[serde(skip_serializing_if = "Option::is_none")]
61    pub reason: Option<String>,
62}
63
64/// Covering ROA entry
65#[derive(Debug, Clone, Serialize)]
66pub struct CoveringRoa {
67    /// ROA prefix
68    pub prefix: String,
69
70    /// Maximum prefix length
71    pub max_length: u8,
72
73    /// Origin ASN
74    pub origin_asn: u32,
75
76    /// Trust anchor
77    pub ta: String,
78}
79
80/// Response for rpki.validate
81#[derive(Debug, Clone, Serialize)]
82pub struct RpkiValidateResponse {
83    /// Validation result
84    pub validation: ValidationDetails,
85
86    /// Covering ROAs (if any)
87    pub covering_roas: Vec<CoveringRoa>,
88}
89
90/// Handler for rpki.validate method
91pub 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        // Validate prefix format
102        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        // NOTE: `MonocleDatabase` / `RpkiRepository<'_>` are not `Send`.
116        // `handle()` must produce a `Send` future, so we must not hold any DB-backed
117        // values across an `.await`. Do all DB work first, then await only to send.
118        let response = {
119            // Open the database
120            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            // Check if RPKI data is available
127            if rpki_repo.is_empty() {
128                return Err(WsError::not_initialized("RPKI"));
129            }
130
131            // Perform validation (DB API expects &str)
132            let (state, covering) = rpki_repo
133                .validate(&params.prefix, params.asn)
134                .map_err(|e| WsError::operation_failed(e.to_string()))?;
135
136            // Build response
137            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// =============================================================================
181// rpki.roas
182// =============================================================================
183
184/// Parameters for rpki.roas
185#[derive(Debug, Clone, Default, Deserialize, Serialize)]
186pub struct RpkiRoasParams {
187    /// Filter by origin ASN
188    #[serde(default)]
189    pub asn: Option<u32>,
190
191    /// Filter by prefix
192    #[serde(default)]
193    pub prefix: Option<String>,
194
195    /// Historical date (YYYY-MM-DD format)
196    #[serde(default)]
197    pub date: Option<String>,
198
199    /// Data source: cloudflare, ripe, rpkiviews
200    #[serde(default)]
201    pub source: Option<String>,
202}
203
204/// ROA entry in response
205#[derive(Debug, Clone, Serialize)]
206pub struct RoaEntry {
207    /// ROA prefix
208    pub prefix: String,
209
210    /// Maximum prefix length
211    pub max_length: u8,
212
213    /// Origin ASN
214    pub origin_asn: u32,
215
216    /// Trust anchor
217    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/// Response for rpki.roas
232#[derive(Debug, Clone, Serialize)]
233pub struct RpkiRoasResponse {
234    /// List of ROAs
235    pub roas: Vec<RoaEntry>,
236
237    /// Total count
238    pub count: usize,
239}
240
241/// Handler for rpki.roas method
242pub 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        // Validate prefix if provided
253        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        // Validate date if provided
260        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        // Validate source if provided
267        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        // NOTE: `MonocleDatabase` / `RpkiRepository<'_>` are not `Send`.
289        // Do all DB work before any `.await`.
290        let response = {
291            // DB-first: query local database only.
292            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 the repo is empty, we treat this as not initialized.
299            if rpki_repo.is_empty() {
300                return Err(WsError::not_initialized("RPKI"));
301            }
302
303            // Parse date if provided (currently DB query does not support historical snapshots).
304            // We validate earlier; here we fail if a date is explicitly requested to avoid silently
305            // lying about historical results.
306            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            // Optional prefix filter
313            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            // Collect ROAs from DB repo and apply filters locally.
322            // Note: this keeps the handler DB-first (no network IO) even if filtering is in-memory.
323            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// =============================================================================
352// rpki.aspas
353// =============================================================================
354
355/// Parameters for rpki.aspas
356#[derive(Debug, Clone, Default, Deserialize, Serialize)]
357pub struct RpkiAspasParams {
358    /// Filter by customer ASN
359    #[serde(default)]
360    pub customer_asn: Option<u32>,
361
362    /// Filter by provider ASN
363    #[serde(default)]
364    pub provider_asn: Option<u32>,
365
366    /// Historical date (YYYY-MM-DD format)
367    #[serde(default)]
368    pub date: Option<String>,
369
370    /// Data source: cloudflare, ripe, rpkiviews
371    #[serde(default)]
372    pub source: Option<String>,
373}
374
375/// ASPA entry in response
376#[derive(Debug, Clone, Serialize)]
377pub struct AspaEntry {
378    /// Customer ASN
379    pub customer_asn: u32,
380
381    /// Provider ASNs
382    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/// Response for rpki.aspas
395#[derive(Debug, Clone, Serialize)]
396pub struct RpkiAspasResponse {
397    /// List of ASPAs
398    pub aspas: Vec<AspaEntry>,
399
400    /// Total count
401    pub count: usize,
402}
403
404/// Handler for rpki.aspas method
405pub 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        // Validate date if provided
416        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        // Validate source if provided
423        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        // NOTE: `MonocleDatabase` / `RpkiRepository<'_>` are not `Send`.
445        // Do all DB work before any `.await`.
446        let response = {
447            // DB-first: query local database only.
448            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 the repo is empty, we treat this as not initialized.
455            if rpki_repo.is_empty() {
456                return Err(WsError::not_initialized("RPKI"));
457            }
458
459            // Parse date if provided (currently DB query does not support historical snapshots).
460            // We validate earlier; here we fail if a date is explicitly requested to avoid silently
461            // lying about historical results.
462            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            // Collect ASPAs from DB repo and apply filters locally.
469            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// =============================================================================
498// Tests
499// =============================================================================
500
501#[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        // Valid params
516        let params = RpkiValidateParams {
517            prefix: "1.1.1.0/24".to_string(),
518            asn: 13335,
519        };
520        assert!(RpkiValidateHandler::validate(&params).is_ok());
521
522        // Invalid prefix
523        let params = RpkiValidateParams {
524            prefix: "not-a-prefix".to_string(),
525            asn: 13335,
526        };
527        assert!(RpkiValidateHandler::validate(&params).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        // Valid params
550        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(&params).is_ok());
557
558        // Invalid prefix
559        let params = RpkiRoasParams {
560            prefix: Some("invalid".to_string()),
561            ..Default::default()
562        };
563        assert!(RpkiRoasHandler::validate(&params).is_err());
564
565        // Invalid date
566        let params = RpkiRoasParams {
567            date: Some("not-a-date".to_string()),
568            ..Default::default()
569        };
570        assert!(RpkiRoasHandler::validate(&params).is_err());
571
572        // Invalid source
573        let params = RpkiRoasParams {
574            source: Some("invalid-source".to_string()),
575            ..Default::default()
576        };
577        assert!(RpkiRoasHandler::validate(&params).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}