From 994a8e983eef9071d73c9b2daad9bd42aac0b1aa Mon Sep 17 00:00:00 2001 From: chaos Date: Tue, 19 Sep 2023 17:50:52 +0100 Subject: [PATCH] changes to the piped module to make config better --- module/backend.nix | 212 +++++++++-------- module/default.nix | 537 +++++++++++++++++++++++--------------------- module/frontend.nix | 38 ++-- module/nginx.nix | 13 -- module/proxy.nix | 65 ++++-- readme.md | 52 +++++ 6 files changed, 524 insertions(+), 393 deletions(-) delete mode 100644 module/nginx.nix create mode 100644 readme.md diff --git a/module/backend.nix b/module/backend.nix index 1f5aa6f..3bae479 100644 --- a/module/backend.nix +++ b/module/backend.nix @@ -4,58 +4,76 @@ pkgs, ... }: let - inherit (lib.modules) mkIf; + inherit (lib.modules) mkIf mkDefault; inherit (lib.trivial) boolToString; inherit (lib.attrsets) mapAttrsToList optionalAttrs; inherit (lib.strings) optionalString concatStringsSep; - inherit (pkgs) writeText writeShellScript; + inherit (pkgs) writeText; cfg = config.services.piped; + backendConfig = cfg.backend; + nginxConfig = backendConfig.nginx; + databaseConfig = backendConfig.database; + + proxyConfig = cfg.proxy; + frontendConfig = cfg.frontend; + + backendSettings = backendConfig.settings; sedEscapeSlashes = "sed 's#/#\\\/#'"; - sedEscapeSingleQuotes = "sed \"s#'#\\\'#\""; - backendConfig = + backendConfigAttrs = { - PORT = cfg.internalBackendPort; - HTTP_WORKERS = cfg.httpWorkers; - PROXY_PART = "https://${cfg.proxyDomain}"; - API_URL = "https://${cfg.backendDomain}"; - FRONTEND_URL = "https://${cfg.frontendDomain}"; - DISABLE_REGISTRATION = cfg.disableRegistrations; - COMPROMISED_PASSWORD_CHECK = cfg.enableCompromisedPasswordCheck; - FEED_RETENTION = cfg.feedRetentionDays; - SUBSCRIPTIONS_EXPIRY = cfg.subscriptionRetentionDays; - SPONSORBLOCK_SERVERS = concatStringsSep "," cfg.sponsorblockServers; - DISABLE_RYD = cfg.disableRYD; - DISABLE_LBRY = cfg.disableLBRYStreams; - RYD_PROXY_URL = cfg.rydAPIURL; - SENTRY_DSN = cfg.sentryDSN; - "hibernate.connection.url" = "jdbc:postgresql://${cfg.postgresDBHost}:${toString cfg.postgresDBPort}/${cfg.postgresDBName}"; - "hibernate.connection.driver_class" = "org.postgresql.Driver"; - "hibernate.dialect" = cfg.databaseDialect; - "hibernate.connection.username" = "${cfg.postgresDBUsername}"; - "hibernate.connection.password" = - if cfg.postgresDBPasswordFile == null - then cfg.postgresDBPassword - else "POSTGRES_PASSWORD_PLACEHOLDER"; + PORT = backendConfig.internalPort; + PROXY_PART = "https://${proxyConfig.domain}"; + API_URL = "https://${backendConfig.domain}"; + FRONTEND_URL = "https://${frontendConfig.domain}"; + + DISABLE_REGISTRATION = backendSettings.disableRegistrations; + + FEED_RETENTION = backendSettings.feedRetentionDays; + SUBSCRIPTIONS_EXPIRY = backendSettings.subscriptionRetentionDays; + HTTP_WORKERS = backendSettings.httpWorkers; + + SPONSORBLOCK_SERVERS = concatStringsSep "," backendSettings.sponsorblockServers; + DISABLE_LBRY = backendSettings.disableLBRYStreams; + COMPROMISED_PASSWORD_CHECK = backendSettings.enableCompromisedPasswordCheck; + + DISABLE_RYD = backendConfig.ryd.disable; + RYD_PROXY_URL = backendConfig.ryd.apiURL; + + SENTRY_DSN = backendSettings.sentryDSN; + + "hibernate.connection.url" = let + inherit (databaseConfig) host port name databaseOptions; + in "jdbc:postgresql://${host}:${toString port}/${name}${databaseOptions}"; + "hibernate.connection.driver_class" = databaseConfig.driverClass; + "hibernate.dialect" = databaseConfig.dialect; + "hibernate.connection.username" = "${databaseConfig.username}"; } - // (optionalAttrs cfg.enableCaptcha { - CAPTCHA_API_URL = cfg.captchaAPIURL; + // (optionalAttrs databaseConfig.usePassword { + "hibernate.connection.password" = + if databaseConfig.passwordFile == null + then databaseConfig.password + else "DATABASE_PASSWORD_PLACEHOLDER"; + }) + // (optionalAttrs backendConfig.captcha.enable { + CAPTCHA_API_URL = backendConfig.captcha.apiURL; # This is substituted in the PreStart of piped-backend.service CAPTCHA_API_KEY = - if cfg.captchaAPIKeyFile != "" + if backendConfig.captcha.apiKeyFile != null then "CAPTCHA_API_KEY_FILE_PLACEHOLDER" - else cfg.captchaAPIKey; + else backendSettings.apiKey; }) - // (optionalAttrs cfg.enableFederation { - MATRIX_SERVER = cfg.matrixServerAddr; + // (optionalAttrs backendConfig.federation.enable { + MATRIX_SERVER = backendConfig.federation.matrixServerAddr; # also substituted MATRIX_TOKEN = - if cfg.matrixTokenFile != "" + if backendConfig.federation.matrixTokenFile != "" then "MATRIX_TOKEN_FILE_PLACEHOLDER" - else cfg.matrixToken; - }); + else backendConfig.federation.matrixToken; + }) + // backendSettings.extraSettings; cfgValueToString = value: if builtins.isBool value @@ -69,43 +87,64 @@ backendConfigFile = writeText "config.properties" - (cfgToString backendConfig); + (cfgToString backendConfigAttrs); in { - config = mkIf (cfg.enable && !cfg.disableBackend) { - systemd.tmpfiles.rules = ["d /run/piped-backend - piped piped"]; + config = mkIf (cfg.enable && backendConfig.enable) { + systemd.tmpfiles.rules = [ + "d /run/piped-backend - piped piped" + "f /run/piped-backend/config.properties 660 piped piped" + ]; + + # I ran into some weird issues with ExecStartPre in piped-backend for this script + # so moved it to its own unit to run before + # some sort of syscall issue with + systemd.services.piped-backend-config-init = { + serviceConfig.User = "piped"; + wantedBy = ["piped-backend.service"]; + script = let + confFile = "/run/piped-backend/config.properties"; + in '' + cat ${backendConfigFile} > ${confFile} + + ${optionalString + (backendConfig.captcha.enable && backendConfig.captcha.apiKeyFile != null) '' + VALUE="$(cat ${backendConfig.captcha.apiKeyFile} | ${sedEscapeSlashes})" + sed -i "s/CAPTCHA_API_KEY_FILE_PLACEHOLDER/$VALUE/" ${confFile} + ''} + + ${optionalString + (backendConfig.federation.enable && backendConfig.federation.matrixTokenFile != null) '' + VALUE="$(cat ${backendConfig.federation.matrixTokenFile} | ${sedEscapeSlashes})" + sed -i "s/MATRIX_TOKEN_FILE_PLACEHOLDER/$VALUE/" ${confFile} + ''} + + ${optionalString + (databaseConfig.passwordFile != null) '' + VALUE=$(cat ${databaseConfig.passwordFile} | ${sedEscapeSlashes}) + sed -i "s/DATABASE_PASSWORD_PLACEHOLDER/$VALUE/" ${confFile} + ''} + + chown piped:piped ${confFile} + ''; + }; systemd.services.piped-backend = { wantedBy = ["multi-user.target"]; + wants = [ + "piped-backend-config-init.service" + ]; + after = [ + "piped-backend-config-init.service" + ]; + serviceConfig = { - WorkingDirectory = "/run/piped-backend"; - ExecStartPre = let - confFile = "/run/piped-backend/config.properties"; - in - writeShellScript "piped-backend-init" '' - [ -f "${confFile}" ] && rm ${confFile} - cp ${backendConfigFile} ${confFile} - chmod 660 ${confFile} - - ${optionalString (cfg.enableCaptcha && cfg.captchaAPIKeyFile != "") '' - sed -i "s/CAPTCHA_API_KEY_FILE_PLACEHOLDER/$(cat ${cfg.captchaAPIKeyFile} | ${sedEscapeSlashes})/" ${confFile} - ''} - - ${optionalString - (cfg.enableFederation && cfg.matrixTokenFile != "") '' - sed -i "s/MATRIX_TOKEN_FILE_PLACEHOLDER/$(cat ${cfg.matrixTokenFile} | ${sedEscapeSlashes})/" ${confFile} - ''} - - ${optionalString - (cfg.postgresDBPasswordFile != null) '' - sed -i "s/POSTGRES_PASSWORD_PLACEHOLDER/$(cat ${cfg.postgresDBPasswordFile} | ${sedEscapeSlashes})/" ${confFile} - ''} - ''; - - ExecStart = "${cfg.backendPackage}/bin/piped-backend"; + ExecStart = "${backendConfig.package}/bin/piped-backend"; RestartSec = "5s"; User = "piped"; + WorkingDirectory = "/run/piped-backend"; + CapabilityBoundingSet = ""; PrivateDevices = true; PrivateUsers = true; @@ -119,43 +158,30 @@ in { }; }; - # Set password for piped when running postgres as piped won't run with - # passwordless authentication - systemd.services.piped-password = mkIf (!cfg.disablePostgresDB) { - serviceConfig = { - Type = "oneshot"; - User = "postgres"; - }; - - wantedBy = ["piped-backend.service"]; - wants = ["postgresql.service"]; - after = ["postgresql.service"]; - - script = '' - ${config.services.postgresql.package}/bin/psql -c "ALTER USER ${cfg.postgresDBUsername} WITH PASSWORD '${ - if cfg.postgresDBPasswordFile != null - then "$(cat ${cfg.postgresDBPasswordFile} | ${sedEscapeSingleQuotes})" - else cfg.postgresDBPassword - }';" - ''; - }; - - services.postgresql = mkIf (!cfg.disablePostgresDB) { + services.postgresql = mkIf (!databaseConfig.disablePostgresDB) { enable = true; ensureUsers = [ { - name = cfg.postgresDBUsername; - ensurePermissions."DATABASE ${cfg.postgresDBName}" = "ALL PRIVILEGES"; + name = databaseConfig.username; + ensurePermissions."DATABASE ${databaseConfig.name}" = "ALL PRIVILEGES"; } ]; - ensureDatabases = [cfg.postgresDBName]; + ensureDatabases = [ + databaseConfig.name + ]; + authentication = mkIf (databaseConfig.password == "") '' + host ${databaseConfig.name} ${databaseConfig.username} samehost peer map=${databaseConfig.name} + ''; }; - services.nginx.virtualHosts."${cfg.backendDomain}" = mkIf (!cfg.disableNginx) { - forceSSL = cfg.nginxForceSSL; - enableACME = cfg.nginxEnableACME; - locations."/" = { - proxyPass = "http://127.0.0.1:${toString cfg.internalBackendPort}"; + services.nginx = mkIf (!nginxConfig.disableNginx) { + enable = true; + virtualHosts."${backendConfig.domain}" = { + forceSSL = mkDefault nginxConfig.forceSSL; + enableACME = mkDefault nginxConfig.enableACME; + locations."/" = { + proxyPass = "http://127.0.0.1:${toString backendConfig.internalPort}"; + }; }; }; }; diff --git a/module/default.nix b/module/default.nix index d3bf9b7..16cf1f5 100644 --- a/module/default.nix +++ b/module/default.nix @@ -13,271 +13,301 @@ in { options.services.piped = { enable = mkEnableOption "piped"; - frontendPackage = mkPackageOption pkgs "piped-frontend" {}; - frontendDomain = mkOption { - type = types.str; - description = "Domain where the frontend will be displayed"; + frontend = { + # Enable implies turning on nginx for frontend as it is static files + enable = mkOption { + type = types.bool; + default = cfg.enable; + }; + package = mkPackageOption pkgs "piped-frontend" {}; + domain = mkOption { + type = types.str; + description = "Domain where the frontend will be displayed"; + }; + + nginx = { + forceSSL = mkOption { + type = types.bool; + default = true; + }; + + enableACME = mkOption { + type = types.bool; + default = true; + }; + }; }; - backendPackage = mkPackageOption pkgs "piped-backend" {}; - backendDomain = mkOption { - type = types.nullOr types.str; - description = "Domain where the backend will be hosted. Set to null for project's default backend"; + backend = { + enable = mkOption { + type = types.bool; + default = cfg.enable; + }; + package = mkPackageOption pkgs "piped-backend" {}; + domain = mkOption { + type = types.str; + description = "Domain used for the backend server"; + }; + + internalPort = mkOption { + type = types.number; + default = 3001; + }; + + nginx = { + disableNginx = mkOption { + type = types.bool; + default = false; + description = "Turn off nginx for proxying backend, use backend.internalPort instead"; + }; + + forceSSL = mkOption { + type = types.bool; + default = true; + }; + + enableACME = mkOption { + type = types.bool; + default = true; + }; + }; + + database = { + disablePostgresDB = mkOption { + type = types.bool; + default = false; + description = "Manually configure a database instead"; + }; + + host = mkOption { + type = types.str; + default = "127.0.0.1"; + description = "Database host"; + }; + + port = mkOption { + type = types.number; + default = 5432; + description = "Database port"; + }; + + name = mkOption { + type = types.str; + default = "piped"; + description = "Database name"; + }; + + username = mkOption { + type = types.str; + default = "piped"; + description = "Database username"; + }; + + usePassword = mkOption { + type = types.bool; + default = true; + description = "Should use database password? If false then use passwordless auth or configure cert-based auth in databaseOptions"; + }; + + password = mkOption { + type = types.str; + default = ""; + description = "Database password (default is to allow passwordless auth for samehost for piped when empty)"; + }; + + passwordFile = mkOption { + type = types.nullOr types.path; + default = null; + description = "Database password file, loaded at runtime"; + }; + + databaseOptions = mkOption { + type = types.str; + default = ""; + description = "Appended to end of "; + }; + + driverClass = mkOption { + type = types.str; + default = "org.postgresql.Driver"; + description = "The driver class to be used for the database connection"; + }; + + dialect = mkOption { + type = types.str; + default = "org.hibernate.dialect.PostgreSQLDialect"; + example = "org.hibernate.dialect.CockroachDialect"; + description = "The dialect class to be used for the database connection"; + }; + }; + + captcha = { + enable = mkEnableOption "piped-backend-captcha"; + + apiURL = mkOption { + type = types.str; + default = ""; + description = "API URL for Captcha (unknown protocol/standard, upstream project uses api.capmonster.cloud in example config)"; + }; + + apiKey = mkOption { + type = types.str; + default = ""; + description = "API Key for Captcha"; + }; + + apiKeyFile = mkOption { + type = types.nullOr types.str; + default = null; + description = "API Key File for Captcha"; + }; + }; + + ryd = { + disable = mkOption { + type = types.bool; + default = false; + description = "Disables querying a Return YouTube Dislike server"; + }; + + apiURL = mkOption { + type = types.str; + default = "https://ryd-proxy.kavin.rocks"; + description = "API URL for a Return YouTube Dislike server"; + }; + }; + + federation = { + # for Piped's Federation Shenanigan + # https://github.com/TeamPiped/piped-federation#how-to-join + enable = mkEnableOption "piped-backend-federation"; + + matrixServerAddr = mkOption { + type = types.str; + default = ""; + description = "Matrix server address for federation"; + }; + + matrixToken = mkOption { + type = types.str; + default = ""; + description = "Matrix access token"; + }; + + matrixTokenFile = mkOption { + type = types.nullOr types.str; + default = null; + description = "Matrix access token file"; + }; + }; + + settings = { + extraSettings = mkOption { + type = types.attrs; + default = {}; + description = "extra settings to pass to config.properties"; + }; + + disableRegistrations = mkOption { + type = types.bool; + default = false; + description = "Disable user registrations"; + }; + + httpWorkers = mkOption { + type = types.number; + default = 2; + description = "Number of workers for HTTP backend tasks"; + }; + + feedRetentionDays = mkOption { + type = types.number; + default = 30; + description = "Days feed is stored for"; + }; + + subscriptionRetentionDays = mkOption { + type = types.number; + default = 30; + description = "Days subscriptions are stored for unauthenticated users"; + }; + + sponsorblockServers = mkOption { + type = types.listOf types.str; + default = ["https://sponsor.ajay.app" "https://sponsorblock.kavin.rocks"]; + description = "Days subscriptions are stored for unauthenticated users"; + }; + + disableLBRYStreams = mkOption { + type = types.bool; + default = false; + description = "Disable showing streams provided by LBRY"; + }; + + enableCompromisedPasswordCheck = mkOption { + type = types.bool; + default = true; + description = "Use the haveibeenpwned API to check if user password have been compromised"; + }; + + sentryDSN = mkOption { + type = types.nullOr types.str; + default = null; + description = "Public DSN for sentry error reporting"; + }; + }; }; - proxyPackage = mkPackageOption pkgs "piped-proxy" {}; - proxyDomain = mkOption { - type = types.str; - description = "Domain used for the proxy server"; - }; + proxy = { + enable = mkOption { + type = types.bool; + default = cfg.enable; + }; + package = mkPackageOption pkgs "piped-proxy" {}; + domain = mkOption { + type = types.str; + description = "Domain used for the proxy server"; + }; - disableNginx = mkOption { - type = types.bool; - default = false; - description = "Turn off module's nginx servers, this means you will need to manually proxy everything"; - }; + internalPort = mkOption { + type = types.number; + default = 3002; + }; - nginxForceSSL = mkOption { - type = types.bool; - default = true; - description = "Should SSL/TLS be force enabled by nginx"; - }; + nginx = { + disableNginx = mkOption { + type = types.bool; + default = false; + description = "Turn off nginx for proxying proxy, use proxy.internalPort instead, you will need the relevant extra nginx config in proxy.nix or similar for good performance"; + }; - nginxEnableACME = mkOption { - type = types.bool; - default = true; - description = "Should ACME be force enabled by nginx"; - }; + forceSSL = mkOption { + type = types.bool; + default = true; + }; - disableFrontend = mkOption { - type = types.bool; - default = false; - description = "Don't host frontend"; - }; + enableACME = mkOption { + type = types.bool; + default = true; + }; + }; - disableBackend = mkOption { - type = types.bool; - default = false; - description = "Don't host backend"; - }; - - disableProxy = mkOption { - type = types.bool; - default = false; - description = "Don't host proxy"; - }; - - disablePostgresDB = mkOption { - type = types.bool; - default = false; - description = "Manually configure postgres instead"; - }; - - postgresDBHost = mkOption { - type = types.str; - default = "127.0.0.1"; - description = "Host for postgres"; - }; - - postgresDBPort = mkOption { - type = types.number; - default = 5432; - description = "Port for postgres"; - }; - - postgresDBName = mkOption { - type = types.str; - default = "piped"; - description = "Database name for piped"; - }; - - postgresDBUsername = mkOption { - type = types.str; - default = "piped"; - description = "Postgres username"; - }; - - postgresDBPassword = mkOption { - type = types.str; - default = "password"; - description = "Password to use for postgres"; - }; - - postgresDBPasswordFile = mkOption { - type = types.nullOr types.str; - default = null; - description = "Password file to use for postgres, loaded at runtime"; - }; - - databaseDialect = mkOption { - type = types.str; - default = "org.hibernate.dialect.CockroachDialect"; - }; - - proxyIPv4Only = mkOption { - type = types.bool; - default = false; - description = "Only use IPv4 when querying youtube's servers"; - }; - - proxyNginxExtraConfig = mkOption { - type = types.lines; - default = '' - proxy_buffering on; - proxy_buffers 1024 16k; - proxy_set_header X-Forwarded-For ""; - proxy_set_header CF-Connecting-IP ""; - proxy_hide_header "alt-svc"; - sendfile on; - sendfile_max_chunk 512k; - tcp_nopush on; - aio threads=default; - aio_write on; - directio 16m; - proxy_hide_header Cache-Control; - proxy_hide_header etag; - proxy_http_version 1.1; - proxy_set_header Connection keep-alive; - proxy_max_temp_file_size 32m; - access_log off; - ''; - description = "Extra config for nginx on piped-proxy"; - }; - - httpWorkers = mkOption { - type = types.number; - default = 2; - description = "Number of workers for HTTP backend tasks"; - }; - - feedRetentionDays = mkOption { - type = types.number; - default = 30; - description = "Days feed is stored for"; - }; - - subscriptionRetentionDays = mkOption { - type = types.number; - default = 30; - description = "Days subscriptions are stored for unauthenticated users"; - }; - - sponsorblockServers = mkOption { - type = types.listOf types.str; - default = ["https://sponsor.ajay.app" "https://sponsorblock.kavin.rocks"]; - description = "Days subscriptions are stored for unauthenticated users"; - }; - - disableRegistrations = mkOption { - type = types.bool; - default = false; - description = "Disable user registrations"; - }; - - disableLBRYStreams = mkOption { - type = types.bool; - default = false; - description = "Disable showing streams provided by LBRY Youtube Partnership"; - }; - - enableCompromisedPasswordCheck = mkOption { - type = types.bool; - default = true; - description = "Use the haveibeenpwned API to check if user password have been compromised"; - }; - - enableCaptcha = mkOption { - type = types.bool; - default = true; - description = "Enable captcha for registrations"; - }; - - sentryDSN = mkOption { - type = types.str; - default = ""; - description = "Public DSN for sentry error reporting"; - }; - - captchaAPIURL = mkOption { - type = types.str; - default = ""; - description = "API URL for Captcha"; - }; - - # TODO: Key & KeyFile should be only one or the other used - - captchaAPIKey = mkOption { - type = types.str; - default = ""; - description = "API Key for Captcha"; - }; - - captchaAPIKeyFile = mkOption { - type = types.str; - default = ""; - description = "API Key File for Captcha"; - }; - - # TODO: run this, requires a go app and Tor server for proxy - #enableRYDServer = mkOption { - # type = types.bool; - # default = true; - # description = "Run a RYD Proxy Server to use"; - #}; - - disableRYD = mkOption { - type = types.bool; - #default = if cfg.enableRYDServer then false else true; - default = false; - description = "Disables querying a Return YouTube Dislike server"; - }; - - rydAPIURL = mkOption { - type = types.str; - #default = if cfg.enableRYDServer then cfg.rydProxyDomain else "https://ryd-proxy.kavin.rocks"; - default = "https://ryd-proxy.kavin.rocks"; - description = "API URL for a Return YouTube Dislike server"; - }; - - # for Piped's Federation Shenanigan - # https://github.com/TeamPiped/piped-federation#how-to-join - enableFederation = mkOption { - type = types.bool; - default = false; - description = "Enable federation of something"; - }; - - matrixServerAddr = mkOption { - type = types.str; - default = ""; - description = "Matrix server address for federation"; - }; - - # TODO: make so only one of these options can be used - matrixToken = mkOption { - type = types.str; - default = ""; - description = "Matrix access token"; - }; - - matrixTokenFile = mkOption { - type = types.str; - default = ""; - description = "Matrix access token file"; - }; - - internalBackendPort = mkOption { - type = types.number; - default = 3001; - }; - - internalProxyPort = mkOption { - type = types.number; - default = 3002; + proxyIPv4Only = mkOption { + type = types.bool; + default = false; + description = "Only use IPv4 when querying youtube's servers"; + }; }; }; - config = mkIf (cfg.enable && (!cfg.disableBackend || !cfg.disableProxy)) { + config = mkIf (cfg.enable && (cfg.backend.enable || cfg.proxy.enable)) { + assertions = [ + { + assertion = cfg.backend.captcha.enable && cfg.backend.captcha.apiKey != "" && cfg.backend.captcha.apiKeyFile != null -> cfg.certsDir != null; + message = "you must use either of services.piped.backend.captcha.{apiKey,apiKeyFile}"; + } + ]; + users.users."piped" = { isSystemUser = true; group = "piped"; @@ -289,6 +319,5 @@ in { ./backend.nix ./frontend.nix ./proxy.nix - ./nginx.nix ]; } diff --git a/module/frontend.nix b/module/frontend.nix index e438469..5715743 100644 --- a/module/frontend.nix +++ b/module/frontend.nix @@ -3,26 +3,36 @@ lib, ... }: let - inherit (lib.modules) mkIf; + inherit (lib.modules) mkIf mkDefault; cfg = config.services.piped; - frontendPackage = - cfg.frontendPackage.override {backendDomain = cfg.backendDomain;}; + backendConfig = cfg.backend; + frontendConfig = cfg.frontend; + nginxConfig = frontendConfig.nginx; + + frontendPackage = frontendConfig.package.override { + backendDomain = backendConfig.domain; + }; in { - config = mkIf (cfg.enable && !cfg.disableFrontend && !cfg.disableNginx) { + config = mkIf (cfg.enable && frontendConfig.enable) { # https://github.com/TeamPiped/Piped/blob/master/docker/nginx.conf - services.nginx.virtualHosts."${cfg.frontendDomain}" = { - forceSSL = cfg.nginxForceSSL; - enableACME = cfg.nginxEnableACME; - locations."/" = { - root = "${frontendPackage}/share/piped-frontend"; - index = "index.html index.htm"; + services.nginx = { + enable = true; + + virtualHosts."${frontendConfig.domain}" = { + forceSSL = mkDefault nginxConfig.forceSSL; + enableACME = mkDefault nginxConfig.enableACME; + + locations."/" = { + root = "${frontendPackage}/share/piped-frontend"; + index = "index.html index.htm"; + }; + # I have no idea why try_files for Single Page Apps doesn't work here + extraConfig = '' + error_page 404 =200 /index.html; + ''; }; - # I have no idea why try_files for Single Page Apps doesn't work here - extraConfig = '' - error_page 404 =200 /index.html; - ''; }; }; } diff --git a/module/nginx.nix b/module/nginx.nix deleted file mode 100644 index fc3e21e..0000000 --- a/module/nginx.nix +++ /dev/null @@ -1,13 +0,0 @@ -{ - config, - lib, - ... -}: let - inherit (lib.modules) mkIf; - - cfg = config.services.piped; -in { - config = mkIf (cfg.enable && !cfg.disableNginx) { - services.nginx.enable = true; - }; -} diff --git a/module/proxy.nix b/module/proxy.nix index 3c276c2..f823802 100644 --- a/module/proxy.nix +++ b/module/proxy.nix @@ -3,17 +3,40 @@ lib, ... }: let - inherit (lib.modules) mkIf; + inherit (lib.modules) mkIf mkDefault; cfg = config.services.piped; + + proxyConfig = cfg.proxy; + nginxConfig = proxyConfig.nginx; + + defaultNginxExtraConfig = '' + proxy_buffering on; + proxy_buffers 1024 16k; + proxy_set_header X-Forwarded-For ""; + proxy_set_header CF-Connecting-IP ""; + proxy_hide_header "alt-svc"; + sendfile on; + sendfile_max_chunk 512k; + tcp_nopush on; + aio threads=default; + aio_write on; + directio 16m; + proxy_hide_header Cache-Control; + proxy_hide_header etag; + proxy_http_version 1.1; + proxy_set_header Connection keep-alive; + proxy_max_temp_file_size 32m; + access_log off; + ''; in { - config = mkIf (cfg.enable && !cfg.disableProxy) { + config = mkIf (cfg.enable && proxyConfig.enable) { systemd.services.piped-proxy = { wantedBy = ["multi-user.target"]; - environment.BIND = "0.0.0.0:${toString cfg.internalProxyPort}"; - environment.IPV4_ONLY = mkIf cfg.proxyIPv4Only "1"; + environment.BIND = "0.0.0.0:${toString proxyConfig.internalPort}"; + environment.IPV4_ONLY = mkIf proxyConfig.proxyIPv4Only "1"; serviceConfig = { - ExecStart = "${cfg.proxyPackage}/bin/piped-proxy"; + ExecStart = "${proxyConfig.package}/bin/piped-proxy"; RestartSec = "5s"; User = "piped"; @@ -31,24 +54,28 @@ in { }; }; - services.nginx.virtualHosts."${cfg.proxyDomain}" = mkIf (!cfg.disableNginx) { - forceSSL = cfg.nginxForceSSL; - enableACME = cfg.nginxEnableACME; - locations."/" = { - proxyPass = "http://localhost:${toString cfg.internalProxyPort}"; - extraConfig = - cfg.proxyNginxExtraConfig - + '' + services.nginx = mkIf (!nginxConfig.disableNginx) { + enable = true; + + virtualHosts."${proxyConfig.domain}" = { + forceSSL = mkDefault nginxConfig.forceSSL; + enableACME = mkDefault nginxConfig.enableACME; + + locations."/" = { + proxyPass = "http://localhost:${toString proxyConfig.internalPort}"; + extraConfig = '' + ${defaultNginxExtraConfig} add_header Cache-Control "public, max-age=604800"; ''; - }; - locations."~ (/videoplayback|/api/v4/|/api/manifest/)" = { - proxyPass = "http://localhost:${toString cfg.internalProxyPort}"; - extraConfig = - cfg.proxyNginxExtraConfig - + '' + }; + + locations."~ (/videoplayback|/api/v4/|/api/manifest/)" = { + proxyPass = "http://localhost:${toString proxyConfig.internalPort}"; + extraConfig = '' + ${defaultNginxExtraConfig} add_header Cache-Control private always; ''; + }; }; }; }; diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..95ade67 --- /dev/null +++ b/readme.md @@ -0,0 +1,52 @@ +# Piped-Flake +This is a flake which allows you to run Piped on NixOS. + +This should allow for more advanced and declarative configuration than the upstream docker containers. It also includes more documentation on config files. + +## How to run + +This should provide a working piped instance. + +You can look at the options in module/default.nix for more information. + +```nix +nixpkgs.overlays = [ inputs.piped-flake.overlays.default ]; +imports = [ inputs.piped-flake.nixosModules.default ]; + +services.piped = let + baseDomain = "example.org"; +in { + enable = true; + + frontend = { + domain = "${baseDomain}"; + }; + + backend = { + domain = "backend.${baseDomain}"; + }; + + proxy = { + domain = "proxy.${baseDomain}"; + }; +} +``` + +## Supported Systems + +`x86_64-linux` and `aarch64-linux` are both supported + +However if you are deploying to `aarch64-linux` from a non-arm64 host via `qemu-user`` or using aarch64 builders without `--max-jobs=0` then the build of piped-backend will fail. + +https://github.com/NixOS/nixpkgs/issues/255780 + +This appears to be a problem with upstream gradle. + +For now you will have to build on a `aarch64-linux` host or use one as builder with `--max-jobs=0` to not use local host to build. + +You can use the below on a `aarch64-linux` host to build and copy the built backend to your computer. + +```sh +nix build .#piped-backend --system aarch64-linux +nix-copy-closure --to root@host-ip --use-substitutes $(readlink result) +``` \ No newline at end of file