Skip to content

Commit 815daea

Browse files
committed
TLS Connection Support
1 parent 8c0ce4c commit 815daea

7 files changed

Lines changed: 232 additions & 40 deletions

File tree

.github/workflows/ci.yml

Lines changed: 27 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -12,36 +12,46 @@ jobs:
1212
outputs:
1313
version: ${{ steps.supported-versions-matrix.outputs.version }}
1414
steps:
15-
- uses: actions/checkout@v1
15+
- uses: actions/checkout@v4
1616
- id: supported-versions-matrix
1717
uses: WyriHaximus/github-action-composer-php-versions-in-range@v1
1818
tests:
19-
services:
20-
postgres:
21-
image: postgres:${{ matrix.postgres }}
22-
env:
23-
POSTGRES_PASSWORD: postgres
24-
POSTGRES_INITDB_ARGS: --auth-host=md5
25-
# Set health checks to wait until postgres has started
26-
options: >-
27-
--health-cmd pg_isready
28-
--health-interval 10s
29-
--health-timeout 5s
30-
--health-retries 5
31-
ports:
32-
- 5432:5432
19+
# services:
20+
# postgres:
21+
# image: postgres:${{ matrix.postgres }}
22+
# env:
23+
# POSTGRES_PASSWORD: postgres
24+
# POSTGRES_INITDB_ARGS: --auth-host=md5
25+
# # Set health checks to wait until postgres has started
26+
# options: >-
27+
# --health-cmd pg_isready
28+
# --health-interval 10s
29+
# --health-timeout 5s
30+
# --health-retries 5
31+
# ports:
32+
# - 5432:5432
3333
name: Testing on PHP ${{ matrix.php }} with ${{ matrix.composer }} dependency preference against Postgres ${{ matrix.postgres }}
3434
strategy:
3535
fail-fast: false
3636
matrix:
3737
php: ${{ fromJson(needs.supported-versions-matrix.outputs.version) }}
38-
postgres: [12, 13, 14, 15]
38+
# postgres: [12, 13, 14, 15, 16, 17]
39+
postgres: [17]
3940
composer: [lowest, locked, highest]
41+
# php: ["8.4", "7.0"]
42+
# postgres: [15]
43+
# composer: [locked]
44+
tls: [false,true]
4045
needs:
4146
- supported-versions-matrix
4247
runs-on: ubuntu-latest
4348
steps:
44-
- uses: actions/checkout@v3
49+
- uses: actions/checkout@v4
50+
- uses: ikalnytskyi/action-setup-postgres@v7
51+
with:
52+
postgres-version: ${{ matrix.postgres }}
53+
ssl: ${{ matrix.tls }}
54+
id: postgres
4555
- run: |
4656
PGPASSWORD=postgres psql -h localhost -U postgres -c "CREATE USER pgasync"
4757
PGPASSWORD=postgres psql -h localhost -U postgres -c "ALTER ROLE pgasync PASSWORD 'pgasync'"

README.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,35 @@ $client->listen('some_channel')
7979
$client->query("NOTIFY some_channel, 'Hello World'")->subscribe();
8080
```
8181

82+
## Example - Connecting over TLS with CA certificate file
83+
```php
84+
85+
$client = new PgAsync\Client([
86+
"host" => "127.0.0.1",
87+
"port" => "5432",
88+
"user" => "matt",
89+
"database" => "matt",
90+
"tls" => "verify-full",
91+
"tls_connector_flags" => [
92+
"cafile" => "/path/to/ca.crt",
93+
],
94+
]);
95+
96+
$client->query('SELECT * FROM channel')->subscribe(
97+
function ($row) {
98+
var_dump($row);
99+
},
100+
function ($e) {
101+
echo "Failed.\n";
102+
},
103+
function () {
104+
echo "Complete.\n";
105+
}
106+
);
107+
108+
109+
```
110+
82111
## Install
83112
With [composer](https://getcomposer.org/) install into you project with:
84113

composer.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,9 @@
3636
"php": ">=7.0.0",
3737
"voryx/event-loop": "^3.0 || ^2.0.2",
3838
"reactivex/rxphp": "^2.0",
39-
"react/socket": "^1.0 || ^0.8 || ^0.7",
40-
"evenement/evenement": "^2.0 | ^3.0"
39+
"react/promise-stream": "^1.5",
40+
"evenement/evenement": "^2.0 | ^3.0",
41+
"wyrihaximus/react-opportunistic-tls": "^1"
4142
},
4243
"require-dev": {
4344
"phpunit/phpunit": ">=8.5.23 || ^6.5.5",

src/PgAsync/Connection.php

Lines changed: 82 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
use PgAsync\Command\PasswordMessage;
1313
use PgAsync\Command\SaslInitialResponse;
1414
use PgAsync\Command\SaslResponse;
15+
use PgAsync\Command\SSLRequest;
1516
use PgAsync\Command\Sync;
1617
use PgAsync\Command\Terminate;
1718
use PgAsync\Message\Authentication;
@@ -32,9 +33,13 @@
3233
use PgAsync\Message\ReadyForQuery;
3334
use PgAsync\Message\RowDescription;
3435
use PgAsync\Command\StartupMessage;
36+
use React\EventLoop\Loop;
3537
use React\EventLoop\LoopInterface;
36-
use React\Socket\Connector;
38+
use React\Promise\Promise;
39+
use React\Socket\ConnectionInterface;
40+
use WyriHaximus\React\Socket\Connector;
3741
use React\Socket\ConnectorInterface;
42+
use WyriHaximus\React\Socket\OpportunisticTlsConnectionInterface;
3843
use React\Stream\DuplexStreamInterface;
3944
use Rx\Disposable\CallbackDisposable;
4045
use Rx\Disposable\EmptyDisposable;
@@ -43,6 +48,8 @@
4348
use Rx\ObserverInterface;
4449
use Rx\SchedulerInterface;
4550
use Rx\Subject\Subject;
51+
use function React\Promise\resolve;
52+
use function React\Promise\Stream\first;
4653

4754
class Connection extends EventEmitter
4855
{
@@ -73,6 +80,16 @@ class Connection extends EventEmitter
7380
const CONNECTION_NEEDED = 8; /* Internal state: connect() needed */
7481
const CONNECTION_CLOSED = 9;
7582

83+
// Reference table: https://www.postgresql.org/docs/current/libpq-ssl.html#LIBPQ-SSL-PROTECTION
84+
const TLS_MODE_DISABLE = 'disable';
85+
const TLS_MODE_ALLOW = 'allow';
86+
const TLS_MODE_PREFER = 'prefer';
87+
const TLS_MODE_REQUIRE = 'require';
88+
const TLS_MODE_VERIFY_CA = 'verify-ca';
89+
const TLS_MODE_VERIFY_FULL = 'verify-full';
90+
const TLS_MODE_LIST_FULL = [self::TLS_MODE_DISABLE, self::TLS_MODE_ALLOW, self::TLS_MODE_PREFER, self::TLS_MODE_REQUIRE, self::TLS_MODE_VERIFY_CA, self::TLS_MODE_VERIFY_FULL];
91+
const TLS_MODE_LIST_REQUIRED = [self::TLS_MODE_REQUIRE, self::TLS_MODE_VERIFY_CA, self::TLS_MODE_VERIFY_FULL];
92+
7693
private $queryState;
7794
private $queryType;
7895
private $connStatus;
@@ -134,6 +151,8 @@ class Connection extends EventEmitter
134151

135152
/** @var bool */
136153
private $auto_disconnect = false;
154+
private $tls = self::TLS_MODE_PREFER;
155+
private $tlsConnectorFlags = [];
137156
private $password;
138157

139158
public function __construct(array $parameters, LoopInterface $loop, ConnectorInterface $connector = null)
@@ -158,6 +177,19 @@ public function __construct(array $parameters, LoopInterface $loop, ConnectorInt
158177
unset($parameters['password']);
159178
}
160179

180+
if (array_key_exists('tls', $parameters)) {
181+
if (!in_array($this->tls, self::TLS_MODE_LIST_FULL)) {
182+
throw new \InvalidArgumentException('TLS mode must be one off "' . implode(', ', self::TLS_MODE_LIST_FULL) . ' but got "' . $parameters['tls'] . '" instead');
183+
}
184+
$this->tls = $parameters['tls'];
185+
unset($parameters['tls']);
186+
}
187+
188+
if (array_key_exists('tls_connector_flags', $parameters)) {
189+
$this->tlsConnectorFlags = $parameters['tls_connector_flags'];
190+
unset($parameters['tls_connector_flags']);
191+
}
192+
161193
if (isset($parameters['auto_disconnect'])) {
162194
$this->auto_disconnect = $parameters['auto_disconnect'];
163195
unset($parameters['auto_disconnect']);
@@ -172,8 +204,17 @@ public function __construct(array $parameters, LoopInterface $loop, ConnectorInt
172204
$this->queryState = static::STATE_BUSY;
173205
$this->queryType = static::QUERY_SIMPLE;
174206
$this->connStatus = static::CONNECTION_NEEDED;
175-
$this->socket = $connector ?: new Connector($loop);
176-
$this->uri = 'tcp://' . $parameters['host'] . ':' . $parameters['port'];
207+
$this->socket = $connector ?: new Connector($loop, [
208+
'tls' => [
209+
'verify_peer' => $this->tls === self::TLS_MODE_VERIFY_FULL,
210+
'verify_peer_name' => $this->tls === self::TLS_MODE_VERIFY_FULL,
211+
'allow_self_signed' => $this->tls !== self::TLS_MODE_VERIFY_FULL,
212+
] + $this->tlsConnectorFlags,
213+
]);
214+
// We always url `opportunistic+tls` as scheme because the logic required for using `tcp` on TLS `disable`
215+
// mode is more complex than worth it when connecting to the server. And the `SecureConnector` gives us a
216+
// plaint text connection with all TLS flags already set and ready to use for all the other modes.
217+
$this->uri = 'opportunistic+tls://' . $parameters['host'] . ':' . $parameters['port'];
177218
$this->notificationSubject = new Subject();
178219
$this->cancelPending = false;
179220
$this->cancelRequested = false;
@@ -191,23 +232,43 @@ private function start()
191232
$this->connStatus = static::CONNECTION_STARTED;
192233

193234
$this->socket->connect($this->uri)->then(
194-
function (DuplexStreamInterface $stream) {
195-
$this->stream = $stream;
196-
$this->connStatus = static::CONNECTION_MADE;
197-
198-
$stream->on('close', [$this, 'onClose']);
235+
function (OpportunisticTlsConnectionInterface $stream) {
236+
(new Promise(function (callable $resolve, callable $reject) use ($stream) {
237+
if ($this->tls !== self::TLS_MODE_DISABLE) {
238+
first($stream)->then(function ($data) use ($resolve, $reject, $stream) {
239+
if (trim($data) === 'S') {
240+
$stream->enableEncryption()->then($resolve, $reject);
241+
return;
242+
}
243+
244+
if (in_array($this->tls, self::TLS_MODE_LIST_REQUIRED)) {
245+
$reject(new \RuntimeException('Failed to encrypt connection while required'));
246+
return;
247+
}
248+
249+
$resolve($stream);
250+
}, $reject);
251+
252+
$ssl = new SSLRequest();
253+
$stream->write($ssl->encodedMessage());
254+
return;
255+
}
199256

200-
$stream->on('data', [$this, 'onData']);
257+
$resolve($stream);
258+
}))->then(function (DuplexStreamInterface $stream) {
259+
$this->stream = $stream;
260+
$this->connStatus = static::CONNECTION_MADE;
201261

202-
// $ssl = new SSLRequest();
203-
// $stream->write($ssl->encodedMessage());
262+
$stream->on('close', [$this, 'onClose']);
263+
$stream->on('data', [$this, 'onData']);
204264

205-
$startupParameters = $this->parameters;
206-
unset($startupParameters['host'], $startupParameters['port']);
265+
$startupParameters = $this->parameters;
266+
unset($startupParameters['host'], $startupParameters['port']);
207267

208-
$startup = new StartupMessage();
209-
$startup->setParameters($startupParameters);
210-
$stream->write($startup->encodedMessage());
268+
$startup = new StartupMessage();
269+
$startup->setParameters($startupParameters);
270+
$stream->write($startup->encodedMessage());
271+
});
211272
},
212273
function ($e) {
213274
// connection error
@@ -596,11 +657,11 @@ function (ObserverInterface $observer, SchedulerInterface $scheduler = null) use
596657
$this->processQueue();
597658

598659
return new CallbackDisposable(function () use ($q) {
599-
if ($this->currentCommand === $q && $q->isActive()) {
600-
$this->cancelRequested = true;
601-
}
602-
$q->cancel();
603-
});
660+
if ($this->currentCommand === $q && $q->isActive()) {
661+
$this->cancelRequested = true;
662+
}
663+
$q->cancel();
664+
});
604665
}
605666
);
606667

tests/tls/postgres.tls.conf

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Refs: https://github.com/postgres/postgres/blob/REL_14_18/src/backend/utils/misc/postgresql.conf.sample
2+
3+
listen_addresses = '*'
4+
port = 5432
5+
ssl = on
6+
ssl_cert_file = '/etc/postgresql/server.crt'
7+
ssl_key_file = '/etc/postgresql/server.key'

tests/tls/server.crt

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
-----BEGIN CERTIFICATE-----
2+
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
32+
-----END CERTIFICATE-----

tests/tls/server.key

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
-----BEGIN PRIVATE KEY-----
2+
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
51+
wRJVjpt6cqfFR5QdYVRLIYN0vKGvSsU=
52+
-----END PRIVATE KEY-----

0 commit comments

Comments
 (0)