source: trunk/src/allmydata/web/common.py

Last change on this file was 774b4f2, checked in by Itamar Turner-Trauring <itamar@…>, at 2023-11-21T13:31:34Z

Fix mypy errors

  • Property mode set to 100644
File size: 29.0 KB
Line 
1"""
2Ported to Python 3.
3"""
4from __future__ import annotations
5
6from six import ensure_str
7import sys
8if sys.version_info[:2] >= (3, 9):
9    from importlib.resources import files as resource_files, as_file
10else:
11    from importlib_resources import files as resource_files, as_file
12from contextlib import ExitStack
13import weakref
14from typing import Optional, Union, TypeVar, overload
15from typing_extensions import Literal
16
17import time
18import json
19from functools import wraps
20from base64 import urlsafe_b64decode
21
22from hyperlink import (
23    DecodedURL,
24)
25
26from eliot import (
27    Message,
28    start_action,
29)
30from eliot.twisted import (
31    DeferredContext,
32)
33
34from twisted.web import (
35    http,
36    resource,
37    template,
38    static,
39)
40from twisted.web.iweb import (
41    IRequest,
42)
43from twisted.web.template import (
44    tags,
45)
46from twisted.web.server import (
47    NOT_DONE_YET,
48)
49from twisted.web.util import (
50    DeferredResource,
51    FailureElement,
52    redirectTo,
53)
54from twisted.python.reflect import (
55    fullyQualifiedName,
56)
57from twisted.python import log
58from twisted.python.failure import (
59    Failure,
60)
61from twisted.internet.defer import (
62    CancelledError,
63    maybeDeferred,
64)
65from twisted.web.resource import (
66    IResource,
67)
68
69from allmydata.dirnode import ONLY_FILES, _OnlyFiles
70from allmydata import blacklist
71from allmydata.interfaces import (
72    EmptyPathnameComponentError,
73    ExistingChildError,
74    FileTooLargeError,
75    MustBeDeepImmutableError,
76    MustBeReadonlyError,
77    MustNotBeUnknownRWError,
78    NoSharesError,
79    NoSuchChildError,
80    NotEnoughSharesError,
81    MDMF_VERSION,
82    SDMF_VERSION,
83)
84from allmydata.mutable.common import UnrecoverableFileError
85from allmydata.util.time_format import (
86    format_delta,
87    format_time,
88)
89from allmydata.util.encodingutil import (
90    quote_output,
91    quote_output_u,
92    to_bytes,
93)
94from allmydata.util import abbreviate
95from allmydata.crypto.rsa import PrivateKey, PublicKey, create_signing_keypair_from_string
96
97
98class WebError(Exception):
99    def __init__(self, text, code=http.BAD_REQUEST):
100        self.text = text
101        self.code = code
102
103
104def get_filenode_metadata(filenode):
105    metadata = {'mutable': filenode.is_mutable()}
106    if metadata['mutable']:
107        mutable_type = filenode.get_version()
108        assert mutable_type in (SDMF_VERSION, MDMF_VERSION)
109        if mutable_type == MDMF_VERSION:
110            file_format = "MDMF"
111        else:
112            file_format = "SDMF"
113    else:
114        file_format = "CHK"
115    metadata['format'] = file_format
116    size = filenode.get_size()
117    if size is not None:
118        metadata['size'] = size
119    return metadata
120
121def boolean_of_arg(arg):  # type: (bytes) -> bool
122    assert isinstance(arg, bytes)
123    if arg.lower() not in (b"true", b"t", b"1", b"false", b"f", b"0", b"on", b"off"):
124        raise WebError("invalid boolean argument: %r" % (arg,), http.BAD_REQUEST)
125    return arg.lower() in (b"true", b"t", b"1", b"on")
126
127
128def parse_replace_arg(replace: bytes) -> Union[bool,_OnlyFiles]:
129    assert isinstance(replace, bytes)
130    if replace.lower() == b"only-files":
131        return ONLY_FILES
132    try:
133        return boolean_of_arg(replace)
134    except WebError:
135        raise WebError("invalid replace= argument: %r" % (ensure_str(replace),), http.BAD_REQUEST)
136
137
138def get_format(req, default="CHK"):
139    arg = get_arg(req, "format", None)
140    if not arg:
141        if boolean_of_arg(get_arg(req, "mutable", "false")):
142            return "SDMF"
143        return default
144    if arg.upper() == b"CHK":
145        return "CHK"
146    elif arg.upper() == b"SDMF":
147        return "SDMF"
148    elif arg.upper() == b"MDMF":
149        return "MDMF"
150    else:
151        raise WebError("Unknown format: %s, I know CHK, SDMF, MDMF" % str(arg, "ascii"),
152                       http.BAD_REQUEST)
153
154def get_mutable_type(file_format): # accepts result of get_format()
155    if file_format == "SDMF":
156        return SDMF_VERSION
157    elif file_format == "MDMF":
158        return MDMF_VERSION
159    else:
160        # this is also used to identify which formats are mutable. Use
161        #  if get_mutable_type(file_format) is not None:
162        #      do_mutable()
163        #  else:
164        #      do_immutable()
165        return None
166
167
168def parse_offset_arg(offset):  # type: (bytes) -> Union[int,None]
169    # XXX: This will raise a ValueError when invoked on something that
170    # is not an integer. Is that okay? Or do we want a better error
171    # message? Since this call is going to be used by programmers and
172    # their tools rather than users (through the wui), it is not
173    # inconsistent to return that, I guess.
174    if offset is not None:
175        return int(offset)
176
177    return offset
178
179
180def get_root(req):  # type: (IRequest) -> str
181    """
182    Get a relative path with parent directory segments that refers to the root
183    location known to the given request.  This seems a lot like the constant
184    absolute path **/** but it will behave differently if the Tahoe-LAFS HTTP
185    server is reverse-proxied and mounted somewhere other than at the root.
186
187    :param twisted.web.iweb.IRequest req: The request to consider.
188
189    :return: A string like ``../../..`` with the correct number of segments to
190        reach the root.
191    """
192    if not IRequest.providedBy(req):
193        raise TypeError(
194            "get_root requires IRequest provider, got {!r}".format(req),
195        )
196    depth = len(req.prepath) + len(req.postpath)
197    link = "/".join([".."] * depth)
198    return link
199
200
201def convert_children_json(nodemaker, children_json):
202    """I convert the JSON output of GET?t=json into the dict-of-nodes input
203    to both dirnode.create_subdirectory() and
204    client.create_directory(initial_children=). This is used by
205    t=mkdir-with-children and t=mkdir-immutable"""
206    children = {}
207    if children_json:
208        data = json.loads(children_json)
209        for (namex, (ctype, propdict)) in list(data.items()):
210            namex = str(namex)
211            writecap = to_bytes(propdict.get("rw_uri"))
212            readcap = to_bytes(propdict.get("ro_uri"))
213            metadata = propdict.get("metadata", {})
214            # name= argument is just for error reporting
215            childnode = nodemaker.create_from_cap(writecap, readcap, name=namex)
216            children[namex] = (childnode, metadata)
217    return children
218
219
220def compute_rate(bytes, seconds):
221    if bytes is None:
222      return None
223
224    if seconds is None or seconds == 0:
225      return None
226
227    # negative values don't make sense here
228    assert bytes > -1
229    assert seconds > 0
230
231    return bytes / seconds
232
233
234def abbreviate_rate(data):
235    """
236    Convert number of bytes/second into human readable strings (unicode).
237
238    Uses metric measures, so 1000 not 1024, e.g. 21.8kBps, 554.4kBps, 4.37MBps.
239
240    :param data: Either ``None`` or integer.
241
242    :return: Unicode string.
243    """
244    if data is None:
245        return u""
246    r = float(data)
247    if r > 1000000:
248        return u"%1.2fMBps" % (r/1000000)
249    if r > 1000:
250        return u"%.1fkBps" % (r/1000)
251    return u"%.0fBps" % r
252
253
254def abbreviate_size(data):
255    """
256    Convert number of bytes into human readable strings (unicode).
257
258    Uses metric measures, so 1000 not 1024, e.g. 21.8kB, 554.4kB, 4.37MB.
259
260    :param data: Either ``None`` or integer.
261
262    :return: Unicode string.
263    """
264    if data is None:
265        return u""
266    r = float(data)
267    if r > 1000000000:
268        return u"%1.2fGB" % (r/1000000000)
269    if r > 1000000:
270        return u"%1.2fMB" % (r/1000000)
271    if r > 1000:
272        return u"%.1fkB" % (r/1000)
273    return u"%.0fB" % r
274
275def plural(sequence_or_length):
276    if isinstance(sequence_or_length, int):
277        length = sequence_or_length
278    else:
279        length = len(sequence_or_length)
280    if length == 1:
281        return ""
282    return "s"
283
284def text_plain(text, req):
285    req.setHeader("content-type", "text/plain")
286    req.setHeader("content-length", b"%d" % len(text))
287    return text
288
289def spaces_to_nbsp(text):
290    return str(text).replace(u' ', u'\u00A0')
291
292def render_time_delta(time_1, time_2):
293    return spaces_to_nbsp(format_delta(time_1, time_2))
294
295def render_time(t):
296    return spaces_to_nbsp(format_time(time.localtime(t)))
297
298def render_time_attr(t):
299    return format_time(time.localtime(t))
300
301
302# XXX: to make UnsupportedMethod return 501 NOT_IMPLEMENTED instead of 500
303# Internal Server Error, we either need to do that ICanHandleException trick,
304# or make sure that childFactory returns a WebErrorResource (and never an
305# actual exception). The latter is growing increasingly annoying.
306
307def should_create_intermediate_directories(req):
308    t = str(get_arg(req, "t", "").strip(), "ascii")
309    return bool(req.method in (b"PUT", b"POST") and
310                t not in ("delete", "rename", "rename-form", "check"))
311
312def humanize_exception(exc):
313    """
314    Like ``humanize_failure`` but for an exception.
315
316    :param Exception exc: The exception to describe.
317
318    :return: See ``humanize_failure``.
319    """
320    if isinstance(exc, EmptyPathnameComponentError):
321        return ("The webapi does not allow empty pathname components, "
322                "i.e. a double slash", http.BAD_REQUEST)
323    if isinstance(exc, ExistingChildError):
324        return ("There was already a child by that name, and you asked me "
325                "to not replace it.", http.CONFLICT)
326    if isinstance(exc, NoSuchChildError):
327        quoted_name = quote_output_u(exc.args[0], quotemarks=False)
328        return ("No such child: %s" % quoted_name, http.NOT_FOUND)
329    if isinstance(exc, NotEnoughSharesError):
330        t = ("NotEnoughSharesError: This indicates that some "
331             "servers were unavailable, or that shares have been "
332             "lost to server departure, hard drive failure, or disk "
333             "corruption. You should perform a filecheck on "
334             "this object to learn more.\n\nThe full error message is:\n"
335             "%s") % str(exc)
336        return (t, http.GONE)
337    if isinstance(exc, NoSharesError):
338        t = ("NoSharesError: no shares could be found. "
339             "Zero shares usually indicates a corrupt URI, or that "
340             "no servers were connected, but it might also indicate "
341             "severe corruption. You should perform a filecheck on "
342             "this object to learn more.\n\nThe full error message is:\n"
343             "%s") % str(exc)
344        return (t, http.GONE)
345    if isinstance(exc, UnrecoverableFileError):
346        t = ("UnrecoverableFileError: the directory (or mutable file) could "
347             "not be retrieved, because there were insufficient good shares. "
348             "This might indicate that no servers were connected, "
349             "insufficient servers were connected, the URI was corrupt, or "
350             "that shares have been lost due to server departure, hard drive "
351             "failure, or disk corruption. You should perform a filecheck on "
352             "this object to learn more.")
353        return (t, http.GONE)
354    if isinstance(exc, MustNotBeUnknownRWError):
355        quoted_name = quote_output(exc.args[1], encoding="utf-8")
356        immutable = exc.args[2]
357        if immutable:
358            t = ("MustNotBeUnknownRWError: an operation to add a child named "
359                 "%s to a directory was given an unknown cap in a write slot.\n"
360                 "If the cap is actually an immutable readcap, then using a "
361                 "webapi server that supports a later version of Tahoe may help.\n\n"
362                 "If you are using the webapi directly, then specifying an immutable "
363                 "readcap in the read slot (ro_uri) of the JSON PROPDICT, and "
364                 "omitting the write slot (rw_uri), would also work in this "
365                 "case.") % quoted_name
366        else:
367            t = ("MustNotBeUnknownRWError: an operation to add a child named "
368                 "%s to a directory was given an unknown cap in a write slot.\n"
369                 "Using a webapi server that supports a later version of Tahoe "
370                 "may help.\n\n"
371                 "If you are using the webapi directly, specifying a readcap in "
372                 "the read slot (ro_uri) of the JSON PROPDICT, as well as a "
373                 "writecap in the write slot if desired, would also work in this "
374                 "case.") % quoted_name
375        return (t, http.BAD_REQUEST)
376    if isinstance(exc, MustBeDeepImmutableError):
377        quoted_name = quote_output(exc.args[1], encoding="utf-8")
378        t = ("MustBeDeepImmutableError: a cap passed to this operation for "
379             "the child named %s, needed to be immutable but was not. Either "
380             "the cap is being added to an immutable directory, or it was "
381             "originally retrieved from an immutable directory as an unknown "
382             "cap.") % quoted_name
383        return (t, http.BAD_REQUEST)
384    if isinstance(exc, MustBeReadonlyError):
385        quoted_name = quote_output(exc.args[1], encoding="utf-8")
386        t = ("MustBeReadonlyError: a cap passed to this operation for "
387             "the child named '%s', needed to be read-only but was not. "
388             "The cap is being passed in a read slot (ro_uri), or was retrieved "
389             "from a read slot as an unknown cap.") % quoted_name
390        return (t, http.BAD_REQUEST)
391    if isinstance(exc, blacklist.FileProhibited):
392        t = "Access Prohibited: %s" % quote_output(exc.reason, encoding="utf-8", quotemarks=False)
393        return (t, http.FORBIDDEN)
394    if isinstance(exc, WebError):
395        return (exc.text, exc.code)
396    if isinstance(exc, FileTooLargeError):
397        return ("FileTooLargeError: %s" % (exc,), http.REQUEST_ENTITY_TOO_LARGE)
398    return (str(exc), None)
399
400
401def humanize_failure(f):
402    """
403    Create an human-oriented description of a failure along with some HTTP
404    metadata.
405
406    :param Failure f: The failure to describe.
407
408    :return (bytes, int): A tuple of some prose and an HTTP code describing
409        the failure.
410    """
411    return humanize_exception(f.value)
412
413
414class NeedOperationHandleError(WebError):
415    pass
416
417
418class SlotsSequenceElement(template.Element):
419    """
420    ``SlotsSequenceElement` is a minimal port of Nevow's sequence renderer for
421    twisted.web.template.
422
423    Tags passed in to be templated will have two renderers available: ``item``
424    and ``tag``.
425    """
426
427    def __init__(self, tag, seq):
428        self.loader = template.TagLoader(tag)
429        self.seq = seq
430
431    @template.renderer
432    def header(self, request, tag):
433        return tag
434
435    @template.renderer
436    def item(self, request, tag):
437        """
438        A template renderer for each sequence item.
439
440        ``tag`` will be cloned for each item in the sequence provided, and its
441        slots filled from the sequence item. Each item must be dict-like enough
442        for ``tag.fillSlots(**item)``. Each cloned tag will be siblings with no
443        separator beween them.
444        """
445        for item in self.seq:
446            yield tag.clone(deep=False).fillSlots(**item)
447
448    @template.renderer
449    def empty(self, request, tag):
450        """
451        A template renderer for empty sequences.
452
453        This renderer will either return ``tag`` unmodified if the provided
454        sequence has no items, or return the empty string if there are any
455        items.
456        """
457        if len(self.seq) > 0:
458            return u''
459        else:
460            return tag
461
462
463def exception_to_child(getChild):
464    """
465    Decorate ``getChild`` method with exception handling behavior to render an
466    error page reflecting the exception.
467    """
468    @wraps(getChild)
469    def g(self, name, req):
470        # Bind the method to the instance so it has a better
471        # fullyQualifiedName later on.  This is not necessary on Python 3.
472        bound_getChild = getChild.__get__(self, type(self))
473
474        action = start_action(
475            action_type=u"allmydata:web:common-getChild",
476            uri=req.uri,
477            method=req.method,
478            name=name,
479            handler=fullyQualifiedName(bound_getChild),
480        )
481        with action.context():
482            result = DeferredContext(maybeDeferred(bound_getChild, name, req))
483            result.addCallbacks(
484                _getChild_done,
485                _getChild_failed,
486                callbackArgs=(self,),
487            )
488            result = result.addActionFinish()
489        return DeferredResource(result)
490    return g
491
492
493def _getChild_done(child, parent):
494    Message.log(
495        message_type=u"allmydata:web:common-getChild:result",
496        result=fullyQualifiedName(type(child)),
497    )
498    if child is None:
499        return resource.NoResource()
500    return child
501
502
503def _getChild_failed(reason):
504    text, code = humanize_failure(reason)
505    return resource.ErrorPage(code, "Error", text)
506
507
508def render_exception(render):
509    """
510    Decorate a ``render_*`` method with exception handling behavior to render
511    an error page reflecting the exception.
512    """
513    @wraps(render)
514    def g(self, request):
515        # Bind the method to the instance so it has a better
516        # fullyQualifiedName later on.  This is not necessary on Python 3.
517        bound_render = render.__get__(self, type(self))
518
519        action = start_action(
520            action_type=u"allmydata:web:common-render",
521            uri=request.uri,
522            method=request.method,
523            handler=fullyQualifiedName(bound_render),
524        )
525        if getattr(request, "dont_apply_extra_processing", False):
526            with action:
527                return bound_render(request)
528
529        with action.context():
530            result = DeferredContext(maybeDeferred(bound_render, request))
531            # Apply `_finish` all of our result handling logic to whatever it
532            # returned.
533            result.addBoth(_finish, bound_render, request)
534            d = result.addActionFinish()
535
536        # If the connection is lost then there's no point running our _finish
537        # logic because it has nowhere to send anything.  There may also be no
538        # point in finishing whatever operation was being performed because
539        # the client cannot be informed of its result.  Also, Twisted Web
540        # raises exceptions from some Request methods if they're used after
541        # the connection is lost.
542        request.notifyFinish().addErrback(
543            lambda ignored: d.cancel(),
544        )
545        return NOT_DONE_YET
546
547    return g
548
549
550def _finish(result, render, request):
551    """
552    Try to finish rendering the response to a request.
553
554    This implements extra convenience functionality not provided by Twisted
555    Web.  Various resources in Tahoe-LAFS made use of this functionality when
556    it was provided by Nevow.  Rather than making that application code do the
557    more tedious thing itself, we duplicate the functionality here.
558
559    :param result: Something returned by a render method which we can turn
560        into a response.
561
562    :param render: The original render method which produced the result.
563
564    :param request: The request being responded to.
565
566    :return: ``None``
567    """
568    if isinstance(result, Failure):
569        if result.check(CancelledError):
570            return
571        Message.log(
572            message_type=u"allmydata:web:common-render:failure",
573            message=result.getErrorMessage(),
574        )
575        _finish(
576            _renderHTTP_exception(request, result),
577            render,
578            request,
579        )
580    elif IResource.providedBy(result):
581        # If result is also using @render_exception then we don't want to
582        # double-apply the logic.  This leads to an attempt to double-finish
583        # the request.  If it isn't using @render_exception then you should
584        # fix it so it is.
585        Message.log(
586            message_type=u"allmydata:web:common-render:resource",
587            resource=fullyQualifiedName(type(result)),
588        )
589        result.render(request)
590    elif isinstance(result, str):
591        Message.log(
592            message_type=u"allmydata:web:common-render:unicode",
593        )
594        request.write(result.encode("utf-8"))
595        request.finish()
596    elif isinstance(result, bytes):
597        Message.log(
598            message_type=u"allmydata:web:common-render:bytes",
599        )
600        request.write(result)
601        request.finish()
602    elif isinstance(result, DecodedURL):
603        Message.log(
604            message_type=u"allmydata:web:common-render:DecodedURL",
605        )
606        _finish(redirectTo(result.to_text().encode("utf-8"), request), render, request)
607    elif result is None:
608        Message.log(
609            message_type=u"allmydata:web:common-render:None",
610        )
611        request.finish()
612    elif result == NOT_DONE_YET:
613        Message.log(
614            message_type=u"allmydata:web:common-render:NOT_DONE_YET",
615        )
616        pass
617    else:
618        Message.log(
619            message_type=u"allmydata:web:common-render:unknown",
620        )
621        log.err("Request for {!r} handled by {!r} returned unusable {!r}".format(
622            request.uri,
623            fullyQualifiedName(render),
624            result,
625        ))
626        request.setResponseCode(http.INTERNAL_SERVER_ERROR)
627        _finish(b"Internal Server Error", render, request)
628
629
630def _renderHTTP_exception(request, failure):
631    try:
632        text, code = humanize_failure(failure)
633    except:
634        log.msg("exception in humanize_failure")
635        log.msg("argument was %s" % (failure,))
636        log.err()
637        text = str(failure)
638        code = None
639
640    if code is not None:
641        return _renderHTTP_exception_simple(request, text, code)
642
643    accept = request.getHeader("accept")
644    if not accept:
645        accept = "*/*"
646    if "*/*" in accept or "text/*" in accept or "text/html" in accept:
647        request.setResponseCode(http.INTERNAL_SERVER_ERROR)
648        return template.renderElement(
649            request,
650            tags.html(
651                tags.head(
652                    tags.title(u"Exception"),
653                ),
654                tags.body(
655                    FailureElement(failure),
656                ),
657            ),
658        )
659
660    # use plain text
661    traceback = failure.getTraceback()
662    return _renderHTTP_exception_simple(
663        request,
664        traceback,
665        http.INTERNAL_SERVER_ERROR,
666    )
667
668
669def _renderHTTP_exception_simple(request, text, code):
670    request.setResponseCode(code)
671    request.setHeader("content-type", "text/plain;charset=utf-8")
672    if isinstance(text, str):
673        text = text.encode("utf-8")
674    request.setHeader("content-length", b"%d" % len(text))
675    return text
676
677
678def handle_when_done(req, d):
679    when_done = get_arg(req, "when_done", None)
680    if when_done:
681        d.addCallback(lambda res: DecodedURL.from_text(when_done.decode("utf-8")))
682    return d
683
684
685def url_for_string(req, url_string):
686    """
687    Construct a universal URL using the given URL string.
688
689    :param IRequest req: The request being served.  If ``redir_to`` is not
690        absolute then this is used to determine the net location of this
691        server and the resulting URL is made to point at it.
692
693    :param bytes url_string: A byte string giving a universal or absolute URL.
694
695    :return DecodedURL: An absolute URL based on this server's net location
696        and the given URL string.
697    """
698    url = DecodedURL.from_text(url_string.decode("utf-8"))
699    if not url.host:
700        root = req.URLPath()
701        netloc = root.netloc.split(b":", 1)
702        if len(netloc) == 1:
703            host = netloc
704            port = None
705        else:
706            host = netloc[0]
707            port = int(netloc[1])
708        url = url.replace(
709            scheme=root.scheme.decode("ascii"),
710            host=host.decode("ascii"),
711            port=port,
712        )
713    return url
714
715T = TypeVar("T")
716
717@overload
718def get_arg(req: IRequest, argname: str | bytes, default: Optional[T] = None, *, multiple: Literal[False] = False) -> T | bytes: ...
719
720@overload
721def get_arg(req: IRequest, argname: str | bytes, default: Optional[T] = None, *, multiple: Literal[True]) -> T | tuple[bytes, ...]: ...
722
723def get_arg(req: IRequest, argname: str | bytes, default: Optional[T] = None, *, multiple: bool = False) -> None | T | bytes | tuple[bytes, ...]:
724    """Extract an argument from either the query args (req.args) or the form
725    body fields (req.fields). If multiple=False, this returns a single value
726    (or the default, which defaults to None), and the query args take
727    precedence. If multiple=True, this returns a tuple of arguments (possibly
728    empty), starting with all those in the query args.
729
730    :param TahoeLAFSRequest req: The request to consider.
731
732    :return: Either bytes or tuple of bytes.
733    """
734    # Need to import here to prevent circular import:
735    from ..webish import TahoeLAFSRequest
736
737    if isinstance(argname, str):
738        argname_bytes = argname.encode("utf-8")
739    else:
740        argname_bytes = argname
741
742    results : list[bytes] = []
743    if req.args is not None and argname_bytes in req.args:
744        results.extend(req.args[argname_bytes])
745    argname_unicode = str(argname_bytes, "utf-8")
746    if isinstance(req, TahoeLAFSRequest) and req.fields and argname_unicode in req.fields:
747        # In all but one or two unit tests, the request will be a
748        # TahoeLAFSRequest.
749        value = req.fields[argname_unicode].value
750        if isinstance(value, str):
751            value = value.encode("utf-8")
752        results.append(value)
753    if multiple:
754        return tuple(results)
755    if results:
756        return results[0]
757
758    if isinstance(default, str):
759        return default.encode("utf-8")
760    return default
761
762
763class MultiFormatResource(resource.Resource, object):
764    """
765    ``MultiFormatResource`` is a ``resource.Resource`` that can be rendered in
766    a number of different formats.
767
768    Rendered format is controlled by a query argument (given by
769    ``self.formatArgument``).  Different resources may support different
770    formats but ``json`` is a pretty common one.  ``html`` is the default
771    format if nothing else is given as the ``formatDefault``.
772    """
773    formatArgument = "t"
774    formatDefault = None  # type: Optional[str]
775
776    def render(self, req):
777        """
778        Dispatch to a renderer for a particular format, as selected by a query
779        argument.
780
781        A renderer for the format given by the query argument matching
782        ``formatArgument`` will be selected and invoked.  render_HTML will be
783        used as a default if no format is selected (either by query arguments
784        or by ``formatDefault``).
785
786        :return: The result of the selected renderer.
787        """
788        t = get_arg(req, self.formatArgument, self.formatDefault)
789        # It's either bytes or None.
790        if isinstance(t, bytes):
791            t = str(t, "ascii")
792        renderer = self._get_renderer(t)
793        result = renderer(req)
794        # On Python 3, json.dumps() returns Unicode for example, but
795        # twisted.web expects bytes. Instead of updating every single render
796        # method, just handle Unicode one time here.
797        if isinstance(result, str):
798            result = result.encode("utf-8")
799        return result
800
801    def _get_renderer(self, fmt):
802        """
803        Get the renderer for the indicated format.
804
805        :param str fmt: The format.  If a method with a prefix of ``render_``
806            and a suffix of this format (upper-cased) is found, it will be
807            used.
808
809        :return: A callable which takes a twisted.web Request and renders a
810            response.
811        """
812        renderer = None
813
814        if fmt is not None:
815            try:
816                renderer = getattr(self, "render_{}".format(fmt.upper()))
817            except AttributeError:
818                return resource.ErrorPage(
819                    http.BAD_REQUEST,
820                    "Bad Format",
821                    "Unknown {} value: {!r}".format(self.formatArgument, fmt),
822                ).render
823
824        if renderer is None:
825            renderer = self.render_HTML
826
827        return renderer
828
829
830def abbreviate_time(data):
831    """
832    Convert number of seconds into human readable string.
833
834    :param data: Either ``None`` or integer or float, seconds.
835
836    :return: Unicode string.
837    """
838    # 1.23s, 790ms, 132us
839    if data is None:
840        return u""
841    s = float(data)
842    if s >= 10:
843        return abbreviate.abbreviate_time(data)
844    if s >= 1.0:
845        return u"%.2fs" % s
846    if s >= 0.01:
847        return u"%.0fms" % (1000*s)
848    if s >= 0.001:
849        return u"%.1fms" % (1000*s)
850    return u"%.0fus" % (1000000*s)
851
852def get_keypair(request: IRequest) -> tuple[PublicKey, PrivateKey] | None:
853    """
854    Load a keypair from a urlsafe-base64-encoded RSA private key in the
855    **private-key** argument of the given request, if there is one.
856    """
857    privkey_der = get_arg(request, "private-key", default=None, multiple=False)
858    if privkey_der is None:
859        return None
860    privkey, pubkey = create_signing_keypair_from_string(urlsafe_b64decode(privkey_der))
861    return pubkey, privkey
862
863
864def add_static_children(root: IResource):
865    """
866    Add static files from C{allmydata.web} to the given resource.
867
868    Package resources may be on the filesystem, or they may be in a zip
869    or something, so we need to do a bit more work to serve them as
870    static files.
871    """
872    temporary_file_manager = ExitStack()
873    static_dir = resource_files("allmydata.web") / "static"
874    for child in static_dir.iterdir():
875        child_path = child.name.encode("utf-8")
876        root.putChild(child_path, static.File(
877            str(temporary_file_manager.enter_context(as_file(child)))
878        ))
879    weakref.finalize(root, temporary_file_manager.close)
Note: See TracBrowser for help on using the repository browser.