]> git.ipfire.org Git - thirdparty/strongswan.git/blame - src/libcharon/plugins/vici/ruby/lib/vici.rb
vici: Some code style fixes in the Ruby bindings
[thirdparty/strongswan.git] / src / libcharon / plugins / vici / ruby / lib / vici.rb
CommitLineData
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
30module 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
663end