forked from chromiumembedded/cef
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathcrash_server.py
339 lines (275 loc) · 11.7 KB
/
crash_server.py
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
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
#!/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).
testkey_small1=small
testkey_small2=small
testkey_medium1=medium
testkey_medium2=medium
testkey_large1=large
testkey_large2=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 __future__ import absolute_import
from __future__ import print_function
import cgi
import datetime
import json
import os
import shutil
import sys
import uuid
import zlib
is_python2 = sys.version_info.major == 2
if is_python2:
from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
from cStringIO import StringIO as BytesIO
else:
from http.server import BaseHTTPRequestHandler, HTTPServer
from io import BytesIO, open
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, data):
""" 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=BytesIO(data),
headers=self.headers,
environ={
'REQUEST_METHOD': 'POST',
'CONTENT_TYPE': self.headers['Content-Type'],
})
def _get_chunk_size(self):
# Read to the next "\r\n".
size_str = self.rfile.read(2)
while size_str[-2:] != b"\r\n":
size_str += self.rfile.read(1)
# Remove the trailing "\r\n".
size_str = size_str[:-2]
return int(size_str, 16)
def _get_chunk_data(self, chunk_size):
data = self.rfile.read(chunk_size)
assert len(data) == chunk_size
# Skip the trailing "\r\n".
self.rfile.read(2)
return data
def _unchunk_request(self, compressed):
""" Read a chunked request body. Optionally decompress the result. """
if compressed:
d = zlib.decompressobj(16 + zlib.MAX_WBITS)
# Chunked format is: <size>\r\n<bytes>\r\n<size>\r\n<bytes>\r\n0\r\n
unchunked = b""
while True:
chunk_size = self._get_chunk_size()
print('Chunk size 0x%x' % chunk_size)
if (chunk_size == 0):
break
chunk_data = self._get_chunk_data(chunk_size)
if compressed:
unchunked += d.decompress(chunk_data)
else:
unchunked += chunk_data
if compressed:
unchunked += d.flush()
return unchunked
def _create_new_dump_id(self):
""" Breakpad requires a 16 digit hexadecimal dump ID. """
return uuid.uuid4().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.encode('utf-8'))
dmp_stream = None
metadata = {}
# Request body may be chunked and/or gzip compressed. For example:
#
# 3029 branch on Windows:
# User-Agent: Crashpad/0.8.0
# Host: localhost:8080
# Connection: Keep-Alive
# Transfer-Encoding: chunked
# Content-Type: multipart/form-data; boundary=---MultipartBoundary-vp5j9HdSRYK8DvX2DhtpqEbMNjSN1wnL---
# Content-Encoding: gzip
#
# 2987 branch on Windows:
# User-Agent: Crashpad/0.8.0
# Host: localhost:8080
# Connection: Keep-Alive
# Content-Type: multipart/form-data; boundary=---MultipartBoundary-qFhorGA40vDJ1fgmc2mjorL0fRfKOqup---
# Content-Length: 609894
#
# 2883 branch on Linux:
# User-Agent: Wget/1.15 (linux-gnu)
# Host: localhost:8080
# Accept: */*
# Connection: Keep-Alive
# Content-Type: multipart/form-data; boundary=--------------------------83572861f14cc736
# Content-Length: 32237
# Content-Encoding: gzip
print(self.headers)
chunked = 'Transfer-Encoding' in self.headers and self.headers['Transfer-Encoding'].lower(
) == 'chunked'
compressed = 'Content-Encoding' in self.headers and self.headers['Content-Encoding'].lower(
) == 'gzip'
if chunked:
request_body = self._unchunk_request(compressed)
else:
content_length = int(self.headers[
'Content-Length']) if 'Content-Length' in self.headers else 0
if content_length > 0:
request_body = self.rfile.read(content_length)
else:
request_body = self.rfile.read()
if compressed:
request_body = zlib.decompress(request_body, 16 + zlib.MAX_WBITS)
# Parse the multi-part request.
form_data = self._parse_post_data(request_body)
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')
if is_python2:
with open(meta_file, 'w') as fp:
json.dump(
metadata,
fp,
ensure_ascii=False,
encoding='utf-8',
indent=2,
sort_keys=True)
else:
with open(meta_file, 'w', encoding='utf-8') as fp:
json.dump(metadata, fp, indent=2, sort_keys=True)
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)