|
30 | 30 | from twisted.python import failure
|
31 | 31 | from twisted.web import resource
|
32 | 32 | from twisted.web.server import NOT_DONE_YET, Request
|
33 |
| -from twisted.web.static import NoRangeStaticProducer |
| 33 | +from twisted.web.static import File, NoRangeStaticProducer |
34 | 34 | from twisted.web.util import redirectTo
|
35 | 35 |
|
36 | 36 | import synapse.events
|
@@ -202,12 +202,7 @@ def return_html_error(
|
202 | 202 | else:
|
203 | 203 | body = error_template.render(code=code, msg=msg)
|
204 | 204 |
|
205 |
| - body_bytes = body.encode("utf-8") |
206 |
| - request.setResponseCode(code) |
207 |
| - request.setHeader(b"Content-Type", b"text/html; charset=utf-8") |
208 |
| - request.setHeader(b"Content-Length", b"%i" % (len(body_bytes),)) |
209 |
| - request.write(body_bytes) |
210 |
| - finish_request(request) |
| 205 | + respond_with_html(request, code, body) |
211 | 206 |
|
212 | 207 |
|
213 | 208 | def wrap_async_request_handler(h):
|
@@ -420,6 +415,18 @@ def render(self, request):
|
420 | 415 | return NOT_DONE_YET
|
421 | 416 |
|
422 | 417 |
|
| 418 | +class StaticResource(File): |
| 419 | + """ |
| 420 | + A resource that represents a plain non-interpreted file or directory. |
| 421 | +
|
| 422 | + Differs from the File resource by adding clickjacking protection. |
| 423 | + """ |
| 424 | + |
| 425 | + def render_GET(self, request: Request): |
| 426 | + set_clickjacking_protection_headers(request) |
| 427 | + return super().render_GET(request) |
| 428 | + |
| 429 | + |
423 | 430 | def _options_handler(request):
|
424 | 431 | """Request handler for OPTIONS requests
|
425 | 432 |
|
@@ -530,7 +537,7 @@ def respond_with_json_bytes(
|
530 | 537 | code (int): The HTTP response code.
|
531 | 538 | json_bytes (bytes): The json bytes to use as the response body.
|
532 | 539 | send_cors (bool): Whether to send Cross-Origin Resource Sharing headers
|
533 |
| - http://www.w3.org/TR/cors/ |
| 540 | + https://fetch.spec.whatwg.org/#http-cors-protocol |
534 | 541 | Returns:
|
535 | 542 | twisted.web.server.NOT_DONE_YET"""
|
536 | 543 |
|
@@ -568,6 +575,59 @@ def set_cors_headers(request):
|
568 | 575 | )
|
569 | 576 |
|
570 | 577 |
|
| 578 | +def respond_with_html(request: Request, code: int, html: str): |
| 579 | + """ |
| 580 | + Wraps `respond_with_html_bytes` by first encoding HTML from a str to UTF-8 bytes. |
| 581 | + """ |
| 582 | + respond_with_html_bytes(request, code, html.encode("utf-8")) |
| 583 | + |
| 584 | + |
| 585 | +def respond_with_html_bytes(request: Request, code: int, html_bytes: bytes): |
| 586 | + """ |
| 587 | + Sends HTML (encoded as UTF-8 bytes) as the response to the given request. |
| 588 | +
|
| 589 | + Note that this adds clickjacking protection headers and finishes the request. |
| 590 | +
|
| 591 | + Args: |
| 592 | + request: The http request to respond to. |
| 593 | + code: The HTTP response code. |
| 594 | + html_bytes: The HTML bytes to use as the response body. |
| 595 | + """ |
| 596 | + # could alternatively use request.notifyFinish() and flip a flag when |
| 597 | + # the Deferred fires, but since the flag is RIGHT THERE it seems like |
| 598 | + # a waste. |
| 599 | + if request._disconnected: |
| 600 | + logger.warning( |
| 601 | + "Not sending response to request %s, already disconnected.", request |
| 602 | + ) |
| 603 | + return |
| 604 | + |
| 605 | + request.setResponseCode(code) |
| 606 | + request.setHeader(b"Content-Type", b"text/html; charset=utf-8") |
| 607 | + request.setHeader(b"Content-Length", b"%d" % (len(html_bytes),)) |
| 608 | + |
| 609 | + # Ensure this content cannot be embedded. |
| 610 | + set_clickjacking_protection_headers(request) |
| 611 | + |
| 612 | + request.write(html_bytes) |
| 613 | + finish_request(request) |
| 614 | + |
| 615 | + |
| 616 | +def set_clickjacking_protection_headers(request: Request): |
| 617 | + """ |
| 618 | + Set headers to guard against clickjacking of embedded content. |
| 619 | +
|
| 620 | + This sets the X-Frame-Options and Content-Security-Policy headers which instructs |
| 621 | + browsers to not allow the HTML of the response to be embedded onto another |
| 622 | + page. |
| 623 | +
|
| 624 | + Args: |
| 625 | + request: The http request to add the headers to. |
| 626 | + """ |
| 627 | + request.setHeader(b"X-Frame-Options", b"DENY") |
| 628 | + request.setHeader(b"Content-Security-Policy", b"frame-ancestors 'none';") |
| 629 | + |
| 630 | + |
571 | 631 | def finish_request(request):
|
572 | 632 | """ Finish writing the response to the request.
|
573 | 633 |
|
|
0 commit comments