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 +2 -0
- data/Gemfile +7 -0
- data/README.rdoc +47 -0
- data/Rakefile +8 -0
- data/examples/https.rb +15 -0
- data/examples/spdy.rb +23 -0
- data/lib/net/http/spdy.rb +93 -0
- data/lib/net/http/spdy/generic_request.rb +38 -0
- data/lib/net/http/spdy/response.rb +17 -0
- data/lib/net/http/spdy/stream.rb +142 -0
- data/lib/net/http/spdy/stream_session.rb +113 -0
- data/lib/net/http/spdy/version.rb +5 -0
- data/net-http-spdy.gemspec +23 -0
- data/test/run-test.rb +16 -0
- data/test/test_access_to_web.rb +40 -0
- data/vender/spdy/README.md +43 -0
- data/vender/spdy/lib/spdy.rb +7 -0
- data/vender/spdy/lib/spdy/compat.rb +17 -0
- data/vender/spdy/lib/spdy/compressor.rb +79 -0
- data/vender/spdy/lib/spdy/parser.rb +146 -0
- data/vender/spdy/lib/spdy/protocol.rb +269 -0
- data/vender/spdy/lib/spdy/version.rb +3 -0
- metadata +101 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
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
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,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,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
|
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
|