plum 0.0.2 → 0.0.3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: aa11d64c7ed018b7ede5e9275adcd9abdb091003
4
- data.tar.gz: 5fb20e87686039b5dec5f9b053719935b9788c06
3
+ metadata.gz: 4fae3d5fc60fdcda63be201fe0349e497ed0c4a2
4
+ data.tar.gz: e9a1ac852f57acfef9d1eb9585c5b5cca427ce3c
5
5
  SHA512:
6
- metadata.gz: 1b0095b8f3510be4442524baadcb27a385530753ff0e63ea4eb80bd2ff1755757bbe913ac7977ded238dcb5cc46ad6e6830d720254640a59b4b5fd2e20f8ff1a
7
- data.tar.gz: 87e4540bed0f1ec556b177fe16e208527acd43abcea2ef2edbd40fac3070348c7b74891b5d9acf5fe56dbca372940c5c4f771fc60ee05fd29dc26a1106f3ee89
6
+ metadata.gz: 0f66ed1a0dda7c39d14b0126d871b0c9978069c2b1a27bcb541d9dc9e0e5c0654d86439ff5c27a650d877f827a3a924ee20028f38245d5a9ceb09f3f69a1998c
7
+ data.tar.gz: 95a3eb71873bb321e4ee683820b4057205a2fde167d38b1e09f605f0dd70e606f0ba6f67e1b3fd6d9a6a08e12c77167d1aee32cd7bec034278892f33651c682f
data/.travis.yml CHANGED
@@ -7,8 +7,8 @@ install:
7
7
  - echo openssl_version=1.0.2d >> $rvm_path/user/db
8
8
  - rvm pkg install openssl
9
9
  - $rvm_path/usr/bin/openssl version
10
- - rvm install 2.2.2-alpn --patch https://gist.githubusercontent.com/rhenium/b1711edcc903e8887a51/raw/2309e469f5a3ba15917d804ac61b19e62b3d8faf/ruby-openssl-alpn-no-tests-and-docs.patch --with-openssl-dir=$rvm_path/usr
11
- - rvm use 2.2.2-alpn
10
+ - rvm install 2.2.3-alpn --patch https://gist.githubusercontent.com/rhenium/b1711edcc903e8887a51/raw/2309e469f5a3ba15917d804ac61b19e62b3d8faf/ruby-openssl-alpn-no-tests-and-docs.patch --with-openssl-dir=$rvm_path/usr
11
+ - rvm use 2.2.3-alpn
12
12
  - bundle install
13
13
  script:
14
14
  - bundle exec rake test
data/README.md CHANGED
@@ -1,10 +1,16 @@
1
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
2
  A minimal implementation of HTTP/2 server.
3
3
 
4
+ ## Examples
5
+ * examples/ - Minimal usage.
6
+ * [rhenium/plum-server](https://github.com/rhenium/plum-server) - A example server for https://rhe.jp and http://rhe.jp.
7
+
4
8
  ## Requirements
5
- * OpenSSL 1.0.2+
6
- * Ruby 2.2 with [ALPN support](https://gist.github.com/rhenium/b1711edcc903e8887a51) and [ECDH support (r51348)](https://bugs.ruby-lang.org/projects/ruby-trunk/repository/revisions/51348/diff?format=diff) or latest Ruby 2.3.0-dev.
7
- * [http-parser.rb gem](https://rubygems.org/gems/http_parser.rb) if you use "http" URI scheme.
9
+ * Ruby
10
+ * Ruby 2.2 with [ALPN support patch](https://gist.github.com/rhenium/b1711edcc903e8887a51) and [ECDH support patch (r51348)](https://bugs.ruby-lang.org/projects/ruby-trunk/repository/revisions/51348/diff?format=diff)
11
+ * or latest Ruby 2.3.0-dev
12
+ * OpenSSL 1.0.2 or newer (HTTP/2 requires ALPN)
13
+ * [http-parser.rb gem](https://rubygems.org/gems/http_parser.rb) (HTTP/1.1 parser; if you use "http" URI scheme)
8
14
 
9
15
  ## TODO
10
16
  * **Better API**
@@ -74,7 +74,7 @@ module Plum
74
74
  #
75
75
  # @param args [Hash] The argument to pass to Stram.new.
76
76
  def reserve_stream(**args)
77
- next_id = ((@streams.keys.last / 2).to_i + 1) * 2
77
+ next_id = (@streams.keys.select(&:even?).max || 0) + 2
78
78
  stream = new_stream(next_id, state: :reserved_local, **args)
79
79
  stream
80
80
  end
@@ -98,10 +98,6 @@ module Plum
98
98
  end
99
99
 
100
100
  def new_stream(stream_id, **args)
101
- if @streams.size > 0 && @streams.keys.max >= stream_id
102
- raise Plum::ConnectionError.new(:protocol_error)
103
- end
104
-
105
101
  stream = Stream.new(self, stream_id, **args)
106
102
  callback(:stream, stream)
107
103
  @streams[stream_id] = stream
@@ -142,7 +138,10 @@ module Plum
142
138
  if @streams.key?(frame.stream_id)
143
139
  stream = @streams[frame.stream_id]
144
140
  else
145
- raise ConnectionError.new(:protocol_error) if frame.stream_id.even? # stream started by client must have odd ID
141
+ if frame.stream_id.even? || (@streams.size > 0 && @streams.keys.select(&:odd?).max >= frame.stream_id)
142
+ raise Plum::ConnectionError.new(:protocol_error)
143
+ end
144
+
146
145
  stream = new_stream(frame.stream_id)
147
146
  end
148
147
  stream.receive_frame(frame)
@@ -162,6 +161,7 @@ module Plum
162
161
  when :ping
163
162
  receive_ping(frame)
164
163
  when :goaway
164
+ callback(:goaway, frame)
165
165
  goaway
166
166
  close
167
167
  when :data, :headers, :priority, :rst_stream, :push_promise, :continuation
@@ -26,6 +26,11 @@ module Plum
26
26
  send_immediately Frame.goaway(last_id, error_type)
27
27
  end
28
28
 
29
+ # Returns whether peer enables server push or not
30
+ def push_enabled?
31
+ @remote_settings[:enable_push] == 1
32
+ end
33
+
29
34
  private
30
35
  def update_local_settings(new_settings)
31
36
  old_settings = @local_settings.dup
data/lib/plum/errors.rb CHANGED
@@ -1,6 +1,5 @@
1
1
  module Plum
2
2
  class Error < StandardError; end
3
- class LegacyHTTPError < Error; end
4
3
  class HPACKError < Error; end
5
4
  class HTTPError < Error
6
5
  ERROR_CODES = {
@@ -33,4 +32,14 @@ module Plum
33
32
  end
34
33
  class ConnectionError < HTTPError; end
35
34
  class StreamError < HTTPError; end
35
+
36
+ class LegacyHTTPError < Error
37
+ attr_reader :headers, :data, :parser
38
+
39
+ def initialize(headers, data, parser)
40
+ @headers = headers
41
+ @data = data
42
+ @parser = parser
43
+ end
44
+ end
36
45
  end
@@ -90,6 +90,8 @@ module Plum
90
90
  raise local_error.new(:protocol_error)
91
91
  end
92
92
 
93
+ callback(:window_update, wsi)
94
+
93
95
  @send_remaining_window += wsi
94
96
  consume_send_buffer
95
97
  end
@@ -35,7 +35,7 @@ module Plum
35
35
  settings != nil)
36
36
  switch_protocol(settings)
37
37
  else
38
- raise LegacyHTTPError.new
38
+ raise LegacyHTTPError.new(@_headers, @_body, parser)
39
39
  end
40
40
  }
41
41
 
data/lib/plum/stream.rb CHANGED
@@ -211,6 +211,7 @@ module Plum
211
211
  raise ConnectionError.new(:protocol_error)
212
212
  end
213
213
 
214
+ callback(:rst_stream, frame)
214
215
  @state = :closed # MUST NOT send RST_STREAM
215
216
  end
216
217
  end
@@ -2,9 +2,9 @@ using Plum::BinaryString
2
2
 
3
3
  module Plum
4
4
  module StreamUtils
5
- # Responds to HTTP request.
5
+ # Responds to a HTTP request.
6
6
  #
7
- # @param headers [Hash<String, String>] The response headers.
7
+ # @param headers [Enumerable<String, String>] The response headers.
8
8
  # @param body [String, IO] The response body.
9
9
  def respond(headers, body = nil, end_stream: true) # TODO: priority, padding
10
10
  if body
@@ -17,7 +17,7 @@ module Plum
17
17
 
18
18
  # Reserves a stream to server push. Sends PUSH_PROMISE and create new stream.
19
19
  #
20
- # @param headers [Hash<String, String>] The *request* headers. It must contain all of them: ':authority', ':method', ':scheme' and ':path'.
20
+ # @param headers [Enumerable<String, String>] The *request* headers. It must contain all of them: ':authority', ':method', ':scheme' and ':path'.
21
21
  # @return [Stream] The stream to send push response.
22
22
  def promise(headers)
23
23
  stream = @connection.reserve_stream(weight: self.weight + 1, parent: self)
@@ -29,7 +29,10 @@ module Plum
29
29
  stream
30
30
  end
31
31
 
32
- private
32
+ # Sends response headers. If the encoded frame is larger than MAX_FRAME_SIZE, the headers will be splitted into HEADERS frame and CONTINUATION frame(s).
33
+ #
34
+ # @param headers [Enumerable<String, String>] The response headers.
35
+ # @param end_stream [Boolean] Set END_STREAM flag or not.
33
36
  def send_headers(headers, end_stream:)
34
37
  max = @connection.remote_settings[:max_frame_size]
35
38
  encoded = @connection.hpack_encoder.encode(headers)
@@ -40,6 +43,10 @@ module Plum
40
43
  @state = :half_closed_local if end_stream
41
44
  end
42
45
 
46
+ # Sends DATA frame. If the data is larger than MAX_FRAME_SIZE, DATA frame will be splitted.
47
+ #
48
+ # @param data [String, IO] The data to send.
49
+ # @param end_stream [Boolean] Set END_STREAM flag or not.
43
50
  def send_data(data, end_stream: true)
44
51
  max = @connection.remote_settings[:max_frame_size]
45
52
  if data.is_a?(IO)
data/lib/plum/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Plum
2
- VERSION = "0.0.2"
2
+ VERSION = "0.0.3"
3
3
  end
@@ -47,7 +47,7 @@ class HTTPSConnectionNegotiationTest < Minitest::Test
47
47
 
48
48
  server_thread = Thread.new {
49
49
  begin
50
- timeout(3) {
50
+ Timeout.timeout(3) {
51
51
  sock = ssl_server.accept
52
52
  plum = HTTPSConnection.new(sock)
53
53
  assert_connection_error(:inadequate_security) {
@@ -55,7 +55,7 @@ class HTTPSConnectionNegotiationTest < Minitest::Test
55
55
  plum.run
56
56
  }
57
57
  }
58
- rescue TimeoutError
58
+ rescue Timeout::Error
59
59
  flunk "server timeout"
60
60
  ensure
61
61
  tcp_server.close
@@ -64,7 +64,7 @@ class HTTPSConnectionNegotiationTest < Minitest::Test
64
64
  client_thread = Thread.new {
65
65
  sock = TCPSocket.new("127.0.0.1", LISTEN_PORT)
66
66
  begin
67
- timeout(3) {
67
+ Timeout.timeout(3) {
68
68
  ctx = OpenSSL::SSL::SSLContext.new.tap {|ctx|
69
69
  ctx.alpn_protocols = ["h2"]
70
70
  ctx.ciphers = "AES256-GCM-SHA384"
@@ -74,7 +74,7 @@ class HTTPSConnectionNegotiationTest < Minitest::Test
74
74
  ssl.write Connection::CLIENT_CONNECTION_PREFACE
75
75
  ssl.write Frame.settings.assemble
76
76
  }
77
- rescue TimeoutError
77
+ rescue Timeout::Error
78
78
  flunk "client timeout"
79
79
  ensure
80
80
  sock.close
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: plum
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - rhenium
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-08-18 00:00:00.000000000 Z
11
+ date: 2015-10-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -151,7 +151,6 @@ files:
151
151
  - README.md
152
152
  - Rakefile
153
153
  - bin/.gitkeep
154
- - examples/local_server.rb
155
154
  - examples/non_tls_server.rb
156
155
  - examples/static_server.rb
157
156
  - lib/plum.rb
@@ -220,7 +219,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
220
219
  version: '0'
221
220
  requirements: []
222
221
  rubyforge_project:
223
- rubygems_version: 2.4.5
222
+ rubygems_version: 2.4.5.1
224
223
  signing_key:
225
224
  specification_version: 4
226
225
  summary: A minimal implementation of HTTP/2 server.
@@ -1,207 +0,0 @@
1
- DEBUG = ENV["DEBUG"] || false
2
- HOST = ENV["HOST"]
3
- PORT = ENV["PORT"]
4
- DOCUMENT_ROOT = ENV["DOCUMENT_ROOT"] || "/srv/http"
5
- TLS_CERT = ENV["TLS_CERT"]
6
- TLS_KEY = ENV["TLS_KEY"]
7
-
8
- CONTENT_TYPES = {
9
- /\.html$/ => "text/html",
10
- /\.png$/ => "image/png",
11
- /\.jpg$/ => "image/jpeg",
12
- /\.css$/ => "text/css",
13
- /\.js$/ => "application/javascript",
14
- /\.atom$/ => "application/atom+xml",
15
- }
16
-
17
- $LOAD_PATH << File.expand_path("../../lib", __FILE__)
18
- require "plum"
19
- require "openssl"
20
- require "socket"
21
- begin
22
- require "oga"
23
- HAVE_OGA = true
24
- rescue LoadError
25
- puts "Oga is needed for parsing HTML"
26
- HAVE_OGA = false
27
- end
28
-
29
- begin
30
- require "sslkeylog/autotrace" # for debug
31
- rescue LoadError
32
- end
33
-
34
- def log(con, stream, s)
35
- prefix = "[%02x;%02x] " % [con, stream]
36
- if s.is_a?(Enumerable)
37
- puts s.map {|a| prefix + a.to_s }.join("\n")
38
- else
39
- puts prefix + s.to_s
40
- end
41
- end
42
-
43
- def content_type(filename)
44
- exp, ct = CONTENT_TYPES.lazy.select {|pat, e| pat =~ filename }.first
45
- ct || "texp/plain"
46
- end
47
-
48
- def assets(file)
49
- if /\.html$/ =~ File.basename(file)
50
- doc = Oga.parse_html(File.read(file))
51
- assets = []
52
- doc.xpath("img").each {|img| assets << img.get("src") }
53
- doc.xpath("//html/head/link[@rel='stylesheet']").each {|css| assets << css.get("href") }
54
- doc.xpath("script").each {|js| assets << js.get("src") }
55
- assets.compact.uniq.map {|path|
56
- if path.include?("//")
57
- next nil
58
- end
59
-
60
- if path.start_with?("/")
61
- pa = File.expand_path(DOCUMENT_ROOT + path)
62
- else
63
- pa = File.expand_path(path, file)
64
- end
65
-
66
- if pa.start_with?(DOCUMENT_ROOT) & File.exist?(pa)
67
- pa
68
- else
69
- nil
70
- end
71
- }.compact
72
- else
73
- []
74
- end
75
- end
76
-
77
- ctx = OpenSSL::SSL::SSLContext.new
78
- ctx.ssl_version = :TLSv1_2
79
- ctx.alpn_select_cb = -> protocols {
80
- raise "Client does not support HTTP/2: #{protocols}" unless protocols.include?("h2")
81
- "h2"
82
- }
83
- ctx.tmp_ecdh_callback = -> (sock, ise, keyl) {
84
- OpenSSL::PKey::EC.new("prime256v1")
85
- }
86
-
87
- ctx.cert = OpenSSL::X509::Certificate.new File.read(TLS_CERT)
88
- ctx.key = OpenSSL::PKey::RSA.new File.read(TLS_KEY)
89
- tcp_server = TCPServer.new(HOST, PORT)
90
- ssl_server = OpenSSL::SSL::SSLServer.new(tcp_server, ctx)
91
-
92
- loop do
93
- begin
94
- sock = ssl_server.accept
95
- id = sock.io.fileno
96
- puts "#{id}: accept!"
97
- rescue => e
98
- puts e
99
- next
100
- end
101
-
102
- plum = Plum::HTTPSConnection.new(sock)
103
-
104
- plum.on(:frame) do |frame|
105
- log(id, frame.stream_id, "recv: #{frame.inspect}")
106
- end if DEBUG
107
-
108
- plum.on(:send_frame) do |frame|
109
- log(id, frame.stream_id, "send: #{frame.inspect}")
110
- end if DEBUG
111
-
112
- plum.on(:remote_settings) do |settings|
113
- log(id, 0, settings.map {|name, value| "#{name}: #{value}" }) if DEBUG
114
- end
115
-
116
- plum.on(:connection_error) do |exception|
117
- puts exception
118
- puts exception.backtrace
119
- end if DEBUG
120
-
121
- plum.on(:stream) do |stream|
122
- log(id, stream.id, "stream open")
123
- stream.on(:stream_error) do |exception|
124
- puts exception
125
- puts exception.backtrace
126
- end if DEBUG
127
-
128
- headers = data = nil
129
-
130
- stream.on(:open) do
131
- headers = nil
132
- data = ""
133
- end
134
-
135
- stream.on(:headers) do |headers_|
136
- log(id, stream.id, headers_.map {|name, value| "#{name}: #{value}" }) if DEBUG
137
- headers = headers_.to_h
138
- end
139
-
140
- stream.on(:data) do |data_|
141
- log(id, stream.id, data_) if DEBUG
142
- data << data_
143
- end
144
-
145
- stream.on(:end_stream) do
146
- if headers[":method"] == "GET"
147
- file = File.expand_path(DOCUMENT_ROOT + headers[":path"])
148
- file << "/index.html" if Dir.exist?(file)
149
- if file.start_with?(DOCUMENT_ROOT) && File.exist?(file)
150
- io = File.open(file)
151
- size = File.stat(file).size
152
- i_sts = assets(file).map {|asset|
153
- i_st = stream.promise({
154
- ":authority": headers[":authority"],
155
- ":method": "GET",
156
- ":scheme": "https",
157
- ":path": asset[DOCUMENT_ROOT.size..-1]
158
- })
159
- [i_st, asset]
160
- }
161
- stream.respond({
162
- ":status": "200",
163
- "server": "plum/#{Plum::VERSION}",
164
- "content-type": content_type(file),
165
- "content-length": size
166
- }, io)
167
- i_sts.each do |i_st, asset|
168
- aio = File.open(asset)
169
- asize = File.stat(asset).size
170
- i_st.respond({
171
- ":status": "200",
172
- "server": "plum/#{Plum::VERSION}",
173
- "content-type": content_type(asset),
174
- "content-length": asize
175
- }, aio)
176
- end
177
- else
178
- body = headers.map {|name, value| "#{name}: #{value}" }.join("\n") + "\n" + data
179
- stream.respond({
180
- ":status": "404",
181
- "server": "plum/#{Plum::VERSION}",
182
- "content-type": "text/plain",
183
- "content-length": body.bytesize
184
- }, body)
185
- end
186
- else
187
- # Not implemented
188
- body = headers.map {|name, value| "#{name}: #{value}" }.join("\n") << "\n" << data
189
- stream.respond({
190
- ":status": "501",
191
- "server": "plum/#{Plum::VERSION}",
192
- "content-type": "text/plain",
193
- "content-length": body.bytesize
194
- }, body)
195
- end
196
- end
197
- end
198
-
199
- Thread.new {
200
- begin
201
- plum.run
202
- rescue
203
- puts $!
204
- puts $!.backtrace
205
- end
206
- }
207
- end