Skip to content

Commit

Permalink
Use authentication only within domain
Browse files Browse the repository at this point in the history
When a CardDavClient (and therefore HTTP Client) is created, it is given
authentication info and a base URI. The client may be used to send
requests to any URL, including those with different domain. It may not
be desired in such cases that the authentication information is
transmitted to a different domain. (We assume that the domain is under
control of a trusted party that the user intended to auth with).

This only affects the URI given by the using application. In particular,
if redirections occur during a request, authentication will be used if
it was used for the original URI (the redirection is provided by a
trusted domain).

The particular background: VCards may include URIs, for example for the
photo. These may be somewhat easily injected into a user's addressbook
(i.e. send a VCard and import it). In this case, it might be dangerous
to send the credentials to a server hosting such an URI. If the client
is willing to do this, they have to create a separate client object with
a suited base URI.
  • Loading branch information
mstilkerich committed Jul 18, 2020
1 parent cbf2918 commit 38562a2
Show file tree
Hide file tree
Showing 3 changed files with 78 additions and 24 deletions.
2 changes: 1 addition & 1 deletion src/CardDavClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ class CardDavClient
/** @var string */
protected $base_uri;

/** @var HttpClientAdapterInterface */
/** @var HttpClientAdapter */
protected $httpClient;

/********* PUBLIC FUNCTIONS *********/
Expand Down
76 changes: 62 additions & 14 deletions src/HttpClientAdapterInterface.php → src/HttpClientAdapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,16 @@
* along with PHP-CardDavClient. If not, see <https://www.gnu.org/licenses/>.
*/

declare(strict_types=1);

namespace MStilkerich\CardDavClient;

use Psr\Http\Message\ResponseInterface as Psr7Response;

/**
* Interface for the internal HTTP client adapter.
* Abstract base class for the internal HTTP client adapter.
*
* This interface intends to decouple the rest of this library from the underlying HTTP client library to allow for
* This class intends to decouple the rest of this library from the underlying HTTP client library to allow for
* future replacement.
*
* We aim at staying close to the PSR-18 definition of the Http ClientInterface, however, because Guzzle does currently
Expand All @@ -35,18 +41,11 @@
* So for now, this is not compliant with PSR-18 for simplicity, but we aim at staying close to the definition
* considering a potential later refactoring.
*/

declare(strict_types=1);

namespace MStilkerich\CardDavClient;

use Psr\Http\Message\ResponseInterface as Psr7Response;

/**
* Interface for the internal HTTP client adapter.
*/
interface HttpClientAdapterInterface
abstract class HttpClientAdapter
{
/** @var string The base URI for requests */
protected $baseUri;

/**
* Sends an HTTP request and returns a PSR-7 response.
*
Expand All @@ -66,7 +65,56 @@ interface HttpClientAdapterInterface
* @throws \Psr\Http\Client\NetworkExceptionInterface if the request cannot be sent due to a network failure of any
* kind, including a timeout
*/
public function sendRequest(string $method, string $uri, array $options = []): Psr7Response;
abstract public function sendRequest(string $method, string $uri, array $options = []): Psr7Response;

/**
* Checks whether the given URI has the same domain as the base URI of this HTTP client.
*
* If the given URI does not contain a domain part, true is returned (as when used, it will
* get that part from the base URI).
*
* @param string $uri The URI to check
* @return bool True if the URI shares the same domain as the base URI.
*/
protected function checkSameDomainAsBase(string $uri): bool
{
$result = false;
$compUri = \Sabre\Uri\parse($uri);

// if the URI is relative, the domain is the same
if (isset($compUri["host"])) {
$compBase = \Sabre\Uri\parse($this->baseUri);

$result = strcasecmp(
self::getDomainFromSubdomain($compUri["host"]),
self::getDomainFromSubdomain($compBase["host"] ?? "")
) === 0;
} else {
$result = true;
}

return $result;
}

/**
* Extracts the domain name from a subdomain.
*
* If the given string does not have a subdomain (i.e. top-level domain or domain only),
* it is returned as provided.
*
* @param string $subdomain The subdomain (e.g. sub.example.com)
* @return string The domain of $subdomain (e.g. example.com)
*/
protected static function getDomainFromSubdomain(string $subdomain): string
{
$parts = explode(".", $subdomain);

if (count($parts) > 2) {
$subdomain = implode(".", array_slice($parts, -2));
}

return $subdomain;
}
}

// vim: ts=4:sw=4:expandtab:fenc=utf8:ff=unix:tw=120
24 changes: 15 additions & 9 deletions src/HttpClientAdapterGuzzle.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
/**
* Adapter for the Guzzle HTTP client library.
*/
class HttpClientAdapterGuzzle implements HttpClientAdapterInterface
class HttpClientAdapterGuzzle extends HttpClientAdapter
{
/** @var string[] A list of authentication schemes that can be handled by Guzzle itself,
* independent on whether it works only with the Guzzle Curl HTTP handler or not.
Expand All @@ -61,7 +61,7 @@ class HttpClientAdapterGuzzle implements HttpClientAdapterInterface
/** @var string[] Auth-schemes tried without success */
private $failedAuthSchemes = [];

/** @var ?array Maps lowercase auth-schemes to their CURLAUTH_XXX constant.
/** @var ?int[] Maps lowercase auth-schemes to their CURLAUTH_XXX constant.
* Only values not part of GUZZLE_KNOWN_AUTHSCHEMES are relevant here.
*/
private static $schemeToCurlOpt;
Expand All @@ -76,6 +76,7 @@ class HttpClientAdapterGuzzle implements HttpClientAdapterInterface
*/
public function __construct(string $base_uri, string $username, string $password)
{
$this->baseUri = $base_uri;
$this->username = $username;
$this->password = $password;

Expand All @@ -95,7 +96,7 @@ public function __construct(string $base_uri, string $username, string $password
new MessageFormatter("\"{method} {target} HTTP/{version}\" {code}\n" . MessageFormatter::DEBUG)
));

$guzzleOptions = $this->prepareGuzzleOptions([]);
$guzzleOptions = $this->prepareGuzzleOptions();
$guzzleOptions['handler'] = $stack;
$guzzleOptions['http_errors'] = false; // no exceptions on 4xx/5xx status, also required by PSR-18
$guzzleOptions['base_uri'] = $base_uri;
Expand All @@ -106,23 +107,28 @@ public function __construct(string $base_uri, string $username, string $password
/**
* Sends a PSR-7 request and returns a PSR-7 response.
*
* The given URI may be relative to the base URI given on construction of this object or a full URL.
* Authentication is only attempted in case the domain name of the request URI matches that of the base URI
* (subdomains may differ).
*
* @param array $options Options for the HTTP client, and default request options. May include any of the options
* accepted by {@see HttpClientAdapterInterface::sendRequest()}.
* accepted by {@see HttpClientAdapter::sendRequest()}.
*/
public function sendRequest(string $method, string $uri, array $options = []): Psr7Response
{
$guzzleOptions = $this->prepareGuzzleOptions($options);
$doAuth = $this->checkSameDomainAsBase($uri);
$guzzleOptions = $this->prepareGuzzleOptions($options, $doAuth);

try {
$response = $this->client->request($method, $uri, $guzzleOptions);

if ($response->getStatusCode() == 401) {
if ($doAuth && $response->getStatusCode() == 401) {
foreach ($this->getSupportedAuthSchemes($response) as $scheme) {
$this->authScheme = $scheme;

Config::$logger->debug("Trying auth scheme $scheme");

$guzzleOptions = $this->prepareGuzzleOptions($options);
$guzzleOptions = $this->prepareGuzzleOptions($options, $doAuth);
$response = $this->client->request($method, $uri, $guzzleOptions);

if ($response->getStatusCode() != 401) {
Expand All @@ -149,7 +155,7 @@ public function sendRequest(string $method, string $uri, array $options = []): P
}

/********* PRIVATE FUNCTIONS *********/
private function prepareGuzzleOptions(array $options): array
private function prepareGuzzleOptions(array $options = [], bool $doAuth = false): array
{
$guzzleOptions = [];
$curlLoaded = extension_loaded("curl");
Expand All @@ -172,7 +178,7 @@ private function prepareGuzzleOptions(array $options): array
];
}

if (isset($this->authScheme)) {
if ($doAuth && isset($this->authScheme)) {
$authScheme = $this->authScheme;
Config::$logger->debug("Using auth scheme $authScheme");

Expand Down

0 comments on commit 38562a2

Please sign in to comment.