From c54807700c5333c0653cb98ab2f3cc7780155512 Mon Sep 17 00:00:00 2001 From: Daniel Marcotte Date: Mon, 5 May 2014 14:30:15 -0700 Subject: [PATCH] Add SSL support for JRuby - Implement MiniSSL for JRuby - Modify `Binder` and `MiniSSL::Context` to to accommodate the fact that Java SSL demands a java keystore rather than a key/cert pair - Change the MiniSSL native extension interface to take a `MiniSSL::Context` rather than a key/cert pair so that each extension can grab keys off the context as appropriate --- ext/puma_http11/mini_ssl.c | 11 +- ext/puma_http11/org/jruby/puma/MiniSSL.java | 502 ++++++++++++-------- lib/puma/binder.rb | 50 +- lib/puma/minissl.rb | 44 +- test/test_minissl.rb | 32 +- test/test_puma_server.rb | 35 -- test/test_puma_server_ssl.rb | 91 ++++ 7 files changed, 462 insertions(+), 303 deletions(-) create mode 100644 test/test_puma_server_ssl.rb diff --git a/ext/puma_http11/mini_ssl.c b/ext/puma_http11/mini_ssl.c index 299c7d5f..1ea37c43 100644 --- a/ext/puma_http11/mini_ssl.c +++ b/ext/puma_http11/mini_ssl.c @@ -36,15 +36,18 @@ ms_conn* engine_alloc(VALUE klass, VALUE* obj) { return conn; } -VALUE engine_init_server(VALUE self, VALUE key, VALUE cert) { +VALUE engine_init_server(VALUE self, VALUE mini_ssl_ctx) { VALUE obj; SSL_CTX* ctx; SSL* ssl; ms_conn* conn = engine_alloc(self, &obj); - StringValue(key); - StringValue(cert); + ID sym_key = rb_intern("key"); + VALUE key = rb_funcall(mini_ssl_ctx, sym_key, 0); + + ID sym_cert = rb_intern("cert"); + VALUE cert = rb_funcall(mini_ssl_ctx, sym_cert, 0); ctx = SSL_CTX_new(SSLv23_server_method()); conn->ctx = ctx; @@ -184,7 +187,7 @@ void Init_mini_ssl(VALUE puma) { eError = rb_define_class_under(mod, "SSLError", rb_eStandardError); - rb_define_singleton_method(eng, "server", engine_init_server, 2); + rb_define_singleton_method(eng, "server", engine_init_server, 1); rb_define_singleton_method(eng, "client", engine_init_client, 0); rb_define_method(eng, "inject", engine_inject, 1); diff --git a/ext/puma_http11/org/jruby/puma/MiniSSL.java b/ext/puma_http11/org/jruby/puma/MiniSSL.java index 1ad2602a..883ba863 100644 --- a/ext/puma_http11/org/jruby/puma/MiniSSL.java +++ b/ext/puma_http11/org/jruby/puma/MiniSSL.java @@ -2,29 +2,34 @@ package org.jruby.puma; import org.jruby.Ruby; import org.jruby.RubyClass; -import org.jruby.RubyHash; import org.jruby.RubyModule; -import org.jruby.RubyNumeric; import org.jruby.RubyObject; import org.jruby.RubyString; - import org.jruby.anno.JRubyMethod; - import org.jruby.runtime.Block; import org.jruby.runtime.ObjectAllocator; import org.jruby.runtime.ThreadContext; import org.jruby.runtime.builtin.IRubyObject; - -import org.jruby.exceptions.RaiseException; - import org.jruby.util.ByteList; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLEngineResult; +import javax.net.ssl.SSLException; +import javax.net.ssl.SSLSession; +import java.io.FileInputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.security.KeyManagementException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.UnrecoverableKeyException; +import java.security.cert.CertificateException; -import javax.net.ssl.*; -import javax.net.ssl.SSLEngineResult.*; -import java.io.*; -import java.security.*; -import java.nio.*; +import static javax.net.ssl.SSLEngineResult.Status; +import static javax.net.ssl.SSLEngineResult.HandshakeStatus; public class MiniSSL extends RubyObject { private static ObjectAllocator ALLOCATOR = new ObjectAllocator() { @@ -33,9 +38,12 @@ public class MiniSSL extends RubyObject { } }; + // set to true to switch on our low-fi trace logging + private static boolean DEBUG = false; + public static void createMiniSSL(Ruby runtime) { RubyModule mPuma = runtime.defineModule("Puma"); - RubyModule ssl = mPuma.defineModuleUnder("MiniSSL"); + RubyModule ssl = mPuma.defineModuleUnder("MiniSSL"); mPuma.defineClassUnder("SSLError", runtime.getClass("IOError"), @@ -45,245 +53,329 @@ public class MiniSSL extends RubyObject { eng.defineAnnotatedMethods(MiniSSL.class); } - private Ruby runtime; - private SSLContext sslc; + /** + * Fairly transparent wrapper around {@link java.nio.ByteBuffer} which adds the enhancements we need + */ + private static class MiniSSLBuffer { + ByteBuffer buffer; - private SSLEngine engine; + private MiniSSLBuffer(int capacity) { buffer = ByteBuffer.allocate(capacity); } + private MiniSSLBuffer(byte[] initialContents) { buffer = ByteBuffer.wrap(initialContents); } + + public void clear() { buffer.clear(); } + public void compact() { buffer.compact(); } + public void flip() { buffer.flip(); } + public boolean hasRemaining() { return buffer.hasRemaining(); } + public int position() { return buffer.position(); } + + public ByteBuffer getRawBuffer() { + return buffer; + } + + /** + * Writes bytes to the buffer after ensuring there's room + */ + public void put(byte[] bytes) { + if (buffer.remaining() < bytes.length) { + resize(buffer.limit() + bytes.length); + } + buffer.put(bytes); + } + + /** + * Ensures that newCapacity bytes can be written to this buffer, only re-allocating if necessary + */ + public void resize(int newCapacity) { + if (newCapacity > buffer.capacity()) { + ByteBuffer dstTmp = ByteBuffer.allocate(newCapacity); + buffer.flip(); + dstTmp.put(buffer); + buffer = dstTmp; + } else { + buffer.limit(newCapacity); + } + } + + /** + * Drains the buffer to a ByteList, or returns null for an empty buffer + */ + public ByteList asByteList() { + buffer.flip(); + if (!buffer.hasRemaining()) { + buffer.clear(); + return null; + } + + byte[] bss = new byte[buffer.limit()]; + + buffer.get(bss); + buffer.clear(); + return new ByteList(bss); + } + + @Override + public String toString() { return buffer.toString(); } + } + + private SSLEngine engine; + private MiniSSLBuffer inboundNetData; + private MiniSSLBuffer outboundAppData; + private MiniSSLBuffer outboundNetData; - private ByteBuffer peerAppData; - private ByteBuffer peerNetData; - private ByteBuffer netData; - private ByteBuffer dummy; - public MiniSSL(Ruby runtime, RubyClass klass) { super(runtime, klass); - - this.runtime = runtime; } @JRubyMethod(meta = true) - public static IRubyObject server(ThreadContext context, IRubyObject recv, IRubyObject key, IRubyObject cert) { - RubyClass klass = (RubyClass) recv; - IRubyObject newInstance = klass.newInstance(context, - new IRubyObject[] { key, cert }, - Block.NULL_BLOCK); + public static IRubyObject server(ThreadContext context, IRubyObject recv, IRubyObject miniSSLContext) { + RubyClass klass = (RubyClass) recv; - return newInstance; + return klass.newInstance(context, + new IRubyObject[] { miniSSLContext }, + Block.NULL_BLOCK); } @JRubyMethod - public IRubyObject initialize(IRubyObject key, IRubyObject cert) - throws java.security.KeyStoreException, - java.io.FileNotFoundException, - java.io.IOException, - java.io.FileNotFoundException, - java.security.NoSuchAlgorithmException, - java.security.KeyManagementException, - java.security.cert.CertificateException, - java.security.UnrecoverableKeyException - { + public IRubyObject initialize(ThreadContext threadContext, IRubyObject miniSSLContext) + throws KeyStoreException, IOException, CertificateException, NoSuchAlgorithmException, UnrecoverableKeyException, KeyManagementException { KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType()); - KeyStore ts = KeyStore.getInstance(KeyStore.getDefaultType()); - char[] pass = "blahblah".toCharArray(); - - ks.load(new FileInputStream(key.convertToString().asJavaString()), - pass); - ts.load(new FileInputStream(cert.convertToString().asJavaString()), - pass); + char[] password = miniSSLContext.callMethod(threadContext, "keystore_pass").convertToString().asJavaString().toCharArray(); + ks.load(new FileInputStream(miniSSLContext.callMethod(threadContext, "keystore").convertToString().asJavaString()), + password); KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509"); - kmf.init(ks, pass); - - TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509"); - tmf.init(ts); + kmf.init(ks, password); SSLContext sslCtx = SSLContext.getInstance("TLS"); - sslCtx.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null); - - sslc = sslCtx; - - engine = sslc.createSSLEngine(); + sslCtx.init(kmf.getKeyManagers(), null, null); + engine = sslCtx.createSSLEngine(); engine.setUseClientMode(false); - // engine.setNeedClientAuth(true); SSLSession session = engine.getSession(); - peerNetData = ByteBuffer.allocate(session.getPacketBufferSize()); - peerAppData = ByteBuffer.allocate(session.getApplicationBufferSize()); - netData = ByteBuffer.allocate(session.getPacketBufferSize()); - peerNetData.limit(0); - peerAppData.limit(0); - netData.limit(0); + inboundNetData = new MiniSSLBuffer(session.getPacketBufferSize()); + outboundAppData = new MiniSSLBuffer(session.getApplicationBufferSize()); + outboundAppData.flip(); + outboundNetData = new MiniSSLBuffer(session.getPacketBufferSize()); - peerNetData.clear(); - peerAppData.clear(); - netData.clear(); - - dummy = ByteBuffer.allocate(0); - return this; } @JRubyMethod public IRubyObject inject(IRubyObject arg) { - byte[] bytes = arg.convertToString().getBytes(); + try { + byte[] bytes = arg.convertToString().getBytes(); - peerNetData.limit(peerNetData.limit() + bytes.length); + log("Net Data post pre-inject: " + inboundNetData); + inboundNetData.put(bytes); + log("Net Data post post-inject: " + inboundNetData); - log("capacity: " + peerNetData.capacity() + " limit: " + peerNetData.limit()); + log("inject(): " + bytes.length + " encrypted bytes from request"); + return this; + } catch (Exception e) { + e.printStackTrace(); + throw new RuntimeException(e); + } + } - peerNetData.put(bytes); + private enum SSLOperation { + WRAP, + UNWRAP + } - log("netData: " + peerNetData.position() + "/" + peerAppData.limit()); - return this; + private SSLEngineResult doOp(SSLOperation sslOp, MiniSSLBuffer src, MiniSSLBuffer dst) throws SSLException { + SSLEngineResult res = null; + boolean retryOp = true; + while (retryOp) { + switch (sslOp) { + case WRAP: + res = engine.wrap(src.getRawBuffer(), dst.getRawBuffer()); + break; + case UNWRAP: + res = engine.unwrap(src.getRawBuffer(), dst.getRawBuffer()); + break; + default: + throw new IllegalStateException("Unknown SSLOperation: " + sslOp); + } + + switch (res.getStatus()) { + case BUFFER_OVERFLOW: + log("SSLOp#doRun(): overflow"); + log("SSLOp#doRun(): dst data at overflow: " + dst); + // increase the buffer size to accommodate the overflowing data + int newSize = Math.max(engine.getSession().getPacketBufferSize(), engine.getSession().getApplicationBufferSize()); + dst.resize(newSize + dst.position()); + // retry the operation + retryOp = true; + break; + case BUFFER_UNDERFLOW: + log("SSLOp#doRun(): underflow"); + log("SSLOp#doRun(): src data at underflow: " + src); + // need to wait for more data to come in before we retry + retryOp = false; + break; + default: + // other cases are OK and CLOSED. We're done here. + retryOp = false; + } + } + + // after each op, run any delegated tasks if needed + if(engine.getHandshakeStatus() == HandshakeStatus.NEED_TASK) { + Runnable runnable; + while ((runnable = engine.getDelegatedTask()) != null) { + runnable.run(); + } + } + + return res; } @JRubyMethod - public IRubyObject read() throws javax.net.ssl.SSLException, Exception { - peerAppData.clear(); - peerNetData.flip(); - SSLEngineResult res; + public IRubyObject read() throws Exception { + try { + inboundNetData.flip(); - log("available read: " + peerNetData.position() + "/ " + peerNetData.limit()); + if(!inboundNetData.hasRemaining()) { + return getRuntime().getNil(); + } - if(!peerNetData.hasRemaining()) { - return getRuntime().getNil(); + log("read(): inboundNetData prepped for read: " + inboundNetData); + + MiniSSLBuffer inboundAppData = new MiniSSLBuffer(engine.getSession().getApplicationBufferSize()); + SSLEngineResult res = doOp(SSLOperation.UNWRAP, inboundNetData, inboundAppData); + log("read(): after initial unwrap", engine, res); + + log("read(): Net Data post unwrap: " + inboundNetData); + + HandshakeStatus handshakeStatus = engine.getHandshakeStatus(); + boolean done = false; + while (!done) { + switch (handshakeStatus) { + case NEED_WRAP: + res = doOp(SSLOperation.WRAP, inboundAppData, outboundNetData); + log("read(): after handshake wrap", engine, res); + break; + case NEED_UNWRAP: + res = doOp(SSLOperation.UNWRAP, inboundNetData, inboundAppData); + log("read(): after handshake unwrap", engine, res); + if (res.getStatus() == Status.BUFFER_UNDERFLOW) { + // need more data before we can shake more hands + done = true; + } + break; + default: + done = true; + } + handshakeStatus = engine.getHandshakeStatus(); + } + + if (inboundNetData.hasRemaining()) { + log("Net Data post pre-compact: " + inboundNetData); + inboundNetData.compact(); + log("Net Data post post-compact: " + inboundNetData); + } else { + log("Net Data post pre-reset: " + inboundNetData); + inboundNetData.clear(); + log("Net Data post post-reset: " + inboundNetData); + } + + ByteList appDataByteList = inboundAppData.asByteList(); + if (appDataByteList == null) { + return getRuntime().getNil(); + } + + RubyString str = getRuntime().newString(""); + str.setValue(appDataByteList); + + logPlain("\n"); + log("read(): begin dump of request data >>>>\n"); + if (str.asJavaString().getBytes().length < 1000) { + logPlain(str.asJavaString() + "\n"); + } + logPlain("Num bytes: " + str.asJavaString().getBytes().length + "\n"); + log("read(): end dump of request data <<<<\n"); + return str; + } catch (Exception e) { + e.printStackTrace(); + throw new RuntimeException(e); } - - do { - res = engine.unwrap(peerNetData, peerAppData); - } while(res.getStatus() == SSLEngineResult.Status.OK && - res.getHandshakeStatus() == SSLEngineResult.HandshakeStatus.NEED_UNWRAP && - res.bytesProduced() == 0); - - log("read: ", res); - - if(peerNetData.hasRemaining()) { - log("STILL HAD peerNetData!"); - } - - peerNetData.position(0); - peerNetData.limit(0); - - HandshakeStatus hsStatus = runDelegatedTasks(res, engine); - - if(res.getStatus() == SSLEngineResult.Status.BUFFER_UNDERFLOW) { - return getRuntime().getNil(); - } - - if(hsStatus == HandshakeStatus.NEED_WRAP) { - netData.clear(); - log("netData: " + netData.limit()); - engine.wrap(dummy, netData); - return getRuntime().getNil(); - } - - if(hsStatus == HandshakeStatus.NEED_UNWRAP) { - return getRuntime().getNil(); - - // log("peerNet: " + peerNetData.position() + "/" + peerNetData.limit()); - // log("peerApp: " + peerAppData.position() + "/" + peerAppData.limit()); - - // peerNetData.compact(); - - // log("peerNet: " + peerNetData.position() + "/" + peerNetData.limit()); - // do { - // res = engine.unwrap(peerNetData, peerAppData); - // } while(res.getStatus() == SSLEngineResult.Status.OK && - // res.getHandshakeStatus() == SSLEngineResult.HandshakeStatus.NEED_UNWRAP && - // res.bytesProduced() == 0); - // return getRuntime().getNil(); - } - - // if(peerAppData.position() == 0 && - // res.getStatus() == SSLEngineResult.Status.OK && - // peerNetData.hasRemaining()) { - // res = engine.unwrap(peerNetData, peerAppData); - // } - - byte[] bss = new byte[peerAppData.limit()]; - - peerAppData.get(bss); - - RubyString str = getRuntime().newString(""); - str.setValue(new ByteList(bss)); - - return str; } - private static HandshakeStatus runDelegatedTasks(SSLEngineResult result, - SSLEngine engine) throws Exception { - - HandshakeStatus hsStatus = result.getHandshakeStatus(); - - if(hsStatus == HandshakeStatus.NEED_TASK) { - Runnable runnable; - while ((runnable = engine.getDelegatedTask()) != null) { - log("\trunning delegated task..."); - runnable.run(); - } - hsStatus = engine.getHandshakeStatus(); - if (hsStatus == HandshakeStatus.NEED_TASK) { - throw new Exception( - "handshake shouldn't need additional tasks"); - } - log("\tnew HandshakeStatus: " + hsStatus); - } - - return hsStatus; - } - - - private static void log(String str, SSLEngineResult result) { - System.out.println("The format of the SSLEngineResult is: \n" + - "\t\"getStatus() / getHandshakeStatus()\" +\n" + - "\t\"bytesConsumed() / bytesProduced()\"\n"); - - HandshakeStatus hsStatus = result.getHandshakeStatus(); - log(str + - result.getStatus() + "/" + hsStatus + ", " + - result.bytesConsumed() + "/" + result.bytesProduced() + - " bytes"); - if (hsStatus == HandshakeStatus.FINISHED) { - log("\t...ready for application data"); + private static void log(String str, SSLEngine engine, SSLEngineResult result) { + if (DEBUG) { + log(str + " " + result.getStatus() + "/" + engine.getHandshakeStatus() + + "---bytes consumed: " + result.bytesConsumed() + + ", bytes produced: " + result.bytesProduced()); } } private static void log(String str) { - System.out.println(str); - } - - - - @JRubyMethod - public IRubyObject write(IRubyObject arg) throws javax.net.ssl.SSLException { - log("write from: " + netData.position()); - - byte[] bls = arg.convertToString().getBytes(); - ByteBuffer src = ByteBuffer.wrap(bls); - - SSLEngineResult res = engine.wrap(src, netData); - - return getRuntime().newFixnum(res.bytesConsumed()); - } - - @JRubyMethod - public IRubyObject extract() { - netData.flip(); - - if(!netData.hasRemaining()) { - return getRuntime().getNil(); + if (DEBUG) { + System.out.println("MiniSSL.java: " + str); } + } - byte[] bss = new byte[netData.limit()]; + private static void logPlain(String str) { + if (DEBUG) { + System.out.println(str); + } + } - netData.get(bss); - netData.clear(); + @JRubyMethod + public IRubyObject write(IRubyObject arg) { + try { + log("write(): begin dump of response data >>>>\n"); + logPlain("\n"); + if (arg.asJavaString().getBytes().length < 1000) { + logPlain(arg.asJavaString() + "\n"); + } + logPlain("Num bytes: " + arg.asJavaString().getBytes().length + "\n"); + log("write(): end dump of response data <<<<\n"); - RubyString str = getRuntime().newString(""); - str.setValue(new ByteList(bss)); + byte[] bls = arg.convertToString().getBytes(); + outboundAppData = new MiniSSLBuffer(bls); - return str; + return getRuntime().newFixnum(bls.length); + } catch (Exception e) { + e.printStackTrace(); + throw new RuntimeException(e); + } + } + + @JRubyMethod + public IRubyObject extract() throws SSLException { + try { + ByteList dataByteList = outboundNetData.asByteList(); + if (dataByteList != null) { + RubyString str = getRuntime().newString(""); + str.setValue(dataByteList); + return str; + } + + if (!outboundAppData.hasRemaining()) { + return getRuntime().getNil(); + } + + outboundNetData.clear(); + SSLEngineResult res = doOp(SSLOperation.WRAP, outboundAppData, outboundNetData); + log("extract(): bytes consumed: " + res.bytesConsumed() + "\n"); + log("extract(): bytes produced: " + res.bytesProduced() + "\n"); + dataByteList = outboundNetData.asByteList(); + if (dataByteList == null) { + return getRuntime().getNil(); + } + + RubyString str = getRuntime().newString(""); + str.setValue(dataByteList); + + log("extract(): " + dataByteList.getRealSize() + " encrypted bytes for response"); + + return str; + } catch (Exception e) { + e.printStackTrace(); + throw new RuntimeException(e); + } } } diff --git a/lib/puma/binder.rb b/lib/puma/binder.rb index 484756fb..f565700f 100644 --- a/lib/puma/binder.rb +++ b/lib/puma/binder.rb @@ -121,27 +121,37 @@ module Puma @listeners << [str, io] when "ssl" - if IS_JRUBY - @events.error "SSL not supported on JRuby" - raise UnsupportedOption - end - params = Rack::Utils.parse_query uri.query require 'puma/minissl' ctx = MiniSSL::Context.new - unless params['key'] - @events.error "Please specify the SSL key via 'key='" + + if defined?(JRUBY_VERSION) + unless params['keystore'] + @events.error "Please specify the Java keystore via 'keystore='" + end + + ctx.keystore = params['keystore'] + + unless params['keystore-pass'] + @events.error "Please specify the Java keystore password via 'keystore-pass='" + end + + ctx.keystore_pass = params['keystore-pass'] + else + unless params['key'] + @events.error "Please specify the SSL key via 'key='" + end + + ctx.key = params['key'] + + unless params['cert'] + @events.error "Please specify the SSL cert via 'cert='" + end + + ctx.cert = params['cert'] end - ctx.key = params['key'] - - unless params['cert'] - @events.error "Please specify the SSL cert via 'cert='" - end - - ctx.cert = params['cert'] - ctx.verify_mode = MiniSSL::VERIFY_NONE if fd = @inherited_fds.delete(str) @@ -215,11 +225,6 @@ module Puma def add_ssl_listener(host, port, ctx, optimize_for_latency=true, backlog=1024) - if IS_JRUBY - @events.error "SSL not supported on JRuby" - raise UnsupportedOption - end - require 'puma/minissl' s = TCPServer.new(host, port) @@ -239,11 +244,6 @@ module Puma end def inherited_ssl_listener(fd, ctx) - if IS_JRUBY - @events.error "SSL not supported on JRuby" - raise UnsupportedOption - end - require 'puma/minissl' s = TCPServer.for_fd(fd) @ios << MiniSSL::Server.new(s, ctx) diff --git a/lib/puma/minissl.rb b/lib/puma/minissl.rb index 8b185e7b..0ae9ab75 100644 --- a/lib/puma/minissl.rb +++ b/lib/puma/minissl.rb @@ -69,7 +69,7 @@ module Puma return data.bytesize if need == 0 - data = data[need..-1] + data = data[wrote..-1] end end @@ -91,31 +91,35 @@ module Puma class Context attr_accessor :verify_mode - attr_reader :key - attr_reader :cert + if defined?(JRUBY_VERSION) + # jruby-specific Context properties: java uses a keystore and password pair rather than a cert/key pair + attr_reader :keystore + attr_accessor :keystore_pass - def key=(key) - raise ArgumentError, "No such key file '#{key}'" unless File.exist? key - @key = key - end + def keystore=(keystore) + raise ArgumentError, "No such keystore file '#{keystore}'" unless File.exist? keystore + @keystore = keystore + end + else + # non-jruby Context properties + attr_reader :key + attr_reader :cert - def cert=(cert) - raise ArgumentError, "No such cert file '#{cert}'" unless File.exist? cert - @cert = cert + def key=(key) + raise ArgumentError, "No such key file '#{key}'" unless File.exist? key + @key = key + end + + def cert=(cert) + raise ArgumentError, "No such cert file '#{cert}'" unless File.exist? cert + @cert = cert + end end end VERIFY_NONE = 0 VERIFY_PEER = 1 - #if defined?(JRUBY_VERSION) - #class Engine - #def self.server(key, cert) - #new(key, cert) - #end - #end - #end - class Server def initialize(socket, ctx) @socket = socket @@ -128,14 +132,14 @@ module Puma def accept io = @socket.accept - engine = Engine.server @ctx.key, @ctx.cert + engine = Engine.server @ctx Socket.new io, engine end def accept_nonblock io = @socket.accept_nonblock - engine = Engine.server @ctx.key, @ctx.cert + engine = Engine.server @ctx Socket.new io, engine end diff --git a/test/test_minissl.rb b/test/test_minissl.rb index dabc0e3c..aeba9317 100644 --- a/test/test_minissl.rb +++ b/test/test_minissl.rb @@ -1,25 +1,29 @@ require 'test/unit' - -unless defined? JRUBY_VERSION - require 'puma' require 'puma/minissl' class TestMiniSSL < Test::Unit::TestCase - def test_raises_with_invalid_key_file - ctx = Puma::MiniSSL::Context.new + if defined?(JRUBY_VERSION) + def test_raises_with_invalid_keystore_file + ctx = Puma::MiniSSL::Context.new - exception = assert_raise(ArgumentError) { ctx.key = "/no/such/key" } - assert_equal("No such key file '/no/such/key'", exception.message) - end + exception = assert_raise(ArgumentError) { ctx.keystore = "/no/such/keystore" } + assert_equal("No such keystore file '/no/such/keystore'", exception.message) + end + else + def test_raises_with_invalid_key_file + ctx = Puma::MiniSSL::Context.new - def test_raises_with_invalid_cert_file - ctx = Puma::MiniSSL::Context.new + exception = assert_raise(ArgumentError) { ctx.key = "/no/such/key" } + assert_equal("No such key file '/no/such/key'", exception.message) + end - exception = assert_raise(ArgumentError) { ctx.cert = "/no/such/cert" } - assert_equal("No such cert file '/no/such/cert'", exception.message) + def test_raises_with_invalid_cert_file + ctx = Puma::MiniSSL::Context.new + + exception = assert_raise(ArgumentError) { ctx.cert = "/no/such/cert" } + assert_equal("No such cert file '/no/such/cert'", exception.message) + end end end - -end diff --git a/test/test_puma_server.rb b/test/test_puma_server.rb index 218936f6..76ef4e10 100644 --- a/test/test_puma_server.rb +++ b/test/test_puma_server.rb @@ -18,47 +18,12 @@ class TestPumaServer < Test::Unit::TestCase @events = Puma::Events.new STDOUT, STDERR @server = Puma::Server.new @app, @events - - if defined?(JRUBY_VERSION) - @ssl_key = File.expand_path "../../examples/puma/keystore.jks", __FILE__ - @ssl_cert = @ssl_key - else - @ssl_key = File.expand_path "../../examples/puma/puma_keypair.pem", __FILE__ - @ssl_cert = File.expand_path "../../examples/puma/cert_puma.pem", __FILE__ - end end def teardown @server.stop(true) end - def test_url_scheme_for_https - ctx = Puma::MiniSSL::Context.new - - ctx.key = @ssl_key - ctx.cert = @ssl_cert - - ctx.verify_mode = Puma::MiniSSL::VERIFY_NONE - - @server.add_ssl_listener @host, @port, ctx - @server.run - - http = Net::HTTP.new @host, @port - http.use_ssl = true - http.verify_mode = OpenSSL::SSL::VERIFY_NONE - - body = nil - http.start do - req = Net::HTTP::Get.new "/", {} - - http.request(req) do |rep| - body = rep.body - end - end - - assert_equal "https", body - end unless defined? JRUBY_VERSION - def test_proper_stringio_body data = nil diff --git a/test/test_puma_server_ssl.rb b/test/test_puma_server_ssl.rb new file mode 100644 index 00000000..4a5dd14b --- /dev/null +++ b/test/test_puma_server_ssl.rb @@ -0,0 +1,91 @@ +require "rbconfig" +require 'test/unit' +require 'socket' +require 'openssl' + +require 'puma/minissl' +require 'puma/server' + +require 'net/https' + +class TestPumaServerSSL < Test::Unit::TestCase + + def setup + @port = 3212 + @host = "127.0.0.1" + + @app = lambda { |env| [200, {}, [env['rack.url_scheme']]] } + + ctx = Puma::MiniSSL::Context.new + + if defined?(JRUBY_VERSION) + ctx.keystore = File.expand_path "../../examples/puma/keystore.jks", __FILE__ + ctx.keystore_pass = 'blahblah' + else + ctx.key = File.expand_path "../../examples/puma/puma_keypair.pem", __FILE__ + ctx.cert = File.expand_path "../../examples/puma/cert_puma.pem", __FILE__ + end + + ctx.verify_mode = Puma::MiniSSL::VERIFY_NONE + + @events = Puma::Events.new STDOUT, STDERR + @server = Puma::Server.new @app, @events + @server.add_ssl_listener @host, @port, ctx + @server.run + + @http = Net::HTTP.new @host, @port + @http.use_ssl = true + @http.verify_mode = OpenSSL::SSL::VERIFY_NONE + end + + def teardown + @server.stop(true) + end + + def test_url_scheme_for_https + body = nil + @http.start do + req = Net::HTTP::Get.new "/", {} + + @http.request(req) do |rep| + body = rep.body + end + end + + assert_equal "https", body + end + + def test_very_large_return + giant = "x" * 2056610 + + @server.app = proc do + [200, {}, [giant]] + end + + body = nil + @http.start do + req = Net::HTTP::Get.new "/" + @http.request(req) do |rep| + body = rep.body + end + end + + assert_equal giant.bytesize, body.bytesize + end + + def test_form_submit + body = nil + @http.start do + req = Net::HTTP::Post.new '/' + req.set_form_data('a' => '1', 'b' => '2') + + @http.request(req) do |rep| + body = rep.body + end + + end + + assert_equal "https", body + end + +end \ No newline at end of file