# Minimum Nginx version required:  1.15.3 (released Aug 28, 2018)
# Please check your Nginx installation features the following modules via 'nginx -V':
# STANDARD HTTP MODULES: Core, Proxy, Rewrite, Access, Gzip, Headers, HTTP/2, Log, Real IP, SSL, Thread Pool, Upstream, AIO Multithreading, Map.
# THIRD PARTY MODULES:   None.

server {
  listen 80;
  listen [::]:80;
  server_name ${WEBSERVER_HOST};

  server_tokens off; # don't advertise the nginx version in responses and error pages

  location ^~ /.well-known/acme-challenge/ {
    default_type "text/plain";
    root /var/www/certbot;
  }
  location / { return 301 https://$host$request_uri; }
}

upstream backend {
  server ${PEERTUBE_HOST};

  keepalive         16;

  # Must stay below the keep-alive timeout of PeerTube (5s, NodeJS default)
  keepalive_timeout 4s;
}

##
# ---------------------------------------------------------------------------
# Horizontal scalability
# ---------------------------------------------------------------------------
#
# Leave this section untouched if you run a single PeerTube process.
#
# Horizontal scalability allows to offload most of the requests to `--role=secondary` processes.
# See https://docs.joinpeertube.org/maintain/horizontal-scalability for more information.
#
# If you want to enable horizontal scalability, add one `server` line per secondary to the pools below.
# `secondary_sticky_pool` sends the requests of a client to the same process (view/watch-time tracking and socket.io connections)
#
# For example:
#
#   upstream secondary_pool {
#     server 127.0.0.1:9100;
#     server 127.0.0.1:9101;
#
#     # Add `backup` keyword if you want this server to be used only when the secondary ones are not available
#     server ${PEERTUBE_HOST};
#
#     keepalive         16;
#     keepalive_timeout 4s;
#   }
#
#   upstream secondary_sticky_pool {
#     hash $remote_addr consistent;
#
#     server 127.0.0.1:9100;
#     server 127.0.0.1:9101;
#
#     server ${PEERTUBE_HOST};
#
#     keepalive         16;
#     keepalive_timeout 4s;
#   }
#
# If a CDN or another load balancer sits in front, set `set_real_ip_from` / `real_ip_header` nginx directives.
##

upstream secondary_pool {
  server ${PEERTUBE_HOST};

  keepalive         16;
  keepalive_timeout 4s;
}

upstream secondary_sticky_pool {
  hash $remote_addr consistent;

  server ${PEERTUBE_HOST};

  keepalive         16;
  keepalive_timeout 4s;
}

# Leave this section untouched, even if you don't use horizontal scalability.
# Every request is routed to `secondary_pool`, except the ones listed below.
#
# A secondary answers `421 Misdirected Request` to unsupported endpoints, and `@api` replays these requests on the primary.
#
# This list is not exhaustive:
#  * it avoids this round trip for the frequent primary-only requests,
#  * it routes the requests that cannot be replayed because nginx streams their body (`proxy_request_buffering off` locations)
#
# method + path -> pool
map $request_method$uri $peertube_pool {
  default                                                  secondary_pool;

  # Frequent requests only served by the primary
  "~^[A-Z]+/(static|tracker)/"                             backend;
  "~^[A-Z]+/plugins/[^/]+(/[^/]+)?/(router|auth)(/|$)"     backend;
  "~^POST/api/v1/server/logs/client$"                      backend;

  # Unbuffered request only served by the primary: cannot be replayed
  "~^POST/api/v1/runners/jobs/[^/]+/update$"               backend;

  # View/watch-time tracking: sticky to one process per viewer
  "~^(PUT|POST)/api/v1/videos/[^/]+/(views|watching)$"     secondary_sticky_pool;
}

server {
  # On nginx >= 1.25.1 `http2` as a `listen` parameter is deprecated and logs a warning on startup
  # If your nginx is recent enough, replace the two lines below by `listen 443 ssl;` + `listen [::]:443 ssl;` + `http2 on;`
  listen 443 ssl http2;
  listen [::]:443 ssl http2;

  server_name ${WEBSERVER_HOST};

  # Don't advertise the nginx version in responses and error pages
  server_tokens off;

  access_log /var/log/nginx/peertube.access.log; # reduce I/O with buffer=10m flush=5m
  error_log  /var/log/nginx/peertube.error.log;

  ssl_certificate     /etc/letsencrypt/live/${WEBSERVER_HOST}/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/${WEBSERVER_HOST}/privkey.pem;

  location ^~ /.well-known/acme-challenge/ {
    default_type "text/plain";
    root /var/www/certbot;
  }

  ##
  # Security hardening based on the Mozilla "intermediate" guideline v5.7
  # The DHE ciphers of the guideline are left out because they require a `ssl_dhparam` file
  ##

  ssl_protocols             TLSv1.2 TLSv1.3;
  ssl_prefer_server_ciphers off;
  ssl_ciphers               ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305;
  ssl_session_timeout       1d; # defaults to 5m
  ssl_session_cache         shared:peertube_ssl:10m; # estimated to 40k sessions
  ssl_session_tickets       off;
  # HSTS (https://hstspreload.org)
  # A location that sets its own `add_header` drops this one: copy it in these locations
  #add_header Strict-Transport-Security "max-age=63072000" always;

  ##
  # Application
  # ---------------------------------------------------------------------------
  #
  # IMPORTANT, read before editing a `location` below.
  #
  # `try_files /dev/null @named` performs an internal redirect that adds subtleties:
  #   * `client_max_body_size` is read from the location matching the URI (the `location ~ ...` block)
  #     except for request bodies sent without `Content-Length` (for example chunked), that are checked against the `@named` block
  #   * `add_header` of the location matching the URI only applies to the responses nginx generates there (413, 403...),
  #     the headers of a proxied response are read from the `@named` block
  #   * `proxy_*` directives are read from the `@named` block
  ##

  # Inherited by the locations below that don't set their own `proxy_set_header` / `proxy_*_timeout`
  # A location that sets any `proxy_set_header` drops all these headers: repeat them there
  proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
  proxy_set_header Host            $host; # Use $http_host if you use a non standard HTTP port
  proxy_set_header X-Real-IP       $remote_addr;

  # Required by the `keepalive` of the upstreams
  proxy_set_header Connection      "";
  proxy_http_version 1.1;

  proxy_connect_timeout 10m;
  proxy_send_timeout    10m;
  proxy_read_timeout    10m;

  location @api {
    send_timeout 10m;

    # Only applies to request bodies sent without `Content-Length`
    # Matches the largest limit of the locations that use `try_files /dev/null @api`
    client_max_body_size 12M;

    proxy_pass http://$peertube_pool;

    # Only intercepts the status codes that have an `error_page`
    proxy_intercept_errors on;
    error_page 421 = @api_primary;
  }

  # Same as @api, on the primary
  location @api_primary {
    send_timeout 10m;

    proxy_pass http://backend;
  }

  location / {
    try_files /dev/null @api;
  }

  ##
  # Uploads
  #
  # The upload locations below stream the request body directly to PeerTube instead of writing it to /var/lib/nginx first
  # They proxy without `try_files` so `client_max_body_size` also applies to request bodies sent without `Content-Length`
  # We can't rely on the 421 error code for these endpoints because the request body is not kept
  ##

  # Resumable uploads (PeerTube checks the file size declared when the upload starts) and runner job results
  location ~ ^/api/v1/(videos/(upload-resumable|[^/]+/source/replace-resumable)|users/[^/]+/imports/import-resumable|runners/jobs/[^/]+/(update|success))$ {
    client_max_body_size 0;

    send_timeout            10m;
    proxy_request_buffering off;

    proxy_pass http://$peertube_pool;
  }

  location ~ ^/api/v1/videos/(upload|[^/]+/studio/edit)$ {
    limit_except POST HEAD { deny all; }

    # This is the maximum upload size, which roughly matches the maximum size of a video file
    client_max_body_size                      12G; # default is 1M
    # Maximum file size advertised to the client, with a margin for the request overhead (8G * 1.4 <= 12G)
    add_header            X-File-Maximum-Size 8G always;

    send_timeout            10m;
    proxy_request_buffering off;

    proxy_pass http://$peertube_pool;
  }

  # View tracking (PUT|POST /views and /watching)
  location ~ ^/api/v1/videos/[^/]+/(views|watching)$ {
    client_max_body_size 100k; # default is 1M

    try_files /dev/null @api;
  }

  # Endpoints that accept an image (avatar, banner, thumbnail, preview, channel logo, instance logo)
  location ~ ^/api/v1/(videos|video-playlists|video-channels|users/me|config/instance-(avatar|banner|logo)) {
    client_max_body_size                      12M; # default is 1M
    # Maximum file size advertised to the client, with a margin for the request overhead (8M * 1.4 <= 12M)
    add_header            X-File-Maximum-Size 8M always;

    try_files /dev/null @api;
  }

  ##
  # Websocket
  ##

  location @api_websocket {
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    # Use $http_host if you use a non standard HTTP port
    proxy_set_header Host            $host;
    proxy_set_header X-Real-IP       $remote_addr;
    proxy_set_header Upgrade         $http_upgrade;
    proxy_set_header Connection      "upgrade";

    # Peers send a message to the tracker every 15 minutes: don't close the websocket before then
    proxy_read_timeout 15m;

    proxy_pass http://backend;
  }

  # Same as @api_websocket, but socket.io connections can be served by a secondary process
  location @api_socket_io {
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    # Use $http_host if you use a non standard HTTP port
    proxy_set_header Host            $host;
    proxy_set_header X-Real-IP       $remote_addr;
    proxy_set_header Upgrade         $http_upgrade;
    proxy_set_header Connection      "upgrade";

    proxy_pass http://secondary_sticky_pool;
  }

  location /socket.io/ {
    try_files /dev/null @api_socket_io;
  }

  location /tracker/socket {
    try_files /dev/null @api_websocket;
  }

  # Plugin websocket routes
  location ~ ^/plugins/[^/]+(/[^/]+)?/ws/ {
    try_files /dev/null @api_websocket;
  }

  ##
  # Performance optimizations
  ##

  root /var/www/peertube/storage;

  # Compress the static client assets and the public XML feeds, for improved client load times
  gzip              on;
  gzip_vary         on;
  # Do not include `application/json` to prevent BREACH (CVE-2013-3587)
  # text/html is always compressed by HttpGzipModule
  gzip_types        text/css
                    application/javascript
                    font/truetype
                    font/opentype
                    application/vnd.ms-fontobject
                    image/svg+xml
                    # For /feeds/ endpoints and /sitemap.xml
                    text/xml
                    application/xml
                    application/rss+xml
                    application/atom+xml;
  gzip_min_length   1000;
  gzip_buffers      16 8k;
  gzip_comp_level   2;

  client_body_timeout       30s;
  client_header_timeout     10s;
  send_timeout              10s;
  keepalive_timeout         10s;
  resolver_timeout          10s;
  reset_timedout_connection on;
  proxy_ignore_client_abort on;

  # Send headers in one piece
  tcp_nopush                on;
  # Don't buffer data sent, good for small data bursts in real time
  tcp_nodelay               on;

  # Buffered request bodies (e.g. image uploads, up to 12M above) are written to /var/lib/nginx by default
  # The video upload locations above stream their body to PeerTube instead, so they don't use this directory
  # If you have a small /var/lib partition, you may want to store them in a different place
  # See https://nginx.org/en/docs/http/ngx_http_core_module.html#client_body_temp_path
  #client_body_temp_path /var/www/peertube/storage/nginx/;

  # Optional block
  # Bypass PeerTube for performance reasons
  # Should be consistent with client-overrides assets list in client.ts server controller
  location ~ ^/client/(assets/images/(default-playlist\.jpg|default-avatar-account\.png|default-avatar-account-48x48\.png|default-avatar-video-channel\.png|default-avatar-video-channel-48x48\.png))$ {
    add_header Cache-Control "public, max-age=31536000, immutable"; # Cache 1 year

    root /var/www/peertube;

    try_files /storage/client-overrides/$1 /peertube-latest/client/dist/$1 @api;
  }

  # Optional block
  # Bypass PeerTube for performance reasons
  location ~ ^/client/(.*\.(js|css|png|svg|woff2|otf|ttf|woff|eot))$ {
    add_header Cache-Control "public, max-age=31536000, immutable"; # Cache 1 year

    alias /var/www/peertube/peertube-latest/client/dist/$1;
  }

  # Proxied without `try_files`, to rate limit the proxied response
  location ~ ^(/static/(webseed|web-videos|streaming-playlists/hls)/private/)|^/object-storage-proxy/|^/download {
    # Prevent too much proxying, which results in high disk write IO
    proxy_max_temp_file_size 50M;
    proxy_limit_rate 5M;

    # Private static files are only served by the primary, private object storage files and downloads by any process
    proxy_pass http://$peertube_pool;
  }

  # Optional block
  # Bypass PeerTube for performance reasons
  location ~ ^/static/(?<peertube_static_path>(web-videos|redundancy|streaming-playlists)/.+)$ {
    limit_rate_after 5M;
    limit_rate       5M;

    # These files are public and unauthenticated, so they can be read cross origin
    add_header Access-Control-Allow-Origin  '*' always;
    add_header Access-Control-Allow-Methods 'GET, OPTIONS' always;
    add_header Access-Control-Allow-Headers 'Range,DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type' always;
    add_header Access-Control-Max-Age       1728000 always; # Preflight request can be cached 20 days

    # Keep this `if` free of `add_header` or it will replace the whole `add_header` above
    if ($request_method = 'OPTIONS') {
      return 204;
    }

    # Enables direct copying data from one file descriptor to another
    sendfile on;
    sendfile_max_chunk 1M; # prevent one fast connection from entirely occupying the worker process. should be > 800k.
    aio threads;

    try_files /$peertube_static_path @api;
  }
}
