Chris@427: # -*- coding: utf-8 -*- Chris@427: # Chris@427: # EasyMercurial Chris@427: # Chris@427: # Based on hgExplorer by Jari Korhonen Chris@427: # Copyright (c) 2010 Jari Korhonen Chris@427: # Copyright (c) 2010 Chris Cannam Chris@427: # Copyright (c) 2010 Queen Mary, University of London Chris@427: # Chris@427: # This program is free software; you can redistribute it and/or Chris@427: # modify it under the terms of the GNU General Public License as Chris@427: # published by the Free Software Foundation; either version 2 of the Chris@427: # License, or (at your option) any later version. See the file Chris@427: # COPYING included with this distribution for more information. Chris@427: Chris@449: import sys, os, stat, urllib, urllib2, urlparse, platform, hashlib Chris@427: Chris@438: from mercurial.i18n import _ Chris@433: from mercurial import ui, util, error Chris@427: try: Chris@427: from mercurial.url import passwordmgr Chris@427: except: Chris@427: from mercurial.httprepo import passwordmgr Chris@427: Chris@427: # The value assigned here may be modified during installation, by Chris@427: # replacing its default value with another one. We can't compare Chris@427: # against its default value, because then the comparison text would Chris@427: # get modified as well. So, compare using prefix only. Chris@427: # Chris@427: easyhg_import_path = 'NO_EASYHG_IMPORT_PATH' Chris@427: if not easyhg_import_path.startswith('NO_'): Chris@427: # We have an installation path: append it twice, once with Chris@427: # the Python version suffixed Chris@440: version_suffix = 'Py%d.%d' % (sys.version_info[0], sys.version_info[1]) Chris@427: sys.path.append(easyhg_import_path + "/" + version_suffix) Chris@427: sys.path.append(easyhg_import_path) Chris@427: Chris@427: # Try to load the PyQt4 module that we need. If this fails, we should Chris@427: # bail out later (in uisetup), because if we bail out now, Mercurial Chris@427: # will just continue without us and report success. The invoking Chris@427: # application needs to be able to discover whether the module load Chris@427: # succeeded or not, so we need to ensure that Mercurial itself returns Chris@427: # failure if it didn't. Chris@427: # Chris@427: easyhg_pyqt_ok = True Chris@427: try: Chris@427: from PyQt4 import Qt, QtGui Chris@427: except ImportError: Chris@427: easyhg_pyqt_ok = False Chris@427: easyhg_qtapp = None Chris@427: Chris@440: # These imports are optional, we just can't use the authfile (i.e. Chris@438: # "remember this password") feature without them Chris@438: # Chris@437: easyhg_authfile_imports_ok = True Chris@437: try: Chris@437: from Crypto.Cipher import AES Chris@437: import ConfigParser # Mercurial version won't write files Chris@437: import base64 Chris@437: except ImportError: Chris@445: print "EasyHg: Failed to import required modules for authfile support" Chris@437: easyhg_authfile_imports_ok = False Chris@433: Chris@431: Chris@439: def encrypt_salted(text, key): Chris@439: salt = os.urandom(8) Chris@439: text = '%d.%s.%s' % (len(text), base64.b64encode(salt), text) Chris@431: text += (16 - len(text) % 16) * ' ' Chris@446: cipher = AES.new(key, AES.MODE_CBC) Chris@431: return base64.b64encode(cipher.encrypt(text)) Chris@431: Chris@439: def decrypt_salted(ctext, key): Chris@446: cipher = AES.new(key, AES.MODE_CBC) Chris@431: text = cipher.decrypt(base64.b64decode(ctext)) Chris@431: (tlen, d, text) = text.partition('.') Chris@439: (salt, d, text) = text.partition('.') Chris@431: return text[0:int(tlen)] Chris@431: Chris@427: # from mercurial_keyring by Marcin Kasperski Chris@427: def canonical_url(authuri): Chris@427: parsed_url = urlparse.urlparse(authuri) Chris@427: return "%s://%s%s" % (parsed_url.scheme, parsed_url.netloc, Chris@427: parsed_url.path) Chris@427: Chris@436: def load_config(pcfg, pfile): Chris@434: fp = None Chris@434: try: Chris@434: fp = open(pfile) Chris@434: except: Chris@440: return Chris@440: pcfg.readfp(fp) Chris@440: fp.close() Chris@434: Chris@444: def save_config(ui, pcfg, pfile): Chris@434: ofp = None Chris@434: try: Chris@434: ofp = open(pfile, 'w') Chris@434: except: Chris@444: ui.write("failed to open authfile %s for writing\n" % pfile) Chris@434: raise Chris@448: if platform.system() != 'Windows': Chris@448: try: Chris@448: os.fchmod(ofp.fileno(), stat.S_IRUSR | stat.S_IWUSR) Chris@448: except: Chris@448: ofp.close() Chris@448: ui.write("failed to set permissions on authfile %s\n" % pfile) Chris@448: raise Chris@434: pcfg.write(ofp) Chris@434: ofp.close() Chris@434: Chris@436: def get_from_config(pcfg, sect, key): Chris@436: data = None Chris@436: try: Chris@436: data = pcfg.get(sect, key) Chris@436: except (ConfigParser.NoOptionError, ConfigParser.NoSectionError): Chris@436: pass Chris@436: return data Chris@436: Chris@436: def get_boolean_from_config(pcfg, sect, key, deflt): Chris@436: data = deflt Chris@436: try: Chris@436: data = pcfg.getboolean(sect, key) Chris@436: except (ConfigParser.NoOptionError, ConfigParser.NoSectionError): Chris@436: pass Chris@436: return data Chris@436: Chris@436: def set_to_config(pcfg, sect, key, data): Chris@436: if not pcfg.has_section(sect): Chris@436: pcfg.add_section(sect) Chris@436: pcfg.set(sect, key, data) Chris@436: Chris@449: def remote_key(uri, user, key): Chris@440: # generate a "safe-for-config-file" key representing uri+user Chris@449: s = '%s@@%s' % (uri, user) Chris@449: h = hashlib.sha1() Chris@449: h.update(key) Chris@449: h.update(s) Chris@449: return h.hexdigest() Chris@438: Chris@440: Chris@440: def uisetup(ui): Chris@440: if not easyhg_pyqt_ok: Chris@440: raise util.Abort(_('Failed to load PyQt4 module required by easyhg.py')) Chris@440: global easyhg_qtapp Chris@440: easyhg_qtapp = QtGui.QApplication([]) Chris@440: Chris@440: def monkeypatch_method(cls): Chris@440: def decorator(func): Chris@440: setattr(cls, func.__name__, func) Chris@440: return func Chris@440: return decorator Chris@440: Chris@440: orig_find = passwordmgr.find_user_password Chris@440: Chris@427: @monkeypatch_method(passwordmgr) Chris@427: def find_user_password(self, realm, authuri): Chris@427: Chris@427: if not self.ui.interactive(): Chris@427: return orig_find(self, realm, authuri) Chris@427: if not easyhg_pyqt_ok: Chris@427: return orig_find(self, realm, authuri) Chris@427: Chris@427: authinfo = urllib2.HTTPPasswordMgrWithDefaultRealm.find_user_password( Chris@427: self, realm, authuri) Chris@427: user, passwd = authinfo Chris@427: Chris@427: if user and passwd: Chris@427: return orig_find(self, realm, authuri) Chris@427: Chris@440: # self.ui.write("want username and/or password for %s\n" % authuri) Chris@427: Chris@440: short_uri = canonical_url(authuri) Chris@427: Chris@440: authkey = self.ui.config('easyhg', 'authkey') Chris@440: authfile = self.ui.config('easyhg', 'authfile') Chris@440: use_authfile = (easyhg_authfile_imports_ok and authkey and authfile) Chris@450: if authfile: authfile = os.path.expanduser(authfile) Chris@427: Chris@427: dialog = QtGui.QDialog() Chris@427: layout = QtGui.QGridLayout() Chris@427: dialog.setLayout(layout) Chris@427: Chris@440: layout.addWidget(QtGui.QLabel(_('
Please provide your login details for the repository at%s
:') % short_uri), 0, 0, 1, 2)
Chris@427:
Chris@440: user_field = QtGui.QLineEdit()
Chris@427: if user:
Chris@440: user_field.setText(user)
Chris@427: layout.addWidget(QtGui.QLabel(_('User:')), 1, 0)
Chris@440: layout.addWidget(user_field, 1, 1)
Chris@427:
Chris@440: passwd_field = QtGui.QLineEdit()
Chris@440: passwd_field.setEchoMode(QtGui.QLineEdit.Password)
Chris@427: if passwd:
Chris@440: passwd_field.setText(passwd)
Chris@427: layout.addWidget(QtGui.QLabel(_('Password:')), 2, 0)
Chris@440: layout.addWidget(passwd_field, 2, 1)
Chris@427:
Chris@440: user_field.connect(user_field, Qt.SIGNAL("textChanged(QString)"),
Chris@440: passwd_field, Qt.SLOT("clear()"))
Chris@435:
Chris@440: remember_field = None
Chris@440: remember = False
Chris@450:
Chris@440: authconfig = None
Chris@450: authdata = None
Chris@433:
Chris@436: if use_authfile:
Chris@450:
Chris@440: authconfig = ConfigParser.RawConfigParser()
Chris@440: load_config(authconfig, authfile)
Chris@440: remember = get_boolean_from_config(authconfig, 'preferences',
Chris@440: 'remember', False)
Chris@450:
Chris@450: if not user:
Chris@450: authdata = get_from_config(authconfig, 'user',
Chris@450: remote_key(short_uri, '', authkey))
Chris@450: if authdata:
Chris@450: user = decrypt_salted(authdata, authkey)
Chris@450: user_field.setText(user)
Chris@450:
Chris@450: if not passwd:
Chris@450: authdata = get_from_config(authconfig, 'auth',
Chris@450: remote_key(short_uri, user, authkey))
Chris@450: if authdata:
Chris@450: passwd = decrypt_salted(authdata, authkey)
Chris@450: passwd_field.setText(passwd)
Chris@450:
Chris@440: remember_field = QtGui.QCheckBox()
Chris@440: remember_field.setChecked(remember)
Chris@450: remember_field.setText(_('Remember these details while EasyMercurial is running'))
Chris@440: layout.addWidget(remember_field, 3, 1)
Chris@431:
Chris@427: bb = QtGui.QDialogButtonBox()
Chris@427: ok = bb.addButton(bb.Ok)
Chris@427: cancel = bb.addButton(bb.Cancel)
Chris@427: cancel.setDefault(False)
Chris@427: cancel.setAutoDefault(False)
Chris@427: ok.setDefault(True)
Chris@427: bb.connect(ok, Qt.SIGNAL("clicked()"), dialog, Qt.SLOT("accept()"))
Chris@427: bb.connect(cancel, Qt.SIGNAL("clicked()"), dialog, Qt.SLOT("reject()"))
Chris@431: layout.addWidget(bb, 4, 0, 1, 2)
Chris@427:
Chris@428: dialog.setWindowTitle(_('EasyMercurial: Login'))
Chris@427: dialog.show()
Chris@428:
Chris@428: if not user:
Chris@440: user_field.setFocus(True)
Chris@428: elif not passwd:
Chris@440: passwd_field.setFocus(True)
Chris@450: else:
Chris@450: ok.setFocus(True)
Chris@428:
Chris@427: dialog.raise_()
Chris@427: ok = dialog.exec_()
Chris@436: if not ok:
Chris@436: raise util.Abort(_('password entry cancelled'))
Chris@431:
Chris@440: user = user_field.text()
Chris@440: passwd = passwd_field.text()
Chris@432:
Chris@436: if use_authfile:
Chris@440: remember = remember_field.isChecked()
Chris@440: set_to_config(authconfig, 'preferences', 'remember', remember)
Chris@438: if user:
Chris@450: if remember:
Chris@450: authdata = encrypt_salted(user, authkey)
Chris@450: set_to_config(authconfig, 'user', remote_key(short_uri, '', authkey), authdata)
Chris@450: else:
Chris@450: set_to_config(authconfig, 'user', remote_key(short_uri, '', authkey), '')
Chris@450: if passwd:
Chris@450: if remember:
Chris@440: authdata = encrypt_salted(passwd, authkey)
Chris@449: set_to_config(authconfig, 'auth', remote_key(short_uri, user, authkey), authdata)
Chris@438: else:
Chris@449: set_to_config(authconfig, 'auth', remote_key(short_uri, user, authkey), '')
Chris@444: save_config(self.ui, authconfig, authfile)
Chris@433:
Chris@436: self.add_password(realm, authuri, user, passwd)
Chris@427: return (user, passwd)
Chris@427:
Chris@427: