kopia lustrzana https://gitlab.com/jaywink/federation
Merge pull request #33 from jaywink/fetch-hcard
Add utilities to fetch Diaspora user discovery documentsmerge-requests/130/head
commit
43fbc4acae
|
@ -0,0 +1,5 @@
|
||||||
|
[run]
|
||||||
|
omit =
|
||||||
|
setup.py
|
||||||
|
federation/__init__.py
|
||||||
|
*/tests/*
|
|
@ -0,0 +1 @@
|
||||||
|
__version__ = "0.3.2"
|
|
@ -0,0 +1 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
|
@ -0,0 +1,84 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from unittest.mock import patch
|
||||||
|
from urllib.parse import quote
|
||||||
|
|
||||||
|
from federation.hostmeta.generators import DiasporaWebFinger, DiasporaHostMeta
|
||||||
|
from federation.utils.diaspora import retrieve_diaspora_hcard, retrieve_diaspora_webfinger, retrieve_diaspora_host_meta
|
||||||
|
|
||||||
|
|
||||||
|
class TestRetrieveDiasporaHCard(object):
|
||||||
|
@patch("federation.utils.diaspora.retrieve_diaspora_webfinger", return_value=None)
|
||||||
|
def test_retrieve_webfinger_is_called(self, mock_retrieve):
|
||||||
|
retrieve_diaspora_hcard("bob@localhost")
|
||||||
|
assert mock_retrieve.called_with("bob@localhost")
|
||||||
|
|
||||||
|
@patch("federation.utils.diaspora.fetch_document")
|
||||||
|
@patch("federation.utils.diaspora.retrieve_diaspora_webfinger")
|
||||||
|
def test_fetch_document_is_called(self, mock_retrieve, mock_fetch):
|
||||||
|
mock_retrieve.return_value = DiasporaWebFinger(
|
||||||
|
"bob@localhost", "https://localhost", "123", "456"
|
||||||
|
).xrd
|
||||||
|
mock_fetch.return_value = "document", None, None
|
||||||
|
document = retrieve_diaspora_hcard("bob@localhost")
|
||||||
|
mock_fetch.assert_called_with("https://localhost/hcard/users/123")
|
||||||
|
assert document == "document"
|
||||||
|
|
||||||
|
@patch("federation.utils.diaspora.fetch_document")
|
||||||
|
@patch("federation.utils.diaspora.retrieve_diaspora_webfinger")
|
||||||
|
def test_returns_none_on_fetch_document_exception(self, mock_retrieve, mock_fetch):
|
||||||
|
mock_retrieve.return_value = DiasporaWebFinger(
|
||||||
|
"bob@localhost", "https://localhost", "123", "456"
|
||||||
|
).xrd
|
||||||
|
mock_fetch.return_value = None, None, ValueError()
|
||||||
|
document = retrieve_diaspora_hcard("bob@localhost")
|
||||||
|
mock_fetch.assert_called_with("https://localhost/hcard/users/123")
|
||||||
|
assert document == None
|
||||||
|
|
||||||
|
|
||||||
|
class TestRetrieveDiasporaWebfinger(object):
|
||||||
|
@patch("federation.utils.diaspora.retrieve_diaspora_host_meta", return_value=None)
|
||||||
|
def test_retrieve_host_meta_is_called(self, mock_retrieve):
|
||||||
|
retrieve_diaspora_webfinger("bob@localhost")
|
||||||
|
mock_retrieve.assert_called_with("localhost")
|
||||||
|
|
||||||
|
@patch("federation.utils.diaspora.XRD.parse_xrd")
|
||||||
|
@patch("federation.utils.diaspora.fetch_document")
|
||||||
|
@patch("federation.utils.diaspora.retrieve_diaspora_host_meta", return_value=None)
|
||||||
|
def test_retrieve_fetch_document_is_called(self, mock_retrieve, mock_fetch, mock_xrd):
|
||||||
|
mock_retrieve.return_value = DiasporaHostMeta(
|
||||||
|
webfinger_host="https://localhost"
|
||||||
|
).xrd
|
||||||
|
mock_fetch.return_value = "document", None, None
|
||||||
|
mock_xrd.return_value = "document"
|
||||||
|
document = retrieve_diaspora_webfinger("bob@localhost")
|
||||||
|
mock_fetch.assert_called_with("https://localhost/webfinger?q=%s" % quote("bob@localhost"))
|
||||||
|
assert document == "document"
|
||||||
|
|
||||||
|
@patch("federation.utils.diaspora.fetch_document")
|
||||||
|
@patch("federation.utils.diaspora.retrieve_diaspora_host_meta", return_value=None)
|
||||||
|
def test_returns_none_on_fetch_document_exception(self, mock_retrieve, mock_fetch):
|
||||||
|
mock_retrieve.return_value = DiasporaHostMeta(
|
||||||
|
webfinger_host="https://localhost"
|
||||||
|
).xrd
|
||||||
|
mock_fetch.return_value = None, None, ValueError()
|
||||||
|
document = retrieve_diaspora_webfinger("bob@localhost")
|
||||||
|
mock_fetch.assert_called_with("https://localhost/webfinger?q=%s" % quote("bob@localhost"))
|
||||||
|
assert document == None
|
||||||
|
|
||||||
|
|
||||||
|
class TestRetrieveDiasporaHostMeta(object):
|
||||||
|
@patch("federation.utils.diaspora.XRD.parse_xrd")
|
||||||
|
@patch("federation.utils.diaspora.fetch_document")
|
||||||
|
def test_fetch_document_is_called(self, mock_fetch, mock_xrd):
|
||||||
|
mock_fetch.return_value = "document", None, None
|
||||||
|
mock_xrd.return_value = "document"
|
||||||
|
document = retrieve_diaspora_host_meta("localhost")
|
||||||
|
mock_fetch.assert_called_with(host="localhost", path="/.well-known/host-meta")
|
||||||
|
assert document == "document"
|
||||||
|
|
||||||
|
@patch("federation.utils.diaspora.fetch_document")
|
||||||
|
def test_returns_none_on_fetch_document_exception(self, mock_fetch):
|
||||||
|
mock_fetch.return_value = None, None, ValueError()
|
||||||
|
document = retrieve_diaspora_host_meta("localhost")
|
||||||
|
mock_fetch.assert_called_with(host="localhost", path="/.well-known/host-meta")
|
||||||
|
assert document == None
|
|
@ -0,0 +1,88 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from unittest.mock import patch, Mock, call
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from requests import HTTPError
|
||||||
|
from requests.exceptions import SSLError, RequestException
|
||||||
|
|
||||||
|
from federation.utils.network import fetch_document, USER_AGENT
|
||||||
|
|
||||||
|
|
||||||
|
class TestFetchDocument(object):
|
||||||
|
call_args = {"timeout": 10, "headers": {'user-agent': USER_AGENT}}
|
||||||
|
|
||||||
|
def test_raises_without_url_and_host(self):
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
fetch_document()
|
||||||
|
|
||||||
|
@patch("federation.utils.network.requests.get")
|
||||||
|
def test_url_is_called(self, mock_get):
|
||||||
|
mock_get.return_value = Mock(status_code=200, text="foo")
|
||||||
|
fetch_document("https://localhost")
|
||||||
|
assert mock_get.called
|
||||||
|
|
||||||
|
@patch("federation.utils.network.requests.get")
|
||||||
|
def test_host_is_called_with_https_first_then_http(self, mock_get):
|
||||||
|
def mock_failing_https_get(url, *args, **kwargs):
|
||||||
|
if url.find("https://") > -1:
|
||||||
|
raise HTTPError()
|
||||||
|
return Mock(status_code=200, text="foo")
|
||||||
|
mock_get.side_effect = mock_failing_https_get
|
||||||
|
fetch_document(host="localhost")
|
||||||
|
assert mock_get.call_count == 2
|
||||||
|
assert mock_get.call_args_list == [
|
||||||
|
call("https://localhost/", **self.call_args),
|
||||||
|
call("http://localhost/", **self.call_args),
|
||||||
|
]
|
||||||
|
|
||||||
|
@patch("federation.utils.network.requests.get")
|
||||||
|
def test_host_is_sanitized(self, mock_get):
|
||||||
|
mock_get.return_value = Mock(status_code=200, text="foo")
|
||||||
|
fetch_document(host="http://localhost")
|
||||||
|
assert mock_get.call_args_list == [
|
||||||
|
call("https://localhost/", **self.call_args)
|
||||||
|
]
|
||||||
|
|
||||||
|
@patch("federation.utils.network.requests.get")
|
||||||
|
def test_path_is_sanitized(self, mock_get):
|
||||||
|
mock_get.return_value = Mock(status_code=200, text="foo")
|
||||||
|
fetch_document(host="localhost", path="foobar/bazfoo")
|
||||||
|
assert mock_get.call_args_list == [
|
||||||
|
call("https://localhost/foobar/bazfoo", **self.call_args)
|
||||||
|
]
|
||||||
|
|
||||||
|
@patch("federation.utils.network.requests.get")
|
||||||
|
def test_exception_is_raised_if_both_protocols_fail(self, mock_get):
|
||||||
|
mock_get.side_effect = HTTPError
|
||||||
|
doc, code, exc = fetch_document(host="localhost")
|
||||||
|
assert mock_get.call_count == 2
|
||||||
|
assert doc == None
|
||||||
|
assert code == None
|
||||||
|
assert exc.__class__ == HTTPError
|
||||||
|
|
||||||
|
@patch("federation.utils.network.requests.get")
|
||||||
|
def test_exception_is_raised_if_url_fails(self, mock_get):
|
||||||
|
mock_get.side_effect = HTTPError
|
||||||
|
doc, code, exc = fetch_document("localhost")
|
||||||
|
assert mock_get.call_count == 1
|
||||||
|
assert doc == None
|
||||||
|
assert code == None
|
||||||
|
assert exc.__class__ == HTTPError
|
||||||
|
|
||||||
|
@patch("federation.utils.network.requests.get")
|
||||||
|
def test_exception_is_raised_if_http_fails_and_raise_ssl_errors_true(self, mock_get):
|
||||||
|
mock_get.side_effect = SSLError
|
||||||
|
doc, code, exc = fetch_document("localhost")
|
||||||
|
assert mock_get.call_count == 1
|
||||||
|
assert doc == None
|
||||||
|
assert code == None
|
||||||
|
assert exc.__class__ == SSLError
|
||||||
|
|
||||||
|
@patch("federation.utils.network.requests.get")
|
||||||
|
def test_exception_is_raised_on_network_error(self, mock_get):
|
||||||
|
mock_get.side_effect = RequestException
|
||||||
|
doc, code, exc = fetch_document(host="localhost")
|
||||||
|
assert mock_get.call_count == 1
|
||||||
|
assert doc == None
|
||||||
|
assert code == None
|
||||||
|
assert exc.__class__ == RequestException
|
|
@ -0,0 +1 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
|
@ -0,0 +1,61 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from urllib.parse import quote
|
||||||
|
|
||||||
|
from xrd import XRD
|
||||||
|
|
||||||
|
from federation.utils.network import fetch_document
|
||||||
|
|
||||||
|
|
||||||
|
def retrieve_diaspora_hcard(handle):
|
||||||
|
"""Retrieve a remote Diaspora hCard document.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
handle (str) - Remote handle to retrieve
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str (HTML document)
|
||||||
|
"""
|
||||||
|
webfinger = retrieve_diaspora_webfinger(handle)
|
||||||
|
if not webfinger:
|
||||||
|
return None
|
||||||
|
url = webfinger.find_link(rels="http://microformats.org/profile/hcard").href
|
||||||
|
document, code, exception = fetch_document(url)
|
||||||
|
if exception:
|
||||||
|
return None
|
||||||
|
return document
|
||||||
|
|
||||||
|
|
||||||
|
def retrieve_diaspora_webfinger(handle):
|
||||||
|
"""Retrieve a remote Diaspora webfinger document.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
handle (str) - Remote handle to retrieve
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
XRD
|
||||||
|
"""
|
||||||
|
hostmeta = retrieve_diaspora_host_meta(handle.split("@")[1])
|
||||||
|
if not hostmeta:
|
||||||
|
return None
|
||||||
|
url = hostmeta.find_link(rels="lrdd").template.replace("{uri}", quote(handle))
|
||||||
|
document, code, exception = fetch_document(url)
|
||||||
|
if exception:
|
||||||
|
return None
|
||||||
|
xrd = XRD.parse_xrd(document)
|
||||||
|
return xrd
|
||||||
|
|
||||||
|
|
||||||
|
def retrieve_diaspora_host_meta(host):
|
||||||
|
"""Retrieve a remote Diaspora host-meta document.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
host (str) - Host to retrieve from
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
XRD
|
||||||
|
"""
|
||||||
|
document, code, exception = fetch_document(host=host, path="/.well-known/host-meta")
|
||||||
|
if exception:
|
||||||
|
return None
|
||||||
|
xrd = XRD.parse_xrd(document)
|
||||||
|
return xrd
|
|
@ -0,0 +1,76 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from requests.exceptions import RequestException, HTTPError, SSLError
|
||||||
|
|
||||||
|
from federation import __version__
|
||||||
|
|
||||||
|
logger = logging.getLogger("social-federation")
|
||||||
|
|
||||||
|
USER_AGENT = "python/social-federation/%s" % __version__
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_document(url=None, host=None, path="/", timeout=10, raise_ssl_errors=True):
|
||||||
|
"""Helper method to fetch remote document.
|
||||||
|
|
||||||
|
Must be given either the `url` or `host`.
|
||||||
|
If `url` is given, only that will be tried without falling back to http from https.
|
||||||
|
If `host` given, `path` will be added to it. Will fall back to http on non-success status code.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
url (str) - Full url to fetch, including protocol
|
||||||
|
host (str) - Domain part only without path or protocol
|
||||||
|
path (str) - Path without domain (defaults to "/")
|
||||||
|
timeout (int) - Seconds to wait for response (defaults to 10)
|
||||||
|
raise_ssl_errors (bool) - Pass False if you want to try HTTP even for sites with SSL errors (default True)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
document (str) - The full document or None
|
||||||
|
status_code (int) - Status code returned or None
|
||||||
|
error (obj) - Exception raised, if any
|
||||||
|
"""
|
||||||
|
if not url and not host:
|
||||||
|
raise ValueError("Need url or host.")
|
||||||
|
|
||||||
|
logger.debug("fetch_document: url=%s, host=%s, path=%s, timeout=%s, raise_ssl_errors=%s",
|
||||||
|
url, host, path, timeout, raise_ssl_errors)
|
||||||
|
headers = {'user-agent': USER_AGENT}
|
||||||
|
if url:
|
||||||
|
# Use url since it was given
|
||||||
|
logger.debug("fetch_document: trying %s", url)
|
||||||
|
try:
|
||||||
|
response = requests.get(url, timeout=timeout, headers=headers)
|
||||||
|
logger.debug("fetch_document: found document, code %s", response.status_code)
|
||||||
|
return response.text, response.status_code, None
|
||||||
|
except RequestException as ex:
|
||||||
|
logger.debug("fetch_document: exception %s", ex)
|
||||||
|
return None, None, ex
|
||||||
|
# Build url with some little sanitizing
|
||||||
|
host_string = host.replace("http://", "").replace("https://", "").strip("/")
|
||||||
|
path_string = path if path.startswith("/") else "/%s" % path
|
||||||
|
url = "https://%s%s" % (host_string, path_string)
|
||||||
|
logger.debug("fetch_document: trying %s", url)
|
||||||
|
try:
|
||||||
|
response = requests.get(url, timeout=timeout, headers=headers)
|
||||||
|
logger.debug("fetch_document: found document, code %s", response.status_code)
|
||||||
|
response.raise_for_status()
|
||||||
|
return response.text, response.status_code, None
|
||||||
|
except (HTTPError, SSLError) as ex:
|
||||||
|
if isinstance(ex, SSLError) and raise_ssl_errors:
|
||||||
|
logger.debug("fetch_document: exception %s", ex)
|
||||||
|
return None, None, ex
|
||||||
|
# Try http then
|
||||||
|
url = url.replace("https://", "http://")
|
||||||
|
logger.debug("fetch_document: trying %s", url)
|
||||||
|
try:
|
||||||
|
response = requests.get(url, timeout=timeout, headers=headers)
|
||||||
|
logger.debug("fetch_document: found document, code %s", response.status_code)
|
||||||
|
response.raise_for_status()
|
||||||
|
return response.text, response.status_code, None
|
||||||
|
except RequestException as ex:
|
||||||
|
logger.debug("fetch_document: exception %s", ex)
|
||||||
|
return None, None, ex
|
||||||
|
except RequestException as ex:
|
||||||
|
logger.debug("fetch_document: exception %s", ex)
|
||||||
|
return None, None, ex
|
5
setup.py
5
setup.py
|
@ -1,13 +1,15 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
from setuptools import setup, find_packages
|
from setuptools import setup, find_packages
|
||||||
|
|
||||||
|
from federation import __version__
|
||||||
|
|
||||||
|
|
||||||
description = 'Python library for abstracting social federation protocols'
|
description = 'Python library for abstracting social federation protocols'
|
||||||
|
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
name='Social-Federation',
|
name='Social-Federation',
|
||||||
version='0.3.2',
|
version=__version__,
|
||||||
description=description,
|
description=description,
|
||||||
long_description=description,
|
long_description=description,
|
||||||
author='Jason Robinson',
|
author='Jason Robinson',
|
||||||
|
@ -25,6 +27,7 @@ setup(
|
||||||
"pycrypto>=2.6.0, <3.0.0",
|
"pycrypto>=2.6.0, <3.0.0",
|
||||||
"python-dateutil>=2.4.0, <3.0.0",
|
"python-dateutil>=2.4.0, <3.0.0",
|
||||||
"python-xrd==0.1",
|
"python-xrd==0.1",
|
||||||
|
"requests>=2.8.0",
|
||||||
],
|
],
|
||||||
include_package_data=True,
|
include_package_data=True,
|
||||||
classifiers=[
|
classifiers=[
|
||||||
|
|
Ładowanie…
Reference in New Issue