Skip to content

Commit 35cff1f

Browse files
committed
Add: basic auth.
1 parent 85d9771 commit 35cff1f

File tree

5 files changed

+96
-28
lines changed

5 files changed

+96
-28
lines changed

server/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,10 @@ lexer_explain = []
5050
# ------------
5151
[dependencies]
5252
actix-files = "0.6"
53+
actix-http = "3.9.0"
5354
actix-rt = "2.9.0"
5455
actix-web = "4"
56+
actix-web-httpauth = "0.8.2"
5557
actix-ws = "0.3.0"
5658
bytes = { version = "1", features = ["serde"] }
5759
chrono = "0.4"

server/src/main.rs

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ use clap::{Parser, Subcommand};
3636
use log::LevelFilter;
3737

3838
// ### Local
39-
use code_chat_editor::webserver::{self, GetServerUrlError, path_to_url};
39+
use code_chat_editor::webserver::{self, Credentials, GetServerUrlError, path_to_url};
4040

4141
// Data structures
4242
// ---------------
@@ -78,6 +78,10 @@ enum Commands {
7878
/// Control logging verbosity.
7979
#[arg(short, long)]
8080
log: Option<LevelFilter>,
81+
82+
/// Define the username:password used to limit access to the server. By default, access is unlimited.
83+
#[arg(short, long, value_parser = parse_credentials)]
84+
credentials: Option<Credentials>,
8185
},
8286
/// Start the webserver in a child process then exit.
8387
Start {
@@ -96,15 +100,15 @@ enum Commands {
96100
impl Cli {
97101
fn run(self, addr: &SocketAddr) -> Result<(), Box<dyn std::error::Error>> {
98102
match &self.command {
99-
Commands::Serve { log } => {
103+
Commands::Serve { log, credentials } => {
100104
#[cfg(debug_assertions)]
101105
if let Some(TestMode::Sleep) = self.test_mode {
102106
// For testing, don't start the server at all.
103107
std::thread::sleep(std::time::Duration::from_secs(10));
104108
return Ok(());
105109
}
106110
webserver::configure_logger(log.unwrap_or(LevelFilter::Info))?;
107-
webserver::main(addr).unwrap();
111+
webserver::main(addr, credentials.clone()).unwrap();
108112
}
109113
Commands::Start { open } => {
110114
// Poll the server to ensure it starts.
@@ -309,6 +313,21 @@ fn port_in_range(s: &str) -> Result<u16, String> {
309313
}
310314
}
311315

316+
fn parse_credentials(s: &str) -> Result<Credentials, String> {
317+
let split_: Vec<_> = s.split(":").collect();
318+
if split_.len() != 2 {
319+
Err(format!(
320+
"Unable to parse credentials as username:password; found {} colon-separated string(s)",
321+
split_.len()
322+
))
323+
} else {
324+
Ok(Credentials {
325+
username: split_[0].to_string(),
326+
password: split_[1].to_string(),
327+
})
328+
}
329+
}
330+
312331
fn fix_addr(addr: &SocketAddr) -> SocketAddr {
313332
if addr.ip() == IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)) {
314333
let mut addr = *addr;

server/src/webserver.rs

Lines changed: 70 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,9 @@ use actix_web::{
4444
error::Error,
4545
get,
4646
http::header::{ContentType, DispositionType},
47-
web,
47+
middleware, web,
4848
};
49+
use actix_web_httpauth::{extractors::basic::BasicAuth, middleware::HttpAuthentication};
4950
use actix_ws::AggregatedMessage;
5051
use bytes::Bytes;
5152
use dunce::simplified;
@@ -284,23 +285,31 @@ struct UpdateMessageContents {
284285
/// Define the [state](https://actix.rs/docs/application/#state) available to
285286
/// all endpoints.
286287
pub struct AppState {
287-
// Provide methods to control the server.
288+
/// Provide methods to control the server.
288289
server_handle: Mutex<Option<ServerHandle>>,
289-
// The number of the next connection ID to assign.
290+
/// The number of the next connection ID to assign.
290291
connection_id: Mutex<u32>,
291-
// The port this server listens on.
292+
/// The port this server listens on.
292293
port: u16,
293-
// For each connection ID, store a queue tx for the HTTP server to send
294-
// requests to the processing task for that ID.
294+
/// For each connection ID, store a queue tx for the HTTP server to send
295+
/// requests to the processing task for that ID.
295296
processing_task_queue_tx: Arc<Mutex<HashMap<String, Sender<ProcessingTaskHttpRequest>>>>,
296-
// For each (connection ID, requested URL) store channel to send the
297-
// matching response to the HTTP task.
297+
/// For each (connection ID, requested URL) store channel to send the
298+
/// matching response to the HTTP task.
298299
filewatcher_client_queues: Arc<Mutex<HashMap<String, WebsocketQueues>>>,
299-
// For each connection ID, store the queues for the VSCode IDE.
300+
/// For each connection ID, store the queues for the VSCode IDE.
300301
vscode_ide_queues: Arc<Mutex<HashMap<String, WebsocketQueues>>>,
301302
vscode_client_queues: Arc<Mutex<HashMap<String, WebsocketQueues>>>,
302-
// Connection IDs that are currently in use.
303+
/// Connection IDs that are currently in use.
303304
vscode_connection_id: Arc<Mutex<HashSet<String>>>,
305+
/// The auth credentials if authentication is used.
306+
credentials: Option<Credentials>,
307+
}
308+
309+
#[derive(Clone)]
310+
pub struct Credentials {
311+
pub username: String,
312+
pub password: String,
304313
}
305314

306315
// Macros
@@ -1333,32 +1342,69 @@ async fn client_websocket(
13331342
// Webserver core
13341343
// --------------
13351344
#[actix_web::main]
1336-
pub async fn main(addr: &SocketAddr) -> std::io::Result<()> {
1337-
run_server(addr).await
1345+
pub async fn main(addr: &SocketAddr, credentials: Option<Credentials>) -> std::io::Result<()> {
1346+
run_server(addr, credentials).await
13381347
}
13391348

1340-
pub async fn run_server(addr: &SocketAddr) -> std::io::Result<()> {
1349+
pub async fn run_server(
1350+
addr: &SocketAddr,
1351+
credentials: Option<Credentials>,
1352+
) -> std::io::Result<()> {
13411353
// Connect to the Capture Database
13421354
//let _event_capture = EventCapture::new("config.json").await?;
13431355

13441356
// Pre-load the bundled files before starting the webserver.
13451357
let _ = &*BUNDLED_FILES_MAP;
1346-
let app_data = make_app_data(addr.port());
1358+
let app_data = make_app_data(addr.port(), credentials);
13471359
let app_data_server = app_data.clone();
1348-
let server =
1349-
match HttpServer::new(move || configure_app(App::new(), &app_data_server)).bind(addr) {
1350-
Ok(server) => server.run(),
1351-
Err(err) => {
1352-
error!("Unable to bind to {addr} - {err}");
1353-
return Err(err);
1354-
}
1355-
};
1360+
let server = match HttpServer::new(move || {
1361+
let auth = HttpAuthentication::with_fn(basic_validator);
1362+
configure_app(
1363+
App::new().wrap(middleware::Condition::new(
1364+
app_data_server.credentials.is_some(),
1365+
auth,
1366+
)),
1367+
&app_data_server,
1368+
)
1369+
})
1370+
.bind(addr)
1371+
{
1372+
Ok(server) => server.run(),
1373+
Err(err) => {
1374+
error!("Unable to bind to {addr} - {err}");
1375+
return Err(err);
1376+
}
1377+
};
13561378
// Store the server handle in the global state.
13571379
*(app_data.server_handle.lock().unwrap()) = Some(server.handle());
13581380
// Start the server.
13591381
server.await
13601382
}
13611383

1384+
// Use HTTP basic authentication (if provided) to mediate access.
1385+
async fn basic_validator(
1386+
req: ServiceRequest,
1387+
credentials: BasicAuth,
1388+
) -> Result<ServiceRequest, (Error, ServiceRequest)> {
1389+
// Get the provided credentials.
1390+
let expected_credentials = &req
1391+
.app_data::<actix_web::web::Data<AppState>>()
1392+
.unwrap()
1393+
.credentials
1394+
.as_ref()
1395+
.unwrap();
1396+
if credentials.user_id() == expected_credentials.username
1397+
&& credentials.password() == Some(&expected_credentials.password)
1398+
{
1399+
Ok(req)
1400+
} else {
1401+
Err((
1402+
actix_web::error::ErrorUnauthorized("Incorrect username or password."),
1403+
req,
1404+
))
1405+
}
1406+
}
1407+
13621408
pub fn configure_logger(level: LevelFilter) -> Result<(), Box<dyn std::error::Error>> {
13631409
#[cfg(not(debug_assertions))]
13641410
let l4rs = ROOT_PATH.clone();
@@ -1383,7 +1429,7 @@ pub fn configure_logger(level: LevelFilter) -> Result<(), Box<dyn std::error::Er
13831429
// closure passed to `HttpServer::new` and moved/cloned in." Putting this code
13841430
// inside `configure_app` places it inside the closure which calls
13851431
// `configure_app`, preventing globally shared state.
1386-
fn make_app_data(port: u16) -> web::Data<AppState> {
1432+
fn make_app_data(port: u16, credentials: Option<Credentials>) -> web::Data<AppState> {
13871433
web::Data::new(AppState {
13881434
server_handle: Mutex::new(None),
13891435
connection_id: Mutex::new(0),
@@ -1393,6 +1439,7 @@ fn make_app_data(port: u16) -> web::Data<AppState> {
13931439
vscode_ide_queues: Arc::new(Mutex::new(HashMap::new())),
13941440
vscode_client_queues: Arc::new(Mutex::new(HashMap::new())),
13951441
vscode_connection_id: Arc::new(Mutex::new(HashSet::new())),
1442+
credentials,
13961443
})
13971444
}
13981445

server/src/webserver/filewatcher.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -757,7 +757,7 @@ mod tests {
757757
WebsocketQueues,
758758
impl Service<Request, Response = ServiceResponse<BoxBody>, Error = actix_web::Error> + use<>,
759759
) {
760-
let app_data = make_app_data(IP_PORT);
760+
let app_data = make_app_data(IP_PORT, None);
761761
let app = test::init_service(configure_app(App::new(), &app_data)).await;
762762

763763
// Load in a test source file to create a websocket.

server/src/webserver/vscode/tests.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ use crate::{
6666
lazy_static! {
6767
// Run a single webserver for all tests.
6868
static ref WEBSERVER_HANDLE: JoinHandle<Result<(), Error>> =
69-
actix_rt::spawn(async move { run_server(&SocketAddr::new("127.0.0.1".parse().unwrap(), IP_PORT)).await });
69+
actix_rt::spawn(async move { run_server(&SocketAddr::new("127.0.0.1".parse().unwrap(), IP_PORT), None).await });
7070
}
7171

7272
// Send a message via a websocket.

0 commit comments

Comments
 (0)