http-protocol 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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