root / pykota / trunk / bin / pknotify @ 2829

Revision 2829, 15.2 kB (checked in by jerome, 18 years ago)

Did a pass with pylint.

  • 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 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 signal
30import xmlrpclib
31
32try :
33    import PAM
34except ImportError :   
35    hasPAM = 0
36else :   
37    hasPAM = 1
38
39from pykota.tool import Tool, PyKotaCommandLineError, crashed, N_
40
41__doc__ = N_("""pknotify v%(__version__)s (c) %(__years__)s %(__author__)s
42
43Notifies or ask questions to end users who launched the PyKotIcon application.
44
45command line usage :
46
47  pknotify  [options]  [arguments]
48
49options :
50
51  -v | --version             Prints pknotify's version number then exits.
52  -h | --help                Prints this message then exits.
53 
54  -d | --destination h[:p]   Sets the destination hostname and optional
55                             port onto which contact the remote PyKotIcon
56                             application. This option is mandatory.
57                             When not specified, the port defaults to 7654.
58                             
59  -a | --ask                 Tells pknotify to ask something to the end
60                             user. Then pknotify will output the result.
61                       
62  -C | --checkauth           When --ask is used and both an 'username' and a
63                             'password' are asked to the end user, then
64                             pknotify will try to authenticate the user
65                             through PAM. If authentified, this program
66                             will print "AUTH=YES", else "AUTH=NO".
67                             If a field is missing, "AUTH=IMPOSSIBLE" will
68                             be printed. If the user is authenticated, then
69                             "USERNAME=xxxx" will be printed as well.
70                             
71  -c | --confirm             Tells pknotify to ask for either a confirmation                       
72                             or abortion.
73                             
74  -D | --denyafter N         With --checkauth above, makes pknotify loop                           
75                             up to N times if the password is incorrect.
76                             After having reached the limit, "DENY" will
77                             be printed, which effectively rejects the job.
78                             The default value of N is 1, meaning the job
79                             is denied after the first unsuccessful try.
80                             
81  -n | --notify              Tells pknotify to send an informational message
82                             to the end user.
83                             
84  -q | --quit                Tells pknotify to send a message asking the
85                             PyKotIcon application to exit. This option can
86                             be combined with the other ones to make PyKotIcon
87                             exit after having sent the answer from the dialog.
88                             
89  -t | --timeout T           Tells pknotify to ignore the end user's answer if
90                             it comes past T seconds after the dialog box being
91                             opened. The default value is 0 seconds, which
92                             tells pknotify to wait indefinitely.
93                             Use this option to avoid having an user who
94                             leaved his computer stall a whole print queue.
95                             
96  You MUST specify either --ask, --confirm, --notify or --quit.
97
98  arguments :             
99 
100    -a | --ask : Several arguments are accepted, of the form
101                 "label:varname:defaultvalue". The result will
102                 be printed to stdout in the following format :
103                 VAR1NAME=VAR1VALUE
104                 VAR2NAME=VAR2VALUE
105                 ...
106                 If the dialog was cancelled, nothing will be
107                 printed. If one of the varname is 'password'
108                 then this field is asked as a password (you won't
109                 see what you type in), and is NOT printed. Although
110                 it is not printed, it will be used to check if
111                 authentication is valid if you specify --checkauth.
112                 
113    -c | --confirm : A single argument is expected, representing the
114                     message to display. If the dialog is confirmed
115                     then pknotify will print OK, else CANCEL.
116                     
117    -n | --notify : A single argument is expected, representing the                 
118                    message to display. In this case pknotify will
119                    always print OK.
120                   
121examples :                   
122
123  pknotify -d client:7654 --confirm "This job costs :\n10 credits !"
124 
125  Would display the cost of a print job and asks for confirmation.
126 
127  pknotify --destination $PYKOTAJOBORIGINATINGHOSTNAME:7654 \\
128           --checkauth --ask "Your name:username:" "Your password:password:"
129           
130  Asks an username and password, and checks if they are valid.         
131  NB : The PYKOTAJOBORIGINATINGHOSTNAME environment variable is
132  only set if you launch pknotify from cupspykota through a directive
133  in ~pykota/pykota.conf
134 
135  The TCP port you'll use must be reachable on the client from the
136  print server.
137""")
138       
139class TimeoutError(Exception) :       
140    """An exception for timeouts."""
141    def __init__(self, message = ""):
142        self.message = message
143        Exception.__init__(self, message)
144    def __repr__(self):
145        return self.message
146    __str__ = __repr__
147   
148class PyKotaNotify(Tool) :       
149    """A class for pknotify."""
150    def sanitizeMessage(self, msg) :
151        """Replaces \\n and returns a messagee in xmlrpclib Binary format."""
152        return xmlrpclib.Binary(self.userCharsetToUTF8(msg.replace("\\n", "\n")))
153       
154    def convPAM(self, auth, queries=[], userdata=None) :
155        """Prepares PAM datas."""
156        response = []
157        for (query, qtype) in queries :
158            if qtype == PAM.PAM_PROMPT_ECHO_OFF :
159                response.append((self.password, 0))
160            elif qtype in (PAM.PAM_PROMPT_ECHO_ON, PAM.PAM_ERROR_MSG, PAM.PAM_TEXT_INFO) :
161                self.printInfo("Unexpected PAM query : %s (%s)" % (query, qtype), "warn")
162                response.append(('', 0))
163            else:
164                return None
165        return response
166
167    def checkAuth(self, username, password) :   
168        """Checks if we could authenticate an username with a password."""
169        if not hasPAM :   
170            raise PyKotaCommandLineError, _("You MUST install PyPAM for this functionnality to work !")
171        else :   
172            retcode = False
173            self.password = password
174            self.regainPriv()
175            auth = PAM.pam()
176            auth.start("passwd")
177            auth.set_item(PAM.PAM_USER, username)
178            auth.set_item(PAM.PAM_CONV, self.convPAM)
179            try :
180                auth.authenticate()
181                auth.acct_mgmt()
182            except PAM.error, resp :
183                self.printInfo(_("Authentication error for user %s : %s") % (username, resp), "warn")
184            except :
185                self.printInfo(_("Internal error : can't authenticate user %s") % username, "error")
186            else :
187                self.logdebug(_("Password correct for user %s") % username)
188                retcode = True
189            self.dropPriv()   
190            return retcode
191           
192    def alarmHandler(self, signum, frame) :       
193        """Alarm handler."""
194        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)
195       
196    def main(self, arguments, options) :
197        """Notifies or asks questions to end users through PyKotIcon."""
198        try :
199            (self.destination, self.port) = options["destination"].split(":")
200            self.port = int(self.port)
201        except ValueError :
202            self.destination = options["destination"]
203            self.port = 7654
204           
205        try :
206            denyafter = int(options["denyafter"])
207            if denyafter < 1 :
208                raise ValueError
209        except (ValueError, TypeError) :       
210            denyafter = 1
211           
212        try :   
213            self.timeout = int(options["timeout"])
214            if self.timeout < 0 :
215                raise ValueError
216        except (ValueError, TypeError) :
217            self.timeout = 0
218           
219        if self.timeout :
220            signal.signal(signal.SIGALRM, self.alarmHandler)
221            signal.alarm(self.timeout)
222           
223        try :   
224            server = xmlrpclib.ServerProxy("http://%s:%s" % (self.destination, self.port))
225            if options["ask"] :
226                try :
227                    denyafter = int(options["denyafter"])
228                    if denyafter < 1 :
229                        raise ValueError
230                except (ValueError, TypeError) :   
231                    denyafter = 1
232                labels = []
233                varnames = []
234                varvalues = {}
235                for arg in arguments :
236                    try :
237                        (label, varname, varvalue) = arg.split(":", 2)
238                    except ValueError :   
239                        raise PyKotaCommandLineError, "argument '%s' is invalid !" % arg
240                    labels.append(self.sanitizeMessage(label))
241                    varname = varname.lower()
242                    varnames.append(varname)
243                    varvalues[varname] = self.sanitizeMessage(varvalue)
244                   
245                passnumber = 1   
246                authok = None
247                while (authok != "AUTH=YES") and (passnumber <= denyafter) :
248                    result = server.askDatas(labels, varnames, varvalues)   
249                    if not options["checkauth"] :
250                        break
251                    if result["isValid"] :
252                        if ("username" in varnames) and ("password" in varnames) :
253                            if self.checkAuth(self.UTF8ToUserCharset(result["username"].data[:]), 
254                                              self.UTF8ToUserCharset(result["password"].data[:])) :
255                                authok = "AUTH=YES"
256                            else :   
257                                authok = "AUTH=NO"
258                        else :       
259                            authok = "AUTH=IMPOSSIBLE"       
260                    passnumber += 1       
261                               
262                if options["checkauth"] and options["denyafter"] \
263                   and (passnumber > denyafter) \
264                   and (authok != "AUTH=YES") :
265                    print "DENY"
266                if result["isValid"] :
267                    for varname in varnames :
268                        if (varname != "password") \
269                           and ((varname != "username") or (authok in (None, "AUTH=YES"))) :
270                            print "%s=%s" % (varname.upper(), self.UTF8ToUserCharset(result[varname].data[:]))
271                    if authok is not None :       
272                        print authok       
273            elif options["confirm"] :
274                print server.showDialog(self.sanitizeMessage(arguments[0]), True)
275            elif options["notify"] :
276                print server.showDialog(self.sanitizeMessage(arguments[0]), False)
277               
278            if options["quit"] :   
279                server.quitApplication()
280        except (socket.error, socket.gaierror), msg :
281            raise PyKotaCommandLineError, "%s : %s" % (_("Connection error"), str(msg))
282        except TimeoutError, msg :   
283            self.printInfo(msg, "warn")
284            print "CANCEL"      # Timeout occured : job is cancelled.
285           
286        if self.timeout :   
287            signal.alarm(0)   
288       
289if __name__ == "__main__" :
290    retcode = 0
291    try :
292        defaults = { \
293                     "timeout" : 0,
294                   }
295        short_options = "vhd:acnqCD:t:"
296        long_options = ["help", "version", "destination=", "denyafter=", \
297                        "timeout=", "ask", "checkauth", "confirm", "notify", \
298                        "quit" ]
299       
300        # Initializes the command line tool
301        notifier = PyKotaNotify(doc=__doc__)
302        notifier.deferredInit()
303       
304        # parse and checks the command line
305        (options, args) = notifier.parseCommandline(sys.argv[1:], short_options, long_options)
306       
307        # sets long options
308        options["help"] = options["h"] or options["help"]
309        options["version"] = options["v"] or options["version"]
310        options["destination"] = options["d"] or options["destination"]
311        options["ask"] = options["a"] or options["ask"]
312        options["confirm"] = options["c"] or options["confirm"]
313        options["notify"] = options["n"] or options["notify"]
314        options["quit"] = options["q"] or options["quit"]
315        options["checkauth"] = options["C"] or options["checkauth"]
316        options["denyafter"] = options["D"] or options["denyafter"]
317        options["timeout"] = options["t"] or options["timeout"] or defaults["timeout"]
318       
319        if options["help"] :
320            notifier.display_usage_and_quit()
321        elif options["version"] :
322            notifier.display_version_and_quit()
323        elif (options["ask"] and (options["confirm"] or options["notify"])) \
324             or (options["confirm"] and (options["ask"] or options["notify"])) \
325             or ((options["checkauth"] or options["denyafter"]) and not options["ask"]) \
326             or (options["notify"] and (options["ask"] or options["confirm"])) :
327            raise PyKotaCommandLineError, _("incompatible options, see help.")
328        elif (not options["destination"]) \
329             or not (options["quit"] or options["ask"] or options["confirm"] or options["notify"]) :
330            raise PyKotaCommandLineError, _("some options are mandatory, see help.")
331        elif (not args) and (not options["quit"]) :
332            raise PyKotaCommandLineError, _("some options require arguments, see help.")
333        else :
334            retcode = notifier.main(args, options)
335    except KeyboardInterrupt :       
336        sys.stderr.write("\nInterrupted with Ctrl+C !\n")
337        retcode = -3
338    except PyKotaCommandLineError, msg :   
339        sys.stderr.write("%s : %s\n" % (sys.argv[0], msg))
340        retcode = -2
341    except SystemExit :       
342        pass
343    except :
344        try :
345            notifier.crashed("%s failed" % sys.argv[0])
346        except :   
347            crashed("%s failed" % sys.argv[0])
348        retcode = -1
349       
350    sys.exit(retcode)   
Note: See TracBrowser for help on using the browser.