292 lines
7.9 KiB
Python
Raw Normal View History

2020-06-12 23:01:02 -06:00
# -*- coding: utf-8 -*-
# Tahoe-LAFS -- secure, distributed storage grid
#
# Copyright © 2020 The Tahoe-LAFS Software Foundation
#
# This file is part of Tahoe-LAFS.
#
# See the docs/about.rst file for licensing information.
"""
Test-helpers for clients that use the WebUI.
"""
2020-06-11 13:26:09 -06:00
import hashlib
import attr
from hyperlink import DecodedURL
from twisted.web.resource import (
Resource,
)
from twisted.web.iweb import (
IBodyProducer,
)
2020-06-13 01:14:45 -06:00
from twisted.web import (
http,
)
2020-06-11 13:26:09 -06:00
from twisted.internet.defer import (
succeed,
)
from twisted.python.failure import (
Failure,
)
from treq.client import (
HTTPClient,
FileBodyProducer,
)
from treq.testing import (
RequestTraversalAgent,
)
2020-06-11 13:26:09 -06:00
from zope.interface import implementer
import allmydata.uri
from allmydata.util import (
base32,
)
from allmydata.interfaces import (
ExistingChildError,
)
from allmydata.web.common import (
humanize_failure,
)
2020-06-12 23:01:02 -06:00
__all__ = (
"create_fake_tahoe_root",
"create_tahoe_treq_client",
)
2020-06-11 15:34:47 -06:00
class _FakeTahoeRoot(Resource, object):
"""
2020-06-12 23:05:32 -06:00
An in-memory 'fake' of a Tahoe WebUI root. Currently it only
implements (some of) the `/uri` resource.
"""
def __init__(self, uri=None):
2020-06-12 23:06:33 -06:00
"""
:param uri: a Resource to handle the `/uri` tree.
"""
Resource.__init__(self) # this is an old-style class :(
self._uri = uri
self.putChild(b"uri", self._uri)
2020-06-11 19:57:21 -06:00
def add_data(self, kind, data):
return self._uri.add_data(kind, data)
KNOWN_CAPABILITIES = [
getattr(allmydata.uri, t).BASE_STRING
for t in dir(allmydata.uri)
if hasattr(getattr(allmydata.uri, t), 'BASE_STRING')
]
2020-06-11 13:26:09 -06:00
def capability_generator(kind):
"""
2020-06-13 00:39:12 -06:00
Deterministically generates a streap of valid capabilities of the
given kind. The N, K and size values aren't related to anything
real.
2020-06-11 13:26:09 -06:00
:param str kind: the kind of capability, like `URI:CHK`
2020-06-11 13:26:09 -06:00
:returns: a generator that yields new capablities of a particular
kind.
"""
if kind not in KNOWN_CAPABILITIES:
raise ValueError(
2020-06-11 13:26:09 -06:00
"Unknown capability kind '{} (valid are {})'".format(
kind,
2020-06-11 13:26:09 -06:00
", ".join(KNOWN_CAPABILITIES),
)
)
2020-06-11 13:26:09 -06:00
# what we do here is to start with empty hashers for the key and
# ueb_hash and repeatedly feed() them a zero byte on each
# iteration .. so the same sequence of capabilities will always be
# produced. We could add a seed= argument if we wanted to produce
# different sequences.
number = 0
2020-06-11 13:26:09 -06:00
key_hasher = hashlib.new("sha256")
2020-06-12 23:08:04 -06:00
ueb_hasher = hashlib.new("sha256") # ueb means "URI Extension Block"
2020-06-11 13:26:09 -06:00
# capabilities are "prefix:<128-bits-base32>:<256-bits-base32>:N:K:size"
while True:
number += 1
2020-06-11 13:26:09 -06:00
key_hasher.update("\x00")
ueb_hasher.update("\x00")
key = base32.b2a(key_hasher.digest()[:16]) # key is 16 bytes
ueb_hash = base32.b2a(ueb_hasher.digest()) # ueb hash is 32 bytes
2020-06-12 22:57:38 -06:00
cap = u"{kind}{key}:{ueb_hash}:{n}:{k}:{size}".format(
2020-06-11 13:26:09 -06:00
kind=kind,
key=key,
ueb_hash=ueb_hash,
n=1,
k=1,
size=number * 1000,
)
yield cap.encode("ascii")
2020-06-11 15:34:47 -06:00
class _FakeTahoeUriHandler(Resource, object):
"""
2020-06-12 23:05:32 -06:00
An in-memory fake of (some of) the `/uri` endpoint of a Tahoe
WebUI
"""
isLeaf = True
_data = None
2020-06-11 13:26:09 -06:00
_capability_generators = None
def _generate_capability(self, kind):
"""
:param str kind: any valid capability-string type
:returns: the next capability-string for the given kind
"""
if self._capability_generators is None:
self._capability_generators = dict()
if kind not in self._capability_generators:
self._capability_generators[kind] = capability_generator(kind)
capability = next(self._capability_generators[kind])
return capability
def add_data(self, kind, data, allow_duplicate=False):
"""
2020-06-11 13:26:09 -06:00
adds some data to our grid
:returns: a capability-string
"""
2020-06-13 00:39:57 -06:00
if not isinstance(data, bytes):
raise TypeError("'data' must be bytes")
2020-06-11 13:26:09 -06:00
if self._data is None:
self._data = dict()
for k in self._data:
if self._data[k] == data:
if allow_duplicate:
return k
raise ValueError(
"Duplicate data"
)
cap = self._generate_capability(kind)
2020-06-13 00:41:22 -06:00
if cap in self._data:
raise ValueError("already have '{}'".format(cap))
self._data[cap] = data
2020-06-11 13:26:09 -06:00
return cap
def render_PUT(self, request):
data = request.content.read()
2020-06-13 01:14:45 -06:00
request.setResponseCode(http.CREATED) # real code does this for brand-new files
replace = request.args.get("replace", None)
try:
return self.add_data("URI:CHK:", data, allow_duplicate=replace)
except ValueError:
msg, code = humanize_failure(Failure(ExistingChildError()))
request.setResponseCode(code)
return msg
def render_POST(self, request):
2020-06-11 13:26:09 -06:00
t = request.args[u"t"][0]
data = request.content.read()
2020-06-11 13:26:09 -06:00
type_to_kind = {
"mkdir-immutable": "URI:DIR2-CHK:"
}
kind = type_to_kind[t]
return self.add_data(kind, data)
def render_GET(self, request):
uri = DecodedURL.from_text(request.uri.decode('utf8'))
2020-06-13 00:46:55 -06:00
capability = None
for arg, value in uri.query:
if arg == u"uri":
capability = value
if capability is None:
raise Exception(
"No ?uri= arguent in GET '{}'".format(
uri.to_string()
)
)
if self._data is None or capability not in self._data:
return u"No data for '{}'".format(capability).decode("ascii")
return self._data[capability]
def create_fake_tahoe_root():
"""
2020-06-11 13:26:09 -06:00
:returns: an IResource instance that will handle certain Tahoe URI
endpoints similar to a real Tahoe server.
"""
root = _FakeTahoeRoot(
uri=_FakeTahoeUriHandler(),
)
2020-06-11 13:26:09 -06:00
return root
2020-06-01 09:06:46 -06:00
@implementer(IBodyProducer)
class _SynchronousProducer(object):
"""
A partial implementation of an :obj:`IBodyProducer` which produces its
entire payload immediately. There is no way to access to an instance of
this object from :obj:`RequestTraversalAgent` or :obj:`StubTreq`, or even a
:obj:`Resource: passed to :obj:`StubTreq`.
This does not implement the :func:`IBodyProducer.stopProducing` method,
because that is very difficult to trigger. (The request from
`RequestTraversalAgent` would have to be canceled while it is still in the
transmitting state), and the intent is to use `RequestTraversalAgent` to
make synchronous requests.
"""
def __init__(self, body):
"""
Create a synchronous producer with some bytes.
"""
if isinstance(body, FileBodyProducer):
body = body._inputFile.read()
2020-06-01 09:06:46 -06:00
if not isinstance(body, bytes):
raise ValueError(
"'body' must be bytes not '{}'".format(type(body))
2020-06-01 09:06:46 -06:00
)
self.body = body
self.length = len(body)
def startProducing(self, consumer):
"""
Immediately produce all data.
"""
consumer.write(self.body)
return succeed(None)
def create_tahoe_treq_client(root=None):
"""
2020-06-12 23:05:32 -06:00
:param root: an instance created via `create_fake_tahoe_root`. The
caller might want a copy of this to call `.add_data` for example.
:returns: an instance of treq.client.HTTPClient wired up to
in-memory fakes of the Tahoe WebUI. Only a subset of the real
WebUI is available.
2020-06-01 09:06:46 -06:00
"""
if root is None:
2020-06-11 13:26:09 -06:00
root = create_fake_tahoe_root()
2020-06-01 09:06:46 -06:00
client = HTTPClient(
agent=RequestTraversalAgent(root),
data_to_body_producer=_SynchronousProducer,
)
2020-06-11 13:26:09 -06:00
return client