Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implemented rewrite to real path in dump_path #315

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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 change: 1 addition & 0 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
t00-simple = import ./tests/t00-simple.nix testArgs;
t01-signing = import ./tests/t01-signing.nix testArgs;
t02-varnish = import ./tests/t02-varnish.nix testArgs;
t03-chroot = import ./tests/t03-chroot.nix testArgs;
} // {
clippy = config.packages.harmonia.override ({
enableClippy = true;
Expand Down
7 changes: 6 additions & 1 deletion harmonia/src/config.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use std::fs::read_to_string;

use crate::store::Store;
use anyhow::{Context, Result};
use base64::{engine::general_purpose, Engine};
use serde::Deserialize;
Expand Down Expand Up @@ -33,8 +34,11 @@ pub(crate) struct Config {
pub(crate) priority: usize,
#[serde(default)]
pub(crate) sign_key_path: Option<String>,
#[serde(default)]

#[serde(skip)]
pub(crate) secret_key: Option<String>,
#[serde(skip)]
pub(crate) store: Store,
Mic92 marked this conversation as resolved.
Show resolved Hide resolved
}

fn get_secret_key(sign_key_path: Option<&str>) -> Result<Option<String>> {
Expand Down Expand Up @@ -65,5 +69,6 @@ pub(crate) fn load() -> Result<Config> {
)
.with_context(|| format!("Couldn't parse config file '{settings_file}'"))?;
settings.secret_key = get_secret_key(settings.sign_key_path.as_deref())?;
settings.store = Store::new();
Ok(settings)
}
1 change: 1 addition & 0 deletions harmonia/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ mod narinfo;
mod narlist;
mod root;
mod serve;
mod store;
mod version;

fn nixhash(hash: &str) -> Option<String> {
Expand Down
24 changes: 16 additions & 8 deletions harmonia/src/nar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ use sync::mpsc::Sender;
use tokio::fs::File;
use tokio::io::AsyncReadExt;

use crate::config::Config;
use crate::{cache_control_max_age_1y, some_or_404};
use std::ffi::{OsStr, OsString};
use tokio::{sync, task};
Expand Down Expand Up @@ -269,9 +270,9 @@ async fn dump_symlink(frame: &Frame, tx: &Sender<Result<Bytes, ThreadSafeError>>
Ok(())
}

async fn dump_path(path: &Path, tx: &Sender<Result<Bytes, ThreadSafeError>>) -> Result<()> {
async fn dump_path(path: PathBuf, tx: &Sender<Result<Bytes, ThreadSafeError>>) -> Result<()> {
write_byte_slices(tx, &[b"nix-archive-1"]).await?;
let mut stack = vec![Frame::new(path.to_owned()).await?];
let mut stack = vec![Frame::new(path).await?];

while let Some(frame) = stack.last_mut() {
let file_type = frame.metadata.file_type();
Expand Down Expand Up @@ -325,6 +326,7 @@ pub(crate) async fn get(
path: web::Path<PathParams>,
req: HttpRequest,
q: web::Query<NarRequest>,
settings: web::Data<Config>,
) -> Result<HttpResponse, Box<dyn Error>> {
// Extract the narhash from the query parameter, and bail out if it's missing or invalid.
let narhash = some_or_404!(Some(path.narhash.as_str()));
Expand Down Expand Up @@ -386,7 +388,11 @@ pub(crate) async fn get(

let (tx2, mut rx2) = tokio::sync::mpsc::channel::<Result<Bytes, ThreadSafeError>>(1000);
task::spawn(async move {
let err = dump_path(Path::new(&store_path), &tx2).await;
// If Nix is set to a non-root store, physical store paths will differ from
// logical paths. Below we check if that is the case, and rewrite to physical
// before dumping.

let err = dump_path(settings.store.get_real_path(&store_path), &tx2).await;
if let Err(err) = err {
log::error!("Error dumping path {}: {:?}", store_path, err);
}
Expand Down Expand Up @@ -426,7 +432,7 @@ pub(crate) async fn get(
});
} else {
task::spawn(async move {
let err = dump_path(Path::new(&store_path), &tx).await;
let err = dump_path(settings.store.get_real_path(&store_path), &tx).await;
if let Err(err) = err {
log::error!("Error dumping path {}: {:?}", store_path, err);
}
Expand All @@ -443,12 +449,14 @@ pub(crate) async fn get(
#[cfg(test)]
mod test {
use super::*;
use std::{path::PathBuf, process::Command};
use crate::store::Store;
use std::process::Command;

async fn dump_to_vec(path: PathBuf) -> Result<Vec<u8>> {
async fn dump_to_vec(path: String) -> Result<Vec<u8>> {
let store = Store::new();
let (tx, mut rx) = tokio::sync::mpsc::channel::<Result<Bytes, ThreadSafeError>>(1000);
task::spawn(async move {
let e = dump_path(&path, &tx).await;
let e = dump_path(store.get_real_path(&path), &tx).await;
if let Err(e) = e {
eprintln!("Error dumping path: {:?}", e);
}
Expand Down Expand Up @@ -521,7 +529,7 @@ mod test {

std::os::unix::fs::symlink("sometarget", dir.join("symlink"))?;

let nar_dump = dump_to_vec(dir.to_path_buf()).await?;
let nar_dump = dump_to_vec(dir.to_str().unwrap().to_owned()).await?;
let res = Command::new("nix-store")
.arg("--dump")
.arg(dir)
Expand Down
26 changes: 17 additions & 9 deletions harmonia/src/serve.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ use askama_escape::{escape as escape_html_entity, Html};
use percent_encoding::{utf8_percent_encode, CONTROLS};
use std::fmt::Write;

use crate::{nixhash, some_or_404, ServerResult, BOOTSTRAP_SOURCE, CARGO_NAME, CARGO_VERSION};
use crate::{
config::Config, nixhash, some_or_404, ServerResult, BOOTSTRAP_SOURCE, CARGO_NAME, CARGO_VERSION,
};

/// Returns percent encoded file URL path.
macro_rules! encode_file_url {
Expand Down Expand Up @@ -46,10 +48,12 @@ fn file_size(bytes: u64) -> String {
}
}

pub(crate) fn directory_listing(url_prefix: &Path, fs_path: &Path) -> ServerResult {
let path_without_store = fs_path
.strip_prefix(libnixstore::get_store_dir())
.unwrap_or(fs_path);
pub(crate) fn directory_listing(
url_prefix: &Path,
fs_path: &Path,
real_store: &str,
) -> ServerResult {
let path_without_store = fs_path.strip_prefix(real_store).unwrap_or(fs_path);
let index_of = format!(
"Index of {}",
escape_html_entity(&path_without_store.to_string_lossy(), Html)
Expand Down Expand Up @@ -123,11 +127,15 @@ pub(crate) fn directory_listing(url_prefix: &Path, fs_path: &Path) -> ServerResu
.body(html))
}

pub(crate) async fn get(path: web::Path<(String, PathBuf)>, req: HttpRequest) -> ServerResult {
pub(crate) async fn get(
path: web::Path<(String, PathBuf)>,
req: HttpRequest,
settings: web::Data<Config>,
) -> ServerResult {
let (hash, dir) = path.into_inner();
let dir = dir.strip_prefix("/").unwrap_or(&dir);

let store_path = PathBuf::from(some_or_404!(nixhash(&hash)));
let store_path = settings.store.get_real_path(&some_or_404!(nixhash(&hash)));
let full_path = if dir == Path::new("") {
store_path.clone()
} else {
Expand All @@ -137,7 +145,7 @@ pub(crate) async fn get(path: web::Path<(String, PathBuf)>, req: HttpRequest) ->
.canonicalize()
.with_context(|| format!("cannot resolve nix store path: {}", full_path.display()))?;

if !full_path.starts_with(libnixstore::get_store_dir()) {
if !full_path.starts_with(settings.store.real_store()) {
return Ok(HttpResponse::NotFound().finish());
}

Expand All @@ -158,7 +166,7 @@ pub(crate) async fn get(path: web::Path<(String, PathBuf)>, req: HttpRequest) ->
} else {
url_prefix.join(dir)
};
directory_listing(&url_prefix, &full_path)
directory_listing(&url_prefix, &full_path, settings.store.real_store())
} else {
Ok(NamedFile::open_async(&full_path)
.await
Expand Down
42 changes: 42 additions & 0 deletions harmonia/src/store.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
use std::path::PathBuf;

#[derive(Default, Debug)]
pub struct Store {
virtual_store: String,
real_store: Option<String>,
}

impl Store {
pub fn new() -> Self {
let real_store = libnixstore::get_real_store_dir();
let virtual_store = libnixstore::get_store_dir();

if virtual_store == real_store {
return Self {
virtual_store,
real_store: None,
};
}
Self {
virtual_store,
real_store: Some(real_store),
}
}
pub fn get_real_path(&self, virtual_path: &str) -> PathBuf {
if let Some(real_store) = &self.real_store {
if virtual_path.starts_with(&self.virtual_store) {
return PathBuf::from(format!(
"{}{}",
real_store,
&virtual_path[self.virtual_store.len()..]
));
}
}
PathBuf::from(virtual_path)
}
pub fn real_store(&self) -> &str {
self.real_store
.as_ref()
.map_or(&self.virtual_store, |s| s.as_str())
}
}
1 change: 1 addition & 0 deletions libnixstore/include/nix.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ rust::String sign_string(rust::Str secret_key, rust::Str msg);
bool check_signature(rust::Str public_key, rust::Str sig, rust::Str msg);
InternalDrv derivation_from_path(rust::Str drv_path);
rust::String get_store_dir();
rust::String get_real_store_dir();
rust::String get_build_log(rust::Str derivation_path);
rust::String get_nar_list(rust::Str store_path);

Expand Down
8 changes: 8 additions & 0 deletions libnixstore/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ mod ffi {
fn check_signature(public_key: &str, sig: &str, msg: &str) -> Result<bool>;
fn derivation_from_path(drv_path: &str) -> Result<InternalDrv>;
fn get_store_dir() -> String;
fn get_real_store_dir() -> String;
fn get_build_log(derivation_path: &str) -> Result<String>;
fn get_nar_list(store_path: &str) -> Result<String>;
}
Expand Down Expand Up @@ -218,6 +219,13 @@ pub fn get_store_dir() -> String {
ffi::get_store_dir()
}

#[inline]
#[must_use]
/// Returns the physical path to the nix store.
pub fn get_real_store_dir() -> String {
ffi::get_real_store_dir()
}

#[inline]
#[must_use]
/// Return the build log of the specified store path, if available, or null otherwise.
Expand Down
11 changes: 11 additions & 0 deletions libnixstore/src/nix.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#include <nix/globals.hh>
#include <nix/shared.hh>
#include <nix/store-api.hh>
#include <nix/local-fs-store.hh>
#include <nix/log-store.hh>
#include <nix/content-address.hh>
#include <nix/util.hh>
Expand Down Expand Up @@ -192,6 +193,16 @@ rust::String get_store_dir() {
return nix::settings.nixStore;
}

rust::String get_real_store_dir() {
auto store = get_store();
auto *fsstore = dynamic_cast<nix::LocalFSStore *>(&(*store));

if (fsstore != nullptr)
return fsstore->getRealStoreDir();
else
return get_store_dir();
}

rust::String get_build_log(rust::Str derivation_path) {
auto store = get_store();
auto path = store->parseStorePath(STRING_VIEW(derivation_path));
Expand Down
66 changes: 66 additions & 0 deletions tests/t03-chroot.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
(import ./lib.nix)
({ ... }:
{
name = "t03-chroot";

nodes = {
harmonia = { pkgs, ... }:
{
imports = [ ../module.nix ];

services.harmonia-dev.enable = true;
# We need to manipulate the target store first
systemd.services."harmonia-dev".wantedBy = pkgs.lib.mkForce [ ];

networking.firewall.allowedTCPPorts = [ 5000 ];
nix.settings.store = "/guest?read-only=1";
nix.extraOptions = ''
experimental-features = nix-command read-only-local-store
'';
};

client01 = { lib, ... }:
{
nix.settings.require-sigs = false;
nix.settings.substituters = lib.mkForce [ "http://harmonia:5000" ];
nix.extraOptions = ''
experimental-features = nix-command
'';
};
};

testScript =
''
import json
start_all()

harmonia.wait_until_succeeds("echo 'test contents' > /my-file")
harmonia.wait_until_succeeds("mkdir /my-dir && cp /my-file /my-dir/")
f = harmonia.wait_until_succeeds("nix store --store /guest add-file /my-file")
d = harmonia.wait_until_succeeds("nix store --store /guest add-path /my-dir")
harmonia.systemctl("start harmonia-dev.service")
harmonia.wait_for_unit("harmonia-dev.service")

client01.wait_until_succeeds("curl -f http://harmonia:5000/version")
client01.succeed("curl -f http://harmonia:5000/nix-cache-info")

client01.wait_until_succeeds(f"nix copy --from http://harmonia:5000/ {f}")
client01.succeed(f"grep 'test contents' {f}")

dhash = d.removeprefix("/nix/store/")
dhash = dhash[:dhash.find('-')]
out = client01.wait_until_succeeds(f"curl -v http://harmonia:5000/{dhash}.ls")
data = json.loads(out)
print(out)
assert data["version"] == 1, "version is not correct"
assert data["root"]["entries"]["my-file"]["type"] == "regular", "expect my-file file in listing"

out = client01.wait_until_succeeds(f"curl -v http://harmonia:5000/serve/{dhash}/")
print(out)
assert "my-file" in out, "my-file not in listing"

out = client01.wait_until_succeeds(f"curl -v http://harmonia:5000/serve/{dhash}/my-file").strip()
print(out)
assert "test contents" == out, f"expected 'test contents', got '{out}'"
'';
})