mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-04-27 15:17:38 +00:00
Added password protection
This commit is contained in:
parent
f8ac02d5e5
commit
f328e2c1cb
5 changed files with 68 additions and 2 deletions
7
tty-fwd/Cargo.lock
generated
7
tty-fwd/Cargo.lock
generated
|
|
@ -77,6 +77,12 @@ dependencies = [
|
||||||
"windows-sys 0.59.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "data-encoding"
|
||||||
|
version = "2.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dirs"
|
name = "dirs"
|
||||||
version = "5.0.1"
|
version = "5.0.1"
|
||||||
|
|
@ -571,6 +577,7 @@ dependencies = [
|
||||||
"argument-parser",
|
"argument-parser",
|
||||||
"bytes",
|
"bytes",
|
||||||
"ctrlc",
|
"ctrlc",
|
||||||
|
"data-encoding",
|
||||||
"dirs",
|
"dirs",
|
||||||
"http",
|
"http",
|
||||||
"jiff",
|
"jiff",
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ regex = "1.10"
|
||||||
dirs = "5.0"
|
dirs = "5.0"
|
||||||
notify = "6.1.1"
|
notify = "6.1.1"
|
||||||
ctrlc = "3.4.2"
|
ctrlc = "3.4.2"
|
||||||
|
data-encoding = "2.5"
|
||||||
|
|
||||||
[target.'cfg(windows)'.dependencies]
|
[target.'cfg(windows)'.dependencies]
|
||||||
windows-sys = { version = "0.59", features = ["Win32_System_Console"] }
|
windows-sys = { version = "0.59", features = ["Win32_System_Console"] }
|
||||||
|
|
|
||||||
|
|
@ -127,6 +127,7 @@ When running with `--serve`, the following REST API endpoints are available:
|
||||||
- `--serve`: Start HTTP API server on specified address/port
|
- `--serve`: Start HTTP API server on specified address/port
|
||||||
- `--static-path`: Directory to serve static files from (requires --serve)
|
- `--static-path`: Directory to serve static files from (requires --serve)
|
||||||
- `--cleanup`: Remove exited sessions
|
- `--cleanup`: Remove exited sessions
|
||||||
|
- `--password`: Enables an HTTP basic auth password (username is ignored)
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
use data_encoding::BASE64;
|
||||||
use notify::{Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher};
|
use notify::{Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher};
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
@ -85,6 +86,33 @@ struct ApiResponse {
|
||||||
session_id: Option<String>,
|
session_id: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn check_basic_auth(req: &HttpRequest, expected_password: &str) -> bool {
|
||||||
|
if let Some(auth_header) = req.headers().get("authorization") {
|
||||||
|
if let Ok(auth_str) = auth_header.to_str() {
|
||||||
|
if let Some(credentials) = auth_str.strip_prefix("Basic ") {
|
||||||
|
if let Ok(decoded_bytes) = BASE64.decode(credentials.as_bytes()) {
|
||||||
|
if let Ok(decoded_str) = String::from_utf8(decoded_bytes) {
|
||||||
|
if let Some(colon_pos) = decoded_str.find(':') {
|
||||||
|
let password = &decoded_str[colon_pos + 1..];
|
||||||
|
return password == expected_password;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unauthorized_response() -> Response<String> {
|
||||||
|
Response::builder()
|
||||||
|
.status(StatusCode::UNAUTHORIZED)
|
||||||
|
.header("WWW-Authenticate", "Basic realm=\"tty-fwd\"")
|
||||||
|
.header("Content-Type", "text/plain")
|
||||||
|
.body("Unauthorized".to_string())
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
fn get_mime_type(file_path: &Path) -> &'static str {
|
fn get_mime_type(file_path: &Path) -> &'static str {
|
||||||
match file_path.extension().and_then(|ext| ext.to_str()) {
|
match file_path.extension().and_then(|ext| ext.to_str()) {
|
||||||
Some("html") | Some("htm") => "text/html",
|
Some("html") | Some("htm") => "text/html",
|
||||||
|
|
@ -203,16 +231,32 @@ pub fn start_server(
|
||||||
bind_address: &str,
|
bind_address: &str,
|
||||||
control_path: PathBuf,
|
control_path: PathBuf,
|
||||||
static_path: Option<String>,
|
static_path: Option<String>,
|
||||||
|
password: Option<String>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
fs::create_dir_all(&control_path)?;
|
fs::create_dir_all(&control_path)?;
|
||||||
|
|
||||||
let server = HttpServer::bind(bind_address)
|
let server = HttpServer::bind(bind_address)
|
||||||
.map_err(|e| anyhow::anyhow!("Failed to bind server: {}", e))?;
|
.map_err(|e| anyhow::anyhow!("Failed to bind server: {}", e))?;
|
||||||
println!("HTTP API server listening on {}", bind_address);
|
|
||||||
|
// Set up auth if password is provided
|
||||||
|
let auth_password = if let Some(ref password) = password {
|
||||||
|
println!(
|
||||||
|
"HTTP API server listening on {} with Basic Auth enabled (any username)",
|
||||||
|
bind_address
|
||||||
|
);
|
||||||
|
Some(password.clone())
|
||||||
|
} else {
|
||||||
|
println!(
|
||||||
|
"HTTP API server listening on {} with no authentication",
|
||||||
|
bind_address
|
||||||
|
);
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
for req in server.incoming() {
|
for req in server.incoming() {
|
||||||
let control_path = control_path.clone();
|
let control_path = control_path.clone();
|
||||||
let static_path = static_path.clone();
|
let static_path = static_path.clone();
|
||||||
|
let auth_password = auth_password.clone();
|
||||||
|
|
||||||
thread::spawn(move || {
|
thread::spawn(move || {
|
||||||
let mut req = match req {
|
let mut req = match req {
|
||||||
|
|
@ -229,6 +273,14 @@ pub fn start_server(
|
||||||
|
|
||||||
println!("{:?} {} (full URI: {})", method, path, full_uri);
|
println!("{:?} {} (full URI: {})", method, path, full_uri);
|
||||||
|
|
||||||
|
// Check authentication if enabled (but skip /api/health)
|
||||||
|
if let Some(ref expected_password) = auth_password {
|
||||||
|
if path != "/api/health" && !check_basic_auth(&req, expected_password) {
|
||||||
|
let _ = req.respond(unauthorized_response());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Check for static file serving first
|
// Check for static file serving first
|
||||||
if method == &Method::GET && !path.starts_with("/api/") {
|
if method == &Method::GET && !path.starts_with("/api/") {
|
||||||
if let Some(ref static_dir) = static_path {
|
if let Some(ref static_dir) = static_path {
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ fn main() -> Result<(), anyhow::Error> {
|
||||||
let mut cleanup = false;
|
let mut cleanup = false;
|
||||||
let mut serve_address = None::<String>;
|
let mut serve_address = None::<String>;
|
||||||
let mut static_path = None::<String>;
|
let mut static_path = None::<String>;
|
||||||
|
let mut password = None::<String>;
|
||||||
let mut cmdline = Vec::<OsString>::new();
|
let mut cmdline = Vec::<OsString>::new();
|
||||||
|
|
||||||
while let Some(param) = parser.param()? {
|
while let Some(param) = parser.param()? {
|
||||||
|
|
@ -82,6 +83,9 @@ fn main() -> Result<(), anyhow::Error> {
|
||||||
p if p.is_long("static-path") => {
|
p if p.is_long("static-path") => {
|
||||||
static_path = Some(parser.value()?);
|
static_path = Some(parser.value()?);
|
||||||
}
|
}
|
||||||
|
p if p.is_long("password") => {
|
||||||
|
password = Some(parser.value()?);
|
||||||
|
}
|
||||||
p if p.is_pos() => {
|
p if p.is_pos() => {
|
||||||
cmdline.push(parser.value()?);
|
cmdline.push(parser.value()?);
|
||||||
}
|
}
|
||||||
|
|
@ -107,6 +111,7 @@ fn main() -> Result<(), anyhow::Error> {
|
||||||
println!(
|
println!(
|
||||||
" --static-path <path> Path to static files directory for HTTP server"
|
" --static-path <path> Path to static files directory for HTTP server"
|
||||||
);
|
);
|
||||||
|
println!(" --password <password> Enable basic auth with random username and specified password");
|
||||||
println!(" --help Show this help message");
|
println!(" --help Show this help message");
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
@ -171,7 +176,7 @@ fn main() -> Result<(), anyhow::Error> {
|
||||||
std::process::exit(0);
|
std::process::exit(0);
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
return crate::api_server::start_server(&addr, control_path, static_path);
|
return crate::api_server::start_server(&addr, control_path, static_path, password);
|
||||||
}
|
}
|
||||||
|
|
||||||
let exit_code = sessions::spawn_command(control_path, session_name, cmdline)?;
|
let exit_code = sessions::spawn_command(control_path, session_name, cmdline)?;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue