root / pykota / trunk / bin / pknotify @ 3457

Revision 3442, 15.2 kB (checked in by jerome, 16 years ago)

More work done on pknotify. Still not useable...

  • 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 run
39from pykota.commandline import PyKotaOptionParser, \
40                               checkandset_positiveint
41from pykota.errors import PyKotaToolError, \
42                          PyKotaCommandLineError, \
43                          PyKotaTimeoutError
44from pykota.tool import Tool
45
46class PyKotaNotify(Tool) :
47    """A class for pknotify."""
48    def UTF8ToUserCharset(self, text) :
49        """Converts from UTF-8 to user's charset."""
50        if text is None :
51            return None
52        else :
53            return text.decode("UTF-8", "replace").encode(self.charset, "replace")
54
55    def userCharsetToUTF8(self, text) :
56        """Converts from user's charset to UTF-8."""
57        if text is None :
58            return None
59        else :
60            return text.decode(self.charset, "replace").encode("UTF-8", "replace")
61
62    def sanitizeMessage(self, msg) :
63        """Replaces \\n and returns a messagee in xmlrpclib Binary format."""
64        return xmlrpclib.Binary(self.userCharsetToUTF8(msg.replace("\\n", "\n")))
65
66    def convPAM(self, auth, queries=[], userdata=None) :
67        """Prepares PAM datas."""
68        response = []
69        for (query, qtype) in queries :
70            if qtype == PAM.PAM_PROMPT_ECHO_OFF :
71                response.append((self.password, 0))
72            elif qtype in (PAM.PAM_PROMPT_ECHO_ON, PAM.PAM_ERROR_MSG, PAM.PAM_TEXT_INFO) :
73                self.printInfo("Unexpected PAM query : %s (%s)" % (query, qtype), "warn")
74                response.append(('', 0))
75            else:
76                return None
77        return response
78
79    def checkAuth(self, username, password) :
80        """Checks if we could authenticate an username with a password."""
81        if not hasPAM :
82            raise PyKotaToolError, "You MUST install PyPAM for this functionnality to work !"
83        else :
84            retcode = False
85            self.password = password
86            auth = PAM.pam()
87            auth.start("passwd")
88            auth.set_item(PAM.PAM_USER, username)
89            auth.set_item(PAM.PAM_CONV, self.convPAM)
90            try :
91                auth.authenticate()
92                auth.acct_mgmt()
93            except PAM.error, response :
94                self.printInfo(_("Authentication error for user %(username)s : %(response)s") % locals(), "warn")
95            except :
96                self.printInfo(_("Internal error : can't authenticate user %(username)s") % locals(), "error")
97            else :
98                self.logdebug("Entered password is correct for user %s" % username)
99                retcode = True
100            return retcode
101
102    def alarmHandler(self, signum, frame) :
103        """Alarm handler."""
104        raise PyKotaTimeoutError, _("The end user at %s:%i didn't answer within %i seconds. The print job will be cancelled.") % (self.destination, self.port, self.timeout)
105
106    def main(self, arguments, options) :
107        """Notifies or asks questions to end users through PyKotIcon."""
108        try :
109            (self.destination, self.port) = options.destination.split(":")
110            self.port = int(self.port)
111        except ValueError :
112            self.destination = options.destination
113            self.port = 7654
114
115        try :
116            self.timeout = options.timeout
117            if self.timeout < 0 :
118                raise ValueError
119        except (ValueError, TypeError) :
120            self.timeout = 0
121
122        if self.timeout :
123            signal.signal(signal.SIGALRM, self.alarmHandler)
124            signal.alarm(self.timeout)
125
126        try :
127            try :
128                server = xmlrpclib.ServerProxy("http://%s:%s" % (self.destination, self.port))
129                if options.action == "ask" :
130                    try :
131                        if options.denyafter < 1 :
132                            raise ValueError
133                    except (ValueError, TypeError) :
134                        options.denyafter = 1
135                    labels = []
136                    varnames = []
137                    varvalues = {}
138                    for arg in arguments :
139                        try :
140                            (label, varname, varvalue) = arg.split(":", 2)
141                        except ValueError :
142                            raise PyKotaCommandLineError, "argument '%s' is invalid !" % arg
143                        labels.append(self.sanitizeMessage(label))
144                        varname = varname.lower()
145                        varnames.append(varname)
146                        varvalues[varname] = self.sanitizeMessage(varvalue)
147
148                    passnumber = 1
149                    authok = None
150                    while (authok != "AUTH=YES") and (passnumber <= options.denyafter) :
151                        result = server.askDatas(labels, varnames, varvalues)
152                        if not options.checkauth :
153                            break
154                        if result["isValid"] :
155                            if ("username" in varnames) and ("password" in varnames) :
156                                if self.checkAuth(self.UTF8ToUserCharset(result["username"].data[:]),
157                                                  self.UTF8ToUserCharset(result["password"].data[:])) :
158                                    authok = "AUTH=YES"
159                                else :
160                                    authok = "AUTH=NO"
161                            else :
162                                authok = "AUTH=IMPOSSIBLE"
163                        passnumber += 1
164
165                    if options.checkauth and options.denyafter \
166                       and (passnumber > options.denyafter) \
167                       and (authok != "AUTH=YES") :
168                        print "DENY"
169                    if result["isValid"] :
170                        for varname in varnames :
171                            if (varname != "password") \
172                               and ((varname != "username") or (authok in (None, "AUTH=YES"))) :
173                                print "%s=%s" % (varname.upper(), self.UTF8ToUserCharset(result[varname].data[:]))
174                        if authok is not None :
175                            print authok
176                elif options.action == "confirm" :
177                    print server.showDialog(self.sanitizeMessage(arguments[0]), True)
178                elif options.action == "notify" :
179                    print server.showDialog(self.sanitizeMessage(arguments[0]), False)
180
181                if options.quit :
182                    server.quitApplication()
183            except (xmlrpclib.ProtocolError, socket.error, socket.gaierror), msg :
184                print options.noremote
185                #try :
186                #    errnum = msg.args[0]
187                #except (AttributeError, IndexError) :
188                #    pass
189                #else :
190                #    if errnum == errno.ECONNREFUSED :
191                #        raise PyKotaToolError, "%s : %s" % (str(msg), (_("Are you sure that PyKotIcon is running and accepting incoming connections on %s:%s ?") % (self.destination, self.port)))
192                self.printInfo("%s : %s" % (_("Connection error"), str(msg)), "warn")
193            except PyKotaTimeoutError, msg :
194                self.printInfo(msg, "warn")
195                print "CANCEL"      # Timeout occured : job is cancelled.
196        finally :
197            if self.timeout :
198                signal.alarm(0)
199
200if __name__ == "__main__" :
201    sys.stderr.write("The pknotify command line tool is currently broken in this development tree. Please use a stable release instead.\n")
202    sys.exit(-1)
203    parser = PyKotaOptionParser(description=_("Notifies end users who have launched the PyKotIcon client side graphical desktop helper."),
204                                usage="pknotify [options] [arguments]")
205    parser.add_option("-a", "--ask",
206                            action="store_const",
207                            const="ask",
208                            dest="action",
209                            help=_("Ask something to the remote user, then print the result."))
210    parser.add_option("-c", "--confirm",
211                            action="store_const",
212                            const="confirm",
213                            dest="action",
214                            help=_("Ask the remote user to confirm or abort, then print the result."))
215    parser.add_option("-C", "--checkauth",
216                            dest="checkauth",
217                            help=_("When --ask is used, if both an username and password are asked to the end user, pknotify tries to authenticate these username and password through PAM. If this is successful, both 'AUTH=YES' and 'USERNAME=theusername' are printed. If unsuccessful, 'AUTH=NO' is printed. Finally, if one field is missing, 'AUTH=IMPOSSIBLE' is printed."))
218    parser.add_option("-D", "--denyafter",
219                            type="int",
220                            action="callback",
221                            callback=checkandset_positiveint,
222                            default=1,
223                            dest="denyafter",
224                            help=_("When --checkauth is used, this option tell pknotify to loop up to this value times or until the password is correct for the returned username. If authentication was impossible with the username and password received from the remote user, 'DENY' is printed, rejecting the print job. The default value is %default, meaning pknotify asks a single time."))
225    parser.add_option("-d", "--destination",
226                            dest="destination",
227                            help=_("Indicate the mandatory remote hostname or IP address and optional TCP port where PyKotIcon is listening for incoming connections from pknotify. If not specified, the port defaults to 7654."))
228    parser.add_option("-n", "--notify",
229                            action="store_const",
230                            const="notify",
231                            dest="action",
232                            help=_("Send an informational message to the remote user."))
233    parser.add_option("-N", "--noremote",
234                            dest="noremote",
235                            default="CANCEL",
236                            help=_("Tell pknotify what to print if it can't connect to a remote PyKotIcon application. The default value is 'CANCEL', which tells PyKota to cancel the print job. The only other supported value is 'CONTINUE', which tells PyKota to continue the processing of the current job."))
237    parser.add_option("-q", "--quit",
238                            dest="quit",
239                            help=_("Ask the remote PyKotIcon application to quit. When combined with other command line options, any other action is performed first."))
240    parser.add_option("-t", "--timeout",
241                            type="int",
242                            action="callback",
243                            callback=checkandset_positiveint,
244                            default=0,
245                            dest="timeout",
246                            help=_("Ensure that pknotify won't wait more than timeout seconds for an answer from the remote user. This avoids end users stalling a print queue because they don't answer in time. The default value is %default, making pknotify wait indefinitely."))
247    run(parser, PyKotaNotify)
248
249"""
250  arguments :
251
252    -a | --ask : Several arguments are accepted, of the form
253                 "label:varname:defaultvalue". The result will
254                 be printed to stdout in the following format :
255                 VAR1NAME=VAR1VALUE
256                 VAR2NAME=VAR2VALUE
257                 ...
258                 If the dialog was cancelled, nothing will be
259                 printed. If one of the varname is 'password'
260                 then this field is asked as a password (you won't
261                 see what you type in), and is NOT printed. Although
262                 it is not printed, it will be used to check if
263                 authentication is valid if you specify --checkauth.
264
265    -c | --confirm : A single argument is expected, representing the
266                     message to display. If the dialog is confirmed
267                     then pknotify will print OK, else CANCEL.
268
269    -n | --notify : A single argument is expected, representing the
270                    message to display. In this case pknotify will
271                    always print OK.
272
273examples :
274
275  pknotify -d client:7654 --noremote CONTINUE --confirm "This job costs 10 credits"
276
277  Would display the cost of the print job and asks for confirmation.
278  If the end user doesn't have PyKotIcon running and accepting connections
279  from the print server, PyKota will consider that the end user accepted
280  to print this job.
281
282  pknotify --destination $PYKOTAJOBORIGINATINGHOSTNAME:7654 \\
283           --checkauth --ask "Your name:username:" "Your password:password:"
284
285  Asks an username and password, and checks if they are valid.
286  NB : The PYKOTAJOBORIGINATINGHOSTNAME environment variable is
287  only set if you launch pknotify from cupspykota through a directive
288  in ~pykota/pykota.conf
289
290  The TCP port you'll use must be reachable on the client from the
291  print server.
292"""
293
294"""
295        defaults = { \
296                     "timeout" : 0,
297                     "noremote" : "CANCEL",
298                   }
299        short_options = "vhd:acnqCD:t:N:"
300        long_options = ["help", "version", "destination=", "denyafter=", \
301                        "timeout=", "ask", "checkauth", "confirm", "notify", \
302                        "quit", "noremote=" ]
303
304        elif (options["ask"] and (options["confirm"] or options["notify"])) \
305             or (options["confirm"] and (options["ask"] or options["notify"])) \
306             or ((options["checkauth"] or options["denyafter"]) and not options["ask"]) \
307             or (options["notify"] and (options["ask"] or options["confirm"])) :
308            raise PyKotaCommandLineError, _("incompatible options, see help.")
309        elif (not options["destination"]) \
310             or not (options["quit"] or options["ask"] or options["confirm"] or options["notify"]) :
311            raise PyKotaCommandLineError, _("some options are mandatory, see help.")
312        elif options["noremote"] not in ("CANCEL", "CONTINUE") :
313            raise PyKotaCommandLineError, _("incorrect value for the --noremote command line switch, see help.")
314        elif (not args) and (not options["quit"]) :
315            raise PyKotaCommandLineError, _("some options require arguments, see help.")
316"""
Note: See TracBrowser for help on using the browser.