diff --git a/.gitignore b/.gitignore index 42b1ccfcbb44930aebeb004ddf4209287bcf1b28..30e654056e43e9e03e588644949da56f82dde611 100644 --- a/.gitignore +++ b/.gitignore @@ -1,67 +1,55 @@ -## .gitignore - -# The rest of autoconf & results - -config.h -config.h.in~ +# Outputs +*.so *.o *.a -*.so -*.la *.lo -sentinel-proxy -sentinel-device-token -libsentinel-device-token +*.pc +.libs +sentinel-device-token +sentinel-proxy +libsentinel-device-token.la +sentinel-proxy-*.tar.gz +sentinel-proxy-*.tar.xz +sentinel-proxy-*.zip + +# Test outputs +/tests/unit/unittests +test-suite*.log +tests/*/*.log +tests/*/*.trs + +# Coverage output +*.gcno +*.gcda +/sentinel-proxy-*-coverage* + +## Autotools +/.m4 +/.aux # http://www.gnu.org/software/automake - +Makefile Makefile.in -/ar-lib -/mdate-sh -/py-compile -/test-driver -/ylwrap -.deps/ - +.deps +.dirstamp # http://www.gnu.org/software/autoconf - autom4te.cache /autoscan.log /autoscan-*.log /aclocal.m4 -/compile -/config.guess -/config.h.in /config.log /config.status -/config.sub /configure /configure.scan -/depcomp -/install-sh -/missing -/stamp-h1 - +/aminclude_static.am # https://www.gnu.org/software/libtool/ - -/libtool /ltmain.sh -/.libs - +/libtool # http://www.gnu.org/software/texinfo - /texinfo.tex -# http://www.gnu.org/software/m4/ - -m4/libtool.m4 -m4/ltoptions.m4 -m4/ltsugar.m4 -m4/ltversion.m4 -m4/lt~obsolete.m4 - -# Generated Makefile -# (meta build system like autotools, -# can automatically generate from config.status script -# (which is called by configure script)) -Makefile +# test keys and certificates +tests/manual/ca1_certs/ +tests/manual/ca2_certs/ +tests/manual/keys/ +tests/manual/self_sign_certs/ diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000000000000000000000000000000000000..43b347fa9368828309118470127d669cc544a862 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,103 @@ +image: registry.nic.cz/turris/sentinel/proxy + +stages: + - build + - test + - coverage + - release + +## Build stage ################################################################### +build: + stage: build + script: + - ./bootstrap + - ./configure + - make + artifacts: + when: on_failure + expire_in: 1 week + paths: + - config.log + +dist: + stage: build + script: + - ./bootstrap + - ./configure + - make dist + artifacts: + expire_in: 1 month + paths: + - sentinel-proxy-*.tar.gz + - sentinel-proxy-*.tar.xz + - sentinel-proxy-*.zip + +## Test stage #################################################################### +.test: + stage: test + needs: [build] + before_script: + - ./bootstrap + - ./configure --enable-tests --enable-valgrind --enable-linters + +.test-check: + extends: .test + artifacts: + when: on_failure + expire_in: 1 week + paths: + - 'test-suite*.log' + - 'tests/*/*.log' + - 'tests/*/*.trs' + +check: + extends: .test-check + script: + - make check + +distcheck: + extends: .test-check + script: + - make distcheck + +valgrind: + extends: .test-check + script: + - make check-valgrind-memcheck + +lint: + allow_failure: true + extends: .test + script: + - make lint + +## Code Coverage stage ########################################################### +coverage: + stage: coverage + needs: [check] + script: + - ./bootstrap + - ./configure --enable-tests --enable-code-coverage + - make check-code-coverage V=1 + - mv sentinel-proxy-*-coverage sentinel-proxy-coverage + coverage: '/lines\.\.\.\.\.\.: (\d+.\d+%)/' + artifacts: + expire_in: 1 month + expose_as: 'Code Coverage' + paths: + - 'sentinel-proxy-coverage/' + +## Release creation ############################################################## +release: + stage: release + image: registry.gitlab.com/gitlab-org/release-cli:latest + rules: + - if: '$CI_COMMIT_TAG' + needs: + - job: dist + artifacts: true + before_script: + - apk update + - apk add bash curl + script: + - .gitlab-ci/release.sh diff --git a/.gitlab-ci/Dockerfile b/.gitlab-ci/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..60d65b3f8727b038670bf491228da7e1618ab876 --- /dev/null +++ b/.gitlab-ci/Dockerfile @@ -0,0 +1,41 @@ +# Wee need unstable for now because of check version +FROM debian:stable + +ENV HOME /root + +RUN \ + apt-get update && \ + apt-get -y upgrade && \ + apt-get -y install --no-install-recommends \ + autoconf autoconf-archive automake libtool cmake \ + make pkg-config gcc gperf \ + git ca-certificates \ + libssl-dev zlib1g-dev libczmq-dev libconfig-dev libmsgpack-dev \ + check cppcheck valgrind lcov \ + zip xz-utils \ + && \ + apt-get clean + +# Compile paho-mqtt +RUN \ + git clone --branch "v1.3.9" https://github.com/eclipse/paho.mqtt.c.git && \ + cd paho.mqtt.c && \ + cmake -Bbuild -H. -DPAHO_WITH_SSL=ON && \ + cmake --build build/ --target install && ldconfig && \ + cd .. && \ + rm -rf paho.mqtt.c + +# Compile logc +RUN \ + git clone --branch "v0.2.1" "https://gitlab.nic.cz/turris/logc.git" && \ + cd logc && \ + ./bootstrap && ./configure --prefix=/usr && \ + make install && \ + cd .. && \ + rm -rf logc + + + +CMD [ "bash" ] + +# vim: ft=dockerfile diff --git a/.gitlab-ci/release.sh b/.gitlab-ci/release.sh new file mode 100755 index 0000000000000000000000000000000000000000..dfddf8b8175debc8e3574137fd74fa90a0d22f87 --- /dev/null +++ b/.gitlab-ci/release.sh @@ -0,0 +1,32 @@ +#!/bin/bash +set -eu + +VERSION="$(echo "${CI_COMMIT_TAG}" | sed -nE 's/v([0-9]+)\.([0-9]+)\.([0-9]+).*/\1.\2.\3/p')" +CHANGELOG="$(awk ' + BEGIN { + flag = 0 + } + /^## / { + if (!flag) { + flag = 1 + next + } else + exit + } + flag { + print + } + ' CHANGELOG.md)" + +declare -a args +for dist in sentinel-proxy-*.tar.gz sentinel-proxy-*.tar.xz sentinel-proxy-*.zip; do + URL="${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/generic/sentinel-proxy/${VERSION}/${dist}" + curl --header "JOB-TOKEN: ${CI_JOB_TOKEN}" --upload-file "${dist}" "${URL}" + args+=("--assets-link" "{\"name\":\"${dist}\",\"url\":\"${URL}\"}") +done + +release-cli create \ + --name "Release ${CI_COMMIT_TAG#v}" \ + --tag-name "$CI_COMMIT_TAG" \ + --description "$CHANGELOG" \ + "${args[@]}" diff --git a/CHANGELOG.md b/CHANGELOG.md index a3fc6ae1563f194a7bb52d14403da92207219cf8..a2a795972de99eefad9636c0ac91088868116c27 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,23 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased +### Added +- logc - logging support +- connected ZMQ peers monitoring +- check - unit tests +- cppcheck - linting +- lcov - code coverage +- valgrind +- Gitlab CI +- pkg-config and version scripts for device token library +- more fields in configuration file - it can hold the whole config now +- manual overall tests + +### Changed +- split server CLI option to server and port options +- CLI options - names, arguments and their descriptions updated + ## [1.4] - 2021-05-31 ### Added - Sending status messages to server diff --git a/Makefile.am b/Makefile.am index 26f3b41fd2d09f319f93ac0b26d53986690a48ee..58ba8f4be22cdfbc492a646d3399d75cd877d5a0 100644 --- a/Makefile.am +++ b/Makefile.am @@ -1,39 +1,19 @@ -lib_LTLIBRARIES = \ - libsentinel-device-token.la -libsentinel_device_token_la_LDFLAGS = -version-info 1:0:0 +bin_PROGRAMS = +lib_LTLIBRARIES = +pkgconfig_DATA = +EXTRA_DIST = +linted_files = -libsentineldir = $(includedir)/sentinel -libsentinel_HEADERS = \ - device_token.h +include proxy/Makefile.am +include device-token/Makefile.am -bin_PROGRAMS = \ - sentinel-proxy \ - sentinel-device-token -sentinel_proxy_SOURCES = \ - sentinel_proxy.c \ - proxy_conf.c -sentinel_device_token_SOURCES = \ - sentinel_device_token.c -libsentinel_device_token_la_SOURCES = \ - device_token.c +if ENABLE_LINTERS +include lint.am +endif -sentinel_proxy_CFLAGS = \ - $(LIBCRYPTO_CFLAGS) \ - $(ZLIB_CFLAGS) \ - $(LIBCZMQ_CFLAGFS) \ - $(LIBCONFIG_CFLAGS) -sentinel_proxy_LDFLAGS = \ - $(LIBCRYPTO_LIBS) \ - $(ZLIB_LIBS) \ - $(LIBCZMQ_LIBS) \ - $(LIBCONFIG_LIBS) -sentinel_proxy_LDADD = \ - libsentinel-device-token.la -sentinel_device_token_CFLAGS = \ - $(ZLIB_CFLAGS) -sentinel_device_token_LDFLAGS = \ - $(ZLIB_LIBS) -sentinel_device_token_LDADD = \ - libsentinel-device-token.la +if ENABLE_TESTS +DISTCHECK_CONFIGURE_FLAGS = --enable-tests +include tests/Makefile.am +endif diff --git a/README.md b/README.md index 60942411d78066377b265b78a0ae59fba4710212..a1adcbf199d982e48c4fa8c097b0a70b8d102d71 100644 --- a/README.md +++ b/README.md @@ -1,41 +1,106 @@ # Sentinel-proxy -Sentinel-proxy sends messages received over ZMQ to Sentinel server (over MQTT). +It relays messages received over ZMQ to Sentinel server over MQTT. ## Dependencies - - [czmq](https://github.com/zeromq/czmq) - - [libz](https://github.com/madler/zlib) - [libcrypto](https://github.com/openssl/openssl) - - [libpaho-mqtt](https://github.com/eclipse/paho.mqtt.c) + - [libz](https://github.com/madler/zlib) + - [czmq](https://github.com/zeromq/czmq) - [libconfig](https://github.com/hyperrealm/libconfig) + - [libpaho-mqtt](https://github.com/eclipse/paho.mqtt.c) + - [msgpack](https://github.com/msgpack/msgpack-c) + - [logc](https://gitlab.nic.cz/turris/logc) + - On non-glibc [argp-standalone](http://www.lysator.liu.se/~nisse/misc) -## Build dependencies - - - autotools +For build: + - [autotools](https://www.gnu.org/software/automake/manual/html_node/Autotools-Introduction.html) + - [autoconf-archive](https://www.gnu.org/software/autoconf-archive/Introduction.html) - [libtool](https://www.gnu.org/software/libtool/) - [pkg-config](https://www.freedesktop.org/wiki/Software/pkg-config/) +For tests: + - [check](https://libcheck.github.io/check) + - Optionally [valgrind](http://www.valgrind.org) + +For linting: + - [cppcheck](https://github.com/danmar/cppcheck) + +For code coverage: + - [lcov](http://ltp.sourceforge.net/coverage/lcov.php) + ## Build instructions for Autotools ``` -./bootstrap -./cofigure +./configure make -# optionally: -make install ``` - Use `./configure --help` to see all configure options -## Run +Subsequent installation can be done with `make install`. + +When you do not use distribution archive then you have to run initially +``` +./bootstrap +``` + +## Running tests + +### Unit + +Unit tests are in directory `tests/unit` . To run all tests you can just simply run: +`make check` + +You can also run tests with Valgrind: +`make check-valgrind` + +To run checks with just one specific Valgrind test such as memtest you can run: +`make check-valgrind-memcheck` + +Source code of project can be also linted with cppcheck by running: +`make lint` + +There is also possibility to generate code coverage for test cases. To do so you +can run: +`make check-code-coverage` + +### Manual +For overall not yet automated tests go to `test/manual` and see `README.md`. + +## Running and configuration ``` -sentinel_proxy [-S server] [-s local_socket] [--ca CA_file] [--cert cert_file] \ -[--key key_file] [--token device_token] [--config config_file] +sentinel-proxy [--ca-cert=ca_cert_file] [--cl-cert=client_cert_file] +[--config=config_file] [--cl-key=client_key_file] [--port=port] +[--zmq-sock=zmq_socket_path] [--server=server] +[--token=device_token] ``` -All parameters except device token have default values. These values can be -overridden by values loaded from config file and subsequently by cli options. -Config file absence is silently ignored until it is explicitly passed as a -cli option. In such a case it's absence would end up in error state. +All parameters except device token have default values. +These values can be overridden by values loaded from config file and CLI options. +Device token can be only specified either by CLI option or in configuration file. +For information about device token and how to generate it please see next section. +The priorities of configuration are following: +CLI options > configuration file > default values. +CLI options have higher priority than conf. file, which has in turn higher +priority than default configuration. + +Configuration file absence is silently ignored until it is explicitly passed as +a CLI option. In such a case it's absence would end up in error state. + +# Sentinel-device-token + +It is a library and a small CLI utility which purpose is to generate and validate +device token. Device token is 64 hex character long string used to uniquely +and anonymously identify a user of Sentinel for purposes of following provided +services. + +To generate a new device token and print it to standard output run: +``` +sentinel-device-token -c +``` + +To validate a device token run: +``` +sentinel-device-token -v paste_here_your_token +``` diff --git a/bootstrap b/bootstrap index 7ef2be9a76dbf083dd117b5db01391b7b1ff7c76..e2c49681bf59706ed0c191f82e3ec34839afed72 100755 --- a/bootstrap +++ b/bootstrap @@ -1,2 +1,10 @@ -#! /bin/sh +#!/bin/sh + +# Directory used for m4 macros and auxiliary scripts of autoconf +mkdir -p .m4 .aux + +# On old versions of autoconf-archive this file is not present so fake it +[ -f aminclude_static.am ] || echo "# dummy" > aminclude_static.am + +# And finally autoreconf autoreconf --install --symlink diff --git a/configure.ac b/configure.ac index ef9bd35ce4b9faa11393e12fab239bbaa1aa4927..d5650b3b275fb9eed70bc6d0ef236938813f7cfc 100644 --- a/configure.ac +++ b/configure.ac @@ -1,55 +1,100 @@ -AC_INIT([sentinel-proxy], [1.4], []) +AC_INIT([sentinel-proxy], [1.4], [tech.support@turris.cz]) +AC_CONFIG_MACRO_DIRS([.m4]) +AC_CONFIG_AUX_DIR([.aux]) + +AM_INIT_AUTOMAKE([ + foreign silent-rules subdir-objects + -Wall -Wno-portability + dist-zip dist-xz +]) +AM_SILENT_RULES([yes]) -AM_INIT_AUTOMAKE([foreign -Wall -Wno-portability]) AC_PROG_CC +AC_GNU_SOURCE AM_PROG_AR LT_INIT +PKG_INSTALLDIR + +AX_CHECK_COMPILE_FLAG([-std=c11], , AC_MSG_ERROR([Compiler with C11 standard support is required])) +AX_APPEND_FLAG([-std=c11]) -PKG_CHECK_MODULES([LIBCRYPTO], [libcrypto]) -PKG_CHECK_MODULES([ZLIB], [zlib]) -PKG_CHECK_MODULES([LIBCZMQ], [libczmq]) -PKG_CHECK_MODULES([LIBCONFIG], [libconfig]) +PKG_CHECK_MODULES([libcrypto], [libcrypto]) +PKG_CHECK_MODULES([zlib], [zlib]) +PKG_CHECK_MODULES([libczmq], [libczmq]) +PKG_CHECK_MODULES([libconfig], [libconfig]) PKG_CHECK_MODULES([msgpack], [msgpack]) +PKG_CHECK_MODULES([logc], [logc >= 0.2.0 logc_argp]) +PKG_CHECK_MODULES([openssl], [openssl]) AC_CHECK_LIB(paho-mqtt3cs, MQTTClient_create, , AC_MSG_ERROR([Cannot find libpaho-mqtt])) - -dnl Check if we have argp available from our libc otherwise check for standalone version AC_LINK_IFELSE( - [AC_LANG_PROGRAM([#include ], [argp_parse(0,1,NULL,0,0,0);])],, - AC_CHECK_LIB([argp], [argp_parse], , [echo "Cannot find libargp"; exit -1]) + [AC_LANG_PROGRAM([#include ],[argp_parse(0,1,NULL,0,0,0);])],, + [argp_standalone_required="yes"] ) +AS_IF([test "$argp_standalone_required" = "yes"], + AC_CHECK_LIB([argp], [argp_parse], , AC_MSG_ERROR([Unable to find libargp]))) + -# set default options -AC_ARG_VAR([defconfig], [default path to the configuration file]) -AS_IF([test -z "${defconfig}"], [defconfig="/tmp/etc/sentinel-proxy.cfg"]) -AC_DEFINE_UNQUOTED([DEFAULT_CONFIG_FILE], ["${defconfig}"], - [Default path to the configuration file]) +dnl Options from environment +AC_ARG_VAR([defconfigfile], [default path to the configuration file]) +AS_IF([test -z "${defconfigfile}"], + [defconfigfile="/tmp/etc/sentinel-proxy.cfg"]) +AC_DEFINE_UNQUOTED([DEFAULT_CONFIG_FILE], ["${defconfigfile}"], + [Default path to the configuration file]) -AC_ARG_VAR([defserver], [default Sentinel server]) -AS_IF([test -z "${defserver}"], [defserver="ssl://sentinel.turris.cz:1883"]) +AC_ARG_VAR([defserver], [default Sentinel server address]) +AS_IF([test -z "${defserver}"], [defserver="sentinel.turris.cz"]) AC_DEFINE_UNQUOTED([DEFAULT_SERVER], ["${defserver}"], - [Default Sentinel server]) - -AC_ARG_VAR([defsocket], [default path to local socket]) -AS_IF([test -z "${defsocket}"], [defsocket="ipc:///tmp/sentinel_pull.sock"]) -AC_DEFINE_UNQUOTED([DEFAULT_LOCAL_SOCKET], ["${defsocket}"], - [Default path to local socket]) - -AC_ARG_VAR([defca], [default path to CA certificate file]) -AS_IF([test -z "${defca}"], [defca="/etc/sentinel/ca.pem"]) -AC_DEFINE_UNQUOTED([DEFAULT_CA_FILE], ["${defca}"], - [Default path to CA certificate file]) - -AC_ARG_VAR([defcert], [default path to client certificate file]) -AS_IF([test -z "${defcert}"], [defcert="/etc/sentinel/mqtt_cert.pem"]) -AC_DEFINE_UNQUOTED([DEFAULT_CERT_FILE], ["${defcert}"], - [Default path to client certificate file]) - -AC_ARG_VAR([defkey], [default path to client key file]) -AS_IF([test -z "${defkey}"], [defkey="/etc/sentinel/mqtt_key.pem"]) -AC_DEFINE_UNQUOTED([DEFAULT_KEY_FILE], ["${defkey}"], - [Default path to client key file]) - -AC_CONFIG_HEADERS([config.h]) -AC_CONFIG_FILES([Makefile]) + [Default Sentinel server adress]) + +AC_ARG_VAR([defport], [default Sentinel server port]) +AS_IF([test -z "${defport}"], [defport=1883]) +AC_DEFINE_UNQUOTED([DEFAULT_PORT], [${defport}], + [Default Sentinel server port]) + +AC_ARG_VAR([defzmqsockpath], [default path to ZMQ socket]) +AS_IF([test -z "${defzmqsockpath}"], + [defzmqsockpath="ipc:///tmp/sentinel_pull.sock"]) +AC_DEFINE_UNQUOTED([DEFAULT_ZMQ_SOCK_PATH], ["${defzmqsockpath}"], + [Default path to local socket]) + +AC_ARG_VAR([defcacertfile], [default path to CA certificate file]) +AS_IF([test -z "${defcacertfile}"], [defcacertfile="/etc/sentinel/ca.pem"]) +AC_DEFINE_UNQUOTED([DEFAULT_CA_CERT_FILE], ["${defcacertfile}"], + [Default path to CA certificate file]) + +AC_ARG_VAR([defmqttclientcertfile], [default path to MQTT client certificate file]) +AS_IF([test -z "${defmqttclientcertfile}"], + [defmqttclientcertfile="/etc/sentinel/mqtt_cert.pem"]) +AC_DEFINE_UNQUOTED([DEFAULT_MQTT_CLIENT_CERT_FILE], ["${defmqttclientcertfile}"], + [Default path to client certificate file]) + +AC_ARG_VAR([defmqttclientkeyfile], [default path to client key file]) +AS_IF([test -z "${defmqttclientkeyfile}"], + [defmqttclientkeyfile="/etc/sentinel/mqtt_key.pem"]) +AC_DEFINE_UNQUOTED([DEFAULT_MQTT_CLIENT_KEY_FILE], ["${defmqttclientkeyfile}"], + [Default path to client key file]) + + +dnl Tests +AC_ARG_ENABLE([tests], AC_HELP_STRING([--enable-tests], [Whether to enable unit tests])) +AM_CONDITIONAL([ENABLE_TESTS], [test "x$enable_tests" = "xyes"]) +AS_IF([test "x$enable_tests" = "xyes"], [ + PKG_CHECK_MODULES([check], [check >= 0.11]) + AC_REQUIRE_AUX_FILE([tap-driver.sh]) +]) +AX_VALGRIND_CHECK + +dnl Linters +AC_ARG_ENABLE([linters], AC_HELP_STRING([--enable-linters], [Whether to enable code linting support (cppcheck)])) +AM_CONDITIONAL([ENABLE_LINTERS], [test "x$enable_linters" = "xyes"]) +AS_IF([test "x$enable_linters" = "xyes"], [ + AC_PATH_PROG([CPPCHECK], [cppcheck]) + AS_IF([test -z "$CPPCHECK" ], AC_MSG_ERROR([Missing linter cppcheck])) +]) + +dnl Coverage +AX_CODE_COVERAGE + +AC_CONFIG_FILES([Makefile device-token/libsentinel-device-token.pc]) AC_OUTPUT diff --git a/device-token/Makefile.am b/device-token/Makefile.am new file mode 100644 index 0000000000000000000000000000000000000000..c1aa79fa560fb42ab80bb22addd704b0922e3908 --- /dev/null +++ b/device-token/Makefile.am @@ -0,0 +1,35 @@ +bin_PROGRAMS += sentinel-device-token +lib_LTLIBRARIES += libsentinel-device-token.la +pkgconfig_DATA += %reldir%/libsentinel-device-token.pc +EXTRA_DIST += %reldir%/libsentinel-device-token.version + + +sentinel_device_token_SOURCES = \ + %reldir%/sentinel_device_token.c +sentinel_device_token_CFLAGS = \ + -isystem "$(srcdir)/include" +sentinel_device_token_LDADD = \ + libsentinel-device-token.la + + +libsentineldir = $(includedir)/sentinel +libsentinel_HEADERS = \ + include/device_token.h +libsentinel_device_token_la_SOURCES = \ + %reldir%/device_token.c +libsentinel_device_token_la_CFLAGS = \ + -isystem "$(srcdir)/include" \ + ${libcrypto_CFLAGS} \ + ${zlib_CFLAGS} \ + ${CODE_COVERAGE_CFLAGS} +libsentinel_device_token_la_LDFLAGS = \ + ${libcrypto_LIBS} \ + ${zlib_LIBS} \ + ${CODE_COVERAGE_LIBS} \ + -Wl,--version-script='$(srcdir)/%reldir%/libsentinel-device-token.version' \ + -version-info 1:0:0 + + +linted_files += \ + ${sentinel_device_token_SOURCES} \ + ${libsentinel_device_token_la_SOURCES} diff --git a/device_token.c b/device-token/device_token.c similarity index 99% rename from device_token.c rename to device-token/device_token.c index 06d0145e5818d3131a45a0941b31e96a1708fc9a..6ee2255dc6a30aa68bd233a176170640dc1f7338 100644 --- a/device_token.c +++ b/device-token/device_token.c @@ -1,13 +1,11 @@ -#include "device_token.h" - #include #include #include #include #include - #include #include +#include #define CRC_BYTES 4 // Every byte of device token random data equals two hexadecimal characters diff --git a/device-token/libsentinel-device-token.pc.in b/device-token/libsentinel-device-token.pc.in new file mode 100644 index 0000000000000000000000000000000000000000..27b0021e477baa5d9e346dd94cce2921f5857f51 --- /dev/null +++ b/device-token/libsentinel-device-token.pc.in @@ -0,0 +1,10 @@ +prefix=@prefix@ +exec_prefix=@exec_prefix@ +libdir=@libdir@ +includedir=@includedir@ + +Name: libsentinel-device-token +Description: Turris Sentinel Device Token library +Version: @PACKAGE_VERSION@ +Libs: -L${libdir} -lsentinel-device-token +Cflags: -I${includedir} diff --git a/device-token/libsentinel-device-token.version b/device-token/libsentinel-device-token.version new file mode 100644 index 0000000000000000000000000000000000000000..25a7000429e59a346998ed22de6f6f6952b88ad0 --- /dev/null +++ b/device-token/libsentinel-device-token.version @@ -0,0 +1,8 @@ +V0.0 { + global: + device_token_state_msg; + device_token_generate; + device_token_verify; + + local: *; +}; diff --git a/sentinel_device_token.c b/device-token/sentinel_device_token.c similarity index 98% rename from sentinel_device_token.c rename to device-token/sentinel_device_token.c index 58e7cede2c034d1772c6c9cfcb4b75a46915883d..e8641b4a504ca0f845c78394d75fca679c111c4c 100644 --- a/sentinel_device_token.c +++ b/device-token/sentinel_device_token.c @@ -20,9 +20,7 @@ #include #include #include - -#include "config.h" -#include "device_token.h" +#include enum actions { NO_ACTION, diff --git a/example.cfg b/example.cfg index be52fcb33c4776e2da62c8bb2a8e1b5649ff46d2..13904573e51492426f7bebd614ffc8fcccde5fae 100644 --- a/example.cfg +++ b/example.cfg @@ -1,2 +1,9 @@ -# This is 64 characters long string to identify this device. -#device_token = "please_paste_here_your_token" +# This is 64 hex character long string for more information see README. +# Paste here your token instead." +# device_token = "c1da24cf9063bf444d66271eb6c2c1b2ae364697ce07d05a8a60d3df08c93f50" +# server = "sentinel.com" +# port = 12345 +# client_cert_file = "./sentinel-cert.pem" +# client_key_file = "./sentinel-key.pem" +# ca_cert_file = "./sentinel-ca-cert.pem" +# zmq_socket_path = "ipc:///tmp/sentinel.sock" diff --git a/device_token.h b/include/device_token.h similarity index 100% rename from device_token.h rename to include/device_token.h diff --git a/lint.am b/lint.am new file mode 100644 index 0000000000000000000000000000000000000000..f4e4761b43456f1a77106f8d5affcfa689d51d41 --- /dev/null +++ b/lint.am @@ -0,0 +1,11 @@ +lint_V = $(lint_V_@AM_V@) +lint_V_ = $(lint_V_@AM_DEFAULT_V@) +lint_V_0 = @echo " LINT " $@; + +lint: $(linted_files) + $(lint_V)$(CPPCHECK) --error-exitcode=1 \ + --enable=warning \ + --std=c11 \ + --template='{file}:{line},{severity},{id},{message}' \ + --inline-suppr \ + $^ diff --git a/proxy/Makefile.am b/proxy/Makefile.am new file mode 100644 index 0000000000000000000000000000000000000000..a73803272a3c3b482429409a07bf3e2b7827440c --- /dev/null +++ b/proxy/Makefile.am @@ -0,0 +1,39 @@ +bin_PROGRAMS += sentinel-proxy + + +proxy_sources = \ + %reldir%/common.h \ + %reldir%/con_peer_list.h \ + %reldir%/con_peer_list.c \ + %reldir%/log.h \ + %reldir%/log.c \ + %reldir%/proxy_conf.h \ + %reldir%/proxy_conf.c \ + %reldir%/proxy_mqtt.h \ + %reldir%/proxy_mqtt.c \ + %reldir%/proxy_zmq.h \ + %reldir%/proxy_zmq.c + +sentinel_proxy_SOURCES = \ + %reldir%/sentinel_proxy.c \ + ${proxy_sources} +sentinel_proxy_CFLAGS = \ + -isystem "$(srcdir)/include" \ + ${libcrypto_CFLAGS} \ + ${libczmq_CFLAGFS} \ + ${libconfig_CFLAGS} \ + ${msgpack_CFLAGS} \ + ${logc_CFLAGS} \ + ${CODE_COVERAGE_CFLAGS} +sentinel_proxy_LDFLAGS = \ + ${libconfig_LIBS} \ + ${libcrypto_LIBS} \ + ${libczmq_LIBS} \ + ${msgpack_LIBS} \ + ${logc_LIBS} \ + ${CODE_COVERAGE_LIBS} +sentinel_proxy_LDADD = \ + libsentinel-device-token.la + + +linted_files += ${sentinel_proxy_SOURCES} diff --git a/proxy/common.h b/proxy/common.h new file mode 100644 index 0000000000000000000000000000000000000000..d08bdf20b92b93420cec8ff396e8fd82b3e1a74c --- /dev/null +++ b/proxy/common.h @@ -0,0 +1,27 @@ +/* + * Turris:Sentinel Proxy - Main MQTT gateway to Sentinel infrastructure + * Copyright (C) 2018 - 2021 CZ.NIC z.s.p.o. (https://www.nic.cz/) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#ifndef __SENTINEL_PROXY_COMMON_H__ +#define __SENTINEL_PROXY_COMMON_H__ + +#include + +#define TOPIC_PREFIX "sentinel/collect/" +#define TOPIC_PREFIX_LEN strlen(TOPIC_PREFIX) +#define ZMQ_MAX_TOPIC_LEN 256 + +#endif /*__SENTINEL_PROXY_COMMON_H__*/ diff --git a/proxy/con_peer_list.c b/proxy/con_peer_list.c new file mode 100644 index 0000000000000000000000000000000000000000..c27e3486025e473dd7727aaf888b12367240838f --- /dev/null +++ b/proxy/con_peer_list.c @@ -0,0 +1,92 @@ +/* + * Turris:Sentinel Proxy - Main MQTT gateway to Sentinel infrastructure + * Copyright (C) 2018 - 2021 CZ.NIC z.s.p.o. (https://www.nic.cz/) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "con_peer_list.h" +#include "log.h" + +void init_peer(struct con_peer *p) { + TRACE_FUNC; + p->fd = -1; + p->topic = malloc(CON_PEER_TOPIC_MEM_LEN); + p->topic[0] = '\0'; +} + +void set_peer(struct con_peer *p, int fd, char *topic) { + TRACE_FUNC; + p->fd = fd; + strcpy(p->topic, topic); +} + +void destroy_peer(struct con_peer *p) { + TRACE_FUNC; + if (p) + free(p->topic); +} + +void init_con_peer_list(struct con_peer_list *list) { + TRACE_FUNC; + list->alloc_size = CON_PEER_LIST_DEFAULT_LEN; + list->peers = malloc(list->alloc_size * sizeof(*list->peers)); + for (size_t i = 0; i < list->alloc_size; i++) + init_peer(&list->peers[i]); +} + +void add_peer(struct con_peer_list *list, int fd, char *topic) { + TRACE_FUNC; + bool added = false; + for (size_t i = 0; i < list->alloc_size; i++) + if (list->peers[i].fd == -1) { + set_peer(&list->peers[i], fd, topic); + info("Connected peer with topic %s and session ID %d", topic, fd); + added = true; + break; + } + if (!added) { + size_t old_size = list->alloc_size; + list->alloc_size *= 2; + list->peers = realloc(list->peers, list->alloc_size + * sizeof(*list->peers)); + for(size_t i = old_size; i < list->alloc_size; i++) + init_peer(&list->peers[i]); + set_peer(&list->peers[old_size], fd, topic); + info("Connected peer with topic %s and session ID %d", topic, fd); + } + +} + +void del_peer(struct con_peer_list *list, int fd) { + TRACE_FUNC; + for(size_t i = 0; i < list->alloc_size; i++) { + if (list->peers[i].fd == fd) { + info("Disconnected peer with topic %s and session ID %d", + list->peers[i].topic, fd); + list->peers[i].fd = -1; + break; + } + } +} + +void destroy_con_peer_list(struct con_peer_list *list) { + TRACE_FUNC; + if(list) { + for(size_t i = 0; i < list->alloc_size; i++) + destroy_peer(&list->peers[i]); + free(list->peers); + list->peers = NULL; + } +} diff --git a/proxy/con_peer_list.h b/proxy/con_peer_list.h new file mode 100644 index 0000000000000000000000000000000000000000..b4e5656983c608e14ac94994820e955e04ed488c --- /dev/null +++ b/proxy/con_peer_list.h @@ -0,0 +1,72 @@ +/* + * Turris:Sentinel Proxy - Main MQTT gateway to Sentinel infrastructure + * Copyright (C) 2018 - 2021 CZ.NIC z.s.p.o. (https://www.nic.cz/) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#ifndef __SENTINEL_PROXY_CON_PEER_LIST_H__ +#define __SENTINEL_PROXY_CON_PEER_LIST_H__ + +#include "common.h" + +#define CON_PEER_TOPIC_MEM_LEN (ZMQ_MAX_TOPIC_LEN + 1) +#define CON_PEER_LIST_DEFAULT_LEN 4 + +struct con_peer { + int fd; + char *topic; +}; + +struct con_peer_list { + size_t alloc_size; + struct con_peer *peers; +}; + +// Initializes statically or dynamically allocated con_peer struct. +// fd is set to -1. topic stores ptr to memory allocated by malloc with exactly +// CON_PEER_TOPIC_MEM_LEN bytes. +void init_peer(struct con_peer *p) __attribute__((nonnull)); + +// Sets attributes of conn_peer struct. topic is copied by strcpy and MUST be +// NULL terminated C string. Caller is responsible that topic is NOT greater +// than memory allocated by init_peer(). +// NOTE: that is achived by fact that messages with topic longer than +// ZMQ_MAX_TOPIC_LEN are NOT allowed thus are never inserted in the list +void set_peer(struct con_peer *p, int fd, char *topic) +__attribute__((nonnull)); + +// If p is not NULL frees memory allocated by init_peer(). +void destroy_peer(struct con_peer *p) __attribute__((nonnull)); + +// Initializes statically or dynamically allocated con_peer_list struct. +// Allocates memory for CON_PEER_LIST_DEFAULT_LEN of peers and initialize +// all the peers. +void init_con_peer_list(struct con_peer_list *list) __attribute__((nonnull)); + +// Adds peer to the first free spot in the list. The free peer spot has fd == -1. +// If there is no free spot, more memory is allocated for the new peers. +// Caller is responsible to assure that each added peer has an unique fd. +// The peers are uniquely identified by fd e.g. for the delete operation. +// It copies topic with given len to internal peer struct. It MUST be NULL +// terminated C string +void add_peer(struct con_peer_list *list, int fd, char *topic) +__attribute__((nonnull)); + +// Deletes the first peer with given fd in the list by setting the fd to -1. +void del_peer(struct con_peer_list *list, int fd) __attribute__((nonnull)); + +// Calls destroy_peer() on all the peers in the list. +void destroy_con_peer_list(struct con_peer_list *list); + +#endif /*__SENTINEL_PROXY_CON_PEER_LIST_H__*/ diff --git a/proxy/log.c b/proxy/log.c new file mode 100644 index 0000000000000000000000000000000000000000..3cc55c5f0259a5c1e8fb4eff3dfd84b510c39519 --- /dev/null +++ b/proxy/log.c @@ -0,0 +1,21 @@ +/* + * Turris:Sentinel Proxy - Main MQTT gateway to Sentinel infrastructure + * Copyright (C) 2018 - 2021 CZ.NIC z.s.p.o. (https://www.nic.cz/) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "log.h" + +APP_LOG(sentinel_proxy); diff --git a/proxy_conf.h b/proxy/log.h similarity index 57% rename from proxy_conf.h rename to proxy/log.h index e67cc6ca497b3463683449cacc224d04cc98a8c2..17bfc231a25a06c9dfa77deb1a323248db3f235e 100644 --- a/proxy_conf.h +++ b/proxy/log.h @@ -1,6 +1,6 @@ /* * Turris:Sentinel Proxy - Main MQTT gateway to Sentinel infrastructure - * Copyright (C) 2020 CZ.NIC z.s.p.o. (https://www.nic.cz/) + * Copyright (C) 2018 - 2021 CZ.NIC z.s.p.o. (https://www.nic.cz/) * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -15,25 +15,17 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ +#ifndef __SENTINEL_PROXY_LOG_H__ +#define __SENTINEL_PROXY_LOG_H__ -#ifndef __SENTINEL_PROXY_PROXY_CONF_H__ -#define __SENTINEL_PROXY_PROXY_CONF_H__ +#define DEFLOG log_sentinel_proxy // use our log as default one in logc macros -#include +#include +#include -#include "device_token.h" +#define TRACE_FUNC trace(__func__) -struct proxy_conf { - const char *upstream_srv; - const char *local_socket; - const char *ca_file; - const char *client_cert_file; - const char *client_key_file; - char device_token[DEVICE_TOKEN_LEN + 1]; - const char *config_file; - bool custom_conf_file; -}; +extern log_t log_sentinel_proxy; -void load_conf(int argc, char *argv[], struct proxy_conf *conf); +#endif /*__SENTINEL_PROXY_LOG_H__*/ -#endif /*__SENTINEL_PROXY_PROXY_CONF_H__*/ diff --git a/proxy/proxy_conf.c b/proxy/proxy_conf.c new file mode 100644 index 0000000000000000000000000000000000000000..0c149622817c5c4a4793e8881e9ed17b3c824cf1 --- /dev/null +++ b/proxy/proxy_conf.c @@ -0,0 +1,231 @@ +/* + * Turris:Sentinel Proxy - Main MQTT gateway to Sentinel infrastructure + * Copyright (C) 2018 - 2021 CZ.NIC z.s.p.o. (https://www.nic.cz/) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include +#include +#include + +#include "proxy_conf.h" +#include "log.h" + +#define xstr(s) str(s) +#define str(s) #s + +#define CONF_FIELD_SERVER "server" +#define CONF_FIELD_PORT "port" +#define CONF_FIELD_CL_CERT_FILE "client_cert_file" +#define CONF_FIELD_CL_KEY_FILE "client_key_file" +#define CONF_FIELD_CA_CERT_FILE "ca_cert_file" +#define CONF_FIELD_ZMQ_SOCK_PATH "zmq_socket_path" +#define CONF_FIELD_DEVICE_TOKEN "device_token" + +const char *argp_program_version = PACKAGE_STRING; +const char *argp_program_bug_address = ""; +static const char doc[] = PACKAGE_NAME " - Turris Sentinel data gateway"; +static const struct argp_option options[] = { + {"server", 'S', "server", 0, + "Sentinel server address - default is: " DEFAULT_SERVER}, + {"port", 'p', "port", 0, + "Sentinel server port - default is: " xstr(DEFAULT_PORT)}, + {"zmq-sock", 's', "zmq_socket_path", 0, + "ZMQ socket path - default is: " DEFAULT_ZMQ_SOCK_PATH}, + {"ca-cert", 'c', "ca_cert_file", 0, + "Path to Sentinel CA certificate file - default is: " DEFAULT_CA_CERT_FILE}, + {"cl-cert", 'C', "client_cert_file", 0, + "Path to Sentinel client certificate file - default is: " DEFAULT_MQTT_CLIENT_CERT_FILE}, + {"cl-key", 'K', "client_key_file", 0, + "Path to Sentinel client key file - default is: " DEFAULT_MQTT_CLIENT_KEY_FILE}, + {"token", 't', "device_token", 0, "Sentinel device token"}, + {"config", 'f', "config_file", 0, + "Path to configuration file - default is: " DEFAULT_CONFIG_FILE}, + // For development and testing only !!! + {"disable-serv-check", 'd', NULL, OPTION_HIDDEN, NULL}, + {NULL} +}; +static bool port_once_parsed; + +__attribute__((nonnull)) bool is_accessible(const char *filename) { + TRACE_FUNC; + return (access(filename, R_OK) == 0); +} + +__attribute__((nonnull)) void verify_access(const char *filename) { + TRACE_FUNC; + if (!is_accessible(filename)) + critical("%s can't be accessed.", filename); +} + +int parse_port(const char *str) { + TRACE_FUNC; + char *end_ptr; + errno = 0; + long result = strtol(str, &end_ptr, 10); + if (errno || // conversion error + end_ptr == str || // no digits + *end_ptr != '\0' || // number in the begining of a text + result < 1 || // negative value and zero + result > 65335) // max port value + return -1; + return (int)result; +} + +error_t parse_opt (int key, char *arg, struct argp_state *state) { + TRACE_FUNC; + int ret = 0; + struct proxy_conf *conf = (struct proxy_conf *) state->input; + int tmp; + switch (key) { + case 'S': + conf->mqtt_broker = arg; + break; + case 'p': + if ((tmp = parse_port(arg)) == -1) { + if (!port_once_parsed) + warning("Port has invalid value. Ignoring this option.\nUsing configuration file or further default port instead"); + } else + conf->mqtt_port = tmp; + port_once_parsed = true; + break; + case 's': + conf->zmq_sock_path = arg; + break; + case 'c': + conf->ca_cert_file = arg; + break; + case 'C': + conf->mqtt_client_cert_file = arg; + break; + case 'K': + conf->mqtt_client_key_file = arg; + break; + case 't': + conf->device_token = arg; + break; + case 'f': + conf->config_file = arg; + break; + case 'd': + conf->disable_serv_check = true; + break; + default: + ret = ARGP_ERR_UNKNOWN; + break; + } + return ret; +} + +void load_cli_opts(int argc, char *argv[], struct proxy_conf *conf) { + // This function might be called multiple times and must be idempotent + TRACE_FUNC; + // set our log to be configured by logc_argp + logc_argp_log = log_sentinel_proxy; + struct argp argp = { + .options = options, + .parser = parse_opt, + .doc = doc, + .children = (struct argp_child[]){{&logc_argp_parser, 0, "Logging", 2}, + {NULL}}, + }; + argp_parse(&argp, argc, argv, 0, 0, conf); +} + +void load_conf_str(const config_t *cf, const char *name, char **dest, + const char *def) { + TRACE_FUNC; + const char *tmp = NULL; + if (config_lookup_string(cf, name, &tmp) == CONFIG_TRUE + && (def == NULL || !strcmp(*dest, def))) + *dest = strdup(tmp); +} + +void load_config_file(const char *path, struct proxy_conf *conf) { + TRACE_FUNC; + config_t cfg; + config_init(&cfg); + if (config_read_file(&cfg, path) != CONFIG_TRUE) { + warning("Wrong syntax in config file: %s on line %d: %s.\nIgnoring config file. Using CLI options or further default configuration instead.", + path, config_error_line(&cfg), config_error_text(&cfg)); + config_destroy(&cfg); + return; + } + load_conf_str(&cfg, CONF_FIELD_DEVICE_TOKEN, &conf->device_token, NULL); + load_conf_str(&cfg, CONF_FIELD_SERVER, &conf->mqtt_broker, DEFAULT_SERVER); + load_conf_str(&cfg, CONF_FIELD_CL_CERT_FILE, &conf->mqtt_client_cert_file, + DEFAULT_MQTT_CLIENT_CERT_FILE); + load_conf_str(&cfg, CONF_FIELD_CL_KEY_FILE, &conf->mqtt_client_key_file, + DEFAULT_MQTT_CLIENT_KEY_FILE); + load_conf_str(&cfg, CONF_FIELD_CA_CERT_FILE, &conf->ca_cert_file, + DEFAULT_CA_CERT_FILE); + load_conf_str(&cfg, CONF_FIELD_ZMQ_SOCK_PATH, &conf->zmq_sock_path, + DEFAULT_ZMQ_SOCK_PATH); + if (conf->mqtt_port == DEFAULT_PORT) + config_lookup_int(&cfg, CONF_FIELD_PORT, &conf->mqtt_port); + config_destroy(&cfg); +} + +void init_conf(struct proxy_conf *conf) { + TRACE_FUNC; + conf->disable_serv_check = false; + conf->mqtt_port = DEFAULT_PORT; + conf->device_token = NULL; + conf->mqtt_broker = DEFAULT_SERVER; + conf->zmq_sock_path = DEFAULT_ZMQ_SOCK_PATH; + conf->ca_cert_file = DEFAULT_CA_CERT_FILE; + conf->mqtt_client_cert_file = DEFAULT_MQTT_CLIENT_CERT_FILE; + conf->mqtt_client_key_file = DEFAULT_MQTT_CLIENT_KEY_FILE; + conf->config_file = DEFAULT_CONFIG_FILE; +} + +void load_conf(int argc, char *argv[], struct proxy_conf *conf) { + // We load cli params first (to get config file path most notably) Then we + // load config file if exists end is readable. If that is succesfull we have + // to load cli params once more - since they have higher priority. + TRACE_FUNC; + port_once_parsed = false; + load_cli_opts(argc, argv, conf); + if (is_accessible(conf->config_file)) { + load_config_file(conf->config_file, conf); + load_cli_opts(argc, argv, conf); + } else + warning("Config file %s can't be accessed.\nUsing CLI options or further default configuration instead.", + conf->config_file); + enum dt_state verify_status = device_token_verify(conf->device_token); + if (verify_status != DT_OK) + if (verify_status == DT_UNDEF) + critical("Sentinel Device token must be specified\nUse CLI options or configuartion for that."); + else + critical("Failed to verify Sentinel Device Token: %s", + device_token_state_msg(verify_status)); + verify_access(conf->mqtt_client_cert_file); + verify_access(conf->mqtt_client_key_file); + if (!conf->disable_serv_check) + // we need CA cert only if server check is enabled + verify_access(conf->ca_cert_file); + info("Sentinel Device token check passed"); + info("Using following configuration:"); + info("Sentinel server: %s", conf->mqtt_broker); + info("Sentinel server port: %d", conf->mqtt_port); + info("ZMQ socket: %s", conf->zmq_sock_path); + info("Sentinel client certificate: %s", conf->mqtt_client_cert_file); + info("Sentinel client key: %s", conf->mqtt_client_key_file); + if (conf->disable_serv_check) + warning("Sentinel server verification disabled!!!"); + else + // we need CA cert only if server check is enabled + info("Sentinel CA certificate: %s", conf->ca_cert_file); +} diff --git a/proxy/proxy_conf.h b/proxy/proxy_conf.h new file mode 100644 index 0000000000000000000000000000000000000000..e48441457d3d5b06f0c5ec4c8634b02dd3d9bb9e --- /dev/null +++ b/proxy/proxy_conf.h @@ -0,0 +1,70 @@ +/* + * Turris:Sentinel Proxy - Main MQTT gateway to Sentinel infrastructure + * Copyright (C) 2018 - 2021 CZ.NIC z.s.p.o. (https://www.nic.cz/) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef __SENTINEL_PROXY_PROXY_CONF_H__ +#define __SENTINEL_PROXY_PROXY_CONF_H__ + +#include +#include +#include + +struct proxy_conf { + bool disable_serv_check; + int mqtt_port; + char *mqtt_broker; + char *mqtt_client_cert_file; + char *mqtt_client_key_file; + char *ca_cert_file; + char *zmq_sock_path; + char *config_file; + char *device_token; +}; + +// NOTE: This is private API exposed just for the testing. +// It is NOT supposed to be used anywhere else. + +// Converts C NULL terminated string representing port number (0 - 65535) to integer value . +// If string contains invalid port value it returns -1. +int parse_port(const char *str) __attribute__((nonnull)); + +// Loads configuration from CLI options and their arguments. +// conf struct MUST be first properly initialized by init_conf(). +void load_cli_opts(int argc, char *argv[], struct proxy_conf *conf) +__attribute__((nonnull)); + +// Loads configuration from given configuration file. +// conf struct MUST be first properly initialized by init_conf(). +void load_config_file(const char *path, struct proxy_conf *conf) +__attribute__((nonnull)); + + +// NOTE: This is public API intended for normal use. + +// Allocates memory for conf struct fields and initializes them with default values. +void init_conf(struct proxy_conf *conf) __attribute__((nonnull)); + +// Loads configuration from CLI options and configuration file. +// conf struct MUST be first properly initialized by init_conf(). +// The priorities of configuration is following: +// CLi options > configuration file > default conf +// CLI options have higher priority than conf. file, which has higher priority +// than default configuration. +void load_conf(int argc, char *argv[], struct proxy_conf *conf) +__attribute__((nonnull)); + +#endif /*__SENTINEL_PROXY_PROXY_CONF_H__*/ diff --git a/proxy/proxy_mqtt.c b/proxy/proxy_mqtt.c new file mode 100644 index 0000000000000000000000000000000000000000..16d058570334c858d483fc00e15af836426cd23d --- /dev/null +++ b/proxy/proxy_mqtt.c @@ -0,0 +1,306 @@ +/* + * Turris:Sentinel Proxy - Main MQTT gateway to Sentinel infrastructure + * Copyright (C) 2018 - 2021 CZ.NIC z.s.p.o. (https://www.nic.cz/) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include +#include +#include + +#include "proxy_mqtt.h" +#include "common.h" +#include "log.h" + +#define MQTT_CLIENT_ID_MAX_LEN 64 +// https://www.hivemq.com/blog/mqtt-essentials-part-6-mqtt-quality-of-service-levels +#define MQTT_QOS_LEVEL 0 +#define MQTT_KEEPALIVE_INTERVAL 60 // seconds +#define MQTT_KEEPALIVE_TIMEOUT_S (MQTT_KEEPALIVE_INTERVAL / 2) //seconds +#define MQTT_KEEPALIVE_TIMEOUT_MS (MQTT_KEEPALIVE_TIMEOUT_S * 1000) // miliseconds +#define MQTT_DISCONNECT_TIMEOUT (5 * 1000) // miliseconds +#define SENTINEL_HEARTBEAT_TIMEOUT (5 * 60 * 1000) // miliseconds + +#define PACK_STR(packer, str) do { \ + msgpack_pack_str(packer, strlen(str)); \ + msgpack_pack_str_body(packer, str, strlen(str)); \ + } while(0); + +void compose_sentinel_mesg(msgpack_sbuffer *sbuff, msgpack_packer *pk, + const struct sentinel_status_mesg *mesg) { + TRACE_FUNC; + msgpack_packer_init(pk, sbuff, msgpack_sbuffer_write); + msgpack_pack_map(pk, 2); + PACK_STR(pk, "action"); + PACK_STR(pk, mesg->action); + PACK_STR(pk, "ts"); + msgpack_pack_long_long(pk, mesg->ts); +} + +void mqtt_connect(struct mqtt *mqtt) __attribute__((nonnull)); + +int mqtt_client_yeld_cb(zloop_t *loop, int timer_id, void *arg) { + // It must return 0. If -1 is returned event loop is terminated. + TRACE_FUNC; + struct mqtt *mqtt = (struct mqtt *)arg; + if (MQTTClient_isConnected(mqtt->client)) + MQTTClient_yield(); + else + mqtt_connect(mqtt); + return 0; +} + +__attribute__((nonnull)) void send_heartbeat(struct mqtt *mqtt) { + TRACE_FUNC; + mqtt->status_mesg->action = HEARTBEAT_EV; + mqtt->status_mesg->ts = time(NULL); + compose_sentinel_mesg(mqtt->sbuff, mqtt->packer, mqtt->status_mesg); + if (MQTTClient_publish(mqtt->client, mqtt->status_topic, mqtt->sbuff->size, + mqtt->sbuff->data, MQTT_QOS_LEVEL, 0, NULL) != MQTTCLIENT_SUCCESS) + error("Cannot send heartbeat"); + msgpack_sbuffer_clear(mqtt->sbuff); +} + +int sentinel_heartbeat_cb(zloop_t *loop, int timer_id, void *arg) { + // It must return 0. If -1 is returned event loop is terminated. + TRACE_FUNC; + send_heartbeat((struct mqtt *)arg); + return 0; +} + +__attribute__((nonnull)) void mqtt_connect(struct mqtt *mqtt) { + TRACE_FUNC; + if (MQTTClient_connect(mqtt->client, mqtt->conn_opts) == MQTTCLIENT_SUCCESS) { + info("Connected to MQTT broker"); + send_heartbeat(mqtt); + // The first time the MQTT client is really connected we start timer + // for sending Sentinel heartbeats + if (mqtt->sentinel_heartbeat_timer_id == -1) { + mqtt->sentinel_heartbeat_timer_id = zloop_timer(mqtt->zloop, + SENTINEL_HEARTBEAT_TIMEOUT, 0, sentinel_heartbeat_cb, mqtt); + assert(mqtt->sentinel_heartbeat_timer_id != -1); + } + } else { + error("Cannot connect to MQTT broker\nReconnect in %d s", + MQTT_KEEPALIVE_TIMEOUT_S); + } + // The first time mqtt_connect() is called we start timer for periodical + // connection checks and reconnects if NOT connected + // or periodical calls of MQTTClient_yield() + // which the application must call regularly + if (mqtt->mqtt_keep_alive_timer_id == -1) { + mqtt->mqtt_keep_alive_timer_id = zloop_timer(mqtt->zloop, + MQTT_KEEPALIVE_TIMEOUT_MS, 0, mqtt_client_yeld_cb, mqtt); + assert(mqtt->mqtt_keep_alive_timer_id != -1); + } +} + +__attribute__((nonnull)) void send_disconnect(struct mqtt *mqtt) { + TRACE_FUNC; + mqtt->status_mesg->action = DISCONNECT_EV; + mqtt->status_mesg->ts = time(NULL); + compose_sentinel_mesg(mqtt->sbuff, mqtt->packer, mqtt->status_mesg); + if (MQTTClient_publish(mqtt->client, mqtt->status_topic, mqtt->sbuff->size, + mqtt->sbuff->data, MQTT_QOS_LEVEL, 0, NULL) != MQTTCLIENT_SUCCESS) + error("Cannot send disconnect"); + msgpack_sbuffer_clear(mqtt->sbuff); +} + +__attribute__((nonnull)) void mqtt_disconnect(struct mqtt *mqtt) { + TRACE_FUNC; + zloop_timer_end(mqtt->zloop, mqtt->sentinel_heartbeat_timer_id); + zloop_timer_end(mqtt->zloop, mqtt->mqtt_keep_alive_timer_id); + send_disconnect(mqtt); + // leave some time to deliver in-flight messages + if (MQTTClient_disconnect(mqtt->client, MQTT_DISCONNECT_TIMEOUT) + == MQTTCLIENT_SUCCESS) { + info("Disconnected from MQTT broker"); + } + else + error("Cannot disconnect from MQTT broker"); +} + +void compose_last_will(msgpack_sbuffer *sbuff, msgpack_packer *pk) { + TRACE_FUNC; + msgpack_packer_init(pk, sbuff, msgpack_sbuffer_write); + msgpack_pack_map(pk, 1); + PACK_STR(pk, "action"); + PACK_STR(pk, LAST_WILL_DISCONNECT_EV); + // No time stamp here - this message is sent by MQTT broker + // Time stamp is added later by Sentinel Smash +} + +__attribute__((nonnull)) +void client_setup(const struct proxy_conf *conf, struct mqtt *mqtt) { + TRACE_FUNC; + MQTTClient_SSLOptions ssl_opts = MQTTClient_SSLOptions_initializer; + // For client verification at server side + // File in PEM format containing public certificate chain of the client. + ssl_opts.keyStore = conf->mqtt_client_cert_file; + // File in PEM format containing client's private key. + ssl_opts.privateKey = conf->mqtt_client_key_file; + if (conf->disable_serv_check) { + // Disables verification of the server certificate. + ssl_opts.enableServerCertAuth = 0; + // Disables post-connect checks, including that a server certificate + // matches the given host name. + ssl_opts.verify = 0; + } else { + ssl_opts.enableServerCertAuth = 1; + ssl_opts.verify = 1; + // The file in PEM format containing the public digital certificates + // trusted by the client. + ssl_opts.trustStore = conf->ca_cert_file; + } + + MQTTClient_willOptions lw_opts = MQTTClient_willOptions_initializer; + lw_opts.topicName = mqtt->status_topic; + // must be NULL to allow binary payload + lw_opts.message = NULL; + compose_last_will(mqtt->last_will_payload, mqtt->packer); + lw_opts.payload.len = mqtt->last_will_payload->size; + lw_opts.payload.data = mqtt->last_will_payload->data; + + MQTTClient_connectOptions conn_opts = MQTTClient_connectOptions_initializer; + // The time interval in seconds after which unacknowledged publish + // requests are retried during a TCP session. + conn_opts.retryInterval = 5; + // Defines the maximum time in seconds that should pass without + // communication between the client and the server. + conn_opts.keepAliveInterval = MQTT_KEEPALIVE_INTERVAL; + // Controls how many messages can be in-flight simultaneously. + // Setting this flag to false allows up to 10 messages to be in-flight. + conn_opts.reliable = 0; + // When cleansession is true, the session state information is discarded at + // connect and disconnect. + conn_opts.cleansession = 1; + + mqtt->conn_opts = malloc(sizeof(*mqtt->conn_opts)); + mqtt->ssl_opts = malloc(sizeof(*mqtt->ssl_opts)); + mqtt->will_opts = malloc(sizeof(*mqtt->will_opts)); + memcpy(mqtt->conn_opts, &conn_opts, sizeof(*mqtt->conn_opts)); + memcpy(mqtt->ssl_opts, &ssl_opts, sizeof(*mqtt->ssl_opts)); + memcpy(mqtt->will_opts, &lw_opts, sizeof(*mqtt->will_opts)); + mqtt->conn_opts->ssl = mqtt->ssl_opts; + mqtt->conn_opts->will = mqtt->will_opts; +} + +void build_data_topic(char **topic_buf, char **topic_prefix_end, + char *client_id, char *device_token) { + TRACE_FUNC; + size_t tmp_len = 0; + FILE *tmp = open_memstream(topic_buf, &tmp_len); + fprintf(tmp, "%s%s/%s/", TOPIC_PREFIX, client_id, device_token); + fclose(tmp); + // we need more space for topic suffix from received ZMQ message + *topic_buf = realloc(*topic_buf, tmp_len + ZMQ_MAX_TOPIC_LEN); + *topic_prefix_end = *topic_buf + tmp_len; +} + +void update_data_topic(char *topic_prefix_end, char *topic, size_t topic_len) { + TRACE_FUNC; + strncpy(topic_prefix_end, topic + TOPIC_PREFIX_LEN, + topic_len - TOPIC_PREFIX_LEN); + topic_prefix_end[topic_len - TOPIC_PREFIX_LEN] = '\0'; +} + +void build_status_topic(char **topic_buf, char *client_id, char *device_token) { + TRACE_FUNC; + size_t tmp_len = 0; + FILE *tmp = open_memstream(topic_buf, &tmp_len); + fprintf(tmp, "%s%s/%s/status", TOPIC_PREFIX, client_id, device_token); + fclose(tmp); +} + +void build_server_uri(char **uri_buf, char *server, int port) { + TRACE_FUNC; + size_t tmp_len = 0; + FILE *tmp = open_memstream(uri_buf, &tmp_len); + fprintf(tmp, "ssl://%s:%d", server, port); + fclose(tmp); +} + +void get_client_id(const char *filename, char **id) { + TRACE_FUNC; + FILE *fp = fopen(filename, "r"); + assert(fp); + X509 *cert = PEM_read_X509(fp, NULL, NULL, NULL); + fclose(fp); + assert(cert); + X509_NAME *subject_name = X509_get_subject_name(cert); + assert(subject_name); + *id = malloc(sizeof(**id) * (MQTT_CLIENT_ID_MAX_LEN + 1)); + assert(X509_NAME_get_text_by_NID(subject_name, NID_commonName, *id, + MQTT_CLIENT_ID_MAX_LEN) != -1); + X509_free(cert); +} + +void init_mqtt(struct mqtt *mqtt, zloop_t *zloop, const struct proxy_conf *conf) { + TRACE_FUNC; + get_client_id(conf->mqtt_client_cert_file, &mqtt->client_id); + build_data_topic(&mqtt->data_topic, &mqtt->data_topic_prefix_end, + mqtt->client_id, conf->device_token); + build_status_topic(&mqtt->status_topic, mqtt->client_id, conf->device_token); + build_server_uri(&mqtt->server_uri, conf->mqtt_broker, conf->mqtt_port); + + mqtt->packer = msgpack_packer_new(NULL, NULL); + assert(mqtt->packer); + mqtt->sbuff = msgpack_sbuffer_new(); + assert(mqtt->sbuff); + msgpack_sbuffer_init(mqtt->sbuff); + mqtt->last_will_payload = msgpack_sbuffer_new(); + assert(mqtt->last_will_payload); + msgpack_sbuffer_init(mqtt->last_will_payload); + mqtt->status_mesg = malloc(sizeof(*mqtt->status_mesg)); + + client_setup(conf, mqtt); + assert(MQTTClient_create(&mqtt->client, + mqtt->server_uri, mqtt->client_id, MQTTCLIENT_PERSISTENCE_NONE, NULL) + == MQTTCLIENT_SUCCESS); + mqtt->zloop = zloop; + mqtt->sentinel_heartbeat_timer_id = -1; + mqtt->mqtt_keep_alive_timer_id = -1; + mqtt_connect(mqtt); +} + +void destroy_mqtt(struct mqtt *mqtt) { + TRACE_FUNC; + if (mqtt) { + if (MQTTClient_isConnected(mqtt->client)) + mqtt_disconnect(mqtt); + MQTTClient_destroy(&mqtt->client); + free(mqtt->will_opts); + free(mqtt->ssl_opts); + free(mqtt->conn_opts); + free(mqtt->status_mesg); + msgpack_sbuffer_free(mqtt->sbuff); + msgpack_sbuffer_free(mqtt->last_will_payload); + msgpack_packer_free(mqtt->packer); + free(mqtt->status_topic); + free(mqtt->data_topic); + free(mqtt->client_id); + free(mqtt->server_uri); + } +} + +void mqtt_send_data(struct mqtt *mqtt, char *topic, size_t topic_len, + char *data, size_t data_len) { + TRACE_FUNC; + // complete MQTT data topic + update_data_topic(mqtt->data_topic_prefix_end, topic, topic_len); + if (MQTTClient_publish(mqtt->client, mqtt->data_topic, (int)data_len, data, + MQTT_QOS_LEVEL, 0, NULL) != MQTTCLIENT_SUCCESS) + error("Cannot send data"); +} diff --git a/proxy/proxy_mqtt.h b/proxy/proxy_mqtt.h new file mode 100644 index 0000000000000000000000000000000000000000..87895599a294e88579edd3c9bc3a8613e8ae77da --- /dev/null +++ b/proxy/proxy_mqtt.h @@ -0,0 +1,137 @@ +/* + * Turris:Sentinel Proxy - Main MQTT gateway to Sentinel infrastructure + * Copyright (C) 2018 - 2021 CZ.NIC z.s.p.o. (https://www.nic.cz/) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#ifndef __SENTINEL_PROXY_MQTT_H__ +#define __SENTINEL_PROXY_MQTT_H__ + +#include +#include +#include + +#include "proxy_conf.h" + +#define HEARTBEAT_EV "heartbeat" +#define DISCONNECT_EV "disconnect" +#define LAST_WILL_DISCONNECT_EV "last_will_disconnect" + +struct sentinel_status_mesg { + char *action; + long long int ts; +}; + +struct mqtt { + char *client_id; + char *status_topic; + char *data_topic; + char *data_topic_prefix_end; + char *server_uri; + MQTTClient client; + MQTTClient_connectOptions *conn_opts; + MQTTClient_SSLOptions *ssl_opts; + MQTTClient_willOptions *will_opts; + msgpack_sbuffer *last_will_payload; + msgpack_sbuffer *sbuff; + msgpack_packer *packer; + struct sentinel_status_mesg *status_mesg; + zloop_t *zloop; + int sentinel_heartbeat_timer_id; + int mqtt_keep_alive_timer_id; +}; + +// NOTE: This is private API exposed just for the testing. +// It is NOT supposed to be used anywhere else. + +// Composes Sentinel message based on values given by mesg. +// It msgpacks map containing action and ts keys with appropriate values from mesg. +// sbuff and pk must be allocated before calling this. +// sbuff MUST be also initialized prior calling this. +// It is intended to be called multiple times on the same sbuff. +// Call msgpack_sbuffer_clear after the content of the buffer is not useful +// anymore and before any subsequent call of this. +void compose_sentinel_mesg(msgpack_sbuffer *sbuff, msgpack_packer *pk, + const struct sentinel_status_mesg *mesg) __attribute__((nonnull)); + +// Composes MQTT last will payload. +// It msgpacks map containing action as a key and LAST_WILL_DISCONNECT_EV as a value. +// sbuff and pk must be allocated before calling this. +// sbuff MUST be also initialized prior calling this. +void compose_last_will(msgpack_sbuffer *sbuff, msgpack_packer *pk) +__attribute__((nonnull)); + +// Composes first part (without topic part coming from received ZMQ message) +// of MQTT data topic NULL terminated string from given client_id and +// device_token and stores it at *topic_buf. Memory at *topic_buf is allocated +// first for THE WHOLE topic. *topic_buf is overwritten so it SHOULD NOT point +// to any dynamically allocated memory before calling this. +// The end of first part of the topic composed by this function is stored at +// topic_prefix_end. The second part of the topic is supposed to be updated by +// update_data_topic(). +void build_data_topic(char **topic_buf, char **topic_prefix_end, + char *client_id, char *device_token) __attribute__((nonnull)); + +// Updates the second part of MQTT data topic first composed by build_data_topic(). +// Copies (topic_len - TOPIC_PREFIX_LEN) bytes from (topic + TOPIC_PREFIX_LEN) +// to topic_prefix_end and puts NULL string terminated character at the end +// of the whole topic string. Does NOT allocates any memory. +// It MUST be used only after calling build_data_topic(). +void update_data_topic(char *topic_prefix_end, char *topic, size_t topic_len) +__attribute__((nonnull)); + +// Composes MQTT status topic NULL terminated string from given client_id and +// device_token and stores it at *topic_buf. Memory at *topic_buf is allocated first. +// *topic_buf is overwritten so it SHOULD NOT point to any dynamically +// allocated memory before calling this. +void build_status_topic(char **topic_buf, char *client_id, char *device_token) +__attribute__((nonnull)); + +// Composes server URI NULL terminated string from given server and port +// and stores it at *uri_buf. Memory at *uri_buf is allocated first. +// *uri_buf is overwritten so it SHOULD NOT point to any dynamically +// allocated memory before calling this. +void build_server_uri(char **uri_buf, char *server, int port) +__attribute__((nonnull)); + +// Gets Common Name from TLS certificate and stores it at *id. +// Memory at *id is allocated first. *id is overwritten by malloc() so +// it SHOULD NOT point to any dynamically allocated memory before calling this. +// If getting Common Name is NOT possible the whole process is aborted. +void get_client_id(const char *filename, char **id) +__attribute__((nonnull)); + +// NOTE: This is public API intended for normal use. + +// Initializes given mqtt struct. It allocates memory, prepares all the +// configuration based on conf, connects to MQTT broker and adds appropriate +// callbacks to event loop. If any of these fails the whole process is aborted. +// DOES assert check for mqtt, zloop and conf. +// For the subsequent MQTT client functionality passed event loop MUST be +// started after calling this. +void init_mqtt(struct mqtt *mqtt, zloop_t *zloop, const struct proxy_conf *conf) +__attribute__((nonnull)); + +// If mqtt is not NULL, removes all callbacks from event loop, disconnects from +// MQTT broker and frees all the memory hold by mqtt. +void destroy_mqtt(struct mqtt *mqtt); + +// Sends given data to MQTT broker. topic is message topic received from ZMQ, +// which is internally transformed to MQTT data topic. Data are sent to MQTT +// broker as they are without any transformation. +void mqtt_send_data(struct mqtt *mqtt, char *topic, size_t topic_len, + char *data, size_t data_len) __attribute__((nonnull)); + +#endif /*__SENTINEL_PROXY_MQTT_H__*/ + diff --git a/proxy/proxy_zmq.c b/proxy/proxy_zmq.c new file mode 100644 index 0000000000000000000000000000000000000000..b8968f72660c93852a9e78d770a7a70011a491cb --- /dev/null +++ b/proxy/proxy_zmq.c @@ -0,0 +1,118 @@ +/* + * Turris:Sentinel Proxy - Main MQTT gateway to Sentinel infrastructure + * Copyright (C) 2018 - 2021 CZ.NIC z.s.p.o. (https://www.nic.cz/) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "proxy_zmq.h" +#include "common.h" +#include "log.h" + +int check_msg(size_t frames, unsigned char *topic, size_t topic_len) { + TRACE_FUNC; + if (topic == NULL) { + error("topic is NULL"); + return -1; + } + if (frames < 1 || frames > 2) { + error("Received and ignoring %ld parts malformed message", frames); + return -1; + } + if(topic_len <= TOPIC_PREFIX_LEN + || topic_len > ZMQ_MAX_TOPIC_LEN + || strncmp(TOPIC_PREFIX, topic, TOPIC_PREFIX_LEN)) { + error("Wrong message topic"); + return -1; + } + return 0; +} + +int recv_data_cb(zloop_t *loop, zsock_t *reader, void *arg) { + // It must return 0. If -1 is returned event loop is terminated. + TRACE_FUNC; + zmsg_t *msg = zmsg_recv(reader); + if (!msg) { + error("Cannot receive from data socket"); + return 0; + } + // A message is supposed to have exactly one or two frames. + // The first frame MUST be always a message topic. + size_t msg_size = zmsg_size(msg); + zframe_t *topic_frame = zmsg_first(msg); + size_t topic_len = zframe_size(topic_frame); + unsigned char *topic = zframe_data(topic_frame); + if (check_msg(msg_size, topic, topic_len)) + goto ret; + struct zmq *zmq = (struct zmq *)arg; + if (msg_size == 1) { + // First welcome message + // WARNING: __fd is NOT official nor documented use of czmq API !!! + // It can potentionally change at any time. + add_peer(zmq->con_peer_list, atoi(zframe_meta(topic_frame, "__fd")), + (char *)topic); + } + if (msg_size == 2) { + // Normal message with data + zframe_t *payload_frame = zmsg_last(msg); + mqtt_send_data(zmq->mqtt, (char *)topic, topic_len, + (char *)zframe_data(payload_frame), zframe_size(payload_frame)); + } +ret: + zmsg_destroy(&msg); + return 0; +} + +int monitor_cb(zloop_t *loop, zsock_t *reader, void *arg) { + // It must return 0. If -1 is returned event loop is terminated. + TRACE_FUNC; + zmsg_t *msg = zmsg_recv(reader); + zmsg_first(msg); + // Message from the monitor has two parts. The first is DISCONNECTED string + // and the second is session/connection file descriptor string. + zframe_t *frame = zmsg_next(msg); + del_peer(((struct zmq *)arg)->con_peer_list, atoi(zframe_data(frame))); + zmsg_destroy(&msg); + return 0; +} + +void init_zmq(struct zmq *zmq, struct mqtt *mqtt ,zloop_t *zloop, + const char *sock_path) { + TRACE_FUNC; + zmq->data_sock = zsock_new(ZMQ_PULL); + assert(zmq->data_sock); + zmq->monitor = zactor_new(zmonitor, zmq->data_sock); + assert(zmq->monitor); + assert(zstr_sendx(zmq->monitor, "LISTEN", "DISCONNECTED", NULL) == 0); + assert(zstr_sendx(zmq->monitor, "START", NULL) == 0); + assert(zsock_wait(zmq->monitor) == 0); + assert(zsock_bind(zmq->data_sock, "%s", sock_path) == 0); + assert(zloop_reader(zloop, zmq->data_sock, recv_data_cb, zmq) == 0); + assert(zloop_reader(zloop, (zsock_t*)zmq->monitor, monitor_cb, zmq) == 0); + zmq->mqtt = mqtt; + zmq->zloop = zloop; + zmq->con_peer_list = malloc(sizeof(*zmq->con_peer_list)); + init_con_peer_list(zmq->con_peer_list); +} + +void destroy_zmq(struct zmq *zmq) { + TRACE_FUNC; + if (zmq) { + zloop_reader_end(zmq->zloop, zmq->data_sock); + zloop_reader_end(zmq->zloop, (zsock_t*)zmq->monitor); + zactor_destroy(&zmq->monitor); + zsock_destroy(&zmq->data_sock); + destroy_con_peer_list(zmq->con_peer_list); + } +} diff --git a/proxy/proxy_zmq.h b/proxy/proxy_zmq.h new file mode 100644 index 0000000000000000000000000000000000000000..a8148420360e1e1d232f6c15e5f900c94c0cecf0 --- /dev/null +++ b/proxy/proxy_zmq.h @@ -0,0 +1,57 @@ +/* + * Turris:Sentinel Proxy - Main MQTT gateway to Sentinel infrastructure + * Copyright (C) 2018 - 2021 CZ.NIC z.s.p.o. (https://www.nic.cz/) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#ifndef __SENTINEL_PROXY_ZMQ_H__ +#define __SENTINEL_PROXY_ZMQ_H__ + +#include + +#include "proxy_mqtt.h" +#include "con_peer_list.h" + +struct zmq { + zsock_t *data_sock; + zactor_t *monitor; + zloop_t *zloop; + struct mqtt *mqtt; + struct con_peer_list *con_peer_list; +}; + +// NOTE: This is private API exposed just for the testing. +// It is NOT supposed to be used anywhere else. + +// Checks whether received ZMQ message has correct number of frames and +// correct topic. In case a message is OK returns 0 otherwise -1 is returned. +int check_msg(size_t frames, unsigned char *topic, size_t topic_len); + +// NOTE: This is public API intended for normal use. + +// Initializes given zmq struct. It allocates memory, binds to given ZMQ +// endpoint, starts ZMQ socket monitor and adds appropriate callbacks to event +// loop. If any of these fails the whole process is aborted. +// DOES assert check for zmq, mqtt, zloop and sock_path. +// For the subsequent data PULLing from ZMQ endpoint and forwarding them to given +// MQTT client, passed event loop MUST be started after calling this. +void init_zmq(struct zmq *zmq, struct mqtt *mqtt, zloop_t *zloop, + const char *sock_path) __attribute__((nonnull)); + +// If zmq is not NULL, removes all callbacks from event loop, destroys ZMQ +// PULL socket and ZMQ socket monitor and frees all the memory hold by zmq. +void destroy_zmq(struct zmq *zmq); + +#endif /*__SENTINEL_PROXY_ZMQ_H__*/ + diff --git a/proxy/sentinel_proxy.c b/proxy/sentinel_proxy.c new file mode 100644 index 0000000000000000000000000000000000000000..0710a70bee6247b6982e0ff3ed57c111c446025d --- /dev/null +++ b/proxy/sentinel_proxy.c @@ -0,0 +1,42 @@ +/* + * Turris:Sentinel Proxy - Main MQTT gateway to Sentinel infrastructure + * Copyright (C) 2018 - 2020 CZ.NIC z.s.p.o. (https://www.nic.cz/) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "proxy_conf.h" +#include "proxy_zmq.h" +#include "proxy_mqtt.h" +#include "log.h" + +int main(int argc, char *argv[]) { + struct proxy_conf proxy_conf; + init_conf(&proxy_conf); + load_conf(argc, argv, &proxy_conf); + + struct mqtt mqtt; + struct zmq zmq; + zloop_t *zloop = zloop_new(); + assert(zloop); + init_mqtt(&mqtt, zloop, &proxy_conf); + init_zmq(&zmq, &mqtt, zloop, proxy_conf.zmq_sock_path); + + zloop_start(zloop); + + destroy_zmq(&zmq); + destroy_mqtt(&mqtt); + zloop_destroy(&zloop); + return 0; +} diff --git a/proxy_conf.c b/proxy_conf.c deleted file mode 100644 index b264d6388cca26d9f39babe52e510fad309e5ccb..0000000000000000000000000000000000000000 --- a/proxy_conf.c +++ /dev/null @@ -1,133 +0,0 @@ -/* - * Turris:Sentinel Proxy - Main MQTT gateway to Sentinel infrastructure - * Copyright (C) 2020 CZ.NIC z.s.p.o. (https://www.nic.cz/) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#include -#include -#include -#include -#include -#include - -#include "proxy_conf.h" -#include "config.h" - -const char *argp_program_version = PACKAGE_STRING; -const char *argp_program_bug_address = ""; -static char doc[] = "Sentinel:Proxy - Turris:Sentinel data gateway"; -static struct argp_option options[] = { - {"server", 'S', "server", 0, "Sentinel server address" }, - {"socket", 's', "socket", 0, "Local ZMQ socket" }, - {"ca", 'c', "ca_file", 0, "Path to Sentinel CA file"}, - {"cert", 'C', "cert_file", 0, "Path to MQTT cert file"}, - {"key", 'K', "key_file", 0, "Path to MQTT key file"}, - {"token", 't', "device_token", 0, "Sentinel device token"}, - {"config", 'f', "config_file", 0, "Path to config file"}, - {NULL} -}; - -static bool is_accessible(const char *filename) { - return (access(filename, R_OK) == 0); -} - -static void verify_access(const char *filename) { - if (!is_accessible(filename)) { - fprintf(stderr, "%s can't be accessed\n", filename); - exit(EXIT_FAILURE); - } -} - -static error_t parse_opt (int key, char *arg, struct argp_state *state) { - struct proxy_conf *conf = state->input; - switch (key) { - case 'S': - conf->upstream_srv = arg; - break; - case 's': - conf->local_socket = arg; - break; - case 'c': - conf->ca_file = arg; - break; - case 'C': - conf->client_cert_file = arg; - break; - case 'K': - conf->client_key_file = arg; - break; - case 't': - strncpy(conf->device_token, arg, DEVICE_TOKEN_LEN + 1); - break; - case 'f': - conf->config_file = arg; - conf->custom_conf_file = true; - break; - case ARGP_KEY_ARG: - if (state->arg_num >= 1) - /* Too many arguments. */ - argp_usage(state); - break; - default: - return ARGP_ERR_UNKNOWN; - } - return 0; -} - -static void load_cli_opts(int argc, char *argv[], struct proxy_conf *conf) { - // This function might be called multiple times and must be idempotent - struct argp argp = {options, parse_opt, 0, doc}; - argp_parse(&argp, argc, argv, 0, 0, conf); -} - -static void load_config_file(const char *path, struct proxy_conf *conf) { - config_t cfg; - const char *tmp=""; - config_init(&cfg); - if(!config_read_file(&cfg, path)){ - fprintf(stderr, "error reading config file %s: %s:%d - %s\n", - path, config_error_file(&cfg), config_error_line(&cfg), - config_error_text(&cfg)); - config_destroy(&cfg); - exit(EXIT_FAILURE); - } - config_lookup_string(&cfg, "device_token", &tmp); - strncpy(conf->device_token, tmp, DEVICE_TOKEN_LEN + 1); - config_destroy(&cfg); -} - -void load_conf(int argc, char *argv[], struct proxy_conf *conf) { - // We load cli params first (to get config file path most notably) Then we - // load config file if exists end is readable. If that is succesfull we have - // to load cli params once more - since they have higher priority. - load_cli_opts(argc, argv, conf); - if (is_accessible(conf->config_file)) { - load_config_file(conf->config_file, conf); - load_cli_opts(argc, argv, conf); - } else { - fprintf(stderr, "WARN: config file %s can't be accessed\n", - conf->config_file); - } - int verify_status = device_token_verify(conf->device_token); - fprintf(stderr, "%s\n", device_token_state_msg(verify_status)); - if (verify_status) - exit(EXIT_FAILURE); - verify_access(conf->ca_file); - verify_access(conf->client_cert_file); - verify_access(conf->client_key_file); - fprintf(stderr, "Using:\nCA cert: %s\nclient cert: %s\nclient private key: %s\n", - conf->ca_file, conf->client_cert_file, conf->client_key_file); -} diff --git a/sentinel_proxy.c b/sentinel_proxy.c deleted file mode 100644 index 6a364ab6f1d2574026d539babe0b4fb0565487ae..0000000000000000000000000000000000000000 --- a/sentinel_proxy.c +++ /dev/null @@ -1,319 +0,0 @@ -/* - * Turris:Sentinel Proxy - Main MQTT gateway to Sentinel infrastructure - * Copyright (C) 2018-2020 CZ.NIC z.s.p.o. (https://www.nic.cz/) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#include -#include -#include -#include -#include -#include - -#include "config.h" -#include "proxy_conf.h" - -#define CERT_NAME_MAX_LEN 64 -#define TOPIC_PREFIX "sentinel/collect/" -#define HEARTBEAT_TIMEOUT (60 * 5) // seconds -#define ZMQ_MAX_TOPIC_LEN 256 -#define ZMQ_MAX_MSG_SIZE (1024 * 1024 * 2) -#define ZMQ_MAX_WAITING_MESSAGES 50 -// QoS levels - see here: -// https://www.hivemq.com/blog/mqtt-essentials-part-6-mqtt-quality-of-service-levels -#define MQTT_QOS 0 -#define MQTT_KEEPALIVE_INTERVAL 60 // seconds -#define MQTT_KEEPALIVE_TIMEOUT (MQTT_KEEPALIVE_INTERVAL / 2) //seconds - -#define CHECK_ERR(CMD, ...) \ - do { \ - if (CMD) { \ - fprintf(stderr, __VA_ARGS__); \ - } \ - } while (0) - -#define CHECK_ERR_FATAL(CMD, ...) \ - do { \ - if (CMD) { \ - fprintf(stderr, __VA_ARGS__); \ - exit(EXIT_FAILURE); \ - } \ - } while (0) - -// WARNING: be aware of goto !! -#define CHECK_ERR_GT(CMD, LABEL, ...) \ - do { \ - if (CMD) { \ - fprintf(stderr, __VA_ARGS__); \ - goto LABEL; \ - } \ - } while (0) - -#define PACK_STR(packer, str) do { \ - msgpack_pack_str(packer, strlen(str)); \ - msgpack_pack_str_body(packer, str, strlen(str)); \ - } while(0); - -struct reader_arg { - MQTTClient *mqtt_client; - char *mqtt_topic_buff; - char *mqtt_topic_prefix_end; -}; - -struct keep_alive_arg { - MQTTClient *client; - MQTTClient_connectOptions *conn_opts; - char *status_topic; -}; - -struct status_mesg { - char *action; - long long int ts; -}; - -static inline void compose_status_mesg(msgpack_sbuffer *sbuff, struct status_mesg *mesg) { - msgpack_sbuffer_init(sbuff); - msgpack_packer pk; - msgpack_packer_init(&pk, sbuff, msgpack_sbuffer_write); - msgpack_pack_map(&pk, 2); - PACK_STR(&pk, "action"); - PACK_STR(&pk, mesg->action); - PACK_STR(&pk, "ts"); - msgpack_pack_long_long(&pk, mesg->ts); -} - -static inline void mqtt_disconnect(MQTTClient *client, const char *status_topic) { - msgpack_sbuffer buff; - struct status_mesg mesg = { .action = "disconnect", .ts = time(NULL) }; - compose_status_mesg(&buff, &mesg); - MQTTClient_publish(*client, status_topic, buff.size, buff.data, MQTT_QOS, 0, - NULL); - msgpack_sbuffer_destroy(&buff); - // leave some time to deliver in-flight messages - MQTTClient_disconnect(*client, 5000); // 5s -} - -static inline void send_heartbeat(struct keep_alive_arg *arg) { - msgpack_sbuffer buff; - struct status_mesg mesg = { .action = "heartbeat", .ts = time(NULL) }; - compose_status_mesg(&buff, &mesg); - MQTTClient_publish(*arg->client, arg->status_topic, buff.size, buff.data, - MQTT_QOS, 0, NULL); - msgpack_sbuffer_destroy(&buff); -} - -static void mqtt_connect(struct keep_alive_arg *arg) { - int ret = MQTTClient_connect(*arg->client, arg->conn_opts); - CHECK_ERR(ret != MQTTCLIENT_SUCCESS, - "Could't connect to server\nReconnect in %d s\n", MQTT_KEEPALIVE_TIMEOUT); - send_heartbeat(arg); -}; - -static int heartbeat_handler(zloop_t *loop, int timer_id, void *arg) { - // It must return 0. If -1 is returned event loop is terminated. - send_heartbeat((struct keep_alive_arg *) arg); - return 0; -}; - -static int mqtt_keep_alive_timer_handler(zloop_t *loop, int timer_id, void *arg) { - // It must return 0. If -1 is returned event loop is terminated. - struct keep_alive_arg *keep_al_arg = (struct keep_alive_arg *)arg; - if (MQTTClient_isConnected(*keep_al_arg->client)) - MQTTClient_yield(); - else - mqtt_connect(keep_al_arg); - return 0; -} - -static int zmq_reader_handler(zloop_t *loop, zsock_t *reader, void *arg) { - // It must return 0. If -1 is returned event loop is terminated. - struct reader_arg *read_arg = (struct reader_arg *)arg; - zmsg_t *msg = zmsg_recv(reader); - CHECK_ERR_GT(!msg, err, "receiving ZMQ message was interrupted\n"); - CHECK_ERR_GT(zmsg_size(msg) != 2, err , - "ignoring mallformed message (%ld parts)\n", zmsg_size(msg)); - // extract zmq topic - zframe_t *topic_frame = zmsg_first(msg); - size_t msg_topic_len = zframe_size(topic_frame); - unsigned char *msg_topic = zframe_data(topic_frame); - // check zmq topic - CHECK_ERR_GT(msg_topic_len < strlen(TOPIC_PREFIX) - || msg_topic_len > ZMQ_MAX_TOPIC_LEN - || strncmp(TOPIC_PREFIX, msg_topic, strlen(TOPIC_PREFIX)), err, - "wrong zmq message topic\n"); - // compose mqtt topic - strncpy(read_arg->mqtt_topic_prefix_end, msg_topic + strlen(TOPIC_PREFIX), - msg_topic_len - strlen(TOPIC_PREFIX)); - read_arg->mqtt_topic_prefix_end[msg_topic_len - strlen(TOPIC_PREFIX)] = 0; - // send - zframe_t *payload_frame = zmsg_last(msg); - int ret = MQTTClient_publish(*read_arg->mqtt_client, read_arg->mqtt_topic_buff, - (int)zframe_size(payload_frame), zframe_data(payload_frame), MQTT_QOS, 0, NULL); - if (ret != MQTTCLIENT_SUCCESS) { - fprintf(stderr, "message was not published, err code:%d\n", ret); - // TODO buffer message - // try to send again later - } -err: - zmsg_destroy(&msg); - return 0; -} - -// It alocates memory and returns pointer to it. -// Caller is responsible for its freeing. -static char *get_name_from_cert(const char *filename) { - // get common name from subject of X509 certificate - // this function must return valid name or exit the program - X509 *cert = NULL; - FILE *fp = fopen(filename, "r"); - CHECK_ERR_FATAL(!fp, "cannot open certificate file\n"); - PEM_read_X509(fp, &cert, NULL, NULL); - fclose(fp); - CHECK_ERR_FATAL(!cert, "cannot read X509 certificate\n"); - // TODO: maybe do some aditional checks if it's sentinel CA? check issuer? - char *ret = malloc(CERT_NAME_MAX_LEN); - ret[0] = 0; - X509_NAME_get_text_by_NID(X509_get_subject_name(cert), NID_commonName, - ret, CERT_NAME_MAX_LEN); - X509_free(cert); - CHECK_ERR_FATAL(!strlen(ret), "couldn't get name from cert\n"); - return ret; -} - -static inline void prep_last_will(msgpack_sbuffer *payload) { - msgpack_sbuffer_init(payload); - msgpack_packer pk; - msgpack_packer_init(&pk, payload, msgpack_sbuffer_write); - msgpack_pack_map(&pk, 1); - PACK_STR(&pk, "action"); - PACK_STR(&pk, "last_will_disconnect"); - // No time stamp here - // This message is sent by MQTT broker -} - -static inline void mqtt_setup(MQTTClient_connectOptions *conn_opts, - const struct proxy_conf *conf, const char *status_topic, - msgpack_sbuffer *lwt_payload) { - conn_opts->retryInterval = 5; - conn_opts->keepAliveInterval = MQTT_KEEPALIVE_INTERVAL; - conn_opts->reliable = 0; - conn_opts->cleansession = 1; - conn_opts->ssl->enableServerCertAuth = 1; - conn_opts->ssl->trustStore = conf->ca_file; - conn_opts->ssl->keyStore = conf->client_cert_file; - conn_opts->ssl->privateKey = conf->client_key_file; - conn_opts->ssl->verify = 1; - conn_opts->will->topicName = status_topic; - conn_opts->will->message = NULL; // must be NULL to allow binary payload - prep_last_will(lwt_payload); - conn_opts->will->payload.len = lwt_payload->size; - conn_opts->will->payload.data = lwt_payload->data; -} - -static void run_proxy(const struct proxy_conf *conf) { - char *client_id = get_name_from_cert(conf->client_cert_file); - // TODO: client_id length should be checked (once its format is fixed) - fprintf(stderr, "got name from certificate: %s\n", client_id); - - // mqtt setup - char *data_topic = NULL; - size_t data_topic_prefix_len = 0; - FILE *tmp = open_memstream(&data_topic, &data_topic_prefix_len); - fprintf(tmp, "%s%s/%s/", TOPIC_PREFIX, client_id, conf->device_token); - fclose(tmp); - // we need more space for topic suffix from received ZMQ message - data_topic = realloc(data_topic, data_topic_prefix_len + ZMQ_MAX_TOPIC_LEN); - - char *status_topic = NULL; - size_t status_topic_len = 0; - tmp = open_memstream(&status_topic, &status_topic_len); - fprintf(tmp, "%s%s/%s/status", TOPIC_PREFIX, client_id, conf->device_token); - fclose(tmp); - - MQTTClient client; - int ret = MQTTClient_create(&client, conf->upstream_srv, client_id, - MQTTCLIENT_PERSISTENCE_NONE, NULL); - CHECK_ERR_FATAL(ret != MQTTCLIENT_SUCCESS, "Couldn't create mqtt client\n"); - MQTTClient_connectOptions conn_opts = MQTTClient_connectOptions_initializer; - MQTTClient_SSLOptions ssl_opts = MQTTClient_SSLOptions_initializer; - MQTTClient_willOptions lw_opts = MQTTClient_willOptions_initializer; - conn_opts.ssl = &ssl_opts; - conn_opts.will = &lw_opts; - msgpack_sbuffer last_will_payload; - mqtt_setup(&conn_opts, conf, status_topic, &last_will_payload); - - // zmq setup - zsock_t *receiver = zsock_new(ZMQ_PULL); - CHECK_ERR_FATAL(!receiver, "Couldn't create zmq socket\n"); - zsock_set_maxmsgsize(receiver, ZMQ_MAX_MSG_SIZE); - zsock_set_rcvhwm(receiver, ZMQ_MAX_WAITING_MESSAGES); - ret = zsock_bind(receiver, "%s", conf->local_socket); - CHECK_ERR_FATAL(ret == -1, "Couldn't bind to local ZMQ socket\n"); - - zloop_t *loop = zloop_new(); - CHECK_ERR_FATAL(!loop, "couldn't create zloop\n"); - struct reader_arg read_arg = { - .mqtt_client = &client, - .mqtt_topic_buff = data_topic, - .mqtt_topic_prefix_end = data_topic + data_topic_prefix_len - }; - ret = zloop_reader(loop, receiver, zmq_reader_handler, &read_arg); - CHECK_ERR_FATAL(ret == -1, "couldn't register zloop reader\n"); - - struct keep_alive_arg keep_al_arg = { - .client = &client, - .conn_opts = &conn_opts, - .status_topic = status_topic - }; - ret = zloop_timer(loop, MQTT_KEEPALIVE_TIMEOUT * 1000, 0, - mqtt_keep_alive_timer_handler, &keep_al_arg); - CHECK_ERR_FATAL(ret == -1, "couldn't register zloop keep alive handler\n"); - - ret = zloop_timer(loop, HEARTBEAT_TIMEOUT * 1000, 0, heartbeat_handler, - &keep_al_arg); - CHECK_ERR_FATAL(ret == -1, "couldn't register zloop heartbeat handler\n"); - - // start - fprintf(stderr, "connecting to %s, listening on %s\n", conf->upstream_srv, - conf->local_socket); - mqtt_connect(&keep_al_arg); - zloop_start(loop); - // teardown - zloop_destroy(&loop); - zsock_destroy(&receiver); - mqtt_disconnect(&client, status_topic); - MQTTClient_destroy(&client); - free(data_topic); - free(status_topic); - free(client_id); - msgpack_sbuffer_destroy(&last_will_payload); -} - -int main(int argc, char *argv[]) { - struct proxy_conf proxy_conf = { - .upstream_srv = DEFAULT_SERVER, - .local_socket = DEFAULT_LOCAL_SOCKET, - .ca_file = DEFAULT_CA_FILE, - .client_cert_file = DEFAULT_CERT_FILE, - .client_key_file = DEFAULT_KEY_FILE, - .device_token[0] = '\0', - .config_file = DEFAULT_CONFIG_FILE, - .custom_conf_file = false - }; - load_conf(argc, argv, &proxy_conf); - run_proxy(&proxy_conf); - return 0; -} diff --git a/tests/Makefile.am b/tests/Makefile.am new file mode 100644 index 0000000000000000000000000000000000000000..bd230b0bb5058b5f45a248ae02f52538471ed3a8 --- /dev/null +++ b/tests/Makefile.am @@ -0,0 +1,36 @@ +TESTS = +check_PROGRAMS = + +include %reldir%/unit/Makefile.am +include %reldir%/manual/Makefile.am + + +LOG_DRIVER = \ + env AM_TAP_AWK='$(AWK)' TEST_OUTPUT_TAP="/dev/stdout" \ + '$(SHELL)' '$(srcdir)/.aux/tap-driver.sh' + +# Valgrind ####################################################################### +VALGRIND_memcheck_FLAGS = \ + --leak-check=full \ + --show-leak-kinds=definite,indirect,possible \ + --track-fds=yes \ + --track-origins=yes \ + --trace-children=no \ + --child-silent-after-fork=yes +@VALGRIND_CHECK_RULES@ + +# Rules generated for valgrind are for some reason called *-am. This is just an alias +check-valgrind: check-valgrind-am +define check_valgrind_rule +check-valgrind-$(1): check-valgrind-$(1)-am +endef +$(foreach tool,$(valgrind_tools),$(eval $(call check_valgrind_rule,$(tool)))) + +# Coverage ####################################################################### +CODE_COVERAGE_LCOV_OPTIONS := --no-external +CODE_COVERAGE_GENHTML_OPTIONS := --prefix $(dir $(abs_top_builddir)) + +include $(srcdir)/aminclude_static.am + +clean-local: code-coverage-clean +distclean-local: code-coverage-dist-clean diff --git a/tests/manual/Makefile.am b/tests/manual/Makefile.am new file mode 100644 index 0000000000000000000000000000000000000000..7918649afe79653a4e332ef030c8ad57f7b0f2f7 --- /dev/null +++ b/tests/manual/Makefile.am @@ -0,0 +1,65 @@ +# NOTE: paths must match CAs configurations in folder ./ca_conf/ +CONF_DIR = %reldir%/ca_conf +KEYS_DIR = %reldir%/keys +SELF_SIGNED_CERT_DIR = %reldir%/self_sign_certs +CA1_CERTS_DIR = %reldir%/ca1_certs +CA2_CERTS_DIR = %reldir%/ca2_certs +SIGN_REQS_DIR = %reldir%/sign_reqs +CA1_DIR = %reldir%/ca1 +CA2_DIR = %reldir%/ca2 + +clean-test-certs: + rm -rf $(KEYS_DIR) $(SELF_SIGNED_CERT_DIR) $(CA1_CERTS_DIR) \ + $(CA2_CERTS_DIR) $(CA1_DIR) $(CA2_DIR) $(SIGN_REQS_DIR) + +clean-local: clean-test-certs + +gen-test-certs: clean-test-certs + mkdir -p $(KEYS_DIR) $(SELF_SIGNED_CERT_DIR) $(CA1_CERTS_DIR) \ + $(CA2_CERTS_DIR) $(SIGN_REQS_DIR) $(CA1_DIR)/{db,newcerts} \ + $(CA2_DIR)/{db,newcerts} + + openssl ecparam -genkey -name secp384r1 -out $(KEYS_DIR)/ca1.pem + openssl req -new -x509 -config $(CONF_DIR)/ca1.cnf -days 365 \ + -key $(KEYS_DIR)/ca1.pem -out $(CA1_CERTS_DIR)/ca1.pem + touch $(CA1_DIR)/db/index.txt + echo 01 > $(CA1_DIR)/db/serial + + openssl ecparam -genkey -name secp384r1 -out $(KEYS_DIR)/ca2.pem + openssl req -new -x509 -config $(CONF_DIR)/ca2.cnf -days 365 \ + -key $(KEYS_DIR)/ca2.pem -out $(CA2_CERTS_DIR)/ca2.pem + touch $(CA2_DIR)/db/index.txt + echo 01 > $(CA2_DIR)/db/serial + + openssl ecparam -genkey -name prime256v1 -out $(KEYS_DIR)/mosquitto.pem + openssl req -new -batch -subj '/CN=mosquitto.lan' \ + -key $(KEYS_DIR)/mosquitto.pem -out $(SIGN_REQS_DIR)/mosquitto.pem + openssl ca -config $(CONF_DIR)/ca1.cnf -batch -extensions server_ext \ + -in $(SIGN_REQS_DIR)/mosquitto.pem -out $(CA1_CERTS_DIR)/mosquitto.pem + openssl ca -config $(CONF_DIR)/ca2.cnf -batch -extensions server_ext \ + -in $(SIGN_REQS_DIR)/mosquitto.pem -out $(CA2_CERTS_DIR)/mosquitto.pem + openssl x509 -req -days 365 -signkey $(KEYS_DIR)/mosquitto.pem \ + -in $(SIGN_REQS_DIR)/mosquitto.pem \ + -out $(SELF_SIGNED_CERT_DIR)/mosquitto.pem + + openssl ecparam -genkey -name prime256v1 -out $(KEYS_DIR)/proxy.pem + openssl req -new -batch -subj '/CN=proxy' \ + -key $(KEYS_DIR)/proxy.pem -out $(SIGN_REQS_DIR)//proxy.pem + openssl ca -config $(CONF_DIR)/ca1.cnf -batch -extensions client_ext \ + -in $(SIGN_REQS_DIR)/proxy.pem -out $(CA1_CERTS_DIR)/proxy.pem + openssl ca -config $(CONF_DIR)/ca2.cnf -batch -extensions client_ext \ + -in $(SIGN_REQS_DIR)/proxy.pem -out $(CA2_CERTS_DIR)/proxy.pem + openssl x509 -req -days 365 -signkey $(KEYS_DIR)/proxy.pem \ + -in $(SIGN_REQS_DIR)/proxy.pem -out $(SELF_SIGNED_CERT_DIR)/proxy.pem + + openssl ecparam -genkey -name prime256v1 -out $(KEYS_DIR)/smash.pem + openssl req -new -batch -subj '/CN=smash' \ + -key $(KEYS_DIR)/smash.pem -out $(SIGN_REQS_DIR)//smash.pem + openssl ca -config $(CONF_DIR)/ca1.cnf -batch -extensions client_ext \ + -in $(SIGN_REQS_DIR)/smash.pem -out $(CA1_CERTS_DIR)/smash.pem + openssl ca -config $(CONF_DIR)/ca2.cnf -batch -extensions client_ext \ + -in $(SIGN_REQS_DIR)/smash.pem -out $(CA2_CERTS_DIR)/smash.pem + openssl x509 -req -days 365 -signkey $(KEYS_DIR)/smash.pem \ + -in $(SIGN_REQS_DIR)/smash.pem -out $(SELF_SIGNED_CERT_DIR)/smash.pem + + rm -rf $(CA1_DIR) $(CA2_DIR) $(SIGN_REQS_DIR) $(KEYS_DIR)/{ca1.pem,ca2.pem} diff --git a/tests/manual/README.md b/tests/manual/README.md new file mode 100644 index 0000000000000000000000000000000000000000..5f2792015e1291e6aeafc8638021a984710ddbec --- /dev/null +++ b/tests/manual/README.md @@ -0,0 +1,49 @@ +# Sentinel-proxy manual tests + +This directory contains naive tools - input generator, output capture and run +scripts for overall not yet automated testing of Proxy. +All the necessary TLS keys and certificates can be generated by running: +``` +make gen-test-certs +``` +in top-level project directory. + +Proxy serves as a data gateway passing messages from ZMQ over MQTT to Sentinel +server infrastructure. Natural way to test it is to generate input data and +observe the outgoing data. `input_generator` and `output_capture` serves for +that. `mosquitto` - MQTT broker is needed as MQTT clients (Proxy and +`output_capture`) needs the broker for relaying the data between them. + +## Dependencies +- sentinel-proxy +- Python +- [mosquitto](https://mosquitto.org/) +- openssl - for TLS keys and certificate generation by `make` + + +## Running and configuration + +Run the scripts in following order: + +``` +./run_mosquitto.sh +./run_out_capture.sh +./run_proxy.sh +./run_input_generator.sh +``` + +To modify testing configuration edit `mosquitto.conf` or run scripts directly. + +You can run multiple instances of `input_generator` and `output_capture`. +`input_generator` generates by default messages under topic based on shell PID +in which the script is running. `output_capture` is by default configured to +subscribe to all MQTT topics generated by Proxy. + +*NOTE*: For observing of Sentinel heartbeat messages coming out of Proxy is +convenient to compile it with short heartbeat timeout. Change +SENTINEL_HEARTBEAT_TIMEOUT macro in `proxy/proxy_mqtt.c` + +*NOTE*: Common Name used is server - MQTT broker certificate MUST +match server passed to Proxy. You can manipulate `/etc/hosts` file so that +passed server name is resolved to real IP address of MQTT broker - usually +`localhost`. diff --git a/tests/manual/ca_conf/ca1.cnf b/tests/manual/ca_conf/ca1.cnf new file mode 100644 index 0000000000000000000000000000000000000000..bcd686d1523188ef7bdbc03e43b51cd7856232b4 --- /dev/null +++ b/tests/manual/ca_conf/ca1.cnf @@ -0,0 +1,103 @@ +[ ca ] +default_ca = ca1 + +[ ca1 ] +dir = tests/manual +private_key = $dir/keys/ca1.pem +certificate = $dir/ca1_certs/ca1.pem +serial = $dir/ca1/db/serial +database = $dir/ca1/db/index.txt +new_certs_dir = $dir/ca1/newcerts +unique_subject = no + +# certificates options ------------------------------------- +default_days = 365 +# use public-key default method +default_md = default + +# policy for certificates (defined below) +policy = policy_cn_only +email_in_dn = no +# do not preserve extra DN parameters in request +preserve = no + + +# policy definitions --------------------------------------- +[ policy_cn_only ] +commonName = supplied + +[ policy_any ] +countryName = optional +stateOrProvinceName = optional +localityName = optional +organizationName = optional +organizationalUnitName = optional +commonName = supplied + + +# server certificate extensions ---------------------------- +[ server_ext ] + +basicConstraints = critical,CA:FALSE + +# Typical key usage for server cert +keyUsage = digitalSignature +extendedKeyUsage = serverAuth + +# PKIX recommendations +subjectKeyIdentifier = hash +authorityKeyIdentifier = keyid,issuer + +# Subject Alternative Name - needs to be supplied, if needed! +#subjectAltName = @alt_name + + +# client certificate extensions ---------------------------- +[ client_ext ] + +basicConstraints = critical,CA:FALSE + +# Typical key usage for server cert +keyUsage = digitalSignature +extendedKeyUsage = clientAuth + +# PKIX recommendations +subjectKeyIdentifier = hash +authorityKeyIdentifier = keyid,issuer + + + +# Request section ==================================================== +[ req ] +# DN +distinguished_name = req_distinguished_name +prompt = no +# mask for permitted string types +string_mask = utf8only + + +# extensions for self-signed certificate +x509_extensions = ca_ext + + +# default DN parameters ------------------------------------ +[ req_distinguished_name ] +# Subject fields +C = CZ +L = Prague +O = CZ.NIC, z.s.p.o. +OU = Turris +# Common name +CN = Sentinel Development CA 1 + + +# self-signed extensions ----------------------------------- +[ ca_ext ] + +# PKIX recommendation. +subjectKeyIdentifier = hash +authorityKeyIdentifier = keyid:always,issuer +basicConstraints = critical,CA:TRUE + +# only for issuing certs and CRL +keyUsage = cRLSign, keyCertSign diff --git a/tests/manual/ca_conf/ca2.cnf b/tests/manual/ca_conf/ca2.cnf new file mode 100644 index 0000000000000000000000000000000000000000..89d080955d75effd995d9023fa67ce7fdf4b3793 --- /dev/null +++ b/tests/manual/ca_conf/ca2.cnf @@ -0,0 +1,103 @@ +[ ca ] +default_ca = ca2 + +[ ca2 ] +dir = tests/manual +private_key = $dir/keys/ca2.pem +certificate = $dir/ca2_certs/ca2.pem +serial = $dir/ca2/db/serial +database = $dir/ca2/db/index.txt +new_certs_dir = $dir/ca2/newcerts +unique_subject = no + +# certificates options ------------------------------------- +default_days = 365 +# use public-key default method +default_md = default + +# policy for certificates (defined below) +policy = policy_cn_only +email_in_dn = no +# do not preserve extra DN parameters in request +preserve = no + + +# policy definitions --------------------------------------- +[ policy_cn_only ] +commonName = supplied + +[ policy_any ] +countryName = optional +stateOrProvinceName = optional +localityName = optional +organizationName = optional +organizationalUnitName = optional +commonName = supplied + + +# server certificate extensions ---------------------------- +[ server_ext ] + +basicConstraints = critical,CA:FALSE + +# Typical key usage for server cert +keyUsage = digitalSignature +extendedKeyUsage = serverAuth + +# PKIX recommendations +subjectKeyIdentifier = hash +authorityKeyIdentifier = keyid,issuer + +# Subject Alternative Name - needs to be supplied, if needed! +#subjectAltName = @alt_name + + +# client certificate extensions ---------------------------- +[ client_ext ] + +basicConstraints = critical,CA:FALSE + +# Typical key usage for server cert +keyUsage = digitalSignature +extendedKeyUsage = clientAuth + +# PKIX recommendations +subjectKeyIdentifier = hash +authorityKeyIdentifier = keyid,issuer + + + +# Request section ==================================================== +[ req ] +# DN +distinguished_name = req_distinguished_name +prompt = no +# mask for permitted string types +string_mask = utf8only + + +# extensions for self-signed certificate +x509_extensions = ca_ext + + +# default DN parameters ------------------------------------ +[ req_distinguished_name ] +# Subject fields +C = CZ +L = Prague +O = CZ.NIC, z.s.p.o. +OU = Turris +# Common name +CN = Sentinel Development CA 2 + + +# self-signed extensions ----------------------------------- +[ ca_ext ] + +# PKIX recommendation. +subjectKeyIdentifier = hash +authorityKeyIdentifier = keyid:always,issuer +basicConstraints = critical,CA:TRUE + +# only for issuing certs and CRL +keyUsage = cRLSign, keyCertSign diff --git a/tests/manual/input_generator.py b/tests/manual/input_generator.py new file mode 100755 index 0000000000000000000000000000000000000000..fb063f1e70452fd5559be40d0ee703c5fd2bac28 --- /dev/null +++ b/tests/manual/input_generator.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 + +import argparse +import zmq +import time +import random +import string + + +def run(socket, topic): + with zmq.Context() as context, context.socket(zmq.PUSH) as zmq_sock: + zmq_sock.connect(socket) + zmq_sock.send_multipart([topic]) + while True: + data = bytes([random.choice(string.ascii_letters.encode()) + for _ in range(random.randint(5, 15))]) + print(f"sending message: {topic}, {data}") + zmq_sock.send_multipart([topic, data]) + time.sleep(3) + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument( + "-s", "--socket", + dest="socket_path", + required=True, + type=str, + help="Path to ZMQ output socket" + ) + parser.add_argument( + "-t", "--topic", + dest="topic", + required=True, + type=str, + help="topic of generated data" + ) + options = parser.parse_args() + print(f"Data collector running on socket: {options.socket_path}") + print(f"with topic: {options.topic}") + run(options.socket_path, options.topic.encode()) + + +if __name__ == "__main__": + main() diff --git a/tests/manual/mosquitto.conf b/tests/manual/mosquitto.conf new file mode 100644 index 0000000000000000000000000000000000000000..e5a9d52dda837f546f26b8a397934a4c0ceac499 --- /dev/null +++ b/tests/manual/mosquitto.conf @@ -0,0 +1,53 @@ +# Set server port +listener 9099 + +################################################################################ + +# By default an SSL/TLS enabled listener will operate in a similar fashion to a +# https enabled web server, in that the server has a certificate signed by a +# CA and the client will verify that it is a trusted certificate. The overall +# aim is encryption of the network traffic. certfile and keyfile must be +# present to enable certificate based TLS encryption. + +# Path to the PEM encoded server key. +keyfile keys/mosquitto.pem + +# Path to the PEM encoded server certificate. +certfile self_sign_certs/mosquitto.pem +# certfile ca1_certs/mosquitto.pem +# certfile ca2_certs/mosquitto.pem + +################################################################################ + +# Determines whether clients that connect without providing a username are +# allowed to connect. If set to false then another means of connection should +# be created to control authenticated client access. +# All listeners now require authentication to be configured. This is with the +# exception of the case where no listener configuration is provided +# and hence the listener is bound to the loopback interface, as described above. +allow_anonymous true + +# If require_certificate is true, you may set use_identity_as_username to true +# to use the CN value from the client certificate as a username. +# If this is true, the password_file option will not be used for this listener. +# This takes priority over use_subject_as_username if both are set to true. +# use_identity_as_username true + +################################################################################ + +# By setting to true, a client connecting to this listener must provide a valid +# certificate in order for the network connection to proceed. This allows access +# to the broker to be controlled outside of the mechanisms provided by MQTT. +# require_certificate true + +# Defines the path to a file containing the PEM encoded CA certificates that +# are trusted when checking incoming client certificates. +# cafile ca1_certs/ca1.pem +# cafile ca1_certs/ca1.pem + +################################################################################ + +# Logging +log_dest stdout +log_type all +log_timestamp true diff --git a/tests/manual/output_capture.py b/tests/manual/output_capture.py new file mode 100755 index 0000000000000000000000000000000000000000..a01797148174ee7f0b099fbb60e738f29f66db31 --- /dev/null +++ b/tests/manual/output_capture.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python3 + +import argparse +import ssl +import paho.mqtt.client as paho +import msgpack + + +def my_argparser(argparser): + argparser.add_argument("-H", "--hostname", + dest="hostname", + required=True, + type=str, + help="Hostname of server to connect to") + argparser.add_argument("-p", "--port", + dest="port", + required=True, + type=int, + help="Port of server to connect to") + argparser.add_argument("-t", "--topic", + dest="topic", + required=True, + type=str, + help="Topic to subscribe to") + argparser.add_argument("-c", "--cert-file", + dest="cert_file", + required=True, + type=str, + help="File with certificate for TLS connection") + argparser.add_argument("-k", "--key-file", + dest="key_file", + required=True, + type=str, + help="File with key for TLS connection") + return argparser + + +def on_connect(client, userdata, flags, rc): + print("Connected to MQTT broker") + client.subscribe(userdata["topic"]) + + +def on_subscribe(client, userdata, mid, granted_qos): + print("Subscribed to %s topic", userdata["topic"]) + + +def on_disconnect(client, userdata, rc): + print("Client was disconnected\nReconnecting") + client.reconnect() + + +def on_message(client, userdata, msg): + try: + data = msgpack.unpackb(msg.payload) + except (msgpack.exceptions.UnpackException, msgpack.exceptions.ExtraData): + data = msg.payload + print(f"On topic:\n{msg.topic}\nReceived:\n{data}") + + +def main(): + parser = my_argparser(argparse.ArgumentParser()) + options = parser.parse_args() + client_data = {"topic": options.topic} + + client = paho.Client(client_id="Output capture of Sentinel Proxy") + # does NOT require broker certificate NOR verifies its hostname + client.tls_set(cert_reqs=ssl.CERT_NONE, certfile=options.cert_file, + keyfile=options.key_file) + client.user_data_set(client_data) + client.on_connect = on_connect + client.on_disconnect = on_disconnect + client.on_message = on_message + + client.connect(options.hostname, options.port) + client.loop_forever() + + +if __name__ == "__main__": + main() diff --git a/tests/manual/run_input_generator.sh b/tests/manual/run_input_generator.sh new file mode 100755 index 0000000000000000000000000000000000000000..00ded5130ada3d798f6b9f76ea1a8ecf2da82cea --- /dev/null +++ b/tests/manual/run_input_generator.sh @@ -0,0 +1,8 @@ +#!/bin/sh + +topic="sentinel/collect/generator$$" +socket="ipc:///tmp/proxy_input.sock" + +./input_generator.py \ + --socket $socket \ + --topic $topic diff --git a/tests/manual/run_mosquitto.sh b/tests/manual/run_mosquitto.sh new file mode 100755 index 0000000000000000000000000000000000000000..13a81dbfa4046b9e920dede155756b425f64dead --- /dev/null +++ b/tests/manual/run_mosquitto.sh @@ -0,0 +1,4 @@ +#!/bin/sh + +/usr/sbin/mosquitto \ + --config-file ./mosquitto.conf diff --git a/tests/manual/run_out_capture.sh b/tests/manual/run_out_capture.sh new file mode 100755 index 0000000000000000000000000000000000000000..92562bc4d5f6f0eb2d6022c77a23452cc3461168 --- /dev/null +++ b/tests/manual/run_out_capture.sh @@ -0,0 +1,16 @@ +#!/bin/sh + +host="localhost" +cert_file="self_sign_certs/smash.pem" +# cert_file="ca1_certs/smash.pem" +# cert_file="ca2_certs/smash.pem" +topic="sentinel/collect/+/+/+" +port="9099" +key="keys/smash.pem" + +./output_capture.py \ + --hostname $host \ + --cert-file $cert_file \ + --topic $topic \ + --port $port \ + --key-file $key diff --git a/tests/manual/run_proxy.sh b/tests/manual/run_proxy.sh new file mode 100755 index 0000000000000000000000000000000000000000..0c2ffe7405e64655d9851737b89c0ac1a7ac6dc0 --- /dev/null +++ b/tests/manual/run_proxy.sh @@ -0,0 +1,33 @@ +#!/bin/sh + +device_token="c1da24cf9063bf444d66271eb6c2c1b2ae364697ce07d05a8a60d3df08c93f50" +server="localhost" +# NOTE: to run mosquitto.lan as localhost change /etc/hotsts +# server="mosquitto.lan" +port="9099" +zmq_socket_path="ipc:///tmp/proxy_input.sock" +client_cert_file="self_sign_certs/proxy.pem" +# client_cert_file="ca1_certs/proxy.pem" +# client_cert_file="ca2_certs/proxy.pem" +client_key_file="keys/proxy.pem" +ca_cert_file="ca1_certs/ca1.pem" +# ca_cert_file="ca2_certs/ca2.pem" + +valgrind \ + --leak-check=full \ + --show-leak-kinds=definite,indirect,possible \ + --track-fds=yes \ + --track-origins=yes \ + --error-exitcode=1 \ + --show-leak-kinds=definite,indirect,possible --track-fds=yes \ + --error-exitcode=1 --track-origins=yes \ +../../sentinel-proxy \ + --log-level=-5 \ + --token $device_token \ + --server $server \ + --port $port \ + --zmq-sock $zmq_socket_path \ + --cl-cert $client_cert_file \ + --cl-key $client_key_file \ + --ca-cert $ca_cert_file \ + --disable-serv-check diff --git a/tests/unit/Makefile.am b/tests/unit/Makefile.am new file mode 100644 index 0000000000000000000000000000000000000000..592f5be402c2cf528fbf23df54a242d722dbd051 --- /dev/null +++ b/tests/unit/Makefile.am @@ -0,0 +1,26 @@ +TESTS += %reldir%/unittests + +check_PROGRAMS += %reldir%/unittests + +%canon_reldir%_unittests_SOURCES = \ + ${proxy_sources} \ + %reldir%/unittests.c \ + %reldir%/con_peer.c \ + %reldir%/con_peer_list.c \ + %reldir%/proxy_conf.c \ + %reldir%/proxy_zmq.c \ + %reldir%/proxy_mqtt.c + +%canon_reldir%_unittests_CFLAGS = \ + $(sentinel_proxy_CFLAGS) \ + $(check_CFLAGS) +%canon_reldir%_unittests_LDFLAGS = \ + $(sentinel_proxy_LDFLAGS) \ + $(check_LIBS) +%canon_reldir%_unittests_LDADD = \ + $(sentinel_proxy_LDADD) + +AM_TESTS_ENVIRONMENT = export DATA_DIR="$(top_srcdir)/%reldir%/data/"; + +dist-hook: + cp -rf "$(top_srcdir)/%reldir%/data/" "$(distdir)/%reldir%/data/" diff --git a/tests/unit/con_peer.c b/tests/unit/con_peer.c new file mode 100644 index 0000000000000000000000000000000000000000..ff91038323aa5205c5f4206d2cc9a7dc551bfb70 --- /dev/null +++ b/tests/unit/con_peer.c @@ -0,0 +1,62 @@ +/* + * Turris:Sentinel Proxy - Main MQTT gateway to Sentinel infrastructure + * Copyright (C) 2018 - 2021 CZ.NIC z.s.p.o. (https://www.nic.cz/) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include + +#include "../../proxy/con_peer_list.h" + +static struct con_peer con_peer; + +static void setup() { + init_peer(&con_peer); +} + +static void teardown() { + destroy_peer(&con_peer); +} + +START_TEST(init_peer_test) { + // init is done by setup() in con_peer_fixtures.c + ck_assert_int_eq(con_peer.fd, -1); + ck_assert_ptr_ne(con_peer.topic, NULL); +} + +START_TEST(set_peer_test) { + int fd = 454; + char *topic = "test topic"; + size_t topic_len = strlen(topic); + set_peer(&con_peer, fd, topic); + ck_assert_int_eq(con_peer.fd, fd); + ck_assert_mem_eq(con_peer.topic, topic, topic_len); +} + +void unittests_add_suite(Suite*); + +__attribute__((constructor)) +static void suite() { + Suite *suite = suite_create("con_peer"); + + TCase *basic_tc = tcase_create("basic"); + tcase_add_checked_fixture(basic_tc, setup, teardown); + tcase_add_test(basic_tc, init_peer_test); + tcase_add_test(basic_tc, set_peer_test); + + suite_add_tcase(suite, basic_tc); + + unittests_add_suite(suite); +} diff --git a/tests/unit/con_peer_list.c b/tests/unit/con_peer_list.c new file mode 100644 index 0000000000000000000000000000000000000000..440f493c90955ed7274bd1930aeec5b9ee92779b --- /dev/null +++ b/tests/unit/con_peer_list.c @@ -0,0 +1,149 @@ +/* + * Turris:Sentinel Proxy - Main MQTT gateway to Sentinel infrastructure + * Copyright (C) 2018 - 2021 CZ.NIC z.s.p.o. (https://www.nic.cz/) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include +#include + +#include "../../proxy/con_peer_list.h" + +static struct con_peer_list con_peer_list; + +static void setup() { + init_con_peer_list(&con_peer_list); +} + +static void teardown() { + destroy_con_peer_list(&con_peer_list); +} + +struct peer { + int fd; + char *topic; +}; + +START_TEST(init_test) { + ck_assert_int_eq(con_peer_list.alloc_size, CON_PEER_LIST_DEFAULT_LEN); + ck_assert_ptr_ne(con_peer_list.peers, NULL); +} + +const struct peer peers[] = { + {1, "aadasds"}, + {2, "jljljlkjlkjlkj"}, + {3, "wqewrwerwer"}, + {4, "poipjljlfjsldjflsjdfk"}, + {5, "advavdnbavsndvansdvnasvd"}, + {6, "pibvzhwidqd,d"}, + {7, "g"}, + {8, "asds"}, + {9, "asdadajvb vdvzdsv"}, +}; + +START_TEST(add_test) { + for(size_t i = 0; i < (sizeof(peers) / sizeof(*peers)); i++) { + // add peer + size_t t_len = strlen(peers[i].topic); + add_peer(&con_peer_list, peers[i].fd, peers[i].topic); + + // find peer in the list + bool is_there = false; + size_t j = 0; + for(; j < con_peer_list.alloc_size; j++) { + if (peers[i].fd == con_peer_list.peers[j].fd) { + is_there = true; + break; + } + } + if (is_there) { + // check the peer + ck_assert_int_eq(con_peer_list.peers[j].fd, peers[i].fd); + ck_assert_mem_eq(con_peer_list.peers[j].topic, peers[i].topic, t_len); + } else { + ck_assert(false); + } + } +} + +START_TEST(add_test2) { + // To check that there is no new allocation when peers fits into list + const struct peer peers[CON_PEER_LIST_DEFAULT_LEN] = { + {1, "aadasds"}, + {2, "jljljlkjlkjlkj"}, + {3, "wqewrwerwer"}, + {4, "poipjljlfjsldjflsjdfk"}, + }; + for(size_t i = 0; i < CON_PEER_LIST_DEFAULT_LEN; i++) + add_peer(&con_peer_list, peers[i].fd, peers[i].topic); + + ck_assert_uint_eq(con_peer_list.alloc_size, CON_PEER_LIST_DEFAULT_LEN); +} + +START_TEST(add_test3) { + // To check that the list expands memory when peers does NOT fit into it +#define LEN (CON_PEER_LIST_DEFAULT_LEN + 1) + const struct peer peers[LEN] = { + {1, "aadasds"}, + {2, "jljljlkjlkjlkj"}, + {3, "wqewrwerwer"}, + {4, "poipjljlfjsldjflsjdfk"}, + {5, "advavdnbavsndvansdvnasvd"}, + }; + for(size_t i = 0; i < LEN; i++) + add_peer(&con_peer_list, peers[i].fd, peers[i].topic); + + ck_assert_uint_eq(con_peer_list.alloc_size, CON_PEER_LIST_DEFAULT_LEN * 2); + + for(size_t i = LEN; i < con_peer_list.alloc_size; i++) { + ck_assert_int_eq(con_peer_list.peers[i].fd, -1); + ck_assert_ptr_ne(con_peer_list.peers[i].topic, NULL); + } +} + +START_TEST(del_test) { + // add peers + for(size_t i = 0; i < (sizeof(peers) / sizeof(*peers)); i++) + add_peer(&con_peer_list, peers[i].fd, peers[i].topic); + // delete peers + for(size_t i = 0; i < (sizeof(peers) / sizeof(*peers)); i++) { + int fd = peers[i].fd; + del_peer(&con_peer_list, fd); + // check that fd is not in the list + for(size_t j = 0; j < con_peer_list.alloc_size; j++) + ck_assert_int_ne(fd, con_peer_list.peers[i].fd); + } +} + +void unittests_add_suite(Suite*); + +__attribute__((constructor)) +static void suite() { + Suite *suite = suite_create("con_peer_list"); + + TCase *basic_case = tcase_create("basic case"); + tcase_add_checked_fixture(basic_case, setup, + teardown); + + tcase_add_test(basic_case, init_test); + tcase_add_test(basic_case, add_test); + tcase_add_test(basic_case, add_test2); + tcase_add_test(basic_case, add_test3); + tcase_add_test(basic_case, del_test); + + suite_add_tcase(suite, basic_case); + + unittests_add_suite(suite); +} diff --git a/tests/unit/data/test_cert.pem b/tests/unit/data/test_cert.pem new file mode 100644 index 0000000000000000000000000000000000000000..acbd1c1da545c1d3f83247040acfe1ed93d4967b --- /dev/null +++ b/tests/unit/data/test_cert.pem @@ -0,0 +1,53 @@ +Certificate: + Data: + Version: 3 (0x2) + Serial Number: 3 (0x3) + Signature Algorithm: ecdsa-with-SHA256 + Issuer: C=CZ, L=Prague, O=CZ.NIC, z.s.p.o., OU=Turris, CN=Sentinel Development CA 1 + Validity + Not Before: May 21 16:19:19 2021 GMT + Not After : May 16 16:19:19 2022 GMT + Subject: CN=proxy + Subject Public Key Info: + Public Key Algorithm: id-ecPublicKey + Public-Key: (256 bit) + pub: + 04:18:a5:f7:a9:49:ec:a0:aa:00:a8:3c:b3:62:83: + e8:d5:48:cb:ee:42:2b:95:d7:01:7b:82:b1:94:da: + 7e:ca:2b:d0:a3:0d:80:ab:ac:30:91:0e:6a:16:50: + 35:aa:0e:81:d3:61:98:33:e7:a2:7d:94:9e:3d:55: + 17:15:6d:5c:f7 + ASN1 OID: prime256v1 + NIST CURVE: P-256 + X509v3 extensions: + X509v3 Basic Constraints: critical + CA:FALSE + X509v3 Key Usage: + Digital Signature + X509v3 Extended Key Usage: + TLS Web Client Authentication + X509v3 Subject Key Identifier: + 99:6F:CF:DB:7A:A1:8C:BD:B6:56:20:69:B2:BB:CD:1D:43:6A:BA:AB + X509v3 Authority Key Identifier: + keyid:8E:D4:98:AA:3F:8D:0A:F4:2B:54:8D:64:31:9A:5E:11:49:59:0A:89 + + Signature Algorithm: ecdsa-with-SHA256 + 30:65:02:31:00:94:b2:b4:86:bd:cc:a8:a9:e3:97:70:fb:8b: + 55:42:6e:8b:36:43:f7:0e:12:16:19:c8:d6:5d:17:62:11:5e: + 6c:2e:cf:24:16:cf:86:83:28:67:f3:bc:26:9a:93:d5:4f:02: + 30:14:f4:45:95:1b:49:81:b5:58:50:17:d4:fa:f3:5e:8c:1d: + a0:5c:17:ac:24:2d:54:d4:67:15:07:38:2d:b2:00:1e:7f:ad: + 86:3e:7d:5b:75:2e:5e:bf:06:23:77:3e:c2 +-----BEGIN CERTIFICATE----- +MIIB/zCCAYWgAwIBAgIBAzAKBggqhkjOPQQDAjBuMQswCQYDVQQGEwJDWjEPMA0G +A1UEBwwGUHJhZ3VlMRkwFwYDVQQKDBBDWi5OSUMsIHoucy5wLm8uMQ8wDQYDVQQL +DAZUdXJyaXMxIjAgBgNVBAMMGVNlbnRpbmVsIERldmVsb3BtZW50IENBIDEwHhcN +MjEwNTIxMTYxOTE5WhcNMjIwNTE2MTYxOTE5WjAQMQ4wDAYDVQQDDAVwcm94eTBZ +MBMGByqGSM49AgEGCCqGSM49AwEHA0IABBil96lJ7KCqAKg8s2KD6NVIy+5CK5XX +AXuCsZTafsor0KMNgKusMJEOahZQNaoOgdNhmDPnon2Unj1VFxVtXPejcjBwMAwG +A1UdEwEB/wQCMAAwCwYDVR0PBAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMCMB0G +A1UdDgQWBBSZb8/beqGMvbZWIGmyu80dQ2q6qzAfBgNVHSMEGDAWgBSO1JiqP40K +9CtUjWQxml4RSVkKiTAKBggqhkjOPQQDAgNoADBlAjEAlLK0hr3MqKnjl3D7i1VC +bos2Q/cOEhYZyNZdF2IRXmwuzyQWz4aDKGfzvCaak9VPAjAU9EWVG0mBtVhQF9T6 +816MHaBcF6wkLVTUZxUHOC2yAB5/rYY+fVt1Ll6/BiN3PsI= +-----END CERTIFICATE----- diff --git a/tests/unit/data/test_config.cfg b/tests/unit/data/test_config.cfg new file mode 100644 index 0000000000000000000000000000000000000000..c0aac1b1e1e40ef1f5da6b0be269dcc23320e214 --- /dev/null +++ b/tests/unit/data/test_config.cfg @@ -0,0 +1,7 @@ +server = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaa" +port = 55555 +client_cert_file = "bbbbbbbbbbbbbbbbbbbbbbb" +client_key_file = "ccccccccccccccccccccccccccccccc" +ca_cert_file = "dddddddddddddddddddddddddddddddd" +zmq_socket_path = "eeeeeeeeeeeeeeeeeeeee" +device_token = "fffffffffffffffffffffffffffffffffffffffffff" diff --git a/tests/unit/proxy_conf.c b/tests/unit/proxy_conf.c new file mode 100644 index 0000000000000000000000000000000000000000..9784bbf4363c6b1b4de2bea7d78f1fbc2d0f060e --- /dev/null +++ b/tests/unit/proxy_conf.c @@ -0,0 +1,141 @@ +/* + * Turris:Sentinel Proxy - Main MQTT gateway to Sentinel infrastructure + * Copyright (C) 2018 - 2021 CZ.NIC z.s.p.o. (https://www.nic.cz/) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include +#include +#include + +#include "../../proxy/proxy_conf.h" + +static struct proxy_conf proxy_conf; + +static void setup() { + init_conf(&proxy_conf); +} + +START_TEST(parse_port_test_valid) { + ck_assert_int_eq(parse_port("12345"), 12345); +} + +START_TEST(parse_port_test_invalid) { + ck_assert_int_eq(parse_port("-1"), -1); + ck_assert_int_eq(parse_port("0"), -1); + ck_assert_int_eq(parse_port("65536"), -1); + ck_assert_int_eq(parse_port("asasasas"), -1); + ck_assert_int_eq(parse_port(""), -1); + ck_assert_int_eq(parse_port("65hghg"), -1); + ck_assert_int_eq(parse_port("65 hghg"), -1); +} + +START_TEST(init_test) { + // init is done by setup() in proxy_conf_fixtures + ck_assert_int_eq(proxy_conf.disable_serv_check, false); + ck_assert_int_eq(proxy_conf.mqtt_port, DEFAULT_PORT); + ck_assert_str_eq(proxy_conf.mqtt_broker, DEFAULT_SERVER); + ck_assert_str_eq(proxy_conf.mqtt_client_cert_file, + DEFAULT_MQTT_CLIENT_CERT_FILE); + ck_assert_str_eq(proxy_conf.mqtt_client_key_file, + DEFAULT_MQTT_CLIENT_KEY_FILE); + ck_assert_str_eq(proxy_conf.ca_cert_file, DEFAULT_CA_CERT_FILE); + ck_assert_str_eq(proxy_conf.zmq_sock_path, DEFAULT_ZMQ_SOCK_PATH); + ck_assert_str_eq(proxy_conf.config_file, DEFAULT_CONFIG_FILE); + ck_assert_ptr_eq(proxy_conf.device_token, NULL); +} + +static char *get_test_config_file_path() { + char *dir = getenv("DATA_DIR"); + if (!dir) + dir = "./tests/unit/data/"; + char *path; + asprintf(&path, "%s/%s", dir, "test_config.cfg"); + return path; +} + +START_TEST(load_config_file_test) { + char *config_file = get_test_config_file_path(); + // load_config_file("./tests/unit/test_config.cfg", &proxy_conf); + load_config_file(config_file, &proxy_conf); + // this is NOT changed by config file + ck_assert_int_eq(proxy_conf.disable_serv_check, false); + ck_assert_str_eq(proxy_conf.config_file, DEFAULT_CONFIG_FILE); + // this is changed + ck_assert_str_eq(proxy_conf.mqtt_broker, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); + ck_assert_int_eq(proxy_conf.mqtt_port, 55555); + ck_assert_str_eq(proxy_conf.mqtt_client_cert_file, "bbbbbbbbbbbbbbbbbbbbbbb"); + ck_assert_str_eq(proxy_conf.mqtt_client_key_file, + "ccccccccccccccccccccccccccccccc"); + ck_assert_str_eq(proxy_conf.ca_cert_file, "dddddddddddddddddddddddddddddddd"); + ck_assert_str_eq(proxy_conf.zmq_sock_path, "eeeeeeeeeeeeeeeeeeeee"); + ck_assert_str_eq(proxy_conf.device_token, + "fffffffffffffffffffffffffffffffffffffffffff"); + free(config_file); +} + +START_TEST(load_cli_opts_test) { + char server[] = "adsasdasdasdasdasdasd"; + char port[] = "12345"; + char socket[] = "iuhk"; + char ca[] = "aqqqqqqqqqqweqwrqr"; + char cert[] = "mkjlkjloppppppp"; + char key[] = "mnnbcvzlkjsdasdgjaskadl"; + char token[] = "bgjhfkualshdladl"; + char config[] = "r7yhhbvz80igiwg87wt8dyk"; + char *argv[] = {"cmd", + "--server", server, + "--port", port, + "--zmq-sock", socket, + "--ca-cert", ca, + "--cl-cert", cert, + "--cl-key", key, + "--token", token, + "--config", config, + "--disable-serv-check", + }; + int argc = sizeof(argv) / sizeof(*argv); + load_cli_opts(argc, argv, &proxy_conf); + ck_assert_int_eq(proxy_conf.disable_serv_check, true); + ck_assert_int_eq(proxy_conf.mqtt_port, 12345); + ck_assert_str_eq(proxy_conf.mqtt_broker, server); + ck_assert_str_eq(proxy_conf.mqtt_client_cert_file, cert); + ck_assert_str_eq(proxy_conf.mqtt_client_key_file, key); + ck_assert_str_eq(proxy_conf.ca_cert_file, ca); + ck_assert_str_eq(proxy_conf.zmq_sock_path, socket); + ck_assert_str_eq(proxy_conf.config_file, config); + ck_assert_str_eq(proxy_conf.device_token, token); +} + +void unittests_add_suite(Suite*); + +__attribute__((constructor)) +static void suite() { + Suite *suite = suite_create("proxy_conf"); + + TCase *parse_port_tc = tcase_create("parse_port"); + tcase_add_test(parse_port_tc, parse_port_test_valid); + tcase_add_test(parse_port_tc, parse_port_test_invalid); + suite_add_tcase(suite, parse_port_tc); + + TCase *conf_tc = tcase_create("conf"); + tcase_add_checked_fixture(conf_tc, setup, NULL); + tcase_add_test(conf_tc, init_test); + tcase_add_test(conf_tc, load_config_file_test); + tcase_add_test(conf_tc, load_cli_opts_test); + suite_add_tcase(suite, conf_tc); + + unittests_add_suite(suite); +} diff --git a/tests/unit/proxy_mqtt.c b/tests/unit/proxy_mqtt.c new file mode 100644 index 0000000000000000000000000000000000000000..bc43b470fbf5fa707d3b668d492b35f6c997497e --- /dev/null +++ b/tests/unit/proxy_mqtt.c @@ -0,0 +1,184 @@ +/* + * Turris:Sentinel Proxy - Main MQTT gateway to Sentinel infrastructure + * Copyright (C) 2018 - 2021 CZ.NIC z.s.p.o. (https://www.nic.cz/) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include +#include + +#include "../../proxy/proxy_mqtt.h" +#include "../../proxy/common.h" + +#define ck_assert_msgpack_str(msgpack, string) do { \ + ck_assert_int_eq(msgpack.type, MSGPACK_OBJECT_STR); \ + ck_assert_int_eq(msgpack.via.str.size, strlen(string)); \ + ck_assert_mem_eq(msgpack.via.str.ptr, string, strlen(string)); \ + } while (false) + +#define ck_assert_msgpack_uint(msgpack, num) do { \ + ck_assert_int_eq(msgpack.type, MSGPACK_OBJECT_POSITIVE_INTEGER); \ + ck_assert_int_eq(msgpack.via.u64, num); \ + } while (false) + + +START_TEST(compose_sentinel_msg_test) { + msgpack_sbuffer sbuf; + msgpack_sbuffer_init(&sbuf); + msgpack_packer pk; + msgpack_packer_init(&pk, &sbuf, msgpack_sbuffer_write); + msgpack_unpacked upkd; + msgpack_unpacked_init(&upkd); + + struct sentinel_status_mesg sentinel_msg = { + .action = "sdfsdfsadf", + .ts = 4686435445443445435, + }; + compose_sentinel_mesg(&sbuf, &pk, &sentinel_msg); + + ck_assert_int_eq(msgpack_unpack_next(&upkd, sbuf.data, sbuf.size, NULL), + MSGPACK_UNPACK_SUCCESS); + msgpack_object r = upkd.data; + ck_assert_int_eq(r.type, MSGPACK_OBJECT_MAP); + ck_assert_int_eq(r.via.map.size, 2); + ck_assert_msgpack_str(r.via.map.ptr[0].key, "action"); + ck_assert_msgpack_str(r.via.map.ptr[0].val, sentinel_msg.action); + ck_assert_msgpack_str(r.via.map.ptr[1].key, "ts"); + ck_assert_msgpack_uint(r.via.map.ptr[1].val, sentinel_msg.ts); + + msgpack_sbuffer_destroy(&sbuf); + msgpack_unpacked_destroy(&upkd); +} + +START_TEST(compose_last_will_test) { + msgpack_sbuffer sbuf; + msgpack_sbuffer_init(&sbuf); + msgpack_packer pk; + msgpack_packer_init(&pk, &sbuf, msgpack_sbuffer_write); + msgpack_unpacked upkd; + msgpack_unpacked_init(&upkd); + + compose_last_will(&sbuf, &pk); + + ck_assert_int_eq(msgpack_unpack_next(&upkd, sbuf.data, sbuf.size, NULL), + MSGPACK_UNPACK_SUCCESS); + msgpack_object r = upkd.data; + ck_assert_int_eq(r.type, MSGPACK_OBJECT_MAP); + ck_assert_int_eq(r.via.map.size, 1); + ck_assert_msgpack_str(r.via.map.ptr[0].key, "action"); + ck_assert_msgpack_str(r.via.map.ptr[0].val, LAST_WILL_DISCONNECT_EV); + + msgpack_sbuffer_destroy(&sbuf); + msgpack_unpacked_destroy(&upkd); +} + +START_TEST(build_data_t_test) { + char *id = "abcde"; + char *token = "124fgh"; + char *res = "sentinel/collect/abcde/124fgh/"; + + char *buff = NULL; + char *end = NULL; + build_data_topic(&buff, &end, id, token); + ck_assert_str_eq(buff, res); + ck_assert_ptr_eq(end, buff + strlen(res)); + free(buff); +} + +START_TEST(update_data_t_test) { + char *topic = "fusfdslhfdiuhsdfhlshdflhsaldf sdfsdfsdhsdf sdfhshfdlhslhdfsdf4s21f2sf"; + size_t t_len = strlen(topic); + + char *buff = malloc(t_len); + update_data_topic(buff, topic, t_len); + ck_assert_str_eq(buff, topic + TOPIC_PREFIX_LEN); + free(buff); +} + +START_TEST(bild_status_t_tests) { + char *id = "abcde"; + char *token = "124fgh"; + char *res = "sentinel/collect/abcde/124fgh/status"; + + char *buff = NULL; + build_status_topic(&buff, id, token); + ck_assert_str_eq(buff, res); + free(buff); +} + +START_TEST(build_server_uri_test) { + char *server = "example.com"; + int port = 12345; + char *res = "ssl://example.com:12345"; + + char *buff = NULL; + build_server_uri(&buff, server, port); + ck_assert_str_eq(buff, res); + free(buff); +} + +static char *get_test_cert_file_path() { + char *dir = getenv("DATA_DIR"); + if (!dir) + dir = "./tests/unit/data/"; + char *path; + asprintf(&path, "%s/%s", dir, "test_cert.pem"); + return path; +} + +START_TEST(get_client_id_test) { + // const char *cert_file = "./tests/unit/test_cert.pem"; + char *cert_file = get_test_cert_file_path(); + char *id = NULL; + get_client_id(cert_file, &id); + ck_assert_str_eq(id, "proxy"); + free(id); + free(cert_file); +} + +void unittests_add_suite(Suite*); + +__attribute__((constructor)) +static void suite() { + Suite *suite = suite_create("proxy_mqtt"); + + TCase *get_name_from_cert_tc = tcase_create("get_client_id"); + tcase_add_test(get_name_from_cert_tc, get_client_id_test); + suite_add_tcase(suite, get_name_from_cert_tc); + + TCase *data_topic_tc = tcase_create("data topic"); + tcase_add_test(data_topic_tc, build_data_t_test); + tcase_add_test(data_topic_tc, update_data_t_test); + suite_add_tcase(suite, data_topic_tc); + + TCase *status_topic_tc = tcase_create("status topic"); + tcase_add_test(status_topic_tc, bild_status_t_tests); + suite_add_tcase(suite, status_topic_tc); + + TCase *server_uri_tc = tcase_create("server uri"); + tcase_add_test(server_uri_tc, build_server_uri_test); + suite_add_tcase(suite, server_uri_tc); + + TCase *comp_last_will_tc = tcase_create("compose_last_will"); + tcase_add_test(comp_last_will_tc, compose_last_will_test); + suite_add_tcase(suite, comp_last_will_tc); + + TCase *comp_sentinel_msg_tc = tcase_create("compose_sentinel_message"); + tcase_add_test(comp_sentinel_msg_tc, compose_sentinel_msg_test); + suite_add_tcase(suite, comp_sentinel_msg_tc); + + unittests_add_suite(suite); +} + diff --git a/tests/unit/proxy_zmq.c b/tests/unit/proxy_zmq.c new file mode 100644 index 0000000000000000000000000000000000000000..6ad2387e7bd9b6c0cbba14e3cb48216d6e213bb5 --- /dev/null +++ b/tests/unit/proxy_zmq.c @@ -0,0 +1,84 @@ +/* + * Turris:Sentinel Proxy - Main MQTT gateway to Sentinel infrastructure + * Copyright (C) 2018 - 2021 CZ.NIC z.s.p.o. (https://www.nic.cz/) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include + +#include "../../proxy/common.h" +#include "../../proxy/proxy_zmq.h" + +struct msg { + size_t frames; + char *topic; + size_t topic_len; +}; + +#define t1 TOPIC_PREFIX "a" +#define t2 TOPIC_PREFIX "gjahhiusjajshysygsgs" +#define t3 TOPIC_PREFIX "sadsdSfsSSd/fsdf/sdJfsdafsdafsdfbnhmjh/k6jhk435hk4545s4fd4h2s4d2sdshdcC as A JCHS AsjcasasA S JHsJ /ASDA JDska SD shda AD Shd jalsd,asdkKLASKLaKLD2Y873D S A;;Ajs;lHALDH SKG AGjshg kHAGDJHGASJG DJKASGDJHG AJKSDGGJSDlkHLHd;l'afjdlasdfjhsdjf" + +static struct msg valid_msgs[] = { + {1, t1, strlen(t1)}, + {1, t2, strlen(t2)}, + {1, t3, strlen(t3)}, + {2, t1, strlen(t1)}, + {2, t2, strlen(t2)}, + {2, t3, strlen(t3)}, +}; + +START_TEST(check_msg_test_valid) { + ck_assert_int_eq(check_msg(valid_msgs[_i].frames, + valid_msgs[_i].topic, valid_msgs[_i].topic_len), 0); +} + +#define t_inv1 "sentinel/collect" +#define t_inv2 "axasfdgfh" +#define t_inv3 TOPIC_PREFIX "sadsddsdsSSd/fsdf/sdJfsdafsdafsdfbnhmjh/k6jhk435hk4545s4fd4h2s4d2sdshdcC as A JCHS AsjcasasA S JHsJ /ASDA JDska SD shda AD Shd jalsd,asdkKLASKLaKLD2Y873D S A;;Ajs;lHALDH SKG AGjshg kHAGDJHGASJG DJKASGDJHG AJKSDGGJSDlkHLHd;l'afjdlasdfjhsdjf" + +static struct msg invalid_msgs[] = { + // wrong frames + {0, t1, strlen(t1)}, + {3, t1, strlen(t1)}, + // wrong topic + {1, TOPIC_PREFIX, strlen(TOPIC_PREFIX)}, + {1, t_inv1, strlen(t_inv1)}, + {1, t_inv2, strlen(t_inv2)}, + {1, NULL, 0}, + {2, t_inv3, strlen(t_inv3)}, +}; + +START_TEST(check_msg_test_invalid) { + ck_assert_int_eq(check_msg(invalid_msgs[_i].frames, + invalid_msgs[_i].topic, invalid_msgs[_i].topic_len), -1); +} + +void unittests_add_suite(Suite*); + +__attribute__((constructor)) +static void suite() { + Suite *suite = suite_create("proxy_zmq"); + + TCase *check_msg_tc = tcase_create("check_msg"); + tcase_add_loop_test(check_msg_tc, check_msg_test_valid, 0, + sizeof(valid_msgs) / sizeof(*valid_msgs)); + tcase_add_loop_test(check_msg_tc, check_msg_test_invalid, 0, + sizeof(invalid_msgs) / sizeof(*invalid_msgs)); + suite_add_tcase(suite, check_msg_tc); + + unittests_add_suite(suite); +} + diff --git a/tests/unit/unittests.c b/tests/unit/unittests.c new file mode 100644 index 0000000000000000000000000000000000000000..90ec5c4b2864852a47cec9b9dfe35e3a1902695b --- /dev/null +++ b/tests/unit/unittests.c @@ -0,0 +1,51 @@ +/* + * Turris:Sentinel Proxy - Main MQTT gateway to Sentinel infrastructure + * Copyright (C) 2018 - 2021 CZ.NIC z.s.p.o. (https://www.nic.cz/) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#include +#include +#include +#include + +static Suite **suites = NULL; +static size_t suites_len = 0, suites_size = 1; + +void unittests_add_suite(Suite *s) { + if (suites == NULL || suites_len == suites_size) + suites = realloc(suites, (suites_size *= 2) * sizeof *suites); + suites[suites_len++] = s; +} + +int main(void) { + SRunner *runner = srunner_create(NULL); + + for (size_t i = 0; i < suites_len; i++) + srunner_add_suite(runner, suites[i]); + + char *test_output_tap = getenv("TEST_OUTPUT_TAP"); + if (test_output_tap && *test_output_tap != '\0') + srunner_set_tap(runner, test_output_tap); + char *test_output_xml = getenv("TEST_OUTPUT_XML"); + if (test_output_xml && *test_output_xml != '\0') + srunner_set_xml(runner, test_output_xml); + srunner_set_fork_status(runner, CK_FORK); // We have to fork to catch signals + + srunner_run_all(runner, CK_NORMAL); + int failed = srunner_ntests_failed(runner); + + srunner_free(runner); + return (bool)failed; +}