1 | #! /usr/bin/env python |
---|
2 | # -*- coding: utf-8 -*- |
---|
3 | # |
---|
4 | # PyKota : Print Quotas for CUPS |
---|
5 | # |
---|
6 | # (c) 2003-2009 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 | |
---|
24 | import sys |
---|
25 | import socket |
---|
26 | import errno |
---|
27 | import signal |
---|
28 | import xmlrpclib |
---|
29 | |
---|
30 | try : |
---|
31 | import PAM |
---|
32 | except ImportError : |
---|
33 | hasPAM = False |
---|
34 | else : |
---|
35 | hasPAM = True |
---|
36 | |
---|
37 | import pykota.appinit |
---|
38 | from pykota.utils import run |
---|
39 | from pykota.commandline import PyKotaOptionParser, \ |
---|
40 | checkandset_positiveint |
---|
41 | from pykota.errors import PyKotaToolError, \ |
---|
42 | PyKotaCommandLineError, \ |
---|
43 | PyKotaTimeoutError |
---|
44 | from pykota.tool import Tool |
---|
45 | |
---|
46 | class 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 | |
---|
200 | if __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 | |
---|
273 | examples : |
---|
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 | """ |
---|