ruby-utcp 1.1.5 → 1.1.6
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 +4 -4
- data/CHANGELOG.md +15 -1
- data/README.md +19 -2
- data/lib/utcp/models.rb +21 -3
- data/lib/utcp/openapi_converter.rb +86 -23
- data/lib/utcp/protocols/graphql.rb +9 -2
- data/lib/utcp/protocols/grpc.rb +16 -5
- data/lib/utcp/protocols/http.rb +37 -5
- data/lib/utcp/protocols/http_stream_support.rb +13 -4
- data/lib/utcp/protocols/mcp.rb +26 -17
- data/lib/utcp/protocols/sse.rb +58 -19
- data/lib/utcp/protocols/streamable_http.rb +42 -16
- data/lib/utcp/protocols/tcp.rb +21 -9
- data/lib/utcp/protocols/udp.rb +2 -0
- data/lib/utcp/protocols/webrtc.rb +107 -24
- data/lib/utcp/protocols/webrtc_native_cleanup.rb +39 -0
- data/lib/utcp/protocols/websocket.rb +19 -5
- data/lib/utcp/response_limits.rb +114 -0
- data/lib/utcp/version.rb +1 -1
- metadata +3 -1
|
@@ -2,27 +2,45 @@
|
|
|
2
2
|
|
|
3
3
|
module UTCP
|
|
4
4
|
class WebRTCPeer
|
|
5
|
-
def initialize(template)
|
|
6
|
-
gem "webrtc-ruby", ">= 1.0.0"
|
|
7
|
-
require "webrtc"
|
|
8
|
-
WebRTC.init
|
|
5
|
+
def initialize(template, connection: nil)
|
|
9
6
|
@template = template
|
|
10
7
|
@mutex = Mutex.new
|
|
11
8
|
@condition = ConditionVariable.new
|
|
12
|
-
@
|
|
9
|
+
@pending = {}
|
|
10
|
+
@io_mutex = Mutex.new
|
|
11
|
+
@closed = false
|
|
13
12
|
@candidates = []
|
|
14
|
-
configuration = { disable_auto_negotiation: true }
|
|
13
|
+
configuration = { disable_auto_negotiation: true, max_message_size: template.max_response_bytes }
|
|
15
14
|
configuration[:ice_servers] = template.ice_servers unless template.ice_servers.empty?
|
|
16
|
-
|
|
15
|
+
unless connection
|
|
16
|
+
gem "webrtc-ruby", ">= 1.0.0"
|
|
17
|
+
require "webrtc"
|
|
18
|
+
require_relative "webrtc_native_cleanup"
|
|
19
|
+
WebRTC.init
|
|
20
|
+
@native_cleanup = WebRTCNativeCleanup.new
|
|
21
|
+
connection = WebRTC::RTCPeerConnection.new(configuration)
|
|
22
|
+
end
|
|
23
|
+
@connection = connection
|
|
17
24
|
install_candidate_handler
|
|
18
25
|
@channel = @connection.create_data_channel(template.data_channel_name)
|
|
19
26
|
install_channel_handlers
|
|
20
27
|
rescue LoadError => error
|
|
28
|
+
close
|
|
21
29
|
raise MissingDependencyError,
|
|
22
30
|
"WebRTC requires the optional 'webrtc-ruby' gem and libdatachannel: #{error.message}"
|
|
31
|
+
rescue StandardError
|
|
32
|
+
close
|
|
33
|
+
raise
|
|
23
34
|
end
|
|
24
35
|
|
|
25
36
|
def connect
|
|
37
|
+
@io_mutex.synchronize do
|
|
38
|
+
@mutex.synchronize { assert_open! }
|
|
39
|
+
connect_peer
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def connect_peer
|
|
26
44
|
offer = @connection.create_offer.await
|
|
27
45
|
# webrtc-ruby creates and installs the local offer in one native operation.
|
|
28
46
|
wait_for_ice_gathering
|
|
@@ -34,38 +52,82 @@ module UTCP
|
|
|
34
52
|
rescue StandardError
|
|
35
53
|
nil
|
|
36
54
|
end
|
|
37
|
-
@candidates.each { |candidate| post_candidate(candidate) }
|
|
55
|
+
@mutex.synchronize { @candidates.dup }.each { |candidate| post_candidate(candidate) }
|
|
38
56
|
wait_for_channel
|
|
39
57
|
response
|
|
40
58
|
end
|
|
41
59
|
|
|
42
60
|
def request(payload, timeout: @template.timeout)
|
|
43
61
|
identifier = payload.fetch("id")
|
|
44
|
-
@channel.send_text(JSON.generate(payload))
|
|
45
62
|
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout
|
|
63
|
+
state = { deadline: deadline }
|
|
46
64
|
@mutex.synchronize do
|
|
47
|
-
|
|
65
|
+
assert_open!
|
|
66
|
+
raise ValidationError, "Duplicate WebRTC request id" if @pending.key?(identifier)
|
|
67
|
+
if @pending.length >= @template.max_pending_requests
|
|
68
|
+
raise ToolCallError, "WebRTC exceeds max_pending_requests"
|
|
69
|
+
end
|
|
70
|
+
@pending[identifier] = state
|
|
71
|
+
end
|
|
72
|
+
@io_mutex.synchronize do
|
|
73
|
+
@mutex.synchronize { assert_open! }
|
|
74
|
+
@channel.send_text(JSON.generate(payload))
|
|
75
|
+
end
|
|
76
|
+
@mutex.synchronize do
|
|
77
|
+
loop do
|
|
78
|
+
assert_open!
|
|
79
|
+
return state[:response] if state.key?(:response)
|
|
48
80
|
remaining = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
49
81
|
raise TimeoutError, "WebRTC response timed out" unless remaining.positive?
|
|
50
82
|
@condition.wait(@mutex, remaining)
|
|
51
83
|
end
|
|
52
|
-
@responses.delete(identifier)
|
|
53
84
|
end
|
|
85
|
+
ensure
|
|
86
|
+
@mutex.synchronize { @pending.delete(identifier) if @pending[identifier].equal?(state) }
|
|
54
87
|
end
|
|
55
88
|
|
|
56
89
|
def close
|
|
57
|
-
@
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
90
|
+
@mutex.synchronize do
|
|
91
|
+
return nil if @closed
|
|
92
|
+
@closed = true
|
|
93
|
+
@pending.clear
|
|
94
|
+
@candidates.clear
|
|
95
|
+
@condition.broadcast
|
|
96
|
+
end
|
|
97
|
+
@io_mutex.synchronize do
|
|
98
|
+
if @native_cleanup
|
|
99
|
+
@native_cleanup.close(@channel, @connection)
|
|
100
|
+
else
|
|
101
|
+
begin
|
|
102
|
+
@channel.close if @channel
|
|
103
|
+
@channel.destroy if @channel&.respond_to?(:destroy)
|
|
104
|
+
ensure
|
|
105
|
+
@connection.close if @connection
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
61
109
|
nil
|
|
62
110
|
end
|
|
63
111
|
|
|
64
112
|
private
|
|
65
113
|
|
|
114
|
+
private :connect_peer
|
|
115
|
+
|
|
116
|
+
def assert_open!
|
|
117
|
+
raise ToolCallError, "WebRTC peer closed" if @closed
|
|
118
|
+
raise ToolCallError, @failure if @failure
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def fail_peer(message)
|
|
122
|
+
@mutex.synchronize do
|
|
123
|
+
@failure ||= message unless @closed
|
|
124
|
+
@condition.broadcast
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
66
128
|
def install_candidate_handler
|
|
67
129
|
@connection.on_ice_candidate do |candidate|
|
|
68
|
-
@mutex.synchronize { @candidates << candidate } if candidate
|
|
130
|
+
@mutex.synchronize { @candidates << candidate unless @closed } if candidate
|
|
69
131
|
end
|
|
70
132
|
end
|
|
71
133
|
|
|
@@ -73,15 +135,24 @@ module UTCP
|
|
|
73
135
|
@channel_open = false
|
|
74
136
|
@channel.on_open do
|
|
75
137
|
@mutex.synchronize do
|
|
76
|
-
@channel_open = true
|
|
138
|
+
@channel_open = true unless @closed
|
|
77
139
|
@condition.broadcast
|
|
78
140
|
end
|
|
79
141
|
end
|
|
142
|
+
@channel.on_close { fail_peer("WebRTC channel closed") } if @channel.respond_to?(:on_close)
|
|
80
143
|
@channel.on_message do |message|
|
|
144
|
+
if message.data.bytesize > @template.max_response_bytes
|
|
145
|
+
fail_peer("WebRTC response exceeds max_response_bytes")
|
|
146
|
+
next
|
|
147
|
+
end
|
|
81
148
|
envelope = JSON.parse(message.data)
|
|
149
|
+
next unless envelope.is_a?(Hash)
|
|
82
150
|
identifier = envelope["id"]
|
|
83
151
|
@mutex.synchronize do
|
|
84
|
-
|
|
152
|
+
state = @pending[identifier]
|
|
153
|
+
next unless state && !@closed && !state.key?(:response)
|
|
154
|
+
next if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= state[:deadline]
|
|
155
|
+
state[:response] = envelope.key?("result") ? envelope["result"] : envelope
|
|
85
156
|
@condition.broadcast
|
|
86
157
|
end
|
|
87
158
|
rescue JSON::ParserError
|
|
@@ -93,9 +164,9 @@ module UTCP
|
|
|
93
164
|
return unless @connection.respond_to?(:on_ice_gathering_state_change)
|
|
94
165
|
|
|
95
166
|
complete = @connection.ice_gathering_state == :complete
|
|
96
|
-
@connection.on_ice_gathering_state_change do
|
|
167
|
+
@connection.on_ice_gathering_state_change do |state|
|
|
97
168
|
@mutex.synchronize do
|
|
98
|
-
complete =
|
|
169
|
+
complete = state == :complete
|
|
99
170
|
@condition.broadcast if complete
|
|
100
171
|
end
|
|
101
172
|
end
|
|
@@ -112,7 +183,9 @@ module UTCP
|
|
|
112
183
|
def wait_for_flag(timeout)
|
|
113
184
|
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout
|
|
114
185
|
@mutex.synchronize do
|
|
115
|
-
|
|
186
|
+
loop do
|
|
187
|
+
assert_open!
|
|
188
|
+
return if yield
|
|
116
189
|
remaining = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
117
190
|
raise TimeoutError, "WebRTC connection timed out" unless remaining.positive?
|
|
118
191
|
@condition.wait(@mutex, remaining)
|
|
@@ -138,7 +211,14 @@ module UTCP
|
|
|
138
211
|
http.verify_mode = OpenSSL::SSL::VERIFY_PEER if http.use_ssl?
|
|
139
212
|
http.open_timeout = [@template.timeout, 10].min
|
|
140
213
|
http.read_timeout = @template.timeout
|
|
141
|
-
|
|
214
|
+
http.write_timeout = @template.timeout if http.respond_to?(:write_timeout=)
|
|
215
|
+
response = Timeout.timeout(@template.timeout, TimeoutError, "WebRTC signaling timed out") do
|
|
216
|
+
http.start do |connection|
|
|
217
|
+
connection.request(request) do |incoming|
|
|
218
|
+
incoming.body = LimitedHTTPResponse.new(incoming, @template.max_response_bytes).body
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
end
|
|
142
222
|
unless response.code.to_i.between?(200, 299)
|
|
143
223
|
raise ToolCallError.new("WebRTC signaling failed with status #{response.code}",
|
|
144
224
|
status: response.code.to_i, response_body: response.body)
|
|
@@ -159,6 +239,7 @@ module UTCP
|
|
|
159
239
|
def register_manual(client, template)
|
|
160
240
|
assert_webrtc_template!(template)
|
|
161
241
|
response = peer_for(client, template).connect
|
|
242
|
+
ResponseByteBudget.new(template.max_response_bytes, "WebRTC discovery").consume_value(response)
|
|
162
243
|
payload = if response.is_a?(Hash) && response.key?("tools") && !response.key?("utcp_version")
|
|
163
244
|
{
|
|
164
245
|
"utcp_version" => VERSION,
|
|
@@ -187,7 +268,7 @@ module UTCP
|
|
|
187
268
|
def call_tool(client, tool_name, tool_args, template)
|
|
188
269
|
assert_webrtc_template!(template)
|
|
189
270
|
identifier = SecureRandom.uuid
|
|
190
|
-
peer_for(client, template).request(
|
|
271
|
+
response = peer_for(client, template).request(
|
|
191
272
|
{
|
|
192
273
|
"id" => identifier,
|
|
193
274
|
"tool" => tool_name.to_s.split(".").last,
|
|
@@ -195,6 +276,7 @@ module UTCP
|
|
|
195
276
|
},
|
|
196
277
|
timeout: template.timeout
|
|
197
278
|
)
|
|
279
|
+
ResponseByteBudget.new(template.max_response_bytes, "WebRTC").consume_value(response)
|
|
198
280
|
rescue Error
|
|
199
281
|
raise
|
|
200
282
|
rescue StandardError => error
|
|
@@ -214,7 +296,8 @@ module UTCP
|
|
|
214
296
|
end
|
|
215
297
|
|
|
216
298
|
def peer_key(client, template)
|
|
217
|
-
[client, template.name, template.signaling_server, template.peer_id, template.data_channel_name
|
|
299
|
+
[client, template.name, template.signaling_server, template.peer_id, template.data_channel_name,
|
|
300
|
+
template.max_response_bytes, template.max_pending_requests]
|
|
218
301
|
end
|
|
219
302
|
end
|
|
220
303
|
WebrtcCommunicationProtocol = WebRTCProtocol
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module UTCP
|
|
4
|
+
# Stock webrtc-ruby 1.0 binds these waiting destructors without releasing the
|
|
5
|
+
# GVL. Keep the compatibility binding local to UTCP-owned objects: callbacks
|
|
6
|
+
# must be able to finish while native destruction waits for them.
|
|
7
|
+
class WebRTCNativeCleanup
|
|
8
|
+
MUTEX = Mutex.new
|
|
9
|
+
|
|
10
|
+
def self.bindings
|
|
11
|
+
MUTEX.synchronize do
|
|
12
|
+
@bindings ||= Module.new do
|
|
13
|
+
extend ::FFI::Library
|
|
14
|
+
ffi_lib WebRTC::FFI::LIB_PATH
|
|
15
|
+
attach_function :destroy_channel, :webrtc_data_channel_destroy, [:pointer], :void, blocking: true
|
|
16
|
+
attach_function :destroy_peer, :webrtc_peer_connection_destroy, [:pointer], :void, blocking: true
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def initialize
|
|
22
|
+
@bindings = self.class.bindings
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def close(channel, connection)
|
|
26
|
+
# WebRTCPeer serializes this transfer with all native I/O and elects one
|
|
27
|
+
# closer. Detach handles before callbacks can observe or reuse them.
|
|
28
|
+
channel_pointer = channel.instance_variable_get(:@ptr) if channel
|
|
29
|
+
peer_pointer = connection.ptr if connection
|
|
30
|
+
channel.instance_variable_set(:@ptr, nil) if channel
|
|
31
|
+
connection.instance_variable_set(:@ptr, nil) if connection
|
|
32
|
+
begin
|
|
33
|
+
@bindings.destroy_channel(channel_pointer) if channel_pointer && !channel_pointer.null?
|
|
34
|
+
ensure
|
|
35
|
+
@bindings.destroy_peer(peer_pointer) if peer_pointer && !peer_pointer.null?
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -28,10 +28,12 @@ module UTCP
|
|
|
28
28
|
GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
|
|
29
29
|
MAX_HEADER_SIZE = 65_536
|
|
30
30
|
MAX_MESSAGE_SIZE = 16 * 1024 * 1024
|
|
31
|
+
attr_accessor :max_response_bytes
|
|
31
32
|
|
|
32
33
|
def initialize(url, headers = {}, protocol = nil, timeout = 30)
|
|
33
34
|
@uri = WebSocketURLSecurity.validate!(url)
|
|
34
35
|
@timeout = Float(timeout)
|
|
36
|
+
@max_response_bytes = MAX_MESSAGE_SIZE
|
|
35
37
|
@read_buffer = +"".b
|
|
36
38
|
@closed = false
|
|
37
39
|
open_socket
|
|
@@ -57,7 +59,7 @@ module UTCP
|
|
|
57
59
|
message = +"".b
|
|
58
60
|
message_opcode = nil
|
|
59
61
|
loop do
|
|
60
|
-
fin, opcode, payload = read_frame
|
|
62
|
+
fin, opcode, payload = read_frame(@max_response_bytes - message.bytesize)
|
|
61
63
|
case opcode
|
|
62
64
|
when 0x0
|
|
63
65
|
raise ToolCallError, "unexpected WebSocket continuation frame" unless message_opcode
|
|
@@ -78,9 +80,12 @@ module UTCP
|
|
|
78
80
|
else
|
|
79
81
|
raise ToolCallError, "unsupported WebSocket opcode #{opcode}"
|
|
80
82
|
end
|
|
81
|
-
raise ToolCallError, "WebSocket message exceeds
|
|
83
|
+
raise ToolCallError, "WebSocket message exceeds max_response_bytes" if message.bytesize > @max_response_bytes
|
|
82
84
|
return [message_opcode, message] if fin
|
|
83
85
|
end
|
|
86
|
+
rescue Error
|
|
87
|
+
close
|
|
88
|
+
raise
|
|
84
89
|
end
|
|
85
90
|
|
|
86
91
|
def close
|
|
@@ -198,7 +203,7 @@ module UTCP
|
|
|
198
203
|
@socket.write(header + mask + masked)
|
|
199
204
|
end
|
|
200
205
|
|
|
201
|
-
def read_frame
|
|
206
|
+
def read_frame(remaining = @max_response_bytes)
|
|
202
207
|
head = read_exact(2)
|
|
203
208
|
first, second = head.unpack("CC")
|
|
204
209
|
fin = (first & 0x80) != 0
|
|
@@ -207,7 +212,8 @@ module UTCP
|
|
|
207
212
|
length = second & 0x7F
|
|
208
213
|
length = read_exact(2).unpack1("n") if length == 126
|
|
209
214
|
length = read_exact(8).unpack1("Q>") if length == 127
|
|
210
|
-
|
|
215
|
+
maximum = opcode >= 8 ? 125 : remaining
|
|
216
|
+
raise ToolCallError, "WebSocket frame exceeds max_response_bytes" if length > maximum
|
|
211
217
|
|
|
212
218
|
mask = masked ? read_exact(4) : nil
|
|
213
219
|
payload = read_exact(length)
|
|
@@ -220,7 +226,7 @@ module UTCP
|
|
|
220
226
|
def read_exact(length)
|
|
221
227
|
while @read_buffer.bytesize < length
|
|
222
228
|
wait_readable
|
|
223
|
-
@read_buffer << @socket.readpartial([4096, length - @read_buffer.bytesize].
|
|
229
|
+
@read_buffer << @socket.readpartial([4096, length - @read_buffer.bytesize].min)
|
|
224
230
|
end
|
|
225
231
|
@read_buffer.slice!(0, length)
|
|
226
232
|
rescue EOFError
|
|
@@ -255,8 +261,10 @@ module UTCP
|
|
|
255
261
|
assert_websocket_template!(template)
|
|
256
262
|
entry, transient = connection_for(client, template, {})
|
|
257
263
|
payload = entry.mutex.synchronize do
|
|
264
|
+
configure_response_limit(entry.connection, template)
|
|
258
265
|
entry.connection.send_text(JSON.generate("type" => "utcp"))
|
|
259
266
|
_opcode, bytes = entry.connection.read_message
|
|
267
|
+
ResponseByteBudget.new(template.max_response_bytes, "WebSocket").consume(bytes)
|
|
260
268
|
bytes
|
|
261
269
|
end
|
|
262
270
|
success(template, manual_from_payload(template, payload, source: "WebSocket discovery response"))
|
|
@@ -283,11 +291,13 @@ module UTCP
|
|
|
283
291
|
args = Utils.stringify_keys(tool_args || {})
|
|
284
292
|
entry, transient, message_args = connection_for(client, template, args, include_arguments: true)
|
|
285
293
|
result = entry.mutex.synchronize do
|
|
294
|
+
configure_response_limit(entry.connection, template)
|
|
286
295
|
message = format_message(template, message_args)
|
|
287
296
|
entry.connection.send_text(message)
|
|
288
297
|
frame = entry.connection.read_message
|
|
289
298
|
raise ToolCallError, "WebSocket closed without a response" unless frame
|
|
290
299
|
|
|
300
|
+
ResponseByteBudget.new(template.max_response_bytes, "WebSocket").consume(frame[1])
|
|
291
301
|
decode_message(frame[1], template.response_format, frame[0])
|
|
292
302
|
end
|
|
293
303
|
result
|
|
@@ -301,6 +311,10 @@ module UTCP
|
|
|
301
311
|
|
|
302
312
|
private
|
|
303
313
|
|
|
314
|
+
def configure_response_limit(connection, template)
|
|
315
|
+
connection.max_response_bytes = template.max_response_bytes if connection.respond_to?(:max_response_bytes=)
|
|
316
|
+
end
|
|
317
|
+
|
|
304
318
|
def assert_websocket_template!(template)
|
|
305
319
|
return if template.is_a?(WebSocketCallTemplate)
|
|
306
320
|
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "timeout"
|
|
4
|
+
|
|
5
|
+
module UTCP
|
|
6
|
+
# Ruby extension for network transports; CLI, file and text are excluded.
|
|
7
|
+
module ResponseLimits
|
|
8
|
+
DEFAULT_MAX_RESPONSE_BYTES = 100 * 1024 * 1024
|
|
9
|
+
DEFAULT_MAX_EVENT_BYTES = 1024 * 1024
|
|
10
|
+
DEFAULT_MAX_RESPONSE_ITEMS = 10_000
|
|
11
|
+
|
|
12
|
+
attr_accessor :max_response_bytes
|
|
13
|
+
|
|
14
|
+
def initialize(max_response_bytes: DEFAULT_MAX_RESPONSE_BYTES, **options)
|
|
15
|
+
super(**options)
|
|
16
|
+
@max_response_bytes = positive_limit(max_response_bytes, "max_response_bytes")
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def to_h
|
|
20
|
+
super.merge("max_response_bytes" => max_response_bytes)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def positive_limit(value, name)
|
|
26
|
+
result = Integer(value)
|
|
27
|
+
raise ValidationError.new("must be greater than zero", path: name) unless result.positive?
|
|
28
|
+
result
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
module HTTPResponseLimits
|
|
33
|
+
include ResponseLimits
|
|
34
|
+
attr_accessor :max_event_bytes, :max_response_items, :total_timeout
|
|
35
|
+
|
|
36
|
+
def initialize(max_event_bytes: DEFAULT_MAX_EVENT_BYTES,
|
|
37
|
+
max_response_items: DEFAULT_MAX_RESPONSE_ITEMS, total_timeout: nil, **options)
|
|
38
|
+
super(**options)
|
|
39
|
+
@max_event_bytes = positive_limit(max_event_bytes, "max_event_bytes")
|
|
40
|
+
@max_response_items = positive_limit(max_response_items, "max_response_items")
|
|
41
|
+
@total_timeout = total_timeout.nil? ? nil : Float(total_timeout)
|
|
42
|
+
if @total_timeout && (!@total_timeout.finite? || !@total_timeout.positive?)
|
|
43
|
+
raise ValidationError.new("must be finite and greater than zero", path: "total_timeout")
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def to_h
|
|
48
|
+
super.merge("max_event_bytes" => max_event_bytes,
|
|
49
|
+
"max_response_items" => max_response_items).tap do |value|
|
|
50
|
+
value["total_timeout"] = total_timeout if total_timeout
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
class ResponseByteBudget
|
|
57
|
+
attr_reader :remaining
|
|
58
|
+
|
|
59
|
+
def initialize(maximum, context = "Transport")
|
|
60
|
+
@remaining = maximum
|
|
61
|
+
@context = context
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def consume(bytes)
|
|
65
|
+
@remaining -= bytes.to_s.bytesize
|
|
66
|
+
raise ToolCallError, "#{@context} response exceeds max_response_bytes" if @remaining.negative?
|
|
67
|
+
bytes
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def consume_value(value)
|
|
71
|
+
consume(value.is_a?(String) ? value : JSON.generate(value))
|
|
72
|
+
value
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Count decoded bytes before a parser or an accumulating caller receives them.
|
|
77
|
+
class LimitedHTTPResponse
|
|
78
|
+
def initialize(response, maximum)
|
|
79
|
+
@response = response
|
|
80
|
+
@maximum = maximum
|
|
81
|
+
@bytes = 0
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def code
|
|
85
|
+
@response.code
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def [](name)
|
|
89
|
+
@response[name]
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def read_body
|
|
93
|
+
consume = lambda do |chunk|
|
|
94
|
+
@bytes += chunk.to_s.bytesize
|
|
95
|
+
raise ToolCallError, "HTTP response exceeds max_response_bytes (#{@maximum})" if @bytes > @maximum
|
|
96
|
+
yield chunk
|
|
97
|
+
end
|
|
98
|
+
if @response.respond_to?(:read_body)
|
|
99
|
+
@response.read_body { |chunk| consume.call(chunk) }
|
|
100
|
+
else
|
|
101
|
+
consume.call(@response.body) unless @response.body.nil?
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def body
|
|
106
|
+
result = nil
|
|
107
|
+
read_body do |chunk|
|
|
108
|
+
result ||= +"".b
|
|
109
|
+
result << chunk.to_s.b
|
|
110
|
+
end
|
|
111
|
+
result
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
data/lib/utcp/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ruby-utcp
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.1.
|
|
4
|
+
version: 1.1.6
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- ruby-utcp contributors
|
|
@@ -183,9 +183,11 @@ files:
|
|
|
183
183
|
- lib/utcp/protocols/text.rb
|
|
184
184
|
- lib/utcp/protocols/udp.rb
|
|
185
185
|
- lib/utcp/protocols/webrtc.rb
|
|
186
|
+
- lib/utcp/protocols/webrtc_native_cleanup.rb
|
|
186
187
|
- lib/utcp/protocols/websocket.rb
|
|
187
188
|
- lib/utcp/registry.rb
|
|
188
189
|
- lib/utcp/repository.rb
|
|
190
|
+
- lib/utcp/response_limits.rb
|
|
189
191
|
- lib/utcp/serializer.rb
|
|
190
192
|
- lib/utcp/utils.rb
|
|
191
193
|
- lib/utcp/variables.rb
|