infra/services/s3-revproxy/default.nix

141 lines
4.4 KiB
Nix

# 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/floral/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 = [ "*" ];
};
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";
};
};
targets = mapAttrs (name: bucket: mkTarget { inherit name bucket; }) cfg.targets;
};
environmentFile = config.age.secrets.s3-revproxy-api-keys.path;
};
};
}