root / pykota / trunk / bin / pknotify @ 3260

Revision 3260, 16.6 kB (checked in by jerome, 16 years ago)

Changed license to GNU GPL v3 or later.
Changed Python source encoding from ISO-8859-15 to UTF-8 (only ASCII
was used anyway).

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