Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
65 / 65
100.00% covered (success)
100.00%
5 / 5
CRAP
100.00% covered (success)
100.00%
1 / 1
http_auth_subscriber
100.00% covered (success)
100.00%
65 / 65
100.00% covered (success)
100.00%
5 / 5
20
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 on_kernel_request
100.00% covered (success)
100.00%
23 / 23
100.00% covered (success)
100.00%
1 / 1
9
 get_credentials
100.00% covered (success)
100.00%
29 / 29
100.00% covered (success)
100.00%
1 / 1
8
 send_auth_challenge
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 getSubscribedEvents
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2/**
3*
4* This file is part of the phpBB Forum Software package.
5*
6* @copyright (c) phpBB Limited <https://www.phpbb.com>
7* @license GNU General Public License, version 2 (GPL-2.0)
8*
9* For full copyright and license information, please see
10* the docs/CREDITS.txt file.
11*
12*/
13
14namespace phpbb\feed\event;
15
16use phpbb\auth\auth;
17use phpbb\config\config;
18use phpbb\language\language;
19use phpbb\request\request_interface;
20use phpbb\user;
21use Symfony\Component\EventDispatcher\EventSubscriberInterface;
22use Symfony\Component\HttpFoundation\Response;
23use Symfony\Component\HttpKernel\Event\RequestEvent;
24use Symfony\Component\HttpKernel\KernelEvents;
25
26/**
27 * Event subscriber for HTTP authentication on feed routes
28 */
29class http_auth_subscriber implements EventSubscriberInterface
30{
31    /** @var auth */
32    protected $auth;
33
34    /** @var config */
35    protected $config;
36
37    /** @var language */
38    protected $language;
39
40    /** @var request_interface */
41    protected $request;
42
43    /** @var user */
44    protected $user;
45
46    /**
47     * Constructor
48     *
49     * @param auth                $auth        Auth object
50     * @param config            $config        Config object
51     * @param language    $language    Language object
52     * @param request_interface    $request    Request object
53     * @param user                $user        User object
54     */
55    public function __construct(auth $auth, config $config, language $language, request_interface $request, user $user)
56    {
57        $this->auth = $auth;
58        $this->config = $config;
59        $this->language = $language;
60        $this->request = $request;
61        $this->user = $user;
62    }
63
64    /**
65     * Handle HTTP authentication for feed routes
66     *
67     * @param RequestEvent $event
68     * @return void
69     */
70    public function on_kernel_request(RequestEvent $event)
71    {
72        // Check if HTTP authentication is enabled
73        if (!$this->config['feed_http_auth'])
74        {
75            return;
76        }
77
78        $request = $event->getRequest();
79        $route = $request->attributes->get('_route');
80
81        // Only apply to feed routes
82        if (strpos($route, 'phpbb_feed_') !== 0)
83        {
84            return;
85        }
86
87        // Only allow HTTP authentication in secure context (HTTPS)
88        if (!$request->isSecure())
89        {
90            return;
91        }
92
93        // User is already logged in, no need to authenticate
94        if (!empty($this->user->data['is_registered']))
95        {
96            return;
97        }
98
99        // Get HTTP authentication credentials
100        [$username, $password] = $this->get_credentials();
101
102        // If no credentials provided, send authentication challenge
103        if ($username === null || $password === null)
104        {
105            $this->send_auth_challenge($event);
106            return;
107        }
108
109        // Attempt to login with the provided credentials
110        $auth_result = $this->auth->login($username, $password, false, true, false);
111
112        if ($auth_result['status'] == LOGIN_SUCCESS)
113        {
114            // Reload ACL for the newly logged-in user
115            $this->auth->acl($this->user->data);
116            return;
117        }
118        else if ($auth_result['status'] == LOGIN_ERROR_ATTEMPTS)
119        {
120            // Too many login attempts
121            $response = new Response($this->language->lang('LOGIN_ERROR_ATTEMPTS'), Response::HTTP_UNAUTHORIZED);
122            $event->setResponse($response);
123            return;
124        }
125
126        // Authentication failed, send challenge
127        $this->send_auth_challenge($event);
128    }
129
130    /**
131     * Retrieve HTTP authentication credentials from server variables
132     *
133     * @return array [username, password] Array containing the username and password, or null if not found
134     */
135    protected function get_credentials(): array
136    {
137        $username_keys = [
138            'PHP_AUTH_USER',
139            'Authorization',
140            'REMOTE_USER',
141            'REDIRECT_REMOTE_USER',
142            'HTTP_AUTHORIZATION',
143            'REDIRECT_HTTP_AUTHORIZATION',
144            'REMOTE_AUTHORIZATION',
145            'REDIRECT_REMOTE_AUTHORIZATION',
146            'AUTH_USER',
147        ];
148
149        $password_keys = [
150            'PHP_AUTH_PW',
151            'REMOTE_PASSWORD',
152            'AUTH_PASSWORD',
153        ];
154
155        $username = null;
156        foreach ($username_keys as $key)
157        {
158            if ($this->request->is_set($key, request_interface::SERVER))
159            {
160                $username = htmlspecialchars_decode($this->request->server($key));
161                break;
162            }
163        }
164
165        $password = null;
166        foreach ($password_keys as $key)
167        {
168            if ($this->request->is_set($key, request_interface::SERVER))
169            {
170                $password =  htmlspecialchars_decode($this->request->server($key));
171                break;
172            }
173        }
174
175        // Decode Basic authentication header if needed
176        if (!is_null($username) && is_null($password) && strpos($username, 'Basic ') === 0)
177        {
178            [$username, $password] = explode(':', base64_decode(substr($username, 6)), 2);
179        }
180
181        return [$username, $password];
182    }
183
184    /**
185     * Send HTTP authentication challenge
186     *
187     * @param RequestEvent $event
188     * @return void
189     */
190    protected function send_auth_challenge(RequestEvent $event)
191    {
192        $realm = $this->config['sitename'];
193
194        // Filter out non-ASCII characters per RFC2616
195        $realm = preg_replace('/[\x80-\xFF]/', '?', $realm);
196
197        $response = new Response($this->language->lang('NOT_AUTHORISED'), Response::HTTP_UNAUTHORIZED);
198        $response->headers->set('WWW-Authenticate', 'Basic realm="' . $realm . ' - Feed"');
199        $event->setResponse($response);
200    }
201
202    /**
203     * {@inheritdoc}
204     */
205    public static function getSubscribedEvents(): array
206    {
207        return [
208            KernelEvents::REQUEST => ['on_kernel_request', 5],
209        ];
210    }
211}