net-http-spdy 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,2 @@
1
+ Gemfile.lock
2
+ pkg/
data/Gemfile ADDED
@@ -0,0 +1,7 @@
1
+ source "http://rubygems.org"
2
+
3
+ gemspec
4
+
5
+ group :test do
6
+ gem 'test-unit', '~> 2.5.3'
7
+ end
data/README.rdoc ADDED
@@ -0,0 +1,47 @@
1
+ = net-http-spdy
2
+
3
+ * https://github.com/authorNari/net-http-spdy
4
+
5
+ == DESCRIPTION:
6
+
7
+ A SPDY HTTP client implementation with extended Net:HTTP.
8
+
9
+ == INSTALL:
10
+
11
+ gem install net-http-spdy
12
+
13
+ == FEATURES/PROBLEMS:
14
+
15
+ * Supports SSL/TLS in Ruby 2.0.0 or later
16
+
17
+ == COMPARING:
18
+
19
+ % time ruby examples/https.rb
20
+ ruby examples/https.rb 0.12s user 0.01s system 4% cpu 2.766 total
21
+ % time ruby examples/spdy.rb
22
+ ruby examples/spdy.rb 0.38s user 0.03s system 26% cpu 1.553 total
23
+
24
+ == LICENSE:
25
+
26
+ (The MIT License)
27
+
28
+ Copyright (c) 2013 Narihiro Nakamura
29
+
30
+ Permission is hereby granted, free of charge, to any person obtaining
31
+ a copy of this software and associated documentation files (the
32
+ 'Software'), to deal in the Software without restriction, including
33
+ without limitation the rights to use, copy, modify, merge, publish,
34
+ distribute, sublicense, and/or sell copies of the Software, and to
35
+ permit persons to whom the Software is furnished to do so, subject to
36
+ the following conditions:
37
+
38
+ The above copyright notice and this permission notice shall be
39
+ included in all copies or substantial portions of the Software.
40
+
41
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
42
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
43
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
44
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
45
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
46
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
47
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
3
+
4
+ task :default => %w(test)
5
+
6
+ task :test do
7
+ sh "/usr/bin/env ruby test/run-test.rb"
8
+ end
data/examples/https.rb ADDED
@@ -0,0 +1,15 @@
1
+ require 'net/http/persistent'
2
+
3
+ flag_uris = %w(
4
+ images_sm/ad_flag.png images_sm/ae_flag.png
5
+ images_sm/af_flag.png images_sm/ag_flag.png
6
+ images_sm/ai_flag.png images_sm/am_flag.png
7
+ images_sm/ao_flag.png images_sm/ar_flag.png
8
+ images_sm/as_flag.png images_sm/at_flag.png).map do |path|
9
+ URI('https://www.modspdy.com/world-flags/' + path)
10
+ end
11
+ uri = URI('https://www.modspdy.com/')
12
+ http = Net::HTTP::Persistent.new
13
+ flag_uris.each do |uri|
14
+ http.request(uri)
15
+ end
data/examples/spdy.rb ADDED
@@ -0,0 +1,23 @@
1
+ require 'net/http/spdy'
2
+
3
+ flag_uris = %w(
4
+ images_sm/ad_flag.png images_sm/ae_flag.png
5
+ images_sm/af_flag.png images_sm/ag_flag.png
6
+ images_sm/ai_flag.png images_sm/am_flag.png
7
+ images_sm/ao_flag.png images_sm/ar_flag.png
8
+ images_sm/as_flag.png images_sm/at_flag.png).map do |path|
9
+ URI('https://www.modspdy.com/world-flags/' + path)
10
+ end
11
+ fetch_threads = []
12
+ uri = URI('https://www.modspdy.com/world-flags/')
13
+ http = Net::HTTP::SPDY.new(uri.host, uri.port)
14
+ http.use_ssl = true
15
+ http.start
16
+ flag_uris.each do |uri|
17
+ req = Net::HTTP::Get.new(uri)
18
+ fetch_threads << Thread.start do
19
+ http.request(req)
20
+ end
21
+ end
22
+ fetch_threads.each(&:join)
23
+ http.finish
@@ -0,0 +1,93 @@
1
+ # -*- coding: utf-8 -*-
2
+ require "bundler"
3
+ Bundler.require(:default)
4
+ require 'net/http'
5
+ $: << File.join(File.dirname(__FILE__), "../../../vender/spdy/lib/")
6
+ require 'spdy'
7
+ require 'net/http/spdy/stream'
8
+ require 'net/http/spdy/stream_session'
9
+ require 'net/http/spdy/generic_request'
10
+ require 'net/http/spdy/response'
11
+ require 'openssl'
12
+
13
+ class Net::HTTP::SPDY < Net::HTTP
14
+ def initialize(address, port = nil)
15
+ super
16
+ @npn_select_cb = ->(protocols) do
17
+ prot = protocols.detect{|pr| pr == "spdy/2"}
18
+ if prot.nil?
19
+ raise "This server doesn't support SPDYv2"
20
+ end
21
+ prot
22
+ end
23
+ end
24
+
25
+ if RUBY_VERSION >= "2.0.0"
26
+ SSL_IVNAMES << :@npn_protocols
27
+ SSL_ATTRIBUTES << :npn_protocols
28
+ SSL_IVNAMES << :@npn_select_cb
29
+ SSL_ATTRIBUTES << :npn_select_cb
30
+ end
31
+
32
+ def connect
33
+ super
34
+ @stream_session = StreamSession.new(@socket)
35
+ end
36
+
37
+ undef :close_on_empty_response= if defined?(self.close_on_empty_response=true)
38
+
39
+ private
40
+
41
+ def transport_request(req)
42
+ begin_transport req
43
+ req.uri = URI((use_ssl? ? "https://" : "http://") + addr_port + req.path)
44
+ stream = @stream_session.create(req.uri)
45
+ res = nil
46
+ res = catch(:response) {
47
+ req.exec stream, @curr_http_version, edit_path(req.path)
48
+ begin
49
+ r = Net::HTTPResponse.read_new(stream)
50
+ end while res.kind_of?(Net::HTTPContinue)
51
+
52
+ r.uri = req.uri
53
+
54
+ r.reading_body(stream, req.response_body_permitted?) {
55
+ yield res if block_given?
56
+ }
57
+ r
58
+ }
59
+ stream.assocs.each do |s|
60
+ if not s.eof?
61
+ begin
62
+ r = Net::HTTPResponse.read_new(s)
63
+ end while r.kind_of?(Net::HTTPContinue)
64
+ yield r if block_given?
65
+ res.associated_responses << r
66
+ end
67
+ end
68
+
69
+ end_transport req, res
70
+ res
71
+ rescue => exception
72
+ D "Conn close because of error #{exception}"
73
+ @socket.close if @socket and not @socket.closed?
74
+ raise exception
75
+ end
76
+
77
+ def end_transport(req, res)
78
+ # nothing to do
79
+ end
80
+
81
+ def sspi_auth?(res)
82
+ return false
83
+ end
84
+
85
+ def keep_alive?(req, res)
86
+ return false
87
+ end
88
+
89
+ def proxy?
90
+ # TODO: supports proxy
91
+ return false
92
+ end
93
+ end
@@ -0,0 +1,38 @@
1
+ class Net::HTTPGenericRequest
2
+ attr_accessor :uri
3
+
4
+ alias exec_wo_spdy exec
5
+ def exec(sock, ver, path) #:nodoc: internal use only
6
+ exec_wo_spdy(sock, ver, path)
7
+ if is_spdy?(sock) && (@body || @body_stream || @body_data)
8
+ sock.close_write
9
+ end
10
+ end
11
+
12
+ private
13
+ alias send_request_with_body_stream_wo_spdy send_request_with_body_stream
14
+ def send_request_with_body_stream(sock, ver, path, f)
15
+ if is_spdy?(sock)
16
+ stream = sock
17
+ write_header stream, ver, path
18
+ wait_for_continue stream, ver if stream.sock.continue_timeout
19
+ IO.copy_stream(f, stream)
20
+ else
21
+ send_request_with_body_stream_wo_spdy
22
+ end
23
+ end
24
+
25
+ alias write_header_wo_spdy write_header
26
+ def write_header(sock, ver, path)
27
+ if is_spdy?(sock)
28
+ stream = sock
29
+ stream.write_headers(@method, path, ver, self)
30
+ else
31
+ write_header_wo_spdy(sock, ver, path)
32
+ end
33
+ end
34
+
35
+ def is_spdy?(sock)
36
+ sock.kind_of?(Net::HTTP::SPDY::Stream)
37
+ end
38
+ end
@@ -0,0 +1,17 @@
1
+ class Net::HTTPResponse
2
+ if RUBY_VERSION <= "2.0.0"
3
+ attr_accessor :uri
4
+ end
5
+
6
+ attr_reader :associated_responses
7
+
8
+ def has_associatd_response?
9
+ @associated_responses ||= []
10
+ not @response.empty?
11
+ end
12
+
13
+ def associated_responses
14
+ @associated_responses ||= []
15
+ return @associated_responses
16
+ end
17
+ end
@@ -0,0 +1,142 @@
1
+ require 'forwardable'
2
+
3
+ class Net::HTTP::SPDY::Stream
4
+ extend Forwardable
5
+
6
+ attr_accessor :buf, :eof, :connected
7
+ attr_reader :sock, :uri, :assocs, :new_assoc, :id
8
+ def_delegators :@sock, :io, :closed?, :close
9
+
10
+ def initialize(id, session, sock, uri)
11
+ @id = id
12
+ @session = session
13
+ @sock = sock
14
+ @buf = ""
15
+ @eof = false
16
+ @uri = uri
17
+ @assocs = []
18
+ @new_assoc = nil
19
+ @connected = false
20
+ end
21
+
22
+ def eof?
23
+ @buf.empty? && (@eof || @sock.eof?)
24
+ end
25
+
26
+ def read(len, dest = '', ignore_eof = false)
27
+ while @buf.size < len
28
+ read_frame(ignore_eof)
29
+ break if ignore_eof && eof?
30
+ end
31
+ dest << rbuf_consume(len)
32
+ return dest
33
+ end
34
+
35
+ def read_all(dest = '')
36
+ dest << rbuf_consume(@buf.size)
37
+ until eof?
38
+ read_frame(false)
39
+ dest << rbuf_consume(@buf.size)
40
+ end
41
+ return dest
42
+ end
43
+
44
+ def readuntil(terminator, ignore_eof = false)
45
+ idx = @buf.index(terminator)
46
+ while idx.nil?
47
+ read_frame(ignore_eof)
48
+ idx = @buf.index(terminator)
49
+ idx = @buf.size if ignore_eof && eof?
50
+ end
51
+ return rbuf_consume(idx + terminator.size)
52
+ end
53
+
54
+ def readline
55
+ readuntil("\n").chop
56
+ end
57
+
58
+ def write(buf)
59
+ d = SPDY::Protocol::Data::Frame.new
60
+ d.create(stream_id: @id, data: buf)
61
+ @session.monitor.synchronize do
62
+ @sock.write d.to_binary_s
63
+ end
64
+ end
65
+ alias << write
66
+
67
+ def write_headers(method, path, ver, request)
68
+ h = {
69
+ 'method' => method,
70
+ 'url' => path,
71
+ 'version' => "HTTP/#{ver}",
72
+ 'scheme' => request.uri.scheme
73
+ }
74
+ h['host'] = request.delete("host").first
75
+
76
+ %w(Connection Host Keep-Alive Proxy-Connection Transfer-Encoding).each do |h|
77
+ raise ArgumentError, "#{h} cannot send with SPDY" if not request[h].nil?
78
+ end
79
+
80
+ sr = SPDY::Protocol::Control::SynStream.new(zlib_session: @session.parser.zlib_session)
81
+ request.each_header do |key, value|
82
+ h[key.downcase] = value
83
+ end
84
+ if request.request_body_permitted?
85
+ sr.create(stream_id: @id, headers: h)
86
+ else
87
+ sr.create(stream_id: @id, headers: h, flags: 1)
88
+ end
89
+ @sock.debug_output.puts h if @sock.debug_output
90
+ @session.monitor.synchronize do
91
+ # wait to open a lower stream
92
+ @session.monitor_cond.wait_while do
93
+ @session.streams.detect{|s| s.id < @id && !s.connected }
94
+ end
95
+ @sock.write sr.to_binary_s
96
+ @connected = true
97
+ @session.monitor_cond.broadcast
98
+ end
99
+ end
100
+
101
+ def close_write
102
+ d = SPDY::Protocol::Data::Frame.new
103
+ d.create(stream_id: @id, flags: 1)
104
+ @session.monitor.synchronize do
105
+ @sock.write d.to_binary_s
106
+ end
107
+ end
108
+
109
+ private
110
+
111
+ BUFSIZE = 1024 * 16
112
+ def read_frame(ignore_eof)
113
+ while buf.empty?
114
+ raise EOFError if eof? && !ignore_eof
115
+ begin
116
+ @session.monitor.synchronize do
117
+ return if not buf.empty?
118
+ s = @sock.io.read_nonblock(BUFSIZE)
119
+ @session.parse(s)
120
+ end
121
+ rescue IO::WaitReadable
122
+ if IO.select([@sock.io], nil, nil, @sock.read_timeout)
123
+ retry
124
+ else
125
+ raise Net::ReadTimeout
126
+ end
127
+ rescue IO::WaitWritable
128
+ if IO.select(nil, [@sock.io], nil, @sock.read_timeout)
129
+ retry
130
+ else
131
+ raise Net::ReadTimeout
132
+ end
133
+ end
134
+ end
135
+ rescue EOFError
136
+ raise unless ignore_eof
137
+ end
138
+
139
+ def rbuf_consume(len)
140
+ @buf.slice!(0, len)
141
+ end
142
+ end
@@ -0,0 +1,113 @@
1
+ # -*- coding: utf-8 -*-
2
+ require 'monitor'
3
+
4
+ class Net::HTTP::SPDY
5
+ class StreamError < StandardError; end
6
+
7
+ class StreamSession
8
+ attr_reader :parser, :monitor, :monitor_cond
9
+
10
+ def initialize(sock)
11
+ @sock = sock
12
+ @highest_id = -1
13
+ @streams = {}
14
+ @parser = create_parser
15
+ @monitor = Monitor.new
16
+ @monitor_cond = @monitor.new_cond
17
+ end
18
+
19
+ def create(uri)
20
+ @monitor.synchronize do
21
+ @highest_id += 2
22
+ end
23
+ return push(@highest_id, uri)
24
+ end
25
+
26
+ def known?(id)
27
+ @streams.has_key?(id)
28
+ end
29
+
30
+ def push(id, uri, connected=false)
31
+ s = Stream.new(id, self, @sock, uri)
32
+ s.connected = connected
33
+ @streams[id] = s
34
+ return @streams[id]
35
+ end
36
+
37
+ def parse(buf)
38
+ @parser << buf
39
+ end
40
+
41
+ def streams
42
+ @streams.values
43
+ end
44
+
45
+ private
46
+
47
+ def create_parser
48
+ parser = SPDY::Parser.new
49
+ parser.on_open do |id, assoc_id, pri|
50
+ @sock.debug_output.puts "on_open: id=<#{id}>,assoc_id=<#{assoc_id}>" if @sock.debug_output
51
+ if assoc_id
52
+ s = @streams.fetch(id)
53
+ s.new_assoc = @assoc_id
54
+ end
55
+ end
56
+ parser.on_headers do |id, headers|
57
+ @sock.debug_output.puts "on_headers: id=<#{id}>, headers=<#{headers}>" if @sock.debug_output
58
+ s = @streams.fetch(id)
59
+ if s.new_assoc
60
+ # TODO: check invalid header and send PROTOCOL_ERROR
61
+ uri = URI(headers.delete('url'))
62
+ assoc = push(assoc_id, uri, true)
63
+ s.new_assoc = nil
64
+ s.assocs << assoc
65
+ else
66
+ s = @streams.fetch(id)
67
+ end
68
+
69
+ # TODO: check invalid header
70
+ h = ["#{headers['version']} #{headers['status']}"]
71
+ code, msg = headers.delete('status').split(" ")
72
+ r = ::Net::HTTPResponse.new(headers.delete('version'), code, msg)
73
+ r.each_capitalized{|k,v| h << "#{k}: #{v}" }
74
+ h << "\r\n"
75
+
76
+ @sock.debug_output.puts %Q[->#{h.join("\r\n").dump}] if @sock.debug_output
77
+ s.buf << h.join("\r\n")
78
+ end
79
+ parser.on_body do |id, data|
80
+ @sock.debug_output.puts "on_body: id=<#{id}>" if @sock.debug_output
81
+ s = @streams.fetch(id)
82
+ @sock.debug_output.puts %Q[->#{data.dump}] if @sock.debug_output
83
+ s.buf << data
84
+ end
85
+ parser.on_message_complete do |id|
86
+ @sock.debug_output.puts "on_message_complete: id=<#{id}>" if @sock.debug_output
87
+ # TODO: send frame to close
88
+ @streams.fetch(id).eof = true
89
+ @streams.delete(id)
90
+ end
91
+ parser.on_reset do |id, status|
92
+ @sock.debug_output.puts "on_reset: id=<#{id}>, status=<#{status}>" if @sock.debug_output
93
+ case status
94
+ when 1
95
+ raise StreamError, 'received PROTOCOL_ERROR'
96
+ when 2
97
+ raise StreamError, 'received INVALID_STREAM'
98
+ when 3
99
+ raise StreamError, 'received REFUSED_STREAM'
100
+ when 4
101
+ raise StreamError, 'received UNSUPPORTED_VERSION'
102
+ when 5
103
+ raise StreamError, 'received CANCEL'
104
+ when 6
105
+ raise StreamError, 'received INTERNAL_ERROR'
106
+ when 7
107
+ raise StreamError, 'received FLOW_CONTROL_ERROR'
108
+ end
109
+ end
110
+ return parser
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,5 @@
1
+ require 'net/http'
2
+
3
+ class Net::HTTP::SPDY < Net::HTTP
4
+ VERSION = '0.0.1'
5
+ end
@@ -0,0 +1,23 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "net/http/spdy/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "net-http-spdy"
7
+ s.version = Net::HTTP::SPDY::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ["Narihiro Nakamura"]
10
+ s.email = ["authornari@gmail.com"]
11
+ s.homepage = "https://github.com/authorNari/spdy"
12
+ s.summary = "A SPDY HTTP client implementation atop Net:HTTP"
13
+ s.description = s.summary
14
+
15
+ s.files = `git ls-files`.split($\)
16
+ s.executables = s.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
+ s.test_files = s.files.grep(%r{^(test|spec|features)/})
18
+ s.name = "net-http-spdy"
19
+ s.require_paths = ["lib"]
20
+
21
+ s.add_dependency "bindata"
22
+ s.add_dependency "ffi-zlib"
23
+ end
data/test/run-test.rb ADDED
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env ruby
2
+ base_dir = File.expand_path(File.dirname(__FILE__))
3
+ top_dir = File.expand_path("..", base_dir)
4
+ $LOAD_PATH.unshift(File.join(top_dir, "lib"))
5
+ $LOAD_PATH.unshift(File.join(top_dir, "test"))
6
+
7
+ require "bundler"
8
+ Bundler.require(:default, :test)
9
+ require "test/unit"
10
+
11
+ require "net/http/spdy"
12
+
13
+ test_file = "./test/test_*.rb"
14
+ Dir.glob(test_file) do |file|
15
+ require file
16
+ end
@@ -0,0 +1,40 @@
1
+ class TestAccessToWeb < Test::Unit::TestCase
2
+ def test_get_google
3
+ uri = URI('https://www.google.com/')
4
+ http = Net::HTTP::SPDY.new(uri.host, uri.port)
5
+ http.use_ssl = true
6
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
7
+ http.start do |http|
8
+ req = Net::HTTP::Get.new(uri)
9
+ res = http.request(req)
10
+ assert_include ["200", "302"], res.code
11
+ end
12
+ end
13
+
14
+ def test_get_world_flags_parallel
15
+ flag_uris = %w(
16
+ images_sm/ad_flag.png images_sm/ae_flag.png
17
+ images_sm/af_flag.png images_sm/ag_flag.png
18
+ images_sm/ai_flag.png images_sm/am_flag.png
19
+ images_sm/ao_flag.png images_sm/ar_flag.png
20
+ images_sm/as_flag.png images_sm/at_flag.png).map do |path|
21
+ URI('https://www.modspdy.com/world-flags/' + path)
22
+ end
23
+ fetch_threads = []
24
+ uri = URI('https://www.modspdy.com/world-flags/')
25
+ http = Net::HTTP::SPDY.new(uri.host, uri.port)
26
+ http.use_ssl = true
27
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
28
+ #http.set_debug_output $stderr
29
+ http.start
30
+ flag_uris.each do |uri|
31
+ req = Net::HTTP::Get.new(uri)
32
+ #fetch_threads << Thread.start do
33
+ res = http.request(req)
34
+ assert_equal "200", res.code
35
+ #end
36
+ end
37
+ fetch_threads.each(&:join)
38
+ http.finish
39
+ end
40
+ end
@@ -0,0 +1,43 @@
1
+ # SPDY
2
+
3
+ SPDY is an experimental protocol designed to reduce latency of web pages. The SPDY v2 draft is the foundation for the HTTP 2.0 initiative led by the HTTPbis working group. In lab tests, SPDY shows 64% reduction in page load times! For more details, check out the [official site](https://sites.google.com/a/chromium.org/dev/spdy).
4
+
5
+ Today, SPDY support is available in Chrome, Firefox, and Opera on the client, and on Apache, Nginx, Jetty, node.js and others on the server. All of Google web services, when running over SSL, are available through SPDY! In other words, if you are using Google products over SSL, chances are, you are fetching the content from Google servers over SPDY, not HTTP.
6
+
7
+ * [HTTPBis - HTTP 2.0 Charter](http://datatracker.ietf.org/wg/httpbis/charter/)
8
+ * [Life beyond HTTP 1.1: Google's SPDY](http://www.igvita.com/2011/04/07/life-beyond-http-11-googles-spdy)
9
+
10
+ ## Protocol Parser
11
+
12
+ SPDY specification (draft 2) defines its own framing and message exchange protocol which is layered on top of a raw TCP connection. This gem implements a basic, pure Ruby parser for the SPDY v2 protocol:
13
+
14
+ ```ruby
15
+ s = SPDY::Parser.new
16
+
17
+ s.on_headers_complete { |stream_id, headers| ... }
18
+ s.on_body { |stream_id, data| ... }
19
+ s.on_message_complete { |stream_id| ... }
20
+
21
+ s << recieved_data
22
+ ```
23
+
24
+ However, parsing the data is not enough, to do the full exchange you also have to respond to a SPDY client with appropriate 'control' and 'data' frames:
25
+
26
+ ```ruby
27
+ sr = SPDY::Protocol::Control::SynReply.new
28
+ headers = {'Content-Type' => 'text/plain', 'status' => '200 OK', 'version' => 'HTTP/1.1'}
29
+ sr.create(:stream_id => 1, :headers => headers)
30
+ send_data sr.to_binary_s
31
+
32
+ # or, to send a data frame
33
+
34
+ d = SPDY::Protocol::Data::Frame.new
35
+ d.create(:stream_id => 1, :data => "This is SPDY.")
36
+ send_data d.to_binary_s
37
+ ```
38
+
39
+ See example eventmachine server in *examples/spdy_server.rb* for a minimal SPDY "hello world" server.
40
+
41
+ ### License
42
+
43
+ MIT License - Copyright (c) 2011 Ilya Grigorik
@@ -0,0 +1,7 @@
1
+ require 'bindata'
2
+ require 'ffi/zlib'
3
+
4
+ require 'spdy/compat'
5
+ require 'spdy/protocol'
6
+ require 'spdy/compressor'
7
+ require 'spdy/parser'
@@ -0,0 +1,17 @@
1
+ if Object.instance_methods.include? "type"
2
+ class BinData::DSLMixin::DSLParser
3
+ def name_is_reserved_in_1_8?(name)
4
+ return false if name == "type"
5
+ name_is_reserved_in_1_9?(name)
6
+ end
7
+ alias_method :name_is_reserved_in_1_9?, :name_is_reserved?
8
+ alias_method :name_is_reserved?, :name_is_reserved_in_1_8?
9
+
10
+ def name_shadows_method_in_1_8?(name)
11
+ return false if name == "type"
12
+ name_shadows_method_in_1_9?(name)
13
+ end
14
+ alias_method :name_shadows_method_in_1_9?, :name_shadows_method?
15
+ alias_method :name_shadows_method?, :name_shadows_method_in_1_8?
16
+ end
17
+ end
@@ -0,0 +1,79 @@
1
+ module SPDY
2
+ class Zlib
3
+
4
+ DICT = \
5
+ "optionsgetheadpostputdeletetraceacceptaccept-charsetaccept-encodingaccept-" \
6
+ "languageauthorizationexpectfromhostif-modified-sinceif-matchif-none-matchi" \
7
+ "f-rangeif-unmodifiedsincemax-forwardsproxy-authorizationrangerefererteuser" \
8
+ "-agent10010120020120220320420520630030130230330430530630740040140240340440" \
9
+ "5406407408409410411412413414415416417500501502503504505accept-rangesageeta" \
10
+ "glocationproxy-authenticatepublicretry-afterservervarywarningwww-authentic" \
11
+ "ateallowcontent-basecontent-encodingcache-controlconnectiondatetrailertran" \
12
+ "sfer-encodingupgradeviawarningcontent-languagecontent-lengthcontent-locati" \
13
+ "oncontent-md5content-rangecontent-typeetagexpireslast-modifiedset-cookieMo" \
14
+ "ndayTuesdayWednesdayThursdayFridaySaturdaySundayJanFebMarAprMayJunJulAugSe" \
15
+ "pOctNovDecchunkedtext/htmlimage/pngimage/jpgimage/gifapplication/xmlapplic" \
16
+ "ation/xhtmltext/plainpublicmax-agecharset=iso-8859-1utf-8gzipdeflateHTTP/1" \
17
+ ".1statusversionurl\0"
18
+
19
+ CHUNK = 10*1024 # this is silly, but it'll do for now
20
+
21
+ def initialize
22
+ @inflate_zstream = FFI::Zlib::Z_stream.new
23
+ result = FFI::Zlib.inflateInit(@inflate_zstream)
24
+ raise "invalid stream" if result != FFI::Zlib::Z_OK
25
+
26
+ @deflate_zstream = FFI::Zlib::Z_stream.new
27
+ result = FFI::Zlib.deflateInit(@deflate_zstream, FFI::Zlib::Z_DEFAULT_COMPRESSION)
28
+ raise "invalid stream" if result != FFI::Zlib::Z_OK
29
+
30
+ result = FFI::Zlib.deflateSetDictionary(@deflate_zstream, DICT, DICT.size)
31
+ raise "invalid dictionary" if result != FFI::Zlib::Z_OK
32
+ end
33
+
34
+ def reset
35
+ result = FFI::Zlib.inflateReset(@inflate_zstream)
36
+ raise "invalid stream" if result != FFI::Zlib::Z_OK
37
+
38
+ result = FFI::Zlib.deflateReset(@deflate_zstream)
39
+ raise "invalid stream" if result != FFI::Zlib::Z_OK
40
+ end
41
+
42
+ def inflate(data)
43
+ in_buf = FFI::MemoryPointer.from_string(data)
44
+ out_buf = FFI::MemoryPointer.new(CHUNK)
45
+
46
+ @inflate_zstream[:avail_in] = in_buf.size-1
47
+ @inflate_zstream[:avail_out] = CHUNK
48
+ @inflate_zstream[:next_in] = in_buf
49
+ @inflate_zstream[:next_out] = out_buf
50
+
51
+ result = FFI::Zlib.inflate(@inflate_zstream, FFI::Zlib::Z_SYNC_FLUSH)
52
+ if result == FFI::Zlib::Z_NEED_DICT
53
+ result = FFI::Zlib.inflateSetDictionary(@inflate_zstream, DICT, DICT.size)
54
+ raise "invalid dictionary" if result != FFI::Zlib::Z_OK
55
+
56
+ result = FFI::Zlib.inflate(@inflate_zstream, FFI::Zlib::Z_SYNC_FLUSH)
57
+ end
58
+
59
+ raise "cannot inflate" if result != FFI::Zlib::Z_OK && result != FFI::Zlib::Z_STREAM_END
60
+
61
+ out_buf.get_bytes(0, CHUNK - @inflate_zstream[:avail_out])
62
+ end
63
+
64
+ def deflate(data)
65
+ in_buf = FFI::MemoryPointer.from_string(data)
66
+ out_buf = FFI::MemoryPointer.new(CHUNK)
67
+
68
+ @deflate_zstream[:avail_in] = in_buf.size-1
69
+ @deflate_zstream[:avail_out] = CHUNK
70
+ @deflate_zstream[:next_in] = in_buf
71
+ @deflate_zstream[:next_out] = out_buf
72
+
73
+ result = FFI::Zlib.deflate(@deflate_zstream, FFI::Zlib::Z_SYNC_FLUSH)
74
+ raise "cannot deflate" if result != FFI::Zlib::Z_OK
75
+
76
+ out_buf.get_bytes(0, CHUNK - @deflate_zstream[:avail_out])
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,146 @@
1
+ module SPDY
2
+ class Parser
3
+ include Protocol
4
+
5
+ attr :zlib_session
6
+
7
+ def initialize
8
+ @buffer = ''
9
+ @zlib_session = Zlib.new
10
+ end
11
+
12
+ def <<(data)
13
+ @buffer << data
14
+ try_parse
15
+ end
16
+
17
+ def on_open(&blk)
18
+ @on_open = blk
19
+ end
20
+
21
+ def on_ping(&blk)
22
+ @on_ping = blk
23
+ end
24
+
25
+ def on_headers(&blk)
26
+ @on_headers = blk
27
+ end
28
+
29
+ def on_settings(&blk)
30
+ @on_settings = blk
31
+ end
32
+
33
+ def on_body(&blk)
34
+ @on_body = blk
35
+ end
36
+
37
+ def on_message_complete(&blk)
38
+ @on_message_complete = blk
39
+ end
40
+
41
+ def on_reset(&blk)
42
+ @on_reset = blk
43
+ end
44
+
45
+ private
46
+
47
+ def handle_headers(pckt)
48
+ headers = pckt.uncompressed_data.to_h
49
+ if @on_headers && !headers.empty?
50
+ @on_headers.call(pckt.header.stream_id.to_i,
51
+ headers)
52
+ end
53
+ end
54
+
55
+ def try_parse
56
+ return if @buffer.empty?
57
+ type = @buffer[0,1].unpack('C').first >> 7 & 0x01
58
+ pckt = nil
59
+
60
+ case type
61
+ when CONTROL_BIT
62
+ return if @buffer.size < 12
63
+ pckt = Control::Header.new.read(@buffer[0,12])
64
+
65
+ case pckt.type.to_i
66
+ when 1 then # SYN_STREAM
67
+ pckt = Control::SynStream.new({:zlib_session => @zlib_session})
68
+ pckt.parse(@buffer)
69
+
70
+ if @on_open
71
+ @on_open.call(pckt.header.stream_id.to_i,
72
+ (pckt.associated_to_stream_id.to_i rescue nil),
73
+ (pckt.pri.to_i rescue nil))
74
+ end
75
+ handle_headers(pckt)
76
+
77
+ @on_message_complete.call(pckt.header.stream_id) if @on_message_complete && fin?(pckt.header)
78
+
79
+ when 2 then # SYN_REPLY
80
+ pckt = Control::SynReply.new({:zlib_session => @zlib_session})
81
+ pckt.parse(@buffer)
82
+ handle_headers(pckt)
83
+
84
+ @on_message_complete.call(pckt.header.stream_id) if @on_message_complete && fin?(pckt.header)
85
+
86
+ when 3 then # RST_STREAM
87
+ return if @buffer.size < 16
88
+ pckt = Control::RstStream.new({:zlib_session => @zlib_session})
89
+ pckt.read(@buffer)
90
+
91
+ @on_reset.call(pckt.stream_id, pckt.status_code) if @on_reset
92
+
93
+ when 4 then # SETTINGS
94
+ return if @buffer.size < 16
95
+ pckt = Control::Settings.new({:zlib_session => @zlib_session})
96
+ pckt.read(@buffer)
97
+
98
+ @on_settings.call(pckt.stream_id, pckt.status_code) if @on_settings
99
+
100
+ when 6 then # PING
101
+ pckt = Control::Ping.new({:zlib_session => @zlib_session})
102
+ pckt.read(@buffer)
103
+
104
+ @on_ping.call(pckt.ping_id) if @on_ping
105
+
106
+
107
+
108
+ when 8 then # HEADERS
109
+ pckt = Control::Headers.new({:zlib_session => @zlib_session})
110
+ pckt.parse(@buffer)
111
+
112
+ @on_headers.call(pckt.header.stream_id, pckt.uncompressed_data.to_h) if @on_headers
113
+
114
+ else
115
+ raise "invalid control frame: #{pckt.type}"
116
+ end
117
+
118
+ when DATA_BIT
119
+ return if @buffer.size < 8
120
+
121
+ pckt = Data::Frame.new.read(@buffer)
122
+ @on_body.call(pckt.stream_id, pckt.data) if @on_body
123
+ @on_message_complete.call(pckt.stream_id) if @on_message_complete && fin?(pckt)
124
+
125
+ else
126
+ raise 'unknown packet type'
127
+ end
128
+
129
+ # remove parsed data from the buffer
130
+ @buffer.slice!(0...pckt.num_bytes)
131
+
132
+ # try parsing another frame
133
+ try_parse
134
+
135
+ rescue IOError => e
136
+ # rescue partial parse and wait for more data
137
+ end
138
+
139
+ private
140
+
141
+ def fin?(packet)
142
+ (packet.flags == 1) rescue false
143
+ end
144
+
145
+ end
146
+ end
@@ -0,0 +1,269 @@
1
+ module SPDY
2
+ module Protocol
3
+
4
+ CONTROL_BIT = 1
5
+ DATA_BIT = 0
6
+ VERSION = 2
7
+
8
+ SETTINGS_UPLOAD_BANDWIDTH = 1
9
+ SETTINGS_DOWNLOAD_BANDWIDTH = 2
10
+ SETTINGS_ROUND_TRIP_TIME = 3
11
+ SETTINGS_MAX_CONCURRENT_STREAMS = 4
12
+ SETTINGS_CURRENT_CWND = 5
13
+
14
+ module Control
15
+ module Helpers
16
+ def initialize_instance
17
+ super
18
+ @zlib_session = @params[:zlib_session]
19
+ end
20
+
21
+ def parse(chunk)
22
+ head = Control::Header.new.read(chunk)
23
+ self.read(chunk)
24
+
25
+ if data.length > 0
26
+ data = @zlib_session.inflate(self.data.to_s)
27
+ self.uncompressed_data = NV.new.read(data)
28
+ else
29
+ self.uncompressed_data = NV.new
30
+ end
31
+
32
+ self
33
+ end
34
+
35
+ def build(opts = {})
36
+ self.header.type = opts[:type]
37
+ self.header.len = opts[:len]
38
+
39
+ self.header.flags = opts[:flags] || 0
40
+ self.header.stream_id = opts[:stream_id]
41
+
42
+ nv = SPDY::Protocol::NV.new
43
+ nv.create(opts[:headers])
44
+
45
+ nv = @zlib_session.deflate(nv.to_binary_s)
46
+ self.header.len = self.header.len.to_i + nv.size
47
+
48
+ self.data = nv
49
+
50
+ self
51
+ end
52
+ end
53
+
54
+ class Header < BinData::Record
55
+ hide :u1
56
+
57
+ bit1 :frame, :initial_value => CONTROL_BIT
58
+ bit15 :version, :initial_value => VERSION
59
+ bit16 :type
60
+
61
+ bit8 :flags
62
+ bit24 :len
63
+
64
+ bit1 :u1
65
+ bit31 :stream_id
66
+ end
67
+
68
+ class SynStream < BinData::Record
69
+ attr_accessor :uncompressed_data
70
+ include Helpers
71
+
72
+ hide :u1, :u2
73
+
74
+ header :header
75
+
76
+ bit1 :u1
77
+ bit31 :associated_to_stream_id
78
+
79
+ bit2 :pri
80
+ bit14 :u2
81
+
82
+ string :data, :read_length => lambda { header.len - 10 }
83
+
84
+ def create(opts = {})
85
+ build({:type => 1, :len => 10}.merge(opts))
86
+ end
87
+ end
88
+
89
+ class SynReply < BinData::Record
90
+ attr_accessor :uncompressed_data
91
+ include Helpers
92
+
93
+ header :header
94
+ bit16 :unused
95
+ string :data, :read_length => lambda { header.len - 6 }
96
+
97
+ def create(opts = {})
98
+ build({:type => 2, :len => 6}.merge(opts))
99
+ end
100
+ end
101
+
102
+ class RstStream < BinData::Record
103
+ hide :u1
104
+
105
+ bit1 :frame, :initial_value => CONTROL_BIT
106
+ bit15 :version, :initial_value => VERSION
107
+ bit16 :type, :value => 3
108
+
109
+ bit8 :flags, :value => 0
110
+ bit24 :len, :value => 8
111
+
112
+ bit1 :u1
113
+ bit31 :stream_id
114
+
115
+ bit32 :status_code
116
+
117
+ def parse(chunk)
118
+ self.read(chunk)
119
+ self
120
+ end
121
+
122
+ def create(opts = {})
123
+ self.stream_id = opts.fetch(:stream_id, 1)
124
+ self.status_code = opts.fetch(:status_code, 5)
125
+ self
126
+ end
127
+ end
128
+
129
+ class Settings < BinData::Record
130
+ bit1 :frame, :initial_value => CONTROL_BIT
131
+ bit15 :version, :initial_value => VERSION
132
+ bit16 :type, :value => 4
133
+
134
+ bit8 :flags
135
+ bit24 :len, :value => lambda { pairs * 8 }
136
+ bit32 :pairs
137
+
138
+ array :headers, :initial_length => :pairs do
139
+ bit32 :id_data
140
+ bit32 :value_data
141
+ end
142
+
143
+ def parse(chunk)
144
+ self.read(chunk)
145
+ self
146
+ end
147
+
148
+ def create(opts = {})
149
+ opts.each do |k, v|
150
+ key = SPDY::Protocol.const_get(k.to_s.upcase)
151
+ self.headers << { :id_data => key , :value_data => v }
152
+ end
153
+ self.pairs = opts.size
154
+ self
155
+ end
156
+ end
157
+
158
+ class Ping < BinData::Record
159
+ bit1 :frame, :initial_value => CONTROL_BIT
160
+ bit15 :version, :initial_value => VERSION
161
+ bit16 :type, :value => 6
162
+
163
+ bit8 :flags, :value => 0
164
+ bit24 :len, :value => 4
165
+
166
+ bit32 :ping_id
167
+
168
+ def parse(chunk)
169
+ self.read(chunk)
170
+ self
171
+ end
172
+
173
+ def create(opts = {})
174
+ self.ping_id = opts.fetch(:ping_id, 1)
175
+ self
176
+ end
177
+ end
178
+
179
+ class Goaway < BinData::Record
180
+ hide :u1
181
+
182
+ bit1 :frame, :initial_value => CONTROL_BIT
183
+ bit15 :version, :initial_value => VERSION
184
+ bit16 :type, :value => 7
185
+
186
+ bit8 :flags, :value => 0
187
+ bit24 :len, :value => 4
188
+
189
+ bit1 :u1
190
+ bit31 :stream_id
191
+
192
+ def parse(chunk)
193
+ self.read(chunk)
194
+ self
195
+ end
196
+
197
+ def create(opts = {})
198
+ self.stream_id = opts.fetch(:stream_id, 1)
199
+ self
200
+ end
201
+ end
202
+
203
+
204
+ class Headers < BinData::Record
205
+ attr_accessor :uncompressed_data
206
+ include Helpers
207
+
208
+ header :header
209
+ bit16 :unused
210
+ string :data, :read_length => lambda { header.len - 6 }
211
+
212
+ def create(opts = {})
213
+ build({:type => 8, :len => 6}.merge(opts))
214
+ end
215
+ end
216
+ end
217
+
218
+ module Data
219
+ class Frame < BinData::Record
220
+ bit1 :frame, :initial_value => DATA_BIT
221
+ bit31 :stream_id
222
+
223
+ bit8 :flags, :initial_value => 0
224
+ bit24 :len, :initial_value => 0
225
+
226
+ string :data, :read_length => :len
227
+
228
+ def create(opts = {})
229
+ self.stream_id = opts[:stream_id]
230
+ self.flags = opts[:flags] if opts[:flags]
231
+
232
+ if opts[:data]
233
+ self.len = opts[:data].size
234
+ self.data = opts[:data]
235
+ end
236
+
237
+ self
238
+ end
239
+ end
240
+ end
241
+
242
+ class NV < BinData::Record
243
+ bit16 :pairs
244
+ array :headers, :initial_length => :pairs do
245
+ bit16 :name_len
246
+ string :name_data, :read_length => :name_len
247
+
248
+ bit16 :value_len
249
+ string :value_data, :read_length => :value_len
250
+ end
251
+
252
+ def create(opts = {})
253
+ opts.each do |k, v|
254
+ self.headers << {:name_len => k.size, :name_data => k.downcase, :value_len => v.size, :value_data => v}
255
+ end
256
+
257
+ self.pairs = opts.size
258
+ self
259
+ end
260
+
261
+ def to_h
262
+ headers.inject({}) do |h, v|
263
+ h[v.name_data.to_s] = v.value_data.to_s
264
+ h
265
+ end
266
+ end
267
+ end
268
+ end
269
+ end
@@ -0,0 +1,3 @@
1
+ module Spdy
2
+ VERSION = "0.0.4"
3
+ end
metadata ADDED
@@ -0,0 +1,101 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: net-http-spdy
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Narihiro Nakamura
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-01-19 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: bindata
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ - !ruby/object:Gem::Dependency
31
+ name: ffi-zlib
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ description: A SPDY HTTP client implementation atop Net:HTTP
47
+ email:
48
+ - authornari@gmail.com
49
+ executables: []
50
+ extensions: []
51
+ extra_rdoc_files: []
52
+ files:
53
+ - .gitignore
54
+ - Gemfile
55
+ - README.rdoc
56
+ - Rakefile
57
+ - examples/https.rb
58
+ - examples/spdy.rb
59
+ - lib/net/http/spdy.rb
60
+ - lib/net/http/spdy/generic_request.rb
61
+ - lib/net/http/spdy/response.rb
62
+ - lib/net/http/spdy/stream.rb
63
+ - lib/net/http/spdy/stream_session.rb
64
+ - lib/net/http/spdy/version.rb
65
+ - net-http-spdy.gemspec
66
+ - test/run-test.rb
67
+ - test/test_access_to_web.rb
68
+ - vender/spdy/README.md
69
+ - vender/spdy/lib/spdy.rb
70
+ - vender/spdy/lib/spdy/compat.rb
71
+ - vender/spdy/lib/spdy/compressor.rb
72
+ - vender/spdy/lib/spdy/parser.rb
73
+ - vender/spdy/lib/spdy/protocol.rb
74
+ - vender/spdy/lib/spdy/version.rb
75
+ homepage: https://github.com/authorNari/spdy
76
+ licenses: []
77
+ post_install_message:
78
+ rdoc_options: []
79
+ require_paths:
80
+ - lib
81
+ required_ruby_version: !ruby/object:Gem::Requirement
82
+ none: false
83
+ requirements:
84
+ - - ! '>='
85
+ - !ruby/object:Gem::Version
86
+ version: '0'
87
+ required_rubygems_version: !ruby/object:Gem::Requirement
88
+ none: false
89
+ requirements:
90
+ - - ! '>='
91
+ - !ruby/object:Gem::Version
92
+ version: '0'
93
+ requirements: []
94
+ rubyforge_project:
95
+ rubygems_version: 1.8.23
96
+ signing_key:
97
+ specification_version: 3
98
+ summary: A SPDY HTTP client implementation atop Net:HTTP
99
+ test_files:
100
+ - test/run-test.rb
101
+ - test/test_access_to_web.rb