1use std::sync::Arc;
13
14use serde_json::Value;
15
16use crate::cache::{Cache, CacheKey, CacheMode, Fetched};
17use crate::config::{Config, DEFAULT_CDP, HydraMode};
18use crate::error::{Error, ErrorKind, Result};
19use crate::http::{Allowlist, Client, Hydra, Request};
20use crate::jwt;
21use crate::output::Output;
22use crate::paths;
23use crate::session::{LOCATION_COOKIE, Session};
24use crate::{cdp, hydra, sso};
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28enum ResolvedHydra {
29 Native,
30 Cdp,
31}
32
33pub struct Context {
34 pub config: Config,
35 pub out: Output,
36 session: Session,
37 client: Arc<Client>,
38 allow: Allowlist,
40 hydra_skew: i64,
42 cache: Cache,
44 refresh_tried: bool,
47}
48
49const REFRESH_MARGIN_SECS: i64 = 15 * 60;
52
53impl Context {
54 pub fn new(out: Output, allow: Allowlist) -> Result<Self> {
57 let config = Config::load(&paths::config_path()?)?;
58 let session = Session::load_default()?;
59 let client = Arc::new(Client::new(&session, &config, allow.clone())?);
60 let cache = Cache::from_config(&config.cache, CacheMode::Normal);
61 Ok(Self {
62 cache,
63 config,
64 out,
65 session,
66 client,
67 allow,
68 hydra_skew: 30,
69 refresh_tried: false,
70 })
71 }
72
73 pub async fn refresh_session(&mut self) -> Result<()> {
78 self.refresh_tried = true;
79 let ua = self
80 .config
81 .user_agent
82 .clone()
83 .or_else(|| self.session.user_agent.clone())
84 .unwrap_or_else(|| crate::http::DEFAULT_UA.to_string());
85 sso::silent_refresh(&mut self.session, &ua).await?;
86 self.session.save_default()?;
87 self.client = Arc::new(Client::new(
88 &self.session,
89 &self.config,
90 self.allow.clone(),
91 )?);
92 Ok(())
93 }
94
95 pub async fn restore_address_from_browser(&mut self, cdp_base: &str) -> Result<bool> {
100 let (cookies, _) = cdp::default_context_cookies(cdp_base).await?;
101 if !self
102 .session
103 .replace_named_cookies(LOCATION_COOKIE, &cookies)
104 {
105 return Ok(false);
106 }
107 self.session.save_default()?;
108 self.client = Arc::new(Client::new(
109 &self.session,
110 &self.config,
111 self.allow.clone(),
112 )?);
113 Ok(true)
114 }
115
116 pub fn set_location(&mut self, loc: &Value) -> Result<()> {
120 let json = serde_json::to_string(loc).map_err(|e| Error::parse(e.to_string()))?;
121 let value = urlencoding::encode(&json).into_owned();
122 self.session.set_location_cookie(&value);
123 self.session.save_default()?;
124 self.client = Arc::new(Client::new(
125 &self.session,
126 &self.config,
127 self.allow.clone(),
128 )?);
129 Ok(())
130 }
131
132 pub async fn after_login(&mut self) -> Vec<String> {
136 let mut notes = Vec::new();
137 if self.session.cookie(LOCATION_COOKIE).is_none() {
138 let base = self.cdp_base();
139 match self.restore_address_from_browser(&base).await {
140 Ok(true) => notes.push("copied the saved delivery address from the browser".into()),
141 Ok(false) => notes.push(
142 "no saved delivery address in the session or the browser; set one on \
143 food.grab.com, then `grab auth import --address-only`"
144 .into(),
145 ),
146 Err(_) => notes.push(
147 "no saved delivery address and the browser was unreachable; later run \
148 `grab auth import --address-only`"
149 .into(),
150 ),
151 }
152 }
153 if !self.has_mart_session()
154 && let Err(e) = self.login_mart().await
155 {
156 notes.push(if e.kind == ErrorKind::AuthMissing {
157 format!("GrabMart: {}", e.message)
158 } else {
159 format!(
160 "GrabMart session not set up now ({}); `grab mart quote` retries it",
161 e.kind.as_str()
162 )
163 });
164 }
165 if !self.session.hydra_valid(self.hydra_skew)
166 && let Err(e) = self.mint_hydra().await
167 {
168 notes.push(format!(
169 "hydra not minted now ({}); commands mint it on demand",
170 e.kind.as_str()
171 ));
172 }
173 notes
174 }
175
176 pub async fn login_mart(&mut self) -> Result<()> {
181 let ua = self
182 .config
183 .user_agent
184 .clone()
185 .or_else(|| self.session.user_agent.clone())
186 .unwrap_or_else(|| crate::http::DEFAULT_UA.to_string());
187 sso::silent_login(sso::Service::Mart, &mut self.session, &ua).await?;
188 self.session.save_default()?;
189 self.client = Arc::new(Client::new(
190 &self.session,
191 &self.config,
192 self.allow.clone(),
193 )?);
194 Ok(())
195 }
196
197 pub fn has_mart_session(&self) -> bool {
199 !sso::mart_session_cookies(&self.session).is_empty()
200 }
201
202 fn session_expiring(&self) -> bool {
206 self.session.gfc_logged_in()
207 && self
208 .session
209 .gfc_expires_at()
210 .is_some_and(|e| e - jwt::now() < REFRESH_MARGIN_SECS)
211 }
212
213 #[must_use]
216 pub fn with_cache_mode(mut self, mode: CacheMode) -> Self {
217 self.cache = Cache::from_config(&self.config.cache, mode);
218 self
219 }
220
221 pub fn cache(&self) -> &Cache {
224 &self.cache
225 }
226
227 pub async fn cached_call(&mut self, key: &CacheKey, req: &Request) -> Result<Value> {
231 let cache = self.cache.clone();
232 cache.get_or_fetch(key, || self.call(req)).await
233 }
234
235 pub async fn cached_call_meta(
239 &mut self,
240 key: &CacheKey,
241 req: &Request,
242 ) -> Result<Fetched<Value>> {
243 let cache = self.cache.clone();
244 cache.get_or_fetch_meta(key, || self.call(req)).await
245 }
246
247 pub fn session(&self) -> &Session {
248 &self.session
249 }
250
251 pub fn cdp_base(&self) -> String {
252 self.config
253 .cdp
254 .clone()
255 .unwrap_or_else(|| DEFAULT_CDP.to_string())
256 }
257
258 fn resolve_hydra_mode(&self) -> ResolvedHydra {
259 let key_present = self.hydra_key_pem().is_some();
260 match self.config.hydra_mode.unwrap_or_default() {
261 HydraMode::Native => ResolvedHydra::Native,
262 HydraMode::Cdp => ResolvedHydra::Cdp,
263 HydraMode::Auto => {
264 if key_present {
265 ResolvedHydra::Native
266 } else {
267 ResolvedHydra::Cdp
268 }
269 }
270 }
271 }
272
273 fn hydra_key_path(&self) -> Option<std::path::PathBuf> {
274 if let Some(p) = &self.config.hydra_key {
275 return Some(p.clone());
276 }
277 let default = paths::config_dir().ok()?.join("hydra-key.pem");
278 default.exists().then_some(default)
279 }
280
281 fn hydra_key_pem(&self) -> Option<String> {
282 std::fs::read_to_string(self.hydra_key_path()?).ok()
283 }
284
285 pub async fn ensure_hydra(&mut self) -> Result<String> {
288 if self.session.hydra_valid(self.hydra_skew)
289 && let Some(tok) = &self.session.hydra_jwt
290 {
291 return Ok(tok.clone());
292 }
293 self.mint_hydra().await
294 }
295
296 pub async fn mint_hydra(&mut self) -> Result<String> {
298 match self.resolve_hydra_mode() {
299 ResolvedHydra::Native => {
300 let pem = self.hydra_key_pem().ok_or_else(|| {
301 Error::new(
302 ErrorKind::HydraExpired,
303 "native hydra minting needs a signing key at \
304 <config>/hydra-key.pem (see docs/hydra.md); or set \
305 hydra_mode = \"cdp\"",
306 )
307 })?;
308 let ver = hydra::DEFAULT_VER;
309 let (tok, exp) = hydra::mint_native(&pem, &hydra::DeviceFields::random(), ver)?;
310 self.session.hydra_jwt = Some(tok.clone());
311 self.session.hydra_exp = Some(exp);
312 self.session.save_default()?;
313 Ok(tok)
314 }
315 ResolvedHydra::Cdp => {
316 let base = self.cdp_base();
317 let page = self
318 .config
319 .hydra_page
320 .clone()
321 .unwrap_or_else(|| "https://food.grab.com/ph/en/".to_string());
322 cdp::mint_hydra(&base, &page, &mut self.session).await?;
323 self.session.save_default()?;
324 self.session.hydra_jwt.clone().ok_or_else(|| {
325 Error::new(ErrorKind::HydraExpired, "hydra mint produced no token")
326 })
327 }
328 }
329 }
330
331 pub async fn call(&mut self, req: &Request) -> Result<Value> {
334 if self.config.auto_refresh() && !self.refresh_tried && self.session_expiring() {
337 let _ = self.refresh_session().await;
338 }
339 let token = if req.hydra == Hydra::Required {
340 Some(self.ensure_hydra().await?)
341 } else {
342 None
343 };
344
345 match self.client.send(req, token.as_deref()).await {
346 Err(e)
347 if e.kind == ErrorKind::HydraExpired
348 && req.hydra == Hydra::Required
349 && self.config.auto_refresh() =>
350 {
351 let fresh = self.mint_hydra().await?;
353 self.client.send(req, Some(&fresh)).await
354 }
355 Err(e)
356 if e.kind == ErrorKind::AuthExpired
357 && req.refreshable
358 && !self.refresh_tried
359 && self.config.auto_refresh() =>
360 {
361 self.refresh_session().await?;
365 let token = if req.hydra == Hydra::Required {
366 Some(self.ensure_hydra().await?)
367 } else {
368 None
369 };
370 self.client.send(req, token.as_deref()).await
371 }
372 other => other,
373 }
374 }
375
376 pub fn auth_status(&self) -> Value {
379 let now = jwt::now();
380 let loc = crate::location::Location::from_session(&self.session).ok();
381 let cookies: Vec<Value> = self
382 .session
383 .cookies
384 .iter()
385 .map(|c| {
386 let exp = if c.expires > 0.0 {
387 Some(c.expires as i64)
388 } else {
389 None
390 };
391 serde_json::json!({
392 "name": c.name,
393 "domain": c.domain,
394 "expires": exp,
395 "expires_in_secs": exp.map(|e| e - now),
396 "expired": exp.is_some_and(|e| e <= now),
397 })
398 })
399 .collect();
400 serde_json::json!({
401 "session_path": paths::session_path().ok().map(|p| p.display().to_string()),
402 "cookies": cookies,
403 "gfc_session_present": self.session.cookie("gfc_session").is_some(),
404 "gfc_session_logged_in": self.session.gfc_logged_in(),
405 "gfc_session_exp": self.session.gfc_expires_at(),
406 "gfc_session_expires_in_secs": self.session.gfc_expires_at().map(|e| e - now),
407 "hydra_exp": self.session.hydra_exp,
408 "hydra_expires_in_secs": self.session.hydra_exp.map(|e| e - now),
409 "hydra_valid": self.session.hydra_valid(self.hydra_skew),
410 "web_app_version_present": self.session.web_app_version.is_some(),
411 "payment_type_id_present":
412 self.session.payment_type_id.is_some() || self.config.payment_type_id.is_some(),
413 "has_saved_address": self.session.cookie("location").is_some(),
414 "address_detail": loc.as_ref().and_then(|l| l.address_detail.clone()),
415 "note_to_driver": loc.as_ref().and_then(|l| l.note_to_driver.clone()),
416 })
417 }
418}