Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
4e9829c
Implement TLS and more logging
NiLuJe Aug 22, 2023
fdf6a40
Don't require TLS stuff with TLS disabled
NiLuJe Aug 23, 2023
cddf90f
Update deps
NiLuJe Aug 23, 2023
4051811
Support X-Real-IP when behind a proxy
NiLuJe Aug 23, 2023
c859a1e
Some more logging tweaks
NiLuJe Aug 23, 2023
5642317
And more logging tweaks
NiLuJe Aug 23, 2023
1d7b9f0
Tweak & unify logging some more
NiLuJe Aug 23, 2023
8ee448e
Possibly slightly janky way to deal with permission issues to access
NiLuJe Aug 23, 2023
8ba10e7
Serve a rbotos.txt
NiLuJe Aug 26, 2023
6a96c3f
DRYer
NiLuJe Aug 26, 2023
f6eb7a8
Update deps
NiLuJe Aug 26, 2023
07dc42a
Update deps
NiLuJe Sep 20, 2023
a7d4e6b
Update deps
NiLuJe Oct 7, 2023
0538e1d
Update deps
NiLuJe Nov 16, 2023
a655e02
Update deps
NiLuJe Dec 3, 2023
5237c7e
Upgrade deps
NiLuJe Dec 24, 2023
84e18b8
Update deps
NiLuJe Jan 10, 2024
31220c6
Update deps
NiLuJe Mar 3, 2024
8342bdc
Update deps
NiLuJe Apr 21, 2024
4503a7a
Update deps
NiLuJe Jun 14, 2024
51d7b89
Update deps
NiLuJe Jul 6, 2024
11ab3fa
Update deps
NiLuJe Oct 5, 2024
471b8ca
Upgrade deps
NiLuJe Oct 5, 2024
8507733
Update deps
NiLuJe Feb 15, 2025
195ccb5
Upgrade deps
NiLuJe Feb 15, 2025
c6c033f
Resolve deprecation warnings from shadow-rs
NiLuJe Feb 15, 2025
b08278b
Update for axum 0.8
NiLuJe Feb 15, 2025
b984f21
Update deps
NiLuJe Mar 21, 2025
70a98f5
Upgrade deps
NiLuJe Mar 21, 2025
87c9759
Update deps
NiLuJe Apr 13, 2025
a0ea5f0
Update deps
NiLuJe Jul 25, 2025
f7adc72
Update deps
NiLuJe Oct 30, 2025
f45b33a
Update deps
NiLuJe Mar 20, 2026
ad19dba
Upgrade deps
NiLuJe Mar 20, 2026
59b3989
Update for axum-server 0.8
NiLuJe Mar 20, 2026
9633543
Linting pass
NiLuJe Mar 20, 2026
7add8a0
Update deps
NiLuJe Apr 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,317 changes: 917 additions & 400 deletions Cargo.lock

Large diffs are not rendered by default.

9 changes: 6 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ panic = 'abort'
strip = 'debuginfo'

[dependencies]
axum = { version = "0.6", features = [] }
axum = { version = "0.8", features = [] }
axum-server = { version = "0.8", features = ["tls-rustls"] }
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
Expand All @@ -20,7 +21,9 @@ sled = { version = "0", features = ["no_logs"] }
log = { version = "0", features = ["release_max_level_info"] }
tracing = { version = "0", features = ["release_max_level_info"] }
tracing-subscriber = "0"
shadow-rs = "0"
shadow-rs = "1"
tower = { version = "0.5", features = ["tokio", "tracing"] }
tower-http = { version = "0.6", features = ["tokio", "tower", "trace", "tracing"] }

[build-dependencies]
shadow-rs = "0"
shadow-rs = "1"
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,16 @@ for docker
./docker/make.sh
```

## TLS

Generate self-signed certificates:

```bash
openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:secp384r1 -days 3650 -nodes -keyout key.pem -out cert.pem
```

## Systemd

For non-Docker deployment, a systemd unit file and its matching env file are available in [contrib](contrib/).

## WIP
6 changes: 4 additions & 2 deletions build.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
fn main() -> shadow_rs::SdResult<()> {
shadow_rs::new()
use shadow_rs::ShadowBuilder;

fn main() {
ShadowBuilder::builder().build().unwrap();
}
10 changes: 10 additions & 0 deletions contrib/kosync
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# KOSync config

# Listen on IPv6 + IPv4 (Assuming /proc/sys/net/ipv6/bindv6only is at its default)
KOSYNC_ADDR="[::]:3000"

# Disable TLS if set
#KOSYNC_NO_TLS="1"
# Path to the TLS certs.
KOSYNC_CERT="tls/cert.pem"
KOSYNC_KEY="tls/key.pem"
5 changes: 5 additions & 0 deletions contrib/kosync-cert-install.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/bin/sh

# This is executed will full privileges *before* KOSync gets daemonized.
# You can use this opportunity to install SSL certificates in the proper place,
# as you would most likely not have permission to access them later on.
19 changes: 19 additions & 0 deletions contrib/kosync.service
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
[Unit]
Description=A KOSync server implemented in Rust
After=network.target remote-fs.target nss-lookup.target

[Service]
Type=exec
EnvironmentFile=/etc/conf.d/kosync
ExecStartPre=+/usr/local/bin/kosync-cert-install.sh
ExecStart=/usr/local/bin/kosync
WorkingDirectory=/var/lib/kosync
User=kosync
Group=kosync
ProtectSystem=full
ProtectHome=yes
ReadWritePaths=/var/lib/kosync
StandardOutput=journal

[Install]
WantedBy=multi-user.target
107 changes: 82 additions & 25 deletions src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
// 2023 (c) Lzyor

use axum::{
extract::{Path, State},
http::{Request, StatusCode},
extract::{Path, Request, State},
http::StatusCode,
middleware::Next,
response::{IntoResponse, Response},
Extension, Json,
Expand All @@ -17,16 +17,26 @@ use tracing::{instrument, Level};
use crate::{
db::DB,
defs::{Error, ProgressState, FIELD_LEN_LIMIT},
utils::{is_valid_field, is_valid_key_field, now_timestamp},
utils::{is_valid_field, is_valid_key_field, now_timestamp, get_remote_addr},
};

#[derive(Debug, Clone)]
pub struct Authed(pub String);

pub async fn auth<B>(
pub async fn public(
req: Request,
next: Next,
) -> Result<Response, Error> {
let headers = req.headers();
let addr = get_remote_addr(headers, req.extensions());
tracing::info!("{} - {} {} {:?} {:?}", addr, req.method(), req.uri(), req.version(), headers);
Ok(next.run(req).await)
}

pub async fn auth(
State(db): State<DB>,
mut req: Request<B>,
next: Next<B>,
mut req: Request,
next: Next,
) -> Result<Response, Error> {
let headers = req.headers();
let check = |name| {
Expand All @@ -35,23 +45,37 @@ pub async fn auth<B>(
.and_then(|v| v.to_str().ok())
.filter(|v| v.len() <= FIELD_LEN_LIMIT && is_valid_field(v))
};
let addr = get_remote_addr(headers, req.extensions());
tracing::info!("{} - {} {} {:?}", addr, req.method(), req.uri(), req.version());
match (check("x-auth-user"), check("x-auth-key")) {
(Some(user), Some(key)) => match db.get_user(user) {
Ok(Some(k)) if k == key => {
tracing::debug!("auth: {:?}", user);
tracing::debug!("{} - AUTH - ok", user);
let user = user.to_owned();
req.extensions_mut().insert(Authed(user));
Ok(next.run(req).await)
}
Ok(_) => Err(Error::Unauthorized),
Err(_) => Err(Error::Internal),
Ok(_) => {
tracing::warn!("{} - AUTH - unauthorized: {:?}", user, headers);
Err(Error::Unauthorized)
},
Err(_) => {
tracing::error!("{} - AUTH - tripped an internal server error: {:?}", user, headers);
Err(Error::Internal)
},
},
_ => {
tracing::warn!("N/A - AUTH - no tokens in headers {:?}", headers);
Err(Error::Unauthorized)
},
_ => Err(Error::Unauthorized),
}
}

#[instrument(level = Level::DEBUG)]
pub async fn auth_user() -> impl IntoResponse {
pub async fn auth_user(
Extension(Authed(user)): Extension<Authed>,
) -> impl IntoResponse {
tracing::info!("{} - LOGIN", user);
(StatusCode::OK, Json(json!({"authorized": "OK"})))
}

Expand All @@ -67,17 +91,25 @@ pub async fn create_user(
Json(data): Json<CreateUser>,
) -> Result<impl IntoResponse, Error> {
if !is_valid_key_field(&data.username) || !is_valid_field(&data.password) {
tracing::error!("N/A - REGISTER - invalid request: {:?}", data);
return Err(Error::InvalidRequest);
}
if let Ok(Some(_)) = db.get_user(&data.username) {
tracing::warn!("{} - REGISTER - user already exists", data.username);
return Err(Error::UserExists);
}
match db.put_user(&data.username, &data.password) {
Ok(_) => Ok((
StatusCode::CREATED,
Json(json!({"username": data.username})),
)),
Err(_) => Err(Error::Internal),
Ok(_) => {
tracing::info!("{} - REGISTER - ok", data.username);
Ok((
StatusCode::CREATED,
Json(json!({"username": data.username})),
))
},
Err(_) => {
tracing::error!("{} - REGISTER - tripped an internal server error", data.username);
Err(Error::Internal)
},
}
}

Expand All @@ -90,12 +122,22 @@ pub async fn get_progress(
Extension(Authed(user)): Extension<Authed>,
) -> Result<impl IntoResponse, Error> {
if !is_valid_key_field(&doc) {
tracing::error!("{} - PULL - 'document' field not provided", user);
return Err(Error::DocumentFieldMissing);
}
match db.get_doc(&user, &doc) {
Ok(Some(value)) => Ok(Json(value).into_response()),
Ok(None) => Ok(Json(json!({ "document": doc })).into_response()),
Err(_) => Err(Error::Internal),
Ok(Some(value)) => {
tracing::info!("{} - PULL - {} <= {} on {}", user, doc, value.percentage, value.device);
Ok(Json(value).into_response())
},
Ok(None) => {
tracing::info!("{} - PULL - {} <= None", user, doc);
Ok(Json(json!({ "document": doc })).into_response())
},
Err(_) => {
tracing::error!("{} - PULL - tripped an internal server error", user);
Err(Error::Internal)
},
}
}

Expand All @@ -107,15 +149,30 @@ pub async fn update_progress(
) -> impl IntoResponse {
data.timestamp = Some(now_timestamp());
match db.put_doc(&user, &data.document, &data) {
Ok(_) => Ok(Json(json!({
"document": data.document,
"timestamp": data.timestamp
}))),
Err(_) => Err(Error::Internal),
Ok(_) => {
tracing::info!("{} - PUSH - {} => {} on {}", user, data.document, data.percentage, data.device);
Ok(Json(json!({
"document": data.document,
"timestamp": data.timestamp
})))
},
Err(_) => {
tracing::error!("{} - PUSH - tripped an internal server error", user);
Err(Error::Internal)
},
}
}

#[instrument(level = Level::DEBUG)]
pub async fn healthcheck() -> impl IntoResponse {
pub async fn healthcheck(
Extension(Authed(user)): Extension<Authed>,
) -> impl IntoResponse {
tracing::info!("{} - HEALTH CHECK", user);
(StatusCode::OK, Json(json!({"state": "OK"})))
}

#[instrument(level = Level::DEBUG)]
pub async fn robots(
) -> &'static str {
"User-agent: *\nDisallow: /\n"
}
4 changes: 3 additions & 1 deletion src/defs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ use axum::{
use serde::{Deserialize, Serialize};
use serde_json::json;

pub const DEFAULT_ADDR: &str = "0.0.0.0:3000";
pub const DEFAULT_ADDR: &str = "[::]:3000";
pub const DEFAULT_TLS_CERT: &str = "tls/cert.pem";
pub const DEFAULT_TLS_PRIVKEY: &str = "tls/key.pem";
pub const DEFAULT_TREE_NAME: &str = "kosync";
pub const DEFAULT_DB_PATH: &str = "data/kosync";
pub const FIELD_LEN_LIMIT: usize = 4096;
Expand Down
Loading