Skip to content

Commit

Permalink
Merge pull request #88 from rtm-ctrlz/feat-ssl
Browse files Browse the repository at this point in the history
feat: add support for SSL
  • Loading branch information
WyriHaximus committed Feb 5, 2020
2 parents ff267a0 + f003f83 commit 2987215
Show file tree
Hide file tree
Showing 16 changed files with 406 additions and 7 deletions.
5 changes: 4 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,18 @@ env:
jobs:
- PREFER_LOWEST="--prefer-lowest"
- PREFER_LOWEST=""
- SSL_TEST="yes" SSL_CA="ssl/ca.pem" SSL_PEER_NAME="server.rmq"
- SSL_TEST="yes" SSL_CA="ssl/ca.pem" SSL_PEER_NAME="server.rmq" SSL_CLIENT_CERT="ssl/client.pem" SSL_CLIENT_KEY="ssl/client.key"

jobs:
allow_failures:
- php: nightly

install:
- composer update --dev --prefer-source $PREFER_LOWEST
- composer update --prefer-source $PREFER_LOWEST

before_script:
- if [ ! -z "$SSL_TEST" ] ; then test/ssl/travis.sh; fi
- sudo rabbitmqctl add_vhost testvhost
- sudo rabbitmqctl add_user testuser testpassword
- sudo rabbitmqctl set_permissions -p testvhost testuser ".*" ".*" ".*"
Expand Down
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,33 @@ $bunny = new Client($connection);
$bunny->connect();
```

### Connecting with SSL/TLS

Options for SSL-connections should be specified as array `ssl`:

```php
$connection = [
'host' => 'HOSTNAME',
'vhost' => 'VHOST', // The default vhost is /
'user' => 'USERNAME', // The default user is guest
'password' => 'PASSWORD', // The default password is guest
'ssl' => [
'cafile' => 'ca.pem',
'local_cert' => 'client.cert',
'local_pk' => 'client.key',
],
];

$bunny = new Client($connection);
$bunny->connect();
```

For options description - please see [SSL context options](https://www.php.net/manual/en/context.ssl.php).

Note: invalid SSL configuration will cause connection failure.

See also [common configuration variants](examples/ssl/).

### Publish a message

Now that we have a connection with the server we need to create a channel and declare a queue to communicate over before we can publish a message, or subscribe to a queue for that matter.
Expand Down
18 changes: 18 additions & 0 deletions examples/ssl/01-selfsigned.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

/*
* Use case
* - self-signed certificates
* - peer name (for certificates checks) will be taken from `host`
*
* See also RabbitMQ config: tests/ssl/rabbitmq.ssl.verify_none.conf
*/
$clientConfig = [
'host' => 'rabbitmq.example.com',
// ...
'ssl' => [
'cafile' => 'ca.pem',
'allow_self_signed' => true,
'verify_peer' => true,
],
];
20 changes: 20 additions & 0 deletions examples/ssl/02-peer-verify.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

/*
* Use case
* - self-signed certificates
* - peer name (for certificates checks) should not depend on `host`
* 'rabbitmq.company.ltd' will be used
*
* See also RabbitMQ config: tests/ssl/rabbitmq.ssl.verify_none.conf
*/
$clientConfig = [
'host' => 'rabbitmq.example.com',
// ...
'ssl' => [
'cafile' => 'ca.pem',
'allow_self_signed' => true,
'verify_peer' => true,
'peer_name' => 'rabbitmq.company.ltd',
],
];
20 changes: 20 additions & 0 deletions examples/ssl/03-client-verify.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

/*
* Use case
* - client certificate should be used
* - file `client.pem`:
* - contents both certificate and key
*
* See also RabbitMQ config: tests/ssl/rabbitmq.ssl.verify_peer.conf
*/
$clientConfig = [
'host' => 'rabbitmq.example.com',
// ...
'ssl' => [
'cafile' => 'ca.pem',
'allow_self_signed' => true,
'verify_peer' => true,
'local_cert' => 'client.pem',
],
];
22 changes: 22 additions & 0 deletions examples/ssl/04-client-verify-passphrase.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

/*
* Use case
* - client certificate should be used
* - file `client.pem`:
* - contents both certificate and key
* - encoded with a passphrase
*
* See also RabbitMQ config: tests/ssl/rabbitmq.ssl.verify_peer.conf
*/
$clientConfig = [
'host' => 'rabbitmq.example.com',
// ...
'ssl' => [
'cafile' => 'ca.pem',
'allow_self_signed' => true,
'verify_peer' => true,
'local_cert' => 'client.pem',
'passphrase' => 'passphrase-for-client.pem',
],
];
25 changes: 25 additions & 0 deletions examples/ssl/05-client-verify-multiple-files.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

/*
* Use case
* - client certificate should be used
* - file `client.cert` is a client certificate
* - file `client.key`:
* - is a private key client certificate
* - encoded with a passphrase
*
* See also RabbitMQ config: tests/ssl/rabbitmq.ssl.verify_peer.conf
*/
$clientConfig = [
'host' => 'rabbitmq.example.com',
// ...
'ssl' => [
'cafile' => 'ca.pem',
'allow_self_signed' => true,
'verify_peer' => true,
'local_cert' => 'client.cert',
'local_pk' => 'client.key',
'passphrase' => 'passphrase-for-client.key',
],
];

23 changes: 17 additions & 6 deletions src/Bunny/AbstractClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@
use Psr\Log\LoggerInterface;
use React\Promise;

use function is_array;
use function stream_context_create;
use function stream_context_set_option;

/**
* Base class for synchronous and asynchronous AMQP/RabbitMQ client.
*
Expand Down Expand Up @@ -209,10 +213,18 @@ protected function enqueue(AbstractFrame $frame)
protected function getStream()
{
if ($this->stream === null) {
// TODO: SSL
$streamScheme = 'tcp';

$context = stream_context_create();
if (isset($this->options['ssl']) && is_array($this->options['ssl'])) {
if (!stream_context_set_option($context, ['ssl' => $this->options['ssl']])) {
throw new ClientException("Failed to set SSL-options.");
}
$streamScheme = 'ssl';
}

// see https://github.com/nrk/predis/blob/v1.0/src/Connection/StreamConnection.php
$uri = "tcp://{$this->options["host"]}:{$this->options["port"]}";
$uri = $streamScheme."://{$this->options["host"]}:{$this->options["port"]}";
$flags = STREAM_CLIENT_CONNECT;

if (isset($this->options["async_connect"]) && !!$this->options["async_connect"]) {
Expand All @@ -228,16 +240,15 @@ protected function getStream()

$uri .= (strpos($this->options["path"], "/") === 0) ? $this->options["path"] : "/" . $this->options["path"];
}

// tcp_nodelay was added in 7.1.0
if (PHP_VERSION_ID >= 70100) {
$context = stream_context_create([
stream_context_set_option(
$context, [
"socket" => [
"tcp_nodelay" => true
]
]);
} else {
$context = stream_context_create();
}

$this->stream = @stream_socket_client($uri, $errno, $errstr, (float)$this->options["timeout"], $flags, $context);
Expand Down
145 changes: 145 additions & 0 deletions test/Bunny/SSLTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
<?php

namespace Bunny;

use Bunny\Async\Client as AsyncClient;
use Bunny\Exception\ClientException;
use Bunny\Test\Exception\TimeoutException;
use PHPUnit\Framework\TestCase;

use React\EventLoop\Factory;

use function dirname;
use function file_exists;
use function getenv;
use function is_file;
use function putenv;

class SSLTest extends TestCase
{

public function testConnect()
{
$options = $this->getOptions();

$client = new Client($options);
$client->connect();
$client->disconnect();

$this->assertTrue(true);
}

public function testConnectAsync() {
$options = $this->getOptions();
$loop = Factory::create();

$loop->addTimer(5, function () {
throw new TimeoutException();
});

$client = new AsyncClient($loop, $options);
$client->connect()->then(function (AsyncClient $client) {
return $client->disconnect();
})->then(function () use ($loop) {
$loop->stop();
})->done();

$loop->run();

$this->assertTrue(true);
}

public function testConnectWithMissingClientCert()
{
$options = $this->getOptions();
if (!isset($options['ssl']['local_cert'])) {
$this->markTestSkipped('No client certificate is used');
}

// let's try without client certificate - it should fail
unset($options['ssl']['local_cert'], $options['ssl']['local_pk']);

$this->expectException(ClientException::class);

$client = new Client($options);
$client->connect();
$client->disconnect();
}

public function testConnectToTcpPort()
{
$options = $this->getOptions();
unset($options['port']);

$this->expectException(ClientException::class);

$client = new Client($options);
$client->connect();
$client->disconnect();
}

public function testConnectWithWrongPeerName()
{
putenv('SSL_PEER_NAME=not-existsing-peer-name' . time());
$options = $this->getOptions();

$this->expectException(ClientException::class);

$client = new Client($options);
$client->connect();
$client->disconnect();
}

protected function getOptions()
{
// should we do SSL-tests
if (empty(getenv('SSL_TEST'))) {
$this->markTestSkipped('Skipped due empty ENV-variable "SSL_TEST"');
}

// checking CA-file
$caFile = getenv('SSL_CA');
if (empty($caFile)) {
$this->fail('Missing CA file ENV-variable: "SSL_CA"');
}
$testsDir = dirname(__DIR__);
$caFile = $testsDir . '/' . $caFile;
if (!file_exists($caFile) || !is_file($caFile)) {
$this->fail('Missing CA file: "' . $caFile . '"');
}

$peerName = getenv('SSL_PEER_NAME');
if (empty($peerName)) {
// setting default value from tests/ssl/Makefile
$peerName = 'server.rmq';
}

// minimal SSL-options
$options = [
'port' => 5673,
'ssl' => [
// for tests we are using self-signed certificates
'allow_self_signed' => true,
'cafile' => $caFile,
'peer_name' => $peerName,
],
];


$certFile = getenv('SSL_CLIENT_CERT');
$keyFile = getenv('SSL_CLIENT_KEY');
if (!empty($certFile) && !empty($keyFile)) {
$certFile = $testsDir . '/' . $certFile;
$keyFile = $testsDir . '/' . $keyFile;
if (!file_exists($certFile) || !is_file($certFile)) {
$this->fail('Missing certificate file: "' . $certFile . '"');
}
if (!file_exists($keyFile) || !is_file($keyFile)) {
$this->fail('Missing key file: "' . $keyFile . '"');
}
$options['ssl']['local_cert'] = $certFile;
$options['ssl']['local_pk'] = $keyFile;
}
return $options;
}
}
4 changes: 4 additions & 0 deletions test/ssl/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/*.key
/*.csr
/*.pem
/*.srl

0 comments on commit 2987215

Please sign in to comment.