From 65a903181cd5cdd78b4df7eacc1c574f0ef8e95c Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Sat, 29 Nov 2014 04:08:54 +0000 Subject: initial cut at OpenSSL support The current CA model and code quality of OpenSSL have long put me off from supporting TLS; however but efforts such as "Let's Encrypt" and the fallout from Heartbleed give me hope for the future. This implements, as much as possible, a "hands-off" approach to TLS support via OpenSSL. This implementation allows us to shift responsibility away from us to users and upstreams (the Ruby 'openssl' extension maintainers, software packagers, and OpenSSL project itself). This is also perhaps the easiest way for now for us, while being most powerful for users. It requires users to configure their own OpenSSL context object which we'll use as-is. This context object is used as the :ssl_ctx parameter to the "listen" directive in the yahns configuration file: require 'openssl' # we will not do this for the user, even ctx = OpenSSL::SSL::SSLContext.new # user must configure ctx here... listen 443, ssl_ctx: ctx This way, in case we support GnuTLS or other TLS libraries, there'll be less confusion as to what a user is actually using. Note: this feature requires Ruby 2.1 and later for non-kgio {read,write}_nonblock(.. exception: false) support. --- lib/yahns/config.rb | 2 ++ lib/yahns/openssl_client.rb | 52 +++++++++++++++++++++++++++++++++++++++++++++ lib/yahns/openssl_server.rb | 21 ++++++++++++++++++ lib/yahns/server.rb | 15 +++++++------ lib/yahns/socket_helper.rb | 17 +++++++++++---- 5 files changed, 96 insertions(+), 11 deletions(-) create mode 100644 lib/yahns/openssl_client.rb create mode 100644 lib/yahns/openssl_server.rb (limited to 'lib') diff --git a/lib/yahns/config.rb b/lib/yahns/config.rb index f354961..e880d92 100644 --- a/lib/yahns/config.rb +++ b/lib/yahns/config.rb @@ -202,6 +202,8 @@ class Yahns::Config # :nodoc: raise ArgumentError, "#{var}: not boolean: #{key}=#{value.inspect}" end + require_relative('openssl_server') if options[:ssl_ctx] + options[:yahns_app_ctx] = @block.ctx @config_listeners.include?(address) and raise ArgumentError, "listen #{address} already in use" diff --git a/lib/yahns/openssl_client.rb b/lib/yahns/openssl_client.rb new file mode 100644 index 0000000..e4e76c9 --- /dev/null +++ b/lib/yahns/openssl_client.rb @@ -0,0 +1,52 @@ +# Copyright (C) 2014, all contributors +# License: GPLv3 or later (see COPYING for details) + +require_relative 'sendfile_compat' + +# this is to be included into a Kgio::Socket-derived class +# this requires Ruby 2.1 and later for "exception: false" +module Yahns::OpenSSLClient # :nodoc: + include Yahns::SendfileCompat + + def yahns_init_ssl(ssl_ctx) + @need_accept = true + @ssl = OpenSSL::SSL::SSLSocket.new(self, ssl_ctx) + end + + def kgio_trywrite(buf) + rv = @ssl.write_nonblock(buf, exception: false) + Integer === rv and + rv = buf.bytesize == rv ? nil : buf.byteslice(rv, buf.bytesize) + rv + end + + def kgio_syssend(buf, flags) + kgio_trywrite(buf) + end + + def kgio_tryread(len, buf) + if @need_accept + # most protocols require read before write, so we start the negotiation + # process here: + begin + @ssl.accept_nonblock + rescue IO::WaitReadable + return :wait_readable + rescue IO::WaitWritable + return :wait_writable + end + @need_accept = false + end + @ssl.read_nonblock(len, buf, exception: false) + end + + def shutdown(*args) + @ssl.shutdown(*args) + super # BasicSocket#shutdown + end + + def close + @ssl.close + super # IO#close + end +end diff --git a/lib/yahns/openssl_server.rb b/lib/yahns/openssl_server.rb new file mode 100644 index 0000000..3940892 --- /dev/null +++ b/lib/yahns/openssl_server.rb @@ -0,0 +1,21 @@ +# Copyright (C) 2014, all contributors +# License: GPLv3 or later (see COPYING for details) + +require_relative 'acceptor' +require_relative 'openssl_client' + +class Yahns::OpenSSLServer < Kgio::TCPServer # :nodoc: + include Yahns::Acceptor + + def self.wrap(fd, ssl_ctx) + srv = for_fd(fd) + srv.instance_variable_set(:@ssl_ctx, ssl_ctx) + srv + end + + def kgio_accept(klass, flags) + io = super + io.yahns_init_ssl(@ssl_ctx) + io + end +end diff --git a/lib/yahns/server.rb b/lib/yahns/server.rb index 1196d2d..e05a0e4 100644 --- a/lib/yahns/server.rb +++ b/lib/yahns/server.rb @@ -177,10 +177,9 @@ class Yahns::Server # :nodoc: tries = 5 begin - io = bind_listen(address, sock_opts(address)) - unless Yahns::TCPServer === io || Yahns::UNIXServer === io - io = server_cast(io) - end + opts = sock_opts(address) + io = bind_listen(address, opts) + io = server_cast(io, opts) unless io.class.name.start_with?('Yahns::') @logger.info "listening on addr=#{sock_name(io)} fd=#{io.fileno}" @listeners << io io @@ -298,7 +297,7 @@ class Yahns::Server # :nodoc: end def sock_opts(io) - @config.config_listeners[sock_name(io)] + @config.config_listeners[sock_name(io)] || {} end def inherit_listeners! @@ -315,9 +314,10 @@ class Yahns::Server # :nodoc: # accept4(2). inherited = ENV['YAHNS_FD'].to_s.split(',').map! do |fd| io = Socket.for_fd(fd.to_i) - set_server_sockopt(io, sock_opts(io)) + opts = sock_opts(io) + set_server_sockopt(io, opts) @logger.info "inherited addr=#{sock_name(io)} fd=#{fd}" - server_cast(io) + server_cast(io, opts) end @listeners.replace(inherited) @@ -368,6 +368,7 @@ class Yahns::Server # :nodoc: ctx.queue = queues[qegg] ||= qegg_vivify(qegg, fdmap) ctx = ctx.dup ctx.__send__(:include, l.expire_mod) + ctx.__send__(:include, Yahns::OpenSSLClient) if opts[:ssl_ctx] ctx_list << ctx # acceptors feed the the queues l.spawn_acceptor(opts[:threads] || 1, @logger, ctx) diff --git a/lib/yahns/socket_helper.rb b/lib/yahns/socket_helper.rb index 6e1830f..66df8b0 100644 --- a/lib/yahns/socket_helper.rb +++ b/lib/yahns/socket_helper.rb @@ -16,7 +16,7 @@ module Yahns::SocketHelper # :nodoc: end def set_server_sockopt(sock, opt) - opt = {backlog: 1024}.merge!(opt || {}) + opt = {backlog: 1024}.merge!(opt) sock.close_on_exec = true TCPSocket === sock and sock.setsockopt(:IPPROTO_TCP, :TCP_NODELAY, 1) @@ -97,7 +97,12 @@ module Yahns::SocketHelper # :nodoc: sock.bind(Socket.pack_sockaddr_in(port, addr)) sock.autoclose = false - Yahns::TCPServer.for_fd(sock.fileno) + + if ssl_ctx = opt[:ssl_ctx] + Yahns::OpenSSLServer.wrap(sock.fileno, ssl_ctx) + else + Yahns::TCPServer.for_fd(sock.fileno) + end end # returns rfc2732-style (e.g. "[::1]:666") addresses for IPv6 @@ -128,11 +133,15 @@ module Yahns::SocketHelper # :nodoc: end # casts a given Socket to be a TCPServer or UNIXServer - def server_cast(sock) + def server_cast(sock, opts) sock.autoclose = false begin Socket.unpack_sockaddr_in(sock.getsockname) - Yahns::TCPServer.for_fd(sock.fileno) + if ssl_ctx = opts[:ssl_ctx] + Yahns::OpenSSLServer.wrap(sock.fileno, ssl_ctx) + else + Yahns::TCPServer.for_fd(sock.fileno) + end rescue ArgumentError Yahns::UNIXServer.for_fd(sock.fileno) end -- cgit v1.2.3-24-ge0c7