spdy 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +5 -0
- data/.rspec +0 -0
- data/Gemfile +3 -0
- data/README.md +42 -0
- data/Rakefile +7 -0
- data/examples/spdy_server.rb +54 -0
- data/lib/spdy/compressor.rb +68 -0
- data/lib/spdy/parser.rb +88 -0
- data/lib/spdy/protocol.rb +122 -0
- data/lib/spdy/version.rb +3 -0
- data/lib/spdy.rb +6 -0
- data/spdy.gemspec +25 -0
- data/spec/compressor_spec.rb +14 -0
- data/spec/helper.rb +15 -0
- data/spec/parser_spec.rb +115 -0
- data/spec/protocol_spec.rb +63 -0
- metadata +107 -0
data/.rspec
ADDED
File without changes
|
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
# SPDY: An experimental protocol for a faster web
|
2
|
+
|
3
|
+
SPDY was developed at Google as part of the "let's make the web faster" initiative. SPDY ("SPeeDY") is an application-layer protocol for transporting content over the web, designed specifically for minimal latency. 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 is built into Chrome + Google web-server infrastructure and is currently serving over 90% of all the SSL traffic. Yes, you read that right.. If you're using Chrome, and you're using Google products, chances are, you are fetching the content from Google servers over SPDY, not HTTP.
|
6
|
+
|
7
|
+
See: [Life beyond HTTP 1.1: Google's SPDY](http://www.igvita.com/2011/04/07/life-beyond-http-11-googles-spdy)
|
8
|
+
|
9
|
+
## Protocol Parser
|
10
|
+
|
11
|
+
SPDY specification (Draft 2) defines its own framing and message exchange protocol which is layered on top of a raw TCP/SSL connection. This gem implements a basic, pure Ruby parser for the SPDY protocol:
|
12
|
+
|
13
|
+
s = SPDY::Parser.new
|
14
|
+
|
15
|
+
s.on_headers_complete { |stream_id, associated_stream, priority, headers| ... }
|
16
|
+
s.on_body { |stream_id, data| ... }
|
17
|
+
s.on_message_complete { |stream_id| ... }
|
18
|
+
|
19
|
+
s << recieved_data
|
20
|
+
|
21
|
+
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:
|
22
|
+
|
23
|
+
sr = SPDY::Protocol::Control::SynReply.new
|
24
|
+
headers = {'Content-Type' => 'text/plain', 'status' => '200 OK', 'version' => 'HTTP/1.1'}
|
25
|
+
sr.create(:stream_id => 1, :headers => headers)
|
26
|
+
send_data sr.to_binary_s
|
27
|
+
|
28
|
+
# or, to send a data frame
|
29
|
+
|
30
|
+
d = SPDY::Protocol::Data::Frame.new
|
31
|
+
d.create(:stream_id => 1, :data => "This is SPDY.")
|
32
|
+
send_data d.to_binary_s
|
33
|
+
|
34
|
+
See example eventmachine server in *examples/spdy_server.rb* for a minimal SPDY "hello world" server.
|
35
|
+
|
36
|
+
## Todo:
|
37
|
+
|
38
|
+
- implement support for all [control frames](https://sites.google.com/a/chromium.org/dev/spdy/spdy-protocol/spdy-protocol-draft2#TOC-Control-frames1)
|
39
|
+
|
40
|
+
### License
|
41
|
+
|
42
|
+
(MIT License) - Copyright (c) 2011 Ilya Grigorik
|
data/Rakefile
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
$: << 'lib' << '../lib'
|
2
|
+
|
3
|
+
require 'eventmachine'
|
4
|
+
require 'spdy'
|
5
|
+
|
6
|
+
class SPDYHandler < EM::Connection
|
7
|
+
def post_init
|
8
|
+
@parser = SPDY::Parser.new
|
9
|
+
@parser.on_headers_complete do |stream_id, associated_stream, priority, headers|
|
10
|
+
p [:SPDY_HEADERS, headers]
|
11
|
+
|
12
|
+
sr = SPDY::Protocol::Control::SynReply.new
|
13
|
+
h = {'Content-Type' => 'text/plain', 'status' => '200 OK', 'version' => 'HTTP/1.1'}
|
14
|
+
sr.create(:stream_id => 1, :headers => h)
|
15
|
+
send_data sr.to_binary_s
|
16
|
+
|
17
|
+
p [:SPDY, :sent, :SYN_REPLY]
|
18
|
+
|
19
|
+
d = SPDY::Protocol::Data::Frame.new
|
20
|
+
d.create(:stream_id => 1, :data => "This is SPDY.")
|
21
|
+
send_data d.to_binary_s
|
22
|
+
|
23
|
+
p [:SPDY, :sent, :DATA]
|
24
|
+
|
25
|
+
d = SPDY::Protocol::Data::Frame.new
|
26
|
+
d.create(:stream_id => 1, :flags => 1)
|
27
|
+
send_data d.to_binary_s
|
28
|
+
|
29
|
+
p [:SPDY, :sent, :DATA_FIN]
|
30
|
+
end
|
31
|
+
|
32
|
+
start_tls
|
33
|
+
end
|
34
|
+
|
35
|
+
def receive_data(data)
|
36
|
+
@parser << data
|
37
|
+
end
|
38
|
+
|
39
|
+
def unbind
|
40
|
+
p [:SPDY, :connection_closed]
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
EM.run do
|
45
|
+
EM.start_server '0.0.0.0', 10000, SPDYHandler
|
46
|
+
end
|
47
|
+
|
48
|
+
# (1) start the SPDY eventmachine server
|
49
|
+
# > ruby spdy_server.rb
|
50
|
+
#
|
51
|
+
# (2) start Chrome and force it to use SPDY over SSL.. on OSX:
|
52
|
+
# > /Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --use-spdy=ssl
|
53
|
+
#
|
54
|
+
# (3) visit https://localhost:1000/
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module SPDY
|
2
|
+
module 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 self.inflate(data)
|
22
|
+
in_buf = FFI::MemoryPointer.from_string(data)
|
23
|
+
out_buf = FFI::MemoryPointer.new(CHUNK)
|
24
|
+
|
25
|
+
zstream = FFI::Zlib::Z_stream.new
|
26
|
+
zstream[:avail_in] = in_buf.size
|
27
|
+
zstream[:avail_out] = CHUNK
|
28
|
+
zstream[:next_in] = in_buf
|
29
|
+
zstream[:next_out] = out_buf
|
30
|
+
|
31
|
+
result = FFI::Zlib.inflateInit(zstream)
|
32
|
+
raise "invalid stream" if result != FFI::Zlib::Z_OK
|
33
|
+
|
34
|
+
result = FFI::Zlib.inflate(zstream, FFI::Zlib::Z_SYNC_FLUSH)
|
35
|
+
raise "invalid stream" if result != FFI::Zlib::Z_NEED_DICT
|
36
|
+
|
37
|
+
result = FFI::Zlib.inflateSetDictionary(zstream, DICT, DICT.size)
|
38
|
+
raise "invalid dictionary" if result != FFI::Zlib::Z_OK
|
39
|
+
|
40
|
+
result = FFI::Zlib.inflate(zstream, FFI::Zlib::Z_SYNC_FLUSH)
|
41
|
+
raise "cannot inflate" if result != FFI::Zlib::Z_OK
|
42
|
+
|
43
|
+
out_buf.get_bytes(0, zstream[:total_out])
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.deflate(data)
|
47
|
+
in_buf = FFI::MemoryPointer.from_string(data)
|
48
|
+
out_buf = FFI::MemoryPointer.new(CHUNK)
|
49
|
+
|
50
|
+
zstream = FFI::Zlib::Z_stream.new
|
51
|
+
zstream[:avail_in] = in_buf.size-1
|
52
|
+
zstream[:avail_out] = CHUNK
|
53
|
+
zstream[:next_in] = in_buf
|
54
|
+
zstream[:next_out] = out_buf
|
55
|
+
|
56
|
+
result = FFI::Zlib.deflateInit(zstream, -1)
|
57
|
+
raise "invalid stream" if result != FFI::Zlib::Z_OK
|
58
|
+
|
59
|
+
result = FFI::Zlib.deflateSetDictionary(zstream, DICT, DICT.size)
|
60
|
+
raise "invalid dictionary" if result != FFI::Zlib::Z_OK
|
61
|
+
|
62
|
+
result = FFI::Zlib.deflate(zstream, FFI::Zlib::Z_SYNC_FLUSH)
|
63
|
+
raise "cannot deflate" if result != FFI::Zlib::Z_OK
|
64
|
+
|
65
|
+
out_buf.get_bytes(0, zstream[:total_out])
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
data/lib/spdy/parser.rb
ADDED
@@ -0,0 +1,88 @@
|
|
1
|
+
module SPDY
|
2
|
+
class Parser
|
3
|
+
include Protocol
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
@buffer = ''
|
7
|
+
end
|
8
|
+
|
9
|
+
def <<(data)
|
10
|
+
@buffer << data
|
11
|
+
try_parse
|
12
|
+
end
|
13
|
+
|
14
|
+
def on_headers_complete(&blk)
|
15
|
+
@on_headers_complete = blk
|
16
|
+
end
|
17
|
+
|
18
|
+
def on_body(&blk)
|
19
|
+
@on_body = blk
|
20
|
+
end
|
21
|
+
|
22
|
+
def on_message_complete(&blk)
|
23
|
+
@on_message_complete = blk
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def try_parse
|
29
|
+
type = @buffer[0,1].unpack('C').first >> 7 & 0x01
|
30
|
+
pckt = nil
|
31
|
+
|
32
|
+
case type
|
33
|
+
when CONTROL_BIT
|
34
|
+
return if @buffer.size < 12
|
35
|
+
pckt = Control::Header.new.read(@buffer[0,12])
|
36
|
+
|
37
|
+
case pckt.type.to_i
|
38
|
+
when 1 then # SYN_STREAM
|
39
|
+
pckt = Control::SynStream.new
|
40
|
+
pckt.read(@buffer)
|
41
|
+
|
42
|
+
headers = {}
|
43
|
+
if pckt.data.size > 0
|
44
|
+
data = Zlib.inflate(pckt.data.to_s)
|
45
|
+
headers = NV.new.read(data).to_h
|
46
|
+
end
|
47
|
+
|
48
|
+
if @on_headers_complete
|
49
|
+
@on_headers_complete.call(pckt.header.stream_id.to_i,
|
50
|
+
pckt.associated_to_stream_id.to_i,
|
51
|
+
pckt.pri.to_i,
|
52
|
+
headers)
|
53
|
+
end
|
54
|
+
|
55
|
+
when 2 then # SYN_REPLY
|
56
|
+
raise 'SYN_REPLY not handled yet'
|
57
|
+
else
|
58
|
+
raise 'invalid control frame'
|
59
|
+
end
|
60
|
+
|
61
|
+
@on_message_complete.call(pckt.header.stream_id) if @on_message_complete && fin?(pckt.header)
|
62
|
+
|
63
|
+
when DATA_BIT
|
64
|
+
return if @buffer.size < 8
|
65
|
+
|
66
|
+
pckt = Data::Frame.new.read(@buffer)
|
67
|
+
@on_body.call(pckt.stream_id, pckt.data) if @on_body
|
68
|
+
@on_message_complete.call(pckt.stream_id) if @on_message_complete && fin?(pckt)
|
69
|
+
|
70
|
+
else
|
71
|
+
raise 'unknown packet type'
|
72
|
+
end
|
73
|
+
|
74
|
+
# remove parsed data from the buffer
|
75
|
+
@buffer.slice!(0..pckt.num_bytes)
|
76
|
+
|
77
|
+
rescue IOError
|
78
|
+
# rescue partial parse and wait for more data
|
79
|
+
end
|
80
|
+
|
81
|
+
private
|
82
|
+
|
83
|
+
def fin?(packet)
|
84
|
+
(packet.flags == 1) rescue false
|
85
|
+
end
|
86
|
+
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
module SPDY
|
2
|
+
module Protocol
|
3
|
+
|
4
|
+
CONTROL_BIT = 1
|
5
|
+
DATA_BIT = 0
|
6
|
+
VERSION = 2
|
7
|
+
|
8
|
+
module Control
|
9
|
+
class Header < BinData::Record
|
10
|
+
hide :u1
|
11
|
+
|
12
|
+
bit1 :frame, :initial_value => CONTROL_BIT
|
13
|
+
bit15 :version, :initial_value => VERSION
|
14
|
+
bit16 :type
|
15
|
+
|
16
|
+
bit8 :flags
|
17
|
+
bit24 :len
|
18
|
+
|
19
|
+
bit1 :u1
|
20
|
+
bit31 :stream_id
|
21
|
+
end
|
22
|
+
|
23
|
+
class SynStream < BinData::Record
|
24
|
+
hide :u1, :u2
|
25
|
+
|
26
|
+
header :header
|
27
|
+
|
28
|
+
bit1 :u1
|
29
|
+
bit31 :associated_to_stream_id
|
30
|
+
|
31
|
+
bit2 :pri
|
32
|
+
bit14 :u2
|
33
|
+
|
34
|
+
string :data, :read_length => lambda { header.len - 10 }
|
35
|
+
end
|
36
|
+
|
37
|
+
class SynReply < BinData::Record
|
38
|
+
attr_accessor :uncompressed_data
|
39
|
+
|
40
|
+
header :header
|
41
|
+
bit16 :unused
|
42
|
+
string :data, :read_length => lambda { header.len - 6 }
|
43
|
+
|
44
|
+
def parse(chunk)
|
45
|
+
self.read(chunk)
|
46
|
+
|
47
|
+
data = Zlib.inflate(self.data.to_s)
|
48
|
+
self.uncompressed_data = NV.new.read(data)
|
49
|
+
self
|
50
|
+
end
|
51
|
+
|
52
|
+
def create(opts = {})
|
53
|
+
self.header.type = 2
|
54
|
+
self.header.len = 6
|
55
|
+
|
56
|
+
self.header.flags = opts[:flags] || 0
|
57
|
+
self.header.stream_id = opts[:stream_id]
|
58
|
+
|
59
|
+
nv = SPDY::Protocol::NV.new
|
60
|
+
nv.create(opts[:headers])
|
61
|
+
|
62
|
+
nv = SPDY::Zlib.deflate(nv.to_binary_s)
|
63
|
+
self.header.len = self.header.len.to_i + nv.size
|
64
|
+
self.data = nv
|
65
|
+
|
66
|
+
self
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
module Data
|
72
|
+
class Frame < BinData::Record
|
73
|
+
bit1 :frame, :initial_value => DATA_BIT
|
74
|
+
bit31 :stream_id
|
75
|
+
|
76
|
+
bit8 :flags, :initial_value => 0
|
77
|
+
bit24 :len, :initial_value => 0
|
78
|
+
|
79
|
+
string :data, :read_length => :len
|
80
|
+
|
81
|
+
def create(opts = {})
|
82
|
+
self.stream_id = opts[:stream_id]
|
83
|
+
self.flags = opts[:flags] if opts[:flags]
|
84
|
+
|
85
|
+
if opts[:data]
|
86
|
+
self.len = opts[:data].size
|
87
|
+
self.data = opts[:data]
|
88
|
+
end
|
89
|
+
|
90
|
+
self
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
class NV < BinData::Record
|
96
|
+
bit16 :pairs
|
97
|
+
array :headers, :initial_length => :pairs do
|
98
|
+
bit16 :name_len
|
99
|
+
string :name_data, :read_length => :name_len
|
100
|
+
|
101
|
+
bit16 :value_len
|
102
|
+
string :value_data, :read_length => :value_len
|
103
|
+
end
|
104
|
+
|
105
|
+
def create(opts = {})
|
106
|
+
opts.each do |k, v|
|
107
|
+
self.headers << {:name_len => k.size, :name_data => k, :value_len => v.size, :value_data => v}
|
108
|
+
end
|
109
|
+
|
110
|
+
self.pairs = opts.size
|
111
|
+
self
|
112
|
+
end
|
113
|
+
|
114
|
+
def to_h
|
115
|
+
headers.inject({}) do |h, v|
|
116
|
+
h[v.name_data.to_s] = v.value_data.to_s
|
117
|
+
h
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
data/lib/spdy/version.rb
ADDED
data/lib/spdy.rb
ADDED
data/spdy.gemspec
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "spdy/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "spdy"
|
7
|
+
s.version = Spdy::VERSION
|
8
|
+
s.platform = Gem::Platform::RUBY
|
9
|
+
s.authors = ["Ilya Grigorik"]
|
10
|
+
s.email = ["ilya@igvita.com"]
|
11
|
+
s.homepage = "https://github.com/igrigorik/spdy"
|
12
|
+
s.summary = "SPDY is an experiment with protocols for the web"
|
13
|
+
s.description = s.summary
|
14
|
+
|
15
|
+
s.rubyforge_project = "spdy"
|
16
|
+
|
17
|
+
s.add_dependency "bindata"
|
18
|
+
s.add_dependency "ffi-zlib"
|
19
|
+
s.add_development_dependency "rspec"
|
20
|
+
|
21
|
+
s.files = `git ls-files`.split("\n")
|
22
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
23
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
24
|
+
s.require_paths = ["lib"]
|
25
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
describe SPDY::Zlib do
|
4
|
+
it "should inflate header with custom dictionary" do
|
5
|
+
SPDY::Zlib.inflate(COMPRESSED_HEADER).should match('HTTP/1.1')
|
6
|
+
end
|
7
|
+
|
8
|
+
it "should deflate header with custom dictionary" do
|
9
|
+
orig = SPDY::Zlib.inflate(COMPRESSED_HEADER)
|
10
|
+
rinse = SPDY::Zlib.inflate(SPDY::Zlib.deflate(orig))
|
11
|
+
|
12
|
+
orig.should == rinse
|
13
|
+
end
|
14
|
+
end
|
data/spec/helper.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'rspec'
|
2
|
+
require 'spdy'
|
3
|
+
|
4
|
+
COMPRESSED_HEADER = "8\xEA\xDF\xA2Q\xB2b\xE0f`\x83\xA4\x17\x86(4\xDBu0\xAC\xD7\x06\x89\xC2\xFDa]hk\xA0g\xA9\x83p\x13X\xC0B\a\xEE?\x1D-}-\xB0\x98)\x03\x1Fjff\x90\xF3\f\xF6\x87:U\a\xECV\xB0:s\x1D\x88zc\x06~\xB4\xEC\xCE \b\xF2\x8C\x0E\xD47:\xC5)\xC9\x19p5\xB0\x14\xC2\xC0\x97\x9A\xA7\e\x1A\xAC\x93\nu\b\x03\e$V\x19Z\x8A\x8D\x8C\xD3\xD2u\x8D\v\x8C\xCC\x8D\x92\v,\xCB\r\xE2\x8Bl\xCD\xAD\x15\f\xB3\xCD\v\xCDu3\rR\xCC\xD3\x8B\v\r-\xCC\x81\xA2\x06\xD6\n\xF1 '\x96$\xA5&\x96\x18\x01d[\x9Cj\x9CUQ\x92dT\x99e\x9C\x9A\x93\x93j\f\x94\x8D//)\x8F/\xCB,\x8E\afy[k\x85\xB2\xC4\xBC\xCC\x92\xCA\xF8\xCC\x14\xDB4c#\x8B\xE4$3\x13c\x93d`\xFEM12N154OI32O\x03\x16\x04\xA6\x96I\f,\xA0\xC2\x88\x81\x0F\x94bs@L+K\x03\x03\x03\x06\xB6\\`!\x98\x9F\xC2\xC0\xEC\xEE\x1A\xC2\xC0V\f\xCC7\xB9\xA9\f\xAC\x19%%\x05\xC5\f\xCC\xA0\bb\xD4g\xE0B\x94*\fe\xBE\xF9U\x9999\x89\xFA\xA6z\x06\x00)h\xF8&&g\xE6\x95\xE4\x17gX+x\x02\x13z\x8E\x02P@\xC1?X!B\xC1\xD0 \xDE,\xDE\\S\xC1\x11\x18\x87\xA9\xE1\xA9I\xDE\x99%\xFA\xA6\xC6&zF&\n\x1A\xDE\x1E!\xBE>:\n9\x99\xD9\xA9\n\xEE\xA9\xC9\xD9\xF9\x9A\n\xCE\x19\xC0\xD22U\xDF\xD0P\xCF@\xCF\xCC\xD2L\xCF\xC8B!81-\xB1(\x13\xAA\x89\x81\x1D\x9Af\x188`I\t\x00\x00\x00\xFF\xFF"
|
5
|
+
|
6
|
+
SYN_STREAM = "\x80\x02\x00\x01\x01\x00\x01E\x00\x00\x00\x01\x00\x00\x00\x00\x00\x008\xEA\xDF\xA2Q\xB2b\xE0f`\x83\xA4\x17\x86(4\xDBu0\xAC\xD7\x06\x89\xC2\xFDa]hk\xA0g\xA9\x83p\x13X\xC0B\a\xEE?\x1D-}-\xB0\x98)\x03\x1Fjff\x90\xF3\f\xF6\x87:U\a\xECV\xB0:s\x1D\x88zc\x06~\xB4\xEC\xCE \b\xF2\x8C\x0E\xD47:\xC5)\xC9\x19p5\xB0\x14\xC2\xC0\x97\x9A\xA7\e\x1A\xAC\x93\nu\b\x03/J:d\xE0\x84\x86\x96\xAD\x01\x03\v\xA8``\xE0342\xD73\x00BC+K\x03\x03\x03\x06\xB6\\`\x81\x94\x9F\xC2\xC0\xEC\xEE\x1A\xC2\xC0V\f\xD4\x9B\x9B\xCA\xC0\x9AQRRP\xCC\xC0\f\n,\x11}{\x80\x80a\x9Do\x9B\xA8\x06,\x10\x80\xC5\x86mVq~\x1E\x03\x17\"\xD33\x94\xF9\xE6We\xE6\xE4$\xEA\x9B\xEA\x19(h\xF8&&g\xE6\x95\xE4\x17gX+x\x02\xD3a\x8E\x02P@\xC1?X!B\xC1\xD0 \xDE,\xDE\\S\xC1\x11\x18\xC4\xA9\xE1\xA9I\xDE\x99%\xFA\xA6\xC6&zF&\n\x1A\xDE\x1E!\xBE>:\n9\x99\xD9\xA9\n\xEE\xA9\xC9\xD9\xF9\x9A\n\xCE\x19\xC0\xC2,U\xDF\xD0\x10\xE8X3K3=#\v\x85\xE0\xC4\xB4\xC4\xA2L\xA8&\x06vh\x942p\xC0b\x1A\x00\x00\x00\xFF\xFF"
|
7
|
+
|
8
|
+
SYN_REPLY = "\x80\x02\x00\x02\x00\x00\x005\x00\x00\x00\x01\x00\x00x\xbb\xdf\xa2Q\xb2b`f\xe0q\x86\x06R\x080\x90\x18\xb8\x10v0\xb0A\x943\xb0\x01\x93\xb1\x82\xbf7\x03;T#\x03\x07\xcc<\x00\x00\x00\x00\xff\xff"
|
9
|
+
|
10
|
+
RST_STREAM = "\x80\x02\x00\x03\x00\x00\x00\b\x00\x00\x00\x01\x00\x00\x00\x01"
|
11
|
+
|
12
|
+
DATA = "\x00\x00\x00\x01\x00\x00\x00\rThis is SPDY."
|
13
|
+
DATA_FIN = "\x00\x00\x00\x01\x01\x00\x00\x00"
|
14
|
+
|
15
|
+
NV = "\x00\x03\x00\x0cContent-Type\x00\ntext/plain\x00\x06status\x00\x06200 OK\x00\x07version\x00\x08HTTP/1.1"
|
data/spec/parser_spec.rb
ADDED
@@ -0,0 +1,115 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
describe SPDY::Parser do
|
4
|
+
let(:s) { SPDY::Parser.new }
|
5
|
+
|
6
|
+
context "callbacks" do
|
7
|
+
it "should accept header callback" do
|
8
|
+
lambda do
|
9
|
+
s.on_headers_complete {}
|
10
|
+
end.should_not raise_error
|
11
|
+
end
|
12
|
+
|
13
|
+
it "should accept body callback" do
|
14
|
+
lambda do
|
15
|
+
s.on_body {}
|
16
|
+
end.should_not raise_error
|
17
|
+
end
|
18
|
+
|
19
|
+
it "should accept message complete callback" do
|
20
|
+
lambda do
|
21
|
+
s.on_message_complete {}
|
22
|
+
end.should_not raise_error
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
it "should accept incoming data" do
|
27
|
+
lambda { s << DATA }.should_not raise_error
|
28
|
+
end
|
29
|
+
|
30
|
+
it "should reassemble broken packets" do
|
31
|
+
stream, data = nil
|
32
|
+
s.on_body { |stream_id, d| stream, data = stream_id, d }
|
33
|
+
|
34
|
+
lambda { s << DATA[0...DATA.size - 10] }.should_not raise_error
|
35
|
+
lambda { s << DATA[DATA.size-10..DATA.size] }.should_not raise_error
|
36
|
+
|
37
|
+
stream.should == 1
|
38
|
+
data.should == 'This is SPDY.'
|
39
|
+
|
40
|
+
fired = false
|
41
|
+
s.on_headers_complete { fired = true }
|
42
|
+
s << SYN_STREAM
|
43
|
+
|
44
|
+
fired.should be_true
|
45
|
+
end
|
46
|
+
|
47
|
+
context "CONTROL" do
|
48
|
+
it "should parse SYN_STREAM packet" do
|
49
|
+
fired = false
|
50
|
+
s.on_headers_complete { fired = true }
|
51
|
+
s << SYN_STREAM
|
52
|
+
|
53
|
+
fired.should be_true
|
54
|
+
end
|
55
|
+
|
56
|
+
it "should return parsed headers" do
|
57
|
+
sid, asid, pri, headers = nil
|
58
|
+
s.on_headers_complete do |stream, astream, priority, head|
|
59
|
+
sid = stream; asid = astream; pri = priority; headers = head
|
60
|
+
end
|
61
|
+
|
62
|
+
s << SYN_STREAM
|
63
|
+
|
64
|
+
sid.should == 1
|
65
|
+
asid.should == 0
|
66
|
+
pri.should == 0
|
67
|
+
|
68
|
+
headers.class.should == Hash
|
69
|
+
headers['version'].should == "HTTP/1.1"
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
context "DATA" do
|
74
|
+
it "should parse data packet" do
|
75
|
+
stream, data = nil
|
76
|
+
s.on_body { |stream_id, d| stream, data = stream_id, d }
|
77
|
+
s << DATA
|
78
|
+
|
79
|
+
stream.should == 1
|
80
|
+
data.should == 'This is SPDY.'
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
context "FIN" do
|
85
|
+
it "should invoke message_complete on FIN flag in CONTROL packet" do
|
86
|
+
f1, f2 = false
|
87
|
+
s.on_headers_complete { f1 = true }
|
88
|
+
s.on_message_complete { |s| f2 = s }
|
89
|
+
|
90
|
+
sr = SPDY::Protocol::Control::SynStream.new
|
91
|
+
sr.header.stream_id = 3
|
92
|
+
sr.header.type = 1
|
93
|
+
sr.header.flags = 0x01
|
94
|
+
sr.header.len = 10
|
95
|
+
|
96
|
+
s << sr.to_binary_s
|
97
|
+
|
98
|
+
f1.should be_true
|
99
|
+
f2.should == 3
|
100
|
+
end
|
101
|
+
|
102
|
+
it "should invoke message_complete on FIN flag in DATA packet" do
|
103
|
+
f1, f2 = false
|
104
|
+
s.on_body { f1 = true }
|
105
|
+
s.on_message_complete { |s| f2 = s }
|
106
|
+
|
107
|
+
s << DATA_FIN
|
108
|
+
|
109
|
+
f1.should be_true
|
110
|
+
f2.should == 1
|
111
|
+
end
|
112
|
+
|
113
|
+
end
|
114
|
+
|
115
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
describe SPDY::Protocol do
|
4
|
+
|
5
|
+
context "NV" do
|
6
|
+
it "should create an NV packet" do
|
7
|
+
nv = SPDY::Protocol::NV.new
|
8
|
+
nv.create({'Content-Type' => 'text/plain', 'status' => '200 OK', 'version' => 'HTTP/1.1'})
|
9
|
+
|
10
|
+
nv.to_binary_s.should == NV
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
context "SYN_REPLY" do
|
15
|
+
it "should create a SYN_REPLY packet" do
|
16
|
+
sr = SPDY::Protocol::Control::SynReply.new
|
17
|
+
|
18
|
+
headers = {'Content-Type' => 'text/plain', 'status' => '200 OK', 'version' => 'HTTP/1.1'}
|
19
|
+
sr.create(:stream_id => 1, :headers => headers)
|
20
|
+
|
21
|
+
sr.header.version.should == 2
|
22
|
+
sr.header.stream_id.should == 1
|
23
|
+
|
24
|
+
sr.header.len.should > 50
|
25
|
+
sr.data.should_not be_nil
|
26
|
+
|
27
|
+
sr.to_binary_s.should == SYN_REPLY
|
28
|
+
end
|
29
|
+
|
30
|
+
it "should parse SYN_REPLY packet" do
|
31
|
+
sr = SPDY::Protocol::Control::SynReply.new
|
32
|
+
sr.parse(SYN_REPLY)
|
33
|
+
|
34
|
+
sr.header.type.should == 2
|
35
|
+
sr.uncompressed_data.to_h.class.should == Hash
|
36
|
+
sr.uncompressed_data.to_h['status'].should == '200 OK'
|
37
|
+
|
38
|
+
sr.to_binary_s.should == SYN_REPLY
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
context "DATA" do
|
43
|
+
it "should create a data frame" do
|
44
|
+
data = "This is SPDY."
|
45
|
+
|
46
|
+
d = SPDY::Protocol::Data::Frame.new
|
47
|
+
d.create(:stream_id => 1, :data => data)
|
48
|
+
|
49
|
+
d.to_binary_s.should == DATA
|
50
|
+
end
|
51
|
+
|
52
|
+
it "should create a FIN data frame" do
|
53
|
+
d = SPDY::Protocol::Data::Frame.new
|
54
|
+
d.create(:stream_id => 1, :flags => 1)
|
55
|
+
|
56
|
+
d.to_binary_s.should == DATA_FIN
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# context "RST_STREAM" do
|
61
|
+
# it "should parse reset packet"
|
62
|
+
# end
|
63
|
+
end
|
metadata
ADDED
@@ -0,0 +1,107 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: spdy
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease:
|
5
|
+
version: 0.0.1
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Ilya Grigorik
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
|
13
|
+
date: 2011-04-07 00:00:00 -04:00
|
14
|
+
default_executable:
|
15
|
+
dependencies:
|
16
|
+
- !ruby/object:Gem::Dependency
|
17
|
+
name: bindata
|
18
|
+
prerelease: false
|
19
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
20
|
+
none: false
|
21
|
+
requirements:
|
22
|
+
- - ">="
|
23
|
+
- !ruby/object:Gem::Version
|
24
|
+
version: "0"
|
25
|
+
type: :runtime
|
26
|
+
version_requirements: *id001
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: ffi-zlib
|
29
|
+
prerelease: false
|
30
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
31
|
+
none: false
|
32
|
+
requirements:
|
33
|
+
- - ">="
|
34
|
+
- !ruby/object:Gem::Version
|
35
|
+
version: "0"
|
36
|
+
type: :runtime
|
37
|
+
version_requirements: *id002
|
38
|
+
- !ruby/object:Gem::Dependency
|
39
|
+
name: rspec
|
40
|
+
prerelease: false
|
41
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
42
|
+
none: false
|
43
|
+
requirements:
|
44
|
+
- - ">="
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: "0"
|
47
|
+
type: :development
|
48
|
+
version_requirements: *id003
|
49
|
+
description: SPDY is an experiment with protocols for the web
|
50
|
+
email:
|
51
|
+
- ilya@igvita.com
|
52
|
+
executables: []
|
53
|
+
|
54
|
+
extensions: []
|
55
|
+
|
56
|
+
extra_rdoc_files: []
|
57
|
+
|
58
|
+
files:
|
59
|
+
- .gitignore
|
60
|
+
- .rspec
|
61
|
+
- Gemfile
|
62
|
+
- README.md
|
63
|
+
- Rakefile
|
64
|
+
- examples/spdy_server.rb
|
65
|
+
- lib/spdy.rb
|
66
|
+
- lib/spdy/compressor.rb
|
67
|
+
- lib/spdy/parser.rb
|
68
|
+
- lib/spdy/protocol.rb
|
69
|
+
- lib/spdy/version.rb
|
70
|
+
- spdy.gemspec
|
71
|
+
- spec/compressor_spec.rb
|
72
|
+
- spec/helper.rb
|
73
|
+
- spec/parser_spec.rb
|
74
|
+
- spec/protocol_spec.rb
|
75
|
+
has_rdoc: true
|
76
|
+
homepage: https://github.com/igrigorik/spdy
|
77
|
+
licenses: []
|
78
|
+
|
79
|
+
post_install_message:
|
80
|
+
rdoc_options: []
|
81
|
+
|
82
|
+
require_paths:
|
83
|
+
- lib
|
84
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
85
|
+
none: false
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: "0"
|
90
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
91
|
+
none: false
|
92
|
+
requirements:
|
93
|
+
- - ">="
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: "0"
|
96
|
+
requirements: []
|
97
|
+
|
98
|
+
rubyforge_project: spdy
|
99
|
+
rubygems_version: 1.6.2
|
100
|
+
signing_key:
|
101
|
+
specification_version: 3
|
102
|
+
summary: SPDY is an experiment with protocols for the web
|
103
|
+
test_files:
|
104
|
+
- spec/compressor_spec.rb
|
105
|
+
- spec/helper.rb
|
106
|
+
- spec/parser_spec.rb
|
107
|
+
- spec/protocol_spec.rb
|