tahoe-lafs/src/allmydata/web/root.py

579 lines
20 KiB
Python
Raw Normal View History

2021-01-14 14:40:10 +00:00
from future.utils import PY3
2021-01-12 19:24:11 +00:00
2019-09-04 05:48:01 +00:00
import os
import time
2021-01-12 19:39:20 +00:00
from urllib.parse import quote as urlquote
2020-04-30 22:56:03 +00:00
from hyperlink import DecodedURL, URL
from pkg_resources import resource_filename
2019-09-04 05:06:16 +00:00
from twisted.web import (
http,
resource,
2020-04-30 22:56:03 +00:00
static,
2019-09-04 05:06:16 +00:00
)
from twisted.web.util import redirectTo, Redirect
2020-04-27 20:42:03 +00:00
from twisted.python.filepath import FilePath
from twisted.web.template import (
Element,
XMLFile,
renderer,
renderElement,
2020-05-05 13:25:49 +00:00
tags,
2020-04-27 20:42:03 +00:00
)
import allmydata # to display import path
2020-12-21 15:21:39 +00:00
from allmydata.util import log, jsonbytes as json
from allmydata.interfaces import IFileNode
2020-04-30 22:56:03 +00:00
from allmydata.web import (
filenode,
directory,
unlinked,
status,
)
from allmydata.web import storage
2017-07-25 15:36:06 +00:00
from allmydata.web.common import (
abbreviate_size,
WebError,
exception_to_child,
2017-07-25 15:36:06 +00:00
get_arg,
MultiFormatResource,
SlotsSequenceElement,
2017-07-25 15:36:06 +00:00
get_format,
get_mutable_type,
render_exception,
2017-07-25 15:36:06 +00:00
render_time_delta,
render_time,
render_time_attr,
)
2019-03-21 19:00:57 +00:00
from allmydata.web.private import (
create_private_tree,
)
from allmydata import uri
2019-09-04 01:46:17 +00:00
class URIHandler(resource.Resource, object):
"""
I live at /uri . There are several operations defined on /uri itself,
mostly involved with creation of unlinked files and directories.
"""
def __init__(self, client):
2019-09-04 01:46:17 +00:00
super(URIHandler, self).__init__()
self.client = client
@render_exception
2019-09-04 05:06:16 +00:00
def render_GET(self, req):
"""
Historically, accessing this via "GET /uri?uri=<capabilitiy>"
was/is a feature -- which simply redirects to the more-common
"GET /uri/<capability>" with any other query args
preserved. New code should use "/uri/<cap>"
2019-09-04 05:06:16 +00:00
"""
uri_arg = req.args.get(b"uri", [None])[0]
if uri_arg is None:
raise WebError("GET /uri requires uri=")
2019-09-04 19:59:00 +00:00
# shennanigans like putting "%2F" or just "/" itself, or ../
# etc in the <cap> might be a vector for weirdness so we
# validate that this is a valid capability before proceeding.
cap = uri.from_string(uri_arg)
if isinstance(cap, uri.UnknownURI):
raise WebError("Invalid capability")
2019-09-04 19:59:00 +00:00
# so, using URL.from_text(req.uri) isn't going to work because
# it seems Nevow was creating absolute URLs including
# host/port whereas req.uri is absolute (but lacks host/port)
redir_uri = URL.from_text(req.prePathURL().decode('utf8'))
2021-01-12 19:39:20 +00:00
redir_uri = redir_uri.child(urlquote(uri_arg))
# add back all the query args that AREN'T "?uri="
for k, values in req.args.items():
2019-09-27 17:52:27 +00:00
if k != b"uri":
for v in values:
redir_uri = redir_uri.add(k.decode('utf8'), v.decode('utf8'))
return redirectTo(redir_uri.to_text().encode('utf8'), req)
@render_exception
2019-09-04 05:09:00 +00:00
def render_PUT(self, req):
"""
either "PUT /uri" to create an unlinked file, or
"PUT /uri?t=mkdir" to create an unlinked directory
"""
t = get_arg(req, "t", "").strip()
if t == "":
file_format = get_format(req, "CHK")
mutable_type = get_mutable_type(file_format)
if mutable_type is not None:
return unlinked.PUTUnlinkedSSK(req, self.client, mutable_type)
else:
return unlinked.PUTUnlinkedCHK(req, self.client)
if t == "mkdir":
return unlinked.PUTUnlinkedCreateDirectory(req, self.client)
2019-09-04 05:09:00 +00:00
errmsg = (
"/uri accepts only PUT, PUT?t=mkdir, POST?t=upload, "
"and POST?t=mkdir"
)
raise WebError(errmsg, http.BAD_REQUEST)
@render_exception
2019-09-04 05:09:00 +00:00
def render_POST(self, req):
"""
"POST /uri?t=upload&file=newfile" to upload an
unlinked file or "POST /uri?t=mkdir" to create a
new directory
"""
t = get_arg(req, "t", "").strip()
if t in ("", "upload"):
file_format = get_format(req)
mutable_type = get_mutable_type(file_format)
if mutable_type is not None:
return unlinked.POSTUnlinkedSSK(req, self.client, mutable_type)
else:
return unlinked.POSTUnlinkedCHK(req, self.client)
if t == "mkdir":
return unlinked.POSTUnlinkedCreateDirectory(req, self.client)
elif t == "mkdir-with-children":
return unlinked.POSTUnlinkedCreateDirectoryWithChildren(req,
self.client)
elif t == "mkdir-immutable":
return unlinked.POSTUnlinkedCreateImmutableDirectory(req,
self.client)
errmsg = ("/uri accepts only PUT, PUT?t=mkdir, POST?t=upload, "
"and POST?t=mkdir")
raise WebError(errmsg, http.BAD_REQUEST)
@exception_to_child
2019-09-04 01:46:17 +00:00
def getChild(self, name, req):
2019-09-04 05:09:00 +00:00
"""
Most requests look like /uri/<cap> so this fetches the capability
and creates and appropriate handler (depending on the kind of
capability it was passed).
"""
# this is in case a URI like "/uri/?uri=<valid capability>" is
# passed -- we re-direct to the non-trailing-slash version so
# that there is just one valid URI for "uri" resource.
2019-09-10 23:52:20 +00:00
if not name:
2020-04-18 07:50:53 +00:00
u = DecodedURL.from_text(req.uri.decode('utf8'))
u = u.replace(
path=(s for s in u.path if s), # remove empty segments
)
return Redirect(u.to_uri().to_text().encode('utf8'))
try:
node = self.client.create_node_from_uri(name)
return directory.make_handler_for(node, self.client)
2021-01-04 19:23:12 +00:00
except (TypeError, AssertionError) as e:
log.msg(format="Failed to parse cap, perhaps due to bug: %(e)s",
e=e, level=log.WEIRD)
2019-09-04 01:46:17 +00:00
raise WebError(
"'{}' is not a valid file- or directory- cap".format(name)
)
2019-09-11 00:53:54 +00:00
class FileHandler(resource.Resource, object):
# I handle /file/$FILECAP[/IGNORED] , which provides a URL from which a
# file can be downloaded correctly by tools like "wget".
def __init__(self, client):
2019-09-11 00:53:54 +00:00
super(FileHandler, self).__init__()
self.client = client
@exception_to_child
2019-09-11 00:53:54 +00:00
def getChild(self, name, req):
2021-01-22 20:07:03 +00:00
if req.method not in (b"GET", b"HEAD"):
raise WebError("/file can only be used with GET or HEAD")
# 'name' must be a file URI
try:
node = self.client.create_node_from_uri(name)
except (TypeError, AssertionError):
Overhaul IFilesystemNode handling, to simplify tests and use POLA internally. * stop using IURI as an adapter * pass cap strings around instead of URI instances * move filenode/dirnode creation duties from Client to new NodeMaker class * move other Client duties to KeyGenerator, SecretHolder, History classes * stop passing Client reference to dirnode/filenode constructors - pass less-powerful references instead, like StorageBroker or Uploader * always create DirectoryNodes by wrapping a filenode (mutable for now) * remove some specialized mock classes from unit tests Detailed list of changes (done one at a time, then merged together) always pass a string to create_node_from_uri(), not an IURI instance always pass a string to IFilesystemNode constructors, not an IURI instance stop using IURI() as an adapter, switch on cap prefix in create_node_from_uri() client.py: move SecretHolder code out to a separate class test_web.py: hush pyflakes client.py: move NodeMaker functionality out into a separate object LiteralFileNode: stop storing a Client reference immutable Checker: remove Client reference, it only needs a SecretHolder immutable Upload: remove Client reference, leave SecretHolder and StorageBroker immutable Repairer: replace Client reference with StorageBroker and SecretHolder immutable FileNode: remove Client reference mutable.Publish: stop passing Client mutable.ServermapUpdater: get StorageBroker in constructor, not by peeking into Client reference MutableChecker: reference StorageBroker and History directly, not through Client mutable.FileNode: removed unused indirection to checker classes mutable.FileNode: remove Client reference client.py: move RSA key generation into a separate class, so it can be passed to the nodemaker move create_mutable_file() into NodeMaker test_dirnode.py: stop using FakeClient mockups, use NoNetworkGrid instead. This simplifies the code, but takes longer to run (17s instead of 6s). This should come down later when other cleanups make it possible to use simpler (non-RSA) fake mutable files for dirnode tests. test_mutable.py: clean up basedir names client.py: move create_empty_dirnode() into NodeMaker dirnode.py: get rid of DirectoryNode.create remove DirectoryNode.init_from_uri, refactor NodeMaker for customization, simplify test_web's mock Client to match stop passing Client to DirectoryNode, make DirectoryNode.create_with_mutablefile the normal DirectoryNode constructor, start removing client from NodeMaker remove Client from NodeMaker move helper status into History, pass History to web.Status instead of Client test_mutable.py: fix minor typo
2009-08-15 11:02:56 +00:00
# I think this can no longer be reached
raise WebError("'%s' is not a valid file- or directory- cap"
% name)
if not IFileNode.providedBy(node):
raise WebError("'%s' is not a file-cap" % name)
return filenode.FileNodeDownloadHandler(self.client, node)
@render_exception
2020-10-21 14:12:48 +00:00
def render_GET(self, req):
raise WebError("/file must be followed by a file-cap and a name",
http.NOT_FOUND)
class IncidentReporter(MultiFormatResource):
"""Handler for /report_incident POST request"""
@render_exception
def render(self, req):
2021-01-22 20:07:03 +00:00
if req.method != b"POST":
raise WebError("/report_incident can only be used with POST")
log.msg(format="User reports incident through web page: %(details)s",
details=get_arg(req, "details", ""),
level=log.WEIRD, umid="LkD9Pw")
req.setHeader("content-type", "text/plain; charset=UTF-8")
return b"An incident report has been saved to logs/incidents/ in the node directory."
SPACE = u"\u00A0"*2
2020-04-27 20:42:03 +00:00
class Root(MultiFormatResource):
addSlash = True
2019-03-21 18:39:52 +00:00
def __init__(self, client, clock=None, now_fn=None):
2020-05-04 18:20:05 +00:00
"""
Render root page ("/") of the URI.
:client allmydata.client._Client: a stats provider.
:clock: unused here.
:now_fn: a function that returns current time.
"""
2020-04-27 20:42:03 +00:00
super(Root, self).__init__()
2020-05-01 21:38:24 +00:00
self._client = client
self._now_fn = now_fn
2019-08-06 23:06:57 +00:00
self.putChild(b"uri", URIHandler(client))
2021-01-12 19:24:11 +00:00
self.putChild(b"cap", URIHandler(client))
2019-03-21 19:00:57 +00:00
# Handler for everything beneath "/private", an area of the resource
# hierarchy which is only accessible with the private per-node API
# auth token.
2021-01-12 19:24:11 +00:00
self.putChild(b"private", create_private_tree(client.get_auth_token))
2021-01-12 19:24:11 +00:00
self.putChild(b"file", FileHandler(client))
self.putChild(b"named", FileHandler(client))
self.putChild(b"status", status.Status(client.get_history()))
self.putChild(b"statistics", status.Statistics(client.stats_provider))
2011-11-17 21:49:23 +00:00
static_dir = resource_filename("allmydata.web", "static")
for filen in os.listdir(static_dir):
child_path = filen
2021-01-14 14:40:10 +00:00
if PY3:
child_path = filen.encode("utf-8")
self.putChild(child_path, static.File(os.path.join(static_dir, filen)))
2021-01-12 19:24:11 +00:00
self.putChild(b"report_incident", IncidentReporter())
2019-08-06 23:06:57 +00:00
@exception_to_child
2019-08-06 23:06:57 +00:00
def getChild(self, path, request):
2020-04-30 22:49:05 +00:00
if not path:
# Render "/" path.
return self
2019-08-06 23:06:57 +00:00
if path == "helper_status":
# the Helper isn't attached until after the Tub starts, so this child
# needs to created on each request
2020-05-01 21:38:24 +00:00
return status.HelperStatus(self._client.helper)
if path == "storage":
# Storage isn't initialized until after the web hierarchy is
# constructed so this child needs to be created later than
# `__init__`.
try:
2020-05-01 21:38:24 +00:00
storage_server = self._client.getServiceNamed("storage")
except KeyError:
storage_server = None
2020-05-01 21:38:24 +00:00
return storage.StorageStatus(storage_server, self._client.nickname)
@render_exception
2020-04-27 20:42:03 +00:00
def render_HTML(self, req):
2020-05-01 21:38:24 +00:00
return renderElement(req, RootElement(self._client, self._now_fn))
@render_exception
2017-07-25 15:36:06 +00:00
def render_JSON(self, req):
req.setHeader("content-type", "application/json; charset=utf-8")
2020-05-01 21:38:24 +00:00
intro_summaries = [s.summary for s in self._client.introducer_connection_statuses()]
sb = self._client.get_storage_broker()
servers = self._describe_known_servers(sb)
result = {
"introducers": {
"statuses": intro_summaries,
},
"servers": servers
}
return json.dumps(result, indent=1) + "\n"
def _describe_known_servers(self, broker):
return sorted(list(
self._describe_server(server)
for server
in broker.get_known_servers()
))
def _describe_server(self, server):
status = server.get_connection_status()
description = {
u"nodeid": server.get_serverid(),
u"connection_status": status.summary,
u"available_space": server.get_available_space(),
u"nickname": server.get_nickname(),
u"version": None,
u"last_received_data": status.last_received_time,
}
version = server.get_version()
if version is not None:
description[u"version"] = version["application-version"]
return description
2020-04-27 20:42:03 +00:00
class RootElement(Element):
loader = XMLFile(FilePath(__file__).sibling("welcome.xhtml"))
2020-04-28 15:47:18 +00:00
def __init__(self, client, now_fn):
2020-04-27 20:42:03 +00:00
super(RootElement, self).__init__()
self._client = client
2020-04-28 15:47:18 +00:00
self._now_fn = now_fn
2020-04-27 20:42:03 +00:00
_connectedalts = {
"not-configured": "Not Configured",
"yes": "Connected",
"no": "Disconnected",
}
2020-04-27 20:42:03 +00:00
@renderer
def my_nodeid(self, req, tag):
tubid_s = "TubID: "+self._client.get_long_tubid()
return tags.td(self._client.get_long_nodeid(), title=tubid_s)
@renderer
def my_nickname(self, req, tag):
return tag(self._client.nickname)
2017-01-11 02:25:42 +00:00
def _connected_introducers(self):
return len([1 for cs in self._client.introducer_connection_statuses()
if cs.connected])
2020-04-28 11:17:22 +00:00
@renderer
def connected_introducers(self, req, tag):
return tag(str(self._connected_introducers()))
@renderer
def connected_to_at_least_one_introducer(self, req, tag):
if self._connected_introducers():
return "yes"
return "no"
@renderer
def connected_to_at_least_one_introducer_alt(self, req, tag):
state = self.connected_to_at_least_one_introducer(req, tag)
return self._connectedalts.get(state)
2020-04-27 21:19:19 +00:00
@renderer
def services(self, req, tag):
ul = tags.ul()
try:
2020-04-27 21:19:19 +00:00
ss = self._client.getServiceNamed("storage")
stats = ss.get_stats()
if stats["storage_server.accepting_immutable_shares"]:
msg = "accepting new shares"
else:
msg = "not accepting new shares (read-only)"
available = stats.get("storage_server.disk_avail")
if available is not None:
msg += ", %s available" % abbreviate_size(available)
2020-05-05 13:25:31 +00:00
ul(tags.li(tags.a("Storage Server", href="storage"), ": ", msg))
except KeyError:
2020-04-27 21:19:19 +00:00
ul(tags.li("Not running storage server"))
2020-04-27 21:19:19 +00:00
if self._client.helper:
stats = self._client.helper.get_stats()
active_uploads = stats["chk_upload_helper.active_uploads"]
2020-04-27 21:19:19 +00:00
ul(tags.li("Helper: %d active uploads" % (active_uploads,)))
else:
2020-04-27 21:19:19 +00:00
ul(tags.li("Not running helper"))
2020-04-27 21:19:19 +00:00
return tag(ul)
2020-04-27 20:55:10 +00:00
@renderer
def introducer_description(self, req, tag):
connected_count = self._connected_introducers()
if connected_count == 0:
2020-04-27 20:55:10 +00:00
return tag("No introducers connected")
elif connected_count == 1:
2020-04-27 20:55:10 +00:00
return tag("1 introducer connected")
else:
2020-04-27 20:55:10 +00:00
return tag("%s introducers connected" % (connected_count,))
2020-04-28 11:19:47 +00:00
@renderer
def total_introducers(self, req, tag):
2020-04-28 11:48:28 +00:00
return tag(str(len(self._get_introducers())))
# In case we configure multiple introducers
2020-04-28 11:47:36 +00:00
@renderer
def introducers(self, req, tag):
ix = self._get_introducers()
if not ix:
return tag("No introducers")
return tag
def _get_introducers(self):
return self._client.introducer_connection_statuses()
2020-04-27 21:11:36 +00:00
@renderer
def helper_furl_prefix(self, req, tag):
try:
2020-04-27 21:11:36 +00:00
uploader = self._client.getServiceNamed("uploader")
except KeyError:
2020-04-27 21:11:36 +00:00
return tag("None")
furl, connected = uploader.get_helper_info()
if not furl:
2020-04-27 21:11:36 +00:00
return tag("None")
# trim off the secret swissnum
(prefix, _, swissnum) = furl.rpartition("/")
2020-04-27 21:11:36 +00:00
return tag("%s/[censored]" % (prefix,))
def _connected_to_helper(self):
try:
2020-04-27 21:01:37 +00:00
uploader = self._client.getServiceNamed("uploader")
except KeyError:
return "no" # we don't even have an Uploader
furl, connected = uploader.get_helper_info()
if furl is None:
return "not-configured"
if connected:
return "yes"
return "no"
@renderer
def helper_description(self, req, tag):
if self._connected_to_helper() == "no":
return tag("Helper not connected")
return tag("Helper")
@renderer
def connected_to_helper(self, req, tag):
return tag(self._connected_to_helper())
2020-04-27 21:01:37 +00:00
@renderer
def connected_to_helper_alt(self, req, tag):
return tag(self._connectedalts.get(self._connected_to_helper()))
@renderer
def known_storage_servers(self, req, tag):
sb = self._client.get_storage_broker()
return tag(str(len(sb.get_all_serverids())))
@renderer
def connected_storage_servers(self, req, tag):
sb = self._client.get_storage_broker()
return tag(str(len(sb.get_connected_servers())))
@renderer
def services_table(self, req, tag):
rows = [ self._describe_server_and_connection(server)
for server in self._services() ]
return SlotsSequenceElement(tag, rows)
@renderer
def introducers_table(self, req, tag):
rows = [ self._describe_connection_status(cs)
for cs in self._get_introducers() ]
return SlotsSequenceElement(tag, rows)
2020-04-28 11:14:35 +00:00
def _services(self):
sb = self._client.get_storage_broker()
return sorted(sb.get_known_servers(), key=lambda s: s.get_serverid())
2020-05-01 21:42:46 +00:00
@staticmethod
def _describe_server(server):
"""Return a dict containing server stats."""
peerid = server.get_longname()
nickname = server.get_nickname()
version = server.get_announcement().get("my-version", "")
2020-04-28 11:14:35 +00:00
space = server.get_available_space()
if space is not None:
available_space = abbreviate_size(space)
else:
available_space = "N/A"
2020-04-28 11:14:35 +00:00
return {
"peerid": peerid,
"nickname": nickname,
"version": version,
"available_space": available_space,
}
def _describe_server_and_connection(self, server):
"""Return a dict containing both server and connection stats."""
srvstat = self._describe_server(server)
cs = server.get_connection_status()
constat = self._describe_connection_status(cs)
2020-05-04 18:05:33 +00:00
return dict(list(srvstat.items()) + list(constat.items()))
def _describe_connection_status(self, cs):
"""Return a dict containing some connection stats."""
others = cs.non_connected_statuses
if cs.connected:
summary = cs.summary
if others:
hints = "\n".join(["* %s: %s\n" % (which, others[which])
for which in sorted(others)])
details = "Other hints:\n" + hints
2020-04-28 11:14:35 +00:00
else:
details = "(no other hints)"
else:
details = tags.ul()
for which in sorted(others):
details(tags.li("%s: %s" % (which, others[which])))
summary = [cs.summary, details]
connected = "yes" if cs.connected else "no"
connected_alt = self._connectedalts[connected]
since = cs.last_connection_time
if since is not None:
service_connection_status_rel_time = render_time_delta(since, self._now_fn())
service_connection_status_abs_time = render_time_attr(since)
else:
service_connection_status_rel_time = "N/A"
service_connection_status_abs_time = "N/A"
last_received_data_time = cs.last_received_time
if last_received_data_time is not None:
last_received_data_abs_time = render_time_attr(last_received_data_time)
last_received_data_rel_time = render_time_delta(last_received_data_time, self._now_fn())
else:
last_received_data_abs_time = "N/A"
last_received_data_rel_time = "N/A"
return {
"summary": summary,
"details": details,
"service_connection_status": connected,
"service_connection_status_alt": connected_alt,
"service_connection_status_abs_time": service_connection_status_abs_time,
"service_connection_status_rel_time": service_connection_status_rel_time,
"last_received_data_abs_time": last_received_data_abs_time,
"last_received_data_rel_time": last_received_data_rel_time,
}
@renderer
def incident_button(self, req, tag):
# this button triggers a foolscap-logging "incident"
form = tags.form(
tags.fieldset(
tags.input(type="hidden", name="t", value="report-incident"),
"What went wrong?"+SPACE,
tags.input(type="text", name="details"), SPACE,
tags.input(type="submit", value=u"Save \u00BB"),
),
action="report_incident",
method="post",
enctype="multipart/form-data"
)
return tags.div(form)
2020-04-28 11:55:10 +00:00
@renderer
def rendered_at(self, req, tag):
2020-05-01 20:50:30 +00:00
return tag(render_time(time.time()))
2020-04-28 11:56:27 +00:00
@renderer
def version(self, req, tag):
return tag(allmydata.__full_version__)
2020-04-28 11:56:27 +00:00
2020-04-28 11:57:15 +00:00
@renderer
def import_path(self, req, tag):
2020-05-01 20:50:30 +00:00
return tag(str(allmydata))