Skip to content

Commit

Permalink
Merge pull request #341 from saloonphp/feature/add-certificate-authen…
Browse files Browse the repository at this point in the history
…ticator

Feature | Added certificate authenticator
  • Loading branch information
Sammyjo20 authored Dec 2, 2023
2 parents f7d2df9 + 8147e91 commit d84cda4
Show file tree
Hide file tree
Showing 8 changed files with 124 additions and 3 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ jobs:
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, bcmath, intl, exif, iconv, fileinfo
extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, bcmath, intl, exif, iconv, fileinfo, xsl, sodium
coverage: none

- name: Setup problem matchers
Expand Down
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"league/flysystem": "^3.0",
"pestphp/pest": "^2.6",
"phpstan/phpstan": "^1.9",
"saloonphp/xml-wrangler": "^1.1",
"spatie/ray": "^1.33",
"symfony/dom-crawler": "^6.0"
},
Expand Down
45 changes: 45 additions & 0 deletions src/Http/Auth/CertificateAuthenticator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

declare(strict_types=1);

namespace Saloon\Http\Auth;

use GuzzleHttp\RequestOptions;
use Saloon\Http\PendingRequest;
use Saloon\Contracts\Authenticator;
use Saloon\Http\Senders\GuzzleSender;
use Saloon\Exceptions\SaloonException;

class CertificateAuthenticator implements Authenticator
{
/**
* Constructor
*/
public function __construct(
public string $path,
public ?string $password = null,
) {
//
}

/**
* Apply the authentication to the request.
*
* @throws \Saloon\Exceptions\SaloonException
*/
public function set(PendingRequest $pendingRequest): void
{
if (! $pendingRequest->getConnector()->sender() instanceof GuzzleSender) {
throw new SaloonException('The CertificateAuthenticator is only supported when using the GuzzleSender.');
}

// See: https://docs.guzzlephp.org/en/stable/request-options.html#cert

$path = $this->path;
$password = $this->password;

$certificate = is_string($password) ? [$path, $password] : $path;

$pendingRequest->config()->add(RequestOptions::CERT, $certificate);
}
}
27 changes: 25 additions & 2 deletions src/Http/Response.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use Saloon\Traits\Macroable;
use InvalidArgumentException;
use Saloon\Helpers\ArrayHelpers;
use Saloon\XmlWrangler\XmlReader;
use Illuminate\Support\Collection;
use Saloon\Contracts\FakeResponse;
use Saloon\Repositories\ArrayStore;
Expand Down Expand Up @@ -136,11 +137,19 @@ public function getPsrResponse(): ResponseInterface
*/
public function body(): string
{
return $this->stream()->getContents();
$stream = $this->stream();

$contents = $stream->getContents();

if ($stream->isSeekable()) {
$stream->rewind();
}

return $contents;
}

/**
* Get the body as a stream. Don't forget to close the stream after using ->close().
* Get the body as a stream.
*/
public function stream(): StreamInterface
{
Expand Down Expand Up @@ -227,6 +236,8 @@ public function object(): object

/**
* Convert the XML response into a SimpleXMLElement.
*
* @deprecated Use the xmlReader method instead.
*/
public function xml(mixed ...$arguments): SimpleXMLElement|bool
{
Expand All @@ -237,6 +248,18 @@ public function xml(mixed ...$arguments): SimpleXMLElement|bool
return simplexml_load_string($this->decodedXml, ...$arguments);
}

/**
* Load the XML response into a reader
*
* Requires XML Wrangler (composer require saloonphp/xml-wrangler)
*
* @see https://github.com/saloonphp/xml-wrangler
*/
public function xmlReader(): XmlReader
{
return XmlReader::fromSaloonResponse($this);
}

/**
* Get the JSON decoded body of the response as a collection.
*
Expand Down
11 changes: 11 additions & 0 deletions src/Traits/Auth/AuthenticatesRequests.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use Saloon\Http\Auth\TokenAuthenticator;
use Saloon\Http\Auth\DigestAuthenticator;
use Saloon\Http\Auth\HeaderAuthenticator;
use Saloon\Http\Auth\CertificateAuthenticator;

trait AuthenticatesRequests
{
Expand Down Expand Up @@ -95,4 +96,14 @@ public function withHeaderAuth(string $accessToken, string $headerName = 'Author
{
return $this->authenticate(new HeaderAuthenticator($accessToken, $headerName));
}

/**
* Authenticate the request with a certificate.
*
* @return $this
*/
public function withCertificateAuth(string $path, ?string $password = null): static
{
return $this->authenticate(new CertificateAuthenticator($path, $password));
}
}
1 change: 1 addition & 0 deletions tests/Fixtures/Saloon/xml.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"statusCode":200,"headers":{"Date":"Sat, 02 Dec 2023 16:01:23 GMT","Content-Type":"text\/xml; charset=UTF-8","Content-Length":"1317","Connection":"keep-alive","access-control-allow-origin":"*","Cache-Control":"no-cache, private","x-frame-options":"SAMEORIGIN","x-xss-protection":"1; mode=block","x-content-type-options":"nosniff","CF-Cache-Status":"DYNAMIC","Report-To":"{\"endpoints\":[{\"url\":\"https:\\\/\\\/a.nel.cloudflare.com\\\/report\\\/v3?s=8t7HkDmpXwrJNaQUDwqb6GXENRT2oZt3tqla7AT69PlDzz%2FoF6lUjsYS%2BYYE7%2FR9x3mRtRFgOGqlr0zT0byUcjWetwrtOaO4O%2BGho%2B1Frpk3erLyDD0kQEp6TA%2BhiUqeB%2FSYRXWwO9%2FHohaktPsv\"}],\"group\":\"cf-nel\",\"max_age\":604800}","NEL":"{\"success_fraction\":0,\"report_to\":\"cf-nel\",\"max_age\":604800}","Server":"cloudflare","CF-RAY":"82f4c9cb9b1f638b-LHR","alt-svc":"h3=\":443\"; ma=86400"},"data":"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<breakfast_menu name=\"Big G's Breakfasts\">\n <food soldOut=\"false\" bestSeller=\"true\">\n <name>Belgian Waffles<\/name>\n <price>$5.95<\/price>\n <description>Two of our famous Belgian Waffles with plenty of real maple syrup<\/description>\n <calories>650<\/calories>\n <\/food>\n <food soldOut=\"false\" bestSeller=\"false\">\n <name>Strawberry Belgian Waffles<\/name>\n <price>$7.95<\/price>\n <description>Light Belgian waffles covered with strawberries and whipped cream<\/description>\n <calories>900<\/calories>\n <\/food>\n <food soldOut=\"false\" bestSeller=\"true\">\n <name>Berry-Berry Belgian Waffles<\/name>\n <price>$8.95<\/price>\n <description>Light Belgian waffles covered with an assortment of fresh berries and whipped cream<\/description>\n <calories>900<\/calories>\n <\/food>\n <food soldOut=\"true\" bestSeller=\"false\">\n <name>French Toast<\/name>\n <price>$4.50<\/price>\n <description>Thick slices made from our homemade sourdough bread<\/description>\n <calories>600<\/calories>\n <\/food>\n <food soldOut=\"false\" bestSeller=\"false\">\n <name>Homestyle Breakfast<\/name>\n <price>$6.95<\/price>\n <description>Two eggs, bacon or sausage, toast, and our ever-popular hash browns<\/description>\n <calories>950<\/calories>\n <\/food>\n<\/breakfast_menu>\n"}
25 changes: 25 additions & 0 deletions tests/Unit/AuthenticatesRequestsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

declare(strict_types=1);

use GuzzleHttp\RequestOptions;
use Saloon\Exceptions\SaloonException;
use Saloon\Tests\Fixtures\Requests\UserRequest;
use Saloon\Tests\Fixtures\Connectors\ArraySenderConnector;
Expand Down Expand Up @@ -61,3 +62,27 @@

expect($query)->toHaveKey('X-Authorization', 'Sammyjo20');
});

test('you can add a certificate to a request', function () {
$certPath = __DIR__ . '/certificate.cer';

$requestA = UserRequest::make()->withCertificateAuth($certPath);

$pendingRequestA = connector()->createPendingRequest($requestA);
$configA = $pendingRequestA->config()->all();

expect($configA)->toBe([
RequestOptions::CERT => $certPath,
]);

// Test with password

$requestB = UserRequest::make()->withCertificateAuth($certPath, 'example');

$pendingRequestB = connector()->createPendingRequest($requestB);
$configB = $pendingRequestB->config()->all();

expect($configB)->toBe([
RequestOptions::CERT => [$certPath, 'example'],
]);
});
15 changes: 15 additions & 0 deletions tests/Unit/ResponseTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
use Saloon\Exceptions\Request\RequestException;
use Saloon\Tests\Fixtures\Requests\UserRequest;
use Saloon\Tests\Fixtures\Connectors\TestConnector;
use Saloon\Tests\Fixtures\Requests\CustomEndpointRequest;

test('you can get the original pending request', function () {
$mockClient = new MockClient([
Expand Down Expand Up @@ -239,6 +240,20 @@
expect($simpleXml)->toBeInstanceOf(SimpleXMLElement::class);
});

test('the xmlReader method will return an XmlReader instance', function () {
$mockClient = new MockClient([
MockResponse::fixture('xml'),
]);

$request = new CustomEndpointRequest;
$request->setEndpoint('/breakfast-menu');

$response = connector()->send($request, $mockClient);
$reader = $response->xmlReader();

expect($reader->value('food.2.name')->sole())->toBe('Berry-Berry Belgian Waffles');
});

test('the headers method returns an array store', function () {
$mockClient = new MockClient([
MockResponse::make(['name' => 'Sam', 'work' => 'Codepotato'], 200, ['X-Greeting' => 'Howdy']),
Expand Down

0 comments on commit d84cda4

Please sign in to comment.