1use std::{net::IpAddr, time::Instant};
17
18use anyhow::Context as _;
19
20use crate::{
21 api::http::model::{PgWapSessionManager, Session},
22 pg_wap::{auth::AuthService, session_manager::WapSessionManager},
23};
24
25mod auth;
26pub mod session_manager;
27
28pub use auth::AuthInfo;
29
30#[derive(Clone)]
32pub struct WapControl {
33 auth_service: AuthService,
34 session_manager: WapSessionManager,
35 data_plane_port: u16,
36 ap_id: String,
37}
38
39impl WapControl {
40 pub fn new(
42 session_manager: WapSessionManager,
43 auth_duration: std::time::Duration,
44 ap_id: String,
45 data_plane_port: u16,
46 ) -> Self {
47 let auth_service = AuthService::new(auth_duration);
48
49 Self {
50 session_manager,
51 auth_service,
52 ap_id,
53 data_plane_port,
54 }
55 }
56
57 fn ap_id(&self) -> &str {
58 &self.ap_id
59 }
60}
61
62impl PgWapSessionManager for WapControl {
63 fn new_session(
64 &self,
65 client_ip: IpAddr,
66 target_domains: &[&str],
67 ) -> Result<Session, anyhow::Error> {
68 let now = Instant::now();
69 let auth_info = self
70 .auth_service
71 .authenticate(now, client_ip, target_domains);
72 self.session_manager
73 .add_session_authentication(auth_info.clone());
74
75 let valid_until = chrono::Utc::now()
76 + chrono::Duration::from_std(auth_info.valid_until.saturating_duration_since(now))
77 .context("auth service returned out of bounds duration")?;
78 tracing::info!(%client_ip, %valid_until, ?target_domains, "Granted IP access");
79
80 Ok(Session {
81 ip: auth_info.ip,
82 ap_id: self.ap_id().to_string(),
83 data_plane_port: self.data_plane_port,
84 target_domains: auth_info.targets,
85 valid_until,
86 })
87 }
88}
89
90#[cfg(test)]
91mod tests {
92 use std::{net::IpAddr, time::Duration};
93
94 use super::*;
95
96 fn control() -> WapControl {
97 WapControl::new(
98 WapSessionManager::new(),
99 Duration::from_secs(60),
100 "test-wap".to_string(),
101 8443,
102 )
103 }
104
105 #[test]
106 fn new_session_authenticates_for_requested_target_domains() {
107 let control = control();
108 let client_ip = IpAddr::from([10, 0, 0, 1]);
109
110 let session = control
111 .new_session(client_ip, &["a.example.com", "b.example.com"])
112 .expect("new_session");
113
114 assert_eq!(
116 session.target_domains,
117 vec!["a.example.com".to_string(), "b.example.com".to_string()]
118 );
119 assert_eq!(session.ip, client_ip);
120 assert_eq!(session.data_plane_port, 8443);
121
122 let authed = control
124 .session_manager
125 .authenticated_sessions_for_ip(client_ip);
126 assert_eq!(authed.len(), 1);
127 assert_eq!(
128 authed[0].targets,
129 vec!["a.example.com".to_string(), "b.example.com".to_string()]
130 );
131 }
132
133 #[test]
134 fn authentication_is_scoped_to_supplied_domains() {
135 let control = control();
136 let client_ip = IpAddr::from([10, 0, 0, 2]);
137
138 control
139 .new_session(client_ip, &["a.example.com"])
140 .expect("new_session");
141
142 let authed = control
143 .session_manager
144 .authenticated_sessions_for_ip(client_ip);
145 assert_eq!(authed.len(), 1);
146 assert!(authed[0].targets.contains(&"a.example.com".to_string()));
147 assert!(
148 !authed[0]
149 .targets
150 .contains(&"not-requested.example.com".to_string())
151 );
152 }
153}