Compare commits

...

1 Commits

Author SHA1 Message Date
4598993dfe prototype copyparty reverse proxy config 2026-08-17 12:45:40 -04:00
4 changed files with 139 additions and 1 deletions

View File

@@ -43,6 +43,10 @@ in
enable = true;
inherit (cfg) accountName stateDir;
passwordFile = "/run/secrets/${cfg.passwordSecret}";
reverseProxy = {
enable = cfg.reverseProxy.enable;
inherit (cfg.reverseProxy) trustedSources;
};
volumes = lib.mapAttrs (_urlPath: volume: {
inherit (volume) path access flags;
}) cfg.volumes;
@@ -77,6 +81,12 @@ in
description = "Host path bind-mounted as Copyparty's state directory.";
};
reverseProxy.trustedSources = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
description = "IP ranges that may supply Copyparty's forwarded client IP header.";
};
volumes = lib.mkOption {
type = lib.types.attrsOf volumeType;
default = { };

View File

@@ -101,6 +101,7 @@
enable = true;
host = "files.alexion.dev";
backend = "http://10.23.20.126:3923";
trustedSources = [ "10.23.20.109/32" ];
};
volumes = {
"/inbox" = {

View File

@@ -48,6 +48,14 @@ let
''}
'';
renderTrustedSource = source: " xff-src: ${source}";
renderedReverseProxy = lib.optionalString cfg.reverseProxy.enable ''
rproxy: 1
xff-hdr: x-forwarded-for
${lib.concatStringsSep "\n" (map renderTrustedSource cfg.reverseProxy.trustedSources)}
'';
renderedVolumes = lib.concatStringsSep "\n" (lib.mapAttrsToList renderVolume cfg.volumes);
configScript = pkgs.writeShellScript "copyparty-config" ''
@@ -68,7 +76,7 @@ let
ses-db: ${cfg.stateDir}/sessions.db
chpw-db: ${cfg.stateDir}/chpw.json
shr-db: ${cfg.stateDir}/shares.db
${renderedReverseProxy}
[accounts]
${cfg.accountName}: $password
@@ -128,6 +136,16 @@ in
default = { };
description = "Copyparty volumes keyed by their URL path.";
};
reverseProxy = {
enable = lib.mkEnableOption "reverse-proxy trust settings for Copyparty";
trustedSources = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
description = "IP ranges that may supply Copyparty's forwarded client IP header.";
};
};
};
config = lib.mkIf cfg.enable {
@@ -136,6 +154,10 @@ in
assertion = cfg.volumes != { };
message = "modules.services.copyparty.volumes must declare at least one served volume.";
}
{
assertion = !cfg.reverseProxy.enable || cfg.reverseProxy.trustedSources != [ ];
message = "modules.services.copyparty.reverseProxy.trustedSources must name at least one trusted proxy source when reverse-proxy support is enabled.";
}
];
users.users.${cfg.user} = {

View File

@@ -0,0 +1,105 @@
#!/usr/bin/env bash
set -euo pipefail
bold=$'\033[1m'
reset=$'\033[0m'
tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT
mkdir -p "$tmp/state" "$tmp/root"
printf '%sQuestion%s\n' "$bold" "$reset"
printf 'Does the proposed copyparty reverse-proxy config parse and make a proxied HTTPS-origin login-shaped request pass the CORS gate?\n\n'
printf '%sNix-generated config fragment%s\n' "$bold" "$reset"
config_script=$(nix eval --raw '.#nixosConfigurations.pikachu.config.containers.copyparty.config.systemd.services.copyparty.serviceConfig.ExecStartPre')
awk '/\[global\]/{show=1} /\[accounts\]/{show=0} show {print}' "$config_script" | grep -E 'rproxy|xff-hdr|xff-src' || true
pkg=$(nix eval --raw '.#nixosConfigurations.pikachu.config.containers.copyparty.config.modules.services.copyparty.package.outPath')
make_conf() {
local file=$1 port=$2 rproxy=$3
cat > "$file" <<EOF
[global]
i: 127.0.0.1
p: $port
usernames
http-only
no-crt
no-thumb
no-snap
no-rescan
ses-db: $tmp/state/sessions-$port.db
chpw-db: $tmp/state/chpw-$port.json
shr-db: $tmp/state/shares-$port.db
$rproxy
[accounts]
alexion: prototype-password
[/]
$tmp/root
accs:
A: alexion
EOF
}
start_server() {
local conf=$1 log=$2
"$pkg/bin/copyparty" -c "$conf" >"$log" 2>&1 &
local pid=$!
for _ in {1..50}; do
if grep -q 'listening' "$log"; then
printf '%s' "$pid"
return 0
fi
sleep 0.1
done
cat "$log"
kill "$pid" 2>/dev/null || true
return 1
}
request() {
local label=$1 port=$2 host=$3 proto=${4:-}
local headers="$tmp/$label.headers"
local body="$tmp/$label.body"
local args=(
-sS
-D "$headers"
-o "$body"
-X POST
-H "Host: $host"
-H 'Origin: https://files.alexion.dev'
-H 'X-Forwarded-For: 10.23.20.20'
)
if [[ -n "$proto" ]]; then
args+=(-H "X-Forwarded-Proto: $proto")
fi
curl "${args[@]}" "http://127.0.0.1:$port/?pw=alexion:prototype-password" >/dev/null || true
printf '%-24s %s %s\n' "$label" "$(head -1 "$headers" | tr -d '\r')" "$(head -1 "$body")"
}
rproxy_config=' rproxy: 1
xff-hdr: x-forwarded-for
xff-src: 127.0.0.1/32'
make_conf "$tmp/rproxy.conf" 39234 "$rproxy_config"
pid=$(start_server "$tmp/rproxy.conf" "$tmp/rproxy.log")
printf '\n%sCORS gate probes%s\n' "$bold" "$reset"
request with-forwarded-https 39234 files.alexion.dev https
request missing-forwarded-proto 39234 files.alexion.dev
request wrong-forwarded-host 39234 '10.23.20.126:3923' https
kill "$pid" 2>/dev/null || true
wait "$pid" 2>/dev/null || true
printf '\n%sRelevant server log%s\n' "$bold" "$reset"
grep -E 'cors-reject|POST|http405|got proxied request|untrusted source|listening' "$tmp/rproxy.log" || true
printf '\n%sNginx proxy headers%s\n' "$bold" "$reset"
toplevel=$(nix build --print-out-paths '.#nixosConfigurations.pikachu.config.containers.reverse-proxy.config.system.build.toplevel' --no-link)
nginx_conf=$(readlink -f "$toplevel/etc/nginx/nginx.conf")
include_path=$(awk '/server_name files\.alexion\.dev;/{in_server=1} in_server && /include .*recommended-proxy_set_header/{print $2; exit}' "$nginx_conf" | tr -d ';')
cat "$include_path"