1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191
|
# This endpoint responds to both preflight requests and the subsequent requests.
#
# Its behavior can be configured with various search/GET parameters, all of
# which are optional:
#
# - treat-as-public-once: Must be a valid UUID if set.
# If set, then this endpoint expects to receive a non-preflight request first,
# for which it sets the `Content-Security-Policy: treat-as-public-address`
# response header. This allows testing "DNS rebinding", where a URL first
# resolves to the public IP address space, then a non-public IP address space.
# - preflight-uuid: Must be a valid UUID if set, distinct from the value of the
# `treat-as-public-once` parameter if both are set.
# If set, then this endpoint expects to receive a preflight request first
# followed by a regular request, as in the regular CORS protocol. If the
# `treat-as-public-once` header is also set, it takes precedence: this
# endpoint expects to receive a non-preflight request first, then a preflight
# request, then finally a regular request.
# If unset, then this endpoint expects to receive no preflight request, only
# a regular (non-OPTIONS) request.
# - preflight-headers: Valid values are:
# - cors: this endpoint responds with valid CORS headers to preflights. These
# should be sufficient for non-PNA preflight requests to succeed, but not
# for PNA-specific preflight requests.
# - cors+pna: this endpoint responds with valid CORS and PNA headers to
# preflights. These should be sufficient for both non-PNA preflight
# requests and PNA-specific preflight requests to succeed.
# - cors+pna+sw: this endpoint responds with valid CORS and PNA headers and
# "Access-Control-Allow-Headers: Service-Worker" to preflights. These should
# be sufficient for both non-PNA preflight requests and PNA-specific
# preflight requests to succeed. This allows the main request to fetch a
# service worker script.
# - unspecified, or any other value: this endpoint responds with no CORS or
# PNA headers. Preflight requests should fail.
# - final-headers: Valid values are:
# - cors: this endpoint responds with valid CORS headers to CORS-enabled
# non-preflight requests. These should be sufficient for non-preflighted
# CORS-enabled requests to succeed.
# - unspecified: this endpoint responds with no CORS headers to non-preflight
# requests. This should fail CORS-enabled requests, but be sufficient for
# no-CORS requests.
#
# The following parameters only affect non-preflight responses:
#
# - redirect: If set, the response code is set to 301 and the `Location`
# response header is set to this value.
# - mime-type: If set, the `Content-Type` response header is set to this value.
# - file: Specifies a path (relative to this file's directory) to a file. If
# set, the response body is copied from this file.
# - random-js-prefix: If set to any value, the response body is prefixed with
# a Javascript comment line containing a random value. This is useful in
# service worker tests, since service workers are only updated if the new
# script is not byte-for-byte identical with the old script.
# - body: If set and `file` is not, the response body is set to this value.
#
import os
import random
from wptserve.utils import isomorphic_encode
_ACAO = ("Access-Control-Allow-Origin", "*")
_ACAPN = ("Access-Control-Allow-Private-Network", "true")
_ACAH = ("Access-Control-Allow-Headers", "Service-Worker")
def _get_response_headers(method, mode, origin):
acam = ("Access-Control-Allow-Methods", method)
if mode == b"cors":
return [acam, _ACAO]
if mode == b"cors+pna":
return [acam, _ACAO, _ACAPN]
if mode == b"cors+pna+sw":
return [acam, _ACAO, _ACAPN, _ACAH]
if mode == b"navigation":
return [
acam,
("Access-Control-Allow-Origin", origin),
("Access-Control-Allow-Credentials", "true"),
_ACAPN,
]
return []
def _get_expect_single_preflight(request):
return request.GET.get(b"expect-single-preflight")
def _is_preflight_optional(request):
return request.GET.get(b"is-preflight-optional") or \
request.GET.get(b"file-if-no-preflight-received")
def _get_preflight_uuid(request):
return request.GET.get(b"preflight-uuid")
def _is_loaded_in_fenced_frame(request):
return request.GET.get(b"is-loaded-in-fenced-frame")
def _should_treat_as_public_once(request):
uuid = request.GET.get(b"treat-as-public-once")
if uuid is None:
# If the search parameter is not given, never treat as public.
return False
# If the parameter is given, we treat the request as public only if the UUID
# has never been seen and stashed.
result = request.server.stash.take(uuid) is None
request.server.stash.put(uuid, "")
return result
def _handle_preflight_request(request, response):
if _should_treat_as_public_once(request):
return (400, [], "received preflight for first treat-as-public request")
uuid = _get_preflight_uuid(request)
if uuid is None:
return (400, [], "missing `preflight-uuid` param from preflight URL")
value = request.server.stash.take(uuid)
request.server.stash.put(uuid, "preflight")
if _get_expect_single_preflight(request) and value is not None:
return (400, [], "received duplicated preflight")
method = request.headers.get("Access-Control-Request-Method")
mode = request.GET.get(b"preflight-headers")
origin = request.headers.get("Origin")
headers = _get_response_headers(method, mode, origin)
return (headers, "preflight")
def _final_response_body(request, missing_preflight):
file_name = None
if missing_preflight and not request.GET.get(b"is-preflight-optional"):
file_name = request.GET.get(b"file-if-no-preflight-received")
if file_name is None:
file_name = request.GET.get(b"file")
if file_name is None:
return request.GET.get(b"body") or "success"
prefix = b""
if request.GET.get(b"random-js-prefix"):
value = random.randint(0, 1000000000)
prefix = isomorphic_encode("// Random value: {}\n\n".format(value))
path = os.path.join(os.path.dirname(isomorphic_encode(__file__)), file_name)
with open(path, 'rb') as f:
contents = f.read()
return prefix + contents
def _handle_final_request(request, response):
missing_preflight = False
if _should_treat_as_public_once(request):
headers = [("Content-Security-Policy", "treat-as-public-address"),]
else:
uuid = _get_preflight_uuid(request)
if uuid is not None:
missing_preflight = request.server.stash.take(uuid) is None
if missing_preflight and not _is_preflight_optional(request):
return (405, [], "no preflight received")
request.server.stash.put(uuid, "final")
mode = request.GET.get(b"final-headers")
origin = request.headers.get("Origin")
headers = _get_response_headers(request.method, mode, origin)
redirect = request.GET.get(b"redirect")
if redirect is not None:
headers.append(("Location", redirect))
return (301, headers, b"")
mime_type = request.GET.get(b"mime-type")
if mime_type is not None:
headers.append(("Content-Type", mime_type),)
if _is_loaded_in_fenced_frame(request):
headers.append(("Supports-Loading-Mode", "fenced-frame"))
body = _final_response_body(request, missing_preflight)
return (headers, body)
def main(request, response):
try:
if request.method == "OPTIONS":
return _handle_preflight_request(request, response)
else:
return _handle_final_request(request, response)
except BaseException as e:
# Surface exceptions to the client, where they show up as assertion errors.
return (500, [("X-exception", str(e))], "exception: {}".format(e))
|