plum 0.0.2 → 0.0.3

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