subprocess.Popen, new pin code, disabled users, bugfixes
[uccvend-vendserver.git] / sql-edition / servers / VendServer.py
index 515a659..2a9a55a 100755 (executable)
@@ -10,7 +10,7 @@ import logging, logging.handlers
 from traceback import format_tb
 if USE_DB: import pg
 from time import time, sleep, mktime, localtime
 from traceback import format_tb
 if USE_DB: import pg
 from time import time, sleep, mktime, localtime
-from popen2 import popen2
+from subprocess import Popen, PIPE
 from LATClient import LATClient, LATClientException
 from SerialClient import SerialClient, SerialClientException
 from VendingMachine import VendingMachine, VendingException
 from LATClient import LATClient, LATClientException
 from SerialClient import SerialClient, SerialClientException
 from VendingMachine import VendingMachine, VendingException
@@ -21,7 +21,7 @@ from Idler import GreetingIdler,TrainIdler,GrayIdler,StringIdler,ClockIdler,Fort
 from SnackConfig import get_snack#, get_snacks
 import socket
 from posix import geteuid
 from SnackConfig import get_snack#, get_snacks
 import socket
 from posix import geteuid
-from LDAPConnector import get_uid, set_card_id
+from LDAPConnector import get_uid,get_uname, set_card_id
 
 CREDITS="""
 This vending machine software brought to you by:
 
 CREDITS="""
 This vending machine software brought to you by:
@@ -94,13 +94,10 @@ class DispenseDatabase:
 
 def scroll_options(username, mk, welcome = False):
        if welcome:
 
 def scroll_options(username, mk, welcome = False):
        if welcome:
-               # Balance checking: crap code, [DAA]'s fault
-               # Updated 2011 to handle new dispense [MRD]
-               raw_acct = os.popen('dispense acct %s' % username)
-               acct = raw_acct.read()
+               # Balance checking
+               acct, unused = Popen(['dispense', 'acct', username], close_fds=True, stdout=PIPE).communicate()
                # this is fucking appalling
                balance = acct[acct.find("$")+1:acct.find("(")].strip()
                # this is fucking appalling
                balance = acct[acct.find("$")+1:acct.find("(")].strip()
-               raw_acct.close()
         
                msg = [(center('WELCOME'), False, TEXT_SPEED),
                           (center(username), False, TEXT_SPEED),
         
                msg = [(center('WELCOME'), False, TEXT_SPEED),
                           (center(username), False, TEXT_SPEED),
@@ -112,10 +109,8 @@ def scroll_options(username, mk, welcome = False):
        # Get coke contents
        cokes = []
        for i in range(0, 7):
        # Get coke contents
        cokes = []
        for i in range(0, 7):
-               cmd = 'dispense iteminfo coke:%i' % i
-               raw = os.popen(cmd)
-               info = raw.read()
-               raw.close()
+               args = ('dispense', 'iteminfo', 'coke:%i' % i)
+               info, unused = Popen(args, close_fds=True, stdout=PIPE).communicate()
                m = re.match("\s*[a-z]+:\d+\s+(\d+)\.(\d\d)\s+([^\n]+)", info)
                cents = int(m.group(1))*100 + int(m.group(2))
                cokes.append('%i %i %s' % (i, cents, m.group(3)));
                m = re.match("\s*[a-z]+:\d+\s+(\d+)\.(\d\d)\s+([^\n]+)", info)
                cents = int(m.group(1))*100 + int(m.group(2))
                cokes.append('%i %i %s' % (i, cents, m.group(3)));
@@ -143,30 +138,82 @@ def scroll_options(username, mk, welcome = False):
        msg.append((choices, False, None))
        mk.set_messages(msg)
 
        msg.append((choices, False, None))
        mk.set_messages(msg)
 
-def get_acct_state(uid):
-       try:
-               info = pwd.getpwuid(uid)
-       except KeyError:
-               logging.info('getting pin for uid %d: user not in password file'%uid)
-               return 'invalid'
-       ret = os.system('dispense acct %s' % (info.pw_name))
-       if ret != 0:
-               return 'invalid'
-
-       # TODO: Disabled account check (done in server pin check now)   
+_pin_uid = 0
+_pin_uname = 'root'
+_pin_pin = '----'
 
 
-       return 'good'
+def _check_pin(uid, pin):
+       global _pin_uid
+       global _pin_uname
+       global _pin_pin
+       print "_check_pin('",uid,"',---)"
+       if uid != _pin_uid:
+               try:
+                       info = pwd.getpwuid(uid)
+               except KeyError:
+                       logging.info('getting pin for uid %d: user not in password file'%uid)
+                       return None
+               if info.pw_dir == None: return False
+               pinfile = os.path.join(info.pw_dir, '.pin')
+               try:
+                       s = os.stat(pinfile)
+               except OSError:
+                       logging.info('getting pin for uid %d: .pin not found in home directory'%uid)
+                       return None
+               if s.st_mode & 077:
+                       logging.info('getting pin for uid %d: .pin has wrong permissions. Fixing.'%uid)
+                       os.chmod(pinfile, 0600)
+               try:
+                       f = file(pinfile)
+               except IOError:
+                       logging.info('getting pin for uid %d: I cannot read pin file'%uid)
+                       return None
+               pinstr = f.readline()
+               f.close()
+               if not re.search('^'+'[0-9]'*PIN_LENGTH+'$', pinstr):
+                       logging.info('getting pin for uid %d: %s not a good pin'%(uid,repr(pinstr)))
+                       return None
+               _pin_uid = uid
+               _pin_pin = pinstr
+               _pin_uname = info.pw_name
+       else:
+               pinstr = _pin_pin
+       if pin == int(pinstr):
+               logging.info("Pin correct for %d",uid)
+       else:
+               logging.info("Pin incorrect for %d",uid)
+       return pin == int(pinstr)
+
+def acct_is_disabled(name=None):
+       global _pin_uname
+       if name == None:
+               name = _pin_uname
+       acct, unused = Popen(['dispense', 'acct', _pin_uname], close_fds=True, stdout=PIPE).communicate()
+       # this is fucking appalling
+       flags = acct[acct.find("(")+1:acct.find(")")].strip()
+       if 'disabled' in flags:
+               return True
+       if 'internal' in flags:
+               return True
+       return False
+
+def has_good_pin(uid):
+       return _check_pin(uid, None) != None
 
 def verify_user_pin(uid, pin, skip_pin_check=False):
 
 def verify_user_pin(uid, pin, skip_pin_check=False):
-       info = pwd.getpwuid(uid)
-       if skip_pin_check:
-               logging.info('accepted mifare for uid %d (%s)'%(uid,info.pw_name))
-       elif os.system('dispense pincheck %04i %s' % (pin, info.pw_name)) != 0:
+       if skip_pin_check or _check_pin(uid, pin) == True:
+               info = pwd.getpwuid(uid)
+               if skip_pin_check:
+                       if acct_is_disabled(info.pw_name):
+                               logging.info('refused mifare for disabled acct uid %d (%s)'%(uid,info.pw_name))
+                               return '-disabled-'
+                       logging.info('accepted mifare for uid %d (%s)'%(uid,info.pw_name))
+               else:
+                       logging.info('accepted pin for uid %d (%s)'%(uid,info.pw_name))
+               return info.pw_name
+       else:
                logging.info('refused pin for uid %d'%(uid))
                return None
                logging.info('refused pin for uid %d'%(uid))
                return None
-       else:
-               logging.info('accepted pin for uid %d (%s)'%(uid,info.pw_name))
-       return info.pw_name
 
 
 def cookie(v):
 
 
 def cookie(v):
@@ -225,6 +272,8 @@ def setup_idlers(v):
                ClockIdler(v),
                StringIdler(v),
                TrainIdler(v),
                ClockIdler(v),
                StringIdler(v),
                TrainIdler(v),
+               # "Hello World" in brainfuck
+               StringIdler(v, text=">+++++++++[<++++++++>-]<.>+++++++[<++++>-]<+.+++++++..+++.[-]>++++++++[<++++>-] <.>+++++++++++[<++++++++>-]<-.--------.+++.------.--------.[-]>++++++++[<++++>- ]<+.[-]++++++++++."),
                ]
        disabled = [
                ]
                ]
        disabled = [
                ]
@@ -420,7 +469,6 @@ def make_selection(v, vstatus):
                        v.display('SEEMS NOT')
                else:
                        v.display('GOT DRINK!')
                        v.display('SEEMS NOT')
                else:
                        v.display('GOT DRINK!')
-                       #v.display('SEE FRIDGE')
        else:
                # first see if it's a named slot
                try:
        else:
                # first see if it's a named slot
                try:
@@ -429,14 +477,18 @@ def make_selection(v, vstatus):
                        price, shortname, name = get_snack( '--' )
                dollarprice = "$%.2f" % ( price / 100.0 )
                v.display(vstatus.cur_selection+' - %s'%dollarprice)
                        price, shortname, name = get_snack( '--' )
                dollarprice = "$%.2f" % ( price / 100.0 )
                v.display(vstatus.cur_selection+' - %s'%dollarprice)
-               exitcode = os.system('dispense -u "%s" give \>sales\:snack %d "%s"'%(vstatus.username, price, name)) >> 8
-               # For some reason, this causes the machine and this code to desync
-#              exitcode = os.system('dispense -u "%s" snack:%s'%(vstatus.username, vstatus.cur_selection)) >> 8
+#              exitcode = os.system('dispense -u "%s" give \>snacksales %d "%s"'%(vstatus.username, price, name)) >> 8
+#              exitcode = os.system('dispense -u "%s" give \>sales\:snack %d "%s"'%(vstatus.username, price, name)) >> 8
+               exitcode = os.system('dispense -u "%s" snack:%s'%(vstatus.username, vstatus.cur_selection)) >> 8
                if (exitcode == 0):
                        # magic dispense syslog service
                        syslog.syslog(syslog.LOG_INFO | syslog.LOG_LOCAL4, "vended %s (slot %s) for %s" % (name, vstatus.cur_selection, vstatus.username))
                if (exitcode == 0):
                        # magic dispense syslog service
                        syslog.syslog(syslog.LOG_INFO | syslog.LOG_LOCAL4, "vended %s (slot %s) for %s" % (name, vstatus.cur_selection, vstatus.username))
-                       v.vend(vstatus.cur_selection)
-                       v.display('THANK YOU')
+                       (worked, code, string) = v.vend(vstatus.cur_selection)
+                       if worked:
+                               v.display('THANK YOU')
+                       else:
+                               print "Vend Failed:", code, string
+                               v.display('VEND FAIL')
                elif (exitcode == 5):   # RV_BALANCE
                        v.display('NO MONEY?')
                elif (exitcode == 4):   # RV_ARGUMENTS (zero give causes arguments)
                elif (exitcode == 5):   # RV_BALANCE
                        v.display('NO MONEY?')
                elif (exitcode == 4):   # RV_ARGUMENTS (zero give causes arguments)
@@ -451,7 +503,9 @@ def make_selection(v, vstatus):
 
 def price_check(v, vstatus):
        if vstatus.cur_selection[1] == '8':
 
 def price_check(v, vstatus):
        if vstatus.cur_selection[1] == '8':
-               v.display(center('SEE COKE'))
+               args = ('dispense', 'iteminfo', 'coke:' + vstatus.cur_selection[0])
+               info, unused = Popen(args, close_fds=True, stdout=PIPE).communicate()
+               dollarprice = re.match("\s*[a-z]+:\d+\s+(\d+\.\d\d)\s+([^\n]+)", info).group(1)
        else:
                # first see if it's a named slot
                try:
        else:
                # first see if it's a named slot
                try:
@@ -459,7 +513,7 @@ def price_check(v, vstatus):
                except:
                        price, shortname, name = get_snack( '--' )
                dollarprice = "$%.2f" % ( price / 100.0 )
                except:
                        price, shortname, name = get_snack( '--' )
                dollarprice = "$%.2f" % ( price / 100.0 )
-               v.display(vstatus.cur_selection+' - %s'%dollarprice)
+       v.display(vstatus.cur_selection+' - %s'%dollarprice)
 
 
 def handle_getting_pin_key(state, event, params, v, vstatus):
 
 
 def handle_getting_pin_key(state, event, params, v, vstatus):
@@ -569,35 +623,21 @@ Wouldn't you prefer a nice game of chess?
 
                        return
 
 
                        return
 
-               acct_state = get_acct_state(uid)
-               if acct_state == 'invalid':
-                       logging.info('user '+vstatus.cur_user+' is not in the database')
-                       vstatus.mk.set_messages(
-                               [(' '*10+'INVALID PIN SETUP'+' '*11, False, 3)])
-                       vstatus.cur_user = ''
-                       vstatus.cur_pin = ''
-                       
-                       reset_idler(v, vstatus, 3)
-                       return
-               elif acct_state == 'locked':
-                       logging.info('user '+vstatus.cur_user+' is locked')
+               if not has_good_pin(uid):
+                       logging.info('user '+vstatus.cur_user+' has a bad PIN')
                        vstatus.mk.set_messages(
                                [(' '*10+'INVALID PIN SETUP'+' '*11, False, 3)])
                        vstatus.cur_user = ''
                        vstatus.cur_pin = ''
                        
                        reset_idler(v, vstatus, 3)
                        vstatus.mk.set_messages(
                                [(' '*10+'INVALID PIN SETUP'+' '*11, False, 3)])
                        vstatus.cur_user = ''
                        vstatus.cur_pin = ''
                        
                        reset_idler(v, vstatus, 3)
+
                        return
                        return
-               elif acct_state == 'good':
-                       vstatus.cur_pin = ''
-                       vstatus.mk.set_message('PIN: ')
-                       logging.info('need pin for user %s'%vstatus.cur_user)
-                       vstatus.change_state(STATE_GETTING_PIN)
-                       return
-               else:
-                       logging.error('user '+vstatus.cur_user+' has an unknown account state'+acct_state)
+               
+               if acct_is_disabled():
+                       logging.info('user '+vstatus.cur_user+' is disabled')
                        vstatus.mk.set_messages(
                        vstatus.mk.set_messages(
-                               [(' '*10+'INVALID PIN SETUP'+' '*11, False, 3)])
+                               [(' '*11+'ACCOUNT DISABLED'+' '*11, False, 3)])
                        vstatus.cur_user = ''
                        vstatus.cur_pin = ''
                        
                        vstatus.cur_user = ''
                        vstatus.cur_pin = ''
                        
@@ -605,6 +645,13 @@ Wouldn't you prefer a nice game of chess?
                        return
 
 
                        return
 
 
+               vstatus.cur_pin = ''
+               vstatus.mk.set_message('PIN: ')
+               logging.info('need pin for user %s'%vstatus.cur_user)
+               vstatus.change_state(STATE_GETTING_PIN)
+               return
+
+
 def handle_idle_key(state, event, params, v, vstatus):
        #print "handle_idle_key (s,e,p)", state, " ", event, " ", params
 
 def handle_idle_key(state, event, params, v, vstatus):
        #print "handle_idle_key (s,e,p)", state, " ", event, " ", params
 
@@ -784,11 +831,23 @@ def handle_mifare_event(state, event, params, v, vstatus):
        try:
                vstatus.cur_user = get_uid(card_id)
                logging.info('Mapped card id to uid %s'%vstatus.cur_user)
        try:
                vstatus.cur_user = get_uid(card_id)
                logging.info('Mapped card id to uid %s'%vstatus.cur_user)
-               vstatus.username = verify_user_pin(int(vstatus.cur_user), None, True)
+               vstatus.username = get_uname(vstatus.cur_user)
+               if acct_is_disabled(vstatus.username):
+                       vstatus.username = '-disabled-'
        except ValueError:
                vstatus.username = None
        except ValueError:
                vstatus.username = None
+       if vstatus.username == '-disabled-':
+               v.beep(40, False)
+               vstatus.mk.set_messages(
+                       [(center('ACCT DISABLED'), False, 1.0),
+                        (center('SORRY'), False, 0.5)])
+               vstatus.cur_user = ''
+               vstatus.cur_pin = ''
+               vstatus.username = None
        
        
-       if vstatus.username:
+               reset_idler(v, vstatus, 2)
+               return
+       elif vstatus.username:
                v.beep(0, False)
                vstatus.cur_selection = ''
                vstatus.change_state(STATE_GET_SELECTION)
                v.beep(0, False)
                vstatus.cur_selection = ''
                vstatus.change_state(STATE_GET_SELECTION)

UCC git Repository :: git.ucc.asn.au