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@431
|
20 from mercurial import ui, util, config, 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@431
|
60 #!!! should be in a class here
|
Chris@431
|
61
|
Chris@431
|
62 def encrypt(text, key):
|
Chris@431
|
63 text = '%d.%s' % (len(text), text)
|
Chris@431
|
64 text += (16 - len(text) % 16) * ' '
|
Chris@431
|
65 cipher = AES.new(key)
|
Chris@431
|
66 return base64.b64encode(cipher.encrypt(text))
|
Chris@431
|
67
|
Chris@431
|
68 def decrypt(ctext, key):
|
Chris@431
|
69 cipher = AES.new(key)
|
Chris@431
|
70 text = cipher.decrypt(base64.b64decode(ctext))
|
Chris@431
|
71 (tlen, d, text) = text.partition('.')
|
Chris@431
|
72 return text[0:int(tlen)]
|
Chris@431
|
73
|
Chris@427
|
74 def monkeypatch_method(cls):
|
Chris@427
|
75 def decorator(func):
|
Chris@427
|
76 setattr(cls, func.__name__, func)
|
Chris@427
|
77 return func
|
Chris@427
|
78 return decorator
|
Chris@427
|
79
|
Chris@427
|
80 def uisetup(ui):
|
Chris@427
|
81 if not easyhg_pyqt_ok:
|
Chris@427
|
82 raise util.Abort(_('Failed to load PyQt4 module required by easyhg.py'))
|
Chris@427
|
83 global easyhg_qtapp
|
Chris@427
|
84 easyhg_qtapp = QtGui.QApplication([])
|
Chris@427
|
85
|
Chris@427
|
86 orig_find = passwordmgr.find_user_password
|
Chris@427
|
87
|
Chris@427
|
88 # from mercurial_keyring by Marcin Kasperski
|
Chris@427
|
89 def canonical_url(authuri):
|
Chris@427
|
90 """
|
Chris@427
|
91 Strips query params from url. Used to convert urls like
|
Chris@427
|
92 https://repo.machine.com/repos/apps/module?pairs=0000000000000000000000000000000000000000-0000000000000000000000000000000000000000&cmd=between
|
Chris@427
|
93 to
|
Chris@427
|
94 https://repo.machine.com/repos/apps/module
|
Chris@427
|
95 """
|
Chris@427
|
96 parsed_url = urlparse.urlparse(authuri)
|
Chris@427
|
97 return "%s://%s%s" % (parsed_url.scheme, parsed_url.netloc,
|
Chris@427
|
98 parsed_url.path)
|
Chris@427
|
99
|
Chris@427
|
100 @monkeypatch_method(passwordmgr)
|
Chris@427
|
101 def find_user_password(self, realm, authuri):
|
Chris@427
|
102
|
Chris@427
|
103 if not self.ui.interactive():
|
Chris@427
|
104 return orig_find(self, realm, authuri)
|
Chris@427
|
105 if not easyhg_pyqt_ok:
|
Chris@427
|
106 return orig_find(self, realm, authuri)
|
Chris@427
|
107
|
Chris@427
|
108 authinfo = urllib2.HTTPPasswordMgrWithDefaultRealm.find_user_password(
|
Chris@427
|
109 self, realm, authuri)
|
Chris@427
|
110 user, passwd = authinfo
|
Chris@427
|
111
|
Chris@427
|
112 if user and passwd:
|
Chris@427
|
113 return orig_find(self, realm, authuri)
|
Chris@427
|
114
|
Chris@427
|
115 self.ui.write("want username and/or password for %s\n" % authuri)
|
Chris@427
|
116
|
Chris@427
|
117 uri = canonical_url(authuri)
|
Chris@427
|
118
|
Chris@431
|
119 pkey = ('%s@@%s' % (uri, user)).replace('=', '__')
|
Chris@431
|
120 pekey = self.ui.config('easyhg', 'authkey')
|
Chris@432
|
121 pfile = os.path.expanduser(self.ui.config('easyhg', 'authfile'))
|
Chris@431
|
122 pdata = None
|
Chris@431
|
123
|
Chris@431
|
124 self.ui.write("pekey is %s\n" % pekey)
|
Chris@431
|
125 self.ui.write("pfile is %s\n" % pfile)
|
Chris@427
|
126
|
Chris@427
|
127 dialog = QtGui.QDialog()
|
Chris@427
|
128 layout = QtGui.QGridLayout()
|
Chris@427
|
129 dialog.setLayout(layout)
|
Chris@427
|
130
|
Chris@432
|
131 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
|
132
|
Chris@427
|
133 userfield = QtGui.QLineEdit()
|
Chris@427
|
134 if user:
|
Chris@427
|
135 userfield.setText(user)
|
Chris@427
|
136 layout.addWidget(QtGui.QLabel(_('User:')), 1, 0)
|
Chris@427
|
137 layout.addWidget(userfield, 1, 1)
|
Chris@427
|
138
|
Chris@427
|
139 passfield = QtGui.QLineEdit()
|
Chris@427
|
140 passfield.setEchoMode(QtGui.QLineEdit.Password)
|
Chris@427
|
141 if passwd:
|
Chris@428
|
142 passfield.setText(passwd)
|
Chris@427
|
143 layout.addWidget(QtGui.QLabel(_('Password:')), 2, 0)
|
Chris@427
|
144 layout.addWidget(passfield, 2, 1)
|
Chris@427
|
145
|
Chris@431
|
146 remember = None
|
Chris@431
|
147 if pekey and pfile:
|
Chris@431
|
148 # load pwd from our cache file, decrypt with given key
|
Chris@431
|
149 pcfg = config.config()
|
Chris@431
|
150 fp = None
|
Chris@431
|
151 try:
|
Chris@431
|
152 fp = open(pfile)
|
Chris@431
|
153 except:
|
Chris@431
|
154 self.ui.write("failed to open authfile %s\n" % pfile)
|
Chris@432
|
155 if fp and not passwd:
|
Chris@431
|
156 pcfg.read(pfile)
|
Chris@431
|
157 pdata = pcfg.get('auth', pkey)
|
Chris@432
|
158 if pdata:
|
Chris@432
|
159 cachedpwd = decrypt(pdata, pekey)
|
Chris@431
|
160 passfield.setText(cachedpwd)
|
Chris@432
|
161 fp.close()
|
Chris@431
|
162 remember = QtGui.QCheckBox()
|
Chris@431
|
163 remember.setText(_('Remember this password until EasyMercurial exits'))
|
Chris@431
|
164 layout.addWidget(remember, 3, 1)
|
Chris@431
|
165
|
Chris@427
|
166 bb = QtGui.QDialogButtonBox()
|
Chris@427
|
167 ok = bb.addButton(bb.Ok)
|
Chris@427
|
168 cancel = bb.addButton(bb.Cancel)
|
Chris@427
|
169 cancel.setDefault(False)
|
Chris@427
|
170 cancel.setAutoDefault(False)
|
Chris@427
|
171 ok.setDefault(True)
|
Chris@427
|
172 bb.connect(ok, Qt.SIGNAL("clicked()"), dialog, Qt.SLOT("accept()"))
|
Chris@427
|
173 bb.connect(cancel, Qt.SIGNAL("clicked()"), dialog, Qt.SLOT("reject()"))
|
Chris@431
|
174 layout.addWidget(bb, 4, 0, 1, 2)
|
Chris@427
|
175
|
Chris@428
|
176 dialog.setWindowTitle(_('EasyMercurial: Login'))
|
Chris@427
|
177 dialog.show()
|
Chris@428
|
178
|
Chris@428
|
179 if not user:
|
Chris@428
|
180 userfield.setFocus(True)
|
Chris@428
|
181 elif not passwd:
|
Chris@428
|
182 passfield.setFocus(True)
|
Chris@428
|
183
|
Chris@427
|
184 dialog.raise_()
|
Chris@427
|
185 ok = dialog.exec_()
|
Chris@427
|
186 if ok:
|
Chris@427
|
187 self.ui.write('Dialog accepted\n')
|
Chris@427
|
188 user = userfield.text()
|
Chris@427
|
189 passwd = passfield.text()
|
Chris@431
|
190
|
Chris@431
|
191 #!!! create pfile if necessary (with proper permissions), append auth data to it
|
Chris@432
|
192 if pekey and pfile:
|
Chris@432
|
193
|
Chris@432
|
194 ofp = None
|
Chris@432
|
195
|
Chris@432
|
196 try:
|
Chris@432
|
197 ofp = open(pfile, 'a')
|
Chris@432
|
198 except:
|
Chris@432
|
199 self.ui.write("failed to open authfile %s for writing\n" % pfile)
|
Chris@432
|
200 raise
|
Chris@432
|
201
|
Chris@432
|
202 try:
|
Chris@432
|
203 os.fchmod(ofp.fileno(), stat.S_IRUSR | stat.S_IWUSR) #!!! Windows equivalent?
|
Chris@432
|
204 except:
|
Chris@432
|
205 ofp.close()
|
Chris@432
|
206 ofp = None
|
Chris@432
|
207 self.ui.write("failed to set proper permissions on authfile %s\n" % pfile)
|
Chris@432
|
208 raise
|
Chris@432
|
209
|
Chris@432
|
210 if ofp:
|
Chris@432
|
211 pdata = encrypt(passwd, pekey)
|
Chris@432
|
212 ofp.write('[auth]\n')
|
Chris@432
|
213 ofp.write(pkey + '=' + pdata + '\n')
|
Chris@432
|
214 ofp.close()
|
Chris@432
|
215
|
Chris@431
|
216
|
Chris@431
|
217 # if passwd and keyring_key != '' and not from_keyring:
|
Chris@431
|
218 # keyring_key = '%s@@%s' % (uri, user)
|
Chris@431
|
219 ## keyring.set_password('Mercurial', keyring_key, passwd)
|
Chris@427
|
220 self.add_password(realm, authuri, user, passwd)
|
Chris@427
|
221 else:
|
Chris@427
|
222 raise util.Abort(_('password entry cancelled'))
|
Chris@427
|
223 return (user, passwd)
|
Chris@427
|
224
|
Chris@427
|
225
|
Chris@427
|
226
|