227 lines
6.0 KiB
Python
Raw Normal View History

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-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-11 15:34:47 -06:00
class _FakeTahoeRoot(Resource, object):
"""
This is a sketch of how an in-memory 'fake' of a Tahoe
WebUI. Ultimately, this will live in Tahoe
"""
def __init__(self, uri=None):
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)
@attr.s
class _FakeCapability(object):
"""
"""
data=attr.ib()
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):
"""
: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")
ueb_hasher = hashlib.new("sha256")
# 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
cap = u"{kind}:{key}:{ueb_hash}:{n}:{k}:{size}".format(
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):
"""
"""
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
2020-06-11 13:26:09 -06:00
def add_data(self, kind, data):
"""
2020-06-11 13:26:09 -06:00
adds some data to our grid
:returns: a capability-string
"""
2020-06-11 13:26:09 -06:00
assert isinstance(data, bytes)
cap = self._generate_capability(kind)
if self._data is None:
self._data = dict()
2020-06-11 13:26:09 -06:00
assert cap not in self._data, "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-12 22:08:36 -06:00
request.setResponseCode(201) # real code does this for brand-new files
2020-06-11 13:26:09 -06:00
return self.add_data("URI:CHK:", data)
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'))
# XXX FIXME
capability = uri.query[0][1]
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):
"""
"""
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