1 // Copyright 2023 Google LLC
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 // https://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14
15 use std::{
16 collections::{HashMap, HashSet},
17 ffi::OsStr,
18 fs,
19 io::BufReader,
20 net::TcpStream,
21 path::{Path, PathBuf},
22 str::FromStr,
23 sync::Arc,
24 };
25
26 use http::{Request, Uri};
27 use log::warn;
28
29 use crate::{
30 captures::captures_handler::handle_capture,
31 devices::devices_handler::handle_device,
32 transport::websocket::{handle_websocket, run_websocket_transport},
33 version::VERSION,
34 };
35
36 use super::{
37 http_request::parse_http_request,
38 http_router::Router,
39 server_response::{ResponseWritable, ServerResponseWritable, ServerResponseWriter},
40 };
41
42 const PATH_PREFIXES: [&str; 3] = ["js", "assets", "node_modules/tslib"];
43
ui_path(suffix: &str) -> PathBuf44 fn ui_path(suffix: &str) -> PathBuf {
45 let mut path = std::env::current_exe().unwrap();
46 path.pop();
47 path.push("netsim-ui");
48 for subpath in suffix.split('/') {
49 path.push(subpath);
50 }
51 path
52 }
53
54 /// Collect queries and output key and values into Vec
collect_query(param: &str) -> Result<HashMap<&str, &str>, &str>55 pub fn collect_query(param: &str) -> Result<HashMap<&str, &str>, &str> {
56 let mut result = HashMap::new();
57 if param.is_empty() {
58 return Ok(result);
59 }
60 for word in param.split('&') {
61 if let Some(equal) = word.find('=') {
62 if result.insert(&word[..equal], &word[equal + 1..]).is_some() {
63 return Err("Query has duplicate keys");
64 }
65 }
66 }
67 // TODO: Check if initial ChipInfo is included
68 Ok(result)
69 }
70
create_filename_hash_set() -> HashSet<String>71 pub fn create_filename_hash_set() -> HashSet<String> {
72 let mut valid_files: HashSet<String> = HashSet::new();
73 for path_prefix in PATH_PREFIXES {
74 let dir_path = ui_path(path_prefix);
75 if let Ok(mut file) = fs::read_dir(dir_path) {
76 while let Some(Ok(entry)) = file.next() {
77 valid_files.insert(entry.path().to_str().unwrap().to_string());
78 }
79 } else {
80 warn!("netsim-ui doesn't exist");
81 }
82 }
83 valid_files
84 }
85
check_valid_file_path(path: &str, valid_files: &HashSet<String>) -> bool86 fn check_valid_file_path(path: &str, valid_files: &HashSet<String>) -> bool {
87 let filepath = match path.strip_prefix('/') {
88 Some(stripped_path) => ui_path(stripped_path),
89 None => ui_path(path),
90 };
91 valid_files.contains(filepath.as_path().to_str().unwrap())
92 }
93
to_content_type(file_path: &Path) -> &str94 fn to_content_type(file_path: &Path) -> &str {
95 match file_path.extension().and_then(OsStr::to_str) {
96 Some("html") => "text/html",
97 Some("txt") => "text/plain",
98 Some("jpg") | Some("jpeg") => "image/jpeg",
99 Some("png") => "image/png",
100 Some("js") => "application/javascript",
101 Some("svg") => "image/svg+xml",
102 _ => "application/octet-stream",
103 }
104 }
105
handle_file(method: &str, path: &str, writer: ResponseWritable)106 fn handle_file(method: &str, path: &str, writer: ResponseWritable) {
107 if method == "GET" {
108 let filepath = match path.strip_prefix('/') {
109 Some(stripped_path) => ui_path(stripped_path),
110 None => ui_path(path),
111 };
112 if let Ok(body) = fs::read(&filepath) {
113 writer.put_ok_with_vec(to_content_type(&filepath), body, vec![]);
114 return;
115 }
116 }
117 let body = format!("404 not found (netsim): handle_file with unknown path {path}");
118 writer.put_error(404, body.as_str());
119 }
120
121 // TODO handlers accept additional "context" including filepath
handle_index(request: &Request<Vec<u8>>, _param: &str, writer: ResponseWritable)122 fn handle_index(request: &Request<Vec<u8>>, _param: &str, writer: ResponseWritable) {
123 handle_file(request.method().as_str(), "index.html", writer)
124 }
125
handle_static(request: &Request<Vec<u8>>, path: &str, writer: ResponseWritable)126 fn handle_static(request: &Request<Vec<u8>>, path: &str, writer: ResponseWritable) {
127 // The path verification happens in the closure wrapper around handle_static.
128 handle_file(request.method().as_str(), path, writer)
129 }
130
handle_version(_request: &Request<Vec<u8>>, _param: &str, writer: ResponseWritable)131 fn handle_version(_request: &Request<Vec<u8>>, _param: &str, writer: ResponseWritable) {
132 let body = format!("{{\"version\": \"{}\"}}", VERSION);
133 writer.put_ok("text/plain", body.as_str(), vec![]);
134 }
135
handle_dev(request: &Request<Vec<u8>>, _param: &str, writer: ResponseWritable)136 fn handle_dev(request: &Request<Vec<u8>>, _param: &str, writer: ResponseWritable) {
137 handle_file(request.method().as_str(), "dev.html", writer)
138 }
139
handle_connection(mut stream: TcpStream, valid_files: Arc<HashSet<String>>, dev: bool)140 pub fn handle_connection(mut stream: TcpStream, valid_files: Arc<HashSet<String>>, dev: bool) {
141 let mut router = Router::new();
142 router.add_route(Uri::from_static("/"), Box::new(handle_index));
143 router.add_route(Uri::from_static("/version"), Box::new(handle_version));
144 router.add_route(Uri::from_static("/v1/devices"), Box::new(handle_device));
145 router.add_route(Uri::from_static(r"/v1/devices/{id}"), Box::new(handle_device));
146 router.add_route(Uri::from_static("/v1/captures"), Box::new(handle_capture));
147 router.add_route(Uri::from_static(r"/v1/captures/{id}"), Box::new(handle_capture));
148 router.add_route(Uri::from_static(r"/v1/websocket/{radio}"), Box::new(handle_websocket));
149
150 // Adding additional routes in dev mode.
151 if dev {
152 router.add_route(Uri::from_static("/dev"), Box::new(handle_dev));
153 }
154
155 // A closure for checking if path is a static file we wish to serve, and call handle_static
156 let handle_static_wrapper =
157 move |request: &Request<Vec<u8>>, path: &str, writer: ResponseWritable| {
158 for prefix in PATH_PREFIXES {
159 let new_path = format!("{prefix}/{path}");
160 if check_valid_file_path(new_path.as_str(), &valid_files) {
161 handle_static(request, new_path.as_str(), writer);
162 return;
163 }
164 }
165 let body = format!("404 not found (netsim): Invalid path {path}");
166 writer.put_error(404, body.as_str());
167 };
168
169 // Connecting all path prefixes to handle_static_wrapper
170 for prefix in PATH_PREFIXES {
171 router.add_route(
172 Uri::from_str(format!(r"/{prefix}/{{path}}").as_str()).unwrap(),
173 Box::new(handle_static_wrapper.clone()),
174 )
175 }
176
177 if let Ok(request) = parse_http_request::<&TcpStream>(&mut BufReader::new(&stream)) {
178 let mut response_writer = ServerResponseWriter::new(&mut stream);
179 router.handle_request(&request, &mut response_writer);
180 if let Some(response) = response_writer.get_response() {
181 // Status code of 101 represents switching of protocols from HTTP to Websocket
182 if response.status().as_u16() == 101 {
183 match collect_query(request.uri().query().unwrap_or("")) {
184 Ok(queries) => run_websocket_transport(stream, queries),
185 Err(err) => warn!("{err}"),
186 };
187 }
188 }
189 } else {
190 let mut response_writer = ServerResponseWriter::new(&mut stream);
191 let body = "404 not found (netsim): parse header failed";
192 response_writer.put_error(404, body);
193 };
194 }
195
196 #[cfg(test)]
197 mod tests {
198 use super::*;
199
200 #[test]
test_collect_query()201 fn test_collect_query() {
202 // Single query pair
203 let mut expected = HashMap::new();
204 expected.insert("name", "hello");
205 assert_eq!(collect_query("name=hello"), Ok(expected));
206
207 // Multiple query pair
208 let mut expected = HashMap::new();
209 expected.insert("name", "hello");
210 expected.insert("kind", "bt");
211 assert_eq!(collect_query("name=hello&kind=bt"), Ok(expected));
212
213 // Check for duplicate keys
214 assert_eq!(collect_query("name=hello&name=world"), Err("Query has duplicate keys"));
215
216 // Empty query string
217 assert_eq!(collect_query(""), Ok(HashMap::new()));
218 }
219 }
220