plum 0.1.3 → 0.2.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.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +84 -12
  3. data/circle.yml +27 -0
  4. data/examples/client/large.rb +20 -0
  5. data/examples/client/twitter.rb +51 -0
  6. data/examples/non_tls_server.rb +15 -9
  7. data/examples/static_server.rb +30 -23
  8. data/lib/plum.rb +9 -2
  9. data/lib/plum/client.rb +198 -0
  10. data/lib/plum/client/client_session.rb +91 -0
  11. data/lib/plum/client/connection.rb +19 -0
  12. data/lib/plum/client/legacy_client_session.rb +118 -0
  13. data/lib/plum/client/response.rb +100 -0
  14. data/lib/plum/client/upgrade_client_session.rb +46 -0
  15. data/lib/plum/connection.rb +58 -65
  16. data/lib/plum/connection_utils.rb +1 -1
  17. data/lib/plum/errors.rb +7 -3
  18. data/lib/plum/flow_control.rb +3 -3
  19. data/lib/plum/rack/listener.rb +3 -3
  20. data/lib/plum/rack/server.rb +1 -0
  21. data/lib/plum/rack/session.rb +5 -2
  22. data/lib/plum/server/connection.rb +42 -0
  23. data/lib/plum/{http_connection.rb → server/http_connection.rb} +7 -14
  24. data/lib/plum/{https_connection.rb → server/https_connection.rb} +2 -9
  25. data/lib/plum/stream.rb +54 -24
  26. data/lib/plum/stream_utils.rb +0 -12
  27. data/lib/plum/version.rb +1 -1
  28. data/plum.gemspec +2 -2
  29. data/test/plum/client/test_client.rb +152 -0
  30. data/test/plum/client/test_connection.rb +11 -0
  31. data/test/plum/client/test_legacy_client_session.rb +90 -0
  32. data/test/plum/client/test_response.rb +74 -0
  33. data/test/plum/client/test_upgrade_client_session.rb +45 -0
  34. data/test/plum/connection/test_handle_frame.rb +4 -1
  35. data/test/plum/{test_http_connection.rb → server/test_http_connection.rb} +4 -4
  36. data/test/plum/{test_https_connection.rb → server/test_https_connection.rb} +14 -8
  37. data/test/plum/test_connection.rb +9 -2
  38. data/test/plum/test_connection_utils.rb +9 -0
  39. data/test/plum/test_error.rb +1 -2
  40. data/test/plum/test_frame_factory.rb +37 -0
  41. data/test/plum/test_stream.rb +24 -4
  42. data/test/plum/test_stream_utils.rb +0 -1
  43. data/test/test_helper.rb +5 -2
  44. data/test/utils/assertions.rb +9 -9
  45. data/test/utils/client.rb +19 -0
  46. data/test/utils/server.rb +6 -6
  47. data/test/utils/string_socket.rb +15 -0
  48. metadata +36 -12
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: df4cac7083913b918a193ac157d4fd88bd1c0022
4
- data.tar.gz: ef49c0a591dbcb1fa671b9f206705531f006e025
3
+ metadata.gz: 4194e9fa59a660fc839ea356770a05a627dba684
4
+ data.tar.gz: 5b4f22bd734f7d7df5dca01667716ecc3dc4e51e
5
5
  SHA512:
6
- metadata.gz: 635b191791c1f6465d0a8e8f3d84c763506f9449c5e685c3ce4c80ed1b5bd22f1fc906d0f1f4c74a47e52e1d44dbea6245282575f88eff61af784fae8be1dbb2
7
- data.tar.gz: 30cdfdd10ff68e47521f6d5928f920300706f9722bf32002844c18b2ced3d7215e2a24165a37abdcd9a53009173d5efc578ef5ddee61a766c2ca90af60cc9707
6
+ metadata.gz: 987380963b7702bd5a6aa0f4ab817ae515f34cbd1b74ba48ea2b46f5e27169173c80821af49f353bc61363a634107290bc4a43abe599a21707cbaadbc6446f0f
7
+ data.tar.gz: bd645808edbcf763c896e00085ab3d69d89639f44b1de09c3ac39f7ed2c79d6c5f313f2791dad57c43d31d68eeb9b59e745ba3e4040835e5670543f9284c905e
data/README.md CHANGED
@@ -1,5 +1,9 @@
1
- # Plum [![Build Status](https://travis-ci.org/rhenium/plum.png?branch=master)](https://travis-ci.org/rhenium/plum) [![Code Climate](https://codeclimate.com/github/rhenium/plum/badges/gpa.svg)](https://codeclimate.com/github/rhenium/plum) [![Test Coverage](https://codeclimate.com/github/rhenium/plum/badges/coverage.svg)](https://codeclimate.com/github/rhenium/plum/coverage)
2
- A minimal pure Ruby implementation of HTTP/2 library / server.
1
+ # Plum: An HTTP/2 Library for Ruby
2
+ A pure Ruby HTTP/2 server and client implementation.
3
+
4
+ WARNING: Plum is currently under heavy development. You *will* encounter bugs when using it.
5
+
6
+ [![Circle CI](https://circleci.com/gh/rhenium/plum.svg?style=svg)](https://circleci.com/gh/rhenium/plum) [![Build Status](https://travis-ci.org/rhenium/plum.png?branch=master)](https://travis-ci.org/rhenium/plum) [![Code Climate](https://codeclimate.com/github/rhenium/plum/badges/gpa.svg)](https://codeclimate.com/github/rhenium/plum) [![Test Coverage](https://codeclimate.com/github/rhenium/plum/badges/coverage.svg)](https://codeclimate.com/github/rhenium/plum/coverage)
3
7
 
4
8
  ## Requirements
5
9
  * Ruby
@@ -7,18 +11,21 @@ A minimal pure Ruby implementation of HTTP/2 library / server.
7
11
  * or latest Ruby 2.3.0-dev
8
12
  * OpenSSL 1.0.2 or newer (HTTP/2 requires ALPN)
9
13
  * Optional:
10
- * [http-parser.rb gem](https://rubygems.org/gems/http_parser.rb) (HTTP/1.1 parser; if you use "http" URI scheme)
11
- * [rack gem](https://rubygems.org/gems/rack) if you use Plum as Rack server.
14
+ * [http_parser.rb gem](https://rubygems.org/gems/http_parser.rb) (HTTP/1.x parser; if you use "http" URI scheme)
15
+ * [rack gem](https://rubygems.org/gems/rack) (if you use Plum as Rack server)
16
+
17
+ ## Installation
18
+ ```sh
19
+ gem install plum
20
+ ```
12
21
 
13
22
  ## Usage
14
- ### As a library
15
- * See documentation: http://www.rubydoc.info/gems/plum
16
- * See examples in `examples/`
17
- * [rhenium/plum-server](https://github.com/rhenium/plum-server) - A static-file server for https://rhe.jp and http://rhe.jp.
23
+ * Documentation: http://www.rubydoc.info/gems/plum
24
+ * Some examples are in `examples/`
18
25
 
19
26
  ### As a Rack-compatible server
20
27
 
21
- Most existing Rack-based applications (plum doesn't support Rack hijack API) should work without modification.
28
+ Most existing Rack-based applications should work without modification.
22
29
 
23
30
  ```ruby
24
31
  # config.ru
@@ -26,7 +33,7 @@ App = -> env {
26
33
  [
27
34
  200,
28
35
  { "Content-Type" => "text/plain" },
29
- [" request: #{env["REQUEST_METHOD"]} #{env["PATH_INFO"]}"]
36
+ ["request: #{env["REQUEST_METHOD"]} #{env["PATH_INFO"]}"]
30
37
  ]
31
38
  }
32
39
 
@@ -36,13 +43,78 @@ run App
36
43
  You can run it:
37
44
 
38
45
  ```sh
39
- % plum -e production -p 8080 --https config.ru
46
+ % plum -e production -p 8080 --https --cert server.crt --key server.key config.ru
40
47
  ```
41
48
 
42
- By default, Plum generates a dummy server certificate if `--cert` and `--key` options are not specified.
49
+ NOTE: If `--cert` and `--key` are omitted, a temporary dummy certificate will be generated.
50
+
51
+ ### As a HTTP/2 (HTTP/1.x) client library
52
+ If the server does't support HTTP/2, `Plum::Client` tries to use HTTP/1.x instead.
53
+
54
+ ```
55
+ +-----------------+
56
+ |:http2 option | false
57
+ |(default: true) |-------> HTTP/1.x
58
+ +-----------------+
59
+ v true
60
+ +-----------------+
61
+ |:scheme option | "http"
62
+ |(default:"https")|-------> Try Upgrade from HTTP/1.1
63
+ +-----------------+
64
+ v "https"
65
+ +-----------------+
66
+ | ALPN(/NPN) | failed
67
+ | negotiation |-------> HTTP/1.x
68
+ +-----------------+
69
+ | "h2"
70
+ v
71
+ HTTP/2
72
+ ```
73
+
74
+ ##### Sequential request
75
+ ```ruby
76
+ client = Plum::Client.start("http2.rhe.jp", 443, user_agent: "nyaan")
77
+ res1 = client.get!("/", headers: { "accept" => "*/*" })
78
+ puts res1.body # => "..."
79
+ res2 = client.post!("/post", "data")
80
+ puts res2.body # => "..."
81
+
82
+ client.close
83
+ ```
84
+
85
+ ##### Parallel request
86
+ ```ruby
87
+ res1 = res2 = nil
88
+ Plum::Client.start("rhe.jp", 443, http2_settings: { max_frame_size: 32768 }) { |client|
89
+ res1 = client.get("/")
90
+ res2 = client.post("/post", "data")
91
+ # res1.status == nil ; because it's async request
92
+ } # wait for response(s) and close
93
+
94
+ p res1.status # => "200"
95
+ ```
96
+
97
+ ##### Download a large file
98
+ ```ruby
99
+ Plum::Client.start("http2.rhe.jp", 443, hostname: "assets.rhe.jp") { |client|
100
+ client.get("/large") do |res| # called when received response headers
101
+ p res.status # => "200"
102
+ File.open("/tmp/large.file", "wb") { |file|
103
+ res.on_chunk do |chunk| # called when each chunk of response body arrived
104
+ file << chunk
105
+ end
106
+ }
107
+ end
108
+ }
109
+ ```
43
110
 
44
111
  ## TODO
45
112
  * **Better API**
113
+ * Plum::Client
114
+ * PING frame handling
115
+ * Server Push support
116
+ * Stream Priority support
117
+ * Better HTTP/1.x support
46
118
 
47
119
  ## License
48
120
  MIT License
@@ -0,0 +1,27 @@
1
+ machine:
2
+ environment:
3
+ CODECLIMATE_REPO_TOKEN: f5092ab344fac7f2de9d7332e00597642a4d24e3d560f7d7f329172a2e5a2def
4
+ dependencies:
5
+ pre:
6
+ - echo openssl_url=https://www.openssl.org/source >> $rvm_path/user/db
7
+ - echo openssl_version=1.0.2d >> $rvm_path/user/db
8
+ - rvm pkg install openssl
9
+ - >
10
+ case $CIRCLE_NODE_INDEX in
11
+ 0)
12
+ rvm install 2.2.3-alpn --with-openssl-dir=$rvm_path/usr --patch https://gist.githubusercontent.com/rhenium/b1711edcc903e8887a51/raw/2309e469f5a3ba15917d804ac61b19e62b3d8faf/ruby-openssl-alpn-no-tests-and-docs.patch
13
+ rvm use 2.2.3-alpn --default
14
+ ;;
15
+ 1)
16
+ rvm install ruby-head --with-openssl-dir=$rvm_path/usr
17
+ rvm use ruby-head --default
18
+ ;;
19
+ esac
20
+ override:
21
+ - gem install bundler
22
+ - bundle install
23
+ test:
24
+ override:
25
+ - $rvm_path/usr/bin/openssl version
26
+ - ruby -v
27
+ - bundle exec rake test
@@ -0,0 +1,20 @@
1
+ # -*- frozen-string-literal: true -*-
2
+ # client/large.rb: download 3 large files in parallel
3
+ $LOAD_PATH.unshift File.expand_path("../../../lib", __FILE__)
4
+ require "plum"
5
+
6
+ def log(s)
7
+ puts "[#{Time.now.strftime("%Y/%m/%d %H:%M:%S.%L")}] #{s}"
8
+ end
9
+
10
+ Plum::Client.start("http2.golang.org", 443, http2_settings: { max_frame_size: 32768 }) { |rest|
11
+ 3.times { |i|
12
+ rest.get("/file/go.src.tar.gz",
13
+ headers: { "accept-encoding" => "identity;q=1" }) { |res|
14
+ log "#{i}: #{res.status}"
15
+ res.on_chunk { |chunk|
16
+ log "#{i}: chunk: #{chunk.size}"
17
+ }
18
+ }
19
+ }
20
+ }
@@ -0,0 +1,51 @@
1
+ # -*- frozen-string-literal: true -*-
2
+ # client/twitter.rb
3
+ # Twitter の User stream に(現在はサーバーが非対応のため)HTTP/1.1 を使用して接続する。
4
+ # 「にゃーん」を含むツイートを受信したら、REST API で HTTP/2 を使用して返信する。
5
+ $LOAD_PATH.unshift File.expand_path("../../../lib", __FILE__)
6
+ require "plum"
7
+ require "json"
8
+ require "cgi"
9
+ require "simple_oauth"
10
+
11
+ credentials = { consumer_key: "",
12
+ consumer_secret: "",
13
+ token: "",
14
+ token_secret: "" }
15
+
16
+ rest = Plum::Client.start("api.twitter.com", 443)
17
+ Plum::Client.start("userstream.twitter.com", 443) { |streaming|
18
+ streaming.get("/1.1/user.json",
19
+ headers: { "authorization" => SimpleOAuth::Header.new(:get, "https://userstream.twitter.com/1.1/user.json", {}, credentials).to_s,
20
+ "accept-encoding" => "identity;q=1" }) { |res| # plum doesn't have built-in gzip/deflate decoder
21
+ if res.status != "200"
22
+ puts "failed userstream"
23
+ exit
24
+ end
25
+
26
+ buf = String.new
27
+ res.on_chunk { |chunk| # when received DATA frame
28
+ buf << chunk
29
+ *msgs, buf = buf.split("\r\n", -1)
30
+
31
+ msgs.each do |msg|
32
+ next if msg.empty?
33
+
34
+ json = JSON.parse(msg)
35
+ next unless json["user"] # unless it is a tweet
36
+
37
+ puts "@#{json["user"]["screen_name"]}: #{json["text"]}"
38
+
39
+ if /にゃーん/ =~ json["text"]
40
+ args = { "status" => "@#{json["user"]["screen_name"]} にゃーん",
41
+ "in_reply_to_status_id" => json["id"].to_s }
42
+ rest.post!("/1.1/statuses/update.json",
43
+ args.map { |k, v| "#{k}=#{CGI.escape(v)}" }.join("&"),
44
+ headers: { "authorization" => SimpleOAuth::Header.new(:post, "https://api.twitter.com/1.1/statuses/update.json", args, credentials).to_s,
45
+ "content-type" => "application/x-www-form-urlencoded" })
46
+ end
47
+ end
48
+ }
49
+ }
50
+ }
51
+ rest.close
@@ -25,7 +25,7 @@ loop do
25
25
  next
26
26
  end
27
27
 
28
- plum = Plum::HTTPConnection.new(sock)
28
+ plum = Plum::HTTPServerConnection.new(sock)
29
29
 
30
30
  plum.on(:frame) do |frame|
31
31
  log(id, frame.stream_id, "recv: #{frame.inspect}")
@@ -77,42 +77,48 @@ loop do
77
77
  <input type=submit>
78
78
  </form>
79
79
  EOF
80
- stream.respond({
80
+ stream.send_headers({
81
81
  ":status": "200",
82
82
  "server": "plum",
83
83
  "content-type": "text/html",
84
84
  "content-length": body.bytesize
85
- }, body)
85
+ }, end_stream: false)
86
+ stream.send_data(body, end_stream: true)
86
87
  when ["POST", "/post.page"]
87
88
  body = "Posted value is: #{CGI.unescape(data).gsub("<", "&lt;").gsub(">", "&gt;")}<br> <a href=/>Back to top page</a>"
88
- stream.respond({
89
+ stream.send_headers({
89
90
  ":status": "200",
90
91
  "server": "plum",
91
92
  "content-type": "text/html",
92
93
  "content-length": body.bytesize
93
- }, body)
94
+ }, end_stream: false)
95
+ stream.send_data(body, end_stream: true)
94
96
  else
95
97
  body = "Page not found! <a href=/>Back to top page</a>"
96
- stream.respond({
98
+ stream.send_headers({
97
99
  ":status": "404",
98
100
  "server": "plum",
99
101
  "content-type": "text/html",
100
102
  "content-length": body.bytesize
101
- }, body)
103
+ }, end_stream: false)
104
+ stream.send_data(body, end_stream: true)
102
105
  end
103
106
  end
104
107
  end
105
108
 
106
109
  Thread.new {
107
110
  begin
108
- plum.run
111
+ while !sock.closed? && !sock.eof?
112
+ plum << sock.readpartial(1024)
113
+ end
109
114
  rescue Plum::LegacyHTTPError
115
+ data = "Use modern web browser with HTTP/2 support."
110
116
  resp = "HTTP/1.1 505 HTTP Version Not Supported\r\n"
111
117
  "Content-Type: text/plain\r\n"
112
118
  "Content-Length: #{data.bytesize}\r\n"
113
119
  "Server: plum/#{Plum::VERSION}\r\n"
114
120
  "\r\n"
115
- "Use modern web browser with HTTP/2 support."
121
+ "#{data}"
116
122
 
117
123
  sock.write(resp)
118
124
  rescue
@@ -43,7 +43,7 @@ loop do
43
43
  next
44
44
  end
45
45
 
46
- plum = Plum::HTTPSConnection.new(sock)
46
+ plum = Plum::HTTPSServerConnection.new(sock)
47
47
 
48
48
  plum.on(:frame) do |frame|
49
49
  log(id, frame.stream_id, "recv: #{frame.inspect}")
@@ -95,59 +95,66 @@ loop do
95
95
  <input type=submit>
96
96
  </form>
97
97
  EOF
98
- stream.respond({
98
+ stream.send_headers({
99
99
  ":status": "200",
100
100
  "server": "plum",
101
101
  "content-type": "text/html",
102
- "content-length": body.size
103
- }, body)
102
+ "content-length": body.bytesize
103
+ }, end_stream: false)
104
+ stream.send_data(body, end_stream: true)
104
105
  when ["GET", "/abc.html"]
105
106
  body = "ABC! <a href=/>Back to top page</a><br><img src=/image.nyan>"
107
+ stream.send_headers({
108
+ ":status": "200",
109
+ "server": "plum",
110
+ "content-type": "text/html",
111
+ "content-length": body.bytesize
112
+ }, end_stream: false)
106
113
  i_stream = stream.promise({
107
114
  ":authority": "localhost:40443",
108
115
  ":method": "GET",
109
116
  ":scheme": "https",
110
117
  ":path": "/image.nyan"
111
118
  })
112
- stream.respond({
113
- ":status": "200",
114
- "server": "plum",
115
- "content-type": "text/html",
116
- "content-length": body.size
117
- }, body)
118
- image = ("iVBORw0KGgoAAAANSUhEUgAAAEAAAABAAgMAAADXB5lNAAAACVBMVEX///93o0jG/4mTMy20AAAA"
119
- "bklEQVQ4y2NgoAoIRQJkCoSimIdTgJGBBU1ABE1A1AVdBQuaACu6gCALhhZ0axlZCDgMWYAB6ilU"
120
- "35IoADEMxWyyBDD45AhQCFahM0kXWIVu3sAJrILzyBcgytoFeATABBcXWohhCEC14BCgGAAAX1ZQ"
119
+ stream.send_data(body, end_stream: true)
120
+ image = ("iVBORw0KGgoAAAANSUhEUgAAAEAAAABAAgMAAADXB5lNAAAACVBMVEX///93o0jG/4mTMy20AAAA" \
121
+ "bklEQVQ4y2NgoAoIRQJkCoSimIdTgJGBBU1ABE1A1AVdBQuaACu6gCALhhZ0axlZCDgMWYAB6ilU" \
122
+ "35IoADEMxWyyBDD45AhQCFahM0kXWIVu3sAJrILzyBcgytoFeATABBcXWohhCEC14BCgGAAAX1ZQ" \
121
123
  "ZtJp0zAAAAAASUVORK5CYII=").unpack("m")[0]
122
- i_stream.respond({
124
+ i_stream.send_headers({
123
125
  ":status": "200",
124
126
  "server": "plum",
125
127
  "content-type": "image/png",
126
- "content-length": image.size
127
- }, image)
128
+ "content-length": image.bytesize
129
+ }, end_stream: false)
130
+ i_stream.send_data(image, end_stream: true)
128
131
  when ["POST", "/post.page"]
129
132
  body = "Posted value is: #{CGI.unescape(data).gsub("<", "&lt;").gsub(">", "&gt;")}<br> <a href=/>Back to top page</a>"
130
- stream.respond({
133
+ stream.send_headers({
131
134
  ":status": "200",
132
135
  "server": "plum",
133
136
  "content-type": "text/html",
134
- "content-length": body.size
135
- }, body)
137
+ "content-length": body.bytesize
138
+ }, end_stream: false)
139
+ stream.send_data(body, end_stream: true)
136
140
  else
137
141
  body = "Page not found! <a href=/>Back to top page</a>"
138
- stream.respond({
142
+ stream.send_headers({
139
143
  ":status": "404",
140
144
  "server": "plum",
141
145
  "content-type": "text/html",
142
- "content-length": body.size
143
- }, body)
146
+ "content-length": body.bytesize
147
+ }, end_stream: false)
148
+ stream.send_data(body, end_stream: true)
144
149
  end
145
150
  end
146
151
  end
147
152
 
148
153
  Thread.new {
149
154
  begin
150
- plum.run
155
+ while !sock.closed? && !sock.eof?
156
+ plum << sock.readpartial(1024)
157
+ end
151
158
  rescue
152
159
  puts $!
153
160
  puts $!.backtrace
@@ -17,7 +17,14 @@ require "plum/frame"
17
17
  require "plum/flow_control"
18
18
  require "plum/connection_utils"
19
19
  require "plum/connection"
20
- require "plum/https_connection"
21
- require "plum/http_connection"
22
20
  require "plum/stream_utils"
23
21
  require "plum/stream"
22
+ require "plum/server/connection"
23
+ require "plum/server/https_connection"
24
+ require "plum/server/http_connection"
25
+ require "plum/client"
26
+ require "plum/client/response"
27
+ require "plum/client/connection"
28
+ require "plum/client/client_session"
29
+ require "plum/client/legacy_client_session"
30
+ require "plum/client/upgrade_client_session"
@@ -0,0 +1,198 @@
1
+ # -*- frozen-string-literal: true -*-
2
+ module Plum
3
+ class Client
4
+ DEFAULT_CONFIG = {
5
+ http2: true,
6
+ scheme: "https",
7
+ hostname: nil,
8
+ verify_mode: OpenSSL::SSL::VERIFY_PEER,
9
+ ssl_context: nil,
10
+ http2_settings: {},
11
+ user_agent: "plum/#{Plum::VERSION}",
12
+ }.freeze
13
+
14
+ attr_reader :host, :port, :config
15
+ attr_reader :socket, :session
16
+
17
+ # Creates a new HTTP client and starts communication.
18
+ # A shorthand for `Plum::Client.new(args).start(&block)`
19
+ def self.start(host, port = nil, config = {}, &block)
20
+ client = self.new(host, port, config)
21
+ client.start(&block)
22
+ end
23
+
24
+ # Creates a new HTTP client.
25
+ # @param host [String | IO] the host to connect, or IO object.
26
+ # @param port [Integer] the port number to connect
27
+ # @param config [Hash<Symbol, Object>] the client configuration
28
+ def initialize(host, port = nil, config = {})
29
+ if host.is_a?(IO)
30
+ @socket = host
31
+ else
32
+ @host = host
33
+ @port = port || (config[:scheme] == "https" ? 443 : 80)
34
+ end
35
+ @config = DEFAULT_CONFIG.merge(hostname: host).merge(config)
36
+ @started = false
37
+ end
38
+
39
+ # Starts communication.
40
+ # If block passed, waits for asynchronous requests and closes the connection after calling the block.
41
+ def start(&block)
42
+ raise IOError, "Session already started" if @started
43
+ _start
44
+ if block_given?
45
+ begin
46
+ ret = yield(self)
47
+ resume
48
+ return ret
49
+ ensure
50
+ close
51
+ end
52
+ end
53
+ self
54
+ end
55
+
56
+ # Resume communication with the server, until the specified (or all running) requests are complete.
57
+ # @param response [Response] if specified, waits only for the response
58
+ # @return [Response] if parameter response is specified
59
+ def resume(response = nil)
60
+ if response
61
+ @session.succ until response.failed? || response.finished?
62
+ response
63
+ else
64
+ @session.succ until @session.empty?
65
+ end
66
+ end
67
+
68
+ # Closes the connection immediately.
69
+ def close
70
+ @session.close if @session
71
+ ensure
72
+ @socket.close if @socket
73
+ end
74
+
75
+ # Creates a new HTTP request.
76
+ # @param headers [Hash<String, String>] the request headers
77
+ # @param body [String] the request body
78
+ # @param options [Hash<Symbol, Object>] request options
79
+ # @param block [Proc] if passed, it will be called when received response headers.
80
+ def request(headers, body, options = {}, &block)
81
+ raise ArgumentError, ":method and :path headers are required" unless headers[":method"] && headers[":path"]
82
+ @session.request(headers, body, options, &block)
83
+ end
84
+
85
+ # @!method get!
86
+ # @!method head!
87
+ # @!method delete!
88
+ # @param path [String] the absolute path to request (translated into :path header)
89
+ # @param options [Hash<Symbol, Object>] the request options
90
+ # @param block [Proc] if specified, calls the block when finished
91
+ # Shorthand method for `Client#resume(Client#request(*args))`
92
+
93
+ # @!method get
94
+ # @!method head
95
+ # @!method delete
96
+ # @param path [String] the absolute path to request (translated into :path header)
97
+ # @param options [Hash<Symbol, Object>] the request options
98
+ # @param block [Proc] if specified, calls the block when finished
99
+ # Shorthand method for `#request`
100
+ %w(GET HEAD DELETE).each { |method|
101
+ define_method(:"#{method.downcase}!") do |path, options = {}, &block|
102
+ resume _request_helper(method, path, nil, options, &block)
103
+ end
104
+ define_method(:"#{method.downcase}") do |path, options = {}, &block|
105
+ _request_helper(method, path, nil, options, &block)
106
+ end
107
+ }
108
+ # @!method post!
109
+ # @!method put!
110
+ # @param path [String] the absolute path to request (translated into :path header)
111
+ # @param body [String] the request body
112
+ # @param options [Hash<Symbol, Object>] the request options
113
+ # @param block [Proc] if specified, calls the block when finished
114
+ # Shorthand method for `Client#resume(Client#request(*args))`
115
+
116
+ # @!method post
117
+ # @!method put
118
+ # @param path [String] the absolute path to request (translated into :path header)
119
+ # @param body [String] the request body
120
+ # @param options [Hash<Symbol, Object>] the request options
121
+ # @param block [Proc] if specified, calls the block when finished
122
+ # Shorthand method for `#request`
123
+ %w(POST PUT).each { |method|
124
+ define_method(:"#{method.downcase}!") do |path, body, options = {}, &block|
125
+ resume _request_helper(method, path, body, options, &block)
126
+ end
127
+ define_method(:"#{method.downcase}") do |path, body, options = {}, &block|
128
+ _request_helper(method, path, body, options, &block)
129
+ end
130
+ }
131
+
132
+ private
133
+ # @return [Boolean] http2 nego?
134
+ def _connect
135
+ @socket = TCPSocket.open(@host, @port)
136
+
137
+ if @config[:scheme] == "https"
138
+ ctx = @config[:ssl_context] || new_ssl_ctx
139
+ @socket = OpenSSL::SSL::SSLSocket.new(@socket, ctx)
140
+ @socket.hostname = @config[:hostname] if @socket.respond_to?(:hostname=)
141
+ @socket.sync_close = true
142
+ @socket.connect
143
+ @socket.post_connection_check(@config[:hostname]) if ctx.verify_mode != OpenSSL::SSL::VERIFY_NONE
144
+
145
+ @socket.respond_to?(:alpn_protocol) && @socket.alpn_protocol == "h2" ||
146
+ @socket.respond_to?(:npn_protocol) && @socket.npn_protocol == "h2"
147
+ end
148
+ end
149
+
150
+ def _start
151
+ @started = true
152
+
153
+ klass = @config[:http2] ? ClientSession : LegacyClientSession
154
+ nego = @socket || _connect
155
+
156
+ if @config[:http2]
157
+ if @config[:scheme] == "https"
158
+ klass = nego ? ClientSession : LegacyClientSession
159
+ else
160
+ klass = UpgradeClientSession
161
+ end
162
+ else
163
+ klass = LegacyClientSession
164
+ end
165
+
166
+ @session = klass.new(@socket, @config)
167
+ end
168
+
169
+ def new_ssl_ctx
170
+ ctx = OpenSSL::SSL::SSLContext.new
171
+ ctx.ssl_version = :TLSv1_2
172
+ ctx.verify_mode = @config[:verify_mode]
173
+ cert_store = OpenSSL::X509::Store.new
174
+ cert_store.set_default_paths
175
+ ctx.cert_store = cert_store
176
+ if @config[:http2]
177
+ ctx.ciphers = "ALL:!" + HTTPSServerConnection::CIPHER_BLACKLIST.join(":!")
178
+ if ctx.respond_to?(:alpn_protocols)
179
+ ctx.alpn_protocols = ["h2", "http/1.1"]
180
+ end
181
+ if ctx.respond_to?(:npn_select_cb) # TODO: RFC 7540 does not define protocol negotiation with NPN
182
+ ctx.npn_select_cb = -> protocols {
183
+ protocols.include?("h2") ? "h2" : protocols.first
184
+ }
185
+ end
186
+ end
187
+ ctx
188
+ end
189
+
190
+ def _request_helper(method, path, body, options, &block)
191
+ base = { ":method" => method,
192
+ ":path" => path,
193
+ "user-agent" => @config[:user_agent] }
194
+ base.merge!(options[:headers]) if options[:headers]
195
+ request(base, body, options, &block)
196
+ end
197
+ end
198
+ end