1
0
mirror of https://github.com/binary-kitchen/doorlockd synced 2024-12-22 10:24:26 +01:00

pydoorlock: WebApp: outsource WebApp

Signed-off-by: Ralf Ramsauer <ralf@binary-kitchen.de>
This commit is contained in:
Ralf Ramsauer 2018-10-08 03:11:24 +02:00
parent 2cdf9d1b27
commit 5b296b777d
3 changed files with 153 additions and 141 deletions

154
doorlockd
View File

@ -22,20 +22,14 @@ import sys
from configparser import ConfigParser from configparser import ConfigParser
from enum import Enum from enum import Enum
from os.path import join from os.path import abspath, join
from serial import Serial from serial import Serial
from subprocess import Popen from subprocess import Popen
from threading import Thread from threading import Thread
from time import sleep from time import sleep
from flask import abort, Flask, jsonify, render_template, request
from flask_socketio import SocketIO
from flask_wtf import FlaskForm
from wtforms import PasswordField, StringField, SubmitField
from wtforms.validators import DataRequired, Length
from pydoorlock.Authenticator import Authenticator, AuthMethod, AuthenticationResult from pydoorlock.Authenticator import Authenticator, AuthMethod, AuthenticationResult
from pydoorlock.WebApp import AuthenticationForm from pydoorlock.WebApp import webapp_run, emit_doorstate
from pydoorlock.Door import DoorState from pydoorlock.Door import DoorState
SYSCONFDIR = '.' SYSCONFDIR = '.'
@ -43,10 +37,11 @@ PREFIX = '.'
root_prefix = join(PREFIX, 'share', 'doorlockd') root_prefix = join(PREFIX, 'share', 'doorlockd')
sounds_prefix = join(root_prefix, 'sounds') sounds_prefix = join(root_prefix, 'sounds')
static_folder = join(root_prefix, 'static')
template_folder = join(root_prefix, 'templates')
scripts_prefix = join(root_prefix, 'scripts') scripts_prefix = join(root_prefix, 'scripts')
static_folder = abspath(join(root_prefix, 'static'))
template_folder = abspath(join(root_prefix, 'templates'))
__author__ = 'Ralf Ramsauer' __author__ = 'Ralf Ramsauer'
__copyright = 'Copyright (c) Ralf Ramsauer, 2018' __copyright = 'Copyright (c) Ralf Ramsauer, 2018'
__license__ = 'GPLv2' __license__ = 'GPLv2'
@ -60,9 +55,6 @@ date_fmt = '%Y-%m-%d %H:%M:%S'
log_fmt = '%(asctime)-15s %(levelname)-8s %(message)s' log_fmt = '%(asctime)-15s %(levelname)-8s %(message)s'
log = logging.getLogger() log = logging.getLogger()
webapp = Flask(__name__)
socketio = SocketIO(webapp, async_mode='threading')
class Config: class Config:
config_topic = 'doorlock' config_topic = 'doorlock'
@ -80,17 +72,11 @@ class Config:
cfg = Config(SYSCONFDIR) cfg = Config(SYSCONFDIR)
# Booleans # Booleans
debug = cfg.boolean('DEBUG')
simulate_serial = cfg.boolean('SIMULATE_SERIAL') simulate_serial = cfg.boolean('SIMULATE_SERIAL')
run_hooks = cfg.boolean('RUN_HOOKS') run_hooks = cfg.boolean('RUN_HOOKS')
sounds = cfg.boolean('SOUNDS') sounds = cfg.boolean('SOUNDS')
serial_port = cfg.str('SERIAL_PORT') serial_port = cfg.str('SERIAL_PORT')
room = cfg.str('ROOM')
title = cfg.str('TITLE')
welcome = cfg.str('WELCOME')
html_title = '%s (%s - v%s)' % (title, __status__, __version__)
wave_emergency = 'emergency_unlock.wav' wave_emergency = 'emergency_unlock.wav'
@ -211,18 +197,18 @@ class DoorHandler:
if rx == DoorHandler.BUTTON_CLOSE: if rx == DoorHandler.BUTTON_CLOSE:
self.close() self.close()
log.info('Closed due to Button press') log.info('Closed due to Button press')
logic.emit_status(LogicResponse.ButtonLock) #emit_status(LogicResponse.ButtonLock)
elif rx == DoorHandler.BUTTON_OPEN: elif rx == DoorHandler.BUTTON_OPEN:
self.open() self.open()
log.info('Opened due to Button press') log.info('Opened due to Button press')
logic.emit_status(LogicResponse.ButtonUnlock) #emit_status(LogicResponse.ButtonUnlock)
elif rx == DoorHandler.BUTTON_PRESENT: elif rx == DoorHandler.BUTTON_PRESENT:
self.present() self.present()
log.info('Present due to Button press') log.info('Present due to Button press')
logic.emit_status(LogicResponse.ButtonPresent) #emit_status(LogicResponse.ButtonPresent)
elif rx == DoorHandler.CMD_EMERGENCY_SWITCH: elif rx == DoorHandler.CMD_EMERGENCY_SWITCH:
log.warning('Emergency unlock') log.warning('Emergency unlock')
logic.emit_status(LogicResponse.EmergencyUnlock) #emit_status(LogicResponse.EmergencyUnlock)
else: else:
log.error('Received unknown message "%s" from AVR' % rx) log.error('Received unknown message "%s" from AVR' % rx)
@ -268,11 +254,12 @@ class DoorHandler:
def request(self, state): def request(self, state):
if state == DoorState.Closed: if state == DoorState.Closed:
return self.close() err = self.close()
elif state == DoorState.Present: elif state == DoorState.Present:
return self.present() err = self.present()
elif state == DoorState.Open: elif state == DoorState.Open:
return self.open() err = self.open()
emit_doorstate()
class Logic: class Logic:
@ -291,118 +278,13 @@ class Logic:
err = self._request(state, credentials) err = self._request(state, credentials)
if err == LogicResponse.Success or err == LogicResponse.AlreadyActive: if err == LogicResponse.Success or err == LogicResponse.AlreadyActive:
sound_helper(old_state, self.door_handler.state, False) sound_helper(old_state, self.door_handler.state, False)
self.emit_status(err)
return err return err
def emit_status(self, message=None):
led = self.state.to_img()
if message is None:
message = self.state.to_html()
else:
message = str(message)
socketio.emit('status', {'led': led, 'message': message})
@property @property
def state(self): def state(self):
return self.door_handler.state return self.door_handler.state
@socketio.on('request_status')
@socketio.on('connect')
def on_connect():
logic.emit_status()
@webapp.route('/display')
def display():
return render_template('display.html',
room=room,
title=title,
welcome=welcome)
@webapp.route('/api', methods=['POST'])
def api():
def json_response(response, msg=None):
json = dict()
json['err'] = response.value
json['msg'] = response.to_html() if msg is None else msg
if response == LogicResponse.Success or \
response == LogicResponse.AlreadyActive:
# TBD: Remove 'open'. No more users. Still used in App Version 2.1.1!
json['open'] = logic.state.is_open()
json['status'] = logic.state.value
return jsonify(json)
method = request.form.get('method')
user = request.form.get('user')
password = request.form.get('pass')
command = request.form.get('command')
if method == 'local':
method = AuthMethod.LOCAL_USER_DB
else: # 'ldap' or default
method = AuthMethod.LDAP_USER_PW
if any(v is None for v in [user, password, command]):
log.warning('Incomplete API request')
abort(400)
if not user or not password:
log.warning('Invalid username or password format')
return json_response(LogicResponse.Inval,
'Invalid username or password format')
credentials = method, user, password
if command == 'status':
return json_response(logic.try_auth(credentials))
desired_state = DoorState.from_string(command)
if not desired_state:
return json_response(LogicResponse.Inval, "Invalid command requested")
log.info('Incoming API request from %s' % user.encode('utf-8'))
log.info(' desired state: %s' % desired_state)
log.info(' current state: %s' % logic.state)
response = logic.request(desired_state, credentials)
return json_response(response)
@webapp.route('/', methods=['GET', 'POST'])
def home():
authentication_form = AuthenticationForm()
response = None
if request.method == 'POST' and authentication_form.validate():
user = authentication_form.username.data
password = authentication_form.password.data
method = authentication_form.method
credentials = method, user, password
log.info('Incoming request from %s' % user.encode('utf-8'))
log.info(' authentication method: %s' % method)
desired_state = authentication_form.desired_state
log.info(' desired state: %s' % desired_state)
log.info(' current state: %s' % logic.state)
response = logic.request(desired_state, credentials)
log.info(' response: %s' % response)
# Don't trust python, zero credentials
user = password = credentials = None
return render_template('index.html',
authentication_form=authentication_form,
auth_backends=logic.auth.backends,
response=response,
state_text=logic.state.to_html(),
led=logic.state.to_img(),
banner='%s - %s' % (title, room))
if __name__ == '__main__': if __name__ == '__main__':
logging.basicConfig(level=log_level, stream=sys.stdout, logging.basicConfig(level=log_level, stream=sys.stdout,
format=log_fmt, datefmt=date_fmt) format=log_fmt, datefmt=date_fmt)
@ -411,13 +293,7 @@ if __name__ == '__main__':
logic = Logic() logic = Logic()
host = 'localhost' webapp_run(cfg, logic, __status__, __version__, template_folder,
if debug: static_folder)
host = '0.0.0.0'
webapp.config['SECRET_KEY'] = cfg.str('SECRET_KEY')
webapp.template_folder=template_folder
webapp.static_folder=static_folder
socketio.run(webapp, host=host, port=8080, use_reloader=False, debug=debug)
sys.exit(0) sys.exit(0)

View File

@ -1,7 +1,6 @@
from enum import Enum from enum import Enum
class DoorState(Enum): class DoorState(Enum):
# These numbers are used by the App since version 3.0, do NOT change them
Open = 0 Open = 0
Present = 1 Present = 1
Closed = 2 Closed = 2
@ -29,7 +28,7 @@ class DoorState(Enum):
led = 'green' led = 'green'
return 'static/led-%s.png' % led return 'static/led-%s.png' % led
def to_html(self): def __str__(self):
if self == DoorState.Open: if self == DoorState.Open:
return 'Offen' return 'Offen'
elif self == DoorState.Present: elif self == DoorState.Present:

View File

@ -1,3 +1,7 @@
import logging
from flask import abort, Flask, jsonify, render_template, request
from flask_socketio import SocketIO
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
from wtforms import PasswordField, StringField, SubmitField from wtforms import PasswordField, StringField, SubmitField
from wtforms.validators import DataRequired, Length from wtforms.validators import DataRequired, Length
@ -5,6 +9,17 @@ from wtforms.validators import DataRequired, Length
from .Door import DoorState from .Door import DoorState
from .Authenticator import AuthMethod from .Authenticator import AuthMethod
log = logging.getLogger()
webapp = Flask(__name__)
socketio = SocketIO(webapp, async_mode='threading')
def emit_doorstate():
state = logic.state
socketio.emit('status', {'led': state.to_img(), 'message': str(state)})
class AuthenticationForm(FlaskForm): class AuthenticationForm(FlaskForm):
username = StringField('Username', [Length(min=3, max=25)]) username = StringField('Username', [Length(min=3, max=25)])
password = PasswordField('Password', [DataRequired()]) password = PasswordField('Password', [DataRequired()])
@ -32,3 +47,125 @@ class AuthenticationForm(FlaskForm):
self.method = AuthMethod.LDAP_USER_PW self.method = AuthMethod.LDAP_USER_PW
return True return True
@socketio.on('request_status')
@socketio.on('connect')
def on_connect():
emit_doorstate()
@webapp.route('/display')
def display():
return render_template('display.html',
room=room,
title=title,
welcome=welcome)
@webapp.route('/api', methods=['POST'])
def api():
def json_response(response, msg=None):
json = dict()
json['err'] = response.value
json['msg'] = response.to_html() if msg is None else msg
if response == LogicResponse.Success or \
response == LogicResponse.AlreadyActive:
# TBD: Remove 'open'. No more users. Still used in App Version 2.1.1!
json['open'] = logic.state.is_open()
json['status'] = logic.state.value
return jsonify(json)
method = request.form.get('method')
user = request.form.get('user')
password = request.form.get('pass')
command = request.form.get('command')
if method == 'local':
method = AuthMethod.LOCAL_USER_DB
else: # 'ldap' or default
method = AuthMethod.LDAP_USER_PW
if any(v is None for v in [user, password, command]):
log.warning('Incomplete API request')
abort(400)
if not user or not password:
log.warning('Invalid username or password format')
return json_response(LogicResponse.Inval,
'Invalid username or password format')
credentials = method, user, password
if command == 'status':
return json_response(logic.try_auth(credentials))
desired_state = DoorState.from_string(command)
if not desired_state:
return json_response(LogicResponse.Inval, "Invalid command requested")
log.info('Incoming API request from %s' % user.encode('utf-8'))
log.info(' desired state: %s' % desired_state)
log.info(' current state: %s' % logic.state)
response = logic.request(desired_state, credentials)
return json_response(response)
@webapp.route('/', methods=['GET', 'POST'])
def home():
authentication_form = AuthenticationForm()
response = None
if request.method == 'POST' and authentication_form.validate():
user = authentication_form.username.data
password = authentication_form.password.data
method = authentication_form.method
credentials = method, user, password
log.info('Incoming request from %s' % user.encode('utf-8'))
log.info(' authentication method: %s' % method)
desired_state = authentication_form.desired_state
log.info(' desired state: %s' % desired_state)
log.info(' current state: %s' % logic.state)
response = logic.request(desired_state, credentials)
log.info(' response: %s' % response)
# Don't trust python, zero credentials
user = password = credentials = None
return render_template('index.html',
authentication_form=authentication_form,
auth_backends=logic.auth.backends,
response=response,
state_text=str(logic.state),
led=logic.state.to_img(),
banner='%s - %s' % (title, room))
def webapp_run(cfg, my_logic, status, version, template_folder, static_folder):
global logic
logic = my_logic
debug = cfg.boolean('DEBUG')
host = 'localhost'
if debug:
host = '0.0.0.0'
global room
room = cfg.str('ROOM')
global title
title = cfg.str('TITLE')
global welcome
welcome = cfg.str('WELCOME')
global html_title
html_title = '%s (%s - v%s)' % (title, status, version)
webapp.config['SECRET_KEY'] = cfg.str('SECRET_KEY')
webapp.template_folder = template_folder
webapp.static_folder = static_folder
socketio.run(webapp, host=host, port=8080, use_reloader=False, debug=debug)