Skip to content

Commit 9486936

Browse files
Improve TCP+SSL server connection handling (#608)
* Improve TCP+SSL server connection handling When starting an TCP+SSL server, the handler would be called when the underlying TCP connection is established but before the SSL handshake has taken place. At that point, the client's `:ssl-session` value is still a placeholder (e.g. `.isValid` returns `false`, the cipher suite is reported as `"SSL_NULL_WITH_NULL_NULL"` and the peer principal is not available). Also, the stream is not ready for use, so any writes would be buffered until after the handshake is complete. Since there is no way to be notified of the handshake completion, the existing test case worked around this by looking up the SSL session only after having received a message for the first time which implies that the session has been successfully established since. However, this is a pretty awkward API and only works for protocols in which the client actually is the first to transmit anything. There's also a bit of a gotcha: The value of `:ssl-session` mutably changes to a different value after the handshake, so holding on to the initial placeholder value and only starting to use it later won't do the trick. This patch changes the semantics so that the handler is only invoked after the session was successfully established. While technically a breaking change, it appears unlikely that any existing users would rely on the original behavior since the placeholder SSL session value is pretty much useless. The new behavior should be more in line with user expectation and arguably is more correct (e.g. the handler doesn't have to wait for I/O before being able to peruse the SSL session info). * Elaborate semantics of `ssl-context` parameter in docstring * Add changelog entry * Add some HTTPS server tests around SSL session handling
1 parent 88fdf95 commit 9486936

File tree

4 files changed

+82
-21
lines changed

4 files changed

+82
-21
lines changed

CHANGES.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
### Unreleased
2+
3+
* Make TCP+SSL server call handler only after successful SSL handshake
4+
15
### 0.5.0
26

37
* Add initial clj-kondo hooks

src/aleph/tcp.clj

Lines changed: 29 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@
1313
[io.netty.channel
1414
Channel
1515
ChannelHandler
16-
ChannelPipeline]))
16+
ChannelHandlerContext
17+
ChannelPipeline]
18+
[io.netty.handler.ssl
19+
SslHandler]))
1720

1821
(p/def-derived-map TcpConnection [^Channel ch]
1922
:server-name (netty/channel-server-name ch)
@@ -25,7 +28,16 @@
2528

2629
(defn- ^ChannelHandler server-channel-handler
2730
[handler {:keys [raw-stream?] :as options}]
28-
(let [in (atom nil)]
31+
(let [in (atom nil)
32+
call-handler (fn [^ChannelHandlerContext ctx]
33+
(let [ch (.channel ctx)]
34+
(handler
35+
(doto
36+
(s/splice
37+
(netty/sink ch true netty/to-byte-buf)
38+
(reset! in (netty/source ch)))
39+
(reset-meta! {:aleph/channel ch}))
40+
(->TcpConnection ch))))]
2941
(netty/channel-inbound-handler
3042

3143
:exception-caught
@@ -40,15 +52,20 @@
4052

4153
:channel-active
4254
([_ ctx]
43-
(let [ch (.channel ctx)]
44-
(handler
45-
(doto
46-
(s/splice
47-
(netty/sink ch true netty/to-byte-buf)
48-
(reset! in (netty/source ch)))
49-
(reset-meta! {:aleph/channel ch}))
50-
(->TcpConnection ch)))
51-
(.fireChannelActive ctx))
55+
(if-let [^SslHandler ssl-handler (-> ctx .pipeline (.get SslHandler))]
56+
(-> ssl-handler
57+
.handshakeFuture
58+
netty/wrap-future
59+
(d/on-realized (fn [_]
60+
(call-handler ctx))
61+
;; No need to handle errors here since
62+
;; the SSL handler will terminate the
63+
;; whole pipeline by throwing a
64+
;; `javax.net.ssl.SSLHandshakeException`
65+
;; in this case anyway.
66+
(fn [_])))
67+
(call-handler ctx))
68+
(.fireChannelActive ctx))
5269

5370
:channel-read
5471
([_ ctx msg]
@@ -65,7 +82,7 @@
6582
|:---|:-----
6683
| `port` | the port the server will bind to. If `0`, the server will bind to a random port.
6784
| `socket-address` | a `java.net.SocketAddress` specifying both the port and interface to bind to.
68-
| `ssl-context` | an `io.netty.handler.ssl.SslContext` object. If a self-signed certificate is all that's required, `(aleph.netty/self-signed-ssl-context)` will suffice.
85+
| `ssl-context` | an `io.netty.handler.ssl.SslContext` object. If given, the server will only accept SSL connections and call the handler once the SSL session has been successfully established. If a self-signed certificate is all that's required, `(aleph.netty/self-signed-ssl-context)` will suffice.
6986
| `bootstrap-transform` | a function that takes an `io.netty.bootstrap.ServerBootstrap` object, which represents the server, and modifies it.
7087
| `pipeline-transform` | a function that takes an `io.netty.channel.ChannelPipeline` object, which represents a connection, and modifies it.
7188
| `raw-stream?` | if true, messages from the stream will be `io.netty.buffer.ByteBuf` objects rather than byte-arrays. This will minimize copying, but means that care must be taken with Netty's buffer reference counting. Only recommended for advanced users."

test/aleph/http_test.clj

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
(:require
33
[aleph.flow :as flow]
44
[aleph.http :as http]
5+
[aleph.http.core :as http.core]
56
[aleph.netty :as netty]
67
[aleph.ssl :as ssl]
78
[aleph.tcp :as tcp]
@@ -18,7 +19,8 @@
1819
(io.netty.handler.codec.http HttpMessage)
1920
(java.io File)
2021
(java.util.concurrent TimeoutException)
21-
(java.util.zip GZIPInputStream ZipException)))
22+
(java.util.zip GZIPInputStream ZipException)
23+
(javax.net.ssl SSLSession)))
2224

2325
;;;
2426

@@ -260,6 +262,35 @@
260262
{:body (io/file "test/file.txt")
261263
:pool client-pool}))))))))
262264

265+
(defn ssl-session-capture-handler [ssl-session-atom]
266+
(fn [req]
267+
(reset! ssl-session-atom (http.core/ring-request-ssl-session req))
268+
{:status 200 :body "ok"}))
269+
270+
(deftest test-ssl-session-access
271+
(let [ssl-session (atom nil)]
272+
(with-handler-options
273+
(ssl-session-capture-handler ssl-session)
274+
default-ssl-options
275+
(is (= 200 (:status @(http-get (str "https://localhost:" port)))))
276+
(is (some? @ssl-session))
277+
(when-let [^SSLSession s @ssl-session]
278+
(is (.isValid s))
279+
(is (not (str/includes? "NULL" (.getCipherSuite s))))))))
280+
281+
(deftest test-ssl-with-plain-client-request
282+
(let [ssl-session (atom nil)]
283+
(with-handler-options
284+
(ssl-session-capture-handler ssl-session)
285+
default-ssl-options
286+
;; Note the intentionally wrong "http" scheme here
287+
(is (some-> (http-get (str "http://localhost:" port))
288+
(d/catch identity)
289+
deref
290+
ex-message
291+
(str/includes? "connection was closed")))
292+
(is (nil? @ssl-session)))))
293+
263294
(deftest test-invalid-body
264295
(let [client-url (str "http://localhost:" port)]
265296
(with-handler identity

test/aleph/tcp_ssl_test.clj

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,8 @@
1616

1717
(defn ssl-echo-handler [ssl-session]
1818
(fn [s c]
19-
(s/connect
20-
;; note we need to capture the SSL session *after* we start
21-
;; reading data. Otherwise, the session might not be set up yet.
22-
(s/map (fn [msg]
23-
(reset! ssl-session (:ssl-session c))
24-
msg)
25-
s)
26-
s)))
19+
(reset! ssl-session (:ssl-session c))
20+
(s/connect s s)))
2721

2822
(deftest test-ssl-echo
2923
(let [ssl-session (atom nil)]
@@ -38,3 +32,18 @@
3832
(is (some? @ssl-session) "SSL session should be defined")
3933
(is (= (.getSubjectDN ^X509Certificate ssl/client-cert)
4034
(.getSubjectDN ^X509Certificate (first (.getPeerCertificates @ssl-session)))))))))
35+
36+
(deftest test-failed-ssl-handshake
37+
(let [ssl-session (atom nil)]
38+
(with-server (tcp/start-server (ssl-echo-handler ssl-session)
39+
{:port 10001
40+
:ssl-context ssl/server-ssl-context})
41+
(let [c @(tcp/client {:host "localhost"
42+
:port 10001
43+
:ssl-context (netty/ssl-client-context
44+
;; Note the intentionally wrong private key here
45+
{:private-key ssl/server-key
46+
:certificate-chain (into-array X509Certificate [ssl/client-cert])
47+
:trust-store (into-array X509Certificate [ssl/ca-cert])})})]
48+
(is (nil? @(s/take! c)))
49+
(is (nil? @ssl-session) "SSL session should be undefined")))))

0 commit comments

Comments
 (0)