1 /*
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
9  *
10  *   http://www.apache.org/licenses/LICENSE-2.0
11  *
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
17  * under the License.
18  */
19 module thrift.async.ssl;
20 
21 import core.thread : Fiber;
22 import core.time : Duration;
23 import std.array : empty;
24 import std.conv : to;
25 import std.exception : enforce;
26 import std.socket;
27 import deimos.openssl.err;
28 import deimos.openssl.ssl;
29 import thrift.base;
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;
36 
37 /**
38  * Provides SSL/TLS encryption for async sockets.
39  *
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
42  * been verified.
43  *
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.
46  */
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
52 // work item.
53 final class TAsyncSSLSocket : TBaseTransport {
54   /**
55    * Constructor.
56    *
57    * Params:
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
61    *     communication.
62    */
63   this(TAsyncSocket underlyingSocket, TSSLContext context) {
64     socket_ = underlyingSocket;
65     context_ = context;
66     serverSide_ = context.serverSide;
67     accessManager_ = context.accessManager;
68   }
69 
70   override bool isOpen() @property {
71     if (ssl_ is null || !socket_.isOpen) return false;
72 
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);
77   }
78 
79   override bool peek() {
80     if (!isOpen) return false;
81     checkHandshake();
82 
83     byte bt = void;
84     auto rc = SSL_peek(ssl_, &bt, bt.sizeof);
85     sslEnforce(rc >= 0, "SSL_peek");
86 
87     if (rc == 0) {
88       ERR_clear_error();
89     }
90     return (rc > 0);
91   }
92 
93   override void open() {
94     enforce(!serverSide_, "Cannot open a server-side SSL socket.");
95     if (isOpen) return;
96 
97     if (ssl_) {
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.
101       cleanupSSL();
102     }
103 
104     socket_.open();
105   }
106 
107   override void close() {
108     if (!isOpen) return;
109 
110     if (ssl_ !is null) {
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;
118 
119       int rc = void;
120       auto doneMutex = new Mutex;
121       auto doneCond = new Condition(doneMutex);
122       synchronized (doneMutex) {
123         socket_.asyncManager.execute(socket_, {
124           rc = SSL_shutdown(ssl_);
125           if (rc == 0) {
126             rc = SSL_shutdown(ssl_);
127           }
128           synchronized (doneMutex) doneCond.notifyAll();
129         });
130         doneCond.wait();
131       }
132 
133       if (rc < 0) {
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());
138       }
139 
140       cleanupSSL();
141     }
142 
143     socket_.close();
144   }
145 
146   override size_t read(ubyte[] buf) {
147     checkHandshake();
148     auto rc = SSL_read(ssl_, buf.ptr, cast(int)buf.length);
149     sslEnforce(rc >= 0, "SSL_read");
150     return rc;
151   }
152 
153   override void write(in ubyte[] buf) {
154     checkHandshake();
155 
156     // Loop in case SSL_MODE_ENABLE_PARTIAL_WRITE is set in SSL_CTX.
157     size_t written = 0;
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");
162       written += bytes;
163     }
164   }
165 
166   override void flush() {
167     checkHandshake();
168 
169     auto bio = SSL_get_wbio(ssl_);
170     enforce(bio !is null, new TSSLException("SSL_get_wbio returned null"));
171 
172     auto rc = BIO_flush(bio);
173     sslEnforce(rc == 1, "BIO_flush");
174   }
175 
176   /**
177    * Whether to use client or server side SSL handshake protocol.
178    */
179   bool serverSide() @property const {
180     return serverSide_;
181   }
182 
183   /// Ditto
184   void serverSide(bool value) @property {
185     serverSide_ = value;
186   }
187 
188   /**
189    * The access manager to use.
190    */
191   void accessManager(TAccessManager value) @property {
192     accessManager_ = value;
193   }
194 
195 private:
196   /**
197    * If the condition is false, cleans up the SSL connection and throws the
198    * exception for the last SSL error.
199    */
200   void sslEnforce(bool condition, string location) {
201     if (!condition) {
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);
205       cleanupSSL();
206       throw e;
207     }
208   }
209 
210   /**
211    * Frees the SSL connection object and clears the SSL error state.
212    */
213   void cleanupSSL() {
214     SSL_free(ssl_);
215     ssl_ = null;
216     ERR_remove_state(0);
217   }
218 
219   /**
220    * Makes sure the SSL connection is up and running, and initializes it if not.
221    */
222   void checkHandshake() {
223     enforce(socket_.isOpen, new TTransportException(
224       TTransportException.Type.NOT_OPEN));
225 
226     if (ssl_ !is null) return;
227     ssl_ = context_.createSSL();
228 
229     auto bio = createTTransportBIO(socket_, false);
230     SSL_set_bio(ssl_, bio, bio);
231 
232     int rc = void;
233     if (serverSide_) {
234       rc = SSL_accept(ssl_);
235     } else {
236       rc = SSL_connect(ssl_);
237     }
238     enforce(rc > 0, getSSLException());
239 
240     auto addr = socket_.getPeerAddress();
241     authorize(ssl_, accessManager_, addr,
242       (serverSide_ ? addr.toHostNameString() : socket_.host));
243   }
244 
245   TAsyncSocket socket_;
246   bool serverSide_;
247   SSL* ssl_;
248   TSSLContext context_;
249   TAccessManager accessManager_;
250 }
251 
252 /**
253  * Wraps passed TAsyncSocket instances into TAsyncSSLSockets.
254  *
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.
260  *
261  * Example:
262  * ---
263  * auto context = nwe TSSLContext();
264  * ... // Configure SSL context.
265  * auto factory = new TAsyncSSLSocketFactory(context);
266  *
267  * auto socket = new TAsyncSocket(someAsyncManager, host, port);
268  * socket.open();
269  *
270  * auto client = new TAsyncClient!Service(transport, factory,
271  *   new TBinaryProtocolFactory!());
272  * ---
273  */
274 class TAsyncSSLSocketFactory : TTransportFactory {
275   ///
276   this(TSSLContext context) {
277     context_ = context;
278   }
279 
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
286     ));
287     return new TAsyncSSLSocket(socket, context_);
288   }
289 
290 private:
291   TSSLContext context_;
292 }