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,49 @@
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 'connection'
22
+
23
+ module HTTP
24
+ module Protocol
25
+ module HTTP2
26
+ class Client < Connection
27
+ def initialize(framer, *args)
28
+ super(framer, 1, *args)
29
+ end
30
+
31
+ def send_connection_preface(settings = nil)
32
+ if @state == :new
33
+ @framer.write_connection_preface
34
+
35
+ send_settings(settings)
36
+
37
+ yield if block_given?
38
+
39
+ read_frame do |frame|
40
+ raise ProtocolError, "First frame must be SettingsFrame, but got #{frame.class}" unless frame.is_a? SettingsFrame
41
+ end
42
+ else
43
+ raise ProtocolError, "Cannot send connection preface in state #{@state}"
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,309 @@
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 'framer'
22
+ require_relative 'flow_control'
23
+
24
+ require 'http/hpack/context'
25
+ require 'http/hpack/compressor'
26
+ require 'http/hpack/decompressor'
27
+
28
+ module HTTP
29
+ module Protocol
30
+ module HTTP2
31
+ class Connection
32
+ include FlowControl
33
+
34
+ def initialize(framer, next_stream_id)
35
+ @state = :new
36
+ @streams = {}
37
+
38
+ @framer = framer
39
+ @next_stream_id = next_stream_id
40
+ @last_stream_id = 0
41
+
42
+ @local_settings = PendingSettings.new
43
+ @remote_settings = Settings.new
44
+
45
+ @decoder = HPACK::Context.new
46
+ @encoder = HPACK::Context.new
47
+
48
+ @local_window = Window.new(@local_settings.initial_window_size)
49
+ @remote_window = Window.new(@remote_settings.initial_window_size)
50
+ end
51
+
52
+ def id
53
+ 0
54
+ end
55
+
56
+ def maximum_frame_size
57
+ @remote_settings.maximum_frame_size
58
+ end
59
+
60
+ def maximum_concurrent_streams
61
+ [@local_settings.maximum_concurrent_streams, @remote_settings.maximum_concurrent_streams].min
62
+ end
63
+
64
+ attr :framer
65
+
66
+ # Connection state (:new, :open, :closed).
67
+ attr_accessor :state
68
+
69
+ # Current settings value for local and peer
70
+ attr_accessor :local_settings
71
+ attr_accessor :remote_settings
72
+
73
+ # Our window for receiving data. When we receive data, it reduces this window.
74
+ # If the window gets too small, we must send a window update.
75
+ attr :local_window
76
+
77
+ # Our window for sending data. When we send data, it reduces this window.
78
+ attr :remote_window
79
+
80
+ def closed?
81
+ @state == :closed
82
+ end
83
+
84
+ def encode_headers(headers, buffer = String.new.b)
85
+ HPACK::Compressor.new(buffer, @encoder).encode(headers)
86
+
87
+ return buffer
88
+ end
89
+
90
+ def decode_headers(data)
91
+ HPACK::Decompressor.new(data, @decoder).decode
92
+ end
93
+
94
+ # Streams are identified with an unsigned 31-bit integer. Streams initiated by a client MUST use odd-numbered stream identifiers; those initiated by the server MUST use even-numbered stream identifiers. A stream identifier of zero (0x0) is used for connection control messages; the stream identifier of zero cannot be used to establish a new stream.
95
+ def next_stream_id
96
+ id = @next_stream_id
97
+
98
+ @next_stream_id += 2
99
+
100
+ return id
101
+ end
102
+
103
+ attr :streams
104
+
105
+ def read_frame
106
+ frame = @framer.read_frame
107
+ # puts "#{self.class} #{@state} read_frame: #{frame.inspect}"
108
+
109
+ yield frame if block_given?
110
+
111
+ frame.apply(self)
112
+
113
+ return frame
114
+ rescue ProtocolError => error
115
+ send_goaway(error.code || PROTOCOL_ERROR, error.message)
116
+ raise
117
+ end
118
+
119
+ def send_settings(changes)
120
+ @local_settings.append(changes)
121
+
122
+ frame = SettingsFrame.new
123
+ frame.pack(changes)
124
+
125
+ write_frame(frame)
126
+ end
127
+
128
+ def send_goaway(error_code = 0, message = "")
129
+ frame = GoawayFrame.new
130
+ frame.pack @last_stream_id, error_code, message
131
+
132
+ write_frame(frame)
133
+
134
+ @state = :closed
135
+ end
136
+
137
+ def write_frame(frame)
138
+ # puts "#{self.class} #{@state} write_frame: #{frame.inspect}"
139
+ @framer.write_frame(frame)
140
+ end
141
+
142
+ def send_ping(data)
143
+ if @state != :closed
144
+ frame = PingFrame.new
145
+ frame.pack data
146
+
147
+ write_frame(frame)
148
+ else
149
+ raise ProtocolError, "Cannot send ping in state #{@state}"
150
+ end
151
+ end
152
+
153
+ def update_local_settings(changes)
154
+ capacity = @local_settings.initial_window_size
155
+ @streams.each_value do |stream|
156
+ stream.local_window.capacity = capacity
157
+ end
158
+ end
159
+
160
+ def update_remote_settings(changes)
161
+ capacity = @remote_settings.initial_window_size
162
+ @streams.each_value do |stream|
163
+ stream.remote_window.capacity = capacity
164
+ end
165
+ end
166
+
167
+ # In addition to changing the flow-control window for streams that are not yet active, a SETTINGS frame can alter the initial flow-control window size for streams with active flow-control windows (that is, streams in the "open" or "half-closed (remote)" state). When the value of SETTINGS_INITIAL_WINDOW_SIZE changes, a receiver MUST adjust the size of all stream flow-control windows that it maintains by the difference between the new value and the old value.
168
+ #
169
+ # @return [Boolean] whether the frame was an acknowledgement
170
+ def process_settings(frame)
171
+ if frame.acknowledgement?
172
+ # The remote end has confirmed the settings have been received:
173
+ changes = @local_settings.acknowledge
174
+
175
+ update_local_settings(changes)
176
+
177
+ return true
178
+ else
179
+ # The remote end is updating the settings, we reply with acknowledgement:
180
+ reply = frame.acknowledge
181
+
182
+ write_frame(reply)
183
+
184
+ changes = frame.unpack
185
+ @remote_settings.update(changes)
186
+
187
+ update_remote_settings(changes)
188
+
189
+ return false
190
+ end
191
+ end
192
+
193
+ def open!
194
+ @local_window.capacity = self.local_settings.initial_window_size
195
+ @remote_window.capacity = self.remote_settings.initial_window_size
196
+
197
+ @state = :open
198
+
199
+ return self
200
+ end
201
+
202
+ def receive_settings(frame)
203
+ if @state == :new
204
+ # We transition to :open when we receive acknowledgement of first settings frame:
205
+ open! if process_settings(frame)
206
+ elsif @state != :closed
207
+ process_settings(frame)
208
+ else
209
+ raise ProtocolError, "Cannot receive settings in state #{@state}"
210
+ end
211
+ end
212
+
213
+ def receive_ping(frame)
214
+ if @state != :closed
215
+ unless frame.acknowledgement?
216
+ reply = frame.acknowledge
217
+
218
+ write_frame(reply)
219
+ end
220
+ else
221
+ raise ProtocolError, "Cannot receive ping in state #{@state}"
222
+ end
223
+ end
224
+
225
+ def receive_data(frame)
226
+ consume_local_window(frame)
227
+
228
+ if stream = @streams[frame.stream_id]
229
+ stream.receive_data(frame)
230
+
231
+ if stream.closed?
232
+ @streams.delete(stream.id)
233
+ end
234
+ else
235
+ raise ProtocolError, "Bad stream"
236
+ end
237
+ end
238
+
239
+ def create_stream(stream_id = next_stream_id)
240
+ stream = Stream.new(self, stream_id)
241
+
242
+ @last_stream_id = stream_id
243
+
244
+ return stream
245
+ end
246
+
247
+ def receive_headers(frame)
248
+ if stream = @streams[frame.stream_id]
249
+ stream.receive_headers(frame)
250
+
251
+ if stream.closed?
252
+ @streams.delete(stream.id)
253
+ end
254
+ elsif frame.stream_id > @last_stream_id
255
+ if @streams.count < self.maximum_concurrent_streams
256
+ stream = create_stream(frame.stream_id)
257
+ stream.receive_headers(frame)
258
+
259
+ @streams[stream.id] = stream
260
+ else
261
+ raise ProtocolError, "Exceeded maximum concurrent streams"
262
+ end
263
+ end
264
+ end
265
+
266
+ def receive_priority(frame)
267
+ if stream = @streams[frame.stream_id]
268
+ stream.receive_priority(frame)
269
+ else
270
+ raise ProtocolError, "Bad stream"
271
+ end
272
+ end
273
+
274
+ def receive_reset_stream(frame)
275
+ if stream = @streams[frame.stream_id]
276
+ stream.receive_reset_stream(frame)
277
+
278
+ @streams.delete(stream.id)
279
+ else
280
+ raise ProtocolError, "Bad stream"
281
+ end
282
+ end
283
+
284
+ def receive_window_update(frame)
285
+ if frame.connection?
286
+ super
287
+ elsif stream = @streams[frame.stream_id]
288
+ stream.receive_window_update(frame)
289
+ elsif frame.stream_id <= @last_stream_id
290
+ # The stream was closed/deleted, ignore
291
+ else
292
+ raise ProtocolError, "Cannot update window of non-existant stream: #{frame.stream_id}"
293
+ end
294
+ end
295
+
296
+ def window_updated
297
+ # This is very inefficient, but workable.
298
+ @streams.each_value do |stream|
299
+ stream.window_updated unless stream.closed?
300
+ end
301
+ end
302
+
303
+ def receive_frame(frame)
304
+ warn "Unhandled frame #{frame.inspect}"
305
+ end
306
+ end
307
+ end
308
+ end
309
+ end
@@ -0,0 +1,86 @@
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
+ module Continued
27
+ def end_headers?
28
+ @flags & END_HEADERS
29
+ end
30
+
31
+ def read(io)
32
+ super
33
+
34
+ unless end_headers?
35
+ @continuation = ContinuationFrame.new
36
+
37
+ @continuation.read(io)
38
+ end
39
+ end
40
+
41
+ def write(io)
42
+ super
43
+
44
+ if continuation = self.continuation
45
+ continuation.write(io)
46
+ end
47
+ end
48
+
49
+ attr_accessor :continuation
50
+
51
+ def pack(data, **options)
52
+ maximum_size = options[:maximum_size]
53
+
54
+ if maximum_size and data.bytesize > maximum_size
55
+ clear_flags(END_HEADERS)
56
+
57
+ super(data.byteslice(0, maximum_size), **options)
58
+
59
+ remainder = data.byteslice(maximum_size, data.bytesize-maximum_size)
60
+
61
+ @continuation = ContinuationFrame.new
62
+ @continuation.pack(remainder, maximum_size: maximum_size)
63
+ else
64
+ set_flags(END_HEADERS)
65
+
66
+ super data, **options
67
+
68
+ @continuation = nil
69
+ end
70
+ end
71
+ end
72
+
73
+ # The CONTINUATION frame is used to continue a sequence of header block fragments. Any number of CONTINUATION frames can be sent, as long as the preceding frame is on the same stream and is a HEADERS, PUSH_PROMISE, or CONTINUATION frame without the END_HEADERS flag set.
74
+ #
75
+ # +---------------------------------------------------------------+
76
+ # | Header Block Fragment (*) ...
77
+ # +---------------------------------------------------------------+
78
+ #
79
+ class ContinuationFrame < Frame
80
+ include Continued
81
+
82
+ TYPE = 0x9
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,64 @@
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 'frame'
23
+ require_relative 'padded'
24
+
25
+ module HTTP
26
+ module Protocol
27
+ module HTTP2
28
+ # DATA frames convey arbitrary, variable-length sequences of octets associated with a stream. One or more DATA frames are used, for instance, to carry HTTP request or response payloads.
29
+ #
30
+ # DATA frames MAY also contain padding. Padding can be added to DATA frames to obscure the size of messages.
31
+ #
32
+ # +---------------+
33
+ # |Pad Length? (8)|
34
+ # +---------------+-----------------------------------------------+
35
+ # | Data (*) ...
36
+ # +---------------------------------------------------------------+
37
+ # | Padding (*) ...
38
+ # +---------------------------------------------------------------+
39
+ #
40
+ class DataFrame < Frame
41
+ include Padded
42
+
43
+ TYPE = 0x0
44
+
45
+ def end_stream?
46
+ flag_set?(END_STREAM)
47
+ end
48
+
49
+ def pack(data, *)
50
+ if data
51
+ super
52
+ else
53
+ @length = 0
54
+ set_flags(END_STREAM)
55
+ end
56
+ end
57
+
58
+ def apply(connection)
59
+ connection.receive_data(self)
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end