Class: H2::Client

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

Constant Summary collapse

CONNECTION_EVENTS =
[
  :close,
  :frame,
  :goaway,
  :promise
]
ALPN_PROTOCOLS =
['h2']
DEFAULT_MAXLEN =
4096

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(addr: nil, port: nil, url: nil, tls: {}) {|_self| ... } ⇒ Client

Returns a new instance of Client.

Yields:

  • (_self)

Yield Parameters:

  • _self (H2::Client)

    the object that the method was called on

Raises:

  • (ArgumentError)


22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# File 'lib/h2/client.rb', line 22

def initialize addr: nil, port: nil, url: nil, tls: {}
  raise ArgumentError if url.nil? && (addr.nil? || port.nil?)

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

  @tls     = tls
  @streams = {}
  @socket  = TCPSocket.new(@addr, @port)
  @socket  = tls_socket @socket if @tls
  @client  = HTTP2::Client.new

  init_blocking
  yield self if block_given?
  bind_events

  read
end

Instance Attribute Details

#clientObject (readonly)

Returns the value of attribute client.



20
21
22
# File 'lib/h2/client.rb', line 20

def client
  @client
end

#last_streamObject

Returns the value of attribute last_stream.



19
20
21
# File 'lib/h2/client.rb', line 19

def last_stream
  @last_stream
end

#readerObject (readonly)

Returns the value of attribute reader.



20
21
22
# File 'lib/h2/client.rb', line 20

def reader
  @reader
end

#schemeObject (readonly)

Returns the value of attribute scheme.



20
21
22
# File 'lib/h2/client.rb', line 20

def scheme
  @scheme
end

#socketObject (readonly)

Returns the value of attribute socket.



20
21
22
# File 'lib/h2/client.rb', line 20

def socket
  @socket
end

#streamsObject (readonly)

Returns the value of attribute streams.



20
21
22
# File 'lib/h2/client.rb', line 20

def streams
  @streams
end

Instance Method Details

#_read(maxlen = DEFAULT_MAXLEN) ⇒ Object



142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
# File 'lib/h2/client.rb', line 142

def _read maxlen = DEFAULT_MAXLEN
  begin
    data = nil
    selector = [@socket]

    loop do
      data = @socket.read_nonblock maxlen, exception: false
      case data
      when :wait_readable
        IO.select selector
      when NilClass
        break
      else
        begin
          @client << data
        rescue HTTP2::Error::ProtocolError => pe
          STDERR.puts 'mystery protocol error!'
          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



123
124
125
126
127
# File 'lib/h2/client.rb', line 123

def add_params params, path
  appendage = path.index('?') ? '&' : '?'
  path << appendage
  path << URI.encode_www_form(params)
end

#add_stream(method:, path:, stream:, &block) ⇒ Object



114
115
116
117
118
119
120
121
# File 'lib/h2/client.rb', line 114

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



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

def bind_events
  CONNECTION_EVENTS.each do |e|
    @client.on(e){|*a| __send__ "on_#{e}", *a}
  end
end

#build_headers(method:, path:, headers:) ⇒ Object



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

def build_headers method:, path:, headers:
  h = {
    AUTHORITY_KEY => [@addr, @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



54
55
56
57
# File 'lib/h2/client.rb', line 54

def close
  unblock!
  @socket.close unless closed?
end

#closed?Boolean

Returns:

  • (Boolean)


50
51
52
# File 'lib/h2/client.rb', line 50

def closed?
  @socket.closed?
end

#create_ssl_contextObject

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



231
232
233
234
235
236
237
238
239
240
241
242
# File 'lib/h2/client.rb', line 231

def create_ssl_context
  ctx                = OpenSSL::SSL::SSLContext.new
  ctx.alpn_protocols = ALPN_PROTOCOLS
  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 )
  ctx
end

#eof?Boolean

Returns:

  • (Boolean)


59
60
61
# File 'lib/h2/client.rb', line 59

def eof?
  @socket.eof?
end

#goaway(block: false) ⇒ Object



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

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

#goaway!Object



63
64
65
# File 'lib/h2/client.rb', line 63

def goaway!
  goaway block: true
end

#on_closeObject




173
174
175
176
# File 'lib/h2/client.rb', line 173

def on_close
  on :close
  close
end

#on_frame(bytes) ⇒ Object



178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
# File 'lib/h2/client.rb', line 178

def on_frame bytes
  on :frame, bytes

  if ::H2::Client::TCPSocket === @socket
    total = bytes.bytesize
    loop do
      n = @socket.sendmsg_nonblock bytes, exception: false
      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_goaway(*args) ⇒ Object



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

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

#on_promise(promise) ⇒ Object



204
205
206
207
208
209
210
211
212
213
214
215
216
217
# File 'lib/h2/client.rb', line 204

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




131
132
133
134
135
136
137
138
139
140
# File 'lib/h2/client.rb', line 131

def read maxlen = DEFAULT_MAXLEN
  main = Thread.current
  @reader = Thread.new do
    begin
      _read maxlen
    rescue => e
      main.raise e
    end
  end
end

#request(method:, path:, headers: {}, params: {}, body: nil, &block) ⇒ Object



85
86
87
88
89
90
91
92
93
94
# File 'lib/h2/client.rb', line 85

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

#stringify_headers(hash) ⇒ Object



96
97
98
99
100
101
102
# File 'lib/h2/client.rb', line 96

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




221
222
223
224
225
226
227
# File 'lib/h2/client.rb', line 221

def tls_socket socket
  socket = OpenSSL::SSL::SSLSocket.new socket, create_ssl_context
  socket.sync_close = true
  socket.hostname = @addr
  socket.connect
  socket
end