From e58c867a82f4c884eaeda93519ed8464d052837c Mon Sep 17 00:00:00 2001 From: Dag Date: Thu, 25 Jan 2024 18:20:02 +0100 Subject: [PATCH] feat: token authentication (#3927) --- README.md | 30 +++++++------- actions/DisplayAction.php | 1 + actions/FrontpageAction.php | 7 +--- actions/SetBridgeCacheAction.php | 68 -------------------------------- config.default.ini.php | 14 ++----- lib/BridgeCard.php | 39 ++++++++---------- lib/RssBridge.php | 18 +++++++++ lib/http.php | 14 +++++++ static/connectivity.js | 2 +- static/style.css | 2 +- templates/base.html.php | 2 + templates/frontpage.html.php | 16 +------- templates/token.html.php | 20 ++++++++++ 13 files changed, 95 insertions(+), 138 deletions(-) delete mode 100644 actions/SetBridgeCacheAction.php create mode 100644 templates/token.html.php diff --git a/README.md b/README.md index d6d1046c..8e8ae117 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,13 @@ Officially hosted instance: https://rss-bridge.org/bridge01/ IRC channel #rssbridge at https://libera.chat/ +[Full documentation](https://rss-bridge.github.io/rss-bridge/index.html) + +Alternatively find another +[public instance](https://rss-bridge.github.io/rss-bridge/General/Public_Hosts.html). + +Requires minimum PHP 7.4. + [![LICENSE](https://img.shields.io/badge/license-UNLICENSE-blue.svg)](UNLICENSE) [![GitHub release](https://img.shields.io/github/release/rss-bridge/rss-bridge.svg?logo=github)](https://github.com/rss-bridge/rss-bridge/releases/latest) @@ -44,15 +51,6 @@ IRC channel #rssbridge at https://libera.chat/ * `YoutubeBridge`: [Fetches videos by username/channel/playlist/search](https://rss-bridge.org/bridge01/#bridge-YoutubeBridge) * `YouTubeCommunityTabBridge`: [Fetches posts from a channel's community tab](https://rss-bridge.org/bridge01/#bridge-YouTubeCommunityTabBridge) -[Full documentation](https://rss-bridge.github.io/rss-bridge/index.html) - -Check out RSS-Bridge right now on https://rss-bridge.org/bridge01/ - -Alternatively find another -[public instance](https://rss-bridge.github.io/rss-bridge/General/Public_Hosts.html). - -Requires minimum PHP 7.4. - ## Tutorial ### How to install on traditional shared web hosting @@ -259,6 +257,14 @@ Learn more in ## How-to +### How to password-protect the instance (token) + +Modify `config.ini.php`: + + [authentication] + + token = "hunter2" + ### How to remove all cache items As current user: @@ -332,8 +338,6 @@ Learn more in [bridge api](https://rss-bridge.github.io/rss-bridge/Bridge_API/in ### How to enable all bridges -Modify `config.ini.php`: - enabled_bridges[] = * ### How to enable some bridges @@ -390,9 +394,7 @@ Modify `report_limit` so that an error must occur 3 times before it is reported. The report count is reset to 0 each day. -### How to password-protect the instance - -HTTP basic access authentication: +### How to password-protect the instance (HTTP Basic Auth) [authentication] diff --git a/actions/DisplayAction.php b/actions/DisplayAction.php index 915eace5..ed063825 100644 --- a/actions/DisplayAction.php +++ b/actions/DisplayAction.php @@ -102,6 +102,7 @@ class DisplayAction implements ActionInterface $bridge->loadConfiguration(); // Remove parameters that don't concern bridges $remove = [ + 'token', 'action', 'bridge', 'format', diff --git a/actions/FrontpageAction.php b/actions/FrontpageAction.php index c0f819d0..32795c45 100644 --- a/actions/FrontpageAction.php +++ b/actions/FrontpageAction.php @@ -4,8 +4,6 @@ final class FrontpageAction implements ActionInterface { public function execute(Request $request) { - $showInactive = (bool) $request->get('show_inactive'); - $messages = []; $activeBridges = 0; @@ -22,10 +20,8 @@ final class FrontpageAction implements ActionInterface $body = ''; foreach ($bridgeClassNames as $bridgeClassName) { if ($bridgeFactory->isEnabled($bridgeClassName)) { - $body .= BridgeCard::render($bridgeClassName); + $body .= BridgeCard::render($bridgeClassName, $request); $activeBridges++; - } elseif ($showInactive) { - $body .= BridgeCard::render($bridgeClassName, false) . "\n"; } } @@ -37,7 +33,6 @@ final class FrontpageAction implements ActionInterface 'bridges' => $body, 'active_bridges' => $activeBridges, 'total_bridges' => count($bridgeClassNames), - 'show_inactive' => $showInactive, ]); } } diff --git a/actions/SetBridgeCacheAction.php b/actions/SetBridgeCacheAction.php deleted file mode 100644 index 5b1c6f53..00000000 --- a/actions/SetBridgeCacheAction.php +++ /dev/null @@ -1,68 +0,0 @@ -cache = RssBridge::getCache(); - } - - public function execute(Request $request) - { - $requestArray = $request->toArray(); - - // Authentication - $accessTokenInConfig = Configuration::getConfig('authentication', 'access_token'); - if (!$accessTokenInConfig) { - return new Response('Access token is not set in this instance', 403, ['content-type' => 'text/plain']); - } - if (isset($requestArray['access_token'])) { - $accessTokenGiven = $requestArray['access_token']; - } else { - $header = trim($_SERVER['HTTP_AUTHORIZATION'] ?? ''); - $position = strrpos($header, 'Bearer '); - if ($position !== false) { - $accessTokenGiven = substr($header, $position + 7); - } else { - $accessTokenGiven = ''; - } - } - if (!$accessTokenGiven) { - return new Response('No access token given', 403, ['content-type' => 'text/plain']); - } - if (! hash_equals($accessTokenInConfig, $accessTokenGiven)) { - return new Response('Incorrect access token', 403, ['content-type' => 'text/plain']); - } - - // Begin actual work - $key = $requestArray['key'] ?? null; - if (!$key) { - return new Response('You must specify key', 400, ['content-type' => 'text/plain']); - } - - $bridgeFactory = new BridgeFactory(); - - $bridgeName = $requestArray['bridge'] ?? null; - $bridgeClassName = $bridgeFactory->createBridgeClassName($bridgeName); - if (!$bridgeClassName) { - return new Response(sprintf('Bridge not found: %s', $bridgeName), 400, ['content-type' => 'text/plain']); - } - - // whitelist control - if (!$bridgeFactory->isEnabled($bridgeClassName)) { - return new Response('This bridge is not whitelisted', 401, ['content-type' => 'text/plain']); - } - - $bridge = $bridgeFactory->create($bridgeClassName); - $bridge->loadConfiguration(); - $value = $requestArray['value']; - - $cacheKey = get_class($bridge) . '_' . $key; - $ttl = 86400 * 3; - $this->cache->set($cacheKey, $value, $ttl); - - return new Response('done', 200, ['Content-Type' => 'text/plain']); - } -} diff --git a/config.default.ini.php b/config.default.ini.php index ee1e54c9..91a4c4fe 100644 --- a/config.default.ini.php +++ b/config.default.ini.php @@ -102,21 +102,13 @@ by_bridge = false [authentication] -; Enables basic authentication for all requests to this RSS-Bridge instance. -; -; Warning: You'll have to upgrade existing feeds after enabling this option! -; -; true = enabled -; false = disabled (default) +; HTTP basic authentication enable = false - username = "admin" - -; The password cannot be the empty string if authentication is enabled. password = "" -; This will be used only for actions that require privileged access -access_token = "" +; Token authentication (URL) +token = "" [error] diff --git a/lib/BridgeCard.php b/lib/BridgeCard.php index 6b812740..e5456f33 100644 --- a/lib/BridgeCard.php +++ b/lib/BridgeCard.php @@ -2,14 +2,7 @@ final class BridgeCard { - /** - * Render bridge card - * - * @param class-string $bridgeClassName The bridge name - * @param bool $isActive Indicates if the bridge is active or not - * @return string The bridge card - */ - public static function render($bridgeClassName, $isActive = true) + public static function render(string $bridgeClassName, Request $request): string { $bridgeFactory = new BridgeFactory(); @@ -47,19 +40,21 @@ final class BridgeCard

{$name}

{$description}

+ CARD; - // If we don't have any parameter for the bridge, we print a generic form to load it. + $token = $request->attribute('token'); + if (count($contexts) === 0) { // The bridge has zero parameters - $card .= self::renderForm($bridgeClassName, $isActive); + $card .= self::renderForm($bridgeClassName, '', [], $token); } elseif (count($contexts) === 1 && array_key_exists('global', $contexts)) { // The bridge has a single context with key 'global' - $card .= self::renderForm($bridgeClassName, $isActive, '', $contexts['global']); + $card .= self::renderForm($bridgeClassName, '', $contexts['global'], $token); } else { // The bridge has one or more contexts (named or unnamed) foreach ($contexts as $contextName => $contextParameters) { @@ -77,7 +72,7 @@ final class BridgeCard $card .= '
' . $contextName . '
' . PHP_EOL; } - $card .= self::renderForm($bridgeClassName, $isActive, $contextName, $contextParameters); + $card .= self::renderForm($bridgeClassName, $contextName, $contextParameters, $token); } } @@ -99,17 +94,21 @@ final class BridgeCard private static function renderForm( string $bridgeClassName, - bool $isActive = false, - string $contextName = '', - array $contextParameters = [] + string $contextName, + array $contextParameters, + ?string $token ) { $form = << +
- EOD; + if ($token) { + // todo: maybe escape the token? + $form .= sprintf('', $token); + } + if (!empty($contextName)) { $form .= sprintf('', $contextName); } @@ -167,11 +166,7 @@ final class BridgeCard $form .= ''; } - if ($isActive) { - $form .= ''; - } else { - $form .= 'Inactive'; - } + $form .= ''; return $form . '
' . PHP_EOL; } diff --git a/lib/RssBridge.php b/lib/RssBridge.php index c8d11596..1bb5f5ea 100644 --- a/lib/RssBridge.php +++ b/lib/RssBridge.php @@ -47,6 +47,7 @@ final class RssBridge ]), 503); } + // HTTP Basic auth check if (Configuration::getConfig('authentication', 'enable')) { if (Configuration::getConfig('authentication', 'password') === '') { return new Response('The authentication password cannot be the empty string', 500); @@ -71,6 +72,23 @@ final class RssBridge // At this point the username and password was correct } + // Add token as attribute to request + $request = $request->withAttribute('token', $request->get('token')); + + // Token authentication check + if (Configuration::getConfig('authentication', 'token')) { + if (! $request->attribute('token')) { + return new Response(render(__DIR__ . '/../templates/token.html.php', [ + 'message' => '', + ]), 401); + } + if (! hash_equals(Configuration::getConfig('authentication', 'token'), $request->attribute('token'))) { + return new Response(render(__DIR__ . '/../templates/token.html.php', [ + 'message' => 'Invalid token', + ]), 401); + } + } + $action = $request->get('action', 'Frontpage'); $actionName = strtolower($action) . 'Action'; $actionName = implode(array_map('ucfirst', explode('-', $actionName))); diff --git a/lib/http.php b/lib/http.php index d53909b4..e4f9bf48 100644 --- a/lib/http.php +++ b/lib/http.php @@ -170,6 +170,7 @@ final class Request { private array $get; private array $server; + private array $attributes; private function __construct() { @@ -180,6 +181,7 @@ final class Request $self = new self(); $self->get = $_GET; $self->server = $_SERVER; + $self->attributes = []; return $self; } @@ -200,6 +202,18 @@ final class Request return $this->server[$key] ?? $default; } + public function withAttribute(string $name, $value = true): self + { + $clone = clone $this; + $clone->attributes[$name] = $value; + return $clone; + } + + public function attribute(string $key, $default = null) + { + return $this->attributes[$key] ?? $default; + } + public function toArray(): array { return $this->get; diff --git a/static/connectivity.js b/static/connectivity.js index 55ee9434..2f39ce6b 100644 --- a/static/connectivity.js +++ b/static/connectivity.js @@ -48,7 +48,7 @@ function buildTable(bridgeList) { // Link to the actual bridge on frontpage var a = document.createElement('a'); - a.href = remote + "/?show_inactive=1#bridge-" + bridge; + a.href = remote + "/?#bridge-" + bridge; a.target = '_blank'; a.innerText = '[Show]'; a.style.marginLeft = '5px'; diff --git a/static/style.css b/static/style.css index cfaf66a1..4e6b1b2d 100644 --- a/static/style.css +++ b/static/style.css @@ -287,7 +287,7 @@ p.maintainer { } /* Hide all forms on the frontpage by default */ -form { +form.bridge-form { display: none; } diff --git a/templates/base.html.php b/templates/base.html.php index ca31823d..d2557599 100644 --- a/templates/base.html.php +++ b/templates/base.html.php @@ -7,6 +7,8 @@ <?= e($_title ?? 'RSS-Bridge') ?> + + diff --git a/templates/frontpage.html.php b/templates/frontpage.html.php index a0d274da..c1182673 100644 --- a/templates/frontpage.html.php +++ b/templates/frontpage.html.php @@ -1,4 +1,4 @@ - +