fb925e7a创建于 2020年2月18日历史提交
#!/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]

    assert len(size_str) <= 4

    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)