// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. use std::cell::RefCell; use std::path::PathBuf; use std::rc::Rc; use std::sync::Arc; use async_trait::async_trait; use deno_core::error::AnyError; use deno_core::op; use deno_core::serde::Deserialize; use deno_core::serde::Serialize; use deno_core::ByteString; use deno_core::OpState; use deno_core::Resource; use deno_core::ResourceId; mod sqlite; pub use sqlite::SqliteBackedCache; #[derive(Clone)] pub struct CreateCache(pub Arc C>); deno_core::extension!(deno_cache, deps = [ deno_webidl, deno_web, deno_url, deno_fetch ], parameters=[CA: Cache], ops = [ op_cache_storage_open, op_cache_storage_has, op_cache_storage_delete, op_cache_put, op_cache_match, op_cache_delete, ], esm = [ "01_cache.js" ], options = { maybe_create_cache: Option>, }, state = |state, options| { if let Some(create_cache) = options.maybe_create_cache { state.put(create_cache); } }, ); pub fn get_declaration() -> PathBuf { PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("lib.deno_cache.d.ts") } #[derive(Deserialize, Serialize, Debug, Clone)] #[serde(rename_all = "camelCase")] pub struct CachePutRequest { pub cache_id: i64, pub request_url: String, pub request_headers: Vec<(ByteString, ByteString)>, pub response_headers: Vec<(ByteString, ByteString)>, pub response_has_body: bool, pub response_status: u16, pub response_status_text: String, } #[derive(Deserialize, Serialize, Debug)] #[serde(rename_all = "camelCase")] pub struct CacheMatchRequest { pub cache_id: i64, pub request_url: String, pub request_headers: Vec<(ByteString, ByteString)>, } #[derive(Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct CacheMatchResponse(CacheMatchResponseMeta, Option); #[derive(Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct CacheMatchResponseMeta { pub response_status: u16, pub response_status_text: String, pub request_headers: Vec<(ByteString, ByteString)>, pub response_headers: Vec<(ByteString, ByteString)>, } #[derive(Deserialize, Serialize, Debug)] #[serde(rename_all = "camelCase")] pub struct CacheDeleteRequest { pub cache_id: i64, pub request_url: String, } #[async_trait] pub trait Cache: Clone { async fn storage_open(&self, cache_name: String) -> Result; async fn storage_has(&self, cache_name: String) -> Result; async fn storage_delete(&self, cache_name: String) -> Result; async fn put( &self, request_response: CachePutRequest, ) -> Result>, AnyError>; async fn r#match( &self, request: CacheMatchRequest, ) -> Result< Option<(CacheMatchResponseMeta, Option>)>, AnyError, >; async fn delete(&self, request: CacheDeleteRequest) -> Result; } #[op] pub async fn op_cache_storage_open( state: Rc>, cache_name: String, ) -> Result where CA: Cache + 'static, { let cache = get_cache::(&state)?; cache.storage_open(cache_name).await } #[op] pub async fn op_cache_storage_has( state: Rc>, cache_name: String, ) -> Result where CA: Cache + 'static, { let cache = get_cache::(&state)?; cache.storage_has(cache_name).await } #[op] pub async fn op_cache_storage_delete( state: Rc>, cache_name: String, ) -> Result where CA: Cache + 'static, { let cache = get_cache::(&state)?; cache.storage_delete(cache_name).await } #[op] pub async fn op_cache_put( state: Rc>, request_response: CachePutRequest, ) -> Result, AnyError> where CA: Cache + 'static, { let cache = get_cache::(&state)?; match cache.put(request_response).await? { Some(resource) => { let rid = state.borrow_mut().resource_table.add_rc_dyn(resource); Ok(Some(rid)) } None => Ok(None), } } #[op] pub async fn op_cache_match( state: Rc>, request: CacheMatchRequest, ) -> Result, AnyError> where CA: Cache + 'static, { let cache = get_cache::(&state)?; match cache.r#match(request).await? { Some((meta, None)) => Ok(Some(CacheMatchResponse(meta, None))), Some((meta, Some(resource))) => { let rid = state.borrow_mut().resource_table.add_rc_dyn(resource); Ok(Some(CacheMatchResponse(meta, Some(rid)))) } None => Ok(None), } } #[op] pub async fn op_cache_delete( state: Rc>, request: CacheDeleteRequest, ) -> Result where CA: Cache + 'static, { let cache = get_cache::(&state)?; cache.delete(request).await } pub fn get_cache(state: &Rc>) -> Result where CA: Cache + 'static, { let mut state = state.borrow_mut(); if let Some(cache) = state.try_borrow::() { Ok(cache.clone()) } else { let create_cache = state.borrow::>().clone(); let cache = create_cache.0(); state.put(cache); Ok(state.borrow::().clone()) } } /// Check if headers, mentioned in the vary header, of query request /// and cached request are equal. pub fn vary_header_matches( vary_header: &ByteString, query_request_headers: &[(ByteString, ByteString)], cached_request_headers: &[(ByteString, ByteString)], ) -> bool { let vary_header = match std::str::from_utf8(vary_header) { Ok(vary_header) => vary_header, Err(_) => return false, }; let headers = get_headers_from_vary_header(vary_header); for header in headers { let query_header = get_header(&header, query_request_headers); let cached_header = get_header(&header, cached_request_headers); if query_header != cached_header { return false; } } true } #[test] fn test_vary_header_matches() { let vary_header = ByteString::from("accept-encoding"); let query_request_headers = vec![( ByteString::from("accept-encoding"), ByteString::from("gzip"), )]; let cached_request_headers = vec![( ByteString::from("accept-encoding"), ByteString::from("gzip"), )]; assert!(vary_header_matches( &vary_header, &query_request_headers, &cached_request_headers )); let vary_header = ByteString::from("accept-encoding"); let query_request_headers = vec![( ByteString::from("accept-encoding"), ByteString::from("gzip"), )]; let cached_request_headers = vec![(ByteString::from("accept-encoding"), ByteString::from("br"))]; assert!(!vary_header_matches( &vary_header, &query_request_headers, &cached_request_headers )); } /// Get headers from the vary header. pub fn get_headers_from_vary_header(vary_header: &str) -> Vec { vary_header .split(',') .map(|s| s.trim().to_lowercase()) .collect() } #[test] fn test_get_headers_from_vary_header() { let headers = get_headers_from_vary_header("accept-encoding"); assert_eq!(headers, vec!["accept-encoding"]); let headers = get_headers_from_vary_header("accept-encoding, user-agent"); assert_eq!(headers, vec!["accept-encoding", "user-agent"]); } /// Get value for the header with the given name. pub fn get_header( name: &str, headers: &[(ByteString, ByteString)], ) -> Option { headers .iter() .find(|(k, _)| { if let Ok(k) = std::str::from_utf8(k) { k.eq_ignore_ascii_case(name) } else { false } }) .map(|(_, v)| v.to_owned()) } #[test] fn test_get_header() { let headers = vec![ ( ByteString::from("accept-encoding"), ByteString::from("gzip"), ), ( ByteString::from("content-type"), ByteString::from("application/json"), ), ( ByteString::from("vary"), ByteString::from("accept-encoding"), ), ]; let value = get_header("accept-encoding", &headers); assert_eq!(value, Some(ByteString::from("gzip"))); let value = get_header("content-type", &headers); assert_eq!(value, Some(ByteString::from("application/json"))); let value = get_header("vary", &headers); assert_eq!(value, Some(ByteString::from("accept-encoding"))); } /// Serialize headers into bytes. pub fn serialize_headers(headers: &[(ByteString, ByteString)]) -> Vec { let mut serialized_headers = Vec::new(); for (name, value) in headers { serialized_headers.extend_from_slice(name); serialized_headers.extend_from_slice(b"\r\n"); serialized_headers.extend_from_slice(value); serialized_headers.extend_from_slice(b"\r\n"); } serialized_headers } /// Deserialize bytes into headers. pub fn deserialize_headers( serialized_headers: &[u8], ) -> Vec<(ByteString, ByteString)> { let mut headers = Vec::new(); let mut piece = None; let mut start = 0; for (i, byte) in serialized_headers.iter().enumerate() { if byte == &b'\r' && serialized_headers.get(i + 1) == Some(&b'\n') { if piece.is_none() { piece = Some(start..i); } else { let name = piece.unwrap(); let value = start..i; headers.push(( ByteString::from(&serialized_headers[name]), ByteString::from(&serialized_headers[value]), )); piece = None; } start = i + 2; } } assert!(piece.is_none()); assert_eq!(start, serialized_headers.len()); headers }