]>
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 | # | |
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 | ||
b164cc8e MW |
245 | ## |
246 | # Receive data from socket, until len bytes read | |
247 | def recv_all(len) | |
248 | encoding = "" | |
249 | while encoding.length < len do | |
250 | encoding << @socket.recv(len - encoding.length) | |
251 | end | |
252 | encoding | |
253 | end | |
254 | ||
255 | ## | |
256 | # Send data to socket, until all bytes sent | |
257 | def send_all(encoding) | |
258 | len = 0 | |
259 | while len < encoding.length do | |
260 | len += @socket.send(encoding[len..-1], 0) | |
261 | end | |
262 | end | |
263 | ||
1038d965 MW |
264 | ## |
265 | # Write a packet prefixed by its length over the transport socket. Type | |
266 | # specifies the message, the optional label and message get appended. | |
267 | def write(type, label, message) | |
268 | encoding = "" | |
269 | if label | |
270 | encoding << label.length << label | |
271 | end | |
272 | if message | |
273 | encoding << message.encoding | |
274 | end | |
b164cc8e | 275 | send_all([encoding.length + 1, type].pack("Nc") + encoding) |
1038d965 MW |
276 | end |
277 | ||
278 | ## | |
279 | # Read a packet from the transport socket. Returns the packet type, and | |
280 | # if available in the packet a label and the contained message. | |
281 | def read | |
b164cc8e MW |
282 | len = recv_all(4).unpack("N")[0] |
283 | encoding = recv_all(len) | |
1038d965 MW |
284 | type = encoding.unpack("c")[0] |
285 | len = 1 | |
286 | case type | |
287 | when CMD_REQUEST, EVENT_REGISTER, EVENT_UNREGISTER, EVENT | |
288 | label = encoding[2, encoding[1].unpack("c")[0]] | |
289 | len += label.length + 1 | |
290 | when CMD_RESPONSE, CMD_UNKNOWN, EVENT_CONFIRM, EVENT_UNKNOWN | |
291 | label = nil | |
292 | else | |
293 | raise TransportError, "invalid message: #{type}" | |
294 | end | |
295 | if encoding.length == len | |
296 | return type, label, Message.new | |
297 | end | |
298 | return type, label, Message.new(encoding[len..-1]) | |
299 | end | |
300 | ||
301 | def dispatch_event(name, message) | |
302 | @events[name].each do |handler| | |
303 | handler.call(name, message) | |
304 | end | |
305 | end | |
306 | ||
307 | def read_and_dispatch_event | |
308 | type, label, message = read | |
309 | p | |
310 | if type == EVENT | |
311 | dispatch_event(label, message) | |
312 | else | |
313 | raise TransportError, "unexpected message: #{type}" | |
314 | end | |
315 | end | |
316 | ||
317 | def read_and_dispatch_events | |
318 | loop do | |
319 | type, label, message = read | |
320 | if type == EVENT | |
321 | dispatch_event(label, message) | |
322 | else | |
323 | return type, label, message | |
324 | end | |
325 | end | |
326 | end | |
327 | ||
328 | ## | |
329 | # Send a command with a given name, and optionally a message. Returns | |
330 | # the reply message on success. | |
331 | def request(name, message = nil) | |
332 | write(CMD_REQUEST, name, message) | |
333 | type, label, message = read_and_dispatch_events | |
334 | case type | |
335 | when CMD_RESPONSE | |
336 | return message | |
337 | when CMD_UNKNOWN | |
338 | raise CommandUnknownError, name | |
339 | else | |
340 | raise CommandError, "invalid response for #{name}" | |
341 | end | |
342 | end | |
343 | ||
344 | ## | |
345 | # Register a handler method for the given event name | |
346 | def register(name, handler) | |
347 | write(EVENT_REGISTER, name, nil) | |
348 | type, label, message = read_and_dispatch_events | |
349 | case type | |
350 | when EVENT_CONFIRM | |
351 | if @events.has_key?(name) | |
352 | @events[name] += [handler] | |
353 | else | |
354 | @events[name] = [handler]; | |
355 | end | |
356 | when EVENT_UNKNOWN | |
357 | raise EventUnknownError, name | |
358 | else | |
359 | raise EventError, "invalid response for #{name} register" | |
360 | end | |
361 | end | |
362 | ||
363 | ## | |
364 | # Unregister a handler method for the given event name | |
365 | def unregister(name, handler) | |
366 | write(EVENT_UNREGISTER, name, nil) | |
367 | type, label, message = read_and_dispatch_events | |
368 | case type | |
369 | when EVENT_CONFIRM | |
370 | @events[name] -= [handler] | |
371 | when EVENT_UNKNOWN | |
372 | raise EventUnknownError, name | |
373 | else | |
374 | raise EventError, "invalid response for #{name} unregister" | |
375 | end | |
376 | end | |
377 | end | |
378 | ||
379 | ||
380 | ## | |
381 | # The Connection class provides the high-level interface to monitor, configure | |
382 | # and control the IKE daemon. It takes a connected stream-oriented Socket for | |
383 | # the communication with the IKE daemon. | |
384 | # | |
385 | # This class takes and returns ruby objects for the exchanged message data. | |
386 | # * Sections get encoded as Hash, containing other sections as Hash, or | |
387 | # * Key/Values, where the values are Strings as Hash values | |
388 | # * Lists get encoded as Arrays with String values | |
389 | # Non-String values that are not a Hash nor an Array get converted with .to_s | |
390 | # during encoding. | |
391 | class Connection | |
392 | ||
393 | def initialize(socket) | |
394 | @transp = Transport.new(socket) | |
395 | end | |
396 | ||
397 | ## | |
398 | # List matching loaded connections. The provided closure is invoked | |
399 | # for each matching connection. | |
400 | def list_conns(match = nil, &block) | |
401 | call_with_event("list-conns", Message.new(match), "list-conn", &block) | |
402 | end | |
403 | ||
404 | ## | |
405 | # List matching active SAs. The provided closure is invoked for each | |
406 | # matching SA. | |
407 | def list_sas(match = nil, &block) | |
408 | call_with_event("list-sas", Message.new(match), "list-sa", &block) | |
409 | end | |
410 | ||
411 | ## | |
412 | # List matching installed policies. The provided closure is invoked | |
413 | # for each matching policy. | |
414 | def list_policies(match, &block) | |
415 | call_with_event("list-policies", Message.new(match), "list-policy", | |
416 | &block) | |
417 | end | |
418 | ||
419 | ## | |
420 | # List matching loaded certificates. The provided closure is invoked | |
421 | # for each matching certificate definition. | |
422 | def list_certs(match = nil, &block) | |
423 | call_with_event("list-certs", Message.new(match), "list-cert", &block) | |
424 | end | |
425 | ||
426 | ## | |
427 | # Load a connection into the daemon. | |
428 | def load_conn(conn) | |
429 | check_success(@transp.request("load-conn", Message.new(conn))) | |
430 | end | |
431 | ||
432 | ## | |
433 | # Unload a connection from the daemon. | |
434 | def unload_conn(conn) | |
435 | check_success(@transp.request("unload-conn", Message.new(conn))) | |
436 | end | |
437 | ||
438 | ## | |
439 | # Get the names of connections managed by vici. | |
440 | def get_conns() | |
441 | @transp.request("get-conns").root | |
442 | end | |
443 | ||
444 | ## | |
445 | # Clear all loaded credentials. | |
446 | def clear_creds() | |
447 | check_success(@transp.request("clear-creds")) | |
448 | end | |
449 | ||
450 | ## | |
451 | # Load a certificate into the daemon. | |
452 | def load_cert(cert) | |
453 | check_success(@transp.request("load-cert", Message.new(cert))) | |
454 | end | |
455 | ||
456 | ## | |
457 | # Load a private key into the daemon. | |
458 | def load_key(key) | |
459 | check_success(@transp.request("load-key", Message.new(key))) | |
460 | end | |
461 | ||
462 | ## | |
463 | # Load a shared key into the daemon. | |
464 | def load_shared(shared) | |
465 | check_success(@transp.request("load-shared", Message.new(shared))) | |
466 | end | |
467 | ||
468 | ## | |
469 | # Load a virtual IP / attribute pool | |
470 | def load_pool(pool) | |
471 | check_success(@transp.request("load-pool", Message.new(pool))) | |
472 | end | |
473 | ||
474 | ## | |
475 | # Unload a virtual IP / attribute pool | |
476 | def unload_pool(pool) | |
477 | check_success(@transp.request("unload-pool", Message.new(pool))) | |
478 | end | |
479 | ||
480 | ## | |
481 | # Get the currently loaded pools. | |
482 | def get_pools() | |
483 | @transp.request("get-pools").root | |
484 | end | |
485 | ||
486 | ## | |
487 | # Initiate a connection. The provided closure is invoked for each log line. | |
488 | def initiate(options, &block) | |
489 | check_success(call_with_event("initiate", Message.new(options), | |
490 | "control-log", &block)) | |
491 | end | |
492 | ||
493 | ## | |
494 | # Terminate a connection. The provided closure is invoked for each log line. | |
495 | def terminate(options, &block) | |
496 | check_success(call_with_event("terminate", Message.new(options), | |
497 | "control-log", &block)) | |
498 | end | |
499 | ||
500 | ## | |
501 | # Install a shunt/route policy. | |
502 | def install(policy) | |
503 | check_success(@transp.request("install", Message.new(policy))) | |
504 | end | |
505 | ||
506 | ## | |
507 | # Uninstall a shunt/route policy. | |
508 | def uninstall(policy) | |
509 | check_success(@transp.request("uninstall", Message.new(policy))) | |
510 | end | |
511 | ||
512 | ## | |
513 | # Reload strongswan.conf settings. | |
514 | def reload_settings | |
515 | check_success(@transp.request("reload-settings", nil)) | |
516 | end | |
517 | ||
518 | ## | |
519 | # Get daemon statistics and information. | |
520 | def stats | |
521 | @transp.request("stats", nil).root | |
522 | end | |
523 | ||
524 | ## | |
525 | # Get daemon version information | |
526 | def version | |
527 | @transp.request("version", nil).root | |
528 | end | |
529 | ||
530 | ## | |
531 | # Listen for a set of event messages. This call is blocking, and invokes | |
532 | # the passed closure for each event received. The closure receives the | |
533 | # event name and the event message as argument. To stop listening, the | |
534 | # closure may raise a StopEventListening exception, the only catched | |
535 | # exception. | |
536 | def listen_events(events, &block) | |
537 | self.class.instance_eval do | |
538 | define_method(:listen_event) do |label, message| | |
539 | block.call(label, message.root) | |
540 | end | |
541 | end | |
542 | events.each do |event| | |
543 | @transp.register(event, method(:listen_event)) | |
544 | end | |
545 | begin | |
546 | loop do | |
547 | @transp.read_and_dispatch_event | |
548 | end | |
549 | rescue StopEventListening | |
550 | ensure | |
551 | events.each do |event| | |
552 | @transp.unregister(event, method(:listen_event)) | |
553 | end | |
554 | end | |
555 | end | |
556 | ||
557 | ## | |
558 | # Issue a command request, but register for a specific event while the | |
559 | # command is active. VICI uses this mechanism to stream potentially large | |
560 | # data objects continuously. The provided closure is invoked for all | |
561 | # event messages. | |
562 | def call_with_event(command, request, event, &block) | |
563 | self.class.instance_eval do | |
564 | define_method(:call_event) do |label, message| | |
565 | block.call(message.root) | |
566 | end | |
567 | end | |
568 | @transp.register(event, method(:call_event)) | |
569 | begin | |
570 | reply = @transp.request(command, request) | |
571 | ensure | |
572 | @transp.unregister(event, method(:call_event)) | |
573 | end | |
574 | reply | |
575 | end | |
576 | ||
577 | ## | |
578 | # Check if the reply of a command indicates "success", otherwise raise a | |
579 | # CommandExecError exception | |
580 | def check_success(reply) | |
581 | root = reply.root | |
582 | if root["success"] != "yes" | |
583 | raise CommandExecError, root["errmsg"] | |
584 | end | |
585 | root | |
586 | end | |
587 | end | |
588 | end |