diff --git a/.github/workflows/puma-no-ssl.yml b/.github/workflows/puma-no-ssl.yml new file mode 100644 index 0000000000..339da67d50 --- /dev/null +++ b/.github/workflows/puma-no-ssl.yml @@ -0,0 +1,51 @@ +name: No SSL + +on: [push, pull_request] + +jobs: + build: + name: >- + ${{ matrix.os }} ${{ matrix.ruby }} + env: + CI: true + TESTOPTS: -v + DISABLE_SSL: no_ssl + + runs-on: ${{ matrix.os }} + if: | + !( contains(github.event.pull_request.title, '[ci skip]') + || contains(github.event.pull_request.title, '[skip ci]') + || contains(github.event.head_commit.message, '[ci skip]') + || contains(github.event.head_commit.message, '[skip ci]')) + strategy: + fail-fast: false + matrix: + include: + - { os: ubuntu-20.04, ruby: 2.7 } + - { os: ubuntu-20.04, ruby: jruby } + - { os: windows-2019, ruby: 2.7 } + + steps: + - name: repo checkout + uses: actions/checkout@v2 + + - name: load ruby, ragel + uses: MSP-Greg/setup-ruby-pkgs@v1 + with: + ruby-version: ${{ matrix.ruby }} + apt-get: ragel + brew: ragel + mingw: _upgrade_ openssl ragel + + # won't run on Ruby 2.2, see puma.yml + - name: bundle install + shell: pwsh + run: bundle install --jobs 4 --retry 3 + + - name: compile + run: bundle exec rake compile + + - name: test + id: test + timeout-minutes: 10 + run: bundle exec rake test:all diff --git a/History.md b/History.md index 256942fb6e..84f712b087 100644 --- a/History.md +++ b/History.md @@ -1,6 +1,7 @@ ## 5.0.0 * Features + * Allow compiling without OpenSSL and dynamically load files needed for SSL, add 'no ssl' CI (#2305) * EXPERIMENTAL: Add `fork_worker` option and `refork` command for reduced memory usage by forking from a worker process instead of the master process. (#2099) * EXPERIMENTAL: Added `wait_for_less_busy_worker` config. This may reduce latency on MRI through inserting a small delay before re-listening on the socket if worker is busy (#2079). * EXPERIMENTAL: Added `nakayoshi_fork` option. Reduce memory usage in preloaded cluster-mode apps by GCing before fork and compacting, where available. (#2093, #2256) diff --git a/README.md b/README.md index 6b375d7e1b..80b756afcf 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,18 @@ $ puma Without arguments, puma will look for a rackup (.ru) file in working directory called `config.ru`. +## SSL Connection Support + +Puma will install/compile with support for ssl sockets, assuming OpenSSL +development files are installed on the system. + +If the system does not have OpenSSL development files installed, Puma will +install/compile, but it will not allow ssl connections. + +If the system has OpenSSL development files installed, but you don't want Puma +to use ssl connections, set ENV['DISABLE_SSL'] to any value before installing +Puma. + ## Frameworks ### Rails diff --git a/Rakefile b/Rakefile index ef11de517f..4ca600c444 100644 --- a/Rakefile +++ b/Rakefile @@ -47,6 +47,29 @@ if !Puma.jruby? end else # Java (JRuby) + # ::Rake::JavaExtensionTask.source_files supplies the list of files to + # compile. At present, it only works with a glob prefixed with @ext_dir. + # override it so we can select the files + class ::Rake::JavaExtensionTask + def source_files + if ENV["DISABLE_SSL"] + # uses no_ssl/PumaHttp11Service.java, removes MiniSSL.java + FileList[ + File.join(@ext_dir, "no_ssl/PumaHttp11Service.java"), + File.join(@ext_dir, "org/jruby/puma/Http11.java"), + File.join(@ext_dir, "org/jruby/puma/Http11Parser.java") + ] + else + FileList[ + File.join(@ext_dir, "PumaHttp11Service.java"), + File.join(@ext_dir, "org/jruby/puma/Http11.java"), + File.join(@ext_dir, "org/jruby/puma/Http11Parser.java"), + File.join(@ext_dir, "org/jruby/puma/MiniSSL.java") + ] + end + end + end + Rake::JavaExtensionTask.new("puma_http11", gemspec) do |ext| ext.lib_dir = "lib/puma" end diff --git a/ext/puma_http11/no_ssl/PumaHttp11Service.java b/ext/puma_http11/no_ssl/PumaHttp11Service.java new file mode 100644 index 0000000000..5701e83f61 --- /dev/null +++ b/ext/puma_http11/no_ssl/PumaHttp11Service.java @@ -0,0 +1,15 @@ +package puma; + +import java.io.IOException; + +import org.jruby.Ruby; +import org.jruby.runtime.load.BasicLibraryService; + +import org.jruby.puma.Http11; + +public class PumaHttp11Service implements BasicLibraryService { + public boolean basicLoad(final Ruby runtime) throws IOException { + Http11.createHttp11(runtime); + return true; + } +} diff --git a/ext/puma_http11/puma_http11.c b/ext/puma_http11/puma_http11.c index 962cb8476e..b27b2534cf 100644 --- a/ext/puma_http11/puma_http11.c +++ b/ext/puma_http11/puma_http11.c @@ -434,7 +434,9 @@ VALUE HttpParser_body(VALUE self) { return http->body; } +#ifdef HAVE_OPENSSL_BIO_H void Init_mini_ssl(VALUE mod); +#endif void Init_puma_http11() { @@ -463,5 +465,7 @@ void Init_puma_http11() rb_define_method(cHttpParser, "body", HttpParser_body, 0); init_common_fields(); +#ifdef HAVE_OPENSSL_BIO_H Init_mini_ssl(mPuma); +#endif } diff --git a/lib/puma.rb b/lib/puma.rb index 86f5d97a27..1e8f8e2c19 100644 --- a/lib/puma.rb +++ b/lib/puma.rb @@ -10,6 +10,9 @@ require 'thread' +require_relative 'puma/puma_http11' +require_relative 'puma/detect' + module Puma autoload :Const, 'puma/const' autoload :Server, 'puma/server' @@ -33,4 +36,12 @@ def self.set_thread_name(name) return unless Thread.current.respond_to?(:name=) Thread.current.name = "puma #{name}" end + + unless HAS_SSL + module MiniSSL + # this class is defined so that it exists when Puma is compiled + # without ssl support, as Server and Reactor use it in rescue statements. + class SSLError < StandardError ; end + end + end end diff --git a/lib/puma/binder.rb b/lib/puma/binder.rb index ef9c24fb1a..805bcd0cb6 100644 --- a/lib/puma/binder.rb +++ b/lib/puma/binder.rb @@ -5,10 +5,16 @@ require 'puma/const' require 'puma/util' -require 'puma/minissl/context_builder' require 'puma/configuration' module Puma + + if HAS_SSL + require 'puma/minissl' + require 'puma/minissl/context_builder' + require 'puma/accept_nonblock' + end + class Binder include Puma::Const @@ -155,6 +161,9 @@ def parse(binds, logger, log_msg = 'Listening') @listeners << [str, io] when "ssl" + + raise "Puma compiled without SSL support" unless HAS_SSL + params = Util.parse_query uri.query ctx = MiniSSL::ContextBuilder.new(params, @events).context @@ -245,9 +254,8 @@ def inherit_tcp_listener(host, port, fd) def add_ssl_listener(host, port, ctx, optimize_for_latency=true, backlog=1024) - require 'puma/minissl' - MiniSSL.check + raise "Puma compiled without SSL support" unless HAS_SSL if host == "localhost" loopback_addresses.each do |addr| @@ -264,7 +272,6 @@ def add_ssl_listener(host, port, ctx, s.setsockopt(Socket::SOL_SOCKET,Socket::SO_REUSEADDR, true) s.listen backlog - ssl = MiniSSL::Server.new s, ctx env = @proto_env.dup env[HTTPS_KEY] = HTTPS @@ -275,8 +282,7 @@ def add_ssl_listener(host, port, ctx, end def inherit_ssl_listener(fd, ctx) - require 'puma/minissl' - MiniSSL.check + raise "Puma compiled without SSL support" unless HAS_SSL if fd.kind_of? TCPServer s = fd diff --git a/lib/puma/detect.rb b/lib/puma/detect.rb index 5e8682c917..fa57c70192 100644 --- a/lib/puma/detect.rb +++ b/lib/puma/detect.rb @@ -1,6 +1,13 @@ # frozen_string_literal: true module Puma + # at present, MiniSSL::Engine is only defined in extension code, not in minissl.rb + HAS_SSL = const_defined?(:MiniSSL, false) && MiniSSL.const_defined?(:Engine, false) + + def self.ssl? + HAS_SSL + end + IS_JRUBY = defined?(JRUBY_VERSION) def self.jruby? diff --git a/lib/puma/minissl.rb b/lib/puma/minissl.rb index b0f5847f8c..fd21200510 100644 --- a/lib/puma/minissl.rb +++ b/lib/puma/minissl.rb @@ -10,7 +10,6 @@ module Puma module MiniSSL - # define constant at runtime, as it's easy to determine at built time, # but Puma could (it shouldn't) be loaded with an older OpenSSL version HAS_TLS1_3 = !IS_JRUBY && @@ -203,8 +202,6 @@ def peercert class SSLError < StandardError # Define this for jruby even though it isn't used. end - - def self.check; end end class Context diff --git a/lib/puma/minissl/context_builder.rb b/lib/puma/minissl/context_builder.rb index 667b3ae5c4..f499b5b4ad 100644 --- a/lib/puma/minissl/context_builder.rb +++ b/lib/puma/minissl/context_builder.rb @@ -2,9 +2,6 @@ module Puma module MiniSSL class ContextBuilder def initialize(params, events) - require 'puma/minissl' - MiniSSL.check - @params = params @events = events end diff --git a/lib/puma/reactor.rb b/lib/puma/reactor.rb index 780903d296..8fcc2fa267 100644 --- a/lib/puma/reactor.rb +++ b/lib/puma/reactor.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true require 'puma/util' -require 'puma/minissl' +require 'puma/minissl' if ::Puma::HAS_SSL require 'nio' diff --git a/lib/puma/runner.rb b/lib/puma/runner.rb index 4a9a9c2ae4..022b39c704 100644 --- a/lib/puma/runner.rb +++ b/lib/puma/runner.rb @@ -2,7 +2,6 @@ require 'puma/server' require 'puma/const' -require 'puma/minissl/context_builder' module Puma # Generic class that is used by `Puma::Cluster` and `Puma::Single` to diff --git a/lib/puma/server.rb b/lib/puma/server.rb index 9e3ce90cbc..0aceb23a0c 100644 --- a/lib/puma/server.rb +++ b/lib/puma/server.rb @@ -9,12 +9,9 @@ require 'puma/reactor' require 'puma/client' require 'puma/binder' -require 'puma/accept_nonblock' require 'puma/util' require 'puma/io_buffer' -require 'puma/puma_http11' - require 'socket' require 'forwardable' diff --git a/test/test_binder.rb b/test/test_binder.rb index f8a93e6e6c..ab80c636c8 100644 --- a/test/test_binder.rb +++ b/test/test_binder.rb @@ -1,16 +1,15 @@ # frozen_string_literal: true require_relative "helper" -require_relative "helpers/ssl" +require_relative "helpers/ssl" if ::Puma::HAS_SSL require_relative "helpers/tmp_path" require "puma/binder" -require "puma/puma_http11" require "puma/events" require "puma/configuration" class TestBinderBase < Minitest::Test - include SSLHelper + include SSLHelper if ::Puma::HAS_SSL include TmpPath def setup @@ -58,12 +57,14 @@ def test_connected_ports end def test_localhost_addresses_dont_alter_listeners_for_ssl_addresses + skip 'No ssl support' unless ::Puma::HAS_SSL @binder.parse ["ssl://localhost:0?#{ssl_query}"], @events assert_empty @binder.listeners end def test_home_alters_listeners_for_ssl_addresses + skip 'No ssl support' unless ::Puma::HAS_SSL port = UniquePort.call @binder.parse ["ssl://127.0.0.1:#{port}?#{ssl_query}"], @events @@ -81,7 +82,9 @@ def test_correct_zero_port end def test_correct_zero_port_ssl + skip 'No ssl support' unless ::Puma::HAS_SSL skip("Implement later") + ssl_regex = %r!ssl://127.0.0.1:(\d+)! @binder.parse ["ssl://localhost:0?#{ssl_query}"], @events @@ -101,7 +104,9 @@ def test_logs_all_localhost_bindings end def test_logs_all_localhost_bindings_ssl + skip 'No ssl support' unless ::Puma::HAS_SSL skip("Incorrectly logs localhost, not 127.0.0.1") + @binder.parse ["ssl://localhost:0?#{ssl_query}"], @events assert_match %r!ssl://127.0.0.1:(\d+)!, @events.stdout.string @@ -145,18 +150,21 @@ def test_pre_existing_unix end def test_binder_parses_tlsv1_disabled + skip 'No ssl support' unless ::Puma::HAS_SSL @binder.parse ["ssl://0.0.0.0:0?#{ssl_query}&no_tlsv1=true"], @events assert ssl_context_for_binder.no_tlsv1 end def test_binder_parses_tlsv1_enabled + skip 'No ssl support' unless ::Puma::HAS_SSL @binder.parse ["ssl://0.0.0.0:0?#{ssl_query}&no_tlsv1=false"], @events refute ssl_context_for_binder.no_tlsv1 end def test_binder_parses_tlsv1_tlsv1_1_unspecified_defaults_to_enabled + skip 'No ssl support' unless ::Puma::HAS_SSL @binder.parse ["ssl://0.0.0.0:0?#{ssl_query}"], @events refute ssl_context_for_binder.no_tlsv1 @@ -164,18 +172,21 @@ def test_binder_parses_tlsv1_tlsv1_1_unspecified_defaults_to_enabled end def test_binder_parses_tlsv1_1_disabled + skip 'No ssl support' unless ::Puma::HAS_SSL @binder.parse ["ssl://0.0.0.0:0?#{ssl_query}&no_tlsv1_1=true"], @events assert ssl_context_for_binder.no_tlsv1_1 end def test_binder_parses_tlsv1_1_enabled + skip 'No ssl support' unless ::Puma::HAS_SSL @binder.parse ["ssl://0.0.0.0:0?#{ssl_query}&no_tlsv1_1=false"], @events refute ssl_context_for_binder.no_tlsv1_1 end def test_env_contains_protoenv + skip 'No ssl support' unless ::Puma::HAS_SSL @binder.parse ["ssl://localhost:0?#{ssl_query}"], @events env_hash = @binder.envs[@binder.ios.first] @@ -186,6 +197,7 @@ def test_env_contains_protoenv end def test_env_contains_stderr + skip 'No ssl support' unless ::Puma::HAS_SSL @binder.parse ["ssl://localhost:0?#{ssl_query}"], @events env_hash = @binder.envs[@binder.ios.first] @@ -346,6 +358,7 @@ def @binder.socket_activation_fd(int); @sock_fd; end def assert_parsing_logs_uri(order = [:unix, :tcp]) skip UNIX_SKT_MSG if order.include?(:unix) && !UNIX_SKT_EXIST + skip 'No ssl support' unless ::Puma::HAS_SSL unix_path = tmp_path('.sock') prepared_paths = { @@ -373,6 +386,8 @@ def assert_parsing_logs_uri(order = [:unix, :tcp]) class TestBinderJRuby < TestBinderBase def test_binder_parses_jruby_ssl_options + skip 'No ssl support' unless ::Puma::HAS_SSL + keystore = File.expand_path "../../examples/puma/keystore.jks", __FILE__ ssl_cipher_list = "TLS_DHE_RSA_WITH_AES_128_CBC_SHA,TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256" @@ -385,6 +400,8 @@ def test_binder_parses_jruby_ssl_options class TestBinderMRI < TestBinderBase def test_binder_parses_ssl_cipher_filter + skip 'No ssl support' unless ::Puma::HAS_SSL + ssl_cipher_filter = "AES@STRENGTH" @binder.parse ["ssl://0.0.0.0?#{ssl_query}&ssl_cipher_filter=#{ssl_cipher_filter}"], @events diff --git a/test/test_cli.rb b/test/test_cli.rb index 3043426251..c5e279672e 100644 --- a/test/test_cli.rb +++ b/test/test_cli.rb @@ -1,12 +1,12 @@ require_relative "helper" -require_relative "helpers/ssl" +require_relative "helpers/ssl" if ::Puma::HAS_SSL require_relative "helpers/tmp_path" require "puma/cli" require "json" class TestCLI < Minitest::Test - include SSLHelper + include SSLHelper if ::Puma::HAS_SSL include TmpPath def setup @@ -67,6 +67,8 @@ def test_control_for_tcp end def test_control_for_ssl + skip 'No ssl support' unless ::Puma::HAS_SSL + require "net/http" control_port = UniquePort.call control_host = "127.0.0.1" diff --git a/test/test_config.rb b/test/test_config.rb index dffd48c6e2..773447b2c7 100644 --- a/test/test_config.rb +++ b/test/test_config.rb @@ -44,6 +44,7 @@ def test_app_from_app_DSL end def test_ssl_configuration_from_DSL + skip 'No ssl support' unless ::Puma::HAS_SSL conf = Puma::Configuration.new do |config| config.load "test/config/ssl_config.rb" end @@ -61,6 +62,7 @@ def test_ssl_configuration_from_DSL def test_ssl_bind skip_on :jruby + skip 'No ssl support' unless ::Puma::HAS_SSL conf = Puma::Configuration.new do |c| c.ssl_bind "0.0.0.0", "9292", { @@ -78,6 +80,7 @@ def test_ssl_bind def test_ssl_bind_with_cipher_filter skip_on :jruby + skip 'No ssl support' unless ::Puma::HAS_SSL cipher_filter = "!aNULL:AES+SHA" conf = Puma::Configuration.new do |c| @@ -95,6 +98,7 @@ def test_ssl_bind_with_cipher_filter end def test_ssl_bind_with_ca + skip 'No ssl support' unless ::Puma::HAS_SSL conf = Puma::Configuration.new do |c| c.ssl_bind "0.0.0.0", "9292", { cert: "/path/to/cert", diff --git a/test/test_minissl.rb b/test/test_minissl.rb index 08d6529d1b..f9caf6d4e4 100644 --- a/test/test_minissl.rb +++ b/test/test_minissl.rb @@ -1,6 +1,6 @@ require_relative "helper" -require "puma/minissl" +require "puma/minissl" if ::Puma::HAS_SSL class TestMiniSSL < Minitest::Test @@ -26,4 +26,4 @@ def test_raises_with_invalid_cert_file assert_equal("No such cert file '/no/such/cert'", exception.message) end end -end +end if ::Puma::HAS_SSL diff --git a/test/test_puma_server_ssl.rb b/test/test_puma_server_ssl.rb index 09423b663b..f32f967dee 100644 --- a/test/test_puma_server_ssl.rb +++ b/test/test_puma_server_ssl.rb @@ -1,44 +1,38 @@ +# Nothing in this file runs if Puma isn't compiled with ssl support +# +# helper is required first since it loads Puma, which needs to be +# loaded so HAS_SSL is defined require_relative "helper" -require "puma/minissl" -require "puma/puma_http11" -require "puma/events" -require "net/http" - -#——————————————————————————————————————————————————————————————————————————————— -# NOTE: ALL TESTS BYPASSED IF DISABLE_SSL IS TRUE -#——————————————————————————————————————————————————————————————————————————————— - -class SSLEventsHelper < ::Puma::Events - attr_accessor :addr, :cert, :error - - def ssl_error(error, peeraddr, peercert) - self.error = error - self.addr = peeraddr - self.cert = peercert + +if ::Puma::HAS_SSL + require "puma/minissl" + require "puma/events" + require "net/http" + + class SSLEventsHelper < ::Puma::Events + attr_accessor :addr, :cert, :error + + def ssl_error(error, peeraddr, peercert) + self.error = error + self.addr = peeraddr + self.cert = peercert + end end -end -DISABLE_SSL = begin - Puma::Server.class - Puma::MiniSSL.check - # net/http (loaded in helper) does not necessarily load OpenSSL - require "openssl" unless Object.const_defined? :OpenSSL - if Puma::IS_JRUBY - puts "", RUBY_DESCRIPTION, "RUBYOPT: #{ENV['RUBYOPT']}", - " OpenSSL", - "OPENSSL_LIBRARY_VERSION: #{OpenSSL::OPENSSL_LIBRARY_VERSION}", - " OPENSSL_VERSION: #{OpenSSL::OPENSSL_VERSION}", "" - else - puts "", RUBY_DESCRIPTION, "RUBYOPT: #{ENV['RUBYOPT']}", - " Puma::MiniSSL OpenSSL", - "OPENSSL_LIBRARY_VERSION: #{Puma::MiniSSL::OPENSSL_LIBRARY_VERSION.ljust 32}#{OpenSSL::OPENSSL_LIBRARY_VERSION}", - " OPENSSL_VERSION: #{Puma::MiniSSL::OPENSSL_VERSION.ljust 32}#{OpenSSL::OPENSSL_VERSION}", "" - end - rescue - true - else - false - end + # net/http (loaded in helper) does not necessarily load OpenSSL + require "openssl" unless Object.const_defined? :OpenSSL + if Puma::IS_JRUBY + puts "", RUBY_DESCRIPTION, "RUBYOPT: #{ENV['RUBYOPT']}", + " OpenSSL", + "OPENSSL_LIBRARY_VERSION: #{OpenSSL::OPENSSL_LIBRARY_VERSION}", + " OPENSSL_VERSION: #{OpenSSL::OPENSSL_VERSION}", "" + else + puts "", RUBY_DESCRIPTION, "RUBYOPT: #{ENV['RUBYOPT']}", + " Puma::MiniSSL OpenSSL", + "OPENSSL_LIBRARY_VERSION: #{Puma::MiniSSL::OPENSSL_LIBRARY_VERSION.ljust 32}#{OpenSSL::OPENSSL_LIBRARY_VERSION}", + " OPENSSL_VERSION: #{Puma::MiniSSL::OPENSSL_VERSION.ljust 32}#{OpenSSL::OPENSSL_VERSION}", "" + end +end class TestPumaServerSSL < Minitest::Test parallelize_me! @@ -247,7 +241,7 @@ def test_http_rejection assert busy_threads.zero?, "Our connection is wasn't dropped" end -end unless DISABLE_SSL +end if ::Puma::HAS_SSL # client-side TLS authentication tests class TestPumaServerSSLClient < Minitest::Test @@ -345,4 +339,4 @@ def test_verify_client_cert http.verify_mode = OpenSSL::SSL::VERIFY_PEER end end -end unless DISABLE_SSL +end if ::Puma::HAS_SSL diff --git a/test/test_pumactl.rb b/test/test_pumactl.rb index e5c5ec7e62..485adb832b 100644 --- a/test/test_pumactl.rb +++ b/test/test_pumactl.rb @@ -150,6 +150,8 @@ def test_control_url_and_status end def test_control_ssl + skip 'No ssl support' unless ::Puma::HAS_SSL + host = "127.0.0.1" port = UniquePort.call url = "ssl://#{host}:#{port}?#{ssl_query}"