summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorKazuki Yamaguchi <k@rhe.jp>2015-08-14 17:31:33 +0900
committerKazuki Yamaguchi <k@rhe.jp>2015-08-14 17:31:33 +0900
commit881f8c942d450e28db1bc85928068a22ce37a656 (patch)
tree489528238e7258ca95b515017e51e256e32ba40d
parentcdebfcf496dd65cff5d52667b625d49d1643bf16 (diff)
downloadplum-881f8c942d450e28db1bc85928068a22ce37a656.tar.gz
add support for HTTP/2 over TCP ('http' URI scheme)
-rw-r--r--README.md2
-rw-r--r--examples/local_server.rb1
-rw-r--r--examples/non_tls_server.rb115
-rw-r--r--lib/plum.rb1
-rw-r--r--lib/plum/connection.rb26
-rw-r--r--lib/plum/http_connection.rb88
-rw-r--r--lib/plum/https_connection.rb15
-rw-r--r--plum.gemspec1
8 files changed, 211 insertions, 38 deletions
diff --git a/README.md b/README.md
index 2d9cb52..a19a019 100644
--- a/README.md
+++ b/README.md
@@ -4,9 +4,9 @@ A minimal implementation of HTTP/2 server.
## Requirements
* OpenSSL 1.0.2+
* 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.
+* [http-parser.rb gem](https://rubygems.org/gems/http_parser.rb) if you use "http" URI scheme.
## TODO
-* "http" URIs support (upgrade from HTTP/1.1)
* Stream Priority (RFC 7540 5.3)
* Better API
diff --git a/examples/local_server.rb b/examples/local_server.rb
index c844a87..a777174 100644
--- a/examples/local_server.rb
+++ b/examples/local_server.rb
@@ -11,6 +11,7 @@ CONTENT_TYPES = {
/\.jpg$/ => "image/jpeg",
/\.css$/ => "text/css",
/\.js$/ => "application/javascript",
+ /\.atom$/ => "application/atom+xml",
}
$LOAD_PATH << File.expand_path("../../lib", __FILE__)
diff --git a/examples/non_tls_server.rb b/examples/non_tls_server.rb
new file mode 100644
index 0000000..804e3d2
--- /dev/null
+++ b/examples/non_tls_server.rb
@@ -0,0 +1,115 @@
+$LOAD_PATH << File.expand_path("../../lib", __FILE__)
+require "plum"
+require "socket"
+require "cgi"
+
+def log(con, stream, s)
+ prefix = "[%02x;%02x] " % [con, stream]
+ if s.is_a?(Enumerable)
+ puts s.map {|a| prefix + a.to_s }.join("\n")
+ else
+ puts prefix + s.to_s
+ end
+end
+
+tcp_server = TCPServer.new("0.0.0.0", 40080)
+
+loop do
+ begin
+ sock = tcp_server.accept
+ id = sock.fileno
+ puts "#{id}: accept!"
+ rescue => e
+ STDERR.puts e
+ next
+ end
+
+ plum = Plum::HTTPConnection.new(sock)
+
+ plum.on(:frame) do |frame|
+ log(id, frame.stream_id, "recv: #{frame.inspect}")
+ end
+
+ plum.on(:send_frame) do |frame|
+ log(id, frame.stream_id, "send: #{frame.inspect}")
+ end
+
+ plum.on(:connection_error) do |exception|
+ puts exception
+ puts exception.backtrace
+ end
+
+ plum.on(:stream) do |stream|
+ stream.on(:stream_error) do |exception|
+ puts exception
+ puts exception.backtrace
+ end
+
+ stream.on(:send_deferred) do |frame|
+ log(id, frame.stream_id, "send (deferred): #{frame.inspect}")
+ end
+
+ headers = data = nil
+
+ stream.on(:open) do
+ headers = nil
+ data = ""
+ end
+
+ stream.on(:headers) do |headers_|
+ log(id, stream.id, headers_.map {|name, value| "#{name}: #{value}" })
+ headers = headers_.to_h
+ end
+
+ stream.on(:data) do |data_|
+ log(id, stream.id, data_)
+ data << data_
+ end
+
+ stream.on(:end_stream) do
+ case [headers[":method"], headers[":path"]]
+ when ["GET", "/"]
+ body = "Hello World! <a href=/abc.html>ABC</a> <a href=/fgsd>Not found</a>"
+ body << <<-EOF
+ <form action=post.page method=post>
+ <input type=text name=key value=default_value>
+ <input type=submit>
+ </form>
+ EOF
+ stream.respond({
+ ":status": "200",
+ "server": "plum",
+ "content-type": "text/html",
+ "content-length": body.size
+ }, body)
+ when ["POST", "/post.page"]
+ body = "Posted value is: #{CGI.unescape(data).gsub("<", "&lt;").gsub(">", "&gt;")}<br> <a href=/>Back to top page</a>"
+ stream.respond({
+ ":status": "200",
+ "server": "plum",
+ "content-type": "text/html",
+ "content-length": body.size
+ }, body)
+ else
+ body = "Page not found! <a href=/>Back to top page</a>"
+ stream.respond({
+ ":status": "404",
+ "server": "plum",
+ "content-type": "text/html",
+ "content-length": body.size
+ }, body)
+ end
+ end
+ end
+
+ Thread.new {
+ begin
+ plum.run
+ rescue
+ puts $!
+ puts $!.backtrace
+ ensure
+ sock.close
+ end
+ }
+end
diff --git a/lib/plum.rb b/lib/plum.rb
index 8842d6b..2715ccc 100644
--- a/lib/plum.rb
+++ b/lib/plum.rb
@@ -1,5 +1,6 @@
require "openssl"
require "socket"
+require "base64"
require "plum/version"
require "plum/errors"
require "plum/binary_string"
diff --git a/lib/plum/connection.rb b/lib/plum/connection.rb
index 32a7d2e..2c2536a 100644
--- a/lib/plum/connection.rb
+++ b/lib/plum/connection.rb
@@ -65,10 +65,6 @@ module Plum
receive_frame(frame)
end
end
- rescue ConnectionError => e
- callback(:connection_error, e)
- goaway(e.http2_error_type)
- close
end
alias << receive
@@ -87,8 +83,20 @@ module Plum
@io.write(frame.assemble)
end
+ def negotiate!
+ if CLIENT_CONNECTION_PREFACE.start_with?(@buffer.byteslice(0, 24))
+ if @buffer.bytesize >= 24
+ @buffer.byteshift(24)
+ @state = :waiting_settings
+ settings(@local_settings)
+ end
+ else
+ raise ConnectionError.new(:protocol_error) # (MAY) send GOAWAY. sending.
+ end
+ end
+
def new_stream(stream_id, **args)
- if @streams.size > 0 && @streams.keys.last >= stream_id
+ if @streams.size > 0 && @streams.keys.max >= stream_id
raise Plum::ConnectionError.new(:protocol_error)
end
@@ -140,6 +148,10 @@ module Plum
end
stream.receive_frame(frame)
end
+ rescue ConnectionError => e
+ callback(:connection_error, e)
+ goaway(e.http2_error_type)
+ close
end
def receive_control_frame(frame)
@@ -164,7 +176,7 @@ module Plum
end
end
- def receive_settings(frame)
+ def receive_settings(frame, send_ack: true)
if frame.flags.include?(:ack)
raise ConnectionError.new(:frame_size_error) if frame.length != 0
return
@@ -178,7 +190,7 @@ module Plum
callback(:remote_settings, @remote_settings, old_remote_settings)
- send_immediately Frame.settings(:ack)
+ send_immediately Frame.settings(:ack) if send_ack
end
def apply_remote_settings(old_remote_settings)
diff --git a/lib/plum/http_connection.rb b/lib/plum/http_connection.rb
index bde8b7d..e5e63f6 100644
--- a/lib/plum/http_connection.rb
+++ b/lib/plum/http_connection.rb
@@ -1,33 +1,91 @@
+using Plum::BinaryString
+
module Plum
class HTTPConnection < Connection
def initialize(io, local_settings = {})
+ require "http/parser"
super
+ @_http_parser = setup_parser
+ @_upgrade_retry = false # After sent 426 (Upgrade Required)
end
private
- def negotiate!
- if @buffer.bytesize >= 4
- if CLIENT_CONNECTION_PREFACE.start_with?(@buffer)
- negotiate_with_knowledge
+ def setup_parser
+ headers = nil
+ body = ""
+ parser = HTTP::Parser.new
+ parser.on_message_begin = proc { }
+ parser.on_headers_complete = proc {|_headers| headers = _headers }
+ parser.on_body = proc {|chunk| body << chunk }
+ parser.on_message_complete = proc do |env|
+ # Upgrade from HTTP/1.1
+ heads = headers.map {|n, v| [n.downcase, v] }.to_h
+ connection = heads["connection"] || ""
+ upgrade = heads["upgrade"] || ""
+ settings = heads["http2-settings"]
+
+ if (connection.split(", ").sort == ["Upgrade", "HTTP2-Settings"].sort &&
+ upgrade.split(", ").include?("h2c") &&
+ settings != nil)
+ respond_switching_protocol
+ self.on(:negotiated) {
+ _frame = Frame.new(type: :settings, stream_id: 0, payload: Base64.urlsafe_decode64(settings))
+ receive_settings(_frame, send_ack: false) # HTTP2-Settings
+ process_first_request(parser, heads, body)
+ }
else
- negotiate_with_upgrade
+ respond_not_supported
+ close
end
end
- # next
+
+ parser
end
- def negotiate_with_knowledge
- if @buffer.bytesize >= 24
- if @buffer.byteshift(24) == CLIENT_CONNECTION_PREFACE
- @state = :waiting_settings
- settings(@local_settings)
- end
+ def negotiate!
+ begin
+ super
+ rescue ConnectionError
+ # Upgrade from HTTP/1.1
+ offset = @_http_parser << @buffer
+ @buffer.byteshift(offset)
end
- # next
end
- def negotiate_with_upgrade
- raise NotImplementedError, "Parsing HTTP/1.1 is hard..."
+ def respond_switching_protocol
+ resp = ""
+ resp << "HTTP/1.1 101 Switching Protocols\r\n"
+ resp << "Connection: Upgrade\r\n"
+ resp << "Upgrade: h2c\r\n"
+ resp << "\r\n"
+
+ io.write(resp)
+ end
+
+ def respond_not_supported
+ data = "Use modern web browser with HTTP/2 support."
+
+ resp = ""
+ resp << "HTTP/1.1 505 HTTP Version Not Supported\r\n"
+ resp << "Content-Type: text/plain\r\n"
+ resp << "Content-Length: #{data.bytesize}\r\n"
+ resp << "\r\n"
+ resp << data
+
+ io.write(resp)
+ end
+
+ def process_first_request(parser, heads, dat)
+ stream = new_stream(1)
+ heads = heads.merge({ ":method" => parser.http_method,
+ ":path" => parser.request_url,
+ ":authority" => heads["host"] })
+ .reject {|n, v| ["connection", "http2-settings", "upgrade", "host"].include?(n) }
+ encoder = HPACK::Encoder.new(0, indexing: false) # don't pollute connection's HPACK context
+ headers = Frame.headers(1, encoder.encode(heads), :end_headers) # stream ID is 1
+ headers.split_headers(local_settings[:max_frame_size]).each {|hfrag| stream.receive_frame(hfrag) }
+ data = Frame.data(1, dat, :end_stream)
+ data.split_data(local_settings[:max_frame_size]).each {|dfrag| stream.receive_frame(dfrag) }
end
end
end
diff --git a/lib/plum/https_connection.rb b/lib/plum/https_connection.rb
index d05da40..e96a785 100644
--- a/lib/plum/https_connection.rb
+++ b/lib/plum/https_connection.rb
@@ -5,20 +5,5 @@ module Plum
def initialize(io, local_settings = {})
super
end
-
- private
- def negotiate!
- return if @buffer.empty?
-
- if CLIENT_CONNECTION_PREFACE.start_with?(@buffer.byteslice(0, 24))
- if @buffer.bytesize >= 24
- @buffer.byteshift(24)
- @state = :waiting_settings
- settings(@local_settings)
- end
- else
- raise ConnectionError.new(:protocol_error) # (MAY) send GOAWAY. sending.
- end
- end
end
end
diff --git a/plum.gemspec b/plum.gemspec
index 2292b62..f8bf2a5 100644
--- a/plum.gemspec
+++ b/plum.gemspec
@@ -19,6 +19,7 @@ Gem::Specification.new do |spec|
spec.require_paths = ["lib"]
spec.add_development_dependency "bundler", "~> 1.10"
+ spec.add_development_dependency "http-parser.rb"
spec.add_development_dependency "rake"
spec.add_development_dependency "yard"
spec.add_development_dependency "minitest", "~> 5.7.0"