PublicAuth.php 6.0 KB

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