Class: H2::Client

Inherits:
Object
  • Object
show all
Extended by:
Celluloid::ClassMethods, Concurrent::ClassMethods
Includes:
Blockable, Celluloid, Concurrent, ExceptionlessIO, HeaderStringifier, On
Defined in:
lib/h2/client.rb,
lib/h2/client/celluloid.rb,
lib/h2/client/concurrent.rb,
lib/h2/client/tcp_socket.rb

Defined Under Namespace

Modules: Celluloid, Concurrent, ExceptionlessIO Classes: ReadGate, TCPSocket

Constant Summary collapse

PARSER_EVENTS =
[
  :close,
  :frame,
  :frame_sent,
  :goaway,
  :promise
]
ALPN_PROTOCOLS =

include FrameDebugger

['h2']
DEFAULT_MAXLEN =
4096
RE_IP_ADDR =
Regexp.union Resolv::IPv4::Regex, Resolv::IPv6::Regex

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from Celluloid::ClassMethods

thread_pool

Methods included from Concurrent::ClassMethods

thread_pool

Methods included from On

#on

Methods included from Blockable

#block!, #init_blocking, #unblock!

Constructor Details

#initialize(host: nil, port: 443, url: nil, lazy: true, tls: {}) {|_self| ... } ⇒ H2::Client

create a new h2 client

Parameters:

  • host (String) (defaults to: nil)

    IP address or hostname

  • port (Integer) (defaults to: 443)

    TCP port (default: 443)

  • url (String, URI) (defaults to: nil)

    full URL to parse (optional: existing URI instance)

  • lazy (Boolean) (defaults to: true)

    if true, awaits first stream to initiate connection (default: true)

  • tls (Hash, FalseClass) (defaults to: {})

    TLS options (optional: false do not use TLS)

Options Hash (tls:):

  • :cafile (String)

    path to CA file

Yields:

  • (_self)

Yield Parameters:

  • _self (H2::Client)

    the object that the method was called on

Raises:

  • (ArgumentError)


39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
# 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

Instance Attribute Details

#clientObject (readonly)

Returns the value of attribute client.



26
27
28
# File 'lib/h2/client.rb', line 26

def client
  @client
end

#last_streamObject

Returns the value of attribute last_stream.



25
26
27
# File 'lib/h2/client.rb', line 25

def last_stream
  @last_stream
end

#readerObject (readonly)

Returns the value of attribute reader.



26
27
28
# File 'lib/h2/client.rb', line 26

def reader
  @reader
end

#schemeObject (readonly)

Returns the value of attribute scheme.



26
27
28
# File 'lib/h2/client.rb', line 26

def scheme
  @scheme
end

#socketObject (readonly)

Returns the value of attribute socket.



26
27
28
# File 'lib/h2/client.rb', line 26

def socket
  @socket
end

#streamsObject (readonly)

Returns the value of attribute streams.



26
27
28
# File 'lib/h2/client.rb', line 26

def streams
  @streams
end

Instance Method Details

#_read(maxlen = DEFAULT_MAXLEN) ⇒ Object

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

Parameters:

  • maxlen (Integer) (defaults to: DEFAULT_MAXLEN)

    maximum number of bytes to read



227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
# 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) ⇒ Object

add query string parameters the given request path String



185
186
187
188
189
# 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) ⇒ Object

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.



174
175
176
177
178
179
180
181
# 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_eventsObject

binds all connection events to their respective on_ handlers



116
117
118
119
120
# 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:) ⇒ Object

builds headers Hash with appropriate ordering



161
162
163
164
165
166
167
168
169
# 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

#closeObject

close the connection



86
87
88
89
# File 'lib/h2/client.rb', line 86

def close
  unblock!
  socket.close unless closed?
end

#closed?Boolean

Returns true if the connection is closed.

Returns:

  • (Boolean)

    true if the connection is closed



80
81
82
# File 'lib/h2/client.rb', line 80

def closed?
  connected? && socket.closed?
end

#connectObject

initiate the connection



68
69
70
71
72
# File 'lib/h2/client.rb', line 68

def connect
  @socket = TCPSocket.new(@host, @port)
  @socket = tls_socket socket if @tls
  read
end

#connected?Boolean

Returns:

  • (Boolean)


74
75
76
# File 'lib/h2/client.rb', line 74

def connected?
  !!socket
end

#create_ssl_contextObject

builds a new SSLContext suitable for use in ‘h2’ connections



362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
# 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?Boolean

Returns:

  • (Boolean)


91
92
93
# File 'lib/h2/client.rb', line 91

def eof?
  socket.eof?
end

#goaway(block: false) ⇒ Object

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

Parameters:

  • block (Boolean) (defaults to: false)

    waits for close if true, returns immediately otherwise

Returns:

  • false if already closed

  • nil



108
109
110
111
112
# File 'lib/h2/client.rb', line 108

def goaway block: false
  return false if closed?
  @client.goaway
  block! if block
end

#goaway!Object

send a goaway frame and wait until the connection is closed



97
98
99
# File 'lib/h2/client.rb', line 97

def goaway!
  goaway block: true
end

#on_closeObject

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



269
270
271
272
# File 'lib/h2/client.rb', line 269

def on_close
  on :close
  close
end

#on_frame(bytes) ⇒ Object

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

Parameters:

  • bytes (String)


279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
# 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) ⇒ Object

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



304
305
306
307
308
309
# 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) ⇒ Object

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



323
324
325
326
# File 'lib/h2/client.rb', line 323

def on_goaway *args
  on :goaway, *args
  close
end

#on_promise(promise) ⇒ Object

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



331
332
333
334
335
336
337
338
339
340
341
342
343
344
# 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) ⇒ Object

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

Parameters:

  • maxlen (Integer) (defaults to: DEFAULT_MAXLEN)

    maximum number of bytes to read



210
211
212
213
214
215
216
217
218
219
220
# 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) ⇒ Object

fake exceptionless IO for reading on older ruby versions

Parameters:

  • maxlen (Integer)

    maximum number of bytes to read



259
260
261
262
263
# 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: {}, params: {}, body: nil) {|H2::Stream| ... } ⇒ H2::Stream

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

Parameters:

  • method (Symbol)

    HTTP request method

  • path (String)

    request path

  • headers (Hash) (defaults to: {})

    request headers

  • params (Hash) (defaults to: {})

    request query string parameters

  • body (String) (defaults to: nil)

    request body

Yields:

Returns:



144
145
146
147
148
149
150
151
152
153
154
# 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

#selectorObject

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



195
196
197
# File 'lib/h2/client.rb', line 195

def selector
  @selector ||= [socket]
end

#set_ssl_context_protocols(ctx) ⇒ Object



381
382
383
# File 'lib/h2/client.rb', line 381

def set_ssl_context_protocols ctx
  ctx.alpn_protocols = ALPN_PROTOCOLS
end

#tls_socket(socket) ⇒ Object

build, configure, and return TLS socket

Parameters:



352
353
354
355
356
357
358
# 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) ⇒ Object

fake exceptionless IO for writing on older ruby versions

Parameters:

  • bytes (String)


315
316
317
318
319
# File 'lib/h2/client.rb', line 315

def write_to_socket bytes
  socket.write_nonblock bytes
rescue IO::WaitWritable
  :wait_writable
end