1from __future__ import print_function
2try:
3    from http.server import HTTPServer, SimpleHTTPRequestHandler
4except ImportError:
5    from BaseHTTPServer import HTTPServer
6    from SimpleHTTPServer import SimpleHTTPRequestHandler
7import os
8import sys
9try:
10    from urlparse import urlparse
11    from urllib import unquote
12except ImportError:
13    from urllib.parse import urlparse, unquote
14
15import posixpath
16
17if sys.version_info.major >= 3:
18    from io import StringIO, BytesIO
19else:
20    from io import BytesIO, BytesIO as StringIO
21
22import re
23import shutil
24import threading
25import time
26import socket
27import itertools
28
29import Reporter
30try:
31    import configparser
32except ImportError:
33    import ConfigParser as configparser
34
35###
36# Various patterns matched or replaced by server.
37
38kReportFileRE = re.compile('(.*/)?report-(.*)\\.html')
39
40kBugKeyValueRE = re.compile('<!-- BUG([^ ]*) (.*) -->')
41
42#  <!-- REPORTPROBLEM file="crashes/clang_crash_ndSGF9.mi" stderr="crashes/clang_crash_ndSGF9.mi.stderr.txt" info="crashes/clang_crash_ndSGF9.mi.info" -->
43
44kReportCrashEntryRE = re.compile('<!-- REPORTPROBLEM (.*?)-->')
45kReportCrashEntryKeyValueRE = re.compile(' ?([^=]+)="(.*?)"')
46
47kReportReplacements = []
48
49# Add custom javascript.
50kReportReplacements.append((re.compile('<!-- SUMMARYENDHEAD -->'), """\
51<script language="javascript" type="text/javascript">
52function load(url) {
53  if (window.XMLHttpRequest) {
54    req = new XMLHttpRequest();
55  } else if (window.ActiveXObject) {
56    req = new ActiveXObject("Microsoft.XMLHTTP");
57  }
58  if (req != undefined) {
59    req.open("GET", url, true);
60    req.send("");
61  }
62}
63</script>"""))
64
65# Insert additional columns.
66kReportReplacements.append((re.compile('<!-- REPORTBUGCOL -->'),
67                            '<td></td><td></td>'))
68
69# Insert report bug and open file links.
70kReportReplacements.append((re.compile('<!-- REPORTBUG id="report-(.*)\\.html" -->'),
71                            ('<td class="Button"><a href="report/\\1">Report Bug</a></td>' +
72                             '<td class="Button"><a href="javascript:load(\'open/\\1\')">Open File</a></td>')))
73
74kReportReplacements.append((re.compile('<!-- REPORTHEADER -->'),
75                                       '<h3><a href="/">Summary</a> > Report %(report)s</h3>'))
76
77kReportReplacements.append((re.compile('<!-- REPORTSUMMARYEXTRA -->'),
78                            '<td class="Button"><a href="report/%(report)s">Report Bug</a></td>'))
79
80# Insert report crashes link.
81
82# Disabled for the time being until we decide exactly when this should
83# be enabled. Also the radar reporter needs to be fixed to report
84# multiple files.
85
86#kReportReplacements.append((re.compile('<!-- REPORTCRASHES -->'),
87#                            '<br>These files will automatically be attached to ' +
88#                            'reports filed here: <a href="report_crashes">Report Crashes</a>.'))
89
90###
91# Other simple parameters
92
93kShare = posixpath.join(posixpath.dirname(__file__), '../share/scan-view')
94kConfigPath = os.path.expanduser('~/.scanview.cfg')
95
96###
97
98__version__ = "0.1"
99
100__all__ = ["create_server"]
101
102class ReporterThread(threading.Thread):
103    def __init__(self, report, reporter, parameters, server):
104        threading.Thread.__init__(self)
105        self.report = report
106        self.server = server
107        self.reporter = reporter
108        self.parameters = parameters
109        self.success = False
110        self.status = None
111
112    def run(self):
113        result = None
114        try:
115            if self.server.options.debug:
116                print("%s: SERVER: submitting bug."%(sys.argv[0],), file=sys.stderr)
117            self.status = self.reporter.fileReport(self.report, self.parameters)
118            self.success = True
119            time.sleep(3)
120            if self.server.options.debug:
121                print("%s: SERVER: submission complete."%(sys.argv[0],), file=sys.stderr)
122        except Reporter.ReportFailure as e:
123            self.status = e.value
124        except Exception as e:
125            s = StringIO()
126            import traceback
127            print('<b>Unhandled Exception</b><br><pre>', file=s)
128            traceback.print_exc(file=s)
129            print('</pre>', file=s)
130            self.status = s.getvalue()
131
132class ScanViewServer(HTTPServer):
133    def __init__(self, address, handler, root, reporters, options):
134        HTTPServer.__init__(self, address, handler)
135        self.root = root
136        self.reporters = reporters
137        self.options = options
138        self.halted = False
139        self.config = None
140        self.load_config()
141
142    def load_config(self):
143        self.config = configparser.RawConfigParser()
144
145        # Add defaults
146        self.config.add_section('ScanView')
147        for r in self.reporters:
148            self.config.add_section(r.getName())
149            for p in r.getParameters():
150              if p.saveConfigValue():
151                self.config.set(r.getName(), p.getName(), '')
152
153        # Ignore parse errors
154        try:
155            self.config.read([kConfigPath])
156        except:
157            pass
158
159        # Save on exit
160        import atexit
161        atexit.register(lambda: self.save_config())
162
163    def save_config(self):
164        # Ignore errors (only called on exit).
165        try:
166            f = open(kConfigPath,'w')
167            self.config.write(f)
168            f.close()
169        except:
170            pass
171
172    def halt(self):
173        self.halted = True
174        if self.options.debug:
175            print("%s: SERVER: halting." % (sys.argv[0],), file=sys.stderr)
176
177    def serve_forever(self):
178        while not self.halted:
179            if self.options.debug > 1:
180                print("%s: SERVER: waiting..." % (sys.argv[0],), file=sys.stderr)
181            try:
182                self.handle_request()
183            except OSError as e:
184                print('OSError',e.errno)
185
186    def finish_request(self, request, client_address):
187        if self.options.autoReload:
188            import ScanView
189            self.RequestHandlerClass = reload(ScanView).ScanViewRequestHandler
190        HTTPServer.finish_request(self, request, client_address)
191
192    def handle_error(self, request, client_address):
193        # Ignore socket errors
194        info = sys.exc_info()
195        if info and isinstance(info[1], socket.error):
196            if self.options.debug > 1:
197                print("%s: SERVER: ignored socket error." % (sys.argv[0],), file=sys.stderr)
198            return
199        HTTPServer.handle_error(self, request, client_address)
200
201# Borrowed from Quixote, with simplifications.
202def parse_query(qs, fields=None):
203    if fields is None:
204        fields = {}
205    for chunk in (_f for _f in qs.split('&') if _f):
206        if '=' not in chunk:
207            name = chunk
208            value = ''
209        else:
210            name, value = chunk.split('=', 1)
211        name = unquote(name.replace('+', ' '))
212        value = unquote(value.replace('+', ' '))
213        item = fields.get(name)
214        if item is None:
215            fields[name] = [value]
216        else:
217            item.append(value)
218    return fields
219
220class ScanViewRequestHandler(SimpleHTTPRequestHandler):
221    server_version = "ScanViewServer/" + __version__
222    dynamic_mtime = time.time()
223
224    def do_HEAD(self):
225        try:
226            SimpleHTTPRequestHandler.do_HEAD(self)
227        except Exception as e:
228            self.handle_exception(e)
229
230    def do_GET(self):
231        try:
232            SimpleHTTPRequestHandler.do_GET(self)
233        except Exception as e:
234            self.handle_exception(e)
235
236    def do_POST(self):
237        """Serve a POST request."""
238        try:
239            length = self.headers.getheader('content-length') or "0"
240            try:
241                length = int(length)
242            except:
243                length = 0
244            content = self.rfile.read(length)
245            fields = parse_query(content)
246            f = self.send_head(fields)
247            if f:
248                self.copyfile(f, self.wfile)
249                f.close()
250        except Exception as e:
251            self.handle_exception(e)
252
253    def log_message(self, format, *args):
254        if self.server.options.debug:
255            sys.stderr.write("%s: SERVER: %s - - [%s] %s\n" %
256                             (sys.argv[0],
257                              self.address_string(),
258                              self.log_date_time_string(),
259                              format%args))
260
261    def load_report(self, report):
262        path = os.path.join(self.server.root, 'report-%s.html'%report)
263        data = open(path).read()
264        keys = {}
265        for item in kBugKeyValueRE.finditer(data):
266            k,v = item.groups()
267            keys[k] = v
268        return keys
269
270    def load_crashes(self):
271        path = posixpath.join(self.server.root, 'index.html')
272        data = open(path).read()
273        problems = []
274        for item in kReportCrashEntryRE.finditer(data):
275            fieldData = item.group(1)
276            fields = dict([i.groups() for i in
277                           kReportCrashEntryKeyValueRE.finditer(fieldData)])
278            problems.append(fields)
279        return problems
280
281    def handle_exception(self, exc):
282        import traceback
283        s = StringIO()
284        print("INTERNAL ERROR\n", file=s)
285        traceback.print_exc(file=s)
286        f = self.send_string(s.getvalue(), 'text/plain')
287        if f:
288            self.copyfile(f, self.wfile)
289            f.close()
290
291    def get_scalar_field(self, name):
292        if name in self.fields:
293            return self.fields[name][0]
294        else:
295            return None
296
297    def submit_bug(self, c):
298        title = self.get_scalar_field('title')
299        description = self.get_scalar_field('description')
300        report = self.get_scalar_field('report')
301        reporterIndex = self.get_scalar_field('reporter')
302        files = []
303        for fileID in self.fields.get('files',[]):
304            try:
305                i = int(fileID)
306            except:
307                i = None
308            if i is None or i<0 or i>=len(c.files):
309                return (False, 'Invalid file ID')
310            files.append(c.files[i])
311
312        if not title:
313            return (False, "Missing title.")
314        if not description:
315            return (False, "Missing description.")
316        try:
317            reporterIndex = int(reporterIndex)
318        except:
319            return (False, "Invalid report method.")
320
321        # Get the reporter and parameters.
322        reporter = self.server.reporters[reporterIndex]
323        parameters = {}
324        for o in reporter.getParameters():
325            name = '%s_%s'%(reporter.getName(),o.getName())
326            if name not in self.fields:
327                return (False,
328                        'Missing field "%s" for %s report method.'%(name,
329                                                                    reporter.getName()))
330            parameters[o.getName()] = self.get_scalar_field(name)
331
332        # Update config defaults.
333        if report != 'None':
334            self.server.config.set('ScanView', 'reporter', reporterIndex)
335            for o in reporter.getParameters():
336              if o.saveConfigValue():
337                name = o.getName()
338                self.server.config.set(reporter.getName(), name, parameters[name])
339
340        # Create the report.
341        bug = Reporter.BugReport(title, description, files)
342
343        # Kick off a reporting thread.
344        t = ReporterThread(bug, reporter, parameters, self.server)
345        t.start()
346
347        # Wait for thread to die...
348        while t.isAlive():
349            time.sleep(.25)
350        submitStatus = t.status
351
352        return (t.success, t.status)
353
354    def send_report_submit(self):
355        report = self.get_scalar_field('report')
356        c = self.get_report_context(report)
357        if c.reportSource is None:
358            reportingFor = "Report Crashes > "
359            fileBug = """\
360<a href="/report_crashes">File Bug</a> > """%locals()
361        else:
362            reportingFor = '<a href="/%s">Report %s</a> > ' % (c.reportSource,
363                                                                   report)
364            fileBug = '<a href="/report/%s">File Bug</a> > ' % report
365        title = self.get_scalar_field('title')
366        description = self.get_scalar_field('description')
367
368        res,message = self.submit_bug(c)
369
370        if res:
371            statusClass = 'SubmitOk'
372            statusName = 'Succeeded'
373        else:
374            statusClass = 'SubmitFail'
375            statusName = 'Failed'
376
377        result = """
378<head>
379  <title>Bug Submission</title>
380  <link rel="stylesheet" type="text/css" href="/scanview.css" />
381</head>
382<body>
383<h3>
384<a href="/">Summary</a> >
385%(reportingFor)s
386%(fileBug)s
387Submit</h3>
388<form name="form" action="">
389<table class="form">
390<tr><td>
391<table class="form_group">
392<tr>
393  <td class="form_clabel">Title:</td>
394  <td class="form_value">
395    <input type="text" name="title" size="50" value="%(title)s" disabled>
396  </td>
397</tr>
398<tr>
399  <td class="form_label">Description:</td>
400  <td class="form_value">
401<textarea rows="10" cols="80" name="description" disabled>
402%(description)s
403</textarea>
404  </td>
405</table>
406</td></tr>
407</table>
408</form>
409<h1 class="%(statusClass)s">Submission %(statusName)s</h1>
410%(message)s
411<p>
412<hr>
413<a href="/">Return to Summary</a>
414</body>
415</html>"""%locals()
416        return self.send_string(result)
417
418    def send_open_report(self, report):
419        try:
420            keys = self.load_report(report)
421        except IOError:
422            return self.send_error(400, 'Invalid report.')
423
424        file = keys.get('FILE')
425        if not file or not posixpath.exists(file):
426            return self.send_error(400, 'File does not exist: "%s"' % file)
427
428        import startfile
429        if self.server.options.debug:
430            print('%s: SERVER: opening "%s"'%(sys.argv[0],
431                                                            file), file=sys.stderr)
432
433        status = startfile.open(file)
434        if status:
435            res = 'Opened: "%s"' % file
436        else:
437            res = 'Open failed: "%s"' % file
438
439        return self.send_string(res, 'text/plain')
440
441    def get_report_context(self, report):
442        class Context(object):
443            pass
444        if report is None or report == 'None':
445            data = self.load_crashes()
446            # Don't allow empty reports.
447            if not data:
448                raise ValueError('No crashes detected!')
449            c = Context()
450            c.title = 'clang static analyzer failures'
451
452            stderrSummary = ""
453            for item in data:
454                if 'stderr' in item:
455                    path = posixpath.join(self.server.root, item['stderr'])
456                    if os.path.exists(path):
457                        lns = itertools.islice(open(path), 0, 10)
458                        stderrSummary += '%s\n--\n%s' % (item.get('src',
459                                                                  '<unknown>'),
460                                                         ''.join(lns))
461
462            c.description = """\
463The clang static analyzer failed on these inputs:
464%s
465
466STDERR Summary
467--------------
468%s
469""" % ('\n'.join([item.get('src','<unknown>') for item in data]),
470       stderrSummary)
471            c.reportSource = None
472            c.navMarkup = "Report Crashes > "
473            c.files = []
474            for item in data:
475                c.files.append(item.get('src',''))
476                c.files.append(posixpath.join(self.server.root,
477                                              item.get('file','')))
478                c.files.append(posixpath.join(self.server.root,
479                                              item.get('clangfile','')))
480                c.files.append(posixpath.join(self.server.root,
481                                              item.get('stderr','')))
482                c.files.append(posixpath.join(self.server.root,
483                                              item.get('info','')))
484            # Just in case something failed, ignore files which don't
485            # exist.
486            c.files = [f for f in c.files
487                       if os.path.exists(f) and os.path.isfile(f)]
488        else:
489            # Check that this is a valid report.
490            path = posixpath.join(self.server.root, 'report-%s.html' % report)
491            if not posixpath.exists(path):
492                raise ValueError('Invalid report ID')
493            keys = self.load_report(report)
494            c = Context()
495            c.title = keys.get('DESC','clang error (unrecognized')
496            c.description = """\
497Bug reported by the clang static analyzer.
498
499Description: %s
500File: %s
501Line: %s
502"""%(c.title, keys.get('FILE','<unknown>'), keys.get('LINE', '<unknown>'))
503            c.reportSource = 'report-%s.html' % report
504            c.navMarkup = """<a href="/%s">Report %s</a> > """ % (c.reportSource,
505                                                                  report)
506
507            c.files = [path]
508        return c
509
510    def send_report(self, report, configOverrides=None):
511        def getConfigOption(section, field):
512            if (configOverrides is not None and
513                section in configOverrides and
514                field in configOverrides[section]):
515                return configOverrides[section][field]
516            return self.server.config.get(section, field)
517
518        # report is None is used for crashes
519        try:
520            c = self.get_report_context(report)
521        except ValueError as e:
522            return self.send_error(400, e.message)
523
524        title = c.title
525        description= c.description
526        reportingFor = c.navMarkup
527        if c.reportSource is None:
528            extraIFrame = ""
529        else:
530            extraIFrame = """\
531<iframe src="/%s" width="100%%" height="40%%"
532        scrolling="auto" frameborder="1">
533  <a href="/%s">View Bug Report</a>
534</iframe>""" % (c.reportSource, c.reportSource)
535
536        reporterSelections = []
537        reporterOptions = []
538
539        try:
540            active = int(getConfigOption('ScanView','reporter'))
541        except:
542            active = 0
543        for i,r in enumerate(self.server.reporters):
544            selected = (i == active)
545            if selected:
546                selectedStr = ' selected'
547            else:
548                selectedStr = ''
549            reporterSelections.append('<option value="%d"%s>%s</option>'%(i,selectedStr,r.getName()))
550            options = '\n'.join([ o.getHTML(r,title,getConfigOption) for o in r.getParameters()])
551            display = ('none','')[selected]
552            reporterOptions.append("""\
553<tr id="%sReporterOptions" style="display:%s">
554  <td class="form_label">%s Options</td>
555  <td class="form_value">
556    <table class="form_inner_group">
557%s
558    </table>
559  </td>
560</tr>
561"""%(r.getName(),display,r.getName(),options))
562        reporterSelections = '\n'.join(reporterSelections)
563        reporterOptionsDivs = '\n'.join(reporterOptions)
564        reportersArray = '[%s]'%(','.join([repr(r.getName()) for r in self.server.reporters]))
565
566        if c.files:
567            fieldSize = min(5, len(c.files))
568            attachFileOptions = '\n'.join(["""\
569<option value="%d" selected>%s</option>""" % (i,v) for i,v in enumerate(c.files)])
570            attachFileRow = """\
571<tr>
572  <td class="form_label">Attach:</td>
573  <td class="form_value">
574<select style="width:100%%" name="files" multiple size=%d>
575%s
576</select>
577  </td>
578</tr>
579""" % (min(5, len(c.files)), attachFileOptions)
580        else:
581            attachFileRow = ""
582
583        result = """<html>
584<head>
585  <title>File Bug</title>
586  <link rel="stylesheet" type="text/css" href="/scanview.css" />
587</head>
588<script language="javascript" type="text/javascript">
589var reporters = %(reportersArray)s;
590function updateReporterOptions() {
591  index = document.getElementById('reporter').selectedIndex;
592  for (var i=0; i < reporters.length; ++i) {
593    o = document.getElementById(reporters[i] + "ReporterOptions");
594    if (i == index) {
595      o.style.display = "";
596    } else {
597      o.style.display = "none";
598    }
599  }
600}
601</script>
602<body onLoad="updateReporterOptions()">
603<h3>
604<a href="/">Summary</a> >
605%(reportingFor)s
606File Bug</h3>
607<form name="form" action="/report_submit" method="post">
608<input type="hidden" name="report" value="%(report)s">
609
610<table class="form">
611<tr><td>
612<table class="form_group">
613<tr>
614  <td class="form_clabel">Title:</td>
615  <td class="form_value">
616    <input type="text" name="title" size="50" value="%(title)s">
617  </td>
618</tr>
619<tr>
620  <td class="form_label">Description:</td>
621  <td class="form_value">
622<textarea rows="10" cols="80" name="description">
623%(description)s
624</textarea>
625  </td>
626</tr>
627
628%(attachFileRow)s
629
630</table>
631<br>
632<table class="form_group">
633<tr>
634  <td class="form_clabel">Method:</td>
635  <td class="form_value">
636    <select id="reporter" name="reporter" onChange="updateReporterOptions()">
637    %(reporterSelections)s
638    </select>
639  </td>
640</tr>
641%(reporterOptionsDivs)s
642</table>
643<br>
644</td></tr>
645<tr><td class="form_submit">
646  <input align="right" type="submit" name="Submit" value="Submit">
647</td></tr>
648</table>
649</form>
650
651%(extraIFrame)s
652
653</body>
654</html>"""%locals()
655
656        return self.send_string(result)
657
658    def send_head(self, fields=None):
659        if (self.server.options.onlyServeLocal and
660            self.client_address[0] != '127.0.0.1'):
661            return self.send_error(401, 'Unauthorized host.')
662
663        if fields is None:
664            fields = {}
665        self.fields = fields
666
667        o = urlparse(self.path)
668        self.fields = parse_query(o.query, fields)
669        path = posixpath.normpath(unquote(o.path))
670
671        # Split the components and strip the root prefix.
672        components = path.split('/')[1:]
673
674        # Special case some top-level entries.
675        if components:
676            name = components[0]
677            if len(components)==2:
678                if name=='report':
679                    return self.send_report(components[1])
680                elif name=='open':
681                    return self.send_open_report(components[1])
682            elif len(components)==1:
683                if name=='quit':
684                    self.server.halt()
685                    return self.send_string('Goodbye.', 'text/plain')
686                elif name=='report_submit':
687                    return self.send_report_submit()
688                elif name=='report_crashes':
689                    overrides = { 'ScanView' : {},
690                                  'Radar' : {},
691                                  'Email' : {} }
692                    for i,r in enumerate(self.server.reporters):
693                        if r.getName() == 'Radar':
694                            overrides['ScanView']['reporter'] = i
695                            break
696                    overrides['Radar']['Component'] = 'llvm - checker'
697                    overrides['Radar']['Component Version'] = 'X'
698                    return self.send_report(None, overrides)
699                elif name=='favicon.ico':
700                    return self.send_path(posixpath.join(kShare,'bugcatcher.ico'))
701
702        # Match directory entries.
703        if components[-1] == '':
704            components[-1] = 'index.html'
705
706        relpath = '/'.join(components)
707        path = posixpath.join(self.server.root, relpath)
708
709        if self.server.options.debug > 1:
710            print('%s: SERVER: sending path "%s"'%(sys.argv[0],
711                                                                 path), file=sys.stderr)
712        return self.send_path(path)
713
714    def send_404(self):
715        self.send_error(404, "File not found")
716        return None
717
718    def send_path(self, path):
719        # If the requested path is outside the root directory, do not open it
720        rel = os.path.abspath(path)
721        if not rel.startswith(os.path.abspath(self.server.root)):
722          return self.send_404()
723
724        ctype = self.guess_type(path)
725        if ctype.startswith('text/'):
726            # Patch file instead
727            return self.send_patched_file(path, ctype)
728        else:
729            mode = 'rb'
730        try:
731            f = open(path, mode)
732        except IOError:
733            return self.send_404()
734        return self.send_file(f, ctype)
735
736    def send_file(self, f, ctype):
737        # Patch files to add links, but skip binary files.
738        self.send_response(200)
739        self.send_header("Content-type", ctype)
740        fs = os.fstat(f.fileno())
741        self.send_header("Content-Length", str(fs[6]))
742        self.send_header("Last-Modified", self.date_time_string(fs.st_mtime))
743        self.end_headers()
744        return f
745
746    def send_string(self, s, ctype='text/html', headers=True, mtime=None):
747        encoded_s = s.encode('utf-8')
748        if headers:
749            self.send_response(200)
750            self.send_header("Content-type", ctype)
751            self.send_header("Content-Length", str(len(encoded_s)))
752            if mtime is None:
753                mtime = self.dynamic_mtime
754            self.send_header("Last-Modified", self.date_time_string(mtime))
755            self.end_headers()
756        return BytesIO(encoded_s)
757
758    def send_patched_file(self, path, ctype):
759        # Allow a very limited set of variables. This is pretty gross.
760        variables = {}
761        variables['report'] = ''
762        m = kReportFileRE.match(path)
763        if m:
764            variables['report'] = m.group(2)
765
766        try:
767            f = open(path,'rb')
768        except IOError:
769            return self.send_404()
770        fs = os.fstat(f.fileno())
771        data = f.read().decode('utf-8')
772        for a,b in kReportReplacements:
773            data = a.sub(b % variables, data)
774        return self.send_string(data, ctype, mtime=fs.st_mtime)
775
776
777def create_server(address, options, root):
778    import Reporter
779
780    reporters = Reporter.getReporters()
781
782    return ScanViewServer(address, ScanViewRequestHandler,
783                          root,
784                          reporters,
785                          options)
786