##// END OF EJS Templates
pypi install option documented
Marcin Kasperski -
r28:eec03adc default
parent child Browse files
Show More
@@ -1,480 +1,495 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 """
10 """
11 =================
11 =================
12 mercurial_keyring
12 mercurial_keyring
13 =================
13 =================
14
14
15 Mercurial extension to securely save HTTP and SMTP authentication
15 Mercurial extension to securely save HTTP and SMTP authentication
16 passwords in password databases (Gnome Keyring, KDE KWallet,
16 passwords in password databases (Gnome Keyring, KDE KWallet,
17 OSXKeyChain, specific solutions for Win32 and command line). Uses and
17 OSXKeyChain, specific solutions for Win32 and command line). Uses and
18 wraps services of the keyring_ library.
18 wraps services of the keyring_ library.
19
19
20 .. _keyring: http://pypi.python.org/pypi/keyring
20 .. _keyring: http://pypi.python.org/pypi/keyring
21
21
22 How does it work
22 How does it work
23 ================
23 ================
24
24
25 The extension prompts for the password on the first pull/push (in case
25 The extension prompts for the password on the first pull/push (in case
26 of HTTP) or first email (in case of SMTP), just like it is done by
26 of HTTP) or first email (in case of SMTP), just like it is done by
27 default, but saves the given password (keyed by the combination of
27 default, but saves the given password (keyed by the combination of
28 username and remote repository url - for HTTP - or smtp server
28 username and remote repository url - for HTTP - or smtp server
29 address - for SMTP) in the password database. On successive runs it
29 address - for SMTP) in the password database. On successive runs it
30 checks for the username in ``.hg/hgrc``, then for suitable password in the
30 checks for the username in ``.hg/hgrc``, then for suitable password in the
31 password database, and uses those credentials if found.
31 password database, and uses those credentials if found.
32
32
33 In case password turns out to be incorrect (either because it was
33 In case password turns out to be incorrect (either because it was
34 invalid, or because it was changed on the server) it just prompts the
34 invalid, or because it was changed on the server) it just prompts the
35 user again.
35 user again.
36
36
37 Installation
37 Installation
38 ============
38 ============
39
39
40 Install keyring library:
40 Install keyring library:
41
41
42 ::
42 ::
43
43
44 easy_install keyring
44 easy_install keyring
45
45
46 (or ``pip keyring``). On Debian "Sid" the library can be also
46 (or ``pip keyring``). On Debian "Sid" the library can be also
47 installed from the official archive (packages ``python-keyring``,
47 installed from the official archive (packages ``python-keyring``,
48 ``python-keyring-gnome`` and ``python-keyring-kwallet``).
48 ``python-keyring-gnome`` and ``python-keyring-kwallet``).
49
49
50 Then either save ``mercurial_keyring.py`` anywhere and put the
50 Then use one of the three options:
51 following in ~/.hgrc (or /etc/mercurial/hgrc):
51
52 a) download ``mercurial_keyring.py``, save it anywhere you like and
53 put the following in ``~/.hgrc`` (or ``/etc/mercurial/hgrc``):
52
54
53 ::
55 ::
54
56
55 [extensions]
57 [extensions]
56 hgext.mercurial_keyring = /path/to/mercurial_keyring.py
58 hgext.mercurial_keyring = /path/to/mercurial_keyring.py
57
59
58 or save ``mercurial_keyring.py`` to ``mercurial/hgext`` directory and
60 b) save ``mercurial_keyring.py`` to ``mercurial/hgext`` directory and
59 use
61 use
60
62
61 ::
63 ::
62
64
63 [extensions]
65 [extensions]
64 hgext.mercurial_keyring =
66 hgext.mercurial_keyring =
65
67
68 c) install ``mercurial_keyring`` using ``easy_install``:
69
70 ::
71
72 easy_install mercurial_keyring
73
74 and then configure ``~/.hgrc`` so:
75
76 ::
77
78 [extensions]
79 mercurial_keyring =
80
66 Password backend configuration
81 Password backend configuration
67 ==============================
82 ==============================
68
83
69 The library should usually pick the most appropriate password backend
84 The library should usually pick the most appropriate password backend
70 without configuration. Still, if necessary, it can be configured using
85 without configuration. Still, if necessary, it can be configured using
71 ``~/keyringrc.cfg`` file (``keyringrc.cfg`` in the home directory of
86 ``~/keyringrc.cfg`` file (``keyringrc.cfg`` in the home directory of
72 the current user). Refer to keyring_ docs for more details.
87 the current user). Refer to keyring_ docs for more details.
73
88
74 ''I considered handling similar options in hgrc, but decided that
89 ''I considered handling similar options in hgrc, but decided that
75 single user may use more than one keyring-based script. Still, I am
90 single user may use more than one keyring-based script. Still, I am
76 open to suggestions.''
91 open to suggestions.''
77
92
78 Repository configuration (HTTP)
93 Repository configuration (HTTP)
79 ===============================
94 ===============================
80
95
81 Edit repository-local ``.hg/hgrc`` and save there the remote
96 Edit repository-local ``.hg/hgrc`` and save there the remote
82 repository path and the username, but do not save the password. For
97 repository path and the username, but do not save the password. For
83 example:
98 example:
84
99
85 ::
100 ::
86
101
87 [paths]
102 [paths]
88 myremote = https://my.server.com/hgrepo/someproject
103 myremote = https://my.server.com/hgrepo/someproject
89
104
90 [auth]
105 [auth]
91 myremote.schemes = http https
106 myremote.schemes = http https
92 myremote.prefix = my.server.com/hgrepo
107 myremote.prefix = my.server.com/hgrepo
93 myremote.username = mekk
108 myremote.username = mekk
94
109
95 Simpler form with url-embedded name can also be used:
110 Simpler form with url-embedded name can also be used:
96
111
97 ::
112 ::
98
113
99 [paths]
114 [paths]
100 bitbucket = https://User@bitbucket.org/User/project_name/
115 bitbucket = https://User@bitbucket.org/User/project_name/
101
116
102 Note: if both username and password are given in ``.hg/hgrc``,
117 Note: if both username and password are given in ``.hg/hgrc``,
103 extension will use them without using the password database. If
118 extension will use them without using the password database. If
104 username is not given, extension will prompt for credentials every
119 username is not given, extension will prompt for credentials every
105 time, also without saving the password.
120 time, also without saving the password.
106
121
107 Repository configuration (SMTP)
122 Repository configuration (SMTP)
108 ===============================
123 ===============================
109
124
110 Edit either repository-local ``.hg/hgrc``, or ``~/.hgrc`` and set
125 Edit either repository-local ``.hg/hgrc``, or ``~/.hgrc`` and set
111 there all standard email and smtp properties, including smtp
126 there all standard email and smtp properties, including smtp
112 username, but without smtp password. For example:
127 username, but without smtp password. For example:
113
128
114 ::
129 ::
115
130
116 [email]
131 [email]
117 method = smtp
132 method = smtp
118 from = Joe Doe <Joe.Doe@remote.com>
133 from = Joe Doe <Joe.Doe@remote.com>
119
134
120 [smtp]
135 [smtp]
121 host = smtp.gmail.com
136 host = smtp.gmail.com
122 port = 587
137 port = 587
123 username = JoeDoe@gmail.com
138 username = JoeDoe@gmail.com
124 tls = true
139 tls = true
125
140
126 Just as in case of HTTP, you *must* set username, but *must not* set
141 Just as in case of HTTP, you *must* set username, but *must not* set
127 password here to use the extension, in other cases it will revert to
142 password here to use the extension, in other cases it will revert to
128 the default behaviour.
143 the default behaviour.
129
144
130 Usage
145 Usage
131 =====
146 =====
132
147
133 Configure the repository as above, then just pull, push, etc.
148 Configure the repository as above, then just pull, push, etc.
134 You should be asked for the password only once (per every
149 You should be asked for the password only once (per every
135 username+remote_repository_url combination).
150 username+remote_repository_url combination).
136
151
137 Similarly, for email, configure as above and just email.
152 Similarly, for email, configure as above and just email.
138 Again, you will be asked for the password once (per every
153 Again, you will be asked for the password once (per every
139 username+email_server_name+email_server_port).
154 username+email_server_name+email_server_port).
140
155
141 Implementation details
156 Implementation details
142 ======================
157 ======================
143
158
144 The extension is monkey-patching the mercurial passwordmgr class to
159 The extension is monkey-patching the mercurial passwordmgr class to
145 replace the find_user_password method. Detailed order of operations
160 replace the find_user_password method. Detailed order of operations
146 is described in the comments inside the code.
161 is described in the comments inside the code.
147
162
148 """
163 """
149
164
150 from mercurial import hg, repo, util
165 from mercurial import hg, repo, util
151 from mercurial.i18n import _
166 from mercurial.i18n import _
152 try:
167 try:
153 from mercurial.url import passwordmgr
168 from mercurial.url import passwordmgr
154 except:
169 except:
155 from mercurial.httprepo import passwordmgr
170 from mercurial.httprepo import passwordmgr
156 from mercurial.httprepo import httprepository
171 from mercurial.httprepo import httprepository
157 from mercurial import mail
172 from mercurial import mail
158
173
159 import keyring
174 import keyring
160 from urlparse import urlparse
175 from urlparse import urlparse
161 import urllib2
176 import urllib2
162 import smtplib, socket
177 import smtplib, socket
163
178
164 KEYRING_SERVICE = "Mercurial"
179 KEYRING_SERVICE = "Mercurial"
165
180
166 ############################################################
181 ############################################################
167
182
168 def monkeypatch_method(cls):
183 def monkeypatch_method(cls):
169 def decorator(func):
184 def decorator(func):
170 setattr(cls, func.__name__, func)
185 setattr(cls, func.__name__, func)
171 return func
186 return func
172 return decorator
187 return decorator
173
188
174 ############################################################
189 ############################################################
175
190
176 class PasswordStore(object):
191 class PasswordStore(object):
177 """
192 """
178 Helper object handling keyring usage (password save&restore,
193 Helper object handling keyring usage (password save&restore,
179 the way passwords are keyed in the keyring).
194 the way passwords are keyed in the keyring).
180 """
195 """
181 def __init__(self):
196 def __init__(self):
182 self.cache = dict()
197 self.cache = dict()
183 def get_http_password(self, url, username):
198 def get_http_password(self, url, username):
184 return keyring.get_password(KEYRING_SERVICE,
199 return keyring.get_password(KEYRING_SERVICE,
185 self._format_http_key(url, username))
200 self._format_http_key(url, username))
186 def set_http_password(self, url, username, password):
201 def set_http_password(self, url, username, password):
187 keyring.set_password(KEYRING_SERVICE,
202 keyring.set_password(KEYRING_SERVICE,
188 self._format_http_key(url, username),
203 self._format_http_key(url, username),
189 password)
204 password)
190 def clear_http_password(self, url, username):
205 def clear_http_password(self, url, username):
191 self.set_http_password(url, username, "")
206 self.set_http_password(url, username, "")
192 def _format_http_key(self, url, username):
207 def _format_http_key(self, url, username):
193 return "%s@@%s" % (username, url)
208 return "%s@@%s" % (username, url)
194 def get_smtp_password(self, machine, port, username):
209 def get_smtp_password(self, machine, port, username):
195 return keyring.get_password(
210 return keyring.get_password(
196 KEYRING_SERVICE,
211 KEYRING_SERVICE,
197 self._format_smtp_key(machine, port, username))
212 self._format_smtp_key(machine, port, username))
198 def set_smtp_password(self, machine, port, username, password):
213 def set_smtp_password(self, machine, port, username, password):
199 keyring.set_password(
214 keyring.set_password(
200 KEYRING_SERVICE,
215 KEYRING_SERVICE,
201 self._format_smtp_key(machine, port, username),
216 self._format_smtp_key(machine, port, username),
202 password)
217 password)
203 def clear_smtp_password(self, machine, port, username):
218 def clear_smtp_password(self, machine, port, username):
204 self.set_smtp_password(url, username, "")
219 self.set_smtp_password(url, username, "")
205 def _format_smtp_key(self, machine, port, username):
220 def _format_smtp_key(self, machine, port, username):
206 return "%s@@%s:%s" % (username, machine, str(port))
221 return "%s@@%s:%s" % (username, machine, str(port))
207
222
208 password_store = PasswordStore()
223 password_store = PasswordStore()
209
224
210 ############################################################
225 ############################################################
211
226
212 class HTTPPasswordHandler(object):
227 class HTTPPasswordHandler(object):
213 """
228 """
214 Actual implementation of password handling (user prompting,
229 Actual implementation of password handling (user prompting,
215 configuration file searching, keyring save&restore).
230 configuration file searching, keyring save&restore).
216
231
217 Object of this class is bound as passwordmgr attribute.
232 Object of this class is bound as passwordmgr attribute.
218 """
233 """
219 def __init__(self):
234 def __init__(self):
220 self.pwd_cache = {}
235 self.pwd_cache = {}
221 self.last_reply = None
236 self.last_reply = None
222
237
223 def find_auth(self, pwmgr, realm, authuri):
238 def find_auth(self, pwmgr, realm, authuri):
224 """
239 """
225 Actual implementation of find_user_password - different
240 Actual implementation of find_user_password - different
226 ways of obtaining the username and password.
241 ways of obtaining the username and password.
227 """
242 """
228 ui = pwmgr.ui
243 ui = pwmgr.ui
229
244
230 # If we are called again just after identical previous
245 # If we are called again just after identical previous
231 # request, then the previously returned auth must have been
246 # request, then the previously returned auth must have been
232 # wrong. So we note this to force password prompt (and avoid
247 # wrong. So we note this to force password prompt (and avoid
233 # reusing bad password indifinitely).
248 # reusing bad password indifinitely).
234 after_bad_auth = (self.last_reply \
249 after_bad_auth = (self.last_reply \
235 and (self.last_reply['realm'] == realm) \
250 and (self.last_reply['realm'] == realm) \
236 and (self.last_reply['authuri'] == authuri))
251 and (self.last_reply['authuri'] == authuri))
237
252
238 # Strip arguments to get actual remote repository url.
253 # Strip arguments to get actual remote repository url.
239 base_url = self.canonical_url(authuri)
254 base_url = self.canonical_url(authuri)
240
255
241 # Extracting possible username (or password)
256 # Extracting possible username (or password)
242 # stored directly in repository url
257 # stored directly in repository url
243 user, pwd = urllib2.HTTPPasswordMgrWithDefaultRealm.find_user_password(
258 user, pwd = urllib2.HTTPPasswordMgrWithDefaultRealm.find_user_password(
244 pwmgr, realm, authuri)
259 pwmgr, realm, authuri)
245 if user and pwd:
260 if user and pwd:
246 self._debug_reply(ui, _("Auth data found in repository URL"),
261 self._debug_reply(ui, _("Auth data found in repository URL"),
247 base_url, user, pwd)
262 base_url, user, pwd)
248 self.last_reply = dict(realm=realm,authuri=authuri,user=user)
263 self.last_reply = dict(realm=realm,authuri=authuri,user=user)
249 return user, pwd
264 return user, pwd
250
265
251 # Checking the memory cache (there may be many http calls per command)
266 # Checking the memory cache (there may be many http calls per command)
252 cache_key = (realm, base_url)
267 cache_key = (realm, base_url)
253 if not after_bad_auth:
268 if not after_bad_auth:
254 cached_auth = self.pwd_cache.get(cache_key)
269 cached_auth = self.pwd_cache.get(cache_key)
255 if cached_auth:
270 if cached_auth:
256 user, pwd = cached_auth
271 user, pwd = cached_auth
257 self._debug_reply(ui, _("Cached auth data found"),
272 self._debug_reply(ui, _("Cached auth data found"),
258 base_url, user, pwd)
273 base_url, user, pwd)
259 self.last_reply = dict(realm=realm,authuri=authuri,user=user)
274 self.last_reply = dict(realm=realm,authuri=authuri,user=user)
260 return user, pwd
275 return user, pwd
261
276
262 # Loading username and maybe password from [auth] in .hg/hgrc
277 # Loading username and maybe password from [auth] in .hg/hgrc
263 nuser, pwd = self.load_hgrc_auth(ui, base_url)
278 nuser, pwd = self.load_hgrc_auth(ui, base_url)
264 if nuser:
279 if nuser:
265 if user:
280 if user:
266 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)))
281 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)))
267 user = nuser
282 user = nuser
268 if pwd:
283 if pwd:
269 self.pwd_cache[cache_key] = user, pwd
284 self.pwd_cache[cache_key] = user, pwd
270 self._debug_reply(ui, _("Auth data set in .hg/hgrc"),
285 self._debug_reply(ui, _("Auth data set in .hg/hgrc"),
271 base_url, user, pwd)
286 base_url, user, pwd)
272 self.last_reply = dict(realm=realm,authuri=authuri,user=user)
287 self.last_reply = dict(realm=realm,authuri=authuri,user=user)
273 return user, pwd
288 return user, pwd
274 else:
289 else:
275 ui.debug(_("Username found in .hg/hgrc: %s\n" % user))
290 ui.debug(_("Username found in .hg/hgrc: %s\n" % user))
276
291
277 # Loading password from keyring.
292 # Loading password from keyring.
278 # Only if username is known (so we know the key) and we are
293 # Only if username is known (so we know the key) and we are
279 # not after failure (so we don't reuse the bad password).
294 # not after failure (so we don't reuse the bad password).
280 if user and not after_bad_auth:
295 if user and not after_bad_auth:
281 pwd = password_store.get_http_password(base_url, user)
296 pwd = password_store.get_http_password(base_url, user)
282 if pwd:
297 if pwd:
283 self.pwd_cache[cache_key] = user, pwd
298 self.pwd_cache[cache_key] = user, pwd
284 self._debug_reply(ui, _("Keyring password found"),
299 self._debug_reply(ui, _("Keyring password found"),
285 base_url, user, pwd)
300 base_url, user, pwd)
286 self.last_reply = dict(realm=realm,authuri=authuri,user=user)
301 self.last_reply = dict(realm=realm,authuri=authuri,user=user)
287 return user, pwd
302 return user, pwd
288
303
289 # Is the username permanently set?
304 # Is the username permanently set?
290 fixed_user = (user and True or False)
305 fixed_user = (user and True or False)
291
306
292 # Last resort: interactive prompt
307 # Last resort: interactive prompt
293 if not ui.interactive():
308 if not ui.interactive():
294 raise util.Abort(_('mercurial_keyring: http authorization required'))
309 raise util.Abort(_('mercurial_keyring: http authorization required'))
295 ui.write(_("http authorization required\n"))
310 ui.write(_("http authorization required\n"))
296 ui.status(_("realm: %s\n") % realm)
311 ui.status(_("realm: %s\n") % realm)
297 if fixed_user:
312 if fixed_user:
298 ui.write(_("user: %s (fixed in .hg/hgrc)\n" % user))
313 ui.write(_("user: %s (fixed in .hg/hgrc)\n" % user))
299 else:
314 else:
300 user = ui.prompt(_("user:"), default=None)
315 user = ui.prompt(_("user:"), default=None)
301 pwd = ui.getpass(_("password: "))
316 pwd = ui.getpass(_("password: "))
302
317
303 if fixed_user:
318 if fixed_user:
304 # Saving password to the keyring.
319 # Saving password to the keyring.
305 # It is done only if username is permanently set.
320 # It is done only if username is permanently set.
306 # Otherwise we won't be able to find the password so it
321 # Otherwise we won't be able to find the password so it
307 # does not make much sense to preserve it
322 # does not make much sense to preserve it
308 ui.debug("Saving password for %s to keyring\n" % user)
323 ui.debug("Saving password for %s to keyring\n" % user)
309 password_store.set_http_password(base_url, user, pwd)
324 password_store.set_http_password(base_url, user, pwd)
310
325
311 # Saving password to the memory cache
326 # Saving password to the memory cache
312 self.pwd_cache[cache_key] = user, pwd
327 self.pwd_cache[cache_key] = user, pwd
313
328
314 self._debug_reply(ui, _("Manually entered password"),
329 self._debug_reply(ui, _("Manually entered password"),
315 base_url, user, pwd)
330 base_url, user, pwd)
316 self.last_reply = dict(realm=realm,authuri=authuri,user=user)
331 self.last_reply = dict(realm=realm,authuri=authuri,user=user)
317 return user, pwd
332 return user, pwd
318
333
319 def load_hgrc_auth(self, ui, base_url):
334 def load_hgrc_auth(self, ui, base_url):
320 """
335 """
321 Loading username and possibly password from [auth] in local
336 Loading username and possibly password from [auth] in local
322 repo .hgrc
337 repo .hgrc
323 """
338 """
324 # Theoretically 3 lines below should do:
339 # Theoretically 3 lines below should do:
325
340
326 #auth_token = self.readauthtoken(base_url)
341 #auth_token = self.readauthtoken(base_url)
327 #if auth_token:
342 #if auth_token:
328 # user, pwd = auth.get('username'), auth.get('password')
343 # user, pwd = auth.get('username'), auth.get('password')
329
344
330 # Unfortunately they do not work, readauthtoken always return
345 # Unfortunately they do not work, readauthtoken always return
331 # None. Why? Because ui (self.ui of passwordmgr) describes the
346 # None. Why? Because ui (self.ui of passwordmgr) describes the
332 # *remote* repository, so does *not* contain any option from
347 # *remote* repository, so does *not* contain any option from
333 # local .hg/hgrc.
348 # local .hg/hgrc.
334
349
335 # Workaround: we recreate the repository object
350 # Workaround: we recreate the repository object
336 repo_root = ui.config("bundle", "mainreporoot")
351 repo_root = ui.config("bundle", "mainreporoot")
337 if repo_root:
352 if repo_root:
338 from mercurial.ui import ui as _ui
353 from mercurial.ui import ui as _ui
339 import os
354 import os
340 local_ui = _ui(ui)
355 local_ui = _ui(ui)
341 local_ui.readconfig(os.path.join(repo_root, ".hg", "hgrc"))
356 local_ui.readconfig(os.path.join(repo_root, ".hg", "hgrc"))
342 local_passwordmgr = passwordmgr(local_ui)
357 local_passwordmgr = passwordmgr(local_ui)
343 auth_token = local_passwordmgr.readauthtoken(base_url)
358 auth_token = local_passwordmgr.readauthtoken(base_url)
344 if auth_token:
359 if auth_token:
345 return auth_token.get('username'), auth_token.get('password')
360 return auth_token.get('username'), auth_token.get('password')
346 return None, None
361 return None, None
347
362
348 def canonical_url(self, authuri):
363 def canonical_url(self, authuri):
349 """
364 """
350 Strips query params from url. Used to convert urls like
365 Strips query params from url. Used to convert urls like
351 https://repo.machine.com/repos/apps/module?pairs=0000000000000000000000000000000000000000-0000000000000000000000000000000000000000&cmd=between
366 https://repo.machine.com/repos/apps/module?pairs=0000000000000000000000000000000000000000-0000000000000000000000000000000000000000&cmd=between
352 to
367 to
353 https://repo.machine.com/repos/apps/module
368 https://repo.machine.com/repos/apps/module
354 """
369 """
355 parsed_url = urlparse(authuri)
370 parsed_url = urlparse(authuri)
356 return "%s://%s%s" % (parsed_url.scheme, parsed_url.netloc,
371 return "%s://%s%s" % (parsed_url.scheme, parsed_url.netloc,
357 parsed_url.path)
372 parsed_url.path)
358
373
359 def _debug_reply(self, ui, msg, url, user, pwd):
374 def _debug_reply(self, ui, msg, url, user, pwd):
360 ui.debug("%s. Url: %s, user: %s, passwd: %s\n" % (
375 ui.debug("%s. Url: %s, user: %s, passwd: %s\n" % (
361 msg, url, user, pwd and '*' * len(pwd) or 'not set'))
376 msg, url, user, pwd and '*' * len(pwd) or 'not set'))
362
377
363 ############################################################
378 ############################################################
364
379
365 @monkeypatch_method(passwordmgr)
380 @monkeypatch_method(passwordmgr)
366 def find_user_password(self, realm, authuri):
381 def find_user_password(self, realm, authuri):
367 """
382 """
368 keyring-based implementation of username/password query
383 keyring-based implementation of username/password query
369 for HTTP(S) connections
384 for HTTP(S) connections
370
385
371 Passwords are saved in gnome keyring, OSX/Chain or other platform
386 Passwords are saved in gnome keyring, OSX/Chain or other platform
372 specific storage and keyed by the repository url
387 specific storage and keyed by the repository url
373 """
388 """
374 # Extend object attributes
389 # Extend object attributes
375 if not hasattr(self, '_pwd_handler'):
390 if not hasattr(self, '_pwd_handler'):
376 self._pwd_handler = HTTPPasswordHandler()
391 self._pwd_handler = HTTPPasswordHandler()
377
392
378 return self._pwd_handler.find_auth(self, realm, authuri)
393 return self._pwd_handler.find_auth(self, realm, authuri)
379
394
380 ############################################################
395 ############################################################
381
396
382 def try_smtp_login(ui, smtp_obj, username, password):
397 def try_smtp_login(ui, smtp_obj, username, password):
383 """
398 """
384 Attempts smtp login on smtp_obj (smtplib.SMTP) using username and
399 Attempts smtp login on smtp_obj (smtplib.SMTP) using username and
385 password.
400 password.
386
401
387 Returns:
402 Returns:
388 - True if login succeeded
403 - True if login succeeded
389 - False if login failed due to the wrong credentials
404 - False if login failed due to the wrong credentials
390
405
391 Throws Abort exception if login failed for any other reason.
406 Throws Abort exception if login failed for any other reason.
392
407
393 Immediately returns False if password is empty
408 Immediately returns False if password is empty
394 """
409 """
395 if not password:
410 if not password:
396 return False
411 return False
397 try:
412 try:
398 ui.note(_('(authenticating to mail server as %s)\n') %
413 ui.note(_('(authenticating to mail server as %s)\n') %
399 (username))
414 (username))
400 smtp_obj.login(username, password)
415 smtp_obj.login(username, password)
401 return True
416 return True
402 except smtplib.SMTPException, inst:
417 except smtplib.SMTPException, inst:
403 if inst.smtp_code == 535:
418 if inst.smtp_code == 535:
404 ui.status(_("SMTP login failed: %s\n\n") % inst.smtp_error)
419 ui.status(_("SMTP login failed: %s\n\n") % inst.smtp_error)
405 return False
420 return False
406 else:
421 else:
407 raise util.Abort(inst)
422 raise util.Abort(inst)
408
423
409 def keyring_supported_smtp(ui, username):
424 def keyring_supported_smtp(ui, username):
410 """
425 """
411 keyring-integrated replacement for mercurial.mail._smtp
426 keyring-integrated replacement for mercurial.mail._smtp
412 Used only when configuration file contains username, but
427 Used only when configuration file contains username, but
413 does not contain the password.
428 does not contain the password.
414
429
415 Most of the routine below is copied as-is from
430 Most of the routine below is copied as-is from
416 mercurial.mail._smtp. The only changed part is
431 mercurial.mail._smtp. The only changed part is
417 marked with #>>>>> and #<<<<< markers
432 marked with #>>>>> and #<<<<< markers
418 """
433 """
419 local_hostname = ui.config('smtp', 'local_hostname')
434 local_hostname = ui.config('smtp', 'local_hostname')
420 s = smtplib.SMTP(local_hostname=local_hostname)
435 s = smtplib.SMTP(local_hostname=local_hostname)
421 mailhost = ui.config('smtp', 'host')
436 mailhost = ui.config('smtp', 'host')
422 if not mailhost:
437 if not mailhost:
423 raise util.Abort(_('no [smtp]host in hgrc - cannot send mail'))
438 raise util.Abort(_('no [smtp]host in hgrc - cannot send mail'))
424 mailport = int(ui.config('smtp', 'port', 25))
439 mailport = int(ui.config('smtp', 'port', 25))
425 ui.note(_('sending mail: smtp host %s, port %s\n') %
440 ui.note(_('sending mail: smtp host %s, port %s\n') %
426 (mailhost, mailport))
441 (mailhost, mailport))
427 s.connect(host=mailhost, port=mailport)
442 s.connect(host=mailhost, port=mailport)
428 if ui.configbool('smtp', 'tls'):
443 if ui.configbool('smtp', 'tls'):
429 if not hasattr(socket, 'ssl'):
444 if not hasattr(socket, 'ssl'):
430 raise util.Abort(_("can't use TLS: Python SSL support "
445 raise util.Abort(_("can't use TLS: Python SSL support "
431 "not installed"))
446 "not installed"))
432 ui.note(_('(using tls)\n'))
447 ui.note(_('(using tls)\n'))
433 s.ehlo()
448 s.ehlo()
434 s.starttls()
449 s.starttls()
435 s.ehlo()
450 s.ehlo()
436
451
437 #>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
452 #>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
438 stored = password = password_store.get_smtp_password(
453 stored = password = password_store.get_smtp_password(
439 mailhost, mailport, username)
454 mailhost, mailport, username)
440 # No need to check whether password was found as try_smtp_login
455 # No need to check whether password was found as try_smtp_login
441 # just returns False if it is absent.
456 # just returns False if it is absent.
442 while not try_smtp_login(ui, s, username, password):
457 while not try_smtp_login(ui, s, username, password):
443 password = ui.getpass(_("Password for %s on %s:%d: ") % (username, mailhost, mailport))
458 password = ui.getpass(_("Password for %s on %s:%d: ") % (username, mailhost, mailport))
444
459
445 if stored != password:
460 if stored != password:
446 password_store.set_smtp_password(
461 password_store.set_smtp_password(
447 mailhost, mailport, username, password)
462 mailhost, mailport, username, password)
448 #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
463 #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
449
464
450 def send(sender, recipients, msg):
465 def send(sender, recipients, msg):
451 try:
466 try:
452 return s.sendmail(sender, recipients, msg)
467 return s.sendmail(sender, recipients, msg)
453 except smtplib.SMTPRecipientsRefused, inst:
468 except smtplib.SMTPRecipientsRefused, inst:
454 recipients = [r[1] for r in inst.recipients.values()]
469 recipients = [r[1] for r in inst.recipients.values()]
455 raise util.Abort('\n' + '\n'.join(recipients))
470 raise util.Abort('\n' + '\n'.join(recipients))
456 except smtplib.SMTPException, inst:
471 except smtplib.SMTPException, inst:
457 raise util.Abort(inst)
472 raise util.Abort(inst)
458
473
459 return send
474 return send
460
475
461 ############################################################
476 ############################################################
462
477
463 orig_smtp = mail._smtp
478 orig_smtp = mail._smtp
464
479
465 @monkeypatch_method(mail)
480 @monkeypatch_method(mail)
466 def _smtp(ui):
481 def _smtp(ui):
467 """
482 """
468 build an smtp connection and return a function to send email
483 build an smtp connection and return a function to send email
469
484
470 This is the monkeypatched version of _smtp(ui) function from
485 This is the monkeypatched version of _smtp(ui) function from
471 mercurial/mail.py. It calls the original unless username
486 mercurial/mail.py. It calls the original unless username
472 without password is given in the configuration.
487 without password is given in the configuration.
473 """
488 """
474 username = ui.config('smtp', 'username')
489 username = ui.config('smtp', 'username')
475 password = ui.config('smtp', 'password')
490 password = ui.config('smtp', 'password')
476
491
477 if username and not password:
492 if username and not password:
478 return keyring_supported_smtp(ui, username)
493 return keyring_supported_smtp(ui, username)
479 else:
494 else:
480 return orig_smtp(ui)
495 return orig_smtp(ui)
General Comments 0
You need to be logged in to leave comments. Login now