Move HTTP auth to PHP

This commit is contained in:
Alex Cabal 2022-07-14 12:50:21 -05:00
parent e290758a9a
commit 30442c0c62
11 changed files with 110 additions and 86 deletions

View file

@ -6,7 +6,7 @@ PHP 7+ is required.
```shell
# Install Apache, PHP, PHP-FPM, and various other dependencies.
sudo apt install -y git composer php-fpm php-cli php-gd php-xml php-apcu php-mbstring php-intl php-curl php-zip apache2 apache2-utils libfcgi0ldbl task-spooler ipv6calc mariadb-server libaprutil1-dbd-mysql attr libapache2-mod-xsendfile
sudo apt install -y git composer php-fpm php-cli php-gd php-xml php-apcu php-mbstring php-intl php-curl php-zip apache2 apache2-utils libfcgi0ldbl task-spooler ipv6calc mariadb-server attr libapache2-mod-xsendfile
# Create the site root and logs root and clone this repo into it.
sudo mkdir /standardebooks.org/
@ -26,7 +26,7 @@ echo -e "127.0.0.1\tstandardebooks.test" | sudo tee -a /etc/hosts
openssl req -x509 -nodes -days 99999 -newkey rsa:4096 -subj "/CN=standardebooks.test" -keyout /standardebooks.org/web/config/ssl/standardebooks.test.key -sha256 -out /standardebooks.org/web/config/ssl/standardebooks.test.crt
# Enable the necessary Apache modules.
sudo a2enmod headers expires ssl rewrite proxy proxy_fcgi authn_dbd authn_socache xsendfile
sudo a2enmod headers expires ssl rewrite proxy proxy_fcgi xsendfile
# Link and enable the SE Apache configuration file.
sudo ln -s /standardebooks.org/web/config/apache/standardebooks.test.conf /etc/apache2/sites-available/

View file

@ -273,8 +273,8 @@ Define webroot /standardebooks.org/web
RewriteCond expr "tolower(%{REQUEST_METHOD}) =~ /^post$/"
RewriteRule ^/polls/([^/\.]+)/votes$ /polls/votes/post.php?pollurlname=$1 [L]
# Feeds
# Rewrite old links to feeds
# Rewrite rules for feeds eeds
# Redirect old feed URLs
RewriteRule ^/(opds|rss|atom)(.*)$ /feeds/$1$2 [R=301,L]
# If we ask for /opds/all?query=xyz, rewrite that to the search page.
@ -283,31 +283,12 @@ Define webroot /standardebooks.org/web
RewriteRule ^/feeds/(atom|rss)/([^/\.]+)$ /feeds/collection.php?type=$1&name=$2
RewriteRule ^/feeds/(.+\.xml)$ /feeds/download.php?path=$1
# Rewrite rules for bulk downloads
RewriteRule ^/bulk-downloads/(.+\.zip)$ /bulk-downloads/download.php?path=$1
RewriteRule ^/bulk-downloads/([^/\.]+)$ /bulk-downloads/collection.php?name=$1
# Enable mod_authn_dbd
# DBDriver mysql
# DBDParams "dbname=se user=www-data"
# # HTTP Basic Auth configuration for /feeds
# <DirectoryMatch "^${webroot}/www/feeds/(opds|rss|atom)">
# AuthType Basic
# AuthName "Enter your Patrons Circle email address and leave the password empty."
# Require valid-user
# # Credentials caching to prevent slamming the DB. socache must be ahead of dbd
# AuthBasicProvider socache dbd
# AuthnCacheProvideFor dbd
# AuthnCacheContext ${domain}
# # mod_authn_dbd SQL query to authenticate a user
# # The hash is simply the hash of a blank password. We're only interested in the username/API key.
# # We have to do this tortured query instead of a cleaner one, because the AuthDBDUserPWQuery
# # function will only replace %s EXACTLY ONCE. We cannot have more than one %s in the query string.
# AuthDBDUserPWQuery "select '$apr1$13q1pnGf$vQnIj94BXP1EPdL/4ISba.' from Users u inner join Benefits b using (UserId) where %s in (u.Email, u.Uuid) and b.CanAccessFeeds = true limit 1"
# </DirectoryMatch>
# Specific config for /bulk-downloads
<DirectoryMatch "${webroot}/www/bulk-downloads">
# Both directives are required
@ -316,22 +297,11 @@ Define webroot /standardebooks.org/web
</DirectoryMatch>
# Specific config for /feeds
<DirectoryMatch "^${webroot}/www/feeds/(opds|rss|atom)">
ErrorDocument 401 /feeds/401
<FilesMatch "^(style\.php|new-releases\.xml|index\.php|index\.xml)$">
# Disable HTTP Basic auth for the feed XSL stylesheet and the new releases feeds
Require all granted
</FilesMatch>
</DirectoryMatch>
# Emit content-types for OPDS feeds, as some clients require a strictly correct content-type in order to work
<DirectoryMatch "^${webroot}/www/feeds/opds">
Header set Content-Type "application/atom+xml;profile=opds-catalog;kind=acquisition; charset=utf-8"
<FilesMatch "^index\.xml$">
Header set Content-Type "application/atom+xml;profile=opds-catalog;kind=navigation; charset=utf-8"
</FilesMatch>
<DirectoryMatch "^${webroot}/www/feeds">
# This must be defined at the top level /feeds/ directory
# Both directives are required
XSendFile on
XSendFilePath /standardebooks.org/web/www/feeds
</DirectoryMatch>
# Emit content-types for RSS/Atom feeds

View file

@ -255,8 +255,8 @@ Define webroot /standardebooks.org/web
RewriteCond expr "tolower(%{REQUEST_METHOD}) =~ /^post$/"
RewriteRule ^/polls/([^/\.]+)/votes$ /polls/votes/post.php?pollurlname=$1 [L]
# Feeds
# Rewrite old links to feeds
# Rewrite rules for feeds eeds
# Redirect old feed URLs
RewriteRule ^/(opds|rss|atom)(.*)$ /feeds/$1$2 [R=301,L]
# If we ask for /opds/all?query=xyz, rewrite that to the search page.
@ -265,31 +265,12 @@ Define webroot /standardebooks.org/web
RewriteRule ^/feeds/(atom|rss)/([^/\.]+)$ /feeds/collection.php?type=$1&name=$2
RewriteRule ^/feeds/(.+\.xml)$ /feeds/download.php?path=$1
# Rewrite rules for bulk downloads
RewriteRule ^/bulk-downloads/(.+\.zip)$ /bulk-downloads/download.php?path=$1
RewriteRule ^/bulk-downloads/([^/\.]+)$ /bulk-downloads/collection.php?name=$1
# Enable mod_authn_dbd
DBDriver mysql
DBDParams "dbname=se user=www-data"
# HTTP Basic Auth configuration for /feeds
<DirectoryMatch "^${webroot}/www/feeds/(opds|rss|atom)">
AuthType Basic
AuthName "Enter your Patrons Circle email address and leave the password empty."
Require valid-user
# Credentials caching to prevent slamming the DB. socache must be ahead of dbd
AuthBasicProvider socache dbd
AuthnCacheProvideFor dbd
AuthnCacheContext ${domain}
# mod_authn_dbd SQL query to authenticate a user
# The hash is simply the hash of a blank password. We're only interested in the username/API key.
# We have to do this tortured query instead of a cleaner one, because the AuthDBDUserPWQuery
# function will only replace %s EXACTLY ONCE. We cannot have more than one %s in the query string.
AuthDBDUserPWQuery "select '$apr1$13q1pnGf$vQnIj94BXP1EPdL/4ISba.' from Users u inner join Benefits b using (UserId) where %s in (u.Email, u.Uuid) and b.CanAccessFeeds = true limit 1"
</DirectoryMatch>
# Specific config for /bulk-downloads
<DirectoryMatch "${webroot}/www/bulk-downloads">
# Both directives are required
@ -298,22 +279,11 @@ Define webroot /standardebooks.org/web
</DirectoryMatch>
# Specific config for /feeds
<DirectoryMatch "^${webroot}/www/feeds/(opds|rss|atom)">
ErrorDocument 401 /feeds/401
<FilesMatch "^(style\.php|new-releases\.xml|index\.php|index\.xml)$">
# Disable HTTP Basic auth for the feed XSL stylesheet and the new releases feeds
Require all granted
</FilesMatch>
</DirectoryMatch>
# Emit content-types for OPDS feeds, as some clients require a strictly correct content-type in order to work
<DirectoryMatch "^${webroot}/www/feeds/opds">
Header set Content-Type "application/atom+xml;profile=opds-catalog;kind=acquisition; charset=utf-8"
<FilesMatch "^index\.xml$">
Header set Content-Type "application/atom+xml;profile=opds-catalog;kind=navigation; charset=utf-8"
</FilesMatch>
<DirectoryMatch "^${webroot}/www/feeds">
# This must be defined at the top level /feeds/ directory
# Both directives are required
XSendFile on
XSendFilePath /standardebooks.org/web/www/feeds
</DirectoryMatch>
# Emit content-types for RSS/Atom feeds

View file

@ -0,0 +1,6 @@
CREATE TABLE `FeedUserAgents` (
`UserAgentId` int(11) unsigned NOT NULL AUTO_INCREMENT,
`UserAgent` text NOT NULL,
`Created` datetime NOT NULL,
PRIMARY KEY (`UserAgentId`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

View file

@ -37,6 +37,12 @@ if($GLOBALS['User'] === null){
// log them in while we're here.
$session = new Session();
try{
$session->Create($httpBasicAuthLogin);
$GLOBALS['User'] = $session->User;
}
catch(Exception $ex){
// Do nothing
}
}
}

View file

@ -26,7 +26,7 @@ try{
// Everything OK, serve the file using Apache.
// The xsendfile Apache module tells Apache to serve the file, including not-modified or etag headers.
// Much more efficien than reading it in PHP and outputting it that way.
// Much more efficient than reading it in PHP and outputting it that way.
header('X-Sendfile: ' . WEB_ROOT . $path);
header('Content-Type: application/zip');
header('Content-Disposition: attachment; filename="' . basename($path) . '"');

View file

@ -17,7 +17,7 @@ require_once('Core.php');
<p>The fifteen latest Standard Ebooks, most-recently-released first.</p>
</li>
<li>
<p><a href="<? if($GLOBALS['User'] !== null){ ?>https://<?= rawurlencode($GLOBALS['User']->Email) ?>@<?= SITE_DOMAIN ?><? } ?>/feeds/atom/all">All ebooks</a></p>
<p><a href="/feeds/atom/all">All ebooks</a></p>
<p class="url"><? if($GLOBALS['User'] !== null){ ?>https://<?= rawurlencode($GLOBALS['User']->Email) ?>@<?= SITE_DOMAIN ?><? }else{ ?><?= SITE_URL ?><? } ?>/feeds/atom/all</p>
<p>All Standard Ebooks, most-recently-released first.</p>
</li>

View file

@ -68,7 +68,7 @@ catch(Safe\Exceptions\ApcuException $ex){
<ul class="feed">
<? foreach($feeds as $feed){ ?>
<li>
<p><a href="<? if($GLOBALS['User'] !== null){ ?>https://<?= rawurlencode($GLOBALS['User']->Email) ?>@<?= SITE_DOMAIN ?><? } ?><?= Formatter::ToPlainText($feed->Url) ?>"><?= Formatter::ToPlainText($feed->Label) ?></a></p>
<p><a href="<?= Formatter::ToPlainText($feed->Url) ?>"><?= Formatter::ToPlainText($feed->Label) ?></a></p>
<p class="url"><? if($GLOBALS['User'] !== null){ ?>https://<?= rawurlencode($GLOBALS['User']->Email) ?>@<?= SITE_DOMAIN ?><? }else{ ?><?= SITE_URL ?><? } ?><?= Formatter::ToPlainText($feed->Url) ?></p>
</li>
<? } ?>

72
www/feeds/download.php Normal file
View file

@ -0,0 +1,72 @@
<?
require_once('Core.php');
use function Safe\preg_match;
// This page is blocked by HTTP Basic auth.
// Basic authorization is handled in Core.php. By the time we get here,
// a valid user has a session.
$path = HttpInput::Str(GET, 'path', false) ?? '';
$isUserAgentAllowed = false;
try{
$path = '/feeds/' . $path;
if(!is_file(WEB_ROOT . $path)){
throw new Exceptions\InvalidFileException();
}
// Certain user agents may bypass login entirely
if(isset($_SERVER['HTTP_USER_AGENT'])){
$isUserAgentAllowed = (bool)Db::QueryInt('select count(*) from FeedUserAgents where instr(?, UserAgent) limit 1', [$_SERVER['HTTP_USER_AGENT']]);
}
if(!$isUserAgentAllowed){
if($GLOBALS['User'] === null){
throw new Exceptions\LoginRequiredException();
}
if(!preg_match('/\.xml$/ius', $path)){
throw new Exceptions\InvalidPermissionsException();
}
if(!$GLOBALS['User']->Benefits->CanAccessFeeds){
throw new Exceptions\InvalidPermissionsException();
}
}
// Everything OK, serve the file using Apache.
// The xsendfile Apache module tells Apache to serve the file, including not-modified or etag headers.
// Much more efficient than reading it in PHP and outputting it that way.
header('X-Sendfile: ' . WEB_ROOT . $path);
if(preg_match('/^\/feeds\/opds/', $path)){
header('Content-Type: application/atom+xml;profile=opds-catalog;kind=acquisition; charset=utf-8');
if(preg_match('/\/index\.xml$/', $path)){
header('Content-Type: application/atom+xml;profile=opds-catalog;kind=navigation; charset=utf-8');
}
}
elseif(preg_match('/^\/feeds\/rss/', $path)){
header('Content-Type: application/rss+xml');
}
elseif(preg_match('/^\/feeds\/atom/', $path)){
header('Content-Type: application/atom+xml');
}
exit();
}
catch(Exceptions\LoginRequiredException $ex){
header('WWW-Authenticate: Basic realm="Enter your Patrons Circle email address and leave the password empty."');
http_response_code(401);
}
catch(Exceptions\InvalidPermissionsException $ex){
http_response_code(403);
}
catch(Exceptions\InvalidFileException $ex){
Template::Emit404();
}
// Print the login info page
include(WEB_ROOT . '/feeds/401.php');

View file

@ -64,7 +64,7 @@ catch(Exceptions\InvalidCollectionException $ex){
<? } ?>
<ul class="feed">
<li>
<p><a href="<? if($GLOBALS['User'] !== null){ ?>https://<?= rawurlencode($GLOBALS['User']->Email) ?>@<?= SITE_DOMAIN ?><? } ?>/feeds/<?= $type ?>/<?= $name ?>/<?= $target?>"><?= Formatter::ToPlainText($label) ?></a></p>
<p><a href="/feeds/<?= $type ?>/<?= $name ?>/<?= $target?>"><?= Formatter::ToPlainText($label) ?></a></p>
<p class="url"><? if($GLOBALS['User'] !== null){ ?>https://<?= rawurlencode($GLOBALS['User']->Email) ?>@<?= SITE_DOMAIN ?><? }else{ ?><?= SITE_URL ?><? } ?>/feeds/<?= $type ?>/<?= $name ?>/<?= $target?></p>
</li>
</ul>

View file

@ -19,7 +19,7 @@ require_once('Core.php');
<p>To connect your ereading app to our catalog, enter the URL below when prompted by your app:</p>
<ul class="feed">
<li>
<p><a href="<? if($GLOBALS['User'] !== null){ ?>https://<?= rawurlencode($GLOBALS['User']->Email) ?>@<?= SITE_DOMAIN ?><? } ?>/feeds/opds">The Standard Ebooks OPDS feed</a></p>
<p><a href="/feeds/opds">The Standard Ebooks OPDS feed</a></p>
<p class="url"><? if($GLOBALS['User'] !== null){ ?>https://<?= rawurlencode($GLOBALS['User']->Email) ?>@<?= SITE_DOMAIN ?><? }else{ ?><?= SITE_URL ?><? } ?>/feeds/opds</p>
</li>
</ul>