mirror of
https://github.com/RSS-Bridge/rss-bridge.git
synced 2025-03-31 22:05:20 +03:00
feat: token authentication (#3927)
This commit is contained in:
parent
d08d13f2c8
commit
e58c867a82
13 changed files with 95 additions and 138 deletions
30
README.md
30
README.md
|
@ -10,6 +10,13 @@ Officially hosted instance: https://rss-bridge.org/bridge01/
|
||||||
|
|
||||||
IRC channel #rssbridge at https://libera.chat/
|
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.
|
||||||
|
|
||||||
|
|
||||||
[](UNLICENSE)
|
[](UNLICENSE)
|
||||||
[](https://github.com/rss-bridge/rss-bridge/releases/latest)
|
[](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)
|
* `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)
|
* `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
|
## Tutorial
|
||||||
|
|
||||||
### How to install on traditional shared web hosting
|
### How to install on traditional shared web hosting
|
||||||
|
@ -259,6 +257,14 @@ Learn more in
|
||||||
|
|
||||||
## How-to
|
## How-to
|
||||||
|
|
||||||
|
### How to password-protect the instance (token)
|
||||||
|
|
||||||
|
Modify `config.ini.php`:
|
||||||
|
|
||||||
|
[authentication]
|
||||||
|
|
||||||
|
token = "hunter2"
|
||||||
|
|
||||||
### How to remove all cache items
|
### How to remove all cache items
|
||||||
|
|
||||||
As current user:
|
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
|
### How to enable all bridges
|
||||||
|
|
||||||
Modify `config.ini.php`:
|
|
||||||
|
|
||||||
enabled_bridges[] = *
|
enabled_bridges[] = *
|
||||||
|
|
||||||
### How to enable some 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.
|
The report count is reset to 0 each day.
|
||||||
|
|
||||||
### How to password-protect the instance
|
### How to password-protect the instance (HTTP Basic Auth)
|
||||||
|
|
||||||
HTTP basic access authentication:
|
|
||||||
|
|
||||||
[authentication]
|
[authentication]
|
||||||
|
|
||||||
|
|
|
@ -102,6 +102,7 @@ class DisplayAction implements ActionInterface
|
||||||
$bridge->loadConfiguration();
|
$bridge->loadConfiguration();
|
||||||
// Remove parameters that don't concern bridges
|
// Remove parameters that don't concern bridges
|
||||||
$remove = [
|
$remove = [
|
||||||
|
'token',
|
||||||
'action',
|
'action',
|
||||||
'bridge',
|
'bridge',
|
||||||
'format',
|
'format',
|
||||||
|
|
|
@ -4,8 +4,6 @@ final class FrontpageAction implements ActionInterface
|
||||||
{
|
{
|
||||||
public function execute(Request $request)
|
public function execute(Request $request)
|
||||||
{
|
{
|
||||||
$showInactive = (bool) $request->get('show_inactive');
|
|
||||||
|
|
||||||
$messages = [];
|
$messages = [];
|
||||||
$activeBridges = 0;
|
$activeBridges = 0;
|
||||||
|
|
||||||
|
@ -22,10 +20,8 @@ final class FrontpageAction implements ActionInterface
|
||||||
$body = '';
|
$body = '';
|
||||||
foreach ($bridgeClassNames as $bridgeClassName) {
|
foreach ($bridgeClassNames as $bridgeClassName) {
|
||||||
if ($bridgeFactory->isEnabled($bridgeClassName)) {
|
if ($bridgeFactory->isEnabled($bridgeClassName)) {
|
||||||
$body .= BridgeCard::render($bridgeClassName);
|
$body .= BridgeCard::render($bridgeClassName, $request);
|
||||||
$activeBridges++;
|
$activeBridges++;
|
||||||
} elseif ($showInactive) {
|
|
||||||
$body .= BridgeCard::render($bridgeClassName, false) . "\n";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -37,7 +33,6 @@ final class FrontpageAction implements ActionInterface
|
||||||
'bridges' => $body,
|
'bridges' => $body,
|
||||||
'active_bridges' => $activeBridges,
|
'active_bridges' => $activeBridges,
|
||||||
'total_bridges' => count($bridgeClassNames),
|
'total_bridges' => count($bridgeClassNames),
|
||||||
'show_inactive' => $showInactive,
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,68 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
class SetBridgeCacheAction implements ActionInterface
|
|
||||||
{
|
|
||||||
private CacheInterface $cache;
|
|
||||||
|
|
||||||
public function __construct()
|
|
||||||
{
|
|
||||||
$this->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']);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -102,21 +102,13 @@ by_bridge = false
|
||||||
|
|
||||||
[authentication]
|
[authentication]
|
||||||
|
|
||||||
; Enables basic authentication for all requests to this RSS-Bridge instance.
|
; HTTP basic authentication
|
||||||
;
|
|
||||||
; Warning: You'll have to upgrade existing feeds after enabling this option!
|
|
||||||
;
|
|
||||||
; true = enabled
|
|
||||||
; false = disabled (default)
|
|
||||||
enable = false
|
enable = false
|
||||||
|
|
||||||
username = "admin"
|
username = "admin"
|
||||||
|
|
||||||
; The password cannot be the empty string if authentication is enabled.
|
|
||||||
password = ""
|
password = ""
|
||||||
|
|
||||||
; This will be used only for actions that require privileged access
|
; Token authentication (URL)
|
||||||
access_token = ""
|
token = ""
|
||||||
|
|
||||||
[error]
|
[error]
|
||||||
|
|
||||||
|
|
|
@ -2,14 +2,7 @@
|
||||||
|
|
||||||
final class BridgeCard
|
final class BridgeCard
|
||||||
{
|
{
|
||||||
/**
|
public static function render(string $bridgeClassName, Request $request): string
|
||||||
* Render bridge card
|
|
||||||
*
|
|
||||||
* @param class-string<BridgeAbstract> $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)
|
|
||||||
{
|
{
|
||||||
$bridgeFactory = new BridgeFactory();
|
$bridgeFactory = new BridgeFactory();
|
||||||
|
|
||||||
|
@ -47,19 +40,21 @@ final class BridgeCard
|
||||||
|
|
||||||
<h2><a href="{$uri}">{$name}</a></h2>
|
<h2><a href="{$uri}">{$name}</a></h2>
|
||||||
<p class="description">{$description}</p>
|
<p class="description">{$description}</p>
|
||||||
|
|
||||||
<input type="checkbox" class="showmore-box" id="showmore-{$bridgeClassName}" />
|
<input type="checkbox" class="showmore-box" id="showmore-{$bridgeClassName}" />
|
||||||
<label class="showmore" for="showmore-{$bridgeClassName}">Show more</label>
|
<label class="showmore" for="showmore-{$bridgeClassName}">Show more</label>
|
||||||
|
|
||||||
|
|
||||||
CARD;
|
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) {
|
if (count($contexts) === 0) {
|
||||||
// The bridge has zero parameters
|
// The bridge has zero parameters
|
||||||
$card .= self::renderForm($bridgeClassName, $isActive);
|
$card .= self::renderForm($bridgeClassName, '', [], $token);
|
||||||
} elseif (count($contexts) === 1 && array_key_exists('global', $contexts)) {
|
} elseif (count($contexts) === 1 && array_key_exists('global', $contexts)) {
|
||||||
// The bridge has a single context with key 'global'
|
// The bridge has a single context with key 'global'
|
||||||
$card .= self::renderForm($bridgeClassName, $isActive, '', $contexts['global']);
|
$card .= self::renderForm($bridgeClassName, '', $contexts['global'], $token);
|
||||||
} else {
|
} else {
|
||||||
// The bridge has one or more contexts (named or unnamed)
|
// The bridge has one or more contexts (named or unnamed)
|
||||||
foreach ($contexts as $contextName => $contextParameters) {
|
foreach ($contexts as $contextName => $contextParameters) {
|
||||||
|
@ -77,7 +72,7 @@ final class BridgeCard
|
||||||
$card .= '<h5>' . $contextName . '</h5>' . PHP_EOL;
|
$card .= '<h5>' . $contextName . '</h5>' . 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(
|
private static function renderForm(
|
||||||
string $bridgeClassName,
|
string $bridgeClassName,
|
||||||
bool $isActive = false,
|
string $contextName,
|
||||||
string $contextName = '',
|
array $contextParameters,
|
||||||
array $contextParameters = []
|
?string $token
|
||||||
) {
|
) {
|
||||||
$form = <<<EOD
|
$form = <<<EOD
|
||||||
<form method="GET" action="?">
|
<form method="GET" action="?" class="bridge-form">
|
||||||
<input type="hidden" name="action" value="display" />
|
<input type="hidden" name="action" value="display" />
|
||||||
<input type="hidden" name="bridge" value="{$bridgeClassName}" />
|
<input type="hidden" name="bridge" value="{$bridgeClassName}" />
|
||||||
|
|
||||||
EOD;
|
EOD;
|
||||||
|
|
||||||
|
if ($token) {
|
||||||
|
// todo: maybe escape the token?
|
||||||
|
$form .= sprintf('<input type="hidden" name="token" value="%s" />', $token);
|
||||||
|
}
|
||||||
|
|
||||||
if (!empty($contextName)) {
|
if (!empty($contextName)) {
|
||||||
$form .= sprintf('<input type="hidden" name="context" value="%s" />', $contextName);
|
$form .= sprintf('<input type="hidden" name="context" value="%s" />', $contextName);
|
||||||
}
|
}
|
||||||
|
@ -167,11 +166,7 @@ final class BridgeCard
|
||||||
$form .= '</div>';
|
$form .= '</div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($isActive) {
|
$form .= '<button type="submit" name="format" formtarget="_blank" value="Html">Generate feed</button>';
|
||||||
$form .= '<button type="submit" name="format" formtarget="_blank" value="Html">Generate feed</button>';
|
|
||||||
} else {
|
|
||||||
$form .= '<span style="font-weight: bold;">Inactive</span>';
|
|
||||||
}
|
|
||||||
|
|
||||||
return $form . '</form>' . PHP_EOL;
|
return $form . '</form>' . PHP_EOL;
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,6 +47,7 @@ final class RssBridge
|
||||||
]), 503);
|
]), 503);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HTTP Basic auth check
|
||||||
if (Configuration::getConfig('authentication', 'enable')) {
|
if (Configuration::getConfig('authentication', 'enable')) {
|
||||||
if (Configuration::getConfig('authentication', 'password') === '') {
|
if (Configuration::getConfig('authentication', 'password') === '') {
|
||||||
return new Response('The authentication password cannot be the empty string', 500);
|
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
|
// 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');
|
$action = $request->get('action', 'Frontpage');
|
||||||
$actionName = strtolower($action) . 'Action';
|
$actionName = strtolower($action) . 'Action';
|
||||||
$actionName = implode(array_map('ucfirst', explode('-', $actionName)));
|
$actionName = implode(array_map('ucfirst', explode('-', $actionName)));
|
||||||
|
|
14
lib/http.php
14
lib/http.php
|
@ -170,6 +170,7 @@ final class Request
|
||||||
{
|
{
|
||||||
private array $get;
|
private array $get;
|
||||||
private array $server;
|
private array $server;
|
||||||
|
private array $attributes;
|
||||||
|
|
||||||
private function __construct()
|
private function __construct()
|
||||||
{
|
{
|
||||||
|
@ -180,6 +181,7 @@ final class Request
|
||||||
$self = new self();
|
$self = new self();
|
||||||
$self->get = $_GET;
|
$self->get = $_GET;
|
||||||
$self->server = $_SERVER;
|
$self->server = $_SERVER;
|
||||||
|
$self->attributes = [];
|
||||||
return $self;
|
return $self;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -200,6 +202,18 @@ final class Request
|
||||||
return $this->server[$key] ?? $default;
|
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
|
public function toArray(): array
|
||||||
{
|
{
|
||||||
return $this->get;
|
return $this->get;
|
||||||
|
|
|
@ -48,7 +48,7 @@ function buildTable(bridgeList) {
|
||||||
|
|
||||||
// Link to the actual bridge on frontpage
|
// Link to the actual bridge on frontpage
|
||||||
var a = document.createElement('a');
|
var a = document.createElement('a');
|
||||||
a.href = remote + "/?show_inactive=1#bridge-" + bridge;
|
a.href = remote + "/?#bridge-" + bridge;
|
||||||
a.target = '_blank';
|
a.target = '_blank';
|
||||||
a.innerText = '[Show]';
|
a.innerText = '[Show]';
|
||||||
a.style.marginLeft = '5px';
|
a.style.marginLeft = '5px';
|
||||||
|
|
|
@ -287,7 +287,7 @@ p.maintainer {
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Hide all forms on the frontpage by default */
|
/* Hide all forms on the frontpage by default */
|
||||||
form {
|
form.bridge-form {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,8 @@
|
||||||
<title><?= e($_title ?? 'RSS-Bridge') ?></title>
|
<title><?= e($_title ?? 'RSS-Bridge') ?></title>
|
||||||
<link href="static/style.css?2023-03-24" rel="stylesheet">
|
<link href="static/style.css?2023-03-24" rel="stylesheet">
|
||||||
<link rel="icon" type="image/png" href="static/favicon.png">
|
<link rel="icon" type="image/png" href="static/favicon.png">
|
||||||
|
|
||||||
|
<script src="static/rss-bridge.js"></script>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
<script src="static/rss-bridge.js"></script>
|
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', rssbridge_toggle_bridge);
|
document.addEventListener('DOMContentLoaded', rssbridge_toggle_bridge);
|
||||||
document.addEventListener('DOMContentLoaded', rssbridge_list_search);
|
document.addEventListener('DOMContentLoaded', rssbridge_list_search);
|
||||||
|
@ -42,20 +42,6 @@
|
||||||
|
|
||||||
<?= $active_bridges ?>/<?= $total_bridges ?> active bridges.<br>
|
<?= $active_bridges ?>/<?= $total_bridges ?> active bridges.<br>
|
||||||
|
|
||||||
<?php if ($active_bridges !== $total_bridges): ?>
|
|
||||||
<?php if ($show_inactive): ?>
|
|
||||||
<a href="?show_inactive=0">
|
|
||||||
<button class="small">Hide inactive bridges</button>
|
|
||||||
</a>
|
|
||||||
<br>
|
|
||||||
<?php else: ?>
|
|
||||||
<a href="?show_inactive=1">
|
|
||||||
<button class="small">Show inactive bridges</button>
|
|
||||||
</a>
|
|
||||||
<br>
|
|
||||||
<?php endif; ?>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
<?php if ($admin_email): ?>
|
<?php if ($admin_email): ?>
|
||||||
|
|
20
templates/token.html.php
Normal file
20
templates/token.html.php
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* This template renders a form for user to enter a auth token if it's enabled
|
||||||
|
*/
|
||||||
|
|
||||||
|
?>
|
||||||
|
|
||||||
|
<h1>
|
||||||
|
Authentication with token required
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<?= e($message) ?>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<form action="" method="get">
|
||||||
|
<label for="token">Token:</label>
|
||||||
|
<input type="password" name="token" id="token" placeholder="token">
|
||||||
|
<input type="submit" value="OK">
|
||||||
|
</form>
|
Loading…
Add table
Reference in a new issue