##// END OF EJS Templates
#45 Added hg keyring_clear
Marcin Kasperski -
r191:03cb673d default
parent child Browse files
Show More
@@ -1,279 +1,316 b''
1 1 .. -*- mode: rst; compile-command: "rst2html README.txt README.html" -*-
2 2
3 =================
3 =======================================================
4 4 Mercurial Keyring
5 =================
5 =======================================================
6 6
7 7 Mercurial Keyring is a Mercurial_ extension used to securely save HTTP
8 8 and SMTP authentication passwords in password databases (Gnome
9 9 Keyring, KDE KWallet, OSXKeyChain, Windows Vault etc).
10 10
11 11 With ``mercurial_keyring`` active, Mercurial remembers your passwords
12 12 and reuses them without prompting (as if you stored them in ``.hgrc``),
13 13 but password storage is reasonably secure.
14 14
15 15 Actual password storage is implemented by the keyring_ library, this
16 16 extension glues it to Mercurial.
17 17
18 18 .. contents::
19 19 :local:
20 20 :depth: 2
21 21
22 22 .. sectnum::
23 23
24 24 .. _keyring: http://pypi.python.org/pypi/keyring
25 25 .. _Mercurial: http://mercurial.selenic.com
26 26
27 27 How does it work
28 ================
28 =======================================================
29 29
30 30 On your first pull or push to HTTP url (or first email sent via given
31 31 SMTP server), you are prompted for the password, just like bare
32 32 Mercurial does. But the password you entered is saved to appropriate
33 33 password database. On successive runs, whenever the password is
34 34 needed, ``mercurial_keyring`` checks for password in password
35 35 database, and uses it without troubling you.
36 36
37 37 In case password turns out to be incorrect (for example, because you
38 changed it, or entered it incorrectly), ``mercurial_keyring`` wipes
39 it, and prompts you again.
38 changed it, or entered it incorrectly), ``mercurial_keyring`` prompts
39 you again, and overwrites the password.
40 40
41 41 You can use many passwords (for various remote urls). Saved passwords
42 42 are identified by pair of username and url prefix. See below for
43 43 information how to configure those properly.
44 44
45 45 Installation
46 ============
46 =======================================================
47 47
48 48 Prerequisites
49 49 -------------
50 50
51 51 This extension requires keyring_ and `mercurial_extension_utils`_ to
52 52 work. In many cases both will be installed automatically while you
53 53 install ``mercurial_keyring``, but you may need to control the process.
54 54
55 55 The keyring_ library can usually be installed by::
56 56
57 57 pip install --user keyring
58 58
59 59 (or ``easy_install keyring``), but on some systems it is preferable to
60 60 use official distribution archive. For example, on Debian and Ubuntu,
61 61 you may install ``python-keyring`` and either ``python-keyring-gnome``
62 62 or ``python-keyring-kwallet`` packages::
63 63
64 64 sudo apt-get install python-keyring python-keyring-gnome
65 65
66 66 (this will save you the need to provide working compiler and various
67 67 development libraries).
68 68
69 69 The `mercurial_extension_utils`_ module is tiny Python-only module,
70 70 which can be installed by::
71 71
72 72 pip install --user mercurial_extension_utils
73 73
74 74 but in some cases (Windows…) require more care. See
75 75 `mercurial_extension_utils`_ documentation.
76 76
77 77
78 78 Extension installation
79 79 ----------------------
80 80
81 81 There are two possible ways of installing the extension: using PyPi package,
82 82 or using source clone.
83 83
84 84 To install as a package::
85 85
86 86 pip install --user mercurial_keyring
87 87
88 88 (or ``sudo pip install mercurial_keyring`` for system-wide
89 89 installation) and then enable it in ``~/.hgrc`` (or
90 90 ``/etc/mercurial/hgrc`` or ``Mercurial.ini``) using::
91 91
92 92 [extensions]
93 93 mercurial_keyring =
94 94
95 95 To install as source clone, install keyring_ according to instructions above, then
96 96 clone::
97 97
98 98 hg clone https://bitbucket.org/Mekk/mercurial_keyring/
99 99 hg clone https://bitbucket.org/Mekk/mercurial-extension_utils/
100 100
101 101 and configure Mercurial by telling it full path to the extension
102 102 (in )::
103 103
104 104 [extensions]
105 105 mercurial_keyring = /path/to/mercurial_keyring/mercurial_keyring.py
106 106
107 107 .. _the code:
108 108 .. _mercurial_keyring.py: http://bitbucket.org/Mekk/mercurial_keyring/src/tip/mercurial_keyring.py
109 109
110 110 Password backend configuration
111 ==============================
111 =======================================================
112 112
113 113 The library should usually pick the most appropriate password backend
114 114 without configuration. Still, if necessary, it can be configured using
115 115 ``keyringrc.cfg`` file. Refer to keyring_ docs for more details.
116 116
117 117 .. note::
118 118
119 119 With current (as I write) keyring (5.6), this file is (on Linux)
120 120 located at ``~/.local/share/python_keyring/keyringrc.cfg`` and
121 121 it's example content look like::
122 122
123 123 [backend]
124 124 default-keyring=keyring.backends.Gnome.Keyring
125 125 # default-keyring=keyring.backends.kwallet.Keyring
126 126
127 127 For list of known backends run ``pydoc keyring.backends``.
128 128
129 129
130 130 ``hgrc`` configuration (HTTP)
131 ===============================
131 =======================================================
132 132
133 133 Mercurial Keyring uses standard Mercurial ``[auth]`` configuration to
134 134 detect your username (on given remote) and url prefix. You are
135 135 strongly advised to configure both.
136 136
137 137 Without the username ``mercurial_keyring`` can't save or restore
138 138 passwords, so it disables itself.
139 139
140 140 Without url prefix ``mercurial_keyring`` works, but binds passwords to
141 141 repository urls. That means you will have to (re)enter password for
142 142 every repository cloned from given remote (and that there will be many
143 143 copies of this password in secure storage).
144 144
145 145 Repository level configuration
146 146 ------------------------------------
147 147
148 148 Edit repository-local ``.hg/hgrc`` and save there the remote
149 149 repository path and the username, but do not save the password. For
150 150 example:
151 151
152 152 ::
153 153
154 154 [paths]
155 155 myremote = https://my.server.com/hgrepo/someproject
156 156
157 157 [auth]
158 158 myremote.prefix = https://my.server.com/hgrepo
159 159 myremote.username = John
160 160
161 161 Simpler form with url-embedded name can also be used:
162 162
163 163 ::
164 164
165 165 [paths]
166 166 bitbucket = https://John@my.server.com/hgrepo/someproject/
167 167
168 168 Note that all repositories sharing the same ``prefix`` share the same
169 169 password.
170 170
171 171 Mercurial allows also for password in ``.hg/hgrc`` (either given by
172 172 ``Β«prefixΒ».password``, or embedded in url). If such password is found,
173 173 Mercurial Keyring disables itself.
174 174
175 175
176 176 Account-level configuration
177 177 ---------------------------
178 178
179 179 If you are consistent about remote repository nicknames, you can
180 180 configure the username in your `~/.hgrc` (`.hgrc` in your home
181 181 directory). For example, write there::
182 182
183 183 [auth]
184 184 acme.prefix = hg.acme.com/repositories
185 185 acme.username = johnny
186 186 acme.schemes = http https
187 187 bitbucket.prefix = https://bitbucket.org
188 188 bitbucket.username = Mekk
189 189 mydep.prefix = https://dev.acmeorg.com
190 190 mydep.username = drmartin
191 191
192 192 and as long as you will be using alias ``acme`` for repositories like
193 193 ``https://hg.acme.com/repositories/my_beautiful_app``, username
194 194 ``johnny`` will be used, and the same password reused. Similarly
195 195 any ``hg push bitbucket`` will share the same password.
196 196
197 197 With such config repository-level ``.hg/hgrc`` need only contain
198 198 ``[paths]``.
199 199
200 200 Additional advantage of this method is that it works also during
201 201 `clone`.
202 202
203 203
204 204 .. note::
205 205
206 206 Mercurial Keyring works well with `Path Pattern`_. On my setup I use::
207 207
208 208 [path_pattern]
209 209 bitbucket.local = ~/devel/{below}
210 210 bitbucket.remote = https://bitbucket.org/Mekk/{below:/=-}
211 211
212 212 so all my repositories understand ``hg push bitbucket`` without
213 213 any repository-level configuration.
214 214
215 215
216 216 ``hgrc`` configuration (SMTP)
217 ===============================
217 =======================================================
218 218
219 219 Edit either repository-local ``.hg/hgrc``, or ``~/.hgrc`` and set
220 220 there all standard email and smtp properties, including SMTP
221 221 username, but without SMTP password. For example:
222 222
223 223 ::
224 224
225 225 [email]
226 226 method = smtp
227 227 from = Joe Doe <Joe.Doe@remote.com>
228 228
229 229 [smtp]
230 230 host = smtp.gmail.com
231 231 port = 587
232 232 username = JoeDoe@gmail.com
233 233 tls = true
234 234
235 235 Just as in case of HTTP, you *must* set username, but *must not* set
236 236 password here to use the extension, in other cases it will revert to
237 237 the default behavior.
238 238
239 239 Usage
240 =====
240 ======================================================
241
242 Saving and restoring passwords
243 -------------------------------------------------------
241 244
242 245 Configure the repository as above, then just ``hg pull``, ``hg push``,
243 246 etc. You should be asked for the password only once (per every
244 247 username and remote repository prefix or url combination).
245 248
246 249 Similarly, for email, configure as above and just ``hg email``.
247 250 Again, you will be asked for the password once (per every username and
248 251 email server address combination).
249 252
253 Checking password status (``hg keyring_check``)
254 -------------------------------------------------------
255
256 The ``keyring_check`` command can be used to check whether/which
257 password(s) are saved. It can be used in three ways:
258
259 - without parameters, it prints info related to all HTTP paths
260 defined for current repository (everything from ``hg paths``
261 that resolves to HTTP url)::
262
263 hg keyring_check
264
265 - given alias as param, it prints info about this alias::
266
267 hg keyring_check work
268
269 - finally, any path can be checked::
270
271 hg keyring_check https://bitbucket.org/Mekk/mercurial_keyring
272
273 Deleting saved password (``hg keyring_clear``)
274 -------------------------------------------------------
275
276 The ``keyring_clear`` command removes saved password related to given
277 path. It can be used in two ways:
278
279 - given alias as param, it drops password used by this alias::
280
281 hg keyring_clear work
282
283 - given full path, it drops password related to this path::
284
285 hg keyring_clear https://bitbucket.org/Mekk/mercurial_keyring
286
250 287 Implementation details
251 ======================
288 =======================================================
252 289
253 290 The extension is monkey-patching the mercurial ``passwordmgr`` class
254 291 to replace the ``find_user_password`` method. Detailed order of operations
255 292 is described in the comments inside `the code`_.
256 293
257 294 History
258 ==========
295 =======================================================
259 296
260 297 See `HISTORY.txt`_.
261 298
262 299 Development
263 ===========
300 =======================================================
264 301
265 302 Development is tracked on BitBucket, see
266 303 http://bitbucket.org/Mekk/mercurial_keyring/
267 304
268 305
269 306 Additional notes
270 ================
307 =======================================================
271 308
272 309 Information about this extension is also available
273 310 on Mercurial Wiki: http://mercurial.selenic.com/wiki/KeyringExtension
274 311
275 312 .. _HISTORY.txt: http://bitbucket.org/Mekk/mercurial_keyring/src/tip/HISTORY.txt
276 313 .. _TortoiseHg: http://tortoisehg.bitbucket.org/
277 314 .. _Mercurial: http://mercurial.selenic.com
278 315 .. _mercurial_extension_utils: https://bitbucket.org/Mekk/mercurial-extension_utils/
279 316 .. _Path Pattern: https://bitbucket.org/Mekk/mercurial-path_pattern/
@@ -1,740 +1,771 b''
1 1 # -*- coding: utf-8 -*-
2 2 #
3 3 # mercurial_keyring: save passwords in password database
4 4 #
5 5 # Copyright (c) 2009 Marcin Kasperski <Marcin.Kasperski@mekk.waw.pl>
6 6 # All rights reserved.
7 7 #
8 8 # Redistribution and use in source and binary forms, with or without
9 9 # modification, are permitted provided that the following conditions
10 10 # are met:
11 11 # 1. Redistributions of source code must retain the above copyright
12 12 # notice, this list of conditions and the following disclaimer.
13 13 # 2. Redistributions in binary form must reproduce the above copyright
14 14 # notice, this list of conditions and the following disclaimer in the
15 15 # documentation and/or other materials provided with the distribution.
16 16 # 3. The name of the author may not be used to endorse or promote products
17 17 # derived from this software without specific prior written permission.
18 18 #
19 19 # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
20 20 # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
21 21 # OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
22 22 # IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
23 23 # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
24 24 # NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25 25 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26 26 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27 27 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
28 28 # THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29 29 #
30 30 # See README.txt for more details.
31 31
32 32 '''securely save HTTP and SMTP passwords to encrypted storage
33 33
34 34 mercurial_keyring securely saves HTTP and SMTP passwords in password
35 35 databases (Gnome Keyring, KDE KWallet, OSXKeyChain, Win32 crypto
36 36 services).
37 37
38 38 The process is automatic. Whenever bare Mercurial just prompts for
39 39 the password, Mercurial with mercurial_keyring enabled checks whether
40 40 saved password is available first. If so, it is used. If not, you
41 41 will be prompted for the password, but entered password will be
42 42 saved for the future use.
43 43
44 44 In case saved password turns out to be invalid (HTTP or SMTP login
45 45 fails) it is dropped, and you are asked for current password.
46 46
47 47 Actual password storage is implemented by Python keyring library, this
48 48 extension glues those services to Mercurial. Consult keyring
49 49 documentation for information how to configure actual password
50 50 backend (by default keyring guesses, usually correctly, for example
51 51 you get KDE Wallet under KDE, and Gnome Keyring under Gnome or Unity).
52 52 '''
53 53
54 54 from mercurial import util, sslutil
55 55 from mercurial.i18n import _
56 56 from mercurial.url import passwordmgr
57 57 from mercurial import mail
58 58 from mercurial.mail import SMTPS, STARTTLS
59 59 from mercurial import encoding
60 60
61 61 from urlparse import urlparse
62 62 import urllib2
63 63 import smtplib
64 64 import socket
65 65 import os
66 66 import sys
67 import re
67 68
68 69 # pylint: disable=invalid-name, line-too-long, protected-access, too-many-arguments
69 70
70 71 ###########################################################################
71 72 # Specific import trickery
72 73 ###########################################################################
73 74
74 75
75 76 def import_meu():
76 77 """
77 78 Convoluted import of mercurial_extension_utils, which helps
78 79 TortoiseHg/Win setups. This routine and it's use below
79 80 performs equivalent of
80 81 from mercurial_extension_utils import monkeypatch_method
81 82 but looks for some non-path directories.
82 83 """
83 84 try:
84 85 import mercurial_extension_utils
85 86 except ImportError:
86 87 my_dir = os.path.dirname(__file__)
87 88 sys.path.extend([
88 89 # In the same dir (manual or site-packages after pip)
89 90 my_dir,
90 91 # Developer clone
91 92 os.path.join(os.path.dirname(my_dir), "extension_utils"),
92 93 # Side clone
93 94 os.path.join(os.path.dirname(my_dir), "mercurial-extension_utils"),
94 95 ])
95 96 try:
96 97 import mercurial_extension_utils
97 98 except ImportError:
98 99 raise util.Abort(_("""Can not import mercurial_extension_utils.
99 100 Please install this module in Python path.
100 101 See Installation chapter in https://bitbucket.org/Mekk/mercurial-dynamic_username/ for details
101 102 (and for info about TortoiseHG on Windows, or other bundled Python)."""))
102 103 return mercurial_extension_utils
103 104
104 105 meu = import_meu()
105 106 monkeypatch_method = meu.monkeypatch_method
106 107
107 108
108 109 def import_keyring():
109 110 """
110 111 Importing keyring happens to be costly if wallet is slow, so we delay it
111 112 until it is really needed. The routine below also works around various
112 113 demandimport-related problems.
113 114 """
114 115 if 'keyring' in sys.modules:
115 116 return sys.modules['keyring']
116 117 # mercurial.demandimport incompatibility workaround.
117 118 # various keyring backends fail as they can't properly import helper
118 119 # modules (as demandimport modifies python import behaviour).
119 120 # If you get import errors with demandimport in backtrace, try
120 121 # guessing what to block and extending the list below.
121 122 from mercurial import demandimport
122 123 for blocked_module in [
123 124 "gobject._gobject",
124 125 "configparser",
125 126 "json",
126 127 "abc",
127 128 "io",
128 129 "keyring",
129 130 "gdata.docs.service",
130 131 "gdata.service",
131 132 "types",
132 133 "atom.http",
133 134 "atom.http_interface",
134 135 "atom.service",
135 136 "atom.token_store",
136 137 "ctypes",
137 138 "secretstorage.exceptions",
138 139 "fs.opener",
139 140 ]:
140 141 if blocked_module not in demandimport.ignore:
141 142 demandimport.ignore.append(blocked_module)
142 143
143 144 # Various attempts to define is_demandimport_enabled
144 145 try:
145 146 # Since Mercurial 2.9.1
146 147 is_demandimport_enabled = demandimport.isenabled
147 148 except AttributeError:
148 149 def is_demandimport_enabled():
149 150 """Checks whether demandimport is enabled at the moment"""
150 151 return __import__ == demandimport._demandimport
151 152
152 153 # Shut up warning about uninitialized logging for new keyring versions.
153 154 # But beware 2.6…
154 155 try:
155 156 import logging
156 157 logging.getLogger("keyring").addHandler(logging.NullHandler())
157 158 except: # pylint: disable=bare-except
158 159 pass
159 160
160 161 # Temporarily disable demandimport to make the need of extending
161 162 # the list above less likely.
162 163 if is_demandimport_enabled():
163 164 demandimport.disable()
164 165 try:
165 166 import keyring
166 167 finally:
167 168 demandimport.enable()
168 169 else:
169 170 import keyring
170 171 return keyring
171 172
172 173 #################################################################
173 174 # Actual implementation
174 175 #################################################################
175 176
176 177 KEYRING_SERVICE = "Mercurial"
177 178
178 179
179 180 class PasswordStore(object):
180 181 """
181 182 Helper object handling keyring usage (password save&restore,
182 183 the way passwords are keyed in the keyring).
183 184 """
184 185 def __init__(self):
185 186 self.cache = dict()
186 187
187 188 def get_http_password(self, url, username):
188 189 """
189 190 Checks whether password of username for url is available,
190 191 returns it or None
191 192 """
192 193 return self._read_password_from_keyring(
193 194 self._format_http_key(url, username))
194 195
195 196 def set_http_password(self, url, username, password):
196 197 """Saves password to keyring"""
197 198 self._save_password_to_keyring(
198 199 self._format_http_key(url, username),
199 200 password)
200 201
201 202 def clear_http_password(self, url, username):
202 203 """Drops saved password"""
203 204 self.set_http_password(url, username, "")
204 205
205 206 @staticmethod
206 207 def _format_http_key(url, username):
207 208 """Construct actual key for password identification"""
208 209 return "%s@@%s" % (username, url)
209 210
210 211 def get_smtp_password(self, machine, port, username):
211 212 """Checks for SMTP password in keyring, returns
212 213 password or None"""
213 214 return self._read_password_from_keyring(
214 215 self._format_smtp_key(machine, port, username))
215 216
216 217 def set_smtp_password(self, machine, port, username, password):
217 218 """Saves SMTP password to keyring"""
218 219 self._save_password_to_keyring(
219 220 self._format_smtp_key(machine, port, username),
220 221 password)
221 222
222 223 def clear_smtp_password(self, machine, port, username):
223 224 """Drops saved SMTP password"""
224 225 self.set_smtp_password(machine, port, username, "")
225 226
226 227 @staticmethod
227 228 def _format_smtp_key(machine, port, username):
228 229 """Construct key for SMTP password identification"""
229 230 return "%s@@%s:%s" % (username, machine, str(port))
230 231
231 232 @staticmethod
232 233 def _read_password_from_keyring(pwdkey):
233 234 """Physically read from keyring"""
234 235 keyring = import_keyring()
235 236 password = keyring.get_password(KEYRING_SERVICE, pwdkey)
236 237 # Reverse recoding from next routine
237 238 if isinstance(password, unicode):
238 239 return encoding.tolocal(password.encode('utf-8'))
239 240 return password
240 241
241 242 @staticmethod
242 243 def _save_password_to_keyring(pwdkey, password):
243 244 """Physically write to keyring"""
244 245 keyring = import_keyring()
245 246 # keyring in general expects unicode.
246 247 # Mercurial provides "local" encoding. See #33
247 248 password = encoding.fromlocal(password).decode('utf-8')
248 249 keyring.set_password(
249 250 KEYRING_SERVICE, pwdkey, password)
250 251
251 252 password_store = PasswordStore()
252 253
253 254
254 255 ############################################################
255 256 # Various utils
256 257 ############################################################
257 258
258 259 def _debug(ui, msg):
259 260 """Generic debug message"""
260 261 ui.debug("keyring: " + msg + "\n")
261 262
262 263
263 264 class PwdCache(object):
264 265 """Short term cache, used to preserve passwords
265 266 if they are used twice during a command"""
266 267 def __init__(self):
267 268 self._cache = {}
268 269
269 270 def store(self, realm, url, user, pwd):
270 271 """Saves password"""
271 272 cache_key = (realm, url, user)
272 273 self._cache[cache_key] = pwd
273 274
274 275 def check(self, realm, url, user):
275 276 """Checks for cached password"""
276 277 cache_key = (realm, url, user)
277 278 return self._cache.get(cache_key)
278 279
279 280
280 281 ############################################################
281 282 # HTTP password management
282 283 ############################################################
283 284
284 285
285 286 class HTTPPasswordHandler(object):
286 287 """
287 288 Actual implementation of password handling (user prompting,
288 289 configuration file searching, keyring save&restore).
289 290
290 291 Object of this class is bound as passwordmgr attribute.
291 292 """
292 293 def __init__(self):
293 294 self.pwd_cache = PwdCache()
294 295 self.last_reply = None
295 296
296 297 # Markers and also names used in debug notes. Password source
297 298 SRC_URL = "repository URL"
298 299 SRC_CFGAUTH = "hgrc"
299 300 SRC_MEMCACHE = "temporary cache"
300 301 SRC_KEYRING = "keyring"
301 302
302 303 def get_credentials(self, pwmgr, realm, authuri, skip_caches=False):
303 304 """
304 305 Looks up for user credentials in various places, returns them
305 306 and information about their source.
306 307
307 308 Used internally inside find_auth and inside informative
308 309 commands (thiis method doesn't cache, doesn't detect bad
309 310 passwords etc, doesn't prompt interactively, doesn't store
310 311 password in keyring).
311 312
312 313 Returns: user, password, SRC_*, actual_url
313 314
314 315 If not found, password and SRC is None, user can be given or
315 316 not, url is always set
316 317 """
317 318 ui = pwmgr.ui
318 319
319 320 # Strip arguments to get actual remote repository url.
320 321 base_url = self.canonical_url(authuri)
321 322
322 323 # Extract username (or password) stored directly in url
323 324 url_user, url_pwd \
324 325 = urllib2.HTTPPasswordMgrWithDefaultRealm.find_user_password(
325 326 pwmgr, realm, authuri)
326 327 if url_user and url_pwd:
327 328 return url_user, url_pwd, self.SRC_URL, base_url
328 329
329 330 # Consult configuration to normalize url to prefix, and find username
330 331 # (and maybe password)
331 332 auth_user, auth_pwd, keyring_url = self.get_url_config(
332 333 ui, base_url, url_user)
333 334 if auth_user and url_user and (url_user != auth_user):
334 335 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, url_user, auth_user)))
335 336 if auth_user and auth_pwd:
336 337 return auth_user, auth_pwd, self.SRC_CFGAUTH, keyring_url
337 338
338 339 if skip_caches:
339 340 return auth_user, None, None, keyring_url
340 341
341 342 # Check memory cache (reuse )
342 343 # Checking the memory cache (there may be many http calls per command)
343 344 cached_pwd = self.pwd_cache.check(realm, keyring_url, auth_user)
344 345 if cached_pwd:
345 346 return auth_user, cached_pwd, self.SRC_MEMCACHE, keyring_url
346 347
347 348 # Load from keyring.
348 349 if auth_user:
349 350 ui.debug(_("keyring: looking for password (user %s, url %s)\n") % (auth_user, keyring_url))
350 351 keyring_pwd = password_store.get_http_password(keyring_url, auth_user)
351 352 if keyring_pwd:
352 353 return auth_user, keyring_pwd, self.SRC_KEYRING, keyring_url
353 354
354 355 return auth_user, None, None, keyring_url
355 356
356 357
357 358 @staticmethod
358 359 def prompt_interactively(ui, user, realm, url):
359 360 """Actual interactive prompt"""
360 361 if not ui.interactive():
361 362 raise util.Abort(_('mercurial_keyring: http authorization required but program used in non-interactive mode'))
362 363
363 364 if not user:
364 365 ui.status(_("keyring: username not specified in hgrc (or in url). Password will not be saved.\n"))
365 366
366 367 ui.write(_("http authorization required\n"))
367 368 ui.status(_("realm: %s\n") % realm)
368 369 ui.status(_("url: %s\n") % url)
369 370 if user:
370 371 ui.write(_("user: %s (fixed in hgrc or url)\n" % user))
371 372 else:
372 373 user = ui.prompt(_("user:"), default=None)
373 374 pwd = ui.getpass(_("password: "))
374 375 return user, pwd
375 376
376 377 def find_auth(self, pwmgr, realm, authuri, req):
377 378 """
378 379 Actual implementation of find_user_password - different
379 380 ways of obtaining the username and password.
380 381
381 382 Returns pair username, password
382 383 """
383 384 ui = pwmgr.ui
384 385 after_bad_auth = self._after_bad_auth(ui, realm, authuri, req)
385 386
386 387 # Look in url, cache, etc
387 388 user, pwd, src, final_url = self.get_credentials(
388 389 pwmgr, realm, authuri, skip_caches=after_bad_auth)
389 390 if pwd:
390 391 if src != self.SRC_MEMCACHE:
391 392 self.pwd_cache.store(realm, final_url, user, pwd)
392 393 self._note_last_reply(realm, authuri, user, req)
393 394 _debug(ui, _("Password found in " + src))
394 395 return user, pwd
395 396
396 397 # Last resort: interactive prompt
397 398 user, pwd = self.prompt_interactively(ui, user, realm, final_url)
398 399
399 400 if user:
400 401 # Saving password to the keyring.
401 402 # It is done only if username is permanently set.
402 403 # Otherwise we won't be able to find the password so it
403 404 # does not make much sense to preserve it
404 405 _debug(ui, _("Saving password for %s to keyring") % user)
405 406 password_store.set_http_password(final_url, user, pwd)
406 407
407 408 # Saving password to the memory cache
408 409 self.pwd_cache.store(realm, final_url, user, pwd)
409 410 self._note_last_reply(realm, authuri, user, req)
410 411 _debug(ui, _("Manually entered password"))
411 412 return user, pwd
412 413
413 414 def get_url_config(self, ui, base_url, user):
414 415 """
415 416 Checks configuration to decide whether/which username, prefix,
416 417 and password are configured for given url. Consults [auth] section.
417 418
418 419 Returns tuple (username, password, prefix) containing elements
419 420 found. username and password can be None (if unset), if prefix
420 421 is not found, url itself is returned.
421 422 """
422 423 from mercurial.httpconnection import readauthforuri
423 424 _debug(ui, _("Checking for hgrc info about url %s, user %s") % (base_url, user))
424 425 res = readauthforuri(ui, base_url, user)
425 426 if res:
426 427 group, auth_token = res
427 428 else:
428 429 auth_token = None
429 430
430 431 if auth_token:
431 432 username = auth_token.get('username')
432 433 password = auth_token.get('password')
433 434 prefix = auth_token.get('prefix')
434 435 else:
435 436 username = None
436 437 password = None
437 438 prefix = None
438 439
439 440 password_url = self.password_url(base_url, prefix)
440 441 return username, password, password_url
441 442
442 443 def _note_last_reply(self, realm, authuri, user, req):
443 444 """
444 445 Internal helper. Saves info about auth-data obtained,
445 446 preserves them in last_reply, and returns pair user, pwd
446 447 """
447 448 self.last_reply = dict(realm=realm, authuri=authuri,
448 449 user=user, req=req)
449 450
450 451 def _after_bad_auth(self, ui, realm, authuri, req):
451 452 """
452 453 If we are called again just after identical previous
453 454 request, then the previously returned auth must have been
454 455 wrong. So we note this to force password prompt (and avoid
455 456 reusing bad password indefinitely).
456 457
457 458 This routine checks for this condition.
458 459 """
459 460 if self.last_reply:
460 461 if (self.last_reply['realm'] == realm) \
461 462 and (self.last_reply['authuri'] == authuri) \
462 463 and (self.last_reply['req'] == req):
463 464 _debug(ui, _("Working after bad authentication, cached passwords not used %s") % str(self.last_reply))
464 465 return True
465 466 return False
466 467
467 468 @staticmethod
468 469 def password_url(base_url, prefix):
469 470 """Calculates actual url identifying the password. Takes
470 471 configured prefix under consideration (so can be shorter
471 472 than repo url)"""
472 473 if not prefix or prefix == '*':
473 474 return base_url
474 475 scheme, hostpath = base_url.split('://', 1)
475 476 p = prefix.split('://', 1)
476 477 if len(p) > 1:
477 478 prefix_host_path = p[1]
478 479 else:
479 480 prefix_host_path = prefix
480 481 password_url = scheme + '://' + prefix_host_path
481 482 return password_url
482 483
483 484 @staticmethod
484 485 def canonical_url(authuri):
485 486 """
486 487 Strips query params from url. Used to convert urls like
487 488 https://repo.machine.com/repos/apps/module?pairs=0000000000000000000000000000000000000000-0000000000000000000000000000000000000000&cmd=between
488 489 to
489 490 https://repo.machine.com/repos/apps/module
490 491 """
491 492 parsed_url = urlparse(authuri)
492 493 return "%s://%s%s" % (parsed_url.scheme, parsed_url.netloc,
493 494 parsed_url.path)
494 495
495 496 ############################################################
496 497 # Mercurial monkey-patching
497 498 ############################################################
498 499
499 500
500 501 @monkeypatch_method(passwordmgr)
501 502 def find_user_password(self, realm, authuri):
502 503 """
503 504 keyring-based implementation of username/password query
504 505 for HTTP(S) connections
505 506
506 507 Passwords are saved in gnome keyring, OSX/Chain or other platform
507 508 specific storage and keyed by the repository url
508 509 """
509 510 # Extend object attributes
510 511 if not hasattr(self, '_pwd_handler'):
511 512 self._pwd_handler = HTTPPasswordHandler()
512 513
513 514 if hasattr(self, '_http_req'):
514 515 req = self._http_req
515 516 else:
516 517 req = None
517 518
518 519 return self._pwd_handler.find_auth(self, realm, authuri, req)
519 520
520 521
521 522 @monkeypatch_method(urllib2.AbstractBasicAuthHandler, "http_error_auth_reqed")
522 523 def basic_http_error_auth_reqed(self, authreq, host, req, headers):
523 524 """Preserves current HTTP request so it can be consulted
524 525 in find_user_password above"""
525 526 self.passwd._http_req = req
526 527 try:
527 528 return basic_http_error_auth_reqed.orig(self, authreq, host, req, headers)
528 529 finally:
529 530 self.passwd._http_req = None
530 531
531 532
532 533 @monkeypatch_method(urllib2.AbstractDigestAuthHandler, "http_error_auth_reqed")
533 534 def digest_http_error_auth_reqed(self, authreq, host, req, headers):
534 535 """Preserves current HTTP request so it can be consulted
535 536 in find_user_password above"""
536 537 self.passwd._http_req = req
537 538 try:
538 539 return digest_http_error_auth_reqed.orig(self, authreq, host, req, headers)
539 540 finally:
540 541 self.passwd._http_req = None
541 542
542 543 ############################################################
543 544 # SMTP support
544 545 ############################################################
545 546
546 547
547 548 def try_smtp_login(ui, smtp_obj, username, password):
548 549 """
549 550 Attempts smtp login on smtp_obj (smtplib.SMTP) using username and
550 551 password.
551 552
552 553 Returns:
553 554 - True if login succeeded
554 555 - False if login failed due to the wrong credentials
555 556
556 557 Throws Abort exception if login failed for any other reason.
557 558
558 559 Immediately returns False if password is empty
559 560 """
560 561 if not password:
561 562 return False
562 563 try:
563 564 ui.note(_('(authenticating to mail server as %s)\n') %
564 565 (username))
565 566 smtp_obj.login(username, password)
566 567 return True
567 568 except smtplib.SMTPException, inst:
568 569 if inst.smtp_code == 535:
569 570 ui.status(_("SMTP login failed: %s\n\n") % inst.smtp_error)
570 571 return False
571 572 else:
572 573 raise util.Abort(inst)
573 574
574 575
575 576 def keyring_supported_smtp(ui, username):
576 577 """
577 578 keyring-integrated replacement for mercurial.mail._smtp
578 579 Used only when configuration file contains username, but
579 580 does not contain the password.
580 581
581 582 Most of the routine below is copied as-is from
582 583 mercurial.mail._smtp. The only changed part is
583 584 marked with # >>>>> and # <<<<< markers
584 585 """
585 586 local_hostname = ui.config('smtp', 'local_hostname')
586 587 tls = ui.config('smtp', 'tls', 'none')
587 588 # backward compatible: when tls = true, we use starttls.
588 589 starttls = tls == 'starttls' or util.parsebool(tls)
589 590 smtps = tls == 'smtps'
590 591 if (starttls or smtps) and not util.safehasattr(socket, 'ssl'):
591 592 raise util.Abort(_("can't use TLS: Python SSL support not installed"))
592 593 mailhost = ui.config('smtp', 'host')
593 594 if not mailhost:
594 595 raise util.Abort(_('smtp.host not configured - cannot send mail'))
595 596 verifycert = ui.config('smtp', 'verifycert', 'strict')
596 597 if verifycert not in ['strict', 'loose']:
597 598 if util.parsebool(verifycert) is not False:
598 599 raise util.Abort(_('invalid smtp.verifycert configuration: %s')
599 600 % (verifycert))
600 601 verifycert = False
601 602 if (starttls or smtps) and verifycert:
602 603 sslkwargs = sslutil.sslkwargs(ui, mailhost)
603 604 else:
604 605 sslkwargs = {}
605 606 if smtps:
606 607 ui.note(_('(using smtps)\n'))
607 608 s = SMTPS(sslkwargs, local_hostname=local_hostname)
608 609 elif starttls:
609 610 s = STARTTLS(sslkwargs, local_hostname=local_hostname)
610 611 else:
611 612 s = smtplib.SMTP(local_hostname=local_hostname)
612 613 if smtps:
613 614 defaultport = 465
614 615 else:
615 616 defaultport = 25
616 617 mailport = util.getport(ui.config('smtp', 'port', defaultport))
617 618 ui.note(_('sending mail: smtp host %s, port %s\n') %
618 619 (mailhost, mailport))
619 620 s.connect(host=mailhost, port=mailport)
620 621 if starttls:
621 622 ui.note(_('(using starttls)\n'))
622 623 s.ehlo()
623 624 s.starttls()
624 625 s.ehlo()
625 626 if (starttls or smtps) and verifycert:
626 627 ui.note(_('(verifying remote certificate)\n'))
627 628 sslutil.validator(ui, mailhost)(s.sock, verifycert == 'strict')
628 629
629 630 # >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
630 631 stored = password = password_store.get_smtp_password(
631 632 mailhost, mailport, username)
632 633 # No need to check whether password was found as try_smtp_login
633 634 # just returns False if it is absent.
634 635 while not try_smtp_login(ui, s, username, password):
635 636 password = ui.getpass(_("Password for %s on %s:%d: ") % (username, mailhost, mailport))
636 637
637 638 if stored != password:
638 639 password_store.set_smtp_password(
639 640 mailhost, mailport, username, password)
640 641 # <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
641 642
642 643 def send(sender, recipients, msg):
643 644 try:
644 645 return s.sendmail(sender, recipients, msg)
645 646 except smtplib.SMTPRecipientsRefused, inst:
646 647 recipients = [r[1] for r in inst.recipients.values()]
647 648 raise util.Abort('\n' + '\n'.join(recipients))
648 649 except smtplib.SMTPException, inst:
649 650 raise util.Abort(inst)
650 651
651 652 return send
652 653
653 654 ############################################################
654 655 # SMTP monkeypatching
655 656 ############################################################
656 657
657 658
658 659 @monkeypatch_method(mail)
659 660 def _smtp(ui):
660 661 """
661 662 build an smtp connection and return a function to send email
662 663
663 664 This is the monkeypatched version of _smtp(ui) function from
664 665 mercurial/mail.py. It calls the original unless username
665 666 without password is given in the configuration.
666 667 """
667 668 username = ui.config('smtp', 'username')
668 669 password = ui.config('smtp', 'password')
669 670
670 671 if username and not password:
671 672 return keyring_supported_smtp(ui, username)
672 673 else:
673 674 return _smtp.orig(ui)
674 675
675 676
676 677 ############################################################
677 678 # Custom commands
678 679 ############################################################
679 680
680 def cmd_keyring_check(ui, repo, *path_args, **opts):
681 _re_http_url = re.compile(r'^https?://')
682
683 def is_http_path(url):
684 return bool(_re_http_url.search(url))
685
686
687 def cmd_keyring_check(ui, repo, *path_args, **opts): # pylint: disable=unused-argument
681 688 """
682 689 Prints basic info (whether password is currently saved, and how is
683 690 it identified) for given path or for all defined repo paths which are HTTP.
684 691 """
685 import re
686
687 re_http_url = re.compile(r'^https?://')
688 692 defined_paths = [(name, url)
689 693 for name, url in ui.configitems('paths')]
690 694 if path_args:
691 695 # Maybe parameter is an alias
692 696 defined_paths_dic = dict(defined_paths)
693 697 paths = [(path_arg, defined_paths_dic.get(path_arg, path_arg))
694 698 for path_arg in path_args]
695 699 else:
696 700 paths = [(name, url) for name, url in defined_paths]
697 701
698 702 if not paths:
699 ui.status(_("keyring_check: no paths defined"))
703 ui.status(_("keyring_check: no paths defined\n"))
700 704
701 705 handler = HTTPPasswordHandler()
702 706
703 707 ui.status(_("keyring password save status:\n"))
704 708 for name, url in paths:
705 if not re_http_url.search(url):
709 if not is_http_path(url):
706 710 if path_args:
707 711 ui.status(_(" %s: non-http path (%s)\n") % (name, url))
708 712 continue
709 user, pwd, source, final_url = handler.get_credentials(passwordmgr(ui), name, url)
713 user, pwd, source, final_url = handler.get_credentials(
714 passwordmgr(ui), name, url)
710 715 if pwd:
711 716 ui.status(_(" %s: password available, source: %s, bound to user %s, url %s\n") % (
712 717 name, source, user, final_url))
713 718 elif user:
714 719 ui.status(_(" %s: password not available, once entered, will be bound to user %s, url %s\n") % (
715 720 name, user, final_url))
716 721 else:
717 722 ui.status(_(" %s: password not available, user unknown, url %s\n") % (
718 723 name, final_url))
719 724
720 725
721 def cmd_keyring_clear(ui, repo, path, **opts):
726 def cmd_keyring_clear(ui, repo, path, **opts): # pylint: disable=unused-argument
727 """
728 Drops password bound to given path (if any is saved).
722 729 """
723 Drops password bound to given path (if any).
724 """
725 # TODO
726 ui.status(_("Not yet implemented"))
730 path_url = path
731 for name, url in ui.configitems('paths'):
732 if name == path:
733 path_url = url
734 break
735 if not is_http_path(path_url):
736 ui.warn(_("%s is not a http path (%s)") % (path, path_url))
737 return
738
739 handler = HTTPPasswordHandler()
740
741 user, pwd, source, final_url = handler.get_credentials(
742 passwordmgr(ui), path, path_url)
743 if not user:
744 ui.status(_("Username not configured for url %s\n") % final_url)
745 return
746 if not pwd:
747 ui.status(_("No password is saved for user %s, url %s\n") % (
748 user, final_url))
749 return
750
751 if source != handler.SRC_KEYRING:
752 ui.status(_("Password for user %s, url %s is saved in %s, not in keyring\n") % (
753 user, final_url, source))
754
755 password_store.clear_http_password(final_url, user)
756 ui.status(_("Password removed for user %s, url %s\n") % (
757 user, final_url))
727 758
728 759
729 760 cmdtable = {
730 761 "keyring_check": (
731 762 cmd_keyring_check,
732 763 [],
733 764 "keyring_check [PATH]",
734 765 ),
735 766 "keyring_clear": (
736 767 cmd_keyring_clear,
737 768 [],
738 769 "keyring_check PATH",
739 770 ),
740 771 }
General Comments 0
You need to be logged in to leave comments. Login now