tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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)