h2 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: b533a3c1702ec83aad19b84c16dcb97badbc669b
4
+ data.tar.gz: e8adb1f561a47a3970d1bed94b276b26220798e5
5
+ SHA512:
6
+ metadata.gz: c948d743e1a8923c0cfc7e87564aedb6e16fbf27173859aa1fd6316e900adf05e661f223e243b2604c611685fc5280e9984c4d25a5f6053c17862af9d31db9e5
7
+ data.tar.gz: bbd51fcf78448aeb6636dc55d0cc22f188c5a44517a4887e01db8d0936efa10133d65c6637237463413224f1d79fbc2c9b2c71b01f22d5ee188ef4c98055f1ae
data/.gitignore ADDED
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
data/.travis.yml ADDED
@@ -0,0 +1,8 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.3.0
5
+ - 2.3.1
6
+ - 2.3.2
7
+ - 2.3.3
8
+ - 2.4.0
@@ -0,0 +1,74 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ In the interest of fostering an open and welcoming environment, we as
6
+ contributors and maintainers pledge to making participation in our project and
7
+ our community a harassment-free experience for everyone, regardless of age, body
8
+ size, disability, ethnicity, gender identity and expression, level of experience,
9
+ nationality, personal appearance, race, religion, or sexual identity and
10
+ orientation.
11
+
12
+ ## Our Standards
13
+
14
+ Examples of behavior that contributes to creating a positive environment
15
+ include:
16
+
17
+ * Using welcoming and inclusive language
18
+ * Being respectful of differing viewpoints and experiences
19
+ * Gracefully accepting constructive criticism
20
+ * Focusing on what is best for the community
21
+ * Showing empathy towards other community members
22
+
23
+ Examples of unacceptable behavior by participants include:
24
+
25
+ * The use of sexualized language or imagery and unwelcome sexual attention or
26
+ advances
27
+ * Trolling, insulting/derogatory comments, and personal or political attacks
28
+ * Public or private harassment
29
+ * Publishing others' private information, such as a physical or electronic
30
+ address, without explicit permission
31
+ * Other conduct which could reasonably be considered inappropriate in a
32
+ professional setting
33
+
34
+ ## Our Responsibilities
35
+
36
+ Project maintainers are responsible for clarifying the standards of acceptable
37
+ behavior and are expected to take appropriate and fair corrective action in
38
+ response to any instances of unacceptable behavior.
39
+
40
+ Project maintainers have the right and responsibility to remove, edit, or
41
+ reject comments, commits, code, wiki edits, issues, and other contributions
42
+ that are not aligned to this Code of Conduct, or to ban temporarily or
43
+ permanently any contributor for other behaviors that they deem inappropriate,
44
+ threatening, offensive, or harmful.
45
+
46
+ ## Scope
47
+
48
+ This Code of Conduct applies both within project spaces and in public spaces
49
+ when an individual is representing the project or its community. Examples of
50
+ representing a project or community include using an official project e-mail
51
+ address, posting via an official social media account, or acting as an appointed
52
+ representative at an online or offline event. Representation of a project may be
53
+ further defined and clarified by project maintainers.
54
+
55
+ ## Enforcement
56
+
57
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
58
+ reported by contacting the project team at kenichi.nakamura@gmail.com. All
59
+ complaints will be reviewed and investigated and will result in a response that
60
+ is deemed necessary and appropriate to the circumstances. The project team is
61
+ obligated to maintain confidentiality with regard to the reporter of an incident.
62
+ Further details of specific enforcement policies may be posted separately.
63
+
64
+ Project maintainers who do not follow or enforce the Code of Conduct in good
65
+ faith may face temporary or permanent repercussions as determined by other
66
+ members of the project's leadership.
67
+
68
+ ## Attribution
69
+
70
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71
+ available at [http://contributor-covenant.org/version/1/4][version]
72
+
73
+ [homepage]: http://contributor-covenant.org
74
+ [version]: http://contributor-covenant.org/version/1/4/
data/Gemfile ADDED
@@ -0,0 +1,11 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem 'http-2', git: 'https://github.com/igrigorik/http-2'
4
+
5
+ gemspec
6
+
7
+ group :development, :test do
8
+ gem 'awesome_print'
9
+ gem 'pry-byebug'
10
+ gem 'reel', require: 'reel/h2', git: 'https://github.com/kenichi/reel', branch: 'h2'
11
+ end
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2016 kenichi nakamura
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,86 @@
1
+ # H2
2
+
3
+ [![Build Status](https://travis-ci.org/kenichi/h2.png?branch=master)](https://travis-ci.org/kenichi/h2)
4
+
5
+ H2 is a basic HTTP/2 client based on the [http-2](https://github.com/igrigorik/http-2) gem.
6
+
7
+ H2 currently uses:
8
+
9
+ * one new thread per client (see [TODO](#TODO) item 3)
10
+ * keyword arguments (>=2.0)
11
+ * exception-less socket IO (>=2.3).
12
+
13
+ ## Usage
14
+
15
+ ```ruby
16
+ require 'h2'
17
+
18
+ #
19
+ # --- one-shot convenience
20
+ #
21
+
22
+ stream = H2.get url: 'https://example.com'
23
+
24
+ stream.ok? #=> true
25
+ stream.headers #=> Hash
26
+ stream.body #=> String
27
+ stream.closed? #=> true
28
+
29
+ client = stream.client #=> H2::Client
30
+
31
+ client.closed? #=> true
32
+
33
+ #
34
+ # --- normal connection
35
+ #
36
+
37
+ client = H2::Client.new addr: 'example.com', port: 443
38
+
39
+ stream = client.get path: '/'
40
+
41
+ stream.ok? #=> true
42
+ stream.headers #=> Hash, method blocks until stream is closed
43
+ stream.body #=> String, method blocks until stream is closed
44
+ stream.closed? #=> true
45
+
46
+ client.closed? #=> false unless server sent GOAWAY
47
+
48
+ stream = client.get path: '/push_promise' do |s| # H2::Stream === s
49
+ s.on :headers do |h|
50
+ if h['ETag'] == 'some_value']
51
+ s.cancel! # already have
52
+ end
53
+ end
54
+ end
55
+
56
+ stream.block! # blocks until this stream and any associated push streams are closed
57
+
58
+ stream.ok? #=> true
59
+ stream.headers #=> Hash
60
+ stream.body #=> String
61
+ stream.closed? #=> true
62
+
63
+ stream.pushes #=> Set
64
+ stream.pushes.each do |pp|
65
+ pp.parent == stream #=> true
66
+ pp.headers #=> Hash
67
+ pp.body #=> String
68
+ end
69
+
70
+ client.goaway!
71
+ ```
72
+
73
+ ## TODO
74
+
75
+ * [x] HTTPS / TLS
76
+ * [ ] push promise cancellation
77
+ * [ ] alternate concurrency models
78
+
79
+ ## Contributing
80
+
81
+ Bug reports and pull requests are welcome on GitHub at https://github.com/kenichi/h2. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
82
+
83
+
84
+ ## License
85
+
86
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,21 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ task default: :test
5
+
6
+ Rake::TestTask.new :test do |t|
7
+ t.test_files = FileList['test/**/*_test.rb']
8
+ end
9
+
10
+ namespace :test do
11
+
12
+ desc 'send TTIN signal to test process'
13
+ task :ttin do
14
+ pid = `ps -ef | grep -v grep | grep -e 'ruby.*_test\.rb' | awk '{print $2}'`.strip
15
+ if !pid.empty?
16
+ puts "TTIN -> #{pid}"
17
+ Process.kill 'TTIN', Integer(pid)
18
+ end
19
+ end
20
+
21
+ end
data/bin/console ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler/setup'
4
+ require 'h2'
5
+ require 'irb'
6
+
7
+ Bundler.require :development
8
+ IRB.start
data/exe/h2 ADDED
@@ -0,0 +1,134 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # h2 CLI
5
+ #
6
+ # ---
7
+
8
+ require 'colored'
9
+ require 'optparse'
10
+
11
+ $: << File.expand_path('../../../http-2/lib', __FILE__)
12
+
13
+ begin # {{{
14
+ require 'h2'
15
+ rescue LoadError
16
+ $: << File.expand_path('../../lib', __FILE__)
17
+ require 'h2'
18
+ end # }}}
19
+
20
+ # --- parse options {{{
21
+
22
+ options = {
23
+ body: nil,
24
+ block: false,
25
+ headers: {},
26
+ goaway: false,
27
+ method: nil,
28
+ tls: {},
29
+ verbose: false
30
+ }
31
+
32
+ OptionParser.new do |o|
33
+
34
+ o.banner = 'Usage: h2 [options] URL'
35
+
36
+ o.on '-b', '--block', 'block until server closes connection' do
37
+ options[:block] = true
38
+ end
39
+
40
+ o.on '--cafile [FILE]', String, 'certificate authority bundle' do |ca|
41
+ raise ArgumentError, "invalid CA file: #{ca}" unless ca && File.exist?(ca)
42
+ options[:tls][:ca_file] = ca
43
+ end
44
+
45
+ o.on '-d', '--data [DATA]', String, 'post body data' do |d|
46
+ options[:method] = :post if options[:method].nil?
47
+ options[:body] = d
48
+ end
49
+
50
+ o.on '--debug', 'debug output' do
51
+ options[:debug] = true
52
+ end
53
+
54
+ o.on '-g', '--goaway', 'send GOAWAY frame when stream is complete' do
55
+ options[:debug] = true
56
+ end
57
+
58
+ o.on '-v', '--verbose', 'turn on verbosity' do
59
+ options[:verbose] = true
60
+ end
61
+
62
+ o.on '-X', '--request [METHOD]', String, 'request method' do |m|
63
+ meth = m.downcase.to_sym
64
+ raise ArgumentError, "invalid method: #{m}" unless H2::REQUEST_METHODS.include? meth
65
+ options[:method] = meth
66
+ end
67
+
68
+ end.parse!
69
+
70
+ options[:method] ||= :get
71
+ options[:tls][:ca_file] ||= ENV['H2_CAFILE'] if ENV['H2_CAFILE']
72
+
73
+ # }}}
74
+
75
+ # --- parse URL {{{
76
+
77
+ url = ARGV[0]
78
+ url = 'https://' + url unless url.start_with? 'http'
79
+ url = URI.parse url
80
+
81
+ # }}}
82
+
83
+ # --- build & connect client {{{
84
+
85
+ client = {
86
+ url: url
87
+ }
88
+
89
+ client[:tls] = options[:tls] unless options[:tls].empty?
90
+
91
+ c = H2::Client.new **client do |c|
92
+ if options[:debug]
93
+ c.client.on(:frame_received) {|f| puts "<< #{f.inspect}".yellow}
94
+ c.client.on(:frame_sent) {|f| puts ">> #{f.inspect}".green}
95
+ end
96
+ end
97
+
98
+ # }}}
99
+
100
+ # --- build & send request {{{
101
+
102
+ request = {
103
+ body: options[:body],
104
+ headers: options[:headers],
105
+ path: url.request_uri
106
+ }
107
+
108
+ if options[:verbose]
109
+ c.build_headers(
110
+ method: options[:method],
111
+ path: url.request_uri,
112
+ headers: request[:headers]
113
+ ).each {|k,v| puts ">> #{k}: #{v}".green}
114
+ end
115
+
116
+ s = c.__send__ options[:method], **request
117
+
118
+ # }}}
119
+
120
+ # --- print response & close {{{
121
+
122
+ if options[:verbose]
123
+ s.headers.each {|k,v| puts "<< #{k}: #{v}".yellow}
124
+ end
125
+
126
+ puts s.body
127
+
128
+ c.goaway if options[:goaway]
129
+ c.block! if options[:block]
130
+ c.close
131
+
132
+ # }}}
133
+
134
+ # vim: ft=ruby fdm=marker fdl=0
data/h2.gemspec ADDED
@@ -0,0 +1,28 @@
1
+ # coding: utf-8
2
+ require_relative './lib/h2/version'
3
+
4
+ IGNORE = %w[ test/ spec/ features/ .gitignore .travis.yml ]
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "h2"
8
+ spec.version = H2::VERSION
9
+ spec.authors = ["Kenichi Nakamura"]
10
+ spec.email = ["kenichi.nakamura@gmail.com"]
11
+ spec.summary = 'an http/2 client based on http-2 and modern ruby'
12
+ spec.description = 'a pure ruby http/2 client based on http-2 for ruby 2.3 and above'
13
+ spec.homepage = 'https://github.com/kenichi/h2'
14
+ spec.license = 'MIT'
15
+ spec.bindir = 'exe'
16
+ spec.executables = ['h2']
17
+ spec.require_paths = ['lib']
18
+ spec.files = `git ls-files`.split.reject {|f| f.start_with? 'test', 'spec', 'features'}
19
+
20
+ spec.required_ruby_version = '>= 2.3'
21
+
22
+ spec.add_dependency 'http-2', '~> 0.8', '>= 0.8.2'
23
+ spec.add_dependency 'colored', '1.2'
24
+
25
+ spec.add_development_dependency "bundler", "~> 1.13"
26
+ spec.add_development_dependency "rake", "~> 10.0"
27
+ spec.add_development_dependency "minitest", "~> 5.0"
28
+ end
data/lib/h2.rb ADDED
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'http/2'
4
+ require 'uri'
5
+
6
+ module H2
7
+
8
+ # http/2 psuedo-headers
9
+ #
10
+ AUTHORITY_KEY = ':authority'
11
+ METHOD_KEY = ':method'
12
+ PATH_KEY = ':path'
13
+ SCHEME_KEY = ':scheme'
14
+ STATUS_KEY = ':status'
15
+
16
+ REQUEST_METHODS = [
17
+ :get,
18
+ :delete,
19
+ :head,
20
+ :options,
21
+ :patch,
22
+ :post,
23
+ :put
24
+ ]
25
+
26
+ class << self
27
+
28
+ REQUEST_METHODS.each do |m|
29
+ define_method m do |**args, &block|
30
+ request method: m, **args, &block
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ def request addr: nil,
37
+ port: nil,
38
+ method:,
39
+ path: '/',
40
+ headers: {},
41
+ params: {},
42
+ body: nil,
43
+ url: nil,
44
+ tls: {},
45
+ &block
46
+
47
+ raise ArgumentError if url.nil? && (addr.nil? || port.nil?)
48
+ if url
49
+ url = URI.parse url unless URI === url
50
+ addr = url.host
51
+ port = url.port
52
+ path = url.request_uri
53
+ end
54
+ c = Client.new addr: addr, port: port, tls: tls
55
+ c.__send__ method, path: path, headers: headers, params: params, body: body, &block
56
+ end
57
+ end
58
+
59
+ module Blockable
60
+
61
+ def init_blocking
62
+ @mutex = Mutex.new
63
+ @condition = ConditionVariable.new
64
+ end
65
+
66
+ def block! timeout = nil
67
+ @mutex.synchronize { @condition.wait @mutex, timeout } if @condition
68
+ end
69
+
70
+ def unblock!
71
+ return unless @condition
72
+ @mutex.synchronize do
73
+ @condition.broadcast
74
+ @condition = nil
75
+ end
76
+ end
77
+
78
+ end
79
+
80
+ module On
81
+
82
+ def on event, *args, &block
83
+ @on ||= {}
84
+ return @on[event]&.call(*args) unless block_given?
85
+ return @on[event] = block if block_given?
86
+ self
87
+ end
88
+
89
+ end
90
+
91
+ end
92
+
93
+ require 'h2/client'
94
+ require 'h2/stream'
95
+ require 'h2/version'
data/lib/h2/client.rb ADDED
@@ -0,0 +1,257 @@
1
+ require 'openssl'
2
+ require 'h2/client/tcp_socket'
3
+
4
+ module H2
5
+ class Client
6
+ include Blockable
7
+ include On
8
+
9
+ CONNECTION_EVENTS = [
10
+ :close,
11
+ :frame,
12
+ :goaway,
13
+ :promise
14
+ ]
15
+
16
+ PROMISE_EVENTS = [
17
+ :headers,
18
+ :data
19
+ ]
20
+
21
+ ALPN_PROTOCOLS = ['h2']
22
+ DEFAULT_MAXLEN = 4096
23
+
24
+ attr_accessor :last_stream
25
+ attr_reader :client, :reader, :scheme, :socket, :streams
26
+
27
+ def initialize addr: nil, port: nil, url: nil, tls: {}
28
+ raise ArgumentError if url.nil? && (addr.nil? || port.nil?)
29
+
30
+ if url
31
+ url = URI.parse url unless URI === url
32
+ @addr = url.host
33
+ @port = url.port
34
+ @scheme = url.scheme
35
+ tls = false if 'http' == @scheme
36
+ else
37
+ @addr = addr
38
+ @port = port
39
+ @scheme = tls ? 'https' : 'http'
40
+ end
41
+
42
+ @tls = tls
43
+ @streams = {}
44
+ @socket = TCPSocket.new(@addr, @port)
45
+ @socket = tls_socket @socket if @tls
46
+ @client = HTTP2::Client.new
47
+
48
+ init_blocking
49
+ yield self if block_given?
50
+ bind_events
51
+
52
+ read
53
+ end
54
+
55
+ def closed?
56
+ @socket.closed?
57
+ end
58
+
59
+ def close
60
+ unblock!
61
+ @socket.close unless closed?
62
+ end
63
+
64
+ def eof?
65
+ @socket.eof?
66
+ end
67
+
68
+ def goaway!
69
+ goaway block: true
70
+ end
71
+
72
+ def goaway block: false
73
+ return false if closed?
74
+ @client.goaway
75
+ block! if block
76
+ end
77
+
78
+ def bind_events
79
+ CONNECTION_EVENTS.each do |e|
80
+ @client.on(e){|*a| __send__ "on_#{e}", *a}
81
+ end
82
+ end
83
+
84
+ REQUEST_METHODS.each do |m|
85
+ define_method m do |**args, &block|
86
+ request method: m, **args, &block
87
+ end
88
+ end
89
+
90
+ def request method:, path:, headers: {}, params: {}, body: nil, &block
91
+ s = @client.new_stream
92
+ stream = add_stream method: method, path: path, stream: s, &block
93
+ add_params params, path unless params.empty?
94
+
95
+ h = build_headers method: method, path: path, headers: headers
96
+ s.headers h, end_stream: body.nil?
97
+ s.data body if body
98
+ stream
99
+ end
100
+
101
+ def stringify_headers hash
102
+ hash.keys.each do |key|
103
+ hash[key] = hash[key].to_s unless String === hash[key]
104
+ hash[key.to_s] = hash.delete key unless String === key
105
+ end
106
+ hash
107
+ end
108
+
109
+ def build_headers method:, path:, headers:
110
+ h = {
111
+ AUTHORITY_KEY => [@addr, @port.to_s].join(':'),
112
+ METHOD_KEY => method.to_s.upcase,
113
+ PATH_KEY => path,
114
+ SCHEME_KEY => @scheme
115
+ }
116
+ h.merge! stringify_headers(headers)
117
+ end
118
+
119
+ def add_stream method:, path:, stream:, &block
120
+ stream_id = stream.id
121
+ @streams[method] ||= {}
122
+ @streams[method][path] ||= []
123
+ stream = Stream.new client: self, stream: stream, &block unless Stream === stream
124
+ @streams[method][path] << stream
125
+ @streams[stream_id] = stream
126
+ stream
127
+ end
128
+
129
+ def add_params params, path
130
+ appendage = path.index('?') ? '&' : '?'
131
+ path << appendage
132
+ path << URI.encode_www_form(params)
133
+ end
134
+
135
+ # ---
136
+
137
+ def read maxlen = DEFAULT_MAXLEN
138
+ main = Thread.current
139
+ @reader = Thread.new do
140
+ begin
141
+ _read maxlen
142
+ rescue => e
143
+ main.raise e
144
+ end
145
+ end
146
+ rescue => e
147
+ STDERR.puts "#{e.message} - closing socket"
148
+ STDERR.puts e.backtrace.map {|l| "\t" + l}
149
+ close
150
+ ensure
151
+ unblock!
152
+ end
153
+
154
+ def _read maxlen = DEFAULT_MAXLEN
155
+ begin
156
+ data = nil
157
+ selector = [@socket]
158
+
159
+ loop do
160
+ data = @socket.read_nonblock maxlen, exception: false
161
+ case data
162
+ when :wait_readable
163
+ IO.select selector
164
+ when NilClass
165
+ break
166
+ else
167
+ begin
168
+ @client << data
169
+ rescue HTTP2::Error::ProtocolError => pe
170
+ STDERR.puts 'mystery protocol error!'
171
+ STDERR.puts pe.backtrace.map {|l| "\t" + l}
172
+ end
173
+ end
174
+ end
175
+
176
+ rescue IOError, Errno::EBADF
177
+ close
178
+ ensure
179
+ unblock!
180
+ end
181
+ end
182
+
183
+ # ---
184
+
185
+ def on_close
186
+ on :close
187
+ close
188
+ end
189
+
190
+ def on_frame bytes
191
+ on :frame, bytes
192
+
193
+ if ::H2::Client::TCPSocket === @socket
194
+ total = bytes.bytesize
195
+ loop do
196
+ n = @socket.sendmsg_nonblock bytes, exception: false
197
+ if n == :wait_writable
198
+ IO.select nil, @socket.selector
199
+ elsif n < total
200
+ bytes = bytes.byteslice n, total
201
+ else
202
+ break
203
+ end
204
+ end
205
+ else
206
+ @socket.write bytes
207
+ end
208
+ @socket.flush
209
+ end
210
+
211
+ def on_goaway *args
212
+ on :goaway, *args
213
+ close
214
+ end
215
+
216
+ def on_promise promise
217
+ on :promise, promise
218
+
219
+ Stream.new client: self,
220
+ parent: @streams[promise.parent.id],
221
+ push: true,
222
+ stream: promise do |p|
223
+ p.on :close do
224
+ method = p.headers[METHOD_KEY].downcase.to_sym rescue :error
225
+ path = p.headers[PATH_KEY]
226
+ add_stream method: method, path: path, stream: p
227
+ end
228
+ end
229
+ end
230
+
231
+ # ---
232
+
233
+ def tls_socket socket
234
+ socket = OpenSSL::SSL::SSLSocket.new socket, create_ssl_context
235
+ socket.sync_close = true
236
+ socket.hostname = @addr
237
+ socket.connect
238
+ socket
239
+ end
240
+
241
+ # builds a new SSLContext suitable for use in 'h2' connections
242
+ #
243
+ def create_ssl_context
244
+ ctx = OpenSSL::SSL::SSLContext.new
245
+ ctx.alpn_protocols = ALPN_PROTOCOLS
246
+ ctx.ca_file = @tls[:ca_file] if @tls[:ca_file]
247
+ ctx.ca_path = @tls[:ca_path] if @tls[:ca_path]
248
+ ctx.ciphers = @tls[:ciphers] || OpenSSL::SSL::SSLContext::DEFAULT_PARAMS[:ciphers]
249
+ ctx.options = @tls[:options] || OpenSSL::SSL::SSLContext::DEFAULT_PARAMS[:options]
250
+ ctx.ssl_version = :TLSv1_2
251
+ ctx.verify_mode = @tls[:verify_mode] || ( OpenSSL::SSL::VERIFY_PEER |
252
+ OpenSSL::SSL::VERIFY_FAIL_IF_NO_PEER_CERT )
253
+ ctx
254
+ end
255
+
256
+ end
257
+ end
@@ -0,0 +1,45 @@
1
+ require 'socket'
2
+
3
+ module H2
4
+ class Client
5
+ class TCPSocket < ::Socket
6
+
7
+ DEFAULT_TIMEOUT = 10
8
+ # ON_LINUX = !!(RUBY_PLATFORM =~ /linux/)
9
+
10
+ attr_reader :selector
11
+
12
+ def initialize addr, port, timeout = DEFAULT_TIMEOUT
13
+
14
+ # resolve name & pack addr
15
+ family, addr = Socket.getaddrinfo(addr, port, nil, :STREAM, nil, AI_ALL).first.values_at(0,3)
16
+ sockaddr = Socket.sockaddr_in port, addr
17
+
18
+ super family, SOCK_STREAM
19
+
20
+ # allow send before ack
21
+ setsockopt IPPROTO_TCP, TCP_NODELAY, 1
22
+
23
+ # cork on linux
24
+ # setsockopt IPPROTO_TCP, TCP_CORK, 1 if ON_LINUX
25
+
26
+ if connect_nonblock(sockaddr, exception: false) == :wait_writable
27
+ if IO.select nil, [self], nil, timeout
28
+ begin
29
+ connect_nonblock sockaddr
30
+ rescue Errno::EISCONN
31
+ rescue
32
+ close
33
+ raise
34
+ end
35
+ else
36
+ close
37
+ raise Errno::ETIMEDOUT
38
+ end
39
+ end
40
+
41
+ end
42
+
43
+ end
44
+ end
45
+ end
data/lib/h2/stream.rb ADDED
@@ -0,0 +1,97 @@
1
+ require 'set'
2
+
3
+ module H2
4
+ class Stream
5
+ include Blockable
6
+ include On
7
+
8
+ STREAM_EVENTS = [
9
+ :close,
10
+ :headers,
11
+ :data
12
+ ]
13
+
14
+ attr_reader :body, :client, :headers, :parent, :pushes, :stream
15
+
16
+ def initialize client:, stream:, push: false, parent: nil
17
+ @body = ''
18
+ @client = client
19
+ @closed = false
20
+ @headers = {}
21
+ @parent = parent
22
+ @push = push
23
+ @pushes = Set.new
24
+ @stream = stream
25
+
26
+ init_blocking
27
+ yield self if block_given?
28
+ bind_events
29
+ end
30
+
31
+ def id
32
+ @stream.id
33
+ end
34
+
35
+ def ok?
36
+ headers[STATUS_KEY] == '200'
37
+ end
38
+
39
+ def closed?
40
+ @closed
41
+ end
42
+
43
+ def push?
44
+ @push
45
+ end
46
+
47
+ def add_push stream
48
+ @pushes << stream
49
+ end
50
+
51
+ def cancel!
52
+ unblock!
53
+ @stream.cancel
54
+ end
55
+
56
+ def block! timeout = nil
57
+ @pushes.each {|p| p.block! timeout}
58
+ super
59
+ end
60
+
61
+ def headers
62
+ block!
63
+ @headers
64
+ end
65
+
66
+ def body
67
+ block!
68
+ @body
69
+ end
70
+
71
+ def bind_events
72
+ @stream.on(:close) do
73
+ @parent.add_push self if @parent && push?
74
+ @client.last_stream = self
75
+ @closed = true
76
+ unblock!
77
+ on :close
78
+ end
79
+
80
+ @stream.on(:headers) do |h|
81
+ h = Hash[h]
82
+ on :headers, h
83
+ @headers.merge! h
84
+ end
85
+
86
+ @stream.on(:data) do |d|
87
+ on :data, d
88
+ @body << d
89
+ end
90
+ end
91
+
92
+ def to_h
93
+ { headers: headers, body: body }
94
+ end
95
+
96
+ end
97
+ end
data/lib/h2/version.rb ADDED
@@ -0,0 +1,3 @@
1
+ module H2
2
+ VERSION = '0.1.0'
3
+ end
metadata ADDED
@@ -0,0 +1,136 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: h2
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Kenichi Nakamura
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2016-12-31 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: http-2
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '0.8'
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: 0.8.2
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - "~>"
28
+ - !ruby/object:Gem::Version
29
+ version: '0.8'
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: 0.8.2
33
+ - !ruby/object:Gem::Dependency
34
+ name: colored
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - '='
38
+ - !ruby/object:Gem::Version
39
+ version: '1.2'
40
+ type: :runtime
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - '='
45
+ - !ruby/object:Gem::Version
46
+ version: '1.2'
47
+ - !ruby/object:Gem::Dependency
48
+ name: bundler
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '1.13'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '1.13'
61
+ - !ruby/object:Gem::Dependency
62
+ name: rake
63
+ requirement: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '10.0'
68
+ type: :development
69
+ prerelease: false
70
+ version_requirements: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '10.0'
75
+ - !ruby/object:Gem::Dependency
76
+ name: minitest
77
+ requirement: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '5.0'
82
+ type: :development
83
+ prerelease: false
84
+ version_requirements: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: '5.0'
89
+ description: a pure ruby http/2 client based on http-2 for ruby 2.3 and above
90
+ email:
91
+ - kenichi.nakamura@gmail.com
92
+ executables:
93
+ - h2
94
+ extensions: []
95
+ extra_rdoc_files: []
96
+ files:
97
+ - ".gitignore"
98
+ - ".travis.yml"
99
+ - CODE_OF_CONDUCT.md
100
+ - Gemfile
101
+ - LICENSE.txt
102
+ - README.md
103
+ - Rakefile
104
+ - bin/console
105
+ - exe/h2
106
+ - h2.gemspec
107
+ - lib/h2.rb
108
+ - lib/h2/client.rb
109
+ - lib/h2/client/tcp_socket.rb
110
+ - lib/h2/stream.rb
111
+ - lib/h2/version.rb
112
+ homepage: https://github.com/kenichi/h2
113
+ licenses:
114
+ - MIT
115
+ metadata: {}
116
+ post_install_message:
117
+ rdoc_options: []
118
+ require_paths:
119
+ - lib
120
+ required_ruby_version: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '2.3'
125
+ required_rubygems_version: !ruby/object:Gem::Requirement
126
+ requirements:
127
+ - - ">="
128
+ - !ruby/object:Gem::Version
129
+ version: '0'
130
+ requirements: []
131
+ rubyforge_project:
132
+ rubygems_version: 2.6.8
133
+ signing_key:
134
+ specification_version: 4
135
+ summary: an http/2 client based on http-2 and modern ruby
136
+ test_files: []