use rocket::fs::NamedFile; use rocket::http::Method; use rocket::http::Status; use rocket::response::{Responder, status}; use rocket::serde::{Deserialize, json::Json}; use rocket::{self, get, launch, post, routes}; use rocket_cors::{AllowedOrigins, CorsOptions}; use std::env; use std::path::Path; #[get("/")] fn index() -> &'static str { "Hello, world!" } #[derive(Deserialize)] #[serde(crate = "rocket::serde")] struct GenerationRequest<'r> { directory: &'r str, common_name: &'r str, } #[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<Path> + 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<Json<Vec<String>>, status::Custom<String>> { 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() { if let Some(name) = path.file_name() { directories.push(name.to_str().unwrap().to_string()) } } } Ok(Json(directories)) } #[get("/get/<directory>")] async fn list_directory(directory: &str) -> Result<Json<Vec<String>>, status::Custom<String>> { 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/<directory>/<file>")] async fn get_file(directory: &str, file: &str) -> Result<NamedFile, status::NotFound<String>> { 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())) } #[post("/generate", data = "<request>")] async fn generate(request: Json<GenerationRequest<'_>>) -> Result<NamedFile, GenerationError> { 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); if env::var("USE_OPENSSL").unwrap_or("no".into()) == "yes" { 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); // 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), } } #[launch] fn rocket() -> _ { let cors = CorsOptions::default() .allowed_origins(AllowedOrigins::all()) .allowed_methods( vec![Method::Get, Method::Post] .into_iter() .map(From::from) .collect(), ) .allow_credentials(true); rocket::build() .mount( "/api/v1", routes![index, list_directories, list_directory, get_file, generate], ) .attach(cors.to_cors().unwrap()) }