root / pykota / branches / specialauth / bin / pknotify @ 3548

Revision 3179, 17.4 kB (checked in by jerome, 18 years ago)

Special DB auth seems to work fine with clear text passwords.

  • Property svn:executable set to *
  • Property svn:keywords set to Author Date Id Revision
Line 
1#! /usr/bin/env python
2# -*- coding: ISO-8859-15 -*-
3
4# A notifier for PyKota
5#
6# PyKota - Print Quotas for CUPS and LPRng
7#
8# (c) 2003, 2004, 2005, 2006, 2007 Jerome Alet <alet@librelogiciel.com>
9# This program is free software; you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation; either version 2 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program; if not, write to the Free Software
21# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
22#
23# $Id$
24#
25#
26
27import sys
28import socket
29import errno
30import signal
31import xmlrpclib
32
33try :
34    import PAM
35except ImportError :   
36    hasPAM = 0
37else :   
38    hasPAM = 1
39
40from pykota.tool import PyKotaTool, PyKotaToolError, PyKotaCommandLineError, crashed, N_
41
42__doc__ = N_("""pknotify v%(__version__)s (c) %(__years__)s %(__author__)s
43
44Notifies or ask questions to end users who launched the PyKotIcon application.
45
46command line usage :
47
48  pknotify  [options]  [arguments]
49
50options :
51
52  -v | --version             Prints pknotify's version number then exits.
53  -h | --help                Prints this message then exits.
54 
55  -d | --destination h[:p]   Sets the destination hostname and optional
56                             port onto which contact the remote PyKotIcon
57                             application. This option is mandatory.
58                             When not specified, the port defaults to 7654.
59                             
60  -a | --ask                 Tells pknotify to ask something to the end
61                             user. Then pknotify will output the result.
62                       
63  -C | --checkauth           When --ask is used and both an 'username' and a
64                             'password' are asked to the end user, then
65                             pknotify will try to authenticate the user
66                             through PAM. If authentified, this program
67                             will print "AUTH=YES", else "AUTH=NO".
68                             If a field is missing, "AUTH=IMPOSSIBLE" will
69                             be printed. If the user is authenticated, then
70                             "USERNAME=xxxx" will be printed as well.
71                             
72  -c | --confirm             Tells pknotify to ask for either a confirmation                       
73                             or abortion.
74                             
75  -D | --denyafter N         With --checkauth above, makes pknotify loop                           
76                             up to N times if the password is incorrect.
77                             After having reached the limit, "DENY" will
78                             be printed, which effectively rejects the job.
79                             The default value of N is 1, meaning the job
80                             is denied after the first unsuccessful try.
81                             
82  -N | --noremote action     If it's impossible to connect to the remote
83                             PyKotIcon machine, do this action instead.
84                             Allowed actions are 'CONTINUE' and 'CANCEL',
85                             which will respectively allow the processing
86                             of the print job to continue, or the job to
87                             be cancelled. The default value is CANCEL.
88                             
89  -n | --notify              Tells pknotify to send an informational message
90                             to the end user.
91                             
92  -q | --quit                Tells pknotify to send a message asking the
93                             PyKotIcon application to exit. This option can
94                             be combined with the other ones to make PyKotIcon
95                             exit after having sent the answer from the dialog.
96                             
97  -t | --timeout T           Tells pknotify to ignore the end user's answer if
98                             it comes past T seconds after the dialog box being
99                             opened. The default value is 0 seconds, which
100                             tells pknotify to wait indefinitely.
101                             Use this option to avoid having an user who
102                             leaved his computer stall a whole print queue.
103                             
104  You MUST specify either --ask, --confirm, --notify or --quit.
105
106  arguments :             
107 
108    -a | --ask : Several arguments are accepted, of the form
109                 "label:varname:defaultvalue". The result will
110                 be printed to stdout in the following format :
111                 VAR1NAME=VAR1VALUE
112                 VAR2NAME=VAR2VALUE
113                 ...
114                 If the dialog was cancelled, nothing will be
115                 printed. If one of the varname is 'password'
116                 then this field is asked as a password (you won't
117                 see what you type in), and is NOT printed. Although
118                 it is not printed, it will be used to check if
119                 authentication is valid if you specify --checkauth.
120                 
121    -c | --confirm : A single argument is expected, representing the
122                     message to display. If the dialog is confirmed
123                     then pknotify will print OK, else CANCEL.
124                     
125    -n | --notify : A single argument is expected, representing the                 
126                    message to display. In this case pknotify will
127                    always print OK.
128                   
129examples :                   
130
131  pknotify -d client:7654 --noremote CONTINUE --confirm "This job costs 10 credits"
132 
133  Would display the cost of the print job and asks for confirmation.
134  If the end user doesn't have PyKotIcon running and accepting connections
135  from the print server, PyKota will consider that the end user accepted
136  to print this job.
137 
138  pknotify --destination $PYKOTAJOBORIGINATINGHOSTNAME:7654 \\
139           --checkauth --ask "Your name:username:" "Your password:password:"
140           
141  Asks an username and password, and checks if they are valid.         
142  NB : The PYKOTAJOBORIGINATINGHOSTNAME environment variable is
143  only set if you launch pknotify from cupspykota through a directive
144  in ~pykota/pykota.conf
145 
146  The TCP port you'll use must be reachable on the client from the
147  print server.
148""")
149       
150class TimeoutError(Exception) :       
151    """An exception for timeouts."""
152    def __init__(self, message = ""):
153        self.message = message
154        Exception.__init__(self, message)
155    def __repr__(self):
156        return self.message
157    __str__ = __repr__
158   
159class PyKotaNotify(PyKotaTool) :       
160    """A class for pknotify."""
161    def sanitizeMessage(self, msg) :
162        """Replaces \\n and returns a messagee in xmlrpclib Binary format."""
163        return xmlrpclib.Binary(self.userCharsetToUTF8(msg.replace("\\n", "\n")))
164       
165    def convPAM(self, auth, queries=[], userdata=None) :
166        """Prepares PAM datas."""
167        response = []
168        for (query, qtype) in queries :
169            if qtype == PAM.PAM_PROMPT_ECHO_OFF :
170                response.append((self.password, 0))
171            elif qtype in (PAM.PAM_PROMPT_ECHO_ON, PAM.PAM_ERROR_MSG, PAM.PAM_TEXT_INFO) :
172                self.printInfo("Unexpected PAM query : %s (%s)" % (query, qtype), "warn")
173                response.append(('', 0))
174            else:
175                return None
176        return response
177
178    def checkAuthPAM(self, username, password) :   
179        """Checks if we could authenticate an username with a password through PAM."""
180        if not hasPAM :   
181            raise PyKotaToolError, _("You MUST install PyPAM for this functionnality to work !")
182        else :   
183            retcode = False
184            self.password = password
185            self.regainPriv()
186            auth = PAM.pam()
187            auth.start("passwd")
188            auth.set_item(PAM.PAM_USER, username)
189            auth.set_item(PAM.PAM_CONV, self.convPAM)
190            try :
191                auth.authenticate()
192                auth.acct_mgmt()
193            except PAM.error, resp :
194                self.printInfo(_("Authentication error for user %s : %s") % (username, resp), "warn")
195            except :
196                self.printInfo(_("Internal error : can't authenticate user %s") % username, "error")
197            else :
198                self.logdebug(_("Password correct for user %s") % username)
199                retcode = True
200            self.dropPriv()   
201            return retcode
202           
203    def checkAuthDB(self, username, password) :   
204        """Checks if we could authenticate an username with a password through PyKota's DB with clear text passwords."""
205        user = self.storage.getUser(username)
206        if user.Exists :
207            if password == user.Password :
208                return True
209        return False       
210       
211    def checkAuth(self, username, password) :   
212        """Checks if we could authenticate an username with a password through PyKota's DB."""
213        return self.checkAuthDB(username, password)
214           
215    def alarmHandler(self, signum, frame) :       
216        """Alarm handler."""
217        raise TimeoutError, _("The end user at %s:%i didn't answer within %i seconds. The print job will be cancelled.") % (self.destination, self.port, self.timeout)
218       
219    def main(self, arguments, options) :
220        """Notifies or asks questions to end users through PyKotIcon."""
221        try :
222            (self.destination, self.port) = options["destination"].split(":")
223            self.port = int(self.port)
224        except ValueError :
225            self.destination = options["destination"]
226            self.port = 7654
227           
228        try :
229            denyafter = int(options["denyafter"])
230            if denyafter < 1 :
231                raise ValueError
232        except (ValueError, TypeError) :       
233            denyafter = 1
234           
235        try :   
236            self.timeout = int(options["timeout"])
237            if self.timeout < 0 :
238                raise ValueError
239        except (ValueError, TypeError) :
240            self.timeout = 0
241           
242        if self.timeout :
243            signal.signal(signal.SIGALRM, self.alarmHandler)
244            signal.alarm(self.timeout)
245           
246        try :   
247            server = xmlrpclib.ServerProxy("http://%s:%s" % (self.destination, self.port))
248            if options["ask"] :
249                try :
250                    denyafter = int(options["denyafter"])
251                    if denyafter < 1 :
252                        raise ValueError
253                except (ValueError, TypeError) :   
254                    denyafter = 1
255                labels = []
256                varnames = []
257                varvalues = {}
258                for arg in arguments :
259                    try :
260                        (label, varname, varvalue) = arg.split(":", 2)
261                    except ValueError :   
262                        raise PyKotaCommandLineError, "argument '%s' is invalid !" % arg
263                    labels.append(self.sanitizeMessage(label))
264                    varname = varname.lower()
265                    varnames.append(varname)
266                    varvalues[varname] = self.sanitizeMessage(varvalue)
267                   
268                passnumber = 1   
269                authok = None
270                while (authok != "AUTH=YES") and (passnumber <= denyafter) :
271                    result = server.askDatas(labels, varnames, varvalues)   
272                    if not options["checkauth"] :
273                        break
274                    if result["isValid"] :
275                        if ("username" in varnames) and ("password" in varnames) :
276                            if self.checkAuth(self.UTF8ToUserCharset(result["username"].data[:]), 
277                                              self.UTF8ToUserCharset(result["password"].data[:])) :
278                                authok = "AUTH=YES"
279                            else :   
280                                authok = "AUTH=NO"
281                        else :       
282                            authok = "AUTH=IMPOSSIBLE"       
283                    passnumber += 1       
284                               
285                if options["checkauth"] and options["denyafter"] \
286                   and (passnumber > denyafter) \
287                   and (authok != "AUTH=YES") :
288                    print "DENY"
289                if result["isValid"] :
290                    for varname in varnames :
291                        if (varname != "password") \
292                           and ((varname != "username") or (authok in (None, "AUTH=YES"))) :
293                            print "%s=%s" % (varname.upper(), self.UTF8ToUserCharset(result[varname].data[:]))
294                    if authok is not None :       
295                        print authok       
296            elif options["confirm"] :
297                print server.showDialog(self.sanitizeMessage(arguments[0]), True)
298            elif options["notify"] :
299                print server.showDialog(self.sanitizeMessage(arguments[0]), False)
300               
301            if options["quit"] :   
302                server.quitApplication()
303        except (socket.error, socket.gaierror), msg :
304            print options["noremote"]
305            try :
306                errnum = msg.args[0]
307            except (AttributeError, IndexError) :
308                pass
309            else :   
310                if errnum == errno.ECONNREFUSED :
311                    raise PyKotaToolError, "%s : %s" % (str(msg), (_("Are you sure that PyKotIcon is running and accepting incoming connections on %s:%s ?") % (self.destination, self.port)))
312            self.printInfo("%s : %s" % (_("Connection error"), str(msg)), "warn")
313        except TimeoutError, msg :   
314            self.printInfo(msg, "warn")
315            print "CANCEL"      # Timeout occured : job is cancelled.
316           
317        if self.timeout :   
318            signal.alarm(0)   
319       
320if __name__ == "__main__" :
321    retcode = 0
322    try :
323        defaults = { \
324                     "timeout" : 0,
325                     "noremote" : "CANCEL",
326                   }
327        short_options = "vhd:acnqCD:t:N:"
328        long_options = ["help", "version", "destination=", "denyafter=", \
329                        "timeout=", "ask", "checkauth", "confirm", "notify", \
330                        "quit", "noremote=" ]
331       
332        # Initializes the command line tool
333        notifier = PyKotaNotify(doc=__doc__)
334        notifier.deferredInit()
335       
336        # parse and checks the command line
337        (options, args) = notifier.parseCommandline(sys.argv[1:], short_options, long_options)
338       
339        # sets long options
340        options["help"] = options["h"] or options["help"]
341        options["version"] = options["v"] or options["version"]
342        options["destination"] = options["d"] or options["destination"]
343        options["ask"] = options["a"] or options["ask"]
344        options["confirm"] = options["c"] or options["confirm"]
345        options["notify"] = options["n"] or options["notify"]
346        options["quit"] = options["q"] or options["quit"]
347        options["checkauth"] = options["C"] or options["checkauth"]
348        options["denyafter"] = options["D"] or options["denyafter"]
349        options["timeout"] = options["t"] or options["timeout"] or defaults["timeout"]
350        options["noremote"] = (options["N"] or options["noremote"] or defaults["noremote"]).upper()
351       
352        if options["help"] :
353            notifier.display_usage_and_quit()
354        elif options["version"] :
355            notifier.display_version_and_quit()
356        elif (options["ask"] and (options["confirm"] or options["notify"])) \
357             or (options["confirm"] and (options["ask"] or options["notify"])) \
358             or ((options["checkauth"] or options["denyafter"]) and not options["ask"]) \
359             or (options["notify"] and (options["ask"] or options["confirm"])) :
360            raise PyKotaCommandLineError, _("incompatible options, see help.")
361        elif (not options["destination"]) \
362             or not (options["quit"] or options["ask"] or options["confirm"] or options["notify"]) :
363            raise PyKotaCommandLineError, _("some options are mandatory, see help.")
364        elif options["noremote"] not in ("CANCEL", "CONTINUE") :
365            raise PyKotaCommandLineError, _("incorrect value for the --noremote command line switch, see help.")
366        elif (not args) and (not options["quit"]) :
367            raise PyKotaCommandLineError, _("some options require arguments, see help.")
368        else :
369            retcode = notifier.main(args, options)
370    except KeyboardInterrupt :       
371        sys.stderr.write("\nInterrupted with Ctrl+C !\n")
372        retcode = -3
373    except PyKotaCommandLineError, msg :   
374        sys.stderr.write("%s : %s\n" % (sys.argv[0], msg))
375        print "CANCEL"  # Forces the cancellation of the print job if a command line switch is incorrect
376        retcode = -2
377    except SystemExit :       
378        pass
379    except :
380        try :
381            notifier.crashed("%s failed" % sys.argv[0])
382        except :   
383            crashed("%s failed" % sys.argv[0])
384        retcode = -1
385       
386    try :
387        notifier.storage.close()
388    except (TypeError, NameError, AttributeError) :   
389        pass
390       
391    sys.exit(retcode)   
Note: See TracBrowser for help on using the browser.