git-instaweb: add Python builtin http.server support

With this patch it is possible to launch git-instaweb by using Python http.server CGI handler via `-d python` option. git-instaweb generates a small wrapper around the http.server (in GIT_DIR/gitweb/) that address a limitation of the CGI handler where CGI scripts have to be in a cgi-bin subdirectory and directory index can't be easily changed. To keep the implementation small, gitweb is running on url `/cgi-bin/gitweb.cgi` and an automatic redirection is done when opening `/`. The generated wrapper is compatible with both Python 2 and 3. Python is by default installed on most modern Linux distributions which enables running `git instaweb -d python` without needing anything else. Signed-off-by: Arti Zirk <arti.zirk@gmail.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Arti Zirk committed Jan 28, 2019 at 15:24 UTC 2eb14bb2d41b27d23120206bbe3d409a83cf042f
2 files changed +128 -2
Documentation/git-instaweb.txt
+2 -1
@@ -29,7 +29,8 @@ OPTIONS
29 The HTTP daemon command-line that will be executed.
30 Command-line options may be specified here, and the
31 configuration file will be added at the end of the command-line.
32 - Currently apache2, lighttpd, mongoose, plackup and webrick are supported.
32 + Currently apache2, lighttpd, mongoose, plackup, python and
33 + webrick are supported.
34 (Default: lighttpd)
35
36 -m::
git-instaweb.sh
+126 -1
@@ -67,6 +67,13 @@ resolve_full_httpd () {
67 httpd_only="${httpd%% *}" # cut on first space
68 return
69 ;;
70 + *python*)
71 + # server is started by running via generated gitweb.py in
72 + # $fqgitdir/gitweb
73 + full_httpd="$fqgitdir/gitweb/gitweb.py"
74 + httpd_only="${httpd%% *}" # cut on first space
75 + return
76 + ;;
77 esac
78
79 httpd_only="$(echo $httpd | cut -f1 -d' ')"
@@ -110,7 +117,7 @@ start_httpd () {
117
118 # don't quote $full_httpd, there can be arguments to it (-f)
119 case "$httpd" in
113 - *mongoose*|*plackup*)
120 + *mongoose*|*plackup*|*python*)
121 #These servers don't have a daemon mode so we'll have to fork it
122 $full_httpd "$conf" &
123 #Save the pid before doing anything else (we'll print it later)
@@ -595,6 +602,121 @@ EOF
602 rm -f "$conf"
603 }
604
605 +python_conf() {
606 + # Python's builtin http.server and its CGI support is very limited.
607 + # CGI handler is capable of running CGI script only from inside a directory.
608 + # Trying to set cgi_directories=["/"] will add double slash to SCRIPT_NAME
609 + # and that in turn breaks gitweb's relative link generation.
610 +
611 + # create a simple web root where $fqgitdir/gitweb/$httpd_only is our root
612 + mkdir -p "$fqgitdir/gitweb/$httpd_only/cgi-bin"
613 + # Python http.server follows the symlinks
614 + ln -sf "$root/gitweb.cgi" "$fqgitdir/gitweb/$httpd_only/cgi-bin/gitweb.cgi"
615 + ln -sf "$root/static" "$fqgitdir/gitweb/$httpd_only/"
616 +
617 + # generate a standalone 'python http.server' script in $fqgitdir/gitweb
618 + # This asumes that python is in user's $PATH
619 + # This script is Python 2 and 3 compatible
620 + cat > "$fqgitdir/gitweb/gitweb.py" <<EOF
621 +#!/usr/bin/env python
622 +import os
623 +import sys
624 +
625 +# Open log file in line buffering mode
626 +accesslogfile = open("$fqgitdir/gitweb/access.log", 'a', buffering=1)
627 +errorlogfile = open("$fqgitdir/gitweb/error.log", 'a', buffering=1)
628 +
629 +# and replace our stdout and stderr with log files
630 +# also do a lowlevel duplicate of the logfile file descriptors so that
631 +# our CGI child process writes any stderr warning also to the log file
632 +_orig_stdout_fd = sys.stdout.fileno()
633 +sys.stdout.close()
634 +os.dup2(accesslogfile.fileno(), _orig_stdout_fd)
635 +sys.stdout = accesslogfile
636 +
637 +_orig_stderr_fd = sys.stderr.fileno()
638 +sys.stderr.close()
639 +os.dup2(errorlogfile.fileno(), _orig_stderr_fd)
640 +sys.stderr = errorlogfile
641 +
642 +from functools import partial
643 +
644 +if sys.version_info < (3, 0): # Python 2
645 + from CGIHTTPServer import CGIHTTPRequestHandler
646 + from BaseHTTPServer import HTTPServer as ServerClass
647 +else: # Python 3
648 + from http.server import CGIHTTPRequestHandler
649 + from http.server import HTTPServer as ServerClass
650 +
651 +
652 +# Those environment variables will be passed to the cgi script
653 +os.environ.update({
654 + "GIT_EXEC_PATH": "$GIT_EXEC_PATH",
655 + "GIT_DIR": "$GIT_DIR",
656 + "GITWEB_CONFIG": "$GITWEB_CONFIG"
657 +})
658 +
659 +
660 +class GitWebRequestHandler(CGIHTTPRequestHandler):
661 +
662 + def log_message(self, format, *args):
663 + # Write access logs to stdout
664 + sys.stdout.write("%s - - [%s] %s\n" %
665 + (self.address_string(),
666 + self.log_date_time_string(),
667 + format%args))
668 +
669 + def do_HEAD(self):
670 + self.redirect_path()
671 + CGIHTTPRequestHandler.do_HEAD(self)
672 +
673 + def do_GET(self):
674 + if self.path == "/":
675 + self.send_response(303, "See Other")
676 + self.send_header("Location", "/cgi-bin/gitweb.cgi")
677 + self.end_headers()
678 + return
679 + self.redirect_path()
680 + CGIHTTPRequestHandler.do_GET(self)
681 +
682 + def do_POST(self):
683 + self.redirect_path()
684 + CGIHTTPRequestHandler.do_POST(self)
685 +
686 + # rewrite path of every request that is not gitweb.cgi to out of cgi-bin
687 + def redirect_path(self):
688 + if not self.path.startswith("/cgi-bin/gitweb.cgi"):
689 + self.path = self.path.replace("/cgi-bin/", "/")
690 +
691 + # gitweb.cgi is the only thing that is ever going to be run here.
692 + # Ignore everything else
693 + def is_cgi(self):
694 + result = False
695 + if self.path.startswith('/cgi-bin/gitweb.cgi'):
696 + result = CGIHTTPRequestHandler.is_cgi(self)
697 + return result
698 +
699 +
700 +bind = "127.0.0.1"
701 +if "$local" == "true":
702 + bind = "0.0.0.0"
703 +
704 +# Set our http root directory
705 +# This is a work around for a missing directory argument in older Python versions
706 +# as this was added to SimpleHTTPRequestHandler in Python 3.7
707 +os.chdir("$fqgitdir/gitweb/$httpd_only/")
708 +
709 +GitWebRequestHandler.protocol_version = "HTTP/1.0"
710 +httpd = ServerClass((bind, $port), GitWebRequestHandler)
711 +
712 +sa = httpd.socket.getsockname()
713 +print("Serving HTTP on", sa[0], "port", sa[1], "...")
714 +httpd.serve_forever()
715 +EOF
716 +
717 + chmod a+x "$fqgitdir/gitweb/gitweb.py"
718 +}
719 +
720 gitweb_conf() {
721 cat > "$fqgitdir/gitweb/gitweb_config.perl" <<EOF
722 #!@@PERL@@
@@ -623,6 +745,9 @@ configure_httpd() {
745 *plackup*)
746 plackup_conf
747 ;;
748 + *python*)
749 + python_conf
750 + ;;
751 *)
752 echo "Unknown httpd specified: $httpd"
753 exit 1