trusted-bidding-signals.py (6634B)
1 import collections 2 import json 3 from urllib.parse import unquote_plus 4 5 from fledge.tentative.resources import fledge_http_server_util 6 7 8 # Script to generate trusted bidding signals. The response depends on the 9 # keys and interestGroupNames - some result in entire response failures, others 10 # affect only their own value. Keys are preferentially used over 11 # interestGroupName, since keys are composible, but some tests need to cover 12 # there being no keys. 13 def main(request, response): 14 hostname = None 15 keys = None 16 interestGroupNames = None 17 18 # Manually parse query params. Can't use request.GET because it unescapes as well as splitting, 19 # and commas mean very different things from escaped commas. 20 for param in request.url_parts.query.split("&"): 21 pair = param.split("=", 1) 22 if len(pair) != 2: 23 fail(response, "Bad query parameter: " + param) 24 return 25 # Browsers should escape query params consistently. 26 if "%20" in pair[1]: 27 fail(response, "Query parameter should escape using '+': " + param) 28 return 29 30 # Hostname can't be empty. The empty string can be a key or interest group name, though. 31 if pair[0] == "hostname" and hostname == None and len(pair[1]) > 0: 32 hostname = pair[1] 33 continue 34 if pair[0] == "keys" and keys == None: 35 keys = list(map(unquote_plus, pair[1].split(","))) 36 continue 37 if pair[0] == "interestGroupNames" and interestGroupNames == None: 38 interestGroupNames = list(map(unquote_plus, pair[1].split(","))) 39 continue 40 if pair[0] == "slotSize" or pair[0] == "allSlotsRequestedSizes": 41 continue 42 fail(response, "Unexpected query parameter: " + param) 43 return 44 45 # If trusted signal keys are passed in, and one of them is "cors", 46 # add appropriate Access-Control-* headers to normal requests. 47 if keys and "cors" in keys and fledge_http_server_util.handle_cors_headers_fail_if_preflight( 48 request, response): 49 return 50 51 # "interestGroupNames" and "hostname" are mandatory. 52 if not hostname: 53 fail(response, "hostname missing") 54 return 55 if not interestGroupNames: 56 fail(response, "interestGroupNames missing") 57 return 58 59 response.status = (200, b"OK") 60 61 # The JSON representation of this is used as the response body. This does 62 # not currently include a "perInterestGroupData" object except for 63 # updateIfOlderThanMs. 64 responseBody = {"keys": {}} 65 66 # Set when certain special keys are observed, used in place of the JSON 67 # representation of `responseBody`, when set. 68 body = None 69 70 contentType = "application/json" 71 adAuctionAllowed = "true" 72 dataVersion = None 73 if keys: 74 for key in keys: 75 value = "default value" 76 if key == "close-connection": 77 # Close connection without writing anything, to simulate a 78 # network error. The write call is needed to avoid writing the 79 # default headers. 80 response.writer.write("") 81 response.close_connection = True 82 return 83 elif key.startswith("replace-body:"): 84 # Replace entire response body. Continue to run through other 85 # keys, to allow them to modify request headers. 86 body = key.split(':', 1)[1] 87 elif key.startswith("data-version:"): 88 dataVersion = key.split(':', 1)[1] 89 elif key == "http-error": 90 response.status = (404, b"Not found") 91 elif key == "no-content-type": 92 contentType = None 93 elif key == "wrong-content-type": 94 contentType = 'text/plain' 95 elif key == "bad-ad-auction-allowed": 96 adAuctionAllowed = "sometimes" 97 elif key == "ad-auction-not-allowed": 98 adAuctionAllowed = "false" 99 elif key == "no-ad-auction-allow": 100 adAuctionAllowed = None 101 elif key == "no-value": 102 continue 103 elif key == "wrong-value": 104 responseBody["keys"]["another-value"] = "another-value" 105 continue 106 elif key == "null-value": 107 value = None 108 elif key == "num-value": 109 value = 1 110 elif key == "string-value": 111 value = "1" 112 elif key == "array-value": 113 value = [1, "foo", None] 114 elif key == "object-value": 115 value = {"a":"b", "c":["d"]} 116 elif key == "interest-group-names": 117 value = json.dumps(interestGroupNames) 118 elif key == "hostname": 119 value = request.GET.first(b"hostname", b"not-found").decode("ASCII") 120 elif key == "headers": 121 value = fledge_http_server_util.headers_to_ascii(request.headers) 122 elif key == "slotSize": 123 value = request.GET.first(b"slotSize", b"not-found").decode("ASCII") 124 elif key == "allSlotsRequestedSizes": 125 value = request.GET.first(b"allSlotsRequestedSizes", b"not-found").decode("ASCII") 126 elif key == "url": 127 value = request.url 128 responseBody["keys"][key] = value 129 130 if "data-version" in interestGroupNames: 131 dataVersion = "4" 132 133 per_interest_group_data = collections.defaultdict(dict) 134 for name in interestGroupNames: 135 if name == "use-update-if-older-than-ms": 136 # One hour in milliseconds. 137 per_interest_group_data[name]["updateIfOlderThanMs"] = 3_600_000 138 elif name == "use-update-if-older-than-ms-small": 139 # A value less than the minimum of 10 minutes. 140 per_interest_group_data[name]["updateIfOlderThanMs"] = 1 141 elif name == "use-update-if-older-than-ms-zero": 142 per_interest_group_data[name]["updateIfOlderThanMs"] = 0 143 elif name == "use-update-if-older-than-ms-negative": 144 per_interest_group_data[name]["updateIfOlderThanMs"] = -1 145 146 if per_interest_group_data: 147 responseBody["perInterestGroupData"] = dict(per_interest_group_data) 148 149 if contentType: 150 response.headers.set("Content-Type", contentType) 151 if adAuctionAllowed: 152 response.headers.set("Ad-Auction-Allowed", adAuctionAllowed) 153 if dataVersion: 154 response.headers.set("Data-Version", dataVersion) 155 response.headers.set("Ad-Auction-Bidding-Signals-Format-Version", "2") 156 157 if body != None: 158 return body 159 return json.dumps(responseBody)