feat(snix/castore-http): initial implementation
The castore-http crate provides both a binary and a library interface to serve a single castore root node over HTTP. The library function `get_root_node_contents` will return a `axum::Response` for a requested path in the castore root node depending on the requested paths type. If the requested path in the root node is a directory, we return: - a index file if there is a file matching one of the configurable `index_names` - a directory listing, if no `index_names` were configured and `auto_index` was enabled - the FORBIDDEN status code if no `index_names` were set nor `auto_index` was enabled If the requested path in the root node is a file, we return the file. If the requested path in the root node is a symlink, we figure out wether the target exists and return a REDIRECT. If the requested path doesn't exist in the root node, we respond with NOT_FOUND The binary wraps this functionality and allows one to specify the desired root node by providing its base-64 encoded representation as well as the other configuration parameters affecting the behavior of `get_root_node_contents`. Change-Id: I737482299f788ec0244c54b52042f9eb655a05c2 Reviewed-on: https://cl.snix.dev/c/snix/+/30245 Autosubmit: Marijan Petričević <marijan.petricevic94@gmail.com> Reviewed-by: Marijan Petričević <marijan.petricevic94@gmail.com> Tested-by: besadii Reviewed-by: Florian Klink <flokli@flokli.de>
This commit is contained in:
parent
bed42b59df
commit
6b48bcc1bf
12 changed files with 1093 additions and 1 deletions
27
snix/Cargo.lock
generated
27
snix/Cargo.lock
generated
|
|
@ -4248,6 +4248,32 @@ dependencies = [
|
||||||
"zstd",
|
"zstd",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "snix-castore-http"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"axum",
|
||||||
|
"axum-extra",
|
||||||
|
"axum-range",
|
||||||
|
"axum-test",
|
||||||
|
"blake3",
|
||||||
|
"bytes",
|
||||||
|
"clap",
|
||||||
|
"data-encoding",
|
||||||
|
"mime",
|
||||||
|
"mime_guess",
|
||||||
|
"path-clean",
|
||||||
|
"prost",
|
||||||
|
"snix-castore",
|
||||||
|
"tokio",
|
||||||
|
"tokio-listener",
|
||||||
|
"tokio-util",
|
||||||
|
"tracing",
|
||||||
|
"tracing-subscriber",
|
||||||
|
"tracing-test",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "snix-cli"
|
name = "snix-cli"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|
@ -4800,6 +4826,7 @@ dependencies = [
|
||||||
"signal-hook-registry",
|
"signal-hook-registry",
|
||||||
"socket2",
|
"socket2",
|
||||||
"tokio-macros",
|
"tokio-macros",
|
||||||
|
"tracing",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
||||||
123
snix/Cargo.nix
123
snix/Cargo.nix
|
|
@ -105,6 +105,16 @@ rec {
|
||||||
# File a bug if you depend on any for non-debug work!
|
# File a bug if you depend on any for non-debug work!
|
||||||
debug = internal.debugCrate { inherit packageId; };
|
debug = internal.debugCrate { inherit packageId; };
|
||||||
};
|
};
|
||||||
|
"snix-castore-http" = rec {
|
||||||
|
packageId = "snix-castore-http";
|
||||||
|
build = internal.buildRustCrateWithFeatures {
|
||||||
|
packageId = "snix-castore-http";
|
||||||
|
};
|
||||||
|
|
||||||
|
# Debug support which might change between releases.
|
||||||
|
# File a bug if you depend on any for non-debug work!
|
||||||
|
debug = internal.debugCrate { inherit packageId; };
|
||||||
|
};
|
||||||
"snix-cli" = rec {
|
"snix-cli" = rec {
|
||||||
packageId = "snix-cli";
|
packageId = "snix-cli";
|
||||||
build = internal.buildRustCrateWithFeatures {
|
build = internal.buildRustCrateWithFeatures {
|
||||||
|
|
@ -13925,6 +13935,109 @@ rec {
|
||||||
};
|
};
|
||||||
resolvedDefaultFeatures = [ "cloud" "default" "fs" "fuse" "integration" "toml" "tonic-reflection" "virtiofs" "xp-composition-cli" "xp-composition-url-refs" ];
|
resolvedDefaultFeatures = [ "cloud" "default" "fs" "fuse" "integration" "toml" "tonic-reflection" "virtiofs" "xp-composition-cli" "xp-composition-url-refs" ];
|
||||||
};
|
};
|
||||||
|
"snix-castore-http" = rec {
|
||||||
|
crateName = "snix-castore-http";
|
||||||
|
version = "0.1.0";
|
||||||
|
edition = "2021";
|
||||||
|
crateBin = [
|
||||||
|
{
|
||||||
|
name = "snix-castore-http";
|
||||||
|
path = "src/main.rs";
|
||||||
|
requiredFeatures = [ ];
|
||||||
|
}
|
||||||
|
];
|
||||||
|
src = lib.cleanSourceWith { filter = sourceFilter; src = ./castore-http; };
|
||||||
|
libName = "snix_castore_http";
|
||||||
|
dependencies = [
|
||||||
|
{
|
||||||
|
name = "anyhow";
|
||||||
|
packageId = "anyhow";
|
||||||
|
}
|
||||||
|
{
|
||||||
|
name = "axum";
|
||||||
|
packageId = "axum";
|
||||||
|
features = [ "tracing" ];
|
||||||
|
}
|
||||||
|
{
|
||||||
|
name = "axum-extra";
|
||||||
|
packageId = "axum-extra";
|
||||||
|
}
|
||||||
|
{
|
||||||
|
name = "axum-range";
|
||||||
|
packageId = "axum-range";
|
||||||
|
}
|
||||||
|
{
|
||||||
|
name = "bytes";
|
||||||
|
packageId = "bytes";
|
||||||
|
}
|
||||||
|
{
|
||||||
|
name = "clap";
|
||||||
|
packageId = "clap";
|
||||||
|
features = [ "derive" ];
|
||||||
|
}
|
||||||
|
{
|
||||||
|
name = "data-encoding";
|
||||||
|
packageId = "data-encoding";
|
||||||
|
}
|
||||||
|
{
|
||||||
|
name = "mime";
|
||||||
|
packageId = "mime";
|
||||||
|
}
|
||||||
|
{
|
||||||
|
name = "mime_guess";
|
||||||
|
packageId = "mime_guess";
|
||||||
|
}
|
||||||
|
{
|
||||||
|
name = "path-clean";
|
||||||
|
packageId = "path-clean";
|
||||||
|
}
|
||||||
|
{
|
||||||
|
name = "prost";
|
||||||
|
packageId = "prost";
|
||||||
|
}
|
||||||
|
{
|
||||||
|
name = "snix-castore";
|
||||||
|
packageId = "snix-castore";
|
||||||
|
}
|
||||||
|
{
|
||||||
|
name = "tokio";
|
||||||
|
packageId = "tokio";
|
||||||
|
features = [ "tracing" ];
|
||||||
|
}
|
||||||
|
{
|
||||||
|
name = "tokio-listener";
|
||||||
|
packageId = "tokio-listener";
|
||||||
|
features = [ "axum07" "clap" "multi-listener" "sd_listen" ];
|
||||||
|
}
|
||||||
|
{
|
||||||
|
name = "tokio-util";
|
||||||
|
packageId = "tokio-util";
|
||||||
|
}
|
||||||
|
{
|
||||||
|
name = "tracing";
|
||||||
|
packageId = "tracing";
|
||||||
|
}
|
||||||
|
{
|
||||||
|
name = "tracing-subscriber";
|
||||||
|
packageId = "tracing-subscriber";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
devDependencies = [
|
||||||
|
{
|
||||||
|
name = "axum-test";
|
||||||
|
packageId = "axum-test";
|
||||||
|
}
|
||||||
|
{
|
||||||
|
name = "blake3";
|
||||||
|
packageId = "blake3";
|
||||||
|
}
|
||||||
|
{
|
||||||
|
name = "tracing-test";
|
||||||
|
packageId = "tracing-test";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
};
|
||||||
"snix-cli" = rec {
|
"snix-cli" = rec {
|
||||||
crateName = "snix-cli";
|
crateName = "snix-cli";
|
||||||
version = "0.1.0";
|
version = "0.1.0";
|
||||||
|
|
@ -15703,6 +15816,14 @@ rec {
|
||||||
packageId = "tokio-macros";
|
packageId = "tokio-macros";
|
||||||
optional = true;
|
optional = true;
|
||||||
}
|
}
|
||||||
|
{
|
||||||
|
name = "tracing";
|
||||||
|
packageId = "tracing";
|
||||||
|
optional = true;
|
||||||
|
usesDefaultFeatures = false;
|
||||||
|
target = { target, features }: (target."tokio_unstable" or false);
|
||||||
|
features = [ "std" ];
|
||||||
|
}
|
||||||
{
|
{
|
||||||
name = "windows-sys";
|
name = "windows-sys";
|
||||||
packageId = "windows-sys 0.52.0";
|
packageId = "windows-sys 0.52.0";
|
||||||
|
|
@ -15747,7 +15868,7 @@ rec {
|
||||||
"tracing" = [ "dep:tracing" ];
|
"tracing" = [ "dep:tracing" ];
|
||||||
"windows-sys" = [ "dep:windows-sys" ];
|
"windows-sys" = [ "dep:windows-sys" ];
|
||||||
};
|
};
|
||||||
resolvedDefaultFeatures = [ "bytes" "default" "fs" "io-std" "io-util" "libc" "macros" "mio" "net" "process" "rt" "rt-multi-thread" "signal" "signal-hook-registry" "socket2" "sync" "test-util" "time" "tokio-macros" "windows-sys" ];
|
resolvedDefaultFeatures = [ "bytes" "default" "fs" "io-std" "io-util" "libc" "macros" "mio" "net" "process" "rt" "rt-multi-thread" "signal" "signal-hook-registry" "socket2" "sync" "test-util" "time" "tokio-macros" "tracing" "windows-sys" ];
|
||||||
};
|
};
|
||||||
"tokio-listener" = rec {
|
"tokio-listener" = rec {
|
||||||
crateName = "tokio-listener";
|
crateName = "tokio-listener";
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ resolver = "2"
|
||||||
members = [
|
members = [
|
||||||
"build",
|
"build",
|
||||||
"castore",
|
"castore",
|
||||||
|
"castore-http",
|
||||||
"cli",
|
"cli",
|
||||||
"eval",
|
"eval",
|
||||||
"eval/builtin-macros",
|
"eval/builtin-macros",
|
||||||
|
|
|
||||||
28
snix/castore-http/Cargo.toml
Normal file
28
snix/castore-http/Cargo.toml
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
[package]
|
||||||
|
name = "snix-castore-http"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow.workspace = true
|
||||||
|
axum = { workspace = true, features = ["tracing"] }
|
||||||
|
axum-extra.workspace = true
|
||||||
|
axum-range.workspace = true
|
||||||
|
bytes.workspace = true
|
||||||
|
clap = { workspace = true, features = ["derive"] }
|
||||||
|
data-encoding.workspace = true
|
||||||
|
mime_guess = "2.0.5"
|
||||||
|
mime = "0.3.17"
|
||||||
|
path-clean.workspace = true
|
||||||
|
prost.workspace = true
|
||||||
|
tokio = { workspace = true, features = [ "tracing"] }
|
||||||
|
tokio-listener = { workspace = true, features = ["axum07", "clap", "multi-listener", "sd_listen"] }
|
||||||
|
tracing.workspace = true
|
||||||
|
tracing-subscriber.workspace = true
|
||||||
|
tokio-util.workspace = true
|
||||||
|
snix-castore = { path = "../castore" }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
axum-test = "16.4.0"
|
||||||
|
blake3.workspace = true
|
||||||
|
tracing-test.workspace = true
|
||||||
5
snix/castore-http/default.nix
Normal file
5
snix/castore-http/default.nix
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
{ depot, ... }:
|
||||||
|
|
||||||
|
(depot.snix.crates.workspaceMembers.snix-castore-http.build.override {
|
||||||
|
runTests = true;
|
||||||
|
})
|
||||||
13
snix/castore-http/src/app_state.rs
Normal file
13
snix/castore-http/src/app_state.rs
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
use snix_castore::{blobservice::BlobService, directoryservice::DirectoryService, Node};
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
pub type AppState = Arc<AppConfig>;
|
||||||
|
|
||||||
|
pub struct AppConfig {
|
||||||
|
pub blob_service: Arc<dyn BlobService>,
|
||||||
|
pub directory_service: Arc<dyn DirectoryService>,
|
||||||
|
pub root_node: Node,
|
||||||
|
pub index_names: Vec<String>,
|
||||||
|
pub auto_index: bool,
|
||||||
|
}
|
||||||
34
snix/castore-http/src/cli.rs
Normal file
34
snix/castore-http/src/cli.rs
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
use clap::Parser;
|
||||||
|
use snix_castore::utils::ServiceUrlsGrpc;
|
||||||
|
|
||||||
|
#[derive(Parser)]
|
||||||
|
#[command(author, version, about)]
|
||||||
|
pub struct CliArgs {
|
||||||
|
/// The address to listen on
|
||||||
|
#[clap(flatten)]
|
||||||
|
pub listen_args: tokio_listener::ListenerAddressLFlag,
|
||||||
|
// The castore services addresses
|
||||||
|
#[clap(flatten)]
|
||||||
|
pub service_addrs: ServiceUrlsGrpc,
|
||||||
|
/// The castore root node to serve, URL-safe base64-encoded
|
||||||
|
#[arg(
|
||||||
|
short,
|
||||||
|
long,
|
||||||
|
help = "The castore root node to serve, URL-safe base64-encoded"
|
||||||
|
)]
|
||||||
|
pub root_node: String,
|
||||||
|
/// The name of the file to serve if a client requests a directory e.g. index.html index.htm
|
||||||
|
#[arg(
|
||||||
|
short,
|
||||||
|
long,
|
||||||
|
help = "The name of the file to serve if a client requests a directory e.g. index.html index.htm"
|
||||||
|
)]
|
||||||
|
pub index_names: Vec<String>,
|
||||||
|
/// Whether a directory listing should be returned if a client requests a directory but none of the `index_names` matched
|
||||||
|
#[arg(
|
||||||
|
short,
|
||||||
|
long,
|
||||||
|
help = "Whether a directory listing should be returned if a client requests a directory but none of the `index_names` matched"
|
||||||
|
)]
|
||||||
|
pub auto_index: bool,
|
||||||
|
}
|
||||||
266
snix/castore-http/src/lib.rs
Normal file
266
snix/castore-http/src/lib.rs
Normal file
|
|
@ -0,0 +1,266 @@
|
||||||
|
pub mod app_state;
|
||||||
|
pub mod cli;
|
||||||
|
pub mod router;
|
||||||
|
pub mod routes;
|
||||||
|
|
||||||
|
use std::path;
|
||||||
|
|
||||||
|
use snix_castore::{
|
||||||
|
blobservice::BlobService,
|
||||||
|
directoryservice::{descend_to, DirectoryService},
|
||||||
|
B3Digest, Directory, Node, Path, SymlinkTarget,
|
||||||
|
};
|
||||||
|
|
||||||
|
use axum::{
|
||||||
|
body::Body,
|
||||||
|
http::{header, StatusCode},
|
||||||
|
response::{AppendHeaders, IntoResponse, Redirect, Response},
|
||||||
|
};
|
||||||
|
use axum_extra::{headers::Range, response::Html, TypedHeader};
|
||||||
|
use axum_range::{KnownSize, Ranged};
|
||||||
|
use path_clean::PathClean;
|
||||||
|
use std::ffi::OsStr;
|
||||||
|
use std::os::unix::ffi::OsStrExt;
|
||||||
|
use tokio_util::io::ReaderStream;
|
||||||
|
use tracing::{debug, error, instrument, warn};
|
||||||
|
|
||||||
|
/// Helper function, descending from the given `root_node` to the `requested_path` specified.
|
||||||
|
/// Returns HTTP Responses or Status Codes.
|
||||||
|
/// If the path points to a regular file, it serves its contents.
|
||||||
|
/// If the path points to a symlink, it sends a redirect to the target (pretending `base_path`, if relative)
|
||||||
|
/// If the path points to a directory, files of `index_names` are tried,
|
||||||
|
/// if no files matched then a directory listing is returned if `auto_index` is enabled.
|
||||||
|
///
|
||||||
|
/// Uses the passed [BlobService] and [DirectoryService]
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
#[instrument(level = "trace", skip_all, fields(base_path, requested_path), err)]
|
||||||
|
pub async fn get_root_node_contents<BS: BlobService, DS: DirectoryService, S: AsRef<str>>(
|
||||||
|
blob_service: BS,
|
||||||
|
directory_service: DS,
|
||||||
|
base_path: &path::Path,
|
||||||
|
root_node: &Node,
|
||||||
|
requested_path: &Path,
|
||||||
|
range_header: Option<TypedHeader<Range>>,
|
||||||
|
index_names: &[S],
|
||||||
|
auto_index: bool,
|
||||||
|
) -> Result<Response, StatusCode> {
|
||||||
|
match root_node {
|
||||||
|
Node::Directory { .. } => {
|
||||||
|
let requested_node = descend_to(&directory_service, root_node.clone(), requested_path)
|
||||||
|
.await
|
||||||
|
.map_err(|err| {
|
||||||
|
error!(err=%err, "an error occured descending");
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR
|
||||||
|
})?
|
||||||
|
.ok_or_else(|| {
|
||||||
|
error!("requested path doesn't exist");
|
||||||
|
StatusCode::NOT_FOUND
|
||||||
|
})?;
|
||||||
|
match requested_node {
|
||||||
|
Node::Directory { digest, .. } => {
|
||||||
|
let requested_directory = directory_service
|
||||||
|
.get(&digest)
|
||||||
|
.await
|
||||||
|
.map_err(|err| {
|
||||||
|
error!(err=%err, "an error occured getting the directory");
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR
|
||||||
|
})?
|
||||||
|
.ok_or_else(|| {
|
||||||
|
error!("directory doesn't exist");
|
||||||
|
StatusCode::NOT_FOUND
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// If there was one or more index configured, try to find it
|
||||||
|
// in the directory requested by the client, by comparing the bytes
|
||||||
|
// of each directories immediate child's path with the bytes of the
|
||||||
|
// configured index name
|
||||||
|
for index_name in index_names {
|
||||||
|
if let Some((found_index_file_path, found_index_node)) = requested_directory
|
||||||
|
.nodes()
|
||||||
|
.find(|(path, _node)| index_name.as_ref().as_bytes() == path.as_ref())
|
||||||
|
{
|
||||||
|
match found_index_node {
|
||||||
|
Node::File { digest, size, .. } => {
|
||||||
|
let found_index_file_path = found_index_file_path.to_string();
|
||||||
|
let found_index_file_path = path::Path::new(OsStr::from_bytes(
|
||||||
|
found_index_file_path.as_bytes(),
|
||||||
|
));
|
||||||
|
return respond_file(
|
||||||
|
blob_service,
|
||||||
|
Some(found_index_file_path),
|
||||||
|
range_header,
|
||||||
|
digest,
|
||||||
|
*size,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
debug!(
|
||||||
|
path = %found_index_file_path,
|
||||||
|
"One of the configured index names matched with a
|
||||||
|
node located in the root node's directory which is
|
||||||
|
not a file"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if auto_index {
|
||||||
|
return respond_directory_list(&requested_directory, requested_path).await;
|
||||||
|
}
|
||||||
|
Err(StatusCode::FORBIDDEN)
|
||||||
|
}
|
||||||
|
Node::File { digest, size, .. } => {
|
||||||
|
let requested_path =
|
||||||
|
path::Path::new(OsStr::from_bytes(requested_path.as_bytes()));
|
||||||
|
respond_file(
|
||||||
|
blob_service,
|
||||||
|
Some(requested_path),
|
||||||
|
range_header,
|
||||||
|
&digest,
|
||||||
|
size,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
Node::Symlink { target } => {
|
||||||
|
let requested_path =
|
||||||
|
path::Path::new(OsStr::from_bytes(requested_path.as_bytes()));
|
||||||
|
respond_symlink(base_path, &target, Some(requested_path)).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Node::File { digest, size, .. } => {
|
||||||
|
if requested_path.to_string() == "" {
|
||||||
|
respond_file(blob_service, None, range_header, digest, *size).await
|
||||||
|
} else {
|
||||||
|
warn!(
|
||||||
|
"The client requested a path but the configured root
|
||||||
|
node being served is a file"
|
||||||
|
);
|
||||||
|
Err(StatusCode::BAD_REQUEST)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Node::Symlink { target } => {
|
||||||
|
if requested_path.to_string() == "" {
|
||||||
|
respond_symlink(base_path, target, None).await
|
||||||
|
} else {
|
||||||
|
warn!(
|
||||||
|
"The client requested a path but the configured root
|
||||||
|
node being served is a symlink"
|
||||||
|
);
|
||||||
|
Err(StatusCode::BAD_REQUEST)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(level = "trace", skip_all)]
|
||||||
|
pub async fn respond_symlink(
|
||||||
|
base_path: &path::Path,
|
||||||
|
symlink_target: &SymlinkTarget,
|
||||||
|
requested_path: Option<&path::Path>,
|
||||||
|
) -> Result<Response, StatusCode> {
|
||||||
|
if symlink_target.as_ref() == b"." {
|
||||||
|
error!("There was a symlink with target '.'");
|
||||||
|
return Err(StatusCode::INTERNAL_SERVER_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
let symlink_target_path = match std::str::from_utf8(symlink_target.as_ref()) {
|
||||||
|
Ok(s) => path::Path::new(s),
|
||||||
|
Err(_) => {
|
||||||
|
error!("Symlink target contains invalid UTF-8");
|
||||||
|
return Err(StatusCode::INTERNAL_SERVER_ERROR);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let symlink_target_path = if symlink_target_path.is_absolute() {
|
||||||
|
symlink_target_path.to_path_buf()
|
||||||
|
} else if let Some(requested_path) = requested_path {
|
||||||
|
let requested_path_parent = requested_path.parent().ok_or_else(|| {
|
||||||
|
error!("failed to retrieve parent path for requested path");
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR
|
||||||
|
})?;
|
||||||
|
base_path
|
||||||
|
.join(requested_path_parent)
|
||||||
|
.join(symlink_target_path)
|
||||||
|
} else {
|
||||||
|
base_path.join(symlink_target_path)
|
||||||
|
};
|
||||||
|
|
||||||
|
let symlink_target_path = symlink_target_path.clean();
|
||||||
|
|
||||||
|
if symlink_target_path.starts_with(path::Component::ParentDir) {
|
||||||
|
error!("the symlink's target path points to a non-existing path");
|
||||||
|
return Err(StatusCode::INTERNAL_SERVER_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
let symlink_target_path_str = symlink_target_path.to_str().ok_or(StatusCode::NOT_FOUND)?;
|
||||||
|
Ok(Redirect::temporary(symlink_target_path_str).into_response())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(level = "trace", skip_all, fields(directory_path, directory))]
|
||||||
|
pub async fn respond_directory_list(
|
||||||
|
directory: &Directory,
|
||||||
|
directory_path: &Path,
|
||||||
|
) -> Result<Response, StatusCode> {
|
||||||
|
let mut directory_list_html = String::new();
|
||||||
|
for (path_component, _node) in directory.nodes() {
|
||||||
|
let directory_path = directory_path
|
||||||
|
.try_join(path_component.as_ref())
|
||||||
|
.expect("Join path");
|
||||||
|
directory_list_html.push_str(&format!(
|
||||||
|
"<li><a href=\"/{directory_path}\">{path_component}</a></li>"
|
||||||
|
))
|
||||||
|
}
|
||||||
|
Ok(Html(format!(
|
||||||
|
"<!DOCTYPE html><html><body>{directory_list_html}</body></html>"
|
||||||
|
))
|
||||||
|
.into_response())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(level = "trace", skip_all, fields(digest, size))]
|
||||||
|
pub async fn respond_file<BS: BlobService>(
|
||||||
|
blob_service: BS,
|
||||||
|
requested_path: Option<&path::Path>,
|
||||||
|
range_header: Option<TypedHeader<Range>>,
|
||||||
|
digest: &B3Digest,
|
||||||
|
size: u64,
|
||||||
|
) -> Result<Response, StatusCode> {
|
||||||
|
let blob_reader = blob_service
|
||||||
|
.open_read(digest)
|
||||||
|
.await
|
||||||
|
.map_err(|err| {
|
||||||
|
error!(err=%err, "failed to read blob");
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR
|
||||||
|
})?
|
||||||
|
.ok_or_else(|| {
|
||||||
|
error!("blob doesn't exist");
|
||||||
|
StatusCode::NOT_FOUND
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let mime_type = requested_path
|
||||||
|
.and_then(path::Path::extension)
|
||||||
|
.and_then(std::ffi::OsStr::to_str)
|
||||||
|
.and_then(|extension| mime_guess::from_ext(extension).first())
|
||||||
|
.unwrap_or(mime::APPLICATION_OCTET_STREAM);
|
||||||
|
match range_header {
|
||||||
|
None => Ok((
|
||||||
|
StatusCode::OK,
|
||||||
|
AppendHeaders([
|
||||||
|
(header::CONTENT_TYPE, mime_type.to_string()),
|
||||||
|
(header::CONTENT_LENGTH, size.to_string()),
|
||||||
|
]),
|
||||||
|
Body::from_stream(ReaderStream::new(blob_reader)),
|
||||||
|
)
|
||||||
|
.into_response()),
|
||||||
|
Some(TypedHeader(range)) => Ok((
|
||||||
|
StatusCode::OK,
|
||||||
|
AppendHeaders([
|
||||||
|
(header::CONTENT_TYPE, mime_type.to_string()),
|
||||||
|
(header::CONTENT_LENGTH, size.to_string()),
|
||||||
|
]),
|
||||||
|
Ranged::new(Some(range), KnownSize::sized(blob_reader, size)).into_response(),
|
||||||
|
)
|
||||||
|
.into_response()),
|
||||||
|
}
|
||||||
|
}
|
||||||
47
snix/castore-http/src/main.rs
Normal file
47
snix/castore-http/src/main.rs
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
use snix_castore_http::cli::CliArgs;
|
||||||
|
|
||||||
|
use anyhow::{bail, Context};
|
||||||
|
use bytes::Bytes;
|
||||||
|
use clap::Parser;
|
||||||
|
use data_encoding::BASE64URL_NOPAD;
|
||||||
|
use prost::Message;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> anyhow::Result<()> {
|
||||||
|
tracing_subscriber::fmt().init();
|
||||||
|
|
||||||
|
let CliArgs {
|
||||||
|
listen_args,
|
||||||
|
service_addrs,
|
||||||
|
root_node,
|
||||||
|
index_names,
|
||||||
|
auto_index,
|
||||||
|
}: CliArgs = snix_castore_http::cli::CliArgs::parse();
|
||||||
|
|
||||||
|
// b64decode the root node passed *by the user*
|
||||||
|
let root_entry_proto = BASE64URL_NOPAD
|
||||||
|
.decode(root_node.as_bytes())
|
||||||
|
.context("unable to decode root node b64")?;
|
||||||
|
|
||||||
|
// check the proto size to be somewhat reasonable before parsing it.
|
||||||
|
if root_entry_proto.len() > 4096 {
|
||||||
|
bail!("rejected, too large root node");
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse the proto
|
||||||
|
let root_entry: snix_castore::proto::Entry = Message::decode(Bytes::from(root_entry_proto))
|
||||||
|
.context("unable to decode root node proto")?;
|
||||||
|
|
||||||
|
let root_node = root_entry
|
||||||
|
.try_into_anonymous_node()
|
||||||
|
.context("root node validation failed")?;
|
||||||
|
|
||||||
|
snix_castore_http::router::gen_router(
|
||||||
|
listen_args,
|
||||||
|
service_addrs,
|
||||||
|
root_node,
|
||||||
|
&index_names,
|
||||||
|
auto_index,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
62
snix/castore-http/src/router.rs
Normal file
62
snix/castore-http/src/router.rs
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
use crate::app_state::{AppConfig, AppState};
|
||||||
|
use crate::routes;
|
||||||
|
|
||||||
|
use snix_castore::utils::ServiceUrlsGrpc;
|
||||||
|
use snix_castore::Node;
|
||||||
|
|
||||||
|
use axum::{routing::get, Router};
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tokio_listener::ListenerAddressLFlag;
|
||||||
|
use tracing::info;
|
||||||
|
|
||||||
|
/// Runs the snix-castore-http server given the specified CLI arguments
|
||||||
|
pub async fn gen_router(
|
||||||
|
listen_args: ListenerAddressLFlag,
|
||||||
|
service_addrs: ServiceUrlsGrpc,
|
||||||
|
root_node: Node,
|
||||||
|
index_names: &[String],
|
||||||
|
auto_index: bool,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
let (blob_service, directory_service) = snix_castore::utils::construct_services(service_addrs)
|
||||||
|
.await
|
||||||
|
.expect("failed to construct services");
|
||||||
|
|
||||||
|
let app_state = Arc::new(AppConfig {
|
||||||
|
blob_service,
|
||||||
|
directory_service,
|
||||||
|
root_node,
|
||||||
|
index_names: index_names.to_vec(),
|
||||||
|
auto_index,
|
||||||
|
});
|
||||||
|
|
||||||
|
let app = app(app_state);
|
||||||
|
|
||||||
|
let listen_address = &listen_args.listen_address.unwrap_or_else(|| {
|
||||||
|
"[::]:9000"
|
||||||
|
.parse()
|
||||||
|
.expect("invalid fallback listen address")
|
||||||
|
});
|
||||||
|
|
||||||
|
let listener = tokio_listener::Listener::bind(
|
||||||
|
listen_address,
|
||||||
|
&Default::default(),
|
||||||
|
&listen_args.listener_options,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
info!(listen_address=%listen_address, "starting daemon");
|
||||||
|
|
||||||
|
tokio_listener::axum07::serve(
|
||||||
|
listener,
|
||||||
|
app.into_make_service_with_connect_info::<tokio_listener::SomeSocketAddrClonable>(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn app(app_state: AppState) -> Router {
|
||||||
|
Router::new()
|
||||||
|
.route("/*path", get(routes::root_node_contents))
|
||||||
|
.route("/", get(routes::root_node_contents))
|
||||||
|
.with_state(app_state)
|
||||||
|
}
|
||||||
484
snix/castore-http/src/routes.rs
Normal file
484
snix/castore-http/src/routes.rs
Normal file
|
|
@ -0,0 +1,484 @@
|
||||||
|
use crate::app_state::AppState;
|
||||||
|
use crate::get_root_node_contents;
|
||||||
|
|
||||||
|
use snix_castore::PathBuf;
|
||||||
|
|
||||||
|
use axum::{
|
||||||
|
extract::{self, State},
|
||||||
|
http::StatusCode,
|
||||||
|
response::Response,
|
||||||
|
};
|
||||||
|
use axum_extra::{headers::Range, TypedHeader};
|
||||||
|
use std::path;
|
||||||
|
use tracing::{debug, instrument};
|
||||||
|
|
||||||
|
#[instrument(level = "trace", ret, skip_all, fields(maybe_path))]
|
||||||
|
pub async fn root_node_contents(
|
||||||
|
maybe_path: Option<extract::Path<String>>,
|
||||||
|
state: State<AppState>,
|
||||||
|
range_header: Option<TypedHeader<Range>>,
|
||||||
|
) -> Result<Response, StatusCode> {
|
||||||
|
let requested_path = maybe_path
|
||||||
|
.map(|extract::Path(path)| PathBuf::from_host_path(path::Path::new(&path), true))
|
||||||
|
.transpose()
|
||||||
|
.map_err(|err| {
|
||||||
|
debug!(%err, "User requested an invalid path");
|
||||||
|
StatusCode::BAD_REQUEST
|
||||||
|
})?;
|
||||||
|
let requested_path = match requested_path.as_ref() {
|
||||||
|
Some(p) => p.as_ref(),
|
||||||
|
None => &PathBuf::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
get_root_node_contents(
|
||||||
|
state.blob_service.clone(),
|
||||||
|
state.directory_service.clone(),
|
||||||
|
path::Path::new("/"),
|
||||||
|
&state.root_node,
|
||||||
|
requested_path,
|
||||||
|
range_header,
|
||||||
|
&state.index_names,
|
||||||
|
state.auto_index,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::{app_state::AppConfig, router::app};
|
||||||
|
|
||||||
|
use snix_castore::{
|
||||||
|
blobservice::{BlobService, MemoryBlobService},
|
||||||
|
directoryservice::{DirectoryService, MemoryDirectoryService},
|
||||||
|
fixtures::{DIRECTORY_COMPLICATED, HELLOWORLD_BLOB_CONTENTS, HELLOWORLD_BLOB_DIGEST},
|
||||||
|
B3Digest, Directory, Node,
|
||||||
|
};
|
||||||
|
|
||||||
|
use axum::http::StatusCode;
|
||||||
|
use std::io::Cursor;
|
||||||
|
use std::sync::{Arc, LazyLock};
|
||||||
|
use tracing_test::traced_test;
|
||||||
|
|
||||||
|
/// Accepts a root node to be served, and returns a [axum_test::TestServer].
|
||||||
|
fn gen_server<S: AsRef<str>>(
|
||||||
|
root_node: Node,
|
||||||
|
index_names: &[S],
|
||||||
|
auto_index: bool,
|
||||||
|
) -> (
|
||||||
|
axum_test::TestServer,
|
||||||
|
impl BlobService,
|
||||||
|
impl DirectoryService,
|
||||||
|
) {
|
||||||
|
let blob_service = Arc::new(MemoryBlobService::default());
|
||||||
|
let directory_service = Arc::new(MemoryDirectoryService::default());
|
||||||
|
|
||||||
|
let app = app(Arc::new(AppConfig {
|
||||||
|
blob_service: blob_service.clone(),
|
||||||
|
directory_service: directory_service.clone(),
|
||||||
|
root_node,
|
||||||
|
index_names: index_names
|
||||||
|
.iter()
|
||||||
|
.map(|index| index.as_ref().to_string())
|
||||||
|
.collect(),
|
||||||
|
auto_index,
|
||||||
|
}));
|
||||||
|
|
||||||
|
(
|
||||||
|
axum_test::TestServer::new(app).unwrap(),
|
||||||
|
blob_service,
|
||||||
|
directory_service,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const INDEX_HTML_BLOB_CONTENTS: &[u8] =
|
||||||
|
b"<!DOCTYPE html><html><body>Hello World!</body></html>";
|
||||||
|
pub static INDEX_HTML_BLOB_DIGEST: LazyLock<B3Digest> =
|
||||||
|
LazyLock::new(|| blake3::hash(INDEX_HTML_BLOB_CONTENTS).as_bytes().into());
|
||||||
|
|
||||||
|
pub static DIRECTORY_NESTED_WITH_SYMLINK: LazyLock<Directory> = LazyLock::new(|| {
|
||||||
|
Directory::try_from_iter([
|
||||||
|
(
|
||||||
|
"nested".try_into().unwrap(),
|
||||||
|
Node::Directory {
|
||||||
|
digest: DIRECTORY_WITH_SYMLINK.digest(),
|
||||||
|
size: DIRECTORY_WITH_SYMLINK.size(),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"index.htm".try_into().unwrap(),
|
||||||
|
Node::File {
|
||||||
|
digest: INDEX_HTML_BLOB_DIGEST.clone(),
|
||||||
|
size: INDEX_HTML_BLOB_CONTENTS.len() as u64,
|
||||||
|
executable: false,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"out_of_base_path_symlink".try_into().unwrap(),
|
||||||
|
Node::Symlink {
|
||||||
|
target: "../index.htm".try_into().unwrap(),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
])
|
||||||
|
.unwrap()
|
||||||
|
});
|
||||||
|
|
||||||
|
pub static DIRECTORY_WITH_SYMLINK: LazyLock<Directory> = LazyLock::new(|| {
|
||||||
|
Directory::try_from_iter([
|
||||||
|
(
|
||||||
|
"index.html".try_into().unwrap(),
|
||||||
|
Node::File {
|
||||||
|
digest: INDEX_HTML_BLOB_DIGEST.clone(),
|
||||||
|
size: INDEX_HTML_BLOB_CONTENTS.len() as u64,
|
||||||
|
executable: false,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"dot".try_into().unwrap(),
|
||||||
|
Node::Symlink {
|
||||||
|
target: ".".try_into().unwrap(),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"symlink".try_into().unwrap(),
|
||||||
|
Node::Symlink {
|
||||||
|
target: "index.html".try_into().unwrap(),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"dot_symlink".try_into().unwrap(),
|
||||||
|
Node::Symlink {
|
||||||
|
target: "./index.html".try_into().unwrap(),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"dotdot_symlink".try_into().unwrap(),
|
||||||
|
Node::Symlink {
|
||||||
|
target: "../index.htm".try_into().unwrap(),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"dotdot_same_symlink".try_into().unwrap(),
|
||||||
|
Node::Symlink {
|
||||||
|
target: "../nested/index.html".try_into().unwrap(),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
])
|
||||||
|
.unwrap()
|
||||||
|
});
|
||||||
|
|
||||||
|
#[traced_test]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_lists_directory_contents_if_auto_index_enabled() {
|
||||||
|
let root_node = Node::Directory {
|
||||||
|
digest: DIRECTORY_COMPLICATED.digest(),
|
||||||
|
size: DIRECTORY_COMPLICATED.size(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// No index but auto-index is enabled
|
||||||
|
let (server, _blob_service, directory_service) = gen_server::<&str>(root_node, &[], true);
|
||||||
|
|
||||||
|
directory_service
|
||||||
|
.put(DIRECTORY_COMPLICATED.clone())
|
||||||
|
.await
|
||||||
|
.expect("Failed to insert directory");
|
||||||
|
|
||||||
|
server
|
||||||
|
.get("/")
|
||||||
|
.expect_success()
|
||||||
|
.await
|
||||||
|
.assert_text_contains("<html><body><li><a href=\"/.keep\">.keep</a></li><li><a href=\"/aa\">aa</a></li><li><a href=\"/keep\">keep</a></li></body></html>");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[traced_test]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_lists_directory_contents_if_auto_index_enabled_for_nested_dir() {
|
||||||
|
let root_node = Node::Directory {
|
||||||
|
digest: DIRECTORY_NESTED_WITH_SYMLINK.digest(),
|
||||||
|
size: DIRECTORY_NESTED_WITH_SYMLINK.size(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// No index but auto-index is enabled
|
||||||
|
let (server, _blob_service, directory_service) = gen_server::<&str>(root_node, &[], true);
|
||||||
|
let mut directory_service_handle = directory_service.put_multiple_start();
|
||||||
|
directory_service_handle
|
||||||
|
.put(DIRECTORY_WITH_SYMLINK.clone())
|
||||||
|
.await
|
||||||
|
.expect("Failed to insert directory");
|
||||||
|
directory_service_handle
|
||||||
|
.put(DIRECTORY_NESTED_WITH_SYMLINK.clone())
|
||||||
|
.await
|
||||||
|
.expect("Failed to insert directory");
|
||||||
|
directory_service_handle
|
||||||
|
.close()
|
||||||
|
.await
|
||||||
|
.expect("Failed to close handle");
|
||||||
|
|
||||||
|
server
|
||||||
|
.get("/nested")
|
||||||
|
.expect_success()
|
||||||
|
.await
|
||||||
|
.assert_text_contains("<!DOCTYPE html><html><body><li><a href=\"/nested/dot\">dot</a></li><li><a href=\"/nested/dot_symlink\">dot_symlink</a></li><li><a href=\"/nested/dotdot_same_symlink\">dotdot_same_symlink</a></li><li><a href=\"/nested/dotdot_symlink\">dotdot_symlink</a></li><li><a href=\"/nested/index.html\">index.html</a></li><li><a href=\"/nested/symlink\">symlink</a></li></body></html>")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[traced_test]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_responds_index_file_if_configured() {
|
||||||
|
let root_node = Node::Directory {
|
||||||
|
digest: DIRECTORY_COMPLICATED.digest(),
|
||||||
|
size: DIRECTORY_COMPLICATED.size(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// .keep is a index file in this test scenario, auto-index is off
|
||||||
|
let (server, blob_service, directory_service) =
|
||||||
|
gen_server::<&str>(root_node, &[".keep"], false);
|
||||||
|
|
||||||
|
directory_service
|
||||||
|
.put(DIRECTORY_COMPLICATED.clone())
|
||||||
|
.await
|
||||||
|
.expect("Failed to insert directory");
|
||||||
|
|
||||||
|
let mut blob_writer = blob_service.open_write().await;
|
||||||
|
tokio::io::copy(&mut Cursor::new(vec![]), &mut blob_writer)
|
||||||
|
.await
|
||||||
|
.expect("Failed to copy file to BlobWriter");
|
||||||
|
blob_writer
|
||||||
|
.close()
|
||||||
|
.await
|
||||||
|
.expect("Failed to close the BlobWriter");
|
||||||
|
|
||||||
|
server.get("/").expect_success().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[traced_test]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_responds_index_file_if_configured_in_nested_dir() {
|
||||||
|
let root_node = Node::Directory {
|
||||||
|
digest: DIRECTORY_NESTED_WITH_SYMLINK.digest(),
|
||||||
|
size: DIRECTORY_NESTED_WITH_SYMLINK.size(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// .keep is a index file in this test scenario, auto-index is off
|
||||||
|
let (server, blob_service, directory_service) =
|
||||||
|
gen_server::<&str>(root_node, &["index.html"], false);
|
||||||
|
|
||||||
|
let mut directory_service_handle = directory_service.put_multiple_start();
|
||||||
|
directory_service_handle
|
||||||
|
.put(DIRECTORY_WITH_SYMLINK.clone())
|
||||||
|
.await
|
||||||
|
.expect("Failed to insert directory");
|
||||||
|
directory_service_handle
|
||||||
|
.put(DIRECTORY_NESTED_WITH_SYMLINK.clone())
|
||||||
|
.await
|
||||||
|
.expect("Failed to insert directory");
|
||||||
|
directory_service_handle
|
||||||
|
.close()
|
||||||
|
.await
|
||||||
|
.expect("Failed to close handle");
|
||||||
|
|
||||||
|
let mut blob_writer = blob_service.open_write().await;
|
||||||
|
tokio::io::copy(&mut Cursor::new(INDEX_HTML_BLOB_CONTENTS), &mut blob_writer)
|
||||||
|
.await
|
||||||
|
.expect("Failed to copy file to BlobWriter");
|
||||||
|
let digest = blob_writer
|
||||||
|
.close()
|
||||||
|
.await
|
||||||
|
.expect("Failed to close the BlobWriter");
|
||||||
|
assert_eq!(digest, *INDEX_HTML_BLOB_DIGEST);
|
||||||
|
|
||||||
|
server.get("/nested").expect_success().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[traced_test]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_responds_forbidden_if_no_index_configured_nor_auto_index_enabled() {
|
||||||
|
let root_node = Node::Directory {
|
||||||
|
digest: DIRECTORY_COMPLICATED.digest(),
|
||||||
|
size: DIRECTORY_COMPLICATED.size(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// no index configured and auto-index disabled
|
||||||
|
let (server, _blob_service, directory_service) = gen_server::<&str>(root_node, &[], false);
|
||||||
|
|
||||||
|
directory_service
|
||||||
|
.put(DIRECTORY_COMPLICATED.clone())
|
||||||
|
.await
|
||||||
|
.expect("Failed to insert directory");
|
||||||
|
|
||||||
|
let response = server.get("/").expect_failure().await;
|
||||||
|
response.assert_status(StatusCode::FORBIDDEN);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[traced_test]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_responds_file() {
|
||||||
|
let root_node = Node::Directory {
|
||||||
|
digest: DIRECTORY_COMPLICATED.digest(),
|
||||||
|
size: DIRECTORY_COMPLICATED.size(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let (server, blob_service, directory_service) = gen_server::<&str>(root_node, &[], false);
|
||||||
|
|
||||||
|
directory_service
|
||||||
|
.put(DIRECTORY_COMPLICATED.clone())
|
||||||
|
.await
|
||||||
|
.expect("Failed to insert directory");
|
||||||
|
|
||||||
|
let mut blob_writer = blob_service.open_write().await;
|
||||||
|
tokio::io::copy(&mut Cursor::new(vec![]), &mut blob_writer)
|
||||||
|
.await
|
||||||
|
.expect("Failed to copy file to BlobWriter");
|
||||||
|
blob_writer
|
||||||
|
.close()
|
||||||
|
.await
|
||||||
|
.expect("Failed to close the BlobWriter");
|
||||||
|
|
||||||
|
server.get("/.keep").expect_success().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[traced_test]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_responds_file_and_correct_content_type() {
|
||||||
|
let root_node = Node::Directory {
|
||||||
|
digest: DIRECTORY_NESTED_WITH_SYMLINK.digest(),
|
||||||
|
size: DIRECTORY_NESTED_WITH_SYMLINK.size(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let (server, blob_service, directory_service) = gen_server::<&str>(root_node, &[], false);
|
||||||
|
|
||||||
|
let mut directory_service_handle = directory_service.put_multiple_start();
|
||||||
|
directory_service_handle
|
||||||
|
.put(DIRECTORY_WITH_SYMLINK.clone())
|
||||||
|
.await
|
||||||
|
.expect("Failed to insert directory");
|
||||||
|
directory_service_handle
|
||||||
|
.put(DIRECTORY_NESTED_WITH_SYMLINK.clone())
|
||||||
|
.await
|
||||||
|
.expect("Failed to insert directory");
|
||||||
|
directory_service_handle
|
||||||
|
.close()
|
||||||
|
.await
|
||||||
|
.expect("Failed to close handle");
|
||||||
|
|
||||||
|
let mut blob_writer = blob_service.open_write().await;
|
||||||
|
tokio::io::copy(&mut Cursor::new(INDEX_HTML_BLOB_CONTENTS), &mut blob_writer)
|
||||||
|
.await
|
||||||
|
.expect("Failed to copy file to BlobWriter");
|
||||||
|
let digest = blob_writer
|
||||||
|
.close()
|
||||||
|
.await
|
||||||
|
.expect("Failed to close the BlobWriter");
|
||||||
|
assert_eq!(digest, *INDEX_HTML_BLOB_DIGEST);
|
||||||
|
|
||||||
|
let response = server.get("/nested/index.html").expect_success().await;
|
||||||
|
response.assert_header("Content-Type", "text/html");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[traced_test]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_responds_redirect_if_symlink() {
|
||||||
|
let root_node = Node::Directory {
|
||||||
|
digest: DIRECTORY_COMPLICATED.digest(),
|
||||||
|
size: DIRECTORY_COMPLICATED.size(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let (server, _blob_service, directory_service) = gen_server::<&str>(root_node, &[], false);
|
||||||
|
|
||||||
|
directory_service
|
||||||
|
.put(DIRECTORY_COMPLICATED.clone())
|
||||||
|
.await
|
||||||
|
.expect("Failed to insert directory");
|
||||||
|
|
||||||
|
let response = server.get("/aa").await;
|
||||||
|
response.assert_status(StatusCode::TEMPORARY_REDIRECT);
|
||||||
|
response.assert_header("Location", "/nix/store/somewhereelse");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[traced_test]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_responds_redirect_with_normalized_path_if_symlink() {
|
||||||
|
let root_node = Node::Directory {
|
||||||
|
digest: DIRECTORY_NESTED_WITH_SYMLINK.digest(),
|
||||||
|
size: DIRECTORY_NESTED_WITH_SYMLINK.size(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let (server, _blob_service, directory_service) = gen_server::<&str>(root_node, &[], false);
|
||||||
|
|
||||||
|
let mut directory_service_handle = directory_service.put_multiple_start();
|
||||||
|
directory_service_handle
|
||||||
|
.put(DIRECTORY_WITH_SYMLINK.clone())
|
||||||
|
.await
|
||||||
|
.expect("Failed to insert directory");
|
||||||
|
directory_service_handle
|
||||||
|
.put(DIRECTORY_NESTED_WITH_SYMLINK.clone())
|
||||||
|
.await
|
||||||
|
.expect("Failed to insert directory");
|
||||||
|
directory_service_handle
|
||||||
|
.close()
|
||||||
|
.await
|
||||||
|
.expect("Failed to close handle");
|
||||||
|
|
||||||
|
let response = server.get("/nested/symlink").await;
|
||||||
|
response.assert_status(StatusCode::TEMPORARY_REDIRECT);
|
||||||
|
response.assert_header("Location", "/nested/index.html");
|
||||||
|
|
||||||
|
let response = server.get("/nested/dot_symlink").await;
|
||||||
|
response.assert_status(StatusCode::TEMPORARY_REDIRECT);
|
||||||
|
response.assert_header("Location", "/nested/index.html");
|
||||||
|
|
||||||
|
let response = server.get("/nested/dotdot_symlink").await;
|
||||||
|
response.assert_status(StatusCode::TEMPORARY_REDIRECT);
|
||||||
|
response.assert_header("Location", "/index.htm");
|
||||||
|
|
||||||
|
let response = server.get("/out_of_base_path_symlink").await;
|
||||||
|
response.assert_status(StatusCode::TEMPORARY_REDIRECT);
|
||||||
|
response.assert_header("Location", "/index.htm");
|
||||||
|
|
||||||
|
let response = server.get("/nested/dot").expect_failure().await;
|
||||||
|
response.assert_status(StatusCode::INTERNAL_SERVER_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[traced_test]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_returns_bad_request_if_not_valid_path() {
|
||||||
|
let root_node = Node::Directory {
|
||||||
|
digest: DIRECTORY_COMPLICATED.digest(),
|
||||||
|
size: DIRECTORY_COMPLICATED.size(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let (server, _blob_service, _directory_service) = gen_server::<&str>(root_node, &[], false);
|
||||||
|
|
||||||
|
// request an invalid path
|
||||||
|
let response = server.get("//aa").expect_failure().await;
|
||||||
|
response.assert_status(StatusCode::BAD_REQUEST);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[traced_test]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_returns_bad_request_if_root_node_is_file_and_path_requested() {
|
||||||
|
let root_node = Node::File {
|
||||||
|
digest: HELLOWORLD_BLOB_DIGEST.clone(),
|
||||||
|
size: HELLOWORLD_BLOB_CONTENTS.len() as u64,
|
||||||
|
executable: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
let (server, _blob_service, _directory_service) = gen_server::<&str>(root_node, &[], false);
|
||||||
|
|
||||||
|
// request a path while the root node is a file
|
||||||
|
let response = server.get("/some-path").expect_failure().await;
|
||||||
|
response.assert_status(StatusCode::BAD_REQUEST);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[traced_test]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_returns_bad_request_if_root_node_is_symlink_and_path_requested() {
|
||||||
|
let root_node = Node::Symlink {
|
||||||
|
target: "/nix/store/somewhereelse".try_into().unwrap(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let (server, _blob_service, _directory_service) = gen_server::<&str>(root_node, &[], false);
|
||||||
|
|
||||||
|
// request a path while the root node is a symlink
|
||||||
|
let response = server.get("/some-path").expect_failure().await;
|
||||||
|
response.assert_status(StatusCode::BAD_REQUEST);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -92,6 +92,10 @@ in
|
||||||
nativeBuildInputs = [ pkgs.protobuf ];
|
nativeBuildInputs = [ pkgs.protobuf ];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
snix-castore-htp = prev: {
|
||||||
|
src = filterRustCrateSrc { root = prev.src.origSrc; };
|
||||||
|
};
|
||||||
|
|
||||||
snix-cli = prev: {
|
snix-cli = prev: {
|
||||||
src = filterRustCrateSrc rec {
|
src = filterRustCrateSrc rec {
|
||||||
root = prev.src.origSrc;
|
root = prev.src.origSrc;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue