##// END OF EJS Templates
Slightly polishing the prefix patch:...
Marcin Kasperski -
r46:e0c8034c default
parent child Browse files
Show More
@@ -1,174 +1,179 b''
1 .. -*- mode: rst -*-
1 .. -*- mode: rst -*-
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
7 ``mercurial_keyring`` is a Mercurial_ extension used to securely save
8 HTTP and SMTP authentication passwords in password databases (Gnome
8 HTTP and SMTP authentication passwords in password databases (Gnome
9 Keyring, KDE KWallet, OSXKeyChain, specific solutions for Win32 and
9 Keyring, KDE KWallet, OSXKeyChain, specific solutions for Win32 and
10 command line). This extension uses and wraps services of the keyring_
10 command line). This extension uses and wraps services of the keyring_
11 library.
11 library.
12
12
13 .. _keyring: http://pypi.python.org/pypi/keyring
13 .. _keyring: http://pypi.python.org/pypi/keyring
14 .. _Mercurial: http://mercurial.selenic.com
14 .. _Mercurial: http://mercurial.selenic.com
15
15
16 How does it work
16 How does it work
17 ================
17 ================
18
18
19 The extension prompts for the password on the first pull/push (in case
19 The extension prompts for the password on the first pull/push (in case
20 of HTTP) or first email (in case of SMTP), just like it is done by
20 of HTTP) or first email (in case of SMTP), just like it is done by
21 default, but saves the password. On successive runs it checks for the
21 default, but saves the password. On successive runs it checks for the
22 username in ``.hg/hgrc``, then for suitable password in the password
22 username in ``.hg/hgrc``, then for suitable password in the password
23 database, and uses those credentials (if found).
23 database, and uses those credentials (if found).
24
24
25 In case password turns out to be incorrect (either because it was
25 In case password turns out to be incorrect (either because it was
26 invalid, or because it was changed on the server) or missing it just
26 invalid, or because it was changed on the server) or missing it just
27 prompts the user again.
27 prompts the user again.
28
28
29 Passwords are identified by the combination of username and remote
29 Passwords are identified by the combination of username and remote
30 repository url (for HTTP) or username and smtp server address (for
30 address, so they can be reused between repositories if they access the
31 SMTP), so they can be reused between repositories if they access
31 same remote repository (or the same SMTP server).
32 the same remote repository.
33
32
34 Installation
33 Installation
35 ============
34 ============
36
35
37 Prerequisites
36 Prerequisites
38 -------------
37 -------------
39
38
40 Install the keyring_ library:
39 Install the keyring_ library:
41
40
42 ::
41 ::
43
42
44 easy_install keyring
43 easy_install keyring
45
44
46 (or ``pip keyring``). On Debian "Sid" the library can be also
45 (or ``pip keyring``). On Debian "Sid" the library can be also
47 installed from the official archive (packages ``python-keyring``
46 installed from the official archive (packages ``python-keyring``
48 and either ``python-keyring-gnome`` or ``python-keyring-kwallet``).
47 and either ``python-keyring-gnome`` or ``python-keyring-kwallet``).
49
48
50 Extension installation
49 Extension installation
51 ----------------------
50 ----------------------
52
51
53 There are two possible ways of installing the extension: using PyPi package,
52 There are two possible ways of installing the extension: using PyPi package,
54 or using individual file.
53 or using individual file.
55
54
56 To install as a package use ``easy_install``:
55 To install as a package use ``easy_install``:
57
56
58 ::
57 ::
59
58
60 easy_install mercurial_keyring
59 easy_install mercurial_keyring
61
60
62 and then enable it in ``~/.hgrc`` (or ``/etc/mercurial/hgrc``) using:
61 and then enable it in ``~/.hgrc`` (or ``/etc/mercurial/hgrc``) using:
63
62
64 ::
63 ::
65
64
66 [extensions]
65 [extensions]
67 mercurial_keyring =
66 mercurial_keyring =
68
67
69 To install using individual file, download the
68 To install using individual file, download the
70 `mercurial_keyring.py`_ file, save it anywhere you like, and
69 `mercurial_keyring.py`_ file, save it anywhere you like, and
71 put the following in ``~/.hgrc`` (or ``/etc/mercurial/hgrc``):
70 put the following in ``~/.hgrc`` (or ``/etc/mercurial/hgrc``):
72
71
73 ::
72 ::
74
73
75 [extensions]
74 [extensions]
76 hgext.mercurial_keyring = /path/to/mercurial_keyring.py
75 hgext.mercurial_keyring = /path/to/mercurial_keyring.py
77
76
78 .. _the code:
77 .. _the code:
79 .. _mercurial_keyring.py: http://bitbucket.org/Mekk/mercurial_keyring/src/tip/mercurial_keyring.py
78 .. _mercurial_keyring.py: http://bitbucket.org/Mekk/mercurial_keyring/src/tip/mercurial_keyring.py
80
79
81 Password backend configuration
80 Password backend configuration
82 ==============================
81 ==============================
83
82
84 The library should usually pick the most appropriate password backend
83 The library should usually pick the most appropriate password backend
85 without configuration. Still, if necessary, it can be configured using
84 without configuration. Still, if necessary, it can be configured using
86 ``~/keyringrc.cfg`` file (``keyringrc.cfg`` in the home directory of
85 ``~/keyringrc.cfg`` file (``keyringrc.cfg`` in the home directory of
87 the current user). Refer to keyring_ docs for more details.
86 the current user). Refer to keyring_ docs for more details.
88
87
89 *I considered handling similar options in hgrc, but decided that
88 *I considered handling similar options in hgrc, but decided that
90 single user may use more than one keyring-based script. Still, I am
89 single user may use more than one keyring-based script. Still, I am
91 open to suggestions.*
90 open to suggestions.*
92
91
93 Repository configuration (HTTP)
92 Repository configuration (HTTP)
94 ===============================
93 ===============================
95
94
96 Edit repository-local ``.hg/hgrc`` and save there the remote
95 Edit repository-local ``.hg/hgrc`` and save there the remote
97 repository path and the username, but do not save the password. For
96 repository path and the username, but do not save the password. For
98 example:
97 example:
99
98
100 ::
99 ::
101
100
102 [paths]
101 [paths]
103 myremote = https://my.server.com/hgrepo/someproject
102 myremote = https://my.server.com/hgrepo/someproject
104
103
105 [auth]
104 [auth]
106 myremote.schemes = http https
105 myremote.schemes = http https
107 myremote.prefix = my.server.com/hgrepo
106 myremote.prefix = my.server.com/hgrepo
108 myremote.username = mekk
107 myremote.username = mekk
109
108
110 Simpler form with url-embedded name can also be used:
109 Simpler form with url-embedded name can also be used:
111
110
112 ::
111 ::
113
112
114 [paths]
113 [paths]
115 bitbucket = https://User@bitbucket.org/User/project_name/
114 bitbucket = https://User@bitbucket.org/User/project_name/
116
115
116 If prefix is specified, it is used to identify the password (so all
117 repositories with the same prefix and the same username will share the
118 same password). Otherwise full repository URL is used for this
119 purpose.
120
117 Note: if both username and password are given in ``.hg/hgrc``,
121 Note: if both username and password are given in ``.hg/hgrc``,
118 extension will use them without using the password database. If
122 extension will use them without using the password database. If
119 username is not given, extension will prompt for credentials every
123 username is not given, extension will prompt for credentials every
120 time, also without saving the password.
124 time, also without saving the password.
121
125
122 Repository configuration (SMTP)
126 Repository configuration (SMTP)
123 ===============================
127 ===============================
124
128
125 Edit either repository-local ``.hg/hgrc``, or ``~/.hgrc`` and set
129 Edit either repository-local ``.hg/hgrc``, or ``~/.hgrc`` and set
126 there all standard email and smtp properties, including SMTP
130 there all standard email and smtp properties, including SMTP
127 username, but without SMTP password. For example:
131 username, but without SMTP password. For example:
128
132
129 ::
133 ::
130
134
131 [email]
135 [email]
132 method = smtp
136 method = smtp
133 from = Joe Doe <Joe.Doe@remote.com>
137 from = Joe Doe <Joe.Doe@remote.com>
134
138
135 [smtp]
139 [smtp]
136 host = smtp.gmail.com
140 host = smtp.gmail.com
137 port = 587
141 port = 587
138 username = JoeDoe@gmail.com
142 username = JoeDoe@gmail.com
139 tls = true
143 tls = true
140
144
141 Just as in case of HTTP, you *must* set username, but *must not* set
145 Just as in case of HTTP, you *must* set username, but *must not* set
142 password here to use the extension, in other cases it will revert to
146 password here to use the extension, in other cases it will revert to
143 the default behavior.
147 the default behavior.
144
148
145 Usage
149 Usage
146 =====
150 =====
147
151
148 Configure the repository as above, then just ``hg pull``, ``hg push``,
152 Configure the repository as above, then just ``hg pull``, ``hg push``,
149 etc. You should be asked for the password only once (per every
153 etc. You should be asked for the password only once (per every
150 username+remote_repository_url combination).
154 username and remote repository prefix or url combination).
155
151
156
152 Similarly, for email, configure as above and just ``hg email``.
157 Similarly, for email, configure as above and just ``hg email``.
153 Again, you will be asked for the password once (per every
158 Again, you will be asked for the password once (per every username and
154 username+email_server_name+email_server_port).
159 email server address combination).
155
160
156 Implementation details
161 Implementation details
157 ======================
162 ======================
158
163
159 The extension is monkey-patching the mercurial ``passwordmgr`` class
164 The extension is monkey-patching the mercurial ``passwordmgr`` class
160 to replace the find_user_password method. Detailed order of operations
165 to replace the find_user_password method. Detailed order of operations
161 is described in the comments inside `the code`_.
166 is described in the comments inside `the code`_.
162
167
163 Development
168 Development
164 ===========
169 ===========
165
170
166 Development is tracked on BitBucket, see
171 Development is tracked on BitBucket, see
167 http://bitbucket.org/Mekk/mercurial_keyring/
172 http://bitbucket.org/Mekk/mercurial_keyring/
168
173
169
174
170 Additional notes
175 Additional notes
171 ================
176 ================
172
177
173 Information about this extension is also available
178 Information about this extension is also available
174 on Mercurial Wiki: http://mercurial.selenic.com/wiki/KeyringExtension
179 on Mercurial Wiki: http://mercurial.selenic.com/wiki/KeyringExtension
@@ -1,366 +1,369 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 #
2 #
3 # mercurial_keyring: save passwords in password database
3 # mercurial_keyring: save passwords in password database
4 #
4 #
5 # Copyright 2009 Marcin Kasperski <Marcin.Kasperski@mekk.waw.pl>
5 # Copyright 2009 Marcin Kasperski <Marcin.Kasperski@mekk.waw.pl>
6 #
6 #
7 # This software may be used and distributed according to the terms
7 # This software may be used and distributed according to the terms
8 # of the GNU General Public License, incorporated herein by reference.
8 # of the GNU General Public License, incorporated herein by reference.
9 #
9 #
10 # See README.txt for more details.
10 # See README.txt for more details.
11
11
12 from mercurial import hg, repo, util
12 from mercurial import hg, repo, util
13 from mercurial.i18n import _
13 from mercurial.i18n import _
14 try:
14 try:
15 from mercurial.url import passwordmgr
15 from mercurial.url import passwordmgr
16 except:
16 except:
17 from mercurial.httprepo import passwordmgr
17 from mercurial.httprepo import passwordmgr
18 from mercurial.httprepo import httprepository
18 from mercurial.httprepo import httprepository
19 from mercurial import mail
19 from mercurial import mail
20
20
21 import keyring
21 import keyring
22 from urlparse import urlparse
22 from urlparse import urlparse
23 import urllib2
23 import urllib2
24 import smtplib, socket
24 import smtplib, socket
25 import os
25
26
26 KEYRING_SERVICE = "Mercurial"
27 KEYRING_SERVICE = "Mercurial"
27
28
28 ############################################################
29 ############################################################
29
30
30 def monkeypatch_method(cls):
31 def monkeypatch_method(cls):
31 def decorator(func):
32 def decorator(func):
32 setattr(cls, func.__name__, func)
33 setattr(cls, func.__name__, func)
33 return func
34 return func
34 return decorator
35 return decorator
35
36
36 ############################################################
37 ############################################################
37
38
38 class PasswordStore(object):
39 class PasswordStore(object):
39 """
40 """
40 Helper object handling keyring usage (password save&restore,
41 Helper object handling keyring usage (password save&restore,
41 the way passwords are keyed in the keyring).
42 the way passwords are keyed in the keyring).
42 """
43 """
43 def __init__(self):
44 def __init__(self):
44 self.cache = dict()
45 self.cache = dict()
45 def get_http_password(self, url, username):
46 def get_http_password(self, url, username):
46 return keyring.get_password(KEYRING_SERVICE,
47 return keyring.get_password(KEYRING_SERVICE,
47 self._format_http_key(url, username))
48 self._format_http_key(url, username))
48 def set_http_password(self, url, username, password):
49 def set_http_password(self, url, username, password):
49 keyring.set_password(KEYRING_SERVICE,
50 keyring.set_password(KEYRING_SERVICE,
50 self._format_http_key(url, username),
51 self._format_http_key(url, username),
51 password)
52 password)
52 def clear_http_password(self, url, username):
53 def clear_http_password(self, url, username):
53 self.set_http_password(url, username, "")
54 self.set_http_password(url, username, "")
54 def _format_http_key(self, url, username):
55 def _format_http_key(self, url, username):
55 return "%s@@%s" % (username, url)
56 return "%s@@%s" % (username, url)
56 def get_smtp_password(self, machine, port, username):
57 def get_smtp_password(self, machine, port, username):
57 return keyring.get_password(
58 return keyring.get_password(
58 KEYRING_SERVICE,
59 KEYRING_SERVICE,
59 self._format_smtp_key(machine, port, username))
60 self._format_smtp_key(machine, port, username))
60 def set_smtp_password(self, machine, port, username, password):
61 def set_smtp_password(self, machine, port, username, password):
61 keyring.set_password(
62 keyring.set_password(
62 KEYRING_SERVICE,
63 KEYRING_SERVICE,
63 self._format_smtp_key(machine, port, username),
64 self._format_smtp_key(machine, port, username),
64 password)
65 password)
65 def clear_smtp_password(self, machine, port, username):
66 def clear_smtp_password(self, machine, port, username):
66 self.set_smtp_password(url, username, "")
67 self.set_smtp_password(url, username, "")
67 def _format_smtp_key(self, machine, port, username):
68 def _format_smtp_key(self, machine, port, username):
68 return "%s@@%s:%s" % (username, machine, str(port))
69 return "%s@@%s:%s" % (username, machine, str(port))
69
70
70 password_store = PasswordStore()
71 password_store = PasswordStore()
71
72
72 ############################################################
73 ############################################################
73
74
74 class HTTPPasswordHandler(object):
75 class HTTPPasswordHandler(object):
75 """
76 """
76 Actual implementation of password handling (user prompting,
77 Actual implementation of password handling (user prompting,
77 configuration file searching, keyring save&restore).
78 configuration file searching, keyring save&restore).
78
79
79 Object of this class is bound as passwordmgr attribute.
80 Object of this class is bound as passwordmgr attribute.
80 """
81 """
81 def __init__(self):
82 def __init__(self):
82 self.pwd_cache = {}
83 self.pwd_cache = {}
83 self.last_reply = None
84 self.last_reply = None
84
85
85 def find_auth(self, pwmgr, realm, authuri):
86 def find_auth(self, pwmgr, realm, authuri):
86 """
87 """
87 Actual implementation of find_user_password - different
88 Actual implementation of find_user_password - different
88 ways of obtaining the username and password.
89 ways of obtaining the username and password.
89 """
90 """
90 ui = pwmgr.ui
91 ui = pwmgr.ui
91
92
92 # If we are called again just after identical previous
93 # If we are called again just after identical previous
93 # request, then the previously returned auth must have been
94 # request, then the previously returned auth must have been
94 # wrong. So we note this to force password prompt (and avoid
95 # wrong. So we note this to force password prompt (and avoid
95 # reusing bad password indifinitely).
96 # reusing bad password indifinitely).
96 after_bad_auth = (self.last_reply \
97 after_bad_auth = (self.last_reply \
97 and (self.last_reply['realm'] == realm) \
98 and (self.last_reply['realm'] == realm) \
98 and (self.last_reply['authuri'] == authuri))
99 and (self.last_reply['authuri'] == authuri))
99
100
100 # Strip arguments to get actual remote repository url.
101 # Strip arguments to get actual remote repository url.
101 base_url = self.canonical_url(authuri)
102 base_url = self.canonical_url(authuri)
102
103
103 # Extracting possible username (or password)
104 # Extracting possible username (or password)
104 # stored directly in repository url
105 # stored directly in repository url
105 user, pwd = urllib2.HTTPPasswordMgrWithDefaultRealm.find_user_password(
106 user, pwd = urllib2.HTTPPasswordMgrWithDefaultRealm.find_user_password(
106 pwmgr, realm, authuri)
107 pwmgr, realm, authuri)
107 if user and pwd:
108 if user and pwd:
108 self._debug_reply(ui, _("Auth data found in repository URL"),
109 self._debug_reply(ui, _("Auth data found in repository URL"),
109 base_url, user, pwd)
110 base_url, user, pwd)
110 self.last_reply = dict(realm=realm,authuri=authuri,user=user)
111 self.last_reply = dict(realm=realm,authuri=authuri,user=user)
111 return user, pwd
112 return user, pwd
112
113
113 # Loading username and maybe password and prefix URL from [auth] in .hg/hgrc
114 # Loading .hg/hgrc [auth] section contents. If prefix is given,
114 nuser, pwd, prefix_url = self.load_hgrc_auth(ui, base_url)
115 # it will be used as a key to lookup password in the keyring.
116 auth_user, pwd, prefix_url = self.load_hgrc_auth(ui, base_url)
115 if prefix_url:
117 if prefix_url:
116 keyring_url = prefix_url
118 keyring_url = prefix_url
117 else:
119 else:
118 keyring_url = base_url
120 keyring_url = base_url
119 ui.debug("keyring URL: %s\n" % keyring_url)
121 ui.debug("keyring URL: %s\n" % keyring_url)
120
122
121 # Checking the memory cache (there may be many http calls per command)
123 # Checking the memory cache (there may be many http calls per command)
122 cache_key = (realm, keyring_url)
124 cache_key = (realm, keyring_url)
123 if not after_bad_auth:
125 if not after_bad_auth:
124 cached_auth = self.pwd_cache.get(cache_key)
126 cached_auth = self.pwd_cache.get(cache_key)
125 if cached_auth:
127 if cached_auth:
126 user, pwd = cached_auth
128 user, pwd = cached_auth
127 self._debug_reply(ui, _("Cached auth data found"),
129 self._debug_reply(ui, _("Cached auth data found"),
128 base_url, user, pwd)
130 base_url, user, pwd)
129 self.last_reply = dict(realm=realm,authuri=authuri,user=user)
131 self.last_reply = dict(realm=realm,authuri=authuri,user=user)
130 return user, pwd
132 return user, pwd
131
133
132 if nuser:
134 if auth_user:
133 if user:
135 if user:
134 raise util.Abort(_('mercurial_keyring: username for %s specified both in repository path (%s) and in .hg/hgrc/[auth] (%s). Please, leave only one of those' % (base_url, user, nuser)))
136 raise util.Abort(_('mercurial_keyring: username for %s specified both in repository path (%s) and in .hg/hgrc/[auth] (%s). Please, leave only one of those' % (base_url, user, auth_user)))
135 user = nuser
137 user = auth_user
136 if pwd:
138 if pwd:
137 self.pwd_cache[cache_key] = user, pwd
139 self.pwd_cache[cache_key] = user, pwd
138 self._debug_reply(ui, _("Auth data set in .hg/hgrc"),
140 self._debug_reply(ui, _("Auth data set in .hg/hgrc"),
139 base_url, user, pwd)
141 base_url, user, pwd)
140 self.last_reply = dict(realm=realm,authuri=authuri,user=user)
142 self.last_reply = dict(realm=realm,authuri=authuri,user=user)
141 return user, pwd
143 return user, pwd
142 else:
144 else:
143 ui.debug(_("Username found in .hg/hgrc: %s\n" % user))
145 ui.debug(_("Username found in .hg/hgrc: %s\n" % user))
144
146
145 # Loading password from keyring.
147 # Loading password from keyring.
146 # Only if username is known (so we know the key) and we are
148 # Only if username is known (so we know the key) and we are
147 # not after failure (so we don't reuse the bad password).
149 # not after failure (so we don't reuse the bad password).
148 if user and not after_bad_auth:
150 if user and not after_bad_auth:
149 pwd = password_store.get_http_password(keyring_url, user)
151 pwd = password_store.get_http_password(keyring_url, user)
150 if pwd:
152 if pwd:
151 self.pwd_cache[cache_key] = user, pwd
153 self.pwd_cache[cache_key] = user, pwd
152 self._debug_reply(ui, _("Keyring password found"),
154 self._debug_reply(ui, _("Keyring password found"),
153 base_url, user, pwd)
155 base_url, user, pwd)
154 self.last_reply = dict(realm=realm,authuri=authuri,user=user)
156 self.last_reply = dict(realm=realm,authuri=authuri,user=user)
155 return user, pwd
157 return user, pwd
156
158
157 # Is the username permanently set?
159 # Is the username permanently set?
158 fixed_user = (user and True or False)
160 fixed_user = (user and True or False)
159
161
160 # Last resort: interactive prompt
162 # Last resort: interactive prompt
161 if not ui.interactive():
163 if not ui.interactive():
162 raise util.Abort(_('mercurial_keyring: http authorization required but program used in non-interactive mode'))
164 raise util.Abort(_('mercurial_keyring: http authorization required but program used in non-interactive mode'))
163 ui.write(_("http authorization required\n"))
165 ui.write(_("http authorization required\n"))
164 ui.status(_("realm: %s\n") % realm)
166 ui.status(_("realm: %s\n") % realm)
165 if fixed_user:
167 if fixed_user:
166 ui.write(_("user: %s (fixed in .hg/hgrc)\n" % user))
168 ui.write(_("user: %s (fixed in .hg/hgrc)\n" % user))
167 else:
169 else:
168 user = ui.prompt(_("user:"), default=None)
170 user = ui.prompt(_("user:"), default=None)
169 pwd = ui.getpass(_("password: "))
171 pwd = ui.getpass(_("password: "))
170
172
171 if fixed_user:
173 if fixed_user:
172 # Saving password to the keyring.
174 # Saving password to the keyring.
173 # It is done only if username is permanently set.
175 # It is done only if username is permanently set.
174 # Otherwise we won't be able to find the password so it
176 # Otherwise we won't be able to find the password so it
175 # does not make much sense to preserve it
177 # does not make much sense to preserve it
176 ui.debug("Saving password for %s to keyring\n" % user)
178 ui.debug("Saving password for %s to keyring\n" % user)
177 password_store.set_http_password(keyring_url, user, pwd)
179 password_store.set_http_password(keyring_url, user, pwd)
178
180
179 # Saving password to the memory cache
181 # Saving password to the memory cache
180 self.pwd_cache[cache_key] = user, pwd
182 self.pwd_cache[cache_key] = user, pwd
181
183
182 self._debug_reply(ui, _("Manually entered password"),
184 self._debug_reply(ui, _("Manually entered password"),
183 base_url, user, pwd)
185 base_url, user, pwd)
184 self.last_reply = dict(realm=realm,authuri=authuri,user=user)
186 self.last_reply = dict(realm=realm,authuri=authuri,user=user)
185 return user, pwd
187 return user, pwd
186
188
187 def load_hgrc_auth(self, ui, base_url):
189 def load_hgrc_auth(self, ui, base_url):
188 """
190 """
189 Loading username and possibly password from [auth] in local
191 Loading [auth] section contents from local .hgrc
190 repo .hgrc
192
193 Returns (username, password, prefix) tuple (every
194 element can be None)
191 """
195 """
192 # Theoretically 3 lines below should do:
196 # Theoretically 3 lines below should do:
193
197
194 #auth_token = self.readauthtoken(base_url)
198 #auth_token = self.readauthtoken(base_url)
195 #if auth_token:
199 #if auth_token:
196 # user, pwd = auth.get('username'), auth.get('password')
200 # user, pwd = auth.get('username'), auth.get('password')
197
201
198 # Unfortunately they do not work, readauthtoken always return
202 # Unfortunately they do not work, readauthtoken always return
199 # None. Why? Because ui (self.ui of passwordmgr) describes the
203 # None. Why? Because ui (self.ui of passwordmgr) describes the
200 # *remote* repository, so does *not* contain any option from
204 # *remote* repository, so does *not* contain any option from
201 # local .hg/hgrc.
205 # local .hg/hgrc.
202
206
203 # Workaround: we recreate the repository object
207 # Workaround: we recreate the repository object
204 repo_root = ui.config("bundle", "mainreporoot")
208 repo_root = ui.config("bundle", "mainreporoot")
205
209
206 from mercurial.ui import ui as _ui
210 from mercurial.ui import ui as _ui
207 import os
208 local_ui = _ui(ui)
211 local_ui = _ui(ui)
209 if repo_root:
212 if repo_root:
210 local_ui.readconfig(os.path.join(repo_root, ".hg", "hgrc"))
213 local_ui.readconfig(os.path.join(repo_root, ".hg", "hgrc"))
211 local_passwordmgr = passwordmgr(local_ui)
214 local_passwordmgr = passwordmgr(local_ui)
212 auth_token = local_passwordmgr.readauthtoken(base_url)
215 auth_token = local_passwordmgr.readauthtoken(base_url)
213 if auth_token:
216 if auth_token:
214 username = auth_token.get('username')
217 username = auth_token.get('username')
215 password = auth_token.get('password')
218 password = auth_token.get('password')
216 prefix = auth_token.get('prefix')
219 prefix = auth_token.get('prefix')
217 shortest_url = self.shortest_url(base_url, prefix)
220 shortest_url = self.shortest_url(base_url, prefix)
218 return username, password, shortest_url
221 return username, password, shortest_url
219
222
220 return None, None
223 return None, None, None
221
224
222 def shortest_url(self, base_url, prefix):
225 def shortest_url(self, base_url, prefix):
223 if not prefix or prefix == '*':
226 if not prefix or prefix == '*':
224 return base_url
227 return base_url
225 scheme, hostpath = base_url.split('://', 1)
228 scheme, hostpath = base_url.split('://', 1)
226 p = prefix.split('://', 1)
229 p = prefix.split('://', 1)
227 if len(p) > 1:
230 if len(p) > 1:
228 prefix_host_path = p[1]
231 prefix_host_path = p[1]
229 else:
232 else:
230 prefix_host_path = prefix
233 prefix_host_path = prefix
231 shortest_url = scheme + '://' + prefix_host_path
234 shortest_url = scheme + '://' + prefix_host_path
232 return shortest_url
235 return shortest_url
233
236
234 def canonical_url(self, authuri):
237 def canonical_url(self, authuri):
235 """
238 """
236 Strips query params from url. Used to convert urls like
239 Strips query params from url. Used to convert urls like
237 https://repo.machine.com/repos/apps/module?pairs=0000000000000000000000000000000000000000-0000000000000000000000000000000000000000&cmd=between
240 https://repo.machine.com/repos/apps/module?pairs=0000000000000000000000000000000000000000-0000000000000000000000000000000000000000&cmd=between
238 to
241 to
239 https://repo.machine.com/repos/apps/module
242 https://repo.machine.com/repos/apps/module
240 """
243 """
241 parsed_url = urlparse(authuri)
244 parsed_url = urlparse(authuri)
242 return "%s://%s%s" % (parsed_url.scheme, parsed_url.netloc,
245 return "%s://%s%s" % (parsed_url.scheme, parsed_url.netloc,
243 parsed_url.path)
246 parsed_url.path)
244
247
245 def _debug_reply(self, ui, msg, url, user, pwd):
248 def _debug_reply(self, ui, msg, url, user, pwd):
246 ui.debug("%s. Url: %s, user: %s, passwd: %s\n" % (
249 ui.debug("%s. Url: %s, user: %s, passwd: %s\n" % (
247 msg, url, user, pwd and '*' * len(pwd) or 'not set'))
250 msg, url, user, pwd and '*' * len(pwd) or 'not set'))
248
251
249 ############################################################
252 ############################################################
250
253
251 @monkeypatch_method(passwordmgr)
254 @monkeypatch_method(passwordmgr)
252 def find_user_password(self, realm, authuri):
255 def find_user_password(self, realm, authuri):
253 """
256 """
254 keyring-based implementation of username/password query
257 keyring-based implementation of username/password query
255 for HTTP(S) connections
258 for HTTP(S) connections
256
259
257 Passwords are saved in gnome keyring, OSX/Chain or other platform
260 Passwords are saved in gnome keyring, OSX/Chain or other platform
258 specific storage and keyed by the repository url
261 specific storage and keyed by the repository url
259 """
262 """
260 # Extend object attributes
263 # Extend object attributes
261 if not hasattr(self, '_pwd_handler'):
264 if not hasattr(self, '_pwd_handler'):
262 self._pwd_handler = HTTPPasswordHandler()
265 self._pwd_handler = HTTPPasswordHandler()
263
266
264 return self._pwd_handler.find_auth(self, realm, authuri)
267 return self._pwd_handler.find_auth(self, realm, authuri)
265
268
266 ############################################################
269 ############################################################
267
270
268 def try_smtp_login(ui, smtp_obj, username, password):
271 def try_smtp_login(ui, smtp_obj, username, password):
269 """
272 """
270 Attempts smtp login on smtp_obj (smtplib.SMTP) using username and
273 Attempts smtp login on smtp_obj (smtplib.SMTP) using username and
271 password.
274 password.
272
275
273 Returns:
276 Returns:
274 - True if login succeeded
277 - True if login succeeded
275 - False if login failed due to the wrong credentials
278 - False if login failed due to the wrong credentials
276
279
277 Throws Abort exception if login failed for any other reason.
280 Throws Abort exception if login failed for any other reason.
278
281
279 Immediately returns False if password is empty
282 Immediately returns False if password is empty
280 """
283 """
281 if not password:
284 if not password:
282 return False
285 return False
283 try:
286 try:
284 ui.note(_('(authenticating to mail server as %s)\n') %
287 ui.note(_('(authenticating to mail server as %s)\n') %
285 (username))
288 (username))
286 smtp_obj.login(username, password)
289 smtp_obj.login(username, password)
287 return True
290 return True
288 except smtplib.SMTPException, inst:
291 except smtplib.SMTPException, inst:
289 if inst.smtp_code == 535:
292 if inst.smtp_code == 535:
290 ui.status(_("SMTP login failed: %s\n\n") % inst.smtp_error)
293 ui.status(_("SMTP login failed: %s\n\n") % inst.smtp_error)
291 return False
294 return False
292 else:
295 else:
293 raise util.Abort(inst)
296 raise util.Abort(inst)
294
297
295 def keyring_supported_smtp(ui, username):
298 def keyring_supported_smtp(ui, username):
296 """
299 """
297 keyring-integrated replacement for mercurial.mail._smtp
300 keyring-integrated replacement for mercurial.mail._smtp
298 Used only when configuration file contains username, but
301 Used only when configuration file contains username, but
299 does not contain the password.
302 does not contain the password.
300
303
301 Most of the routine below is copied as-is from
304 Most of the routine below is copied as-is from
302 mercurial.mail._smtp. The only changed part is
305 mercurial.mail._smtp. The only changed part is
303 marked with #>>>>> and #<<<<< markers
306 marked with #>>>>> and #<<<<< markers
304 """
307 """
305 local_hostname = ui.config('smtp', 'local_hostname')
308 local_hostname = ui.config('smtp', 'local_hostname')
306 s = smtplib.SMTP(local_hostname=local_hostname)
309 s = smtplib.SMTP(local_hostname=local_hostname)
307 mailhost = ui.config('smtp', 'host')
310 mailhost = ui.config('smtp', 'host')
308 if not mailhost:
311 if not mailhost:
309 raise util.Abort(_('no [smtp]host in hgrc - cannot send mail'))
312 raise util.Abort(_('no [smtp]host in hgrc - cannot send mail'))
310 mailport = int(ui.config('smtp', 'port', 25))
313 mailport = int(ui.config('smtp', 'port', 25))
311 ui.note(_('sending mail: smtp host %s, port %s\n') %
314 ui.note(_('sending mail: smtp host %s, port %s\n') %
312 (mailhost, mailport))
315 (mailhost, mailport))
313 s.connect(host=mailhost, port=mailport)
316 s.connect(host=mailhost, port=mailport)
314 if ui.configbool('smtp', 'tls'):
317 if ui.configbool('smtp', 'tls'):
315 if not hasattr(socket, 'ssl'):
318 if not hasattr(socket, 'ssl'):
316 raise util.Abort(_("can't use TLS: Python SSL support "
319 raise util.Abort(_("can't use TLS: Python SSL support "
317 "not installed"))
320 "not installed"))
318 ui.note(_('(using tls)\n'))
321 ui.note(_('(using tls)\n'))
319 s.ehlo()
322 s.ehlo()
320 s.starttls()
323 s.starttls()
321 s.ehlo()
324 s.ehlo()
322
325
323 #>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
326 #>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
324 stored = password = password_store.get_smtp_password(
327 stored = password = password_store.get_smtp_password(
325 mailhost, mailport, username)
328 mailhost, mailport, username)
326 # No need to check whether password was found as try_smtp_login
329 # No need to check whether password was found as try_smtp_login
327 # just returns False if it is absent.
330 # just returns False if it is absent.
328 while not try_smtp_login(ui, s, username, password):
331 while not try_smtp_login(ui, s, username, password):
329 password = ui.getpass(_("Password for %s on %s:%d: ") % (username, mailhost, mailport))
332 password = ui.getpass(_("Password for %s on %s:%d: ") % (username, mailhost, mailport))
330
333
331 if stored != password:
334 if stored != password:
332 password_store.set_smtp_password(
335 password_store.set_smtp_password(
333 mailhost, mailport, username, password)
336 mailhost, mailport, username, password)
334 #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
337 #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
335
338
336 def send(sender, recipients, msg):
339 def send(sender, recipients, msg):
337 try:
340 try:
338 return s.sendmail(sender, recipients, msg)
341 return s.sendmail(sender, recipients, msg)
339 except smtplib.SMTPRecipientsRefused, inst:
342 except smtplib.SMTPRecipientsRefused, inst:
340 recipients = [r[1] for r in inst.recipients.values()]
343 recipients = [r[1] for r in inst.recipients.values()]
341 raise util.Abort('\n' + '\n'.join(recipients))
344 raise util.Abort('\n' + '\n'.join(recipients))
342 except smtplib.SMTPException, inst:
345 except smtplib.SMTPException, inst:
343 raise util.Abort(inst)
346 raise util.Abort(inst)
344
347
345 return send
348 return send
346
349
347 ############################################################
350 ############################################################
348
351
349 orig_smtp = mail._smtp
352 orig_smtp = mail._smtp
350
353
351 @monkeypatch_method(mail)
354 @monkeypatch_method(mail)
352 def _smtp(ui):
355 def _smtp(ui):
353 """
356 """
354 build an smtp connection and return a function to send email
357 build an smtp connection and return a function to send email
355
358
356 This is the monkeypatched version of _smtp(ui) function from
359 This is the monkeypatched version of _smtp(ui) function from
357 mercurial/mail.py. It calls the original unless username
360 mercurial/mail.py. It calls the original unless username
358 without password is given in the configuration.
361 without password is given in the configuration.
359 """
362 """
360 username = ui.config('smtp', 'username')
363 username = ui.config('smtp', 'username')
361 password = ui.config('smtp', 'password')
364 password = ui.config('smtp', 'password')
362
365
363 if username and not password:
366 if username and not password:
364 return keyring_supported_smtp(ui, username)
367 return keyring_supported_smtp(ui, username)
365 else:
368 else:
366 return orig_smtp(ui)
369 return orig_smtp(ui)
General Comments 0
You need to be logged in to leave comments. Login now