{ config, lib, pkgs, ... }: let cfg.nameserver = "1.1.1.1"; cfg.packageDir = "/var/lib/elm-packages"; cfg.port = 7782; # TODO secret files cfg.htpasswd = "/var/lib/certs/package.elm-lang.org/htpasswd"; cfg.sslCertificate = "/var/lib/certs/package.elm-lang.org/fullchain.pem"; cfg.sslCertificateKey = "/var/lib/certs/package.elm-lang.org/key.pem"; semverRegex = "(?<major>0|[1-9]\\d*)\\.(?<minor>0|[1-9]\\d*)\\.(?<patch>0|[1-9]\\d*)(?:-(?<prerelease>(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+(?<buildmetadata>[0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?"; in { services.nginx.virtualHosts."package.elm-lang.org" = { addSSL = true; sslCertificate = cfg.sslCertificate; sslCertificateKey = cfg.sslCertificateKey; locations."/all-packages".extraConfig = '' proxy_pass http://127.0.0.1:${toString config.krebs.htgen.elm-packages-proxy.port}; proxy_pass_header Server; ''; locations."/all-packages/since/".extraConfig = '' proxy_pass http://127.0.0.1:${toString config.krebs.htgen.elm-packages-proxy.port}; proxy_pass_header Server; ''; locations."~ ^/packages/(?<author>[A-Za-z0-9-]+)/(?<pname>[A-Za-z0-9-]+)/(?<version>${semverRegex})\$".extraConfig = '' auth_basic "Restricted Area"; auth_basic_user_file ${cfg.htpasswd}; proxy_set_header X-User $remote_user; proxy_set_header X-Author $author; proxy_set_header X-Package $pname; proxy_set_header X-Version $version; proxy_pass_header Server; proxy_pass http://127.0.0.1:${toString config.krebs.htgen.elm-packages-proxy.port}; ''; locations."~ ^/packages/(?<author>[A-Za-z0-9-]+)/(?<pname>[A-Za-z0-9-]+)/(?<version>${semverRegex})/(?:zipball|elm.json|endpoint.json)\$".extraConfig = '' set $zipball "${cfg.packageDir}/$author/$pname/$version/zipball"; proxy_set_header X-Author $author; proxy_set_header X-Package $pname; proxy_set_header X-Version $version; proxy_set_header X-Zipball $zipball; proxy_pass_header Server; resolver ${cfg.nameserver}; if (-f $zipball) { set $new_uri http://127.0.0.1:${toString config.krebs.htgen.elm-packages-proxy.port}; } if (!-f $zipball) { set $new_uri https://package.elm-lang.org$request_uri; } proxy_pass $new_uri; ''; locations."/search.json".extraConfig = '' proxy_pass http://127.0.0.1:${toString config.krebs.htgen.elm-packages-proxy.port}; proxy_pass_header Server; ''; }; krebs.htgen.elm-packages-proxy = { port = cfg.port; script = /* sh */ ''. ${pkgs.writeDash "elm-packages-proxy.sh" '' PATH=${lib.makeBinPath [ pkgs.attr pkgs.coreutils pkgs.curl pkgs.findutils pkgs.gnugrep pkgs.jq pkgs.p7zip ]} export PATH file_response() {( status_code=$1 status_reason=$2 file=$3 content_type=$4 content_length=$(wc -c "$file" | cut -d\ -f1) printf "HTTP/1.1 $status_code $status_reason\r\n" printf 'Connection: close\r\n' printf 'Content-Length: %d\r\n' "$content_length" printf 'Content-Type: %s\r\n' "$content_type" printf 'Server: %s\r\n' "$Server" printf '\r\n' cat "$file" )} string_response() {( status_code=$1 status_reason=$2 response_body=$3 content_type=$4 printf "HTTP/1.1 $status_code $status_reason\r\n" printf 'Connection: close\r\n' printf 'Content-Length: %d\r\n' "$(expr ''${#response_body} + 1)" printf 'Content-Type: %s\r\n' "$content_type" printf 'Server: %s\r\n' "$Server" printf '\r\n' printf '%s\n' "$response_body" )} case "$Method $Request_URI" in 'GET /packages/'*) author=$req_x_author pname=$req_x_package version=$req_x_version zipball=${cfg.packageDir}/$author/$pname/$version/zipball elmjson=$HOME/cache/$author%2F$pname%2F$version%2Felm.json endpointjson=$HOME/cache/$author%2F$pname%2F$version%2Fendpoint.json mkdir -p "$HOME/cache" case $(basename $Request_URI) in zipball) file_response 200 OK "$zipball" application/zip exit ;; elm.json) if ! test -f "$elmjson"; then 7z x -so "$zipball" \*/elm.json > "$elmjson" fi file_response 200 OK "$elmjson" 'application/json; charset=UTF-8' exit ;; endpoint.json) if ! test -f "$endpointjson"; then hash=$(sha1sum "$zipball" | cut -d\ -f1) url=https://package.elm-lang.org/packages/$author/$pname/$version/zipball jq -n \ --arg hash "$hash" \ --arg url "$url" \ '{ $hash, $url }' \ > "$endpointjson" fi file_response 200 OK "$endpointjson" 'application/json; charset=UTF-8' exit ;; esac ;; 'POST /packages/'*) author=$req_x_author pname=$req_x_package user=$req_x_user version=$req_x_version action=uploading force=''${req_x_force-false} zipball=${cfg.packageDir}/$author/$pname/$version/zipball elmjson=$HOME/cache/$author%2F$pname%2F$version%2Felm.json endpointjson=$HOME/cache/$author%2F$pname%2F$version%2Fendpoint.json if test -e "$zipball"; then if test "$force" = true; then zipball_owner=$(attr -q -g X-User "$zipball" || :) if test "$zipball_owner" = "$req_x_user"; then action=replacing rm -f "$elmjson" rm -f "$endpointjson" else string_response 403 Forbidden \ "package already exists: $author/$pname@$version" \ text/plain exit fi else string_response 409 Conflict \ "package already exists: $author/$pname@$version" \ text/plain exit fi fi echo "user $user is $action package $author/$pname@$version" >&2 # TODO check package mkdir -p "$(dirname "$zipball")" head -c $req_content_length > "$zipball" attr -q -s X-User -V "$user" "$zipball" || : string_response 200 OK \ "package created: $author/$pname@$version" \ text/plain exit ;; 'DELETE /packages/'*) author=$req_x_author pname=$req_x_package user=$req_x_user version=$req_x_version zipball=${cfg.packageDir}/$author/$pname/$version/zipball elmjson=$HOME/cache/$author%2F$pname%2F$version%2Felm.json endpointjson=$HOME/cache/$author%2F$pname%2F$version%2Fendpoint.json if test -e "$zipball"; then zipball_owner=$(attr -q -g X-User "$zipball" || :) if test "$zipball_owner" = "$req_x_user"; then echo "user $user is deleting package $author/$pname@$version" >&2 rm -f "$elmjson" rm -f "$endpointjson" rm "$zipball" string_response 200 OK \ "package deleted: $author/$pname@$version" \ text/plain exit else string_response 403 Forbidden \ "package already exists: $author/$pname@$version" \ text/plain exit fi fi ;; 'GET /all-packages'|'POST /all-packages') response=$(mktemp -t htgen.$$.elm-packages-proxy.all-packages.XXXXXXXX) trap "rm $response >&2" EXIT { # upstream packages curl -fsS https://package.elm-lang.org"$Request_URI" # private packages (cd ${cfg.packageDir}; find -mindepth 3 -maxdepth 3) | jq -Rs ' split("\n") | map( select(.!="") | match("^\\./(?<author>[^/]+)/(?<pname>[^/]+)/(?<version>[^/]+)$").captures | map({key:.name,value:.string}) | from_entries ) | reduce .[] as $item ({}; ($item|"\(.author)/\(.pname)") as $name | . + { "\($name)": ((.[$name] // []) + [$item.version]) } ) ' } | jq -cs add > $response file_response 200 OK "$response" 'application/json; charset=UTF-8' exit ;; 'GET /all-packages/since/'*|'POST /all-packages/since/'*) response=$(mktemp -t htgen.$$.elm-packages-proxy.all-packages.XXXXXXXX) trap "rm $response >&2" EXIT { # upstream packages curl -fsS https://package.elm-lang.org"$Request_URI" # private packages (cd ${cfg.packageDir}; find -mindepth 3 -maxdepth 3) | jq -Rs ' split("\n") | map( select(.!="") | sub("^\\./(?<author>[^/]+)/(?<pname>[^/]+)/(?<version>[^/]+)$";"\(.author)/\(.pname)@\(.version)") ) | sort_by(split("@") | [.[0]]+(.[1]|split(".")|map(tonumber))) | reverse ' } | jq -cs add > $response file_response 200 OK "$response" 'application/json; charset=UTF-8' exit ;; 'GET /search.json') searchjson=$HOME/cache/search.json mkdir -p "$HOME/cache" # update cached search.json ( last_modified=$( if test -f "$searchjson"; then date -Rr "$searchjson" else date -R -d @0 fi ) tempsearchjson=$(mktemp "$searchjson.XXXXXXXX") trap 'rm "$tempsearchjson" >&2' EXIT curl -fsS --compressed https://package.elm-lang.org/search.json \ -H "If-Modified-Since: $last_modified" \ -o "$tempsearchjson" if test -s "$tempsearchjson"; then mv "$tempsearchjson" "$searchjson" trap - EXIT fi ) response=$(mktemp -t htgen.$$.elm-packages-proxy.search.XXXXXXXX) trap 'rm "$response" >&2' EXIT { printf '{"upstream":'; cat "$searchjson" printf ',"private":'; (cd ${cfg.packageDir}; find -mindepth 3 -maxdepth 3) | jq -Rs ' split("\n") | map( select(.!="") | match("^\\./(?<author>[^/]+)/(?<pname>[^/]+)/(?<version>[^/]+)$").captures | map({key:.name,value:.string}) | from_entries ) | map({ key: "\(.author)/\(.pname)", value: .version, }) | from_entries ' printf '}' } | jq -c ' reduce .upstream[] as $upstreamItem ({ private, output: [] }; .private[$upstreamItem.name] as $privateItem | if $privateItem then .output += [$upstreamItem * { version: $privateItem.version }] | .private |= del(.[$upstreamItem.name]) else .output += [$upstreamItem] end ) | .output + (.private | to_entries | sort_by(.key) | map({ name: .key, version: .value, summary: "dummy summary", license: "dummy license", })) ' \ > $response file_response 200 OK "$response" 'application/json; charset=UTF-8' exit ;; esac ''}''; }; }