annotate easyhg2.py @ 433:c20da4213406

Part-way through a more effective version using standard python ConfigParser
author Chris Cannam
date Tue, 28 Jun 2011 10:22:09 +0100
parents 4d0f151fca08
children f8aa7ac57993
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@427 7 # Copyright (c) 2010 Chris Cannam
Chris@427 8 # Copyright (c) 2010 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@432 16 import sys, os, stat
Chris@427 17
Chris@427 18 import urllib, urllib2, urlparse
Chris@427 19
Chris@433 20 from mercurial import ui, util, error
Chris@427 21 try:
Chris@427 22 from mercurial.url import passwordmgr
Chris@427 23 except:
Chris@427 24 from mercurial.httprepo import passwordmgr
Chris@427 25
Chris@427 26 from mercurial.i18n import _
Chris@427 27
Chris@427 28 # The value assigned here may be modified during installation, by
Chris@427 29 # replacing its default value with another one. We can't compare
Chris@427 30 # against its default value, because then the comparison text would
Chris@427 31 # get modified as well. So, compare using prefix only.
Chris@427 32 #
Chris@427 33 easyhg_import_path = 'NO_EASYHG_IMPORT_PATH'
Chris@427 34 if not easyhg_import_path.startswith('NO_'):
Chris@427 35 # We have an installation path: append it twice, once with
Chris@427 36 # the Python version suffixed
Chris@427 37 version_suffix = "Py" + str(sys.version_info[0]) + "." + str(sys.version_info[1]);
Chris@427 38 sys.path.append(easyhg_import_path + "/" + version_suffix)
Chris@427 39 sys.path.append(easyhg_import_path)
Chris@427 40
Chris@427 41 # Try to load the PyQt4 module that we need. If this fails, we should
Chris@427 42 # bail out later (in uisetup), because if we bail out now, Mercurial
Chris@427 43 # will just continue without us and report success. The invoking
Chris@427 44 # application needs to be able to discover whether the module load
Chris@427 45 # succeeded or not, so we need to ensure that Mercurial itself returns
Chris@427 46 # failure if it didn't.
Chris@427 47 #
Chris@427 48 easyhg_pyqt_ok = True
Chris@427 49 try:
Chris@427 50 from PyQt4 import Qt, QtGui
Chris@427 51 except ImportError:
Chris@427 52 easyhg_pyqt_ok = False
Chris@427 53
Chris@427 54 easyhg_qtapp = None
Chris@427 55
Chris@432 56 #!!! same as above for this? or just continue without remember feature?
Chris@431 57 from Crypto.Cipher import AES
Chris@431 58 import base64
Chris@431 59
Chris@433 60 import ConfigParser # Mercurial version won't write files
Chris@433 61
Chris@431 62 #!!! should be in a class here
Chris@431 63
Chris@431 64 def encrypt(text, key):
Chris@431 65 text = '%d.%s' % (len(text), text)
Chris@431 66 text += (16 - len(text) % 16) * ' '
Chris@431 67 cipher = AES.new(key)
Chris@431 68 return base64.b64encode(cipher.encrypt(text))
Chris@431 69
Chris@431 70 def decrypt(ctext, key):
Chris@431 71 cipher = AES.new(key)
Chris@431 72 text = cipher.decrypt(base64.b64decode(ctext))
Chris@431 73 (tlen, d, text) = text.partition('.')
Chris@431 74 return text[0:int(tlen)]
Chris@431 75
Chris@427 76 def monkeypatch_method(cls):
Chris@427 77 def decorator(func):
Chris@427 78 setattr(cls, func.__name__, func)
Chris@427 79 return func
Chris@427 80 return decorator
Chris@427 81
Chris@427 82 def uisetup(ui):
Chris@427 83 if not easyhg_pyqt_ok:
Chris@427 84 raise util.Abort(_('Failed to load PyQt4 module required by easyhg.py'))
Chris@427 85 global easyhg_qtapp
Chris@427 86 easyhg_qtapp = QtGui.QApplication([])
Chris@427 87
Chris@427 88 orig_find = passwordmgr.find_user_password
Chris@427 89
Chris@427 90 # from mercurial_keyring by Marcin Kasperski
Chris@427 91 def canonical_url(authuri):
Chris@427 92 """
Chris@427 93 Strips query params from url. Used to convert urls like
Chris@427 94 https://repo.machine.com/repos/apps/module?pairs=0000000000000000000000000000000000000000-0000000000000000000000000000000000000000&cmd=between
Chris@427 95 to
Chris@427 96 https://repo.machine.com/repos/apps/module
Chris@427 97 """
Chris@427 98 parsed_url = urlparse.urlparse(authuri)
Chris@427 99 return "%s://%s%s" % (parsed_url.scheme, parsed_url.netloc,
Chris@427 100 parsed_url.path)
Chris@427 101
Chris@427 102 @monkeypatch_method(passwordmgr)
Chris@427 103 def find_user_password(self, realm, authuri):
Chris@427 104
Chris@427 105 if not self.ui.interactive():
Chris@427 106 return orig_find(self, realm, authuri)
Chris@427 107 if not easyhg_pyqt_ok:
Chris@427 108 return orig_find(self, realm, authuri)
Chris@427 109
Chris@427 110 authinfo = urllib2.HTTPPasswordMgrWithDefaultRealm.find_user_password(
Chris@427 111 self, realm, authuri)
Chris@427 112 user, passwd = authinfo
Chris@427 113
Chris@427 114 if user and passwd:
Chris@427 115 return orig_find(self, realm, authuri)
Chris@427 116
Chris@427 117 self.ui.write("want username and/or password for %s\n" % authuri)
Chris@427 118
Chris@427 119 uri = canonical_url(authuri)
Chris@427 120
Chris@433 121 pkey = base64.b64encode('%s@@%s' % (uri, user)).replace('=', '_')
Chris@431 122 pekey = self.ui.config('easyhg', 'authkey')
Chris@432 123 pfile = os.path.expanduser(self.ui.config('easyhg', 'authfile'))
Chris@431 124 pdata = None
Chris@431 125
Chris@431 126 self.ui.write("pekey is %s\n" % pekey)
Chris@431 127 self.ui.write("pfile is %s\n" % pfile)
Chris@427 128
Chris@427 129 dialog = QtGui.QDialog()
Chris@427 130 layout = QtGui.QGridLayout()
Chris@427 131 dialog.setLayout(layout)
Chris@427 132
Chris@432 133 layout.addWidget(QtGui.QLabel(_('<h3>Login required</h3><p>Please provide your login details for the repository at<br><code>%s</code>:') % uri), 0, 0, 1, 2)
Chris@427 134
Chris@427 135 userfield = QtGui.QLineEdit()
Chris@427 136 if user:
Chris@427 137 userfield.setText(user)
Chris@427 138 layout.addWidget(QtGui.QLabel(_('User:')), 1, 0)
Chris@427 139 layout.addWidget(userfield, 1, 1)
Chris@427 140
Chris@427 141 passfield = QtGui.QLineEdit()
Chris@427 142 passfield.setEchoMode(QtGui.QLineEdit.Password)
Chris@427 143 if passwd:
Chris@428 144 passfield.setText(passwd)
Chris@427 145 layout.addWidget(QtGui.QLabel(_('Password:')), 2, 0)
Chris@427 146 layout.addWidget(passfield, 2, 1)
Chris@427 147
Chris@431 148 remember = None
Chris@433 149 pcfg = None
Chris@433 150
Chris@431 151 if pekey and pfile:
Chris@431 152 # load pwd from our cache file, decrypt with given key
Chris@433 153 pcfg = ConfigParser.RawConfigParser()
Chris@431 154 fp = None
Chris@433 155 remember_default = False
Chris@431 156 try:
Chris@431 157 fp = open(pfile)
Chris@431 158 except:
Chris@431 159 self.ui.write("failed to open authfile %s\n" % pfile)
Chris@432 160 if fp and not passwd:
Chris@433 161 pcfg.readfp(fp)
Chris@433 162 try:
Chris@433 163 remember_default = pcfg.getboolean('preferences', 'remember')
Chris@433 164 except:
Chris@433 165 remember_default = False
Chris@433 166 try:
Chris@433 167 pdata = pcfg.get('auth', pkey)
Chris@433 168 except ConfigParser.NoOptionError:
Chris@433 169 pdata = None
Chris@432 170 if pdata:
Chris@432 171 cachedpwd = decrypt(pdata, pekey)
Chris@431 172 passfield.setText(cachedpwd)
Chris@432 173 fp.close()
Chris@431 174 remember = QtGui.QCheckBox()
Chris@433 175 remember.setChecked(remember_default)
Chris@431 176 remember.setText(_('Remember this password until EasyMercurial exits'))
Chris@431 177 layout.addWidget(remember, 3, 1)
Chris@431 178
Chris@427 179 bb = QtGui.QDialogButtonBox()
Chris@427 180 ok = bb.addButton(bb.Ok)
Chris@427 181 cancel = bb.addButton(bb.Cancel)
Chris@427 182 cancel.setDefault(False)
Chris@427 183 cancel.setAutoDefault(False)
Chris@427 184 ok.setDefault(True)
Chris@427 185 bb.connect(ok, Qt.SIGNAL("clicked()"), dialog, Qt.SLOT("accept()"))
Chris@427 186 bb.connect(cancel, Qt.SIGNAL("clicked()"), dialog, Qt.SLOT("reject()"))
Chris@431 187 layout.addWidget(bb, 4, 0, 1, 2)
Chris@427 188
Chris@428 189 dialog.setWindowTitle(_('EasyMercurial: Login'))
Chris@427 190 dialog.show()
Chris@428 191
Chris@428 192 if not user:
Chris@428 193 userfield.setFocus(True)
Chris@428 194 elif not passwd:
Chris@428 195 passfield.setFocus(True)
Chris@428 196
Chris@427 197 dialog.raise_()
Chris@427 198 ok = dialog.exec_()
Chris@427 199 if ok:
Chris@427 200 self.ui.write('Dialog accepted\n')
Chris@427 201 user = userfield.text()
Chris@427 202 passwd = passfield.text()
Chris@431 203
Chris@433 204 if pekey and pfile and remember:
Chris@432 205
Chris@432 206 ofp = None
Chris@432 207
Chris@432 208 try:
Chris@433 209 ofp = open(pfile, 'w')
Chris@432 210 except:
Chris@432 211 self.ui.write("failed to open authfile %s for writing\n" % pfile)
Chris@432 212 raise
Chris@432 213
Chris@432 214 try:
Chris@432 215 os.fchmod(ofp.fileno(), stat.S_IRUSR | stat.S_IWUSR) #!!! Windows equivalent?
Chris@432 216 except:
Chris@432 217 ofp.close()
Chris@432 218 ofp = None
Chris@432 219 self.ui.write("failed to set proper permissions on authfile %s\n" % pfile)
Chris@432 220 raise
Chris@432 221
Chris@433 222 #!!! add these sections first...
Chris@433 223
Chris@433 224 if remember.isChecked():
Chris@432 225 pdata = encrypt(passwd, pekey)
Chris@433 226 pcfg.set('auth', pkey, pdata)
Chris@433 227 else:
Chris@433 228 pcfg.set('auth', pkey, '')
Chris@433 229
Chris@433 230 pcfg.set('preferences', 'remember', remember.isChecked())
Chris@433 231 pcfg.write(ofp)
Chris@433 232 ofp.close()
Chris@431 233
Chris@431 234 # if passwd and keyring_key != '' and not from_keyring:
Chris@431 235 # keyring_key = '%s@@%s' % (uri, user)
Chris@431 236 ## keyring.set_password('Mercurial', keyring_key, passwd)
Chris@427 237 self.add_password(realm, authuri, user, passwd)
Chris@427 238 else:
Chris@427 239 raise util.Abort(_('password entry cancelled'))
Chris@427 240 return (user, passwd)
Chris@427 241
Chris@427 242
Chris@427 243