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