vici 5.3.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (2) hide show
  1. data/lib/vici.rb +595 -0
  2. metadata +50 -0
@@ -0,0 +1,595 @@
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
+ # Clear all loaded credentials.
453
+ def clear_creds()
454
+ check_success(@transp.request("clear-creds"))
455
+ end
456
+
457
+ ##
458
+ # Load a certificate into the daemon.
459
+ def load_cert(cert)
460
+ check_success(@transp.request("load-cert", Message.new(cert)))
461
+ end
462
+
463
+ ##
464
+ # Load a private key into the daemon.
465
+ def load_key(key)
466
+ check_success(@transp.request("load-key", Message.new(key)))
467
+ end
468
+
469
+ ##
470
+ # Load a shared key into the daemon.
471
+ def load_shared(shared)
472
+ check_success(@transp.request("load-shared", Message.new(shared)))
473
+ end
474
+
475
+ ##
476
+ # Load a virtual IP / attribute pool
477
+ def load_pool(pool)
478
+ check_success(@transp.request("load-pool", Message.new(pool)))
479
+ end
480
+
481
+ ##
482
+ # Unload a virtual IP / attribute pool
483
+ def unload_pool(pool)
484
+ check_success(@transp.request("unload-pool", Message.new(pool)))
485
+ end
486
+
487
+ ##
488
+ # Get the currently loaded pools.
489
+ def get_pools()
490
+ @transp.request("get-pools").root
491
+ end
492
+
493
+ ##
494
+ # Initiate a connection. The provided closure is invoked for each log line.
495
+ def initiate(options, &block)
496
+ check_success(call_with_event("initiate", Message.new(options),
497
+ "control-log", &block))
498
+ end
499
+
500
+ ##
501
+ # Terminate a connection. The provided closure is invoked for each log line.
502
+ def terminate(options, &block)
503
+ check_success(call_with_event("terminate", Message.new(options),
504
+ "control-log", &block))
505
+ end
506
+
507
+ ##
508
+ # Install a shunt/route policy.
509
+ def install(policy)
510
+ check_success(@transp.request("install", Message.new(policy)))
511
+ end
512
+
513
+ ##
514
+ # Uninstall a shunt/route policy.
515
+ def uninstall(policy)
516
+ check_success(@transp.request("uninstall", Message.new(policy)))
517
+ end
518
+
519
+ ##
520
+ # Reload strongswan.conf settings.
521
+ def reload_settings
522
+ check_success(@transp.request("reload-settings", nil))
523
+ end
524
+
525
+ ##
526
+ # Get daemon statistics and information.
527
+ def stats
528
+ @transp.request("stats", nil).root
529
+ end
530
+
531
+ ##
532
+ # Get daemon version information
533
+ def version
534
+ @transp.request("version", nil).root
535
+ end
536
+
537
+ ##
538
+ # Listen for a set of event messages. This call is blocking, and invokes
539
+ # the passed closure for each event received. The closure receives the
540
+ # event name and the event message as argument. To stop listening, the
541
+ # closure may raise a StopEventListening exception, the only catched
542
+ # exception.
543
+ def listen_events(events, &block)
544
+ self.class.instance_eval do
545
+ define_method(:listen_event) do |label, message|
546
+ block.call(label, message.root)
547
+ end
548
+ end
549
+ events.each do |event|
550
+ @transp.register(event, method(:listen_event))
551
+ end
552
+ begin
553
+ loop do
554
+ @transp.read_and_dispatch_event
555
+ end
556
+ rescue StopEventListening
557
+ ensure
558
+ events.each do |event|
559
+ @transp.unregister(event, method(:listen_event))
560
+ end
561
+ end
562
+ end
563
+
564
+ ##
565
+ # Issue a command request, but register for a specific event while the
566
+ # command is active. VICI uses this mechanism to stream potentially large
567
+ # data objects continuously. The provided closure is invoked for all
568
+ # event messages.
569
+ def call_with_event(command, request, event, &block)
570
+ self.class.instance_eval do
571
+ define_method(:call_event) do |label, message|
572
+ block.call(message.root)
573
+ end
574
+ end
575
+ @transp.register(event, method(:call_event))
576
+ begin
577
+ reply = @transp.request(command, request)
578
+ ensure
579
+ @transp.unregister(event, method(:call_event))
580
+ end
581
+ reply
582
+ end
583
+
584
+ ##
585
+ # Check if the reply of a command indicates "success", otherwise raise a
586
+ # CommandExecError exception
587
+ def check_success(reply)
588
+ root = reply.root
589
+ if root["success"] != "yes"
590
+ raise CommandExecError, root["errmsg"]
591
+ end
592
+ root
593
+ end
594
+ end
595
+ end
metadata ADDED
@@ -0,0 +1,50 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: vici
3
+ version: !ruby/object:Gem::Version
4
+ version: 5.3.5
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Martin Willi
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2016-01-22 00:00:00.000000000 Z
13
+ dependencies: []
14
+ description: ! "\n The strongSwan VICI protocol allows external application to
15
+ monitor,\n configure and control the IKE daemon charon. This ruby gem provides
16
+ a\n native client side implementation of the VICI protocol, well suited to\n
17
+ \ script automated tasks in a relaible way.\n "
18
+ email:
19
+ - martin@strongswan.org
20
+ executables: []
21
+ extensions: []
22
+ extra_rdoc_files: []
23
+ files:
24
+ - lib/vici.rb
25
+ homepage: https://wiki.strongswan.org/projects/strongswan/wiki/Vici
26
+ licenses:
27
+ - MIT
28
+ post_install_message:
29
+ rdoc_options: []
30
+ require_paths:
31
+ - lib
32
+ required_ruby_version: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ required_rubygems_version: !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ! '>='
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ requirements: []
45
+ rubyforge_project:
46
+ rubygems_version: 1.8.23
47
+ signing_key:
48
+ specification_version: 3
49
+ summary: Native ruby interface for strongSwan VICI
50
+ test_files: []