PublicAuth.php 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
  5. * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
  6. * SPDX-License-Identifier: AGPL-3.0-only
  7. */
  8. namespace OCA\DAV\Connector\Sabre;
  9. use OCP\Defaults;
  10. use OCP\IRequest;
  11. use OCP\ISession;
  12. use OCP\Security\Bruteforce\IThrottler;
  13. use OCP\Share\Exceptions\ShareNotFound;
  14. use OCP\Share\IManager;
  15. use OCP\Share\IShare;
  16. use Psr\Log\LoggerInterface;
  17. use Sabre\DAV\Auth\Backend\AbstractBasic;
  18. use Sabre\DAV\Exception\NotAuthenticated;
  19. use Sabre\DAV\Exception\NotFound;
  20. use Sabre\DAV\Exception\ServiceUnavailable;
  21. use Sabre\HTTP;
  22. use Sabre\HTTP\RequestInterface;
  23. use Sabre\HTTP\ResponseInterface;
  24. /**
  25. * Class PublicAuth
  26. *
  27. * @package OCA\DAV\Connector
  28. */
  29. class PublicAuth extends AbstractBasic {
  30. private const BRUTEFORCE_ACTION = 'public_dav_auth';
  31. public const DAV_AUTHENTICATED = 'public_link_authenticated';
  32. private ?IShare $share = null;
  33. public function __construct(
  34. private IRequest $request,
  35. private IManager $shareManager,
  36. private ISession $session,
  37. private IThrottler $throttler,
  38. private LoggerInterface $logger,
  39. ) {
  40. // setup realm
  41. $defaults = new Defaults();
  42. $this->realm = $defaults->getName();
  43. }
  44. /**
  45. * @param RequestInterface $request
  46. * @param ResponseInterface $response
  47. *
  48. * @return array
  49. * @throws NotAuthenticated
  50. * @throws ServiceUnavailable
  51. */
  52. public function check(RequestInterface $request, ResponseInterface $response): array {
  53. try {
  54. $this->throttler->sleepDelayOrThrowOnMax($this->request->getRemoteAddress(), self::BRUTEFORCE_ACTION);
  55. $auth = new HTTP\Auth\Basic(
  56. $this->realm,
  57. $request,
  58. $response
  59. );
  60. $userpass = $auth->getCredentials();
  61. // If authentication provided, checking its validity
  62. if ($userpass && !$this->validateUserPass($userpass[0], $userpass[1])) {
  63. return [false, 'Username or password was incorrect'];
  64. }
  65. return $this->checkToken();
  66. } catch (NotAuthenticated $e) {
  67. throw $e;
  68. } catch (\Exception $e) {
  69. $class = get_class($e);
  70. $msg = $e->getMessage();
  71. $this->logger->error($e->getMessage(), ['exception' => $e]);
  72. throw new ServiceUnavailable("$class: $msg");
  73. }
  74. }
  75. /**
  76. * Extract token from request url
  77. * @return string
  78. * @throws NotFound
  79. */
  80. private function getToken(): string {
  81. $path = $this->request->getPathInfo() ?: '';
  82. // ['', 'dav', 'files', 'token']
  83. $splittedPath = explode('/', $path);
  84. if (count($splittedPath) < 4 || $splittedPath[3] === '') {
  85. throw new NotFound();
  86. }
  87. return $splittedPath[3];
  88. }
  89. /**
  90. * Check token validity
  91. * @return array
  92. * @throws NotFound
  93. * @throws NotAuthenticated
  94. */
  95. private function checkToken(): array {
  96. $token = $this->getToken();
  97. try {
  98. /** @var IShare $share */
  99. $share = $this->shareManager->getShareByToken($token);
  100. } catch (ShareNotFound $e) {
  101. $this->throttler->registerAttempt(self::BRUTEFORCE_ACTION, $this->request->getRemoteAddress());
  102. throw new NotFound();
  103. }
  104. $this->share = $share;
  105. \OC_User::setIncognitoMode(true);
  106. // If already authenticated
  107. if ($this->session->exists(self::DAV_AUTHENTICATED)
  108. && $this->session->get(self::DAV_AUTHENTICATED) === $share->getId()) {
  109. return [true, $this->principalPrefix . $token];
  110. }
  111. // If the share is protected but user is not authenticated
  112. if ($share->getPassword() !== null) {
  113. $this->throttler->registerAttempt(self::BRUTEFORCE_ACTION, $this->request->getRemoteAddress());
  114. throw new NotAuthenticated();
  115. }
  116. return [true, $this->principalPrefix . $token];
  117. }
  118. /**
  119. * Validates a username and password
  120. *
  121. * This method should return true or false depending on if login
  122. * succeeded.
  123. *
  124. * @param string $username
  125. * @param string $password
  126. *
  127. * @return bool
  128. * @throws NotAuthenticated
  129. */
  130. protected function validateUserPass($username, $password) {
  131. $this->throttler->sleepDelayOrThrowOnMax($this->request->getRemoteAddress(), self::BRUTEFORCE_ACTION);
  132. $token = $this->getToken();
  133. try {
  134. $share = $this->shareManager->getShareByToken($token);
  135. } catch (ShareNotFound $e) {
  136. $this->throttler->registerAttempt(self::BRUTEFORCE_ACTION, $this->request->getRemoteAddress());
  137. return false;
  138. }
  139. $this->share = $share;
  140. \OC_User::setIncognitoMode(true);
  141. // check if the share is password protected
  142. if ($share->getPassword() !== null) {
  143. if ($share->getShareType() === IShare::TYPE_LINK
  144. || $share->getShareType() === IShare::TYPE_EMAIL
  145. || $share->getShareType() === IShare::TYPE_CIRCLE) {
  146. if ($this->shareManager->checkPassword($share, $password)) {
  147. // If not set, set authenticated session cookie
  148. if (!$this->session->exists(self::DAV_AUTHENTICATED)
  149. || $this->session->get(self::DAV_AUTHENTICATED) !== $share->getId()) {
  150. $this->session->set(self::DAV_AUTHENTICATED, $share->getId());
  151. }
  152. return true;
  153. }
  154. if ($this->session->exists(PublicAuth::DAV_AUTHENTICATED)
  155. && $this->session->get(PublicAuth::DAV_AUTHENTICATED) === $share->getId()) {
  156. return true;
  157. }
  158. if (in_array('XMLHttpRequest', explode(',', $this->request->getHeader('X-Requested-With')))) {
  159. // do not re-authenticate over ajax, use dummy auth name to prevent browser popup
  160. http_response_code(401);
  161. header('WWW-Authenticate: DummyBasic realm="' . $this->realm . '"');
  162. throw new NotAuthenticated('Cannot authenticate over ajax calls');
  163. }
  164. $this->throttler->registerAttempt(self::BRUTEFORCE_ACTION, $this->request->getRemoteAddress());
  165. return false;
  166. } elseif ($share->getShareType() === IShare::TYPE_REMOTE) {
  167. return true;
  168. }
  169. $this->throttler->registerAttempt(self::BRUTEFORCE_ACTION, $this->request->getRemoteAddress());
  170. return false;
  171. }
  172. return true;
  173. }
  174. public function getShare(): IShare {
  175. assert($this->share !== null);
  176. return $this->share;
  177. }
  178. }