##// END OF EJS Templates
#49 Url-stored usernames should work again.
Marcin Kasperski -
r212:d9a57074 default
parent child Browse files
Show More
@@ -1,259 +1,262 b''
1 1.1.1
2 ~~~~~~~~~~~~~~~~~~
1
3
2 ~~~~~~~~~~~~~~~~~~
4 #49 Fixed the bug due to url-stored usernames did not work (introduced
5 in 1.0.0 and not completely fixed in 1.0.1).
3
6
4 #50 Bad doc url in error message
7 #50 Bad doc url in error message
5
8
6
9
7 1.1.0
10 1.1.0
8 ~~~~~~~~~~~~~~~~~~
11 ~~~~~~~~~~~~~~~~~~
9
12
10 Forward compatibility for Mercurial 3.8 (should not break old mercurials)
13 Forward compatibility for Mercurial 3.8 (should not break old mercurials)
11
14
12 1.0.1
15 1.0.1
13 ~~~~~~~~~~~~~~~~~~
16 ~~~~~~~~~~~~~~~~~~
14
17
15 URLs containing usernames (https://John@some.service/somewhat) were
18 URLs containing usernames (https://John@some.service/somewhat) were
16 not working unless username was also configured separately (username
19 not working unless username was also configured separately (username
17 presence in url was not detected properly).
20 presence in url was not detected properly).
18
21
19 Liberated prefix matching, path https://John@some.service/somewhat can
22 Liberated prefix matching, path https://John@some.service/somewhat can
20 be matched both against prefix https://some.service and against
23 be matched both against prefix https://some.service and against
21 https://John@some.service. That mostly matches what mercurial itself
24 https://John@some.service. That mostly matches what mercurial itself
22 does.
25 does.
23
26
24 1.0.0
27 1.0.0
25 ~~~~~~~~~~~~~~~~~~
28 ~~~~~~~~~~~~~~~~~~
26
29
27 Added
30 Added
28 hg keyring_check
31 hg keyring_check
29 and
32 and
30 hg keyring_clear PATH-OR-ALIAS
33 hg keyring_clear PATH-OR-ALIAS
31 commands
34 commands
32
35
33 Removed obsolete workarounds (compatibility for very old Mercurials -
36 Removed obsolete workarounds (compatibility for very old Mercurials -
34 some for pre-1.0, some for 1.4, some for 1.8/1.9).
37 some for pre-1.0, some for 1.4, some for 1.8/1.9).
35 Mercurial 2.0 is now required.
38 Mercurial 2.0 is now required.
36
39
37 Improved information about path prefix. In particular it is shown
40 Improved information about path prefix. In particular it is shown
38 whenever user is asked for password, for example:
41 whenever user is asked for password, for example:
39 hg pull bitbucket
42 hg pull bitbucket
40 http authorization required
43 http authorization required
41 realm: BitBucket
44 realm: BitBucket
42 url: https://bitbucket.org/Mekk
45 url: https://bitbucket.org/Mekk
43 user: Mekk (fixed in hgrc or url)
46 user: Mekk (fixed in hgrc or url)
44 password:
47 password:
45
48
46 Improved README.
49 Improved README.
47
50
48 Improved debug information.
51 Improved debug information.
49
52
50 0.8.0
53 0.8.0
51 ~~~~~~~~~~~~~~~~~~
54 ~~~~~~~~~~~~~~~~~~
52
55
53 Module is simplified a bit, but requires mercurial_extension_utils.
56 Module is simplified a bit, but requires mercurial_extension_utils.
54 Debug messages are prefixed with keyring: not [HgKeyring]
57 Debug messages are prefixed with keyring: not [HgKeyring]
55
58
56 0.7.1
59 0.7.1
57 ~~~~~~~~~~~~~~~~~~
60 ~~~~~~~~~~~~~~~~~~
58
61
59 #48 NullHandler import failure no longer breaks the extension.
62 #48 NullHandler import failure no longer breaks the extension.
60 May help python 2.6 compatibility.
63 May help python 2.6 compatibility.
61
64
62 0.7.0
65 0.7.0
63 ~~~~~~~~~~~~~~~~~~~
66 ~~~~~~~~~~~~~~~~~~~
64
67
65 Delaying keyring module import until passwords are really needed. It
68 Delaying keyring module import until passwords are really needed. It
66 can noticeably improve Mercurial (non pull/push) performance in some
69 can noticeably improve Mercurial (non pull/push) performance in some
67 cases (no longer slow hg status because D-Bus is busy an keyring tries
70 cases (no longer slow hg status because D-Bus is busy an keyring tries
68 to activate KDE Wallet through it…).
71 to activate KDE Wallet through it…).
69
72
70 0.6.7
73 0.6.7
71 ~~~~~~~~~~~~~~~~~
74 ~~~~~~~~~~~~~~~~~
72
75
73 #46 Fixed syntax of smtp.tls configuration setting (current Mercurials
76 #46 Fixed syntax of smtp.tls configuration setting (current Mercurials
74 doesn't handle "true" anymore, TortoiseHG crashed with mercurial
77 doesn't handle "true" anymore, TortoiseHG crashed with mercurial
75 keyring enabled while currently recommended starttls/smtps/none values
78 keyring enabled while currently recommended starttls/smtps/none values
76 were in use).
79 were in use).
77
80
78 0.6.6
81 0.6.6
79 ~~~~~~~~~~~~~~~~~
82 ~~~~~~~~~~~~~~~~~
80
83
81 #44 Handling some more mercurial versions in demandimport-detection
84 #44 Handling some more mercurial versions in demandimport-detection
82 logic.
85 logic.
83
86
84 0.6.5
87 0.6.5
85 ~~~~~~~~~~~~~~~~~
88 ~~~~~~~~~~~~~~~~~
86
89
87 #36 Shutting up warning about no logging handlers.
90 #36 Shutting up warning about no logging handlers.
88
91
89 0.6.4
92 0.6.4
90 ~~~~~~~~~~~~~~~~~
93 ~~~~~~~~~~~~~~~~~
91
94
92 #44 Pre-2.9.1 Mercurials compatibility (probing for active
95 #44 Pre-2.9.1 Mercurials compatibility (probing for active
93 demandimport differently).
96 demandimport differently).
94
97
95 0.6.3
98 0.6.3
96 ~~~~~~~~~~~~~~~~~
99 ~~~~~~~~~~~~~~~~~
97
100
98 #41 Fix for incorrect demandimport activity check logic, which could
101 #41 Fix for incorrect demandimport activity check logic, which could
99 cause various problems with imports after mercurial_keyring is
102 cause various problems with imports after mercurial_keyring is
100 imported.
103 imported.
101
104
102 0.6.2
105 0.6.2
103 ~~~~~~~~~~~~~~~~~
106 ~~~~~~~~~~~~~~~~~
104
107
105 #33 Fix for UnicodeDecodeErrors happening on some backends (especially
108 #33 Fix for UnicodeDecodeErrors happening on some backends (especially
106 Vault) when passwords with non-ascii characters are in use and native
109 Vault) when passwords with non-ascii characters are in use and native
107 locale is not utf-8. Passwords are no longer saved to keyring backends
110 locale is not utf-8. Passwords are no longer saved to keyring backends
108 as-entered, they are now decoded from local encoding (whichever is
111 as-entered, they are now decoded from local encoding (whichever is
109 detected by Mercurial), then encoded to unicode.
112 detected by Mercurial), then encoded to unicode.
110
113
111 0.6.1
114 0.6.1
112 ~~~~~~~~~~~~~~~~~
115 ~~~~~~~~~~~~~~~~~
113
116
114 #30 Yet another demandimport conflict fixed.
117 #30 Yet another demandimport conflict fixed.
115
118
116 0.6.0
119 0.6.0
117 ~~~~~~~~~~~~~~~~~
120 ~~~~~~~~~~~~~~~~~
118
121
119 #28 Disable demandimport completely during keyring import. Mayhaps it
122 #28 Disable demandimport completely during keyring import. Mayhaps it
120 will resolve (most) demandimport conflict errors.
123 will resolve (most) demandimport conflict errors.
121
124
122 0.5.7
125 0.5.7
123 ~~~~~~~~~~~~~~~~~
126 ~~~~~~~~~~~~~~~~~
124
127
125 #27 Some more demandimport ignores.
128 #27 Some more demandimport ignores.
126
129
127 0.5.6
130 0.5.6
128 ~~~~~~~~~~~~~~~~~
131 ~~~~~~~~~~~~~~~~~
129
132
130 #24, #25 Demandimport fixes (import failures in specific cases).
133 #24, #25 Demandimport fixes (import failures in specific cases).
131
134
132 Better way of demandimport-ignoring modules. In particular, we append
135 Better way of demandimport-ignoring modules. In particular, we append
133 more of them if gobject happens to be on the list.
136 more of them if gobject happens to be on the list.
134
137
135 0.5.5
138 0.5.5
136 ~~~~~~~~~~~~~~~~~
139 ~~~~~~~~~~~~~~~~~
137
140
138 Fix for gnome keyring import problems.
141 Fix for gnome keyring import problems.
139
142
140 0.5.4
143 0.5.4
141 ~~~~~~~~~~~~~~~~~
144 ~~~~~~~~~~~~~~~~~
142
145
143 #22 Some more demandimport ignores (fix import failures).
146 #22 Some more demandimport ignores (fix import failures).
144
147
145 SMTP password was not cleared properly (after detecting that it is
148 SMTP password was not cleared properly (after detecting that it is
146 invalid).
149 invalid).
147
150
148 Clarified license to be modified BSD style license.
151 Clarified license to be modified BSD style license.
149
152
150 0.5.3
153 0.5.3
151 ~~~~~~~~~~~~~~~~~
154 ~~~~~~~~~~~~~~~~~
152
155
153 Remove useless import which caused problems on Mercurial 2.3 when
156 Remove useless import which caused problems on Mercurial 2.3 when
154 demandimport was not enabled
157 demandimport was not enabled
155
158
156 0.5.1
159 0.5.1
157 ~~~~~~~~~~~~~~~~~
160 ~~~~~~~~~~~~~~~~~
158
161
159 Add help text to output for hg help.
162 Add help text to output for hg help.
160
163
161 0.5.0
164 0.5.0
162 ~~~~~~~~~~~~~~~~~
165 ~~~~~~~~~~~~~~~~~
163
166
164 Improved bad password detection. Internally: extension is now able to
167 Improved bad password detection. Internally: extension is now able to
165 properly differentiate between an authentication failure and a new
168 properly differentiate between an authentication failure and a new
166 request to the same url.
169 request to the same url.
167
170
168 Fixes in debug message
171 Fixes in debug message
169
172
170 Further debug messages patching
173 Further debug messages patching
171
174
172 Improving debug messages handling.
175 Improving debug messages handling.
173
176
174 Mercurial Keyring debug messages are now prefixed with
177 Mercurial Keyring debug messages are now prefixed with
175 [HgKeyring] to make distinguishing them easier
178 [HgKeyring] to make distinguishing them easier
176
179
177 0.4.6
180 0.4.6
178 ~~~~~~~~~~~~~~~~~
181 ~~~~~~~~~~~~~~~~~
179
182
180 More compatibility (changed signature of httpconnection.readauthforuri
183 More compatibility (changed signature of httpconnection.readauthforuri
181 , introduced post Mercurial 1.9 - since hg.0593e8f81c71)
184 , introduced post Mercurial 1.9 - since hg.0593e8f81c71)
182
185
183 Fix compatibility code which did not work due to demandimport issues
186 Fix compatibility code which did not work due to demandimport issues
184 (attempts to catch ImportErrors on "from mercurial.url import
187 (attempts to catch ImportErrors on "from mercurial.url import
185 readauthforuri" were not working properly).
188 readauthforuri" were not working properly).
186
189
187 0.4.5
190 0.4.5
188 ~~~~~~~~~~~~~~~~~
191 ~~~~~~~~~~~~~~~~~
189
192
190 Mercurial 1.9 compatibility (readauthforuri has been moved into new
193 Mercurial 1.9 compatibility (readauthforuri has been moved into new
191 httpconnection module).
194 httpconnection module).
192
195
193 0.4.4
196 0.4.4
194 ~~~~~~~~~~~~~~~~~
197 ~~~~~~~~~~~~~~~~~
195
198
196 Mercurial 1.8 compatibility (passwordmgr.readauthtoken() has been
199 Mercurial 1.8 compatibility (passwordmgr.readauthtoken() has been
197 moved into mercurial.url.readauthforuri).
200 moved into mercurial.url.readauthforuri).
198
201
199 0.4.3
202 0.4.3
200 ~~~~~~~~~~~~~~~~~
203 ~~~~~~~~~~~~~~~~~
201
204
202 Keyring fork no longer is needed as keyring releases are available
205 Keyring fork no longer is needed as keyring releases are available
203 again.
206 again.
204
207
205 Workaround for gnomekeyring mercurial.demandimport incompatibility:
208 Workaround for gnomekeyring mercurial.demandimport incompatibility:
206 mercurial.demandimport, which is enabled while in a mercurial
209 mercurial.demandimport, which is enabled while in a mercurial
207 extensions, prevents the correct import of gobject._gobject and
210 extensions, prevents the correct import of gobject._gobject and
208 consequently doesn't allow the loading of the gnomekeyring module,
211 consequently doesn't allow the loading of the gnomekeyring module,
209 which can be used by keyring. This just adds the proper module to
212 which can be used by keyring. This just adds the proper module to
210 demandimport ignore list.
213 demandimport ignore list.
211
214
212 0.4.2
215 0.4.2
213 ~~~~~~~~~~~~~~~~~
216 ~~~~~~~~~~~~~~~~~
214
217
215 No longer raising an error when username is specified both in ~/.hgrc
218 No longer raising an error when username is specified both in ~/.hgrc
216 and <repo>/.hg/hgrc if it is the same in both places.
219 and <repo>/.hg/hgrc if it is the same in both places.
217
220
218 Docs recommend sborho keyring fork.
221 Docs recommend sborho keyring fork.
219
222
220 0.4.1
223 0.4.1
221 ~~~~~~~~~~~~~~~~~
224 ~~~~~~~~~~~~~~~~~
222
225
223 Some tweaks and docs related to prefix handling.
226 Some tweaks and docs related to prefix handling.
224
227
225 Explicit information that keyring is not used due to lack of username.
228 Explicit information that keyring is not used due to lack of username.
226
229
227 0.4.0
230 0.4.0
228 ~~~~~~~~~~~~~~~~~
231 ~~~~~~~~~~~~~~~~~
229
232
230 Store and lookup prefix from [auth] so that password is shared amongst
233 Store and lookup prefix from [auth] so that password is shared amongst
231 shared auth entries
234 shared auth entries
232
235
233 0.3.3
236 0.3.3
234 ~~~~~~~~~~~~~~~~~
237 ~~~~~~~~~~~~~~~~~
235
238
236 Better error message
239 Better error message
237
240
238 0.3.2
241 0.3.2
239 ~~~~~~~~~~~~~~~~~
242 ~~~~~~~~~~~~~~~~~
240
243
241 Doc tweaks
244 Doc tweaks
242
245
243 0.3.1
246 0.3.1
244 ~~~~~~~~~~~~~~~~~
247 ~~~~~~~~~~~~~~~~~
245
248
246 Introduced and documented PyPi package, added setup.py
249 Introduced and documented PyPi package, added setup.py
247
250
248 0.2.0
251 0.2.0
249 ~~~~~~~~~~~~~~~~~
252 ~~~~~~~~~~~~~~~~~
250
253
251 Added handling of SMTP passwords (tested on patchbomb extension but
254 Added handling of SMTP passwords (tested on patchbomb extension but
252 should work on anything what utilizes mercurial.mail)
255 should work on anything what utilizes mercurial.mail)
253
256
254 Docstrings mention Debian keyring packages.
257 Docstrings mention Debian keyring packages.
255
258
256 0.1.1
259 0.1.1
257 ~~~~~~~~~~~~~~~~~
260 ~~~~~~~~~~~~~~~~~
258
261
259 Initial public release
262 Initial public release
@@ -1,778 +1,782 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 #
2 #
3 # mercurial_keyring: save passwords in password database
3 # mercurial_keyring: save passwords in password database
4 #
4 #
5 # Copyright (c) 2009 Marcin Kasperski <Marcin.Kasperski@mekk.waw.pl>
5 # Copyright (c) 2009 Marcin Kasperski <Marcin.Kasperski@mekk.waw.pl>
6 # All rights reserved.
6 # All rights reserved.
7 #
7 #
8 # Redistribution and use in source and binary forms, with or without
8 # Redistribution and use in source and binary forms, with or without
9 # modification, are permitted provided that the following conditions
9 # modification, are permitted provided that the following conditions
10 # are met:
10 # are met:
11 # 1. Redistributions of source code must retain the above copyright
11 # 1. Redistributions of source code must retain the above copyright
12 # notice, this list of conditions and the following disclaimer.
12 # notice, this list of conditions and the following disclaimer.
13 # 2. Redistributions in binary form must reproduce the above copyright
13 # 2. Redistributions in binary form must reproduce the above copyright
14 # notice, this list of conditions and the following disclaimer in the
14 # notice, this list of conditions and the following disclaimer in the
15 # documentation and/or other materials provided with the distribution.
15 # documentation and/or other materials provided with the distribution.
16 # 3. The name of the author may not be used to endorse or promote products
16 # 3. The name of the author may not be used to endorse or promote products
17 # derived from this software without specific prior written permission.
17 # derived from this software without specific prior written permission.
18 #
18 #
19 # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
19 # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
20 # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
20 # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
21 # OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
21 # OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
22 # IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
22 # IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
23 # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
23 # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
24 # NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
24 # NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
25 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
27 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
28 # THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28 # THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29 #
29 #
30 # See README.txt for more details.
30 # See README.txt for more details.
31
31
32 '''securely save HTTP and SMTP passwords to encrypted storage
32 '''securely save HTTP and SMTP passwords to encrypted storage
33
33
34 mercurial_keyring securely saves HTTP and SMTP passwords in password
34 mercurial_keyring securely saves HTTP and SMTP passwords in password
35 databases (Gnome Keyring, KDE KWallet, OSXKeyChain, Win32 crypto
35 databases (Gnome Keyring, KDE KWallet, OSXKeyChain, Win32 crypto
36 services).
36 services).
37
37
38 The process is automatic. Whenever bare Mercurial just prompts for
38 The process is automatic. Whenever bare Mercurial just prompts for
39 the password, Mercurial with mercurial_keyring enabled checks whether
39 the password, Mercurial with mercurial_keyring enabled checks whether
40 saved password is available first. If so, it is used. If not, you
40 saved password is available first. If so, it is used. If not, you
41 will be prompted for the password, but entered password will be
41 will be prompted for the password, but entered password will be
42 saved for the future use.
42 saved for the future use.
43
43
44 In case saved password turns out to be invalid (HTTP or SMTP login
44 In case saved password turns out to be invalid (HTTP or SMTP login
45 fails) it is dropped, and you are asked for current password.
45 fails) it is dropped, and you are asked for current password.
46
46
47 Actual password storage is implemented by Python keyring library, this
47 Actual password storage is implemented by Python keyring library, this
48 extension glues those services to Mercurial. Consult keyring
48 extension glues those services to Mercurial. Consult keyring
49 documentation for information how to configure actual password
49 documentation for information how to configure actual password
50 backend (by default keyring guesses, usually correctly, for example
50 backend (by default keyring guesses, usually correctly, for example
51 you get KDE Wallet under KDE, and Gnome Keyring under Gnome or Unity).
51 you get KDE Wallet under KDE, and Gnome Keyring under Gnome or Unity).
52 '''
52 '''
53
53
54 from mercurial import util, sslutil
54 from mercurial import util, sslutil
55 from mercurial.i18n import _
55 from mercurial.i18n import _
56 from mercurial.url import passwordmgr
56 from mercurial.url import passwordmgr
57 from mercurial import mail
57 from mercurial import mail
58 from mercurial.mail import SMTPS, STARTTLS
58 from mercurial.mail import SMTPS, STARTTLS
59 from mercurial import encoding
59 from mercurial import encoding
60
60
61 import urllib2
61 import urllib2
62 import smtplib
62 import smtplib
63 import socket
63 import socket
64 import os
64 import os
65 import sys
65 import sys
66 import re
66 import re
67
67
68 # pylint: disable=invalid-name, line-too-long, protected-access, too-many-arguments
68 # pylint: disable=invalid-name, line-too-long, protected-access, too-many-arguments
69
69
70 ###########################################################################
70 ###########################################################################
71 # Specific import trickery
71 # Specific import trickery
72 ###########################################################################
72 ###########################################################################
73
73
74
74
75 def import_meu():
75 def import_meu():
76 """
76 """
77 Convoluted import of mercurial_extension_utils, which helps
77 Convoluted import of mercurial_extension_utils, which helps
78 TortoiseHg/Win setups. This routine and it's use below
78 TortoiseHg/Win setups. This routine and it's use below
79 performs equivalent of
79 performs equivalent of
80 from mercurial_extension_utils import monkeypatch_method
80 from mercurial_extension_utils import monkeypatch_method
81 but looks for some non-path directories.
81 but looks for some non-path directories.
82 """
82 """
83 try:
83 try:
84 import mercurial_extension_utils
84 import mercurial_extension_utils
85 except ImportError:
85 except ImportError:
86 my_dir = os.path.dirname(__file__)
86 my_dir = os.path.dirname(__file__)
87 sys.path.extend([
87 sys.path.extend([
88 # In the same dir (manual or site-packages after pip)
88 # In the same dir (manual or site-packages after pip)
89 my_dir,
89 my_dir,
90 # Developer clone
90 # Developer clone
91 os.path.join(os.path.dirname(my_dir), "extension_utils"),
91 os.path.join(os.path.dirname(my_dir), "extension_utils"),
92 # Side clone
92 # Side clone
93 os.path.join(os.path.dirname(my_dir), "mercurial-extension_utils"),
93 os.path.join(os.path.dirname(my_dir), "mercurial-extension_utils"),
94 ])
94 ])
95 try:
95 try:
96 import mercurial_extension_utils
96 import mercurial_extension_utils
97 except ImportError:
97 except ImportError:
98 raise util.Abort(_("""Can not import mercurial_extension_utils.
98 raise util.Abort(_("""Can not import mercurial_extension_utils.
99 Please install this module in Python path.
99 Please install this module in Python path.
100 See Installation chapter in https://bitbucket.org/Mekk/mercurial_keyring/ for details
100 See Installation chapter in https://bitbucket.org/Mekk/mercurial_keyring/ for details
101 (and for info about TortoiseHG on Windows, or other bundled Python)."""))
101 (and for info about TortoiseHG on Windows, or other bundled Python)."""))
102 return mercurial_extension_utils
102 return mercurial_extension_utils
103
103
104 meu = import_meu()
104 meu = import_meu()
105 monkeypatch_method = meu.monkeypatch_method
105 monkeypatch_method = meu.monkeypatch_method
106
106
107
107
108 def import_keyring():
108 def import_keyring():
109 """
109 """
110 Importing keyring happens to be costly if wallet is slow, so we delay it
110 Importing keyring happens to be costly if wallet is slow, so we delay it
111 until it is really needed. The routine below also works around various
111 until it is really needed. The routine below also works around various
112 demandimport-related problems.
112 demandimport-related problems.
113 """
113 """
114 # mercurial.demandimport incompatibility workaround.
114 # mercurial.demandimport incompatibility workaround.
115 # various keyring backends fail as they can't properly import helper
115 # various keyring backends fail as they can't properly import helper
116 # modules (as demandimport modifies python import behaviour).
116 # modules (as demandimport modifies python import behaviour).
117 # If you get import errors with demandimport in backtrace, try
117 # If you get import errors with demandimport in backtrace, try
118 # guessing what to block and extending the list below.
118 # guessing what to block and extending the list below.
119 mod, was_imported_now = meu.direct_import_ext(
119 mod, was_imported_now = meu.direct_import_ext(
120 "keyring", [
120 "keyring", [
121 "gobject._gobject",
121 "gobject._gobject",
122 "configparser",
122 "configparser",
123 "json",
123 "json",
124 "abc",
124 "abc",
125 "io",
125 "io",
126 "keyring",
126 "keyring",
127 "gdata.docs.service",
127 "gdata.docs.service",
128 "gdata.service",
128 "gdata.service",
129 "types",
129 "types",
130 "atom.http",
130 "atom.http",
131 "atom.http_interface",
131 "atom.http_interface",
132 "atom.service",
132 "atom.service",
133 "atom.token_store",
133 "atom.token_store",
134 "ctypes",
134 "ctypes",
135 "secretstorage.exceptions",
135 "secretstorage.exceptions",
136 "fs.opener",
136 "fs.opener",
137 ])
137 ])
138 if was_imported_now:
138 if was_imported_now:
139 # Shut up warning about uninitialized logging by keyring
139 # Shut up warning about uninitialized logging by keyring
140 meu.disable_logging("keyring")
140 meu.disable_logging("keyring")
141 return mod
141 return mod
142
142
143
143
144 #################################################################
144 #################################################################
145 # Actual implementation
145 # Actual implementation
146 #################################################################
146 #################################################################
147
147
148 KEYRING_SERVICE = "Mercurial"
148 KEYRING_SERVICE = "Mercurial"
149
149
150
150
151 class PasswordStore(object):
151 class PasswordStore(object):
152 """
152 """
153 Helper object handling keyring usage (password save&restore,
153 Helper object handling keyring usage (password save&restore,
154 the way passwords are keyed in the keyring).
154 the way passwords are keyed in the keyring).
155 """
155 """
156 def __init__(self):
156 def __init__(self):
157 self.cache = dict()
157 self.cache = dict()
158
158
159 def get_http_password(self, url, username):
159 def get_http_password(self, url, username):
160 """
160 """
161 Checks whether password of username for url is available,
161 Checks whether password of username for url is available,
162 returns it or None
162 returns it or None
163 """
163 """
164 return self._read_password_from_keyring(
164 return self._read_password_from_keyring(
165 self._format_http_key(url, username))
165 self._format_http_key(url, username))
166
166
167 def set_http_password(self, url, username, password):
167 def set_http_password(self, url, username, password):
168 """Saves password to keyring"""
168 """Saves password to keyring"""
169 self._save_password_to_keyring(
169 self._save_password_to_keyring(
170 self._format_http_key(url, username),
170 self._format_http_key(url, username),
171 password)
171 password)
172
172
173 def clear_http_password(self, url, username):
173 def clear_http_password(self, url, username):
174 """Drops saved password"""
174 """Drops saved password"""
175 self.set_http_password(url, username, "")
175 self.set_http_password(url, username, "")
176
176
177 @staticmethod
177 @staticmethod
178 def _format_http_key(url, username):
178 def _format_http_key(url, username):
179 """Construct actual key for password identification"""
179 """Construct actual key for password identification"""
180 return "%s@@%s" % (username, url)
180 return "%s@@%s" % (username, url)
181
181
182 def get_smtp_password(self, machine, port, username):
182 def get_smtp_password(self, machine, port, username):
183 """Checks for SMTP password in keyring, returns
183 """Checks for SMTP password in keyring, returns
184 password or None"""
184 password or None"""
185 return self._read_password_from_keyring(
185 return self._read_password_from_keyring(
186 self._format_smtp_key(machine, port, username))
186 self._format_smtp_key(machine, port, username))
187
187
188 def set_smtp_password(self, machine, port, username, password):
188 def set_smtp_password(self, machine, port, username, password):
189 """Saves SMTP password to keyring"""
189 """Saves SMTP password to keyring"""
190 self._save_password_to_keyring(
190 self._save_password_to_keyring(
191 self._format_smtp_key(machine, port, username),
191 self._format_smtp_key(machine, port, username),
192 password)
192 password)
193
193
194 def clear_smtp_password(self, machine, port, username):
194 def clear_smtp_password(self, machine, port, username):
195 """Drops saved SMTP password"""
195 """Drops saved SMTP password"""
196 self.set_smtp_password(machine, port, username, "")
196 self.set_smtp_password(machine, port, username, "")
197
197
198 @staticmethod
198 @staticmethod
199 def _format_smtp_key(machine, port, username):
199 def _format_smtp_key(machine, port, username):
200 """Construct key for SMTP password identification"""
200 """Construct key for SMTP password identification"""
201 return "%s@@%s:%s" % (username, machine, str(port))
201 return "%s@@%s:%s" % (username, machine, str(port))
202
202
203 @staticmethod
203 @staticmethod
204 def _read_password_from_keyring(pwdkey):
204 def _read_password_from_keyring(pwdkey):
205 """Physically read from keyring"""
205 """Physically read from keyring"""
206 keyring = import_keyring()
206 keyring = import_keyring()
207 password = keyring.get_password(KEYRING_SERVICE, pwdkey)
207 password = keyring.get_password(KEYRING_SERVICE, pwdkey)
208 # Reverse recoding from next routine
208 # Reverse recoding from next routine
209 if isinstance(password, unicode):
209 if isinstance(password, unicode):
210 return encoding.tolocal(password.encode('utf-8'))
210 return encoding.tolocal(password.encode('utf-8'))
211 return password
211 return password
212
212
213 @staticmethod
213 @staticmethod
214 def _save_password_to_keyring(pwdkey, password):
214 def _save_password_to_keyring(pwdkey, password):
215 """Physically write to keyring"""
215 """Physically write to keyring"""
216 keyring = import_keyring()
216 keyring = import_keyring()
217 # keyring in general expects unicode.
217 # keyring in general expects unicode.
218 # Mercurial provides "local" encoding. See #33
218 # Mercurial provides "local" encoding. See #33
219 password = encoding.fromlocal(password).decode('utf-8')
219 password = encoding.fromlocal(password).decode('utf-8')
220 keyring.set_password(
220 keyring.set_password(
221 KEYRING_SERVICE, pwdkey, password)
221 KEYRING_SERVICE, pwdkey, password)
222
222
223 password_store = PasswordStore()
223 password_store = PasswordStore()
224
224
225
225
226 ############################################################
226 ############################################################
227 # Various utils
227 # Various utils
228 ############################################################
228 ############################################################
229
229
230 def _debug(ui, msg):
230 def _debug(ui, msg):
231 """Generic debug message"""
231 """Generic debug message"""
232 ui.debug("keyring: " + msg + "\n")
232 ui.debug("keyring: " + msg + "\n")
233
233
234
234
235 class PwdCache(object):
235 class PwdCache(object):
236 """Short term cache, used to preserve passwords
236 """Short term cache, used to preserve passwords
237 if they are used twice during a command"""
237 if they are used twice during a command"""
238 def __init__(self):
238 def __init__(self):
239 self._cache = {}
239 self._cache = {}
240
240
241 def store(self, realm, url, user, pwd):
241 def store(self, realm, url, user, pwd):
242 """Saves password"""
242 """Saves password"""
243 cache_key = (realm, url, user)
243 cache_key = (realm, url, user)
244 self._cache[cache_key] = pwd
244 self._cache[cache_key] = pwd
245
245
246 def check(self, realm, url, user):
246 def check(self, realm, url, user):
247 """Checks for cached password"""
247 """Checks for cached password"""
248 cache_key = (realm, url, user)
248 cache_key = (realm, url, user)
249 return self._cache.get(cache_key)
249 return self._cache.get(cache_key)
250
250
251
251
252 _re_http_url = re.compile(r'^https?://')
252 _re_http_url = re.compile(r'^https?://')
253
253
254 def is_http_path(url):
254 def is_http_path(url):
255 return bool(_re_http_url.search(url))
255 return bool(_re_http_url.search(url))
256
256
257 ############################################################
257 ############################################################
258 # HTTP password management
258 # HTTP password management
259 ############################################################
259 ############################################################
260
260
261
261
262 class HTTPPasswordHandler(object):
262 class HTTPPasswordHandler(object):
263 """
263 """
264 Actual implementation of password handling (user prompting,
264 Actual implementation of password handling (user prompting,
265 configuration file searching, keyring save&restore).
265 configuration file searching, keyring save&restore).
266
266
267 Object of this class is bound as passwordmgr attribute.
267 Object of this class is bound as passwordmgr attribute.
268 """
268 """
269 def __init__(self):
269 def __init__(self):
270 self.pwd_cache = PwdCache()
270 self.pwd_cache = PwdCache()
271 self.last_reply = None
271 self.last_reply = None
272
272
273 # Markers and also names used in debug notes. Password source
273 # Markers and also names used in debug notes. Password source
274 SRC_URL = "repository URL"
274 SRC_URL = "repository URL"
275 SRC_CFGAUTH = "hgrc"
275 SRC_CFGAUTH = "hgrc"
276 SRC_MEMCACHE = "temporary cache"
276 SRC_MEMCACHE = "temporary cache"
277 SRC_URLCACHE = "urllib temporary cache"
277 SRC_URLCACHE = "urllib temporary cache"
278 SRC_KEYRING = "keyring"
278 SRC_KEYRING = "keyring"
279
279
280 def get_credentials(self, pwmgr, realm, authuri, skip_caches=False):
280 def get_credentials(self, pwmgr, realm, authuri, skip_caches=False):
281 """
281 """
282 Looks up for user credentials in various places, returns them
282 Looks up for user credentials in various places, returns them
283 and information about their source.
283 and information about their source.
284
284
285 Used internally inside find_auth and inside informative
285 Used internally inside find_auth and inside informative
286 commands (thiis method doesn't cache, doesn't detect bad
286 commands (thiis method doesn't cache, doesn't detect bad
287 passwords etc, doesn't prompt interactively, doesn't store
287 passwords etc, doesn't prompt interactively, doesn't store
288 password in keyring).
288 password in keyring).
289
289
290 Returns: user, password, SRC_*, actual_url
290 Returns: user, password, SRC_*, actual_url
291
291
292 If not found, password and SRC is None, user can be given or
292 If not found, password and SRC is None, user can be given or
293 not, url is always set
293 not, url is always set
294 """
294 """
295 ui = pwmgr.ui
295 ui = pwmgr.ui
296
296
297 parsed_url, url_user, url_passwd = self.unpack_url(authuri)
297 parsed_url, url_user, url_passwd = self.unpack_url(authuri)
298 base_url = str(parsed_url)
298 base_url = str(parsed_url)
299 ui.debug(_('keyring: base url: %s, url user: %s, url pwd: %s\n') %
299 ui.debug(_('keyring: base url: %s, url user: %s, url pwd: %s\n') %
300 (base_url, url_user or '', url_passwd and '******' or ''))
300 (base_url, url_user or '', url_passwd and '******' or ''))
301
301
302 # Extract username (or password) stored directly in url
302 # Extract username (or password) stored directly in url
303 if url_user and url_passwd:
303 if url_user and url_passwd:
304 return url_user, url_passwd, self.SRC_URL, base_url
304 return url_user, url_passwd, self.SRC_URL, base_url
305
305
306 # Extract data from urllib (in case it was already stored)
306 # Extract data from urllib (in case it was already stored)
307 urllib_user, urllib_pwd \
307 urllib_user, urllib_pwd \
308 = urllib2.HTTPPasswordMgrWithDefaultRealm.find_user_password(
308 = urllib2.HTTPPasswordMgrWithDefaultRealm.find_user_password(
309 pwmgr, realm, authuri)
309 pwmgr, realm, authuri)
310 if urllib_user and urllib_pwd:
310 if urllib_user and urllib_pwd:
311 return urllib_user, urllib_pwd, self.SRC_URLCACHE, base_url
311 return urllib_user, urllib_pwd, self.SRC_URLCACHE, base_url
312
312
313 actual_user = url_user or urllib_user
314
313 # Consult configuration to normalize url to prefix, and find username
315 # Consult configuration to normalize url to prefix, and find username
314 # (and maybe password)
316 # (and maybe password)
315 auth_user, auth_pwd, keyring_url = self.get_url_config(
317 auth_user, auth_pwd, keyring_url = self.get_url_config(
316 ui, parsed_url, url_user)
318 ui, parsed_url, actual_user)
317 if auth_user and url_user and (url_user != auth_user):
319 if auth_user and actual_user and (actual_user != auth_user):
318 raise util.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, url_user, auth_user)))
320 raise util.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)))
319 if auth_user and auth_pwd:
321 if auth_user and auth_pwd:
320 return auth_user, auth_pwd, self.SRC_CFGAUTH, keyring_url
322 return auth_user, auth_pwd, self.SRC_CFGAUTH, keyring_url
321
323
324 actual_user = actual_user or auth_user
325
322 if skip_caches:
326 if skip_caches:
323 return auth_user, None, None, keyring_url
327 return actual_user, None, None, keyring_url
324
328
325 # Check memory cache (reuse )
329 # Check memory cache (reuse )
326 # Checking the memory cache (there may be many http calls per command)
330 # Checking the memory cache (there may be many http calls per command)
327 cached_pwd = self.pwd_cache.check(realm, keyring_url, auth_user)
331 cached_pwd = self.pwd_cache.check(realm, keyring_url, actual_user)
328 if cached_pwd:
332 if cached_pwd:
329 return auth_user, cached_pwd, self.SRC_MEMCACHE, keyring_url
333 return actual_user, cached_pwd, self.SRC_MEMCACHE, keyring_url
330
334
331 # Load from keyring.
335 # Load from keyring.
332 if auth_user:
336 if actual_user:
333 ui.debug(_("keyring: looking for password (user %s, url %s)\n") % (auth_user, keyring_url))
337 ui.debug(_("keyring: looking for password (user %s, url %s)\n") % (actual_user, keyring_url))
334 keyring_pwd = password_store.get_http_password(keyring_url, auth_user)
338 keyring_pwd = password_store.get_http_password(keyring_url, actual_user)
335 if keyring_pwd:
339 if keyring_pwd:
336 return auth_user, keyring_pwd, self.SRC_KEYRING, keyring_url
340 return actual_user, keyring_pwd, self.SRC_KEYRING, keyring_url
337
341
338 return auth_user, None, None, keyring_url
342 return actual_user, None, None, keyring_url
339
343
340 @staticmethod
344 @staticmethod
341 def prompt_interactively(ui, user, realm, url):
345 def prompt_interactively(ui, user, realm, url):
342 """Actual interactive prompt"""
346 """Actual interactive prompt"""
343 if not ui.interactive():
347 if not ui.interactive():
344 raise util.Abort(_('keyring: http authorization required but program used in non-interactive mode'))
348 raise util.Abort(_('keyring: http authorization required but program used in non-interactive mode'))
345
349
346 if not user:
350 if not user:
347 ui.status(_("keyring: username not specified in hgrc (or in url). Password will not be saved.\n"))
351 ui.status(_("keyring: username not specified in hgrc (or in url). Password will not be saved.\n"))
348
352
349 ui.write(_("http authorization required\n"))
353 ui.write(_("http authorization required\n"))
350 ui.status(_("realm: %s\n") % realm)
354 ui.status(_("realm: %s\n") % realm)
351 ui.status(_("url: %s\n") % url)
355 ui.status(_("url: %s\n") % url)
352 if user:
356 if user:
353 ui.write(_("user: %s (fixed in hgrc or url)\n" % user))
357 ui.write(_("user: %s (fixed in hgrc or url)\n" % user))
354 else:
358 else:
355 user = ui.prompt(_("user:"), default=None)
359 user = ui.prompt(_("user:"), default=None)
356 pwd = ui.getpass(_("password: "))
360 pwd = ui.getpass(_("password: "))
357 return user, pwd
361 return user, pwd
358
362
359 def find_auth(self, pwmgr, realm, authuri, req):
363 def find_auth(self, pwmgr, realm, authuri, req):
360 """
364 """
361 Actual implementation of find_user_password - different
365 Actual implementation of find_user_password - different
362 ways of obtaining the username and password.
366 ways of obtaining the username and password.
363
367
364 Returns pair username, password
368 Returns pair username, password
365 """
369 """
366 ui = pwmgr.ui
370 ui = pwmgr.ui
367 after_bad_auth = self._after_bad_auth(ui, realm, authuri, req)
371 after_bad_auth = self._after_bad_auth(ui, realm, authuri, req)
368
372
369 # Look in url, cache, etc
373 # Look in url, cache, etc
370 user, pwd, src, final_url = self.get_credentials(
374 user, pwd, src, final_url = self.get_credentials(
371 pwmgr, realm, authuri, skip_caches=after_bad_auth)
375 pwmgr, realm, authuri, skip_caches=after_bad_auth)
372 if pwd:
376 if pwd:
373 if src != self.SRC_MEMCACHE:
377 if src != self.SRC_MEMCACHE:
374 self.pwd_cache.store(realm, final_url, user, pwd)
378 self.pwd_cache.store(realm, final_url, user, pwd)
375 self._note_last_reply(realm, authuri, user, req)
379 self._note_last_reply(realm, authuri, user, req)
376 _debug(ui, _("Password found in " + src))
380 _debug(ui, _("Password found in " + src))
377 return user, pwd
381 return user, pwd
378
382
379 # Last resort: interactive prompt
383 # Last resort: interactive prompt
380 user, pwd = self.prompt_interactively(ui, user, realm, final_url)
384 user, pwd = self.prompt_interactively(ui, user, realm, final_url)
381
385
382 if user:
386 if user:
383 # Saving password to the keyring.
387 # Saving password to the keyring.
384 # It is done only if username is permanently set.
388 # It is done only if username is permanently set.
385 # Otherwise we won't be able to find the password so it
389 # Otherwise we won't be able to find the password so it
386 # does not make much sense to preserve it
390 # does not make much sense to preserve it
387 _debug(ui, _("Saving password for %s to keyring") % user)
391 _debug(ui, _("Saving password for %s to keyring") % user)
388 password_store.set_http_password(final_url, user, pwd)
392 password_store.set_http_password(final_url, user, pwd)
389
393
390 # Saving password to the memory cache
394 # Saving password to the memory cache
391 self.pwd_cache.store(realm, final_url, user, pwd)
395 self.pwd_cache.store(realm, final_url, user, pwd)
392 self._note_last_reply(realm, authuri, user, req)
396 self._note_last_reply(realm, authuri, user, req)
393 _debug(ui, _("Manually entered password"))
397 _debug(ui, _("Manually entered password"))
394 return user, pwd
398 return user, pwd
395
399
396 def get_url_config(self, ui, parsed_url, user):
400 def get_url_config(self, ui, parsed_url, user):
397 """
401 """
398 Checks configuration to decide whether/which username, prefix,
402 Checks configuration to decide whether/which username, prefix,
399 and password are configured for given url. Consults [auth] section.
403 and password are configured for given url. Consults [auth] section.
400
404
401 Returns tuple (username, password, prefix) containing elements
405 Returns tuple (username, password, prefix) containing elements
402 found. username and password can be None (if unset), if prefix
406 found. username and password can be None (if unset), if prefix
403 is not found, url itself is returned.
407 is not found, url itself is returned.
404 """
408 """
405 base_url = str(parsed_url)
409 base_url = str(parsed_url)
406
410
407 from mercurial.httpconnection import readauthforuri
411 from mercurial.httpconnection import readauthforuri
408 _debug(ui, _("Checking for hgrc info about url %s, user %s") % (base_url, user))
412 _debug(ui, _("Checking for hgrc info about url %s, user %s") % (base_url, user))
409 res = readauthforuri(ui, base_url, user)
413 res = readauthforuri(ui, base_url, user)
410 # If it user-less version not work, let's try with added username to handle
414 # If it user-less version not work, let's try with added username to handle
411 # both config conventions
415 # both config conventions
412 if (not res) and user:
416 if (not res) and user:
413 parsed_url.user = user
417 parsed_url.user = user
414 res = readauthforuri(ui, str(parsed_url), user)
418 res = readauthforuri(ui, str(parsed_url), user)
415 parsed_url.user = None
419 parsed_url.user = None
416 if res:
420 if res:
417 group, auth_token = res
421 group, auth_token = res
418 else:
422 else:
419 auth_token = None
423 auth_token = None
420
424
421 if auth_token:
425 if auth_token:
422 username = auth_token.get('username')
426 username = auth_token.get('username')
423 password = auth_token.get('password')
427 password = auth_token.get('password')
424 prefix = auth_token.get('prefix')
428 prefix = auth_token.get('prefix')
425 else:
429 else:
426 username = user
430 username = user
427 password = None
431 password = None
428 prefix = None
432 prefix = None
429
433
430 password_url = self.password_url(base_url, prefix)
434 password_url = self.password_url(base_url, prefix)
431
435
432 _debug(ui, _("Password url: %s, user: %s, password: %s (prefix: %s)") % (
436 _debug(ui, _("Password url: %s, user: %s, password: %s (prefix: %s)") % (
433 password_url, username, '********' if password else '', prefix))
437 password_url, username, '********' if password else '', prefix))
434
438
435 return username, password, password_url
439 return username, password, password_url
436
440
437 def _note_last_reply(self, realm, authuri, user, req):
441 def _note_last_reply(self, realm, authuri, user, req):
438 """
442 """
439 Internal helper. Saves info about auth-data obtained,
443 Internal helper. Saves info about auth-data obtained,
440 preserves them in last_reply, and returns pair user, pwd
444 preserves them in last_reply, and returns pair user, pwd
441 """
445 """
442 self.last_reply = dict(realm=realm, authuri=authuri,
446 self.last_reply = dict(realm=realm, authuri=authuri,
443 user=user, req=req)
447 user=user, req=req)
444
448
445 def _after_bad_auth(self, ui, realm, authuri, req):
449 def _after_bad_auth(self, ui, realm, authuri, req):
446 """
450 """
447 If we are called again just after identical previous
451 If we are called again just after identical previous
448 request, then the previously returned auth must have been
452 request, then the previously returned auth must have been
449 wrong. So we note this to force password prompt (and avoid
453 wrong. So we note this to force password prompt (and avoid
450 reusing bad password indefinitely).
454 reusing bad password indefinitely).
451
455
452 This routine checks for this condition.
456 This routine checks for this condition.
453 """
457 """
454 if self.last_reply:
458 if self.last_reply:
455 if (self.last_reply['realm'] == realm) \
459 if (self.last_reply['realm'] == realm) \
456 and (self.last_reply['authuri'] == authuri) \
460 and (self.last_reply['authuri'] == authuri) \
457 and (self.last_reply['req'] == req):
461 and (self.last_reply['req'] == req):
458 _debug(ui, _("Working after bad authentication, cached passwords not used %s") % str(self.last_reply))
462 _debug(ui, _("Working after bad authentication, cached passwords not used %s") % str(self.last_reply))
459 return True
463 return True
460 return False
464 return False
461
465
462 @staticmethod
466 @staticmethod
463 def password_url(base_url, prefix):
467 def password_url(base_url, prefix):
464 """Calculates actual url identifying the password. Takes
468 """Calculates actual url identifying the password. Takes
465 configured prefix under consideration (so can be shorter
469 configured prefix under consideration (so can be shorter
466 than repo url)"""
470 than repo url)"""
467 if not prefix or prefix == '*':
471 if not prefix or prefix == '*':
468 return base_url
472 return base_url
469 scheme, hostpath = base_url.split('://', 1)
473 scheme, hostpath = base_url.split('://', 1)
470 p = prefix.split('://', 1)
474 p = prefix.split('://', 1)
471 if len(p) > 1:
475 if len(p) > 1:
472 prefix_host_path = p[1]
476 prefix_host_path = p[1]
473 else:
477 else:
474 prefix_host_path = prefix
478 prefix_host_path = prefix
475 password_url = scheme + '://' + prefix_host_path
479 password_url = scheme + '://' + prefix_host_path
476 return password_url
480 return password_url
477
481
478 @staticmethod
482 @staticmethod
479 def unpack_url(authuri):
483 def unpack_url(authuri):
480 """
484 """
481 Takes original url for which authentication is attempted and:
485 Takes original url for which authentication is attempted and:
482
486
483 - Strips query params from url. Used to convert urls like
487 - Strips query params from url. Used to convert urls like
484 https://repo.machine.com/repos/apps/module?pairs=0000000000000000000000000000000000000000-0000000000000000000000000000000000000000&cmd=between
488 https://repo.machine.com/repos/apps/module?pairs=0000000000000000000000000000000000000000-0000000000000000000000000000000000000000&cmd=between
485 to
489 to
486 https://repo.machine.com/repos/apps/module
490 https://repo.machine.com/repos/apps/module
487
491
488 - Extracts username and password, if present, and removes them from url
492 - Extracts username and password, if present, and removes them from url
489 (so prefix matching works properly)
493 (so prefix matching works properly)
490
494
491 Returns url, user, password
495 Returns url, user, password
492 where url is mercurial.util.url object already stripped of all those
496 where url is mercurial.util.url object already stripped of all those
493 params.
497 params.
494 """
498 """
495 # mercurial.util.url, rather handy url parser
499 # mercurial.util.url, rather handy url parser
496 parsed_url = util.url(authuri)
500 parsed_url = util.url(authuri)
497 parsed_url.query = ''
501 parsed_url.query = ''
498 parsed_url.fragment = None
502 parsed_url.fragment = None
499 # Strip arguments to get actual remote repository url.
503 # Strip arguments to get actual remote repository url.
500 # base_url = "%s://%s%s" % (parsed_url.scheme, parsed_url.netloc,
504 # base_url = "%s://%s%s" % (parsed_url.scheme, parsed_url.netloc,
501 # parsed_url.path)
505 # parsed_url.path)
502 user = parsed_url.user
506 user = parsed_url.user
503 passwd = parsed_url.passwd
507 passwd = parsed_url.passwd
504 parsed_url.user = None
508 parsed_url.user = None
505 parsed_url.passwd = None
509 parsed_url.passwd = None
506
510
507 return parsed_url, user, passwd
511 return parsed_url, user, passwd
508
512
509
513
510 ############################################################
514 ############################################################
511 # Mercurial monkey-patching
515 # Mercurial monkey-patching
512 ############################################################
516 ############################################################
513
517
514
518
515 @monkeypatch_method(passwordmgr)
519 @monkeypatch_method(passwordmgr)
516 def find_user_password(self, realm, authuri):
520 def find_user_password(self, realm, authuri):
517 """
521 """
518 keyring-based implementation of username/password query
522 keyring-based implementation of username/password query
519 for HTTP(S) connections
523 for HTTP(S) connections
520
524
521 Passwords are saved in gnome keyring, OSX/Chain or other platform
525 Passwords are saved in gnome keyring, OSX/Chain or other platform
522 specific storage and keyed by the repository url
526 specific storage and keyed by the repository url
523 """
527 """
524 # Extend object attributes
528 # Extend object attributes
525 if not hasattr(self, '_pwd_handler'):
529 if not hasattr(self, '_pwd_handler'):
526 self._pwd_handler = HTTPPasswordHandler()
530 self._pwd_handler = HTTPPasswordHandler()
527
531
528 if hasattr(self, '_http_req'):
532 if hasattr(self, '_http_req'):
529 req = self._http_req
533 req = self._http_req
530 else:
534 else:
531 req = None
535 req = None
532
536
533 return self._pwd_handler.find_auth(self, realm, authuri, req)
537 return self._pwd_handler.find_auth(self, realm, authuri, req)
534
538
535
539
536 @monkeypatch_method(urllib2.AbstractBasicAuthHandler, "http_error_auth_reqed")
540 @monkeypatch_method(urllib2.AbstractBasicAuthHandler, "http_error_auth_reqed")
537 def basic_http_error_auth_reqed(self, authreq, host, req, headers):
541 def basic_http_error_auth_reqed(self, authreq, host, req, headers):
538 """Preserves current HTTP request so it can be consulted
542 """Preserves current HTTP request so it can be consulted
539 in find_user_password above"""
543 in find_user_password above"""
540 self.passwd._http_req = req
544 self.passwd._http_req = req
541 try:
545 try:
542 return basic_http_error_auth_reqed.orig(self, authreq, host, req, headers)
546 return basic_http_error_auth_reqed.orig(self, authreq, host, req, headers)
543 finally:
547 finally:
544 self.passwd._http_req = None
548 self.passwd._http_req = None
545
549
546
550
547 @monkeypatch_method(urllib2.AbstractDigestAuthHandler, "http_error_auth_reqed")
551 @monkeypatch_method(urllib2.AbstractDigestAuthHandler, "http_error_auth_reqed")
548 def digest_http_error_auth_reqed(self, authreq, host, req, headers):
552 def digest_http_error_auth_reqed(self, authreq, host, req, headers):
549 """Preserves current HTTP request so it can be consulted
553 """Preserves current HTTP request so it can be consulted
550 in find_user_password above"""
554 in find_user_password above"""
551 self.passwd._http_req = req
555 self.passwd._http_req = req
552 try:
556 try:
553 return digest_http_error_auth_reqed.orig(self, authreq, host, req, headers)
557 return digest_http_error_auth_reqed.orig(self, authreq, host, req, headers)
554 finally:
558 finally:
555 self.passwd._http_req = None
559 self.passwd._http_req = None
556
560
557 ############################################################
561 ############################################################
558 # SMTP support
562 # SMTP support
559 ############################################################
563 ############################################################
560
564
561
565
562 def try_smtp_login(ui, smtp_obj, username, password):
566 def try_smtp_login(ui, smtp_obj, username, password):
563 """
567 """
564 Attempts smtp login on smtp_obj (smtplib.SMTP) using username and
568 Attempts smtp login on smtp_obj (smtplib.SMTP) using username and
565 password.
569 password.
566
570
567 Returns:
571 Returns:
568 - True if login succeeded
572 - True if login succeeded
569 - False if login failed due to the wrong credentials
573 - False if login failed due to the wrong credentials
570
574
571 Throws Abort exception if login failed for any other reason.
575 Throws Abort exception if login failed for any other reason.
572
576
573 Immediately returns False if password is empty
577 Immediately returns False if password is empty
574 """
578 """
575 if not password:
579 if not password:
576 return False
580 return False
577 try:
581 try:
578 ui.note(_('(authenticating to mail server as %s)\n') %
582 ui.note(_('(authenticating to mail server as %s)\n') %
579 (username))
583 (username))
580 smtp_obj.login(username, password)
584 smtp_obj.login(username, password)
581 return True
585 return True
582 except smtplib.SMTPException, inst:
586 except smtplib.SMTPException, inst:
583 if inst.smtp_code == 535:
587 if inst.smtp_code == 535:
584 ui.status(_("SMTP login failed: %s\n\n") % inst.smtp_error)
588 ui.status(_("SMTP login failed: %s\n\n") % inst.smtp_error)
585 return False
589 return False
586 else:
590 else:
587 raise util.Abort(inst)
591 raise util.Abort(inst)
588
592
589
593
590 def keyring_supported_smtp(ui, username):
594 def keyring_supported_smtp(ui, username):
591 """
595 """
592 keyring-integrated replacement for mercurial.mail._smtp
596 keyring-integrated replacement for mercurial.mail._smtp
593 Used only when configuration file contains username, but
597 Used only when configuration file contains username, but
594 does not contain the password.
598 does not contain the password.
595
599
596 Most of the routine below is copied as-is from
600 Most of the routine below is copied as-is from
597 mercurial.mail._smtp. The only changed part is
601 mercurial.mail._smtp. The only changed part is
598 marked with # >>>>> and # <<<<< markers
602 marked with # >>>>> and # <<<<< markers
599 """
603 """
600 local_hostname = ui.config('smtp', 'local_hostname')
604 local_hostname = ui.config('smtp', 'local_hostname')
601 tls = ui.config('smtp', 'tls', 'none')
605 tls = ui.config('smtp', 'tls', 'none')
602 # backward compatible: when tls = true, we use starttls.
606 # backward compatible: when tls = true, we use starttls.
603 starttls = tls == 'starttls' or util.parsebool(tls)
607 starttls = tls == 'starttls' or util.parsebool(tls)
604 smtps = tls == 'smtps'
608 smtps = tls == 'smtps'
605 if (starttls or smtps) and not util.safehasattr(socket, 'ssl'):
609 if (starttls or smtps) and not util.safehasattr(socket, 'ssl'):
606 raise util.Abort(_("can't use TLS: Python SSL support not installed"))
610 raise util.Abort(_("can't use TLS: Python SSL support not installed"))
607 mailhost = ui.config('smtp', 'host')
611 mailhost = ui.config('smtp', 'host')
608 if not mailhost:
612 if not mailhost:
609 raise util.Abort(_('smtp.host not configured - cannot send mail'))
613 raise util.Abort(_('smtp.host not configured - cannot send mail'))
610 verifycert = ui.config('smtp', 'verifycert', 'strict')
614 verifycert = ui.config('smtp', 'verifycert', 'strict')
611 if verifycert not in ['strict', 'loose']:
615 if verifycert not in ['strict', 'loose']:
612 if util.parsebool(verifycert) is not False:
616 if util.parsebool(verifycert) is not False:
613 raise util.Abort(_('invalid smtp.verifycert configuration: %s')
617 raise util.Abort(_('invalid smtp.verifycert configuration: %s')
614 % (verifycert))
618 % (verifycert))
615 verifycert = False
619 verifycert = False
616 if (starttls or smtps) and verifycert:
620 if (starttls or smtps) and verifycert:
617 sslkwargs = sslutil.sslkwargs(ui, mailhost)
621 sslkwargs = sslutil.sslkwargs(ui, mailhost)
618 else:
622 else:
619 sslkwargs = {}
623 sslkwargs = {}
620 if smtps:
624 if smtps:
621 ui.note(_('(using smtps)\n'))
625 ui.note(_('(using smtps)\n'))
622 s = SMTPS(sslkwargs, local_hostname=local_hostname)
626 s = SMTPS(sslkwargs, local_hostname=local_hostname)
623 elif starttls:
627 elif starttls:
624 s = STARTTLS(sslkwargs, local_hostname=local_hostname)
628 s = STARTTLS(sslkwargs, local_hostname=local_hostname)
625 else:
629 else:
626 s = smtplib.SMTP(local_hostname=local_hostname)
630 s = smtplib.SMTP(local_hostname=local_hostname)
627 if smtps:
631 if smtps:
628 defaultport = 465
632 defaultport = 465
629 else:
633 else:
630 defaultport = 25
634 defaultport = 25
631 mailport = util.getport(ui.config('smtp', 'port', defaultport))
635 mailport = util.getport(ui.config('smtp', 'port', defaultport))
632 ui.note(_('sending mail: smtp host %s, port %s\n') %
636 ui.note(_('sending mail: smtp host %s, port %s\n') %
633 (mailhost, mailport))
637 (mailhost, mailport))
634 s.connect(host=mailhost, port=mailport)
638 s.connect(host=mailhost, port=mailport)
635 if starttls:
639 if starttls:
636 ui.note(_('(using starttls)\n'))
640 ui.note(_('(using starttls)\n'))
637 s.ehlo()
641 s.ehlo()
638 s.starttls()
642 s.starttls()
639 s.ehlo()
643 s.ehlo()
640 if (starttls or smtps) and verifycert:
644 if (starttls or smtps) and verifycert:
641 ui.note(_('(verifying remote certificate)\n'))
645 ui.note(_('(verifying remote certificate)\n'))
642 sslutil.validator(ui, mailhost)(s.sock, verifycert == 'strict')
646 sslutil.validator(ui, mailhost)(s.sock, verifycert == 'strict')
643
647
644 # >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
648 # >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
645 stored = password = password_store.get_smtp_password(
649 stored = password = password_store.get_smtp_password(
646 mailhost, mailport, username)
650 mailhost, mailport, username)
647 # No need to check whether password was found as try_smtp_login
651 # No need to check whether password was found as try_smtp_login
648 # just returns False if it is absent.
652 # just returns False if it is absent.
649 while not try_smtp_login(ui, s, username, password):
653 while not try_smtp_login(ui, s, username, password):
650 password = ui.getpass(_("Password for %s on %s:%d: ") % (username, mailhost, mailport))
654 password = ui.getpass(_("Password for %s on %s:%d: ") % (username, mailhost, mailport))
651
655
652 if stored != password:
656 if stored != password:
653 password_store.set_smtp_password(
657 password_store.set_smtp_password(
654 mailhost, mailport, username, password)
658 mailhost, mailport, username, password)
655 # <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
659 # <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
656
660
657 def send(sender, recipients, msg):
661 def send(sender, recipients, msg):
658 try:
662 try:
659 return s.sendmail(sender, recipients, msg)
663 return s.sendmail(sender, recipients, msg)
660 except smtplib.SMTPRecipientsRefused, inst:
664 except smtplib.SMTPRecipientsRefused, inst:
661 recipients = [r[1] for r in inst.recipients.values()]
665 recipients = [r[1] for r in inst.recipients.values()]
662 raise util.Abort('\n' + '\n'.join(recipients))
666 raise util.Abort('\n' + '\n'.join(recipients))
663 except smtplib.SMTPException, inst:
667 except smtplib.SMTPException, inst:
664 raise util.Abort(inst)
668 raise util.Abort(inst)
665
669
666 return send
670 return send
667
671
668 ############################################################
672 ############################################################
669 # SMTP monkeypatching
673 # SMTP monkeypatching
670 ############################################################
674 ############################################################
671
675
672
676
673 @monkeypatch_method(mail)
677 @monkeypatch_method(mail)
674 def _smtp(ui):
678 def _smtp(ui):
675 """
679 """
676 build an smtp connection and return a function to send email
680 build an smtp connection and return a function to send email
677
681
678 This is the monkeypatched version of _smtp(ui) function from
682 This is the monkeypatched version of _smtp(ui) function from
679 mercurial/mail.py. It calls the original unless username
683 mercurial/mail.py. It calls the original unless username
680 without password is given in the configuration.
684 without password is given in the configuration.
681 """
685 """
682 username = ui.config('smtp', 'username')
686 username = ui.config('smtp', 'username')
683 password = ui.config('smtp', 'password')
687 password = ui.config('smtp', 'password')
684
688
685 if username and not password:
689 if username and not password:
686 return keyring_supported_smtp(ui, username)
690 return keyring_supported_smtp(ui, username)
687 else:
691 else:
688 return _smtp.orig(ui)
692 return _smtp.orig(ui)
689
693
690
694
691 ############################################################
695 ############################################################
692 # Custom commands
696 # Custom commands
693 ############################################################
697 ############################################################
694
698
695 cmdtable = {}
699 cmdtable = {}
696 command = meu.command(cmdtable)
700 command = meu.command(cmdtable)
697
701
698
702
699 @command('keyring_check',
703 @command('keyring_check',
700 [],
704 [],
701 _("keyring_check [PATH]"))
705 _("keyring_check [PATH]"))
702 def cmd_keyring_check(ui, repo, *path_args, **opts): # pylint: disable=unused-argument
706 def cmd_keyring_check(ui, repo, *path_args, **opts): # pylint: disable=unused-argument
703 """
707 """
704 Prints basic info (whether password is currently saved, and how is
708 Prints basic info (whether password is currently saved, and how is
705 it identified) for given path or for all defined repo paths which are HTTP.
709 it identified) for given path or for all defined repo paths which are HTTP.
706 """
710 """
707 defined_paths = [(name, url)
711 defined_paths = [(name, url)
708 for name, url in ui.configitems('paths')]
712 for name, url in ui.configitems('paths')]
709 if path_args:
713 if path_args:
710 # Maybe parameter is an alias
714 # Maybe parameter is an alias
711 defined_paths_dic = dict(defined_paths)
715 defined_paths_dic = dict(defined_paths)
712 paths = [(path_arg, defined_paths_dic.get(path_arg, path_arg))
716 paths = [(path_arg, defined_paths_dic.get(path_arg, path_arg))
713 for path_arg in path_args]
717 for path_arg in path_args]
714 else:
718 else:
715 paths = [(name, url) for name, url in defined_paths]
719 paths = [(name, url) for name, url in defined_paths]
716
720
717 if not paths:
721 if not paths:
718 ui.status(_("keyring_check: no paths defined\n"))
722 ui.status(_("keyring_check: no paths defined\n"))
719
723
720 handler = HTTPPasswordHandler()
724 handler = HTTPPasswordHandler()
721
725
722 ui.status(_("keyring password save status:\n"))
726 ui.status(_("keyring password save status:\n"))
723 for name, url in paths:
727 for name, url in paths:
724 if not is_http_path(url):
728 if not is_http_path(url):
725 if path_args:
729 if path_args:
726 ui.status(_(" %s: non-http path (%s)\n") % (name, url))
730 ui.status(_(" %s: non-http path (%s)\n") % (name, url))
727 continue
731 continue
728 user, pwd, source, final_url = handler.get_credentials(
732 user, pwd, source, final_url = handler.get_credentials(
729 passwordmgr(ui), name, url)
733 passwordmgr(ui), name, url)
730 if pwd:
734 if pwd:
731 ui.status(_(" %s: password available, source: %s, bound to user %s, url %s\n") % (
735 ui.status(_(" %s: password available, source: %s, bound to user %s, url %s\n") % (
732 name, source, user, final_url))
736 name, source, user, final_url))
733 elif user:
737 elif user:
734 ui.status(_(" %s: password not available, once entered, will be bound to user %s, url %s\n") % (
738 ui.status(_(" %s: password not available, once entered, will be bound to user %s, url %s\n") % (
735 name, user, final_url))
739 name, user, final_url))
736 else:
740 else:
737 ui.status(_(" %s: password not available, user unknown, url %s\n") % (
741 ui.status(_(" %s: password not available, user unknown, url %s\n") % (
738 name, final_url))
742 name, final_url))
739
743
740
744
741 @command('keyring_clear',
745 @command('keyring_clear',
742 [],
746 [],
743 _('hg keyring_clear PATH'))
747 _('hg keyring_clear PATH'))
744 def cmd_keyring_clear(ui, repo, path, **opts): # pylint: disable=unused-argument
748 def cmd_keyring_clear(ui, repo, path, **opts): # pylint: disable=unused-argument
745 """
749 """
746 Drops password bound to given path (if any is saved).
750 Drops password bound to given path (if any is saved).
747 """
751 """
748 path_url = path
752 path_url = path
749 for name, url in ui.configitems('paths'):
753 for name, url in ui.configitems('paths'):
750 if name == path:
754 if name == path:
751 path_url = url
755 path_url = url
752 break
756 break
753 if not is_http_path(path_url):
757 if not is_http_path(path_url):
754 ui.warn(_("%s is not a http path (%s)") % (path, path_url))
758 ui.warn(_("%s is not a http path (%s)") % (path, path_url))
755 return
759 return
756
760
757 handler = HTTPPasswordHandler()
761 handler = HTTPPasswordHandler()
758
762
759 user, pwd, source, final_url = handler.get_credentials(
763 user, pwd, source, final_url = handler.get_credentials(
760 passwordmgr(ui), path, path_url)
764 passwordmgr(ui), path, path_url)
761 if not user:
765 if not user:
762 ui.status(_("Username not configured for url %s\n") % final_url)
766 ui.status(_("Username not configured for url %s\n") % final_url)
763 return
767 return
764 if not pwd:
768 if not pwd:
765 ui.status(_("No password is saved for user %s, url %s\n") % (
769 ui.status(_("No password is saved for user %s, url %s\n") % (
766 user, final_url))
770 user, final_url))
767 return
771 return
768
772
769 if source != handler.SRC_KEYRING:
773 if source != handler.SRC_KEYRING:
770 ui.status(_("Password for user %s, url %s is saved in %s, not in keyring\n") % (
774 ui.status(_("Password for user %s, url %s is saved in %s, not in keyring\n") % (
771 user, final_url, source))
775 user, final_url, source))
772
776
773 password_store.clear_http_password(final_url, user)
777 password_store.clear_http_password(final_url, user)
774 ui.status(_("Password removed for user %s, url %s\n") % (
778 ui.status(_("Password removed for user %s, url %s\n") % (
775 user, final_url))
779 user, final_url))
776
780
777
781
778 buglink = 'https://bitbucket.org/Mekk/mercurial_keyring/issues'
782 buglink = 'https://bitbucket.org/Mekk/mercurial_keyring/issues'
General Comments 0
You need to be logged in to leave comments. Login now