Move server sources to separate directory

This commit is contained in:
Philipp Matthias Schaefer 2020-11-18 21:59:23 +01:00
parent e5109815e7
commit ec3a5daf2b
9 changed files with 0 additions and 0 deletions

1
source/server/api Normal file
View file

@ -0,0 +1 @@
{"api": "0.13", "space": "Krautspace", "url": "https://kraut.space", "logo": "https://status.krautspace.de/images/krautspace_pixelbanner.png", "location": {"address": "Hackspace Jena e. V., Krautgasse 26, 07743 Jena, Germany", "lat": 50.9292, "lon": 11.5826}, "state": {"open": "true", "lastchange": "1564184086", "icon": {"open": "https://status.krautspace.de/images/krautspace_pixelicon_open.png", "closed": "https://status.krautspace.de/images/krautspace_pixelicon_closed.png"}}, "feeds": {"calendar": {"type": "ical", "url": "https://calcifer.datenknoten.me/tags/krautspace.ics"}}, "contact": {"twitter": "@HackspaceJena", "quitter": "@Krautspace", "facebook": "https://www.facebook.com/HackspaceJena", "email": "office@krautspace.de"}, "issue_report_channels": ["twitter", "email"], "projects": ["https://github.com/HackspaceJena/"]}

View file

@ -0,0 +1,38 @@
{
"api":"0.13",
"space":"Krautspace",
"url":"https://kraut.space",
"logo":"https://status.krautspace.de/images/krautspace_pixelbanner.png",
"location":{
"address":"Hackspace Jena e. V., Krautgasse 26, 07743 Jena, Germany",
"lat":50.9292,
"lon":11.5826
},
"state":{
"open":false,
"lastchange":1563499131,
"icon":{
"open":"https://status.krautspace.de/images/krautspace_pixelicon_open.png",
"closed":"https://status.krautspace.de/images/krautspace_pixelicon_closed.png"
}
},
"feeds":{
"calendar":{
"type":"ical",
"url":"https://calcifer.datenknoten.me/tags/krautspace.ics"
}
},
"contact":{
"twitter":"@HackspaceJena",
"quitter":"@Krautspace",
"facebook":"https://www.facebook.com/HackspaceJena",
"email":"office@krautspace.de"
},
"issue_report_channels":[
"twitter",
"email"
],
"projects":[
"https://github.com/HackspaceJena/"
]
}

View file

@ -0,0 +1,25 @@
# file: statusd.conf
# Statusd.conf is part of doorstatus - a programm to change the krautspaces
# doorstatus. This is the configuration file for the server, who is manage
# the api for door status from krautspace jena.
# Set [server][host] to localhost or 127.0.0.1 if you want listen only to
# localhost.
[general]
timeout = 5.0
loglevel = debug
[server]
host = localhost
port = 10001
cert = ./certs/server-pub.pem
key = ./certs/server-key.pem
[client]
cert = ./certs/client-pub.pem
[api]
api = ./api
template = ./api_template

304
source/server/apistatusd.py Executable file
View file

@ -0,0 +1,304 @@
#!/usr/bin/python3
# file: statusd.py
# date: 26.07.2019
# email: berhsi@web.de
# 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 sys
from time import time, sleep
import configparser
def certs_readable(config):
'''
checks at start, if the needed certificates defined (no nullstring) and
readable.
param 1: dictionary
return: boolean
'''
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('Cannot read {}'.format(i))
return False
return True
def print_config(config):
'''
Logs the config with level debug.
'''
logging.debug('Using config:')
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):
'''
Prints the list of allowed ciphers.
param1: dictionary
return: boolean
'''
print('Available ciphers')
for i in cipherlist:
print('\n')
for j in i.keys():
print('{}: {}'.format(j, i[j]))
print('\n')
def display_peercert(cert):
'''
Displays the values of a given certificate.
param1: dictionary
return: boolean
'''
for i in cert.keys():
print('{}:'.format(i))
if i in ('subject', 'issuer'):
for j in cert[i]:
print('\t{}'.format(j))
else:
print('\t{}'.format(cert[i]))
def receive_buffer_is_valid(raw_data):
'''
Checks validity of the received buffer contents.
param 1: byte
return: boolean
'''
if raw_data in (b'\x00', b'\x01'):
logging.debug('Argument is valid: {}'.format(raw_data))
return True
logging.debug('Argument is not valid: {}'.format(raw_data))
return False
def change_status(raw_data, api):
'''
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 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 Space API JSON into a dict. Returns the dict on success and
False on failure.
param 1: string
return: dict or boolean
'''
logging.debug('Open API file: {}'.format(api))
# 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):
'''
Create a timestamp, changes the value of the given byte into a string
and returns both.
param 1: byte
return: tuple
'''
status = "true" if raw_data == b'\x01' else "false"
timestamp = str(time()).split('.')[0]
logging.debug('Set values for timestamp: {} and status: {}'.format(
timestamp, status))
return (status, timestamp)
def main():
'''
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
(cve-2011-3389)
'''
loglevel = logging.WARNING
formatstring = '%(asctime)s: %(levelname)s: %(message)s'
logging.basicConfig(format=formatstring, level=loglevel)
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'
}
}
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)
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 not certs_readable(config):
logging.error('Cert check failed\nExit')
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.set_ciphers('EECDH+AESGCM') # only ciphers for tls 1.2 and 1.3
context.options = ssl.OP_CIPHER_SERVER_PREFERENCE
# 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')
mySocket.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
keep = mySocket.getsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE)
logging.debug('Socket keepalive: {}'format(keep))
try:
mySocket.bind((config['server']['host'], int(config['server']['port'])))
mySocket.listen(5)
except Exception as e:
logging.error('Unable to bind and listen')
logging.error('{}'.format(e))
sys.exit(1)
logging.info('Listening on {} at Port {}'.format(config['server']['host'],
config['server']['port']))
while True:
try:
fromSocket, fromAddr = mySocket.accept()
logging.info('Client connected: {}:{}'.format(fromAddr[0], fromAddr[1]))
try:
fromSocket.settimeout(float(config['general']['timeout']))
logging.debug('Connection timeout set to {}'.format(
config['general']['timeout']))
except Exception:
logging.error('Cannot set timeout to {}'.format(
config['general']['timeout']))
try:
conn = context.wrap_socket(fromSocket, server_side=True)
conn.settimeout(float(config['general']['timeout']))
except socket.timeout:
logging.error('Socket timeout')
except Exception as e:
logging.error('Connection failed: {}'.format(e))
logging.info('Connection established')
logging.info('Peer certificate commonName: {}'.format(
conn.getpeercert()['subject'][5][0][1]))
logging.debug('Peer certificate serialNumber: {}'.format(
conn.getpeercert()['serialNumber']))
raw_data = conn.recv(1)
if receive_buffer_is_valid(raw_data) 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:
else:
logging.info('Failed to change status')
if conn:
conn.send(b'\x03')
# receive_handle returns false:
else:
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('Keyboard interrupt received')
sys.exit(1)
except Exception as e:
logging.error('{}'.format(e))
continue
finally:
if mySocket:
logging.info('Shutdown socket')
mySocket.shutdown(socket.SHUT_RDWR)
return 0
if __name__ == '__main__':
main()

View file

@ -0,0 +1,20 @@
# file: setstatus.conf
# Setstatus.conf is part of doorstatus - a programm to change the krautspaces
# doorstatus. This is the configuration file for the client who triggers the
# change.
[general]
timeout = 3.0
loglevel = debug
[server]
host = status.kraut.space
port = 10001
cert = ./certs/statusd-pub.pem
fqdn = status.kraut.space
[client]
cert = ./certs/statusclient-pub.pem
key = ./certs/statusclient-key.pem

235
source/server/setstatus.py Executable file
View file

@ -0,0 +1,235 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# file: setstatus.py
# date: 26.07.2019
# email: berhsi@web.de
# Setstatus.py is part of doorstatus - a programm to deal with the
# krautspaces doorstatus.
# client, who connects to the statusserver at port 10001 to update the
# krautspace door status. If no status is given as argument, he reads from
# stdin until input is 0 or 1.
import os
import ssl
import socket
import logging
import configparser
from time import sleep
from sys import exit, argv
# basic configuration
loglevel = logging.WARNING
formatstring = '%(asctime)s: %(levelname)s: %(message)s'
logging.basicConfig(format=formatstring, level=loglevel)
class SetStatus:
def __init__(self):
"""
"""
self.status = None
self.config = None
self.log = None
self.context = None
self.connection = None
self.configfile = './setstatus.conf'
self.config = configparser.ConfigParser()
self.default_config = {
'general': {
'timeout': 5.0,
'loglevel': 'warning'
},
'server': {
'host': 'localhost',
'port': 10001,
'cert': './certs/server-pub.pem',
'fqdn': 'kraut.space'
},
'client': {
'cert': './certs/client-pub.pem',
'key': './certs/client-key.pem'
}
}
def check_status(self):
"""
return: boolean
"""
if self.status in (0, 1):
self.log.debug('Set value to {}'.format(self.status))
self.status = bytes([self.status])
return True
return False
def set_config(self):
"""
"""
self.log = logging.getLogger()
# read config file
self.config.read_dict(self.default_config)
if not self.config.read(self.configfile):
self.log.warning('Configuration file {} not found or not '
' readable.'.format(self.configfile))
self.log.warning('Using default values.')
# set loglevel
config_loglevel = self.config['general']['loglevel']
default_loglevel = self.default_config['general']['loglevel']
if not config_loglevel.lower() in ('critical', 'error','warning',
'info', 'debug'):
self.log.warning('Invalid loglevel {} given. Using default '
'level {}.'.format(config_loglevel,
default_loglevel))
self.config.set('general', 'loglevel', default_loglevel)
self.log.setLevel(config_loglevel.upper())
def check_certs(self, certs):
"""
Check if certs readable.
"""
self.log.debug('Check certificates')
for certfile in certs:
if os.access(certfile, os.R_OK) is False:
self.log.error('Failed to read certificate: {}'.format( \
certfile))
return False
return True
def log_config(self):
"""
Logs the config if loglevel is debug.
"""
logging.debug('Using config:')
for section in self.config.sections():
logging.debug('Section {}'.format(section))
for i in self.config[section]:
logging.debug(' {}: {}'.format(i, self.config[section][i]))
def create_ssl_context(self):
"""
"""
context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH,
cafile=self.config['server']['cert'])
if not context:
self.log.error('Failed to create SSL Context')
return False
context.set_ciphers('EECDH+AESGCM') # only ciphers for tls 1.2 and 1.3
context.options |= getattr(ssl._ssl, 'OP_NO_COMPRESSION', 0)
try:
context.load_cert_chain(certfile=self.config['client']['cert'],
keyfile=self.config['client']['key'])
except Exception as e:
self.log.error('Failed to load cert chain')
return False;
self.log.debug('SSL context created')
return context
def create_ssl_socket(self, config, context):
"""
"""
bare_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
if not bare_socket:
logging.error('Socket creation failed')
return False
logging.debug('Socket created')
try:
secure_socket = context.wrap_socket(bare_socket, server_side=False,
server_hostname=config['server']['fqdn'])
logging.debug('Socket wrapped with SSL')
except Exception as e:
logging.error('Context wrapper failed: {}'.format(e))
return False
try:
secure_socket.settimeout(float(config['general']['timeout']))
except Exception as e:
logging.debug('Failed to set timeout: {}'.format(e))
return secure_socket
def create_ssl_connection(self):
"""
"""
counter = 0
conn = False
while conn is False and counter < 5:
ssl_socket = self.create_ssl_socket(self.config, self.context)
if ssl_socket is False:
exit(4)
try:
self.log.debug('Connect {}: {}'.format(self.config['server']['host'],
int(self.config['server']['port'])))
conn = ssl_socket.connect((self.config['server']['host'],
int(self.config['server']['port'])))
except socket.timeout:
self.log.error('Connection timeout')
ssl_socket.close()
sleep(5)
counter += 1
except Exception as e:
self.log.error('Connection failed: {}'.format(e))
exit(5)
self.log.debug('Conection established')
self.log.debug('Peer certificate commonName: {}'.format(
ssl_socket.getpeercert()['subject'][5][0][1]))
self.log.debug('Peer certificate serialNumber: {}'.format(
ssl_socket.getpeercert()['serialNumber']))
return ssl_socket
def run(self, status):
"""
starts the engine.
param 1: integer
"""
self.status = status
# read config
self.set_config()
# check given status
if self.check_status() is False:
self.log.error('No valid status given')
exit(1)
# log config if level is debug
if self.config['general']['loglevel'].lower() == 'debug':
self.log_config()
# check certificates are readable
certs = (self.config['server']['cert'],
self.config['client']['cert'],
self.config['client']['key'])
if self.check_certs(certs) is False:
exit(2)
# create ssl context
self.context = self.create_ssl_context()
if self.context is False:
exit(3)
# get connection
self.connection = self.create_ssl_connection()
# send status
try:
self.log.debug('Send new status: {}'.format(self.status))
self.connection.send(self.status)
except Exception as e:
self.log.error('Error: {}'.format(e))
exit(6)
try:
response = self.connection.recv(1)
self.log.debug('Server returns: {}'.format(response))
if response == self.status:
self.log.info('Status sucessfull updated')
else:
self.log.error('Failed to update status')
self.log.debug('Disconnect from server')
except Exception as e:
self.log.error('Error: {}'.format(e))
exit(7)

View file

@ -0,0 +1,14 @@
[Unit]
Description=Deamon for setting status API
After=systemd-network.service network.target
[Service]
Type=simple
WorkingDirectory=/opt/doorstatus/
ExecStart=/opt/doorstatus/statusd.py
SyslogIdentifier=doorstatus
User=doorstatus
Group=doorstatus
[Install]
WantedBy=multi-user.target

54
source/server/watchdoor.py Executable file
View file

@ -0,0 +1,54 @@
#!/usr/bin/env python
#coding: utf8
# file: watchdoor.py
# date: 06.09.2020
# mail: berhsi@web.de
# desc: deamon who deals with edge detection at pin 18
import logging
loglevel = logging.INFO
formatstring = '%(asctime)s: %(levelname)s: %(message)s'
logging.basicConfig(format=formatstring, level=loglevel)
import time
import RPi.GPIO as GPIO
try:
from setstatus import SetStatus
except Exception as e:
logging.error('Cant import module setstatus')
logging.error('Error: {}'.format(e))
GPIO.setmode(GPIO.BOARD) # kind of enumeration
GPIO.setup(18, GPIO.IN, pull_up_down = GPIO.PUD_DOWN) # pin 18 (GPIO 24)
channel = 18
def voltage_changed(channel):
'''
callback-function
'''
if GPIO.input(channel) == GPIO.HIGH:
# door closed -> pin hight
logging.info('Pin high triggered - Space closed')
s = SetStatus()
s.run(0)
else:
# door open -> pin low
logging.info('Pin low triggered - Space is open')
s = SetStatus()
s.run(1)
def main():
'''
'''
logging.info('watchdoor.py started')
GPIO.add_event_detect(channel,
GPIO.BOTH,
callback = voltage_changed,
bouncetime = 200)
while 1:
time.sleep(0.1)
if __name__ == '__main__':
main()

View file

@ -0,0 +1,14 @@
[Unit]
Description=Deamon for setting status API
After=systemd-network.service network.target
[Service]
Type=simple
WorkingDirectory=/opt/watchdoor/
ExecStart=/opt/watchdoor/watchdoor.py
SyslogIdentifier=watchdoor
User=statusd
Group=statusd
[Install]
WantedBy=multi-user.target