webview_bundle_protocol/protocol/
local.rs1use async_trait::async_trait;
2use dashmap::DashMap;
3use std::borrow::Cow;
4use std::collections::HashMap;
5use webview_bundle::http;
6use webview_bundle::http::Uri;
7
8pub trait LocalUriResolver: Send + Sync {
9 fn resolve_localhost(&self, uri: &Uri) -> Option<String>;
11
12 fn get_localhost_uri(&self, uri: &Uri) -> Option<String> {
14 if let Some(localhost) = self.resolve_localhost(uri) {
15 let decoded_path = percent_encoding::percent_decode(uri.path().as_bytes())
16 .decode_utf8_lossy()
17 .to_string();
18 let url = format!(
20 "{}/{}{}",
21 localhost.trim_end_matches('/'),
22 decoded_path.trim_start_matches('/'),
23 match uri.query() {
24 Some(q) => format!("?{q}"),
25 None => "".to_string(),
26 },
27 );
28 return Some(url);
29 }
30 None
31 }
32}
33
34#[derive(Default)]
35pub struct MappingLocalUriResolver {
36 mapping: HashMap<String, String>,
37}
38
39impl MappingLocalUriResolver {
40 pub fn new<T: Into<HashMap<String, String>>>(mapping: T) -> Self {
41 Self {
42 mapping: mapping.into(),
43 }
44 }
45}
46
47impl LocalUriResolver for MappingLocalUriResolver {
48 fn resolve_localhost(&self, uri: &Uri) -> Option<String> {
49 match uri.host() {
50 Some(host) => self.mapping.get(host).cloned(),
51 None => None,
52 }
53 }
54}
55
56#[derive(Clone)]
57struct CachedResponse {
58 status: http::StatusCode,
59 headers: http::HeaderMap,
60 body: bytes::Bytes,
61}
62
63pub struct LocalProtocol {
64 uri_resolver: Box<dyn LocalUriResolver + 'static>,
65 cache: DashMap<String, CachedResponse>,
66}
67
68impl LocalProtocol {
69 pub fn new<T: Into<HashMap<String, String>>>(mapping: T) -> Self {
70 Self {
71 uri_resolver: Box::new(MappingLocalUriResolver::new(mapping)),
72 cache: DashMap::default(),
73 }
74 }
75}
76
77#[async_trait]
78impl super::Protocol for LocalProtocol {
79 async fn handle(
80 &self,
81 request: http::Request<Vec<u8>>,
82 ) -> crate::Result<http::Response<Cow<'static, [u8]>>> {
83 let url = self
84 .uri_resolver
85 .get_localhost_uri(request.uri())
86 .ok_or(crate::Error::LocalNotFound)?;
87
88 let mut builder = http::Response::builder();
89
90 let client = reqwest::ClientBuilder::new();
91 let mut proxy_builder = client.build()?.request(request.method().clone(), &url);
92 proxy_builder = proxy_builder.body(request.body().clone());
93 let r = proxy_builder.send().await?;
94 let mut response = None;
95 if r.status() == http::StatusCode::NOT_MODIFIED {
96 response = self.cache.get(&url)
97 }
98 let response = if let Some(r) = response {
99 r
100 } else {
101 let status = r.status();
102 let headers = r.headers().clone();
103 let body = r.bytes().await?;
104 let response = CachedResponse {
105 status,
106 headers,
107 body,
108 };
109 self.cache.insert(url.to_string(), response);
110 self.cache.get(&url).unwrap()
111 };
112 for (name, value) in &response.headers {
113 builder = builder.header(name, value);
114 }
115 let resp = builder
116 .status(response.status)
117 .body(response.body.to_vec().into())?;
118 Ok(resp)
119 }
120}
121
122#[cfg(test)]
123mod tests {
124 use crate::{LocalProtocol, Protocol};
125 use std::collections::HashMap;
126 use std::iter;
127 use std::net::{SocketAddr, TcpListener};
128 use std::sync::atomic::{AtomicUsize, Ordering};
129 use std::sync::Arc;
130 use tiny_http::{Header as TinyHeader, Method, Response as TinyResponse, Server as TinyServer};
131 use webview_bundle::http;
132
133 fn server() -> (SocketAddr, std::thread::JoinHandle<()>) {
134 let listener = TcpListener::bind("127.0.0.1:0").unwrap();
135 let addr = listener.local_addr().unwrap();
136 let server = TinyServer::from_listener(listener, None).unwrap();
137
138 let counter = Arc::new(AtomicUsize::new(0));
139 let counter_for_thread = counter.clone();
140
141 let handle = std::thread::spawn(move || {
142 for request in server.incoming_requests() {
143 let n = counter_for_thread.fetch_add(1, Ordering::SeqCst) + 1;
144 if request.method() == &Method::Get && request.url().starts_with("/index.html") {
145 if n == 1 {
146 let mut resp = TinyResponse::from_string("Hello World");
147 resp.add_header(TinyHeader::from_bytes("Content-Type", "text/plain").unwrap());
148 resp.add_header(TinyHeader::from_bytes("ETag", "\"v1\"").unwrap());
149 let _ = request.respond(resp);
150 } else {
151 let mut resp = TinyResponse::empty(304);
153 resp.add_header(TinyHeader::from_bytes("ETag", "\"v1\"").unwrap());
154 let _ = request.respond(resp);
155 }
156 } else {
157 let _ = request.respond(TinyResponse::empty(404));
158 }
159 }
160 });
161
162 (addr, handle)
163 }
164
165 #[tokio::test]
166 async fn smoke() {
167 let (addr, _) = server();
168 let mapping =
169 iter::once(("nextjs.wvb".to_string(), format!("http://{addr}"))).collect::<HashMap<_, _>>();
170 let protocol = LocalProtocol::new(mapping);
171
172 let first_req = http::Request::builder()
173 .uri("https://nextjs.wvb/index.html")
174 .method("GET")
175 .body(Vec::new())
176 .unwrap();
177 let first_resp = protocol.handle(first_req).await.unwrap();
178 assert_eq!(first_resp.status(), 200);
179 assert_eq!(
180 first_resp.headers().get("content-type").unwrap(),
181 "text/plain"
182 );
183 assert_eq!(first_resp.body().as_ref(), b"Hello World");
184
185 let second_req = http::Request::builder()
186 .uri("https://nextjs.wvb/index.html")
187 .method("GET")
188 .body(Vec::new())
189 .unwrap();
190 let second_resp = protocol.handle(second_req).await.unwrap();
191 assert_eq!(second_resp.status(), 200);
192 assert_eq!(
193 second_resp.headers().get("content-type").unwrap(),
194 "text/plain"
195 );
196 assert_eq!(second_resp.headers().get("etag").unwrap(), "\"v1\"");
197 assert_eq!(first_resp.body().as_ref(), b"Hello World");
198 }
199}