fledge_http_server_util.py (7294B)
1 """Utility functions shared across multiple endpoints.""" 2 from collections import namedtuple 3 from urllib.parse import unquote_plus, urlparse 4 5 def fail(response, body): 6 """Sets up response to fail with the provided response body. 7 8 Args: 9 response: the wptserve Response that was passed to main 10 body: the HTTP response body to use 11 """ 12 response.status = (400, "Bad Request") 13 response.headers.set(b"Content-Type", b"text/plain") 14 response.content = body 15 16 def headers_to_ascii(headers): 17 """Converts a header map with binary values to one with ASCII values. 18 19 Takes a map of header names to list of values that are all binary strings 20 and returns an otherwise identical map where keys and values have both been 21 converted to ASCII strings. 22 23 Args: 24 headers: header map from binary key to binary value 25 26 Returns header map from ASCII string key to ASCII string value 27 """ 28 header_map = {} 29 for pair in headers.items(): 30 values = [] 31 for value in pair[1]: 32 values.append(value.decode("ASCII")) 33 header_map[pair[0].decode("ASCII")] = values 34 return header_map 35 36 def attach_origin_and_credentials_headers(request, response): 37 """Attaches Access-Control-Allow-Origin and Access-Control-Allow-Credentials 38 response headers to a response, if the request indicates they're needed. 39 Only intended for internal use. 40 41 Args: 42 request: the wptserve Request that was passed to main 43 response: the wptserve Response that was passed to main 44 """ 45 if b"origin" in request.headers: 46 response.headers.set(b"Access-Control-Allow-Origin", 47 request.headers.get(b"origin")) 48 49 if b"credentials" in request.headers: 50 response.headers.set(b"Access-Control-Allow-Credentials", 51 request.headers.get(b"credentials")) 52 53 def handle_cors_headers_fail_if_preflight(request, response): 54 """Adds CORS headers if necessary. In the case of CORS preflights, generates 55 a failure response. To be used when CORS preflights are not expected. 56 57 Args: 58 request: the wptserve Request that was passed to main 59 response: the wptserve Response that was passed to main 60 61 Returns True if the request is a CORS preflight, in which case the calling 62 function should immediately return. 63 """ 64 # Handle CORS preflight requests. 65 if request.method == u"OPTIONS": 66 fail(response, "CORS preflight unexpectedly received.") 67 return True 68 69 # Append CORS headers if needed 70 attach_origin_and_credentials_headers(request, response) 71 return False 72 73 def handle_cors_headers_and_preflight(request, response): 74 """Applies CORS logic, either adding CORS headers to response or generating 75 an entire response to preflights. 76 77 Args: 78 request: the wptserve Request that was passed to main 79 response: the wptserve Response that was passed to main 80 81 Returns True if the request is a CORS preflight, in which case the calling 82 function should immediately return. 83 """ 84 # Append CORS headers if needed 85 attach_origin_and_credentials_headers(request, response) 86 87 # Handle CORS preflight requests. 88 if not request.method == u"OPTIONS": 89 return False 90 91 if not b"Access-Control-Request-Method" in request.headers: 92 fail(response, "Failed to get access-control-request-method in preflight!") 93 return True 94 95 if not b"Access-Control-Request-Headers" in request.headers: 96 fail(response, "Failed to get access-control-request-headers in preflight!") 97 return True 98 99 response.headers.set(b"Access-Control-Allow-Methods", 100 request.headers[b"Access-Control-Request-Method"]) 101 102 response.headers.set(b"Access-Control-Allow-Headers", 103 request.headers[b"Access-Control-Request-Headers"]) 104 105 response.status = (204, b"No Content") 106 return True 107 108 def decode_trusted_scoring_signals_params(request): 109 """Decodes query parameters to trusted query params handler. 110 111 Args: 112 request: the wptserve Request that was passed to main 113 114 If successful, returns a named tuple TrustedScoringSignalsParams decoding the 115 various expected query fields, as a hostname, plus a field urlLists which is a list of 116 {type: <render URL type>, urls: <render URL list>} pairs, where <render URL type> is 117 one of the two render URL dictionary keys used in the response ("renderURLs" or 118 "adComponentRenderURLs"). May be of length 1 or 2, depending on whether there 119 are any component URLs. 120 121 On failure, throws a ValueError with a message. 122 """ 123 TrustedScoringSignalsParams = namedtuple( 124 'TrustedScoringSignalsParams', ['hostname', 'urlLists']) 125 126 hostname = None 127 renderUrls = None 128 adComponentRenderURLs = None 129 urlLists = [] 130 131 # Manually parse query params. Can't use request.GET because it unescapes as well as splitting, 132 # and commas mean very different things from escaped commas. 133 for param in request.url_parts.query.split("&"): 134 pair = param.split("=", 1) 135 if len(pair) != 2: 136 raise ValueError("Bad query parameter: " + param) 137 # Browsers should escape query params consistently. 138 if "%20" in pair[1]: 139 raise ValueError("Query parameter should escape using '+': " + param) 140 141 # Hostname can't be empty. The empty string can be a key or interest group name, though. 142 if pair[0] == "hostname" and hostname == None and len(pair[1]) > 0: 143 hostname = pair[1] 144 continue 145 if pair[0] == "renderUrls" and renderUrls == None: 146 renderUrls = list(map(unquote_plus, pair[1].split(","))) 147 urlLists.append({"type":"renderURLs", "urls":renderUrls}) 148 continue 149 if pair[0] == "adComponentRenderUrls" and adComponentRenderURLs == None: 150 adComponentRenderURLs = list(map(unquote_plus, pair[1].split(","))) 151 urlLists.append({"type":"adComponentRenderURLs", "urls":adComponentRenderURLs}) 152 continue 153 # Ignore the various creative scanning params; they're expected, but we 154 # don't parse them here. 155 if (pair[0] == 'adCreativeScanningMetadata' or 156 pair[0] == 'adComponentCreativeScanningMetadata' or 157 pair[0] == 'adSizes' or 158 pair[0] == 'adComponentSizes' or 159 pair[0] == 'adBuyer' or 160 pair[0] == 'adComponentBuyer' or 161 pair[0] == 'adBuyerAndSellerReportingIds'): 162 continue 163 raise ValueError("Unexpected query parameter: " + param) 164 165 # "hostname" and "renderUrls" are mandatory. 166 if not hostname: 167 raise ValueError("hostname missing") 168 if not renderUrls: 169 raise ValueError("renderUrls missing") 170 171 return TrustedScoringSignalsParams(hostname, urlLists) 172 173 def decode_render_url_signals_params(renderUrl): 174 """Decodes signalsParams field encoded inside a renderURL. 175 176 Args: renderUrl to extract signalsParams from. 177 178 Returns an array of fields in signal params string. 179 """ 180 signalsParams = None 181 for param in urlparse(renderUrl).query.split("&"): 182 pair = param.split("=", 1) 183 if len(pair) != 2: 184 continue 185 if pair[0] == "signalsParams": 186 if signalsParams != None: 187 raise ValueError("renderUrl has multiple signalsParams: " + renderUrl) 188 signalsParams = pair[1] 189 190 if signalsParams is None: 191 return [] 192 193 signalsParams = unquote_plus(signalsParams) 194 return signalsParams.split(",")