tor-browser

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

report.py (5475B)


      1 import time
      2 import json
      3 import re
      4 import uuid
      5 
      6 from wptserve.utils import isomorphic_decode
      7 
      8 
      9 def retrieve_from_stash(request, key, timeout, default_value, min_count=None, retain=False):
     10  """Retrieve the set of reports for a given report ID.
     11 
     12  This will extract either the set of reports, credentials, or request count
     13  from the stash (depending on the key passed in) and return it encoded as JSON.
     14 
     15  When retrieving reports, this will not return any reports until min_count
     16  reports have been received.
     17 
     18  If timeout seconds elapse before the requested data can be found in the stash,
     19  or before at least min_count reports are received, default_value will be
     20  returned instead."""
     21  t0 = time.time()
     22  while time.time() - t0 < timeout:
     23    time.sleep(0.5)
     24    with request.server.stash.lock:
     25      value = request.server.stash.take(key=key)
     26      if value is not None:
     27        have_sufficient_reports = (
     28          min_count is None or len(value) >= min_count)
     29        if retain or not have_sufficient_reports:
     30          request.server.stash.put(key=key, value=value)
     31        if have_sufficient_reports:
     32          return json.dumps(value)
     33 
     34  return default_value
     35 
     36 
     37 def main(request, response):
     38  # Handle CORS preflight requests
     39  if request.method == u'OPTIONS':
     40    # Always reject preflights for one subdomain
     41    if b"www2" in request.headers[b"Origin"]:
     42      return (400, [], u"CORS preflight rejected for www2")
     43    return [
     44      (b"Content-Type", b"text/plain"),
     45      (b"Access-Control-Allow-Origin", b"*"),
     46      (b"Access-Control-Allow-Methods", b"post"),
     47      (b"Access-Control-Allow-Headers", b"Content-Type"),
     48    ], u"CORS allowed"
     49 
     50  # Delete reports as requested
     51  if request.method == u'POST':
     52    body = json.loads(request.body)
     53    if (isinstance(body, dict) and "op" in body):
     54      if body["op"] == "DELETE" and "reportIDs" in body:
     55        with request.server.stash.lock:
     56          for key in body["reportIDs"]:
     57            request.server.stash.take(key=key)
     58        return "reports cleared"
     59      response.status = 400
     60      return "op parameter value not recognized"
     61 
     62  if b"reportID" in request.GET:
     63    key = request.GET.first(b"reportID")
     64  elif b"endpoint" in request.GET:
     65    key = uuid.uuid5(uuid.NAMESPACE_OID, isomorphic_decode(
     66      request.GET[b'endpoint'])).urn.encode('ascii')[9:]
     67  else:
     68    response.status = 400
     69    return "Either reportID or endpoint parameter is required."
     70 
     71  # Cookie and count keys are derived from the report ID.
     72  cookie_key = re.sub(b'^....', b'cccc', key)
     73  count_key = re.sub(b'^....', b'dddd', key)
     74 
     75  if request.method == u'GET':
     76    try:
     77      timeout = float(request.GET.first(b"timeout"))
     78    except:
     79      timeout = 0.5
     80    try:
     81      min_count = int(request.GET.first(b"min_count"))
     82    except:
     83      min_count = 1
     84    retain = (b"retain" in request.GET)
     85 
     86    op = request.GET.first(b"op", b"")
     87    if op in (b"retrieve_report", b""):
     88      return [(b"Content-Type", b"application/json")], retrieve_from_stash(request, key, timeout, u'[]', min_count, retain)
     89 
     90    if op == b"retrieve_cookies":
     91      return [(b"Content-Type", b"application/json")], u"{ \"reportCookies\" : " + str(retrieve_from_stash(request, cookie_key, timeout, u"\"None\"")) + u"}"
     92 
     93    if op == b"retrieve_count":
     94      return [(b"Content-Type", b"application/json")], u"{ \"report_count\": %s }" % retrieve_from_stash(request, count_key, timeout, 0)
     95 
     96    response.status = 400
     97    return "op parameter value not recognized."
     98 
     99  # Save cookies.
    100  if len(request.cookies.keys()) > 0:
    101    # Convert everything into strings and dump it into a dict.
    102    temp_cookies_dict = {}
    103    for dict_key in request.cookies.keys():
    104      temp_cookies_dict[isomorphic_decode(dict_key)] = str(
    105        request.cookies.get_list(dict_key))
    106    with request.server.stash.lock:
    107      # Clear any existing cookie data for this request before storing new data.
    108      request.server.stash.take(key=cookie_key)
    109      request.server.stash.put(key=cookie_key, value=temp_cookies_dict)
    110 
    111  # Append new report(s).
    112  new_reports = json.loads(request.body)
    113 
    114  # If the incoming report is a CSP report-uri report, then it will be a single
    115  # dictionary rather than a list of reports. To handle this case, ensure that
    116  # any non-list request bodies are wrapped in a list.
    117  if not isinstance(new_reports, list):
    118    new_reports = [new_reports]
    119 
    120  for report in new_reports:
    121    report[u"metadata"] = {
    122      u"content_type": isomorphic_decode(request.headers[b"Content-Type"]),
    123    }
    124 
    125  with request.server.stash.lock:
    126    reports = request.server.stash.take(key=key)
    127    if reports is None:
    128      reports = []
    129    reports.extend(new_reports)
    130    request.server.stash.put(key=key, value=reports)
    131 
    132  # Increment report submission count. This tracks the number of times this
    133  # reporting endpoint was contacted, rather than the total number of reports
    134  # submitted, which can be seen from the length of the report list.
    135  with request.server.stash.lock:
    136    count = request.server.stash.take(key=count_key)
    137    if count is None:
    138      count = 0
    139    count += 1
    140    request.server.stash.put(key=count_key, value=count)
    141 
    142  # Return acknowledgement report.
    143  response_headers = [(b"Content-Type", b"text/plain")]
    144  # Keep the same as preflight to not send CORS header for www2
    145  if b"www2" not in request.headers[b"Origin"]:
    146    response_headers.append((b"Access-Control-Allow-Origin", b"*"))
    147  return response_headers, b"Recorded report " + request.body