mercurial_keyring.py
406 lines
| 15.1 KiB
| text/x-python
|
PythonLexer
Marcin Kasperski
|
r0 | # -*- coding: utf-8 -*- | ||
Marcin Kasperski
|
r13 | # | ||
# mercurial_keyring: save passwords in password database | ||||
# | ||||
# Copyright 2009 Marcin Kasperski <Marcin.Kasperski@mekk.waw.pl> | ||||
# | ||||
# This software may be used and distributed according to the terms | ||||
# of the GNU General Public License, incorporated herein by reference. | ||||
Marcin Kasperski
|
r35 | # | ||
# See README.txt for more details. | ||||
Marcin Kasperski
|
r0 | |||
from mercurial import hg, repo, util | ||||
from mercurial.i18n import _ | ||||
try: | ||||
Marcin Kasperski
|
r19 | from mercurial.url import passwordmgr | ||
Marcin Kasperski
|
r0 | except: | ||
Marcin Kasperski
|
r19 | from mercurial.httprepo import passwordmgr | ||
Marcin Kasperski
|
r9 | from mercurial.httprepo import httprepository | ||
Marcin Kasperski
|
r24 | from mercurial import mail | ||
Marcin Kasperski
|
r0 | |||
Alan Franzoni
|
r64 | # mercurial.demandimport incompatibility workaround, | ||
# would cause gnomekeyring, one of the possible | ||||
# keyring backends, not to work. | ||||
from mercurial.demandimport import ignore | ||||
if "gobject._gobject" not in ignore: | ||||
ignore.append("gobject._gobject") | ||||
Marcin Kasperski
|
r0 | import keyring | ||
from urlparse import urlparse | ||||
Marcin Kasperski
|
r3 | import urllib2 | ||
Marcin Kasperski
|
r24 | import smtplib, socket | ||
Marcin Kasperski
|
r46 | import os | ||
Marcin Kasperski
|
r0 | |||
Marcin Kasperski
|
r1 | KEYRING_SERVICE = "Mercurial" | ||
Marcin Kasperski
|
r0 | |||
Marcin Kasperski
|
r2 | ############################################################ | ||
Marcin Kasperski
|
r0 | def monkeypatch_method(cls): | ||
def decorator(func): | ||||
setattr(cls, func.__name__, func) | ||||
return func | ||||
return decorator | ||||
Marcin Kasperski
|
r2 | ############################################################ | ||
class PasswordStore(object): | ||||
""" | ||||
Marcin Kasperski
|
r13 | Helper object handling keyring usage (password save&restore, | ||
the way passwords are keyed in the keyring). | ||||
Marcin Kasperski
|
r2 | """ | ||
def __init__(self): | ||||
self.cache = dict() | ||||
Marcin Kasperski
|
r24 | def get_http_password(self, url, username): | ||
Marcin Kasperski
|
r7 | return keyring.get_password(KEYRING_SERVICE, | ||
Marcin Kasperski
|
r24 | self._format_http_key(url, username)) | ||
def set_http_password(self, url, username, password): | ||||
Marcin Kasperski
|
r7 | keyring.set_password(KEYRING_SERVICE, | ||
Marcin Kasperski
|
r24 | self._format_http_key(url, username), | ||
Marcin Kasperski
|
r7 | password) | ||
Marcin Kasperski
|
r24 | def clear_http_password(self, url, username): | ||
self.set_http_password(url, username, "") | ||||
def _format_http_key(self, url, username): | ||||
Marcin Kasperski
|
r7 | return "%s@@%s" % (username, url) | ||
Marcin Kasperski
|
r24 | def get_smtp_password(self, machine, port, username): | ||
return keyring.get_password( | ||||
KEYRING_SERVICE, | ||||
self._format_smtp_key(machine, port, username)) | ||||
def set_smtp_password(self, machine, port, username, password): | ||||
keyring.set_password( | ||||
KEYRING_SERVICE, | ||||
self._format_smtp_key(machine, port, username), | ||||
password) | ||||
def clear_smtp_password(self, machine, port, username): | ||||
self.set_smtp_password(url, username, "") | ||||
def _format_smtp_key(self, machine, port, username): | ||||
return "%s@@%s:%s" % (username, machine, str(port)) | ||||
Marcin Kasperski
|
r2 | |||
password_store = PasswordStore() | ||||
############################################################ | ||||
Marcin Kasperski
|
r24 | class HTTPPasswordHandler(object): | ||
Marcin Kasperski
|
r10 | """ | ||
Actual implementation of password handling (user prompting, | ||||
configuration file searching, keyring save&restore). | ||||
Object of this class is bound as passwordmgr attribute. | ||||
""" | ||||
def __init__(self): | ||||
self.pwd_cache = {} | ||||
self.last_reply = None | ||||
Steve Borho
|
r68 | |||
Marcin Kasperski
|
r10 | def find_auth(self, pwmgr, realm, authuri): | ||
""" | ||||
Marcin Kasperski
|
r13 | Actual implementation of find_user_password - different | ||
ways of obtaining the username and password. | ||||
Marcin Kasperski
|
r10 | """ | ||
ui = pwmgr.ui | ||||
Marcin Kasperski
|
r13 | # If we are called again just after identical previous | ||
# request, then the previously returned auth must have been | ||||
# wrong. So we note this to force password prompt (and avoid | ||||
# reusing bad password indifinitely). | ||||
Marcin Kasperski
|
r10 | after_bad_auth = (self.last_reply \ | ||
Marcin Kasperski
|
r19 | and (self.last_reply['realm'] == realm) \ | ||
and (self.last_reply['authuri'] == authuri)) | ||||
Steve Borho
|
r68 | |||
Marcin Kasperski
|
r13 | # Strip arguments to get actual remote repository url. | ||
Marcin Kasperski
|
r10 | base_url = self.canonical_url(authuri) | ||
# Extracting possible username (or password) | ||||
Marcin Kasperski
|
r13 | # stored directly in repository url | ||
Marcin Kasperski
|
r18 | user, pwd = urllib2.HTTPPasswordMgrWithDefaultRealm.find_user_password( | ||
Marcin Kasperski
|
r19 | pwmgr, realm, authuri) | ||
Marcin Kasperski
|
r10 | if user and pwd: | ||
Steve Borho
|
r68 | self._debug_reply(ui, _("Auth data found in repository URL"), | ||
Marcin Kasperski
|
r19 | base_url, user, pwd) | ||
self.last_reply = dict(realm=realm,authuri=authuri,user=user) | ||||
return user, pwd | ||||
Marcin Kasperski
|
r10 | |||
Steve Borho
|
r68 | # Loading .hg/hgrc [auth] section contents. If prefix is given, | ||
Marcin Kasperski
|
r46 | # it will be used as a key to lookup password in the keyring. | ||
Patrick Mezard
|
r81 | auth_user, pwd, prefix_url = self.load_hgrc_auth(ui, base_url, user) | ||
Dave Dribin
|
r44 | if prefix_url: | ||
keyring_url = prefix_url | ||||
else: | ||||
keyring_url = base_url | ||||
ui.debug("keyring URL: %s\n" % keyring_url) | ||||
Marcin Kasperski
|
r10 | # Checking the memory cache (there may be many http calls per command) | ||
Dave Dribin
|
r44 | cache_key = (realm, keyring_url) | ||
Marcin Kasperski
|
r10 | if not after_bad_auth: | ||
Marcin Kasperski
|
r19 | cached_auth = self.pwd_cache.get(cache_key) | ||
if cached_auth: | ||||
user, pwd = cached_auth | ||||
Steve Borho
|
r68 | self._debug_reply(ui, _("Cached auth data found"), | ||
Marcin Kasperski
|
r19 | base_url, user, pwd) | ||
self.last_reply = dict(realm=realm,authuri=authuri,user=user) | ||||
return user, pwd | ||||
Marcin Kasperski
|
r10 | |||
Marcin Kasperski
|
r46 | if auth_user: | ||
Marcin Kasperski
|
r59 | if user and (user != auth_user): | ||
Marcin Kasperski
|
r46 | raise util.Abort(_('mercurial_keyring: username for %s specified both in repository path (%s) and in .hg/hgrc/[auth] (%s). Please, leave only one of those' % (base_url, user, auth_user))) | ||
user = auth_user | ||||
Marcin Kasperski
|
r19 | if pwd: | ||
self.pwd_cache[cache_key] = user, pwd | ||||
Steve Borho
|
r68 | self._debug_reply(ui, _("Auth data set in .hg/hgrc"), | ||
Marcin Kasperski
|
r19 | base_url, user, pwd) | ||
self.last_reply = dict(realm=realm,authuri=authuri,user=user) | ||||
return user, pwd | ||||
else: | ||||
ui.debug(_("Username found in .hg/hgrc: %s\n" % user)) | ||||
Marcin Kasperski
|
r10 | |||
Steve Borho
|
r68 | # Loading password from keyring. | ||
Marcin Kasperski
|
r18 | # Only if username is known (so we know the key) and we are | ||
# not after failure (so we don't reuse the bad password). | ||||
Marcin Kasperski
|
r10 | if user and not after_bad_auth: | ||
Dave Dribin
|
r44 | pwd = password_store.get_http_password(keyring_url, user) | ||
Marcin Kasperski
|
r19 | if pwd: | ||
self.pwd_cache[cache_key] = user, pwd | ||||
Steve Borho
|
r68 | self._debug_reply(ui, _("Keyring password found"), | ||
Marcin Kasperski
|
r19 | base_url, user, pwd) | ||
self.last_reply = dict(realm=realm,authuri=authuri,user=user) | ||||
return user, pwd | ||||
Steve Borho
|
r68 | |||
Marcin Kasperski
|
r13 | # Is the username permanently set? | ||
Marcin Kasperski
|
r10 | fixed_user = (user and True or False) | ||
# Last resort: interactive prompt | ||||
if not ui.interactive(): | ||||
Marcin Kasperski
|
r40 | raise util.Abort(_('mercurial_keyring: http authorization required but program used in non-interactive mode')) | ||
Marcin Kasperski
|
r52 | |||
if not fixed_user: | ||||
ui.status(_("Username not specified in .hg/hgrc. Keyring will not be used.\n")) | ||||
Steve Borho
|
r68 | |||
Marcin Kasperski
|
r10 | ui.write(_("http authorization required\n")) | ||
ui.status(_("realm: %s\n") % realm) | ||||
if fixed_user: | ||||
Marcin Kasperski
|
r19 | ui.write(_("user: %s (fixed in .hg/hgrc)\n" % user)) | ||
Marcin Kasperski
|
r10 | else: | ||
Marcin Kasperski
|
r19 | user = ui.prompt(_("user:"), default=None) | ||
Marcin Kasperski
|
r10 | pwd = ui.getpass(_("password: ")) | ||
if fixed_user: | ||||
Marcin Kasperski
|
r19 | # Saving password to the keyring. | ||
# It is done only if username is permanently set. | ||||
# Otherwise we won't be able to find the password so it | ||||
# does not make much sense to preserve it | ||||
ui.debug("Saving password for %s to keyring\n" % user) | ||||
Dave Dribin
|
r44 | password_store.set_http_password(keyring_url, user, pwd) | ||
Marcin Kasperski
|
r10 | |||
Marcin Kasperski
|
r13 | # Saving password to the memory cache | ||
Marcin Kasperski
|
r10 | self.pwd_cache[cache_key] = user, pwd | ||
Marcin Kasperski
|
r13 | |||
Steve Borho
|
r68 | self._debug_reply(ui, _("Manually entered password"), | ||
Marcin Kasperski
|
r18 | base_url, user, pwd) | ||
Marcin Kasperski
|
r10 | self.last_reply = dict(realm=realm,authuri=authuri,user=user) | ||
return user, pwd | ||||
Patrick Mezard
|
r81 | def load_hgrc_auth(self, ui, base_url, user): | ||
Marcin Kasperski
|
r10 | """ | ||
Marcin Kasperski
|
r46 | Loading [auth] section contents from local .hgrc | ||
Returns (username, password, prefix) tuple (every | ||||
element can be None) | ||||
Marcin Kasperski
|
r10 | """ | ||
Marcin Kasperski
|
r19 | # Theoretically 3 lines below should do: | ||
Steve Borho
|
r68 | |||
Marcin Kasperski
|
r19 | #auth_token = self.readauthtoken(base_url) | ||
#if auth_token: | ||||
# user, pwd = auth.get('username'), auth.get('password') | ||||
Steve Borho
|
r68 | |||
Marcin Kasperski
|
r13 | # Unfortunately they do not work, readauthtoken always return | ||
# None. Why? Because ui (self.ui of passwordmgr) describes the | ||||
# *remote* repository, so does *not* contain any option from | ||||
# local .hg/hgrc. | ||||
Marcin Kasperski
|
r10 | |||
Marcin Kasperski
|
r50 | # TODO: mercurial 1.4.2 is claimed to resolve this problem | ||
Steve Borho
|
r68 | # (thanks to: http://hg.xavamedia.nl/mercurial/crew/rev/fb45c1e4396f) | ||
Marcin Kasperski
|
r50 | # so since this version workaround implemented below should | ||
# not be necessary. As it will take some time until people | ||||
# migrate to >= 1.4.2, it would be best to implement | ||||
# workaround conditionally. | ||||
Steve Borho
|
r68 | |||
Marcin Kasperski
|
r10 | # Workaround: we recreate the repository object | ||
repo_root = ui.config("bundle", "mainreporoot") | ||||
Dave Dribin
|
r44 | |||
from mercurial.ui import ui as _ui | ||||
local_ui = _ui(ui) | ||||
Marcin Kasperski
|
r10 | if repo_root: | ||
Marcin Kasperski
|
r19 | local_ui.readconfig(os.path.join(repo_root, ".hg", "hgrc")) | ||
Steve Borho
|
r69 | try: | ||
local_passwordmgr = passwordmgr(local_ui) | ||||
auth_token = local_passwordmgr.readauthtoken(base_url) | ||||
except AttributeError: | ||||
Steve Borho
|
r72 | try: | ||
# hg 1.8 | ||||
Patrick Mezard
|
r80 | import mercurial.url | ||
readauthforuri = mercurial.url.readauthforuri | ||||
except (ImportError, AttributeError): | ||||
Steve Borho
|
r72 | # hg 1.9 | ||
Patrick Mezard
|
r80 | import mercurial.httpconnection | ||
readauthforuri = mercurial.httpconnection.readauthforuri | ||||
Patrick Mezard
|
r81 | if readauthforuri.func_code.co_argcount == 3: | ||
# Since hg.0593e8f81c71 | ||||
res = readauthforuri(local_ui, base_url, user) | ||||
else: | ||||
res = readauthforuri(local_ui, base_url) | ||||
Steve Borho
|
r69 | if res: | ||
group, auth_token = res | ||||
else: | ||||
auth_token = None | ||||
Dave Dribin
|
r44 | if auth_token: | ||
username = auth_token.get('username') | ||||
password = auth_token.get('password') | ||||
prefix = auth_token.get('prefix') | ||||
shortest_url = self.shortest_url(base_url, prefix) | ||||
return username, password, shortest_url | ||||
Marcin Kasperski
|
r46 | return None, None, None | ||
Marcin Kasperski
|
r10 | |||
Dave Dribin
|
r44 | def shortest_url(self, base_url, prefix): | ||
if not prefix or prefix == '*': | ||||
return base_url | ||||
scheme, hostpath = base_url.split('://', 1) | ||||
p = prefix.split('://', 1) | ||||
if len(p) > 1: | ||||
prefix_host_path = p[1] | ||||
else: | ||||
prefix_host_path = prefix | ||||
shortest_url = scheme + '://' + prefix_host_path | ||||
return shortest_url | ||||
Marcin Kasperski
|
r10 | def canonical_url(self, authuri): | ||
""" | ||||
Marcin Kasperski
|
r18 | Strips query params from url. Used to convert urls like | ||
Marcin Kasperski
|
r10 | https://repo.machine.com/repos/apps/module?pairs=0000000000000000000000000000000000000000-0000000000000000000000000000000000000000&cmd=between | ||
to | ||||
https://repo.machine.com/repos/apps/module | ||||
""" | ||||
parsed_url = urlparse(authuri) | ||||
Marcin Kasperski
|
r21 | return "%s://%s%s" % (parsed_url.scheme, parsed_url.netloc, | ||
parsed_url.path) | ||||
Marcin Kasperski
|
r10 | |||
def _debug_reply(self, ui, msg, url, user, pwd): | ||||
Marcin Kasperski
|
r18 | ui.debug("%s. Url: %s, user: %s, passwd: %s\n" % ( | ||
msg, url, user, pwd and '*' * len(pwd) or 'not set')) | ||||
Marcin Kasperski
|
r10 | |||
############################################################ | ||||
Marcin Kasperski
|
r0 | @monkeypatch_method(passwordmgr) | ||
def find_user_password(self, realm, authuri): | ||||
""" | ||||
keyring-based implementation of username/password query | ||||
Marcin Kasperski
|
r24 | for HTTP(S) connections | ||
Marcin Kasperski
|
r0 | |||
Passwords are saved in gnome keyring, OSX/Chain or other platform | ||||
specific storage and keyed by the repository url | ||||
""" | ||||
Marcin Kasperski
|
r10 | # Extend object attributes | ||
if not hasattr(self, '_pwd_handler'): | ||||
Marcin Kasperski
|
r24 | self._pwd_handler = HTTPPasswordHandler() | ||
Marcin Kasperski
|
r6 | |||
Marcin Kasperski
|
r10 | return self._pwd_handler.find_auth(self, realm, authuri) | ||
Marcin Kasperski
|
r1 | |||
Marcin Kasperski
|
r24 | ############################################################ | ||
def try_smtp_login(ui, smtp_obj, username, password): | ||||
""" | ||||
Attempts smtp login on smtp_obj (smtplib.SMTP) using username and | ||||
Steve Borho
|
r68 | password. | ||
Marcin Kasperski
|
r24 | |||
Returns: | ||||
- True if login succeeded | ||||
- False if login failed due to the wrong credentials | ||||
Throws Abort exception if login failed for any other reason. | ||||
Immediately returns False if password is empty | ||||
""" | ||||
if not password: | ||||
return False | ||||
try: | ||||
ui.note(_('(authenticating to mail server as %s)\n') % | ||||
(username)) | ||||
smtp_obj.login(username, password) | ||||
return True | ||||
Marcin Kasperski
|
r63 | except smtplib.SMTPException, inst: | ||
Marcin Kasperski
|
r24 | if inst.smtp_code == 535: | ||
ui.status(_("SMTP login failed: %s\n\n") % inst.smtp_error) | ||||
return False | ||||
else: | ||||
raise util.Abort(inst) | ||||
def keyring_supported_smtp(ui, username): | ||||
""" | ||||
keyring-integrated replacement for mercurial.mail._smtp | ||||
Used only when configuration file contains username, but | ||||
does not contain the password. | ||||
Most of the routine below is copied as-is from | ||||
mercurial.mail._smtp. The only changed part is | ||||
marked with #>>>>> and #<<<<< markers | ||||
""" | ||||
local_hostname = ui.config('smtp', 'local_hostname') | ||||
s = smtplib.SMTP(local_hostname=local_hostname) | ||||
mailhost = ui.config('smtp', 'host') | ||||
if not mailhost: | ||||
raise util.Abort(_('no [smtp]host in hgrc - cannot send mail')) | ||||
mailport = int(ui.config('smtp', 'port', 25)) | ||||
ui.note(_('sending mail: smtp host %s, port %s\n') % | ||||
(mailhost, mailport)) | ||||
s.connect(host=mailhost, port=mailport) | ||||
if ui.configbool('smtp', 'tls'): | ||||
if not hasattr(socket, 'ssl'): | ||||
raise util.Abort(_("can't use TLS: Python SSL support " | ||||
"not installed")) | ||||
ui.note(_('(using tls)\n')) | ||||
s.ehlo() | ||||
s.starttls() | ||||
s.ehlo() | ||||
Steve Borho
|
r68 | |||
Marcin Kasperski
|
r24 | #>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> | ||
stored = password = password_store.get_smtp_password( | ||||
mailhost, mailport, username) | ||||
# No need to check whether password was found as try_smtp_login | ||||
# just returns False if it is absent. | ||||
while not try_smtp_login(ui, s, username, password): | ||||
password = ui.getpass(_("Password for %s on %s:%d: ") % (username, mailhost, mailport)) | ||||
if stored != password: | ||||
password_store.set_smtp_password( | ||||
mailhost, mailport, username, password) | ||||
#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< | ||||
def send(sender, recipients, msg): | ||||
try: | ||||
return s.sendmail(sender, recipients, msg) | ||||
Marcin Kasperski
|
r63 | except smtplib.SMTPRecipientsRefused, inst: | ||
Marcin Kasperski
|
r24 | recipients = [r[1] for r in inst.recipients.values()] | ||
raise util.Abort('\n' + '\n'.join(recipients)) | ||||
Marcin Kasperski
|
r63 | except smtplib.SMTPException, inst: | ||
Marcin Kasperski
|
r24 | raise util.Abort(inst) | ||
return send | ||||
############################################################ | ||||
orig_smtp = mail._smtp | ||||
@monkeypatch_method(mail) | ||||
def _smtp(ui): | ||||
""" | ||||
build an smtp connection and return a function to send email | ||||
This is the monkeypatched version of _smtp(ui) function from | ||||
mercurial/mail.py. It calls the original unless username | ||||
without password is given in the configuration. | ||||
""" | ||||
username = ui.config('smtp', 'username') | ||||
password = ui.config('smtp', 'password') | ||||
if username and not password: | ||||
return keyring_supported_smtp(ui, username) | ||||
else: | ||||
return orig_smtp(ui) | ||||