|
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); |
|
|
|
|
|
|
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); |
|
|
|
|
|
|
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
|
|
|
|
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.