diff --git a/src/allmydata/web/check_results.py b/src/allmydata/web/check_results.py index c87254f0d..dfaee38e4 100644 --- a/src/allmydata/web/check_results.py +++ b/src/allmydata/web/check_results.py @@ -14,7 +14,7 @@ from twisted.web.template import ( renderElement, tags, ) -from allmydata.web.common_py3 import ( +from allmydata.web.common import ( exception_to_child, get_arg, get_root, diff --git a/src/allmydata/web/common.py b/src/allmydata/web/common.py index a5f29e263..abba7c267 100644 --- a/src/allmydata/web/common.py +++ b/src/allmydata/web/common.py @@ -1,32 +1,56 @@ +from future.utils import PY2 +from past.builtins import unicode import time import json +from functools import wraps from twisted.web import ( http, resource, server, + template, ) +from twisted.web.template import tags as T +from twisted.web.iweb import IRequest as ITwistedRequest from twisted.python import log -from nevow import appserver -from nevow.inevow import IRequest +if PY2: + from nevow.appserver import DefaultExceptionHandler + from nevow.inevow import IRequest as INevowRequest +else: + class DefaultExceptionHandler: + def __init__(self, *args, **kwargs): + raise NotImplementedError("Still not ported to Python 3") + INevowRequest = None + +from allmydata import blacklist from allmydata.interfaces import ( + EmptyPathnameComponentError, + ExistingChildError, + FileTooLargeError, + MustBeDeepImmutableError, + MustBeReadonlyError, + MustNotBeUnknownRWError, + NoSharesError, + NoSuchChildError, + NotEnoughSharesError, MDMF_VERSION, SDMF_VERSION, ) +from allmydata.mutable.common import UnrecoverableFileError from allmydata.util.hashutil import timing_safe_compare from allmydata.util.time_format import ( format_delta, format_time, ) from allmydata.util.encodingutil import ( + quote_output, to_bytes, ) # Originally part of this module, so still part of its API: from .common_py3 import ( # noqa: F401 - get_arg, abbreviate_time, MultiFormatResource, WebError, humanize_exception, - SlotsSequenceElement, render_exception, get_root, exception_to_child + get_arg, abbreviate_time, MultiFormatResource, WebError, ) @@ -104,6 +128,16 @@ def parse_offset_arg(offset): return offset +def get_root(ctx_or_req): + if PY2: + req = INevowRequest(ctx_or_req) + else: + req = ITwistedRequest(ctx_or_req) + depth = len(req.prepath) + len(req.postpath) + link = "/".join([".."] * depth) + return link + + def convert_children_json(nodemaker, children_json): """I convert the JSON output of GET?t=json into the dict-of-nodes input to both dirnode.create_subdirectory() and @@ -197,6 +231,94 @@ def should_create_intermediate_directories(req): return bool(req.method in ("PUT", "POST") and t not in ("delete", "rename", "rename-form", "check")) +def humanize_exception(exc): + """ + Like ``humanize_failure`` but for an exception. + + :param Exception exc: The exception to describe. + + :return: See ``humanize_failure``. + """ + if isinstance(exc, EmptyPathnameComponentError): + return ("The webapi does not allow empty pathname components, " + "i.e. a double slash", http.BAD_REQUEST) + if isinstance(exc, ExistingChildError): + return ("There was already a child by that name, and you asked me " + "to not replace it.", http.CONFLICT) + if isinstance(exc, NoSuchChildError): + quoted_name = quote_output(exc.args[0], encoding="utf-8", quotemarks=False) + return ("No such child: %s" % quoted_name, http.NOT_FOUND) + if isinstance(exc, NotEnoughSharesError): + t = ("NotEnoughSharesError: This indicates that some " + "servers were unavailable, or that shares have been " + "lost to server departure, hard drive failure, or disk " + "corruption. You should perform a filecheck on " + "this object to learn more.\n\nThe full error message is:\n" + "%s") % str(exc) + return (t, http.GONE) + if isinstance(exc, NoSharesError): + t = ("NoSharesError: no shares could be found. " + "Zero shares usually indicates a corrupt URI, or that " + "no servers were connected, but it might also indicate " + "severe corruption. You should perform a filecheck on " + "this object to learn more.\n\nThe full error message is:\n" + "%s") % str(exc) + return (t, http.GONE) + if isinstance(exc, UnrecoverableFileError): + t = ("UnrecoverableFileError: the directory (or mutable file) could " + "not be retrieved, because there were insufficient good shares. " + "This might indicate that no servers were connected, " + "insufficient servers were connected, the URI was corrupt, or " + "that shares have been lost due to server departure, hard drive " + "failure, or disk corruption. You should perform a filecheck on " + "this object to learn more.") + return (t, http.GONE) + if isinstance(exc, MustNotBeUnknownRWError): + quoted_name = quote_output(exc.args[1], encoding="utf-8") + immutable = exc.args[2] + if immutable: + t = ("MustNotBeUnknownRWError: an operation to add a child named " + "%s to a directory was given an unknown cap in a write slot.\n" + "If the cap is actually an immutable readcap, then using a " + "webapi server that supports a later version of Tahoe may help.\n\n" + "If you are using the webapi directly, then specifying an immutable " + "readcap in the read slot (ro_uri) of the JSON PROPDICT, and " + "omitting the write slot (rw_uri), would also work in this " + "case.") % quoted_name + else: + t = ("MustNotBeUnknownRWError: an operation to add a child named " + "%s to a directory was given an unknown cap in a write slot.\n" + "Using a webapi server that supports a later version of Tahoe " + "may help.\n\n" + "If you are using the webapi directly, specifying a readcap in " + "the read slot (ro_uri) of the JSON PROPDICT, as well as a " + "writecap in the write slot if desired, would also work in this " + "case.") % quoted_name + return (t, http.BAD_REQUEST) + if isinstance(exc, MustBeDeepImmutableError): + quoted_name = quote_output(exc.args[1], encoding="utf-8") + t = ("MustBeDeepImmutableError: a cap passed to this operation for " + "the child named %s, needed to be immutable but was not. Either " + "the cap is being added to an immutable directory, or it was " + "originally retrieved from an immutable directory as an unknown " + "cap.") % quoted_name + return (t, http.BAD_REQUEST) + if isinstance(exc, MustBeReadonlyError): + quoted_name = quote_output(exc.args[1], encoding="utf-8") + t = ("MustBeReadonlyError: a cap passed to this operation for " + "the child named '%s', needed to be read-only but was not. " + "The cap is being passed in a read slot (ro_uri), or was retrieved " + "from a read slot as an unknown cap.") % quoted_name + return (t, http.BAD_REQUEST) + if isinstance(exc, blacklist.FileProhibited): + t = "Access Prohibited: %s" % quote_output(exc.reason, encoding="utf-8", quotemarks=False) + return (t, http.FORBIDDEN) + if isinstance(exc, WebError): + return (exc.text, exc.code) + if isinstance(exc, FileTooLargeError): + return ("FileTooLargeError: %s" % (exc,), http.REQUEST_ENTITY_TOO_LARGE) + return (str(exc), None) + def humanize_failure(f): """ @@ -211,9 +333,9 @@ def humanize_failure(f): return humanize_exception(f.value) -class MyExceptionHandler(appserver.DefaultExceptionHandler, object): +class MyExceptionHandler(DefaultExceptionHandler, object): def simple(self, ctx, text, code=http.BAD_REQUEST): - req = IRequest(ctx) + req = INevowRequest(ctx) req.setResponseCode(code) #req.responseHeaders.setRawHeaders("content-encoding", []) #req.responseHeaders.setRawHeaders("content-disposition", []) @@ -239,17 +361,17 @@ class MyExceptionHandler(appserver.DefaultExceptionHandler, object): # twisted.web.server.Request.render() has support for transforming # this into an appropriate 501 NOT_IMPLEMENTED or 405 NOT_ALLOWED # return code, but nevow does not. - req = IRequest(ctx) + req = INevowRequest(ctx) method = req.method return self.simple(ctx, "I don't know how to treat a %s request." % method, http.NOT_IMPLEMENTED) - req = IRequest(ctx) + req = INevowRequest(ctx) accept = req.getHeader("accept") if not accept: accept = "*/*" if "*/*" in accept or "text/*" in accept or "text/html" in accept: - super = appserver.DefaultExceptionHandler + super = DefaultExceptionHandler return super.renderHTTP_exception(self, ctx, f) # use plain text traceback = f.getTraceback() @@ -260,6 +382,49 @@ class NeedOperationHandleError(WebError): pass +class SlotsSequenceElement(template.Element): + """ + ``SlotsSequenceElement` is a minimal port of nevow's sequence renderer for + twisted.web.template. + + Tags passed in to be templated will have two renderers available: ``item`` + and ``tag``. + """ + + def __init__(self, tag, seq): + self.loader = template.TagLoader(tag) + self.seq = seq + + @template.renderer + def header(self, request, tag): + return tag + + @template.renderer + def item(self, request, tag): + """ + A template renderer for each sequence item. + + ``tag`` will be cloned for each item in the sequence provided, and its + slots filled from the sequence item. Each item must be dict-like enough + for ``tag.fillSlots(**item)``. Each cloned tag will be siblings with no + separator beween them. + """ + for item in self.seq: + yield tag.clone(deep=False).fillSlots(**item) + + @template.renderer + def empty(self, request, tag): + """ + A template renderer for empty sequences. + + This renderer will either return ``tag`` unmodified if the provided + sequence has no items, or return the empty string if there are any + items. + """ + if len(self.seq) > 0: + return u'' + else: + return tag class TokenOnlyWebApi(resource.Resource, object): @@ -314,3 +479,65 @@ class TokenOnlyWebApi(resource.Resource, object): else: raise WebError("'%s' invalid type for 't' arg" % (t,), http.BAD_REQUEST) + +def exception_to_child(f): + """ + Decorate ``getChild`` method with exception handling behavior to render an + error page reflecting the exception. + """ + @wraps(f) + def g(self, name, req): + try: + return f(self, name, req) + except Exception as e: + description, status = humanize_exception(e) + return resource.ErrorPage(status, "Error", description) + return g + + +def render_exception(f): + """ + Decorate a ``render_*`` method with exception handling behavior to render + an error page reflecting the exception. + """ + @wraps(f) + def g(self, request): + try: + return f(self, request) + except Exception as e: + description, status = humanize_exception(e) + return resource.ErrorPage(status, "Error", description).render(request) + return g + + +class ReloadMixin(object): + REFRESH_TIME = 1*60 + + @template.renderer + def refresh(self, req, tag): + if self.monitor.is_finished(): + return "" + # dreid suggests ctx.tag(**dict([("http-equiv", "refresh")])) + # but I can't tell if he's joking or not + tag.attributes["http-equiv"] = "refresh" + tag.attributes["content"] = str(self.REFRESH_TIME) + return tag + + @template.renderer + def reload(self, req, tag): + if self.monitor.is_finished(): + return b"" + # url.gethere would break a proxy, so the correct thing to do is + # req.path[-1] + queryargs + ophandle = req.prepath[-1] + reload_target = ophandle + b"?output=html" + cancel_target = ophandle + b"?t=cancel" + cancel_button = T.form(T.input(type="submit", value="Cancel"), + action=cancel_target, + method="POST", + enctype="multipart/form-data",) + + return (T.h2("Operation still running: ", + T.a("Reload", href=reload_target), + ), + cancel_button,) diff --git a/src/allmydata/web/common_py3.py b/src/allmydata/web/common_py3.py index a94401b45..73130cbab 100644 --- a/src/allmydata/web/common_py3.py +++ b/src/allmydata/web/common_py3.py @@ -6,32 +6,15 @@ Can eventually be merged back into allmydata.web.common. from future.utils import PY2 -from functools import wraps - if PY2: from nevow.inevow import IRequest as INevowRequest else: INevowRequest = None -from twisted.web import resource, http, template -from twisted.web.template import tags as T +from twisted.web import resource, http from twisted.web.iweb import IRequest -from allmydata import blacklist -from allmydata.interfaces import ( - EmptyPathnameComponentError, - ExistingChildError, - FileTooLargeError, - MustBeDeepImmutableError, - MustBeReadonlyError, - MustNotBeUnknownRWError, - NoSharesError, - NoSuchChildError, - NotEnoughSharesError, -) -from allmydata.mutable.common import UnrecoverableFileError from allmydata.util import abbreviate -from allmydata.util.encodingutil import quote_output class WebError(Exception): @@ -137,210 +120,3 @@ def abbreviate_time(data): if s >= 0.001: return "%.1fms" % (1000*s) return "%.0fus" % (1000000*s) - - -def render_exception(f): - """ - Decorate a ``render_*`` method with exception handling behavior to render - an error page reflecting the exception. - """ - @wraps(f) - def g(self, request): - try: - return f(self, request) - except Exception as e: - description, status = humanize_exception(e) - return resource.ErrorPage(status, "Error", description).render(request) - return g - - -class SlotsSequenceElement(template.Element): - """ - ``SlotsSequenceElement` is a minimal port of nevow's sequence renderer for - twisted.web.template. - - Tags passed in to be templated will have two renderers available: ``item`` - and ``tag``. - """ - - def __init__(self, tag, seq): - self.loader = template.TagLoader(tag) - self.seq = seq - - @template.renderer - def header(self, request, tag): - return tag - - @template.renderer - def item(self, request, tag): - """ - A template renderer for each sequence item. - - ``tag`` will be cloned for each item in the sequence provided, and its - slots filled from the sequence item. Each item must be dict-like enough - for ``tag.fillSlots(**item)``. Each cloned tag will be siblings with no - separator beween them. - """ - for item in self.seq: - yield tag.clone(deep=False).fillSlots(**item) - - @template.renderer - def empty(self, request, tag): - """ - A template renderer for empty sequences. - - This renderer will either return ``tag`` unmodified if the provided - sequence has no items, or return the empty string if there are any - items. - """ - if len(self.seq) > 0: - return u'' - else: - return tag - - -def humanize_exception(exc): - """ - Like ``humanize_failure`` but for an exception. - - :param Exception exc: The exception to describe. - - :return: See ``humanize_failure``. - """ - if isinstance(exc, EmptyPathnameComponentError): - return ("The webapi does not allow empty pathname components, " - "i.e. a double slash", http.BAD_REQUEST) - if isinstance(exc, ExistingChildError): - return ("There was already a child by that name, and you asked me " - "to not replace it.", http.CONFLICT) - if isinstance(exc, NoSuchChildError): - quoted_name = quote_output(exc.args[0], encoding="utf-8", quotemarks=False) - return ("No such child: %s" % quoted_name, http.NOT_FOUND) - if isinstance(exc, NotEnoughSharesError): - t = ("NotEnoughSharesError: This indicates that some " - "servers were unavailable, or that shares have been " - "lost to server departure, hard drive failure, or disk " - "corruption. You should perform a filecheck on " - "this object to learn more.\n\nThe full error message is:\n" - "%s") % str(exc) - return (t, http.GONE) - if isinstance(exc, NoSharesError): - t = ("NoSharesError: no shares could be found. " - "Zero shares usually indicates a corrupt URI, or that " - "no servers were connected, but it might also indicate " - "severe corruption. You should perform a filecheck on " - "this object to learn more.\n\nThe full error message is:\n" - "%s") % str(exc) - return (t, http.GONE) - if isinstance(exc, UnrecoverableFileError): - t = ("UnrecoverableFileError: the directory (or mutable file) could " - "not be retrieved, because there were insufficient good shares. " - "This might indicate that no servers were connected, " - "insufficient servers were connected, the URI was corrupt, or " - "that shares have been lost due to server departure, hard drive " - "failure, or disk corruption. You should perform a filecheck on " - "this object to learn more.") - return (t, http.GONE) - if isinstance(exc, MustNotBeUnknownRWError): - quoted_name = quote_output(exc.args[1], encoding="utf-8") - immutable = exc.args[2] - if immutable: - t = ("MustNotBeUnknownRWError: an operation to add a child named " - "%s to a directory was given an unknown cap in a write slot.\n" - "If the cap is actually an immutable readcap, then using a " - "webapi server that supports a later version of Tahoe may help.\n\n" - "If you are using the webapi directly, then specifying an immutable " - "readcap in the read slot (ro_uri) of the JSON PROPDICT, and " - "omitting the write slot (rw_uri), would also work in this " - "case.") % quoted_name - else: - t = ("MustNotBeUnknownRWError: an operation to add a child named " - "%s to a directory was given an unknown cap in a write slot.\n" - "Using a webapi server that supports a later version of Tahoe " - "may help.\n\n" - "If you are using the webapi directly, specifying a readcap in " - "the read slot (ro_uri) of the JSON PROPDICT, as well as a " - "writecap in the write slot if desired, would also work in this " - "case.") % quoted_name - return (t, http.BAD_REQUEST) - if isinstance(exc, MustBeDeepImmutableError): - quoted_name = quote_output(exc.args[1], encoding="utf-8") - t = ("MustBeDeepImmutableError: a cap passed to this operation for " - "the child named %s, needed to be immutable but was not. Either " - "the cap is being added to an immutable directory, or it was " - "originally retrieved from an immutable directory as an unknown " - "cap.") % quoted_name - return (t, http.BAD_REQUEST) - if isinstance(exc, MustBeReadonlyError): - quoted_name = quote_output(exc.args[1], encoding="utf-8") - t = ("MustBeReadonlyError: a cap passed to this operation for " - "the child named '%s', needed to be read-only but was not. " - "The cap is being passed in a read slot (ro_uri), or was retrieved " - "from a read slot as an unknown cap.") % quoted_name - return (t, http.BAD_REQUEST) - if isinstance(exc, blacklist.FileProhibited): - t = "Access Prohibited: %s" % quote_output(exc.reason, encoding="utf-8", quotemarks=False) - return (t, http.FORBIDDEN) - if isinstance(exc, WebError): - return (exc.text, exc.code) - if isinstance(exc, FileTooLargeError): - return ("FileTooLargeError: %s" % (exc,), http.REQUEST_ENTITY_TOO_LARGE) - return (str(exc), None) - - -def get_root(ctx_or_req): - if PY2: - req = INevowRequest(ctx_or_req) - else: - req = IRequest(ctx_or_req) - depth = len(req.prepath) + len(req.postpath) - link = "/".join([".."] * depth) - return link - - -class ReloadMixin(object): - REFRESH_TIME = 1*60 - - @template.renderer - def refresh(self, req, tag): - if self.monitor.is_finished(): - return "" - # dreid suggests ctx.tag(**dict([("http-equiv", "refresh")])) - # but I can't tell if he's joking or not - tag.attributes["http-equiv"] = "refresh" - tag.attributes["content"] = str(self.REFRESH_TIME) - return tag - - @template.renderer - def reload(self, req, tag): - if self.monitor.is_finished(): - return b"" - # url.gethere would break a proxy, so the correct thing to do is - # req.path[-1] + queryargs - ophandle = req.prepath[-1] - reload_target = ophandle + b"?output=html" - cancel_target = ophandle + b"?t=cancel" - cancel_button = T.form(T.input(type="submit", value="Cancel"), - action=cancel_target, - method="POST", - enctype="multipart/form-data",) - - return (T.h2("Operation still running: ", - T.a("Reload", href=reload_target), - ), - cancel_button,) - - -def exception_to_child(f): - """ - Decorate ``getChild`` method with exception handling behavior to render an - error page reflecting the exception. - """ - @wraps(f) - def g(self, name, req): - try: - return f(self, name, req) - except Exception as e: - description, status = humanize_exception(e) - return resource.ErrorPage(status, "Error", description) - return g diff --git a/src/allmydata/web/operations.py b/src/allmydata/web/operations.py index f96feeda4..982f95b47 100644 --- a/src/allmydata/web/operations.py +++ b/src/allmydata/web/operations.py @@ -17,7 +17,7 @@ from allmydata.web.common import ( ) # Originally part of this module, so still part of its API: -from .common_py3 import ( # noqa: F401 +from .common import ( # noqa: F401 ReloadMixin, )