plum 0.0.1

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 (56) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +11 -0
  3. data/.travis.yml +14 -0
  4. data/Gemfile +4 -0
  5. data/Guardfile +7 -0
  6. data/LICENSE +21 -0
  7. data/README.md +14 -0
  8. data/Rakefile +12 -0
  9. data/bin/.gitkeep +0 -0
  10. data/examples/local_server.rb +206 -0
  11. data/examples/static_server.rb +157 -0
  12. data/lib/plum.rb +21 -0
  13. data/lib/plum/binary_string.rb +74 -0
  14. data/lib/plum/connection.rb +201 -0
  15. data/lib/plum/connection_utils.rb +38 -0
  16. data/lib/plum/errors.rb +35 -0
  17. data/lib/plum/event_emitter.rb +19 -0
  18. data/lib/plum/flow_control.rb +97 -0
  19. data/lib/plum/frame.rb +163 -0
  20. data/lib/plum/frame_factory.rb +53 -0
  21. data/lib/plum/frame_utils.rb +50 -0
  22. data/lib/plum/hpack/constants.rb +331 -0
  23. data/lib/plum/hpack/context.rb +55 -0
  24. data/lib/plum/hpack/decoder.rb +145 -0
  25. data/lib/plum/hpack/encoder.rb +105 -0
  26. data/lib/plum/hpack/huffman.rb +42 -0
  27. data/lib/plum/http_connection.rb +33 -0
  28. data/lib/plum/https_connection.rb +24 -0
  29. data/lib/plum/stream.rb +217 -0
  30. data/lib/plum/stream_utils.rb +58 -0
  31. data/lib/plum/version.rb +3 -0
  32. data/plum.gemspec +29 -0
  33. data/test/plum/connection/test_handle_frame.rb +70 -0
  34. data/test/plum/hpack/test_context.rb +63 -0
  35. data/test/plum/hpack/test_decoder.rb +291 -0
  36. data/test/plum/hpack/test_encoder.rb +49 -0
  37. data/test/plum/hpack/test_huffman.rb +36 -0
  38. data/test/plum/stream/test_handle_frame.rb +262 -0
  39. data/test/plum/test_binary_string.rb +64 -0
  40. data/test/plum/test_connection.rb +96 -0
  41. data/test/plum/test_connection_utils.rb +29 -0
  42. data/test/plum/test_error.rb +13 -0
  43. data/test/plum/test_flow_control.rb +167 -0
  44. data/test/plum/test_frame.rb +59 -0
  45. data/test/plum/test_frame_factory.rb +56 -0
  46. data/test/plum/test_frame_utils.rb +46 -0
  47. data/test/plum/test_https_connection.rb +37 -0
  48. data/test/plum/test_stream.rb +32 -0
  49. data/test/plum/test_stream_utils.rb +16 -0
  50. data/test/server.crt +19 -0
  51. data/test/server.csr +16 -0
  52. data/test/server.key +27 -0
  53. data/test/test_helper.rb +28 -0
  54. data/test/utils/assertions.rb +60 -0
  55. data/test/utils/server.rb +63 -0
  56. metadata +234 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 8e9aa768df8ce054c286452ca40c8e6f5671fda5
4
+ data.tar.gz: 3a5dafa95ec6dd94b6b354091a8eee73b88f3214
5
+ SHA512:
6
+ metadata.gz: 3110fdf853b9ab1a7966adff6b24a3665bf792853a63bc814e295cee3a343761894b2fa1a48157a19c349998415bfee7710f3242400081ea8863b48f30028d4f
7
+ data.tar.gz: 061d1e6292eb1cd9d2fc6cd6db1daceb4e483970c577cf504f23deb23a407e44535fa27c57a819159198f41c7b248c015940485bab620a565cca84b0a7f14620
data/.gitignore ADDED
@@ -0,0 +1,11 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ .*.sw*
11
+ .*.local
data/.travis.yml ADDED
@@ -0,0 +1,14 @@
1
+ language: ruby
2
+ addons:
3
+ code_climate:
4
+ repo_token: f5092ab344fac7f2de9d7332e00597642a4d24e3d560f7d7f329172a2e5a2def
5
+ install:
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
+ - $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
12
+ - bundle install
13
+ script:
14
+ - bundle exec rake test
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in plum.gemspec
4
+ gemspec
data/Guardfile ADDED
@@ -0,0 +1,7 @@
1
+ guard :minitest, env: { "SKIP_COVERAGE" => true } do
2
+ # with Minitest::Unit
3
+ watch(%r{^test/(.*)\/?test_(.*)\.rb$})
4
+ watch(%r{^lib/(.*/)?([^/]+)\.rb$}) {|m| ["test/#{m[1]}test_#{m[2]}.rb", "test/#{m[1]}#{m[2]}"] }
5
+ watch(%r{^test/test_helper\.rb$}) { "test" }
6
+ watch(%r{^test/utils/.*\.rb}) { "test" }
7
+ end
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015 Kazuki Yamaguchi
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,14 @@
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 implementation of HTTP/2 server.
3
+
4
+ ## 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
+
8
+ ## TODO
9
+ * "http" URIs support (upgrade from HTTP/1.1)
10
+ * Stream Priority (RFC 7540 5.3)
11
+ * Better API
12
+
13
+ ## License
14
+ MIT License
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+ require "yard"
4
+
5
+ Rake::TestTask.new do |t|
6
+ t.libs << "test" << "lib"
7
+ t.pattern = "test/**/test_*.rb"
8
+ end
9
+
10
+ YARD::Rake::YardocTask.new do |t|
11
+ t.files = ["lib/**/*.rb"]
12
+ end
data/bin/.gitkeep ADDED
File without changes
@@ -0,0 +1,206 @@
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
+ }
15
+
16
+ $LOAD_PATH << File.expand_path("../../lib", __FILE__)
17
+ require "plum"
18
+ require "openssl"
19
+ require "socket"
20
+ begin
21
+ require "oga"
22
+ HAVE_OGA = true
23
+ rescue LoadError
24
+ puts "Oga is needed for parsing HTML"
25
+ HAVE_OGA = false
26
+ end
27
+
28
+ begin
29
+ require "sslkeylog/autotrace" # for debug
30
+ rescue LoadError
31
+ end
32
+
33
+ def log(con, stream, s)
34
+ prefix = "[%02x;%02x] " % [con, stream]
35
+ if s.is_a?(Enumerable)
36
+ puts s.map {|a| prefix + a.to_s }.join("\n")
37
+ else
38
+ puts prefix + s.to_s
39
+ end
40
+ end
41
+
42
+ def content_type(filename)
43
+ exp, ct = CONTENT_TYPES.lazy.select {|pat, e| pat =~ filename }.first
44
+ ct || "texp/plain"
45
+ end
46
+
47
+ def assets(file)
48
+ if /\.html$/ =~ File.basename(file)
49
+ doc = Oga.parse_html(File.read(file))
50
+ assets = []
51
+ doc.xpath("img").each {|img| assets << img.get("src") }
52
+ doc.xpath("//html/head/link[@rel='stylesheet']").each {|css| assets << css.get("href") }
53
+ doc.xpath("script").each {|js| assets << js.get("src") }
54
+ assets.compact.uniq.map {|path|
55
+ if path.include?("//")
56
+ next nil
57
+ end
58
+
59
+ if path.start_with?("/")
60
+ pa = File.expand_path(DOCUMENT_ROOT + path)
61
+ else
62
+ pa = File.expand_path(path, file)
63
+ end
64
+
65
+ if pa.start_with?(DOCUMENT_ROOT) & File.exist?(pa)
66
+ pa
67
+ else
68
+ nil
69
+ end
70
+ }.compact
71
+ else
72
+ []
73
+ end
74
+ end
75
+
76
+ ctx = OpenSSL::SSL::SSLContext.new
77
+ ctx.ssl_version = :TLSv1_2
78
+ ctx.alpn_select_cb = -> protocols {
79
+ raise "Client does not support HTTP/2: #{protocols}" unless protocols.include?("h2")
80
+ "h2"
81
+ }
82
+ ctx.tmp_ecdh_callback = -> (sock, ise, keyl) {
83
+ OpenSSL::PKey::EC.new("prime256v1")
84
+ }
85
+
86
+ ctx.cert = OpenSSL::X509::Certificate.new File.read(TLS_CERT)
87
+ ctx.key = OpenSSL::PKey::RSA.new File.read(TLS_KEY)
88
+ tcp_server = TCPServer.new(HOST, PORT)
89
+ ssl_server = OpenSSL::SSL::SSLServer.new(tcp_server, ctx)
90
+
91
+ loop do
92
+ begin
93
+ sock = ssl_server.accept
94
+ id = sock.io.fileno
95
+ puts "#{id}: accept!"
96
+ rescue => e
97
+ puts e
98
+ next
99
+ end
100
+
101
+ plum = Plum::HTTPSConnection.new(sock)
102
+
103
+ plum.on(:frame) do |frame|
104
+ log(id, frame.stream_id, "recv: #{frame.inspect}")
105
+ end if DEBUG
106
+
107
+ plum.on(:send_frame) do |frame|
108
+ log(id, frame.stream_id, "send: #{frame.inspect}")
109
+ end if DEBUG
110
+
111
+ plum.on(:remote_settings) do |settings|
112
+ log(id, 0, settings.map {|name, value| "#{name}: #{value}" }) if DEBUG
113
+ end
114
+
115
+ plum.on(:connection_error) do |exception|
116
+ puts exception
117
+ puts exception.backtrace
118
+ end if DEBUG
119
+
120
+ plum.on(:stream) do |stream|
121
+ log(id, stream.id, "stream open")
122
+ stream.on(:stream_error) do |exception|
123
+ puts exception
124
+ puts exception.backtrace
125
+ end if DEBUG
126
+
127
+ headers = data = nil
128
+
129
+ stream.on(:open) do
130
+ headers = nil
131
+ data = ""
132
+ end
133
+
134
+ stream.on(:headers) do |headers_|
135
+ log(id, stream.id, headers_.map {|name, value| "#{name}: #{value}" }) if DEBUG
136
+ headers = headers_.to_h
137
+ end
138
+
139
+ stream.on(:data) do |data_|
140
+ log(id, stream.id, data_) if DEBUG
141
+ data << data_
142
+ end
143
+
144
+ stream.on(:end_stream) do
145
+ if headers[":method"] == "GET"
146
+ file = File.expand_path(DOCUMENT_ROOT + headers[":path"])
147
+ file << "/index.html" if Dir.exist?(file)
148
+ if file.start_with?(DOCUMENT_ROOT) && File.exist?(file)
149
+ io = File.open(file)
150
+ size = File.stat(file).size
151
+ i_sts = assets(file).map {|asset|
152
+ i_st = stream.promise({
153
+ ":authority": headers[":authority"],
154
+ ":method": "GET",
155
+ ":scheme": "https",
156
+ ":path": asset[DOCUMENT_ROOT.size..-1]
157
+ })
158
+ [i_st, asset]
159
+ }
160
+ stream.respond({
161
+ ":status": "200",
162
+ "server": "plum/#{Plum::VERSION}",
163
+ "content-type": content_type(file),
164
+ "content-length": size
165
+ }, io)
166
+ i_sts.each do |i_st, asset|
167
+ aio = File.open(asset)
168
+ asize = File.stat(asset).size
169
+ i_st.respond({
170
+ ":status": "200",
171
+ "server": "plum/#{Plum::VERSION}",
172
+ "content-type": content_type(asset),
173
+ "content-length": asize
174
+ }, aio)
175
+ end
176
+ else
177
+ body = headers.map {|name, value| "#{name}: #{value}" }.join("\n") + "\n" + data
178
+ stream.respond({
179
+ ":status": "404",
180
+ "server": "plum/#{Plum::VERSION}",
181
+ "content-type": "text/plain",
182
+ "content-length": body.bytesize
183
+ }, body)
184
+ end
185
+ else
186
+ # Not implemented
187
+ body = headers.map {|name, value| "#{name}: #{value}" }.join("\n") << "\n" << data
188
+ stream.respond({
189
+ ":status": "501",
190
+ "server": "plum/#{Plum::VERSION}",
191
+ "content-type": "text/plain",
192
+ "content-length": body.bytesize
193
+ }, body)
194
+ end
195
+ end
196
+ end
197
+
198
+ Thread.new {
199
+ begin
200
+ plum.run
201
+ rescue
202
+ puts $!
203
+ puts $!.backtrace
204
+ end
205
+ }
206
+ end
@@ -0,0 +1,157 @@
1
+ $LOAD_PATH << File.expand_path("../../lib", __FILE__)
2
+ require "plum"
3
+ require "openssl"
4
+ require "socket"
5
+ require "cgi"
6
+
7
+ begin
8
+ require "sslkeylog/autotrace"
9
+ rescue LoadError
10
+ end
11
+
12
+ def log(con, stream, s)
13
+ prefix = "[%02x;%02x] " % [con, stream]
14
+ if s.is_a?(Enumerable)
15
+ puts s.map {|a| prefix + a.to_s }.join("\n")
16
+ else
17
+ puts prefix + s.to_s
18
+ end
19
+ end
20
+
21
+ ctx = OpenSSL::SSL::SSLContext.new
22
+ ctx.ssl_version = :TLSv1_2
23
+ ctx.alpn_select_cb = -> protocols {
24
+ raise "Client does not support HTTP/2: #{protocols}" unless protocols.include?("h2")
25
+ "h2"
26
+ }
27
+ ctx.tmp_ecdh_callback = -> (sock, ise, keyl) {
28
+ OpenSSL::PKey::EC.new("prime256v1")
29
+ }
30
+ ctx.cert = OpenSSL::X509::Certificate.new File.read(".crt.local")
31
+ ctx.key = OpenSSL::PKey::RSA.new File.read(".key.local")
32
+ tcp_server = TCPServer.new("0.0.0.0", 40443)
33
+ ssl_server = OpenSSL::SSL::SSLServer.new(tcp_server, ctx)
34
+
35
+ loop do
36
+ begin
37
+ sock = ssl_server.accept
38
+ id = sock.io.fileno
39
+ puts "#{id}: accept!"
40
+ rescue => e
41
+ STDERR.puts e
42
+ next
43
+ end
44
+
45
+ plum = Plum::HTTPSConnection.new(sock)
46
+
47
+ plum.on(:frame) do |frame|
48
+ log(id, frame.stream_id, "recv: #{frame.inspect}")
49
+ end
50
+
51
+ plum.on(:send_frame) do |frame|
52
+ log(id, frame.stream_id, "send: #{frame.inspect}")
53
+ end
54
+
55
+ plum.on(:connection_error) do |exception|
56
+ puts exception
57
+ puts exception.backtrace
58
+ end
59
+
60
+ plum.on(:stream) do |stream|
61
+ stream.on(:stream_error) do |exception|
62
+ puts exception
63
+ puts exception.backtrace
64
+ end
65
+
66
+ stream.on(:send_deferred) do |frame|
67
+ log(id, frame.stream_id, "send (deferred): #{frame.inspect}")
68
+ end
69
+
70
+ headers = data = nil
71
+
72
+ stream.on(:open) do
73
+ headers = nil
74
+ data = ""
75
+ end
76
+
77
+ stream.on(:headers) do |headers_|
78
+ log(id, stream.id, headers_.map {|name, value| "#{name}: #{value}" })
79
+ headers = headers_.to_h
80
+ end
81
+
82
+ stream.on(:data) do |data_|
83
+ log(id, stream.id, data_)
84
+ data << data_
85
+ end
86
+
87
+ stream.on(:end_stream) do
88
+ case [headers[":method"], headers[":path"]]
89
+ when ["GET", "/"]
90
+ body = "Hello World! <a href=/abc.html>ABC</a> <a href=/fgsd>Not found</a>"
91
+ body << <<-EOF
92
+ <form action=post.page method=post>
93
+ <input type=text name=key value=default_value>
94
+ <input type=submit>
95
+ </form>
96
+ EOF
97
+ stream.respond({
98
+ ":status": "200",
99
+ "server": "plum",
100
+ "content-type": "text/html",
101
+ "content-length": body.size
102
+ }, body)
103
+ when ["GET", "/abc.html"]
104
+ body = "ABC! <a href=/>Back to top page</a><br><img src=/image.nyan>"
105
+ i_stream = stream.promise({
106
+ ":authority": "localhost:40443",
107
+ ":method": "GET",
108
+ ":scheme": "https",
109
+ ":path": "/image.nyan"
110
+ })
111
+ stream.respond({
112
+ ":status": "200",
113
+ "server": "plum",
114
+ "content-type": "text/html",
115
+ "content-length": body.size
116
+ }, body)
117
+ image = ("iVBORw0KGgoAAAANSUhEUgAAAEAAAABAAgMAAADXB5lNAAAACVBMVEX///93o0jG/4mTMy20AAAA" <<
118
+ "bklEQVQ4y2NgoAoIRQJkCoSimIdTgJGBBU1ABE1A1AVdBQuaACu6gCALhhZ0axlZCDgMWYAB6ilU" <<
119
+ "35IoADEMxWyyBDD45AhQCFahM0kXWIVu3sAJrILzyBcgytoFeATABBcXWohhCEC14BCgGAAAX1ZQ" <<
120
+ "ZtJp0zAAAAAASUVORK5CYII=").unpack("m")[0]
121
+ i_stream.respond({
122
+ ":status": "200",
123
+ "server": "plum",
124
+ "content-type": "image/png",
125
+ "content-length": image.size
126
+ }, image)
127
+ when ["POST", "/post.page"]
128
+ body = "Posted value is: #{CGI.unescape(data).gsub("<", "&lt;").gsub(">", "&gt;")}<br> <a href=/>Back to top page</a>"
129
+ stream.respond({
130
+ ":status": "200",
131
+ "server": "plum",
132
+ "content-type": "text/html",
133
+ "content-length": body.size
134
+ }, body)
135
+ else
136
+ body = "Page not found! <a href=/>Back to top page</a>"
137
+ stream.respond({
138
+ ":status": "404",
139
+ "server": "plum",
140
+ "content-type": "text/html",
141
+ "content-length": body.size
142
+ }, body)
143
+ end
144
+ end
145
+ end
146
+
147
+ Thread.new {
148
+ begin
149
+ plum.run
150
+ rescue
151
+ puts $!
152
+ puts $!.backtrace
153
+ ensure
154
+ sock.close
155
+ end
156
+ }
157
+ end