class H2::Client

Constants

ALPN_PROTOCOLS

include FrameDebugger

DEFAULT_MAXLEN
PARSER_EVENTS
RE_IP_ADDR

Attributes

client[R]
last_stream[RW]
reader[R]
scheme[R]
socket[R]
streams[R]

Public Class Methods

new(host: nil, port: 443, url: nil, lazy: true, tls: {}) { |self| ... } click to toggle source

create a new h2 client

@param [String] host IP address or hostname @param [Integer] port TCP port (default: 443) @param [String,URI] url full URL to parse (optional: existing URI instance) @param [Boolean] lazy if true, awaits first stream to initiate connection (default: true) @param [Hash,FalseClass] tls TLS options (optional: false do not use TLS) @option tls [String] :cafile path to CA file

@return [H2::Client]

# File lib/h2/client.rb, line 39
def initialize host: nil, port: 443, url: nil, lazy: true, tls: {}
  raise ArgumentError if url.nil? && (host.nil? || port.nil?)

  if url
    url     = URI.parse url unless URI === url
    @host   = url.host
    @port   = url.port
    @scheme = url.scheme
    tls     = false if 'http' == @scheme
  else
    @host = host
    @port = port
    @scheme = tls ? 'https' : 'http'
  end

  @tls       = tls
  @streams   = {}
  @client    = HTTP2::Client.new
  @read_gate = ReadGate.new

  init_blocking
  yield self if block_given?
  bind_events

  connect unless lazy
end

Public Instance Methods

_read(maxlen = DEFAULT_MAXLEN) click to toggle source

underyling read loop implementation, handling returned Symbol values and shovelling data into the client parser

@param [Integer] maxlen maximum number of bytes to read

# File lib/h2/client.rb, line 227
def _read maxlen = DEFAULT_MAXLEN
  begin
    data = nil

    loop do
      data = read_from_socket maxlen
      case data
      when :wait_readable
        IO.select selector
      when NilClass
        break
      else
        begin
          @client << data
        rescue HTTP2::Error::ProtocolError => pe
          STDERR.puts "protocol error: #{pe.message}"
          STDERR.puts pe.backtrace.map {|l| "\t" + l}
        end
      end
    end

  rescue IOError, Errno::EBADF
    close
  ensure
    unblock!
  end
end
add_params(params, path) click to toggle source

add query string parameters the given request path String

# File lib/h2/client.rb, line 185
def add_params params, path
  appendage = path.index('?') ? '&' : '?'
  path << appendage
  path << URI.encode_www_form(params)
end
add_stream(method:, path:, stream:, &block) click to toggle source

creates a new stream and adds it to the +@streams+ Hash keyed at both the method Symbol and request path as well as the ID of the stream.

# File lib/h2/client.rb, line 174
def add_stream method:, path:, stream:, &block
  @streams[method] ||= {}
  @streams[method][path] ||= []
  stream = Stream.new client: self, stream: stream, &block unless Stream === stream
  @streams[method][path] << stream
  @streams[stream.id] = stream
  stream
end
bind_events() click to toggle source

binds all connection events to their respective on_ handlers

# File lib/h2/client.rb, line 116
def bind_events
  PARSER_EVENTS.each do |e|
    @client.on(e){|*a| __send__ "on_#{e}", *a}
  end
end
build_headers(method:, path:, headers: h = { AUTHORITY_KEY => [@host, @port.to_s].join(':'), METHOD_KEY => method.to_s.upcase, PATH_KEY => path, SCHEME_KEY => @scheme) click to toggle source

builds headers Hash with appropriate ordering

@see http2.github.io/http2-spec/#rfc.section.8.1.2.1 @see github.com/igrigorik/http-2/pull/136

# File lib/h2/client.rb, line 161
def build_headers method:, path:, headers:
  h = {
    AUTHORITY_KEY => [@host, @port.to_s].join(':'),
    METHOD_KEY    => method.to_s.upcase,
    PATH_KEY      => path,
    SCHEME_KEY    => @scheme
  }.merge USER_AGENT
  h.merge! stringify_headers(headers)
end
close() click to toggle source

close the connection

# File lib/h2/client.rb, line 86
def close
  unblock!
  socket.close unless closed?
end
closed?() click to toggle source

@return true if the connection is closed

# File lib/h2/client.rb, line 80
def closed?
  connected? && socket.closed?
end
connect() click to toggle source

initiate the connection

# File lib/h2/client.rb, line 68
def connect
  @socket = TCPSocket.new(@host, @port)
  @socket = tls_socket socket if @tls
  read
end
connected?() click to toggle source
# File lib/h2/client.rb, line 74
def connected?
  !!socket
end
create_ssl_context() click to toggle source

builds a new SSLContext suitable for use in 'h2' connections

# File lib/h2/client.rb, line 362
def create_ssl_context
  ctx                = OpenSSL::SSL::SSLContext.new
  ctx.ca_file        = @tls[:ca_file] if @tls[:ca_file]
  ctx.ca_path        = @tls[:ca_path] if @tls[:ca_path]
  ctx.ciphers        = @tls[:ciphers] || OpenSSL::SSL::SSLContext::DEFAULT_PARAMS[:ciphers]
  ctx.options        = @tls[:options] || OpenSSL::SSL::SSLContext::DEFAULT_PARAMS[:options]
  ctx.ssl_version    = :TLSv1_2
  ctx.verify_mode    = @tls[:verify_mode] || ( OpenSSL::SSL::VERIFY_PEER |
                                               OpenSSL::SSL::VERIFY_FAIL_IF_NO_PEER_CERT )

  # https://github.com/jruby/jruby-openssl/issues/99
  set_ssl_context_protocols ctx unless H2.jruby?

  ctx
end
eof?() click to toggle source
# File lib/h2/client.rb, line 91
def eof?
  socket.eof?
end
goaway(block: false) click to toggle source

send a goaway frame and optionally wait for the connection to be closed

@param [Boolean] block waits for close if true, returns immediately otherwise

@return false if already closed @return nil

# File lib/h2/client.rb, line 108
def goaway block: false
  return false if closed?
  @client.goaway
  block! if block
end
goaway!() click to toggle source

send a goaway frame and wait until the connection is closed

# File lib/h2/client.rb, line 97
def goaway!
  goaway block: true
end
on_close() click to toggle source

close callback for parser: calls custom handler, then closes connection

# File lib/h2/client.rb, line 269
def on_close
  on :close
  close
end
on_frame(bytes) click to toggle source

frame callback for parser: writes bytes to the +@socket+, and slicing appropriately for given return values

@param [String] bytes

# File lib/h2/client.rb, line 279
def on_frame bytes
  on :frame, bytes

  if ::H2::Client::TCPSocket === socket
    total = bytes.bytesize
    loop do
      n = write_to_socket bytes
      if n == :wait_writable
        IO.select nil, socket.selector
      elsif n < total
        bytes = bytes.byteslice n, total
      else
        break
      end
    end
  else
    socket.write bytes
  end
  socket.flush
end
on_frame_sent(frame) click to toggle source

frame_sent callback for parser: used to wait for initial settings frame to be sent by the client (post-connection-preface) before the read thread responds to server settings frame with ack

# File lib/h2/client.rb, line 304
def on_frame_sent frame
  if @read_gate.first && frame[:type] == :settings
    @read_gate.first = false
    @read_gate.unblock!
  end
end
on_goaway(*args) click to toggle source

goaway callback for parser: calls custom handler, then closes connection

# File lib/h2/client.rb, line 323
def on_goaway *args
  on :goaway, *args
  close
end
on_promise(promise) click to toggle source

push promise callback for parser: creates new Stream with appropriate parent, binds close event, calls custom handler

# File lib/h2/client.rb, line 331
def on_promise promise
  push_promise = Stream.new client: self,
                            parent: @streams[promise.parent.id],
                            push: true,
                            stream: promise do |p|
    p.on :close do
      method = p.headers[METHOD_KEY].downcase.to_sym rescue :error
      path = p.headers[PATH_KEY]
      add_stream method: method, path: path, stream: p
    end
  end

  on :promise, push_promise
end
read(maxlen = DEFAULT_MAXLEN) click to toggle source

creates a new Thread to read the given number of bytes each loop from the current +@socket+

NOTE: initial client frames (settings, etc) should be sent first, since

this is a separate thread, take care to block until this happens

NOTE: this is the override point for celluloid actor pool or concurrent

ruby threadpool support

@param [Integer] maxlen maximum number of bytes to read

# File lib/h2/client.rb, line 210
def read maxlen = DEFAULT_MAXLEN
  main = Thread.current
  @reader = Thread.new do
    @read_gate.block!
    begin
      _read maxlen
    rescue => e
      main.raise e
    end
  end
end
read_from_socket(maxlen) click to toggle source

fake exceptionless IO for reading on older ruby versions

@param [Integer] maxlen maximum number of bytes to read

# File lib/h2/client.rb, line 259
def read_from_socket maxlen
  socket.read_nonblock maxlen
rescue IO::WaitReadable
  :wait_readable
end
request(method:, path:, headers: {}) click to toggle source

initiate a Stream by making a request with the given HTTP method

@param [Symbol] method HTTP request method @param [String] path request path @param [Hash] headers request headers @param [Hash] params request query string parameters @param [String] body request body

@yield [H2::Stream]

@return [H2::Stream]

# File lib/h2/client.rb, line 144
def request method:, path:, headers: {}, params: {}, body: nil, &block
  connect unless connected?
  s = @client.new_stream
  add_params params, path unless params.empty?
  stream = add_stream method: method, path: path, stream: s, &block

  h = build_headers method: method, path: path, headers: headers
  s.headers h, end_stream: body.nil?
  s.data body if body
  stream
end
selector() click to toggle source

maintain a ivar for the Array to send to IO.select

# File lib/h2/client.rb, line 195
def selector
  @selector ||= [socket]
end
set_ssl_context_protocols(ctx) click to toggle source
# File lib/h2/client.rb, line 381
def set_ssl_context_protocols ctx
  ctx.alpn_protocols = ALPN_PROTOCOLS
end
tls_socket(socket) click to toggle source

build, configure, and return TLS socket

@param [TCPSocket] socket unencrypted socket

# File lib/h2/client.rb, line 352
def tls_socket socket
  socket = OpenSSL::SSL::SSLSocket.new socket, create_ssl_context
  socket.sync_close = true
  socket.hostname = @host unless RE_IP_ADDR.match(@host)
  socket.connect
  socket
end
write_to_socket(bytes) click to toggle source

fake exceptionless IO for writing on older ruby versions

@param [String] bytes

# File lib/h2/client.rb, line 315
def write_to_socket bytes
  socket.write_nonblock bytes
rescue IO::WaitWritable
  :wait_writable
end