h2 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/.travis.yml +8 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +11 -0
- data/LICENSE.txt +21 -0
- data/README.md +86 -0
- data/Rakefile +21 -0
- data/bin/console +8 -0
- data/exe/h2 +134 -0
- data/h2.gemspec +28 -0
- data/lib/h2.rb +95 -0
- data/lib/h2/client.rb +257 -0
- data/lib/h2/client/tcp_socket.rb +45 -0
- data/lib/h2/stream.rb +97 -0
- data/lib/h2/version.rb +3 -0
- metadata +136 -0
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
data/.travis.yml
ADDED
data/CODE_OF_CONDUCT.md
ADDED
@@ -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
|
+
[](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
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
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: []
|