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@432
|
16 import sys, os, stat
|
Chris@427
|
17
|
Chris@427
|
18 import urllib, urllib2, urlparse
|
Chris@427
|
19
|
Chris@433
|
20 from mercurial import ui, util, error
|
Chris@427
|
21 try:
|
Chris@427
|
22 from mercurial.url import passwordmgr
|
Chris@427
|
23 except:
|
Chris@427
|
24 from mercurial.httprepo import passwordmgr
|
Chris@427
|
25
|
Chris@427
|
26 from mercurial.i18n import _
|
Chris@427
|
27
|
Chris@427
|
28 # The value assigned here may be modified during installation, by
|
Chris@427
|
29 # replacing its default value with another one. We can't compare
|
Chris@427
|
30 # against its default value, because then the comparison text would
|
Chris@427
|
31 # get modified as well. So, compare using prefix only.
|
Chris@427
|
32 #
|
Chris@427
|
33 easyhg_import_path = 'NO_EASYHG_IMPORT_PATH'
|
Chris@427
|
34 if not easyhg_import_path.startswith('NO_'):
|
Chris@427
|
35 # We have an installation path: append it twice, once with
|
Chris@427
|
36 # the Python version suffixed
|
Chris@427
|
37 version_suffix = "Py" + str(sys.version_info[0]) + "." + str(sys.version_info[1]);
|
Chris@427
|
38 sys.path.append(easyhg_import_path + "/" + version_suffix)
|
Chris@427
|
39 sys.path.append(easyhg_import_path)
|
Chris@427
|
40
|
Chris@427
|
41 # Try to load the PyQt4 module that we need. If this fails, we should
|
Chris@427
|
42 # bail out later (in uisetup), because if we bail out now, Mercurial
|
Chris@427
|
43 # will just continue without us and report success. The invoking
|
Chris@427
|
44 # application needs to be able to discover whether the module load
|
Chris@427
|
45 # succeeded or not, so we need to ensure that Mercurial itself returns
|
Chris@427
|
46 # failure if it didn't.
|
Chris@427
|
47 #
|
Chris@427
|
48 easyhg_pyqt_ok = True
|
Chris@427
|
49 try:
|
Chris@427
|
50 from PyQt4 import Qt, QtGui
|
Chris@427
|
51 except ImportError:
|
Chris@427
|
52 easyhg_pyqt_ok = False
|
Chris@427
|
53
|
Chris@427
|
54 easyhg_qtapp = None
|
Chris@427
|
55
|
Chris@432
|
56 #!!! same as above for this? or just continue without remember feature?
|
Chris@431
|
57 from Crypto.Cipher import AES
|
Chris@431
|
58 import base64
|
Chris@431
|
59
|
Chris@433
|
60 import ConfigParser # Mercurial version won't write files
|
Chris@433
|
61
|
Chris@431
|
62 #!!! should be in a class here
|
Chris@431
|
63
|
Chris@431
|
64 def encrypt(text, key):
|
Chris@431
|
65 text = '%d.%s' % (len(text), text)
|
Chris@431
|
66 text += (16 - len(text) % 16) * ' '
|
Chris@431
|
67 cipher = AES.new(key)
|
Chris@431
|
68 return base64.b64encode(cipher.encrypt(text))
|
Chris@431
|
69
|
Chris@431
|
70 def decrypt(ctext, key):
|
Chris@431
|
71 cipher = AES.new(key)
|
Chris@431
|
72 text = cipher.decrypt(base64.b64decode(ctext))
|
Chris@431
|
73 (tlen, d, text) = text.partition('.')
|
Chris@431
|
74 return text[0:int(tlen)]
|
Chris@431
|
75
|
Chris@427
|
76 def monkeypatch_method(cls):
|
Chris@427
|
77 def decorator(func):
|
Chris@427
|
78 setattr(cls, func.__name__, func)
|
Chris@427
|
79 return func
|
Chris@427
|
80 return decorator
|
Chris@427
|
81
|
Chris@427
|
82 def uisetup(ui):
|
Chris@427
|
83 if not easyhg_pyqt_ok:
|
Chris@427
|
84 raise util.Abort(_('Failed to load PyQt4 module required by easyhg.py'))
|
Chris@427
|
85 global easyhg_qtapp
|
Chris@427
|
86 easyhg_qtapp = QtGui.QApplication([])
|
Chris@427
|
87
|
Chris@427
|
88 orig_find = passwordmgr.find_user_password
|
Chris@427
|
89
|
Chris@427
|
90 # from mercurial_keyring by Marcin Kasperski
|
Chris@427
|
91 def canonical_url(authuri):
|
Chris@427
|
92 """
|
Chris@427
|
93 Strips query params from url. Used to convert urls like
|
Chris@427
|
94 https://repo.machine.com/repos/apps/module?pairs=0000000000000000000000000000000000000000-0000000000000000000000000000000000000000&cmd=between
|
Chris@427
|
95 to
|
Chris@427
|
96 https://repo.machine.com/repos/apps/module
|
Chris@427
|
97 """
|
Chris@427
|
98 parsed_url = urlparse.urlparse(authuri)
|
Chris@427
|
99 return "%s://%s%s" % (parsed_url.scheme, parsed_url.netloc,
|
Chris@427
|
100 parsed_url.path)
|
Chris@427
|
101
|
Chris@434
|
102 def load_config(pfile):
|
Chris@434
|
103 fp = None
|
Chris@434
|
104 try:
|
Chris@434
|
105 fp = open(pfile)
|
Chris@434
|
106 except:
|
Chris@434
|
107 self.ui.write("failed to open authfile %s\n" % pfile)
|
Chris@434
|
108 if fp:
|
Chris@434
|
109 pcfg.readfp(fp)
|
Chris@434
|
110 fp.close()
|
Chris@434
|
111
|
Chris@434
|
112 def save_config(pcfg, pfile):
|
Chris@434
|
113 ofp = None
|
Chris@434
|
114 try:
|
Chris@434
|
115 ofp = open(pfile, 'w')
|
Chris@434
|
116 except:
|
Chris@434
|
117 self.ui.write("failed to open authfile %s for writing\n" % pfile)
|
Chris@434
|
118 raise
|
Chris@434
|
119 try:
|
Chris@434
|
120 os.fchmod(ofp.fileno(), stat.S_IRUSR | stat.S_IWUSR) #!!! Windows equivalent?
|
Chris@434
|
121 except:
|
Chris@434
|
122 ofp.close()
|
Chris@434
|
123 ofp = None
|
Chris@434
|
124 self.ui.write("failed to set proper permissions on authfile %s\n" % pfile)
|
Chris@434
|
125 raise
|
Chris@434
|
126 pcfg.write(ofp)
|
Chris@434
|
127 ofp.close()
|
Chris@434
|
128
|
Chris@427
|
129 @monkeypatch_method(passwordmgr)
|
Chris@427
|
130 def find_user_password(self, realm, authuri):
|
Chris@427
|
131
|
Chris@427
|
132 if not self.ui.interactive():
|
Chris@427
|
133 return orig_find(self, realm, authuri)
|
Chris@427
|
134 if not easyhg_pyqt_ok:
|
Chris@427
|
135 return orig_find(self, realm, authuri)
|
Chris@427
|
136
|
Chris@427
|
137 authinfo = urllib2.HTTPPasswordMgrWithDefaultRealm.find_user_password(
|
Chris@427
|
138 self, realm, authuri)
|
Chris@427
|
139 user, passwd = authinfo
|
Chris@427
|
140
|
Chris@427
|
141 if user and passwd:
|
Chris@427
|
142 return orig_find(self, realm, authuri)
|
Chris@427
|
143
|
Chris@427
|
144 self.ui.write("want username and/or password for %s\n" % authuri)
|
Chris@427
|
145
|
Chris@427
|
146 uri = canonical_url(authuri)
|
Chris@427
|
147
|
Chris@433
|
148 pkey = base64.b64encode('%s@@%s' % (uri, user)).replace('=', '_')
|
Chris@431
|
149 pekey = self.ui.config('easyhg', 'authkey')
|
Chris@434
|
150 pfile = self.ui.config('easyhg', 'authfile')
|
Chris@434
|
151 if pfile: pfile = os.path.expanduser(pfile)
|
Chris@431
|
152 pdata = None
|
Chris@431
|
153
|
Chris@431
|
154 self.ui.write("pekey is %s\n" % pekey)
|
Chris@431
|
155 self.ui.write("pfile is %s\n" % pfile)
|
Chris@427
|
156
|
Chris@427
|
157 dialog = QtGui.QDialog()
|
Chris@427
|
158 layout = QtGui.QGridLayout()
|
Chris@427
|
159 dialog.setLayout(layout)
|
Chris@427
|
160
|
Chris@432
|
161 layout.addWidget(QtGui.QLabel(_('<h3>Login required</h3><p>Please provide your login details for the repository at<br><code>%s</code>:') % uri), 0, 0, 1, 2)
|
Chris@427
|
162
|
Chris@427
|
163 userfield = QtGui.QLineEdit()
|
Chris@427
|
164 if user:
|
Chris@427
|
165 userfield.setText(user)
|
Chris@427
|
166 layout.addWidget(QtGui.QLabel(_('User:')), 1, 0)
|
Chris@427
|
167 layout.addWidget(userfield, 1, 1)
|
Chris@427
|
168
|
Chris@427
|
169 passfield = QtGui.QLineEdit()
|
Chris@427
|
170 passfield.setEchoMode(QtGui.QLineEdit.Password)
|
Chris@427
|
171 if passwd:
|
Chris@428
|
172 passfield.setText(passwd)
|
Chris@427
|
173 layout.addWidget(QtGui.QLabel(_('Password:')), 2, 0)
|
Chris@427
|
174 layout.addWidget(passfield, 2, 1)
|
Chris@427
|
175
|
Chris@435
|
176 userfield.connect(userfield, Qt.SIGNAL("textChanged(QString)"), passfield, Qt.SLOT("clear()"))
|
Chris@435
|
177
|
Chris@431
|
178 remember = None
|
Chris@433
|
179 pcfg = None
|
Chris@433
|
180
|
Chris@431
|
181 if pekey and pfile:
|
Chris@431
|
182 # load pwd from our cache file, decrypt with given key
|
Chris@433
|
183 pcfg = ConfigParser.RawConfigParser()
|
Chris@433
|
184 remember_default = False
|
Chris@434
|
185 load_config(pcfg, pfile)
|
Chris@431
|
186 try:
|
Chris@434
|
187 remember_default = pcfg.getboolean('preferences', 'remember')
|
Chris@431
|
188 except:
|
Chris@434
|
189 remember_default = False
|
Chris@434
|
190 try:
|
Chris@434
|
191 pdata = pcfg.get('auth', pkey)
|
Chris@434
|
192 except ConfigParser.NoOptionError:
|
Chris@434
|
193 pdata = None
|
Chris@434
|
194 if pdata:
|
Chris@434
|
195 cachedpwd = decrypt(pdata, pekey)
|
Chris@434
|
196 passfield.setText(cachedpwd)
|
Chris@431
|
197 remember = QtGui.QCheckBox()
|
Chris@433
|
198 remember.setChecked(remember_default)
|
Chris@431
|
199 remember.setText(_('Remember this password until EasyMercurial exits'))
|
Chris@431
|
200 layout.addWidget(remember, 3, 1)
|
Chris@431
|
201
|
Chris@427
|
202 bb = QtGui.QDialogButtonBox()
|
Chris@427
|
203 ok = bb.addButton(bb.Ok)
|
Chris@427
|
204 cancel = bb.addButton(bb.Cancel)
|
Chris@427
|
205 cancel.setDefault(False)
|
Chris@427
|
206 cancel.setAutoDefault(False)
|
Chris@427
|
207 ok.setDefault(True)
|
Chris@427
|
208 bb.connect(ok, Qt.SIGNAL("clicked()"), dialog, Qt.SLOT("accept()"))
|
Chris@427
|
209 bb.connect(cancel, Qt.SIGNAL("clicked()"), dialog, Qt.SLOT("reject()"))
|
Chris@431
|
210 layout.addWidget(bb, 4, 0, 1, 2)
|
Chris@427
|
211
|
Chris@428
|
212 dialog.setWindowTitle(_('EasyMercurial: Login'))
|
Chris@427
|
213 dialog.show()
|
Chris@428
|
214
|
Chris@428
|
215 if not user:
|
Chris@428
|
216 userfield.setFocus(True)
|
Chris@428
|
217 elif not passwd:
|
Chris@428
|
218 passfield.setFocus(True)
|
Chris@428
|
219
|
Chris@427
|
220 dialog.raise_()
|
Chris@427
|
221 ok = dialog.exec_()
|
Chris@427
|
222 if ok:
|
Chris@427
|
223 self.ui.write('Dialog accepted\n')
|
Chris@427
|
224 user = userfield.text()
|
Chris@427
|
225 passwd = passfield.text()
|
Chris@431
|
226
|
Chris@433
|
227 if pekey and pfile and remember:
|
Chris@432
|
228
|
Chris@433
|
229 #!!! add these sections first...
|
Chris@433
|
230
|
Chris@433
|
231 if remember.isChecked():
|
Chris@432
|
232 pdata = encrypt(passwd, pekey)
|
Chris@433
|
233 pcfg.set('auth', pkey, pdata)
|
Chris@433
|
234 else:
|
Chris@433
|
235 pcfg.set('auth', pkey, '')
|
Chris@433
|
236
|
Chris@433
|
237 pcfg.set('preferences', 'remember', remember.isChecked())
|
Chris@434
|
238
|
Chris@434
|
239 save_config(pcfg, pfile)
|
Chris@431
|
240
|
Chris@431
|
241 # if passwd and keyring_key != '' and not from_keyring:
|
Chris@431
|
242 # keyring_key = '%s@@%s' % (uri, user)
|
Chris@431
|
243 ## keyring.set_password('Mercurial', keyring_key, passwd)
|
Chris@427
|
244 self.add_password(realm, authuri, user, passwd)
|
Chris@427
|
245 else:
|
Chris@427
|
246 raise util.Abort(_('password entry cancelled'))
|
Chris@427
|
247 return (user, passwd)
|
Chris@427
|
248
|
Chris@427
|
249
|
Chris@427
|
250
|