feat(web): provide a directory listing via s3-revproxy
Thanks to Jade Lovelace who built all this machinery for Lix initially. Signed-off-by: Raito Bezarius <masterancpp@gmail.com>
This commit is contained in:
parent
9063138156
commit
c38e9b482f
|
@ -10,7 +10,7 @@
|
||||||
networking.domain = "infra.forkos.org";
|
networking.domain = "infra.forkos.org";
|
||||||
|
|
||||||
bagel.sysadmin.enable = true;
|
bagel.sysadmin.enable = true;
|
||||||
# Buildbot is proxied.
|
# Newsletter is proxied.
|
||||||
bagel.raito.v6-proxy-awareness.enable = true;
|
bagel.raito.v6-proxy-awareness.enable = true;
|
||||||
bagel.newsletter = {
|
bagel.newsletter = {
|
||||||
enable = true;
|
enable = true;
|
||||||
|
@ -27,6 +27,17 @@
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
bagel.services.s3-revproxy = {
|
||||||
|
enable = true;
|
||||||
|
domain = "forkos.org";
|
||||||
|
s3.apiUrl = "s3.delroth.net";
|
||||||
|
targets = {
|
||||||
|
channels = "bagel-channels";
|
||||||
|
releases = "bagel-releases";
|
||||||
|
channel-scripts-test = "bagel-channel-scripts-test";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
i18n.defaultLocale = "en_US.UTF-8";
|
i18n.defaultLocale = "en_US.UTF-8";
|
||||||
|
|
||||||
system.stateVersion = "24.05";
|
system.stateVersion = "24.05";
|
||||||
|
|
|
@ -3,5 +3,6 @@
|
||||||
iusb-spoof = final.callPackage ./iusb-spoof.nix {};
|
iusb-spoof = final.callPackage ./iusb-spoof.nix {};
|
||||||
u-root = final.callPackage ./u-root {};
|
u-root = final.callPackage ./u-root {};
|
||||||
pyroscope = final.callPackage ./pyroscope {};
|
pyroscope = final.callPackage ./pyroscope {};
|
||||||
|
s3-revproxy = final.callPackage ./s3-revproxy {};
|
||||||
})
|
})
|
||||||
]
|
]
|
||||||
|
|
39
overlays/s3-revproxy/default.nix
Normal file
39
overlays/s3-revproxy/default.nix
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
# Originally written by Jade Lovelace for Lix.
|
||||||
|
{ lib, buildGoModule, fetchFromGitHub }:
|
||||||
|
buildGoModule rec {
|
||||||
|
pname = "s3-revproxy";
|
||||||
|
version = "4.15.0";
|
||||||
|
|
||||||
|
src = fetchFromGitHub {
|
||||||
|
owner = "oxyno-zeta";
|
||||||
|
repo = "s3-proxy";
|
||||||
|
rev = "v${version}";
|
||||||
|
hash = "sha256-q0cfAo8Uz7wtKljmSDaJ320bjg2yXydvvxubAsMKzbc=";
|
||||||
|
};
|
||||||
|
|
||||||
|
vendorHash = "sha256-dOwNQtTfOCQcjgNBV/FeWdwbW9xi1OK5YD7PBPPDKOQ=";
|
||||||
|
|
||||||
|
ldflags = [
|
||||||
|
"-X github.com/oxyno-zeta/s3-proxy/pkg/s3-proxy/version.Version=${version}"
|
||||||
|
"-X github.com/oxyno-zeta/s3-proxy/pkg/s3-proxy/version.Metadata="
|
||||||
|
];
|
||||||
|
|
||||||
|
postPatch = ''
|
||||||
|
# Refer to the included templates in the package instead of cwd-relative
|
||||||
|
sed -i "s#Path = \"templates/#Path = \"$out/share/s3-revproxy/templates/#" pkg/s3-proxy/config/config.go
|
||||||
|
'';
|
||||||
|
|
||||||
|
postInstall = ''
|
||||||
|
mkdir -p $out/share/s3-revproxy
|
||||||
|
cp -r templates/ $out/share/s3-revproxy/templates
|
||||||
|
'';
|
||||||
|
|
||||||
|
meta = {
|
||||||
|
description = "S3 Reverse Proxy with GET, PUT and DELETE methods and authentication (OpenID Connect and Basic Auth)";
|
||||||
|
homepage = "https://oxyno-zeta.github.io/s3-proxy";
|
||||||
|
# hm, not having a maintainers entry is kind of inconvenient
|
||||||
|
maintainers = [ ];
|
||||||
|
licenses = lib.licenses.asl20;
|
||||||
|
mainProgram = "s3-proxy";
|
||||||
|
};
|
||||||
|
}
|
|
@ -11,5 +11,6 @@
|
||||||
./baremetal-builder
|
./baremetal-builder
|
||||||
./buildbot
|
./buildbot
|
||||||
./newsletter
|
./newsletter
|
||||||
|
./s3-revproxy
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
131
services/s3-revproxy/default.nix
Normal file
131
services/s3-revproxy/default.nix
Normal file
|
@ -0,0 +1,131 @@
|
||||||
|
# Originally written by Jade Lovelace for Lix.
|
||||||
|
# Adapted for ForkOS.
|
||||||
|
{ lib, config, ... }:
|
||||||
|
let
|
||||||
|
inherit (lib) mkEnableOption mkIf types mkOption mapAttrs mapAttrs' nameValuePair;
|
||||||
|
cfg = config.bagel.services.s3-revproxy;
|
||||||
|
s3RevproxyPort = 10652;
|
||||||
|
mkTarget = { name, bucket ? name }: {
|
||||||
|
mount = {
|
||||||
|
host = "${name}.${cfg.domain}";
|
||||||
|
path = [ "/" ];
|
||||||
|
};
|
||||||
|
actions.GET = {
|
||||||
|
enabled = true;
|
||||||
|
config = {
|
||||||
|
# e.g. /2.90 will 404, so it will redirect to /2.90/ if it is a directory
|
||||||
|
redirectWithTrailingSlashForNotFoundFile = true;
|
||||||
|
indexDocument = "index.html";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
bucket = {
|
||||||
|
name = bucket;
|
||||||
|
region = "garage";
|
||||||
|
s3Endpoint = "https://${cfg.s3.apiUrl}";
|
||||||
|
credentials = {
|
||||||
|
accessKey.env = "AWS_ACCESS_KEY_ID";
|
||||||
|
secretKey.env = "AWS_SECRET_KEY";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
# Makes a subdomain that gets proxied through s3-proxy to provide directory
|
||||||
|
# listings and reasonable 404 pages.
|
||||||
|
# This is not used on cache, since there a directory listing for cache is a
|
||||||
|
# liability at best.
|
||||||
|
mkProxiedSubdomain = subdomain: {
|
||||||
|
enableACME = true;
|
||||||
|
forceSSL = true;
|
||||||
|
locations."/" = {
|
||||||
|
recommendedProxySettings = true;
|
||||||
|
proxyPass = "http://127.0.0.1:${toString s3RevproxyPort}/";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
options.bagel.services.s3-revproxy = {
|
||||||
|
enable = mkEnableOption "a S3 reverse proxy";
|
||||||
|
|
||||||
|
domain = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
};
|
||||||
|
|
||||||
|
s3 = {
|
||||||
|
apiUrl = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
description = "An URL to the S3 API";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
targets = mkOption {
|
||||||
|
type = types.attrsOf types.str;
|
||||||
|
description = ''
|
||||||
|
A mapping between a nice name and a bucket name.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
imports = [
|
||||||
|
./module.nix
|
||||||
|
];
|
||||||
|
|
||||||
|
config = mkIf cfg.enable {
|
||||||
|
age.secrets.s3-revproxy-api-keys.file = ../../secrets/s3-revproxy-api-keys.age;
|
||||||
|
# For each target, generate an entry that passes it to the s3-revproxy.
|
||||||
|
services.nginx.virtualHosts = mapAttrs' (subdomain: _: nameValuePair "${subdomain}.${cfg.domain}" (mkProxiedSubdomain subdomain)) cfg.targets;
|
||||||
|
# this solves garage supporting neither anonymous access nor automatic
|
||||||
|
# directory indexing by simply ignoring garage's web server and replacing it
|
||||||
|
# with overengineered golang instead.
|
||||||
|
services.s3-revproxy = {
|
||||||
|
enable = true;
|
||||||
|
settings = {
|
||||||
|
templates = {
|
||||||
|
helpers = [ ./s3-revproxy-templates/_helpers.tpl ];
|
||||||
|
notFoundError = {
|
||||||
|
headers = {
|
||||||
|
"Content-Type" = "{{ template \"main.headers.contentType\" . }}";
|
||||||
|
};
|
||||||
|
status = "404";
|
||||||
|
};
|
||||||
|
folderList = {
|
||||||
|
path = ./s3-revproxy-templates/folder-list.tpl;
|
||||||
|
headers = {
|
||||||
|
"Content-Type" = "{{ template \"main.headers.contentType\" . }}";
|
||||||
|
};
|
||||||
|
# empty s3 directories are not real and cannot hurt you.
|
||||||
|
# due to redirectWithTrailingSlashForNotFoundFile, garbage file names
|
||||||
|
# get redirected as folders, which then appear as empty, yielding
|
||||||
|
# poor UX.
|
||||||
|
status = ''
|
||||||
|
{{- if eq (len .Entries) 0 -}}
|
||||||
|
404
|
||||||
|
{{- else -}}
|
||||||
|
200
|
||||||
|
{{- end -}}
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/* For metrics and debugging (e.g. pulling the config)
|
||||||
|
internalServer = {
|
||||||
|
listenAddr = "127.0.0.1";
|
||||||
|
port = 1337;
|
||||||
|
};
|
||||||
|
*/
|
||||||
|
server = {
|
||||||
|
listenAddr = "127.0.0.1";
|
||||||
|
port = s3RevproxyPort;
|
||||||
|
|
||||||
|
# it's going right into nginx, so no point
|
||||||
|
compress.enabled = false;
|
||||||
|
cors = {
|
||||||
|
enabled = true;
|
||||||
|
allowMethods = [ "GET" ];
|
||||||
|
allowOrigins = [ "*" ];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
targets = mapAttrs (name: bucket: mkTarget { inherit name bucket; }) cfg.targets;
|
||||||
|
};
|
||||||
|
environmentFile = config.age.secrets.s3-revproxy-api-keys.path;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
69
services/s3-revproxy/module.nix
Normal file
69
services/s3-revproxy/module.nix
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
# Originally, written by Jade Lovelace for Lix.
|
||||||
|
{ config, pkgs, lib, ... }:
|
||||||
|
let cfg = config.services.s3-revproxy;
|
||||||
|
settingsGenerator = pkgs.formats.yaml {};
|
||||||
|
# Needs to be in a directory, so we might as well implement autoreload, why not!
|
||||||
|
configFile = settingsGenerator.generate "config.yaml" cfg.settings;
|
||||||
|
|
||||||
|
inherit (lib) types;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
options.services.s3-revproxy = {
|
||||||
|
enable = lib.mkEnableOption "s3 reverse proxy";
|
||||||
|
package = lib.mkPackageOption pkgs "s3-revproxy" {};
|
||||||
|
settings = lib.mkOption {
|
||||||
|
default = { };
|
||||||
|
type = settingsGenerator.type;
|
||||||
|
description = ''
|
||||||
|
Settings to use for the service. See the documentation at https://oxyno-zeta.github.io/s3-proxy/configuration/structure/
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
environmentFile = lib.mkOption {
|
||||||
|
type = types.nullOr types.path;
|
||||||
|
default = null;
|
||||||
|
description = ''
|
||||||
|
Environment file to use for s3-revproxy.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
config = lib.mkIf cfg.enable {
|
||||||
|
environment.etc."s3-revproxy/config.yaml".source = configFile;
|
||||||
|
systemd.services.s3-revproxy = {
|
||||||
|
wantedBy = [ "multi-user.target" ];
|
||||||
|
|
||||||
|
serviceConfig = {
|
||||||
|
ExecStart = "${lib.getExe cfg.package} --config /etc/s3-revproxy";
|
||||||
|
|
||||||
|
DynamicUser = true;
|
||||||
|
CapabilityBoundingSet = "";
|
||||||
|
NoNewPrivileges = true;
|
||||||
|
PrivateTmp = true;
|
||||||
|
PrivateUsers = true;
|
||||||
|
PrivateDevices = true;
|
||||||
|
ProtectHome = true;
|
||||||
|
ProtectClock = true;
|
||||||
|
ProtectProc = "noaccess";
|
||||||
|
ProcSubset = "pid";
|
||||||
|
UMask = "0077";
|
||||||
|
ProtectKernelLogs = true;
|
||||||
|
ProtectKernelModules = true;
|
||||||
|
ProtectKernelTunables = true;
|
||||||
|
ProtectControlGroups = true;
|
||||||
|
ProtectHostname = true;
|
||||||
|
RestrictSUIDSGID = true;
|
||||||
|
RestrictRealtime = true;
|
||||||
|
RestrictNamespaces = true;
|
||||||
|
LockPersonality = true;
|
||||||
|
RemoveIPC = true;
|
||||||
|
SystemCallFilter = [ "@system-service" "~@privileged" ];
|
||||||
|
RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
|
||||||
|
MemoryDenyWriteExecute = true;
|
||||||
|
SystemCallArchitectures = "native";
|
||||||
|
|
||||||
|
EnvironmentFile = lib.optionals (cfg.environmentFile != null) [ cfg.environmentFile ];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
37
services/s3-revproxy/s3-revproxy-templates/_helpers.tpl
Normal file
37
services/s3-revproxy/s3-revproxy-templates/_helpers.tpl
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
{{/* SPDX-License-Identifier: Apache-2.0 */}}
|
||||||
|
{{/* SPDX-FileCopyrightText: 2024 s3-proxy contributors */}}
|
||||||
|
|
||||||
|
{{- /* This function will allow to get user identifier. */ -}}
|
||||||
|
{{- define "main.userIdentifier" -}}
|
||||||
|
{{- if .User -}}
|
||||||
|
{{- .User.GetIdentifier -}}
|
||||||
|
{{- end -}}
|
||||||
|
{{- end -}}
|
||||||
|
|
||||||
|
|
||||||
|
{{- /* This function will allow to get the content type header from "Accept" header */ -}}
|
||||||
|
{{- define "main.headers.contentType" -}}
|
||||||
|
{{- if contains "application/json" (.Request.Header.Get "Accept") -}}
|
||||||
|
application/json; charset=utf-8
|
||||||
|
{{- else -}}
|
||||||
|
text/html; charset=utf-8
|
||||||
|
{{- end -}}
|
||||||
|
{{- end -}}
|
||||||
|
|
||||||
|
{{- /* This will forge the json output of an error */ -}}
|
||||||
|
{{- define "main.body.errorJsonBody" -}}
|
||||||
|
{"error": {{ .Error.Error | toJson }}}
|
||||||
|
{{- end -}}
|
||||||
|
|
||||||
|
{{- define "notFoundErrorBody" -}}
|
||||||
|
{{- if contains "application/json" (.Request.Header.Get "Accept") -}}
|
||||||
|
{{ template "main.body.errorJsonBody" . }}
|
||||||
|
{{- else -}}
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<h1>Not Found {{ .Request.URL.Path }}</h1>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
{{- end -}}
|
||||||
|
{{- end -}}
|
52
services/s3-revproxy/s3-revproxy-templates/folder-list.tpl
Normal file
52
services/s3-revproxy/s3-revproxy-templates/folder-list.tpl
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
{{/* SPDX-License-Identifier: Apache-2.0 */}}
|
||||||
|
{{/* SPDX-FileCopyrightText: 2024 s3-proxy contributors */}}
|
||||||
|
{{- $root := . -}}
|
||||||
|
{{- if eq (len .Entries) 0 -}}
|
||||||
|
{{- include "notFoundErrorBody" . -}}
|
||||||
|
{{- else -}}
|
||||||
|
|
||||||
|
{{- if contains "application/json" (.Request.Header.Get "Accept") -}}
|
||||||
|
[
|
||||||
|
{{- $maxLen := len $root.Entries -}}
|
||||||
|
{{- range $index, $entry := $root.Entries -}}
|
||||||
|
{"name": {{ $entry.Name | toJson -}}
|
||||||
|
,"etag": {{ $entry.ETag | toJson -}}
|
||||||
|
,"type": {{ $entry.Type | toJson -}}
|
||||||
|
,"size": {{ $entry.Size | toJson -}}
|
||||||
|
,"path": {{ $entry.Path | toJson -}}
|
||||||
|
,"lastModified": {{ $entry.LastModified | date "2006-01-02T15:04:05Z07:00" | toJson -}}
|
||||||
|
}{{- if ne $index (sub $maxLen 1) -}},{{- end -}}
|
||||||
|
{{- end -}}
|
||||||
|
]
|
||||||
|
{{- else -}}
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<h1>Index of {{ .Request.URL.Path }}</h1>
|
||||||
|
<table style="width:100%">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th style="border-right:1px solid black;text-align:start">Entry</th>
|
||||||
|
<th style="border-right:1px solid black;text-align:start">Size</th>
|
||||||
|
<th style="border-right:1px solid black;text-align:start">Last modified</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody style="border-top:1px solid black">
|
||||||
|
<tr>
|
||||||
|
<td style="border-right:1px solid black;padding: 0 5px"><a href="..">..</a></td>
|
||||||
|
<td style="border-right:1px solid black;padding: 0 5px"> - </td>
|
||||||
|
<td style="padding: 0 5px"> - </td>
|
||||||
|
</tr>
|
||||||
|
{{- range .Entries }}
|
||||||
|
<tr>
|
||||||
|
<td style="border-right:1px solid black;padding: 0 5px"><a href="{{ .Path }}">{{ .Name }}</a></td>
|
||||||
|
<td style="border-right:1px solid black;padding: 0 5px">{{- if eq .Type "FOLDER" -}} - {{- else -}}{{ .Size | humanSize }}{{- end -}}</td>
|
||||||
|
<td style="padding: 0 5px">{{ .LastModified }}</td>
|
||||||
|
</tr>
|
||||||
|
{{- end }}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
{{- end -}}
|
||||||
|
{{- end -}}
|
|
@ -0,0 +1,3 @@
|
||||||
|
{{/* SPDX-License-Identifier: Apache-2.0 */}}
|
||||||
|
{{/* SPDX-FileCopyrightText: 2024 s3-proxy contributors */}}
|
||||||
|
{{- include "notFoundErrorBody" . -}}
|
Loading…
Reference in a new issue