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@560: # Copyright (c) 2010-2012 Chris Cannam Chris@560: # Copyright (c) 2010-2012 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@503: import sys, os, stat, urllib, urllib2, urlparse, 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@457: from PyQt4 import Qt, QtCore, 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@452: class EasyHgAuthStore(object): Chris@431: Chris@452: def __init__(self, ui, url, user, passwd): Chris@452: Chris@452: self.ui = ui Chris@452: self.remote_url = url Chris@452: Chris@452: self.user = user Chris@452: self.passwd = passwd Chris@452: Chris@452: self.auth_key = self.ui.config('easyhg', 'authkey') Chris@452: self.auth_file = self.ui.config('easyhg', 'authfile') Chris@452: Chris@452: self.use_auth_file = (easyhg_authfile_imports_ok and Chris@452: self.auth_key and self.auth_file) Chris@452: Chris@458: self.auth_config = None Chris@458: self.auth_cipher = None Chris@458: self.remember = False Chris@458: Chris@452: if self.use_auth_file: Chris@452: self.auth_cipher = AES.new(self.auth_key, AES.MODE_CBC) Chris@452: self.auth_file = os.path.expanduser(self.auth_file) Chris@452: self.load_auth_data() Chris@452: Chris@452: def save(self): Chris@452: if self.use_auth_file: Chris@452: self.save_auth_data() Chris@452: Chris@452: def encrypt(self, text): Chris@452: iv = os.urandom(12) Chris@452: text = '%s.%d.%s.easyhg' % (base64.b64encode(iv), len(text), text) Chris@452: text += (16 - (len(text) % 16)) * ' ' Chris@452: ctext = base64.b64encode(self.auth_cipher.encrypt(text)) Chris@452: return ctext Chris@452: Chris@452: def decrypt(self, ctext): Chris@448: try: Chris@456: text = self.auth_cipher.decrypt(base64.b64decode(ctext)) Chris@456: (iv, d, text) = text.partition('.') Chris@456: (tlen, d, text) = text.partition('.') Chris@452: return text[0:int(tlen)] Chris@448: except: Chris@452: self.ui.write("failed to decrypt/convert cached data!") Chris@452: return '' Chris@452: Chris@452: def argless_url(self): Chris@452: parsed = urlparse.urlparse(self.remote_url) Chris@452: return "%s://%s%s" % (parsed.scheme, parsed.netloc, parsed.path) Chris@452: Chris@452: def pathless_url(self): Chris@452: parsed = urlparse.urlparse(self.remote_url) Chris@452: return "%s://%s" % (parsed.scheme, parsed.netloc) Chris@452: Chris@452: def load_config(self): Chris@453: if not self.auth_config: Chris@453: self.auth_config = ConfigParser.RawConfigParser() Chris@452: fp = None Chris@452: try: Chris@452: fp = open(self.auth_file) Chris@452: except: Chris@452: self.ui.write("unable to read authfile %s, ignoring\n" % self.auth_file) Chris@452: return Chris@452: self.auth_config.readfp(fp) Chris@452: fp.close() Chris@452: Chris@452: def save_config(self): Chris@452: ofp = None Chris@452: try: Chris@452: ofp = open(self.auth_file, 'w') Chris@452: except: Chris@452: self.ui.write("failed to open authfile %s for writing\n" % self.auth_file) Chris@448: raise Chris@503: if os.name == 'posix': Chris@452: try: Chris@452: os.fchmod(ofp.fileno(), stat.S_IRUSR | stat.S_IWUSR) Chris@452: except: Chris@452: ofp.close() Chris@452: self.ui.write("failed to set permissions on authfile %s\n" % self.auth_file) Chris@452: raise Chris@452: self.auth_config.write(ofp) Chris@452: ofp.close() Chris@434: Chris@452: def get_from_config(self, sect, key): Chris@452: data = None Chris@452: try: Chris@452: data = self.auth_config.get(sect, key) Chris@452: except (ConfigParser.NoOptionError, ConfigParser.NoSectionError): Chris@452: pass Chris@452: return data Chris@436: Chris@452: def get_boolean_from_config(self, sect, key, deflt): Chris@452: data = deflt Chris@452: try: Chris@452: data = self.auth_config.getboolean(sect, key) Chris@452: except (ConfigParser.NoOptionError, ConfigParser.NoSectionError): Chris@452: pass Chris@452: return data Chris@436: Chris@452: def set_to_config(self, sect, key, data): Chris@452: if not self.auth_config.has_section(sect): Chris@452: self.auth_config.add_section(sect) Chris@452: self.auth_config.set(sect, key, data) Chris@436: Chris@452: def remote_key(self, url, user): Chris@452: # generate a "safe-for-config-file" key representing uri+user Chris@459: # self.ui.write('generating remote_key for url %s and user %s\n' % (url, user)) Chris@452: s = '%s@@%s' % (url, user) Chris@452: h = hashlib.sha1() Chris@452: h.update(self.auth_key) Chris@452: h.update(s) Chris@452: hx = h.hexdigest() Chris@452: return hx Chris@452: Chris@452: def remote_user_key(self): Chris@453: return self.remote_key(self.pathless_url(), '') Chris@452: Chris@452: def remote_passwd_key(self): Chris@452: return self.remote_key(self.pathless_url(), self.user) Chris@452: Chris@452: def load_auth_data(self): Chris@452: Chris@452: self.load_config() Chris@452: if not self.auth_config: return Chris@452: Chris@452: self.remember = self.get_boolean_from_config( Chris@452: 'preferences', 'remember', False) Chris@452: Chris@452: if not self.user: Chris@452: d = self.get_from_config('user', self.remote_user_key()) Chris@452: if d: Chris@452: self.user = self.decrypt(d) Chris@452: Chris@452: if self.user: Chris@452: d = self.get_from_config('auth', self.remote_passwd_key()) Chris@452: if d: Chris@452: self.passwd = self.decrypt(d) Chris@452: Chris@452: def save_auth_data(self): Chris@452: Chris@453: self.load_config() Chris@452: if not self.auth_config: return Chris@453: Chris@452: self.set_to_config('preferences', 'remember', self.remember) Chris@452: Chris@459: # self.ui.write('aiming to store details for user %s\n' % self.user) Chris@452: Chris@452: if self.remember and self.user: Chris@452: d = self.encrypt(self.user) Chris@452: self.set_to_config('user', self.remote_user_key(), d) Chris@452: else: Chris@452: self.set_to_config('user', self.remote_user_key(), '') Chris@452: Chris@452: if self.remember and self.user and self.passwd: Chris@452: d = self.encrypt(self.passwd) Chris@452: self.set_to_config('auth', self.remote_passwd_key(), d) Chris@452: elif self.user: Chris@452: self.set_to_config('auth', self.remote_passwd_key(), '') Chris@452: Chris@452: self.save_config() Chris@452: Chris@452: class EasyHgAuthDialog(object): Chris@452: Chris@452: auth_store = None Chris@452: Chris@452: def __init__(self, ui, url, user, passwd): Chris@452: self.auth_store = EasyHgAuthStore(ui, url, user, passwd) Chris@452: Chris@470: def ask(self, repeat): Chris@459: Chris@460: if self.auth_store.user and self.auth_store.passwd and self.auth_store.remember: Chris@470: if not repeat: Chris@459: return (self.auth_store.user, self.auth_store.passwd) Chris@459: Chris@452: dialog = QtGui.QDialog() Chris@452: layout = QtGui.QGridLayout() Chris@452: dialog.setLayout(layout) Chris@452: Chris@470: heading = _('Login required') Chris@470: if repeat: Chris@470: heading = _('Login failed: please try again') Chris@470: label_text = _(('

%s

Please provide your login details for the repository at
%s:') % (heading, self.auth_store.argless_url())) Chris@470: layout.addWidget(QtGui.QLabel(label_text), 0, 0, 1, 2) Chris@452: Chris@452: user_field = QtGui.QLineEdit() Chris@452: if self.auth_store.user: user_field.setText(self.auth_store.user) Chris@452: layout.addWidget(QtGui.QLabel(_('User:')), 1, 0) Chris@452: layout.addWidget(user_field, 1, 1) Chris@452: Chris@452: passwd_field = QtGui.QLineEdit() Chris@452: passwd_field.setEchoMode(QtGui.QLineEdit.Password) Chris@452: if self.auth_store.passwd: passwd_field.setText(self.auth_store.passwd) Chris@452: layout.addWidget(QtGui.QLabel(_('Password:')), 2, 0) Chris@452: layout.addWidget(passwd_field, 2, 1) Chris@452: Chris@452: user_field.connect(user_field, Qt.SIGNAL("textChanged(QString)"), Chris@452: passwd_field, Qt.SLOT("clear()")) Chris@452: Chris@452: remember_field = None Chris@452: if self.auth_store.use_auth_file: Chris@452: remember_field = QtGui.QCheckBox() Chris@452: remember_field.setChecked(self.auth_store.remember) Chris@452: remember_field.setText(_('Remember these details while EasyMercurial is running')) Chris@452: layout.addWidget(remember_field, 3, 1) Chris@457: warning_field = QtGui.QLabel() Chris@461: warning_field.setText(_('Do not use this option if anyone else has access to your computer!
')) Chris@457: warning_field.hide() Chris@457: remember_field.connect(remember_field, Qt.SIGNAL("clicked()"), Chris@457: warning_field, Qt.SLOT("show()")) Chris@457: layout.addWidget(warning_field, 4, 1, QtCore.Qt.AlignRight) Chris@452: Chris@452: bb = QtGui.QDialogButtonBox() Chris@452: ok = bb.addButton(bb.Ok) Chris@452: cancel = bb.addButton(bb.Cancel) Chris@452: cancel.setDefault(False) Chris@452: cancel.setAutoDefault(False) Chris@452: ok.setDefault(True) Chris@452: bb.connect(ok, Qt.SIGNAL("clicked()"), dialog, Qt.SLOT("accept()")) Chris@452: bb.connect(cancel, Qt.SIGNAL("clicked()"), dialog, Qt.SLOT("reject()")) Chris@457: layout.addWidget(bb, 5, 0, 1, 2) Chris@452: Chris@452: dialog.setWindowTitle(_('EasyMercurial: Login')) Chris@452: dialog.show() Chris@452: Chris@452: if not self.auth_store.user: Chris@452: user_field.setFocus(True) Chris@452: elif not self.auth_store.passwd: Chris@452: passwd_field.setFocus(True) Chris@452: else: Chris@452: ok.setFocus(True) Chris@452: Chris@452: dialog.raise_() Chris@452: ok = dialog.exec_() Chris@452: if not ok: Chris@452: raise util.Abort(_('password entry cancelled')) Chris@452: Chris@452: self.auth_store.user = user_field.text() Chris@452: self.auth_store.passwd = passwd_field.text() Chris@452: Chris@452: if remember_field: Chris@452: self.auth_store.remember = remember_field.isChecked() Chris@452: Chris@452: self.auth_store.save() Chris@452: Chris@452: return (self.auth_store.user, self.auth_store.passwd) 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@459: if not hasattr(self, '__easyhg_last'): Chris@459: self.__easyhg_last = None Chris@459: 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@470: repeat = False Chris@459: Chris@459: if (realm, authuri) == self.__easyhg_last: Chris@459: # If we are called again just after identical previous Chris@459: # request, then the previously returned auth must have been Chris@459: # wrong. So we note this to force password prompt (and avoid Chris@459: # reusing bad password indefinitely). Thanks to Chris@459: # mercurial_keyring (Marcin Kasperski) for this logic Chris@470: repeat = True Chris@470: Chris@470: if user and passwd and not repeat: Chris@470: return orig_find(self, realm, authuri) Chris@427: Chris@452: dialog = EasyHgAuthDialog(self.ui, authuri, user, passwd) Chris@427: Chris@470: (user, passwd) = dialog.ask(repeat) Chris@433: Chris@470: self.add_password(realm, authuri, user, passwd) Chris@459: self.__easyhg_last = (realm, authuri) Chris@427: return (user, passwd) Chris@427: Chris@427: