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
# from krautspace jena.
# host, where server lives (string with fqdn or ipv4). default ist
# localhost.
HOST = '127.0.0.1'
[general]
timeout = 5.0
loglevel = debug
# port, where the server is listen. default is 100001
PORT = 10001
[server]
host = localhost
port = 10001
cert = ./certs/server.crt
key = ./certs/server.key
# timeout for connection
TIMEOUT = 5
[client]
cert = ./certs/client.crt
# path for ssl keys and certificates. default is the current directory.
SERVER_CERT = './certs/server.crt'
SERVER_KEY = './certs/server.key'
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'
[api]
api = ./api
template = ./api_template

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