Mercurial > hg > easyhg
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 |