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