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