comparison easyhg.py @ 454:6f5acaf27d60

Merge from branch "authfile_refactor"
author Chris Cannam
date Wed, 29 Jun 2011 15:37:20 +0100
parents f2ab2cdd000b
children 856da063d76e
comparison
equal deleted inserted replaced
451:8156537329ff 454:6f5acaf27d60
2 # 2 #
3 # EasyMercurial 3 # EasyMercurial
4 # 4 #
5 # Based on hgExplorer by Jari Korhonen 5 # Based on hgExplorer by Jari Korhonen
6 # Copyright (c) 2010 Jari Korhonen 6 # Copyright (c) 2010 Jari Korhonen
7 # Copyright (c) 2010 Chris Cannam 7 # Copyright (c) 2010-2011 Chris Cannam
8 # Copyright (c) 2010 Queen Mary, University of London 8 # Copyright (c) 2010-2011 Queen Mary, University of London
9 # 9 #
10 # This program is free software; you can redistribute it and/or 10 # This program is free software; you can redistribute it and/or
11 # modify it under the terms of the GNU General Public License as 11 # modify it under the terms of the GNU General Public License as
12 # published by the Free Software Foundation; either version 2 of the 12 # published by the Free Software Foundation; either version 2 of the
13 # License, or (at your option) any later version. See the file 13 # License, or (at your option) any later version. See the file
60 except ImportError: 60 except ImportError:
61 print "EasyHg: Failed to import required modules for authfile support" 61 print "EasyHg: Failed to import required modules for authfile support"
62 easyhg_authfile_imports_ok = False 62 easyhg_authfile_imports_ok = False
63 63
64 64
65 def encrypt_salted(text, key): 65 class EasyHgAuthStore(object):
66 salt = os.urandom(8) 66
67 text = '%d.%s.%s' % (len(text), base64.b64encode(salt), text) 67 ui = None
68 text += (16 - len(text) % 16) * ' ' 68
69 cipher = AES.new(key, AES.MODE_CBC) 69 remote_url = ''
70 return base64.b64encode(cipher.encrypt(text)) 70
71 71 use_auth_file = False
72 def decrypt_salted(ctext, key): 72
73 cipher = AES.new(key, AES.MODE_CBC) 73 auth_config = None
74 text = cipher.decrypt(base64.b64decode(ctext)) 74 auth_file = ''
75 (tlen, d, text) = text.partition('.') 75 auth_key = ''
76 (salt, d, text) = text.partition('.') 76 auth_cipher = None
77 return text[0:int(tlen)] 77
78 78 user = ''
79 def pathless_url(url): 79 passwd = ''
80 parsed_url = urlparse.urlparse(url) 80
81 return "%s://%s" % (parsed_url.scheme, parsed_url.netloc) 81 remember = False
82 82
83 # from mercurial_keyring by Marcin Kasperski 83 def __init__(self, ui, url, user, passwd):
84 def canonical_url(url): 84
85 parsed_url = urlparse.urlparse(url) 85 self.ui = ui
86 return "%s://%s%s" % (parsed_url.scheme, parsed_url.netloc, 86 self.remote_url = url
87 parsed_url.path) 87
88 88 self.user = user
89 def load_config(pcfg, pfile): 89 self.passwd = passwd
90 fp = None 90
91 try: 91 self.auth_key = self.ui.config('easyhg', 'authkey')
92 fp = open(pfile) 92 self.auth_file = self.ui.config('easyhg', 'authfile')
93 except: 93
94 return 94 self.use_auth_file = (easyhg_authfile_imports_ok and
95 pcfg.readfp(fp) 95 self.auth_key and self.auth_file)
96 fp.close() 96
97 97 if self.use_auth_file:
98 def save_config(ui, pcfg, pfile): 98 self.auth_cipher = AES.new(self.auth_key, AES.MODE_CBC)
99 ofp = None 99 self.auth_file = os.path.expanduser(self.auth_file)
100 try: 100 self.load_auth_data()
101 ofp = open(pfile, 'w') 101
102 except: 102 def save(self):
103 ui.write("failed to open authfile %s for writing\n" % pfile) 103 if self.use_auth_file:
104 raise 104 self.save_auth_data()
105 if platform.system() != 'Windows': 105
106 try: 106 def encrypt(self, text):
107 os.fchmod(ofp.fileno(), stat.S_IRUSR | stat.S_IWUSR) 107 iv = os.urandom(12)
108 text = '%s.%d.%s.easyhg' % (base64.b64encode(iv), len(text), text)
109 text += (16 - (len(text) % 16)) * ' '
110 ctext = base64.b64encode(self.auth_cipher.encrypt(text))
111 return ctext
112
113 def decrypt(self, ctext):
114 text = self.auth_cipher.decrypt(base64.b64decode(ctext))
115 (iv, d, text) = text.partition('.')
116 (tlen, d, text) = text.partition('.')
117 try:
118 return text[0:int(tlen)]
108 except: 119 except:
109 ofp.close() 120 self.ui.write("failed to decrypt/convert cached data!")
110 ui.write("failed to set permissions on authfile %s\n" % pfile) 121 return ''
122
123 def argless_url(self):
124 parsed = urlparse.urlparse(self.remote_url)
125 return "%s://%s%s" % (parsed.scheme, parsed.netloc, parsed.path)
126
127 def pathless_url(self):
128 parsed = urlparse.urlparse(self.remote_url)
129 return "%s://%s" % (parsed.scheme, parsed.netloc)
130
131 def load_config(self):
132 if not self.auth_config:
133 self.auth_config = ConfigParser.RawConfigParser()
134 fp = None
135 try:
136 fp = open(self.auth_file)
137 except:
138 self.ui.write("unable to read authfile %s, ignoring\n" % self.auth_file)
139 return
140 self.auth_config.readfp(fp)
141 fp.close()
142
143 def save_config(self):
144 ofp = None
145 try:
146 ofp = open(self.auth_file, 'w')
147 except:
148 self.ui.write("failed to open authfile %s for writing\n" % self.auth_file)
111 raise 149 raise
112 pcfg.write(ofp) 150 if platform.system() != 'Windows':
113 ofp.close() 151 try:
114 152 os.fchmod(ofp.fileno(), stat.S_IRUSR | stat.S_IWUSR)
115 def get_from_config(pcfg, sect, key): 153 except:
116 data = None 154 ofp.close()
117 try: 155 self.ui.write("failed to set permissions on authfile %s\n" % self.auth_file)
118 data = pcfg.get(sect, key) 156 raise
119 except (ConfigParser.NoOptionError, ConfigParser.NoSectionError): 157 self.auth_config.write(ofp)
120 pass 158 ofp.close()
121 return data 159
122 160 def get_from_config(self, sect, key):
123 def get_boolean_from_config(pcfg, sect, key, deflt): 161 data = None
124 data = deflt 162 try:
125 try: 163 data = self.auth_config.get(sect, key)
126 data = pcfg.getboolean(sect, key) 164 except (ConfigParser.NoOptionError, ConfigParser.NoSectionError):
127 except (ConfigParser.NoOptionError, ConfigParser.NoSectionError): 165 pass
128 pass 166 return data
129 return data 167
130 168 def get_boolean_from_config(self, sect, key, deflt):
131 def set_to_config(pcfg, sect, key, data): 169 data = deflt
132 if not pcfg.has_section(sect): 170 try:
133 pcfg.add_section(sect) 171 data = self.auth_config.getboolean(sect, key)
134 pcfg.set(sect, key, data) 172 except (ConfigParser.NoOptionError, ConfigParser.NoSectionError):
135 173 pass
136 def remote_key(uri, user, key): 174 return data
137 # generate a "safe-for-config-file" key representing uri+user 175
138 s = '%s@@%s' % (uri, user) 176 def set_to_config(self, sect, key, data):
139 h = hashlib.sha1() 177 if not self.auth_config.has_section(sect):
140 h.update(key) 178 self.auth_config.add_section(sect)
141 h.update(s) 179 self.auth_config.set(sect, key, data)
142 return h.hexdigest() 180
181 def remote_key(self, url, user):
182 # generate a "safe-for-config-file" key representing uri+user
183 self.ui.write('generating remote_key for url %s and user %s\n' % (url, user))
184 s = '%s@@%s' % (url, user)
185 h = hashlib.sha1()
186 h.update(self.auth_key)
187 h.update(s)
188 hx = h.hexdigest()
189 return hx
190
191 def remote_user_key(self):
192 return self.remote_key(self.pathless_url(), '')
193
194 def remote_passwd_key(self):
195 return self.remote_key(self.pathless_url(), self.user)
196
197 def load_auth_data(self):
198
199 self.load_config()
200 if not self.auth_config: return
201
202 self.remember = self.get_boolean_from_config(
203 'preferences', 'remember', False)
204
205 if not self.user:
206 d = self.get_from_config('user', self.remote_user_key())
207 if d:
208 self.user = self.decrypt(d)
209
210 if self.user:
211 d = self.get_from_config('auth', self.remote_passwd_key())
212 if d:
213 self.passwd = self.decrypt(d)
214
215 def save_auth_data(self):
216
217 self.load_config()
218 if not self.auth_config: return
219
220 self.set_to_config('preferences', 'remember', self.remember)
221
222 self.ui.write('aiming to store details for user %s\n' % self.user)
223
224 if self.remember and self.user:
225 d = self.encrypt(self.user)
226 self.set_to_config('user', self.remote_user_key(), d)
227 else:
228 self.set_to_config('user', self.remote_user_key(), '')
229
230 if self.remember and self.user and self.passwd:
231 d = self.encrypt(self.passwd)
232 self.set_to_config('auth', self.remote_passwd_key(), d)
233 elif self.user:
234 self.set_to_config('auth', self.remote_passwd_key(), '')
235
236 self.save_config()
237
238 class EasyHgAuthDialog(object):
239
240 auth_store = None
241
242 def __init__(self, ui, url, user, passwd):
243 self.auth_store = EasyHgAuthStore(ui, url, user, passwd)
244
245 def ask(self):
246 dialog = QtGui.QDialog()
247 layout = QtGui.QGridLayout()
248 dialog.setLayout(layout)
249
250 layout.addWidget(QtGui.QLabel(_('<h3>Login required</h3><p>Please provide your login details for the repository at<br><code>%s</code>:') % self.auth_store.argless_url()), 0, 0, 1, 2)
251
252 user_field = QtGui.QLineEdit()
253 if self.auth_store.user: user_field.setText(self.auth_store.user)
254 layout.addWidget(QtGui.QLabel(_('User:')), 1, 0)
255 layout.addWidget(user_field, 1, 1)
256
257 passwd_field = QtGui.QLineEdit()
258 passwd_field.setEchoMode(QtGui.QLineEdit.Password)
259 if self.auth_store.passwd: passwd_field.setText(self.auth_store.passwd)
260 layout.addWidget(QtGui.QLabel(_('Password:')), 2, 0)
261 layout.addWidget(passwd_field, 2, 1)
262
263 user_field.connect(user_field, Qt.SIGNAL("textChanged(QString)"),
264 passwd_field, Qt.SLOT("clear()"))
265
266 remember_field = None
267 if self.auth_store.use_auth_file:
268 remember_field = QtGui.QCheckBox()
269 remember_field.setChecked(self.auth_store.remember)
270 remember_field.setText(_('Remember these details while EasyMercurial is running'))
271 layout.addWidget(remember_field, 3, 1)
272
273 bb = QtGui.QDialogButtonBox()
274 ok = bb.addButton(bb.Ok)
275 cancel = bb.addButton(bb.Cancel)
276 cancel.setDefault(False)
277 cancel.setAutoDefault(False)
278 ok.setDefault(True)
279 bb.connect(ok, Qt.SIGNAL("clicked()"), dialog, Qt.SLOT("accept()"))
280 bb.connect(cancel, Qt.SIGNAL("clicked()"), dialog, Qt.SLOT("reject()"))
281 layout.addWidget(bb, 4, 0, 1, 2)
282
283 dialog.setWindowTitle(_('EasyMercurial: Login'))
284 dialog.show()
285
286 if not self.auth_store.user:
287 user_field.setFocus(True)
288 elif not self.auth_store.passwd:
289 passwd_field.setFocus(True)
290 else:
291 ok.setFocus(True)
292
293 dialog.raise_()
294 ok = dialog.exec_()
295 if not ok:
296 raise util.Abort(_('password entry cancelled'))
297
298 self.auth_store.user = user_field.text()
299 self.auth_store.passwd = passwd_field.text()
300
301 if remember_field:
302 self.auth_store.remember = remember_field.isChecked()
303
304 self.auth_store.save()
305
306 return (self.auth_store.user, self.auth_store.passwd)
143 307
144 308
145 def uisetup(ui): 309 def uisetup(ui):
146 if not easyhg_pyqt_ok: 310 if not easyhg_pyqt_ok:
147 raise util.Abort(_('Failed to load PyQt4 module required by easyhg.py')) 311 raise util.Abort(_('Failed to load PyQt4 module required by easyhg.py'))
169 user, passwd = authinfo 333 user, passwd = authinfo
170 334
171 if user and passwd: 335 if user and passwd:
172 return orig_find(self, realm, authuri) 336 return orig_find(self, realm, authuri)
173 337
174 # self.ui.write("want username and/or password for %s\n" % authuri) 338 self.ui.write("want username and/or password for %s\n" % authuri)
175 339
176 short_uri = canonical_url(authuri) 340 dialog = EasyHgAuthDialog(self.ui, authuri, user, passwd)
177 pathless_uri = pathless_url(authuri) 341
178 342 (user, passwd) = dialog.ask()
179 authkey = self.ui.config('easyhg', 'authkey')
180 authfile = self.ui.config('easyhg', 'authfile')
181 use_authfile = (easyhg_authfile_imports_ok and authkey and authfile)
182 if authfile: authfile = os.path.expanduser(authfile)
183
184 dialog = QtGui.QDialog()
185 layout = QtGui.QGridLayout()
186 dialog.setLayout(layout)
187
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)
189
190 user_field = QtGui.QLineEdit()
191 if user:
192 user_field.setText(user)
193 layout.addWidget(QtGui.QLabel(_('User:')), 1, 0)
194 layout.addWidget(user_field, 1, 1)
195
196 passwd_field = QtGui.QLineEdit()
197 passwd_field.setEchoMode(QtGui.QLineEdit.Password)
198 if passwd:
199 passwd_field.setText(passwd)
200 layout.addWidget(QtGui.QLabel(_('Password:')), 2, 0)
201 layout.addWidget(passwd_field, 2, 1)
202
203 user_field.connect(user_field, Qt.SIGNAL("textChanged(QString)"),
204 passwd_field, Qt.SLOT("clear()"))
205
206 remember_field = None
207 remember = False
208
209 authconfig = None
210 authdata = None
211
212 if use_authfile:
213
214 authconfig = ConfigParser.RawConfigParser()
215 load_config(authconfig, authfile)
216 remember = get_boolean_from_config(authconfig, 'preferences',
217 'remember', False)
218
219 if not user:
220 authdata = get_from_config(authconfig, 'user',
221 remote_key(short_uri, '', authkey))
222 if authdata:
223 user = decrypt_salted(authdata, authkey)
224 user_field.setText(user)
225
226 if not passwd:
227 authdata = get_from_config(authconfig, 'auth',
228 remote_key(pathless_uri, user, authkey))
229 if authdata:
230 passwd = decrypt_salted(authdata, authkey)
231 passwd_field.setText(passwd)
232
233 remember_field = QtGui.QCheckBox()
234 remember_field.setChecked(remember)
235 remember_field.setText(_('Remember these details while EasyMercurial is running'))
236 layout.addWidget(remember_field, 3, 1)
237
238 bb = QtGui.QDialogButtonBox()
239 ok = bb.addButton(bb.Ok)
240 cancel = bb.addButton(bb.Cancel)
241 cancel.setDefault(False)
242 cancel.setAutoDefault(False)
243 ok.setDefault(True)
244 bb.connect(ok, Qt.SIGNAL("clicked()"), dialog, Qt.SLOT("accept()"))
245 bb.connect(cancel, Qt.SIGNAL("clicked()"), dialog, Qt.SLOT("reject()"))
246 layout.addWidget(bb, 4, 0, 1, 2)
247
248 dialog.setWindowTitle(_('EasyMercurial: Login'))
249 dialog.show()
250
251 if not user:
252 user_field.setFocus(True)
253 elif not passwd:
254 passwd_field.setFocus(True)
255 else:
256 ok.setFocus(True)
257
258 dialog.raise_()
259 ok = dialog.exec_()
260 if not ok:
261 raise util.Abort(_('password entry cancelled'))
262
263 user = user_field.text()
264 passwd = passwd_field.text()
265
266 if use_authfile:
267 remember = remember_field.isChecked()
268 set_to_config(authconfig, 'preferences', 'remember', remember)
269 if user:
270 if remember:
271 authdata = encrypt_salted(user, authkey)
272 set_to_config(authconfig, 'user', remote_key(short_uri, '', authkey), authdata)
273 else:
274 set_to_config(authconfig, 'user', remote_key(short_uri, '', authkey), '')
275 if passwd:
276 if remember:
277 authdata = encrypt_salted(passwd, authkey)
278 set_to_config(authconfig, 'auth', remote_key(pathless_uri, user, authkey), authdata)
279 else:
280 set_to_config(authconfig, 'auth', remote_key(pathless_uri, user, authkey), '')
281 save_config(self.ui, authconfig, authfile)
282 343
283 self.add_password(realm, authuri, user, passwd) 344 self.add_password(realm, authuri, user, passwd)
284 return (user, passwd) 345 return (user, passwd)
285 346
286 347