1
0
Fork 0
mirror of https://github.com/puma/puma.git synced 2022-11-09 13:48:40 -05:00

Merge branch 'minissl' into just-dash-w

Conflicts:
	ext/puma_http11/puma_http11.c
	lib/puma/cli.rb
	lib/puma/server.rb
This commit is contained in:
Evan Phoenix 2012-09-10 08:41:43 -07:00
commit faf7f9916c
11 changed files with 639 additions and 18 deletions

View file

@ -60,6 +60,8 @@ file 'ext/puma_http11/org/jruby/puma/Http11Parser.java' => ['ext/puma_http11/htt
end
task :ragel => ['ext/puma_http11/org/jruby/puma/Http11Parser.java']
if !IS_JRUBY
# compile extensions using rake-compiler
# C (MRI, Rubinius)
Rake::ExtensionTask.new("puma_http11", HOE.spec) do |ext|
@ -77,11 +79,15 @@ Rake::ExtensionTask.new("puma_http11", HOE.spec) do |ext|
CLEAN.include "lib/puma/puma_http11.rb"
end
else
# Java (JRuby)
Rake::JavaExtensionTask.new("puma_http11", HOE.spec) do |ext|
ext.lib_dir = "lib/puma"
end
end
# the following is a fat-binary stub that will be used when
# require 'puma/puma_http11' and will use either 1.8 or 1.9 version depending
# on RUBY_VERSION

BIN
examples/puma/keystore.jks Normal file

Binary file not shown.

View file

@ -6,10 +6,12 @@ import org.jruby.Ruby;
import org.jruby.runtime.load.BasicLibraryService;
import org.jruby.puma.Http11;
import org.jruby.puma.MiniSSL;
public class PumaHttp11Service implements BasicLibraryService {
public boolean basicLoad(final Ruby runtime) throws IOException {
Http11.createHttp11(runtime);
MiniSSL.createMiniSSL(runtime);
return true;
}
}

View file

@ -2,4 +2,7 @@ require 'mkmf'
dir_config("puma_http11")
$defs.push "-Wno-deprecated-declarations"
$libs += " -lssl -lcrypto "
create_makefile("puma/puma_http11")

189
ext/puma_http11/mini_ssl.c Normal file
View file

@ -0,0 +1,189 @@
#include <ruby.h>
#include <rubyio.h>
#include <openssl/bio.h>
#include <openssl/ssl.h>
#include <openssl/err.h>
typedef struct {
BIO* read;
BIO* write;
SSL* ssl;
SSL_CTX* ctx;
} ms_conn;
void engine_free(ms_conn* conn) {
BIO_free(conn->read);
BIO_free(conn->write);
free(conn);
}
ms_conn* engine_alloc(VALUE klass, VALUE* obj) {
ms_conn* conn;
*obj = Data_Make_Struct(klass, ms_conn, 0, engine_free, conn);
conn->read = BIO_new(BIO_s_mem());
BIO_set_nbio(conn->read, 1);
conn->write = BIO_new(BIO_s_mem());
BIO_set_nbio(conn->write, 1);
conn->ssl = 0;
conn->ctx = 0;
return conn;
}
VALUE engine_init_server(VALUE self, VALUE key, VALUE cert) {
VALUE obj;
ms_conn* conn = engine_alloc(self, &obj);
StringValue(key);
StringValue(cert);
SSL_CTX* ctx = SSL_CTX_new(SSLv23_server_method());
conn->ctx = ctx;
SSL_CTX_use_certificate_file(ctx, RSTRING_PTR(cert), SSL_FILETYPE_PEM);
SSL_CTX_use_PrivateKey_file(ctx, RSTRING_PTR(key), SSL_FILETYPE_PEM);
/* SSL_CTX_set_options(ctx, SSL_OP_SINGLE_DH_USE); */
SSL* ssl = SSL_new(ctx);
conn->ssl = ssl;
/* SSL_set_verify(ssl, SSL_VERIFY_NONE, NULL); */
SSL_set_bio(ssl, conn->read, conn->write);
SSL_set_accept_state(ssl);
return obj;
}
VALUE engine_init_client(VALUE klass) {
VALUE obj;
ms_conn* conn = engine_alloc(klass, &obj);
conn->ctx = SSL_CTX_new(DTLSv1_method());
conn->ssl = SSL_new(conn->ctx);
SSL_set_verify(conn->ssl, SSL_VERIFY_NONE, NULL);
SSL_set_bio(conn->ssl, conn->read, conn->write);
SSL_set_connect_state(conn->ssl);
return obj;
}
VALUE engine_inject(VALUE self, VALUE str) {
ms_conn* conn;
long used;
Data_Get_Struct(self, ms_conn, conn);
StringValue(str);
used = BIO_write(conn->read, RSTRING_PTR(str), RSTRING_LEN(str));
if(used == 0 || used == -1) {
return Qfalse;
}
return INT2FIX(used);
}
static VALUE eError;
void raise_error(SSL* ssl, int result) {
int error = SSL_get_error(ssl, result);
char* msg = ERR_error_string(error, NULL);
ERR_clear_error();
rb_raise(eError, "OpenSSL error: %s - %d", msg, error);
}
VALUE engine_read(VALUE self) {
ms_conn* conn;
char buf[512];
int bytes, n;
Data_Get_Struct(self, ms_conn, conn);
bytes = SSL_read(conn->ssl, (void*)buf, sizeof(buf));
if(bytes > 0) {
return rb_str_new(buf, bytes);
}
if(SSL_want_read(conn->ssl)) return Qnil;
if(SSL_get_error(conn->ssl, bytes) == SSL_ERROR_ZERO_RETURN) {
rb_eof_error();
}
raise_error(conn->ssl, bytes);
return Qnil;
}
VALUE engine_write(VALUE self, VALUE str) {
ms_conn* conn;
char buf[512];
int bytes;
Data_Get_Struct(self, ms_conn, conn);
StringValue(str);
bytes = SSL_write(conn->ssl, (void*)RSTRING_PTR(str), RSTRING_LEN(str));
if(bytes > 0) {
return INT2FIX(bytes);
}
if(SSL_want_write(conn->ssl)) return Qnil;
raise_error(conn->ssl, bytes);
return Qnil;
}
VALUE engine_extract(VALUE self) {
ms_conn* conn;
int bytes;
size_t pending;
char buf[512];
Data_Get_Struct(self, ms_conn, conn);
pending = BIO_pending(conn->write);
if(pending > 0) {
bytes = BIO_read(conn->write, buf, sizeof(buf));
if(bytes > 0) {
return rb_str_new(buf, bytes);
} else if(!BIO_should_retry(conn->write)) {
raise_error(conn->ssl, bytes);
}
}
return Qnil;
}
void Init_mini_ssl(VALUE puma) {
SSL_library_init();
OpenSSL_add_ssl_algorithms();
SSL_load_error_strings();
ERR_load_crypto_strings();
VALUE mod = rb_define_module_under(puma, "MiniSSL");
VALUE eng = rb_define_class_under(mod, "Engine", rb_cObject);
eError = rb_define_class_under(mod, "SSLError", rb_eStandardError);
rb_define_singleton_method(eng, "server", engine_init_server, 2);
rb_define_singleton_method(eng, "client", engine_init_client, 0);
rb_define_method(eng, "inject", engine_inject, 1);
rb_define_method(eng, "read", engine_read, 0);
rb_define_method(eng, "write", engine_write, 1);
rb_define_method(eng, "extract", engine_extract, 0);
}

View file

@ -0,0 +1,289 @@
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.*;
import javax.net.ssl.SSLEngineResult.*;
import java.io.*;
import java.security.*;
import java.nio.*;
public class MiniSSL extends RubyObject {
private static ObjectAllocator ALLOCATOR = new ObjectAllocator() {
public IRubyObject allocate(Ruby runtime, RubyClass klass) {
return new MiniSSL(runtime, klass);
}
};
public static void createMiniSSL(Ruby runtime) {
RubyModule mPuma = runtime.defineModule("Puma");
RubyModule ssl = mPuma.defineModuleUnder("MiniSSL");
mPuma.defineClassUnder("SSLError",
runtime.getClass("IOError"),
runtime.getClass("IOError").getAllocator());
RubyClass eng = ssl.defineClassUnder("Engine",runtime.getObject(),ALLOCATOR);
eng.defineAnnotatedMethods(MiniSSL.class);
}
private Ruby runtime;
private SSLContext sslc;
private SSLEngine engine;
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);
return newInstance;
}
@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
{
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);
KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
kmf.init(ks, pass);
TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509");
tmf.init(ts);
SSLContext sslCtx = SSLContext.getInstance("TLS");
sslCtx.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
sslc = sslCtx;
engine = sslc.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);
peerNetData.clear();
peerAppData.clear();
netData.clear();
dummy = ByteBuffer.allocate(0);
return this;
}
@JRubyMethod
public IRubyObject inject(IRubyObject arg) {
byte[] bytes = arg.convertToString().getBytes();
peerNetData.limit(peerNetData.limit() + bytes.length);
log("capacity: " + peerNetData.capacity() + " limit: " + peerNetData.limit());
peerNetData.put(bytes);
log("netData: " + peerNetData.position() + "/" + peerAppData.limit());
return this;
}
@JRubyMethod
public IRubyObject read() throws javax.net.ssl.SSLException, Exception {
peerAppData.clear();
peerNetData.flip();
SSLEngineResult res;
log("available read: " + peerNetData.position() + "/ " + peerNetData.limit());
if(!peerNetData.hasRemaining()) {
return getRuntime().getNil();
}
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) {
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();
}
byte[] bss = new byte[netData.limit()];
netData.get(bss);
netData.clear();
RubyString str = getRuntime().newString("");
str.setValue(new ByteList(bss));
return str;
}
}

View file

@ -457,6 +457,7 @@ VALUE HttpParser_body(VALUE self) {
}
void Init_io_buffer(VALUE puma);
void Init_mini_ssl(VALUE mod);
void Init_puma_http11()
{
@ -486,4 +487,5 @@ void Init_puma_http11()
init_common_fields();
Init_io_buffer(mPuma);
Init_mini_ssl(mPuma);
}

View file

@ -99,22 +99,22 @@ module Puma
@listeners << [str, io]
when "ssl"
params = Rack::Utils.parse_query uri.query
require 'openssl'
require 'puma/minissl'
ctx = OpenSSL::SSL::SSLContext.new
ctx = MiniSSL::Context.new
unless params['key']
logger.error "Please specify the SSL key via 'key='"
error "Please specify the SSL key via 'key='"
end
ctx.key = OpenSSL::PKey::RSA.new File.read(params['key'])
ctx.key = params['key']
unless params['cert']
logger.error "Please specify the SSL cert via 'cert='"
error "Please specify the SSL cert via 'cert='"
end
ctx.cert = OpenSSL::X509::Certificate.new File.read(params['cert'])
ctx.cert = params['cert']
ctx.verify_mode = OpenSSL::SSL::VERIFY_NONE
ctx.verify_mode = MiniSSL::VERIFY_NONE
if fd = @inherited_fds.delete(str)
logger.log "* Inherited #{str}"
@ -181,6 +181,8 @@ module Puma
def add_ssl_listener(host, port, ctx,
optimize_for_latency=true, backlog=1024)
require 'puma/minissl'
s = TCPServer.new(host, port)
if optimize_for_latency
s.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
@ -188,7 +190,7 @@ module Puma
s.setsockopt(Socket::SOL_SOCKET,Socket::SO_REUSEADDR, true)
s.listen backlog
ssl = OpenSSL::SSL::SSLServer.new(s, ctx)
ssl = MiniSSL::Server.new s, ctx
env = @proto_env.dup
env[HTTPS_KEY] = HTTPS
@envs[ssl] = env
@ -199,7 +201,7 @@ module Puma
def inherited_ssl_listener(fd, ctx)
s = TCPServer.for_fd(fd)
@ios << OpenSSL::SSL::SSLServer.new(s, ctx)
@ios << MiniSSL::Server.new(s, ctx)
s
end

View file

@ -127,7 +127,11 @@ module Puma
def try_to_finish
return read_body unless @read_header
data = @io.readpartial(CHUNK_SIZE)
begin
data = @io.read_nonblock(CHUNK_SIZE)
rescue Errno::EAGAIN
return false
end
if @buffer
@buffer << data
@ -207,7 +211,11 @@ module Puma
want = remain
end
chunk = @io.readpartial(want)
begin
chunk = @io.read_nonblock(want)
rescue Errno::EAGAIN
return false
end
# No chunk means a closed socket
unless chunk

115
lib/puma/minissl.rb Normal file
View file

@ -0,0 +1,115 @@
module Puma::MiniSSL
class Socket
def initialize(socket, engine)
@socket = socket
@engine = engine
end
def to_io
@socket
end
def readpartial(size)
while true
output = @engine.read
return output if output
data = @socket.readpartial(size)
@engine.inject(data)
output = @engine.read
return output if output
while neg_data = @engine.extract
@socket.write neg_data
end
end
end
def read_nonblock(size)
while true
output = @engine.read
return output if output
data = @socket.read_nonblock(size)
@engine.inject(data)
output = @engine.read
return output if output
while neg_data = @engine.extract
@socket.write neg_data
end
end
end
def write(data)
need = data.size
while true
wrote = @engine.write data
enc = @engine.extract
if enc
@socket.write enc
end
need -= wrote
return data.size if need == 0
data = data[need..-1]
end
end
def flush
@socket.flush
end
def close
@socket.close
end
def peeraddr
@socket.peeraddr
end
end
class Context
attr_accessor :key, :cert, :verify_mode
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
@ctx = ctx
end
def to_io
@socket
end
def accept
io = @socket.accept
engine = Engine.server @ctx.key, @ctx.cert
Socket.new io, engine
end
def close
@socket.close
end
end
end

View file

@ -3,6 +3,7 @@ require 'test/unit'
require 'socket'
require 'openssl'
require 'puma/minissl'
require 'puma/server'
require 'net/https'
@ -18,8 +19,13 @@ class TestPumaServer < Test::Unit::TestCase
@events = Puma::Events.new STDOUT, STDERR
@server = Puma::Server.new @app, @events
@ssl_key = File.expand_path "../../examples/puma/puma_keypair.pem", __FILE__
@ssl_cert = File.expand_path "../../examples/puma/cert_puma.pem", __FILE__
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
@ -27,13 +33,12 @@ class TestPumaServer < Test::Unit::TestCase
end
def test_url_scheme_for_https
ctx = OpenSSL::SSL::SSLContext.new
ctx = Puma::MiniSSL::Context.new
ctx.key = OpenSSL::PKey::RSA.new File.read(@ssl_key)
ctx.key = @ssl_key
ctx.cert = @ssl_cert
ctx.cert = OpenSSL::X509::Certificate.new File.read(@ssl_cert)
ctx.verify_mode = OpenSSL::SSL::VERIFY_NONE
ctx.verify_mode = Puma::MiniSSL::VERIFY_NONE
@server.add_ssl_listener @host, @port, ctx
@server.run