Passed
Push — master ( da4c60...acedf0 )
by Pauli
02:52
created

SubsonicMiddleware   A

Complexity

Total Complexity 23

Size/Duplication

Total Lines 139
Duplicated Lines 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 53
dl 0
loc 139
rs 10
c 3
b 0
f 0
wmc 23

6 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 1
A beforeController() 0 4 2
A setupResponseFormat() 0 14 4
A userForPass() 0 3 1
B checkAuthentication() 0 38 11
A afterException() 0 13 4
1
<?php declare(strict_types=1);
2
3
/**
4
 * ownCloud - Music app
5
 *
6
 * This file is licensed under the Affero General Public License version 3 or
7
 * later. See the COPYING file.
8
 *
9
 * @author Pauli Järvinen <[email protected]>
10
 * @copyright Pauli Järvinen 2019 - 2024
11
 */
12
13
namespace OCA\Music\Middleware;
14
15
use OCP\IRequest;
16
use OCP\AppFramework\Controller;
17
use OCP\AppFramework\Http\Response;
18
use OCP\AppFramework\Middleware;
19
20
use OCA\Music\AppFramework\BusinessLayer\BusinessLayerException;
21
use OCA\Music\AppFramework\Core\Logger;
22
use OCA\Music\Controller\SubsonicController;
23
use OCA\Music\Db\AmpacheUserMapper;
24
use OCA\Music\Utility\Util;
25
26
/**
27
 * Checks the authentication on each Subsonic API call before the
28
 * request is allowed to be passed to SubsonicController.
29
 * Map SubsonicExceptions from the controller to proper Subsonic error results.
30
 */
31
class SubsonicMiddleware extends Middleware {
32
	private $request;
33
	private $userMapper;
34
	private $logger;
35
36
	public function __construct(IRequest $request, AmpacheUserMapper $userMapper, Logger $logger) {
37
		$this->request = $request;
38
		$this->userMapper = $userMapper;
39
		$this->logger = $logger;
40
	}
41
42
	/**
43
	 * This function is run before any HTTP request handler method, but it does
44
	 * nothing if the call in question is not routed to SubsonicController. In
45
	 * case of Subsonic call, this checks the user authentication.
46
	 *
47
	 * NOTE: Type declarations cannot be used on this function signature because that would be
48
	 * in conflict with the base class which is not in our hands.
49
	 *
50
	 * @param Controller $controller the controller that is being called
51
	 * @param string $methodName the name of the method
52
	 * @throws SubsonicException when a security check fails
53
	 */
54
	public function beforeController($controller, $methodName) {
55
		if ($controller instanceof SubsonicController) {
56
			$this->setupResponseFormat($controller);
57
			$this->checkAuthentication($controller);
58
		}
59
	}
60
61
	/**
62
	 * Evaluate the response format parameters and setup the controller to use
63
	 * the requested format
64
	 * @param SubsonicController $controller
65
	 * @throws SubsonicException
66
	 */
67
	private function setupResponseFormat(SubsonicController $controller) {
68
		$format = $this->request->getParam('f', 'xml');
69
		$callback = $this->request->getParam('callback');
70
71
		if (!\in_array($format, ['json', 'xml', 'jsonp'])) {
72
			throw new SubsonicException("Unsupported format $format", 0);
73
		}
74
75
		if ($format === 'jsonp' && empty($callback)) {
76
			$format = 'xml';
77
			$this->logger->log("'jsonp' format requested but no arg 'callback' supplied, falling back to 'xml' format", 'debug');
78
		}
79
80
		$controller->setResponseFormat($format, $callback);
81
	}
82
83
	/**
84
	 * Check that valid credentials have been given.
85
	 * Setup the controller with the active user if the authentication is ok.
86
	 * @param SubsonicController $controller
87
	 * @throws SubsonicException
88
	 */
89
	private function checkAuthentication(SubsonicController $controller) {
90
		if ($this->request->getParam('t') !== null) {
91
			throw new SubsonicException('Token-based authentication not supported', 41);
92
		}
93
94
		$user = $this->request->getParam('u');
95
		$apiKey = $this->request->getParam('apiKey');
96
97
		if ($user !== null && $apiKey !== null) {
98
			throw new SubsonicException('Multiple conflicting authentication mechanisms provided', 43);
99
		}
100
		else if ($apiKey !== null) {
101
			$user = $this->userForPass($apiKey);
102
			if ($user !== null) {
103
				$controller->setAuthenticatedUser($user);
104
			} else {
105
				throw new SubsonicException('Invalid API key', 44);
106
			}
107
		}
108
		else if ($user !== null) {
109
			$pass = $this->request->getParam('p');
110
			if ($pass === null) {
111
				throw new SubsonicException('Password argument `p` missing', 10);
112
			}
113
114
			// The password may be given in hexadecimal format
115
			if (Util::startsWith($pass, 'enc:')) {
116
				$pass = \hex2bin(\substr($pass, \strlen('enc:')));
117
			}
118
119
			$user = $this->userMapper->getProperUserId($user);
120
			if ($user !== null && $this->userForPass($pass) == $user) {
121
				$controller->setAuthenticatedUser($user);
122
			} else {
123
				throw new SubsonicException('Wrong username or password', 40);
124
			}
125
		}
126
		else {
127
			// Not passing any credentials is allowed since some parts of the API are allowed without authentication.
128
			// SubsonicController::hanldeRequest needs to check that there is an authenticated user if needed.
129
		}
130
	}
131
132
	/**
133
	 * @param string $pass Password aka API key
134
	 * @return string User ID or null if the $pass was not valid
135
	 */
136
	private function userForPass(string $pass) : ?string {
137
		$hash = \hash('sha256', $pass);
138
		return $this->userMapper->getUserByPasswordHash($hash);
139
	}
140
141
	/**
142
	 * Catch SubsonicException and BusinessLayerException instances thrown when handling
143
	 * Subsonic requests, and render the the appropriate Subsonic error response. Any other
144
	 * exceptions are allowed to flow through, reaching eventually the default handler if
145
	 * no-one else intercepts them. The default handler logs the error and returns response
146
	 * code 500.
147
	 *
148
	 * NOTE: Type declarations cannot be used on this function signature because that would be
149
	 * in conflict with the base class which is not in our hands.
150
	 *
151
	 * @param Controller $controller the controller that was being called
152
	 * @param string $methodName the name of the method that was called on the controller
153
	 * @param \Exception $exception the thrown exception
154
	 * @throws \Exception the passed in exception if it couldn't be handled
155
	 * @return Response a Response object in case the exception could be handled
156
	 */
157
	public function afterException($controller, $methodName, \Exception $exception) {
158
		if ($controller instanceof SubsonicController) {
159
			if ($exception instanceof SubsonicException) {
160
				$this->logger->log($exception->getMessage(), 'debug');
161
				return $controller->subsonicErrorResponse(
162
						$exception->getCode(),
163
						$exception->getMessage()
164
				);
165
			} elseif ($exception instanceof BusinessLayerException) {
166
				return $controller->subsonicErrorResponse(70, 'Entity not found');
167
			}
168
		}
169
		throw $exception;
170
	}
171
}
172