• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python
2# Copyright 2017 The Chromium Embedded Framework Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be found
4# in the LICENSE file.
5"""
6This script implements a simple HTTP server for receiving crash report uploads
7from a Breakpad/Crashpad client (any CEF-based application). This script is
8intended for testing purposes only. An HTTPS server and a system such as Socorro
9(https://wiki.mozilla.org/Socorro) should be used when uploading crash reports
10from production applications.
11
12Usage of this script is as follows:
13
141. Run this script from the command-line. The first argument is the server port
15   number and the second argument is the directory where uploaded report
16   information will be saved:
17
18   > python crash_server.py 8080 /path/to/dumps
19
202. Create a "crash_reporter.cfg" file at the required platform-specific
21   location. On Windows and Linux this file must be placed next to the main
22   application executable. On macOS this file must be placed in the top-level
23   app bundle Resources directory (e.g. "<appname>.app/Contents/Resources"). At
24   a minimum it must contain a "ServerURL=http://localhost:8080" line under the
25   "[Config]" section (make sure the port number matches the value specified in
26   step 1). See comments in include/cef_crash_util.h for a complete
27   specification of this file.
28
29   Example file contents:
30
31   [Config]
32   ServerURL=http://localhost:8080
33   # Disable rate limiting so that all crashes are uploaded.
34   RateLimitEnabled=false
35   MaxUploadsPerDay=0
36
37   [CrashKeys]
38   # The cefclient sample application sets these values (see step 5 below).
39   testkey_small1=small
40   testkey_small2=small
41   testkey_medium1=medium
42   testkey_medium2=medium
43   testkey_large1=large
44   testkey_large2=large
45
463. Load one of the following URLs in the CEF-based application to cause a crash:
47
48   Main (browser) process crash:   chrome://inducebrowsercrashforrealz
49   Renderer process crash:         chrome://crash
50   GPU process crash:              chrome://gpucrash
51
524. When this script successfully receives a crash report upload you will see
53   console output like the following:
54
55   01/10/2017 12:31:23: Dump <id>
56
57   The "<id>" value is a 16 digit hexadecimal string that uniquely identifies
58   the dump. Crash dumps and metadata (product state, command-line flags, crash
59   keys, etc.) will be written to the "<id>.dmp" and "<id>.json" files
60   underneath the directory specified in step 1.
61
62   On Linux Breakpad uses the wget utility to upload crash dumps, so make sure
63   that utility is installed. If the crash is handled correctly then you should
64   see console output like the following when the client uploads a crash dump:
65
66   --2017-01-10 12:31:22--  http://localhost:8080/
67   Resolving localhost (localhost)... 127.0.0.1
68   Connecting to localhost (localhost)|127.0.0.1|:8080... connected.
69   HTTP request sent, awaiting response... 200 OK
70   Length: unspecified [text/html]
71   Saving to: '/dev/fd/3'
72   Crash dump id: <id>
73
74   On macOS when uploading a crash report to this script over HTTP you may
75   receive an error like the following:
76
77   "Transport security has blocked a cleartext HTTP (http://) resource load
78   since it is insecure. Temporary exceptions can be configured via your app's
79   Info.plist file."
80
81   You can work around this error by adding the following key to the Helper app
82   Info.plist file (e.g. "<appname>.app/Contents/Frameworks/
83   <appname> Helper.app/Contents/Info.plist"):
84
85   <key>NSAppTransportSecurity</key>
86   <dict>
87     <!--Allow all connections (for testing only!)-->
88     <key>NSAllowsArbitraryLoads</key>
89     <true/>
90   </dict>
91
925. The cefclient sample application sets test crash key values in the browser
93   and renderer processes. To work properly these values must also be defined
94   in the "[CrashKeys]" section of "crash_reporter.cfg" as shown above.
95
96   In tests/cefclient/browser/client_browser.cc (browser process):
97
98   CefSetCrashKeyValue("testkey1", "value1_browser");
99   CefSetCrashKeyValue("testkey2", "value2_browser");
100   CefSetCrashKeyValue("testkey3", "value3_browser");
101
102   In tests/cefclient/renderer/client_renderer.cc (renderer process):
103
104   CefSetCrashKeyValue("testkey1", "value1_renderer");
105   CefSetCrashKeyValue("testkey2", "value2_renderer");
106   CefSetCrashKeyValue("testkey3", "value3_renderer");
107
108   When crashing the browser or renderer processes with cefclient you should
109   verify that the test crash key values are included in the metadata
110   ("<id>.json") file. Some values may be chunked as described in
111   include/cef_crash_util.h.
112"""
113
114from __future__ import absolute_import
115from __future__ import print_function
116import cgi
117import datetime
118import json
119import os
120import shutil
121import sys
122import uuid
123import zlib
124
125is_python2 = sys.version_info.major == 2
126
127if is_python2:
128  from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
129  from cStringIO import StringIO as BytesIO
130else:
131  from http.server import BaseHTTPRequestHandler, HTTPServer
132  from io import BytesIO, open
133
134
135def print_msg(msg):
136  """ Write |msg| to stdout and flush. """
137  timestr = datetime.datetime.now().strftime("%m/%d/%Y %H:%M:%S")
138  sys.stdout.write("%s: %s\n" % (timestr, msg))
139  sys.stdout.flush()
140
141
142# Key identifying the minidump file.
143minidump_key = 'upload_file_minidump'
144
145
146class CrashHTTPRequestHandler(BaseHTTPRequestHandler):
147
148  def __init__(self, dump_directory, *args):
149    self._dump_directory = dump_directory
150    BaseHTTPRequestHandler.__init__(self, *args)
151
152  def _send_default_response_headers(self):
153    """ Send default response headers. """
154    self.send_response(200)
155    self.send_header('Content-type', 'text/html')
156    self.end_headers()
157
158  def _parse_post_data(self, data):
159    """ Returns a cgi.FieldStorage object for this request or None if this is
160        not a POST request. """
161    if self.command != 'POST':
162      return None
163    return cgi.FieldStorage(
164        fp=BytesIO(data),
165        headers=self.headers,
166        environ={
167            'REQUEST_METHOD': 'POST',
168            'CONTENT_TYPE': self.headers['Content-Type'],
169        })
170
171  def _get_chunk_size(self):
172    # Read to the next "\r\n".
173    size_str = self.rfile.read(2)
174    while size_str[-2:] != b"\r\n":
175      size_str += self.rfile.read(1)
176    # Remove the trailing "\r\n".
177    size_str = size_str[:-2]
178    assert len(size_str) <= 4
179    return int(size_str, 16)
180
181  def _get_chunk_data(self, chunk_size):
182    data = self.rfile.read(chunk_size)
183    assert len(data) == chunk_size
184    # Skip the trailing "\r\n".
185    self.rfile.read(2)
186    return data
187
188  def _unchunk_request(self, compressed):
189    """ Read a chunked request body. Optionally decompress the result. """
190    if compressed:
191      d = zlib.decompressobj(16 + zlib.MAX_WBITS)
192
193    # Chunked format is: <size>\r\n<bytes>\r\n<size>\r\n<bytes>\r\n0\r\n
194    unchunked = b""
195    while True:
196      chunk_size = self._get_chunk_size()
197      print('Chunk size 0x%x' % chunk_size)
198      if (chunk_size == 0):
199        break
200      chunk_data = self._get_chunk_data(chunk_size)
201      if compressed:
202        unchunked += d.decompress(chunk_data)
203      else:
204        unchunked += chunk_data
205
206    if compressed:
207      unchunked += d.flush()
208
209    return unchunked
210
211  def _create_new_dump_id(self):
212    """ Breakpad requires a 16 digit hexadecimal dump ID. """
213    return uuid.uuid4().hex.upper()[0:16]
214
215  def do_GET(self):
216    """ Default empty implementation for handling GET requests. """
217    self._send_default_response_headers()
218    self.wfile.write("<html><body><h1>GET!</h1></body></html>")
219
220  def do_HEAD(self):
221    """ Default empty implementation for handling HEAD requests. """
222    self._send_default_response_headers()
223
224  def do_POST(self):
225    """ Handle a multi-part POST request submitted by Breakpad/Crashpad. """
226    self._send_default_response_headers()
227
228    # Create a unique ID for the dump.
229    dump_id = self._create_new_dump_id()
230
231    # Return the unique ID to the caller.
232    self.wfile.write(dump_id.encode('utf-8'))
233
234    dmp_stream = None
235    metadata = {}
236
237    # Request body may be chunked and/or gzip compressed. For example:
238    #
239    # 3029 branch on Windows:
240    #   User-Agent: Crashpad/0.8.0
241    #   Host: localhost:8080
242    #   Connection: Keep-Alive
243    #   Transfer-Encoding: chunked
244    #   Content-Type: multipart/form-data; boundary=---MultipartBoundary-vp5j9HdSRYK8DvX2DhtpqEbMNjSN1wnL---
245    #   Content-Encoding: gzip
246    #
247    # 2987 branch on Windows:
248    #   User-Agent: Crashpad/0.8.0
249    #   Host: localhost:8080
250    #   Connection: Keep-Alive
251    #   Content-Type: multipart/form-data; boundary=---MultipartBoundary-qFhorGA40vDJ1fgmc2mjorL0fRfKOqup---
252    #   Content-Length: 609894
253    #
254    # 2883 branch on Linux:
255    #   User-Agent: Wget/1.15 (linux-gnu)
256    #   Host: localhost:8080
257    #   Accept: */*
258    #   Connection: Keep-Alive
259    #   Content-Type: multipart/form-data; boundary=--------------------------83572861f14cc736
260    #   Content-Length: 32237
261    #   Content-Encoding: gzip
262    print(self.headers)
263
264    chunked = 'Transfer-Encoding' in self.headers and self.headers['Transfer-Encoding'].lower(
265    ) == 'chunked'
266    compressed = 'Content-Encoding' in self.headers and self.headers['Content-Encoding'].lower(
267    ) == 'gzip'
268    if chunked:
269      request_body = self._unchunk_request(compressed)
270    else:
271      content_length = int(self.headers[
272          'Content-Length']) if 'Content-Length' in self.headers else 0
273      if content_length > 0:
274        request_body = self.rfile.read(content_length)
275      else:
276        request_body = self.rfile.read()
277      if compressed:
278        request_body = zlib.decompress(request_body, 16 + zlib.MAX_WBITS)
279
280    # Parse the multi-part request.
281    form_data = self._parse_post_data(request_body)
282    for key in form_data.keys():
283      if key == minidump_key and form_data[minidump_key].file:
284        dmp_stream = form_data[minidump_key].file
285      else:
286        metadata[key] = form_data[key].value
287
288    if dmp_stream is None:
289      # Exit early if the request is invalid.
290      print_msg('Invalid dump %s' % dump_id)
291      return
292
293    print_msg('Dump %s' % dump_id)
294
295    # Write the minidump to file.
296    dump_file = os.path.join(self._dump_directory, dump_id + '.dmp')
297    with open(dump_file, 'wb') as fp:
298      shutil.copyfileobj(dmp_stream, fp)
299
300    # Write the metadata to file.
301    meta_file = os.path.join(self._dump_directory, dump_id + '.json')
302    if is_python2:
303      with open(meta_file, 'w') as fp:
304        json.dump(
305            metadata,
306            fp,
307            ensure_ascii=False,
308            encoding='utf-8',
309            indent=2,
310            sort_keys=True)
311    else:
312      with open(meta_file, 'w', encoding='utf-8') as fp:
313        json.dump(metadata, fp, indent=2, sort_keys=True)
314
315
316def HandleRequestsUsing(dump_store):
317  return lambda *args: CrashHTTPRequestHandler(dump_directory, *args)
318
319
320def RunCrashServer(port, dump_directory):
321  """ Run the crash handler HTTP server. """
322  httpd = HTTPServer(('', port), HandleRequestsUsing(dump_directory))
323  print_msg('Starting httpd on port %d' % port)
324  httpd.serve_forever()
325
326
327# Program entry point.
328if __name__ == "__main__":
329  if len(sys.argv) != 3:
330    print('Usage: %s <port> <dump_directory>' % os.path.basename(sys.argv[0]))
331    sys.exit(1)
332
333  # Create the dump directory if necessary.
334  dump_directory = sys.argv[2]
335  if not os.path.exists(dump_directory):
336    os.makedirs(dump_directory)
337  if not os.path.isdir(dump_directory):
338    raise Exception('Directory does not exist: %s' % dump_directory)
339
340  RunCrashServer(int(sys.argv[1]), dump_directory)
341