Class: H2::Client

Inherits:
Object
  • Object
show all
Extended by:
Celluloid::ClassMethods, Concurrent::ClassMethods
Includes:
Blockable, Celluloid, Concurrent, ExceptionlessIO, 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: TCPSocket

Constant Summary collapse

PARSER_EVENTS =
[
  :close,
  :frame,
  :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, 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)

  • 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)


36
37
38
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
# File 'lib/h2/client.rb', line 36

def initialize host: nil, port: 443, url: nil, 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 = {}
  @socket  = TCPSocket.new(@host, @port)
  @socket  = tls_socket @socket if @tls
  @client  = HTTP2::Client.new

  @first   = true
  @reading = false

  init_blocking
  yield self if block_given?
  bind_events
end

Instance Attribute Details

#clientObject (readonly)

Returns the value of attribute client.



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

def client
  @client
end

#last_streamObject

Returns the value of attribute last_stream.



23
24
25
# File 'lib/h2/client.rb', line 23

def last_stream
  @last_stream
end

#readerObject (readonly)

Returns the value of attribute reader.



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

def reader
  @reader
end

#schemeObject (readonly)

Returns the value of attribute scheme.



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

def scheme
  @scheme
end

#socketObject (readonly)

Returns the value of attribute socket.



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

def socket
  @socket
end

#streamsObject (readonly)

Returns the value of attribute streams.



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

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



232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
# File 'lib/h2/client.rb', line 232

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



191
192
193
194
195
# File 'lib/h2/client.rb', line 191

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.



180
181
182
183
184
185
186
187
# File 'lib/h2/client.rb', line 180

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



111
112
113
114
115
# File 'lib/h2/client.rb', line 111

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



167
168
169
170
171
172
173
174
175
# File 'lib/h2/client.rb', line 167

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



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

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



67
68
69
# File 'lib/h2/client.rb', line 67

def closed?
  @socket.closed?
end

#create_ssl_contextObject

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



359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
# File 'lib/h2/client.rb', line 359

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)


78
79
80
# File 'lib/h2/client.rb', line 78

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



103
104
105
106
107
# File 'lib/h2/client.rb', line 103

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



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

def goaway!
  goaway block: true
end

#on_closeObject

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



274
275
276
277
# File 'lib/h2/client.rb', line 274

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)


284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
# File 'lib/h2/client.rb', line 284

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

  @first = false if @first
  read unless @first or @reading
end

#on_goaway(*args) ⇒ Object

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



320
321
322
323
# File 'lib/h2/client.rb', line 320

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



328
329
330
331
332
333
334
335
336
337
338
339
340
341
# File 'lib/h2/client.rb', line 328

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!

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



215
216
217
218
219
220
221
222
223
224
225
# File 'lib/h2/client.rb', line 215

def read maxlen = DEFAULT_MAXLEN
  main = Thread.current
  @reader = Thread.new do
    reading!
    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



264
265
266
267
268
# File 'lib/h2/client.rb', line 264

def read_from_socket maxlen
  @socket.read_nonblock maxlen
rescue IO::WaitReadable
  :wait_readable
end

#reading!Object



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

def reading!
  @mutex.synchronize { @reading = true }
end

#reading?Boolean

Returns:

  • (Boolean)


82
83
84
# File 'lib/h2/client.rb', line 82

def reading?
  @mutex.synchronize { @reading }
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:



139
140
141
142
143
144
145
146
147
148
# File 'lib/h2/client.rb', line 139

def request method:, path:, headers: {}, params: {}, body: nil, &block
  s = @client.new_stream
  stream = add_stream method: method, path: path, stream: s, &block
  add_params params, path unless params.empty?

  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



201
202
203
# File 'lib/h2/client.rb', line 201

def selector
  @selector ||= [@socket]
end

#set_ssl_context_protocols(ctx) ⇒ Object



378
379
380
# File 'lib/h2/client.rb', line 378

def set_ssl_context_protocols ctx
  ctx.alpn_protocols = ALPN_PROTOCOLS
end

#stringify_headers(hash) ⇒ Object

mutates the given hash into String keys and values

Parameters:

  • hash (Hash)

    the headers Hash to stringify



154
155
156
157
158
159
160
# File 'lib/h2/client.rb', line 154

def stringify_headers hash
  hash.keys.each do |key|
    hash[key] = hash[key].to_s unless String === hash[key]
    hash[key.to_s] = hash.delete key unless String === key
  end
  hash
end

#tls_socket(socket) ⇒ Object

build, configure, and return TLS socket

Parameters:



349
350
351
352
353
354
355
# File 'lib/h2/client.rb', line 349

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)


312
313
314
315
316
# File 'lib/h2/client.rb', line 312

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