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