thrift 0.23.0 → 0.25.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 (120) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +203 -40
  3. data/ext/binary_protocol_accelerated.c +130 -40
  4. data/ext/bytes.c +14 -0
  5. data/ext/compact_protocol.c +184 -91
  6. data/ext/constants.h +3 -0
  7. data/ext/extconf.rb +12 -4
  8. data/ext/macros.h +11 -0
  9. data/ext/memory_buffer.c +67 -12
  10. data/ext/struct.c +192 -71
  11. data/ext/thrift_native.c +7 -0
  12. data/lib/thrift/bytes.rb +6 -3
  13. data/lib/thrift/client.rb +31 -33
  14. data/lib/thrift/exceptions.rb +11 -8
  15. data/lib/thrift/multiplexed_processor.rb +17 -8
  16. data/lib/thrift/processor.rb +25 -3
  17. data/lib/thrift/protocol/base_protocol.rb +66 -31
  18. data/lib/thrift/protocol/binary_protocol.rb +80 -37
  19. data/lib/thrift/protocol/binary_protocol_accelerated.rb +7 -2
  20. data/lib/thrift/protocol/compact_protocol.rb +204 -88
  21. data/lib/thrift/protocol/header_protocol.rb +15 -3
  22. data/lib/thrift/protocol/json_protocol.rb +158 -109
  23. data/lib/thrift/protocol/multiplexed_protocol.rb +3 -3
  24. data/lib/thrift/protocol/protocol_decorator.rb +6 -2
  25. data/lib/thrift/serializer/deserializer.rb +1 -0
  26. data/lib/thrift/serializer/serializer.rb +7 -0
  27. data/lib/thrift/server/base_server.rb +11 -2
  28. data/lib/thrift/server/nonblocking_server.rb +69 -20
  29. data/lib/thrift/server/rack_application.rb +69 -0
  30. data/lib/thrift/server/simple_server.rb +27 -15
  31. data/lib/thrift/server/thin_http_server.rb +21 -44
  32. data/lib/thrift/server/thread_pool_server.rb +21 -17
  33. data/lib/thrift/server/threaded_server.rb +22 -18
  34. data/lib/thrift/struct.rb +34 -18
  35. data/lib/thrift/struct_union.rb +37 -36
  36. data/lib/thrift/thrift_native.rb +2 -1
  37. data/lib/thrift/transport/base_server_transport.rb +3 -0
  38. data/lib/thrift/transport/base_transport.rb +26 -4
  39. data/lib/thrift/transport/buffered_transport.rb +5 -4
  40. data/lib/thrift/transport/framed_transport.rb +33 -7
  41. data/lib/thrift/transport/header_transport.rb +159 -33
  42. data/lib/thrift/transport/http_client_transport.rb +28 -14
  43. data/lib/thrift/transport/io_stream_transport.rb +3 -1
  44. data/lib/thrift/transport/memory_buffer_transport.rb +14 -9
  45. data/lib/thrift/transport/server_socket.rb +10 -8
  46. data/lib/thrift/transport/socket.rb +85 -48
  47. data/lib/thrift/transport/ssl_server_socket.rb +67 -6
  48. data/lib/thrift/transport/ssl_socket.rb +73 -14
  49. data/lib/thrift/transport/unix_server_socket.rb +6 -2
  50. data/lib/thrift/transport/unix_socket.rb +5 -6
  51. data/lib/thrift/types.rb +23 -22
  52. data/lib/thrift/union.rb +21 -11
  53. data/lib/thrift/uuid.rb +9 -8
  54. data/lib/thrift.rb +40 -39
  55. metadata +7 -266
  56. data/benchmark/Benchmark.thrift +0 -24
  57. data/benchmark/benchmark.rb +0 -285
  58. data/benchmark/client.rb +0 -117
  59. data/benchmark/server.rb +0 -120
  60. data/benchmark/thin_server.rb +0 -45
  61. data/lib/thrift/server/mongrel_http_server.rb +0 -60
  62. data/spec/BaseService.thrift +0 -27
  63. data/spec/ExtendedService.thrift +0 -25
  64. data/spec/Referenced.thrift +0 -44
  65. data/spec/ThriftNamespacedSpec.thrift +0 -53
  66. data/spec/ThriftSpec.thrift +0 -187
  67. data/spec/base_protocol_spec.rb +0 -224
  68. data/spec/base_transport_spec.rb +0 -387
  69. data/spec/binary_protocol_accelerated_spec.rb +0 -46
  70. data/spec/binary_protocol_spec.rb +0 -72
  71. data/spec/binary_protocol_spec_shared.rb +0 -445
  72. data/spec/bytes_spec.rb +0 -99
  73. data/spec/client_spec.rb +0 -166
  74. data/spec/compact_protocol_spec.rb +0 -196
  75. data/spec/constants_demo_spec.rb +0 -101
  76. data/spec/exception_spec.rb +0 -140
  77. data/spec/flat_spec.rb +0 -62
  78. data/spec/header_protocol_spec.rb +0 -475
  79. data/spec/header_transport_spec.rb +0 -386
  80. data/spec/http_client_spec.rb +0 -159
  81. data/spec/json_protocol_spec.rb +0 -552
  82. data/spec/namespaced_spec.rb +0 -66
  83. data/spec/nonblocking_server_spec.rb +0 -361
  84. data/spec/processor_spec.rb +0 -79
  85. data/spec/serializer_spec.rb +0 -66
  86. data/spec/server_socket_spec.rb +0 -84
  87. data/spec/server_spec.rb +0 -186
  88. data/spec/socket_spec.rb +0 -67
  89. data/spec/socket_spec_shared.rb +0 -167
  90. data/spec/spec_helper.rb +0 -69
  91. data/spec/ssl_server_socket_spec.rb +0 -45
  92. data/spec/ssl_socket_spec.rb +0 -87
  93. data/spec/struct_nested_containers_spec.rb +0 -190
  94. data/spec/struct_spec.rb +0 -397
  95. data/spec/support/header_protocol_helper.rb +0 -54
  96. data/spec/thin_http_server_spec.rb +0 -126
  97. data/spec/types_spec.rb +0 -117
  98. data/spec/union_spec.rb +0 -272
  99. data/spec/unix_socket_spec.rb +0 -115
  100. data/spec/uuid_validation_spec.rb +0 -238
  101. data/test/fuzz/Makefile.am +0 -173
  102. data/test/fuzz/README.md +0 -149
  103. data/test/fuzz/fuzz_common.rb +0 -95
  104. data/test/fuzz/fuzz_parse_binary_protocol.rb +0 -22
  105. data/test/fuzz/fuzz_parse_binary_protocol_accelerated.rb +0 -22
  106. data/test/fuzz/fuzz_parse_binary_protocol_accelerated_harness.rb +0 -22
  107. data/test/fuzz/fuzz_parse_binary_protocol_harness.rb +0 -22
  108. data/test/fuzz/fuzz_parse_compact_protocol.rb +0 -22
  109. data/test/fuzz/fuzz_parse_compact_protocol_harness.rb +0 -22
  110. data/test/fuzz/fuzz_parse_json_protocol.rb +0 -22
  111. data/test/fuzz/fuzz_parse_json_protocol_harness.rb +0 -22
  112. data/test/fuzz/fuzz_roundtrip_binary_protocol.rb +0 -22
  113. data/test/fuzz/fuzz_roundtrip_binary_protocol_accelerated.rb +0 -22
  114. data/test/fuzz/fuzz_roundtrip_binary_protocol_accelerated_harness.rb +0 -22
  115. data/test/fuzz/fuzz_roundtrip_binary_protocol_harness.rb +0 -22
  116. data/test/fuzz/fuzz_roundtrip_compact_protocol.rb +0 -22
  117. data/test/fuzz/fuzz_roundtrip_compact_protocol_harness.rb +0 -22
  118. data/test/fuzz/fuzz_roundtrip_json_protocol.rb +0 -22
  119. data/test/fuzz/fuzz_roundtrip_json_protocol_harness.rb +0 -22
  120. data/test/fuzz/fuzz_tracer.rb +0 -28
@@ -1,4 +1,5 @@
1
1
  # encoding: ascii-8bit
2
+ # frozen_string_literal: true
2
3
  #
3
4
  # Licensed to the Apache Software Foundation (ASF) under one
4
5
  # or more contributor license agreements. See the NOTICE file
@@ -18,8 +19,8 @@
18
19
  # under the License.
19
20
  #
20
21
 
21
- require 'stringio'
22
- require 'zlib'
22
+ require "stringio"
23
+ require "zlib"
23
24
 
24
25
  module Thrift
25
26
  # Client type constants for Header protocol
@@ -70,9 +71,18 @@ module Thrift
70
71
  # Header magic value (first 2 bytes of header)
71
72
  HEADER_MAGIC = 0x0FFF
72
73
 
73
- # Maximum frame size (~1GB)
74
+ # The largest frame the length field can express (~1GB). This is the
75
+ # protocol's ceiling, not a sensible default -- see DEFAULT_MAX_FRAME_SIZE.
74
76
  MAX_FRAME_SIZE = 0x3FFFFFFF
75
77
 
78
+ # The largest frame accepted by default (~15.6 MB, matches other Thrift
79
+ # bindings). Defaulting to MAX_FRAME_SIZE let a four-byte header ask for a
80
+ # gigabyte, which is the ceiling rather than a limit.
81
+ DEFAULT_MAX_FRAME_SIZE = 16_384_000
82
+
83
+ # Default decompressed-size cap for ZLIB transform (~15.6 MB, matches other Thrift bindings)
84
+ DEFAULT_MAX_DECOMPRESSED_SIZE = 16_384_000
85
+
76
86
  # Binary protocol version mask and version 1
77
87
  BINARY_VERSION_MASK = 0xffff0000
78
88
  BINARY_VERSION_1 = 0x80010000
@@ -82,6 +92,9 @@ module Thrift
82
92
  COMPACT_VERSION_MASK = 0x1f
83
93
  COMPACT_VERSION = 0x01
84
94
 
95
+ MAX_VARINT32_BYTES = 5
96
+ MAX_VARINT32_LAST_BYTE = 0x0f
97
+
85
98
  attr_reader :protocol_id, :sequence_id, :flags
86
99
 
87
100
  # Creates a new HeaderTransport wrapping the given transport.
@@ -99,7 +112,7 @@ module Thrift
99
112
  HeaderClientType::FRAMED_BINARY,
100
113
  HeaderClientType::UNFRAMED_BINARY,
101
114
  HeaderClientType::FRAMED_COMPACT,
102
- HeaderClientType::UNFRAMED_COMPACT
115
+ HeaderClientType::UNFRAMED_COMPACT,
103
116
  ]
104
117
 
105
118
  @read_buffer = StringIO.new(Bytes.empty_byte_buffer)
@@ -111,7 +124,9 @@ module Thrift
111
124
 
112
125
  @sequence_id = 0
113
126
  @flags = 0
114
- @max_frame_size = MAX_FRAME_SIZE
127
+ @max_frame_size = DEFAULT_MAX_FRAME_SIZE
128
+ @max_decompressed_size = DEFAULT_MAX_DECOMPRESSED_SIZE
129
+ @unframed_bytes_read = 0
115
130
  end
116
131
 
117
132
  def sequence_id=(sequence_id)
@@ -122,6 +137,14 @@ module Thrift
122
137
  @sequence_id = sequence_id
123
138
  end
124
139
 
140
+ def protocol_id=(protocol_id)
141
+ unless [HeaderSubprotocolID::BINARY, HeaderSubprotocolID::COMPACT].include?(protocol_id)
142
+ raise ArgumentError, "Unknown protocol ID: #{protocol_id}"
143
+ end
144
+
145
+ @protocol_id = protocol_id
146
+ end
147
+
125
148
  def open?
126
149
  @transport.open?
127
150
  end
@@ -164,6 +187,9 @@ module Thrift
164
187
  @write_transforms << transform_id unless @write_transforms.include?(transform_id)
165
188
  end
166
189
 
190
+ # The maximum frame size currently in force
191
+ attr_reader :max_frame_size
192
+
167
193
  # Sets the maximum allowed frame size
168
194
  def set_max_frame_size(size)
169
195
  if size <= 0 || size > MAX_FRAME_SIZE
@@ -172,6 +198,14 @@ module Thrift
172
198
  @max_frame_size = size
173
199
  end
174
200
 
201
+ # Sets the maximum allowed decompressed size for ZLIB transforms
202
+ def set_max_decompressed_size(size)
203
+ if size <= 0 || size > MAX_FRAME_SIZE
204
+ raise ArgumentError, "max_decompressed_size must be > 0 and <= #{MAX_FRAME_SIZE}"
205
+ end
206
+ @max_decompressed_size = size
207
+ end
208
+
175
209
  def read(sz)
176
210
  # Try reading from existing buffer
177
211
  data = @read_buffer.read(sz)
@@ -183,7 +217,7 @@ module Thrift
183
217
  # Handle unframed passthrough - read directly from underlying transport
184
218
  if @client_type == HeaderClientType::UNFRAMED_BINARY ||
185
219
  @client_type == HeaderClientType::UNFRAMED_COMPACT
186
- return data + @transport.read(bytes_left)
220
+ return data + read_unframed(bytes_left)
187
221
  end
188
222
 
189
223
  # Need to read the next frame
@@ -201,16 +235,15 @@ module Thrift
201
235
  @write_buffer = StringIO.new(Bytes.empty_byte_buffer)
202
236
 
203
237
  return if payload.empty?
204
- if payload.bytesize > @max_frame_size
205
- raise TransportException.new(TransportException::UNKNOWN, "Attempting to send frame that is too large")
206
- end
207
238
 
208
239
  case @client_type
209
240
  when HeaderClientType::HEADERS
210
241
  flush_header_format(payload)
211
242
  when HeaderClientType::FRAMED_BINARY, HeaderClientType::FRAMED_COMPACT
243
+ validate_frame_size!(payload.bytesize)
212
244
  flush_framed(payload)
213
245
  when HeaderClientType::UNFRAMED_BINARY, HeaderClientType::UNFRAMED_COMPACT
246
+ validate_frame_size!(payload.bytesize)
214
247
  @transport.write(payload)
215
248
  @transport.flush
216
249
  else
@@ -219,7 +252,7 @@ module Thrift
219
252
  end
220
253
 
221
254
  def to_s
222
- "header(#{@transport.to_s})"
255
+ "header(#{@transport})"
223
256
  end
224
257
 
225
258
  # Reads the next frame to detect protocol/client type before decoding.
@@ -229,6 +262,17 @@ module Thrift
229
262
  read_frame(0)
230
263
  end
231
264
 
265
+ def message_boundaries?
266
+ true
267
+ end
268
+
269
+ # Starts a new protocol message without forcing client-type detection.
270
+ def reset_message_size
271
+ return unless @read_buffer.nil? || @read_buffer.eof?
272
+
273
+ @unframed_bytes_read = 0
274
+ end
275
+
232
276
  private
233
277
 
234
278
  # Sets the client type after validation
@@ -241,9 +285,16 @@ module Thrift
241
285
 
242
286
  # Reads the next frame, detecting client type on first read
243
287
  def read_frame(req_sz)
288
+ @read_headers = {}
289
+ @unframed_bytes_read = 0
290
+
244
291
  # Read first 4 bytes - could be frame length or protocol magic
245
- first_word = @transport.read_all(4)
246
- frame_size = first_word.unpack('N').first
292
+ begin
293
+ first_word = @transport.read_all(4)
294
+ rescue EOFError
295
+ raise TransportException.new(TransportException::END_OF_FILE, "Unexpected EOF reading frame size")
296
+ end
297
+ frame_size = first_word.unpack1("N")
247
298
 
248
299
  # Check for unframed binary protocol
249
300
  if (frame_size & BINARY_VERSION_MASK) == BINARY_VERSION_1
@@ -266,16 +317,23 @@ module Thrift
266
317
  if frame_size > @max_frame_size
267
318
  raise TransportException.new(TransportException::UNKNOWN, "Frame size #{frame_size} exceeds maximum #{@max_frame_size}")
268
319
  end
320
+ if frame_size < 4
321
+ raise TransportException.new(TransportException::UNKNOWN, "Frame size #{frame_size} is too small")
322
+ end
269
323
 
270
324
  # Read the complete frame
271
- frame_data = @transport.read_all(frame_size)
325
+ begin
326
+ frame_data = @transport.read_all(frame_size)
327
+ rescue EOFError
328
+ raise TransportException.new(TransportException::END_OF_FILE, "Unexpected EOF reading frame")
329
+ end
272
330
  frame_buf = StringIO.new(frame_data)
273
331
 
274
332
  # Check the second word for protocol type
275
333
  second_word = frame_buf.read(4)
276
334
  frame_buf.rewind
277
335
 
278
- magic = second_word.unpack('n').first
336
+ magic = second_word.unpack1("n")
279
337
 
280
338
  if magic == HEADER_MAGIC
281
339
  if frame_size < 10
@@ -283,7 +341,7 @@ module Thrift
283
341
  end
284
342
  set_client_type(HeaderClientType::HEADERS)
285
343
  @read_buffer = parse_header_format(frame_buf)
286
- elsif (second_word.unpack('N').first & BINARY_VERSION_MASK) == BINARY_VERSION_1
344
+ elsif (second_word.unpack1("N") & BINARY_VERSION_MASK) == BINARY_VERSION_1
287
345
  set_client_type(HeaderClientType::FRAMED_BINARY)
288
346
  @protocol_id = HeaderSubprotocolID::BINARY
289
347
  @read_buffer = frame_buf
@@ -299,26 +357,45 @@ module Thrift
299
357
 
300
358
  # Handles unframed protocol - puts first_word back in buffer
301
359
  def handle_unframed(first_word, req_sz)
360
+ @unframed_bytes_read = first_word.bytesize
361
+ raise_unframed_size_limit if @unframed_bytes_read > @max_frame_size
362
+
302
363
  bytes_left = req_sz - 4
303
- if bytes_left > 0
304
- rest = @transport.read(bytes_left)
364
+ if bytes_left > 0 && @unframed_bytes_read < @max_frame_size
365
+ rest = read_unframed(bytes_left)
305
366
  @read_buffer = StringIO.new(first_word + rest)
306
367
  else
307
368
  @read_buffer = StringIO.new(first_word)
308
369
  end
309
370
  end
310
371
 
372
+ def read_unframed(size)
373
+ remaining = @max_frame_size - @unframed_bytes_read
374
+ raise_unframed_size_limit if remaining <= 0
375
+
376
+ data = @transport.read([size, remaining].min)
377
+ @unframed_bytes_read += data.bytesize
378
+ data
379
+ end
380
+
381
+ def raise_unframed_size_limit
382
+ raise TransportException.new(
383
+ TransportException::SIZE_LIMIT,
384
+ "Unframed message size exceeds maximum #{@max_frame_size}",
385
+ )
386
+ end
387
+
311
388
  # Parses a Header format frame
312
389
  def parse_header_format(buf)
313
390
  # Skip magic (already identified)
314
391
  buf.read(2)
315
392
 
316
393
  # Read flags and sequence ID
317
- @flags = buf.read(2).unpack('n').first
318
- @sequence_id = signed_int32(buf.read(4).unpack('N').first)
394
+ @flags = buf.read(2).unpack1("n")
395
+ @sequence_id = signed_int32(buf.read(4).unpack1("N"))
319
396
 
320
397
  # Read header length (in 32-bit words)
321
- header_words = buf.read(2).unpack('n').first
398
+ header_words = buf.read(2).unpack1("n")
322
399
  if header_words >= 16_384
323
400
  raise TransportException.new(TransportException::UNKNOWN, "Header size is unreasonable")
324
401
  end
@@ -343,7 +420,6 @@ module Thrift
343
420
  transforms << transform_id
344
421
  end
345
422
  # Read info headers
346
- @read_headers = {}
347
423
  while buf.pos < end_of_headers
348
424
  info_type = read_varint32(buf, end_of_headers)
349
425
  if info_type == 0
@@ -369,13 +445,39 @@ module Thrift
369
445
  payload = buf.read
370
446
  transforms.each do |transform_id|
371
447
  if transform_id == HeaderTransformID::ZLIB
372
- payload = Zlib::Inflate.inflate(payload)
448
+ payload = bounded_inflate(payload)
373
449
  end
374
450
  end
375
451
 
376
452
  StringIO.new(payload)
377
453
  end
378
454
 
455
+ # Inflates +compressed+ with a running byte-count check against @max_decompressed_size.
456
+ # Raises TransportException::SIZE_LIMIT if the decompressed output would exceed the limit.
457
+ def bounded_inflate(compressed)
458
+ inflater = Zlib::Inflate.new
459
+ buffer = Bytes.empty_byte_buffer
460
+ append_chunk = lambda do |chunk|
461
+ if buffer.bytesize + chunk.bytesize > @max_decompressed_size
462
+ raise TransportException.new(
463
+ TransportException::SIZE_LIMIT,
464
+ "Decompressed size exceeds limit of #{@max_decompressed_size}",
465
+ )
466
+ end
467
+
468
+ buffer << chunk
469
+ end
470
+ begin
471
+ inflater.inflate(compressed, &append_chunk)
472
+ inflater.finish(&append_chunk)
473
+ buffer
474
+ rescue Zlib::DataError, Zlib::BufError
475
+ raise TransportException.new(TransportException::UNKNOWN, "Invalid ZLIB payload")
476
+ ensure
477
+ inflater.close rescue nil
478
+ end
479
+ end
480
+
379
481
  # Flushes data in Header format
380
482
  def flush_header_format(payload)
381
483
  # Apply transforms
@@ -403,7 +505,6 @@ module Thrift
403
505
  write_varstring(header_buf, key)
404
506
  write_varstring(header_buf, value)
405
507
  end
406
- @write_headers = {}
407
508
  end
408
509
 
409
510
  # Pad header to 4-byte boundary
@@ -414,14 +515,16 @@ module Thrift
414
515
  # Calculate total frame size (excludes the 4-byte length field itself)
415
516
  # Frame = magic(2) + flags(2) + seqid(4) + header_len(2) + header_data + payload
416
517
  frame_size = 2 + 2 + 4 + 2 + header_data.bytesize + payload.bytesize
518
+ validate_frame_size!(frame_size)
519
+ @write_headers = {}
417
520
 
418
521
  # Write complete frame
419
522
  frame = Bytes.empty_byte_buffer
420
- frame << [frame_size].pack('N') # Length
421
- frame << [HEADER_MAGIC].pack('n') # Magic
422
- frame << [@flags].pack('n') # Flags
423
- frame << [unsigned_int32(@sequence_id)].pack('N') # Sequence ID
424
- frame << [header_data.bytesize / 4].pack('n') # Header length (in 32-bit words)
523
+ frame << [frame_size].pack("N") # Length
524
+ frame << [HEADER_MAGIC].pack("n") # Magic
525
+ frame << [@flags].pack("n") # Flags
526
+ frame << [unsigned_int32(@sequence_id)].pack("N") # Sequence ID
527
+ frame << [header_data.bytesize / 4].pack("n") # Header length (in 32-bit words)
425
528
  frame << header_data # Header data
426
529
  frame << payload # Payload
427
530
 
@@ -429,9 +532,18 @@ module Thrift
429
532
  @transport.flush
430
533
  end
431
534
 
535
+ def validate_frame_size!(frame_size)
536
+ return if frame_size <= @max_frame_size
537
+
538
+ raise TransportException.new(
539
+ TransportException::UNKNOWN,
540
+ "Frame size #{frame_size} exceeds maximum #{@max_frame_size}",
541
+ )
542
+ end
543
+
432
544
  # Flushes data in simple framed format (for legacy compatibility)
433
545
  def flush_framed(payload)
434
- frame = [payload.bytesize].pack('N') + payload
546
+ frame = [payload.bytesize].pack("N") + payload
435
547
  @transport.write(frame)
436
548
  @transport.flush
437
549
  end
@@ -446,21 +558,35 @@ module Thrift
446
558
  end
447
559
  byte = io.getbyte
448
560
  raise TransportException.new(TransportException::END_OF_FILE, "Unexpected EOF reading varint") if byte.nil?
561
+
562
+ if shift == (MAX_VARINT32_BYTES - 1) * 7
563
+ if (byte & 0x80) != 0
564
+ raise TransportException.new(TransportException::UNKNOWN, "Variable-length int over 5 bytes.")
565
+ end
566
+ if (byte & ~MAX_VARINT32_LAST_BYTE) != 0
567
+ raise TransportException.new(TransportException::UNKNOWN, "Variable-length int overflows uint32.")
568
+ end
569
+ end
570
+
449
571
  result |= (byte & 0x7f) << shift
450
- break if (byte & 0x80) == 0
572
+ return result if (byte & 0x80) == 0
451
573
  shift += 7
452
574
  end
453
- result
454
575
  end
455
576
 
456
577
  # Writes a varint32 to the given IO
457
578
  def write_varint32(io, n)
579
+ # Encode the low 32 bits: Ruby's >> is arithmetic, so a negative value
580
+ # would keep its sign bit set and never satisfy the (n & ~0x7F) == 0 exit
581
+ # condition below. Masking yields the same unsigned varint read_varint32
582
+ # produces and accepts.
583
+ n &= 0xFFFFFFFF
458
584
  loop do
459
585
  if (n & ~0x7F) == 0
460
- io.write([n].pack('C'))
586
+ io.write([n].pack("C"))
461
587
  break
462
588
  else
463
- io.write([(n & 0x7F) | 0x80].pack('C'))
589
+ io.write([(n & 0x7F) | 0x80].pack("C"))
464
590
  n >>= 7
465
591
  end
466
592
  end
@@ -1,4 +1,5 @@
1
1
  # encoding: ascii-8bit
2
+ # frozen_string_literal: true
2
3
  #
3
4
  # Licensed to the Apache Software Foundation (ASF) under one
4
5
  # or more contributor license agreements. See the NOTICE file
@@ -18,24 +19,31 @@
18
19
  # under the License.
19
20
  #
20
21
 
21
- require 'net/http'
22
- require 'net/https'
23
- require 'openssl'
24
- require 'uri'
25
- require 'stringio'
22
+ require "net/http"
23
+ require "net/https"
24
+ require "openssl"
25
+ require "uri"
26
+ require "stringio"
26
27
 
27
28
  module Thrift
28
29
  class HTTPClientTransport < BaseTransport
29
-
30
30
  def initialize(url, opts = {})
31
31
  @url = URI url
32
- @headers = {'Content-Type' => 'application/x-thrift'}
32
+ @headers = {"Content-Type" => "application/x-thrift"}
33
33
  @outbuf = Bytes.empty_byte_buffer
34
34
  @ssl_verify_mode = opts.fetch(:ssl_verify_mode, OpenSSL::SSL::VERIFY_PEER)
35
+ @ssl_ca_file = opts[:ssl_ca_file]
35
36
  end
36
37
 
37
38
  def open?; true end
38
- def read(sz); @inbuf.read sz end
39
+
40
+ def read(sz)
41
+ data = @inbuf.read sz
42
+ return data unless data.nil?
43
+
44
+ raise TransportException.new(TransportException::END_OF_FILE, "#{self.class.name} reached EOF reading response from #{self}, HTTP status code #{@response_code}")
45
+ end
46
+
39
47
  def write(buf); @outbuf << Bytes.force_binary_encoding(buf) end
40
48
 
41
49
  def add_headers(headers)
@@ -44,20 +52,26 @@ module Thrift
44
52
 
45
53
  def flush
46
54
  http = Net::HTTP.new @url.host, @url.port
47
- http.use_ssl = @url.scheme == 'https'
48
- http.verify_mode = @ssl_verify_mode if @url.scheme == 'https'
55
+ http.use_ssl = @url.scheme == "https"
56
+ if @url.scheme == "https"
57
+ http.verify_mode = @ssl_verify_mode
58
+ http.ca_file = @ssl_ca_file if @ssl_ca_file
59
+ end
49
60
  resp = http.post(@url.request_uri, @outbuf, @headers)
50
- raise TransportException.new(TransportException::UNKNOWN, "#{self.class.name} Could not connect to #{@url}, HTTP status code #{resp.code.to_i}") unless (200..299).include?(resp.code.to_i)
61
+ response_code = resp.code.to_i
62
+ raise TransportException.new(TransportException::UNKNOWN, "#{self.class.name} Could not connect to #{self}, HTTP status code #{response_code}") unless (200..299).cover?(response_code)
51
63
 
52
- data = resp.body
53
- data = Bytes.force_binary_encoding(data)
64
+ @response_code = response_code
65
+ data = Bytes.force_binary_encoding(resp.body || Bytes.empty_byte_buffer)
54
66
  @inbuf = StringIO.new data
55
67
  ensure
56
68
  @outbuf = Bytes.empty_byte_buffer
57
69
  end
58
70
 
59
71
  def to_s
60
- "@{self.url}"
72
+ path = @url.path.to_s
73
+ path = "/" if path.empty?
74
+ "#{@url.scheme}(#{@url.host}:#{@url.port}#{path})"
61
75
  end
62
76
  end
63
77
  end
@@ -1,4 +1,5 @@
1
1
  # encoding: ascii-8bit
2
+ # frozen_string_literal: true
2
3
  #
3
4
  # Licensed to the Apache Software Foundation (ASF) under one
4
5
  # or more contributor license agreements. See the NOTICE file
@@ -35,8 +36,9 @@ module Thrift
35
36
  def write(buf); @output.write(Bytes.force_binary_encoding(buf)) end
36
37
  def close; @input.close; @output.close end
37
38
  def to_io; @input end # we're assuming this is used in a IO.select for reading
39
+
38
40
  def to_s
39
- "iostream(input=#{@input.to_s},output=#{@output.to_s})"
41
+ "iostream(input=#{@input},output=#{@output})"
40
42
  end
41
43
  end
42
44
  end
@@ -1,4 +1,5 @@
1
1
  # encoding: ascii-8bit
2
+ # frozen_string_literal: true
2
3
  #
3
4
  # Licensed to the Apache Software Foundation (ASF) under one
4
5
  # or more contributor license agreements. See the NOTICE file
@@ -20,20 +21,16 @@
20
21
 
21
22
  module Thrift
22
23
  class MemoryBufferTransport < BaseTransport
23
- GARBAGE_BUFFER_SIZE = 4*(2**10) # 4kB
24
+ GARBAGE_BUFFER_SIZE = 4 * (2**10) # 4kB
24
25
 
25
- # If you pass a string to this, you should #dup that string
26
- # unless you want it to be modified by #read and #write
27
- #--
28
- # this behavior is no longer required. If you wish to change it
29
- # go ahead, just make sure the specs pass
26
+ # The transport copies the input buffer and keeps its own mutable storage.
30
27
  def initialize(buffer = nil)
31
- @buf = buffer ? Bytes.force_binary_encoding(buffer) : Bytes.empty_byte_buffer
28
+ @buf = buffer ? Bytes.force_binary_encoding(buffer.dup) : Bytes.empty_byte_buffer
32
29
  @index = 0
33
30
  end
34
31
 
35
32
  def open?
36
- return true
33
+ true
37
34
  end
38
35
 
39
36
  def open
@@ -47,7 +44,7 @@ module Thrift
47
44
  end
48
45
 
49
46
  # this method does not use the passed object directly but copies it
50
- def reset_buffer(new_buf = '')
47
+ def reset_buffer(new_buf = "")
51
48
  @buf.replace Bytes.force_binary_encoding(new_buf)
52
49
  @index = 0
53
50
  end
@@ -57,6 +54,8 @@ module Thrift
57
54
  end
58
55
 
59
56
  def read(len)
57
+ raise TransportException.new(TransportException::NEGATIVE_SIZE, "Negative size") unless len >= 0
58
+
60
59
  data = @buf.slice(@index, len)
61
60
  @index += len
62
61
  @index = @buf.size if @index > @buf.size
@@ -70,6 +69,12 @@ module Thrift
70
69
  data
71
70
  end
72
71
 
72
+ def read_all(size)
73
+ raise TransportException.new(TransportException::NEGATIVE_SIZE, "Negative size") unless size >= 0
74
+
75
+ read(size)
76
+ end
77
+
73
78
  def read_byte
74
79
  raise EOFError.new("Not enough bytes remain in buffer") if @index >= @buf.size
75
80
  val = Bytes.get_string_byte(@buf, @index)
@@ -1,4 +1,5 @@
1
1
  # encoding: ascii-8bit
2
+ # frozen_string_literal: true
2
3
  #
3
4
  # Licensed to the Apache Software Foundation (ASF) under one
4
5
  # or more contributor license agreements. See the NOTICE file
@@ -18,12 +19,12 @@
18
19
  # under the License.
19
20
  #
20
21
 
21
- require 'socket'
22
+ require "socket"
22
23
 
23
24
  module Thrift
24
25
  class ServerSocket < BaseServerTransport
25
- # call-seq: initialize(host = nil, port)
26
- def initialize(host_or_port, port = nil)
26
+ # call-seq: initialize(host = nil, port, client_timeout: DEFAULT_CLIENT_TIMEOUT)
27
+ def initialize(host_or_port, port = nil, client_timeout: DEFAULT_CLIENT_TIMEOUT)
27
28
  if port
28
29
  @host = host_or_port
29
30
  @port = port
@@ -31,10 +32,11 @@ module Thrift
31
32
  @host = nil
32
33
  @port = host_or_port
33
34
  end
35
+ @client_timeout = client_timeout
34
36
  @handle = nil
35
37
  end
36
38
 
37
- attr_reader :handle
39
+ attr_reader :handle, :client_timeout
38
40
 
39
41
  def listen
40
42
  @handle = TCPServer.new(@host, @port)
@@ -45,14 +47,15 @@ module Thrift
45
47
  sock = @handle.accept
46
48
  sock.setsockopt(::Socket::IPPROTO_TCP, ::Socket::TCP_NODELAY, 1)
47
49
  trans = Socket.new
50
+ trans.timeout = @client_timeout
48
51
  trans.handle = sock
49
52
  trans
50
53
  end
51
54
  end
52
55
 
53
56
  def close
54
- @handle.close unless @handle.nil? or @handle.closed?
55
- @handle = nil
57
+ @handle.close unless @handle.nil? or @handle.closed?
58
+ @handle = nil
56
59
  end
57
60
 
58
61
  def closed?
@@ -60,12 +63,11 @@ module Thrift
60
63
  end
61
64
 
62
65
  def to_io
63
- @handle&.to_io || raise(IOError, 'closed stream')
66
+ @handle&.to_io || raise(IOError, "closed stream")
64
67
  end
65
68
 
66
69
  def to_s
67
70
  "socket(#{@host}:#{@port})"
68
71
  end
69
-
70
72
  end
71
73
  end