2 * Licensed to the Apache Software Foundation (ASF) under one
3 * or more contributor license agreements. See the NOTICE file
4 * distributed with this work for additional information
5 * regarding copyright ownership. The ASF licenses this file
6 * to you under the Apache License, Version 2.0 (the
7 * "License"); you may not use this file except in compliance
8 * with the License. You may obtain a copy of the License at
10 * http://www.apache.org/licenses/LICENSE-2.0
12 * Unless required by applicable law or agreed to in writing,
13 * software distributed under the License is distributed on an
14 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 * KIND, either express or implied. See the License for the
16 * specific language governing permissions and limitations
19 module thrift.async.ssl;
21 import core.thread : Fiber;
22 import core.time : Duration;
23 import std.array : empty;
25 import std.exception : enforce;
27 import deimos.openssl.err;
28 import deimos.openssl.ssl;
30 import thrift.async.base;
31 import thrift.async.socket;
32 import thrift.internal.ssl;
33 import thrift.internal.ssl_bio;
34 import thrift.transport.base;
35 import thrift.transport.ssl;
38 * Provides SSL/TLS encryption for async sockets.
40 * This implementation should be considered experimental, as it context-switches
41 * between fibers from within OpenSSL calls, and the safety of this has not yet
44 * For obvious reasons (the SSL connection is stateful), more than one instance
45 * should never be used on a given socket at the same time.
47 // Note: This could easily be extended to other transports in the future as well.
48 // There are only two parts of the implementation which don't work with a generic
49 // TTransport: 1) the certificate verification, for which peer name/address are
50 // needed from the socket, and 2) the connection shutdown, where the associated
51 // async manager is needed because close() is not usually called from within a
53 final class TAsyncSSLSocket : TBaseTransport {
58 * context = The SSL socket context to use. A reference to it is stored so
59 * that it does not get cleaned up while the socket is used.
60 * transport = The underlying async network transport to use for
63 this(TAsyncSocket underlyingSocket, TSSLContext context) {
64 socket_ = underlyingSocket;
66 serverSide_ = context.serverSide;
67 accessManager_ = context.accessManager;
70 override bool isOpen() @property {
71 if (ssl_ is null || !socket_.isOpen) return false;
73 auto shutdown = SSL_get_shutdown(ssl_);
74 bool shutdownReceived = (shutdown & SSL_RECEIVED_SHUTDOWN) != 0;
75 bool shutdownSent = (shutdown & SSL_SENT_SHUTDOWN) != 0;
76 return !(shutdownReceived && shutdownSent);
79 override bool peek() {
80 if (!isOpen) return false;
84 auto rc = SSL_peek(ssl_, &bt, bt.sizeof);
85 sslEnforce(rc >= 0, "SSL_peek");
93 override void open() {
94 enforce(!serverSide_, "Cannot open a server-side SSL socket.");
98 // If the underlying socket was automatically closed because of an error
99 // (i.e. close() was called from inside a socket method), we can land
100 // here with the SSL object still allocated; delete it here.
107 override void close() {
111 // SSL needs to send/receive data over the socket as part of the shutdown
112 // protocol, so we must execute the calls in the context of the associated
113 // async manager. On the other hand, TTransport clients expect the socket
114 // to be closed when close() returns, so we have to block until the
115 // shutdown work item has been executed.
116 import core.sync.condition;
117 import core.sync.mutex;
120 auto doneMutex = new Mutex;
121 auto doneCond = new Condition(doneMutex);
122 synchronized (doneMutex) {
123 socket_.asyncManager.execute(socket_, {
124 rc = SSL_shutdown(ssl_);
126 rc = SSL_shutdown(ssl_);
128 synchronized (doneMutex) doneCond.notifyAll();
134 // Do not throw an exception here as leaving the transport "open" will
135 // probably produce only more errors, and the chance we can do
136 // something about the error e.g. by retrying is very low.
137 logError("Error while shutting down SSL: %s", getSSLException());
146 override size_t read(ubyte[] buf) {
148 auto rc = SSL_read(ssl_, buf.ptr, cast(int)buf.length);
149 sslEnforce(rc >= 0, "SSL_read");
153 override void write(in ubyte[] buf) {
156 // Loop in case SSL_MODE_ENABLE_PARTIAL_WRITE is set in SSL_CTX.
158 while (written < buf.length) {
159 auto bytes = SSL_write(ssl_, buf.ptr + written,
160 cast(int)(buf.length - written));
161 sslEnforce(bytes > 0, "SSL_write");
166 override void flush() {
169 auto bio = SSL_get_wbio(ssl_);
170 enforce(bio !is null, new TSSLException("SSL_get_wbio returned null"));
172 auto rc = BIO_flush(bio);
173 sslEnforce(rc == 1, "BIO_flush");
177 * Whether to use client or server side SSL handshake protocol.
179 bool serverSide() @property const {
184 void serverSide(bool value) @property {
189 * The access manager to use.
191 void accessManager(TAccessManager value) @property {
192 accessManager_ = value;
197 * If the condition is false, cleans up the SSL connection and throws the
198 * exception for the last SSL error.
200 void sslEnforce(bool condition, string location) {
202 // We need to fetch the error first, as the error stack will be cleaned
203 // when shutting down SSL.
204 auto e = getSSLException(location);
211 * Frees the SSL connection object and clears the SSL error state.
220 * Makes sure the SSL connection is up and running, and initializes it if not.
222 void checkHandshake() {
223 enforce(socket_.isOpen, new TTransportException(
224 TTransportException.Type.NOT_OPEN));
226 if (ssl_ !is null) return;
227 ssl_ = context_.createSSL();
229 auto bio = createTTransportBIO(socket_, false);
230 SSL_set_bio(ssl_, bio, bio);
234 rc = SSL_accept(ssl_);
236 rc = SSL_connect(ssl_);
238 enforce(rc > 0, getSSLException());
240 auto addr = socket_.getPeerAddress();
241 authorize(ssl_, accessManager_, addr,
242 (serverSide_ ? addr.toHostNameString() : socket_.host));
245 TAsyncSocket socket_;
248 TSSLContext context_;
249 TAccessManager accessManager_;
253 * Wraps passed TAsyncSocket instances into TAsyncSSLSockets.
255 * Typically used with TAsyncClient. As an unfortunate consequence of the
256 * async client design, the passed transports cannot be statically verified to
257 * be of type TAsyncSocket. Instead, the type is verified at runtime – if a
258 * transport of an unexpected type is passed to getTransport(), it fails,
259 * throwing a TTransportException.
263 * auto context = nwe TSSLContext();
264 * ... // Configure SSL context.
265 * auto factory = new TAsyncSSLSocketFactory(context);
267 * auto socket = new TAsyncSocket(someAsyncManager, host, port);
270 * auto client = new TAsyncClient!Service(transport, factory,
271 * new TBinaryProtocolFactory!());
274 class TAsyncSSLSocketFactory : TTransportFactory {
276 this(TSSLContext context) {
280 override TAsyncSSLSocket getTransport(TTransport transport) {
281 auto socket = cast(TAsyncSocket)transport;
282 enforce(socket, new TTransportException(
283 "TAsyncSSLSocketFactory requires a TAsyncSocket to work on, not a " ~
284 to!string(typeid(transport)) ~ ".",
285 TTransportException.Type.INTERNAL_ERROR
287 return new TAsyncSSLSocket(socket, context_);
291 TSSLContext context_;