ruby-object-framework 1.0.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.
- checksums.yaml +7 -0
- data/API.md +78 -0
- data/README.md +250 -0
- data/THIRD_PARTY_NOTICES.md +45 -0
- data/examples/guides/01_plain_use_case.rb +13 -0
- data/examples/guides/02_composed_application.rb +25 -0
- data/examples/guides/03_order_domain.rb +14 -0
- data/examples/guides/04_scoped_resources.rb +38 -0
- data/examples/guides/05_mapping.rb +39 -0
- data/examples/guides/06_associations.rb +49 -0
- data/examples/guides/07_http.rb +17 -0
- data/examples/guides/08_errors.rb +20 -0
- data/examples/guides/09_sqlite.rb +38 -0
- data/examples/guides/10_grpc.rb +26 -0
- data/examples/guides/11_ractor.rb +15 -0
- data/examples/guides/12_box.rb +11 -0
- data/examples/guides/13_testing.rb +24 -0
- data/examples/guides/manifest.yml +62 -0
- data/examples/orders/application.rb +51 -0
- data/examples/orders/contracts.rb +19 -0
- data/examples/orders/domain.rb +35 -0
- data/examples/orders/export_proto.rb +5 -0
- data/examples/orders/grpc_client.py +24 -0
- data/examples/orders/grpc_server.rb +23 -0
- data/examples/orders/server.rb +18 -0
- data/lib/object_framework/application.rb +136 -0
- data/lib/object_framework/capabilities.rb +59 -0
- data/lib/object_framework/errors.rb +85 -0
- data/lib/object_framework/execution.rb +184 -0
- data/lib/object_framework/grpc/messages.rb +30 -0
- data/lib/object_framework/grpc/server.rb +264 -0
- data/lib/object_framework/grpc/service.rb +55 -0
- data/lib/object_framework/http/io.rb +127 -0
- data/lib/object_framework/http/presentation.rb +55 -0
- data/lib/object_framework/http/protocol.rb +209 -0
- data/lib/object_framework/http/router.rb +80 -0
- data/lib/object_framework/http/server.rb +279 -0
- data/lib/object_framework/http2/connection.rb +399 -0
- data/lib/object_framework/http2/frame.rb +21 -0
- data/lib/object_framework/http2/hpack.rb +206 -0
- data/lib/object_framework/http2/hpack_tables.rb +333 -0
- data/lib/object_framework/http2/inbound.rb +75 -0
- data/lib/object_framework/memory/storage.rb +97 -0
- data/lib/object_framework/persistence/mapping.rb +150 -0
- data/lib/object_framework/persistence/query.rb +97 -0
- data/lib/object_framework/persistence/session.rb +173 -0
- data/lib/object_framework/persistence/transaction.rb +54 -0
- data/lib/object_framework/protobuf.rb +268 -0
- data/lib/object_framework/sqlite/compiler.rb +77 -0
- data/lib/object_framework/sqlite/schema.rb +95 -0
- data/lib/object_framework/sqlite/storage.rb +220 -0
- data/lib/object_framework/sqlite.rb +6 -0
- data/lib/object_framework/version.rb +5 -0
- data/lib/object_framework.rb +31 -0
- metadata +105 -0
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ObjectFramework
|
|
4
|
+
module HTTP2
|
|
5
|
+
Stream = Struct.new(:id, :context, :input, :send_window, :receive_window,
|
|
6
|
+
:remote_closed, :local_closed, :reset, :received, :content_length,
|
|
7
|
+
keyword_init: true)
|
|
8
|
+
|
|
9
|
+
class Connection
|
|
10
|
+
PREFACE = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n".b.freeze
|
|
11
|
+
MAX_WINDOW = 2**31 - 1
|
|
12
|
+
attr_reader :io
|
|
13
|
+
|
|
14
|
+
def initialize(io:, on_stream:, reporter:, header_limit: 32 * 1024,
|
|
15
|
+
stream_limit: 128, idle_timeout: 30, write_timeout: 30,
|
|
16
|
+
tls: false)
|
|
17
|
+
@io, @on_stream, @reporter = io, on_stream, reporter
|
|
18
|
+
@header_limit, @stream_limit, @idle_timeout, @write_timeout, @tls = header_limit, stream_limit, idle_timeout, write_timeout, tls
|
|
19
|
+
@reader = HTTP::Reader.new(io:)
|
|
20
|
+
@decoder, @encoder = HPACK::Decoder.new(header_limit:), HPACK::Encoder.new
|
|
21
|
+
@mutex, @write_mutex, @changed = Mutex.new, Mutex.new, ConditionVariable.new
|
|
22
|
+
@streams, @last_stream, @closed, @draining = {}, 0, false, false
|
|
23
|
+
@peer_initial_window, @send_window, @receive_window = 65_535, 65_535, 65_535
|
|
24
|
+
@peer_frame_size, @continuation, @first_frame = 16_384, nil, true
|
|
25
|
+
@context = ExecutionContext.new
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def run
|
|
29
|
+
handshake if @tls
|
|
30
|
+
raise ProtocolError, "Invalid HTTP/2 connection preface" unless @reader.read_exact(PREFACE.bytesize, deadline: deadline) == PREFACE
|
|
31
|
+
send_frame(4, 0, 0, [3, @stream_limit, 6, @header_limit].pack("nNnN"))
|
|
32
|
+
loop do
|
|
33
|
+
frame = Frame.read(@reader, deadline:)
|
|
34
|
+
if @first_frame
|
|
35
|
+
raise ProtocolError unless frame.type == 4 && frame.stream_id.zero? && (frame.flags & 1).zero?
|
|
36
|
+
@first_frame = false
|
|
37
|
+
end
|
|
38
|
+
raise ProtocolError if @continuation && (frame.type != 9 || frame.stream_id != @continuation[:id])
|
|
39
|
+
begin
|
|
40
|
+
process(frame)
|
|
41
|
+
rescue StreamError => error
|
|
42
|
+
reject_stream(error)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
rescue EOFError, IOError, SystemCallError
|
|
46
|
+
nil
|
|
47
|
+
rescue StandardError => error
|
|
48
|
+
@reporter.report(error, context: @context)
|
|
49
|
+
begin
|
|
50
|
+
code = error.is_a?(ProtocolError) ? error.code : 2
|
|
51
|
+
send_frame(7, 0, 0, [@last_stream, code].pack("NN"))
|
|
52
|
+
rescue StandardError
|
|
53
|
+
nil
|
|
54
|
+
end
|
|
55
|
+
ensure
|
|
56
|
+
close
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def close
|
|
60
|
+
streams = @mutex.synchronize do
|
|
61
|
+
@closed = true
|
|
62
|
+
@changed.broadcast
|
|
63
|
+
@streams.values.dup
|
|
64
|
+
end
|
|
65
|
+
streams.each { |stream| stream.input.cancel }
|
|
66
|
+
io.close unless io.closed?
|
|
67
|
+
rescue IOError, SystemCallError
|
|
68
|
+
nil
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def send_headers(stream, headers, end_stream: false)
|
|
72
|
+
stream.context.cancellation.check! unless end_stream
|
|
73
|
+
block = @encoder.encode(headers)
|
|
74
|
+
raise CapacityExceeded, "Outgoing headers exceed configured limit" if headers.sum { |name, value| name.bytesize + value.bytesize + 32 } > @header_limit
|
|
75
|
+
@write_mutex.synchronize do
|
|
76
|
+
offset, first = 0, true
|
|
77
|
+
begin
|
|
78
|
+
chunk = block.byteslice(offset, @peer_frame_size) || "".b
|
|
79
|
+
offset += chunk.bytesize
|
|
80
|
+
flags = offset == block.bytesize ? 4 : 0
|
|
81
|
+
flags |= 1 if first && end_stream
|
|
82
|
+
write_frame(first ? 1 : 9, flags, stream.id, chunk)
|
|
83
|
+
first = false
|
|
84
|
+
end while offset < block.bytesize
|
|
85
|
+
end
|
|
86
|
+
finish_local(stream) if end_stream
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def send_data(stream, bytes)
|
|
90
|
+
offset = 0
|
|
91
|
+
while offset < bytes.bytesize
|
|
92
|
+
count = @mutex.synchronize do
|
|
93
|
+
loop do
|
|
94
|
+
stream.context.cancellation.check!
|
|
95
|
+
raise Cancelled, "HTTP/2 stream is closed" if @closed || stream.reset || stream.local_closed
|
|
96
|
+
available = [@send_window, stream.send_window, @peer_frame_size, bytes.bytesize - offset].min
|
|
97
|
+
if available.positive?
|
|
98
|
+
@send_window -= available
|
|
99
|
+
stream.send_window -= available
|
|
100
|
+
break available
|
|
101
|
+
end
|
|
102
|
+
remaining = stream.context.cancellation.deadline ? stream.context.cancellation.deadline - HTTP::Timing.now : 0.1
|
|
103
|
+
@changed.wait(@mutex, [[remaining, 0].max, 0.1].min)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
send_frame(0, 0, stream.id, bytes.byteslice(offset, count))
|
|
107
|
+
offset += count
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def reset?(stream)
|
|
112
|
+
@mutex.synchronize { @closed || stream.reset }
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
private
|
|
116
|
+
|
|
117
|
+
def deadline; HTTP::Timing.now + @idle_timeout; end
|
|
118
|
+
|
|
119
|
+
def handshake
|
|
120
|
+
limit = deadline
|
|
121
|
+
loop do
|
|
122
|
+
result = io.accept_nonblock(exception: false)
|
|
123
|
+
break unless %i[wait_readable wait_writable].include?(result)
|
|
124
|
+
HTTP::Timing.wait(io, result == :wait_readable ? :read : :write, limit)
|
|
125
|
+
end
|
|
126
|
+
raise ProtocolError, "TLS must negotiate h2" unless io.alpn_protocol == "h2"
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def process(frame)
|
|
130
|
+
case frame.type
|
|
131
|
+
when 0 then receive_data(frame)
|
|
132
|
+
when 1 then receive_headers(frame)
|
|
133
|
+
when 2
|
|
134
|
+
require_stream(frame)
|
|
135
|
+
require_size(frame, 5)
|
|
136
|
+
raise ProtocolError if frame.payload.unpack1("N") & MAX_WINDOW == frame.stream_id
|
|
137
|
+
when 3 then receive_reset(frame)
|
|
138
|
+
when 4 then receive_settings(frame)
|
|
139
|
+
when 5 then raise ProtocolError, "Clients cannot push streams"
|
|
140
|
+
when 6
|
|
141
|
+
require_connection(frame)
|
|
142
|
+
require_size(frame, 8)
|
|
143
|
+
send_frame(6, 1, 0, frame.payload) if (frame.flags & 1).zero?
|
|
144
|
+
when 7
|
|
145
|
+
require_connection(frame)
|
|
146
|
+
raise ProtocolError.new(code: 6) if frame.payload.bytesize < 8
|
|
147
|
+
@draining = true
|
|
148
|
+
when 8 then receive_window(frame)
|
|
149
|
+
when 9 then receive_continuation(frame)
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def receive_settings(frame)
|
|
154
|
+
require_connection(frame)
|
|
155
|
+
if (frame.flags & 1).positive?
|
|
156
|
+
require_size(frame, 0)
|
|
157
|
+
return
|
|
158
|
+
end
|
|
159
|
+
raise ProtocolError.new(code: 6) unless (frame.payload.bytesize % 6).zero?
|
|
160
|
+
frame.payload.unpack("nN" * (frame.payload.bytesize / 6)).each_slice(2) do |id, value|
|
|
161
|
+
case id
|
|
162
|
+
when 2 then raise ProtocolError unless value <= 1
|
|
163
|
+
when 4
|
|
164
|
+
raise ProtocolError.new(code: 3) if value > MAX_WINDOW
|
|
165
|
+
@mutex.synchronize do
|
|
166
|
+
difference = value - @peer_initial_window
|
|
167
|
+
@streams.each_value do |stream|
|
|
168
|
+
raise ProtocolError.new(code: 3) if stream.send_window + difference > MAX_WINDOW
|
|
169
|
+
stream.send_window += difference
|
|
170
|
+
end
|
|
171
|
+
@peer_initial_window = value
|
|
172
|
+
@changed.broadcast
|
|
173
|
+
end
|
|
174
|
+
when 5
|
|
175
|
+
raise ProtocolError unless value.between?(16_384, 16_777_215)
|
|
176
|
+
@peer_frame_size = value
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
send_frame(4, 1, 0, "".b)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def receive_headers(frame)
|
|
183
|
+
require_stream(frame)
|
|
184
|
+
stream = @mutex.synchronize { @streams[frame.stream_id] }
|
|
185
|
+
fresh = !stream
|
|
186
|
+
if fresh
|
|
187
|
+
raise ProtocolError unless frame.stream_id.odd? && frame.stream_id > @last_stream && !@draining
|
|
188
|
+
@last_stream = frame.stream_id
|
|
189
|
+
else
|
|
190
|
+
raise StreamError.new(frame.stream_id, code: 5) if stream.remote_closed
|
|
191
|
+
raise StreamError.new(frame.stream_id) if (frame.flags & 1).zero?
|
|
192
|
+
end
|
|
193
|
+
payload, = unpad(frame)
|
|
194
|
+
if (frame.flags & 32).positive?
|
|
195
|
+
raise ProtocolError.new(code: 6) if payload.bytesize < 5
|
|
196
|
+
raise ProtocolError if payload.unpack1("N") & MAX_WINDOW == frame.stream_id
|
|
197
|
+
payload = payload.byteslice(5..)
|
|
198
|
+
end
|
|
199
|
+
@continuation = { id: frame.stream_id, bytes: payload, end_stream: (frame.flags & 1).positive?, fresh: }
|
|
200
|
+
complete_headers if (frame.flags & 4).positive?
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def receive_continuation(frame)
|
|
204
|
+
raise ProtocolError unless @continuation && @continuation[:id] == frame.stream_id
|
|
205
|
+
@continuation[:bytes] += frame.payload
|
|
206
|
+
raise CompressionError if @continuation[:bytes].bytesize > @header_limit
|
|
207
|
+
complete_headers if (frame.flags & 4).positive?
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def complete_headers
|
|
211
|
+
block, @continuation = @continuation, nil
|
|
212
|
+
headers = @decoder.decode(block[:bytes])
|
|
213
|
+
begin
|
|
214
|
+
validate_headers(headers, request: block[:fresh])
|
|
215
|
+
rescue ProtocolError => error
|
|
216
|
+
raise StreamError.new(block[:id], error.message, code: error.code)
|
|
217
|
+
end
|
|
218
|
+
if block[:fresh]
|
|
219
|
+
context = ExecutionContext.new
|
|
220
|
+
stream = Stream.new(id: block[:id], context:, send_window: @peer_initial_window,
|
|
221
|
+
receive_window: 65_535, remote_closed: block[:end_stream], local_closed: false,
|
|
222
|
+
reset: false, received: 0, content_length: nil)
|
|
223
|
+
stream.input = Inbound.new(cancellation: context.cancellation,
|
|
224
|
+
on_consume: ->(count) { release_capacity(stream, count) })
|
|
225
|
+
lengths = headers.select { |name, _| name == "content-length" }.map(&:last)
|
|
226
|
+
unless lengths.empty?
|
|
227
|
+
raise ProtocolError unless lengths.size == 1 && /\A[0-9]+\z/.match?(lengths.first)
|
|
228
|
+
stream.content_length = Integer(lengths.first, 10)
|
|
229
|
+
end
|
|
230
|
+
@mutex.synchronize do
|
|
231
|
+
raise StreamError.new(stream.id, "Peer exceeded concurrent stream limit", code: 7) if @streams.size >= @stream_limit
|
|
232
|
+
@streams[stream.id] = stream
|
|
233
|
+
end
|
|
234
|
+
finish_remote(stream) if block[:end_stream]
|
|
235
|
+
@on_stream.call(self, stream, headers)
|
|
236
|
+
else
|
|
237
|
+
stream = @mutex.synchronize { @streams.fetch(block[:id]) }
|
|
238
|
+
finish_remote(stream)
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def validate_headers(headers, request:)
|
|
243
|
+
regular, pseudo = false, {}
|
|
244
|
+
headers.each do |name, value|
|
|
245
|
+
raise ProtocolError unless /\A:?[!#$%&'*+.^_`|~0-9a-z-]+\z/.match?(name)
|
|
246
|
+
raise ProtocolError if value.match?(/[\x00\r\n]/) || value.start_with?(" ", "\t") || value.end_with?(" ", "\t")
|
|
247
|
+
if name.start_with?(":")
|
|
248
|
+
raise ProtocolError if regular || !request || pseudo.key?(name) || !%w[:method :path :scheme :authority].include?(name)
|
|
249
|
+
pseudo[name] = value
|
|
250
|
+
else
|
|
251
|
+
regular = true
|
|
252
|
+
raise ProtocolError if %w[connection keep-alive proxy-connection transfer-encoding upgrade].include?(name)
|
|
253
|
+
raise ProtocolError if name == "te" && value != "trailers"
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
if request
|
|
257
|
+
raise ProtocolError unless pseudo[":method"] && %w[http https].include?(pseudo[":scheme"]) && pseudo[":path"]&.start_with?("/")
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def receive_data(frame)
|
|
262
|
+
require_stream(frame)
|
|
263
|
+
payload, overhead = unpad(frame)
|
|
264
|
+
stream = @mutex.synchronize do
|
|
265
|
+
@receive_window -= frame.payload.bytesize
|
|
266
|
+
raise ProtocolError.new(code: 3) if @receive_window.negative?
|
|
267
|
+
@streams[frame.stream_id]
|
|
268
|
+
end
|
|
269
|
+
unless stream
|
|
270
|
+
raise ProtocolError if frame.stream_id > @last_stream
|
|
271
|
+
release_capacity(nil, frame.payload.bytesize)
|
|
272
|
+
send_frame(3, 0, frame.stream_id, [5].pack("N"))
|
|
273
|
+
return
|
|
274
|
+
end
|
|
275
|
+
if stream.remote_closed
|
|
276
|
+
release_capacity(nil, frame.payload.bytesize)
|
|
277
|
+
raise StreamError.new(stream.id, "DATA after end of stream", code: 5)
|
|
278
|
+
end
|
|
279
|
+
exceeded = false
|
|
280
|
+
@mutex.synchronize do
|
|
281
|
+
stream.receive_window -= frame.payload.bytesize
|
|
282
|
+
exceeded = stream.receive_window.negative?
|
|
283
|
+
stream.received += payload.bytesize
|
|
284
|
+
end
|
|
285
|
+
if exceeded
|
|
286
|
+
release_capacity(nil, frame.payload.bytesize)
|
|
287
|
+
raise StreamError.new(stream.id, code: 3)
|
|
288
|
+
end
|
|
289
|
+
release_capacity(stream, overhead) if overhead.positive?
|
|
290
|
+
stream.input.push(payload)
|
|
291
|
+
finish_remote(stream) if (frame.flags & 1).positive?
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def receive_reset(frame)
|
|
295
|
+
require_stream(frame)
|
|
296
|
+
require_size(frame, 4)
|
|
297
|
+
stream = @mutex.synchronize do
|
|
298
|
+
raise ProtocolError if frame.stream_id > @last_stream
|
|
299
|
+
found = @streams.delete(frame.stream_id)
|
|
300
|
+
found.reset = true if found
|
|
301
|
+
@changed.broadcast
|
|
302
|
+
found
|
|
303
|
+
end
|
|
304
|
+
stream&.input&.cancel
|
|
305
|
+
stream&.input&.discard
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
def receive_window(frame)
|
|
309
|
+
require_size(frame, 4)
|
|
310
|
+
increment = frame.payload.unpack1("N") & MAX_WINDOW
|
|
311
|
+
if increment.zero?
|
|
312
|
+
raise ProtocolError if frame.stream_id.zero?
|
|
313
|
+
raise StreamError.new(frame.stream_id)
|
|
314
|
+
end
|
|
315
|
+
@mutex.synchronize do
|
|
316
|
+
if frame.stream_id.zero?
|
|
317
|
+
raise ProtocolError.new(code: 3) if @send_window + increment > MAX_WINDOW
|
|
318
|
+
@send_window += increment
|
|
319
|
+
elsif (stream = @streams[frame.stream_id])
|
|
320
|
+
raise StreamError.new(stream.id, code: 3) if stream.send_window + increment > MAX_WINDOW
|
|
321
|
+
stream.send_window += increment
|
|
322
|
+
elsif frame.stream_id > @last_stream
|
|
323
|
+
raise ProtocolError
|
|
324
|
+
end
|
|
325
|
+
@changed.broadcast
|
|
326
|
+
end
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
def finish_remote(stream)
|
|
330
|
+
if stream.content_length && stream.received != stream.content_length
|
|
331
|
+
raise StreamError.new(stream.id, "Content length does not match DATA")
|
|
332
|
+
end
|
|
333
|
+
@mutex.synchronize do
|
|
334
|
+
stream.remote_closed = true
|
|
335
|
+
@streams.delete(stream.id) if stream.local_closed
|
|
336
|
+
end
|
|
337
|
+
stream.input.finish
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
def finish_local(stream)
|
|
341
|
+
@mutex.synchronize do
|
|
342
|
+
stream.local_closed = true
|
|
343
|
+
@streams.delete(stream.id) if stream.remote_closed
|
|
344
|
+
@changed.broadcast
|
|
345
|
+
end
|
|
346
|
+
stream.input.discard
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
def release_capacity(stream, count)
|
|
350
|
+
return if count.zero?
|
|
351
|
+
active_stream = @mutex.synchronize do
|
|
352
|
+
return if @closed
|
|
353
|
+
@receive_window += count
|
|
354
|
+
if stream && @streams.key?(stream.id) && !stream.reset
|
|
355
|
+
stream.receive_window += count
|
|
356
|
+
true
|
|
357
|
+
end
|
|
358
|
+
end
|
|
359
|
+
send_frame(8, 0, 0, [count].pack("N"))
|
|
360
|
+
send_frame(8, 0, stream.id, [count].pack("N")) if active_stream
|
|
361
|
+
rescue IOError, SystemCallError
|
|
362
|
+
nil
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
def reject_stream(error)
|
|
366
|
+
stream = @mutex.synchronize do
|
|
367
|
+
found = @streams.delete(error.stream_id)
|
|
368
|
+
found.reset = true if found
|
|
369
|
+
@changed.broadcast
|
|
370
|
+
found
|
|
371
|
+
end
|
|
372
|
+
@reporter.report(error, context: stream&.context || ExecutionContext.new)
|
|
373
|
+
stream&.input&.cancel
|
|
374
|
+
stream&.input&.discard
|
|
375
|
+
send_frame(3, 0, error.stream_id, [error.code].pack("N"))
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
def unpad(frame)
|
|
379
|
+
return [frame.payload, 0] if (frame.flags & 8).zero?
|
|
380
|
+
padding = frame.payload.getbyte(0)
|
|
381
|
+
raise ProtocolError unless padding && padding < frame.payload.bytesize
|
|
382
|
+
[frame.payload.byteslice(1, frame.payload.bytesize - padding - 1), padding + 1]
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
def require_connection(frame); raise ProtocolError unless frame.stream_id.zero?; end
|
|
386
|
+
def require_stream(frame); raise ProtocolError if frame.stream_id.zero?; end
|
|
387
|
+
def require_size(frame, size); raise ProtocolError.new(code: 6) unless frame.payload.bytesize == size; end
|
|
388
|
+
|
|
389
|
+
def send_frame(type, flags, stream_id, payload)
|
|
390
|
+
@write_mutex.synchronize { write_frame(type, flags, stream_id, payload) }
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
def write_frame(type, flags, stream_id, payload)
|
|
394
|
+
frame = Frame.new(type:, flags:, stream_id:, payload:)
|
|
395
|
+
@reader.write(frame.encode, deadline: HTTP::Timing.now + @write_timeout)
|
|
396
|
+
end
|
|
397
|
+
end
|
|
398
|
+
end
|
|
399
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ObjectFramework
|
|
4
|
+
module HTTP2
|
|
5
|
+
Frame = Data.define(:type, :flags, :stream_id, :payload) do
|
|
6
|
+
def encode
|
|
7
|
+
raise ProtocolError, "Frame is too large" if payload.bytesize > 16_777_215
|
|
8
|
+
length = [payload.bytesize].pack("N").byteslice(1, 3)
|
|
9
|
+
length + [type, flags, stream_id & 0x7fff_ffff].pack("CCN") + payload.b
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def self.read(reader, deadline:, max_size: 16_384)
|
|
13
|
+
header = reader.read_exact(9, deadline:)
|
|
14
|
+
length = ("\x00".b + header.byteslice(0, 3)).unpack1("N")
|
|
15
|
+
raise ProtocolError.new("Frame exceeds advertised size", code: 6) if length > max_size
|
|
16
|
+
type, flags, stream_id = header.byteslice(3, 6).unpack("CCN")
|
|
17
|
+
new(type:, flags:, stream_id: stream_id & 0x7fff_ffff, payload: reader.read_exact(length, deadline:))
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "hpack_tables"
|
|
4
|
+
|
|
5
|
+
module ObjectFramework
|
|
6
|
+
module HTTP2
|
|
7
|
+
class ProtocolError < StandardError
|
|
8
|
+
attr_reader :code
|
|
9
|
+
def initialize(message = "HTTP/2 protocol error", code: 1)
|
|
10
|
+
super(message)
|
|
11
|
+
@code = code
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
class CompressionError < ProtocolError
|
|
15
|
+
def initialize(message = "Invalid HPACK block"); super(message, code: 9); end
|
|
16
|
+
end
|
|
17
|
+
class StreamError < ProtocolError
|
|
18
|
+
attr_reader :stream_id
|
|
19
|
+
def initialize(stream_id, message = "Invalid HTTP/2 stream", code: 1)
|
|
20
|
+
super(message, code:)
|
|
21
|
+
@stream_id = stream_id
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
module HPACK
|
|
26
|
+
module Huffman
|
|
27
|
+
TREE = begin
|
|
28
|
+
root = {}
|
|
29
|
+
HUFFMAN_CODES.each_with_index do |(code, length), symbol|
|
|
30
|
+
node = root
|
|
31
|
+
(length - 1).downto(0) { |bit| node = (node[(code >> bit) & 1] ||= {}) }
|
|
32
|
+
node[:symbol] = symbol
|
|
33
|
+
end
|
|
34
|
+
freeze_tree = ->(node) { node.each_value { |value| freeze_tree.call(value) if value.is_a?(Hash) }; node.freeze }
|
|
35
|
+
freeze_tree.call(root)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def self.encode(bytes)
|
|
39
|
+
output, bits, length = +"".b, 0, 0
|
|
40
|
+
bytes.each_byte do |byte|
|
|
41
|
+
code, size = HUFFMAN_CODES.fetch(byte)
|
|
42
|
+
bits, length = (bits << size) | code, length + size
|
|
43
|
+
while length >= 8
|
|
44
|
+
length -= 8
|
|
45
|
+
output << ((bits >> length) & 255)
|
|
46
|
+
end
|
|
47
|
+
bits &= (1 << length) - 1
|
|
48
|
+
end
|
|
49
|
+
output << ((bits << (8 - length)) | ((1 << (8 - length)) - 1)) if length.positive?
|
|
50
|
+
output
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def self.decode(bytes, limit: 32 * 1024)
|
|
54
|
+
output, node, tail, tail_size = +"".b, TREE, 0, 0
|
|
55
|
+
bytes.each_byte do |byte|
|
|
56
|
+
7.downto(0) do |index|
|
|
57
|
+
bit = (byte >> index) & 1
|
|
58
|
+
node = node[bit] || raise(CompressionError)
|
|
59
|
+
tail, tail_size = (tail << 1) | bit, tail_size + 1
|
|
60
|
+
if (symbol = node[:symbol])
|
|
61
|
+
raise CompressionError if symbol == 256 || output.bytesize >= limit
|
|
62
|
+
output << symbol
|
|
63
|
+
node, tail, tail_size = TREE, 0, 0
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
raise CompressionError if tail_size > 7 || tail != (1 << tail_size) - 1
|
|
68
|
+
output
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
module IntegerCoding
|
|
73
|
+
def self.encode(number, prefix:, mask: 0)
|
|
74
|
+
maximum = (1 << prefix) - 1
|
|
75
|
+
return [mask | number].pack("C") if number < maximum
|
|
76
|
+
bytes, number = [mask | maximum].pack("C"), number - maximum
|
|
77
|
+
while number >= 128
|
|
78
|
+
bytes << ((number & 127) | 128)
|
|
79
|
+
number >>= 7
|
|
80
|
+
end
|
|
81
|
+
bytes << number
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
class Cursor
|
|
86
|
+
def initialize(bytes); @bytes, @offset = bytes.b, 0; end
|
|
87
|
+
def done?; @offset == @bytes.bytesize; end
|
|
88
|
+
def peek; @bytes.getbyte(@offset) || raise(CompressionError); end
|
|
89
|
+
|
|
90
|
+
def take(count)
|
|
91
|
+
raise CompressionError if count.negative? || @offset + count > @bytes.bytesize
|
|
92
|
+
result = @bytes.byteslice(@offset, count)
|
|
93
|
+
@offset += count
|
|
94
|
+
result
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def integer(prefix)
|
|
98
|
+
maximum = (1 << prefix) - 1
|
|
99
|
+
value = take(1).getbyte(0) & maximum
|
|
100
|
+
return value if value < maximum
|
|
101
|
+
9.times do |index|
|
|
102
|
+
byte = take(1).getbyte(0)
|
|
103
|
+
value += (byte & 127) << (index * 7)
|
|
104
|
+
raise CompressionError if value > 2**63 - 1
|
|
105
|
+
return value if byte < 128
|
|
106
|
+
end
|
|
107
|
+
raise CompressionError
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def string(limit:)
|
|
111
|
+
huffman = (peek & 128).positive?
|
|
112
|
+
length = integer(7)
|
|
113
|
+
value = take(length)
|
|
114
|
+
value = Huffman.decode(value, limit:) if huffman
|
|
115
|
+
raise CompressionError if value.bytesize > limit
|
|
116
|
+
value
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
class Decoder
|
|
121
|
+
def initialize(table_limit: 4096, header_limit: 32 * 1024)
|
|
122
|
+
@maximum, @capacity, @header_limit = table_limit, table_limit, header_limit
|
|
123
|
+
@dynamic, @size = [], 0
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def decode(bytes)
|
|
127
|
+
cursor, headers, total, saw_header = Cursor.new(bytes), [], 0, false
|
|
128
|
+
until cursor.done?
|
|
129
|
+
first = cursor.peek
|
|
130
|
+
if (first & 128).positive?
|
|
131
|
+
pair = lookup(cursor.integer(7))
|
|
132
|
+
elsif (first & 224) == 32
|
|
133
|
+
raise CompressionError if saw_header
|
|
134
|
+
capacity = cursor.integer(5)
|
|
135
|
+
raise CompressionError if capacity > @maximum
|
|
136
|
+
@capacity = capacity
|
|
137
|
+
evict
|
|
138
|
+
next
|
|
139
|
+
else
|
|
140
|
+
indexed = (first & 64).positive?
|
|
141
|
+
index = cursor.integer(indexed ? 6 : 4)
|
|
142
|
+
name = index.zero? ? cursor.string(limit: @header_limit - total) : lookup(index).first
|
|
143
|
+
value = cursor.string(limit: @header_limit - total)
|
|
144
|
+
pair = [name.freeze, value.freeze].freeze
|
|
145
|
+
add(pair) if indexed
|
|
146
|
+
end
|
|
147
|
+
total += entry_size(pair)
|
|
148
|
+
raise CompressionError if total > @header_limit
|
|
149
|
+
headers << pair
|
|
150
|
+
saw_header = true
|
|
151
|
+
end
|
|
152
|
+
headers.freeze
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
private
|
|
156
|
+
|
|
157
|
+
def lookup(index)
|
|
158
|
+
raise CompressionError if index.zero?
|
|
159
|
+
(index <= STATIC_TABLE.size ? STATIC_TABLE[index - 1] : @dynamic[index - STATIC_TABLE.size - 1]) || raise(CompressionError)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def entry_size(pair); pair[0].bytesize + pair[1].bytesize + 32; end
|
|
163
|
+
|
|
164
|
+
def add(pair)
|
|
165
|
+
size = entry_size(pair)
|
|
166
|
+
if size > @capacity
|
|
167
|
+
@dynamic.clear
|
|
168
|
+
@size = 0
|
|
169
|
+
else
|
|
170
|
+
@dynamic.unshift(pair)
|
|
171
|
+
@size += size
|
|
172
|
+
evict
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def evict
|
|
177
|
+
@size -= entry_size(@dynamic.pop) while @size > @capacity
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
class Encoder
|
|
182
|
+
def encode(headers)
|
|
183
|
+
headers.map do |name, value|
|
|
184
|
+
name, value = name.to_s.b, value.to_s.b
|
|
185
|
+
exact = STATIC_TABLE.index([name, value])
|
|
186
|
+
if exact
|
|
187
|
+
IntegerCoding.encode(exact + 1, prefix: 7, mask: 128)
|
|
188
|
+
else
|
|
189
|
+
index = STATIC_TABLE.index { |pair| pair.first == name }
|
|
190
|
+
# Never index application metadata, including credentials and operation IDs.
|
|
191
|
+
prefix = IntegerCoding.encode(index ? index + 1 : 0, prefix: 4, mask: 16)
|
|
192
|
+
prefix += string(name) unless index
|
|
193
|
+
prefix + string(value)
|
|
194
|
+
end
|
|
195
|
+
end.join.b
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
private
|
|
199
|
+
|
|
200
|
+
def string(bytes)
|
|
201
|
+
IntegerCoding.encode(bytes.bytesize, prefix: 7) + bytes
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
end
|