Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions lib/ConfigLexicon.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
class ConfigLexicon implements ILexicon {
public const GS_TOKENS = 'globalScaleTokens';
public const LOCAL_TOKEN = 'localToken';
public const REDIRECT_WEBDAV = 'redirectWebDAV';

#[\Override]
public function getStrictness(): Strictness {
Expand All @@ -30,6 +31,7 @@ public function getAppConfigs(): array {
return [
new Entry(key: self::GS_TOKENS, type: ValueType::ARRAY, defaultRaw: [], definition: 'list of token+host to navigate through GlobalScale', lazy: true),
new Entry(key: self::LOCAL_TOKEN, type: ValueType::STRING, defaultRaw: '', definition: 'local token to id instance within GlobalScale', lazy: true),
new Entry(key: self::REDIRECT_WEBDAV, type: ValueType::BOOL, defaultRaw: false, definition: 'redirect WebDAV request on Master to Slaves', lazy: false),
];
}

Expand Down
24 changes: 23 additions & 1 deletion lib/Master.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
use OCP\Authentication\IApacheBackend;
use OCP\HintException;
use OCP\Http\Client\IClientService;
use OCP\IAppConfig;
use OCP\IConfig;
use OCP\IRequest;
use OCP\ISession;
Expand Down Expand Up @@ -48,6 +49,7 @@ public function __construct(
private readonly Lookup $lookup,
private readonly IRequest $request,
private readonly IClientService $clientService,
private readonly IAppConfig $appConfig,
private readonly IConfig $config,
private readonly LoggerInterface $logger,
) {
Expand Down Expand Up @@ -269,6 +271,14 @@ protected function redirectUser($uid, $password, $location, array $options = [])
// check for both possible direct webdav end-points
$isDirectWebDavAccess = strpos($requestUri, 'remote.php/webdav') !== false;
$isDirectWebDavAccess = $isDirectWebDavAccess || strpos($requestUri, 'remote.php/dav') !== false;

$authHeader = $this->request->getHeader('Authorization');
$redirectWebDav = $this->appConfig->getValueBool(Application::APP_ID, ConfigLexicon::REDIRECT_WEBDAV);
$hasBasicAuth = $redirectWebDav && $authHeader !== '' && str_starts_with(strtolower($authHeader), 'basic ');

// default redirect status code; overridden below for the 307 forward.
$statusCode = 302;

// direct webdav access with old client or general purpose webdav clients
if ($isClient && $isDirectWebDavAccess) {
$this->logger->debug('redirectUser: client direct webdav request');
Expand All @@ -286,14 +296,26 @@ protected function redirectUser($uid, $password, $location, array $options = [])
// fallback to v1
$redirectUrl = 'nc://login/server:' . $location . '&user:' . urlencode($uid) . '&password:' . urlencode($appToken);
}
} elseif ($isDirectWebDavAccess && $hasBasicAuth) {
// Third-party WebDAV clients authenticated with HTTP Basic
// (curl, rclone, davfs2, sabre/dav based clients, generic DAV
// consumers, etc.): forward the request as-is to the slave with
// a 307 (RFC 9110 §15.4.8) so that PUT, PROPFIND, MKCOL, DELETE,
// COPY and MOVE are not downgraded to GET, and the original
// request URI is preserved end-to-end. The client re-issues the
// same request to the slave, including the Authorization header
// it already presented to the master.
$this->logger->debug('redirectUser: third-party webdav request with Basic Auth, forwarding with 307');
$redirectUrl = rtrim($location, '/') . $requestUri;
$statusCode = 307;
} else {
$this->logger->debug('redirectUser: direct login so forward to target node');
$jwt = $this->createJwt($uid, $password, $options);
$redirectUrl = $location . '/index.php/apps/globalsiteselector/autologin?jwt=' . $jwt;
}

$this->logger->debug('redirectUser: redirecting to: ' . $redirectUrl);
header('Location: ' . $redirectUrl, true, 302);
header('Location: ' . $redirectUrl, true, $statusCode);
die();
}

Expand Down
49 changes: 16 additions & 33 deletions tests/unit/lib/MasterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,46 +15,31 @@
use OCA\GlobalSiteSelector\Master;
use OCA\GlobalSiteSelector\Vendor\Firebase\JWT\JWT;
use OCA\GlobalSiteSelector\Vendor\Firebase\JWT\Key;
use OCP\AppFramework\IAppContainer;
use OCP\HintException;
use OCP\Http\Client\IClientService;
use OCP\IAppConfig;
use OCP\IConfig;
use OCP\IRequest;
use OCP\ISession;
use OCP\Security\ICrypto;
use OCP\Server;
use OCP\ServerVersion;
use PHPUnit\Framework\MockObject\MockObject;
use Psr\Container\ContainerInterface;
use Psr\Log\LoggerInterface;
use Test\TestCase;

class MasterTest extends TestCase {
/** @var GlobalSiteSelector|\PHPUnit_Framework_MockObject_MockObject */
private $gss;

/** @var ICrypto|\PHPUnit_Framework_MockObject_MockObject */
private $crypto;

/** @var Lookup|\PHPUnit_Framework_MockObject_MockObject */
private $lookup;

/** @var IRequest|\PHPUnit_Framework_MockObject_MockObject */
private $request;

/** @var IClientService | \PHPUnit_Framework_MockObject_MockObject */
private $clientService;

/** @var IConfig | \PHPUnit_Framework_MockObject_MockObject */
private $config;

/** @var \PHPUnit_Framework_MockObject_MockObject|LoggerInterface */
private $logger;

/** @var \PHPUnit_Framework_MockObject_MockObject|IAppContainer */
private $container;

/** @var ISession | \PHPUnit_Framework_MockObject_MockObject */
private $session;
private GlobalSiteSelector&MockObject $gss;
private ICrypto&MockObject $crypto;
private Lookup&MockObject $lookup;
private IRequest&MockObject $request;
private IClientService&MockObject $clientService;
private IConfig&MockObject $config;
private IAppConfig&MockObject $appConfig;
private LoggerInterface&MockObject $logger;
private ContainerInterface&MockObject $container;
private ISession&MockObject $session;
private LoginFlowV2Service&MockObject $loginflow;
private ServerVersion $serverVersion;

Expand All @@ -71,16 +56,13 @@ public function setUp(): void {
$this->request = $this->createMock(IRequest::class);
$this->clientService = $this->createMock(IClientService::class);
$this->config = $this->createMock(IConfig::class);
$this->appConfig = $this->createMock(IAppConfig::class);
$this->logger = $this->createMock(LoggerInterface::class);
$this->container = $this->createMock(IAppContainer::class);
$this->container = $this->createMock(ContainerInterface::class);
$this->session = $this->createMock(ISession::class);
}

/**
* @param array $mockMethods
* @return Master|\PHPUnit_Framework_MockObject_MockObject
*/
private function getInstance(array $mockMethods = []) {
private function getInstance(array $mockMethods = []): Master&MockObject {
return $this->getMockBuilder(Master::class)
->setConstructorArgs(
[
Expand All @@ -92,6 +74,7 @@ private function getInstance(array $mockMethods = []) {
$this->lookup,
$this->request,
$this->clientService,
$this->appConfig,
$this->config,
$this->logger
]
Expand Down
Loading