• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 pub(crate) mod http_request;
16 mod http_response;
17 mod http_router;
18 pub(crate) mod server_response;
19 mod thread_pool;
20 
21 use crate::captures::handlers::*;
22 use crate::http_server::http_request::HttpRequest;
23 use crate::http_server::http_router::Router;
24 use crate::http_server::server_response::{
25     ResponseWritable, ServerResponseWritable, ServerResponseWriter,
26 };
27 use crate::version::VERSION;
28 
29 use crate::http_server::thread_pool::ThreadPool;
30 
31 use crate::ffi::get_devices;
32 use crate::ffi::patch_device;
33 use cxx::let_cxx_string;
34 use std::collections::HashSet;
35 use std::ffi::OsStr;
36 use std::fs;
37 use std::io::BufReader;
38 use std::net::TcpListener;
39 use std::net::TcpStream;
40 use std::path::Path;
41 use std::path::PathBuf;
42 use std::sync::Arc;
43 
44 const PATH_PREFIXES: [&str; 3] = ["js", "assets", "node_modules/tslib"];
45 
run_http_server()46 pub fn run_http_server() {
47     let listener = match TcpListener::bind("127.0.0.1:7681") {
48         Ok(listener) => listener,
49         Err(e) => {
50             eprintln!("netsimd: bind error in netsimd frontend http server. {}", e);
51             return;
52         }
53     };
54     let pool = ThreadPool::new(4);
55     println!("netsimd: Frontend http server is listening on http://localhost:7681");
56     let valid_files = Arc::new(create_filename_hash_set());
57     for stream in listener.incoming() {
58         let stream = stream.unwrap();
59         let valid_files = valid_files.clone();
60         pool.execute(move || {
61             handle_connection(stream, valid_files);
62         });
63     }
64 
65     println!("netsimd: Shutting down frontend http server.");
66 }
67 
ui_path(suffix: &str) -> PathBuf68 fn ui_path(suffix: &str) -> PathBuf {
69     let mut path = std::env::current_exe().unwrap();
70     path.pop();
71     path.push("netsim-ui");
72     for subpath in suffix.split('/') {
73         path.push(subpath);
74     }
75     path
76 }
77 
create_filename_hash_set() -> HashSet<String>78 fn create_filename_hash_set() -> HashSet<String> {
79     let mut valid_files: HashSet<String> = HashSet::new();
80     for path_prefix in PATH_PREFIXES {
81         let dir_path = ui_path(path_prefix);
82         if let Ok(mut file) = fs::read_dir(dir_path) {
83             while let Some(Ok(entry)) = file.next() {
84                 valid_files.insert(entry.path().to_str().unwrap().to_string());
85             }
86         } else {
87             println!("netsim-ui doesn't exist");
88         }
89     }
90     valid_files
91 }
92 
check_valid_file_path(path: &str, valid_files: &HashSet<String>) -> bool93 fn check_valid_file_path(path: &str, valid_files: &HashSet<String>) -> bool {
94     let filepath = match path.strip_prefix('/') {
95         Some(stripped_path) => ui_path(stripped_path),
96         None => ui_path(path),
97     };
98     valid_files.contains(filepath.as_path().to_str().unwrap())
99 }
100 
to_content_type(file_path: &Path) -> &str101 fn to_content_type(file_path: &Path) -> &str {
102     match file_path.extension().and_then(OsStr::to_str) {
103         Some("html") => "text/html",
104         Some("txt") => "text/plain",
105         Some("jpg") | Some("jpeg") => "image/jpeg",
106         Some("png") => "image/png",
107         Some("js") => "application/javascript",
108         Some("svg") => "image/svg+xml",
109         _ => "application/octet-stream",
110     }
111 }
112 
handle_file(method: &str, path: &str, writer: ResponseWritable)113 fn handle_file(method: &str, path: &str, writer: ResponseWritable) {
114     if method == "GET" {
115         let filepath = match path.strip_prefix('/') {
116             Some(stripped_path) => ui_path(stripped_path),
117             None => ui_path(path),
118         };
119         if let Ok(body) = fs::read(&filepath) {
120             writer.put_ok_with_vec(to_content_type(&filepath), body, &[]);
121             return;
122         }
123     }
124     let body = format!("404 not found (netsim): handle_file with unknown path {path}");
125     writer.put_error(404, body.as_str());
126 }
127 
handle_pcap_file(request: &HttpRequest, id: &str, writer: ResponseWritable)128 fn handle_pcap_file(request: &HttpRequest, id: &str, writer: ResponseWritable) {
129     if &request.method == "GET" {
130         let mut filepath = std::env::current_exe().unwrap();
131         filepath.pop();
132         filepath.push("/tmp");
133         filepath.push(format!("{}-hci.pcap", id.replace("%20", " ")));
134         if let Ok(body) = fs::read(&filepath) {
135             writer.put_ok_with_vec(to_content_type(&filepath), body, &[]);
136             return;
137         }
138     }
139     let body = "404 not found (netsim): pcap file not exists for the device".to_string();
140     writer.put_error(404, body.as_str());
141 }
142 
143 // TODO handlers accept additional "context" including filepath
handle_index(request: &HttpRequest, _param: &str, writer: ResponseWritable)144 fn handle_index(request: &HttpRequest, _param: &str, writer: ResponseWritable) {
145     handle_file(&request.method, "index.html", writer)
146 }
147 
handle_static(request: &HttpRequest, path: &str, writer: ResponseWritable)148 fn handle_static(request: &HttpRequest, path: &str, writer: ResponseWritable) {
149     // The path verification happens in the closure wrapper around handle_static.
150     handle_file(&request.method, path, writer)
151 }
152 
handle_version(_request: &HttpRequest, _param: &str, writer: ResponseWritable)153 fn handle_version(_request: &HttpRequest, _param: &str, writer: ResponseWritable) {
154     let body = format!("{{\"version\": \"{}\"}}", VERSION);
155     writer.put_ok("text/plain", body.as_str(), &[]);
156 }
157 
handle_devices(request: &HttpRequest, _param: &str, writer: ResponseWritable)158 fn handle_devices(request: &HttpRequest, _param: &str, writer: ResponseWritable) {
159     if &request.method == "GET" {
160         let_cxx_string!(request = "");
161         let_cxx_string!(response = "");
162         let_cxx_string!(error_message = "");
163         let status = get_devices(&request, response.as_mut(), error_message.as_mut());
164         if status == 200 {
165             writer.put_ok("text/plain", response.to_string().as_str(), &[]);
166         } else {
167             let body = format!("404 Not found (netsim): {:?}", error_message.to_string());
168             writer.put_error(404, body.as_str());
169         }
170     } else if &request.method == "PATCH" {
171         let_cxx_string!(new_request = &request.body);
172         let_cxx_string!(response = "");
173         let_cxx_string!(error_message = "");
174         let status = patch_device(&new_request, response.as_mut(), error_message.as_mut());
175         if status == 200 {
176             writer.put_ok("text/plain", response.to_string().as_str(), &[]);
177         } else {
178             let body = format!("404 Not found (netsim): {:?}", error_message.to_string());
179             writer.put_error(404, body.as_str());
180         }
181     } else {
182         let body = format!(
183             "404 Not found (netsim): {:?} is not a valid method for this route",
184             request.method.to_string()
185         );
186         writer.put_error(404, body.as_str());
187     }
188 }
189 
handle_connection(mut stream: TcpStream, valid_files: Arc<HashSet<String>>)190 fn handle_connection(mut stream: TcpStream, valid_files: Arc<HashSet<String>>) {
191     let mut router = Router::new();
192     router.add_route("/", Box::new(handle_index));
193     router.add_route("/version", Box::new(handle_version));
194     router.add_route("/v1/devices", Box::new(handle_devices));
195     router.add_route(r"/pcap/{id}", Box::new(handle_pcap_file));
196     router.add_route(r"/v1/captures", Box::new(handle_capture));
197     router.add_route(r"/v1/captures/{id}", Box::new(handle_capture));
198 
199     // A closure for checking if path is a static file we wish to serve, and call handle_static
200     let handle_static_wrapper =
201         move |request: &HttpRequest, path: &str, writer: ResponseWritable| {
202             for prefix in PATH_PREFIXES {
203                 let new_path = format!("{prefix}/{path}");
204                 if check_valid_file_path(new_path.as_str(), &valid_files) {
205                     handle_static(request, new_path.as_str(), writer);
206                     return;
207                 }
208             }
209             let body = format!("404 not found (netsim): Invalid path {path}");
210             writer.put_error(404, body.as_str());
211         };
212 
213     // Connecting all path prefixes to handle_static_wrapper
214     for prefix in PATH_PREFIXES {
215         router.add_route(
216             format!(r"/{prefix}/{{path}}").as_str(),
217             Box::new(handle_static_wrapper.clone()),
218         )
219     }
220 
221     if let Ok(request) = HttpRequest::parse::<&TcpStream>(&mut BufReader::new(&stream)) {
222         let mut response_writer = ServerResponseWriter::new(&mut stream);
223         router.handle_request(&request, &mut response_writer);
224     } else {
225         let mut response_writer = ServerResponseWriter::new(&mut stream);
226         let body = "404 not found (netsim): parse header failed";
227         response_writer.put_error(404, body);
228     };
229 }
230