Skip to content

Commit 1202906

Browse files
authored
Add SSL/TLS connection support (#12)
* Add SSL/TLS connection support via SSLMode configuration Introduces SSLMode (SSLDisabled | SSLRequired) so callers can opt into TLS by passing an SSLContext through ConnectInfo. Session.create matches on the mode to choose between lori's plaintext and SSL client constructors; the state machine is unchanged since lori handles the TLS handshake before firing on_connected. Adds dual-container test infrastructure: a TLS-enabled Redis Docker image (.ci-dockerfiles/redis-ssl) for CI and local Makefile targets that start both plaintext (port 6379) and TLS (port 6380) containers. Three new integration tests exercise SSL connection failure, connect- and-ready, and SET/GET over TLS. Design: #2 * Add examples/README.md describing each example program * Install openssl package in redis-ssl Dockerfile The redis:7 base image doesn't include the openssl CLI tools needed to generate self-signed certificates at build time. * Fix redis-ssl key permission for redis user The redis:7 image runs as the redis user, but the key was owned by root with 600 permissions. chown to redis:redis before chmod.
1 parent 036533b commit 1202906

17 files changed

Lines changed: 498 additions & 19 deletions
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
FROM redis:7
2+
3+
LABEL org.opencontainers.image.source="https://github.com/ponylang/redis"
4+
5+
RUN apt-get update && apt-get install -y --no-install-recommends openssl && \
6+
rm -rf /var/lib/apt/lists/* && \
7+
mkdir -p /tls && \
8+
openssl req -new -x509 -days 3650 -nodes \
9+
-out /tls/redis.crt -keyout /tls/redis.key \
10+
-subj '/CN=localhost' && \
11+
chown redis:redis /tls/redis.key && \
12+
chmod 600 /tls/redis.key
13+
14+
CMD ["redis-server", "--tls-port", "6379", "--port", "0", \
15+
"--tls-cert-file", "/tls/redis.crt", \
16+
"--tls-key-file", "/tls/redis.key", \
17+
"--tls-auth-clients", "no"]
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
#!/bin/bash
2+
3+
set -o errexit
4+
set -o nounset
5+
6+
#
7+
# *** You should already be logged in to GitHub Container Registry when you run
8+
# this ***
9+
#
10+
11+
DOCKERFILE_DIR="$(dirname "$0")"
12+
13+
docker build --pull \
14+
-t ghcr.io/ponylang/redis-ci-redis-ssl:latest "${DOCKERFILE_DIR}"
15+
docker push ghcr.io/ponylang/redis-ci-redis-ssl:latest

.github/workflows/breakage-against-ponyc-latest.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,13 @@ jobs:
2222
--health-interval 10s
2323
--health-timeout 5s
2424
--health-retries 5
25+
redis-ssl:
26+
image: ghcr.io/ponylang/redis-ci-redis-ssl:latest
27+
options: >-
28+
--health-cmd "redis-cli --tls --insecure ping"
29+
--health-interval 10s
30+
--health-timeout 5s
31+
--health-retries 5
2532
steps:
2633
- uses: actions/checkout@v4.1.1
2734
- name: Unit tests
@@ -31,6 +38,8 @@ jobs:
3138
env:
3239
REDIS_HOST: redis
3340
REDIS_PORT: 6379
41+
REDIS_SSL_HOST: redis-ssl
42+
REDIS_SSL_PORT: 6379
3443
- name: Build examples
3544
run: make build-examples config=debug ssl=libressl
3645
- name: Send alert on failure
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
name: Build CI Image
2+
3+
on:
4+
workflow_dispatch:
5+
6+
permissions:
7+
packages: write
8+
9+
jobs:
10+
redis-ssl:
11+
runs-on: ubuntu-latest
12+
13+
name: redis-ssl
14+
steps:
15+
- name: Checkout
16+
uses: actions/checkout@v4.1.1
17+
- name: Set up Docker Buildx
18+
# v3.10.0
19+
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2
20+
with:
21+
version: v0.23.0
22+
- name: Login to GitHub Container Registry
23+
# v2.2.0
24+
uses: docker/login-action@5139682d94efc37792e6b54386b5b470a68a4737
25+
with:
26+
registry: ghcr.io
27+
username: ${{ github.repository_owner }}
28+
password: ${{ secrets.GITHUB_TOKEN }}
29+
- name: Build and push
30+
run: bash .ci-dockerfiles/redis-ssl/build-and-push.bash

.github/workflows/pr.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,13 @@ jobs:
4848
--health-interval 10s
4949
--health-timeout 5s
5050
--health-retries 5
51+
redis-ssl:
52+
image: ghcr.io/ponylang/redis-ci-redis-ssl:latest
53+
options: >-
54+
--health-cmd "redis-cli --tls --insecure ping"
55+
--health-interval 10s
56+
--health-timeout 5s
57+
--health-retries 5
5158
steps:
5259
- uses: actions/checkout@v4.1.1
5360
- name: Unit tests
@@ -57,5 +64,7 @@ jobs:
5764
env:
5865
REDIS_HOST: redis
5966
REDIS_PORT: 6379
67+
REDIS_SSL_HOST: redis-ssl
68+
REDIS_SSL_PORT: 6379
6069
- name: Build examples
6170
run: make build-examples config=debug ssl=libressl

CLAUDE.md

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ make # build and run all tests (requires Redis running)
77
make unit-tests # run unit tests only (no Redis needed)
88
make integration-tests # run integration tests only (requires Redis)
99
make build-examples # build example programs
10-
make start-redis # start Redis in Docker for local testing
11-
make stop-redis # stop and remove Docker Redis
10+
make start-redis # start plaintext + SSL Redis in Docker
11+
make stop-redis # stop and remove both Redis containers
1212
make clean # clean build artifacts
1313
```
1414

@@ -40,14 +40,21 @@ Package: `redis`
4040
### Session Layer
4141

4242
- `Session` (actor in `session.pony`): Main entry point. Manages connection lifecycle and pub/sub via a state machine. Implements `lori.TCPConnectionActor & lori.ClientLifecycleEventReceiver`. All state machine classes (`_SessionUnopened`, `_SessionConnected`, `_SessionReady`, `_SessionSubscribed`, `_SessionClosed`) are in `session.pony`, following the postgres pattern.
43-
- `ConnectInfo` (in `connect_info.pony`): Connection configuration (host, port, optional password).
43+
- `ConnectInfo` (in `connect_info.pony`): Connection configuration (host, port, optional password, SSL mode).
4444
- `SessionStatusNotify` (in `session_status_notify.pony`): Lifecycle callback interface. All callbacks have default no-op implementations. Callbacks: `redis_session_connected`, `redis_session_connection_failed`, `redis_session_ready`, `redis_session_authentication_failed`, `redis_session_closed`.
4545
- `ResultReceiver` (in `result_receiver.pony`): Command response callback interface. Callbacks: `redis_response`, `redis_command_failed`.
4646
- `SubscriptionNotify` (in `subscription_notify.pony`): Pub/sub callback interface. All callbacks have default no-op implementations. Callbacks: `redis_subscribed`, `redis_unsubscribed`, `redis_message`, `redis_psubscribed`, `redis_punsubscribed`, `redis_pmessage`.
4747
- `ClientError` (in `client_error.pony`): Client-side error trait with `SessionNotReady`, `SessionClosed`, and `SessionInSubscribedMode` primitives.
4848
- `_ResponseHandler` (in `_response_handler.pony`): Loops `_RespParser` over a `buffered.Reader`, delivering parsed `RespValue`s to the current state. Shuts down on `RespMalformed`.
4949
- `_IllegalState` / `_Unreachable` (in `_mort.pony`): Primitives for detecting impossible states.
5050

51+
### SSL/TLS
52+
53+
- `SSLMode` (type alias in `ssl_mode.pony`): `(SSLDisabled | SSLRequired)`. Controls whether the session uses plaintext TCP or SSL/TLS.
54+
- `SSLDisabled` (primitive in `ssl_mode.pony`): Plaintext TCP connection (default).
55+
- `SSLRequired` (class val in `ssl_mode.pony`): Wraps an `SSLContext val` for direct TLS connections. Redis uses direct TLS (typically port 6380) rather than STARTTLS.
56+
- The `ssl/net` package is a transitive dependency via lori (no `corral.json` change needed). Adding `use "ssl/net"` in source files is sufficient.
57+
5158
### Trait Composition
5259

5360
- `_ClosedState`: Mixin for the terminal state — rejects or no-ops all operations.
@@ -80,13 +87,23 @@ In `_SessionSubscribed`, any pipelined commands that were in-flight when SUBSCRI
8087
- Unit tests: `--exclude=integration/` — no external dependencies
8188
- Integration tests: `--only=integration/` — require a running Redis server
8289
- Test names prefixed with `integration/` for filtering
83-
- `_RedisTestConfiguration` reads `REDIS_HOST` and `REDIS_PORT` from environment (defaults to `127.0.0.2`/`6379` on Linux for WSL2 compatibility)
90+
- `_RedisTestConfiguration` reads environment variables for both plaintext and SSL Redis:
91+
- `REDIS_HOST` / `REDIS_PORT` — plaintext (defaults to `127.0.0.2`/`6379` on Linux)
92+
- `REDIS_SSL_HOST` / `REDIS_SSL_PORT` — TLS (defaults to same host/`6380`)
93+
94+
### SSL-to-Plaintext Deadlock
95+
96+
Do not write tests that connect with SSL to a plaintext Redis server. The TLS ClientHello is binary data with no `\r\n`, so Redis's RESP parser buffers it waiting for a line terminator. Meanwhile the SSL client waits for a ServerHello. Neither side sends more data — both block indefinitely. To test the SSL constructor path, connect to a non-listening port instead (TCP connection refused is fast and deterministic).
8497

8598
### CI
8699

87-
Both `pr.yml` and `breakage-against-ponyc-latest.yml` use the `shared-docker-ci-standard-builder-with-libressl-4.2.0` image (for ssl support) and a `redis:7` service container with health checks. Integration tests receive `REDIS_HOST=redis` and `REDIS_PORT=6379` as environment variables. All make targets pass `ssl=libressl`.
100+
Both `pr.yml` and `breakage-against-ponyc-latest.yml` use the `shared-docker-ci-standard-builder-with-libressl-4.2.0` image (for ssl support) and two Redis service containers: `redis` (plaintext) and `redis-ssl` (TLS via `ghcr.io/ponylang/redis-ci-redis-ssl:latest`). Integration tests receive `REDIS_HOST=redis`, `REDIS_PORT=6379`, `REDIS_SSL_HOST=redis-ssl`, and `REDIS_SSL_PORT=6379`. All make targets pass `ssl=libressl`.
101+
102+
The `redis-ssl` CI image is built via `build-ci-image.yml` (manually triggered `workflow_dispatch`). Source: `.ci-dockerfiles/redis-ssl/Dockerfile`. Build locally with `.ci-dockerfiles/redis-ssl/build-and-push.bash`.
88103

89104
## File Layout
90105

91106
- `redis/` — main package source
92107
- `examples/` — example programs
108+
- `assets/` — test certificates for SSL Redis container
109+
- `.ci-dockerfiles/` — Dockerfiles for CI service containers

Makefile

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,15 @@ integration-tests: $(tests_binary)
5252

5353
start-redis:
5454
@docker run --name redis -p 6379:6379 -d redis:7
55+
@docker run --name redis-ssl \
56+
-v $(CURDIR)/assets/test-cert.pem:/tls/redis.crt:ro \
57+
-v $(CURDIR)/assets/test-key.pem:/tls/redis.key.orig:ro \
58+
-p 6380:6379 \
59+
-d --entrypoint sh redis:7 \
60+
-c "cp /tls/redis.key.orig /tls/redis.key && chmod 600 /tls/redis.key && exec redis-server --tls-port 6379 --port 0 --tls-cert-file /tls/redis.crt --tls-key-file /tls/redis.key --tls-auth-clients no"
5561

5662
stop-redis:
57-
@docker stop redis && docker rm redis
63+
@docker stop redis redis-ssl && docker rm redis redis-ssl
5864

5965
$(tests_binary): $(SOURCE_FILES) | $(BUILD_DIR)
6066
$(GET_DEPENDENCIES_WITH)

assets/test-cert.pem

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
-----BEGIN CERTIFICATE-----
2+
MIIDCTCCAfGgAwIBAgIUW6jIM9PbFPvUnHx5o9KOrxBYXZQwDQYJKoZIhvcNAQEL
3+
BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI2MDIxNjEzNTY0M1oXDTM2MDIx
4+
NDEzNTY0M1owFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF
5+
AAOCAQ8AMIIBCgKCAQEA0q/nv9fgUCveG7J90gKimXvp8EE+kIIN6egZoK8eTBz0
6+
3QPj8NhLJaSodpbSwotkny3UixcxQJ+NQj+6abJmS8BZaw4oTLjAAK3aoh4R8pR6
7+
+t0wDE17RrjnsQZzP2A19n9AANvoVFsWyETvLl38qFhLMVwIPcmjUH66D0ZZVE+q
8+
MxCSFaSZFetuYq8zKlYvgpnxTFhRliGLi/Q84vZmKT6Vaa1mnv8bVGWF6wUOiuZb
9+
v6Rc39iG0gCYwdjH2Ig6jsYL4s8xDArSy9SpK4I93E8wLZz4/8NYAuD1aTJMw/+7
10+
uuar2G++2XGTlvoli9WDrERtJJRIA1HYo1uBZxh9MQIDAQABo1MwUTAdBgNVHQ4E
11+
FgQUQqGTYKBeXfu2XHd9QddNZzrDijIwHwYDVR0jBBgwFoAUQqGTYKBeXfu2XHd9
12+
QddNZzrDijIwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAbz16
13+
Lx2JpHLk+F7/5jA35uQPJamIDCxw7f8Xa/7vjHZp+HhNkzuQ1Vff9dUBLl4tANIC
14+
GrH3kCAmEpNlVA8sgDzeHR04ubWWMFIrspGBVNTr4ffExFdjlAWtUj2NM6f8O5Kc
15+
3WxbnkwO/nC2gt5VzpzI0okdHs4EwqhJXCcMy1yomHPryzre0SLRgDKFjH5TtsLL
16+
i/WL5B4pEYguQ+nPNSklo5vibp76ftrm3bABJF/uNNH13rjJDsEY/TLn0tuMHWHO
17+
Ym2wtVSrgx6omwnqpRGPdFf1t2KZRFU54BaVpJXJ+ksgvaAfdnqZRxpL9gEdgagE
18+
CUsb0HtcmNoUq784xQ==
19+
-----END CERTIFICATE-----

assets/test-key.pem

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
-----BEGIN PRIVATE KEY-----
2+
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDSr+e/1+BQK94b
3+
sn3SAqKZe+nwQT6Qgg3p6Bmgrx5MHPTdA+Pw2EslpKh2ltLCi2SfLdSLFzFAn41C
4+
P7ppsmZLwFlrDihMuMAArdqiHhHylHr63TAMTXtGuOexBnM/YDX2f0AA2+hUWxbI
5+
RO8uXfyoWEsxXAg9yaNQfroPRllUT6ozEJIVpJkV625irzMqVi+CmfFMWFGWIYuL
6+
9Dzi9mYpPpVprWae/xtUZYXrBQ6K5lu/pFzf2IbSAJjB2MfYiDqOxgvizzEMCtLL
7+
1Kkrgj3cTzAtnPj/w1gC4PVpMkzD/7u65qvYb77ZcZOW+iWL1YOsRG0klEgDUdij
8+
W4FnGH0xAgMBAAECggEAAlu6NXKc6JYpHmMVLKE0wTuQGY1IE5WFGC8YfwI0Iwgi
9+
8d4N3TUKmEwGH7ezTxQA3GrxVAeT2KRAkEiG1WaMEGvW2bosQiRCRUUCQL94liAf
10+
2p2DC2w1mvZ4ZMn3Rx2+/43v6+KKRfbJY+n+SbpUbkJOFdTGJII/NqgzJyHI37Gf
11+
C3y5cuUBU6AYlSL48wltM3K/Z924z76ahF9WtaN933H9ZCDI/DOK+1BHp90w0kq4
12+
YLjPPNSEhb1DpxKTD9bFg7UfHhPmXEuVMSDg6zMXfok7zPdFRVy/QOxAJ+3GnnbL
13+
QaAGwiavcL8falhAhCvXnAEZP3tFXf66btUYrJh6AQKBgQD5VPIpZL5hHVlMrMaC
14+
yhIf+SFDY9nNhE+7T9QZ6KV+gHae7KQ54eo/1WjapzghaFhLh1AH+a8hrSHGLr8L
15+
AvD8XiWQ8mz3AU9sB37GrHEg1aF6H0WoVDVCFNZOYblLdEnMgUYITPAkghVVT7L3
16+
4ssBVo/GZm+83QokJp294GUBsQKBgQDYUmC8ZxuXoYeAsfa78nDv2nNokYbM14q0
17+
0eKDtYcFSScOf00ColEx28fICKycWqM7t2CH27+1Qn260HC66odx5zy0zH3gDi7x
18+
2IWtbOM2zi6RX5j8DmUg1YZdVWAYWNfpUiGf5+//gjuWQoIsjYz8HxO+0qrVj+1Y
19+
r701fFqTgQKBgQCz0C9U6vLh1uiwA2qmqFSp+mGi2EAbl3BpBglRJhc0RDFILCHE
20+
HMlzf9U+25n/feEvl0aAefFzuAjDhLMCnfxuBPfSsQMkoC1HG6CyZ2qu42KxUBxM
21+
aW5/Wce0f4XChPs0IYcANXx619XK0M+hNk5xiJvn2bh73G2T5MWeSlaOAQKBgDX7
22+
4N5DkPQ3QFySjPN8XkjNKE3pO021YI0db7zUuVxNVCzfAfXtUfvyUBoQaBsg12mY
23+
qsNcyKvQaxYK3IcY7jIztcec3LDkeTyrfzi3WsCbla17wO7uHLij8MMsAi4xRbiZ
24+
YYDJXfBSrNIjyN6beJqWmtPdOPrQzzd5q+tEHEqBAoGAK0nQTGNEXWacs+iwpXYr
25+
LpKxmNavkhy6UVMlbpfmceOljGmrVZK4dgHnEM3oqb9whbGMLK586ZiEgU0hzYGq
26+
adoJxvPp4OSes9OIjjA5bsmGL5lLdzUq6MUunRTWJ2y8CXT3BDtn1tqurk/6TOqT
27+
YEoo8GRKRJiXFN5NVkQeKLc=
28+
-----END PRIVATE KEY-----

examples/README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Examples
2+
3+
Each subdirectory is a self-contained Pony program demonstrating a different part of the redis library.
4+
5+
## basic
6+
7+
Minimal example. Connects to Redis, executes `SET hello world`, prints the response, and closes the session. Shows how to create a `ConnectInfo`, implement `SessionStatusNotify` and `ResultReceiver` on a single actor, and match on `RespValue` variants. Start here if you're new to the library.
8+
9+
## pipeline
10+
11+
Command pipelining. Sends 3 SET commands followed by 3 GET commands without waiting for individual responses — all 6 commands are dispatched immediately in `redis_session_ready`. Responses arrive in order and are tracked with a step counter. Shows how pipelining eliminates round-trip latency for independent commands.
12+
13+
## pubsub
14+
15+
Pub/sub messaging using two sessions. One session subscribes to `demo-channel`, the other publishes a message to it. Demonstrates the `SubscriptionNotify` interface (`redis_subscribed`, `redis_message`, `redis_unsubscribed`) and the two-session pattern required because a subscribed session cannot execute regular commands.
16+
17+
## ssl
18+
19+
SSL/TLS-encrypted connection. Same workflow as `basic` (connects and sends PING) but over TLS using `SSLRequired`. Demonstrates how to create an `SSLContext` with a CA certificate, wrap it in `SSLRequired`, and pass it to `ConnectInfo`. Requires a Redis server configured for TLS. Set `REDIS_HOST`, `REDIS_PORT`, and `REDIS_CA_PATH` environment variables to match your server.

0 commit comments

Comments
 (0)