Skip to content

Commit

Permalink
Basic HTTP auth for sd-server (#2314)
Browse files Browse the repository at this point in the history
* basic http auth

* fix types

* Fix

* auth docs
  • Loading branch information
oscartbeaumont authored Apr 12, 2024
1 parent 785dd74 commit 9f53961
Show file tree
Hide file tree
Showing 6 changed files with 156 additions and 41 deletions.
19 changes: 10 additions & 9 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ env:
CARGO_NET_RETRY: 10
RUST_BACKTRACE: short
RUSTUP_MAX_RETRIES: 10
SD_AUTH: disabled

# Cancel previous runs of the same workflow on the same branch.
concurrency:
Expand Down Expand Up @@ -107,10 +108,10 @@ jobs:
with:
swap-size-mb: 3072
root-reserve-mb: 6144
remove-dotnet: "true"
remove-codeql: "true"
remove-haskell: "true"
remove-docker-images: "true"
remove-dotnet: 'true'
remove-codeql: 'true'
remove-haskell: 'true'
remove-docker-images: 'true'

- name: Symlink target to C:\
if: ${{ runner.os == 'Windows' }}
Expand Down Expand Up @@ -143,7 +144,7 @@ jobs:
if: steps.filter.outcome != 'success' || steps.filter.outputs.changes == 'true'
uses: ./.github/actions/setup-rust
with:
restore-cache: "false"
restore-cache: 'false'

- name: Run rustfmt
if: steps.filter.outcome != 'success' || steps.filter.outputs.changes == 'true'
Expand All @@ -162,10 +163,10 @@ jobs:
with:
swap-size-mb: 3072
root-reserve-mb: 6144
remove-dotnet: "true"
remove-codeql: "true"
remove-haskell: "true"
remove-docker-images: "true"
remove-dotnet: 'true'
remove-codeql: 'true'
remove-haskell: 'true'
remove-docker-images: 'true'

- name: Symlink target to C:\
if: ${{ runner.os == 'Windows' }}
Expand Down
11 changes: 11 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 4 additions & 5 deletions apps/server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,17 @@ ai-models = ["sd-core/ai"]

[dependencies]
# Spacedrive Sub-crates
sd-core = { path = "../../core", features = [
"ffmpeg",
"heif",
] }
sd-core = { path = "../../core", features = ["ffmpeg", "heif"] }

axum = { workspace = true }
axum = { workspace = true, features = ["headers"] }
http = { workspace = true }
rspc = { workspace = true, features = ["axum"] }
tokio = { workspace = true, features = ["sync", "rt-multi-thread", "signal"] }
tracing = { workspace = true }
base64 = { workspace = true }

tempfile = "3.10.1"

include_dir = "0.7.3"
mime_guess = "2.0.4"
secstr = "0.5.1"
106 changes: 102 additions & 4 deletions apps/server/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,64 @@
use std::{env, net::SocketAddr, path::Path};
use std::{collections::HashMap, env, net::SocketAddr, path::Path};

use axum::routing::get;
use axum::{
extract::{FromRequestParts, State},
headers::{authorization::Basic, Authorization},
http::Request,
middleware::{self, Next},
response::{IntoResponse, Response},
routing::get,
TypedHeader,
};
use sd_core::{custom_uri, Node};
use tracing::info;
use secstr::SecStr;
use tracing::{info, warn};

mod utils;

#[cfg(feature = "assets")]
static ASSETS_DIR: include_dir::Dir<'static> =
include_dir::include_dir!("$CARGO_MANIFEST_DIR/../web/dist");

#[derive(Clone)]
pub struct AppState {
auth: HashMap<String, SecStr>,
}

async fn basic_auth<B>(
State(state): State<AppState>,
request: Request<B>,
next: Next<B>,
) -> Response {
let (mut parts, body) = request.into_parts();
let Ok(TypedHeader(Authorization(hdr))) =
TypedHeader::<Authorization<Basic>>::from_request_parts(&mut parts, &()).await
else {
return Response::builder()
.status(401)
.header("WWW-Authenticate", "Basic realm=\"Spacedrive\"")
.body("Unauthorized".into_response().into_body())
.expect("hardcoded response will be valid");
};
let request = Request::from_parts(parts, body);

if state.auth.len() != 0 {
if state
.auth
.get(hdr.username())
.and_then(|pass| Some(*pass == SecStr::from(hdr.password())))
!= Some(true)
{
return Response::builder()
.status(401)
.header("WWW-Authenticate", "Basic realm=\"Spacedrive\"")
.body("Unauthorized".into_response().into_body())
.expect("hardcoded response will be valid");
}
}

next.run(request).await
}

#[tokio::main]
async fn main() {
let data_dir = match env::var("DATA_DIR") {
Expand Down Expand Up @@ -43,6 +92,54 @@ async fn main() {
}
};

let (auth, disabled) = {
let input = env::var("SD_AUTH").unwrap_or_default();

if input == "disabled" {
(Default::default(), true)
} else {
(
input
.split(',')
.collect::<Vec<_>>()
.into_iter()
.enumerate()
.filter_map(|(i, s)| {
if s.len() == 0 {
return None;
}

let mut parts = s.split(':');

let result = parts.next().and_then(|user| {
parts
.next()
.map(|pass| (user.to_string(), SecStr::from(pass)))
});
if result.is_none() {
warn!("Found invalid credential {i}. Skipping...");
}
result
})
.collect::<HashMap<_, _>>(),
false,
)
}
};

// We require credentials in production builds (unless explicitly disabled)
if auth.len() == 0 && !disabled {
#[cfg(not(debug_assertions))]
{
warn!("The 'SD_AUTH' environment variable is not set!");
warn!("If you want to disable auth set 'SD_AUTH=disabled', or");
warn!("Provide your credentials in the following format 'SD_AUTH=username:password,username2:password2'");
std::process::exit(1);
}
}

let state = AppState { auth };

let (node, router) = match Node::new(
data_dir,
sd_core::Env {
Expand Down Expand Up @@ -140,7 +237,8 @@ async fn main() {
#[cfg(not(feature = "assets"))]
let app = app
.route("/", get(|| async { "Spacedrive Server!" }))
.fallback(|| async { "404 Not Found: We're past the event horizon..." });
.fallback(|| async { "404 Not Found: We're past the event horizon..." })
.layer(middleware::from_fn_with_state(state, basic_auth));

let mut addr = "[::]:8080".parse::<SocketAddr>().unwrap(); // This listens on IPv6 and IPv4
addr.set_port(port);
Expand Down
15 changes: 10 additions & 5 deletions docs/product/getting-started/setup.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,20 @@ You can run Spacedrive in a Docker container using the following command.
type="note"
text="For the best performance of the docker container, we recommend to run on Linux (linux/amd64). The container is not yet optimized for other platforms."
/>
<Notice
type="warning"
text="Currently, Spacedrive's Docker Server does not support authentication methods. Use at your own risk as your data will not be protected from other devices (or users) on your network. The feature is under active development and you can check when it will launch on the [roadmap](/roadmap)."
/>

```bash
docker run -d --name spacedrive -p 8080:8080 -v /var/spacedrive:/var/spacedrive ghcr.io/spacedriveapp/spacedrive/server
docker run -d --name spacedrive -p 8080:8080 -e SD_AUTH=admin,spacedrive -v /var/spacedrive:/var/spacedrive ghcr.io/spacedriveapp/spacedrive/server
```

#### Authentication

When using the Spacedrive server you can use the `SD_AUTH` environment variable to configure authentication.

Valid values:
- `SD_AUTH=disabled` - Disables authentication.
- `SD_AUTH=username:password` - Enables authentication for a single user.
- `SD_AUTH=username:password,username1:password1` - Enables authentication with multiple users (you can add as many users as you want).

### Mobile (Preview)

Take your Spacedrive library on the go with our mobile apps. You can join the betas by following the links below.
Expand Down
37 changes: 19 additions & 18 deletions interface/app/$libraryId/debug/sync.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,30 +88,31 @@ const OperationGroup = ({ group }: { group: MessageGroup }) => {

function calculateGroups(messages: CRDTOperation[]) {
return messages.reduce<MessageGroup[]>((acc, op) => {
const { data } = op;
// TODO: fix Typescript
// const { data } = op;

const id = JSON.stringify(op.record_id);
// const id = JSON.stringify(op.record_id);

const latest = (() => {
const latest = acc[acc.length - 1];
// const latest = (() => {
// const latest = acc[acc.length - 1];

if (!latest || latest.model !== op.model || latest.id !== id) {
const group: MessageGroup = {
model: op.model,
id,
messages: []
};
// if (!latest || latest.model !== op.model || latest.id !== id) {
// const group: MessageGroup = {
// model: op.model,
// id,
// messages: []
// };

acc.push(group);
// acc.push(group);

return group;
} else return latest;
})();
// return group;
// } else return latest;
// })();

latest.messages.push({
data,
timestamp: op.timestamp
});
// latest.messages.push({
// data,
// timestamp: op.timestamp
// });

return acc;
}, []);
Expand Down

0 comments on commit 9f53961

Please sign in to comment.