plum 0.1.3 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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