plum 0.0.1

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