summaryrefslogtreecommitdiff
path: root/server
diff options
context:
space:
mode:
Diffstat (limited to 'server')
-rw-r--r--server/Cargo.lock121
-rw-r--r--server/Cargo.toml3
-rw-r--r--server/Rocket.toml12
-rw-r--r--server/migrations/eyeballs.sql2
-rw-r--r--server/src/api_model.rs9
-rw-r--r--server/src/auth.rs207
-rw-r--r--server/src/main.rs36
7 files changed, 357 insertions, 33 deletions
diff --git a/server/Cargo.lock b/server/Cargo.lock
index 181720a..865b62d 100644
--- a/server/Cargo.lock
+++ b/server/Cargo.lock
@@ -18,6 +18,41 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
[[package]]
+name = "aead"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0"
+dependencies = [
+ "crypto-common",
+ "generic-array",
+]
+
+[[package]]
+name = "aes"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
+dependencies = [
+ "cfg-if",
+ "cipher",
+ "cpufeatures",
+]
+
+[[package]]
+name = "aes-gcm"
+version = "0.10.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1"
+dependencies = [
+ "aead",
+ "aes",
+ "cipher",
+ "ctr",
+ "ghash",
+ "subtle",
+]
+
+[[package]]
name = "ahash"
version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -130,6 +165,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
[[package]]
+name = "base64"
+version = "0.22.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
+
+[[package]]
name = "base64ct"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -193,6 +234,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
+name = "cipher"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
+dependencies = [
+ "crypto-common",
+ "inout",
+]
+
+[[package]]
name = "const-oid"
version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -204,7 +255,13 @@ version = "0.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747"
dependencies = [
+ "aes-gcm",
+ "base64 0.22.1",
+ "hkdf",
"percent-encoding",
+ "rand",
+ "sha2",
+ "subtle",
"time",
"version_check",
]
@@ -255,10 +312,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
dependencies = [
"generic-array",
+ "rand_core",
"typenum",
]
[[package]]
+name = "ctr"
+version = "0.9.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835"
+dependencies = [
+ "cipher",
+]
+
+[[package]]
name = "der"
version = "0.7.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -400,6 +467,7 @@ dependencies = [
"rocket_db_pools",
"serde",
"sqlx",
+ "time",
]
[[package]]
@@ -583,6 +651,16 @@ dependencies = [
]
[[package]]
+name = "ghash"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1"
+dependencies = [
+ "opaque-debug",
+ "polyval",
+]
+
+[[package]]
name = "gimli"
version = "0.31.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -918,6 +996,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8fae54786f62fb2918dcfae3d568594e50eb9b5c25bf04371af6fe7516452fb"
[[package]]
+name = "inout"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
name = "is-terminal"
version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1184,6 +1271,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
[[package]]
+name = "opaque-debug"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
+
+[[package]]
name = "overload"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1296,6 +1389,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2"
[[package]]
+name = "polyval"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "opaque-debug",
+ "universal-hash",
+]
+
+[[package]]
name = "powerfmt"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1619,7 +1724,7 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c"
dependencies = [
- "base64",
+ "base64 0.21.7",
]
[[package]]
@@ -1916,7 +2021,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ed31390216d20e538e447a7a9b959e06ed9fc51c37b514b46eb758016ecd418"
dependencies = [
"atoi",
- "base64",
+ "base64 0.21.7",
"bitflags",
"byteorder",
"bytes",
@@ -1958,7 +2063,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c824eb80b894f926f89a0b9da0c7f435d27cdd35b8c655b114e58223918577e"
dependencies = [
"atoi",
- "base64",
+ "base64 0.21.7",
"bitflags",
"byteorder",
"crc",
@@ -2415,6 +2520,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e"
[[package]]
+name = "universal-hash"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea"
+dependencies = [
+ "crypto-common",
+ "subtle",
+]
+
+[[package]]
name = "untrusted"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/server/Cargo.toml b/server/Cargo.toml
index c78b9f7..fc8cb6f 100644
--- a/server/Cargo.toml
+++ b/server/Cargo.toml
@@ -5,7 +5,8 @@ edition = "2021"
[dependencies]
futures = "0.3.31"
-rocket = { version = "0.5.1", features = ["json"] }
+rocket = { version = "0.5.1", features = ["json", "secrets"] }
rocket_db_pools = { version = "0.2.0", features = ["sqlx_mysql"] }
serde = { version = "1.0", features = ["derive"] }
sqlx = { version = "0.7.0", default-features = false, features = ["macros", "migrate"] }
+time = "0.3.34"
diff --git a/server/Rocket.toml b/server/Rocket.toml
index 06369e5..6dd1ad1 100644
--- a/server/Rocket.toml
+++ b/server/Rocket.toml
@@ -1,2 +1,14 @@
+[default]
+secret_key = "itlYmFR2vYKrOmFhupMIn/hyB6lYCCTXz4yaQX89XVg="
+session_max_age_days = 7
+
[default.databases.eyeballs]
url = "mysql://eyeballs:verysecret@127.0.0.1:3306/eyeballs"
+
+[release]
+secret_key = "intentionally invalid"
+session_max_age_days = 30
+
+[release.databases.eyeballs]
+url = "intentionally invalid"
+
diff --git a/server/migrations/eyeballs.sql b/server/migrations/eyeballs.sql
index d8163b2..9699e6c 100644
--- a/server/migrations/eyeballs.sql
+++ b/server/migrations/eyeballs.sql
@@ -6,7 +6,7 @@ CREATE TABLE IF NOT EXISTS projects (
CREATE TABLE IF NOT EXISTS users (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
- username VARCHAR(256) NOT NULL,
+ username VARCHAR(256) NOT NULL UNIQUE,
name VARCHAR(1024) NOT NULL DEFAULT '',
active BOOLEAN NOT NULL DEFAULT 1
);
diff --git a/server/src/api_model.rs b/server/src/api_model.rs
index 1c02f6c..286e11f 100644
--- a/server/src/api_model.rs
+++ b/server/src/api_model.rs
@@ -1,4 +1,4 @@
-use serde::Serialize;
+use rocket::serde::Serialize;
#[derive(Serialize, Copy, Clone)]
pub enum ReviewState {
@@ -68,3 +68,10 @@ pub struct Projects {
pub more: bool,
pub projects: Vec<ProjectEntry>,
}
+
+#[derive(Serialize)]
+pub struct StatusResponse {
+ pub ok: bool,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub error: Option<String>,
+}
diff --git a/server/src/auth.rs b/server/src/auth.rs
new file mode 100644
index 0000000..31e18a0
--- /dev/null
+++ b/server/src/auth.rs
@@ -0,0 +1,207 @@
+use core::net::IpAddr;
+use rocket::fairing::AdHoc;
+use rocket::form::Form;
+use rocket::http::{Cookie, CookieJar, Status};
+use rocket::outcome::{try_outcome, IntoOutcome};
+use rocket::request::{FromRequest, Outcome, Request};
+use rocket::response::status::Unauthorized;
+use rocket::serde::json::{self, Json};
+use rocket::serde::{Deserialize, Serialize};
+use rocket::State;
+use std::collections::BTreeMap;
+use std::sync::Mutex;
+use std::time::Instant;
+use time::Duration;
+
+use crate::api_model;
+
+#[derive(FromForm)]
+struct Login<'r> {
+ username: &'r str,
+ password: &'r str,
+}
+
+#[derive(Debug, Deserialize)]
+#[allow(dead_code)]
+struct AuthConfig {
+ session_max_age_days: u32,
+}
+
+struct SessionsData {
+ active_ids: BTreeMap<u32, Instant>,
+ next_id: u32,
+}
+
+struct Sessions {
+ data: Mutex<SessionsData>,
+}
+
+#[derive(Debug, Deserialize, Serialize)]
+pub struct Session {
+ pub user_id: u64,
+ session_id: u32,
+ remote: String,
+}
+
+#[derive(Debug)]
+pub enum SessionError {
+ Invalid,
+}
+
+const SESSION_COOKIE: &str = "s";
+
+fn validate(sessions: &State<Sessions>, session: &Session, request: &Request<'_>) -> bool {
+ match request.client_ip() {
+ Some(addr) => {
+ if session.remote == addr.to_string() {
+ {
+ let sessions_data = sessions.data.lock().unwrap();
+ match sessions_data.active_ids.get(&session.session_id) {
+ // We could remove the expired session here, but it will be cleaned
+ // next time anyone logs in anyway.
+ Some(&expire) => expire > Instant::now(),
+ None => false,
+ }
+ }
+ } else {
+ false
+ }
+ }
+ None => false,
+ }
+}
+
+#[rocket::async_trait]
+impl<'r> FromRequest<'r> for Session {
+ type Error = SessionError;
+
+ async fn from_request(request: &'r Request<'_>) -> Outcome<Session, SessionError> {
+ let sessions = try_outcome!(request
+ .guard::<&State<Sessions>>()
+ .await
+ .map_error(|_| (Status::Unauthorized, SessionError::Invalid)));
+
+ request
+ .cookies()
+ .get_private(SESSION_COOKIE)
+ .and_then(|cookie| -> Option<Session> { json::from_str(cookie.value()).ok() })
+ .and_then(|session| {
+ if validate(&sessions, &session, request) {
+ Some(session)
+ } else {
+ None
+ }
+ })
+ .or_error((Status::Unauthorized, SessionError::Invalid))
+ }
+}
+
+fn new_session(
+ sessions: &State<Sessions>,
+ user_id: u64,
+ remote: String,
+ max_age: Duration,
+) -> Session {
+ let session_id;
+ {
+ let mut sessions_data = sessions.data.lock().unwrap();
+ session_id = sessions_data.next_id;
+ sessions_data.next_id += 1;
+
+ let now = Instant::now();
+ // Remove expired sessions first
+ sessions_data
+ .active_ids
+ .retain(|_, &mut expire| expire > now);
+
+ sessions_data.active_ids.insert(session_id, now + max_age);
+ }
+ Session {
+ user_id: user_id,
+ session_id: session_id,
+ remote: remote,
+ }
+}
+
+#[post("/login", data = "<login>")]
+fn login(
+ auth_config: &State<AuthConfig>,
+ sessions: &State<Sessions>,
+ ipaddr: IpAddr,
+ cookies: &CookieJar<'_>,
+ login: Form<Login<'_>>,
+) -> Result<Json<api_model::StatusResponse>, Unauthorized<&'static str>> {
+ if login.username == "user" && login.password == "password" {
+ let max_age = Duration::days(i64::from(auth_config.session_max_age_days));
+ let session = new_session(&sessions, 1u64, ipaddr.to_string(), max_age);
+
+ let cookie = Cookie::build((SESSION_COOKIE, json::to_string(&session).unwrap()))
+ .path("/api")
+ .max_age(max_age)
+ .http_only(true)
+ .build();
+
+ cookies.add_private(cookie);
+ Ok(Json(api_model::StatusResponse {
+ ok: true,
+ error: None,
+ }))
+ } else {
+ Err(Unauthorized("Unknown username or password"))
+ }
+}
+
+#[get("/logout")]
+fn logout(
+ session: Session,
+ sessions: &State<Sessions>,
+ cookies: &CookieJar<'_>,
+) -> Json<api_model::StatusResponse> {
+ {
+ let mut sessions_data = sessions.data.lock().unwrap();
+ sessions_data.active_ids.remove(&session.session_id);
+ }
+
+ let cookie = Cookie::build((SESSION_COOKIE, ""))
+ .path("/api")
+ .http_only(true)
+ .build();
+
+ cookies.remove_private(cookie);
+
+ Json(api_model::StatusResponse {
+ ok: true,
+ error: None,
+ })
+}
+
+#[get("/status")]
+fn status(_session: Session) -> Json<api_model::StatusResponse> {
+ Json(api_model::StatusResponse {
+ ok: true,
+ error: None,
+ })
+}
+
+#[catch(401)]
+fn unauthorized() -> Json<api_model::StatusResponse> {
+ Json(api_model::StatusResponse {
+ ok: false,
+ error: Some("Unauthorized".to_string()),
+ })
+}
+
+pub fn stage(basepath: String) -> AdHoc {
+ AdHoc::on_ignite("Auth Stage", |rocket| async {
+ rocket
+ .manage(Sessions {
+ data: Mutex::new(SessionsData {
+ active_ids: BTreeMap::new(),
+ next_id: 1,
+ }),
+ })
+ .attach(AdHoc::config::<AuthConfig>())
+ .mount(basepath.clone(), routes![login, logout, status])
+ .register(basepath, catchers![unauthorized])
+ })
+}
diff --git a/server/src/main.rs b/server/src/main.rs
index f4fec18..223d861 100644
--- a/server/src/main.rs
+++ b/server/src/main.rs
@@ -4,39 +4,18 @@ extern crate rocket;
use futures::{future::TryFutureExt, stream::TryStreamExt};
use rocket::fairing::{self, AdHoc};
-use rocket::request::{self, FromRequest, Outcome, Request};
use rocket::response::status::NotFound;
use rocket::serde::json::Json;
use rocket::{futures, Build, Rocket};
use rocket_db_pools::{sqlx, Connection, Database};
mod api_model;
+mod auth;
#[derive(Database)]
#[database("eyeballs")]
struct Db(sqlx::MySqlPool);
-struct User {
- username: String,
-}
-
-#[derive(Debug)]
-enum UserError {
- Missing,
- Invalid,
-}
-
-#[rocket::async_trait]
-impl<'r> FromRequest<'r> for User {
- type Error = UserError;
-
- async fn from_request(_req: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
- Outcome::Success(User {
- username: String::from("foo"),
- })
- }
-}
-
enum Role {
Reviewer,
Watcher,
@@ -76,7 +55,7 @@ impl TryFrom<u8> for api_model::ReviewState {
#[get("/projects?<limit>&<offset>")]
async fn projects<'r>(
mut db: Connection<Db>,
- _user: User,
+ _session: auth::Session,
limit: Option<u32>,
offset: Option<u32>,
) -> Json<api_model::Projects> {
@@ -117,7 +96,7 @@ async fn projects<'r>(
#[get("/project/<projectid>")]
async fn project<'r>(
mut db: Connection<Db>,
- _user: User,
+ _session: auth::Session,
projectid: u64,
) -> Result<Json<api_model::Project>, NotFound<&'static str>> {
let members = sqlx::query!(
@@ -154,7 +133,7 @@ async fn project<'r>(
#[get("/project/<projectid>/reviews?<limit>&<offset>")]
async fn reviews<'r>(
mut db: Connection<Db>,
- _user: User,
+ _session: auth::Session,
projectid: u64,
limit: Option<u32>,
offset: Option<u32>,
@@ -204,7 +183,7 @@ async fn reviews<'r>(
#[get("/review/<reviewid>")]
async fn review<'r>(
mut db: Connection<Db>,
- _user: User,
+ _session: auth::Session,
reviewid: u64,
) -> Result<Json<api_model::Review>, NotFound<&'static str>> {
let mut users = sqlx::query!(
@@ -277,10 +256,13 @@ async fn run_migrations(rocket: Rocket<Build>) -> fairing::Result {
#[rocket::main]
async fn main() -> Result<(), rocket::Error> {
+ let basepath = "/api/v1";
+
let _rocket = rocket::build()
.attach(Db::init())
.attach(AdHoc::try_on_ignite("Database Migrations", run_migrations))
- .mount("/api/v1", routes![projects, project, reviews, review])
+ .mount(basepath, routes![projects, project, reviews, review])
+ .attach(auth::stage(basepath.to_string()))
.launch()
.await?;