236 lines
8.5 KiB
Python
236 lines
8.5 KiB
Python
|
#!/usr/bin/env python
|
||
|
# Copyright 2017 The Chromium Embedded Framework Authors. All rights reserved.
|
||
|
# Use of this source code is governed by a BSD-style license that can be found
|
||
|
# in the LICENSE file.
|
||
|
|
||
|
"""
|
||
|
This script implements a simple HTTP server for receiving crash report uploads
|
||
|
from a Breakpad/Crashpad client (any CEF-based application). This script is
|
||
|
intended for testing purposes only. An HTTPS server and a system such as Socorro
|
||
|
(https://wiki.mozilla.org/Socorro) should be used when uploading crash reports
|
||
|
from production applications.
|
||
|
|
||
|
Usage of this script is as follows:
|
||
|
|
||
|
1. Run this script from the command-line. The first argument is the server port
|
||
|
number and the second argument is the directory where uploaded report
|
||
|
information will be saved:
|
||
|
|
||
|
> python crash_server.py 8080 /path/to/dumps
|
||
|
|
||
|
2. Create a "crash_reporter.cfg" file at the required platform-specific
|
||
|
location. On Windows and Linux this file must be placed next to the main
|
||
|
application executable. On macOS this file must be placed in the top-level
|
||
|
app bundle Resources directory (e.g. "<appname>.app/Contents/Resources"). At
|
||
|
a minimum it must contain a "ServerURL=http://localhost:8080" line under the
|
||
|
"[Config]" section (make sure the port number matches the value specified in
|
||
|
step 1). See comments in include/cef_crash_util.h for a complete
|
||
|
specification of this file.
|
||
|
|
||
|
Example file contents:
|
||
|
|
||
|
[Config]
|
||
|
ServerURL=http://localhost:8080
|
||
|
# Disable rate limiting so that all crashes are uploaded.
|
||
|
RateLimitEnabled=false
|
||
|
MaxUploadsPerDay=0
|
||
|
|
||
|
[CrashKeys]
|
||
|
# The cefclient sample application sets these values (see step 5 below).
|
||
|
testkey1=small
|
||
|
testkey2=medium
|
||
|
testkey3=large
|
||
|
|
||
|
3. Load one of the following URLs in the CEF-based application to cause a crash:
|
||
|
|
||
|
Main (browser) process crash: chrome://inducebrowsercrashforrealz
|
||
|
Renderer process crash: chrome://crash
|
||
|
GPU process crash: chrome://gpucrash
|
||
|
|
||
|
4. When this script successfully receives a crash report upload you will see
|
||
|
console output like the following:
|
||
|
|
||
|
01/10/2017 12:31:23: Dump <id>
|
||
|
|
||
|
The "<id>" value is a 16 digit hexadecimal string that uniquely identifies
|
||
|
the dump. Crash dumps and metadata (product state, command-line flags, crash
|
||
|
keys, etc.) will be written to the "<id>.dmp" and "<id>.json" files
|
||
|
underneath the directory specified in step 1.
|
||
|
|
||
|
On Linux Breakpad uses the wget utility to upload crash dumps, so make sure
|
||
|
that utility is installed. If the crash is handled correctly then you should
|
||
|
see console output like the following when the client uploads a crash dump:
|
||
|
|
||
|
--2017-01-10 12:31:22-- http://localhost:8080/
|
||
|
Resolving localhost (localhost)... 127.0.0.1
|
||
|
Connecting to localhost (localhost)|127.0.0.1|:8080... connected.
|
||
|
HTTP request sent, awaiting response... 200 OK
|
||
|
Length: unspecified [text/html]
|
||
|
Saving to: '/dev/fd/3'
|
||
|
Crash dump id: <id>
|
||
|
|
||
|
On macOS when uploading a crash report to this script over HTTP you may
|
||
|
receive an error like the following:
|
||
|
|
||
|
"Transport security has blocked a cleartext HTTP (http://) resource load
|
||
|
since it is insecure. Temporary exceptions can be configured via your app's
|
||
|
Info.plist file."
|
||
|
|
||
|
You can work around this error by adding the following key to the Helper app
|
||
|
Info.plist file (e.g. "<appname>.app/Contents/Frameworks/
|
||
|
<appname> Helper.app/Contents/Info.plist"):
|
||
|
|
||
|
<key>NSAppTransportSecurity</key>
|
||
|
<dict>
|
||
|
<!--Allow all connections (for testing only!)-->
|
||
|
<key>NSAllowsArbitraryLoads</key>
|
||
|
<true/>
|
||
|
</dict>
|
||
|
|
||
|
5. The cefclient sample application sets test crash key values in the browser
|
||
|
and renderer processes. To work properly these values must also be defined
|
||
|
in the "[CrashKeys]" section of "crash_reporter.cfg" as shown above.
|
||
|
|
||
|
In tests/cefclient/browser/client_browser.cc (browser process):
|
||
|
|
||
|
CefSetCrashKeyValue("testkey1", "value1_browser");
|
||
|
CefSetCrashKeyValue("testkey2", "value2_browser");
|
||
|
CefSetCrashKeyValue("testkey3", "value3_browser");
|
||
|
|
||
|
In tests/cefclient/renderer/client_renderer.cc (renderer process):
|
||
|
|
||
|
CefSetCrashKeyValue("testkey1", "value1_renderer");
|
||
|
CefSetCrashKeyValue("testkey2", "value2_renderer");
|
||
|
CefSetCrashKeyValue("testkey3", "value3_renderer");
|
||
|
|
||
|
When crashing the browser or renderer processes with cefclient you should
|
||
|
verify that the test crash key values are included in the metadata
|
||
|
("<id>.json") file. Some values may be chunked as described in
|
||
|
include/cef_crash_util.h.
|
||
|
"""
|
||
|
|
||
|
from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
|
||
|
import cgi
|
||
|
import cStringIO
|
||
|
import datetime
|
||
|
import json
|
||
|
import os
|
||
|
import shutil
|
||
|
import sys
|
||
|
import uuid
|
||
|
import zlib
|
||
|
|
||
|
def print_msg(msg):
|
||
|
""" Write |msg| to stdout and flush. """
|
||
|
timestr = datetime.datetime.now().strftime("%m/%d/%Y %H:%M:%S")
|
||
|
sys.stdout.write("%s: %s\n" % (timestr, msg))
|
||
|
sys.stdout.flush()
|
||
|
|
||
|
# Key identifying the minidump file.
|
||
|
minidump_key = 'upload_file_minidump'
|
||
|
|
||
|
class CrashHTTPRequestHandler(BaseHTTPRequestHandler):
|
||
|
def __init__(self, dump_directory, *args):
|
||
|
self._dump_directory = dump_directory
|
||
|
BaseHTTPRequestHandler.__init__(self, *args)
|
||
|
|
||
|
def _send_default_response_headers(self):
|
||
|
""" Send default response headers. """
|
||
|
self.send_response(200)
|
||
|
self.send_header('Content-type', 'text/html')
|
||
|
self.end_headers()
|
||
|
|
||
|
def _parse_post_data(self):
|
||
|
""" Returns a cgi.FieldStorage object for this request or None if this is
|
||
|
not a POST request. """
|
||
|
if self.command != 'POST':
|
||
|
return None
|
||
|
return cgi.FieldStorage(
|
||
|
fp = self.rfile,
|
||
|
headers = self.headers,
|
||
|
environ = {
|
||
|
'REQUEST_METHOD': 'POST',
|
||
|
'CONTENT_TYPE': self.headers['Content-Type'],
|
||
|
})
|
||
|
|
||
|
def _create_new_dump_id(self):
|
||
|
""" Breakpad requires a 16 digit hexadecimal dump ID. """
|
||
|
return str(uuid.uuid4().get_hex().upper()[0:16])
|
||
|
|
||
|
def do_GET(self):
|
||
|
""" Default empty implementation for handling GET requests. """
|
||
|
self._send_default_response_headers()
|
||
|
self.wfile.write("<html><body><h1>GET!</h1></body></html>")
|
||
|
|
||
|
def do_HEAD(self):
|
||
|
""" Default empty implementation for handling HEAD requests. """
|
||
|
self._send_default_response_headers()
|
||
|
|
||
|
def do_POST(self):
|
||
|
""" Handle a multi-part POST request submitted by Breakpad/Crashpad. """
|
||
|
self._send_default_response_headers()
|
||
|
|
||
|
# Create a unique ID for the dump.
|
||
|
dump_id = self._create_new_dump_id()
|
||
|
|
||
|
# Return the unique ID to the caller.
|
||
|
self.wfile.write(dump_id)
|
||
|
|
||
|
dmp_stream = None
|
||
|
metadata = {}
|
||
|
|
||
|
# Breakpad on Linux sends gzipped request contents.
|
||
|
if 'Content-Encoding' in self.headers and self.headers['Content-Encoding'] == 'gzip':
|
||
|
print_msg('Decompressing gzipped request')
|
||
|
self.rfile = cStringIO.StringIO(zlib.decompress(self.rfile.read(), 16+zlib.MAX_WBITS))
|
||
|
|
||
|
# Parse the multi-part request.
|
||
|
form_data = self._parse_post_data()
|
||
|
for key in form_data.keys():
|
||
|
if key == minidump_key and form_data[minidump_key].file:
|
||
|
dmp_stream = form_data[minidump_key].file
|
||
|
else:
|
||
|
metadata[key] = form_data[key].value
|
||
|
|
||
|
if dmp_stream is None:
|
||
|
# Exit early if the request is invalid.
|
||
|
print_msg('Invalid dump %s' % dump_id)
|
||
|
return
|
||
|
|
||
|
print_msg('Dump %s' % dump_id)
|
||
|
|
||
|
# Write the minidump to file.
|
||
|
dump_file = os.path.join(self._dump_directory, dump_id + '.dmp')
|
||
|
with open(dump_file, 'wb') as fp:
|
||
|
shutil.copyfileobj(dmp_stream, fp)
|
||
|
|
||
|
# Write the metadata to file.
|
||
|
meta_file = os.path.join(self._dump_directory, dump_id + '.json')
|
||
|
with open(meta_file, 'w') as fp:
|
||
|
json.dump(metadata, fp)
|
||
|
|
||
|
def HandleRequestsUsing(dump_store):
|
||
|
return lambda *args: CrashHTTPRequestHandler(dump_directory, *args)
|
||
|
|
||
|
def RunCrashServer(port, dump_directory):
|
||
|
""" Run the crash handler HTTP server. """
|
||
|
httpd = HTTPServer(('', port), HandleRequestsUsing(dump_directory))
|
||
|
print_msg('Starting httpd on port %d' % port)
|
||
|
httpd.serve_forever()
|
||
|
|
||
|
# Program entry point.
|
||
|
if __name__ == "__main__":
|
||
|
if len(sys.argv) != 3:
|
||
|
print 'Usage: %s <port> <dump_directory>' % os.path.basename(sys.argv[0])
|
||
|
sys.exit(1)
|
||
|
|
||
|
# Create the dump directory if necessary.
|
||
|
dump_directory = sys.argv[2]
|
||
|
if not os.path.exists(dump_directory):
|
||
|
os.makedirs(dump_directory)
|
||
|
if not os.path.isdir(dump_directory):
|
||
|
raise Exception('Directory does not exist: %s' % dump_directory)
|
||
|
|
||
|
RunCrashServer(int(sys.argv[1]), dump_directory)
|
||
|
|