root / pykota / trunk / bin / pknotify @ 3294

Revision 3294, 17.0 kB (checked in by jerome, 16 years ago)

Added modules to store utility functions and application
intialization code, which has nothing to do in classes.
Modified tool.py accordingly (far from being finished)
Use these new modules where necessary.
Now converts all command line arguments to unicode before
beginning to work. Added a proper logging method for already
encoded query strings.

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