2024-08-30 22:22:27 +00:00
|
|
|
# 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 {
|
2024-10-06 19:20:08 +00:00
|
|
|
age.secrets.s3-revproxy-api-keys.file = ../../secrets/floral/s3-revproxy-api-keys.age;
|
2024-08-30 22:22:27 +00:00
|
|
|
# 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 = [ "*" ];
|
|
|
|
};
|
2024-08-30 22:52:13 +00:00
|
|
|
|
|
|
|
cache = {
|
|
|
|
noCacheEnabled = false;
|
|
|
|
# Taken from the original Perl script.
|
|
|
|
# maxage=600: Serve from cache for 5 minutes.
|
|
|
|
# stale-while-revaliadate=1800: Serve from cache while updating in the background for 30 minutes.
|
|
|
|
# https://web.dev/stale-while-revalidate/
|
|
|
|
# https://developer.fastly.com/learning/concepts/cache-freshness/
|
|
|
|
cacheControl = "maxage=600,stale-while-revalidate=1800,public";
|
|
|
|
};
|
2024-08-30 22:22:27 +00:00
|
|
|
};
|
|
|
|
targets = mapAttrs (name: bucket: mkTarget { inherit name bucket; }) cfg.targets;
|
|
|
|
};
|
|
|
|
environmentFile = config.age.secrets.s3-revproxy-api-keys.path;
|
|
|
|
};
|
|
|
|
};
|
|
|
|
}
|