annotate easyhg.py @ 672:88fa1544b407

Merge from branch qt5. There's much more to be done before we can make another release, but clearly it's going to be done using qt5
author Chris Cannam
date Wed, 05 Dec 2018 09:44:10 +0000
parents e34de484415c
children e4d18e8ef430
rev   line source
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@560 7 # Copyright (c) 2010-2012 Chris Cannam
Chris@560 8 # Copyright (c) 2010-2012 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@503 16 import sys, os, stat, urllib, urllib2, urlparse, 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@656 47 from PyQt4 import QtCore, 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@602 56
Chris@437 57 try:
Chris@437 58 from Crypto.Cipher import AES
Chris@602 59 except ImportError:
Chris@602 60 print "EasyHg: Failed to import Crypto.Cipher module required for authfile support (try installing PyCrypto?)"
Chris@602 61 easyhg_authfile_imports_ok = False
Chris@602 62
Chris@602 63 try:
Chris@437 64 import ConfigParser # Mercurial version won't write files
Chris@437 65 import base64
Chris@437 66 except ImportError:
Chris@602 67 print "EasyHg: Failed to import modules (ConfigParser, base64) required for authfile support"
Chris@437 68 easyhg_authfile_imports_ok = False
Chris@433 69
Chris@431 70
Chris@452 71 class EasyHgAuthStore(object):
Chris@431 72
Chris@452 73 def __init__(self, ui, url, user, passwd):
Chris@452 74
Chris@452 75 self.ui = ui
Chris@452 76 self.remote_url = url
Chris@452 77
Chris@452 78 self.user = user
Chris@452 79 self.passwd = passwd
Chris@452 80
Chris@452 81 self.auth_key = self.ui.config('easyhg', 'authkey')
Chris@452 82 self.auth_file = self.ui.config('easyhg', 'authfile')
Chris@452 83
Chris@452 84 self.use_auth_file = (easyhg_authfile_imports_ok and
Chris@452 85 self.auth_key and self.auth_file)
Chris@452 86
Chris@458 87 self.auth_config = None
Chris@458 88 self.auth_cipher = None
Chris@458 89 self.remember = False
Chris@458 90
Chris@452 91 if self.use_auth_file:
Chris@602 92 self.auth_cipher = AES.new(self.auth_key, AES.MODE_CBC,
Chris@602 93 os.urandom(16))
Chris@452 94 self.auth_file = os.path.expanduser(self.auth_file)
Chris@452 95 self.load_auth_data()
Chris@452 96
Chris@452 97 def save(self):
Chris@452 98 if self.use_auth_file:
Chris@452 99 self.save_auth_data()
Chris@452 100
Chris@452 101 def encrypt(self, text):
Chris@452 102 iv = os.urandom(12)
Chris@452 103 text = '%s.%d.%s.easyhg' % (base64.b64encode(iv), len(text), text)
Chris@452 104 text += (16 - (len(text) % 16)) * ' '
Chris@452 105 ctext = base64.b64encode(self.auth_cipher.encrypt(text))
Chris@452 106 return ctext
Chris@452 107
Chris@452 108 def decrypt(self, ctext):
Chris@448 109 try:
Chris@456 110 text = self.auth_cipher.decrypt(base64.b64decode(ctext))
Chris@456 111 (iv, d, text) = text.partition('.')
Chris@456 112 (tlen, d, text) = text.partition('.')
Chris@452 113 return text[0:int(tlen)]
Chris@448 114 except:
Chris@452 115 self.ui.write("failed to decrypt/convert cached data!")
Chris@452 116 return ''
Chris@452 117
Chris@452 118 def argless_url(self):
Chris@452 119 parsed = urlparse.urlparse(self.remote_url)
Chris@452 120 return "%s://%s%s" % (parsed.scheme, parsed.netloc, parsed.path)
Chris@452 121
Chris@452 122 def pathless_url(self):
Chris@452 123 parsed = urlparse.urlparse(self.remote_url)
Chris@452 124 return "%s://%s" % (parsed.scheme, parsed.netloc)
Chris@452 125
Chris@452 126 def load_config(self):
Chris@453 127 if not self.auth_config:
Chris@453 128 self.auth_config = ConfigParser.RawConfigParser()
Chris@452 129 fp = None
Chris@452 130 try:
Chris@452 131 fp = open(self.auth_file)
Chris@452 132 except:
Chris@452 133 self.ui.write("unable to read authfile %s, ignoring\n" % self.auth_file)
Chris@452 134 return
Chris@452 135 self.auth_config.readfp(fp)
Chris@452 136 fp.close()
Chris@452 137
Chris@452 138 def save_config(self):
Chris@452 139 ofp = None
Chris@452 140 try:
Chris@452 141 ofp = open(self.auth_file, 'w')
Chris@452 142 except:
Chris@452 143 self.ui.write("failed to open authfile %s for writing\n" % self.auth_file)
Chris@448 144 raise
Chris@503 145 if os.name == 'posix':
Chris@452 146 try:
Chris@452 147 os.fchmod(ofp.fileno(), stat.S_IRUSR | stat.S_IWUSR)
Chris@452 148 except:
Chris@452 149 ofp.close()
Chris@452 150 self.ui.write("failed to set permissions on authfile %s\n" % self.auth_file)
Chris@452 151 raise
Chris@452 152 self.auth_config.write(ofp)
Chris@452 153 ofp.close()
Chris@434 154
Chris@452 155 def get_from_config(self, sect, key):
Chris@452 156 data = None
Chris@452 157 try:
Chris@452 158 data = self.auth_config.get(sect, key)
Chris@452 159 except (ConfigParser.NoOptionError, ConfigParser.NoSectionError):
Chris@452 160 pass
Chris@452 161 return data
Chris@436 162
Chris@452 163 def get_boolean_from_config(self, sect, key, deflt):
Chris@452 164 data = deflt
Chris@452 165 try:
Chris@452 166 data = self.auth_config.getboolean(sect, key)
Chris@452 167 except (ConfigParser.NoOptionError, ConfigParser.NoSectionError):
Chris@452 168 pass
Chris@452 169 return data
Chris@436 170
Chris@452 171 def set_to_config(self, sect, key, data):
Chris@452 172 if not self.auth_config.has_section(sect):
Chris@452 173 self.auth_config.add_section(sect)
Chris@452 174 self.auth_config.set(sect, key, data)
Chris@436 175
Chris@452 176 def remote_key(self, url, user):
Chris@452 177 # generate a "safe-for-config-file" key representing uri+user
Chris@459 178 # self.ui.write('generating remote_key for url %s and user %s\n' % (url, user))
Chris@452 179 s = '%s@@%s' % (url, user)
Chris@452 180 h = hashlib.sha1()
Chris@452 181 h.update(self.auth_key)
Chris@452 182 h.update(s)
Chris@452 183 hx = h.hexdigest()
Chris@452 184 return hx
Chris@452 185
Chris@452 186 def remote_user_key(self):
Chris@453 187 return self.remote_key(self.pathless_url(), '')
Chris@452 188
Chris@452 189 def remote_passwd_key(self):
Chris@452 190 return self.remote_key(self.pathless_url(), self.user)
Chris@452 191
Chris@452 192 def load_auth_data(self):
Chris@452 193
Chris@452 194 self.load_config()
Chris@452 195 if not self.auth_config: return
Chris@452 196
Chris@452 197 self.remember = self.get_boolean_from_config(
Chris@452 198 'preferences', 'remember', False)
Chris@452 199
Chris@452 200 if not self.user:
Chris@452 201 d = self.get_from_config('user', self.remote_user_key())
Chris@452 202 if d:
Chris@452 203 self.user = self.decrypt(d)
Chris@452 204
Chris@452 205 if self.user:
Chris@452 206 d = self.get_from_config('auth', self.remote_passwd_key())
Chris@452 207 if d:
Chris@452 208 self.passwd = self.decrypt(d)
Chris@452 209
Chris@452 210 def save_auth_data(self):
Chris@452 211
Chris@453 212 self.load_config()
Chris@452 213 if not self.auth_config: return
Chris@453 214
Chris@452 215 self.set_to_config('preferences', 'remember', self.remember)
Chris@452 216
Chris@459 217 # self.ui.write('aiming to store details for user %s\n' % self.user)
Chris@452 218
Chris@452 219 if self.remember and self.user:
Chris@452 220 d = self.encrypt(self.user)
Chris@452 221 self.set_to_config('user', self.remote_user_key(), d)
Chris@452 222 else:
Chris@452 223 self.set_to_config('user', self.remote_user_key(), '')
Chris@452 224
Chris@452 225 if self.remember and self.user and self.passwd:
Chris@452 226 d = self.encrypt(self.passwd)
Chris@452 227 self.set_to_config('auth', self.remote_passwd_key(), d)
Chris@452 228 elif self.user:
Chris@452 229 self.set_to_config('auth', self.remote_passwd_key(), '')
Chris@452 230
Chris@452 231 self.save_config()
Chris@452 232
Chris@452 233 class EasyHgAuthDialog(object):
Chris@452 234
Chris@452 235 auth_store = None
Chris@452 236
Chris@452 237 def __init__(self, ui, url, user, passwd):
Chris@452 238 self.auth_store = EasyHgAuthStore(ui, url, user, passwd)
Chris@452 239
Chris@470 240 def ask(self, repeat):
Chris@459 241
Chris@460 242 if self.auth_store.user and self.auth_store.passwd and self.auth_store.remember:
Chris@470 243 if not repeat:
Chris@459 244 return (self.auth_store.user, self.auth_store.passwd)
Chris@459 245
Chris@452 246 dialog = QtGui.QDialog()
Chris@452 247 layout = QtGui.QGridLayout()
Chris@452 248 dialog.setLayout(layout)
Chris@452 249
Chris@470 250 heading = _('Login required')
Chris@470 251 if repeat:
Chris@470 252 heading = _('Login failed: please try again')
Chris@470 253 label_text = _(('<h3>%s</h3><p>Please provide your login details for the repository at<br><code>%s</code>:') % (heading, self.auth_store.argless_url()))
Chris@470 254 layout.addWidget(QtGui.QLabel(label_text), 0, 0, 1, 2)
Chris@452 255
Chris@452 256 user_field = QtGui.QLineEdit()
Chris@452 257 if self.auth_store.user: user_field.setText(self.auth_store.user)
Chris@452 258 layout.addWidget(QtGui.QLabel(_('User:')), 1, 0)
Chris@452 259 layout.addWidget(user_field, 1, 1)
Chris@452 260
Chris@452 261 passwd_field = QtGui.QLineEdit()
Chris@452 262 passwd_field.setEchoMode(QtGui.QLineEdit.Password)
Chris@452 263 if self.auth_store.passwd: passwd_field.setText(self.auth_store.passwd)
Chris@452 264 layout.addWidget(QtGui.QLabel(_('Password:')), 2, 0)
Chris@452 265 layout.addWidget(passwd_field, 2, 1)
chris@656 266 user_field.textChanged.connect(passwd_field.clear)
Chris@452 267
Chris@452 268 remember_field = None
Chris@452 269 if self.auth_store.use_auth_file:
Chris@452 270 remember_field = QtGui.QCheckBox()
Chris@452 271 remember_field.setChecked(self.auth_store.remember)
Chris@452 272 remember_field.setText(_('Remember these details while EasyMercurial is running'))
Chris@452 273 layout.addWidget(remember_field, 3, 1)
Chris@457 274 warning_field = QtGui.QLabel()
Chris@461 275 warning_field.setText(_('<qt><i><small>Do not use this option if anyone else has access to your computer!</small></i><br></qt>'))
Chris@457 276 warning_field.hide()
chris@656 277 remember_field.clicked.connect(warning_field.show)
Chris@457 278 layout.addWidget(warning_field, 4, 1, QtCore.Qt.AlignRight)
Chris@452 279
Chris@452 280 bb = QtGui.QDialogButtonBox()
Chris@452 281 ok = bb.addButton(bb.Ok)
Chris@452 282 cancel = bb.addButton(bb.Cancel)
Chris@452 283 cancel.setDefault(False)
Chris@452 284 cancel.setAutoDefault(False)
Chris@452 285 ok.setDefault(True)
chris@656 286 ok.clicked.connect(dialog.accept)
chris@656 287 cancel.clicked.connect(dialog.reject)
Chris@457 288 layout.addWidget(bb, 5, 0, 1, 2)
Chris@452 289
Chris@452 290 dialog.setWindowTitle(_('EasyMercurial: Login'))
Chris@452 291 dialog.show()
Chris@452 292
Chris@452 293 if not self.auth_store.user:
Chris@452 294 user_field.setFocus(True)
Chris@452 295 elif not self.auth_store.passwd:
Chris@452 296 passwd_field.setFocus(True)
Chris@452 297 else:
Chris@452 298 ok.setFocus(True)
Chris@452 299
Chris@452 300 dialog.raise_()
Chris@452 301 ok = dialog.exec_()
Chris@452 302 if not ok:
Chris@452 303 raise util.Abort(_('password entry cancelled'))
Chris@452 304
Chris@452 305 self.auth_store.user = user_field.text()
Chris@452 306 self.auth_store.passwd = passwd_field.text()
Chris@452 307
Chris@452 308 if remember_field:
Chris@452 309 self.auth_store.remember = remember_field.isChecked()
Chris@452 310
Chris@452 311 self.auth_store.save()
Chris@452 312
Chris@452 313 return (self.auth_store.user, self.auth_store.passwd)
Chris@438 314
Chris@440 315
Chris@440 316 def uisetup(ui):
Chris@440 317 if not easyhg_pyqt_ok:
Chris@440 318 raise util.Abort(_('Failed to load PyQt4 module required by easyhg.py'))
Chris@440 319 global easyhg_qtapp
Chris@440 320 easyhg_qtapp = QtGui.QApplication([])
Chris@440 321
Chris@440 322 def monkeypatch_method(cls):
Chris@440 323 def decorator(func):
Chris@440 324 setattr(cls, func.__name__, func)
Chris@440 325 return func
Chris@440 326 return decorator
Chris@440 327
Chris@440 328 orig_find = passwordmgr.find_user_password
Chris@440 329
Chris@427 330 @monkeypatch_method(passwordmgr)
Chris@427 331 def find_user_password(self, realm, authuri):
Chris@427 332
Chris@459 333 if not hasattr(self, '__easyhg_last'):
Chris@459 334 self.__easyhg_last = None
Chris@459 335
Chris@427 336 if not self.ui.interactive():
Chris@427 337 return orig_find(self, realm, authuri)
Chris@427 338 if not easyhg_pyqt_ok:
Chris@427 339 return orig_find(self, realm, authuri)
Chris@427 340
Chris@427 341 authinfo = urllib2.HTTPPasswordMgrWithDefaultRealm.find_user_password(
Chris@427 342 self, realm, authuri)
Chris@427 343 user, passwd = authinfo
Chris@427 344
Chris@470 345 repeat = False
Chris@459 346
Chris@459 347 if (realm, authuri) == self.__easyhg_last:
Chris@459 348 # If we are called again just after identical previous
Chris@459 349 # request, then the previously returned auth must have been
Chris@459 350 # wrong. So we note this to force password prompt (and avoid
Chris@459 351 # reusing bad password indefinitely). Thanks to
Chris@459 352 # mercurial_keyring (Marcin Kasperski) for this logic
Chris@470 353 repeat = True
Chris@470 354
Chris@470 355 if user and passwd and not repeat:
Chris@470 356 return orig_find(self, realm, authuri)
Chris@427 357
Chris@452 358 dialog = EasyHgAuthDialog(self.ui, authuri, user, passwd)
Chris@427 359
Chris@470 360 (user, passwd) = dialog.ask(repeat)
Chris@433 361
Chris@470 362 self.add_password(realm, authuri, user, passwd)
Chris@459 363 self.__easyhg_last = (realm, authuri)
Chris@427 364 return (user, passwd)
Chris@427 365
Chris@427 366