##// END OF EJS Templates
Handle httpconnection.readauthforuri changes (hg.0593e8f81c71)
Patrick Mezard -
r81:049fac04 default
parent child Browse files
Show More
@@ -1,402 +1,406 b''
1 1 # -*- coding: utf-8 -*-
2 2 #
3 3 # mercurial_keyring: save passwords in password database
4 4 #
5 5 # Copyright 2009 Marcin Kasperski <Marcin.Kasperski@mekk.waw.pl>
6 6 #
7 7 # This software may be used and distributed according to the terms
8 8 # of the GNU General Public License, incorporated herein by reference.
9 9 #
10 10 # See README.txt for more details.
11 11
12 12 from mercurial import hg, repo, util
13 13 from mercurial.i18n import _
14 14 try:
15 15 from mercurial.url import passwordmgr
16 16 except:
17 17 from mercurial.httprepo import passwordmgr
18 18 from mercurial.httprepo import httprepository
19 19 from mercurial import mail
20 20
21 21 # mercurial.demandimport incompatibility workaround,
22 22 # would cause gnomekeyring, one of the possible
23 23 # keyring backends, not to work.
24 24 from mercurial.demandimport import ignore
25 25 if "gobject._gobject" not in ignore:
26 26 ignore.append("gobject._gobject")
27 27
28 28 import keyring
29 29 from urlparse import urlparse
30 30 import urllib2
31 31 import smtplib, socket
32 32 import os
33 33
34 34 KEYRING_SERVICE = "Mercurial"
35 35
36 36 ############################################################
37 37
38 38 def monkeypatch_method(cls):
39 39 def decorator(func):
40 40 setattr(cls, func.__name__, func)
41 41 return func
42 42 return decorator
43 43
44 44 ############################################################
45 45
46 46 class PasswordStore(object):
47 47 """
48 48 Helper object handling keyring usage (password save&restore,
49 49 the way passwords are keyed in the keyring).
50 50 """
51 51 def __init__(self):
52 52 self.cache = dict()
53 53 def get_http_password(self, url, username):
54 54 return keyring.get_password(KEYRING_SERVICE,
55 55 self._format_http_key(url, username))
56 56 def set_http_password(self, url, username, password):
57 57 keyring.set_password(KEYRING_SERVICE,
58 58 self._format_http_key(url, username),
59 59 password)
60 60 def clear_http_password(self, url, username):
61 61 self.set_http_password(url, username, "")
62 62 def _format_http_key(self, url, username):
63 63 return "%s@@%s" % (username, url)
64 64 def get_smtp_password(self, machine, port, username):
65 65 return keyring.get_password(
66 66 KEYRING_SERVICE,
67 67 self._format_smtp_key(machine, port, username))
68 68 def set_smtp_password(self, machine, port, username, password):
69 69 keyring.set_password(
70 70 KEYRING_SERVICE,
71 71 self._format_smtp_key(machine, port, username),
72 72 password)
73 73 def clear_smtp_password(self, machine, port, username):
74 74 self.set_smtp_password(url, username, "")
75 75 def _format_smtp_key(self, machine, port, username):
76 76 return "%s@@%s:%s" % (username, machine, str(port))
77 77
78 78 password_store = PasswordStore()
79 79
80 80 ############################################################
81 81
82 82 class HTTPPasswordHandler(object):
83 83 """
84 84 Actual implementation of password handling (user prompting,
85 85 configuration file searching, keyring save&restore).
86 86
87 87 Object of this class is bound as passwordmgr attribute.
88 88 """
89 89 def __init__(self):
90 90 self.pwd_cache = {}
91 91 self.last_reply = None
92 92
93 93 def find_auth(self, pwmgr, realm, authuri):
94 94 """
95 95 Actual implementation of find_user_password - different
96 96 ways of obtaining the username and password.
97 97 """
98 98 ui = pwmgr.ui
99 99
100 100 # If we are called again just after identical previous
101 101 # request, then the previously returned auth must have been
102 102 # wrong. So we note this to force password prompt (and avoid
103 103 # reusing bad password indifinitely).
104 104 after_bad_auth = (self.last_reply \
105 105 and (self.last_reply['realm'] == realm) \
106 106 and (self.last_reply['authuri'] == authuri))
107 107
108 108 # Strip arguments to get actual remote repository url.
109 109 base_url = self.canonical_url(authuri)
110 110
111 111 # Extracting possible username (or password)
112 112 # stored directly in repository url
113 113 user, pwd = urllib2.HTTPPasswordMgrWithDefaultRealm.find_user_password(
114 114 pwmgr, realm, authuri)
115 115 if user and pwd:
116 116 self._debug_reply(ui, _("Auth data found in repository URL"),
117 117 base_url, user, pwd)
118 118 self.last_reply = dict(realm=realm,authuri=authuri,user=user)
119 119 return user, pwd
120 120
121 121 # Loading .hg/hgrc [auth] section contents. If prefix is given,
122 122 # it will be used as a key to lookup password in the keyring.
123 auth_user, pwd, prefix_url = self.load_hgrc_auth(ui, base_url)
123 auth_user, pwd, prefix_url = self.load_hgrc_auth(ui, base_url, user)
124 124 if prefix_url:
125 125 keyring_url = prefix_url
126 126 else:
127 127 keyring_url = base_url
128 128 ui.debug("keyring URL: %s\n" % keyring_url)
129 129
130 130 # Checking the memory cache (there may be many http calls per command)
131 131 cache_key = (realm, keyring_url)
132 132 if not after_bad_auth:
133 133 cached_auth = self.pwd_cache.get(cache_key)
134 134 if cached_auth:
135 135 user, pwd = cached_auth
136 136 self._debug_reply(ui, _("Cached auth data found"),
137 137 base_url, user, pwd)
138 138 self.last_reply = dict(realm=realm,authuri=authuri,user=user)
139 139 return user, pwd
140 140
141 141 if auth_user:
142 142 if user and (user != auth_user):
143 143 raise util.Abort(_('mercurial_keyring: username for %s specified both in repository path (%s) and in .hg/hgrc/[auth] (%s). Please, leave only one of those' % (base_url, user, auth_user)))
144 144 user = auth_user
145 145 if pwd:
146 146 self.pwd_cache[cache_key] = user, pwd
147 147 self._debug_reply(ui, _("Auth data set in .hg/hgrc"),
148 148 base_url, user, pwd)
149 149 self.last_reply = dict(realm=realm,authuri=authuri,user=user)
150 150 return user, pwd
151 151 else:
152 152 ui.debug(_("Username found in .hg/hgrc: %s\n" % user))
153 153
154 154 # Loading password from keyring.
155 155 # Only if username is known (so we know the key) and we are
156 156 # not after failure (so we don't reuse the bad password).
157 157 if user and not after_bad_auth:
158 158 pwd = password_store.get_http_password(keyring_url, user)
159 159 if pwd:
160 160 self.pwd_cache[cache_key] = user, pwd
161 161 self._debug_reply(ui, _("Keyring password found"),
162 162 base_url, user, pwd)
163 163 self.last_reply = dict(realm=realm,authuri=authuri,user=user)
164 164 return user, pwd
165 165
166 166 # Is the username permanently set?
167 167 fixed_user = (user and True or False)
168 168
169 169 # Last resort: interactive prompt
170 170 if not ui.interactive():
171 171 raise util.Abort(_('mercurial_keyring: http authorization required but program used in non-interactive mode'))
172 172
173 173 if not fixed_user:
174 174 ui.status(_("Username not specified in .hg/hgrc. Keyring will not be used.\n"))
175 175
176 176 ui.write(_("http authorization required\n"))
177 177 ui.status(_("realm: %s\n") % realm)
178 178 if fixed_user:
179 179 ui.write(_("user: %s (fixed in .hg/hgrc)\n" % user))
180 180 else:
181 181 user = ui.prompt(_("user:"), default=None)
182 182 pwd = ui.getpass(_("password: "))
183 183
184 184 if fixed_user:
185 185 # Saving password to the keyring.
186 186 # It is done only if username is permanently set.
187 187 # Otherwise we won't be able to find the password so it
188 188 # does not make much sense to preserve it
189 189 ui.debug("Saving password for %s to keyring\n" % user)
190 190 password_store.set_http_password(keyring_url, user, pwd)
191 191
192 192 # Saving password to the memory cache
193 193 self.pwd_cache[cache_key] = user, pwd
194 194
195 195 self._debug_reply(ui, _("Manually entered password"),
196 196 base_url, user, pwd)
197 197 self.last_reply = dict(realm=realm,authuri=authuri,user=user)
198 198 return user, pwd
199 199
200 def load_hgrc_auth(self, ui, base_url):
200 def load_hgrc_auth(self, ui, base_url, user):
201 201 """
202 202 Loading [auth] section contents from local .hgrc
203 203
204 204 Returns (username, password, prefix) tuple (every
205 205 element can be None)
206 206 """
207 207 # Theoretically 3 lines below should do:
208 208
209 209 #auth_token = self.readauthtoken(base_url)
210 210 #if auth_token:
211 211 # user, pwd = auth.get('username'), auth.get('password')
212 212
213 213 # Unfortunately they do not work, readauthtoken always return
214 214 # None. Why? Because ui (self.ui of passwordmgr) describes the
215 215 # *remote* repository, so does *not* contain any option from
216 216 # local .hg/hgrc.
217 217
218 218 # TODO: mercurial 1.4.2 is claimed to resolve this problem
219 219 # (thanks to: http://hg.xavamedia.nl/mercurial/crew/rev/fb45c1e4396f)
220 220 # so since this version workaround implemented below should
221 221 # not be necessary. As it will take some time until people
222 222 # migrate to >= 1.4.2, it would be best to implement
223 223 # workaround conditionally.
224 224
225 225 # Workaround: we recreate the repository object
226 226 repo_root = ui.config("bundle", "mainreporoot")
227 227
228 228 from mercurial.ui import ui as _ui
229 229 local_ui = _ui(ui)
230 230 if repo_root:
231 231 local_ui.readconfig(os.path.join(repo_root, ".hg", "hgrc"))
232 232 try:
233 233 local_passwordmgr = passwordmgr(local_ui)
234 234 auth_token = local_passwordmgr.readauthtoken(base_url)
235 235 except AttributeError:
236 236 try:
237 237 # hg 1.8
238 238 import mercurial.url
239 239 readauthforuri = mercurial.url.readauthforuri
240 240 except (ImportError, AttributeError):
241 241 # hg 1.9
242 242 import mercurial.httpconnection
243 243 readauthforuri = mercurial.httpconnection.readauthforuri
244 res = readauthforuri(local_ui, base_url)
244 if readauthforuri.func_code.co_argcount == 3:
245 # Since hg.0593e8f81c71
246 res = readauthforuri(local_ui, base_url, user)
247 else:
248 res = readauthforuri(local_ui, base_url)
245 249 if res:
246 250 group, auth_token = res
247 251 else:
248 252 auth_token = None
249 253 if auth_token:
250 254 username = auth_token.get('username')
251 255 password = auth_token.get('password')
252 256 prefix = auth_token.get('prefix')
253 257 shortest_url = self.shortest_url(base_url, prefix)
254 258 return username, password, shortest_url
255 259
256 260 return None, None, None
257 261
258 262 def shortest_url(self, base_url, prefix):
259 263 if not prefix or prefix == '*':
260 264 return base_url
261 265 scheme, hostpath = base_url.split('://', 1)
262 266 p = prefix.split('://', 1)
263 267 if len(p) > 1:
264 268 prefix_host_path = p[1]
265 269 else:
266 270 prefix_host_path = prefix
267 271 shortest_url = scheme + '://' + prefix_host_path
268 272 return shortest_url
269 273
270 274 def canonical_url(self, authuri):
271 275 """
272 276 Strips query params from url. Used to convert urls like
273 277 https://repo.machine.com/repos/apps/module?pairs=0000000000000000000000000000000000000000-0000000000000000000000000000000000000000&cmd=between
274 278 to
275 279 https://repo.machine.com/repos/apps/module
276 280 """
277 281 parsed_url = urlparse(authuri)
278 282 return "%s://%s%s" % (parsed_url.scheme, parsed_url.netloc,
279 283 parsed_url.path)
280 284
281 285 def _debug_reply(self, ui, msg, url, user, pwd):
282 286 ui.debug("%s. Url: %s, user: %s, passwd: %s\n" % (
283 287 msg, url, user, pwd and '*' * len(pwd) or 'not set'))
284 288
285 289 ############################################################
286 290
287 291 @monkeypatch_method(passwordmgr)
288 292 def find_user_password(self, realm, authuri):
289 293 """
290 294 keyring-based implementation of username/password query
291 295 for HTTP(S) connections
292 296
293 297 Passwords are saved in gnome keyring, OSX/Chain or other platform
294 298 specific storage and keyed by the repository url
295 299 """
296 300 # Extend object attributes
297 301 if not hasattr(self, '_pwd_handler'):
298 302 self._pwd_handler = HTTPPasswordHandler()
299 303
300 304 return self._pwd_handler.find_auth(self, realm, authuri)
301 305
302 306 ############################################################
303 307
304 308 def try_smtp_login(ui, smtp_obj, username, password):
305 309 """
306 310 Attempts smtp login on smtp_obj (smtplib.SMTP) using username and
307 311 password.
308 312
309 313 Returns:
310 314 - True if login succeeded
311 315 - False if login failed due to the wrong credentials
312 316
313 317 Throws Abort exception if login failed for any other reason.
314 318
315 319 Immediately returns False if password is empty
316 320 """
317 321 if not password:
318 322 return False
319 323 try:
320 324 ui.note(_('(authenticating to mail server as %s)\n') %
321 325 (username))
322 326 smtp_obj.login(username, password)
323 327 return True
324 328 except smtplib.SMTPException, inst:
325 329 if inst.smtp_code == 535:
326 330 ui.status(_("SMTP login failed: %s\n\n") % inst.smtp_error)
327 331 return False
328 332 else:
329 333 raise util.Abort(inst)
330 334
331 335 def keyring_supported_smtp(ui, username):
332 336 """
333 337 keyring-integrated replacement for mercurial.mail._smtp
334 338 Used only when configuration file contains username, but
335 339 does not contain the password.
336 340
337 341 Most of the routine below is copied as-is from
338 342 mercurial.mail._smtp. The only changed part is
339 343 marked with #>>>>> and #<<<<< markers
340 344 """
341 345 local_hostname = ui.config('smtp', 'local_hostname')
342 346 s = smtplib.SMTP(local_hostname=local_hostname)
343 347 mailhost = ui.config('smtp', 'host')
344 348 if not mailhost:
345 349 raise util.Abort(_('no [smtp]host in hgrc - cannot send mail'))
346 350 mailport = int(ui.config('smtp', 'port', 25))
347 351 ui.note(_('sending mail: smtp host %s, port %s\n') %
348 352 (mailhost, mailport))
349 353 s.connect(host=mailhost, port=mailport)
350 354 if ui.configbool('smtp', 'tls'):
351 355 if not hasattr(socket, 'ssl'):
352 356 raise util.Abort(_("can't use TLS: Python SSL support "
353 357 "not installed"))
354 358 ui.note(_('(using tls)\n'))
355 359 s.ehlo()
356 360 s.starttls()
357 361 s.ehlo()
358 362
359 363 #>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
360 364 stored = password = password_store.get_smtp_password(
361 365 mailhost, mailport, username)
362 366 # No need to check whether password was found as try_smtp_login
363 367 # just returns False if it is absent.
364 368 while not try_smtp_login(ui, s, username, password):
365 369 password = ui.getpass(_("Password for %s on %s:%d: ") % (username, mailhost, mailport))
366 370
367 371 if stored != password:
368 372 password_store.set_smtp_password(
369 373 mailhost, mailport, username, password)
370 374 #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
371 375
372 376 def send(sender, recipients, msg):
373 377 try:
374 378 return s.sendmail(sender, recipients, msg)
375 379 except smtplib.SMTPRecipientsRefused, inst:
376 380 recipients = [r[1] for r in inst.recipients.values()]
377 381 raise util.Abort('\n' + '\n'.join(recipients))
378 382 except smtplib.SMTPException, inst:
379 383 raise util.Abort(inst)
380 384
381 385 return send
382 386
383 387 ############################################################
384 388
385 389 orig_smtp = mail._smtp
386 390
387 391 @monkeypatch_method(mail)
388 392 def _smtp(ui):
389 393 """
390 394 build an smtp connection and return a function to send email
391 395
392 396 This is the monkeypatched version of _smtp(ui) function from
393 397 mercurial/mail.py. It calls the original unless username
394 398 without password is given in the configuration.
395 399 """
396 400 username = ui.config('smtp', 'username')
397 401 password = ui.config('smtp', 'password')
398 402
399 403 if username and not password:
400 404 return keyring_supported_smtp(ui, username)
401 405 else:
402 406 return orig_smtp(ui)
General Comments 0
You need to be logged in to leave comments. Login now