Chris@427
|
1 # -*- coding: utf-8 -*-
|
Chris@427
|
2 #
|
Chris@427
|
3 # EasyMercurial
|
Chris@427
|
4 #
|
Chris@427
|
5 # Based on hgExplorer by Jari Korhonen
|
Chris@427
|
6 # Copyright (c) 2010 Jari Korhonen
|
Chris@427
|
7 # Copyright (c) 2010 Chris Cannam
|
Chris@427
|
8 # Copyright (c) 2010 Queen Mary, University of London
|
Chris@427
|
9 #
|
Chris@427
|
10 # This program is free software; you can redistribute it and/or
|
Chris@427
|
11 # modify it under the terms of the GNU General Public License as
|
Chris@427
|
12 # published by the Free Software Foundation; either version 2 of the
|
Chris@427
|
13 # License, or (at your option) any later version. See the file
|
Chris@427
|
14 # COPYING included with this distribution for more information.
|
Chris@427
|
15
|
Chris@449
|
16 import sys, os, stat, urllib, urllib2, urlparse, platform, hashlib
|
Chris@427
|
17
|
Chris@438
|
18 from mercurial.i18n import _
|
Chris@433
|
19 from mercurial import ui, util, error
|
Chris@427
|
20 try:
|
Chris@427
|
21 from mercurial.url import passwordmgr
|
Chris@427
|
22 except:
|
Chris@427
|
23 from mercurial.httprepo import passwordmgr
|
Chris@427
|
24
|
Chris@427
|
25 # The value assigned here may be modified during installation, by
|
Chris@427
|
26 # replacing its default value with another one. We can't compare
|
Chris@427
|
27 # against its default value, because then the comparison text would
|
Chris@427
|
28 # get modified as well. So, compare using prefix only.
|
Chris@427
|
29 #
|
Chris@427
|
30 easyhg_import_path = 'NO_EASYHG_IMPORT_PATH'
|
Chris@427
|
31 if not easyhg_import_path.startswith('NO_'):
|
Chris@427
|
32 # We have an installation path: append it twice, once with
|
Chris@427
|
33 # the Python version suffixed
|
Chris@440
|
34 version_suffix = 'Py%d.%d' % (sys.version_info[0], sys.version_info[1])
|
Chris@427
|
35 sys.path.append(easyhg_import_path + "/" + version_suffix)
|
Chris@427
|
36 sys.path.append(easyhg_import_path)
|
Chris@427
|
37
|
Chris@427
|
38 # Try to load the PyQt4 module that we need. If this fails, we should
|
Chris@427
|
39 # bail out later (in uisetup), because if we bail out now, Mercurial
|
Chris@427
|
40 # will just continue without us and report success. The invoking
|
Chris@427
|
41 # application needs to be able to discover whether the module load
|
Chris@427
|
42 # succeeded or not, so we need to ensure that Mercurial itself returns
|
Chris@427
|
43 # failure if it didn't.
|
Chris@427
|
44 #
|
Chris@427
|
45 easyhg_pyqt_ok = True
|
Chris@427
|
46 try:
|
Chris@427
|
47 from PyQt4 import Qt, QtGui
|
Chris@427
|
48 except ImportError:
|
Chris@427
|
49 easyhg_pyqt_ok = False
|
Chris@427
|
50 easyhg_qtapp = None
|
Chris@427
|
51
|
Chris@440
|
52 # These imports are optional, we just can't use the authfile (i.e.
|
Chris@438
|
53 # "remember this password") feature without them
|
Chris@438
|
54 #
|
Chris@437
|
55 easyhg_authfile_imports_ok = True
|
Chris@437
|
56 try:
|
Chris@437
|
57 from Crypto.Cipher import AES
|
Chris@437
|
58 import ConfigParser # Mercurial version won't write files
|
Chris@437
|
59 import base64
|
Chris@437
|
60 except ImportError:
|
Chris@445
|
61 print "EasyHg: Failed to import required modules for authfile support"
|
Chris@437
|
62 easyhg_authfile_imports_ok = False
|
Chris@433
|
63
|
Chris@431
|
64
|
Chris@439
|
65 def encrypt_salted(text, key):
|
Chris@439
|
66 salt = os.urandom(8)
|
Chris@439
|
67 text = '%d.%s.%s' % (len(text), base64.b64encode(salt), text)
|
Chris@431
|
68 text += (16 - len(text) % 16) * ' '
|
Chris@446
|
69 cipher = AES.new(key, AES.MODE_CBC)
|
Chris@431
|
70 return base64.b64encode(cipher.encrypt(text))
|
Chris@431
|
71
|
Chris@439
|
72 def decrypt_salted(ctext, key):
|
Chris@446
|
73 cipher = AES.new(key, AES.MODE_CBC)
|
Chris@431
|
74 text = cipher.decrypt(base64.b64decode(ctext))
|
Chris@431
|
75 (tlen, d, text) = text.partition('.')
|
Chris@439
|
76 (salt, d, text) = text.partition('.')
|
Chris@431
|
77 return text[0:int(tlen)]
|
Chris@431
|
78
|
Chris@451
|
79 def pathless_url(url):
|
Chris@451
|
80 parsed_url = urlparse.urlparse(url)
|
Chris@451
|
81 return "%s://%s" % (parsed_url.scheme, parsed_url.netloc)
|
Chris@451
|
82
|
Chris@427
|
83 # from mercurial_keyring by Marcin Kasperski
|
Chris@451
|
84 def canonical_url(url):
|
Chris@451
|
85 parsed_url = urlparse.urlparse(url)
|
Chris@427
|
86 return "%s://%s%s" % (parsed_url.scheme, parsed_url.netloc,
|
Chris@427
|
87 parsed_url.path)
|
Chris@427
|
88
|
Chris@436
|
89 def load_config(pcfg, pfile):
|
Chris@434
|
90 fp = None
|
Chris@434
|
91 try:
|
Chris@434
|
92 fp = open(pfile)
|
Chris@434
|
93 except:
|
Chris@440
|
94 return
|
Chris@440
|
95 pcfg.readfp(fp)
|
Chris@440
|
96 fp.close()
|
Chris@434
|
97
|
Chris@444
|
98 def save_config(ui, pcfg, pfile):
|
Chris@434
|
99 ofp = None
|
Chris@434
|
100 try:
|
Chris@434
|
101 ofp = open(pfile, 'w')
|
Chris@434
|
102 except:
|
Chris@444
|
103 ui.write("failed to open authfile %s for writing\n" % pfile)
|
Chris@434
|
104 raise
|
Chris@448
|
105 if platform.system() != 'Windows':
|
Chris@448
|
106 try:
|
Chris@448
|
107 os.fchmod(ofp.fileno(), stat.S_IRUSR | stat.S_IWUSR)
|
Chris@448
|
108 except:
|
Chris@448
|
109 ofp.close()
|
Chris@448
|
110 ui.write("failed to set permissions on authfile %s\n" % pfile)
|
Chris@448
|
111 raise
|
Chris@434
|
112 pcfg.write(ofp)
|
Chris@434
|
113 ofp.close()
|
Chris@434
|
114
|
Chris@436
|
115 def get_from_config(pcfg, sect, key):
|
Chris@436
|
116 data = None
|
Chris@436
|
117 try:
|
Chris@436
|
118 data = pcfg.get(sect, key)
|
Chris@436
|
119 except (ConfigParser.NoOptionError, ConfigParser.NoSectionError):
|
Chris@436
|
120 pass
|
Chris@436
|
121 return data
|
Chris@436
|
122
|
Chris@436
|
123 def get_boolean_from_config(pcfg, sect, key, deflt):
|
Chris@436
|
124 data = deflt
|
Chris@436
|
125 try:
|
Chris@436
|
126 data = pcfg.getboolean(sect, key)
|
Chris@436
|
127 except (ConfigParser.NoOptionError, ConfigParser.NoSectionError):
|
Chris@436
|
128 pass
|
Chris@436
|
129 return data
|
Chris@436
|
130
|
Chris@436
|
131 def set_to_config(pcfg, sect, key, data):
|
Chris@436
|
132 if not pcfg.has_section(sect):
|
Chris@436
|
133 pcfg.add_section(sect)
|
Chris@436
|
134 pcfg.set(sect, key, data)
|
Chris@436
|
135
|
Chris@449
|
136 def remote_key(uri, user, key):
|
Chris@440
|
137 # generate a "safe-for-config-file" key representing uri+user
|
Chris@449
|
138 s = '%s@@%s' % (uri, user)
|
Chris@449
|
139 h = hashlib.sha1()
|
Chris@449
|
140 h.update(key)
|
Chris@449
|
141 h.update(s)
|
Chris@449
|
142 return h.hexdigest()
|
Chris@438
|
143
|
Chris@440
|
144
|
Chris@440
|
145 def uisetup(ui):
|
Chris@440
|
146 if not easyhg_pyqt_ok:
|
Chris@440
|
147 raise util.Abort(_('Failed to load PyQt4 module required by easyhg.py'))
|
Chris@440
|
148 global easyhg_qtapp
|
Chris@440
|
149 easyhg_qtapp = QtGui.QApplication([])
|
Chris@440
|
150
|
Chris@440
|
151 def monkeypatch_method(cls):
|
Chris@440
|
152 def decorator(func):
|
Chris@440
|
153 setattr(cls, func.__name__, func)
|
Chris@440
|
154 return func
|
Chris@440
|
155 return decorator
|
Chris@440
|
156
|
Chris@440
|
157 orig_find = passwordmgr.find_user_password
|
Chris@440
|
158
|
Chris@427
|
159 @monkeypatch_method(passwordmgr)
|
Chris@427
|
160 def find_user_password(self, realm, authuri):
|
Chris@427
|
161
|
Chris@427
|
162 if not self.ui.interactive():
|
Chris@427
|
163 return orig_find(self, realm, authuri)
|
Chris@427
|
164 if not easyhg_pyqt_ok:
|
Chris@427
|
165 return orig_find(self, realm, authuri)
|
Chris@427
|
166
|
Chris@427
|
167 authinfo = urllib2.HTTPPasswordMgrWithDefaultRealm.find_user_password(
|
Chris@427
|
168 self, realm, authuri)
|
Chris@427
|
169 user, passwd = authinfo
|
Chris@427
|
170
|
Chris@427
|
171 if user and passwd:
|
Chris@427
|
172 return orig_find(self, realm, authuri)
|
Chris@427
|
173
|
Chris@440
|
174 # self.ui.write("want username and/or password for %s\n" % authuri)
|
Chris@427
|
175
|
Chris@440
|
176 short_uri = canonical_url(authuri)
|
Chris@451
|
177 pathless_uri = pathless_url(authuri)
|
Chris@427
|
178
|
Chris@440
|
179 authkey = self.ui.config('easyhg', 'authkey')
|
Chris@440
|
180 authfile = self.ui.config('easyhg', 'authfile')
|
Chris@440
|
181 use_authfile = (easyhg_authfile_imports_ok and authkey and authfile)
|
Chris@450
|
182 if authfile: authfile = os.path.expanduser(authfile)
|
Chris@427
|
183
|
Chris@427
|
184 dialog = QtGui.QDialog()
|
Chris@427
|
185 layout = QtGui.QGridLayout()
|
Chris@427
|
186 dialog.setLayout(layout)
|
Chris@427
|
187
|
Chris@440
|
188 layout.addWidget(QtGui.QLabel(_('<h3>Login required</h3><p>Please provide your login details for the repository at<br><code>%s</code>:') % short_uri), 0, 0, 1, 2)
|
Chris@427
|
189
|
Chris@440
|
190 user_field = QtGui.QLineEdit()
|
Chris@427
|
191 if user:
|
Chris@440
|
192 user_field.setText(user)
|
Chris@427
|
193 layout.addWidget(QtGui.QLabel(_('User:')), 1, 0)
|
Chris@440
|
194 layout.addWidget(user_field, 1, 1)
|
Chris@427
|
195
|
Chris@440
|
196 passwd_field = QtGui.QLineEdit()
|
Chris@440
|
197 passwd_field.setEchoMode(QtGui.QLineEdit.Password)
|
Chris@427
|
198 if passwd:
|
Chris@440
|
199 passwd_field.setText(passwd)
|
Chris@427
|
200 layout.addWidget(QtGui.QLabel(_('Password:')), 2, 0)
|
Chris@440
|
201 layout.addWidget(passwd_field, 2, 1)
|
Chris@427
|
202
|
Chris@440
|
203 user_field.connect(user_field, Qt.SIGNAL("textChanged(QString)"),
|
Chris@440
|
204 passwd_field, Qt.SLOT("clear()"))
|
Chris@435
|
205
|
Chris@440
|
206 remember_field = None
|
Chris@440
|
207 remember = False
|
Chris@450
|
208
|
Chris@440
|
209 authconfig = None
|
Chris@450
|
210 authdata = None
|
Chris@433
|
211
|
Chris@436
|
212 if use_authfile:
|
Chris@450
|
213
|
Chris@440
|
214 authconfig = ConfigParser.RawConfigParser()
|
Chris@440
|
215 load_config(authconfig, authfile)
|
Chris@440
|
216 remember = get_boolean_from_config(authconfig, 'preferences',
|
Chris@440
|
217 'remember', False)
|
Chris@450
|
218
|
Chris@450
|
219 if not user:
|
Chris@450
|
220 authdata = get_from_config(authconfig, 'user',
|
Chris@450
|
221 remote_key(short_uri, '', authkey))
|
Chris@450
|
222 if authdata:
|
Chris@450
|
223 user = decrypt_salted(authdata, authkey)
|
Chris@450
|
224 user_field.setText(user)
|
Chris@450
|
225
|
Chris@450
|
226 if not passwd:
|
Chris@450
|
227 authdata = get_from_config(authconfig, 'auth',
|
Chris@451
|
228 remote_key(pathless_uri, user, authkey))
|
Chris@450
|
229 if authdata:
|
Chris@450
|
230 passwd = decrypt_salted(authdata, authkey)
|
Chris@450
|
231 passwd_field.setText(passwd)
|
Chris@450
|
232
|
Chris@440
|
233 remember_field = QtGui.QCheckBox()
|
Chris@440
|
234 remember_field.setChecked(remember)
|
Chris@450
|
235 remember_field.setText(_('Remember these details while EasyMercurial is running'))
|
Chris@440
|
236 layout.addWidget(remember_field, 3, 1)
|
Chris@431
|
237
|
Chris@427
|
238 bb = QtGui.QDialogButtonBox()
|
Chris@427
|
239 ok = bb.addButton(bb.Ok)
|
Chris@427
|
240 cancel = bb.addButton(bb.Cancel)
|
Chris@427
|
241 cancel.setDefault(False)
|
Chris@427
|
242 cancel.setAutoDefault(False)
|
Chris@427
|
243 ok.setDefault(True)
|
Chris@427
|
244 bb.connect(ok, Qt.SIGNAL("clicked()"), dialog, Qt.SLOT("accept()"))
|
Chris@427
|
245 bb.connect(cancel, Qt.SIGNAL("clicked()"), dialog, Qt.SLOT("reject()"))
|
Chris@431
|
246 layout.addWidget(bb, 4, 0, 1, 2)
|
Chris@427
|
247
|
Chris@428
|
248 dialog.setWindowTitle(_('EasyMercurial: Login'))
|
Chris@427
|
249 dialog.show()
|
Chris@428
|
250
|
Chris@428
|
251 if not user:
|
Chris@440
|
252 user_field.setFocus(True)
|
Chris@428
|
253 elif not passwd:
|
Chris@440
|
254 passwd_field.setFocus(True)
|
Chris@450
|
255 else:
|
Chris@450
|
256 ok.setFocus(True)
|
Chris@428
|
257
|
Chris@427
|
258 dialog.raise_()
|
Chris@427
|
259 ok = dialog.exec_()
|
Chris@436
|
260 if not ok:
|
Chris@436
|
261 raise util.Abort(_('password entry cancelled'))
|
Chris@431
|
262
|
Chris@440
|
263 user = user_field.text()
|
Chris@440
|
264 passwd = passwd_field.text()
|
Chris@432
|
265
|
Chris@436
|
266 if use_authfile:
|
Chris@440
|
267 remember = remember_field.isChecked()
|
Chris@440
|
268 set_to_config(authconfig, 'preferences', 'remember', remember)
|
Chris@438
|
269 if user:
|
Chris@450
|
270 if remember:
|
Chris@450
|
271 authdata = encrypt_salted(user, authkey)
|
Chris@450
|
272 set_to_config(authconfig, 'user', remote_key(short_uri, '', authkey), authdata)
|
Chris@450
|
273 else:
|
Chris@450
|
274 set_to_config(authconfig, 'user', remote_key(short_uri, '', authkey), '')
|
Chris@450
|
275 if passwd:
|
Chris@450
|
276 if remember:
|
Chris@440
|
277 authdata = encrypt_salted(passwd, authkey)
|
Chris@451
|
278 set_to_config(authconfig, 'auth', remote_key(pathless_uri, user, authkey), authdata)
|
Chris@438
|
279 else:
|
Chris@451
|
280 set_to_config(authconfig, 'auth', remote_key(pathless_uri, user, authkey), '')
|
Chris@444
|
281 save_config(self.ui, authconfig, authfile)
|
Chris@433
|
282
|
Chris@436
|
283 self.add_password(realm, authuri, user, passwd)
|
Chris@427
|
284 return (user, passwd)
|
Chris@427
|
285
|
Chris@427
|
286
|