]>
Commit | Line | Data |
---|---|---|
1038d965 MW |
1 | ## |
2 | # The Vici module implements a native ruby client side library for the | |
3 | # strongSwan VICI protocol. The Connection class provides a high-level | |
4 | # interface to issue requests or listen for events. | |
5 | # | |
3b394445 TB |
6 | # Copyright (C) 2019 Tobias Brunner |
7 | # HSR Hochschule fuer Technik Rapperswil | |
8 | # | |
1038d965 MW |
9 | # Copyright (C) 2014 Martin Willi |
10 | # Copyright (C) 2014 revosec AG | |
11 | # | |
12 | # Permission is hereby granted, free of charge, to any person obtaining a copy | |
13 | # of this software and associated documentation files (the "Software"), to deal | |
14 | # in the Software without restriction, including without limitation the rights | |
15 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
16 | # copies of the Software, and to permit persons to whom the Software is | |
17 | # furnished to do so, subject to the following conditions: | |
18 | # | |
19 | # The above copyright notice and this permission notice shall be included in | |
20 | # all copies or substantial portions of the Software. | |
21 | # | |
22 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
23 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
24 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
25 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
26 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
27 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | |
28 | # THE SOFTWARE. | |
29 | ||
30 | module Vici | |
1038d965 MW |
31 | ## |
32 | # Vici specific exception all others inherit from | |
33 | class Error < StandardError | |
34 | end | |
35 | ||
36 | ## | |
37 | # Error while parsing a vici message from the daemon | |
38 | class ParseError < Error | |
39 | end | |
40 | ||
41 | ## | |
42 | # Error while encoding a vici message from ruby data structures | |
43 | class EncodeError < Error | |
44 | end | |
45 | ||
46 | ## | |
47 | # Error while exchanging messages over the vici Transport layer | |
48 | class TransportError < Error | |
49 | end | |
50 | ||
51 | ## | |
52 | # Generic vici command execution error | |
53 | class CommandError < Error | |
54 | end | |
55 | ||
56 | ## | |
57 | # Error if an issued vici command is unknown by the daemon | |
58 | class CommandUnknownError < CommandError | |
59 | end | |
60 | ||
61 | ## | |
62 | # Error if a command failed to execute in the daemon | |
63 | class CommandExecError < CommandError | |
64 | end | |
65 | ||
66 | ## | |
67 | # Generic vici event handling error | |
68 | class EventError < Error | |
69 | end | |
70 | ||
71 | ## | |
72 | # Tried to register to / unregister from an unknown vici event | |
73 | class EventUnknownError < EventError | |
74 | end | |
75 | ||
76 | ## | |
77 | # Exception to raise from an event listening closure to stop listening | |
78 | class StopEventListening < Exception | |
79 | end | |
80 | ||
1038d965 MW |
81 | ## |
82 | # The Message class provides the low level encoding and decoding of vici | |
83 | # protocol messages. Directly using this class is usually not required. | |
84 | class Message | |
1038d965 MW |
85 | SECTION_START = 1 |
86 | SECTION_END = 2 | |
87 | KEY_VALUE = 3 | |
88 | LIST_START = 4 | |
89 | LIST_ITEM = 5 | |
90 | LIST_END = 6 | |
91 | ||
92 | def initialize(data = "") | |
cc2ef8f8 TB |
93 | if data.nil? |
94 | @root = {} | |
1038d965 MW |
95 | elsif data.is_a?(Hash) |
96 | @root = data | |
97 | else | |
98 | @encoded = data | |
99 | end | |
100 | end | |
101 | ||
102 | ## | |
103 | # Get the raw byte encoding of an on-the-wire message | |
104 | def encoding | |
cc2ef8f8 | 105 | @encoded = encode(@root) if @encoded.nil? |
1038d965 MW |
106 | @encoded |
107 | end | |
108 | ||
109 | ## | |
110 | # Get the root element of the parsed ruby data structures | |
111 | def root | |
cc2ef8f8 | 112 | @root = parse(@encoded) if @root.nil? |
1038d965 MW |
113 | @root |
114 | end | |
115 | ||
116 | private | |
117 | ||
118 | def encode_name(name) | |
119 | [name.length].pack("c") << name | |
120 | end | |
121 | ||
122 | def encode_value(value) | |
cc2ef8f8 | 123 | value = value.to_s if value.class != String |
1038d965 MW |
124 | [value.length].pack("n") << value |
125 | end | |
126 | ||
127 | def encode_kv(encoding, key, value) | |
128 | encoding << KEY_VALUE << encode_name(key) << encode_value(value) | |
129 | end | |
130 | ||
131 | def encode_section(encoding, key, value) | |
132 | encoding << SECTION_START << encode_name(key) | |
133 | encoding << encode(value) << SECTION_END | |
134 | end | |
135 | ||
136 | def encode_list(encoding, key, value) | |
137 | encoding << LIST_START << encode_name(key) | |
138 | value.each do |item| | |
139 | encoding << LIST_ITEM << encode_value(item) | |
140 | end | |
141 | encoding << LIST_END | |
142 | end | |
143 | ||
144 | def encode(node) | |
145 | encoding = "" | |
146 | node.each do |key, value| | |
cc2ef8f8 TB |
147 | encoding = if value.is_a?(Hash) |
148 | encode_section(encoding, key, value) | |
149 | elsif value.is_a?(Array) | |
150 | encode_list(encoding, key, value) | |
151 | else | |
152 | encode_kv(encoding, key, value) | |
153 | end | |
1038d965 MW |
154 | end |
155 | encoding | |
156 | end | |
157 | ||
158 | def parse_name(encoding) | |
159 | len = encoding.unpack("c")[0] | |
160 | name = encoding[1, len] | |
cc2ef8f8 | 161 | [encoding[(1 + len)..-1], name] |
1038d965 MW |
162 | end |
163 | ||
164 | def parse_value(encoding) | |
165 | len = encoding.unpack("n")[0] | |
166 | value = encoding[2, len] | |
cc2ef8f8 | 167 | [encoding[(2 + len)..-1], value] |
1038d965 MW |
168 | end |
169 | ||
170 | def parse(encoding) | |
cc2ef8f8 | 171 | stack = [{}] |
1038d965 | 172 | list = nil |
cc2ef8f8 | 173 | until encoding.empty? |
1038d965 MW |
174 | type = encoding.unpack("c")[0] |
175 | encoding = encoding[1..-1] | |
176 | case type | |
cc2ef8f8 TB |
177 | when SECTION_START |
178 | encoding, name = parse_name(encoding) | |
179 | stack.push(stack[-1][name] = {}) | |
180 | when SECTION_END | |
181 | raise ParseError, "unexpected section end" if stack.length == 1 | |
182 | stack.pop | |
183 | when KEY_VALUE | |
184 | encoding, name = parse_name(encoding) | |
185 | encoding, value = parse_value(encoding) | |
186 | stack[-1][name] = value | |
187 | when LIST_START | |
188 | encoding, name = parse_name(encoding) | |
189 | stack[-1][name] = [] | |
190 | list = name | |
191 | when LIST_ITEM | |
192 | raise ParseError, "unexpected list item" if list.nil? | |
193 | encoding, value = parse_value(encoding) | |
194 | stack[-1][list].push(value) | |
195 | when LIST_END | |
196 | raise ParseError, "unexpected list end" if list.nil? | |
197 | list = nil | |
198 | else | |
199 | raise ParseError, "invalid type: #{type}" | |
1038d965 MW |
200 | end |
201 | end | |
cc2ef8f8 | 202 | raise ParseError, "unexpected message end" if stack.length > 1 |
1038d965 MW |
203 | stack[0] |
204 | end | |
205 | end | |
206 | ||
1038d965 MW |
207 | ## |
208 | # The Transport class implements to low level segmentation of packets | |
209 | # to the underlying transport stream. Directly using this class is usually | |
210 | # not required. | |
211 | class Transport | |
1038d965 MW |
212 | CMD_REQUEST = 0 |
213 | CMD_RESPONSE = 1 | |
214 | CMD_UNKNOWN = 2 | |
215 | EVENT_REGISTER = 3 | |
216 | EVENT_UNREGISTER = 4 | |
217 | EVENT_CONFIRM = 5 | |
218 | EVENT_UNKNOWN = 6 | |
219 | EVENT = 7 | |
220 | ||
221 | ## | |
222 | # Create a transport layer using a provided socket for communication. | |
223 | def initialize(socket) | |
224 | @socket = socket | |
cc2ef8f8 | 225 | @events = {} |
1038d965 MW |
226 | end |
227 | ||
b164cc8e MW |
228 | ## |
229 | # Receive data from socket, until len bytes read | |
230 | def recv_all(len) | |
231 | encoding = "" | |
cc2ef8f8 | 232 | while encoding.length < len |
78ed3300 | 233 | data = @socket.recv(len - encoding.length) |
cc2ef8f8 | 234 | raise TransportError, "connection closed" if data.empty? |
78ed3300 | 235 | encoding << data |
b164cc8e MW |
236 | end |
237 | encoding | |
238 | end | |
239 | ||
240 | ## | |
241 | # Send data to socket, until all bytes sent | |
242 | def send_all(encoding) | |
243 | len = 0 | |
cc2ef8f8 | 244 | len += @socket.send(encoding[len..-1], 0) while len < encoding.length |
b164cc8e MW |
245 | end |
246 | ||
1038d965 MW |
247 | ## |
248 | # Write a packet prefixed by its length over the transport socket. Type | |
249 | # specifies the message, the optional label and message get appended. | |
250 | def write(type, label, message) | |
251 | encoding = "" | |
cc2ef8f8 TB |
252 | encoding << label.length << label if label |
253 | encoding << message.encoding if message | |
b164cc8e | 254 | send_all([encoding.length + 1, type].pack("Nc") + encoding) |
1038d965 MW |
255 | end |
256 | ||
257 | ## | |
258 | # Read a packet from the transport socket. Returns the packet type, and | |
259 | # if available in the packet a label and the contained message. | |
260 | def read | |
b164cc8e MW |
261 | len = recv_all(4).unpack("N")[0] |
262 | encoding = recv_all(len) | |
1038d965 MW |
263 | type = encoding.unpack("c")[0] |
264 | len = 1 | |
265 | case type | |
cc2ef8f8 TB |
266 | when CMD_REQUEST, EVENT_REGISTER, EVENT_UNREGISTER, EVENT |
267 | label = encoding[2, encoding[1].unpack("c")[0]] | |
268 | len += label.length + 1 | |
269 | when CMD_RESPONSE, CMD_UNKNOWN, EVENT_CONFIRM, EVENT_UNKNOWN | |
270 | label = nil | |
271 | else | |
272 | raise TransportError, "invalid message: #{type}" | |
1038d965 | 273 | end |
cc2ef8f8 TB |
274 | message = if encoding.length == len |
275 | Message.new | |
276 | else | |
277 | Message.new(encoding[len..-1]) | |
278 | end | |
279 | [type, label, message] | |
1038d965 MW |
280 | end |
281 | ||
282 | def dispatch_event(name, message) | |
283 | @events[name].each do |handler| | |
284 | handler.call(name, message) | |
285 | end | |
286 | end | |
287 | ||
288 | def read_and_dispatch_event | |
289 | type, label, message = read | |
cc2ef8f8 TB |
290 | raise TransportError, "unexpected message: #{type}" if type != EVENT |
291 | ||
292 | dispatch_event(label, message) | |
1038d965 MW |
293 | end |
294 | ||
295 | def read_and_dispatch_events | |
296 | loop do | |
297 | type, label, message = read | |
cc2ef8f8 TB |
298 | return type, label, message if type != EVENT |
299 | ||
300 | dispatch_event(label, message) | |
1038d965 MW |
301 | end |
302 | end | |
303 | ||
304 | ## | |
305 | # Send a command with a given name, and optionally a message. Returns | |
306 | # the reply message on success. | |
307 | def request(name, message = nil) | |
308 | write(CMD_REQUEST, name, message) | |
cc2ef8f8 | 309 | type, _label, message = read_and_dispatch_events |
1038d965 | 310 | case type |
cc2ef8f8 TB |
311 | when CMD_RESPONSE |
312 | return message | |
313 | when CMD_UNKNOWN | |
314 | raise CommandUnknownError, name | |
315 | else | |
316 | raise CommandError, "invalid response for #{name}" | |
1038d965 MW |
317 | end |
318 | end | |
319 | ||
320 | ## | |
321 | # Register a handler method for the given event name | |
322 | def register(name, handler) | |
323 | write(EVENT_REGISTER, name, nil) | |
cc2ef8f8 | 324 | type, _label, _message = read_and_dispatch_events |
1038d965 | 325 | case type |
cc2ef8f8 TB |
326 | when EVENT_CONFIRM |
327 | if @events.key?(name) | |
328 | @events[name] += [handler] | |
1038d965 | 329 | else |
cc2ef8f8 TB |
330 | @events[name] = [handler] |
331 | end | |
332 | when EVENT_UNKNOWN | |
333 | raise EventUnknownError, name | |
334 | else | |
335 | raise EventError, "invalid response for #{name} register" | |
1038d965 MW |
336 | end |
337 | end | |
338 | ||
339 | ## | |
340 | # Unregister a handler method for the given event name | |
341 | def unregister(name, handler) | |
342 | write(EVENT_UNREGISTER, name, nil) | |
cc2ef8f8 | 343 | type, _label, _message = read_and_dispatch_events |
1038d965 | 344 | case type |
cc2ef8f8 TB |
345 | when EVENT_CONFIRM |
346 | @events[name] -= [handler] | |
347 | when EVENT_UNKNOWN | |
348 | raise EventUnknownError, name | |
349 | else | |
350 | raise EventError, "invalid response for #{name} unregister" | |
1038d965 MW |
351 | end |
352 | end | |
353 | end | |
354 | ||
1038d965 MW |
355 | ## |
356 | # The Connection class provides the high-level interface to monitor, configure | |
357 | # and control the IKE daemon. It takes a connected stream-oriented Socket for | |
358 | # the communication with the IKE daemon. | |
359 | # | |
360 | # This class takes and returns ruby objects for the exchanged message data. | |
361 | # * Sections get encoded as Hash, containing other sections as Hash, or | |
362 | # * Key/Values, where the values are Strings as Hash values | |
363 | # * Lists get encoded as Arrays with String values | |
364 | # Non-String values that are not a Hash nor an Array get converted with .to_s | |
365 | # during encoding. | |
366 | class Connection | |
cc2ef8f8 TB |
367 | ## |
368 | # Create a connection, optionally using the given socket | |
fb8b119c | 369 | def initialize(socket = nil) |
cc2ef8f8 | 370 | socket = UNIXSocket.new("/var/run/charon.vici") if socket.nil? |
1038d965 MW |
371 | @transp = Transport.new(socket) |
372 | end | |
373 | ||
374 | ## | |
1fef01af TB |
375 | # Get daemon version information |
376 | def version | |
377 | call("version") | |
378 | end | |
379 | ||
380 | ## | |
381 | # Get daemon statistics and information. | |
382 | def stats | |
383 | call("stats") | |
384 | end | |
385 | ||
386 | ## | |
387 | # Reload strongswan.conf settings. | |
388 | def reload_settings | |
389 | call("reload-settings") | |
390 | end | |
391 | ||
392 | ## | |
393 | # Initiate a connection. The provided closure is invoked for each log line. | |
394 | def initiate(options, &block) | |
395 | call_with_event("initiate", Message.new(options), "control-log", &block) | |
396 | end | |
397 | ||
398 | ## | |
399 | # Terminate a connection. The provided closure is invoked for each log line. | |
400 | def terminate(options, &block) | |
401 | call_with_event("terminate", Message.new(options), "control-log", &block) | |
402 | end | |
403 | ||
404 | ## | |
405 | # Initiate the rekeying of an SA. | |
406 | def rekey(options) | |
407 | call("rekey", Message.new(options)) | |
408 | end | |
409 | ||
410 | ## | |
411 | # Redirect an IKE_SA. | |
412 | def redirect(options) | |
413 | call("redirect", Message.new(options)) | |
414 | end | |
415 | ||
416 | ## | |
417 | # Install a shunt/route policy. | |
418 | def install(policy) | |
419 | call("install", Message.new(policy)) | |
420 | end | |
421 | ||
422 | ## | |
423 | # Uninstall a shunt/route policy. | |
424 | def uninstall(policy) | |
425 | call("uninstall", Message.new(policy)) | |
1038d965 MW |
426 | end |
427 | ||
428 | ## | |
429 | # List matching active SAs. The provided closure is invoked for each | |
430 | # matching SA. | |
431 | def list_sas(match = nil, &block) | |
432 | call_with_event("list-sas", Message.new(match), "list-sa", &block) | |
433 | end | |
434 | ||
435 | ## | |
436 | # List matching installed policies. The provided closure is invoked | |
437 | # for each matching policy. | |
438 | def list_policies(match, &block) | |
439 | call_with_event("list-policies", Message.new(match), "list-policy", | |
440 | &block) | |
441 | end | |
442 | ||
1fef01af TB |
443 | ## |
444 | # List matching loaded connections. The provided closure is invoked | |
445 | # for each matching connection. | |
446 | def list_conns(match = nil, &block) | |
447 | call_with_event("list-conns", Message.new(match), "list-conn", &block) | |
448 | end | |
449 | ||
450 | ## | |
451 | # Get the names of connections managed by vici. | |
cc2ef8f8 | 452 | def get_conns |
1fef01af TB |
453 | call("get-conns") |
454 | end | |
455 | ||
1038d965 MW |
456 | ## |
457 | # List matching loaded certificates. The provided closure is invoked | |
458 | # for each matching certificate definition. | |
459 | def list_certs(match = nil, &block) | |
460 | call_with_event("list-certs", Message.new(match), "list-cert", &block) | |
461 | end | |
462 | ||
1fef01af TB |
463 | ## |
464 | # List matching loaded certification authorities. The provided closure is | |
465 | # invoked for each matching certification authority definition. | |
466 | def list_authorities(match = nil, &block) | |
467 | call_with_event("list-authorities", Message.new(match), "list-authority", | |
468 | &block) | |
469 | end | |
470 | ||
471 | ## | |
472 | # Get the names of certification authorities managed by vici. | |
cc2ef8f8 | 473 | def get_authorities |
1fef01af TB |
474 | call("get-authorities") |
475 | end | |
476 | ||
1038d965 MW |
477 | ## |
478 | # Load a connection into the daemon. | |
479 | def load_conn(conn) | |
3b394445 | 480 | call("load-conn", Message.new(conn)) |
1038d965 MW |
481 | end |
482 | ||
483 | ## | |
484 | # Unload a connection from the daemon. | |
485 | def unload_conn(conn) | |
3b394445 | 486 | call("unload-conn", Message.new(conn)) |
1038d965 MW |
487 | end |
488 | ||
489 | ## | |
1fef01af TB |
490 | # Load a certificate into the daemon. |
491 | def load_cert(cert) | |
492 | call("load-cert", Message.new(cert)) | |
1038d965 MW |
493 | end |
494 | ||
2c7cfe76 | 495 | ## |
1fef01af TB |
496 | # Load a private key into the daemon. |
497 | def load_key(key) | |
498 | call("load-key", Message.new(key)) | |
2c7cfe76 AS |
499 | end |
500 | ||
1038d965 | 501 | ## |
1fef01af TB |
502 | # Unload a private key from the daemon. |
503 | def unload_key(key) | |
504 | call("unload-key", Message.new(key)) | |
1038d965 MW |
505 | end |
506 | ||
507 | ## | |
1fef01af | 508 | # Get the identifiers of private keys loaded via vici. |
cc2ef8f8 | 509 | def get_keys |
1fef01af | 510 | call("get-keys") |
1038d965 MW |
511 | end |
512 | ||
513 | ## | |
1fef01af TB |
514 | # Load a private key located on a token into the daemon. |
515 | def load_token(token) | |
516 | call("load-token", Message.new(token)) | |
1038d965 MW |
517 | end |
518 | ||
519 | ## | |
520 | # Load a shared key into the daemon. | |
521 | def load_shared(shared) | |
3b394445 | 522 | call("load-shared", Message.new(shared)) |
1038d965 MW |
523 | end |
524 | ||
525 | ## | |
1fef01af TB |
526 | # Unload a shared key from the daemon. |
527 | def unload_shared(shared) | |
528 | call("unload-shared", Message.new(shared)) | |
1038d965 MW |
529 | end |
530 | ||
531 | ## | |
1fef01af | 532 | # Get the unique identifiers of shared keys loaded via vici. |
cc2ef8f8 | 533 | def get_shared |
1fef01af | 534 | call("get-shared") |
1038d965 MW |
535 | end |
536 | ||
537 | ## | |
1fef01af TB |
538 | # Flush credential cache. |
539 | def flush_certs(match = nil) | |
540 | call("flush-certs", Message.new(match)) | |
1038d965 MW |
541 | end |
542 | ||
543 | ## | |
1fef01af | 544 | # Clear all loaded credentials. |
cc2ef8f8 | 545 | def clear_creds |
1fef01af | 546 | call("clear-creds") |
1038d965 MW |
547 | end |
548 | ||
549 | ## | |
1fef01af TB |
550 | # Load a certification authority into the daemon. |
551 | def load_authority(authority) | |
552 | call("load-authority", Message.new(authority)) | |
1038d965 MW |
553 | end |
554 | ||
43b46b26 | 555 | ## |
1fef01af TB |
556 | # Unload a certification authority from the daemon. |
557 | def unload_authority(authority) | |
558 | call("unload-authority", Message.new(authority)) | |
43b46b26 TB |
559 | end |
560 | ||
1038d965 | 561 | ## |
1fef01af TB |
562 | # Load a virtual IP / attribute pool into the daemon. |
563 | def load_pool(pool) | |
564 | call("load-pool", Message.new(pool)) | |
1038d965 MW |
565 | end |
566 | ||
567 | ## | |
1fef01af TB |
568 | # Unload a virtual IP / attribute pool from the daemon. |
569 | def unload_pool(pool) | |
570 | call("unload-pool", Message.new(pool)) | |
1038d965 MW |
571 | end |
572 | ||
573 | ## | |
1fef01af TB |
574 | # Get the currently loaded pools. |
575 | def get_pools(options) | |
576 | call("get-pools", Message.new(options)) | |
1038d965 MW |
577 | end |
578 | ||
579 | ## | |
1fef01af | 580 | # Get currently loaded algorithms and their implementation. |
cc2ef8f8 | 581 | def get_algorithms |
1fef01af | 582 | call("get-algorithms") |
1038d965 MW |
583 | end |
584 | ||
585 | ## | |
1fef01af TB |
586 | # Get global or connection-specific counters for IKE events. |
587 | def get_counters(options = nil) | |
588 | call("get-counters", Message.new(options)) | |
589 | end | |
590 | ||
591 | ## | |
592 | # Reset global or connection-specific IKE event counters. | |
593 | def reset_counters(options = nil) | |
594 | call("reset-counters", Message.new(options)) | |
1038d965 MW |
595 | end |
596 | ||
597 | ## | |
598 | # Listen for a set of event messages. This call is blocking, and invokes | |
599 | # the passed closure for each event received. The closure receives the | |
600 | # event name and the event message as argument. To stop listening, the | |
ca280574 | 601 | # closure may raise a StopEventListening exception, the only caught |
1038d965 MW |
602 | # exception. |
603 | def listen_events(events, &block) | |
604 | self.class.instance_eval do | |
605 | define_method(:listen_event) do |label, message| | |
606 | block.call(label, message.root) | |
607 | end | |
608 | end | |
609 | events.each do |event| | |
610 | @transp.register(event, method(:listen_event)) | |
611 | end | |
612 | begin | |
613 | loop do | |
614 | @transp.read_and_dispatch_event | |
615 | end | |
616 | rescue StopEventListening | |
617 | ensure | |
618 | events.each do |event| | |
619 | @transp.unregister(event, method(:listen_event)) | |
620 | end | |
621 | end | |
622 | end | |
623 | ||
3b394445 TB |
624 | ## |
625 | # Issue a command request. Checks if the reply of a command indicates | |
626 | # "success", otherwise raises a CommandExecError exception. | |
627 | def call(command, request = nil) | |
628 | check_success(@transp.request(command, request)) | |
629 | end | |
630 | ||
1038d965 MW |
631 | ## |
632 | # Issue a command request, but register for a specific event while the | |
633 | # command is active. VICI uses this mechanism to stream potentially large | |
634 | # data objects continuously. The provided closure is invoked for all | |
635 | # event messages. | |
636 | def call_with_event(command, request, event, &block) | |
637 | self.class.instance_eval do | |
cc2ef8f8 | 638 | define_method(:call_event) do |_label, message| |
1038d965 MW |
639 | block.call(message.root) |
640 | end | |
641 | end | |
642 | @transp.register(event, method(:call_event)) | |
643 | begin | |
644 | reply = @transp.request(command, request) | |
645 | ensure | |
646 | @transp.unregister(event, method(:call_event)) | |
647 | end | |
3b394445 | 648 | check_success(reply) |
1038d965 MW |
649 | end |
650 | ||
651 | ## | |
652 | # Check if the reply of a command indicates "success", otherwise raise a | |
653 | # CommandExecError exception | |
654 | def check_success(reply) | |
655 | root = reply.root | |
3b394445 | 656 | if root.key?("success") && root["success"] != "yes" |
1038d965 MW |
657 | raise CommandExecError, root["errmsg"] |
658 | end | |
cc2ef8f8 | 659 | |
1038d965 MW |
660 | root |
661 | end | |
662 | end | |
663 | end |