xref: /aosp_15_r20/tools/netsim/rust/daemon/src/http_server/http_handlers.rs (revision cf78ab8cffb8fc9207af348f23af247fb04370a6)
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