puma/lib/puma/minissl.rb

437 lines
12 KiB
Ruby

# frozen_string_literal: true
begin
require 'io/wait' unless Puma::HAS_NATIVE_IO_WAIT
rescue LoadError
end
# need for Puma::MiniSSL::OPENSSL constants used in `HAS_TLS1_3`
# use require, see https://github.com/puma/puma/pull/2381
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
@reuse = 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 && ssl_version_state == ['TLSv1.3', 'SSLERR']
end
private :bad_tlsv1_3?
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 engine_read_all
output = @engine.read
while output and additional_output = @engine.read
output << additional_output
end
output
end
def read_nonblock(size, *_)
# *_ is to deal with keyword args that were added
# at some point (and being used in the wild)
while true
output = engine_read_all
return output if output
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
while neg_data = @engine.extract
@socket.write neg_data
end
end
end
def write(data)
return 0 if data.empty?
data_size = data.bytesize
need = data_size
while true
wrote = @engine.write data
enc_wr = +''
while (enc = @engine.extract)
enc_wr << enc
end
@socket.write enc_wr unless enc_wr.empty?
need -= wrote
return data_size if need == 0
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
# 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.
#
def write_nonblock(data, *_)
write data
end
def flush
@socket.flush
end
def close
begin
unless @engine.shutdown
while alert_data = @engine.extract
@socket.write alert_data
end
end
rescue IOError, SystemCallError
Puma::Util.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 IS_JRUBY
OPENSSL_NO_SSL3 = false
OPENSSL_NO_TLS1 = false
end
class Context
attr_accessor :verify_mode
attr_reader :no_tlsv1, :no_tlsv1_1
def initialize
@no_tlsv1 = false
@no_tlsv1_1 = false
@key = nil
@cert = nil
@key_pem = nil
@cert_pem = nil
@reuse = nil
@reuse_cache_size = nil
@reuse_timeout = nil
end
def check_file(file, desc)
raise ArgumentError, "#{desc} file '#{file}' does not exist" unless File.exist? file
raise ArgumentError, "#{desc} file '#{file}' is not readable" unless File.readable? file
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_reader :keystore_type
attr_accessor :keystore_pass
attr_reader :truststore
attr_reader :truststore_type
attr_accessor :truststore_pass
attr_reader :cipher_suites
attr_reader :protocols
def keystore=(keystore)
check_file keystore, 'Keystore'
@keystore = keystore
end
def truststore=(truststore)
# NOTE: historically truststore was assumed the same as keystore, this is kept for backwards
# compatibility, to rely on JVM's trust defaults we allow setting `truststore = :default`
unless truststore.eql?(:default)
raise ArgumentError, "No such truststore file '#{truststore}'" unless File.exist?(truststore)
end
@truststore = truststore
end
def keystore_type=(type)
raise ArgumentError, "Invalid keystore type: #{type.inspect}" unless ['pkcs12', 'jks', nil].include?(type)
@keystore_type = type
end
def truststore_type=(type)
raise ArgumentError, "Invalid truststore type: #{type.inspect}" unless ['pkcs12', 'jks', nil].include?(type)
@truststore_type = type
end
def cipher_suites=(list)
list = list.split(',').map(&:strip) if list.is_a?(String)
@cipher_suites = list
end
# aliases for backwards compatibility
alias_method :ssl_cipher_list, :cipher_suites
alias_method :ssl_cipher_list=, :cipher_suites=
def protocols=(list)
list = list.split(',').map(&:strip) if list.is_a?(String)
@protocols = list
end
def check
raise "Keystore not configured" unless @keystore
# @truststore defaults to @keystore due backwards compatibility
end
else
# non-jruby Context properties
attr_reader :key
attr_reader :cert
attr_reader :ca
attr_reader :cert_pem
attr_reader :key_pem
attr_accessor :ssl_cipher_filter
attr_accessor :verification_flags
attr_reader :reuse, :reuse_cache_size, :reuse_timeout
def key=(key)
check_file key, 'Key'
@key = key
end
def cert=(cert)
check_file cert, 'Cert'
@cert = cert
end
def ca=(ca)
check_file ca, 'ca'
@ca = ca
end
def cert_pem=(cert_pem)
raise ArgumentError, "'cert_pem' is not a String" unless cert_pem.is_a? String
@cert_pem = cert_pem
end
def key_pem=(key_pem)
raise ArgumentError, "'key_pem' is not a String" unless key_pem.is_a? String
@key_pem = key_pem
end
def check
raise "Key not configured" if @key.nil? && @key_pem.nil?
raise "Cert not configured" if @cert.nil? && @cert_pem.nil?
end
# Controls session reuse. Allowed values are as follows:
# * 'off' - matches the behavior of Puma 5.6 and earlier. This is included
# in case reuse 'on' is made the default in future Puma versions.
# * 'dflt' - sets session reuse on, with OpenSSL default cache size of
# 20k and default timeout of 300 seconds.
# * 's,t' - where s and t are integer strings, for size and timeout.
# * 's' - where s is an integer strings for size.
# * ',t' - where t is an integer strings for timeout.
#
def reuse=(reuse_str)
case reuse_str
when 'off'
@reuse = nil
when 'dflt'
@reuse = true
when /\A\d+\z/
@reuse = true
@reuse_cache_size = reuse_str.to_i
when /\A\d+,\d+\z/
@reuse = true
size, time = reuse_str.split ','
@reuse_cache_size = size.to_i
@reuse_timeout = time.to_i
when /\A,\d+\z/
@reuse = true
@reuse_timeout = reuse_str.delete(',').to_i
end
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,
"X509_STRICT" => 0x20,
"ALLOW_PROXY_CERTS" => 0x40,
"POLICY_CHECK" => 0x80,
"EXPLICIT_POLICY" => 0x100,
"INHIBIT_ANY" => 0x200,
"INHIBIT_MAP" => 0x400,
"NOTIFY_POLICY" => 0x800,
"EXTENDED_CRL_SUPPORT" => 0x1000,
"USE_DELTAS" => 0x2000,
"CHECK_SS_SIGNATURE" => 0x4000,
"TRUSTED_FIRST" => 0x8000,
"SUITEB_128_LOS_ONLY" => 0x10000,
"SUITEB_192_LOS" => 0x20000,
"SUITEB_128_LOS" => 0x30000,
"PARTIAL_CHAIN" => 0x80000,
"NO_ALT_CHAINS" => 0x100000,
"NO_CHECK_TIME" => 0x200000
}.freeze
class Server
def initialize(socket, ctx)
@socket = socket
@ctx = ctx
@eng_ctx = IS_JRUBY ? @ctx : SSLContext.new(ctx)
end
def accept
@ctx.check
io = @socket.accept
engine = Engine.server @eng_ctx
Socket.new io, engine
end
def accept_nonblock
@ctx.check
io = @socket.accept_nonblock
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