##// END OF EJS Templates
Patching various links bitbucket → heptapod
Marcin Kasperski -
r283:e95e300e default
parent child Browse files
Show More
@@ -1,422 +1,431 b''
1 .. -*- mode: rst; compile-command: "rst2html README.rst README.html" -*-
1 .. -*- mode: rst; compile-command: "rst2html README.rst 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`` prompts
38 changed it, or entered it incorrectly), ``mercurial_keyring`` prompts
39 you again, and overwrites the password.
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…) requires more care. See
74 but in some cases (Windows…) requires 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 using source clone, install keyring_ according to the
95 To install using source clone, install keyring_ according to the
96 instructions above, then clone::
96 instructions above, then clone::
97
97
98 hg clone https://bitbucket.org/Mekk/mercurial_keyring/
98 hg clone https://foss.heptapod.net/mercurial/mercurial_keyring/
99 hg clone https://bitbucket.org/Mekk/mercurial-extension_utils/
99 hg clone https://foss.heptapod.net/mercurial/mercurial-extension_utils/
100
100
101 and configure Mercurial using full path to the extension module::
101 and configure Mercurial using full path to the extension module::
102
102
103 [extensions]
103 [extensions]
104 mercurial_keyring = /path/to/mercurial_keyring/mercurial_keyring.py
104 mercurial_keyring = /path/to/mercurial_keyring/mercurial_keyring.py
105
105
106 .. _the code:
106 .. _the code:
107 .. _mercurial_keyring.py: http://bitbucket.org/Mekk/mercurial_keyring/src/tip/mercurial_keyring.py
107 .. _mercurial_keyring.py: https://foss.heptapod.net/mercurial/mercurial_keyring/src/tip/mercurial_keyring.py
108
108
109 Password backend configuration
109 Password backend configuration
110 =======================================================
110 =======================================================
111
111
112 The most appropriate password backend should usually be picked without
112 The most appropriate password backend should usually be picked without
113 configuration (considering installed libraries, operating system,
113 configuration (considering installed libraries, operating system,
114 active desktop session). Still, if necessary, it can be configured
114 active desktop session). Still, if necessary, it can be configured
115 using ``keyringrc.cfg`` file. Refer to keyring_ docs for more
115 using ``keyringrc.cfg`` file. Refer to keyring_ docs for more
116 details.
116 details.
117
117
118 .. note::
118 .. note::
119
119
120 With current (as I write) keyring (5.6), this file is (on Linux)
120 With current (as I write) keyring (5.6), this file is (on Linux)
121 located at ``~/.local/share/python_keyring/keyringrc.cfg`` and
121 located at ``~/.local/share/python_keyring/keyringrc.cfg`` and
122 it's example content looks like::
122 it's example content looks like::
123
123
124 [backend]
124 [backend]
125 default-keyring=keyring.backends.Gnome.Keyring
125 default-keyring=keyring.backends.Gnome.Keyring
126 # default-keyring=keyring.backends.kwallet.Keyring
126 # default-keyring=keyring.backends.kwallet.Keyring
127
127
128 For list of known backends run ``pydoc keyring.backends`` or
128 For list of known backends run ``pydoc keyring.backends`` or
129 ``keyring --list-backends`` (which of those commands work,
129 ``keyring --list-backends`` (which of those commands work,
130 depends on the keyring_ version).
130 depends on the keyring_ version).
131
131
132
132
133 ``hgrc`` configuration (HTTP)
133 ``hgrc`` configuration (HTTP)
134 =======================================================
134 =======================================================
135
135
136 Mercurial Keyring uses standard Mercurial ``[auth]`` configuration to
136 Mercurial Keyring uses standard Mercurial ``[auth]`` configuration to
137 detect your username (on given remote) and url prefix. You are
137 detect your username (on given remote) and url prefix. You are
138 strongly advised to configure both.
138 strongly advised to configure both.
139
139
140 Without the username ``mercurial_keyring`` can't save or restore
140 Without the username ``mercurial_keyring`` can't save or restore
141 passwords, so it disables itself.
141 passwords, so it disables itself.
142
142
143 Without url prefix ``mercurial_keyring`` works, but binds passwords to
143 Without url prefix ``mercurial_keyring`` works, but binds passwords to
144 repository urls. That means you will have to (re)enter password for
144 repository urls. That means you will have to (re)enter password for
145 every repository cloned from given remote (and that there will be many
145 every repository cloned from given remote (and that there will be many
146 copies of this password in secure storage).
146 copies of this password in secure storage).
147
147
148 Repository level configuration
148 Repository level configuration
149 ------------------------------------
149 ------------------------------------
150
150
151 Edit repository-local ``.hg/hgrc`` and save there the remote
151 Edit repository-local ``.hg/hgrc`` and save there the remote
152 repository path and the username, but do not save the password. For
152 repository path and the username, but do not save the password. For
153 example:
153 example:
154
154
155 ::
155 ::
156
156
157 [paths]
157 [paths]
158 myremote = https://my.server.com/hgrepo/someproject
158 myremote = https://my.server.com/hgrepo/someproject
159
159
160 [auth]
160 [auth]
161 myremote.prefix = https://my.server.com/hgrepo
161 myremote.prefix = https://my.server.com/hgrepo
162 myremote.username = John
162 myremote.username = John
163
163
164 Simpler form with url-embedded name can also be used:
164 Simpler form with url-embedded name can also be used:
165
165
166 ::
166 ::
167
167
168 [paths]
168 [paths]
169 bitbucket = https://John@my.server.com/hgrepo/someproject/
169 bitbucket = https://John@my.server.com/hgrepo/someproject/
170
170
171 but is not recommended.
171 but is not recommended.
172
172
173 Note that all repositories sharing the same ``prefix`` share the same
173 Note that all repositories sharing the same ``prefix`` share the same
174 password.
174 password.
175
175
176 Mercurial allows also for password in ``.hg/hgrc`` (either given by
176 Mercurial allows also for password in ``.hg/hgrc`` (either given by
177 ``«prefix».password``, or embedded in url). If such password is found,
177 ``«prefix».password``, or embedded in url). If such password is found,
178 Mercurial Keyring disables itself.
178 Mercurial Keyring disables itself.
179
179
180
180
181 Account-level configuration
181 Account-level configuration
182 ---------------------------
182 ---------------------------
183
183
184 If you are consistent about remote repository nicknames, you can
184 If you are consistent about remote repository nicknames, you can
185 configure the username in your `~/.hgrc` (`.hgrc` in your home
185 configure the username in your `~/.hgrc` (`.hgrc` in your home
186 directory). For example, write there::
186 directory). For example, write there::
187
187
188 [auth]
188 [auth]
189 acme.prefix = hg.acme.com/repositories
189 acme.prefix = hg.acme.com/repositories
190 acme.username = johnny
190 acme.username = johnny
191 acme.schemes = http https
191 acme.schemes = http https
192 bitbucket.prefix = https://bitbucket.org
192 bitbucket.prefix = https://bitbucket.org
193 bitbucket.username = Mekk
193 bitbucket.username = Mekk
194 mydep.prefix = https://dev.acmeorg.com
194 mydep.prefix = https://dev.acmeorg.com
195 mydep.username = drmartin
195 mydep.username = drmartin
196
196
197 and as long as you use ``acme`` alias for repositories like
197 and as long as you use ``acme`` alias for repositories like
198 ``https://hg.acme.com/repositories/my_beautiful_app``, username
198 ``https://hg.acme.com/repositories/my_beautiful_app``, username
199 ``johnny`` will be used, and the same password reused. Similarly
199 ``johnny`` will be used, and the same password reused. Similarly
200 any ``hg push bitbucket`` will share the same password.
200 any ``hg push bitbucket`` will share the same password.
201
201
202 With such config repository-level ``.hg/hgrc`` need only contain
202 With such config repository-level ``.hg/hgrc`` need only contain
203 ``[paths]``.
203 ``[paths]``.
204
204
205 Additional advantage of this method is that it works also during
205 Additional advantage of this method is that it works also during
206 `clone`.
206 `clone`.
207
207
208
208
209 .. note::
209 .. note::
210
210
211 Mercurial Keyring works well with `Path Pattern`_. On my setup I use
211 Mercurial Keyring works well with `Path Pattern`_. On my setup I use
212 prefix as above, and::
212 prefix as above, and::
213
213
214 [path_pattern]
214 [path_pattern]
215 bitbucket.local = ~/devel/{below}
215 bitbucket.local = ~/devel/{below}
216 bitbucket.remote = https://bitbucket.org/Mekk/{below:/=-}
216 bitbucket.remote = https://bitbucket.org/Mekk/{below:/=-}
217
217
218 so all my repositories understand ``hg push bitbucket`` without
218 so all my repositories understand ``hg push bitbucket`` without
219 any repository-level configuration.
219 any repository-level configuration.
220
220
221
221
222 ``hgrc`` configuration (SMTP)
222 ``hgrc`` configuration (SMTP)
223 =======================================================
223 =======================================================
224
224
225 Edit either repository-local ``.hg/hgrc``, or ``~/.hgrc`` and set
225 Edit either repository-local ``.hg/hgrc``, or ``~/.hgrc`` and set
226 there all standard email and smtp properties, including SMTP
226 there all standard email and smtp properties, including SMTP
227 username, but without SMTP password. For example:
227 username, but without SMTP password. For example:
228
228
229 ::
229 ::
230
230
231 [email]
231 [email]
232 method = smtp
232 method = smtp
233 from = Joe Doe <Joe.Doe@remote.com>
233 from = Joe Doe <Joe.Doe@remote.com>
234
234
235 [smtp]
235 [smtp]
236 host = smtp.gmail.com
236 host = smtp.gmail.com
237 port = 587
237 port = 587
238 username = JoeDoe@gmail.com
238 username = JoeDoe@gmail.com
239 tls = true
239 tls = true
240
240
241 Just as in case of HTTP, you *must* set username, but *must not* set
241 Just as in case of HTTP, you *must* set username, but *must not* set
242 password here to use the extension, in other cases it will revert to
242 password here to use the extension, in other cases it will revert to
243 the default behavior.
243 the default behavior.
244
244
245 Usage
245 Usage
246 ======================================================
246 ======================================================
247
247
248 Saving and restoring passwords
248 Saving and restoring passwords
249 -------------------------------------------------------
249 -------------------------------------------------------
250
250
251 Configure the repository as above, then just ``hg pull``, ``hg push``,
251 Configure the repository as above, then just ``hg pull``, ``hg push``,
252 etc. You should be asked for the password only once (per every
252 etc. You should be asked for the password only once (per every
253 username and remote repository prefix or url combination).
253 username and remote repository prefix or url combination).
254
254
255 Similarly, for email, configure as above and just ``hg email``.
255 Similarly, for email, configure as above and just ``hg email``.
256 Again, you will be asked for the password once (per every username and
256 Again, you will be asked for the password once (per every username and
257 email server address combination).
257 email server address combination).
258
258
259 Checking password status (``hg keyring_check``)
259 Checking password status (``hg keyring_check``)
260 -------------------------------------------------------
260 -------------------------------------------------------
261
261
262 The ``keyring_check`` command can be used to check whether/which
262 The ``keyring_check`` command can be used to check whether/which
263 password(s) are saved. It can be used in three ways:
263 password(s) are saved. It can be used in three ways:
264
264
265 - without parameters, it prints info related to all HTTP paths
265 - without parameters, it prints info related to all HTTP paths
266 defined for current repository (everything from ``hg paths``
266 defined for current repository (everything from ``hg paths``
267 that resolves to HTTP url)::
267 that resolves to HTTP url)::
268
268
269 hg keyring_check
269 hg keyring_check
270
270
271 - given alias as param, it prints info about this alias::
271 - given alias as param, it prints info about this alias::
272
272
273 hg keyring_check work
273 hg keyring_check work
274
274
275 - finally, any path can be checked::
275 - finally, any path can be checked::
276
276
277 hg keyring_check https://bitbucket.org/Mekk/mercurial_keyring
277 hg keyring_check https://foss.heptapod.net/mercurial/mercurial_keyring
278
278
279 Deleting saved password (``hg keyring_clear``)
279 Deleting saved password (``hg keyring_clear``)
280 -------------------------------------------------------
280 -------------------------------------------------------
281
281
282 The ``keyring_clear`` command removes saved password related to given
282 The ``keyring_clear`` command removes saved password related to given
283 path. It can be used in two ways:
283 path. It can be used in two ways:
284
284
285 - given alias as param, it drops password used by this alias::
285 - given alias as param, it drops password used by this alias::
286
286
287 hg keyring_clear work
287 hg keyring_clear work
288
288
289 - given full path, it drops password related to this path::
289 - given full path, it drops password related to this path::
290
290
291 hg keyring_clear https://bitbucket.org/Mekk/mercurial_keyring
291 hg keyring_clear https://foss.heptapod.net/mercurial/mercurial_keyring
292
292
293 Managing passwords using GUI tools
293 Managing passwords using GUI tools
294 ------------------------------------------------------
294 ------------------------------------------------------
295
295
296 Many password backends provide GUI tools for password management,
296 Many password backends provide GUI tools for password management,
297 for example Gnome Keyring passwords can be managed using ``seahorse``,
297 for example Gnome Keyring passwords can be managed using ``seahorse``,
298 and KDE Wallet using ``kwalletmanager``. Those GUI tools can be used
298 and KDE Wallet using ``kwalletmanager``. Those GUI tools can be used
299 to review, edit, or delete saved passwords.
299 to review, edit, or delete saved passwords.
300
300
301 Unfortunately, as I write, keyring_ library does not allow one to
301 Unfortunately, as I write, keyring_ library does not allow one to
302 configure how/where exactly saved passwords are put in the hierarchy,
302 configure how/where exactly saved passwords are put in the hierarchy,
303 and the place is not always intuitive. For example, in KDE Wallet, all
303 and the place is not always intuitive. For example, in KDE Wallet, all
304 passwords saved using ``mercurial_keyring`` show up in the folder
304 passwords saved using ``mercurial_keyring`` show up in the folder
305 named ``Python``.
305 named ``Python``.
306
306
307 .. note::
307 .. note::
308
308
309 This is slightly problematic in case ``mercurial_keyring`` is not
309 This is slightly problematic in case ``mercurial_keyring`` is not
310 the only program using keyring_ library. Passwords saved by another
310 the only program using keyring_ library. Passwords saved by another
311 Python application or script (which also uses keyring_) will be put
311 Python application or script (which also uses keyring_) will be put
312 into the same place, and it may be unclear which password belongs
312 into the same place, and it may be unclear which password belongs
313 to which program. To remedy this, ``mercurial_keyring`` applies
313 to which program. To remedy this, ``mercurial_keyring`` applies
314 slightly unusual labels of the form
314 slightly unusual labels of the form
315 ``«username»@@«urlprefix»@Mercurial`` - for example my bitbucket
315 ``«username»@@«urlprefix»@Mercurial`` - for example my bitbucket
316 password is labelled ``Mekk@@https://bitbucket.org@Mercurial``.
316 password is labelled ``Mekk@@https://bitbucket.org@Mercurial``.
317
317
318 Implementation details
318 Implementation details
319 =======================================================
319 =======================================================
320
320
321 The extension is monkey-patching the mercurial ``passwordmgr`` class
321 The extension is monkey-patching the mercurial ``passwordmgr`` class
322 to replace the ``find_user_password`` method. Detailed order of operations
322 to replace the ``find_user_password`` method. Detailed order of operations
323 is described in the comments inside `the code`_.
323 is described in the comments inside `the code`_.
324
324
325 Frequent problems
325 Frequent problems
326 =======================================================
326 =======================================================
327
327
328 Most problems people face while using ``mercurial_keyring`` are in
328 Most problems people face while using ``mercurial_keyring`` are in
329 fact problems with ``keyring`` library and it's backends. In
329 fact problems with ``keyring`` library and it's backends. In
330 particular, those can manifest by:
330 particular, those can manifest by:
331
331
332 - technical errors mentioning sentences like ``No recommended backend
332 - technical errors mentioning sentences like ``No recommended backend
333 was available. Install the keyrings.alt package…`` (or similar),
333 was available. Install the keyrings.alt package…`` (or similar),
334
334
335 - warnings like ``keyring: keyring backend doesn't seem to work…``
335 - warnings like ``keyring: keyring backend doesn't seem to work…``
336
336
337 - password prompts on every action (= passwords not being saved).
337 - password prompts on every action (= passwords not being saved).
338
338
339 Those almost always mean that *natural* keyring backend for given
339 Those almost always mean that *natural* keyring backend for given
340 desktop type doesn't work, or is not present at all. For example,
340 desktop type doesn't work, or is not present at all. For example,
341 some necessary runtime component can be down (say, you use Linux, but
341 some necessary runtime component can be down (say, you use Linux, but
342 neither Gnome Keyring, nor KDE Wallet, is running). Or appropriate
342 neither Gnome Keyring, nor KDE Wallet, is running). Or appropriate
343 backend is not installed because it could not be build during keyring_
343 backend is not installed because it could not be build during keyring_
344 library installation (maybe because some required library was not
344 library installation (maybe because some required library was not
345 present at the moment of keyring installation, or maybe because
345 present at the moment of keyring installation, or maybe because
346 compiler as such is not present on the system).
346 compiler as such is not present on the system).
347
347
348 To diagnose such problems, try using ``keyring`` utility, as described
348 To diagnose such problems, try using ``keyring`` utility, as described
349 on keyring_ documentation page, for example by::
349 on keyring_ documentation page, for example by::
350
350
351 keyring --list-backends
351 keyring --list-backends
352 keyring -b keyrings.alt.Gnome.Keyring set testsvc testuser
352 keyring -b keyrings.alt.Gnome.Keyring set testsvc testuser
353 keyring -b keyrings.alt.Gnome.Keyring get testsvc testuser
353 keyring -b keyrings.alt.Gnome.Keyring get testsvc testuser
354
354
355 (of course using appropriate backend). If you miss the ``keyring`` command
355 (of course using appropriate backend). If you miss the ``keyring`` command
356 as such, try ``python -m keyring`` instead::
356 as such, try ``python -m keyring`` instead::
357
357
358 python -m keyring --list-backends
358 python -m keyring --list-backends
359 python -m keyring -b keyrings.alt.Gnome.Keyring set testsvc testuser
359 python -m keyring -b keyrings.alt.Gnome.Keyring set testsvc testuser
360 python -m keyring -b keyrings.alt.Gnome.Keyring get testsvc testuser
360 python -m keyring -b keyrings.alt.Gnome.Keyring get testsvc testuser
361
361
362 If appropriate backend is missing (not listed), or doesn't work
362 If appropriate backend is missing (not listed), or doesn't work
363 (second or third command fails), your keyring is broken. Try looking
363 (second or third command fails), your keyring is broken. Try looking
364 for further pointers in keyring_ documentation, that project mailing
364 for further pointers in keyring_ documentation, that project mailing
365 list, or issue tracker. Typically it will turn out, that you need to
365 list, or issue tracker. Typically it will turn out, that you need to
366 install some missing tool, or library, and reinstall keyring.
366 install some missing tool, or library, and reinstall keyring.
367
367
368 .. note::
368 .. note::
369
369
370 Depending on keyring_ version, installation of some dependency may
370 Depending on keyring_ version, installation of some dependency may
371 resolve problem. For example (as of late 2018), I got KDE Wallet
371 resolve problem. For example (as of late 2018), I got KDE Wallet
372 backend working with pip-installed keyring after::
372 backend working with pip-installed keyring after::
373
373
374 pip install dbus-python
374 pip install dbus-python
375
375
376 Note also, that recent versions of keyring library (since version 12) use Python
376 Note also, that recent versions of keyring library (since version 12) use Python
377 entrypoints to find available backends. Those are incompatible with
377 entrypoints to find available backends. Those are incompatible with
378 some binary packaging methods (like ``py2app``) and may cause
378 some binary packaging methods (like ``py2app``) and may cause
379 problems. In particular there were packaged installations of TortoiseHG
379 problems. In particular there were packaged installations of TortoiseHG
380 which were unable to load keyring backends. See `#61 <https://bitbucket.org/Mekk/mercurial_keyring/issues/61/tortoisehg-encounters-unknown-exception>`_ for some more details.
380 which were unable to load keyring backends. See `#61 <https://foss.heptapod.net/mercurial/mercurial_keyring/issues/61/tortoisehg-encounters-unknown-exception>`_ for some more details.
381
381
382
382
383 If ``keyring`` command works, but mercurial with mercurial_keyring does not,
383 If ``keyring`` command works, but mercurial with mercurial_keyring does not,
384 try enforcing proper backend (by means of ``keyringrc.cfg``, see above).
384 try enforcing proper backend (by means of ``keyringrc.cfg``, see above).
385 Only if this doesn't help, there may be a bug in mercurial_keyring.
385 Only if this doesn't help, there may be a bug in mercurial_keyring.
386
386
387 By far easiest way to have properly working keyring is to use packaged
387 By far easiest way to have properly working keyring is to use packaged
388 binary version (like ``python-keyring`` Ubuntu package, or keyring
388 binary version (like ``python-keyring`` Ubuntu package, or keyring
389 bundled with TortoiseHG on some systems). If you pip-installed keyring
389 bundled with TortoiseHG on some systems). If you pip-installed keyring
390 and it doesn't work, you may consider removing it via ``pip uninstall
390 and it doesn't work, you may consider removing it via ``pip uninstall
391 keyring`` and looking for binary package instead.
391 keyring`` and looking for binary package instead.
392
392
393
393
394
394
395
395
396 History
396 History
397 =======================================================
397 =======================================================
398
398
399 See `HISTORY.rst`_.
399 See `HISTORY.rst`_.
400
400
401 Development
401 Repository, bug reports, enhancement suggestions
402 =======================================================
402 ===================================================
403
404 Development is tracked on HeptaPod, see
405 https://foss.heptapod.net/mercurial/mercurial_keyring/
403
406
404 Development is tracked on BitBucket, see
407 Use issue tracker there for bug reports and enhancement
405 http://bitbucket.org/Mekk/mercurial_keyring/
408 suggestions.
409
410 Thanks to Octobus_ and `Clever Cloud`_ for hosting this service.
411
406
412
407
413
408 Additional notes
414 Additional notes
409 =======================================================
415 =======================================================
410
416
411 Information about this extension is also available
417 Information about this extension is also available
412 on Mercurial Wiki: http://mercurial.selenic.com/wiki/KeyringExtension
418 on Mercurial Wiki: http://mercurial.selenic.com/wiki/KeyringExtension
413
419
414 Check also `other Mercurial extensions I wrote`_.
420 Check also `other Mercurial extensions I wrote`_.
415
421
422 .. _Octobus: https://octobus.net/
423 .. _Clever Cloud: https://www.clever-cloud.com/
424
416 .. _other Mercurial extensions I wrote: http://mekk.bitbucket.io/mercurial.html
425 .. _other Mercurial extensions I wrote: http://mekk.bitbucket.io/mercurial.html
417
426
418 .. _HISTORY.rst: http://bitbucket.org/Mekk/mercurial_keyring/src/tip/HISTORY.rst
427 .. _HISTORY.rst: https://foss.heptapod.net/mercurial/mercurial_keyring/src/tip/HISTORY.rst
419 .. _TortoiseHg: http://tortoisehg.bitbucket.org/
428 .. _TortoiseHg: http://tortoisehg.bitbucket.org/
420 .. _Mercurial: http://mercurial.selenic.com
429 .. _Mercurial: http://mercurial.selenic.com
421 .. _mercurial_extension_utils: https://bitbucket.org/Mekk/mercurial-extension_utils/
430 .. _mercurial_extension_utils: https://foss.heptapod.net/mercurial/mercurial-extension_utils/
422 .. _Path Pattern: https://bitbucket.org/Mekk/mercurial-path_pattern/
431 .. _Path Pattern: https://foss.heptapod.net/mercurial/mercurial-path_pattern/
@@ -1,887 +1,887 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.rst for more details.
30 # See README.rst 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 import socket
54 import socket
55 import os
55 import os
56 import sys
56 import sys
57 import re
57 import re
58 if sys.version_info[0] < 3:
58 if sys.version_info[0] < 3:
59 from urllib2 import (
59 from urllib2 import (
60 HTTPPasswordMgrWithDefaultRealm, AbstractBasicAuthHandler, AbstractDigestAuthHandler)
60 HTTPPasswordMgrWithDefaultRealm, AbstractBasicAuthHandler, AbstractDigestAuthHandler)
61 else:
61 else:
62 from urllib.request import (
62 from urllib.request import (
63 HTTPPasswordMgrWithDefaultRealm, AbstractBasicAuthHandler, AbstractDigestAuthHandler)
63 HTTPPasswordMgrWithDefaultRealm, AbstractBasicAuthHandler, AbstractDigestAuthHandler)
64 import smtplib
64 import smtplib
65
65
66 from mercurial import util, sslutil, error
66 from mercurial import util, sslutil, error
67 from mercurial.i18n import _
67 from mercurial.i18n import _
68 from mercurial.url import passwordmgr
68 from mercurial.url import passwordmgr
69 from mercurial import mail
69 from mercurial import mail
70 from mercurial.mail import SMTPS, STARTTLS
70 from mercurial.mail import SMTPS, STARTTLS
71 from mercurial import encoding
71 from mercurial import encoding
72 from mercurial import ui as uimod
72 from mercurial import ui as uimod
73
73
74 # pylint: disable=invalid-name, line-too-long, protected-access, too-many-arguments
74 # pylint: disable=invalid-name, line-too-long, protected-access, too-many-arguments
75
75
76 ###########################################################################
76 ###########################################################################
77 # Specific import trickery
77 # Specific import trickery
78 ###########################################################################
78 ###########################################################################
79
79
80
80
81 def import_meu():
81 def import_meu():
82 """
82 """
83 Convoluted import of mercurial_extension_utils, which helps
83 Convoluted import of mercurial_extension_utils, which helps
84 TortoiseHg/Win setups. This routine and it's use below
84 TortoiseHg/Win setups. This routine and it's use below
85 performs equivalent of
85 performs equivalent of
86 from mercurial_extension_utils import monkeypatch_method
86 from mercurial_extension_utils import monkeypatch_method
87 but looks for some non-path directories.
87 but looks for some non-path directories.
88 """
88 """
89 try:
89 try:
90 import mercurial_extension_utils
90 import mercurial_extension_utils
91 except ImportError:
91 except ImportError:
92 my_dir = os.path.dirname(__file__)
92 my_dir = os.path.dirname(__file__)
93 sys.path.extend([
93 sys.path.extend([
94 # In the same dir (manual or site-packages after pip)
94 # In the same dir (manual or site-packages after pip)
95 my_dir,
95 my_dir,
96 # Developer clone
96 # Developer clone
97 os.path.join(os.path.dirname(my_dir), "extension_utils"),
97 os.path.join(os.path.dirname(my_dir), "extension_utils"),
98 # Side clone
98 # Side clone
99 os.path.join(os.path.dirname(my_dir), "mercurial-extension_utils"),
99 os.path.join(os.path.dirname(my_dir), "mercurial-extension_utils"),
100 ])
100 ])
101 try:
101 try:
102 import mercurial_extension_utils
102 import mercurial_extension_utils
103 except ImportError:
103 except ImportError:
104 raise error.Abort(_("""Can not import mercurial_extension_utils.
104 raise error.Abort(_("""Can not import mercurial_extension_utils.
105 Please install this module in Python path.
105 Please install this module in Python path.
106 See Installation chapter in https://bitbucket.org/Mekk/mercurial_keyring/ for details
106 See Installation chapter in https://foss.heptapod.net/mercurial/mercurial_keyring/ for details
107 (and for info about TortoiseHG on Windows, or other bundled Python)."""))
107 (and for info about TortoiseHG on Windows, or other bundled Python)."""))
108 return mercurial_extension_utils
108 return mercurial_extension_utils
109
109
110
110
111 meu = import_meu()
111 meu = import_meu()
112 monkeypatch_method = meu.monkeypatch_method
112 monkeypatch_method = meu.monkeypatch_method
113
113
114
114
115 def import_keyring():
115 def import_keyring():
116 """
116 """
117 Importing keyring happens to be costly if wallet is slow, so we delay it
117 Importing keyring happens to be costly if wallet is slow, so we delay it
118 until it is really needed. The routine below also works around various
118 until it is really needed. The routine below also works around various
119 demandimport-related problems.
119 demandimport-related problems.
120 """
120 """
121 # mercurial.demandimport incompatibility workaround.
121 # mercurial.demandimport incompatibility workaround.
122 # various keyring backends fail as they can't properly import helper
122 # various keyring backends fail as they can't properly import helper
123 # modules (as demandimport modifies python import behaviour).
123 # modules (as demandimport modifies python import behaviour).
124 # If you get import errors with demandimport in backtrace, try
124 # If you get import errors with demandimport in backtrace, try
125 # guessing what to block and extending the list below.
125 # guessing what to block and extending the list below.
126 mod, was_imported_now = meu.direct_import_ext(
126 mod, was_imported_now = meu.direct_import_ext(
127 "keyring", [
127 "keyring", [
128 "gobject._gobject",
128 "gobject._gobject",
129 "configparser",
129 "configparser",
130 "json",
130 "json",
131 "abc",
131 "abc",
132 "io",
132 "io",
133 "keyring",
133 "keyring",
134 "gdata.docs.service",
134 "gdata.docs.service",
135 "gdata.service",
135 "gdata.service",
136 "types",
136 "types",
137 "atom.http",
137 "atom.http",
138 "atom.http_interface",
138 "atom.http_interface",
139 "atom.service",
139 "atom.service",
140 "atom.token_store",
140 "atom.token_store",
141 "ctypes",
141 "ctypes",
142 "secretstorage.exceptions",
142 "secretstorage.exceptions",
143 "fs.opener",
143 "fs.opener",
144 "win32ctypes.pywin32",
144 "win32ctypes.pywin32",
145 "win32ctypes.pywin32.pywintypes",
145 "win32ctypes.pywin32.pywintypes",
146 "win32ctypes.pywin32.win32cred",
146 "win32ctypes.pywin32.win32cred",
147 "pywintypes",
147 "pywintypes",
148 "win32cred",
148 "win32cred",
149 ])
149 ])
150 if was_imported_now:
150 if was_imported_now:
151 # Shut up warning about uninitialized logging by keyring
151 # Shut up warning about uninitialized logging by keyring
152 meu.disable_logging("keyring")
152 meu.disable_logging("keyring")
153 return mod
153 return mod
154
154
155
155
156 #################################################################
156 #################################################################
157 # Actual implementation
157 # Actual implementation
158 #################################################################
158 #################################################################
159
159
160 KEYRING_SERVICE = "Mercurial"
160 KEYRING_SERVICE = "Mercurial"
161
161
162
162
163 class PasswordStore(object):
163 class PasswordStore(object):
164 """
164 """
165 Helper object handling keyring usage (password save&restore,
165 Helper object handling keyring usage (password save&restore,
166 the way passwords are keyed in the keyring).
166 the way passwords are keyed in the keyring).
167 """
167 """
168 def __init__(self):
168 def __init__(self):
169 self.cache = dict()
169 self.cache = dict()
170
170
171 def get_http_password(self, url, username):
171 def get_http_password(self, url, username):
172 """
172 """
173 Checks whether password of username for url is available,
173 Checks whether password of username for url is available,
174 returns it or None
174 returns it or None
175 """
175 """
176 return self._read_password_from_keyring(
176 return self._read_password_from_keyring(
177 self._format_http_key(url, username))
177 self._format_http_key(url, username))
178
178
179 def set_http_password(self, url, username, password):
179 def set_http_password(self, url, username, password):
180 """Saves password to keyring"""
180 """Saves password to keyring"""
181 self._save_password_to_keyring(
181 self._save_password_to_keyring(
182 self._format_http_key(url, username),
182 self._format_http_key(url, username),
183 password)
183 password)
184
184
185 def clear_http_password(self, url, username):
185 def clear_http_password(self, url, username):
186 """Drops saved password"""
186 """Drops saved password"""
187 self.set_http_password(url, username, b"")
187 self.set_http_password(url, username, b"")
188
188
189 @staticmethod
189 @staticmethod
190 def _format_http_key(url, username):
190 def _format_http_key(url, username):
191 """Construct actual key for password identification"""
191 """Construct actual key for password identification"""
192 # keyring expects str, mercurial feeds as here mostly with bytes
192 # keyring expects str, mercurial feeds as here mostly with bytes
193 key = "%s@@%s" % (meu.pycompat.sysstr(username),
193 key = "%s@@%s" % (meu.pycompat.sysstr(username),
194 meu.pycompat.sysstr(url))
194 meu.pycompat.sysstr(url))
195 return key
195 return key
196
196
197 @staticmethod
197 @staticmethod
198 def _format_smtp_key(machine, port, username):
198 def _format_smtp_key(machine, port, username):
199 """Construct key for SMTP password identification"""
199 """Construct key for SMTP password identification"""
200 key = "%s@@%s:%s" % (meu.pycompat.sysstr(username),
200 key = "%s@@%s:%s" % (meu.pycompat.sysstr(username),
201 meu.pycompat.sysstr(machine),
201 meu.pycompat.sysstr(machine),
202 str(port))
202 str(port))
203 return key
203 return key
204
204
205 def get_smtp_password(self, machine, port, username):
205 def get_smtp_password(self, machine, port, username):
206 """Checks for SMTP password in keyring, returns
206 """Checks for SMTP password in keyring, returns
207 password or None"""
207 password or None"""
208 return self._read_password_from_keyring(
208 return self._read_password_from_keyring(
209 self._format_smtp_key(machine, port, username))
209 self._format_smtp_key(machine, port, username))
210
210
211 def set_smtp_password(self, machine, port, username, password):
211 def set_smtp_password(self, machine, port, username, password):
212 """Saves SMTP password to keyring"""
212 """Saves SMTP password to keyring"""
213 self._save_password_to_keyring(
213 self._save_password_to_keyring(
214 self._format_smtp_key(machine, port, username),
214 self._format_smtp_key(machine, port, username),
215 password)
215 password)
216
216
217 def clear_smtp_password(self, machine, port, username):
217 def clear_smtp_password(self, machine, port, username):
218 """Drops saved SMTP password"""
218 """Drops saved SMTP password"""
219 self.set_smtp_password(machine, port, username, "")
219 self.set_smtp_password(machine, port, username, "")
220
220
221 @staticmethod
221 @staticmethod
222 def _read_password_from_keyring(pwdkey):
222 def _read_password_from_keyring(pwdkey):
223 """Physically read from keyring"""
223 """Physically read from keyring"""
224 keyring = import_keyring()
224 keyring = import_keyring()
225 try:
225 try:
226 password = keyring.get_password(KEYRING_SERVICE, pwdkey)
226 password = keyring.get_password(KEYRING_SERVICE, pwdkey)
227 except Exception as err:
227 except Exception as err:
228 ui = uimod.ui()
228 ui = uimod.ui()
229 ui.warn(meu.ui_string(
229 ui.warn(meu.ui_string(
230 "keyring: keyring backend doesn't seem to work, password can not be restored. Falling back to prompts. Error details: %s\n",
230 "keyring: keyring backend doesn't seem to work, password can not be restored. Falling back to prompts. Error details: %s\n",
231 err))
231 err))
232 return b''
232 return b''
233 # Reverse recoding from next routine
233 # Reverse recoding from next routine
234 if isinstance(password, meu.pycompat.unicode):
234 if isinstance(password, meu.pycompat.unicode):
235 return encoding.tolocal(password.encode('utf-8'))
235 return encoding.tolocal(password.encode('utf-8'))
236 return password
236 return password
237
237
238 @staticmethod
238 @staticmethod
239 def _save_password_to_keyring(pwdkey, password):
239 def _save_password_to_keyring(pwdkey, password):
240 """Physically write to keyring"""
240 """Physically write to keyring"""
241 keyring = import_keyring()
241 keyring = import_keyring()
242 # keyring in general expects unicode.
242 # keyring in general expects unicode.
243 # Mercurial provides "local" encoding. See #33.
243 # Mercurial provides "local" encoding. See #33.
244 # py3 adds even more fun as we get already unicode from getpass.
244 # py3 adds even more fun as we get already unicode from getpass.
245 # To handle those quirks, let go through encoding.fromlocal in case we
245 # To handle those quirks, let go through encoding.fromlocal in case we
246 # got bytestr here, this will handle both normal py2 and emergency py3 cases.
246 # got bytestr here, this will handle both normal py2 and emergency py3 cases.
247 if isinstance(password, bytes):
247 if isinstance(password, bytes):
248 password = encoding.fromlocal(password).decode('utf-8')
248 password = encoding.fromlocal(password).decode('utf-8')
249 try:
249 try:
250 keyring.set_password(
250 keyring.set_password(
251 KEYRING_SERVICE, pwdkey, password)
251 KEYRING_SERVICE, pwdkey, password)
252 except Exception as err:
252 except Exception as err:
253 ui = uimod.ui()
253 ui = uimod.ui()
254 ui.warn(meu.ui_string(
254 ui.warn(meu.ui_string(
255 "keyring: keyring backend doesn't seem to work, password was not saved. Error details: %s\n",
255 "keyring: keyring backend doesn't seem to work, password was not saved. Error details: %s\n",
256 err))
256 err))
257
257
258
258
259 password_store = PasswordStore()
259 password_store = PasswordStore()
260
260
261
261
262 ############################################################
262 ############################################################
263 # Various utils
263 # Various utils
264 ############################################################
264 ############################################################
265
265
266 class PwdCache(object):
266 class PwdCache(object):
267 """Short term cache, used to preserve passwords
267 """Short term cache, used to preserve passwords
268 if they are used twice during a command"""
268 if they are used twice during a command"""
269 def __init__(self):
269 def __init__(self):
270 self._cache = {}
270 self._cache = {}
271
271
272 def store(self, realm, url, user, pwd):
272 def store(self, realm, url, user, pwd):
273 """Saves password"""
273 """Saves password"""
274 cache_key = (realm, url, user)
274 cache_key = (realm, url, user)
275 self._cache[cache_key] = pwd
275 self._cache[cache_key] = pwd
276
276
277 def check(self, realm, url, user):
277 def check(self, realm, url, user):
278 """Checks for cached password"""
278 """Checks for cached password"""
279 cache_key = (realm, url, user)
279 cache_key = (realm, url, user)
280 return self._cache.get(cache_key)
280 return self._cache.get(cache_key)
281
281
282
282
283 _re_http_url = re.compile(b'^https?://')
283 _re_http_url = re.compile(b'^https?://')
284
284
285
285
286 def is_http_path(url):
286 def is_http_path(url):
287 return bool(_re_http_url.search(url))
287 return bool(_re_http_url.search(url))
288
288
289
289
290 def make_passwordmgr(ui):
290 def make_passwordmgr(ui):
291 """Constructing passwordmgr in a way compatible with various mercurials"""
291 """Constructing passwordmgr in a way compatible with various mercurials"""
292 if hasattr(ui, 'httppasswordmgrdb'):
292 if hasattr(ui, 'httppasswordmgrdb'):
293 return passwordmgr(ui, ui.httppasswordmgrdb)
293 return passwordmgr(ui, ui.httppasswordmgrdb)
294 else:
294 else:
295 return passwordmgr(ui)
295 return passwordmgr(ui)
296
296
297 ############################################################
297 ############################################################
298 # HTTP password management
298 # HTTP password management
299 ############################################################
299 ############################################################
300
300
301
301
302 class HTTPPasswordHandler(object):
302 class HTTPPasswordHandler(object):
303 """
303 """
304 Actual implementation of password handling (user prompting,
304 Actual implementation of password handling (user prompting,
305 configuration file searching, keyring save&restore).
305 configuration file searching, keyring save&restore).
306
306
307 Object of this class is bound as passwordmgr attribute.
307 Object of this class is bound as passwordmgr attribute.
308 """
308 """
309 def __init__(self):
309 def __init__(self):
310 self.pwd_cache = PwdCache()
310 self.pwd_cache = PwdCache()
311 self.last_reply = None
311 self.last_reply = None
312
312
313 # Markers and also names used in debug notes. Password source
313 # Markers and also names used in debug notes. Password source
314 SRC_URL = "repository URL"
314 SRC_URL = "repository URL"
315 SRC_CFGAUTH = "hgrc"
315 SRC_CFGAUTH = "hgrc"
316 SRC_MEMCACHE = "temporary cache"
316 SRC_MEMCACHE = "temporary cache"
317 SRC_URLCACHE = "urllib temporary cache"
317 SRC_URLCACHE = "urllib temporary cache"
318 SRC_KEYRING = "keyring"
318 SRC_KEYRING = "keyring"
319
319
320 def get_credentials(self, pwmgr, realm, authuri, skip_caches=False):
320 def get_credentials(self, pwmgr, realm, authuri, skip_caches=False):
321 """
321 """
322 Looks up for user credentials in various places, returns them
322 Looks up for user credentials in various places, returns them
323 and information about their source.
323 and information about their source.
324
324
325 Used internally inside find_auth and inside informative
325 Used internally inside find_auth and inside informative
326 commands (thiis method doesn't cache, doesn't detect bad
326 commands (thiis method doesn't cache, doesn't detect bad
327 passwords etc, doesn't prompt interactively, doesn't store
327 passwords etc, doesn't prompt interactively, doesn't store
328 password in keyring).
328 password in keyring).
329
329
330 Returns: user, password, SRC_*, actual_url
330 Returns: user, password, SRC_*, actual_url
331
331
332 If not found, password and SRC is None, user can be given or
332 If not found, password and SRC is None, user can be given or
333 not, url is always set
333 not, url is always set
334 """
334 """
335 ui = pwmgr.ui
335 ui = pwmgr.ui
336
336
337 parsed_url, url_user, url_passwd = self.unpack_url(authuri)
337 parsed_url, url_user, url_passwd = self.unpack_url(authuri)
338 base_url = bytes(parsed_url)
338 base_url = bytes(parsed_url)
339 ui.debug(meu.ui_string('keyring: base url: %s, url user: %s, url pwd: %s\n',
339 ui.debug(meu.ui_string('keyring: base url: %s, url user: %s, url pwd: %s\n',
340 base_url, url_user, url_passwd and b'******' or b''))
340 base_url, url_user, url_passwd and b'******' or b''))
341
341
342 # Extract username (or password) stored directly in url
342 # Extract username (or password) stored directly in url
343 if url_user and url_passwd:
343 if url_user and url_passwd:
344 return url_user, url_passwd, self.SRC_URL, base_url
344 return url_user, url_passwd, self.SRC_URL, base_url
345
345
346 # Extract data from urllib (in case it was already stored)
346 # Extract data from urllib (in case it was already stored)
347 if isinstance(pwmgr, HTTPPasswordMgrWithDefaultRealm):
347 if isinstance(pwmgr, HTTPPasswordMgrWithDefaultRealm):
348 urllib_user, urllib_pwd = \
348 urllib_user, urllib_pwd = \
349 HTTPPasswordMgrWithDefaultRealm.find_user_password(
349 HTTPPasswordMgrWithDefaultRealm.find_user_password(
350 pwmgr, realm, authuri)
350 pwmgr, realm, authuri)
351 else:
351 else:
352 urllib_user, urllib_pwd = pwmgr.passwddb.find_user_password(
352 urllib_user, urllib_pwd = pwmgr.passwddb.find_user_password(
353 realm, authuri)
353 realm, authuri)
354 if urllib_user and urllib_pwd:
354 if urllib_user and urllib_pwd:
355 return urllib_user, urllib_pwd, self.SRC_URLCACHE, base_url
355 return urllib_user, urllib_pwd, self.SRC_URLCACHE, base_url
356
356
357 actual_user = url_user or urllib_user
357 actual_user = url_user or urllib_user
358
358
359 # Consult configuration to normalize url to prefix, and find username
359 # Consult configuration to normalize url to prefix, and find username
360 # (and maybe password)
360 # (and maybe password)
361 auth_user, auth_pwd, keyring_url = self.get_url_config(
361 auth_user, auth_pwd, keyring_url = self.get_url_config(
362 ui, parsed_url, actual_user)
362 ui, parsed_url, actual_user)
363 if auth_user and actual_user and (actual_user != auth_user):
363 if auth_user and actual_user and (actual_user != auth_user):
364 raise error.Abort(_('keyring: username for %s specified both in repository path (%s) and in .hg/hgrc/[auth] (%s). Please, leave only one of those' % (base_url, actual_user, auth_user)))
364 raise error.Abort(_('keyring: username for %s specified both in repository path (%s) and in .hg/hgrc/[auth] (%s). Please, leave only one of those' % (base_url, actual_user, auth_user)))
365 if auth_user and auth_pwd:
365 if auth_user and auth_pwd:
366 return auth_user, auth_pwd, self.SRC_CFGAUTH, keyring_url
366 return auth_user, auth_pwd, self.SRC_CFGAUTH, keyring_url
367
367
368 actual_user = actual_user or auth_user
368 actual_user = actual_user or auth_user
369
369
370 if skip_caches:
370 if skip_caches:
371 return actual_user, None, None, keyring_url
371 return actual_user, None, None, keyring_url
372
372
373 # Check memory cache (reuse )
373 # Check memory cache (reuse )
374 # Checking the memory cache (there may be many http calls per command)
374 # Checking the memory cache (there may be many http calls per command)
375 cached_pwd = self.pwd_cache.check(realm, keyring_url, actual_user)
375 cached_pwd = self.pwd_cache.check(realm, keyring_url, actual_user)
376 if cached_pwd:
376 if cached_pwd:
377 return actual_user, cached_pwd, self.SRC_MEMCACHE, keyring_url
377 return actual_user, cached_pwd, self.SRC_MEMCACHE, keyring_url
378
378
379 # Load from keyring.
379 # Load from keyring.
380 if actual_user:
380 if actual_user:
381 ui.debug(meu.ui_string("keyring: looking for password (user %s, url %s)\n",
381 ui.debug(meu.ui_string("keyring: looking for password (user %s, url %s)\n",
382 actual_user, keyring_url))
382 actual_user, keyring_url))
383 keyring_pwd = password_store.get_http_password(keyring_url, actual_user)
383 keyring_pwd = password_store.get_http_password(keyring_url, actual_user)
384 if keyring_pwd:
384 if keyring_pwd:
385 return actual_user, keyring_pwd, self.SRC_KEYRING, keyring_url
385 return actual_user, keyring_pwd, self.SRC_KEYRING, keyring_url
386
386
387 return actual_user, None, None, keyring_url
387 return actual_user, None, None, keyring_url
388
388
389 @staticmethod
389 @staticmethod
390 def prompt_interactively(ui, user, realm, url):
390 def prompt_interactively(ui, user, realm, url):
391 """Actual interactive prompt"""
391 """Actual interactive prompt"""
392 if not ui.interactive():
392 if not ui.interactive():
393 raise error.Abort(_('keyring: http authorization required but program used in non-interactive mode'))
393 raise error.Abort(_('keyring: http authorization required but program used in non-interactive mode'))
394
394
395 if not user:
395 if not user:
396 ui.status(meu.ui_string("keyring: username not specified in hgrc (or in url). Password will not be saved.\n"))
396 ui.status(meu.ui_string("keyring: username not specified in hgrc (or in url). Password will not be saved.\n"))
397
397
398 ui.write(meu.ui_string("http authorization required\n"))
398 ui.write(meu.ui_string("http authorization required\n"))
399 ui.status(meu.ui_string("realm: %s\n",
399 ui.status(meu.ui_string("realm: %s\n",
400 realm))
400 realm))
401 ui.status(meu.ui_string("url: %s\n",
401 ui.status(meu.ui_string("url: %s\n",
402 url))
402 url))
403 if user:
403 if user:
404 ui.write(meu.ui_string("user: %s (fixed in hgrc or url)\n",
404 ui.write(meu.ui_string("user: %s (fixed in hgrc or url)\n",
405 user))
405 user))
406 else:
406 else:
407 user = ui.prompt(meu.ui_string("user:"),
407 user = ui.prompt(meu.ui_string("user:"),
408 default=None)
408 default=None)
409 pwd = ui.getpass(meu.ui_string("password: "))
409 pwd = ui.getpass(meu.ui_string("password: "))
410 return user, pwd
410 return user, pwd
411
411
412 def find_auth(self, pwmgr, realm, authuri, req):
412 def find_auth(self, pwmgr, realm, authuri, req):
413 """
413 """
414 Actual implementation of find_user_password - different
414 Actual implementation of find_user_password - different
415 ways of obtaining the username and password.
415 ways of obtaining the username and password.
416
416
417 Returns pair username, password
417 Returns pair username, password
418 """
418 """
419 ui = pwmgr.ui
419 ui = pwmgr.ui
420 after_bad_auth = self._after_bad_auth(ui, realm, authuri, req)
420 after_bad_auth = self._after_bad_auth(ui, realm, authuri, req)
421
421
422 # Look in url, cache, etc
422 # Look in url, cache, etc
423 user, pwd, src, final_url = self.get_credentials(
423 user, pwd, src, final_url = self.get_credentials(
424 pwmgr, realm, authuri, skip_caches=after_bad_auth)
424 pwmgr, realm, authuri, skip_caches=after_bad_auth)
425 if pwd:
425 if pwd:
426 if src != self.SRC_MEMCACHE:
426 if src != self.SRC_MEMCACHE:
427 self.pwd_cache.store(realm, final_url, user, pwd)
427 self.pwd_cache.store(realm, final_url, user, pwd)
428 self._note_last_reply(realm, authuri, user, req)
428 self._note_last_reply(realm, authuri, user, req)
429 ui.debug(meu.ui_string("keyring: Password found in %s\n",
429 ui.debug(meu.ui_string("keyring: Password found in %s\n",
430 src))
430 src))
431 return user, pwd
431 return user, pwd
432
432
433 # Last resort: interactive prompt
433 # Last resort: interactive prompt
434 user, pwd = self.prompt_interactively(ui, user, realm, final_url)
434 user, pwd = self.prompt_interactively(ui, user, realm, final_url)
435
435
436 if user:
436 if user:
437 # Saving password to the keyring.
437 # Saving password to the keyring.
438 # It is done only if username is permanently set.
438 # It is done only if username is permanently set.
439 # Otherwise we won't be able to find the password so it
439 # Otherwise we won't be able to find the password so it
440 # does not make much sense to preserve it
440 # does not make much sense to preserve it
441 ui.debug(meu.ui_string("keyring: Saving password for %s to keyring\n",
441 ui.debug(meu.ui_string("keyring: Saving password for %s to keyring\n",
442 user))
442 user))
443 try:
443 try:
444 password_store.set_http_password(final_url, user, pwd)
444 password_store.set_http_password(final_url, user, pwd)
445 except Exception as e:
445 except Exception as e:
446 keyring = import_keyring()
446 keyring = import_keyring()
447 if isinstance(e, keyring.errors.PasswordSetError):
447 if isinstance(e, keyring.errors.PasswordSetError):
448 ui.traceback()
448 ui.traceback()
449 ui.warn(meu.ui_string("warning: failed to save password in keyring\n"))
449 ui.warn(meu.ui_string("warning: failed to save password in keyring\n"))
450 else:
450 else:
451 raise e
451 raise e
452
452
453 # Saving password to the memory cache
453 # Saving password to the memory cache
454 self.pwd_cache.store(realm, final_url, user, pwd)
454 self.pwd_cache.store(realm, final_url, user, pwd)
455 self._note_last_reply(realm, authuri, user, req)
455 self._note_last_reply(realm, authuri, user, req)
456 ui.debug(meu.ui_string("keyring: Manually entered password\n"))
456 ui.debug(meu.ui_string("keyring: Manually entered password\n"))
457 return user, pwd
457 return user, pwd
458
458
459 def get_url_config(self, ui, parsed_url, user):
459 def get_url_config(self, ui, parsed_url, user):
460 """
460 """
461 Checks configuration to decide whether/which username, prefix,
461 Checks configuration to decide whether/which username, prefix,
462 and password are configured for given url. Consults [auth] section.
462 and password are configured for given url. Consults [auth] section.
463
463
464 Returns tuple (username, password, prefix) containing elements
464 Returns tuple (username, password, prefix) containing elements
465 found. username and password can be None (if unset), if prefix
465 found. username and password can be None (if unset), if prefix
466 is not found, url itself is returned.
466 is not found, url itself is returned.
467 """
467 """
468 from mercurial.httpconnection import readauthforuri
468 from mercurial.httpconnection import readauthforuri
469 ui.debug(meu.ui_string("keyring: checking for hgrc info about url %s, user %s\n",
469 ui.debug(meu.ui_string("keyring: checking for hgrc info about url %s, user %s\n",
470 parsed_url, user))
470 parsed_url, user))
471
471
472 res = readauthforuri(ui, bytes(parsed_url), user)
472 res = readauthforuri(ui, bytes(parsed_url), user)
473 # If it user-less version not work, let's try with added username to handle
473 # If it user-less version not work, let's try with added username to handle
474 # both config conventions
474 # both config conventions
475 if (not res) and user:
475 if (not res) and user:
476 parsed_url.user = user
476 parsed_url.user = user
477 res = readauthforuri(ui, bytes(parsed_url), user)
477 res = readauthforuri(ui, bytes(parsed_url), user)
478 parsed_url.user = None
478 parsed_url.user = None
479 if res:
479 if res:
480 group, auth_token = res
480 group, auth_token = res
481 else:
481 else:
482 auth_token = None
482 auth_token = None
483
483
484 if auth_token:
484 if auth_token:
485 username = auth_token.get(b'username')
485 username = auth_token.get(b'username')
486 password = auth_token.get(b'password')
486 password = auth_token.get(b'password')
487 prefix = auth_token.get(b'prefix')
487 prefix = auth_token.get(b'prefix')
488 else:
488 else:
489 username = user
489 username = user
490 password = None
490 password = None
491 prefix = None
491 prefix = None
492
492
493 password_url = self.password_url(bytes(parsed_url), prefix)
493 password_url = self.password_url(bytes(parsed_url), prefix)
494
494
495 ui.debug(meu.ui_string("keyring: Password url: %s, user: %s, password: %s (prefix: %s)\n",
495 ui.debug(meu.ui_string("keyring: Password url: %s, user: %s, password: %s (prefix: %s)\n",
496 password_url, username,
496 password_url, username,
497 b'********' if password else b'',
497 b'********' if password else b'',
498 prefix))
498 prefix))
499
499
500 return username, password, password_url
500 return username, password, password_url
501
501
502 def _note_last_reply(self, realm, authuri, user, req):
502 def _note_last_reply(self, realm, authuri, user, req):
503 """
503 """
504 Internal helper. Saves info about auth-data obtained,
504 Internal helper. Saves info about auth-data obtained,
505 preserves them in last_reply, and returns pair user, pwd
505 preserves them in last_reply, and returns pair user, pwd
506 """
506 """
507 self.last_reply = dict(realm=realm, authuri=authuri,
507 self.last_reply = dict(realm=realm, authuri=authuri,
508 user=user, req=req)
508 user=user, req=req)
509
509
510 def _after_bad_auth(self, ui, realm, authuri, req):
510 def _after_bad_auth(self, ui, realm, authuri, req):
511 """
511 """
512 If we are called again just after identical previous
512 If we are called again just after identical previous
513 request, then the previously returned auth must have been
513 request, then the previously returned auth must have been
514 wrong. So we note this to force password prompt (and avoid
514 wrong. So we note this to force password prompt (and avoid
515 reusing bad password indefinitely).
515 reusing bad password indefinitely).
516
516
517 This routine checks for this condition.
517 This routine checks for this condition.
518 """
518 """
519 if self.last_reply:
519 if self.last_reply:
520 if (self.last_reply['realm'] == realm) \
520 if (self.last_reply['realm'] == realm) \
521 and (self.last_reply['authuri'] == authuri) \
521 and (self.last_reply['authuri'] == authuri) \
522 and (self.last_reply['req'] == req):
522 and (self.last_reply['req'] == req):
523 ui.debug(meu.ui_string(
523 ui.debug(meu.ui_string(
524 "keyring: Working after bad authentication, cached passwords not used %s\n",
524 "keyring: Working after bad authentication, cached passwords not used %s\n",
525 str(self.last_reply)))
525 str(self.last_reply)))
526 return True
526 return True
527 return False
527 return False
528
528
529 @staticmethod
529 @staticmethod
530 def password_url(base_url, prefix):
530 def password_url(base_url, prefix):
531 """Calculates actual url identifying the password. Takes
531 """Calculates actual url identifying the password. Takes
532 configured prefix under consideration (so can be shorter
532 configured prefix under consideration (so can be shorter
533 than repo url)"""
533 than repo url)"""
534 if not prefix or prefix == b'*':
534 if not prefix or prefix == b'*':
535 return base_url
535 return base_url
536 scheme, hostpath = base_url.split(b'://', 1)
536 scheme, hostpath = base_url.split(b'://', 1)
537 p = prefix.split(b'://', 1)
537 p = prefix.split(b'://', 1)
538 if len(p) > 1:
538 if len(p) > 1:
539 prefix_host_path = p[1]
539 prefix_host_path = p[1]
540 else:
540 else:
541 prefix_host_path = prefix
541 prefix_host_path = prefix
542 password_url = scheme + b'://' + prefix_host_path
542 password_url = scheme + b'://' + prefix_host_path
543 return password_url
543 return password_url
544
544
545 @staticmethod
545 @staticmethod
546 def unpack_url(authuri):
546 def unpack_url(authuri):
547 """
547 """
548 Takes original url for which authentication is attempted and:
548 Takes original url for which authentication is attempted and:
549
549
550 - Strips query params from url. Used to convert urls like
550 - Strips query params from url. Used to convert urls like
551 https://repo.machine.com/repos/apps/module?pairs=0000000000000000000000000000000000000000-0000000000000000000000000000000000000000&cmd=between
551 https://repo.machine.com/repos/apps/module?pairs=0000000000000000000000000000000000000000-0000000000000000000000000000000000000000&cmd=between
552 to
552 to
553 https://repo.machine.com/repos/apps/module
553 https://repo.machine.com/repos/apps/module
554
554
555 - Extracts username and password, if present, and removes them from url
555 - Extracts username and password, if present, and removes them from url
556 (so prefix matching works properly)
556 (so prefix matching works properly)
557
557
558 Returns url, user, password
558 Returns url, user, password
559 where url is mercurial.util.url object already stripped of all those
559 where url is mercurial.util.url object already stripped of all those
560 params.
560 params.
561 """
561 """
562 # In case of py3, util.url expects bytes
562 # In case of py3, util.url expects bytes
563 authuri = meu.pycompat.bytestr(authuri)
563 authuri = meu.pycompat.bytestr(authuri)
564
564
565 # mercurial.util.url, rather handy url parser
565 # mercurial.util.url, rather handy url parser
566 parsed_url = util.url(authuri)
566 parsed_url = util.url(authuri)
567 parsed_url.query = b''
567 parsed_url.query = b''
568 parsed_url.fragment = None
568 parsed_url.fragment = None
569 # Strip arguments to get actual remote repository url.
569 # Strip arguments to get actual remote repository url.
570 # base_url = "%s://%s%s" % (parsed_url.scheme, parsed_url.netloc,
570 # base_url = "%s://%s%s" % (parsed_url.scheme, parsed_url.netloc,
571 # parsed_url.path)
571 # parsed_url.path)
572 user = parsed_url.user
572 user = parsed_url.user
573 passwd = parsed_url.passwd
573 passwd = parsed_url.passwd
574 parsed_url.user = None
574 parsed_url.user = None
575 parsed_url.passwd = None
575 parsed_url.passwd = None
576
576
577 return parsed_url, user, passwd
577 return parsed_url, user, passwd
578
578
579
579
580 ############################################################
580 ############################################################
581 # Mercurial monkey-patching
581 # Mercurial monkey-patching
582 ############################################################
582 ############################################################
583
583
584
584
585 @monkeypatch_method(passwordmgr)
585 @monkeypatch_method(passwordmgr)
586 def find_user_password(self, realm, authuri):
586 def find_user_password(self, realm, authuri):
587 """
587 """
588 keyring-based implementation of username/password query
588 keyring-based implementation of username/password query
589 for HTTP(S) connections
589 for HTTP(S) connections
590
590
591 Passwords are saved in gnome keyring, OSX/Chain or other platform
591 Passwords are saved in gnome keyring, OSX/Chain or other platform
592 specific storage and keyed by the repository url
592 specific storage and keyed by the repository url
593 """
593 """
594 # In sync with hg 5.0
594 # In sync with hg 5.0
595 assert isinstance(realm, (type(None), str))
595 assert isinstance(realm, (type(None), str))
596 assert isinstance(authuri, str)
596 assert isinstance(authuri, str)
597
597
598 # Extend object attributes
598 # Extend object attributes
599 if not hasattr(self, '_pwd_handler'):
599 if not hasattr(self, '_pwd_handler'):
600 self._pwd_handler = HTTPPasswordHandler()
600 self._pwd_handler = HTTPPasswordHandler()
601
601
602 if hasattr(self, '_http_req'):
602 if hasattr(self, '_http_req'):
603 req = self._http_req
603 req = self._http_req
604 else:
604 else:
605 req = None
605 req = None
606
606
607 return self._pwd_handler.find_auth(self, realm, authuri, req)
607 return self._pwd_handler.find_auth(self, realm, authuri, req)
608
608
609
609
610 @monkeypatch_method(AbstractBasicAuthHandler, "http_error_auth_reqed")
610 @monkeypatch_method(AbstractBasicAuthHandler, "http_error_auth_reqed")
611 def basic_http_error_auth_reqed(self, authreq, host, req, headers):
611 def basic_http_error_auth_reqed(self, authreq, host, req, headers):
612 """Preserves current HTTP request so it can be consulted
612 """Preserves current HTTP request so it can be consulted
613 in find_user_password above"""
613 in find_user_password above"""
614 self.passwd._http_req = req
614 self.passwd._http_req = req
615 try:
615 try:
616 return basic_http_error_auth_reqed.orig(self, authreq, host, req, headers)
616 return basic_http_error_auth_reqed.orig(self, authreq, host, req, headers)
617 finally:
617 finally:
618 self.passwd._http_req = None
618 self.passwd._http_req = None
619
619
620
620
621 @monkeypatch_method(AbstractDigestAuthHandler, "http_error_auth_reqed")
621 @monkeypatch_method(AbstractDigestAuthHandler, "http_error_auth_reqed")
622 def digest_http_error_auth_reqed(self, authreq, host, req, headers):
622 def digest_http_error_auth_reqed(self, authreq, host, req, headers):
623 """Preserves current HTTP request so it can be consulted
623 """Preserves current HTTP request so it can be consulted
624 in find_user_password above"""
624 in find_user_password above"""
625 self.passwd._http_req = req
625 self.passwd._http_req = req
626 try:
626 try:
627 return digest_http_error_auth_reqed.orig(self, authreq, host, req, headers)
627 return digest_http_error_auth_reqed.orig(self, authreq, host, req, headers)
628 finally:
628 finally:
629 self.passwd._http_req = None
629 self.passwd._http_req = None
630
630
631 ############################################################
631 ############################################################
632 # SMTP support
632 # SMTP support
633 ############################################################
633 ############################################################
634
634
635
635
636 def try_smtp_login(ui, smtp_obj, username, password):
636 def try_smtp_login(ui, smtp_obj, username, password):
637 """
637 """
638 Attempts smtp login on smtp_obj (smtplib.SMTP) using username and
638 Attempts smtp login on smtp_obj (smtplib.SMTP) using username and
639 password.
639 password.
640
640
641 Returns:
641 Returns:
642 - True if login succeeded
642 - True if login succeeded
643 - False if login failed due to the wrong credentials
643 - False if login failed due to the wrong credentials
644
644
645 Throws Abort exception if login failed for any other reason.
645 Throws Abort exception if login failed for any other reason.
646
646
647 Immediately returns False if password is empty
647 Immediately returns False if password is empty
648 """
648 """
649 if not password:
649 if not password:
650 return False
650 return False
651 try:
651 try:
652 ui.note(_('(authenticating to mail server as %s)\n') %
652 ui.note(_('(authenticating to mail server as %s)\n') %
653 username)
653 username)
654 smtp_obj.login(username, password)
654 smtp_obj.login(username, password)
655 return True
655 return True
656 except smtplib.SMTPException as inst:
656 except smtplib.SMTPException as inst:
657 if inst.smtp_code == 535:
657 if inst.smtp_code == 535:
658 ui.status(meu.ui_string("SMTP login failed: %s\n\n",
658 ui.status(meu.ui_string("SMTP login failed: %s\n\n",
659 inst.smtp_error))
659 inst.smtp_error))
660 return False
660 return False
661 else:
661 else:
662 raise error.Abort(inst)
662 raise error.Abort(inst)
663
663
664
664
665 def keyring_supported_smtp(ui, username):
665 def keyring_supported_smtp(ui, username):
666 """
666 """
667 keyring-integrated replacement for mercurial.mail._smtp Used only
667 keyring-integrated replacement for mercurial.mail._smtp Used only
668 when configuration file contains username, but does not contain
668 when configuration file contains username, but does not contain
669 the password.
669 the password.
670
670
671 Most of the routine below is copied as-is from
671 Most of the routine below is copied as-is from
672 mercurial.mail._smtp. The critical changed part is marked with #
672 mercurial.mail._smtp. The critical changed part is marked with #
673 >>>>> and # <<<<< markers, there are also some fixes which make
673 >>>>> and # <<<<< markers, there are also some fixes which make
674 the code working on various Mercurials (like parsebool import).
674 the code working on various Mercurials (like parsebool import).
675 """
675 """
676 try:
676 try:
677 from mercurial.utils.stringutil import parsebool
677 from mercurial.utils.stringutil import parsebool
678 except ImportError:
678 except ImportError:
679 from mercurial.utils import parsebool
679 from mercurial.utils import parsebool
680
680
681 local_hostname = ui.config('smtp', 'local_hostname')
681 local_hostname = ui.config('smtp', 'local_hostname')
682 tls = ui.config('smtp', 'tls', 'none')
682 tls = ui.config('smtp', 'tls', 'none')
683 # backward compatible: when tls = true, we use starttls.
683 # backward compatible: when tls = true, we use starttls.
684 starttls = tls == 'starttls' or parsebool(tls)
684 starttls = tls == 'starttls' or parsebool(tls)
685 smtps = tls == 'smtps'
685 smtps = tls == 'smtps'
686 if (starttls or smtps) and not util.safehasattr(socket, 'ssl'):
686 if (starttls or smtps) and not util.safehasattr(socket, 'ssl'):
687 raise error.Abort(_("can't use TLS: Python SSL support not installed"))
687 raise error.Abort(_("can't use TLS: Python SSL support not installed"))
688 mailhost = ui.config('smtp', 'host')
688 mailhost = ui.config('smtp', 'host')
689 if not mailhost:
689 if not mailhost:
690 raise error.Abort(_('smtp.host not configured - cannot send mail'))
690 raise error.Abort(_('smtp.host not configured - cannot send mail'))
691 if getattr(sslutil, 'sslkwargs', None) is None:
691 if getattr(sslutil, 'sslkwargs', None) is None:
692 sslkwargs = None
692 sslkwargs = None
693 elif starttls or smtps:
693 elif starttls or smtps:
694 sslkwargs = sslutil.sslkwargs(ui, mailhost)
694 sslkwargs = sslutil.sslkwargs(ui, mailhost)
695 else:
695 else:
696 sslkwargs = {}
696 sslkwargs = {}
697 if smtps:
697 if smtps:
698 ui.note(_('(using smtps)\n'))
698 ui.note(_('(using smtps)\n'))
699
699
700 # mercurial 3.8 added a mandatory host arg
700 # mercurial 3.8 added a mandatory host arg
701 if not sslkwargs:
701 if not sslkwargs:
702 s = SMTPS(ui, local_hostname=local_hostname, host=mailhost)
702 s = SMTPS(ui, local_hostname=local_hostname, host=mailhost)
703 elif 'host' in SMTPS.__init__.__code__.co_varnames:
703 elif 'host' in SMTPS.__init__.__code__.co_varnames:
704 s = SMTPS(sslkwargs, local_hostname=local_hostname, host=mailhost)
704 s = SMTPS(sslkwargs, local_hostname=local_hostname, host=mailhost)
705 else:
705 else:
706 s = SMTPS(sslkwargs, local_hostname=local_hostname)
706 s = SMTPS(sslkwargs, local_hostname=local_hostname)
707 elif starttls:
707 elif starttls:
708 if not sslkwargs:
708 if not sslkwargs:
709 s = STARTTLS(ui, local_hostname=local_hostname, host=mailhost)
709 s = STARTTLS(ui, local_hostname=local_hostname, host=mailhost)
710 elif 'host' in STARTTLS.__init__.__code__.co_varnames:
710 elif 'host' in STARTTLS.__init__.__code__.co_varnames:
711 s = STARTTLS(sslkwargs, local_hostname=local_hostname, host=mailhost)
711 s = STARTTLS(sslkwargs, local_hostname=local_hostname, host=mailhost)
712 else:
712 else:
713 s = STARTTLS(sslkwargs, local_hostname=local_hostname)
713 s = STARTTLS(sslkwargs, local_hostname=local_hostname)
714 else:
714 else:
715 s = smtplib.SMTP(local_hostname=local_hostname)
715 s = smtplib.SMTP(local_hostname=local_hostname)
716 if smtps:
716 if smtps:
717 defaultport = 465
717 defaultport = 465
718 else:
718 else:
719 defaultport = 25
719 defaultport = 25
720 mailport = util.getport(ui.config('smtp', 'port', defaultport))
720 mailport = util.getport(ui.config('smtp', 'port', defaultport))
721 ui.note(_('sending mail: smtp host %s, port %s\n') %
721 ui.note(_('sending mail: smtp host %s, port %s\n') %
722 (mailhost, mailport))
722 (mailhost, mailport))
723 s.connect(host=mailhost, port=mailport)
723 s.connect(host=mailhost, port=mailport)
724 if starttls:
724 if starttls:
725 ui.note(_('(using starttls)\n'))
725 ui.note(_('(using starttls)\n'))
726 s.ehlo()
726 s.ehlo()
727 s.starttls()
727 s.starttls()
728 s.ehlo()
728 s.ehlo()
729 if starttls or smtps:
729 if starttls or smtps:
730 if getattr(sslutil, 'validatesocket', None):
730 if getattr(sslutil, 'validatesocket', None):
731 ui.note(_('(verifying remote certificate)\n'))
731 ui.note(_('(verifying remote certificate)\n'))
732 sslutil.validatesocket(s.sock)
732 sslutil.validatesocket(s.sock)
733
733
734 # >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
734 # >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
735 stored = password = password_store.get_smtp_password(
735 stored = password = password_store.get_smtp_password(
736 mailhost, mailport, username)
736 mailhost, mailport, username)
737 # No need to check whether password was found as try_smtp_login
737 # No need to check whether password was found as try_smtp_login
738 # just returns False if it is absent.
738 # just returns False if it is absent.
739 while not try_smtp_login(ui, s, username, password):
739 while not try_smtp_login(ui, s, username, password):
740 password = ui.getpass(_("Password for %s on %s:%d: ") % (username, mailhost, mailport))
740 password = ui.getpass(_("Password for %s on %s:%d: ") % (username, mailhost, mailport))
741
741
742 if stored != password:
742 if stored != password:
743 password_store.set_smtp_password(
743 password_store.set_smtp_password(
744 mailhost, mailport, username, password)
744 mailhost, mailport, username, password)
745 # <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
745 # <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
746
746
747 def send(sender, recipients, msg):
747 def send(sender, recipients, msg):
748 try:
748 try:
749 return s.sendmail(sender, recipients, msg)
749 return s.sendmail(sender, recipients, msg)
750 except smtplib.SMTPRecipientsRefused as inst:
750 except smtplib.SMTPRecipientsRefused as inst:
751 recipients = [r[1] for r in inst.recipients.values()]
751 recipients = [r[1] for r in inst.recipients.values()]
752 raise error.Abort('\n' + '\n'.join(recipients))
752 raise error.Abort('\n' + '\n'.join(recipients))
753 except smtplib.SMTPException as inst:
753 except smtplib.SMTPException as inst:
754 raise error.Abort(inst)
754 raise error.Abort(inst)
755
755
756 return send
756 return send
757
757
758 ############################################################
758 ############################################################
759 # SMTP monkeypatching
759 # SMTP monkeypatching
760 ############################################################
760 ############################################################
761
761
762
762
763 @monkeypatch_method(mail)
763 @monkeypatch_method(mail)
764 def _smtp(ui):
764 def _smtp(ui):
765 """
765 """
766 build an smtp connection and return a function to send email
766 build an smtp connection and return a function to send email
767
767
768 This is the monkeypatched version of _smtp(ui) function from
768 This is the monkeypatched version of _smtp(ui) function from
769 mercurial/mail.py. It calls the original unless username
769 mercurial/mail.py. It calls the original unless username
770 without password is given in the configuration.
770 without password is given in the configuration.
771 """
771 """
772 username = ui.config('smtp', 'username')
772 username = ui.config('smtp', 'username')
773 password = ui.config('smtp', 'password')
773 password = ui.config('smtp', 'password')
774
774
775 if username and not password:
775 if username and not password:
776 return keyring_supported_smtp(ui, username)
776 return keyring_supported_smtp(ui, username)
777 else:
777 else:
778 return _smtp.orig(ui)
778 return _smtp.orig(ui)
779
779
780
780
781 ############################################################
781 ############################################################
782 # Custom commands
782 # Custom commands
783 ############################################################
783 ############################################################
784
784
785 cmdtable = {}
785 cmdtable = {}
786 command = meu.command(cmdtable)
786 command = meu.command(cmdtable)
787
787
788
788
789 @command(b'keyring_check',
789 @command(b'keyring_check',
790 [],
790 [],
791 _("keyring_check [PATH]"),
791 _("keyring_check [PATH]"),
792 optionalrepo=True)
792 optionalrepo=True)
793 def cmd_keyring_check(ui, repo, *path_args, **opts): # pylint: disable=unused-argument
793 def cmd_keyring_check(ui, repo, *path_args, **opts): # pylint: disable=unused-argument
794 """
794 """
795 Prints basic info (whether password is currently saved, and how is
795 Prints basic info (whether password is currently saved, and how is
796 it identified) for given path.
796 it identified) for given path.
797
797
798 Can be run without parameters to show status for all (current repository) paths which
798 Can be run without parameters to show status for all (current repository) paths which
799 are HTTP-like.
799 are HTTP-like.
800 """
800 """
801 defined_paths = [(name, url)
801 defined_paths = [(name, url)
802 for name, url in ui.configitems(b'paths')]
802 for name, url in ui.configitems(b'paths')]
803 if path_args:
803 if path_args:
804 # Maybe parameter is an alias
804 # Maybe parameter is an alias
805 defined_paths_dic = dict(defined_paths)
805 defined_paths_dic = dict(defined_paths)
806 paths = [(path_arg, defined_paths_dic.get(path_arg, path_arg))
806 paths = [(path_arg, defined_paths_dic.get(path_arg, path_arg))
807 for path_arg in path_args]
807 for path_arg in path_args]
808 else:
808 else:
809 if not repo:
809 if not repo:
810 ui.status(meu.ui_string("Url to check not specified. Either run ``hg keyring_check https://...``, or run the command inside some repository (to test all defined paths).\n"))
810 ui.status(meu.ui_string("Url to check not specified. Either run ``hg keyring_check https://...``, or run the command inside some repository (to test all defined paths).\n"))
811 return
811 return
812 paths = [(name, url) for name, url in defined_paths]
812 paths = [(name, url) for name, url in defined_paths]
813
813
814 if not paths:
814 if not paths:
815 ui.status(meu.ui_string("keyring_check: no paths defined\n"))
815 ui.status(meu.ui_string("keyring_check: no paths defined\n"))
816 return
816 return
817
817
818 handler = HTTPPasswordHandler()
818 handler = HTTPPasswordHandler()
819
819
820 ui.status(meu.ui_string("keyring password save status:\n"))
820 ui.status(meu.ui_string("keyring password save status:\n"))
821 for name, url in paths:
821 for name, url in paths:
822 if not is_http_path(url):
822 if not is_http_path(url):
823 if path_args:
823 if path_args:
824 ui.status(meu.ui_string(" %s: non-http path (%s)\n",
824 ui.status(meu.ui_string(" %s: non-http path (%s)\n",
825 name, url))
825 name, url))
826 continue
826 continue
827 user, pwd, source, final_url = handler.get_credentials(
827 user, pwd, source, final_url = handler.get_credentials(
828 make_passwordmgr(ui), name, url)
828 make_passwordmgr(ui), name, url)
829 if pwd:
829 if pwd:
830 ui.status(meu.ui_string(
830 ui.status(meu.ui_string(
831 " %s: password available, source: %s, bound to user %s, url %s\n",
831 " %s: password available, source: %s, bound to user %s, url %s\n",
832 name, source, user, final_url))
832 name, source, user, final_url))
833 elif user:
833 elif user:
834 ui.status(meu.ui_string(
834 ui.status(meu.ui_string(
835 " %s: password not available, once entered, will be bound to user %s, url %s\n",
835 " %s: password not available, once entered, will be bound to user %s, url %s\n",
836 name, user, final_url))
836 name, user, final_url))
837 else:
837 else:
838 ui.status(meu.ui_string(
838 ui.status(meu.ui_string(
839 " %s: password not available, user unknown, url %s\n",
839 " %s: password not available, user unknown, url %s\n",
840 name, final_url))
840 name, final_url))
841
841
842
842
843 @command(b'keyring_clear',
843 @command(b'keyring_clear',
844 [],
844 [],
845 _('hg keyring_clear PATH-OR-ALIAS'),
845 _('hg keyring_clear PATH-OR-ALIAS'),
846 optionalrepo=True)
846 optionalrepo=True)
847 def cmd_keyring_clear(ui, repo, path, **opts): # pylint: disable=unused-argument
847 def cmd_keyring_clear(ui, repo, path, **opts): # pylint: disable=unused-argument
848 """
848 """
849 Drops password bound to given path (if any is saved).
849 Drops password bound to given path (if any is saved).
850
850
851 Parameter can be given as full url (``https://John@bitbucket.org``) or as the name
851 Parameter can be given as full url (``https://John@bitbucket.org``) or as the name
852 of path alias (``bitbucket``).
852 of path alias (``bitbucket``).
853 """
853 """
854 path_url = path
854 path_url = path
855 for name, url in ui.configitems(b'paths'):
855 for name, url in ui.configitems(b'paths'):
856 if name == path:
856 if name == path:
857 path_url = url
857 path_url = url
858 break
858 break
859 if not is_http_path(path_url):
859 if not is_http_path(path_url):
860 ui.status(meu.ui_string(
860 ui.status(meu.ui_string(
861 "%s is not a http path (and %s can't be resolved as path alias)\n",
861 "%s is not a http path (and %s can't be resolved as path alias)\n",
862 path, path_url))
862 path, path_url))
863 return
863 return
864
864
865 handler = HTTPPasswordHandler()
865 handler = HTTPPasswordHandler()
866
866
867 user, pwd, source, final_url = handler.get_credentials(
867 user, pwd, source, final_url = handler.get_credentials(
868 make_passwordmgr(ui), path, path_url)
868 make_passwordmgr(ui), path, path_url)
869 if not user:
869 if not user:
870 ui.status(meu.ui_string("Username not configured for url %s\n",
870 ui.status(meu.ui_string("Username not configured for url %s\n",
871 final_url))
871 final_url))
872 return
872 return
873 if not pwd:
873 if not pwd:
874 ui.status(meu.ui_string("No password is saved for user %s, url %s\n",
874 ui.status(meu.ui_string("No password is saved for user %s, url %s\n",
875 user, final_url))
875 user, final_url))
876 return
876 return
877
877
878 if source != handler.SRC_KEYRING:
878 if source != handler.SRC_KEYRING:
879 ui.status(meu.ui_string("Password for user %s, url %s is saved in %s, not in keyring\n",
879 ui.status(meu.ui_string("Password for user %s, url %s is saved in %s, not in keyring\n",
880 user, final_url, source))
880 user, final_url, source))
881
881
882 password_store.clear_http_password(final_url, user)
882 password_store.clear_http_password(final_url, user)
883 ui.status(meu.ui_string("Password removed for user %s, url %s\n",
883 ui.status(meu.ui_string("Password removed for user %s, url %s\n",
884 user, final_url))
884 user, final_url))
885
885
886
886
887 buglink = 'https://bitbucket.org/Mekk/mercurial_keyring/issues'
887 buglink = 'https://foss.heptapod.net/mercurial/mercurial_keyring/issues'
@@ -1,37 +1,37 b''
1 """Setup for mercurial_keyring."""
1 """Setup for mercurial_keyring."""
2
2
3 from setuptools import setup
3 from setuptools import setup
4
4
5 VERSION = '1.3.0'
5 VERSION = '1.3.0'
6 LONG_DESCRIPTION = open("README.rst").read()
6 LONG_DESCRIPTION = open("README.rst").read()
7
7
8 setup(
8 setup(
9 name="mercurial_keyring",
9 name="mercurial_keyring",
10 version=VERSION,
10 version=VERSION,
11 author='Marcin Kasperski',
11 author='Marcin Kasperski',
12 author_email='Marcin.Kasperski@mekk.waw.pl',
12 author_email='Marcin.Kasperski@mekk.waw.pl',
13 url='http://bitbucket.org/Mekk/mercurial_keyring',
13 url='https://foss.heptapod.net/mercurial/mercurial_keyring',
14 description='Mercurial Keyring Extension',
14 description='Mercurial Keyring Extension',
15 long_description=LONG_DESCRIPTION,
15 long_description=LONG_DESCRIPTION,
16 license='BSD',
16 license='BSD',
17 py_modules=['mercurial_keyring'],
17 py_modules=['mercurial_keyring'],
18 keywords="mercurial hg keyring password",
18 keywords="mercurial hg keyring password",
19 classifiers=[
19 classifiers=[
20 'Development Status :: 4 - Beta',
20 'Development Status :: 4 - Beta',
21 'Environment :: Console',
21 'Environment :: Console',
22 'Intended Audience :: Developers',
22 'Intended Audience :: Developers',
23 'License :: DFSG approved',
23 'License :: DFSG approved',
24 'License :: OSI Approved :: BSD License',
24 'License :: OSI Approved :: BSD License',
25 'Programming Language :: Python :: 2',
25 'Programming Language :: Python :: 2',
26 'Programming Language :: Python :: 3',
26 'Programming Language :: Python :: 3',
27 'Operating System :: OS Independent',
27 'Operating System :: OS Independent',
28 'Topic :: Software Development :: Libraries',
28 'Topic :: Software Development :: Libraries',
29 'Topic :: Software Development :: Libraries :: Python Modules',
29 'Topic :: Software Development :: Libraries :: Python Modules',
30 'Topic :: Software Development :: Version Control'
30 'Topic :: Software Development :: Version Control'
31 ],
31 ],
32 install_requires=[
32 install_requires=[
33 'keyring>=0.3',
33 'keyring>=0.3',
34 'mercurial_extension_utils>=1.5.0',
34 'mercurial_extension_utils>=1.5.0',
35 ],
35 ],
36 zip_safe=True,
36 zip_safe=True,
37 )
37 )
General Comments 0
You need to be logged in to leave comments. Login now