{ pkgs, lib, ... }: let zulip-includes = pkgs.stdenv.mkDerivation rec { pname = "zulip-include"; version = "8.4"; src = pkgs.fetchFromGitHub { owner = "zulip"; repo = "zulip"; rev = version; hash = "sha256-wsdVlJ0RDWsKwpvT0TsqqT8v5bubjsPDaGebRiIoQoQ="; }; installPhase = '' mkdir $out cp puppet/zulip/files/nginx/{dhparam.pem,uwsgi_params,zulip-include-frontend/*,zulip-include-common/*} $out sed -i "s/\/etc\/nginx\/zulip-include/\/nix\/store\/$(basename $out)/" $out/* ''; }; host = "son.katvayor.net"; home = "/var/lib/zulip"; loadbalancers = [ ]; in { # nginx.pp services.nginx = { enable = true; recommendedGzipSettings = true; sslDhparam = zulip-includes + "/dhparam.pem"; upstreams = { django.servers."unix:${home}/deployments/uwsgi-socket" = { }; localhost_sso.servers."127.0.0.1:8888" = { }; camo.servers."127.0.0.1:9292" = { }; # TODO later : mess with tornado tornado.servers."127.0.0.1:9800" = { keepalive = 10000; }; }; commonHttpConfig = let trusted-proto = if loadbalancers == [ ] then '' map $remote_addr $trusted_x_forwarded_proto { default $scheme; } map $http_x_forwarded_for $x_proxy_misconfiguration { default ""; "~." "No proxies configured in Zulip, but proxy headers detected from proxy at $remote_addr; see https://zulip.readthedocs.io/en/latest/production/reverse-proxies.html"; } '' else ''''; # TODO later : loadbalancers tornado_map = # TODO later : mess with tornado '' map "" $tornado_server { default http://tornado; } ''; in '' ${trusted-proto} ${tornado_map} ''; virtualHosts.${host} = { forceSSL = true; enableACME = true; extraConfig = '' error_page 502 503 504 /static/webpack-bundles/5xx.html; ''; locations = { # app "/local-static/" = { alias = "${home}/local-static/"; }; "/static/" = { alias = "${home}/prod-static/"; extraConfig = '' include ${zulip-includes}/headers; add_header Access-Control-Allow-Origin *; add_header Timing-Allow-Origin *; error_page 404 /django_static_404.html; location ~ '\.[0-9a-f]{12}\.|[./][0-9a-f]{20}\.' { include ${zulip-includes}/headers; add_header Access-Control-Allow-Origin *; add_header Timing-Allow-Origin *; add_header Cache-Control "public, max-age=31536000, immutable"; } ''; }; "/json/events" = { extraConfig = '' if ($request_method = 'OPTIONS') { # add_header does not propagate into/out of blocks, so this # include cannot be factored out include ${zulip-includes}/headers; add_header Allow 'OPTIONS, GET, DELETE' always; return 204; } if ($request_method !~ ^(GET|DELETE)$ ) { # add_header does not propagate into/out of blocks, so this # include cannot be factored out include ${zulip-includes}/headers; add_header Allow 'OPTIONS, GET, DELETE' always; return 405; } proxy_pass $tornado_server; include ${zulip-includes}/proxy_longpolling; ''; }; "/api/v1/events" = { extraConfig = '' if ($request_method = 'OPTIONS') { include ${zulip-includes}/tornado_cors_headers; add_header Allow 'OPTIONS, GET, DELETE' always; return 204; } if ($request_method !~ ^(GET|DELETE)$ ) { include ${zulip-includes}/headers; add_header Allow 'OPTIONS, GET, DELETE' always; return 405; } include ${zulip-includes}/tornado_cors_headers; proxy_pass $tornado_server; include ${zulip-includes}/proxy_longpolling; ''; }; "~ ^/internal/tornado/(\\d+)(/.*)$" = { extraConfig = '' internal; proxy_pass http://tornado$1$2$is_args$args; include ${zulip-includes}/proxy_longpolling; ''; }; "/" = { extraConfig = '' include ${zulip-includes}/uwsgi_params; ''; }; "/thumbnail" = { extraConfig = '' include ${zulip-includes}/api_headers; include ${zulip-includes}/uwsgi_params; ''; }; "/avatar" = { extraConfig = '' include ${zulip-includes}/api_headers; include ${zulip-includes}/uwsgi_params; ''; }; "/user_uploads" = { extraConfig = '' include ${zulip-includes}/api_headers; include ${zulip-includes}/uwsgi_params; ''; }; "/api/" = { extraConfig = '' include ${zulip-includes}/api_headers; include ${zulip-includes}/uwsgi_params; ''; }; # uploads-internal "~ ^/internal/s3/(?[^/\\s]+)/(?[^\\s]*)" = { extraConfig = '' internal; include ${zulip-includes}/headers; add_header Content-Security-Policy "default-src 'none'; media-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self'; object-src 'self'; plugin-types application/pdf;"; # The components of this path are originally double-URI-escaped # (see zerver/view/upload.py). "location" matches are on # unescaped values, which fills $s3_path with a properly # single-escaped path to pass to the upstream server. # (see associated commit message for more details) set $download_url https://$s3_hostname/$s3_path; proxy_set_header Host $s3_hostname; proxy_ssl_name $s3_hostname; proxy_ssl_server_name on; # Strip off X-amz-cf-id header, which otherwise the request has to # have been signed over, leading to signature mismatches. proxy_set_header x-amz-cf-id ""; # Strip off any auth request headers which the Zulip client might # have sent, as they will not work for S3, and will report an error due # to the signed auth header we also provide. proxy_set_header Authorization ""; proxy_set_header x-amz-security-token ""; # These headers are only valid if there is a body, but better to # strip them to be safe. proxy_set_header Content-Length ""; proxy_set_header Content-Type ""; proxy_set_header Content-MD5 ""; proxy_set_header x-amz-content-sha256 ""; proxy_set_header Expect ""; # Ensure that we only get _one_ of these response headers: the one # that Django added, not the one from S3. proxy_hide_header Cache-Control; proxy_hide_header Expires; proxy_hide_header Set-Cookie; # We are _leaving_ S3 to provide Content-Type, # Content-Disposition, and Accept-Ranges headers, which are the # three remaining headers which nginx would also pass through from # the first response. Django explicitly unsets the first, and # does not set the latter two. proxy_pass $download_url$is_args$args; proxy_cache uploads; # If the S3 response doesn't contain Cache-Control headers (which # we don't expect it to) then we assume they are valid for a very # long time. The size of the cache is controlled by # `s3_disk_cache_size` and read frequency, set via # `s3_cache_inactive_time`. proxy_cache_valid 200 1y; # We only include the requested content-disposition in the cache # key, so that we cache "Content-Disposition: attachment" # separately from the inline version. proxy_cache_key $download_url$s3_disposition_cache_key; ''; }; "/internal/local/uploads/" = { extraConfig = '' internal; include ${zulip-includes}/headers; add_header Content-Security-Policy "default-src 'none'; media-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self'; object-src 'self'; plugin-types application/pdf;"; # Django handles setting Content-Type, Content-Disposition, and Cache-Control. alias ${home}/uploads/files/; ''; }; "/internal/local/user_avatars/" = { extraConfig = '' internal; include ${zulip-includes}/headers; add_header Content-Security-Policy "default-src 'none' img-src 'self'"; include ${zulip-includes}/uploads.types; alias ${home}/uploads/avatars/; ''; }; # external_sso "/accounts/login/sso/" = { extraConfig = '' proxy_pass https://localhost_sso; include ${zulip-includes}/proxy; ''; }; # camo "/external_content/metrics" = { extraConfig = '' return 404; ''; }; "/external_content/" = { extraConfig = '' rewrite /external_content/(.*) /$1 break; proxy_pass http://camo; include ${zulip-includes}/proxy; ''; }; "/health/" = { extraConfig = lib.concatStringsSep "\n" ( map (host: "accept ${host};") loadbalancers ++ [ "deny all;" ] ); }; }; }; }; }