Passed
Pull Request — master (#1078)
by Pauli
05:40 queued 02:37
created

AmpacheMiddleware::startNewSession()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 13
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 8
c 0
b 0
f 0
nc 1
nop 4
dl 0
loc 13
rs 10
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 Morris Jobke <[email protected]>
10
 * @author Pauli Järvinen <[email protected]>
11
 * @copyright Morris Jobke 2013, 2014
12
 * @copyright Pauli Järvinen 2018 - 2023
13
 */
14
15
namespace OCA\Music\Middleware;
16
17
use OCP\IRequest;
18
use OCP\AppFramework\Controller;
19
use OCP\AppFramework\Middleware;
20
21
use OCA\Music\AppFramework\BusinessLayer\BusinessLayerException;
22
use OCA\Music\AppFramework\Core\Logger;
23
use OCA\Music\Controller\AmpacheController;
24
use OCA\Music\Db\AmpacheSession;
25
use OCA\Music\Db\AmpacheSessionMapper;
26
use OCA\Music\Db\AmpacheUserMapper;
27
use OCA\Music\Utility\Random;
28
use OCA\Music\Utility\Util;
29
30
/**
31
 * Handles the session management on Ampache login/logout.
32
 * Checks the authentication on each Ampache API call before the
33
 * request is allowed to be passed to AmpacheController.
34
 * Map identified exceptions from the controller to proper Ampache error results.
35
 */
36
class AmpacheMiddleware extends Middleware {
37
38
	const SESSION_EXPIRY_TIME = 6000;
39
40
	private $request;
41
	private $ampacheSessionMapper;
42
	private $ampacheUserMapper;
43
	private $logger;
44
45
	public function __construct(
46
			IRequest $request, AmpacheSessionMapper $ampacheSessionMapper, AmpacheUserMapper $ampacheUserMapper, Logger $logger) {
47
		$this->request = $request;
48
		$this->ampacheSessionMapper = $ampacheSessionMapper;
49
		$this->ampacheUserMapper = $ampacheUserMapper;
50
		$this->logger = $logger;
51
	}
52
53
	/**
54
	 * This runs all the security checks before a method call. The
55
	 * security checks are determined by inspecting the controller method
56
	 * annotations
57
	 *
58
	 * NOTE: Type declarations cannot be used on this function signature because that would be
59
	 * in conflict with the base class which is not in our hands.
60
	 *
61
	 * @param Controller $controller the controller that is being called
62
	 * @param string $methodName the name of the method
63
	 * @throws AmpacheException when a security check fails
64
	 */
65
	public function beforeController($controller, $methodName) {
66
		if ($controller instanceof AmpacheController) {
67
			if ($methodName === 'jsonApi') {
68
				$controller->setJsonMode(true);
69
			}
70
71
			// authenticate on 'handshake' and check the session token on any other action
72
			$action = $this->request->getParam('action');
73
			if ($action === 'handshake') {
74
				$this->handleHandshake($controller);
75
			} else {
76
				$this->handleNonHandshakeAction($controller, $action);
77
			}
78
		}
79
	}
80
	
81
	private function handleHandshake(AmpacheController $controller) : void {
82
		$user = $this->request->getParam('user');
83
		$timestamp = (int)$this->request->getParam('timestamp');
84
		$auth = $this->request->getParam('auth');
85
		$version = $this->request->getParam('version');
86
87
		$currentTime = \time();
88
		$expiryDate = $currentTime + self::SESSION_EXPIRY_TIME;
89
90
		$this->checkHandshakeTimestamp($timestamp, $currentTime);
91
		$apiKeyId = $this->checkHandshakeAuthentication($user, $timestamp, $auth);
92
		$session = $this->startNewSession($user, $expiryDate, $version, $apiKeyId);
93
		$controller->setSession($session);
94
	}
95
96
	private function checkHandshakeTimestamp(int $timestamp, int $currentTime) : void {
97
		if ($timestamp === 0) {
98
			throw new AmpacheException('Invalid Login - cannot parse time', 401);
99
		}
100
		if ($timestamp < ($currentTime - self::SESSION_EXPIRY_TIME)) {
101
			throw new AmpacheException('Invalid Login - session is outdated', 401);
102
		}
103
		// Allow the timestamp to be at maximum 10 minutes in the future. The client may use its
104
		// own system clock to generate the timestamp and that may differ from the server's time.
105
		if ($timestamp > $currentTime + 600) {
106
			throw new AmpacheException('Invalid Login - timestamp is in future', 401);
107
		}
108
	}
109
110
	private function checkHandshakeAuthentication(?string $user, int $timestamp, ?string $auth) : int {
111
		if ($user === null || $auth === null) {
112
			throw new AmpacheException('Invalid Login - required credentials missing', 401);
113
		}
114
115
		$hashes = $this->ampacheUserMapper->getPasswordHashes($user);
116
117
		foreach ($hashes as $keyId => $hash) {
118
			$expectedHash = \hash('sha256', $timestamp . $hash);
119
120
			if ($expectedHash === $auth) {
121
				return (int)$keyId;
122
			}
123
		}
124
125
		throw new AmpacheException('Invalid Login - passphrase does not match', 401);
126
	}
127
128
	private function startNewSession(string $user, int $expiryDate, ?string $apiVersion, int $apiKeyId) : AmpacheSession {
129
		// create new session
130
		$session = new AmpacheSession();
131
		$session->setUserId($user);
132
		$session->setToken(Random::secure(16));
133
		$session->setExpiry($expiryDate);
134
		$session->setApiVersion(Util::truncate($apiVersion, 16));
135
		$session->setAmpacheUserId($apiKeyId);
136
137
		// save session to the database
138
		$this->ampacheSessionMapper->insert($session);
0 ignored issues
show
Deprecated Code introduced by
The function OCA\Music\AppFramework\D...xtcloudMapper::insert() has been deprecated: 14.0.0 Move over to QBMapper ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

138
		/** @scrutinizer ignore-deprecated */ $this->ampacheSessionMapper->insert($session);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
139
140
		return $session;
141
	}
142
143
	private function handleNonHandshakeAction(AmpacheController $controller, ?string $action) : void {
144
		$token = $this->request->getParam('auth') ?: $this->request->getParam('ssid');
145
146
		// 'ping' is allowed without a session (but if session token is passed, then it has to be valid)
147
		if ($action === 'ping' && empty($token)) {
148
			return;
149
		}
150
151
		$session = $this->getExistingSession($token);
152
		$controller->setSession($session);
153
154
		if ($action === 'goodbye') {
155
			$this->ampacheSessionMapper->delete($session);
0 ignored issues
show
Deprecated Code introduced by
The function OCA\Music\AppFramework\D...xtcloudMapper::delete() has been deprecated: 14.0.0 Move over to QBMapper ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

155
			/** @scrutinizer ignore-deprecated */ $this->ampacheSessionMapper->delete($session);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
156
		}
157
158
		if ($action === null) {
159
			throw new AmpacheException("Required argument 'action' missing", 400);
160
		}
161
	}
162
163
	private function getExistingSession(?string $token) : AmpacheSession {
164
		if (empty($token)) {
165
			throw new AmpacheException('Invalid Login - session token missing', 401);
166
		} else {
167
			try {
168
				// extend the session deadline on any authorized API call
169
				$this->ampacheSessionMapper->extend($token, \time() + self::SESSION_EXPIRY_TIME);
170
				return $this->ampacheSessionMapper->findByToken($token);
171
			} catch (\OCP\AppFramework\Db\DoesNotExistException $e) {
172
				throw new AmpacheException('Invalid Login - invalid session token', 401);
173
			}
174
		}
175
	}
176
177
	/**
178
	 * If an AmpacheException is being caught, the appropiate ampache
179
	 * exception response is rendered
180
	 *
181
	 * NOTE: Type declarations cannot be used on this function signature because that would be
182
	 * in conflict with the base class which is not in our hands.
183
	 *
184
	 * @param Controller $controller the controller that is being called
185
	 * @param string $methodName the name of the method that will be called on
186
	 *                           the controller
187
	 * @param \Exception $exception the thrown exception
188
	 * @throws \Exception the passed in exception if it wasn't handled
189
	 * @return \OCP\AppFramework\Http\Response object if the exception was handled
190
	 */
191
	public function afterException($controller, $methodName, \Exception $exception) {
192
		if ($controller instanceof AmpacheController) {
193
			if ($exception instanceof AmpacheException) {
194
				return $controller->ampacheErrorResponse($exception->getCode(), $exception->getMessage());
195
			} elseif ($exception instanceof BusinessLayerException) {
196
				return $controller->ampacheErrorResponse(404, 'Entity not found');
197
			}
198
		}
199
		throw $exception;
200
	}
201
202
}
203