Merge branch 'vici-python'

Introduce a Python Egg for the vici plugin, contributed by Björn Schuberg.
This commit is contained in:
Martin Willi
2015-03-18 15:03:28 +01:00
23 changed files with 947 additions and 30 deletions
+1
View File
@@ -8,6 +8,7 @@ before_install:
- sudo apt-get update -qq
- sudo apt-get install -qq bison flex gperf gettext
- ./scripts/test.sh deps
- ./scripts/test.sh pydeps
script:
- ./autogen.sh
+4
View File
@@ -35,6 +35,10 @@ strongswan-5.3.0
Windows 7 IKEv2 clients, which announces its services over the tunnel if the
negotiated IPsec policy allows it.
- For the vici plugin a Python Egg has been added to allow Python applications
to control or monitor the IKE daemon using the VICI interface, similar to the
existing ruby gem. The Python library has been contributed by Björn Schuberg.
- EAP server methods now can fulfill public key constraints, such as rightcert
or rightca. Additionally, public key and signature constraints can be
specified for EAP methods in the rightauth keyword. Currently the EAP-TLS and
+18
View File
@@ -69,6 +69,7 @@ ARG_WITH_SET([mpz_powm_sec], [yes], [use the more side-channel resistant
ARG_WITH_SET([dev-headers], [no], [install strongSwan development headers to directory.])
ARG_WITH_SET([printf-hooks], [auto], [force the use of a specific printf hook implementation (auto, builtin, glibc, vstr).])
ARG_WITH_SET([rubygemdir], ["gem environment gemdir"], [path to install ruby gems to])
ARG_WITH_SET([pythoneggdir], ["main site-packages directory"], [path to install python eggs to to])
if test -n "$PKG_CONFIG"; then
systemdsystemunitdir_default=$($PKG_CONFIG --variable=systemdsystemunitdir systemd)
@@ -290,6 +291,7 @@ ARG_DISBL_SET([load-warning], [disable the charon plugin load option warning i
ARG_ENABL_SET([mediation], [enable IKEv2 Mediation Extension.])
ARG_ENABL_SET([unwind-backtraces],[use libunwind to create backtraces for memory leaks and segfaults.])
ARG_ENABL_SET([ruby-gems], [enable installation of provided ruby gems.])
ARG_ENABL_SET([python-eggs], [enable installation of provided python eggs.])
# compile options
ARG_ENABL_SET([coverage], [enable lcov coverage report generation.])
ARG_ENABL_SET([leak-detective], [enable malloc hooks to find memory leaks.])
@@ -1186,6 +1188,19 @@ if test x$ruby_gems = xtrue; then
AC_SUBST(RUBYGEMDIR, "$rubygemdir")
fi
if test x$python_eggs = xtrue; then
AC_PATH_PROG([EASY_INSTALL], [easy_install], [], [$PATH:/bin:/usr/bin:/usr/local/bin])
if test x$EASY_INSTALL = x; then
AC_MSG_ERROR(Python easy_install not found)
fi
if test "x$pythoneggdir" = "xmain site-packages directory"; then
AC_SUBST(PYTHONEGGINSTALLDIR, "")
else
AC_SUBST(PYTHONEGGINSTALLDIR, "--install-dir $pythoneggdir")
fi
AC_PATH_PROG([PY_TEST], [py.test], [], [$PATH:/bin:/usr/bin:/usr/local/bin])
fi
# ===============================================
# collect plugin list for strongSwan components
# ===============================================
@@ -1552,6 +1567,8 @@ AM_CONDITIONAL(USE_SVC, test x$svc = xtrue)
AM_CONDITIONAL(USE_SYSTEMD, test x$systemd = xtrue)
AM_CONDITIONAL(USE_LEGACY_SYSTEMD, test -n "$systemdsystemunitdir" -a "x$systemdsystemunitdir" != xno)
AM_CONDITIONAL(USE_RUBY_GEMS, test x$ruby_gems = xtrue)
AM_CONDITIONAL(USE_PYTHON_EGGS, test x$python_eggs = xtrue)
AM_CONDITIONAL(USE_PY_TEST, test "x$PY_TEST" != x)
# ========================
# set global definitions
@@ -1758,6 +1775,7 @@ AC_CONFIG_FILES([
src/libcharon/plugins/stroke/Makefile
src/libcharon/plugins/vici/Makefile
src/libcharon/plugins/vici/ruby/Makefile
src/libcharon/plugins/vici/python/Makefile
src/libcharon/plugins/updown/Makefile
src/libcharon/plugins/dhcp/Makefile
src/libcharon/plugins/load_tester/Makefile
+7 -1
View File
@@ -52,7 +52,8 @@ all)
DEPS="$DEPS libcurl4-gnutls-dev libsoup2.4-dev libunbound-dev libldns-dev
libmysqlclient-dev libsqlite3-dev clearsilver-dev libfcgi-dev
libnm-glib-dev libnm-glib-vpn-dev libpcsclite-dev libpam0g-dev
binutils-dev libunwind7-dev libjson0-dev iptables-dev"
binutils-dev libunwind7-dev libjson0-dev iptables-dev python-pip"
PYDEPS="pytest"
;;
win*)
CONFIG="--disable-defaults --enable-svc --enable-ikev2
@@ -95,6 +96,11 @@ if test "$1" = "deps"; then
exit $?
fi
if test "$1" = "pydeps"; then
test -z "$PYDEPS" || sudo pip -q install $PYDEPS
exit $?
fi
CONFIG="$CONFIG
--enable-silent-rules
--enable-test-vectors
+4
View File
@@ -74,3 +74,7 @@ SUBDIRS =
if USE_RUBY_GEMS
SUBDIRS += ruby
endif
if USE_PYTHON_EGGS
SUBDIRS += python
endif
+73 -3
View File
@@ -821,9 +821,9 @@ during encoding.
## Connecting to the daemon ##
To create a connection to the daemon, a socket must be passed to the
_Connection_ constructor. There is no default, but on Unix systems usually
a Unix socket over _/var/run/charon.vici_ is used:
To create a connection to the daemon, a socket can be passed to the
_Connection_ constructor. If none is passed, a default Unix socket at
_/var/run/charon.vici_ is used:
require "vici"
require "socket"
@@ -855,3 +855,73 @@ _list-conns_ command and implicitly the _list-conn_ event:
For more details about the ruby gem refer to the comments in the gem source
code or the generated documentation.
# vici Python egg #
The _vici Python egg_ is a pure Python implementation of the VICI protocol to
implement client applications. It is provided in the _python_ subdirectory, and
gets built and installed if strongSwan has been _./configure_'d with
_--enable-vici_ and _--enable-python-eggs_.
The _vici_ module provides a _Session()_ constructor for a high level interface,
the underlying classes are usually not required to build Python applications
using VICI. The _Session_ class provides methods for the supported VICI
commands.
To represent the VICI message data tree, the library converts the binary
encoding to Python data types. The _Session_ class takes and returns Python
objects for the exchanged message data:
* Sections get encoded as OrderedDict, containing other sections, or
* Key/Values, where the values are strings as dictionary values
* Lists get encoded as Python Lists with string values
Values that do not conform to Python dict or list get converted to strings using
str().
## Connecting to the daemon ##
To create a connection to the daemon, a socket can be passed to the _Session_
constructor. If none is passed, a default Unix socket at _/var/run/charon.vici_
is used:
import vici
import socket
s = socket.socket(socket.AF_UNIX)
s.connect("/var/run/charon.vici")
v = vici.Session(s)
## A simple client request ##
An example to print the daemon version information is as simple as:
ver = v.version()
print "{daemon} {version} ({sysname}, {release}, {machine})".format(**ver)
## A request with response iteration ##
The _Session_ class returns an iterable Python generator for streamed events to
continuously stream objects to the caller. The following example lists all
loaded connections using the _list-conns_ command and implicitly the _list-conn_
event:
for conn in v.list_conns():
for key in conn:
print key
Please note that if the returned generator is not iterated completely, it must
be closed using _close()_. This is implicitly done when breaking from a loop,
but an explicit call may be required when directly iterating the generator with
_next()_.
## Sorting in dictionaries ##
In VICI, in some message trees the order of objects in dictionary matters. In
contrast to ruby Hashes, Python dictionaries do not preserve order of added
objects. It is therefore recommended to use OrderedDicts instead of the default
dictionaries. Objects returned by the library use OrderedDicts.
## API documentation ##
For more details about the Python egg refer to the comments in the Python source
code.
@@ -0,0 +1,5 @@
*.pyc
build
dist
vici.egg-info
setup.py
+19
View File
@@ -0,0 +1,19 @@
Copyright (c) 2015 Björn Schuberg
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
@@ -0,0 +1 @@
include LICENSE
@@ -0,0 +1,33 @@
EXTRA_DIST = LICENSE MANIFEST.in \
setup.py.in \
vici/test/__init__.py \
vici/test/test_protocol.py \
vici/__init__.py \
vici/compat.py \
vici/exception.py \
vici/protocol.py \
vici/session.py
setup.py: $(srcdir)/setup.py.in
$(AM_V_GEN) sed \
-e "s:@EGG_VERSION@:$(PACKAGE_VERSION):" \
$(srcdir)/setup.py.in > $@
all-local: dist/vici-$(PACKAGE_VERSION)-py$(PYTHON_VERSION).egg
dist/vici-$(PACKAGE_VERSION)-py$(PYTHON_VERSION).egg: $(EXTRA_DIST) setup.py
(cd $(srcdir); $(PYTHON) setup.py bdist_egg \
-b $(shell readlink -f $(builddir))/build \
-d $(shell readlink -f $(builddir))/dist)
clean-local: setup.py
$(PYTHON) setup.py clean -a
rm -rf vici.egg-info dist setup.py
install-exec-local: dist/vici-$(PACKAGE_VERSION)-py$(PYTHON_VERSION).egg
$(EASY_INSTALL) $(PYTHONEGGINSTALLDIR) \
dist/vici-$(PACKAGE_VERSION)-py$(PYTHON_VERSION).egg
if USE_PY_TEST
TESTS = $(PY_TEST)
endif
@@ -0,0 +1,34 @@
from setuptools import setup
long_description = (
"The strongSwan VICI protocol allows external application to monitor, "
"configure and control the IKE daemon charon. This python package provides "
"a native client side implementation of the VICI protocol, well suited to "
"script automated tasks in a reliable way."
)
setup(
name="vici",
version="@EGG_VERSION@",
description="Native python interface for strongSwan VICI",
author="Bjorn Schuberg",
url="https://wiki.strongswan.org/projects/strongswan/wiki/Vici",
license="MIT",
packages=["vici"],
long_description=long_description,
include_package_data=True,
classifiers=(
"Development Status :: 3 - Alpha",
"Intended Audience :: Developers",
"Intended Audience :: System Administrators",
"License :: OSI Approved :: MIT License",
"Natural Language :: English",
"Programming Language :: Python :: 2.7",
"Programming Language :: Python :: 3.2",
"Programming Language :: Python :: 3.3",
"Programming Language :: Python :: 3.4",
"Topic :: Security",
"Topic :: Software Development :: Libraries",
)
)
@@ -0,0 +1 @@
from .session import Session
@@ -0,0 +1,14 @@
# Help functions for compatibility between python version 2 and 3
# From http://legacy.python.org/dev/peps/pep-0469
try:
dict.iteritems
except AttributeError:
# python 3
def iteritems(d):
return iter(d.items())
else:
# python 2
def iteritems(d):
return d.iteritems()
@@ -0,0 +1,10 @@
"""Exception types that may be thrown by this library."""
class DeserializationException(Exception):
"""Encountered an unexpected byte sequence or missing element type."""
class SessionException(Exception):
"""Session request exception."""
class CommandException(Exception):
"""Command result exception."""
@@ -0,0 +1,196 @@
import io
import socket
import struct
from collections import namedtuple
from collections import OrderedDict
from .compat import iteritems
from .exception import DeserializationException
class Transport(object):
HEADER_LENGTH = 4
MAX_SEGMENT = 512 * 1024
def __init__(self, sock):
self.socket = sock
def send(self, packet):
self.socket.sendall(struct.pack("!I", len(packet)) + packet)
def receive(self):
raw_length = self.socket.recv(self.HEADER_LENGTH)
length, = struct.unpack("!I", raw_length)
payload = self.socket.recv(length)
return payload
def close(self):
self.socket.shutdown(socket.SHUT_RDWR)
self.socket.close()
class Packet(object):
CMD_REQUEST = 0 # Named request message
CMD_RESPONSE = 1 # Unnamed response message for a request
CMD_UNKNOWN = 2 # Unnamed response if requested command is unknown
EVENT_REGISTER = 3 # Named event registration request
EVENT_UNREGISTER = 4 # Named event de-registration request
EVENT_CONFIRM = 5 # Unnamed confirmation for event (de-)registration
EVENT_UNKNOWN = 6 # Unnamed response if event (de-)registration failed
EVENT = 7 # Named event message
ParsedPacket = namedtuple(
"ParsedPacket",
["response_type", "payload"]
)
ParsedEventPacket = namedtuple(
"ParsedEventPacket",
["response_type", "event_type", "payload"]
)
@classmethod
def _named_request(cls, request_type, request, message=None):
request = request.encode()
payload = struct.pack("!BB", request_type, len(request)) + request
if message is not None:
return payload + message
else:
return payload
@classmethod
def request(cls, command, message=None):
return cls._named_request(cls.CMD_REQUEST, command, message)
@classmethod
def register_event(cls, event_type):
return cls._named_request(cls.EVENT_REGISTER, event_type)
@classmethod
def unregister_event(cls, event_type):
return cls._named_request(cls.EVENT_UNREGISTER, event_type)
@classmethod
def parse(cls, packet):
stream = FiniteStream(packet)
response_type, = struct.unpack("!B", stream.read(1))
if response_type == cls.EVENT:
length, = struct.unpack("!B", stream.read(1))
event_type = stream.read(length)
return cls.ParsedEventPacket(response_type, event_type, stream)
else:
return cls.ParsedPacket(response_type, stream)
class Message(object):
SECTION_START = 1 # Begin a new section having a name
SECTION_END = 2 # End a previously started section
KEY_VALUE = 3 # Define a value for a named key in the section
LIST_START = 4 # Begin a named list for list items
LIST_ITEM = 5 # Define an unnamed item value in the current list
LIST_END = 6 # End a previously started list
@classmethod
def serialize(cls, message):
def encode_named_type(marker, name):
name = name.encode()
return struct.pack("!BB", marker, len(name)) + name
def encode_blob(value):
if not isinstance(value, bytes):
value = str(value).encode()
return struct.pack("!H", len(value)) + value
def serialize_list(lst):
segment = bytes()
for item in lst:
segment += struct.pack("!B", cls.LIST_ITEM) + encode_blob(item)
return segment
def serialize_dict(d):
segment = bytes()
for key, value in iteritems(d):
if isinstance(value, dict):
segment += (
encode_named_type(cls.SECTION_START, key)
+ serialize_dict(value)
+ struct.pack("!B", cls.SECTION_END)
)
elif isinstance(value, list):
segment += (
encode_named_type(cls.LIST_START, key)
+ serialize_list(value)
+ struct.pack("!B", cls.LIST_END)
)
else:
segment += (
encode_named_type(cls.KEY_VALUE, key)
+ encode_blob(value)
)
return segment
return serialize_dict(message)
@classmethod
def deserialize(cls, stream):
def decode_named_type(stream):
length, = struct.unpack("!B", stream.read(1))
return stream.read(length).decode()
def decode_blob(stream):
length, = struct.unpack("!H", stream.read(2))
return stream.read(length)
def decode_list_item(stream):
marker, = struct.unpack("!B", stream.read(1))
while marker == cls.LIST_ITEM:
yield decode_blob(stream)
marker, = struct.unpack("!B", stream.read(1))
if marker != cls.LIST_END:
raise DeserializationException(
"Expected end of list at {pos}".format(pos=stream.tell())
)
section = OrderedDict()
section_stack = []
while stream.has_more():
element_type, = struct.unpack("!B", stream.read(1))
if element_type == cls.SECTION_START:
section_name = decode_named_type(stream)
new_section = OrderedDict()
section[section_name] = new_section
section_stack.append(section)
section = new_section
elif element_type == cls.LIST_START:
list_name = decode_named_type(stream)
section[list_name] = [item for item in decode_list_item(stream)]
elif element_type == cls.KEY_VALUE:
key = decode_named_type(stream)
section[key] = decode_blob(stream)
elif element_type == cls.SECTION_END:
if len(section_stack):
section = section_stack.pop()
else:
raise DeserializationException(
"Unexpected end of section at {pos}".format(
pos=stream.tell()
)
)
if len(section_stack):
raise DeserializationException("Expected end of section")
return section
class FiniteStream(io.BytesIO):
def __len__(self):
return len(self.getvalue())
def has_more(self):
return self.tell() < len(self)
@@ -0,0 +1,327 @@
import collections
import socket
from .exception import SessionException, CommandException
from .protocol import Transport, Packet, Message
class Session(object):
def __init__(self, sock=None):
if sock is None:
sock = socket.socket(socket.AF_UNIX)
sock.connect("/var/run/charon.vici")
self.handler = SessionHandler(Transport(sock))
def version(self):
"""Retrieve daemon and system specific version information.
:return: daemon and system specific version information
:rtype: dict
"""
return self.handler.request("version")
def stats(self):
"""Retrieve IKE daemon statistics and load information.
:return: IKE daemon statistics and load information
:rtype: dict
"""
return self.handler.request("stats")
def reload_settings(self):
"""Reload strongswan.conf settings and any plugins supporting reload.
"""
self.handler.request("reload-settings")
def initiate(self, sa):
"""Initiate an SA.
:param sa: the SA to initiate
:type sa: dict
:return: generator for logs emitted as dict
:rtype: generator
"""
return self.handler.streamed_request("initiate", "control-log", sa)
def terminate(self, sa):
"""Terminate an SA.
:param sa: the SA to terminate
:type sa: dict
:return: generator for logs emitted as dict
:rtype: generator
"""
return self.handler.streamed_request("terminate", "control-log", sa)
def install(self, policy):
"""Install a trap, drop or bypass policy defined by a CHILD_SA config.
:param policy: policy to install
:type policy: dict
"""
self.handler.request("install", policy)
def uninstall(self, policy):
"""Uninstall a trap, drop or bypass policy defined by a CHILD_SA config.
:param policy: policy to uninstall
:type policy: dict
"""
self.handler.request("uninstall", policy)
def list_sas(self, filters=None):
"""Retrieve active IKE_SAs and associated CHILD_SAs.
:param filters: retrieve only matching IKE_SAs (optional)
:type filters: dict
:return: generator for active IKE_SAs and associated CHILD_SAs as dict
:rtype: generator
"""
return self.handler.streamed_request("list-sas", "list-sa", filters)
def list_policies(self, filters=None):
"""Retrieve installed trap, drop and bypass policies.
:param filters: retrieve only matching policies (optional)
:type filters: dict
:return: generator for installed trap, drop and bypass policies as dict
:rtype: generator
"""
return self.handler.streamed_request("list-policies", "list-policy",
filters)
def list_conns(self, filters=None):
"""Retrieve loaded connections.
:param filters: retrieve only matching configuration names (optional)
:type filters: dict
:return: generator for loaded connections as dict
:rtype: generator
"""
return self.handler.streamed_request("list-conns", "list-conn",
filters)
def get_conns(self):
"""Retrieve connection names loaded exclusively over vici.
:return: connection names
:rtype: dict
"""
return self.handler.request("get-conns")
def list_certs(self, filters=None):
"""Retrieve loaded certificates.
:param filters: retrieve only matching certificates (optional)
:type filters: dict
:return: generator for loaded certificates as dict
:rtype: generator
"""
return self.handler.streamed_request("list-certs", "list-cert", filters)
def load_conn(self, connection):
"""Load a connection definition into the daemon.
:param connection: connection definition
:type connection: dict
"""
self.handler.request("load-conn", connection)
def unload_conn(self, name):
"""Unload a connection definition.
:param name: connection definition name
:type name: dict
"""
self.handler.request("unload-conn", name)
def load_cert(self, certificate):
"""Load a certificate into the daemon.
:param certificate: PEM or DER encoded certificate
:type certificate: dict
"""
self.handler.request("load-cert", certificate)
def load_key(self, private_key):
"""Load a private key into the daemon.
:param private_key: PEM or DER encoded key
"""
self.handler.request("load-key", private_key)
def load_shared(self, secret):
"""Load a shared IKE PSK, EAP or XAuth secret into the daemon.
:param secret: shared IKE PSK, EAP or XAuth secret
:type secret: dict
"""
self.handler.request("load-shared", secret)
def clear_creds(self):
"""Clear credentials loaded over vici.
Clear all loaded certificate, private key and shared key credentials.
This affects only credentials loaded over vici, but additionally
flushes the credential cache.
"""
self.handler.request("clear-creds")
def load_pool(self, pool):
"""Load a virtual IP pool.
Load an in-memory virtual IP and configuration attribute pool.
Existing pools with the same name get updated, if possible.
:param pool: virtual IP and configuration attribute pool
:type pool: dict
"""
return self.handler.request("load-pool", pool)
def unload_pool(self, pool_name):
"""Unload a virtual IP pool.
Unload a previously loaded virtual IP and configuration attribute pool.
Unloading fails for pools with leases currently online.
:param pool_name: pool by name
:type pool_name: dict
"""
self.handler.request("unload-pool", pool_name)
def get_pools(self):
"""Retrieve loaded pools.
:return: loaded pools
:rtype: dict
"""
return self.handler.request("get-pools")
class SessionHandler(object):
"""Handles client command execution requests over vici."""
def __init__(self, transport):
self.transport = transport
def _communicate(self, packet):
"""Send packet over transport and parse response.
:param packet: packet to send
:type packet: :py:class:`vici.protocol.Packet`
:return: parsed packet in a tuple with message type and payload
:rtype: :py:class:`collections.namedtuple`
"""
self.transport.send(packet)
return Packet.parse(self.transport.receive())
def request(self, command, message=None):
"""Send request with an optional message.
:param command: command to send
:type command: str
:param message: message (optional)
:type message: str
:return: command result
:rtype: dict
"""
if message is not None:
message = Message.serialize(message)
packet = Packet.request(command, message)
response = self._communicate(packet)
if response.response_type != Packet.CMD_RESPONSE:
raise SessionException(
"Unexpected response type {type}, "
"expected '{response}' (CMD_RESPONSE)".format(
type=response.response_type,
response=Packet.CMD_RESPONSE
)
)
command_response = Message.deserialize(response.payload)
if "success" in command_response:
if command_response["success"] != b"yes":
raise CommandException(
"Command failed: {errmsg}".format(
errmsg=command_response["errmsg"]
)
)
return command_response
def streamed_request(self, command, event_stream_type, message=None):
"""Send command request and collect and return all emitted events.
:param command: command to send
:type command: str
:param event_stream_type: event type emitted on command execution
:type event_stream_type: str
:param message: message (optional)
:type message: str
:return: generator for streamed event responses as dict
:rtype: generator
"""
if message is not None:
message = Message.serialize(message)
# subscribe to event stream
packet = Packet.register_event(event_stream_type)
response = self._communicate(packet)
if response.response_type != Packet.EVENT_CONFIRM:
raise SessionException(
"Unexpected response type {type}, "
"expected '{confirm}' (EVENT_CONFIRM)".format(
type=response.response_type,
confirm=Packet.EVENT_CONFIRM,
)
)
# issue command, and read any event messages
packet = Packet.request(command, message)
self.transport.send(packet)
exited = False
while True:
response = Packet.parse(self.transport.receive())
if response.response_type == Packet.EVENT:
if not exited:
try:
yield Message.deserialize(response.payload)
except GeneratorExit:
exited = True
pass
else:
break
if response.response_type == Packet.CMD_RESPONSE:
command_response = Message.deserialize(response.payload)
else:
raise SessionException(
"Unexpected response type {type}, "
"expected '{response}' (CMD_RESPONSE)".format(
type=response.response_type,
response=Packet.CMD_RESPONSE
)
)
# unsubscribe from event stream
packet = Packet.unregister_event(event_stream_type)
response = self._communicate(packet)
if response.response_type != Packet.EVENT_CONFIRM:
raise SessionException(
"Unexpected response type {type}, "
"expected '{confirm}' (EVENT_CONFIRM)".format(
type=response.response_type,
confirm=Packet.EVENT_CONFIRM,
)
)
# evaluate command result, if any
if "success" in command_response:
if command_response["success"] != b"yes":
raise CommandException(
"Command failed: {errmsg}".format(
errmsg=command_response["errmsg"]
)
)
@@ -0,0 +1,144 @@
import pytest
from ..protocol import Packet, Message, FiniteStream
from ..exception import DeserializationException
class TestPacket(object):
# test data definitions for outgoing packet types
cmd_request = b"\x00\x0c" b"command_type"
cmd_request_msg = b"\x00\x07" b"command" b"payload"
event_register = b"\x03\x0a" b"event_type"
event_unregister = b"\x04\x0a" b"event_type"
# test data definitions for incoming packet types
cmd_response = b"\x01" b"reply"
cmd_unknown = b"\x02"
event_confirm = b"\x05"
event_unknown = b"\x06"
event = b"\x07\x03" b"log" b"message"
def test_request(self):
assert Packet.request("command_type") == self.cmd_request
assert Packet.request("command", b"payload") == self.cmd_request_msg
def test_register_event(self):
assert Packet.register_event("event_type") == self.event_register
def test_unregister_event(self):
assert Packet.unregister_event("event_type") == self.event_unregister
def test_parse(self):
parsed_cmd_response = Packet.parse(self.cmd_response)
assert parsed_cmd_response.response_type == Packet.CMD_RESPONSE
assert parsed_cmd_response.payload.getvalue() == self.cmd_response
parsed_cmd_unknown = Packet.parse(self.cmd_unknown)
assert parsed_cmd_unknown.response_type == Packet.CMD_UNKNOWN
assert parsed_cmd_unknown.payload.getvalue() == self.cmd_unknown
parsed_event_confirm = Packet.parse(self.event_confirm)
assert parsed_event_confirm.response_type == Packet.EVENT_CONFIRM
assert parsed_event_confirm.payload.getvalue() == self.event_confirm
parsed_event_unknown = Packet.parse(self.event_unknown)
assert parsed_event_unknown.response_type == Packet.EVENT_UNKNOWN
assert parsed_event_unknown.payload.getvalue() == self.event_unknown
parsed_event = Packet.parse(self.event)
assert parsed_event.response_type == Packet.EVENT
assert parsed_event.payload.getvalue() == self.event
class TestMessage(object):
"""Message (de)serialization test."""
# data definitions for test of de(serialization)
# serialized messages holding a section
ser_sec_unclosed = b"\x01\x08unclosed"
ser_sec_single = b"\x01\x07section\x02"
ser_sec_nested = b"\x01\x05outer\x01\x0asubsection\x02\x02"
# serialized messages holding a list
ser_list_invalid = b"\x04\x07invalid\x05\x00\x02e1\x02\x03sec\x06"
ser_list_0_item = b"\x04\x05empty\x06"
ser_list_1_item = b"\x04\x01l\x05\x00\x02e1\x06"
ser_list_2_item = b"\x04\x01l\x05\x00\x02e1\x05\x00\x02e2\x06"
# serialized messages with key value pairs
ser_kv_pair = b"\x03\x03key\x00\x05value"
ser_kv_zero = b"\x03\x0azerolength\x00\x00"
# deserialized messages holding a section
des_sec_single = { "section": {} }
des_sec_nested = { "outer": { "subsection": {} } }
# deserialized messages holding a list
des_list_0_item = { "empty": [] }
des_list_1_item = { "l": [ b"e1" ] }
des_list_2_item = { "l": [ b"e1", b"e2" ] }
# deserialized messages with key value pairs
des_kv_pair = { "key": b"value" }
des_kv_zero = { "zerolength": b"" }
def test_section_serialization(self):
assert Message.serialize(self.des_sec_single) == self.ser_sec_single
assert Message.serialize(self.des_sec_nested) == self.ser_sec_nested
def test_list_serialization(self):
assert Message.serialize(self.des_list_0_item) == self.ser_list_0_item
assert Message.serialize(self.des_list_1_item) == self.ser_list_1_item
assert Message.serialize(self.des_list_2_item) == self.ser_list_2_item
def test_key_serialization(self):
assert Message.serialize(self.des_kv_pair) == self.ser_kv_pair
assert Message.serialize(self.des_kv_zero) == self.ser_kv_zero
def test_section_deserialization(self):
single = Message.deserialize(FiniteStream(self.ser_sec_single))
nested = Message.deserialize(FiniteStream(self.ser_sec_nested))
assert single == self.des_sec_single
assert nested == self.des_sec_nested
with pytest.raises(DeserializationException):
Message.deserialize(FiniteStream(self.ser_sec_unclosed))
def test_list_deserialization(self):
l0 = Message.deserialize(FiniteStream(self.ser_list_0_item))
l1 = Message.deserialize(FiniteStream(self.ser_list_1_item))
l2 = Message.deserialize(FiniteStream(self.ser_list_2_item))
assert l0 == self.des_list_0_item
assert l1 == self.des_list_1_item
assert l2 == self.des_list_2_item
with pytest.raises(DeserializationException):
Message.deserialize(FiniteStream(self.ser_list_invalid))
def test_key_deserialization(self):
pair = Message.deserialize(FiniteStream(self.ser_kv_pair))
zerolength = Message.deserialize(FiniteStream(self.ser_kv_zero))
assert pair == self.des_kv_pair
assert zerolength == self.des_kv_zero
def test_roundtrip(self):
message = {
"key1": "value1",
"section1": {
"sub-section": {
"key2": b"value2",
},
"list1": [ "item1", "item2" ],
},
}
serialized_message = FiniteStream(Message.serialize(message))
deserialized_message = Message.deserialize(serialized_message)
# ensure that list items and key values remain as undecoded bytes
deserialized_section = deserialized_message["section1"]
assert deserialized_message["key1"] == b"value1"
assert deserialized_section["sub-section"]["key2"] == b"value2"
assert deserialized_section["list1"] == [ b"item1", b"item2" ]
+1 -1
View File
@@ -5,7 +5,7 @@ vici.gemspec: $(srcdir)/vici.gemspec.in
-e "s:@GEM_VERSION@:$(PACKAGE_VERSION):" \
$(srcdir)/vici.gemspec.in > $@
vici-$(PACKAGE_VERSION).gem: vici.gemspec
vici-$(PACKAGE_VERSION).gem: vici.gemspec $(EXTRA_DIST)
(cd $(srcdir); $(GEM) build $(abs_builddir)/vici.gemspec)
[ "$(srcdir)" = "$(builddir)" ] || \
mv $(srcdir)/vici-$(PACKAGE_VERSION).gem $(builddir)
+4 -1
View File
@@ -390,7 +390,10 @@ module Vici
# during encoding.
class Connection
def initialize(socket)
def initialize(socket = nil)
if socket == nil
socket = UNIXSocket.new("/var/run/charon.vici")
end
@transp = Transport.new(socket)
end
+44 -21
View File
@@ -84,6 +84,8 @@ METHOD(vici_builder_t, add, void,
if (value.len > 0xffff)
{
DBG1(DBG_ENC, "vici value exceeds size limit (%zu > %u)",
value.len, 0xffff);
this->error++;
return;
}
@@ -125,22 +127,56 @@ METHOD(vici_builder_t, add, void,
}
}
METHOD(vici_builder_t, vadd_kv, void,
private_vici_builder_t *this, char *key, char *fmt, va_list args)
/**
* Add a list item or a key/value, if key given
*/
static void vadd_kv_or_li(private_vici_builder_t *this, char *key,
char *fmt, va_list args)
{
char buf[2048];
u_char buf[512];
chunk_t value;
ssize_t len;
va_list copy;
len = vsnprintf(buf, sizeof(buf), fmt, args);
if (len < 0 || len >= sizeof(buf))
va_copy(copy, args);
len = vsnprintf(buf, sizeof(buf), fmt, copy);
va_end(copy);
if (len >= sizeof(buf))
{
DBG1(DBG_ENC, "vici builder format buffer exceeds limit");
value = chunk_alloc(len + 1);
len = vsnprintf(value.ptr, value.len, fmt, args);
}
else
{
value = chunk_create(buf, len);
}
if (len < 0)
{
DBG1(DBG_ENC, "vici builder format print failed");
this->error++;
}
else
{
add(this, VICI_KEY_VALUE, key, chunk_create(buf, len));
if (key)
{
add(this, VICI_KEY_VALUE, key, value);
}
else
{
add(this, VICI_LIST_ITEM, value);
}
}
if (value.ptr != buf)
{
free(value.ptr);
}
}
METHOD(vici_builder_t, vadd_kv, void,
private_vici_builder_t *this, char *key, char *fmt, va_list args)
{
vadd_kv_or_li(this, key, fmt, args);
}
METHOD(vici_builder_t, add_kv, void,
@@ -153,23 +189,10 @@ METHOD(vici_builder_t, add_kv, void,
va_end(args);
}
METHOD(vici_builder_t, vadd_li, void,
private_vici_builder_t *this, char *fmt, va_list args)
{
char buf[2048];
ssize_t len;
len = vsnprintf(buf, sizeof(buf), fmt, args);
if (len < 0 || len >= sizeof(buf))
{
DBG1(DBG_ENC, "vici builder format buffer exceeds limit");
this->error++;
}
else
{
add(this, VICI_LIST_ITEM, chunk_create(buf, len));
}
vadd_kv_or_li(this, NULL, fmt, args);
}
METHOD(vici_builder_t, add_li, void,
+4 -1
View File
@@ -508,11 +508,14 @@ static void build_auth_cfgs(peer_cfg_t *peer_cfg, bool local, vici_builder_t *b)
certificate_t *cert;
char *str;
} v;
char buf[32];
int i = 0;
enumerator = peer_cfg->create_auth_cfg_enumerator(peer_cfg, local);
while (enumerator->enumerate(enumerator, &auth))
{
b->begin_section(b, local ? "local" : "remote");
snprintf(buf, sizeof(buf), "%s-%d", local ? "local" : "remote", ++i);
b->begin_section(b, buf);
rules = auth->create_enumerator(auth);
while (rules->enumerate(rules, &rule, &v))
+3 -2
View File
@@ -103,7 +103,7 @@ CALLBACK(conn_sn, int,
{
return vici_parse_cb(res, children_sn, NULL, NULL, NULL);
}
if (streq(name, "local") || streq(name, "remote"))
if (strpfx(name, "local") || strpfx(name, "remote"))
{
hashtable_t *auth;
@@ -112,7 +112,8 @@ CALLBACK(conn_sn, int,
if (ret == 0)
{
printf(" %s %s authentication:\n",
name, auth->get(auth, "class") ?: "unspecified");
strpfx(name, "local") ? "local" : "remote",
auth->get(auth, "class") ?: "unspecified");
if (auth->get(auth, "id"))
{
printf(" id: %s\n", auth->get(auth, "id"));