blob: ea954287004d9159bf29051e4515c01c49659fb1 [file] [log] [blame]
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
pub(crate) mod http_request;
mod http_response;
mod http_router;
pub(crate) mod server_response;
mod thread_pool;
use crate::captures::handlers::*;
use crate::http_server::http_request::HttpRequest;
use crate::http_server::http_router::Router;
use crate::http_server::server_response::{
ResponseWritable, ServerResponseWritable, ServerResponseWriter,
};
use crate::version::VERSION;
use crate::http_server::thread_pool::ThreadPool;
use crate::ffi::get_devices;
use crate::ffi::patch_device;
use cxx::let_cxx_string;
use std::collections::HashSet;
use std::ffi::OsStr;
use std::fs;
use std::io::BufReader;
use std::net::TcpListener;
use std::net::TcpStream;
use std::path::Path;
use std::path::PathBuf;
use std::sync::Arc;
const PATH_PREFIXES: [&str; 3] = ["js", "assets", "node_modules/tslib"];
pub fn run_http_server() {
let listener = match TcpListener::bind("127.0.0.1:7681") {
Ok(listener) => listener,
Err(e) => {
eprintln!("netsimd: bind error in netsimd frontend http server. {}", e);
return;
}
};
let pool = ThreadPool::new(4);
println!("netsimd: Frontend http server is listening on http://localhost:7681");
let valid_files = Arc::new(create_filename_hash_set());
for stream in listener.incoming() {
let stream = stream.unwrap();
let valid_files = valid_files.clone();
pool.execute(move || {
handle_connection(stream, valid_files);
});
}
println!("netsimd: Shutting down frontend http server.");
}
fn ui_path(suffix: &str) -> PathBuf {
let mut path = std::env::current_exe().unwrap();
path.pop();
path.push("netsim-ui");
for subpath in suffix.split('/') {
path.push(subpath);
}
path
}
fn create_filename_hash_set() -> HashSet<String> {
let mut valid_files: HashSet<String> = HashSet::new();
for path_prefix in PATH_PREFIXES {
let dir_path = ui_path(path_prefix);
if let Ok(mut file) = fs::read_dir(dir_path) {
while let Some(Ok(entry)) = file.next() {
valid_files.insert(entry.path().to_str().unwrap().to_string());
}
} else {
println!("netsim-ui doesn't exist");
}
}
valid_files
}
fn check_valid_file_path(path: &str, valid_files: &HashSet<String>) -> bool {
let filepath = match path.strip_prefix('/') {
Some(stripped_path) => ui_path(stripped_path),
None => ui_path(path),
};
valid_files.contains(filepath.as_path().to_str().unwrap())
}
fn to_content_type(file_path: &Path) -> &str {
match file_path.extension().and_then(OsStr::to_str) {
Some("html") => "text/html",
Some("txt") => "text/plain",
Some("jpg") | Some("jpeg") => "image/jpeg",
Some("png") => "image/png",
Some("js") => "application/javascript",
Some("svg") => "image/svg+xml",
_ => "application/octet-stream",
}
}
fn handle_file(method: &str, path: &str, writer: ResponseWritable) {
if method == "GET" {
let filepath = match path.strip_prefix('/') {
Some(stripped_path) => ui_path(stripped_path),
None => ui_path(path),
};
if let Ok(body) = fs::read(&filepath) {
writer.put_ok_with_vec(to_content_type(&filepath), body, &[]);
return;
}
}
let body = format!("404 not found (netsim): handle_file with unknown path {path}");
writer.put_error(404, body.as_str());
}
// TODO handlers accept additional "context" including filepath
fn handle_index(request: &HttpRequest, _param: &str, writer: ResponseWritable) {
handle_file(&request.method, "index.html", writer)
}
fn handle_static(request: &HttpRequest, path: &str, writer: ResponseWritable) {
// The path verification happens in the closure wrapper around handle_static.
handle_file(&request.method, path, writer)
}
fn handle_version(_request: &HttpRequest, _param: &str, writer: ResponseWritable) {
let body = format!("{{\"version\": \"{}\"}}", VERSION);
writer.put_ok("text/plain", body.as_str(), &[]);
}
fn handle_devices(request: &HttpRequest, _param: &str, writer: ResponseWritable) {
if &request.method == "GET" {
let_cxx_string!(request = "");
let_cxx_string!(response = "");
let_cxx_string!(error_message = "");
let status = get_devices(&request, response.as_mut(), error_message.as_mut());
if status == 200 {
writer.put_ok("text/plain", response.to_string().as_str(), &[]);
} else {
let body = format!("404 Not found (netsim): {:?}", error_message.to_string());
writer.put_error(404, body.as_str());
}
} else if &request.method == "PATCH" {
let_cxx_string!(new_request = &request.body);
let_cxx_string!(response = "");
let_cxx_string!(error_message = "");
let status = patch_device(&new_request, response.as_mut(), error_message.as_mut());
if status == 200 {
writer.put_ok("text/plain", response.to_string().as_str(), &[]);
} else {
let body = format!("404 Not found (netsim): {:?}", error_message.to_string());
writer.put_error(404, body.as_str());
}
} else {
let body = format!(
"404 Not found (netsim): {:?} is not a valid method for this route",
request.method.to_string()
);
writer.put_error(404, body.as_str());
}
}
fn handle_connection(mut stream: TcpStream, valid_files: Arc<HashSet<String>>) {
let mut router = Router::new();
router.add_route("/", Box::new(handle_index));
router.add_route("/version", Box::new(handle_version));
router.add_route("/v1/devices", Box::new(handle_devices));
router.add_route(r"/v1/captures", Box::new(handle_capture));
router.add_route(r"/v1/captures/{id}", Box::new(handle_capture));
// A closure for checking if path is a static file we wish to serve, and call handle_static
let handle_static_wrapper =
move |request: &HttpRequest, path: &str, writer: ResponseWritable| {
for prefix in PATH_PREFIXES {
let new_path = format!("{prefix}/{path}");
if check_valid_file_path(new_path.as_str(), &valid_files) {
handle_static(request, new_path.as_str(), writer);
return;
}
}
let body = format!("404 not found (netsim): Invalid path {path}");
writer.put_error(404, body.as_str());
};
// Connecting all path prefixes to handle_static_wrapper
for prefix in PATH_PREFIXES {
router.add_route(
format!(r"/{prefix}/{{path}}").as_str(),
Box::new(handle_static_wrapper.clone()),
)
}
if let Ok(request) = HttpRequest::parse::<&TcpStream>(&mut BufReader::new(&stream)) {
let mut response_writer = ServerResponseWriter::new(&mut stream);
router.handle_request(&request, &mut response_writer);
} else {
let mut response_writer = ServerResponseWriter::new(&mut stream);
let body = "404 not found (netsim): parse header failed";
response_writer.put_error(404, body);
};
}