raioquic 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.
Files changed (63) hide show
  1. checksums.yaml +7 -0
  2. data/.containerignore +4 -0
  3. data/.rubocop.yml +93 -0
  4. data/CHANGELOG.md +5 -0
  5. data/CODE_OF_CONDUCT.md +84 -0
  6. data/Containerfile +6 -0
  7. data/Gemfile +24 -0
  8. data/Gemfile.lock +113 -0
  9. data/LICENSE +28 -0
  10. data/README.md +48 -0
  11. data/Rakefile +16 -0
  12. data/Steepfile +8 -0
  13. data/example/curlcatcher.rb +18 -0
  14. data/example/interoperability/README.md +9 -0
  15. data/example/interoperability/aioquic/aioquic_client.py +47 -0
  16. data/example/interoperability/aioquic/aioquic_server.py +34 -0
  17. data/example/interoperability/key.pem +28 -0
  18. data/example/interoperability/localhost-unasuke-dev.crt +21 -0
  19. data/example/interoperability/quic-go/sample_server.go +61 -0
  20. data/example/interoperability/raioquic_client.rb +42 -0
  21. data/example/interoperability/raioquic_server.rb +43 -0
  22. data/example/parse_curl_example.rb +108 -0
  23. data/lib/raioquic/buffer.rb +202 -0
  24. data/lib/raioquic/core_ext.rb +54 -0
  25. data/lib/raioquic/crypto/README.md +5 -0
  26. data/lib/raioquic/crypto/aesgcm.rb +52 -0
  27. data/lib/raioquic/crypto/backend/aead.rb +52 -0
  28. data/lib/raioquic/crypto/backend.rb +12 -0
  29. data/lib/raioquic/crypto.rb +10 -0
  30. data/lib/raioquic/quic/configuration.rb +81 -0
  31. data/lib/raioquic/quic/connection.rb +2776 -0
  32. data/lib/raioquic/quic/crypto.rb +317 -0
  33. data/lib/raioquic/quic/event.rb +69 -0
  34. data/lib/raioquic/quic/logger.rb +272 -0
  35. data/lib/raioquic/quic/packet.rb +471 -0
  36. data/lib/raioquic/quic/packet_builder.rb +301 -0
  37. data/lib/raioquic/quic/rangeset.rb +113 -0
  38. data/lib/raioquic/quic/recovery.rb +528 -0
  39. data/lib/raioquic/quic/stream.rb +343 -0
  40. data/lib/raioquic/quic.rb +20 -0
  41. data/lib/raioquic/tls.rb +1659 -0
  42. data/lib/raioquic/version.rb +5 -0
  43. data/lib/raioquic.rb +12 -0
  44. data/misc/export_x25519.py +43 -0
  45. data/misc/gen_rfc8448_keypair.rb +90 -0
  46. data/raioquic.gemspec +37 -0
  47. data/sig/raioquic/buffer.rbs +37 -0
  48. data/sig/raioquic/core_ext.rbs +7 -0
  49. data/sig/raioquic/crypto/aesgcm.rbs +20 -0
  50. data/sig/raioquic/crypto/backend/aead.rbs +11 -0
  51. data/sig/raioquic/quic/configuration.rbs +34 -0
  52. data/sig/raioquic/quic/connection.rbs +277 -0
  53. data/sig/raioquic/quic/crypto.rbs +88 -0
  54. data/sig/raioquic/quic/event.rbs +51 -0
  55. data/sig/raioquic/quic/logger.rbs +57 -0
  56. data/sig/raioquic/quic/packet.rbs +157 -0
  57. data/sig/raioquic/quic/packet_builder.rbs +76 -0
  58. data/sig/raioquic/quic/rangeset.rbs +17 -0
  59. data/sig/raioquic/quic/recovery.rbs +142 -0
  60. data/sig/raioquic/quic/stream.rbs +87 -0
  61. data/sig/raioquic/tls.rbs +444 -0
  62. data/sig/raioquic.rbs +9 -0
  63. metadata +121 -0
@@ -0,0 +1,301 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../buffer"
4
+ require_relative "../tls"
5
+ require_relative "packet"
6
+
7
+ module Raioquic
8
+ module Quic
9
+ class PacketBuilder
10
+ PACKET_MAX_SIZE = 1280
11
+ PACKET_LENGTH_SEND_SIZE = 2
12
+ PACKET_NUMBER_SEND_SIZE = 2
13
+
14
+ class QuicDeliveryState
15
+ ACKED = 0
16
+ LOST = 1
17
+ EXPIRED = 2
18
+ end
19
+
20
+ QuicSentPacket = _ = Struct.new( # rubocop:disable Naming/ConstantName
21
+ :epoch,
22
+ :in_flight,
23
+ :is_ack_eliciting,
24
+ :is_crypto_packet,
25
+ :packet_number,
26
+ :packet_type,
27
+ :sent_time,
28
+ :sent_bytes,
29
+ :delivery_handlers,
30
+ :quic_logger_frames,
31
+ )
32
+
33
+ QuicPacketBuilderStop = Class.new(StandardError)
34
+
35
+ # Helper for building QUIC packets.
36
+ class QuicPacketBuilder
37
+ attr_reader :packet_number
38
+ attr_reader :quic_logger_frames
39
+ attr_accessor :max_flight_bytes
40
+ attr_accessor :max_total_bytes
41
+
42
+ # Helper for building QUIC packets.
43
+ def initialize(host_cid:, peer_cid:, version:, is_client:, packet_number: 0, peer_token: "", quic_logger: nil, spin_bit: false)
44
+ @max_flight_bytes = nil
45
+ @max_total_bytes = nil
46
+ @quic_logger_frames = nil
47
+
48
+ @host_cid = host_cid
49
+ @is_client = is_client
50
+ @peer_cid = peer_cid
51
+ @peer_token = peer_token
52
+ @quic_logger = quic_logger
53
+ @spin_bit = spin_bit
54
+ @version = version
55
+
56
+ @datagrams = []
57
+ @datagram_flight_bytes = 0
58
+ @datagram_init = true
59
+ @packets = []
60
+ @flight_bytes = 0
61
+ @total_bytes = 0
62
+
63
+ @header_size = 0
64
+ @packet = nil
65
+ @packet_crypto = nil
66
+ @packet_long_header = false
67
+ @packet_number = packet_number
68
+ @packet_start = 0
69
+ @packet_type = 0
70
+
71
+ @buffer = Buffer.new(capacity: PACKET_MAX_SIZE)
72
+ @buffer_capacity = PACKET_MAX_SIZE
73
+ @flight_capacity = PACKET_MAX_SIZE
74
+ end
75
+
76
+ # Returns true if the current packet is empty
77
+ def packet_is_empty
78
+ raise RuntimeError unless @packet
79
+
80
+ packet_size = @buffer.tell - @packet_start
81
+ # puts "*** Builder#packet_is_empty packet_size: #{packet_size}, header_size: #{@header_size}, buffer#tell: #{@buffer.tell}, @packet_start: #{@packet_start}"
82
+ return packet_size <= @header_size
83
+ end
84
+
85
+ def _packet_size
86
+ @buffer.tell - @packet_start
87
+ end
88
+
89
+ # Returns the remaining number of bytes which can be used in the current packet.
90
+ def remaining_buffer_space
91
+ @buffer_capacity - @buffer.tell - @packet_crypto.aead_tag_size
92
+ end
93
+
94
+ # Returns the remaining number of bytes which can be used in the current packet
95
+ def remaining_flight_space
96
+ @flight_capacity - @buffer.tell - @packet_crypto.aead_tag_size
97
+ end
98
+
99
+ # Returns the assembled datagrams
100
+ def flush
101
+ end_packet if @packet
102
+
103
+ flush_current_datagram
104
+ datagrams = @datagrams.dup
105
+ packets = @packets.dup
106
+ @datagrams = []
107
+ @packets = []
108
+ return [datagrams, packets]
109
+ end
110
+
111
+ # Starts a new frame.
112
+ def start_frame(frame_type:, capacity: 1, handler: nil, handler_args: []) # rubocop:disable Metrics/CyclomaticComplexity
113
+ if remaining_buffer_space < capacity || (
114
+ !Quic::Packet::NON_IN_FLIGHT_FRAME_TYPES.include?(frame_type) && remaining_flight_space < capacity
115
+ )
116
+ raise QuicPacketBuilderStop
117
+ end
118
+
119
+ @buffer.push_uint_var(frame_type)
120
+ @packet.is_ack_eliciting = true unless Quic::Packet::NON_ACK_ELICITING_FRAME_TYPES.include?(frame_type)
121
+ @packet.in_flight = true unless Quic::Packet::NON_IN_FLIGHT_FRAME_TYPES.include?(frame_type)
122
+ @packet.is_crypto_packet = true if frame_type == Quic::Packet::QuicFrameType::CRYPTO
123
+ if handler
124
+ # pp handler_args
125
+ @packet.delivery_handlers.append([handler, handler_args]) # TODO: what's this?
126
+ end
127
+ return @buffer
128
+ end
129
+
130
+ # Starts a new packet.
131
+ def start_packet(packet_type:, crypto:) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/MethodLength
132
+ buf = @buffer
133
+
134
+ # finish previous datagrams
135
+ end_packet if @packet
136
+
137
+ # if there is too little space remaining, start a new datagram
138
+ # FIXME: the limit is arbitrary! (from aioquic)
139
+ packet_start = buf.tell
140
+ if @buffer_capacity - packet_start < 128
141
+ flush_current_datagram
142
+ packet_start = 0
143
+ end
144
+
145
+ # initialize datagram if needed
146
+ # rubocop:disable Style/IfUnlessModifier
147
+ if @datagram_init
148
+ unless @max_total_bytes.nil?
149
+ remaining_total_bytes = @max_total_bytes - @total_bytes
150
+ if remaining_total_bytes < @buffer_capacity
151
+ @buffer_capacity = remaining_total_bytes
152
+ end
153
+ end
154
+
155
+ @flight_capacity = @buffer_capacity
156
+ unless @max_flight_bytes.nil?
157
+ remaining_flight_bytes = @max_flight_bytes - @flight_bytes
158
+ if remaining_flight_bytes < @flight_capacity
159
+ @flight_capacity = remaining_flight_bytes
160
+ end
161
+ end
162
+ @datagram_flight_bytes = 0
163
+ @datagram_init = false
164
+ end
165
+ # rubocop:enable Style/IfUnlessModifier
166
+
167
+ # calculate header size
168
+ packet_long_header = Quic::Packet.is_long_header(packet_type)
169
+ if packet_long_header
170
+ header_size = 11 + @peer_cid.bytesize + @host_cid.bytesize
171
+ if (packet_type & Quic::Packet::PACKET_TYPE_MASK) == Quic::Packet::PACKET_TYPE_INITIAL
172
+ token_length = @peer_token.bytesize
173
+ header_size += Buffer.size_uint_var(token_length) + token_length
174
+ end
175
+ else
176
+ header_size = 3 + @peer_cid.bytesize
177
+ end
178
+
179
+ # check we have enough space
180
+ raise QuicPacketBuilderStop if packet_start + header_size >= @buffer_capacity
181
+
182
+ # determine ack epoch
183
+ # rubocop:disable Style/CaseLikeIf
184
+ epoch = if packet_type == Quic::Packet::PACKET_TYPE_INITIAL
185
+ TLS::Epoch::INITIAL
186
+ elsif packet_type == Quic::Packet::PACKET_TYPE_HANDSHAKE
187
+ TLS::Epoch::HANDSHAKE
188
+ else
189
+ TLS::Epoch::ONE_RTT
190
+ end
191
+ # rubocop:enable Style/CaseLikeIf
192
+
193
+ @header_size = header_size
194
+ @packet = QuicSentPacket.new.tap do |p|
195
+ p.epoch = epoch
196
+ p.in_flight = false
197
+ p.is_ack_eliciting = false
198
+ p.is_crypto_packet = false
199
+ p.packet_number = @packet_number
200
+ p.packet_type = packet_type
201
+ p.delivery_handlers = []
202
+ p.quic_logger_frames = []
203
+ end
204
+ @packet_crypto = crypto
205
+ @packet_long_header = packet_long_header
206
+ @packet_start = packet_start
207
+ @packet_type = packet_type
208
+ @quic_logger_frames = @packet.quic_logger_frames
209
+
210
+ buf.seek(@packet_start + @header_size)
211
+ end
212
+
213
+ # Ends the current packet.
214
+ def end_packet # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/MethodLength
215
+ buf = @buffer
216
+ packet_size = buf.tell - @packet_start
217
+
218
+ if packet_size > @header_size
219
+ # padding to ensure sufficient sample size
220
+ padding_size = Quic::Packet::PACKET_NUMBER_MAX_SIZE - PACKET_NUMBER_SEND_SIZE + @header_size - packet_size
221
+
222
+ # padding for initial datagram
223
+ if @is_client && @packet_type == Quic::Packet::PACKET_TYPE_INITIAL && @packet.is_ack_eliciting && remaining_flight_space && remaining_flight_space > padding_size # rubocop:disable Layout/LineLength
224
+ padding_size = remaining_flight_space
225
+ end
226
+
227
+ if padding_size > 0
228
+ buf.push_bytes("\x00" * padding_size)
229
+ packet_size += padding_size
230
+ @packet.in_flight = true
231
+
232
+ # log frame
233
+ if @quic_logger
234
+ @packet.quic_logger_frames << @quic_logger.encode_padding_frame
235
+ end
236
+ end
237
+
238
+ # write header
239
+ # rubocop:disable Style/IdenticalConditionalBranches
240
+ if @packet_long_header
241
+ length = packet_size - @header_size + PACKET_NUMBER_SEND_SIZE + @packet_crypto.aead_tag_size
242
+ buf.seek(@packet_start)
243
+ buf.push_uint8(@packet_type | (PACKET_NUMBER_SEND_SIZE - 1))
244
+ buf.push_uint32(@version)
245
+ buf.push_uint8(@peer_cid.length)
246
+ buf.push_bytes(@peer_cid)
247
+ buf.push_uint8(@host_cid.length)
248
+ buf.push_bytes(@host_cid)
249
+ if @packet_type & Quic::Packet::PACKET_TYPE_MASK == Quic::Packet::PACKET_TYPE_INITIAL
250
+ buf.push_uint_var(@peer_token.length)
251
+ buf.push_bytes(@peer_token)
252
+ end
253
+ buf.push_uint16(length | 0x4000)
254
+ buf.push_uint16(@packet_number & 0xffff)
255
+ else
256
+ buf.seek(@packet_start)
257
+ buf.push_uint8(@packet_type | ((@spin_bit ? 1 : 0) << 5) | (@packet_crypto.key_phase << 2) | (PACKET_NUMBER_SEND_SIZE - 1))
258
+ buf.push_bytes(@peer_cid)
259
+ buf.push_uint16(@packet_number & 0xffff)
260
+ end
261
+ # rubocop:enable Style/IdenticalConditionalBranches
262
+
263
+ # encrypt in place
264
+ plain = buf.data_slice(start: @packet_start, ends: @packet_start + packet_size)
265
+ buf.seek(@packet_start)
266
+ buf.push_bytes(
267
+ @packet_crypto.encrypt_packet(
268
+ plain_header: plain[0...@header_size].force_encoding(Encoding::ASCII_8BIT),
269
+ plain_payload: plain[@header_size...packet_size].force_encoding(Encoding::ASCII_8BIT),
270
+ packet_number: @packet_number,
271
+ ),
272
+ )
273
+ @packet.sent_bytes = buf.tell - @packet_start
274
+ @packets << @packet
275
+
276
+ @datagram_flight_bytes += @packet.sent_bytes if @packet.in_flight
277
+ flush_current_datagram unless @packet_long_header
278
+ @packet_number += 1
279
+ else # packet_size > @header_size
280
+ # "cancel" the packet
281
+ buf.seek(@packet_start)
282
+ end
283
+
284
+ @packet = nil
285
+ @quic_logger_frames = nil
286
+ end
287
+
288
+ def flush_current_datagram
289
+ datagram_bytes = @buffer.tell
290
+ if datagram_bytes > 0 # rubocop:disable Style/GuardClause
291
+ @datagrams << @buffer.data
292
+ @flight_bytes += @datagram_flight_bytes
293
+ @total_bytes += datagram_bytes
294
+ @datagram_init = true
295
+ @buffer.seek(0)
296
+ end
297
+ end
298
+ end
299
+ end
300
+ end
301
+ end
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Raioquic
4
+ module Quic
5
+ # Raioquic::Quic::Rangeset
6
+ # Migrated from aioquic/src/aioquic/quic/rangeset.py
7
+ class Rangeset
8
+ def initialize(ranges: [])
9
+ @ranges = []
10
+ ranges.each do |r|
11
+ add(r.first, r.last)
12
+ end
13
+ sort
14
+ end
15
+
16
+ def list
17
+ @ranges
18
+ end
19
+
20
+ def add(start, stop = nil)
21
+ stop = start + 1 if stop.nil?
22
+
23
+ @ranges.each_with_index do |r, i|
24
+ # the added range is entirely before current item, insert here
25
+ if stop < r.first
26
+ @ranges.insert(i, (start...stop))
27
+ return # rubocop:disable Lint/NonLocalExitFromIterator
28
+ end
29
+
30
+ # the added range is entirely after current item, keep looking
31
+ next if start > r.last
32
+
33
+ # the added range touches the current item, merge it
34
+ start = [start, r.first].min
35
+ stop = [stop, r.last].max
36
+ while i < @ranges.size - 1 && @ranges[i + 1].first <= stop
37
+ stop = [@ranges[i + 1].last, stop].max
38
+ @ranges.delete_at(i + 1)
39
+ end
40
+ @ranges[i] = start...stop
41
+ return # rubocop:disable Lint/NonLocalExitFromIterator
42
+ end
43
+ # the added range is entirely after all existing items, append it
44
+ @ranges << (start...stop)
45
+ sort
46
+ end
47
+
48
+ def bounds
49
+ @ranges.first&.first...@ranges.last&.last
50
+ end
51
+
52
+ def subtract(start, stop) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
53
+ raise RuntimeError if stop < start
54
+
55
+ i = 0
56
+ while i < @ranges.length
57
+ r = @ranges[i]
58
+
59
+ # the removed range is entirely before current item, stop here
60
+ return if stop <= r.first
61
+
62
+ # the removed range is entirely after current item, keep looking
63
+ if start >= r.last
64
+ i += 1
65
+ next
66
+ end
67
+
68
+ # the removed range completely covers the current item, remove it
69
+ if start <= r.first && stop >= r.last
70
+ @ranges.delete_at(i)
71
+ next
72
+ end
73
+
74
+ # the removed range touches the current item
75
+ if start > r.first
76
+ @ranges[i] = r.first...start
77
+ @ranges.insert(i + 1, stop...r.last) if stop < r.last
78
+ else
79
+ @ranges[i] = stop...r.last
80
+ end
81
+ i += 1
82
+ end
83
+ sort
84
+ end
85
+
86
+ def shift
87
+ @ranges.shift
88
+ end
89
+
90
+ def in?(value)
91
+ @ranges.any? { |r| r.cover?(value) }
92
+ end
93
+
94
+ def length
95
+ @ranges.length
96
+ end
97
+
98
+ def eql?(other)
99
+ return false if other.class != self.class
100
+ return false if other.length != length
101
+
102
+ length.times.all? do |i|
103
+ @ranges[i] == other.list[i]
104
+ end
105
+ end
106
+ alias == eql?
107
+
108
+ private def sort
109
+ @ranges.sort! { |a, b| a.first <=> b.first }
110
+ end
111
+ end
112
+ end
113
+ end