290 lines
8.2 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 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,
)
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):
fresh, cap = self._uri.add_data(kind, data)
return cap
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-30 08:15:58 -06:00
Deterministically generates a stream of valid capabilities of the
2020-06-13 00:39:12 -06:00
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-13 17:28:28 -06:00
@attr.s
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
2020-06-13 17:28:28 -06:00
data = attr.ib(default=attr.Factory(dict))
capability_generators = attr.ib(default=attr.Factory(dict))
2020-06-11 13:26:09 -06:00
def _generate_capability(self, kind):
"""
:param str kind: any valid capability-string type
:returns: the next capability-string for the given kind
"""
2020-06-13 17:28:28 -06:00
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):
"""
2020-06-11 13:26:09 -06:00
adds some data to our grid
:returns: a two-tuple: a bool (True if the data is freshly added) and 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
2020-06-13 17:28:28 -06:00
for k in self.data:
if self.data[k] == data:
return (False, k)
cap = self._generate_capability(kind)
2020-06-30 08:17:34 -06:00
# it should be impossible for this to already be in our data,
# but check anyway to be sure
if cap in self.data:
raise Exception("Internal error; key already exists somehow")
2020-06-13 17:28:28 -06:00
self.data[cap] = data
return (True, cap)
def render_PUT(self, request):
data = request.content.read()
fresh, cap = self.add_data("URI:CHK:", data)
if fresh:
request.setResponseCode(http.CREATED) # real code does this for brand-new files
else:
request.setResponseCode(http.OK) # replaced/modified files
return cap
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]
fresh, cap = self.add_data(kind, data)
return cap
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
# it's legal to use the form "/uri/<capability>"
if capability is None and request.postpath and request.postpath[0]:
capability = request.postpath[0]
# if we don't yet have a capability, that's an error
2020-06-13 00:46:55 -06:00
if capability is None:
request.setResponseCode(http.BAD_REQUEST)
return b"GET /uri requires uri="
# the user gave us a capability; if our Grid doesn't have any
# data for it, that's an error.
2020-06-13 17:28:28 -06:00
if capability not in self.data:
request.setResponseCode(http.BAD_REQUEST)
return u"No data for '{}'".format(capability).decode("ascii")
2020-06-13 17:28:28 -06:00
return self.data[capability]
def create_fake_tahoe_root():
"""
2020-06-13 02:03:19 -06:00
If you wish to pre-populate data into the fake Tahoe grid, retain
2020-06-13 17:22:17 -06:00
a reference to this root by creating it yourself and passing it to
2020-06-13 02:03:19 -06:00
`create_tahoe_treq_client`. For example::
root = create_fake_tahoe_root()
2020-06-13 17:22:17 -06:00
cap_string = root.add_data(...)
2020-06-13 02:03:19 -06:00
client = create_tahoe_treq_client(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