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@432: import sys, os, stat Chris@427: Chris@427: import urllib, urllib2, urlparse Chris@427: 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: from mercurial.i18n import _ 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@427: version_suffix = "Py" + str(sys.version_info[0]) + "." + str(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: Chris@427: easyhg_qtapp = None Chris@427: Chris@432: #!!! same as above for this? or just continue without remember feature? Chris@431: from Crypto.Cipher import AES Chris@431: import base64 Chris@431: Chris@433: import ConfigParser # Mercurial version won't write files Chris@433: Chris@431: #!!! should be in a class here Chris@431: Chris@431: def encrypt(text, key): Chris@431: text = '%d.%s' % (len(text), text) Chris@431: text += (16 - len(text) % 16) * ' ' Chris@431: cipher = AES.new(key) Chris@431: return base64.b64encode(cipher.encrypt(text)) Chris@431: Chris@431: def decrypt(ctext, key): Chris@431: cipher = AES.new(key) Chris@431: text = cipher.decrypt(base64.b64decode(ctext)) Chris@431: (tlen, d, text) = text.partition('.') Chris@431: return text[0:int(tlen)] Chris@431: Chris@427: def monkeypatch_method(cls): Chris@427: def decorator(func): Chris@427: setattr(cls, func.__name__, func) Chris@427: return func Chris@427: return decorator Chris@427: Chris@427: def uisetup(ui): Chris@427: if not easyhg_pyqt_ok: Chris@427: raise util.Abort(_('Failed to load PyQt4 module required by easyhg.py')) Chris@427: global easyhg_qtapp Chris@427: easyhg_qtapp = QtGui.QApplication([]) Chris@427: Chris@427: orig_find = passwordmgr.find_user_password Chris@427: Chris@427: # from mercurial_keyring by Marcin Kasperski Chris@427: def canonical_url(authuri): Chris@427: """ Chris@427: Strips query params from url. Used to convert urls like Chris@427: https://repo.machine.com/repos/apps/module?pairs=0000000000000000000000000000000000000000-0000000000000000000000000000000000000000&cmd=between Chris@427: to Chris@427: https://repo.machine.com/repos/apps/module Chris@427: """ 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@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@427: self.ui.write("want username and/or password for %s\n" % authuri) Chris@427: Chris@427: uri = canonical_url(authuri) Chris@427: Chris@433: pkey = base64.b64encode('%s@@%s' % (uri, user)).replace('=', '_') Chris@431: pekey = self.ui.config('easyhg', 'authkey') Chris@432: pfile = os.path.expanduser(self.ui.config('easyhg', 'authfile')) Chris@431: pdata = None Chris@431: Chris@431: self.ui.write("pekey is %s\n" % pekey) Chris@431: self.ui.write("pfile is %s\n" % pfile) Chris@427: Chris@427: dialog = QtGui.QDialog() Chris@427: layout = QtGui.QGridLayout() Chris@427: dialog.setLayout(layout) Chris@427: Chris@432: layout.addWidget(QtGui.QLabel(_('

Login required

Please provide your login details for the repository at
%s:') % uri), 0, 0, 1, 2) Chris@427: Chris@427: userfield = QtGui.QLineEdit() Chris@427: if user: Chris@427: userfield.setText(user) Chris@427: layout.addWidget(QtGui.QLabel(_('User:')), 1, 0) Chris@427: layout.addWidget(userfield, 1, 1) Chris@427: Chris@427: passfield = QtGui.QLineEdit() Chris@427: passfield.setEchoMode(QtGui.QLineEdit.Password) Chris@427: if passwd: Chris@428: passfield.setText(passwd) Chris@427: layout.addWidget(QtGui.QLabel(_('Password:')), 2, 0) Chris@427: layout.addWidget(passfield, 2, 1) Chris@427: Chris@431: remember = None Chris@433: pcfg = None Chris@433: Chris@431: if pekey and pfile: Chris@431: # load pwd from our cache file, decrypt with given key Chris@433: pcfg = ConfigParser.RawConfigParser() Chris@431: fp = None Chris@433: remember_default = False Chris@431: try: Chris@431: fp = open(pfile) Chris@431: except: Chris@431: self.ui.write("failed to open authfile %s\n" % pfile) Chris@432: if fp and not passwd: Chris@433: pcfg.readfp(fp) Chris@433: try: Chris@433: remember_default = pcfg.getboolean('preferences', 'remember') Chris@433: except: Chris@433: remember_default = False Chris@433: try: Chris@433: pdata = pcfg.get('auth', pkey) Chris@433: except ConfigParser.NoOptionError: Chris@433: pdata = None Chris@432: if pdata: Chris@432: cachedpwd = decrypt(pdata, pekey) Chris@431: passfield.setText(cachedpwd) Chris@432: fp.close() Chris@431: remember = QtGui.QCheckBox() Chris@433: remember.setChecked(remember_default) Chris@431: remember.setText(_('Remember this password until EasyMercurial exits')) Chris@431: layout.addWidget(remember, 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@428: userfield.setFocus(True) Chris@428: elif not passwd: Chris@428: passfield.setFocus(True) Chris@428: Chris@427: dialog.raise_() Chris@427: ok = dialog.exec_() Chris@427: if ok: Chris@427: self.ui.write('Dialog accepted\n') Chris@427: user = userfield.text() Chris@427: passwd = passfield.text() Chris@431: Chris@433: if pekey and pfile and remember: Chris@432: Chris@432: ofp = None Chris@432: Chris@432: try: Chris@433: ofp = open(pfile, 'w') Chris@432: except: Chris@432: self.ui.write("failed to open authfile %s for writing\n" % pfile) Chris@432: raise Chris@432: Chris@432: try: Chris@432: os.fchmod(ofp.fileno(), stat.S_IRUSR | stat.S_IWUSR) #!!! Windows equivalent? Chris@432: except: Chris@432: ofp.close() Chris@432: ofp = None Chris@432: self.ui.write("failed to set proper permissions on authfile %s\n" % pfile) Chris@432: raise Chris@432: Chris@433: #!!! add these sections first... Chris@433: Chris@433: if remember.isChecked(): Chris@432: pdata = encrypt(passwd, pekey) Chris@433: pcfg.set('auth', pkey, pdata) Chris@433: else: Chris@433: pcfg.set('auth', pkey, '') Chris@433: Chris@433: pcfg.set('preferences', 'remember', remember.isChecked()) Chris@433: pcfg.write(ofp) Chris@433: ofp.close() Chris@431: Chris@431: # if passwd and keyring_key != '' and not from_keyring: Chris@431: # keyring_key = '%s@@%s' % (uri, user) Chris@431: ## keyring.set_password('Mercurial', keyring_key, passwd) Chris@427: self.add_password(realm, authuri, user, passwd) Chris@427: else: Chris@427: raise util.Abort(_('password entry cancelled')) Chris@427: return (user, passwd) Chris@427: Chris@427: Chris@427: