tauri_plugin_localhost/
lib.rs1#![doc(
10 html_logo_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png",
11 html_favicon_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png"
12)]
13
14use std::collections::HashMap;
15
16use http::Uri;
17use tauri::{
18 plugin::{Builder as PluginBuilder, TauriPlugin},
19 Runtime,
20};
21use tiny_http::{Header, Response as HttpResponse, Server};
22
23pub struct Request {
24 url: String,
25 body: Vec<u8>,
26 method: String,
27}
28
29impl Request {
30 pub fn url(&self) -> &str {
31 &self.url
32 }
33
34 pub fn body(&self) -> &[u8] {
35 &self.body
36 }
37
38 pub fn method(&self) -> &str {
39 &self.method
40 }
41}
42
43pub struct Response {
44 headers: HashMap<String, String>,
45 status_code: u16,
46 body: Vec<u8>,
47 handled: bool,
48}
49
50impl Response {
51 pub fn add_header<H: Into<String>, V: Into<String>>(&mut self, header: H, value: V) {
52 self.headers.insert(header.into(), value.into());
53 }
54
55 pub fn set_status(&mut self, code: u16) {
56 self.status_code = code;
57 }
58
59 pub fn set_body(&mut self, body: Vec<u8>) {
60 self.body = body;
61 }
62
63 pub fn set_handled(&mut self, handled: bool) {
66 self.handled = handled;
67 }
68}
69
70type OnRequest = Option<Box<dyn Fn(&Request, &mut Response) + Send + Sync>>;
71
72pub struct Builder {
73 port: u16,
74 host: Option<String>,
75 on_request: OnRequest,
76}
77
78impl Builder {
79 pub fn new(port: u16) -> Self {
80 Self {
81 port,
82 host: None,
83 on_request: None,
84 }
85 }
86
87 pub fn host<H: Into<String>>(mut self, host: H) -> Self {
89 self.host = Some(host.into());
90 self
91 }
92
93 pub fn on_request<F: Fn(&Request, &mut Response) + Send + Sync + 'static>(
94 mut self,
95 f: F,
96 ) -> Self {
97 self.on_request.replace(Box::new(f));
98 self
99 }
100
101 pub fn build<R: Runtime>(mut self) -> TauriPlugin<R> {
102 let port = self.port;
103 let host = self.host.unwrap_or("localhost".to_string());
104 let on_request = self.on_request.take();
105
106 PluginBuilder::new("localhost")
107 .setup(move |app, _api| {
108 let asset_resolver = app.asset_resolver();
109 std::thread::spawn(move || {
110 let server =
111 Server::http(format!("{host}:{port}")).expect("Unable to spawn server");
112 for mut req in server.incoming_requests() {
113 let path: String = req
114 .url()
115 .parse::<Uri>()
116 .map(|uri| uri.path().into())
117 .unwrap_or_else(|_| req.url().into());
118
119 let mut body_bytes = Vec::new();
121 let _ = std::io::Read::read_to_end(req.as_reader(), &mut body_bytes);
122
123 let request = Request {
124 url: req.url().into(),
125 body: body_bytes,
126 method: req.method().to_string(),
127 };
128 let mut response = Response {
129 headers: Default::default(),
130 status_code: 200,
131 body: Vec::new(),
132 handled: false,
133 };
134
135 if let Some(on_request) = &on_request {
137 on_request(&request, &mut response);
138 }
139
140 if response.handled {
142 let mut resp = HttpResponse::from_data(response.body)
143 .with_status_code(response.status_code);
144 for (header, value) in response.headers {
145 if let Ok(h) = Header::from_bytes(header.as_bytes(), value) {
146 resp.add_header(h);
147 }
148 }
149 let _ = req.respond(resp);
150 continue;
151 }
152
153 #[allow(unused_mut)]
155 if let Some(mut asset) = asset_resolver.get(path) {
156 response.add_header("Content-Type", asset.mime_type);
157 if let Some(csp) = asset.csp_header {
158 response
159 .headers
160 .insert("Content-Security-Policy".into(), csp);
161 }
162
163 response
164 .headers
165 .insert("Cache-Control".into(), "no-cache".into());
166
167 let mut resp = HttpResponse::from_data(asset.bytes);
168 for (header, value) in response.headers {
169 if let Ok(h) = Header::from_bytes(header.as_bytes(), value) {
170 resp.add_header(h);
171 }
172 }
173 req.respond(resp).expect("unable to setup response");
174 }
175 }
176 });
177 Ok(())
178 })
179 .build()
180 }
181}