Skip to content

Commit

Permalink
Merge pull request #104 from neo4j-php/ssl
Browse files Browse the repository at this point in the history
Created SSL configuration override at driver level
  • Loading branch information
transistive authored Jan 14, 2022
2 parents 7eeaaf2 + 7757f58 commit 2db4982
Show file tree
Hide file tree
Showing 14 changed files with 457 additions and 90 deletions.
122 changes: 65 additions & 57 deletions src/Bolt/BoltConnectionPool.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,12 @@
use Bolt\Bolt;
use Bolt\connection\StreamSocket;
use Exception;
use function explode;
use const FILTER_VALIDATE_IP;
use function filter_var;
use Laudis\Neo4j\Common\BoltConnection;
use Laudis\Neo4j\Contracts\AuthenticateInterface;
use Laudis\Neo4j\Contracts\ConnectionInterface;
use Laudis\Neo4j\Contracts\ConnectionPoolInterface;
use Laudis\Neo4j\Databags\DatabaseInfo;
use Laudis\Neo4j\Databags\DriverConfiguration;
use Laudis\Neo4j\Databags\SessionConfiguration;
use Laudis\Neo4j\Enum\ConnectionProtocol;
use Laudis\Neo4j\Neo4j\RoutingTable;
Expand All @@ -39,8 +37,19 @@
*/
final class BoltConnectionPool implements ConnectionPoolInterface
{
/** @var array<string, list<ConnectionInterface<Bolt>>> */
/** @var array<string, list<BoltConnection>> */
private static array $connectionCache = [];
private DriverConfiguration $driverConfig;
private SslConfigurator $sslConfigurator;

/**
* @psalm-external-mutation-free
*/
public function __construct(DriverConfiguration $driverConfig, SslConfigurator $sslConfigurator)
{
$this->driverConfig = $driverConfig;
$this->sslConfigurator = $sslConfigurator;
}

/**
* @throws Exception
Expand All @@ -60,8 +69,20 @@ public function acquire(
self::$connectionCache[$key] = [];
}

foreach (self::$connectionCache[$key] as $connection) {
foreach (self::$connectionCache[$key] as $i => $connection) {
if (!$connection->isOpen()) {
$sslConfig = $connection->getDriverConfiguration()->getSslConfiguration();
$newSslConfig = $this->driverConfig->getSslConfiguration();
if ($sslConfig->getMode() !== $newSslConfig->getMode() ||
$sslConfig->isVerifyPeer() === $newSslConfig->isVerifyPeer()
) {
$connection = $this->openConnection($connectingTo, $socketTimeout, $uri, $table, $authenticate, $userAgent, $config);

/** @psalm-suppress PropertyTypeCoercion */
self::$connectionCache[$key][$i] = $connection;

return $connection;
}
$connection->open();

$authenticate->authenticateBolt($connection->getImplementation(), $connectingTo, $userAgent);
Expand All @@ -70,9 +91,42 @@ public function acquire(
}
}

$connection = $this->openConnection($connectingTo, $socketTimeout, $uri, $table, $authenticate, $userAgent, $config);

self::$connectionCache[$key][] = $connection;

return $connection;
}

public function canConnect(UriInterface $uri, AuthenticateInterface $authenticate, ?RoutingTable $table = null, ?UriInterface $server = null): bool
{
$connectingTo = $server ?? $uri;
$socket = new StreamSocket($uri->getHost(), $connectingTo->getPort() ?? 7687);

$this->setupSsl($uri, $connectingTo, $table, $socket);

try {
$bolt = new Bolt($socket);
$authenticate->authenticateBolt($bolt, $connectingTo, 'ping');
} catch (Throwable $e) {
return false;
}

return true;
}

private function openConnection(
UriInterface $connectingTo,
float $socketTimeout,
UriInterface $uri,
?RoutingTable $table,
AuthenticateInterface $authenticate,
string $userAgent,
SessionConfiguration $config
): BoltConnection {
$socket = new StreamSocket($connectingTo->getHost(), $connectingTo->getPort() ?? 7687, $socketTimeout);

$this->configureSsl($uri, $connectingTo, $socket, $table);
$this->setupSsl($uri, $connectingTo, $table, $socket);

$bolt = new Bolt($socket);
$authenticate->authenticateBolt($bolt, $connectingTo, $userAgent);
Expand Down Expand Up @@ -104,6 +158,7 @@ public function acquire(
ConnectionProtocol::determineBoltVersion($bolt),
$config->getAccessMode(),
new DatabaseInfo($config->getDatabase()),
$this->driverConfig,
static function () use ($socket, $authenticate, $connectingTo, $userAgent, $originalBolt) {
$bolt = $originalBolt->get();
if ($bolt === null) {
Expand All @@ -117,61 +172,14 @@ static function () use ($socket, $authenticate, $connectingTo, $userAgent, $orig

$connection->open();

self::$connectionCache[$key][] = $connection;

return $connection;
}

private function configureSsl(UriInterface $uri, UriInterface $server, StreamSocket $socket, ?RoutingTable $table): void
private function setupSsl(UriInterface $uri, UriInterface $connectingTo, ?RoutingTable $table, StreamSocket $socket): void
{
$scheme = $uri->getScheme();
$explosion = explode('+', $scheme, 2);
$sslConfig = $explosion[1] ?? '';

if (str_starts_with($sslConfig, 's')) {
// We have to pass a different host when working with ssl on aura.
// There is a strange behaviour where if we pass the uri host on a single
// instance aura deployment, we need to pass the original uri for the
// ssl configuration to be valid.
if ($table && count($table->getWithRole()) > 1) {
$this->enableSsl($server->getHost(), $sslConfig, $socket);
} else {
$this->enableSsl($uri->getHost(), $sslConfig, $socket);
}
$config = $this->sslConfigurator->configure($uri, $connectingTo, $table, $this->driverConfig);
if ($config !== null) {
$socket->setSslContextOptions($config);
}
}

private function enableSsl(string $host, string $sslConfig, StreamSocket $sock): void
{
$options = [
'verify_peer' => true,
'peer_name' => $host,
];
if (!filter_var($host, FILTER_VALIDATE_IP)) {
$options['SNI_enabled'] = true;
}
if ($sslConfig === 's') {
$sock->setSslContextOptions($options);
} elseif ($sslConfig === 'ssc') {
$options['allow_self_signed'] = true;
$sock->setSslContextOptions($options);
}
}

public function canConnect(UriInterface $uri, AuthenticateInterface $authenticate, ?RoutingTable $table = null, ?UriInterface $server = null): bool
{
$connectingTo = $server ?? $uri;
$socket = new StreamSocket($uri->getHost(), $connectingTo->getPort() ?? 7687);

$this->configureSsl($uri, $connectingTo, $socket, $table);

try {
$bolt = new Bolt($socket);
$authenticate->authenticateBolt($bolt, $connectingTo, 'ping');
} catch (Throwable $e) {
return false;
}

return true;
}
}
9 changes: 5 additions & 4 deletions src/Bolt/BoltDriver.php
Original file line number Diff line number Diff line change
Expand Up @@ -91,13 +91,14 @@ public static function create($uri, ?DriverConfiguration $configuration = null,
}

$socketTimeout ??= TransactionConfiguration::DEFAULT_TIMEOUT;
$configuration ??= DriverConfiguration::default();

if ($formatter !== null) {
return new self(
$uri,
$authenticate ?? Authenticate::fromUrl(),
new BoltConnectionPool(),
$configuration ?? DriverConfiguration::default(),
new BoltConnectionPool($configuration, new SslConfigurator()),
$configuration,
$formatter,
$socketTimeout
);
Expand All @@ -106,8 +107,8 @@ public static function create($uri, ?DriverConfiguration $configuration = null,
return new self(
$uri,
$authenticate ?? Authenticate::fromUrl(),
new BoltConnectionPool(),
$configuration ?? DriverConfiguration::default(),
new BoltConnectionPool($configuration, new SslConfigurator()),
$configuration,
OGMFormatter::create(),
$socketTimeout
);
Expand Down
75 changes: 75 additions & 0 deletions src/Bolt/SslConfigurator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<?php

/*
* This file is part of the Laudis Neo4j package.
*
* (c) Laudis technologies <http://laudis.tech>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Laudis\Neo4j\Bolt;

use function count;
use function explode;
use const FILTER_VALIDATE_IP;
use function filter_var;
use Laudis\Neo4j\Databags\DriverConfiguration;
use Laudis\Neo4j\Enum\SslMode;
use Laudis\Neo4j\Neo4j\RoutingTable;
use Psr\Http\Message\UriInterface;

final class SslConfigurator
{
public function configure(UriInterface $uri, UriInterface $server, ?RoutingTable $table, DriverConfiguration $config): ?array
{
$sslMode = $config->getSslConfiguration()->getMode();
$sslConfig = '';
if ($sslMode === SslMode::FROM_URL()) {
$scheme = $uri->getScheme();
$explosion = explode('+', $scheme, 2);
$sslConfig = $explosion[1] ?? '';
} elseif ($sslMode === SslMode::ENABLE()) {
$sslConfig = 's';
} elseif ($sslMode === SslMode::ENABLE_WITH_SELF_SIGNED()) {
$sslConfig = 'ssc';
}

if (str_starts_with($sslConfig, 's')) {
// We have to pass a different host when working with ssl on aura.
// There is a strange behaviour where if we pass the uri host on a single
// instance aura deployment, we need to pass the original uri for the
// ssl configuration to be valid.
if ($table && count($table->getWithRole()) > 1) {
return $this->enableSsl($server->getHost(), $sslConfig, $config);
}

return $this->enableSsl($uri->getHost(), $sslConfig, $config);
}

return null;
}

private function enableSsl(string $host, string $sslConfig, DriverConfiguration $config): ?array
{
$options = [
'verify_peer' => $config->getSslConfiguration()->isVerifyPeer(),
'peer_name' => $host,
];
if (!filter_var($host, FILTER_VALIDATE_IP)) {
$options['SNI_enabled'] = true;
}
if ($sslConfig === 's') {
return $options;
}

if ($sslConfig === 'ssc') {
$options['allow_self_signed'] = true;

return $options;
}

return null;
}
}
13 changes: 13 additions & 0 deletions src/Common/BoltConnection.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
use function call_user_func;
use Laudis\Neo4j\Contracts\ConnectionInterface;
use Laudis\Neo4j\Databags\DatabaseInfo;
use Laudis\Neo4j\Databags\DriverConfiguration;
use Laudis\Neo4j\Enum\AccessMode;
use Laudis\Neo4j\Enum\ConnectionProtocol;
use Psr\Http\Message\UriInterface;
Expand Down Expand Up @@ -45,6 +46,8 @@ final class BoltConnection implements ConnectionInterface
* @psalm-readonly
*/
private $connector;
/** @psalm-readonly */
private DriverConfiguration $driverConfiguration;

/**
* @psalm-mutation-free
Expand All @@ -58,6 +61,7 @@ public function __construct(
ConnectionProtocol $protocol,
AccessMode $accessMode,
DatabaseInfo $databaseInfo,
DriverConfiguration $config,
$connector
) {
$this->serverAgent = $serverAgent;
Expand All @@ -67,6 +71,7 @@ public function __construct(
$this->accessMode = $accessMode;
$this->databaseInfo = $databaseInfo;
$this->connector = $connector;
$this->driverConfiguration = $config;
}

/**
Expand Down Expand Up @@ -148,4 +153,12 @@ public function close(): void
{
$this->bolt = null;
}

/**
* @psalm-mutation-free
*/
public function getDriverConfiguration(): DriverConfiguration
{
return $this->driverConfiguration;
}
}
Loading

0 comments on commit 2db4982

Please sign in to comment.