http-protocol 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,85 @@
1
+ # Copyright, 2018, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
20
+
21
+ require_relative 'window_update_frame'
22
+
23
+ module HTTP
24
+ module Protocol
25
+ module HTTP2
26
+ module FlowControl
27
+ def available_frame_size
28
+ maximum_frame_size = self.maximum_frame_size
29
+ available_size = @remote_window.available
30
+
31
+ if available_size < maximum_frame_size
32
+ return available_size
33
+ else
34
+ return maximum_frame_size
35
+ end
36
+ end
37
+
38
+ # Keep track of the amount of data sent, and fail if is too much.
39
+ def consume_remote_window(frame)
40
+ amount = frame.length
41
+
42
+ # Frames with zero length with the END_STREAM flag set (that is, an empty DATA frame) MAY be sent if there is no available space in either flow-control window.
43
+ if amount.zero? and frame.end_stream?
44
+ # It's okay, we can send. No need to consume, it's empty anyway.
45
+ elsif amount >= 0 and amount <= @remote_window.available
46
+ @remote_window.consume(amount)
47
+ else
48
+ raise FlowControlError, "Trying to send #{frame.inspect}, exceeded window size: #{@remote_window.available}"
49
+ end
50
+ end
51
+
52
+ def consume_local_window(frame)
53
+ amount = frame.length
54
+
55
+ @local_window.consume(amount)
56
+
57
+ if @local_window.limited?
58
+ self.send_window_update(@local_window.used)
59
+ end
60
+ end
61
+
62
+ # Notify the remote end that we are prepared to receive more data:
63
+ def send_window_update(window_increment)
64
+ frame = WindowUpdateFrame.new(self.id)
65
+ frame.pack window_increment
66
+
67
+ write_frame(frame)
68
+
69
+ @local_window.used -= window_increment
70
+ end
71
+
72
+ def receive_window_update(frame)
73
+ was_full = @remote_window.full?
74
+
75
+ @remote_window.expand(frame.unpack)
76
+
77
+ self.window_updated if was_full
78
+ end
79
+
80
+ def window_updated
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,187 @@
1
+ # Copyright, 2018, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
+ # Copyright, 2013, by Ilya Grigorik.
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ # of this software and associated documentation files (the "Software"), to deal
6
+ # in the Software without restriction, including without limitation the rights
7
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ # copies of the Software, and to permit persons to whom the Software is
9
+ # furnished to do so, subject to the following conditions:
10
+ #
11
+ # The above copyright notice and this permission notice shall be included in
12
+ # all copies or substantial portions of the Software.
13
+ #
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20
+ # THE SOFTWARE.
21
+
22
+ require_relative '../error'
23
+
24
+ module HTTP
25
+ module Protocol
26
+ module HTTP2
27
+ END_STREAM = 0x1
28
+ END_HEADERS = 0x4
29
+ PADDED = 0x8
30
+ PRIORITY = 0x20
31
+
32
+ class Frame
33
+ include Comparable
34
+
35
+ # Stream Identifier cannot be bigger than this:
36
+ # https://http2.github.io/http2-spec/#rfc.section.4.1
37
+ VALID_STREAM_ID = 0..0x7fffffff
38
+
39
+ # The absolute maximum bounds for the length field:
40
+ VALID_LENGTH = 0..0xffffff
41
+
42
+ # Used for generating 24-bit frame length:
43
+ LENGTH_HISHIFT = 16
44
+ LENGTH_LOMASK = 0xFFFF
45
+
46
+ # @param length [Integer] the length of the payload, or nil if the header has not been read yet.
47
+ def initialize(stream_id = 0, flags = 0, type = self.class.const_get(:TYPE), length = nil, payload = nil)
48
+ @length = length
49
+ @type = type
50
+ @flags = flags
51
+ @stream_id = stream_id
52
+ @payload = payload
53
+ end
54
+
55
+ def <=> other
56
+ to_ary <=> other.to_ary
57
+ end
58
+
59
+ def to_ary
60
+ [@length, @type, @flags, @stream_id, @payload]
61
+ end
62
+
63
+ # The generic frame header uses the following binary representation:
64
+ #
65
+ # +-----------------------------------------------+
66
+ # | Length (24) |
67
+ # +---------------+---------------+---------------+
68
+ # | Type (8) | Flags (8) |
69
+ # +-+-------------+---------------+-------------------------------+
70
+ # |R| Stream Identifier (31) |
71
+ # +=+=============================================================+
72
+ # | Frame Payload (0...) ...
73
+ # +---------------------------------------------------------------+
74
+
75
+ attr_accessor :length
76
+ attr_accessor :type
77
+ attr_accessor :flags
78
+ attr_accessor :stream_id
79
+ attr_accessor :payload
80
+
81
+ def unpack
82
+ @payload
83
+ end
84
+
85
+ def pack(payload, maximum_size: nil)
86
+ @payload = payload
87
+ @length = payload.bytesize
88
+
89
+ if maximum_size and @length > maximum_size
90
+ raise ProtocolError, "Frame length #{@length} bigger than maximum allowed: #{maximum_size}"
91
+ end
92
+ end
93
+
94
+ def set_flags(mask)
95
+ @flags |= mask
96
+ end
97
+
98
+ def clear_flags(mask)
99
+ @flags &= ~mask
100
+ end
101
+
102
+ def flag_set?(mask)
103
+ @flags & mask != 0
104
+ end
105
+
106
+ # Check if frame is a connection frame: SETTINGS, PING, GOAWAY, and any
107
+ # frame addressed to stream ID = 0.
108
+ #
109
+ # @return [Boolean]
110
+ def connection?
111
+ @stream_id.zero?
112
+ end
113
+
114
+ HEADER_FORMAT = 'CnCCN'.freeze
115
+ STREAM_ID_MASK = 0x7fffffff
116
+
117
+ # Generates common 9-byte frame header.
118
+ # - http://tools.ietf.org/html/draft-ietf-httpbis-http2-16#section-4.1
119
+ #
120
+ # @return [String]
121
+ def header
122
+ unless VALID_LENGTH.include? @length
123
+ raise ProtocolError, "Invalid frame size: #{@length.inspect}"
124
+ end
125
+
126
+ unless VALID_STREAM_ID.include? @stream_id
127
+ raise ProtocolError, "Invalid stream identifier: #{@stream_id.inspect}"
128
+ end
129
+
130
+ [
131
+ # These are guaranteed correct due to the length check above.
132
+ @length >> LENGTH_HISHIFT,
133
+ @length & LENGTH_LOMASK,
134
+ @type,
135
+ @flags,
136
+ @stream_id
137
+ ].pack(HEADER_FORMAT)
138
+ end
139
+
140
+ # Decodes common 9-byte header.
141
+ #
142
+ # @param buffer [String]
143
+ def self.parse_header(buffer)
144
+ length_hi, length_lo, type, flags, stream_id = buffer.unpack(HEADER_FORMAT)
145
+ length = (length_hi << LENGTH_HISHIFT) | length_lo
146
+ stream_id = stream_id & STREAM_ID_MASK
147
+
148
+ return length, type, flags, stream_id
149
+ end
150
+
151
+ def read_header(io)
152
+ @length, @type, @flags, @stream_id = Frame.parse_header(io.read(9))
153
+ end
154
+
155
+ def read_payload(io)
156
+ @payload = io.read(@length)
157
+ end
158
+
159
+ def read(io)
160
+ read_header(io) unless @length
161
+ read_payload(io)
162
+ end
163
+
164
+ def write_header(io)
165
+ io.write self.header
166
+ end
167
+
168
+ def write_payload(io)
169
+ io.write(@payload) if @payload
170
+ end
171
+
172
+ def write(io)
173
+ if @payload and @length != @payload.bytesize
174
+ raise ProtocolError, "Invalid payload size: #{@length} != #{@payload.bytesize}"
175
+ end
176
+
177
+ self.write_header(io)
178
+ self.write_payload(io)
179
+ end
180
+
181
+ def apply(connection)
182
+ connection.receive_frame(self)
183
+ end
184
+ end
185
+ end
186
+ end
187
+ end
@@ -0,0 +1,105 @@
1
+ # Copyright, 2018, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
+ # Copyright, 2013, by Ilya Grigorik.
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ # of this software and associated documentation files (the "Software"), to deal
6
+ # in the Software without restriction, including without limitation the rights
7
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ # copies of the Software, and to permit persons to whom the Software is
9
+ # furnished to do so, subject to the following conditions:
10
+ #
11
+ # The above copyright notice and this permission notice shall be included in
12
+ # all copies or substantial portions of the Software.
13
+ #
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20
+ # THE SOFTWARE.
21
+
22
+ require_relative '../error'
23
+
24
+ require_relative 'data_frame'
25
+ require_relative 'headers_frame'
26
+ require_relative 'priority_frame'
27
+ require_relative 'reset_stream_frame'
28
+ require_relative 'settings_frame'
29
+ require_relative 'push_promise_frame'
30
+ require_relative 'ping_frame'
31
+ require_relative 'goaway_frame'
32
+ require_relative 'window_update_frame'
33
+ require_relative 'continuation_frame'
34
+
35
+ module HTTP
36
+ module Protocol
37
+ module HTTP2
38
+ # HTTP/2 frame type mapping as defined by the spec
39
+ FRAMES = [
40
+ DataFrame,
41
+ HeadersFrame,
42
+ PriorityFrame,
43
+ ResetStreamFrame,
44
+ SettingsFrame,
45
+ PushPromiseFrame,
46
+ PingFrame,
47
+ GoawayFrame,
48
+ WindowUpdateFrame,
49
+ ContinuationFrame,
50
+ ].freeze
51
+
52
+ # Default connection "fast-fail" preamble string as defined by the spec.
53
+ CONNECTION_PREFACE_MAGIC = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n".freeze
54
+
55
+ class Framer
56
+ def initialize(io, frames = FRAMES)
57
+ @io = io
58
+ @frames = frames
59
+
60
+ @buffer = String.new.b
61
+ end
62
+
63
+ def write_connection_preface
64
+ @io.write(CONNECTION_PREFACE_MAGIC)
65
+ end
66
+
67
+ def read_connection_preface
68
+ string = @io.read(CONNECTION_PREFACE_MAGIC.bytesize)
69
+
70
+ unless string == CONNECTION_PREFACE_MAGIC
71
+ raise ProtocolError, "Invalid connection preface: #{string.inspect}"
72
+ end
73
+
74
+ return string
75
+ end
76
+
77
+ def read_frame
78
+ # Read the header:
79
+ length, type, flags, stream_id = read_header
80
+
81
+ # puts "framer: read_frame #{type} #{length}"
82
+
83
+ # Allocate the frame:
84
+ klass = @frames[type] || Frame
85
+ frame = klass.new(stream_id, flags, type, length)
86
+
87
+ # Read the payload:
88
+ frame.read(@io)
89
+
90
+ return frame
91
+ end
92
+
93
+ def write_frame(frame)
94
+ # puts "framer: write_frame #{frame.inspect}"
95
+ frame.write(@io)
96
+ @io.flush
97
+ end
98
+
99
+ def read_header
100
+ return Frame.parse_header(@io.read(9))
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,62 @@
1
+ # Copyright, 2018, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
20
+
21
+ require_relative 'frame'
22
+
23
+ module HTTP
24
+ module Protocol
25
+ module HTTP2
26
+ # The GOAWAY frame is used to initiate shutdown of a connection or to signal serious error conditions. GOAWAY allows an endpoint to gracefully stop accepting new streams while still finishing processing of previously established streams. This enables administrative actions, like server maintenance.
27
+ #
28
+ # +-+-------------------------------------------------------------+
29
+ # |R| Last-Stream-ID (31) |
30
+ # +-+-------------------------------------------------------------+
31
+ # | Error Code (32) |
32
+ # +---------------------------------------------------------------+
33
+ # | Additional Debug Data (*) |
34
+ # +---------------------------------------------------------------+
35
+ #
36
+ class GoawayFrame < Frame
37
+ TYPE = 0x7
38
+ FORMAT = "NN"
39
+
40
+ def connection?
41
+ true
42
+ end
43
+
44
+ def unpack
45
+ data = super
46
+
47
+ last_stream_id, error_code = data.unpack(FORMAT)
48
+
49
+ return last_stream_id, error_code, data.slice(8, data.bytesize-8)
50
+ end
51
+
52
+ def pack(last_stream_id, error_code, data)
53
+ super [last_stream_id, error_code].pack(FORMAT) + data
54
+ end
55
+
56
+ def apply(connection)
57
+ connection.receive_goaway(self)
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,95 @@
1
+ # Copyright, 2018, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
20
+
21
+ require_relative 'frame'
22
+ require_relative 'padded'
23
+ require_relative 'continuation_frame'
24
+ require_relative 'priority_frame'
25
+
26
+ module HTTP
27
+ module Protocol
28
+ module HTTP2
29
+ # The HEADERS frame is used to open a stream, and additionally carries a header block fragment. HEADERS frames can be sent on a stream in the "idle", "reserved (local)", "open", or "half-closed (remote)" state.
30
+ #
31
+ # +---------------+
32
+ # |Pad Length? (8)|
33
+ # +-+-------------+-----------------------------------------------+
34
+ # |E| Stream Dependency? (31) |
35
+ # +-+-------------+-----------------------------------------------+
36
+ # | Weight? (8) |
37
+ # +-+-------------+-----------------------------------------------+
38
+ # | Header Block Fragment (*) ...
39
+ # +---------------------------------------------------------------+
40
+ # | Padding (*) ...
41
+ # +---------------------------------------------------------------+
42
+ #
43
+ class HeadersFrame < Frame
44
+ include Continued, Padded
45
+
46
+ TYPE = 0x1
47
+
48
+ def initialize(*)
49
+ super
50
+
51
+ @priority = nil
52
+ end
53
+
54
+ def priority?
55
+ flag_set?(PRIORITY)
56
+ end
57
+
58
+ def end_headers?
59
+ flag_set?(END_HEADERS)
60
+ end
61
+
62
+ def end_stream?
63
+ flag_set?(END_STREAM)
64
+ end
65
+
66
+ def unpack
67
+ data = super
68
+
69
+ if priority?
70
+ priority = Priority.unpack(data)
71
+ data = data.byteslice(5, data.bytesize - 5)
72
+ end
73
+
74
+ return priority, data
75
+ end
76
+
77
+ def pack(priority, data, *args)
78
+ buffer = String.new.b
79
+
80
+ if priority
81
+ buffer << priority.pack
82
+ end
83
+
84
+ buffer << data
85
+
86
+ super(buffer, *args)
87
+ end
88
+
89
+ def apply(connection)
90
+ connection.receive_headers(self)
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end