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 }