root / pykota / trunk / bin / pknotify @ 2799

Revision 2799, 14.4 kB (checked in by jerome, 18 years ago)

Added timeout option.
Added denyafter option, but no code to support it yet.

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