subprocess.Popen, new pin code, disabled users, bugfixes
[uccvend-vendserver.git] / sql-edition / servers / VendServer.py
index ce75837..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 popen2 import popen2
+from subprocess import Popen, PIPE
 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 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:
@@ -94,13 +94,10 @@ class DispenseDatabase:
 
 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()
-               raw_acct.close()
         
                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):
-               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)));
@@ -143,30 +138,82 @@ def scroll_options(username, mk, welcome = False):
        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):
-       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
-       else:
-               logging.info('accepted pin for uid %d (%s)'%(uid,info.pw_name))
-       return info.pw_name
 
 
 def cookie(v):
@@ -225,6 +272,8 @@ def setup_idlers(v):
                ClockIdler(v),
                StringIdler(v),
                TrainIdler(v),
+               # "Hello World" in brainfuck
+               StringIdler(v, text=">+++++++++[<++++++++>-]<.>+++++++[<++++>-]<+.+++++++..+++.[-]>++++++++[<++++>-] <.>+++++++++++[<++++++++>-]<-.--------.+++.------.--------.[-]>++++++++[<++++>- ]<+.[-]++++++++++."),
                ]
        disabled = [
                ]
@@ -420,7 +469,6 @@ def make_selection(v, vstatus):
                        v.display('SEEMS NOT')
                else:
                        v.display('GOT DRINK!')
-                       #v.display('SEE FRIDGE')
        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)
+#              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
-               # 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
                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)
@@ -451,7 +503,9 @@ def make_selection(v, vstatus):
 
 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:
@@ -459,7 +513,7 @@ def price_check(v, vstatus):
                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):
@@ -569,35 +623,21 @@ Wouldn't you prefer a nice game of chess?
 
                        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)
+
                        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(
-                               [(' '*10+'INVALID PIN SETUP'+' '*11, False, 3)])
+                               [(' '*11+'ACCOUNT DISABLED'+' '*11, False, 3)])
                        vstatus.cur_user = ''
                        vstatus.cur_pin = ''
                        
@@ -605,6 +645,13 @@ Wouldn't you prefer a nice game of chess?
                        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
 
@@ -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)
-               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
+       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)
@@ -1091,9 +1150,8 @@ def do_vend_server(options, config_opts):
                
                try:
                        run_forever(rfh, wfh, options, config_opts)
-               except VendingException as e:
+               except VendingException:
                        logging.error("Connection died, trying again...")
-                       logging.info("Exception: "+e.__str__())
                        logging.info("Trying again in 5 seconds.")
                        sleep(5)
 

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