tor-browser

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

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(",")