lib/puma/minissl.rb in piesync-puma-3.12.6.1 vs lib/puma/minissl.rb in piesync-puma-5.4.0.1

- old
+ new

@@ -1,29 +1,61 @@ # frozen_string_literal: true begin require 'io/wait' - rescue LoadError +rescue LoadError end +# need for Puma::MiniSSL::OPENSSL constants used in `HAS_TLS1_3` +require 'puma/puma_http11' + 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 + # @version 5.0.0 + HAS_TLS1_3 = !IS_JRUBY && + (OPENSSL_VERSION[/ \d+\.\d+\.\d+/].split('.').map(&:to_i) <=> [1,1,1]) != -1 && + (OPENSSL_LIBRARY_VERSION[/ \d+\.\d+\.\d+/].split('.').map(&:to_i) <=> [1,1,1]) !=-1 + class Socket def initialize(socket, engine) @socket = socket @engine = engine @peercert = nil end + # @!attribute [r] to_io def to_io @socket end def closed? @socket.closed? end + # Returns a two element array, + # first is protocol version (SSL_get_version), + # second is 'handshake' state (SSL_state_string) + # + # Used for dropping tcp connections to ssl. + # See OpenSSL ssl/ssl_stat.c SSL_state_string for info + # @!attribute [r] ssl_version_state + # @version 5.0.0 + # + def ssl_version_state + IS_JRUBY ? [nil, nil] : @engine.ssl_vers_st + end + + # Used to check the handshake status, in particular when a TCP connection + # is made with TLSv1.3 as an available protocol + # @version 5.0.0 + def bad_tlsv1_3? + HAS_TLS1_3 && @engine.ssl_vers_st == ['TLSv1.3', 'SSLERR'] + end + private :bad_tlsv1_3? + def readpartial(size) while true output = @engine.read return output if output @@ -52,26 +84,26 @@ # at some point (and being used in the wild) while true output = engine_read_all return output if output - begin - data = @socket.read_nonblock(size, exception: false) - if data == :wait_readable || data == :wait_writable - if @socket.to_io.respond_to?(data) - @socket.to_io.__send__(data) - elsif data == :wait_readable - IO.select([@socket.to_io]) - else - IO.select(nil, [@socket.to_io]) - end - elsif !data - return nil - else - break - end - end while true + data = @socket.read_nonblock(size, exception: false) + if data == :wait_readable || data == :wait_writable + # It would make more sense to let @socket.read_nonblock raise + # EAGAIN if necessary but it seems like it'll misbehave on Windows. + # I don't have a Windows machine to debug this so I can't explain + # exactly whats happening in that OS. Please let me know if you + # find out! + # + # In the meantime, we can emulate the correct behavior by + # capturing :wait_readable & :wait_writable and raising EAGAIN + # ourselves. + raise IO::EAGAINWaitReadable + elsif data.nil? + raise SSLError.exception "HTTP connection?" if bad_tlsv1_3? + return nil + end @engine.inject(data) output = engine_read_all return output if output @@ -83,104 +115,104 @@ end def write(data) return 0 if data.empty? - need = data.bytesize + data_size = data.bytesize + need = data_size while true wrote = @engine.write data - enc = @engine.extract - while enc - @socket.write enc - enc = @engine.extract + enc_wr = ''.dup + while (enc = @engine.extract) + enc_wr << enc end + @socket.write enc_wr unless enc_wr.empty? need -= wrote - return data.bytesize if need == 0 + return data_size if need == 0 - data = data[wrote..-1] + data = data.byteslice(wrote..-1) end end alias_method :syswrite, :write alias_method :<<, :write # This is a temporary fix to deal with websockets code using - # write_nonblock. The problem with implementing it properly + # write_nonblock. + + # The problem with implementing it properly # is that it means we'd have to have the ability to rewind # an engine because after we write+extract, the socket # write_nonblock call might raise an exception and later # code would pass the same data in, but the engine would think - # it had already written the data in. So for the time being - # (and since write blocking is quite rare), go ahead and actually - # block in write_nonblock. + # it had already written the data in. + # + # So for the time being (and since write blocking is quite rare), + # go ahead and actually block in write_nonblock. + # def write_nonblock(data, *_) write data end def flush @socket.flush end - def read_and_drop(timeout = 1) - return :timeout unless IO.select([@socket], nil, nil, timeout) - return :eof unless read_nonblock(1024) - :drop - rescue Errno::EAGAIN - # do nothing - :eagain - end - - def should_drop_bytes? - @engine.init? || !@engine.shutdown - end - def close begin - # Read any drop any partially initialized sockets and any received bytes during shutdown. - # Don't let this socket hold this loop forever. - # If it can't send more packets within 1s, then give up. - while should_drop_bytes? - return if [:timeout, :eof].include?(read_and_drop(1)) + unless @engine.shutdown + while alert_data = @engine.extract + @socket.write alert_data + end end rescue IOError, SystemCallError Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue # nothing ensure @socket.close end end + # @!attribute [r] peeraddr def peeraddr @socket.peeraddr end + # @!attribute [r] peercert def peercert return @peercert if @peercert raw = @engine.peercert return nil unless raw @peercert = OpenSSL::X509::Certificate.new raw end end - if defined?(JRUBY_VERSION) + if IS_JRUBY + OPENSSL_NO_SSL3 = false + OPENSSL_NO_TLS1 = false + class SSLError < StandardError # Define this for jruby even though it isn't used. end - - def self.check; end end class Context attr_accessor :verify_mode + attr_reader :no_tlsv1, :no_tlsv1_1 - if defined?(JRUBY_VERSION) + def initialize + @no_tlsv1 = false + @no_tlsv1_1 = false + end + + if IS_JRUBY # jruby-specific Context properties: java uses a keystore and password pair rather than a cert/key pair attr_reader :keystore attr_accessor :keystore_pass attr_accessor :ssl_cipher_list @@ -219,16 +251,33 @@ def check raise "Key not configured" unless @key raise "Cert not configured" unless @cert end end + + # disables TLSv1 + # @!attribute [w] no_tlsv1= + def no_tlsv1=(tlsv1) + raise ArgumentError, "Invalid value of no_tlsv1=" unless ['true', 'false', true, false].include?(tlsv1) + @no_tlsv1 = tlsv1 + end + + # disables TLSv1 and TLSv1.1. Overrides `#no_tlsv1=` + # @!attribute [w] no_tlsv1_1= + def no_tlsv1_1=(tlsv1_1) + raise ArgumentError, "Invalid value of no_tlsv1_1=" unless ['true', 'false', true, false].include?(tlsv1_1) + @no_tlsv1_1 = tlsv1_1 + end + end VERIFY_NONE = 0 VERIFY_PEER = 1 VERIFY_FAIL_IF_NO_PEER_CERT = 2 + # https://github.com/openssl/openssl/blob/master/include/openssl/x509_vfy.h.in + # /* Certificate verify flags */ VERIFICATION_FLAGS = { "USE_CHECK_TIME" => 0x2, "CRL_CHECK" => 0x4, "CRL_CHECK_ALL" => 0x8, "IGNORE_CRITICAL" => 0x10, @@ -253,33 +302,43 @@ class Server def initialize(socket, ctx) @socket = socket @ctx = ctx + @eng_ctx = IS_JRUBY ? @ctx : SSLContext.new(ctx) end - def to_io - @socket - end - def accept @ctx.check io = @socket.accept - engine = Engine.server @ctx - + engine = Engine.server @eng_ctx Socket.new io, engine end def accept_nonblock @ctx.check io = @socket.accept_nonblock - engine = Engine.server @ctx - + engine = Engine.server @eng_ctx Socket.new io, engine end + # @!attribute [r] to_io + def to_io + @socket + end + + # @!attribute [r] addr + # @version 5.0.0 + def addr + @socket.addr + end + def close @socket.close unless @socket.closed? # closed? call is for Windows + end + + def closed? + @socket.closed? end end end end