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))
|