1#!/usr/bin/env python
2# Copyright (c) 2012 The Chromium Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6"""A tiny web server.
7
8This is intended to be used for testing, and only run from within the examples
9directory.
10"""
11
12import BaseHTTPServer
13import logging
14import optparse
15import os
16import SimpleHTTPServer
17import SocketServer
18import sys
19import urlparse
20
21
22EXAMPLE_PATH=os.path.dirname(os.path.abspath(__file__))
23NACL_SDK_ROOT = os.getenv('NACL_SDK_ROOT', os.path.dirname(EXAMPLE_PATH))
24
25
26if os.path.exists(NACL_SDK_ROOT):
27  sys.path.append(os.path.join(NACL_SDK_ROOT, 'tools'))
28  import decode_dump
29  import getos
30else:
31  NACL_SDK_ROOT=None
32
33last_nexe = None
34last_nmf = None
35
36logging.getLogger().setLevel(logging.INFO)
37
38# Using 'localhost' means that we only accept connections
39# via the loop back interface.
40SERVER_PORT = 5103
41SERVER_HOST = ''
42
43# We only run from the examples directory so that not too much is exposed
44# via this HTTP server.  Everything in the directory is served, so there should
45# never be anything potentially sensitive in the serving directory, especially
46# if the machine might be a multi-user machine and not all users are trusted.
47# We only serve via the loopback interface.
48def SanityCheckDirectory():
49  httpd_path = os.path.abspath(os.path.dirname(__file__))
50  serve_path = os.path.abspath(os.getcwd())
51
52  # Verify we are serving from the directory this script came from, or bellow
53  if serve_path[:len(httpd_path)] == httpd_path:
54    return
55  logging.error('For security, httpd.py should only be run from within the')
56  logging.error('example directory tree.')
57  logging.error('We are currently in %s.' % serve_path)
58  sys.exit(1)
59
60
61# An HTTP server that will quit when |is_running| is set to False.  We also use
62# SocketServer.ThreadingMixIn in order to handle requests asynchronously for
63# faster responses.
64class QuittableHTTPServer(SocketServer.ThreadingMixIn,
65                          BaseHTTPServer.HTTPServer):
66  def serve_forever(self, timeout=0.5):
67    self.is_running = True
68    self.timeout = timeout
69    while self.is_running:
70      self.handle_request()
71
72  def shutdown(self):
73    self.is_running = False
74    return 1
75
76
77# "Safely" split a string at |sep| into a [key, value] pair.  If |sep| does not
78# exist in |str|, then the entire |str| is the key and the value is set to an
79# empty string.
80def KeyValuePair(str, sep='='):
81  if sep in str:
82    return str.split(sep)
83  else:
84    return [str, '']
85
86
87# A small handler that looks for '?quit=1' query in the path and shuts itself
88# down if it finds that parameter.
89class QuittableHTTPHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
90  def send_head(self):
91    """Common code for GET and HEAD commands.
92
93    This sends the response code and MIME headers.
94
95    Return value is either a file object (which has to be copied
96    to the outputfile by the caller unless the command was HEAD,
97    and must be closed by the caller under all circumstances), or
98    None, in which case the caller has nothing further to do.
99
100    """
101    path = self.translate_path(self.path)
102    f = None
103    if os.path.isdir(path):
104        if not self.path.endswith('/'):
105            # redirect browser - doing basically what apache does
106            self.send_response(301)
107            self.send_header("Location", self.path + "/")
108            self.end_headers()
109            return None
110        for index in "index.html", "index.htm":
111            index = os.path.join(path, index)
112            if os.path.exists(index):
113                path = index
114                break
115        else:
116            return self.list_directory(path)
117    ctype = self.guess_type(path)
118    try:
119        # Always read in binary mode. Opening files in text mode may cause
120        # newline translations, making the actual size of the content
121        # transmitted *less* than the content-length!
122        f = open(path, 'rb')
123    except IOError:
124        self.send_error(404, "File not found")
125        return None
126    self.send_response(200)
127    self.send_header("Content-type", ctype)
128    fs = os.fstat(f.fileno())
129    self.send_header("Content-Length", str(fs[6]))
130    self.send_header("Last-Modified", self.date_time_string(fs.st_mtime))
131    self.send_header('Cache-Control','no-cache, must-revalidate')
132    self.send_header('Expires','-1')
133    self.end_headers()
134    return f
135
136  def do_GET(self):
137    global last_nexe, last_nmf
138    (_, _, path, query, _) = urlparse.urlsplit(self.path)
139    url_params = dict([KeyValuePair(key_value)
140                      for key_value in query.split('&')])
141    if 'quit' in url_params and '1' in url_params['quit']:
142      self.send_response(200, 'OK')
143      self.send_header('Content-type', 'text/html')
144      self.send_header('Content-length', '0')
145      self.end_headers()
146      self.server.shutdown()
147      return
148
149    if path.endswith('.nexe'):
150      last_nexe = path
151    if path.endswith('.nmf'):
152      last_nmf = path
153
154    SimpleHTTPServer.SimpleHTTPRequestHandler.do_GET(self)
155
156  def do_POST(self):
157    (_, _,path, query, _) = urlparse.urlsplit(self.path)
158    if 'Content-Length' in self.headers:
159      if not NACL_SDK_ROOT:
160        self.wfile('Could not find NACL_SDK_ROOT to decode trace.')
161        return
162      data = self.rfile.read(int(self.headers['Content-Length']))
163      nexe = '.' + last_nexe
164      nmf = '.' + last_nmf
165      addr = os.path.join(NACL_SDK_ROOT, 'toolchain',
166                          getos.GetPlatform() + '_x86_newlib',
167                          'bin', 'x86_64-nacl-addr2line')
168      decoder = decode_dump.CoreDecoder(nexe, nmf, addr, None, None)
169      info = decoder.Decode(data)
170      trace = decoder.StackTrace(info)
171      decoder.PrintTrace(trace, sys.stdout)
172      decoder.PrintTrace(trace, self.wfile)
173
174
175def Run(server_address,
176        server_class=QuittableHTTPServer,
177        handler_class=QuittableHTTPHandler):
178  httpd = server_class(server_address, handler_class)
179  logging.info("Starting local server on port %d", server_address[1])
180  logging.info("To shut down send http://localhost:%d?quit=1",
181               server_address[1])
182  try:
183    httpd.serve_forever()
184  except KeyboardInterrupt:
185    logging.info("Received keyboard interrupt.")
186    httpd.server_close()
187
188  logging.info("Shutting down local server on port %d", server_address[1])
189
190
191def main():
192  usage_str = "usage: %prog [options] [optional_portnum]"
193  parser = optparse.OptionParser(usage=usage_str)
194  parser.add_option(
195    '--no_dir_check', dest='do_safe_check',
196    action='store_false', default=True,
197    help='Do not ensure that httpd.py is being run from a safe directory.')
198  (options, args) = parser.parse_args(sys.argv)
199  if options.do_safe_check:
200    SanityCheckDirectory()
201  if len(args) > 2:
202    print 'Too many arguments specified.'
203    parser.print_help()
204  elif len(args) == 2:
205    Run((SERVER_HOST, int(args[1])))
206  else:
207    Run((SERVER_HOST, SERVER_PORT))
208  return 0
209
210
211if __name__ == '__main__':
212  sys.exit(main())
213