statusdeamon auf configparser umgestellt. dank an ndo

This commit is contained in:
berhsi 2020-07-09 12:08:06 +02:00
parent a0fda2aaa2
commit c0f8caddfc
3 changed files with 152 additions and 232 deletions

View file

@ -3,25 +3,19 @@
# Configuration file for the server, who is manage the api for door status # Configuration file for the server, who is manage the api for door status
# from krautspace jena. # from krautspace jena.
# host, where server lives (string with fqdn or ipv4). default ist [general]
# localhost. timeout = 5.0
HOST = '127.0.0.1' loglevel = debug
# port, where the server is listen. default is 100001 [server]
PORT = 10001 host = localhost
port = 10001
cert = ./certs/server.crt
key = ./certs/server.key
# timeout for connection [client]
TIMEOUT = 5 cert = ./certs/client.crt
# path for ssl keys and certificates. default is the current directory. [api]
SERVER_CERT = './certs/server.crt' api = ./api
SERVER_KEY = './certs/server.key' template = ./api_template
CLIENT_CERT = './certs/client.crt'
# path to api files
API_TEMPLATE = './api_template'
API = './api'
# loglevel (maybe CRITICAL(50), ERROR(40), WARNING(30), INFO(20), DEBUG(10))
# default is warning
VERBOSITY = 'debug'

View file

@ -1,26 +0,0 @@
# file: statusd.conf
# Configuration file for the server, who is manage the api for door status
# from krautspace jena.
# host, where server lives (string with fqdn or ipv4). default ist
# localhost.
HOST = 'localhost'
# port, where the server is listen. default is 100001
PORT = 10001
# timeout for connection
TIMEOUT = 5
# path for ssl keys and certificates. default is the current directory.
SERVER_CERT = './server.crt'
SERVER_KEY = './server.key'
CLIENT_CERT = './client.crt'
# path to api files
API_TEMPLATE = './api_template'
API = '/path/to//api'
# loglevel (maybe ERROR, INFO, DEBUG) - not implementet at the moment.
VERBOSITY = 'info'

326
statusd.py Executable file → Normal file
View file

@ -4,44 +4,18 @@
# date: 26.07.2019 # date: 26.07.2019
# email: berhsi@web.de # email: berhsi@web.de
# server, which listens for ipv4 connections at port 10001. now with ssl # Status server, listening for door status updates. The IPv4 address and port
# encrypted connection and client side authentication. # to listen on are configurable, by default localhost:10001 is used. The
# connection is secured by TLS and client side authentication.
import json
import logging
import os
import socket import socket
import ssl import ssl
import os import sys
import logging
import json
from time import time, sleep from time import time, sleep
from sys import exit import configparser
def read_config(CONFIGFILE, CONFIG):
'''
reads the given config file and sets the values are founded.
param 1: string
param 2: dictionary
return: boolean
'''
logging.debug('Read configfile {}'.format(CONFIGFILE))
if os.access(CONFIGFILE, os.R_OK):
logging.debug('Configfile is readable')
with open(CONFIGFILE, 'r') as config:
logging.debug('Configfile successfull read')
for line in config.readlines():
if not line[0] in ('#', ';', '\n', '\r'):
key, value = (line.strip().split('='))
key = strip_argument(key).upper()
if key in CONFIG.keys():
value = strip_argument(value)
CONFIG[key] = value
else:
pass
else:
logging.error('Failed to read {}'.format(CONFIGFILE))
logging.error('Using default values')
return False
return True
def certs_readable(config): def certs_readable(config):
@ -51,42 +25,28 @@ def certs_readable(config):
param 1: dictionary param 1: dictionary
return: boolean return: boolean
''' '''
for i in (config['SERVER_KEY'], config['SERVER_CERT'], for i in (config['server']['key'], config['server']['cert'],
config['CLIENT_CERT']): config['client']['cert']):
if i == '' or os.access(i, os.R_OK) is False: if i == '' or os.access(i, os.R_OK) is False:
logging.error('Cant read {}'.format(i)) logging.error('Cannot read {}'.format(i))
return False return False
return True return True
def strip_argument(argument): def print_config(config):
''' '''
Becomes a string and strips at first whitespaces, second apostrops and Logs the config with level debug.
returns the clear string.
param 1: string
return: string
'''
argument = argument.strip()
argument = argument.strip('"')
argument = argument.strip("'")
return argument
def print_config(CONFIG):
'''
Prints the used configuration, if loglevel ist debug.
param 1: dictionary
return: boolean (allways true)
''' '''
logging.debug('Using config:') logging.debug('Using config:')
for i in CONFIG.keys(): for section in config.sections():
logging.debug('{}: {}'.format(i, CONFIG[i])) logging.debug('Section {}'.format(section))
return True for i in config[section]:
logging.debug(' {}: {}'.format(i, config[section][i]))
def print_ciphers(cipherlist): def print_ciphers(cipherlist):
''' '''
This function prints the list of the allowed ciphers. Prints the list of allowed ciphers.
param1: dictionary param1: dictionary
return: boolean return: boolean
''' '''
@ -96,12 +56,11 @@ def print_ciphers(cipherlist):
for j in i.keys(): for j in i.keys():
print('{}: {}'.format(j, i[j])) print('{}: {}'.format(j, i[j]))
print('\n') print('\n')
return True
def display_peercert(cert): def display_peercert(cert):
''' '''
This function displays the values of a given certificate. Displays the values of a given certificate.
param1: dictionary param1: dictionary
return: boolean return: boolean
''' '''
@ -112,78 +71,81 @@ def display_peercert(cert):
print('\t{}'.format(j)) print('\t{}'.format(j))
else: else:
print('\t{}'.format(cert[i])) print('\t{}'.format(cert[i]))
return True
def receive_buffer_is_valid(raw_data): def receive_buffer_is_valid(raw_data):
''' '''
checks, if the received buffer from the connection is valid or not. Checks validity of the received buffer contents.
param 1: byte param 1: byte
return: boolean return: boolean
''' '''
if raw_data == b'\x00' or raw_data == b'\x01': if raw_data in (b'\x00', b'\x01'):
logging.debug('Argument is valid: {}'.format(raw_data)) logging.debug('Argument is valid: {}'.format(raw_data))
return True return True
else:
logging.debug('Argument is not valid: {}'.format(raw_data)) logging.debug('Argument is not valid: {}'.format(raw_data))
return False return False
def change_status(raw_data, api): def change_status(raw_data, api):
''' '''
Becomes the received byte and the path to API file. Grabs the content of Write the new status together with a timestamp into the Space API JSON.
the API with read_api() and replaces "open" and "lastchange". Write all
lines back to API file.
param 1: byte param 1: byte
param 2: string param 2: string
return: boolean return: boolean
''' '''
logging.debug('Change status API') logging.debug('Change status API')
# todo: use walrus operator := when migrating to python >= 3.8
data = read_api(api) data = read_api(api)
if data is not False: if data is False:
status, timestamp = set_values(raw_data) return False
if os.access(api, os.W_OK):
logging.debug('API file is writable') status, timestamp = set_values(raw_data)
with open(api, 'w') as api_file: if os.access(api, os.W_OK):
logging.debug('API file open successfull') logging.debug('API file is writable')
data["state"]["open"] = status with open(api, 'w') as api_file:
data["state"]["lastchange"] = timestamp logging.debug('API file open successfull')
try: data["state"]["open"] = status
json.dump(data, api_file, indent=4) data["state"]["lastchange"] = timestamp
except Exception as e: try:
logging.error('Failed to change API file') json.dump(data, api_file, indent=4)
logging.error('{}'.format(e)) except Exception as e:
logging.debug('API file changed') logging.error('Failed to change API file')
else: logging.error('{}'.format(e))
logging.error('API file is not writable. Wrong permissions?') logging.debug('API file changed')
return False else:
logging.info('Status successfull changed to {}'.format(status)) logging.error('API file is not writable. Wrong permissions?')
return True return False
return False logging.info('Status successfull changed to {}'.format(status))
return True
def read_api(api): def read_api(api):
''' '''
Reads the API file in an buffer und returns the buffer. If anything goes Reads the Space API JSON into a dict. Returns the dict on success and
wrong, it returns False - otherwise it returns the buffer. False on failure.
param 1: string param 1: string
return: string or boolean return: dict or boolean
''' '''
logging.debug('Open API file: {}'.format(api)) logging.debug('Open API file: {}'.format(api))
if os.access(api, os.R_OK):
logging.debug('API is readable') # return early if the API JSON cannot be read
with open(api, 'r') as api_file: if not os.access(api, os.R_OK):
logging.debug('API opened successfull') logging.error('Failed to read API file')
try: return False
api_json_data = json.load(api_file)
logging.debug('API file read successfull') logging.debug('API is readable')
except Exception as e: with open(api, 'r') as api_file:
logging.error('Failed to read API file(): {}'.format(e)) logging.debug('API file successfully opened')
return False try:
return (api_json_data) api_json_data = json.load(api_file)
logging.error('Failed to read API file') logging.debug('API file read successfull')
return False except Exception as e:
logging.error('Failed to read API file: {}'.format(e))
return False
return api_json_data
def set_values(raw_data): def set_values(raw_data):
@ -193,45 +155,21 @@ def set_values(raw_data):
param 1: byte param 1: byte
return: tuple return: tuple
''' '''
status = "true" if raw_data == b'\x01' else "false"
timestamp = str(time()).split('.')[0] timestamp = str(time()).split('.')[0]
if raw_data == b'\x01':
status = "true"
else:
status = "false"
logging.debug('Set values for timestamp: {} and status: {}'.format( logging.debug('Set values for timestamp: {} and status: {}'.format(
timestamp, status)) timestamp, status))
return (status, timestamp) return (status, timestamp)
def read_loglevel(CONFIG):
'''
The function translates the value string from config verbosity option to
a valid logging option.
param1: dictionary
return: boolean or integer
'''
if CONFIG['VERBOSITY'] == 'critical':
loglevel = logging.CRITICAL
elif CONFIG['VERBOSITY'] == 'error':
loglevel = logging.ERROR
elif CONFIG['VERBOSITY'] == 'warning':
loglevel = logging.WARNING
elif CONFIG['VERBOSITY'] == 'info':
loglevel = logging.INFO
elif CONFIG['VERBOSITY'] == 'debug':
loglevel = logging.DEBUG
else:
loglevel = False
return(loglevel)
def main(): def main():
''' '''
The main function - opens a socket, create a ssl context, load certs and The main function - open a socket, create a ssl context, load certs and
listen for connections. at ssl context we set only one available cipher listen for connections. At SSL context we set only one available cipher
suite and disable compression. suite and disable compression.
OP_NO_COMPRESSION: prevention against crime attack OP_NO_COMPRESSION: prevention against CRIME attack
OP_DONT_ISERT_EMPTY_FRAGMENTS: prevention agains cbc 4 attack OP_DONT_ISERT_EMPTY_FRAGMENTS: prevention agains CBC 4 attack
(cve-2011-3389) (cve-2011-3389)
''' '''
@ -239,90 +177,105 @@ def main():
formatstring = '%(asctime)s: %(levelname)s: %(message)s' formatstring = '%(asctime)s: %(levelname)s: %(message)s'
logging.basicConfig(format=formatstring, level=loglevel) logging.basicConfig(format=formatstring, level=loglevel)
CONFIG = { default_config = {
'HOST': 'localhost', 'general': {
'PORT': 10001, 'timeout': 3.0,
'SERVER_CERT': './server.crt', 'loglevel': 'warning'
'SERVER_KEY': './server.key', },
'CLIENT_CERT': './client.crt', 'server': {
'TIMEOUT': 3.0, 'host': 'localhost',
'API': './api', 'port': 10001,
'API_TEMPLATE': './api_template', 'cert': './certs/server.crt',
'VERBOSITY': 'warning' 'key': './certs/server.key'
},
'client': {
'cert': './certs/client.crt'
},
'api': {
'api': './api',
'template': './api_template'
} }
CONFIG_FILE = './statusd.conf' }
read_config(CONFIG_FILE, CONFIG) configfile = './statusd.conf'
loglevel = read_loglevel(CONFIG) config = configparser.ConfigParser()
if loglevel is not False: config.read_dict(default_config)
logger = logging.getLogger() if not config.read(configfile):
logger.setLevel(loglevel) logging.warning('Configuration file %s not found or not readable. Using default values.',
else: configfile)
loglevel = logging.WARNING
logger = logging.getLogger()
logger.setLevel(loglevel)
logging.warning('Invalid value for loglevel. Set default value')
print_config(CONFIG) logger = logging.getLogger()
if not config['general']['loglevel'] in ('critical', 'error', 'warning', 'info', 'debug'):
logging.warning('Invalid loglevel %s given. Using default level %s.',
config['general']['loglevel'],
default_config['general']['loglevel'])
config.set('general', 'loglevel', default_config['general']['loglevel'])
logger.setLevel(config['general']['loglevel'].upper())
print_config(config)
# todo: zertifikate sollten nur lesbar sein! # todo: zertifikate sollten nur lesbar sein!
if not certs_readable(config):
if certs_readable(CONFIG) is False:
logging.error('Cert check failed\nExit') logging.error('Cert check failed\nExit')
exit() sys.exit(1)
context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
context.verify_mode = ssl.CERT_REQUIRED context.verify_mode = ssl.CERT_REQUIRED
context.load_cert_chain(certfile=CONFIG['SERVER_CERT'], context.load_cert_chain(certfile=config['server']['cert'],
keyfile=CONFIG['SERVER_KEY']) keyfile=config['server']['key'])
context.load_verify_locations(cafile=CONFIG['CLIENT_CERT']) context.load_verify_locations(cafile=config['client']['cert'])
context.set_ciphers('EECDH+AESGCM') # only ciphers for tls 1.2 and 1.3 context.set_ciphers('EECDH+AESGCM') # only ciphers for tls 1.2 and 1.3
context.options = ssl.OP_CIPHER_SERVER_PREFERENCE context.options = ssl.OP_CIPHER_SERVER_PREFERENCE
# ssl + kompression = schlecht # ensure, compression is disabled (disabled by default anyway at the moment)
context.options |= getattr(ssl._ssl, 'OP_NO_COMPRESSION', 0) context.options |= ssl.OP_NO_COMPRESSION
logging.debug('SSL context created') logging.debug('SSL context created')
# print_ciphers(context.get_ciphers()) # print_ciphers(context.get_ciphers())
with socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0) as mySocket: with socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0) as mySocket:
logging.debug('Socket created') logging.debug('Socket created')
try: try:
mySocket.bind((CONFIG['HOST'], int(CONFIG['PORT']))) mySocket.bind((config['server']['host'], int(config['server']['port'])))
mySocket.listen(5) mySocket.listen(5)
logging.info('Listen on {} at Port {}'.format(CONFIG['HOST'], logging.info('Listening on {} at Port {}'.format(config['server']['host'],
CONFIG['PORT'])) config['server']['port']))
except Exception as e: except Exception as e:
logging.error('unable to bind and listen') logging.error('Unable to bind and listen')
logging.error('{}'.format(e)) logging.error('{}'.format(e))
exit() sys.exit(1)
while True: while True:
try: try:
fromSocket, fromAddr = mySocket.accept() fromSocket, fromAddr = mySocket.accept()
logging.info('Client connected: {}:{}'.format(fromAddr[0], logging.info('Client connected: {}:{}'.format(fromAddr[0], fromAddr[1]))
fromAddr[1]))
try: try:
fromSocket.settimeout(float(CONFIG['TIMEOUT'])) fromSocket.settimeout(float(config['general']['timeout']))
logging.debug('Connection timeout set to {}'.format( logging.debug('Connection timeout set to {}'.format(
CONFIG['TIMEOUT'])) config['general']['timeout']))
except Exception: except Exception:
logging.error('Canot set timeout to {}'.format( logging.error('Cannot set timeout to {}'.format(
CONFIG['TIMEOUT'])) config['general']['timeout']))
logging.error('Use default value: 3.0') logging.error('Using default value {}'.format(
fromSocket.settimeout(3.0) config['general']['timeout']))
fromSocket.settimeout(config['general']['timeout'])
try: try:
conn = context.wrap_socket(fromSocket, server_side=True) conn = context.wrap_socket(fromSocket, server_side=True)
conn.settimeout(3.0) conn.settimeout(config['general']['timeout'])
# display_peercert(conn.getpeercert()) # display_peercert(conn.getpeercert())
logging.debug('Connection established') logging.debug('Connection established')
logging.debug('Peer certificate commonName: {}'.format logging.debug('Peer certificate commonName: {}'.format(
(conn.getpeercert()['subject'][5][0][1])) conn.getpeercert()['subject'][5][0][1]))
logging.debug('Peer certificate serialNumber: {}'.format logging.debug('Peer certificate serialNumber: {}'.format(
(conn.getpeercert()['serialNumber'])) conn.getpeercert()['serialNumber']))
except socket.timeout: except socket.timeout:
logging.error('Socket timeout') logging.error('Socket timeout')
except Exception as e: except Exception as e:
logging.error('Connection failed: {}'.format(e)) logging.error('Connection failed: {}'.format(e))
raw_data = conn.recv(1) raw_data = conn.recv(1)
if receive_buffer_is_valid(raw_data) is True: if receive_buffer_is_valid(raw_data) is True:
if change_status(raw_data, CONFIG['API']) is True: if change_status(raw_data, config['api']['api']) is True:
logging.debug('Send {} back'.format(raw_data)) logging.debug('Send {} back'.format(raw_data))
conn.send(raw_data) conn.send(raw_data)
# change_status returns false: # change_status returns false:
@ -332,15 +285,14 @@ def main():
conn.send(b'\x03') conn.send(b'\x03')
# receive_handle returns false: # receive_handle returns false:
else: else:
logging.info('Invalid argument recived: {}'.format( logging.info('Invalid argument received: {}'.format(raw_data))
raw_data))
logging.debug('Send {} back'.format(b'\x03')) logging.debug('Send {} back'.format(b'\x03'))
if conn: if conn:
conn.send(b'\x03') conn.send(b'\x03')
sleep(0.1) # protection against dos sleep(0.1) # protection against dos
except KeyboardInterrupt: except KeyboardInterrupt:
logging.info('Exit') logging.info('Exit')
exit() sys.exit(1)
except Exception as e: except Exception as e:
logging.error('{}'.format(e)) logging.error('{}'.format(e))
continue continue