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