mirror of
https://github.com/samsonjs/vdirsyncer.git
synced 2026-04-27 14:57:41 +00:00
A lot of module restructuring
- Split utils up again - Optimize performance when importing a specific storage. This is useful for khal which uses our FilesystemStorage (and doesn't want to import requests).
This commit is contained in:
parent
2320570217
commit
e2eb79d656
9 changed files with 285 additions and 270 deletions
|
|
@ -61,7 +61,7 @@ def test_storage_instance_from_config(monkeypatch):
|
||||||
return 'OK'
|
return 'OK'
|
||||||
|
|
||||||
import vdirsyncer.storage
|
import vdirsyncer.storage
|
||||||
monkeypatch.setitem(vdirsyncer.storage.storage_names, 'lol', lol)
|
monkeypatch.setitem(vdirsyncer.cli.utils.storage_names, 'lol', lol)
|
||||||
config = {'type': 'lol', 'foo': 'bar', 'baz': 1}
|
config = {'type': 'lol', 'foo': 'bar', 'baz': 1}
|
||||||
assert cli.utils.storage_instance_from_config(config) == 'OK'
|
assert cli.utils.storage_instance_from_config(config) == 'OK'
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,8 @@ import pytest
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
import vdirsyncer.doubleclick as doubleclick
|
import vdirsyncer.doubleclick as doubleclick
|
||||||
|
import vdirsyncer.utils.http
|
||||||
|
import vdirsyncer.utils.password
|
||||||
import vdirsyncer.utils as utils
|
import vdirsyncer.utils as utils
|
||||||
|
|
||||||
from .. import blow_up
|
from .. import blow_up
|
||||||
|
|
@ -33,7 +35,7 @@ class EmptyKeyring(object):
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
def empty_password_storages(monkeypatch):
|
def empty_password_storages(monkeypatch):
|
||||||
monkeypatch.setattr('netrc.netrc', EmptyNetrc)
|
monkeypatch.setattr('netrc.netrc', EmptyNetrc)
|
||||||
monkeypatch.setattr(utils, 'keyring', EmptyKeyring())
|
monkeypatch.setattr(utils.password, 'keyring', EmptyKeyring())
|
||||||
|
|
||||||
|
|
||||||
def test_get_password_from_netrc(monkeypatch):
|
def test_get_password_from_netrc(monkeypatch):
|
||||||
|
|
@ -52,7 +54,7 @@ def test_get_password_from_netrc(monkeypatch):
|
||||||
monkeypatch.setattr('netrc.netrc', Netrc)
|
monkeypatch.setattr('netrc.netrc', Netrc)
|
||||||
monkeypatch.setattr('getpass.getpass', blow_up)
|
monkeypatch.setattr('getpass.getpass', blow_up)
|
||||||
|
|
||||||
_password = utils.get_password(username, resource)
|
_password = utils.password.get_password(username, resource)
|
||||||
assert _password == password
|
assert _password == password
|
||||||
assert calls == [hostname]
|
assert calls == [hostname]
|
||||||
|
|
||||||
|
|
@ -66,14 +68,14 @@ def test_get_password_from_system_keyring(monkeypatch):
|
||||||
class KeyringMock(object):
|
class KeyringMock(object):
|
||||||
def get_password(self, resource, _username):
|
def get_password(self, resource, _username):
|
||||||
assert _username == username
|
assert _username == username
|
||||||
assert resource == utils.password_key_prefix + hostname
|
assert resource == utils.password.password_key_prefix + hostname
|
||||||
return password
|
return password
|
||||||
|
|
||||||
monkeypatch.setattr(utils, 'keyring', KeyringMock())
|
monkeypatch.setattr(utils.password, 'keyring', KeyringMock())
|
||||||
|
|
||||||
monkeypatch.setattr('getpass.getpass', blow_up)
|
monkeypatch.setattr('getpass.getpass', blow_up)
|
||||||
|
|
||||||
_password = utils.get_password(username, resource)
|
_password = utils.password.get_password(username, resource)
|
||||||
assert _password == password
|
assert _password == password
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -98,7 +100,7 @@ def test_get_password_from_command(tmpdir):
|
||||||
@doubleclick.click.pass_context
|
@doubleclick.click.pass_context
|
||||||
def fake_app(ctx):
|
def fake_app(ctx):
|
||||||
ctx.obj = {'config': ({'password_command': filepath}, {}, {})}
|
ctx.obj = {'config': ({'password_command': filepath}, {}, {})}
|
||||||
_password = utils.get_password(username, resource)
|
_password = utils.password.get_password(username, resource)
|
||||||
assert _password == password
|
assert _password == password
|
||||||
|
|
||||||
runner = CliRunner()
|
runner = CliRunner()
|
||||||
|
|
@ -112,7 +114,7 @@ def test_get_password_from_prompt():
|
||||||
|
|
||||||
@click.command()
|
@click.command()
|
||||||
def fake_app():
|
def fake_app():
|
||||||
x = utils.get_password(user, resource)
|
x = utils.password.get_password(user, resource)
|
||||||
click.echo('Password is {}'.format(x))
|
click.echo('Password is {}'.format(x))
|
||||||
|
|
||||||
runner = CliRunner()
|
runner = CliRunner()
|
||||||
|
|
@ -127,22 +129,24 @@ def test_get_password_from_prompt():
|
||||||
def test_set_keyring_password(monkeypatch):
|
def test_set_keyring_password(monkeypatch):
|
||||||
class KeyringMock(object):
|
class KeyringMock(object):
|
||||||
def get_password(self, resource, username):
|
def get_password(self, resource, username):
|
||||||
assert resource == utils.password_key_prefix + 'example.com'
|
assert resource == \
|
||||||
|
utils.password.password_key_prefix + 'example.com'
|
||||||
assert username == 'foouser'
|
assert username == 'foouser'
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def set_password(self, resource, username, password):
|
def set_password(self, resource, username, password):
|
||||||
assert resource == utils.password_key_prefix + 'example.com'
|
assert resource == \
|
||||||
|
utils.password.password_key_prefix + 'example.com'
|
||||||
assert username == 'foouser'
|
assert username == 'foouser'
|
||||||
assert password == 'hunter2'
|
assert password == 'hunter2'
|
||||||
|
|
||||||
monkeypatch.setattr(utils, 'keyring', KeyringMock())
|
monkeypatch.setattr(utils.password, 'keyring', KeyringMock())
|
||||||
|
|
||||||
@doubleclick.click.command()
|
@doubleclick.click.command()
|
||||||
@doubleclick.click.pass_context
|
@doubleclick.click.pass_context
|
||||||
def fake_app(ctx):
|
def fake_app(ctx):
|
||||||
ctx.obj = {}
|
ctx.obj = {}
|
||||||
x = utils.get_password('foouser', 'http://example.com/a/b')
|
x = utils.password.get_password('foouser', 'http://example.com/a/b')
|
||||||
click.echo('password is ' + x)
|
click.echo('password is ' + x)
|
||||||
|
|
||||||
runner = CliRunner()
|
runner = CliRunner()
|
||||||
|
|
@ -163,12 +167,12 @@ def test_get_password_from_cache(monkeypatch):
|
||||||
@doubleclick.click.pass_context
|
@doubleclick.click.pass_context
|
||||||
def fake_app(ctx):
|
def fake_app(ctx):
|
||||||
ctx.obj = {}
|
ctx.obj = {}
|
||||||
x = utils.get_password(user, resource)
|
x = utils.password.get_password(user, resource)
|
||||||
click.echo('Password is {}'.format(x))
|
click.echo('Password is {}'.format(x))
|
||||||
monkeypatch.setattr(doubleclick.click, 'prompt', blow_up)
|
monkeypatch.setattr(doubleclick.click, 'prompt', blow_up)
|
||||||
|
|
||||||
assert (user, 'example.com') in ctx.obj['passwords']
|
assert (user, 'example.com') in ctx.obj['passwords']
|
||||||
x = utils.get_password(user, resource)
|
x = utils.password.get_password(user, resource)
|
||||||
click.echo('Password is {}'.format(x))
|
click.echo('Password is {}'.format(x))
|
||||||
|
|
||||||
runner = CliRunner()
|
runner = CliRunner()
|
||||||
|
|
@ -212,13 +216,13 @@ def test_request_ssl(httpsserver):
|
||||||
httpsserver.serve_content('') # we need to serve something
|
httpsserver.serve_content('') # we need to serve something
|
||||||
|
|
||||||
with pytest.raises(requests.exceptions.SSLError) as excinfo:
|
with pytest.raises(requests.exceptions.SSLError) as excinfo:
|
||||||
utils.request('GET', httpsserver.url)
|
utils.http.request('GET', httpsserver.url)
|
||||||
assert 'certificate verify failed' in str(excinfo.value)
|
assert 'certificate verify failed' in str(excinfo.value)
|
||||||
utils.request('GET', httpsserver.url, verify=False)
|
utils.http.request('GET', httpsserver.url, verify=False)
|
||||||
utils.request('GET', httpsserver.url,
|
utils.http.request('GET', httpsserver.url,
|
||||||
verify_fingerprint=sha1)
|
verify_fingerprint=sha1)
|
||||||
utils.request('GET', httpsserver.url, verify_fingerprint=md5)
|
utils.http.request('GET', httpsserver.url, verify_fingerprint=md5)
|
||||||
with pytest.raises(requests.exceptions.SSLError) as excinfo:
|
with pytest.raises(requests.exceptions.SSLError) as excinfo:
|
||||||
utils.request('GET', httpsserver.url,
|
utils.http.request('GET', httpsserver.url,
|
||||||
verify_fingerprint=''.join(reversed(sha1)))
|
verify_fingerprint=''.join(reversed(sha1)))
|
||||||
assert 'Fingerprints did not match' in str(excinfo.value)
|
assert 'Fingerprints did not match' in str(excinfo.value)
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,6 @@ from atomicwrites import atomic_write
|
||||||
|
|
||||||
from .. import DOCS_HOME, PROJECT_HOME, exceptions, log
|
from .. import DOCS_HOME, PROJECT_HOME, exceptions, log
|
||||||
from ..doubleclick import click
|
from ..doubleclick import click
|
||||||
from ..storage import storage_names
|
|
||||||
from ..sync import IdentConflict, StorageEmpty, SyncConflict
|
from ..sync import IdentConflict, StorageEmpty, SyncConflict
|
||||||
from ..utils import expand_path, get_class_init_args
|
from ..utils import expand_path, get_class_init_args
|
||||||
from ..utils.compat import text_type
|
from ..utils.compat import text_type
|
||||||
|
|
@ -32,6 +31,33 @@ except ImportError:
|
||||||
import queue
|
import queue
|
||||||
|
|
||||||
|
|
||||||
|
def _generate_storage_dict():
|
||||||
|
from ..storage.dav import CaldavStorage, CarddavStorage
|
||||||
|
from ..storage.filesystem import FilesystemStorage
|
||||||
|
from ..storage.http import HttpStorage
|
||||||
|
from ..storage.singlefile import SingleFileStorage
|
||||||
|
|
||||||
|
classes = (
|
||||||
|
CaldavStorage,
|
||||||
|
CarddavStorage,
|
||||||
|
FilesystemStorage,
|
||||||
|
HttpStorage,
|
||||||
|
SingleFileStorage
|
||||||
|
)
|
||||||
|
|
||||||
|
rv = {}
|
||||||
|
for cls in classes:
|
||||||
|
key = cls.storage_name
|
||||||
|
assert key
|
||||||
|
assert isinstance(key, str)
|
||||||
|
assert key not in rv
|
||||||
|
rv[key] = cls
|
||||||
|
return rv
|
||||||
|
|
||||||
|
storage_names = _generate_storage_dict()
|
||||||
|
del _generate_storage_dict
|
||||||
|
|
||||||
|
|
||||||
cli_logger = log.get(__name__)
|
cli_logger = log.get(__name__)
|
||||||
|
|
||||||
GENERAL_ALL = frozenset(['status_path', 'password_command'])
|
GENERAL_ALL = frozenset(['status_path', 'password_command'])
|
||||||
|
|
|
||||||
|
|
@ -5,29 +5,3 @@ offer basic CRUD-ish methods for modifying those collections. The exact
|
||||||
interface is described in `vdirsyncer.storage.base`, the `Storage` class should
|
interface is described in `vdirsyncer.storage.base`, the `Storage` class should
|
||||||
be a superclass of all storage classes.
|
be a superclass of all storage classes.
|
||||||
'''
|
'''
|
||||||
|
|
||||||
from .dav import CaldavStorage, CarddavStorage
|
|
||||||
from .filesystem import FilesystemStorage
|
|
||||||
from .http import HttpStorage
|
|
||||||
from .singlefile import SingleFileStorage
|
|
||||||
|
|
||||||
|
|
||||||
def _generate_storage_dict(*classes):
|
|
||||||
rv = {}
|
|
||||||
for cls in classes:
|
|
||||||
key = cls.storage_name
|
|
||||||
assert key
|
|
||||||
assert isinstance(key, str)
|
|
||||||
assert key not in rv
|
|
||||||
rv[key] = cls
|
|
||||||
return rv
|
|
||||||
|
|
||||||
storage_names = _generate_storage_dict(
|
|
||||||
CaldavStorage,
|
|
||||||
CarddavStorage,
|
|
||||||
FilesystemStorage,
|
|
||||||
HttpStorage,
|
|
||||||
SingleFileStorage
|
|
||||||
)
|
|
||||||
|
|
||||||
del _generate_storage_dict
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import datetime
|
||||||
|
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
|
|
||||||
from requests import session as requests_session
|
import requests
|
||||||
from requests.exceptions import HTTPError
|
from requests.exceptions import HTTPError
|
||||||
|
|
||||||
from .base import Item, Storage
|
from .base import Item, Storage
|
||||||
|
|
@ -24,8 +24,8 @@ def _normalize_href(base, href):
|
||||||
schema.'''
|
schema.'''
|
||||||
if not href:
|
if not href:
|
||||||
raise ValueError(href)
|
raise ValueError(href)
|
||||||
x = utils.urlparse.urljoin(base, href)
|
x = utils.compat.urlparse.urljoin(base, href)
|
||||||
x = utils.urlparse.urlsplit(x).path
|
x = utils.compat.urlparse.urlsplit(x).path
|
||||||
return x
|
return x
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -121,7 +121,7 @@ class Discover(object):
|
||||||
rv = root.find('.//{DAV:}current-user-principal/{DAV:}href')
|
rv = root.find('.//{DAV:}current-user-principal/{DAV:}href')
|
||||||
if rv is None:
|
if rv is None:
|
||||||
raise InvalidXMLResponse()
|
raise InvalidXMLResponse()
|
||||||
return utils.urlparse.urljoin(response.url, rv.text)
|
return utils.compat.urlparse.urljoin(response.url, rv.text)
|
||||||
|
|
||||||
def find_home(self, url=None):
|
def find_home(self, url=None):
|
||||||
if url is None:
|
if url is None:
|
||||||
|
|
@ -137,7 +137,7 @@ class Discover(object):
|
||||||
rv = root.find('.//' + self._homeset_tag + '/{*}href')
|
rv = root.find('.//' + self._homeset_tag + '/{*}href')
|
||||||
if rv is None:
|
if rv is None:
|
||||||
raise InvalidXMLResponse()
|
raise InvalidXMLResponse()
|
||||||
return utils.urlparse.urljoin(response.url, rv.text)
|
return utils.compat.urlparse.urljoin(response.url, rv.text)
|
||||||
|
|
||||||
def find_collections(self, url=None):
|
def find_collections(self, url=None):
|
||||||
if url is None:
|
if url is None:
|
||||||
|
|
@ -157,7 +157,7 @@ class Discover(object):
|
||||||
href = response.find('{*}href')
|
href = response.find('{*}href')
|
||||||
if href is None:
|
if href is None:
|
||||||
raise InvalidXMLResponse()
|
raise InvalidXMLResponse()
|
||||||
href = utils.urlparse.urljoin(r.url, href.text)
|
href = utils.compat.urlparse.urljoin(r.url, href.text)
|
||||||
if href not in done:
|
if href not in done:
|
||||||
done.add(href)
|
done.add(href)
|
||||||
yield {'href': href, 'displayname': displayname}
|
yield {'href': href, 'displayname': displayname}
|
||||||
|
|
@ -221,19 +221,19 @@ class DavSession(object):
|
||||||
}
|
}
|
||||||
self.useragent = useragent
|
self.useragent = useragent
|
||||||
self.url = url.rstrip('/') + '/'
|
self.url = url.rstrip('/') + '/'
|
||||||
self.parsed_url = utils.urlparse.urlparse(self.url)
|
self.parsed_url = utils.compat.urlparse.urlparse(self.url)
|
||||||
self._session = None
|
self._session = None
|
||||||
|
|
||||||
def request(self, method, path, **kwargs):
|
def request(self, method, path, **kwargs):
|
||||||
url = self.url
|
url = self.url
|
||||||
if path:
|
if path:
|
||||||
url = utils.urlparse.urljoin(self.url, path)
|
url = utils.compat.urlparse.urljoin(self.url, path)
|
||||||
if self._session is None:
|
if self._session is None:
|
||||||
self._session = requests_session()
|
self._session = requests.session()
|
||||||
|
|
||||||
more = dict(self._settings)
|
more = dict(self._settings)
|
||||||
more.update(kwargs)
|
more.update(kwargs)
|
||||||
return utils.request(method, url, session=self._session, **more)
|
return utils.http.request(method, url, session=self._session, **more)
|
||||||
|
|
||||||
def get_default_headers(self):
|
def get_default_headers(self):
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,10 @@
|
||||||
|
|
||||||
from .base import Item, Storage
|
from .base import Item, Storage
|
||||||
from ..exceptions import NotFoundError
|
from ..exceptions import NotFoundError
|
||||||
from ..utils import expand_path, get_password, request
|
from ..utils import expand_path
|
||||||
from ..utils.compat import iteritems, text_type, urlparse
|
from ..utils.compat import iteritems, text_type, urlparse
|
||||||
|
from ..utils.http import request
|
||||||
|
from ..utils.password import get_password
|
||||||
from ..utils.vobject import split_collection
|
from ..utils.vobject import split_collection
|
||||||
|
|
||||||
USERAGENT = 'vdirsyncer'
|
USERAGENT = 'vdirsyncer'
|
||||||
|
|
|
||||||
|
|
@ -2,28 +2,14 @@
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import threading
|
|
||||||
|
|
||||||
import requests
|
from .compat import iteritems
|
||||||
|
|
||||||
from .compat import iteritems, urlparse
|
|
||||||
from .. import exceptions, log
|
from .. import exceptions, log
|
||||||
from ..doubleclick import click, ctx
|
|
||||||
|
|
||||||
|
|
||||||
logger = log.get(__name__)
|
|
||||||
_missing = object()
|
_missing = object()
|
||||||
|
|
||||||
|
|
||||||
try:
|
|
||||||
import keyring
|
|
||||||
except ImportError:
|
|
||||||
keyring = None
|
|
||||||
|
|
||||||
|
|
||||||
password_key_prefix = 'vdirsyncer:'
|
|
||||||
|
|
||||||
|
|
||||||
def expand_path(p):
|
def expand_path(p):
|
||||||
p = os.path.expanduser(p)
|
p = os.path.expanduser(p)
|
||||||
p = os.path.normpath(p)
|
p = os.path.normpath(p)
|
||||||
|
|
@ -60,204 +46,6 @@ def uniq(s):
|
||||||
yield x
|
yield x
|
||||||
|
|
||||||
|
|
||||||
def get_password(username, resource, _lock=threading.Lock()):
|
|
||||||
"""tries to access saved password or asks user for it
|
|
||||||
|
|
||||||
will try the following in this order:
|
|
||||||
1. read password from netrc (and only the password, username
|
|
||||||
in netrc will be ignored)
|
|
||||||
2. read password from keyring (keyring needs to be installed)
|
|
||||||
3. read password from the command passed as password_command in the
|
|
||||||
general config section with username and host as parameters
|
|
||||||
4a ask user for the password
|
|
||||||
b save in keyring if installed and user agrees
|
|
||||||
|
|
||||||
:param username: user's name on the server
|
|
||||||
:type username: str/unicode
|
|
||||||
:param resource: a resource to which the user has access via password,
|
|
||||||
it will be shortened to just the hostname. It is assumed
|
|
||||||
that each unique username/hostname combination only ever
|
|
||||||
uses the same password.
|
|
||||||
:type resource: str/unicode
|
|
||||||
:return: password
|
|
||||||
:rtype: str/unicode
|
|
||||||
|
|
||||||
|
|
||||||
"""
|
|
||||||
if ctx:
|
|
||||||
password_cache = ctx.obj.setdefault('passwords', {})
|
|
||||||
|
|
||||||
with _lock:
|
|
||||||
host = urlparse.urlsplit(resource).hostname
|
|
||||||
for func in (_password_from_command, _password_from_cache,
|
|
||||||
_password_from_netrc, _password_from_keyring):
|
|
||||||
password = func(username, host)
|
|
||||||
if password is not None:
|
|
||||||
logger.debug('Got password for {} from {}'
|
|
||||||
.format(username, func.__doc__))
|
|
||||||
return password
|
|
||||||
|
|
||||||
prompt = ('Server password for {} at host {}'.format(username, host))
|
|
||||||
password = click.prompt(prompt, hide_input=True)
|
|
||||||
|
|
||||||
if ctx and func is not _password_from_cache:
|
|
||||||
password_cache[(username, host)] = password
|
|
||||||
if keyring is not None and \
|
|
||||||
click.confirm('Save this password in the keyring?',
|
|
||||||
default=False):
|
|
||||||
keyring.set_password(password_key_prefix + host,
|
|
||||||
username, password)
|
|
||||||
|
|
||||||
return password
|
|
||||||
|
|
||||||
|
|
||||||
def _password_from_cache(username, host):
|
|
||||||
'''internal cache'''
|
|
||||||
if ctx:
|
|
||||||
return ctx.obj['passwords'].get((username, host), None)
|
|
||||||
|
|
||||||
|
|
||||||
def _password_from_netrc(username, host):
|
|
||||||
'''.netrc'''
|
|
||||||
from netrc import netrc
|
|
||||||
|
|
||||||
try:
|
|
||||||
netrc_user, account, password = \
|
|
||||||
netrc().authenticators(host) or (None, None, None)
|
|
||||||
if netrc_user == username:
|
|
||||||
return password
|
|
||||||
except IOError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def _password_from_keyring(username, host):
|
|
||||||
'''system keyring'''
|
|
||||||
if keyring is None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
return keyring.get_password(password_key_prefix + host, username)
|
|
||||||
|
|
||||||
|
|
||||||
def _password_from_command(username, host):
|
|
||||||
'''command'''
|
|
||||||
import subprocess
|
|
||||||
|
|
||||||
if not ctx:
|
|
||||||
return None
|
|
||||||
|
|
||||||
try:
|
|
||||||
general, _, _ = ctx.obj['config']
|
|
||||||
command = general['password_command'].split()
|
|
||||||
except KeyError:
|
|
||||||
return None
|
|
||||||
|
|
||||||
if not command:
|
|
||||||
return None
|
|
||||||
|
|
||||||
command[0] = expand_path(command[0])
|
|
||||||
|
|
||||||
try:
|
|
||||||
stdout = subprocess.check_output(command + [username, host],
|
|
||||||
universal_newlines=True)
|
|
||||||
return stdout.strip()
|
|
||||||
except OSError as e:
|
|
||||||
logger.warning('Failed to execute command: {}\n{}'.
|
|
||||||
format(' '.join(command), str(e)))
|
|
||||||
|
|
||||||
|
|
||||||
def _verify_fingerprint_works():
|
|
||||||
try:
|
|
||||||
import requests
|
|
||||||
from pkg_resources import parse_version as ver
|
|
||||||
|
|
||||||
return ver(requests.__version__) >= ver('2.4.1')
|
|
||||||
except Exception:
|
|
||||||
return False
|
|
||||||
|
|
||||||
# https://github.com/shazow/urllib3/pull/444
|
|
||||||
#
|
|
||||||
# Without the above pull request, `verify=False` also disables fingerprint
|
|
||||||
# validation. This is *not* what we want, and it's not possible to replicate
|
|
||||||
# vdirsyncer's current behavior (verifying fingerprints without verifying
|
|
||||||
# against CAs) with older versions of urllib3.
|
|
||||||
#
|
|
||||||
# We check this here instead of setup.py, because:
|
|
||||||
# - Python's packaging stuff doesn't check installed versions.
|
|
||||||
# - The people who don't use `verify_fingerprint` wouldn't care.
|
|
||||||
VERIFY_FINGERPRINT_WORKS = _verify_fingerprint_works()
|
|
||||||
del _verify_fingerprint_works
|
|
||||||
|
|
||||||
|
|
||||||
def _install_fingerprint_adapter(session, fingerprint):
|
|
||||||
prefix = 'https://'
|
|
||||||
try:
|
|
||||||
from requests_toolbelt.adapters.fingerprint import \
|
|
||||||
FingerprintAdapter
|
|
||||||
except ImportError:
|
|
||||||
raise RuntimeError('`verify_fingerprint` can only be used with '
|
|
||||||
'requests-toolbelt versions >= 0.4.0')
|
|
||||||
|
|
||||||
if not isinstance(session.adapters[prefix], FingerprintAdapter):
|
|
||||||
fingerprint_adapter = FingerprintAdapter(fingerprint)
|
|
||||||
session.mount(prefix, fingerprint_adapter)
|
|
||||||
|
|
||||||
|
|
||||||
def request(method, url, session=None, latin1_fallback=True,
|
|
||||||
verify_fingerprint=None, **kwargs):
|
|
||||||
'''
|
|
||||||
Wrapper method for requests, to ease logging and mocking. Parameters should
|
|
||||||
be the same as for ``requests.request``, except:
|
|
||||||
|
|
||||||
:param session: A requests session object to use.
|
|
||||||
:param verify_fingerprint: Optional. SHA1 or MD5 fingerprint of the
|
|
||||||
expected server certificate.
|
|
||||||
:param latin1_fallback: RFC-2616 specifies the default Content-Type of
|
|
||||||
text/* to be latin1, which is not always correct, but exactly what
|
|
||||||
requests is doing. Setting this parameter to False will use charset
|
|
||||||
autodetection (usually ending up with utf8) instead of plainly falling
|
|
||||||
back to this silly default. See
|
|
||||||
https://github.com/kennethreitz/requests/issues/2042
|
|
||||||
'''
|
|
||||||
|
|
||||||
if session is None:
|
|
||||||
session = requests.Session()
|
|
||||||
|
|
||||||
if verify_fingerprint is not None:
|
|
||||||
if not VERIFY_FINGERPRINT_WORKS:
|
|
||||||
raise RuntimeError('`verify_fingerprint` can only be used with '
|
|
||||||
'requests versions >= 2.4.1')
|
|
||||||
_install_fingerprint_adapter(session, verify_fingerprint)
|
|
||||||
kwargs['verify'] = False
|
|
||||||
|
|
||||||
func = session.request
|
|
||||||
|
|
||||||
logger.debug(u'{} {}'.format(method, url))
|
|
||||||
logger.debug(kwargs.get('headers', {}))
|
|
||||||
logger.debug(kwargs.get('data', None))
|
|
||||||
logger.debug('Sending request...')
|
|
||||||
r = func(method, url, **kwargs)
|
|
||||||
|
|
||||||
# See https://github.com/kennethreitz/requests/issues/2042
|
|
||||||
content_type = r.headers.get('Content-Type', '')
|
|
||||||
if not latin1_fallback and \
|
|
||||||
'charset' not in content_type and \
|
|
||||||
content_type.startswith('text/'):
|
|
||||||
logger.debug('Removing latin1 fallback')
|
|
||||||
r.encoding = None
|
|
||||||
|
|
||||||
logger.debug(r.status_code)
|
|
||||||
logger.debug(r.headers)
|
|
||||||
logger.debug(r.content)
|
|
||||||
|
|
||||||
if r.status_code == 412:
|
|
||||||
raise exceptions.PreconditionFailed(r.reason)
|
|
||||||
if r.status_code == 404:
|
|
||||||
raise exceptions.NotFoundError(r.reason)
|
|
||||||
|
|
||||||
r.raise_for_status()
|
|
||||||
return r
|
|
||||||
|
|
||||||
|
|
||||||
def get_etag_from_file(fpath):
|
def get_etag_from_file(fpath):
|
||||||
'''Get mtime-based etag from a filepath.'''
|
'''Get mtime-based etag from a filepath.'''
|
||||||
stat = os.stat(fpath)
|
stat = os.stat(fpath)
|
||||||
|
|
|
||||||
100
vdirsyncer/utils/http.py
Normal file
100
vdirsyncer/utils/http.py
Normal file
|
|
@ -0,0 +1,100 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
from .. import exceptions, log
|
||||||
|
|
||||||
|
|
||||||
|
logger = log.get(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def _verify_fingerprint_works():
|
||||||
|
try:
|
||||||
|
from pkg_resources import parse_version as ver
|
||||||
|
|
||||||
|
return ver(requests.__version__) >= ver('2.4.1')
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# https://github.com/shazow/urllib3/pull/444
|
||||||
|
#
|
||||||
|
# Without the above pull request, `verify=False` also disables fingerprint
|
||||||
|
# validation. This is *not* what we want, and it's not possible to replicate
|
||||||
|
# vdirsyncer's current behavior (verifying fingerprints without verifying
|
||||||
|
# against CAs) with older versions of urllib3.
|
||||||
|
#
|
||||||
|
# We check this here instead of setup.py, because:
|
||||||
|
# - Python's packaging stuff doesn't check installed versions.
|
||||||
|
# - The people who don't use `verify_fingerprint` wouldn't care.
|
||||||
|
VERIFY_FINGERPRINT_WORKS = _verify_fingerprint_works()
|
||||||
|
del _verify_fingerprint_works
|
||||||
|
|
||||||
|
|
||||||
|
def _install_fingerprint_adapter(session, fingerprint):
|
||||||
|
prefix = 'https://'
|
||||||
|
try:
|
||||||
|
from requests_toolbelt.adapters.fingerprint import \
|
||||||
|
FingerprintAdapter
|
||||||
|
except ImportError:
|
||||||
|
raise RuntimeError('`verify_fingerprint` can only be used with '
|
||||||
|
'requests-toolbelt versions >= 0.4.0')
|
||||||
|
|
||||||
|
if not isinstance(session.adapters[prefix], FingerprintAdapter):
|
||||||
|
fingerprint_adapter = FingerprintAdapter(fingerprint)
|
||||||
|
session.mount(prefix, fingerprint_adapter)
|
||||||
|
|
||||||
|
|
||||||
|
def request(method, url, session=None, latin1_fallback=True,
|
||||||
|
verify_fingerprint=None, **kwargs):
|
||||||
|
'''
|
||||||
|
Wrapper method for requests, to ease logging and mocking. Parameters should
|
||||||
|
be the same as for ``requests.request``, except:
|
||||||
|
|
||||||
|
:param session: A requests session object to use.
|
||||||
|
:param verify_fingerprint: Optional. SHA1 or MD5 fingerprint of the
|
||||||
|
expected server certificate.
|
||||||
|
:param latin1_fallback: RFC-2616 specifies the default Content-Type of
|
||||||
|
text/* to be latin1, which is not always correct, but exactly what
|
||||||
|
requests is doing. Setting this parameter to False will use charset
|
||||||
|
autodetection (usually ending up with utf8) instead of plainly falling
|
||||||
|
back to this silly default. See
|
||||||
|
https://github.com/kennethreitz/requests/issues/2042
|
||||||
|
'''
|
||||||
|
|
||||||
|
if session is None:
|
||||||
|
session = requests.Session()
|
||||||
|
|
||||||
|
if verify_fingerprint is not None:
|
||||||
|
if not VERIFY_FINGERPRINT_WORKS:
|
||||||
|
raise RuntimeError('`verify_fingerprint` can only be used with '
|
||||||
|
'requests versions >= 2.4.1')
|
||||||
|
_install_fingerprint_adapter(session, verify_fingerprint)
|
||||||
|
kwargs['verify'] = False
|
||||||
|
|
||||||
|
func = session.request
|
||||||
|
|
||||||
|
logger.debug(u'{} {}'.format(method, url))
|
||||||
|
logger.debug(kwargs.get('headers', {}))
|
||||||
|
logger.debug(kwargs.get('data', None))
|
||||||
|
logger.debug('Sending request...')
|
||||||
|
r = func(method, url, **kwargs)
|
||||||
|
|
||||||
|
# See https://github.com/kennethreitz/requests/issues/2042
|
||||||
|
content_type = r.headers.get('Content-Type', '')
|
||||||
|
if not latin1_fallback and \
|
||||||
|
'charset' not in content_type and \
|
||||||
|
content_type.startswith('text/'):
|
||||||
|
logger.debug('Removing latin1 fallback')
|
||||||
|
r.encoding = None
|
||||||
|
|
||||||
|
logger.debug(r.status_code)
|
||||||
|
logger.debug(r.headers)
|
||||||
|
logger.debug(r.content)
|
||||||
|
|
||||||
|
if r.status_code == 412:
|
||||||
|
raise exceptions.PreconditionFailed(r.reason)
|
||||||
|
if r.status_code == 404:
|
||||||
|
raise exceptions.NotFoundError(r.reason)
|
||||||
|
|
||||||
|
r.raise_for_status()
|
||||||
|
return r
|
||||||
121
vdirsyncer/utils/password.py
Normal file
121
vdirsyncer/utils/password.py
Normal file
|
|
@ -0,0 +1,121 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import threading
|
||||||
|
import urlparse
|
||||||
|
|
||||||
|
from . import expand_path
|
||||||
|
from .. import log
|
||||||
|
from ..doubleclick import click, ctx
|
||||||
|
|
||||||
|
logger = log.get(__name__)
|
||||||
|
password_key_prefix = 'vdirsyncer:'
|
||||||
|
|
||||||
|
try:
|
||||||
|
import keyring
|
||||||
|
except ImportError:
|
||||||
|
keyring = None
|
||||||
|
|
||||||
|
|
||||||
|
def get_password(username, resource, _lock=threading.Lock()):
|
||||||
|
"""tries to access saved password or asks user for it
|
||||||
|
|
||||||
|
will try the following in this order:
|
||||||
|
1. read password from netrc (and only the password, username
|
||||||
|
in netrc will be ignored)
|
||||||
|
2. read password from keyring (keyring needs to be installed)
|
||||||
|
3. read password from the command passed as password_command in the
|
||||||
|
general config section with username and host as parameters
|
||||||
|
4a ask user for the password
|
||||||
|
b save in keyring if installed and user agrees
|
||||||
|
|
||||||
|
:param username: user's name on the server
|
||||||
|
:type username: str/unicode
|
||||||
|
:param resource: a resource to which the user has access via password,
|
||||||
|
it will be shortened to just the hostname. It is assumed
|
||||||
|
that each unique username/hostname combination only ever
|
||||||
|
uses the same password.
|
||||||
|
:type resource: str/unicode
|
||||||
|
:return: password
|
||||||
|
:rtype: str/unicode
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
if ctx:
|
||||||
|
password_cache = ctx.obj.setdefault('passwords', {})
|
||||||
|
|
||||||
|
with _lock:
|
||||||
|
host = urlparse.urlsplit(resource).hostname
|
||||||
|
for func in (_password_from_command, _password_from_cache,
|
||||||
|
_password_from_netrc, _password_from_keyring):
|
||||||
|
password = func(username, host)
|
||||||
|
if password is not None:
|
||||||
|
logger.debug('Got password for {} from {}'
|
||||||
|
.format(username, func.__doc__))
|
||||||
|
return password
|
||||||
|
|
||||||
|
prompt = ('Server password for {} at host {}'.format(username, host))
|
||||||
|
password = click.prompt(prompt, hide_input=True)
|
||||||
|
|
||||||
|
if ctx and func is not _password_from_cache:
|
||||||
|
password_cache[(username, host)] = password
|
||||||
|
if keyring is not None and \
|
||||||
|
click.confirm('Save this password in the keyring?',
|
||||||
|
default=False):
|
||||||
|
keyring.set_password(password_key_prefix + host,
|
||||||
|
username, password)
|
||||||
|
|
||||||
|
return password
|
||||||
|
|
||||||
|
|
||||||
|
def _password_from_cache(username, host):
|
||||||
|
'''internal cache'''
|
||||||
|
if ctx:
|
||||||
|
return ctx.obj['passwords'].get((username, host), None)
|
||||||
|
|
||||||
|
|
||||||
|
def _password_from_netrc(username, host):
|
||||||
|
'''.netrc'''
|
||||||
|
from netrc import netrc
|
||||||
|
|
||||||
|
try:
|
||||||
|
netrc_user, account, password = \
|
||||||
|
netrc().authenticators(host) or (None, None, None)
|
||||||
|
if netrc_user == username:
|
||||||
|
return password
|
||||||
|
except IOError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _password_from_keyring(username, host):
|
||||||
|
'''system keyring'''
|
||||||
|
if keyring is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return keyring.get_password(password_key_prefix + host, username)
|
||||||
|
|
||||||
|
|
||||||
|
def _password_from_command(username, host):
|
||||||
|
'''command'''
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
if not ctx:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
general, _, _ = ctx.obj['config']
|
||||||
|
command = general['password_command'].split()
|
||||||
|
except KeyError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if not command:
|
||||||
|
return None
|
||||||
|
|
||||||
|
command[0] = expand_path(command[0])
|
||||||
|
|
||||||
|
try:
|
||||||
|
stdout = subprocess.check_output(command + [username, host],
|
||||||
|
universal_newlines=True)
|
||||||
|
return stdout.strip()
|
||||||
|
except OSError as e:
|
||||||
|
logger.warning('Failed to execute command: {}\n{}'.
|
||||||
|
format(' '.join(command), str(e)))
|
||||||
Loading…
Reference in a new issue