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,242 @@
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 'ping_frame'
22
+
23
+ module HTTP
24
+ module Protocol
25
+ module HTTP2
26
+ class Settings
27
+ MAXIMUM_ALLOWED_WINDOW_SIZE = 0x7FFFFFFF
28
+ MAXIMUM_ALLOWED_FRAME_SIZE = 0xFFFFFF
29
+
30
+ HEADER_TABLE_SIZE = 0x1
31
+ ENABLE_PUSH = 0x2
32
+ MAXIMUM_CONCURRENT_STREAMS = 0x3
33
+ INITIAL_WINDOW_SIZE = 0x4
34
+ MAXIMUM_FRAME_SIZE = 0x5
35
+ MAXIMUM_HEADER_LIST_SIZE = 0x6
36
+
37
+ # Allows the sender to inform the remote endpoint of the maximum size of the header compression table used to decode header blocks, in octets.
38
+ attr_accessor :header_table_size
39
+
40
+ # This setting can be used to disable server push. An endpoint MUST NOT send a PUSH_PROMISE frame if it receives this parameter set to a value of 0.
41
+ attr :enable_push
42
+
43
+ def enable_push= value
44
+ if @enable_push == 0 || @enable_push == 1
45
+ @enable_push = value
46
+ else
47
+ raise ProtocolError, "Invalid value for enable_push: #{value}"
48
+ end
49
+ end
50
+
51
+ def enable_push?
52
+ @enable_push != 0
53
+ end
54
+
55
+ # Indicates the maximum number of concurrent streams that the sender will allow.
56
+ attr_accessor :maximum_concurrent_streams
57
+
58
+ # Indicates the sender's initial window size (in octets) for stream-level flow control.
59
+ attr :initial_window_size
60
+
61
+ def initial_window_size= value
62
+ if value < MAXIMUM_ALLOWED_WINDOW_SIZE
63
+ @initial_window_size = value
64
+ else
65
+ raise FlowControlError, "Invalid value for initial_window_size: #{value} > #{MAXIMUM_ALLOWED_WINDOW_SIZE}"
66
+ end
67
+ end
68
+
69
+ # Indicates the size of the largest frame payload that the sender is willing to receive, in octets.
70
+ attr :maximum_frame_size
71
+
72
+ def maximum_frame_size= value
73
+ if value <= MAXIMUM_ALLOWED_FRAME_SIZE
74
+ @maximum_frame_size = value
75
+ else
76
+ raise ProtocolError, "Invalid value for maximum_frame_size: #{value} > #{MAXIMUM_ALLOWED_FRAME_SIZE}"
77
+ end
78
+ end
79
+
80
+ # This advisory setting informs a peer of the maximum size of header list that the sender is prepared to accept, in octets.
81
+ attr_accessor :maximum_header_list_size
82
+
83
+ def initialize
84
+ # These limits are taken from the RFC:
85
+ # https://tools.ietf.org/html/rfc7540#section-6.5.2
86
+ @header_table_size = 4096
87
+ @enable_push = 1
88
+ @maximum_concurrent_streams = 0xFFFFFFFF
89
+ @initial_window_size = 0xFFFF # 2**16 - 1
90
+ @maximum_frame_size = 0x3FFF # 2**14 - 1
91
+ @maximum_header_list_size = 0xFFFFFFFF
92
+ end
93
+
94
+ ASSIGN = [
95
+ nil,
96
+ :header_table_size=,
97
+ :enable_push=,
98
+ :maximum_concurrent_streams=,
99
+ :initial_window_size=,
100
+ :maximum_frame_size=,
101
+ :maximum_header_list_size=,
102
+ ]
103
+
104
+ def update(changes)
105
+ changes.each do |key, value|
106
+ if name = ASSIGN[key]
107
+ self.send(name, value)
108
+ end
109
+ end
110
+ end
111
+
112
+ def difference(other)
113
+ changes = []
114
+
115
+ if @header_table_size != other.header_table_size
116
+ changes << [HEADER_TABLE_SIZE, @header_table_size]
117
+ end
118
+
119
+ if @enable_push != other.enable_push
120
+ changes << [ENABLE_PUSH, @enable_push ? 1 : 0]
121
+ end
122
+
123
+ if @maximum_concurrent_streams != other.maximum_concurrent_streams
124
+ changes << [MAXIMUM_CONCURRENT_STREAMS, @maximum_concurrent_streams]
125
+ end
126
+
127
+ if @initial_window_size != other.initial_window_size
128
+ changes << [INITIAL_WINDOW_SIZE, @initial_window_size]
129
+ end
130
+
131
+ if @maximum_frame_size != other.maximum_frame_size
132
+ changes << [MAXIMUM_FRAME_SIZE, @maximum_frame_size]
133
+ end
134
+
135
+ if @maximum_header_list_size != other.maximum_header_list_size
136
+ changes << [MAXIMUM_HEADER_LIST_SIZE, @maximum_header_list_size]
137
+ end
138
+
139
+ return changes
140
+ end
141
+ end
142
+
143
+ class PendingSettings
144
+ def initialize(current = Settings.new)
145
+ @current = current
146
+ @pending = current.dup
147
+
148
+ @queue = []
149
+ end
150
+
151
+ attr :current
152
+ attr :pending
153
+
154
+ def append(changes)
155
+ @queue << changes
156
+ @pending.update(changes)
157
+ end
158
+
159
+ def acknowledge
160
+ if changes = @queue.shift
161
+ @current.update(changes)
162
+
163
+ return changes
164
+ else
165
+ raise ProtocolError.new("Cannot acknowledge settings, no changes pending")
166
+ end
167
+ end
168
+
169
+ def header_table_size
170
+ @current.header_table_size
171
+ end
172
+
173
+ def enable_push
174
+ @current.enable_push
175
+ end
176
+
177
+ def maximum_concurrent_streams
178
+ @current.maximum_concurrent_streams
179
+ end
180
+
181
+ def initial_window_size
182
+ @current.initial_window_size
183
+ end
184
+
185
+ def maximum_frame_size
186
+ @current.maximum_frame_size
187
+ end
188
+
189
+ def maximum_header_list_size
190
+ @current.maximum_header_list_size
191
+ end
192
+ end
193
+
194
+ # The SETTINGS frame conveys configuration parameters that affect how endpoints communicate, such as preferences and constraints on peer behavior. The SETTINGS frame is also used to acknowledge the receipt of those parameters. Individually, a SETTINGS parameter can also be referred to as a "setting".
195
+ #
196
+ # +-------------------------------+
197
+ # | Identifier (16) |
198
+ # +-------------------------------+-------------------------------+
199
+ # | Value (32) |
200
+ # +---------------------------------------------------------------+
201
+ #
202
+ class SettingsFrame < Frame
203
+ TYPE = 0x4
204
+ FORMAT = "nN".freeze
205
+
206
+ include Acknowledgement
207
+
208
+ def connection?
209
+ true
210
+ end
211
+
212
+ def unpack
213
+ super.scan(/....../).map{|s| s.unpack(FORMAT)}
214
+ end
215
+
216
+ def pack(settings = [])
217
+ super settings.map{|s| s.pack(FORMAT)}.join
218
+ end
219
+
220
+ def apply(connection)
221
+ connection.receive_settings(self)
222
+ end
223
+
224
+ def read_payload(io)
225
+ super
226
+
227
+ if @stream_id != 0
228
+ raise ProtocolError, "Settings apply to connection only, but stream_id was given"
229
+ end
230
+
231
+ if acknowledgement? and @length != 0
232
+ raise FrameSizeError, "Settings acknowledgement must not contain payload: #{@payload.inspect}"
233
+ end
234
+
235
+ if (@length % 6) != 0
236
+ raise FrameSizeError, "Invalid frame length"
237
+ end
238
+ end
239
+ end
240
+ end
241
+ end
242
+ end
@@ -0,0 +1,313 @@
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
+ require_relative 'flow_control'
23
+
24
+ module HTTP
25
+ module Protocol
26
+ module HTTP2
27
+ # A single HTTP 2.0 connection can multiplex multiple streams in parallel:
28
+ # multiple requests and responses can be in flight simultaneously and stream
29
+ # data can be interleaved and prioritized.
30
+ #
31
+ # This class encapsulates all of the state, transition, flow-control, and
32
+ # error management as defined by the HTTP 2.0 specification. All you have
33
+ # to do is subscribe to appropriate events (marked with ":" prefix in
34
+ # diagram below) and provide your application logic to handle request
35
+ # and response processing.
36
+ #
37
+ # +--------+
38
+ # send PP | | recv PP
39
+ # ,--------| idle |--------.
40
+ # / | | \
41
+ # v +--------+ v
42
+ # +----------+ | +----------+
43
+ # | | | send H / | |
44
+ # ,------| reserved | | recv H | reserved |------.
45
+ # | | (local) | | | (remote) | |
46
+ # | +----------+ v +----------+ |
47
+ # | | +--------+ | |
48
+ # | | recv ES | | send ES | |
49
+ # | send H | ,-------| open |-------. | recv H |
50
+ # | | / | | \ | |
51
+ # | v v +--------+ v v |
52
+ # | +----------+ | +----------+ |
53
+ # | | half | | | half | |
54
+ # | | closed | | send R / | closed | |
55
+ # | | (remote) | | recv R | (local) | |
56
+ # | +----------+ | +----------+ |
57
+ # | | | | |
58
+ # | | send ES / | recv ES / | |
59
+ # | | send R / v send R / | |
60
+ # | | recv R +--------+ recv R | |
61
+ # | send R / `----------->| |<-----------' send R / |
62
+ # | recv R | closed | recv R |
63
+ # `----------------------->| |<----------------------'
64
+ # +--------+
65
+ #
66
+ # send: endpoint sends this frame
67
+ # recv: endpoint receives this frame
68
+ #
69
+ # H: HEADERS frame (with implied CONTINUATIONs)
70
+ # PP: PUSH_PROMISE frame (with implied CONTINUATIONs)
71
+ # ES: END_STREAM flag
72
+ # R: RST_STREAM frame
73
+ #
74
+ class Stream
75
+ include FlowControl
76
+
77
+ def initialize(connection, id = connection.next_stream_id)
78
+ @connection = connection
79
+ @id = id
80
+
81
+ @connection.streams[@id] = self
82
+
83
+ @state = :idle
84
+
85
+ @priority = nil
86
+ @local_window = connection.local_window.dup
87
+ @remote_window = connection.remote_window.dup
88
+
89
+ @headers = nil
90
+ @data = nil
91
+ end
92
+
93
+ # Stream ID (odd for client initiated streams, even otherwise).
94
+ attr :id
95
+
96
+ # Stream state as defined by HTTP 2.0.
97
+ attr :state
98
+
99
+ attr :headers
100
+ attr :data
101
+
102
+ attr :local_window
103
+ attr :remote_window
104
+
105
+ def maximum_frame_size
106
+ @connection.available_frame_size
107
+ end
108
+
109
+ def write_frame(frame)
110
+ @connection.write_frame(frame)
111
+ end
112
+
113
+ def closed?
114
+ @state == :closed or @state == :reset
115
+ end
116
+
117
+ def send_headers?
118
+ @state == :idle or @state == :reseved_local or @state == :open or @state == :half_closed_remote
119
+ end
120
+
121
+ def send_failure(status, reason)
122
+ if send_headers?
123
+ send_headers(nil, [
124
+ [':status', status.to_s],
125
+ [':reason', reason]
126
+ ], END_STREAM)
127
+ else
128
+ send_reset_stream(PROTOCOL_ERROR)
129
+ end
130
+ end
131
+
132
+ private def write_headers(priority, headers, flags = 0)
133
+ data = @connection.encode_headers(headers)
134
+
135
+ frame = HeadersFrame.new(@id, flags)
136
+ frame.pack(priority, data, maximum_size: @connection.maximum_frame_size)
137
+
138
+ write_frame(frame)
139
+
140
+ return frame
141
+ end
142
+
143
+ #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.
144
+ def send_headers(*args)
145
+ if @state == :idle
146
+ frame = write_headers(*args)
147
+
148
+ if frame.end_stream?
149
+ @state = :half_closed_local
150
+ else
151
+ @state = :open
152
+ end
153
+ elsif @state == :reseved_local
154
+ frame = write_headers(*args)
155
+
156
+ @state = :half_closed_remote
157
+ elsif @state == :open
158
+ frame = write_headers(*args)
159
+
160
+ if frame.end_stream?
161
+ @state = :half_closed_local
162
+ end
163
+ elsif @state == :half_closed_remote
164
+ frame = write_headers(*args)
165
+
166
+ if frame.end_stream?
167
+ close!
168
+ end
169
+ else
170
+ raise ProtocolError, "Cannot send headers in state: #{@state}"
171
+ end
172
+ end
173
+
174
+ def consume_remote_window(frame)
175
+ super
176
+
177
+ @connection.consume_remote_window(frame)
178
+ end
179
+
180
+ private def write_data(data, flags = 0, **options)
181
+ frame = DataFrame.new(@id, flags)
182
+ frame.pack(data, **options)
183
+
184
+ # This might fail if the data payload was too big:
185
+ consume_remote_window(frame)
186
+
187
+ write_frame(frame)
188
+
189
+ return frame
190
+ end
191
+
192
+ def send_data(*args)
193
+ if @state == :open
194
+ frame = write_data(*args)
195
+
196
+ if frame.end_stream?
197
+ @state = :half_closed_local
198
+ end
199
+ elsif @state == :half_closed_remote
200
+ frame = write_data(*args)
201
+
202
+ if frame.end_stream?
203
+ close!
204
+ end
205
+ else
206
+ raise ProtocolError, "Cannot send data in state: #{@state}"
207
+ end
208
+ end
209
+
210
+ def close!(state = :closed)
211
+ @state = state
212
+
213
+ @connection.streams.delete(@id)
214
+ end
215
+
216
+ def send_reset_stream(error_code = 0)
217
+ if @state != :idle and @state != :closed
218
+ frame = ResetStreamFrame.new(@id)
219
+ frame.pack(error_code)
220
+
221
+ write_frame(frame)
222
+
223
+ close!(:reset)
224
+ else
225
+ raise ProtocolError, "Cannot reset stream in state: #{@state}"
226
+ end
227
+ end
228
+
229
+ private def process_headers(frame)
230
+ # Receiving request headers:
231
+ priority, data = frame.unpack
232
+
233
+ if priority
234
+ @priority = priority
235
+ end
236
+
237
+ @connection.decode_headers(data)
238
+ end
239
+
240
+ def receive_headers(frame)
241
+ if @state == :idle
242
+ if frame.end_stream?
243
+ @state = :half_closed_remote
244
+ else
245
+ @state = :open
246
+ end
247
+
248
+ @headers = process_headers(frame)
249
+ elsif @state == :reseved_remote
250
+ @state = :half_closed_local
251
+
252
+ @headers = process_headers(frame)
253
+ elsif @state == :open
254
+ if frame.end_stream?
255
+ @state = :half_closed_remote
256
+ end
257
+
258
+ @headers = process_headers(frame)
259
+ elsif @state == :half_closed_local
260
+ if frame.end_stream?
261
+ close!
262
+ end
263
+
264
+ @headers = process_headers(frame)
265
+ elsif @state == :reset
266
+ # ignore...
267
+ else
268
+ raise ProtocolError, "Cannot receive headers in state: #{@state}"
269
+ end
270
+ end
271
+
272
+ # DATA frames are subject to flow control and can only be sent when a stream is in the "open" or "half-closed (remote)" state. The entire DATA frame payload is included in flow control, including the Pad Length and Padding fields if present. If a DATA frame is received whose stream is not in "open" or "half-closed (local)" state, the recipient MUST respond with a stream error of type STREAM_CLOSED.
273
+ def receive_data(frame)
274
+ if @state == :open
275
+ consume_local_window(frame)
276
+
277
+ if frame.end_stream?
278
+ @state = :half_closed_remote
279
+ end
280
+
281
+ @data = frame.unpack
282
+ elsif @state == :half_closed_local
283
+ consume_local_window(frame)
284
+
285
+ if frame.end_stream?
286
+ close!
287
+ end
288
+
289
+ @data = frame.unpack
290
+ elsif @state == :reset
291
+ # ignore...
292
+ else
293
+ raise ProtocolError, "Cannot receive data in state: #{@state}"
294
+ end
295
+ end
296
+
297
+ def receive_priority(frame)
298
+ @priority = frame.unpack
299
+ end
300
+
301
+ def receive_reset_stream(frame)
302
+ if @state != :idle and @state != :closed
303
+ close!
304
+
305
+ return frame.unpack
306
+ else
307
+ raise ProtocolError, "Cannot reset stream in state: #{@state}"
308
+ end
309
+ end
310
+ end
311
+ end
312
+ end
313
+ end