Skip to main content

px_native/infrastructure/
handler.rs

1//! `ChallengeHandler` adapter for [`SensorNativeSolver`] so it can slot
2//! into the existing routing dispatcher. Pair this with
3//! [`super::native_first::NativeFirstHandler`] to get the
4//! "native first, browser on failure" wiring.
5
6use std::sync::Arc;
7use std::time::Instant;
8
9use async_trait::async_trait;
10use px_core::{CookieJarDelta, Fingerprint, PxAppId};
11use px_errors::AppError;
12use px_pipeline::{ChallengeHandler, HandlerMetrics, HandlerName, HandlerOutcome, PageHtml};
13
14use crate::domain::native_solver::{NativeSolver, SolveContext};
15
16pub struct NativePxHandler {
17    solver: Arc<dyn NativeSolver>,
18    app_id: PxAppId,
19    name: HandlerName,
20}
21
22impl NativePxHandler {
23    pub fn new(solver: Arc<dyn NativeSolver>, app_id: PxAppId) -> Self {
24        Self {
25            solver,
26            app_id,
27            name: "perimeterx-native",
28        }
29    }
30}
31
32#[async_trait]
33impl ChallengeHandler for NativePxHandler {
34    fn name(&self) -> HandlerName {
35        self.name
36    }
37
38    async fn detects(&self, _page: &PageHtml) -> Result<bool, AppError> {
39        Ok(true)
40    }
41
42    async fn solve(&self, page: &PageHtml) -> Result<HandlerOutcome, AppError> {
43        let started = Instant::now();
44        let ctx = SolveContext::new(page.url.clone(), self.app_id.clone(), default_fingerprint());
45        let bundle = self.solver.solve(&ctx).await?;
46        let metrics = HandlerMetrics {
47            solve_ms: started.elapsed().as_millis() as u64,
48            ..Default::default()
49        };
50        Ok(HandlerOutcome::solved_with_ua(
51            self.name,
52            CookieJarDelta {
53                set: bundle.cookies,
54                removed: Vec::new(),
55            },
56            Vec::new(),
57            metrics,
58            bundle.user_agent,
59        ))
60    }
61}
62
63fn default_fingerprint() -> Fingerprint {
64    Fingerprint {
65        user_agent: "Mozilla/5.0 (X11; Linux x86_64; rv:135.0) Gecko/20100101 Firefox/135.0".into(),
66        accept_language: vec!["es-AR".into(), "es".into(), "en-US".into()],
67        screen_width: 1366,
68        screen_height: 768,
69        device_pixel_ratio: 1,
70        timezone: "America/Argentina/Buenos_Aires".into(),
71        platform: "Linux x86_64".into(),
72        webgl_vendor: "Mozilla".into(),
73        webgl_renderer: "Mozilla".into(),
74    }
75}
76
77#[cfg(test)]
78#[allow(clippy::expect_used, clippy::unwrap_used)]
79mod tests {
80    use super::*;
81    use px_core::{NamedCookie, PxCookieBundle};
82    use px_pipeline::HandlerStatus;
83    use std::time::{Duration, SystemTime};
84
85    struct AlwaysOkSolver;
86
87    #[async_trait]
88    impl NativeSolver for AlwaysOkSolver {
89        async fn solve(&self, _ctx: &SolveContext) -> Result<PxCookieBundle, AppError> {
90            Ok(PxCookieBundle::new(
91                vec![NamedCookie {
92                    name: "_px3".into(),
93                    value: "native".into(),
94                    domain: "example.com".into(),
95                    path: "/".into(),
96                }],
97                "ua",
98                SystemTime::now(),
99                Duration::from_secs(60),
100            ))
101        }
102    }
103
104    fn app_id() -> PxAppId {
105        PxAppId::new("PXeT15wiaE").expect("valid app id")
106    }
107
108    #[tokio::test]
109    async fn handler_reports_solved_status() {
110        let handler =
111            NativePxHandler::new(Arc::new(AlwaysOkSolver) as Arc<dyn NativeSolver>, app_id());
112        let page = PageHtml::new("https://www.pedidosya.com.ar/", "");
113        let out = handler.solve(&page).await.expect("solve");
114        assert_eq!(out.status, HandlerStatus::Solved);
115        assert_eq!(out.cookies.set.len(), 1);
116        assert_eq!(out.user_agent.as_deref(), Some("ua"));
117    }
118}