Passed
Push — master ( ab8f86...6a60e0 )
by Pauli
02:57
created

SubsonicMiddleware::afterException()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 13
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 9
nc 4
nop 3
dl 0
loc 13
rs 9.9666
c 0
b 0
f 0
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
		$user = $this->request->getParam('u');
91
		$pass = $this->request->getParam('p');
92
93
		if ($user === null || $pass === null) {
94
			if ($this->request->getParam('t') !== null) {
95
				throw new SubsonicException('Token-based authentication not supported', 41);
96
			} else {
97
				throw new SubsonicException('Required credentials missing', 10);
98
			}
99
		}
100
101
		// The password may be given in hexadecimal format
102
		if (Util::startsWith($pass, 'enc:')) {
103
			$pass = \hex2bin(\substr($pass, \strlen('enc:')));
104
		}
105
106
		$user = $this->userMapper->getProperUserId($user);
107
		if ($user !== null && $this->credentialsAreValid($user, $pass)) {
108
			$controller->setAuthenticatedUser($user);
109
		} else {
110
			throw new SubsonicException('Invalid Login', 40);
111
		}
112
	}
113
114
	/**
115
	 * @param string $user Username
116
	 * @param string $pass Password
117
	 * @return boolean
118
	 */
119
	private function credentialsAreValid(string $user, string $pass) : bool {
120
		$hashes = $this->userMapper->getPasswordHashes($user);
121
122
		foreach ($hashes as $hash) {
123
			if ($hash === \hash('sha256', $pass)) {
124
				return true;
125
			}
126
		}
127
128
		return false;
129
	}
130
131
	/**
132
	 * Catch SubsonicException and BusinessLayerException instances thrown when handling
133
	 * Subsonic requests, and render the the appropriate Subsonic error response. Any other
134
	 * exceptions are allowed to flow through, reaching eventually the default handler if
135
	 * no-one else intercepts them. The default handler logs the error and returns response
136
	 * code 500.
137
	 *
138
	 * NOTE: Type declarations cannot be used on this function signature because that would be
139
	 * in conflict with the base class which is not in our hands.
140
	 *
141
	 * @param Controller $controller the controller that was being called
142
	 * @param string $methodName the name of the method that was called on the controller
143
	 * @param \Exception $exception the thrown exception
144
	 * @throws \Exception the passed in exception if it couldn't be handled
145
	 * @return Response a Response object in case the exception could be handled
146
	 */
147
	public function afterException($controller, $methodName, \Exception $exception) {
148
		if ($controller instanceof SubsonicController) {
149
			if ($exception instanceof SubsonicException) {
150
				$this->logger->log($exception->getMessage(), 'debug');
151
				return $controller->subsonicErrorResponse(
152
						$exception->getCode(),
153
						$exception->getMessage()
154
				);
155
			} elseif ($exception instanceof BusinessLayerException) {
156
				return $controller->subsonicErrorResponse(70, 'Entity not found');
157
			}
158
		}
159
		throw $exception;
160
	}
161
}
162