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

Last change on this file was 53451ab, checked in by meejah <meejah@…>, at 2024-12-06T20:20:18Z

remove Python 3.8 mentions

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