class H2::Client
Constants
- ALPN_PROTOCOLS
include
FrameDebugger
- DEFAULT_MAXLEN
- PARSER_EVENTS
- RE_IP_ADDR
Attributes
Public Class Methods
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
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 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
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
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
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 the connection
# File lib/h2/client.rb, line 86 def close unblock! socket.close unless closed? end
@return true if the connection is closed
# File lib/h2/client.rb, line 80 def closed? connected? && socket.closed? end
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
# File lib/h2/client.rb, line 74 def connected? !!socket end
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
# File lib/h2/client.rb, line 91 def eof? socket.eof? end
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
send a goaway frame and wait until the connection is closed
# File lib/h2/client.rb, line 97 def goaway! goaway block: true end
close callback for parser: calls custom handler, then closes connection
# File lib/h2/client.rb, line 269 def on_close on :close close end
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
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
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
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
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
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
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
maintain a ivar for the Array
to send to IO.select
# File lib/h2/client.rb, line 195 def selector @selector ||= [socket] end
# File lib/h2/client.rb, line 381 def set_ssl_context_protocols ctx ctx.alpn_protocols = ALPN_PROTOCOLS end
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
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