diff --git a/doorlockd b/doorlockd index b674346..ceddb7a 100755 --- a/doorlockd +++ b/doorlockd @@ -17,14 +17,11 @@ FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. """ -import hashlib -import ldap import logging import sys from enum import Enum from os.path import join -from random import sample from serial import Serial from subprocess import Popen from threading import Thread @@ -36,6 +33,8 @@ from flask_wtf import FlaskForm from wtforms import PasswordField, StringField, SubmitField from wtforms.validators import DataRequired, Length +from pydoorlock.Authenticator import Authenticator, AuthMethod, AuthenticationResult + SYSCONFDIR = '.' PREFIX = '.' @@ -66,7 +65,7 @@ webapp = Flask(__name__, webapp.config.from_pyfile(flask_config) socketio = SocketIO(webapp, async_mode='threading') serial_port = webapp.config.get('SERIAL_PORT') -simulate_ldap = webapp.config.get('SIMULATE_LDAP') +simulate_auth = webapp.config.get('SIMULATE_AUTH') simulate_serial = webapp.config.get('SIMULATE_SERIAL') run_hooks = webapp.config.get('RUN_HOOKS') room = webapp.config.get('ROOM') @@ -76,9 +75,6 @@ file_local_db = webapp.config.get('LOCAL_USER_DB') html_title = '%s (%s - v%s)' % (title, __status__, __version__) -ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_DEMAND) -ldap.set_option(ldap.OPT_REFERRALS, 0) - ldap_uri = webapp.config.get('LDAP_URI') ldap_binddn = webapp.config.get('LDAP_BINDDN') @@ -101,32 +97,6 @@ host = 'localhost' if webapp.config.get('DEBUG'): host = '0.0.0.0' -# copied from sudo -eperm_insults = { - 'Wrong! You cheating scum!', - 'And you call yourself a Rocket Scientist!', - 'No soap, honkie-lips.', - 'Where did you learn to type?', - 'Are you on drugs?', - 'My pet ferret can type better than you!', - 'You type like i drive.', - 'Do you think like you type?', - 'Your mind just hasn\'t been the same since the electro-shock, has it?', - 'Maybe if you used more than just two fingers...', - 'BOB says: You seem to have forgotten your passwd, enter another!', - 'stty: unknown mode: doofus', - 'I can\'t hear you -- I\'m using the scrambler.', - 'The more you drive -- the dumber you get.', - 'Listen, broccoli brains, I don\'t have time to listen to this trash.', - 'I\'ve seen penguins that can type better than that.', - 'Have you considered trying to match wits with a rutabaga?', - 'You speak an infinite deal of nothing', -} - - -def choose_insult(): - return(sample(eperm_insults, 1)[0]) - def playsound(filename): if not sounds: @@ -162,11 +132,6 @@ def sound_helper(old_state, new_state, button): playsound(wave_lock) -class AuthMethod(Enum): - LDAP_USER_PW = 1 - LOCAL_USER_DB = 2 - - class DoorState(Enum): # These numbers are used by the App since version 3.0, do NOT change them Open = 0 @@ -211,14 +176,13 @@ class LogicResponse(Enum): # don't break old apps, value 3 is reserved now RESERVED = 3 Inval = 4 - LDAP = 5 EmergencyUnlock = 10, ButtonLock = 11, ButtonUnlock = 12, ButtonPresent = 13, - def to_html(self): + def __str__(self): if self == LogicResponse.Success: return 'Yo, passt.' elif self == LogicResponse.Perm: @@ -227,8 +191,6 @@ class LogicResponse(Enum): return 'Zustand bereits aktiv' elif self == LogicResponse.Inval: return 'Das was du willst geht nicht.' - elif self == LogicResponse.LDAP: - return 'Moep! Geh LDAP fixen!' elif self == LogicResponse.EmergencyUnlock: return '!!! Emergency Unlock !!!' elif self == LogicResponse.ButtonLock: @@ -238,7 +200,7 @@ class LogicResponse(Enum): elif self == LogicResponse.ButtonPresent: return 'Present by button' - return 'Bitte spezifizieren Sie.' + return 'Error' class DoorHandler: @@ -344,61 +306,19 @@ class DoorHandler: class Logic: def __init__(self): + self.auth = Authenticator(simulate_auth) + if ldap_uri and ldap_binddn: + log.info('Initialising LDAP auth backend') + self.auth.enable_ldap_backend(ldap_uri, ldap_binddn) + if file_local_db: + log.info('Initialising local auth backend') + self.auth.enable_local_backend(file_local_db) + self.door_handler = DoorHandler(serial_port) - self.local_db = dict() - - if not file_local_db: - return - - with open(file_local_db, 'r') as f: - for line in f: - line = line.split() - user = line[0] - pwd = line[1].split(':') - self.local_db[user] = pwd - - def _try_auth_local(self, user, password): - if user not in self.local_db: - return LogicResponse.Perm - - stored_pw = self.local_db[user][0] - stored_salt = self.local_db[user][1] - if stored_pw == hashlib.sha256(stored_salt.encode() + password.encode()).hexdigest(): - return LogicResponse.Success - - return LogicResponse.Perm - - def _try_auth_ldap(self, user, password): - if simulate_ldap: - log.info('SIMULATION MODE! ACCEPTING ANYTHING!') - return LogicResponse.Success - - log.info(' Trying to LDAP auth (user, password) as user %s', user) - ldap_username = ldap_binddn % user - try: - l = ldap.initialize(ldap_uri) - l.simple_bind_s(ldap_username, password) - l.unbind_s() - except ldap.INVALID_CREDENTIALS: - log.info(' Invalid credentials') - return LogicResponse.Perm - except ldap.LDAPError as e: - log.info(' LDAP Error: %s' % e) - return LogicResponse.LDAP - return LogicResponse.Success - - def try_auth(self, credentials): - method = credentials[0] - if method == AuthMethod.LDAP_USER_PW: - return self._try_auth_ldap(credentials[1], credentials[2]) - elif method == AuthMethod.LOCAL_USER_DB: - return self._try_auth_local(credentials[1], credentials[2]) - - return LogicResponse.Inval def _request(self, state, credentials): - err = self.try_auth(credentials) - if err != LogicResponse.Success: + err = self.auth.try_auth(credentials) + if err != AuthenticationResult.Success: return err return self.door_handler.request(state) @@ -415,7 +335,7 @@ class Logic: if message is None: message = self.state.to_html() else: - message = message.to_html() + message = str(message) socketio.emit('status', {'led': led, 'message': message}) @@ -541,6 +461,7 @@ def home(): 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(), diff --git a/pydoorlock/Authenticator.py b/pydoorlock/Authenticator.py new file mode 100644 index 0000000..fed98d3 --- /dev/null +++ b/pydoorlock/Authenticator.py @@ -0,0 +1,148 @@ +""" +Doorlockd -- Binary Kitchen's smart door opener + +Copyright (c) Binary Kitchen e.V., 2018 + +Author: + Ralf Ramsauer + +This work is licensed under the terms of the GNU GPL, version 2. See +the LICENSE file in the top-level directory. + +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. +""" + +import hashlib +import ldap +import logging + +from enum import Enum +from random import sample + +log = logging.getLogger() + +# copied from sudo +eperm_insults = { + 'Wrong! You cheating scum!', + 'And you call yourself a Rocket Scientist!', + 'No soap, honkie-lips.', + 'Where did you learn to type?', + 'Are you on drugs?', + 'My pet ferret can type better than you!', + 'You type like i drive.', + 'Do you think like you type?', + 'Your mind just hasn\'t been the same since the electro-shock, has it?', + 'Maybe if you used more than just two fingers...', + 'BOB says: You seem to have forgotten your passwd, enter another!', + 'stty: unknown mode: doofus', + 'I can\'t hear you -- I\'m using the scrambler.', + 'The more you drive -- the dumber you get.', + 'Listen, broccoli brains, I don\'t have time to listen to this trash.', + 'I\'ve seen penguins that can type better than that.', + 'Have you considered trying to match wits with a rutabaga?', + 'You speak an infinite deal of nothing', +} + +def choose_insult(): + return(sample(eperm_insults, 1)[0]) + + +class AuthMethod(Enum): + LDAP_USER_PW = 1 + LOCAL_USER_DB = 2 + + def __str__(self): + if self == AuthMethod.LDAP_USER_PW: + return 'LDAP' + elif self == AuthMethod.LOCAL_USER_DB: + return 'Local' + return 'Error' + + +class AuthenticationResult(Enum): + Success = 0 + Perm = 1 + InternalError = 2 + + def __str__(self): + if self == AuthenticationResult.Success: + return 'Yo, passt!' + elif self == AuthenticationResult.Perm: + return choose_insult() + else: + return 'Internal authentication error' + +class Authenticator: + def __init__(self, simulate=False): + self._simulate = simulate + self._backends = set() + + @property + def backends(self): + return self._backends + + def enable_ldap_backend(self, uri, binddn): + self._ldap_uri = uri + self._ldap_binddn = binddn + self._backends.add(AuthMethod.LDAP_USER_PW) + + ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_DEMAND) + ldap.set_option(ldap.OPT_REFERRALS, 0) + + + def enable_local_backend(self, filename): + self._local_db = dict() + + with open(filename, 'r') as f: + for line in f: + line = line.split() + user = line[0] + pwd = line[1].split(':') + self._local_db[user] = pwd + + self._backends.add(AuthMethod.LOCAL_USER_DB) + + def _try_auth_local(self, user, password): + if user not in self._local_db: + return AuthenticationResult.Perm + + stored_pw = self._local_db[user][0] + stored_salt = self._local_db[user][1] + if stored_pw == hashlib.sha256(stored_salt.encode() + password.encode()).hexdigest(): + return AuthenticationResult.Success + + return AuthenticationResult.Perm + + def _try_auth_ldap(self, user, password): + log.info(' Trying to LDAP auth (user, password) as user %s', user) + ldap_username = self._ldap_binddn % user + try: + l = ldap.initialize(self._ldap_uri) + l.simple_bind_s(ldap_username, password) + l.unbind_s() + except ldap.INVALID_CREDENTIALS: + log.info(' Invalid credentials') + return AuthenticationResult.Perm + except ldap.LDAPError as e: + log.info(' LDAP Error: %s' % e) + return AuthenticationResult.InternalError + return AuthenticationResult.Success + + def try_auth(self, credentials): + if self._simulate: + log.info('SIMULATION MODE! ACCEPTING ANYTHING!') + return AuthenticationResult.Success + + method = credentials[0] + if method not in self._backends: + return AuthenticationResult.InternalError + + if method == AuthMethod.LDAP_USER_PW: + return self._try_auth_ldap(credentials[1], credentials[2]) + elif method == AuthMethod.LOCAL_USER_DB: + return self._try_auth_local(credentials[1], credentials[2]) + + return AuthenticationResult.InternalError diff --git a/pydoorlock/__init__.py b/pydoorlock/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/share/doorlockd/templates/index.html b/share/doorlockd/templates/index.html index 5e88590..041b680 100644 --- a/share/doorlockd/templates/index.html +++ b/share/doorlockd/templates/index.html @@ -26,8 +26,9 @@
@@ -36,7 +37,7 @@ {% if response %}
-

{{ response.to_html() }}

+

{{ response }}

{% endif %}
Die Kitchen ist: {{ state_text }}