diff --git a/phpBB/config/default/container/services_feed.yml b/phpBB/config/default/container/services_feed.yml index f32d0cb4d3..2f27f6915b 100644 --- a/phpBB/config/default/container/services_feed.yml +++ b/phpBB/config/default/container/services_feed.yml @@ -15,6 +15,16 @@ services: - '@language' - '%core.php_ext%' + feed.http_auth_subscriber: + class: phpbb\feed\event\http_auth_subscriber + arguments: + - '@auth' + - '@config' + - '@request' + - '@user' + tags: + - { name: kernel.event_subscriber } + feed.helper: class: phpbb\feed\helper arguments: diff --git a/phpBB/phpbb/feed/event/http_auth_subscriber.php b/phpBB/phpbb/feed/event/http_auth_subscriber.php new file mode 100644 index 0000000000..8b3f03b0bd --- /dev/null +++ b/phpBB/phpbb/feed/event/http_auth_subscriber.php @@ -0,0 +1,242 @@ + +* @license GNU General Public License, version 2 (GPL-2.0) +* +* For full copyright and license information, please see +* the docs/CREDITS.txt file. +* +*/ + +namespace phpbb\feed\event; + +use phpbb\auth\auth; +use phpbb\config\config; +use phpbb\request\request_interface; +use phpbb\user; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Event\GetResponseEvent; +use Symfony\Component\HttpKernel\KernelEvents; + +/** + * Event subscriber for HTTP authentication on feed routes + */ +class http_auth_subscriber implements EventSubscriberInterface +{ + /** @var auth */ + protected $auth; + + /** @var config */ + protected $config; + + /** @var request_interface */ + protected $request; + + /** @var user */ + protected $user; + + /** + * Constructor + * + * @param auth $auth Auth object + * @param config $config Config object + * @param request_interface $request Request object + * @param user $user User object + */ + public function __construct(auth $auth, config $config, request_interface $request, user $user) + { + $this->auth = $auth; + $this->config = $config; + $this->request = $request; + $this->user = $user; + } + + /** + * Handle HTTP authentication for feed routes + * + * @param GetResponseEvent $event + * @return void + */ + public function on_kernel_request(GetResponseEvent $event) + { + $request = $event->getRequest(); + $route = $request->attributes->get('_route'); + + // Only apply to feed routes + if (strpos($route, 'phpbb_feed_') !== 0) + { + return; + } + + // Check if HTTP authentication is enabled + if (!$this->config['feed_http_auth']) + { + return; + } + + // User is already logged in, no need to authenticate + if (!empty($this->user->data['is_registered'])) + { + return; + } + + // Get HTTP authentication credentials + $username = $this->get_http_username(); + $password = $this->get_http_password(); + + // If no credentials provided, send authentication challenge + if ($username === null || $password === null) + { + $this->send_auth_challenge($event); + return; + } + + // Attempt to login with the provided credentials + $auth_result = $this->auth->login($username, $password, false, true, false); + + if ($auth_result['status'] == LOGIN_SUCCESS) + { + return; + } + else if ($auth_result['status'] == LOGIN_ERROR_ATTEMPTS) + { + // Too many login attempts + $response = new Response('', Response::HTTP_UNAUTHORIZED); + $event->setResponse($response); + return; + } + + // Authentication failed, send challenge + $this->send_auth_challenge($event); + } + + /** + * Get HTTP username from request headers + * + * @return string|null + */ + protected function get_http_username() + { + $username_keys = array( + 'PHP_AUTH_USER', + 'Authorization', + 'REMOTE_USER', + 'REDIRECT_REMOTE_USER', + 'HTTP_AUTHORIZATION', + 'REDIRECT_HTTP_AUTHORIZATION', + 'REMOTE_AUTHORIZATION', + 'REDIRECT_REMOTE_AUTHORIZATION', + 'AUTH_USER', + ); + + foreach ($username_keys as $key) + { + if ($this->request->is_set($key, request_interface::SERVER)) + { + $username = html_entity_decode($this->request->server($key), ENT_COMPAT); + + // Decode Basic authentication header + if (strpos($username, 'Basic ') === 0) + { + $credentials = base64_decode(substr($username, 6)); + if (strpos($credentials, ':') !== false) + { + list($username, ) = explode(':', $credentials, 2); + } + } + + return $username; + } + } + + return null; + } + + /** + * Get HTTP password from request headers + * + * @return string|null + */ + protected function get_http_password() + { + $password_keys = array( + 'PHP_AUTH_PW', + 'REMOTE_PASSWORD', + 'AUTH_PASSWORD', + ); + + foreach ($password_keys as $key) + { + if ($this->request->is_set($key, request_interface::SERVER)) + { + return html_entity_decode($this->request->server($key), ENT_COMPAT); + } + } + + // Check if password is in Authorization header (Basic auth) + $username_keys = array( + 'PHP_AUTH_USER', + 'Authorization', + 'REMOTE_USER', + 'REDIRECT_REMOTE_USER', + 'HTTP_AUTHORIZATION', + 'REDIRECT_HTTP_AUTHORIZATION', + 'REMOTE_AUTHORIZATION', + 'REDIRECT_REMOTE_AUTHORIZATION', + 'AUTH_USER', + ); + + foreach ($username_keys as $key) + { + if ($this->request->is_set($key, request_interface::SERVER)) + { + $auth_header = html_entity_decode($this->request->server($key), ENT_COMPAT); + + // Decode Basic authentication header + if (strpos($auth_header, 'Basic ') === 0) + { + $credentials = base64_decode(substr($auth_header, 6)); + if (strpos($credentials, ':') !== false) + { + list(, $password) = explode(':', $credentials, 2); + return $password; + } + } + } + } + + return null; + } + + /** + * Send HTTP authentication challenge + * + * @param GetResponseEvent $event + * @return void + */ + protected function send_auth_challenge(GetResponseEvent $event) + { + $realm = $this->config['sitename']; + // Filter out non-ASCII characters per RFC2616 + $realm = preg_replace('/[\x80-\xFF]/', '?', $realm); + + $response = new Response('', Response::HTTP_UNAUTHORIZED); + $response->headers->set('WWW-Authenticate', 'Basic realm="' . $realm . '"'); + $event->setResponse($response); + } + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents() + { + return array( + // Priority should be high to run after session_begin but before controller + KernelEvents::REQUEST => array('on_kernel_request', 5), + ); + } +}