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