summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/reporting/resources/report.py
blob: b5ee0c07d863e513f246f398134d6c18f4ade150 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
import time
import json
import re
import uuid

from wptserve.utils import isomorphic_decode


def retrieve_from_stash(request, key, timeout, default_value, min_count=None, retain=False):
  """Retrieve the set of reports for a given report ID.

  This will extract either the set of reports, credentials, or request count
  from the stash (depending on the key passed in) and return it encoded as JSON.

  When retrieving reports, this will not return any reports until min_count
  reports have been received.

  If timeout seconds elapse before the requested data can be found in the stash,
  or before at least min_count reports are received, default_value will be
  returned instead."""
  t0 = time.time()
  while time.time() - t0 < timeout:
    time.sleep(0.5)
    with request.server.stash.lock:
      value = request.server.stash.take(key=key)
      if value is not None:
        have_sufficient_reports = (
          min_count is None or len(value) >= min_count)
        if retain or not have_sufficient_reports:
          request.server.stash.put(key=key, value=value)
        if have_sufficient_reports:
          return json.dumps(value)

  return default_value


def main(request, response):
  # Handle CORS preflight requests
  if request.method == u'OPTIONS':
    # Always reject preflights for one subdomain
    if b"www2" in request.headers[b"Origin"]:
      return (400, [], u"CORS preflight rejected for www2")
    return [
      (b"Content-Type", b"text/plain"),
      (b"Access-Control-Allow-Origin", b"*"),
      (b"Access-Control-Allow-Methods", b"post"),
      (b"Access-Control-Allow-Headers", b"Content-Type"),
    ], u"CORS allowed"

  # Delete reports as requested
  if request.method == u'POST':
    body = json.loads(request.body)
    if (isinstance(body, dict) and "op" in body):
      if body["op"] == "DELETE" and "reportIDs" in body:
        with request.server.stash.lock:
          for key in body["reportIDs"]:
            request.server.stash.take(key=key)
        return "reports cleared"
      response.status = 400
      return "op parameter value not recognized"

  if b"reportID" in request.GET:
    key = request.GET.first(b"reportID")
  elif b"endpoint" in request.GET:
    key = uuid.uuid5(uuid.NAMESPACE_OID, isomorphic_decode(
      request.GET[b'endpoint'])).urn.encode('ascii')[9:]
  else:
    response.status = 400
    return "Either reportID or endpoint parameter is required."

  # Cookie and count keys are derived from the report ID.
  cookie_key = re.sub(b'^....', b'cccc', key)
  count_key = re.sub(b'^....', b'dddd', key)

  if request.method == u'GET':
    try:
      timeout = float(request.GET.first(b"timeout"))
    except:
      timeout = 0.5
    try:
      min_count = int(request.GET.first(b"min_count"))
    except:
      min_count = 1
    retain = (b"retain" in request.GET)

    op = request.GET.first(b"op", b"")
    if op in (b"retrieve_report", b""):
      return [(b"Content-Type", b"application/json")], retrieve_from_stash(request, key, timeout, u'[]', min_count, retain)

    if op == b"retrieve_cookies":
      return [(b"Content-Type", b"application/json")], u"{ \"reportCookies\" : " + str(retrieve_from_stash(request, cookie_key, timeout, u"\"None\"")) + u"}"

    if op == b"retrieve_count":
      return [(b"Content-Type", b"application/json")], u"{ \"report_count\": %s }" % retrieve_from_stash(request, count_key, timeout, 0)

    response.status = 400
    return "op parameter value not recognized."

  # Save cookies.
  if len(request.cookies.keys()) > 0:
    # Convert everything into strings and dump it into a dict.
    temp_cookies_dict = {}
    for dict_key in request.cookies.keys():
      temp_cookies_dict[isomorphic_decode(dict_key)] = str(
        request.cookies.get_list(dict_key))
    with request.server.stash.lock:
      # Clear any existing cookie data for this request before storing new data.
      request.server.stash.take(key=cookie_key)
      request.server.stash.put(key=cookie_key, value=temp_cookies_dict)

  # Append new report(s).
  new_reports = json.loads(request.body)

  # If the incoming report is a CSP report-uri report, then it will be a single
  # dictionary rather than a list of reports. To handle this case, ensure that
  # any non-list request bodies are wrapped in a list.
  if not isinstance(new_reports, list):
    new_reports = [new_reports]

  for report in new_reports:
    report[u"metadata"] = {
      u"content_type": isomorphic_decode(request.headers[b"Content-Type"]),
    }

  with request.server.stash.lock:
    reports = request.server.stash.take(key=key)
    if reports is None:
      reports = []
    reports.extend(new_reports)
    request.server.stash.put(key=key, value=reports)

  # Increment report submission count. This tracks the number of times this
  # reporting endpoint was contacted, rather than the total number of reports
  # submitted, which can be seen from the length of the report list.
  with request.server.stash.lock:
    count = request.server.stash.take(key=count_key)
    if count is None:
      count = 0
    count += 1
    request.server.stash.put(key=count_key, value=count)

  # Return acknowledgement report.
  return [(b"Content-Type", b"text/plain")], b"Recorded report " + request.body