# Global configuration; see https://securityheaders.com
Header set X-Frame-Options "sameorigin"
Header set X-Content-Type-Options "nosniff"
Header set X-Xss-Protection "1; mode=block"
Header set Referrer-Policy "no-referrer-when-downgrade"
ServerTokens prod
ServerSignature off
AddDefaultCharset utf-8
UseCanonicalName on
LogLevel warn
AddType image/avif .avif
AddType font/woff2 .woff2
AddOutputFilterByType deflate image/svg+xml
AddOutputFilterByType deflate application/xhtml+xml
AddOutputFilterByType deflate image/vnd.microsoft.icon image/x-icon
TraceEnable off
Protocols h2 h2c http/1.1
# Set up caching directives for infrequently changed files
ExpiresActive on
ExpiresByType application/javascript "access plus 6 months"
ExpiresByType text/javascript "access plus 6 months"
ExpiresByType font/woff2 "access plus 6 months"
ExpiresByType image/avif "access plus 6 months"
ExpiresByType image/gif "access plus 6 months"
ExpiresByType image/jpeg "access plus 6 months"
ExpiresByType image/png "access plus 6 months"
ExpiresByType image/svg+xml "access plus 6 months"
ExpiresByType image/vnd.microsoft.icon "access plus 6 months"
ExpiresByType image/x-icon "access plus 6 months"
ExpiresByType text/css "access plus 6 months"
# These lines are a workaround for an Apache bug that prevents mod_deflate, etags, and ExpiresByType working at the same time.
# This is probably still broken in 18.04. See https://stackoverflow.com/questions/896974/apache-is-not-sending-304-response-if-mod-deflate-and-addoutputfilterbytype-is
# Can possibly be fixed in Ubuntu 22.04 using https://httpd.apache.org/docs/trunk/mod/mod_deflate.html#deflatealteretag
FileETag All
RequestHeader edit "If-None-Match" "^\"(.*)-gzip\"$" "\"$1\""
Header edit "ETag" "^\"(.*[^g][^z][^i][^p])\"$" "\"$1-gzip\""
# SSL hardening; see https://mozilla.github.io/server-side-tls/ssl-config-generator/
SSLProtocol all -SSLv3 -TLSv1 -TLSv1.1
SSLCipherSuite ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256
SSLHonorCipherOrder on
SSLCompression off
SSLSessionTickets off
SSLStaplingCache shmcb:/var/run/ocsp(128000)
# SSL Stapling should be off for testing to prevent errors in log files, and on for live
SSLUseStapling off
SSLStaplingResponderTimeout 5
SSLStaplingReturnResponderErrors off
Define domain standardebooks.test
Define web_root /standardebooks.org/web
Define conf_rewrite_root ${web_root}/config/apache/rewrites
ServerName ${domain}
ServerAlias www.${domain}
RedirectPermanent / https://${domain}/
ServerName ${domain}
ServerAlias www.${domain}
DocumentRoot ${web_root}/www
ErrorDocument 404 /404
ErrorDocument 451 /451
ErrorLog /var/log/local/www-error.log
DirectorySlash Off
RewriteEngine on
SSLEngine on
SSLCertificateFile ${web_root}/config/ssl/${domain}.crt
SSLCertificateKeyFile ${web_root}/config/ssl/${domain}.key
Header always set Strict-Transport-Security "max-age=15768000"
Header set Content-Security-Policy "default-src 'self';"
# Disable .htaccess files
AllowOverride none
# Disable unneeded options
Options none
# Allow access to www/
Require all granted
# Pass HTTP Authorization headers to PHP-FPM
CGIPassAuth on
AddType application/x-mobi8-ebook .azw3
# Serve distributables using the "download" dialog instead of opening in-browser
# Note: the trailing e in the Header directive is required
# In modern browsers this is handled with the a@download attribute, we keep this here for compatibility
SetEnvIf Request_URI ^/ebooks/.+?/downloads/(.+)$ FILENAME=$1
Header set Content-Disposition "attachment; filename=%{FILENAME}e"
# We explicitly set the content-type for items in the /vocab/ directory, because Apache doesn't set it for us,
# and we need a content-type header when using the "nosniff" header. See https://bugzilla.mozilla.org/show_bug.cgi?id=1547076
Header set Content-Type "text/plain"
# Enable HTTP CORS so that browser-based readers like Readium can access opds and ebooks
# Allow fonts for newsletter emails
# See https://github.com/standardebooks/tools/issues/2
Header set Access-Control-Allow-Origin "*"
# We use a different CSP policy for single-page files because our default one doesn't allow inline images or CSS
Header set Content-Security-Policy "default-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline';"
# Remove www from requests
RewriteCond %{HTTP_HOST} ^www\.(.+) [NC]
RewriteRule ^ https://%1%{REQUEST_URI} [R=301,L]
# PHP-FPM configuration
# See https://serverfault.com/questions/450628/apache-2-4-php-fpm-proxypassmatch/510784
# Required for FPM to receive POST data sent with Transfer-Encoding: chunked
SetEnv proxy-sendcl 1
# Forward all PHP requests to the php-fpm pool for this domain.
SetHandler "proxy:unix:///run/php/${domain}.sock|fcgi://${domain}"
Header set Cache-Control "no-store"
# Set some proxy properties.
ProxySet connectiontimeout=5 timeout=240
# In RewriteCond, RewriteRule gets evaluated BEFORE RewriteCond, so $1 refers to the first
# match in RewriteRule
# Rewrite POST /some/url -> POST /some/url/post.php
RewriteCond expr "tolower(%{REQUEST_METHOD}) =~ /^(post|delete|put)$/"
RewriteCond %{DOCUMENT_ROOT}/$1/%1.php -f
RewriteRule ^([^\.]+)$ $1/%1.php [L]
# In case of 404, serve the 404 page specified by ErrorDocument, not the default FPM error page.
# Note that we can't use `ProxyErrorOverride on` because that catches ALL 4xx and 5xx HTTP headers
# and serves the default Apache page for them.
RewriteCond %{REQUEST_FILENAME} \.php$
RewriteCond %{DOCUMENT_ROOT}/%{REQUEST_URI} !-f
RewriteRule (.*) - [H=text/html]
# Received: /filename.php and /filename.php exists in filesystem; Result: 301 redirect to /filename and restart request
RewriteCond %{REQUEST_FILENAME} \.php$
RewriteCond %{DOCUMENT_ROOT}/%{REQUEST_URI} -f
RewriteRule ^/(.+)\.php$ /$1 [R=301,L]
# Received: /filename and /filename.php exists in filesystem; Result: change /filename to /filename.php and continue processing
RewriteCond %{DOCUMENT_ROOT}/%{REQUEST_URI} !-f
RewriteCond %{DOCUMENT_ROOT}/%{REQUEST_URI} !-d
RewriteCond %{DOCUMENT_ROOT}/%{REQUEST_URI}.php -f
RewriteRule ^(.+)$ $1.php [QSA]
# End PHP-FPM configuration
# Received: /filename and /filename.xml exists in filesystem; Result: rewrite to /filename.xml
RewriteCond %{DOCUMENT_ROOT}%{REQUEST_FILENAME}.xml -f
RewriteRule (.*) $1.xml
# Remove trailing slashes
RewriteRule ^/(.+?)/$ /$1 [R=301,L]
# Redirect ToC of XHTML representation of books
RewriteRule ^/ebooks/(.+?)/text$ /ebooks/$1/toc.xhtml [L]
# Received: /filename and /filename.xhtml exists in filesystem; Result: rewrite to /filename.xhtml
RewriteCond %{DOCUMENT_ROOT}%{REQUEST_FILENAME}.xhtml -f
RewriteRule (.*) $1.xhtml
# Redirect index pages
RewriteCond %{DOCUMENT_ROOT}%{REQUEST_FILENAME} -d
RewriteCond %{DOCUMENT_ROOT}%{REQUEST_FILENAME}/index.php -f
RewriteRule (.*) $1/index.php
RewriteCond %{DOCUMENT_ROOT}%{REQUEST_FILENAME} -d
RewriteCond %{DOCUMENT_ROOT}%{REQUEST_FILENAME}/index.xml -f
RewriteRule (.*) $1/index.xml
# Remove newline characters inserted by accident in some email clients
RewriteRule ^(.*)\r\n[\ ]?(.*)$ $1$2 [R=301,N]
RewriteRule ^(.*)/r/n[\ ]?(.*)$ $1$2 [R=301,N]
RewriteRule ^(.*)/[rn]$ $1 [R=301,N]
Include ${conf_rewrite_root}/misc.conf
Include ${conf_rewrite_root}/feeds.conf
Include ${conf_rewrite_root}/ebooks.conf
Include ${conf_rewrite_root}/newsletters.conf
Include ${conf_rewrite_root}/artworks.conf
Include ${conf_rewrite_root}/polls.conf
Include ${conf_rewrite_root}/users.conf
# Specific config for /ebooks///downloads
# Both directives are required
XSendFile on
XSendFilePath ${web_root}/www/ebooks
# Specific config for /bulk-downloads
# Both directives are required
XSendFile on
XSendFilePath ${web_root}/www/bulk-downloads
# Specific config for /feeds
# This must be defined at the top level /feeds/ directory
# Both directives are required
XSendFile on
XSendFilePath ${web_root}/www/feeds