1# This Source Code Form is subject to the terms of the Mozilla Public
2# License, v. 2.0. If a copy of the MPL was not distributed with this
3# file, You can obtain one at http://mozilla.org/MPL/2.0/.
4
5# This module contains code for running an HTTP server to view build info.
6
7from __future__ import absolute_import, unicode_literals
8
9import BaseHTTPServer
10import json
11import os
12
13import requests
14
15
16class HTTPHandler(BaseHTTPServer.BaseHTTPRequestHandler):
17    def do_GET(self):
18        s = self.server.wrapper
19        p = self.path
20
21        if p == '/list':
22            self.send_response(200)
23            self.send_header('Content-Type', 'application/json; charset=utf-8')
24            self.end_headers()
25
26            keys = sorted(s.json_files.keys())
27            json.dump({'files': keys}, self.wfile)
28            return
29
30        if p.startswith('/resources/'):
31            key = p[len('/resources/'):]
32
33            if key not in s.json_files:
34                self.send_error(404)
35                return
36
37            self.send_response(200)
38            self.send_header('Content-Type', 'application/json; charset=utf-8')
39            self.end_headers()
40
41            self.wfile.write(s.json_files[key])
42            return
43
44        if p == '/':
45            p = '/index.html'
46
47        self.serve_docroot(s.doc_root, p[1:])
48
49    def do_POST(self):
50        if self.path == '/shutdown':
51            self.server.wrapper.do_shutdown = True
52            self.send_response(200)
53            return
54
55        self.send_error(404)
56
57    def serve_docroot(self, root, path):
58        local_path = os.path.normpath(os.path.join(root, path))
59
60        # Cheap security. This doesn't resolve symlinks, etc. But, it should be
61        # acceptable since this server only runs locally.
62        if not local_path.startswith(root):
63            self.send_error(404)
64
65        if not os.path.exists(local_path):
66            self.send_error(404)
67            return
68
69        if os.path.isdir(local_path):
70            self.send_error(500)
71            return
72
73        self.send_response(200)
74        ct = 'text/plain'
75        if path.endswith('.html'):
76            ct = 'text/html'
77
78        self.send_header('Content-Type', ct)
79        self.end_headers()
80
81        with open(local_path, 'rb') as fh:
82            self.wfile.write(fh.read())
83
84
85class BuildViewerServer(object):
86    def __init__(self, address='localhost', port=0):
87        # TODO use pkg_resources to obtain HTML resources.
88        pkg_dir = os.path.dirname(os.path.abspath(__file__))
89        doc_root = os.path.join(pkg_dir, 'resources', 'html-build-viewer')
90        assert os.path.isdir(doc_root)
91
92        self.doc_root = doc_root
93        self.json_files = {}
94
95        self.server = BaseHTTPServer.HTTPServer((address, port), HTTPHandler)
96        self.server.wrapper = self
97        self.do_shutdown = False
98
99    @property
100    def url(self):
101        hostname, port = self.server.server_address
102        return 'http://%s:%d/' % (hostname, port)
103
104    def add_resource_json_file(self, key, path):
105        """Register a resource JSON file with the server.
106
107        The file will be made available under the name/key specified."""
108        with open(path, 'rb') as fh:
109            self.json_files[key] = fh.read()
110
111    def add_resource_json_url(self, key, url):
112        """Register a resource JSON file at a URL."""
113        r = requests.get(url)
114        if r.status_code != 200:
115            raise Exception('Non-200 HTTP response code')
116        self.json_files[key] = r.text
117
118    def run(self):
119        while not self.do_shutdown:
120            self.server.handle_request()
121