class Seahorse::Client::H2::Connection
Ruby >= 2.3 and OpenSSL >= 1.0.2
with TLS layer plus ALPN, requires:
(requires Ruby >= 2.1)
H2 Connection build on top of ‘http/2` gem
def _debug_entry(str)
def _debug_entry(str) @logger << str @logger << "\n" end
def _default_ca_bundle
def _default_ca_bundle File.exist?(OpenSSL::X509::DEFAULT_CERT_FILE) ? OpenSSL::X509::DEFAULT_CERT_FILE : nil end
def _default_ca_directory
def _default_ca_directory Dir.exist?(OpenSSL::X509::DEFAULT_CERT_DIR) ? OpenSSL::X509::DEFAULT_CERT_DIR : nil end
def _nonblocking_connect(tcp, addr)
def _nonblocking_connect(tcp, addr) begin tcp.connect_nonblock(addr) rescue IO::WaitWritable unless IO.select(nil, [tcp], nil, connection_timeout) tcp.close raise end begin tcp.connect_nonblock(addr) rescue Errno::EISCONN # tcp socket connected, continue end end end
def _register_h2_callbacks
def _register_h2_callbacks @h2_client.on(:frame) do |bytes| if @socket.nil? msg = 'Connection is closed due to errors, '\ 'you can find errors at async_client.connection.errors' raise Http2ConnectionClosedError.new(msg) else @socket.print(bytes) @socket.flush end end if @http_wire_trace @h2_client.on(:frame_sent) do |frame| debug_output("frame: #{frame.inspect}", :send) end @h2_client.on(:frame_received) do |frame| debug_output("frame: #{frame.inspect}", :receive) end end end
def _tcp_socket(endpoint)
def _tcp_socket(endpoint) tcp = ::Socket.new(SOCKET_FAMILY, ::Socket::SOCK_STREAM, 0) tcp.setsockopt(::Socket::IPPROTO_TCP, ::Socket::TCP_NODELAY, 1) address = ::Socket.getaddrinfo(endpoint.host, nil, SOCKET_FAMILY).first[3] sockaddr = ::Socket.sockaddr_in(endpoint.port, address) [tcp, sockaddr] end
def _tls_context
def _tls_context ssl_ctx = OpenSSL::SSL::SSLContext.new(:TLSv1_2) if ssl_verify_peer? ssl_ctx.verify_mode = OpenSSL::SSL::VERIFY_PEER ssl_ctx.ca_file = ssl_ca_bundle ? ssl_ca_bundle : _default_ca_bundle ssl_ctx.ca_path = ssl_ca_directory ? ssl_ca_directory : _default_ca_directory ssl_ctx.cert_store = ssl_ca_store if ssl_ca_store else ssl_ctx.verify_mode = OpenSSL::SSL::VERIFY_NONE end if enable_alpn debug_output('enabling ALPN for TLS ...') ssl_ctx.alpn_protocols = ['h2'] end ssl_ctx end
def close!
def close! @mutex.synchronize { self.debug_output('closing connection ...') if @socket @socket.close @socket = nil end @status = :closed } end
def closed?
def closed? @status == :closed end
def connect(endpoint)
def connect(endpoint) @mutex.synchronize { if @status == :ready tcp, addr = _tcp_socket(endpoint) debug_output("opening connection to #{endpoint.host}:#{endpoint.port} ...") _nonblocking_connect(tcp, addr) debug_output('opened') if endpoint.scheme == 'https' @socket = OpenSSL::SSL::SSLSocket.new(tcp, _tls_context) @socket.sync_close = true @socket.hostname = endpoint.host debug_output("starting TLS for #{endpoint.host}:#{endpoint.port} ...") @socket.connect debug_output('TLS established') else @socket = tcp end _register_h2_callbacks @status = :active elsif @status == :closed msg = 'Async Client HTTP2 Connection is closed, you may'\ ' use #new_connection to create a new HTTP2 Connection for this client' raise Http2ConnectionClosedError.new(msg) end } end
def debug_output(msg, type = nil)
def debug_output(msg, type = nil) prefix = case type when :send then '-> ' when :receive then '<- ' else '' end return unless @logger _debug_entry(prefix + msg) end
def initialize(options = {})
def initialize(options = {}) OPTIONS.each_pair do |opt_name, default_value| value = options[opt_name].nil? ? default_value : options[opt_name] instance_variable_set("@#{opt_name}", value) end @h2_client = HTTP2::Client.new( settings_max_concurrent_streams: max_concurrent_streams ) @logger = if @http_wire_trace options[:logger] || Logger.new($stdout) end @chunk_size = options[:read_chunk_size] || CHUNKSIZE @errors = [] @status = :ready @mutex = Mutex.new # connection can be shared across requests @socket = nil @socket_thread = nil end
def new_stream
def new_stream begin @h2_client.new_stream rescue => error raise Http2StreamInitializeError.new(error) end end
def start(stream)
def start(stream) @mutex.synchronize { return if @socket_thread @socket_thread = Thread.new do while @socket && !@socket.closed? begin data = @socket.read_nonblock(@chunk_size) @h2_client << data rescue IO::WaitReadable begin unless IO.select([@socket], nil, nil, connection_read_timeout) self.debug_output('socket connection read time out') self.close! else # available, retry to start reading retry end rescue # error can happen when closing the socket # while it's waiting for read self.close! end rescue EOFError self.close! rescue => error self.debug_output(error.inspect) @errors << error self.close! end end @socket_thread = nil end @socket_thread.abort_on_exception = true } end