1use std::sync::Arc;
15
16use serde_json::Value;
17
18use crate::cache::{Cache, CacheHandle, CacheKey, CacheMode, Fetched};
19use crate::config::{Config, DEFAULT_CDP, HydraMode};
20use crate::error::{Error, ErrorKind, Result};
21use crate::http::{Allowlist, Client, Hydra, Request};
22use crate::jwt;
23use crate::session::{LOCATION_COOKIE, Session};
24use crate::store::SessionStore;
25use crate::{cdp, hydra, sso};
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29enum ResolvedHydra {
30 Native,
31 Cdp,
32}
33
34pub struct Context {
35 pub config: Config,
36 session: Session,
37 store: Arc<dyn SessionStore>,
38 client: Arc<Client>,
39 allow: Allowlist,
42 hydra_skew: i64,
44 hydra_key: Option<String>,
46 cache: CacheHandle,
48 refresh_tried: bool,
51}
52
53const REFRESH_MARGIN_SECS: i64 = 15 * 60;
56
57impl Context {
58 pub async fn new(
64 config: Config,
65 store: Arc<dyn SessionStore>,
66 cache: Arc<dyn Cache>,
67 ) -> Result<Self> {
68 let session = store.load().await?.ok_or_else(|| {
69 Error::new(
70 ErrorKind::AuthMissing,
71 "no Grab session: log in first (`grab auth login`, or `grab auth import`)",
72 )
73 })?;
74 let allow = Allowlist::new();
75 let client = Arc::new(Client::new(&session, &config, allow.clone())?);
76 let cache = CacheHandle::new(cache, config.cache.clone(), CacheMode::Normal);
77 Ok(Self {
78 cache,
79 config,
80 session,
81 store,
82 client,
83 allow,
84 hydra_skew: 30,
85 hydra_key: None,
86 refresh_tried: false,
87 })
88 }
89
90 pub fn with_allowlist(mut self, allow: &Allowlist) -> Result<Self> {
92 self.allow.extend(allow);
93 self.rebuild_client()?;
94 Ok(self)
95 }
96
97 #[must_use]
99 pub fn with_hydra_key(mut self, pem: Option<String>) -> Self {
100 self.hydra_key = pem;
101 self
102 }
103
104 fn rebuild_client(&mut self) -> Result<()> {
105 self.client = Arc::new(Client::new(
106 &self.session,
107 &self.config,
108 self.allow.clone(),
109 )?);
110 Ok(())
111 }
112
113 async fn persist(&mut self) -> Result<()> {
115 self.store.save(&self.session).await?;
116 self.rebuild_client()
117 }
118
119 pub async fn set_session(&mut self, session: Session) -> Result<()> {
121 self.session = session;
122 self.refresh_tried = false;
123 self.persist().await
124 }
125
126 pub async fn refresh_session(&mut self) -> Result<()> {
131 self.refresh_tried = true;
132 let ua = self.user_agent();
133 sso::silent_refresh(&mut self.session, &ua).await?;
134 self.persist().await
135 }
136
137 fn user_agent(&self) -> String {
138 self.config
139 .user_agent
140 .clone()
141 .or_else(|| self.session.user_agent.clone())
142 .unwrap_or_else(|| crate::http::DEFAULT_UA.to_string())
143 }
144
145 pub async fn restore_address_from_browser(&mut self, cdp_base: &str) -> Result<bool> {
150 let (cookies, _) = cdp::default_context_cookies(cdp_base).await?;
151 if !self
152 .session
153 .replace_named_cookies(LOCATION_COOKIE, &cookies)
154 {
155 return Ok(false);
156 }
157 self.persist().await?;
158 Ok(true)
159 }
160
161 pub async fn set_location(&mut self, loc: &Value) -> Result<()> {
165 let json = serde_json::to_string(loc).map_err(|e| Error::parse(e.to_string()))?;
166 let value = urlencoding::encode(&json).into_owned();
167 self.session.set_location_cookie(&value);
168 self.persist().await
169 }
170
171 pub async fn after_login(&mut self) -> Vec<String> {
175 let mut notes = Vec::new();
176 if self.session.cookie(LOCATION_COOKIE).is_none() {
177 let base = self.cdp_base();
178 match self.restore_address_from_browser(&base).await {
179 Ok(true) => notes.push("copied the saved delivery address from the browser".into()),
180 Ok(false) => notes.push(
181 "no saved delivery address in the session or the browser; set one on \
182 food.grab.com, then `grab auth import --address-only`"
183 .into(),
184 ),
185 Err(_) => notes.push(
186 "no saved delivery address and the browser was unreachable; later run \
187 `grab auth import --address-only`"
188 .into(),
189 ),
190 }
191 }
192 if !self.has_mart_session()
193 && let Err(e) = self.login_mart().await
194 {
195 notes.push(if e.kind == ErrorKind::AuthMissing {
196 format!("GrabMart: {}", e.message)
197 } else {
198 format!(
199 "GrabMart session not set up now ({}); `grab mart quote` retries it",
200 e.kind.as_str()
201 )
202 });
203 }
204 if !self.session.hydra_valid(self.hydra_skew)
205 && let Err(e) = self.mint_hydra().await
206 {
207 notes.push(format!(
208 "hydra not minted now ({}); commands mint it on demand",
209 e.kind.as_str()
210 ));
211 }
212 notes
213 }
214
215 pub async fn login_mart(&mut self) -> Result<()> {
220 let ua = self.user_agent();
221 sso::silent_login(sso::Service::Mart, &mut self.session, &ua).await?;
222 self.persist().await
223 }
224
225 pub fn has_mart_session(&self) -> bool {
227 !sso::mart_session_cookies(&self.session).is_empty()
228 }
229
230 fn session_expiring(&self) -> bool {
234 self.session.gfc_logged_in()
235 && self
236 .session
237 .gfc_expires_at()
238 .is_some_and(|e| e - jwt::now() < REFRESH_MARGIN_SECS)
239 }
240
241 #[must_use]
245 pub fn with_cache_mode(mut self, mode: CacheMode) -> Self {
246 self.cache = self.cache.with_mode(mode);
247 self
248 }
249
250 pub fn set_cache_mode(&mut self, mode: CacheMode) {
252 self.cache = self.cache.with_mode(mode);
253 }
254
255 pub fn cache(&self) -> &CacheHandle {
258 &self.cache
259 }
260
261 pub async fn cached_call(&mut self, key: &CacheKey, req: &Request) -> Result<Value> {
265 let cache = self.cache.clone();
266 cache.get_or_fetch(key, || self.call(req)).await
267 }
268
269 pub async fn cached_call_meta(
273 &mut self,
274 key: &CacheKey,
275 req: &Request,
276 ) -> Result<Fetched<Value>> {
277 let cache = self.cache.clone();
278 cache.get_or_fetch_meta(key, || self.call(req)).await
279 }
280
281 pub fn session(&self) -> &Session {
282 &self.session
283 }
284
285 pub fn cdp_base(&self) -> String {
286 self.config
287 .cdp
288 .clone()
289 .unwrap_or_else(|| DEFAULT_CDP.to_string())
290 }
291
292 fn resolve_hydra_mode(&self) -> ResolvedHydra {
293 let key_present = cfg!(feature = "native-hydra") && self.hydra_key_pem().is_some();
295 match self.config.hydra_mode.unwrap_or_default() {
296 HydraMode::Native => ResolvedHydra::Native,
297 HydraMode::Cdp => ResolvedHydra::Cdp,
298 HydraMode::Auto => {
299 if key_present {
300 ResolvedHydra::Native
301 } else {
302 ResolvedHydra::Cdp
303 }
304 }
305 }
306 }
307
308 fn hydra_key_pem(&self) -> Option<String> {
309 self.hydra_key.clone()
310 }
311
312 pub async fn ensure_hydra(&mut self) -> Result<String> {
315 if self.session.hydra_valid(self.hydra_skew)
316 && let Some(tok) = &self.session.hydra_jwt
317 {
318 return Ok(tok.clone());
319 }
320 self.mint_hydra().await
321 }
322
323 pub async fn mint_hydra(&mut self) -> Result<String> {
325 match self.resolve_hydra_mode() {
326 ResolvedHydra::Native => {
327 let pem = self.hydra_key_pem().ok_or_else(|| {
328 Error::new(
329 ErrorKind::HydraExpired,
330 "native hydra minting needs a signing key (the CLI reads \
331 <config>/hydra-key.pem, see docs/hydra.md); or set \
332 hydra_mode = \"cdp\"",
333 )
334 })?;
335 let ver = hydra::DEFAULT_VER;
336 let (tok, exp) = hydra::mint_native(&pem, &hydra::DeviceFields::random(), ver)?;
337 self.session.hydra_jwt = Some(tok.clone());
338 self.session.hydra_exp = Some(exp);
339 self.store.save(&self.session).await?;
340 Ok(tok)
341 }
342 ResolvedHydra::Cdp => {
343 let base = self.cdp_base();
344 let page = self
345 .config
346 .hydra_page
347 .clone()
348 .unwrap_or_else(|| "https://food.grab.com/ph/en/".to_string());
349 cdp::mint_hydra(&base, &page, &mut self.session).await?;
350 self.store.save(&self.session).await?;
351 self.session.hydra_jwt.clone().ok_or_else(|| {
352 Error::new(ErrorKind::HydraExpired, "hydra mint produced no token")
353 })
354 }
355 }
356 }
357
358 pub async fn call(&mut self, req: &Request) -> Result<Value> {
361 if self.config.auto_refresh() && !self.refresh_tried && self.session_expiring() {
364 let _ = self.refresh_session().await;
365 }
366 let token = if req.hydra == Hydra::Required {
367 Some(self.ensure_hydra().await?)
368 } else {
369 None
370 };
371
372 match self.client.send(req, token.as_deref()).await {
373 Err(e)
374 if e.kind == ErrorKind::HydraExpired
375 && req.hydra == Hydra::Required
376 && self.config.auto_refresh() =>
377 {
378 let fresh = self.mint_hydra().await?;
380 self.client.send(req, Some(&fresh)).await
381 }
382 Err(e)
383 if e.kind == ErrorKind::AuthExpired
384 && req.refreshable
385 && !self.refresh_tried
386 && self.config.auto_refresh() =>
387 {
388 self.refresh_session().await?;
392 let token = if req.hydra == Hydra::Required {
393 Some(self.ensure_hydra().await?)
394 } else {
395 None
396 };
397 self.client.send(req, token.as_deref()).await
398 }
399 other => other,
400 }
401 }
402
403 pub fn auth_status(&self) -> Value {
406 let now = jwt::now();
407 let loc = crate::location::Location::from_session(&self.session).ok();
408 let cookies: Vec<Value> = self
409 .session
410 .cookies
411 .iter()
412 .map(|c| {
413 let exp = if c.expires > 0.0 {
414 Some(c.expires as i64)
415 } else {
416 None
417 };
418 serde_json::json!({
419 "name": c.name,
420 "domain": c.domain,
421 "expires": exp,
422 "expires_in_secs": exp.map(|e| e - now),
423 "expired": exp.is_some_and(|e| e <= now),
424 })
425 })
426 .collect();
427 serde_json::json!({
428 "cookies": cookies,
429 "gfc_session_present": self.session.cookie("gfc_session").is_some(),
430 "gfc_session_logged_in": self.session.gfc_logged_in(),
431 "gfc_session_exp": self.session.gfc_expires_at(),
432 "gfc_session_expires_in_secs": self.session.gfc_expires_at().map(|e| e - now),
433 "hydra_exp": self.session.hydra_exp,
434 "hydra_expires_in_secs": self.session.hydra_exp.map(|e| e - now),
435 "hydra_valid": self.session.hydra_valid(self.hydra_skew),
436 "web_app_version_present": self.session.web_app_version.is_some(),
437 "payment_type_id_present":
438 self.session.payment_type_id.is_some() || self.config.payment_type_id.is_some(),
439 "has_saved_address": self.session.cookie("location").is_some(),
440 "address_detail": loc.as_ref().and_then(|l| l.address_detail.clone()),
441 "note_to_driver": loc.as_ref().and_then(|l| l.note_to_driver.clone()),
442 })
443 }
444}
445
446#[cfg(test)]
447mod tests {
448 use super::*;
449 use crate::cache::MemoryCache;
450 #[cfg(feature = "native-hydra")]
451 use crate::config::HydraMode;
452 use crate::store::MemorySessionStore;
453
454 fn store_with(session: Option<Session>) -> Arc<MemorySessionStore> {
455 Arc::new(MemorySessionStore::new(session))
456 }
457
458 async fn ctx(store: &Arc<MemorySessionStore>, config: Config) -> Result<Context> {
459 Context::new(config, store.clone(), Arc::new(MemoryCache::default())).await
460 }
461
462 #[tokio::test]
463 async fn new_needs_a_stored_session() {
464 let e = ctx(&store_with(None), Config::default()).await.err();
465 assert_eq!(e.map(|e| e.kind), Some(ErrorKind::AuthMissing));
466 }
467
468 #[tokio::test]
469 async fn location_change_is_saved_through_the_store() -> Result<()> {
470 let store = store_with(Some(Session::default()));
471 let mut c = ctx(&store, Config::default()).await?;
472 assert_eq!(store.save_count(), 0);
473 c.set_location(&serde_json::json!({"latitude": 1.0, "longitude": 2.0}))
474 .await?;
475 assert_eq!(store.save_count(), 1);
476 let saved = store.get().ok_or_else(|| Error::invalid("none"))?;
477 assert!(saved.cookie(LOCATION_COOKIE).is_some());
478 Ok(())
479 }
480
481 #[cfg(feature = "native-hydra")]
482 #[tokio::test]
483 async fn hydra_mint_is_saved_through_the_store() -> Result<()> {
484 let (pem, _) = crate::hydra::tests::test_key();
485 let store = store_with(Some(Session::default()));
486 let config = Config {
487 hydra_mode: Some(HydraMode::Native),
488 ..Config::default()
489 };
490 let mut c = ctx(&store, config).await?.with_hydra_key(Some(pem));
491 let tok = c.ensure_hydra().await?;
492 assert_eq!(store.save_count(), 1);
493 let saved = store.get().ok_or_else(|| Error::invalid("none"))?;
494 assert_eq!(saved.hydra_jwt.as_deref(), Some(tok.as_str()));
495 c.ensure_hydra().await?;
497 assert_eq!(store.save_count(), 1);
498 Ok(())
499 }
500
501 #[tokio::test]
502 async fn failed_refresh_saves_nothing() -> Result<()> {
503 let store = store_with(Some(Session::default()));
505 let mut c = ctx(&store, Config::default()).await?;
506 let e = c.refresh_session().await.err();
507 assert_eq!(e.map(|e| e.kind), Some(ErrorKind::AuthExpired));
508 assert_eq!(store.save_count(), 0);
509 Ok(())
510 }
511
512 #[tokio::test]
513 async fn set_session_replaces_and_saves() -> Result<()> {
514 let store = store_with(Some(Session::default()));
515 let mut c = ctx(&store, Config::default()).await?;
516 let s = Session {
517 web_app_version: Some("build-2".into()),
518 ..Session::default()
519 };
520 c.set_session(s).await?;
521 assert_eq!(c.session().web_app_version.as_deref(), Some("build-2"));
522 assert_eq!(store.save_count(), 1);
523 Ok(())
524 }
525}