use rocket::fs::NamedFile; use rocket::fs::TempFile; use rocket::http::Method; use rocket::http::Status; use rocket::response::Redirect; use rocket::response::{Responder, status}; use rocket::serde::{Deserialize, json::Json}; use rocket::uri; use rocket::{self, get, launch, post, put, routes}; use rocket_cors::{AllowedOrigins, CorsOptions}; use std::env; use std::path::Path; use std::path::PathBuf; #[derive(Deserialize)] #[serde(crate = "rocket::serde")] struct GenerationRequest<'r> { directory: &'r str, common_name: &'r str, #[serde(default)] email: &'r str, #[serde(default)] days: u32, #[serde(default)] use_openssl: Option, } #[derive(Responder)] enum GenerationError { #[response(status = 500, content_type = "json")] InternalError(String), #[response(status = 404, content_type = "json")] DirectoryNotFoundError(String), } fn get_base_directory() -> String { match env::var("GENERATION_BASE_DIRECTORY") { Ok(directory) => directory, Err(_) => "base/".into(), } } async fn check_is_valid_directory( directory: impl AsRef + Clone, ) -> Result<(), GenerationError> { match tokio::fs::metadata(directory.clone()).await { Ok(metadata) => { if !metadata.is_dir() { return Err(GenerationError::DirectoryNotFoundError( "The specified directory is not valid".into(), )); } } Err(_) => { return Err(GenerationError::DirectoryNotFoundError( "The specified directory is not exists".into(), )); } }; // Check if the directory contains a 'vars.bat' and 'template.ovpn' files const REQUIRED_FILES: [&str; 2] = ["vars.bat", "template.ovpn"]; for file_name in REQUIRED_FILES.iter() { let file_path = directory.as_ref().join(file_name); match tokio::fs::metadata(&file_path).await { Ok(metadata) => { if !metadata.is_file() { return Err(GenerationError::DirectoryNotFoundError(format!( "The specified directory is not valid, invalid file: {}", file_name ))); } } Err(_) => { return Err(GenerationError::DirectoryNotFoundError(format!( "The specified directory is not valid, missing file: {}", file_name ))); } }; } Ok(()) } #[get("/get")] async fn list_directories() -> Result>, status::Custom> { let mut reader = tokio::fs::read_dir(get_base_directory()) .await .map_err(|e| { status::Custom( Status::InternalServerError, format!("Failed to read directory: {}", e), ) })?; let mut directories = Vec::new(); while let Ok(Some(entry)) = reader.next_entry().await { let path = entry.path(); if check_is_valid_directory(&path).await.is_ok() && let Some(name) = path.file_name() { directories.push(name.to_str().unwrap().to_string()) } } Ok(Json(directories)) } #[get("/get/")] async fn list_directory(directory: &str) -> Result>, status::Custom> { let base = get_base_directory(); let dir = Path::new(&base).join(directory); if check_is_valid_directory(&dir).await.is_err() { return Err(status::Custom( Status::BadRequest, "The specified directory is not valid".into(), )); } let dir = dir.join("config"); let mut reader = tokio::fs::read_dir(dir).await.map_err(|e| { status::Custom( Status::InternalServerError, format!("Failed to read directory: {}", e), ) })?; let mut files = Vec::new(); while let Ok(Some(entry)) = reader.next_entry().await { let path = entry.path(); if let Ok(meta) = path.metadata() { if !meta.is_file() { continue; } if let Some(ext) = path.extension() { if ext.to_str().unwrap() != "ovpn" { continue; }; if let Some(name) = path.file_name() { files.push(name.to_str().unwrap().to_string()) } } } } Ok(Json(files)) } #[get("/get//")] async fn get_file(directory: &str, file: &str) -> Result> { let dir = Path::new(&get_base_directory()).join(directory); if check_is_valid_directory(&dir).await.is_err() { return Err(status::NotFound( "The specified directory is not valid".into(), )); } let dir = dir.join("config"); let path = dir.join(file); NamedFile::open(&path) .await .map_err(|e| status::NotFound(e.to_string())) } #[put("/put//", format = "plain", data = "")] async fn put_file(directory: &str, name: &str, mut file: TempFile<'_>) -> status::Custom { let dir = Path::new(&get_base_directory()).join(directory); if check_is_valid_directory(&dir).await.is_err() { return status::Custom( Status::BadRequest, "The specified directory is not valid".into(), ); } let dir = dir.join("config"); let path = dir.join(name); // check if the file exists match tokio::fs::metadata(&path).await { Ok(meta) if meta.is_file() => {} _ => { return status::Custom(Status::NotFound, "The specified file is not found".into()); } } if let Err(msg) = file.persist_to(&path).await { return status::Custom( Status::InternalServerError, format!("Failed to write file: {}", msg), ); } status::Custom(Status::NoContent, "".into()) } #[post("/generate", data = "")] async fn generate(request: Json>) -> Result { let dir = Path::new(&get_base_directory()).join(request.directory); check_is_valid_directory(dir.clone()).await?; let generator_bin = env::var("GENERATOR_BIN").unwrap_or("peazyrsa".into()); let mut cmd = tokio::process::Command::new(generator_bin); let mut use_openssl = env::var("USE_OPENSSL").unwrap_or("no".into()) == "yes"; if let Some(req_use_openssl) = request.use_openssl { use_openssl = req_use_openssl; } if use_openssl { let openssl_bin = env::var("OPENSSL_BIN").unwrap_or("openssl".into()); cmd.arg("--with-openssl").arg(openssl_bin); } cmd.arg("-d").arg(&dir).arg(request.common_name); if !request.email.is_empty() { cmd.arg("--email").arg(request.email); } if request.days > 0 { cmd.arg("--days").arg(request.days.to_string()); } // execute the command and check error code let status = cmd .status() .await .map_err(|e| GenerationError::InternalError(format!("Failed to execute command: {}", e)))?; if !status.success() { return Err(GenerationError::InternalError(format!( "Command failed with status: {}", status ))); } // check output file exists let output_file = dir .join("config") .join(format!("{}.ovpn", request.common_name)); if !output_file.exists() { return Err(GenerationError::InternalError( "Output file not found".into(), )); } match NamedFile::open(output_file).await { Err(e) => Err(GenerationError::InternalError(format!( "Failed to open output file: {}", e ))), Ok(f) => Ok(f), } } #[get("/")] fn index_redirect() -> Redirect { Redirect::permanent(uri!("/frontend").to_string() + "/") } #[get("/frontend/")] async fn frontend(path: PathBuf) -> Option { let frontend_base = env::var("FRONTEND_BASE").unwrap_or("../frontend/dist/".into()); let frontend_path = Path::new(&frontend_base).join(path); let frontend_index_path = Path::new(&frontend_base).join("index.html"); match tokio::fs::metadata(&frontend_path).await { Ok(meta) if meta.is_file() => NamedFile::open(frontend_path) .await .or(NamedFile::open(frontend_index_path).await) .ok(), _ => NamedFile::open(&frontend_index_path).await.ok(), } } #[launch] fn rocket() -> _ { let cors = CorsOptions::default() .allowed_origins(AllowedOrigins::all()) .allowed_methods( vec![Method::Get, Method::Post, Method::Put] .into_iter() .map(From::from) .collect(), ) .allow_credentials(true); rocket::build() .mount( "/api/v1", routes![ list_directories, list_directory, get_file, put_file, generate ], ) .attach(cors.to_cors().unwrap()) .mount("/", routes![index_redirect, frontend]) }