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";
|
||||
|
||||
bagel.sysadmin.enable = true;
|
||||
# Buildbot is proxied.
|
||||
# Newsletter is proxied.
|
||||
bagel.raito.v6-proxy-awareness.enable = true;
|
||||
bagel.newsletter = {
|
||||
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";
|
||||
|
||||
system.stateVersion = "24.05";
|
||||
|
|
|
@ -3,5 +3,6 @@
|
|||
iusb-spoof = final.callPackage ./iusb-spoof.nix {};
|
||||
u-root = final.callPackage ./u-root {};
|
||||
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
|
||||
./buildbot
|
||||
./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