diff mbox series

[v2] git-instaweb: Add Python builtin http.server support

Message ID 20190128132458.31401-1-arti.zirk@gmail.com (mailing list archive)
State New, archived
Headers show
Series [v2] git-instaweb: Add Python builtin http.server support | expand

Commit Message

Arti Zirk Jan. 28, 2019, 1:24 p.m. UTC
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>
---

Changes v1..v2:
 - Add compatibily for Python 2, tested with 2.7, 3.4, 3.5, 3.6, 3.7


Notes:
    Base Ref: master
    Web-Diff: https://github.com/artizirk/git/commit/fe30635765
    Checkout: git fetch https://github.com/artizirk/git instaweb-python-v2 && git checkout fe30635765

    ### Interdiff (v1..v2):

    diff --git a/git-instaweb.sh b/git-instaweb.sh
    index c8f9f03447..7c55229773 100755
    --- a/git-instaweb.sh
    +++ b/git-instaweb.sh
    @@ -614,10 +614,11 @@ python_conf() {
     	ln -sf "$root/gitweb.cgi" "$fqgitdir/gitweb/$httpd_only/cgi-bin/gitweb.cgi"
     	ln -sf "$root/static" "$fqgitdir/gitweb/$httpd_only/"

    -	# generate a standalone 'python3 http.server' script in $fqgitdir/gitweb
    -	# This asumes that python3 is in user's $PATH
    +	# generate a standalone 'python http.server' script in $fqgitdir/gitweb
    +	# This asumes that python is in user's $PATH
    +	# This script is Python 2 and 3 compatible
     	cat > "$fqgitdir/gitweb/gitweb.py" <<EOF
    -#!/usr/bin/env python3
    +#!/usr/bin/env python
     import os
     import sys

    @@ -639,7 +640,14 @@ os.dup2(errorlogfile.fileno(), _orig_stderr_fd)
     sys.stderr = errorlogfile

     from functools import partial
    -from http.server import CGIHTTPRequestHandler, test
    +
    +if sys.version_info < (3, 0):  # Python 2
    +	from CGIHTTPServer import CGIHTTPRequestHandler
    +	from BaseHTTPServer import HTTPServer as ServerClass
    +else:  # Python 3
    +	from http.server import CGIHTTPRequestHandler
    +	from http.server import HTTPServer as ServerClass
    +

     # Those environment variables will be passed to the cgi script
     os.environ.update({
    @@ -660,7 +668,7 @@ class GitWebRequestHandler(CGIHTTPRequestHandler):

     	def do_HEAD(self):
     		self.redirect_path()
    -		super().do_HEAD()
    +		CGIHTTPRequestHandler.do_HEAD(self)

     	def do_GET(self):
     		if self.path == "/":
    @@ -669,11 +677,11 @@ class GitWebRequestHandler(CGIHTTPRequestHandler):
     			self.end_headers()
     			return
     		self.redirect_path()
    -		super().do_GET()
    +		CGIHTTPRequestHandler.do_GET(self)

     	def do_POST(self):
     		self.redirect_path()
    -		super().do_POST()
    +		CGIHTTPRequestHandler.do_POST(self)

     	# rewrite path of every request that is not gitweb.cgi to out of cgi-bin
     	def redirect_path(self):
    @@ -685,7 +693,7 @@ class GitWebRequestHandler(CGIHTTPRequestHandler):
     	def is_cgi(self):
     		result = False
     		if self.path.startswith('/cgi-bin/gitweb.cgi'):
    -			result = super().is_cgi()
    +			result = CGIHTTPRequestHandler.is_cgi(self)
     		return result

    @@ -698,7 +706,12 @@ if "$local" == "true":
     # as this was added to SimpleHTTPRequestHandler in Python 3.7
     os.chdir("$fqgitdir/gitweb/$httpd_only/")

    -test(HandlerClass=GitWebRequestHandler, port=$port, bind=bind)
    +GitWebRequestHandler.protocol_version = "HTTP/1.0"
    +httpd = ServerClass((bind, $port), GitWebRequestHandler)
    +
    +sa = httpd.socket.getsockname()
    +print("Serving HTTP on", sa[0], "port", sa[1], "...")
    +httpd.serve_forever()
     EOF

     	chmod a+x "$fqgitdir/gitweb/gitweb.py"

    ### Patches

 Documentation/git-instaweb.txt |   3 +-
 git-instaweb.sh                | 127 ++++++++++++++++++++++++++++++++-
 2 files changed, 128 insertions(+), 2 deletions(-)


base-commit: 16a465bc018d09e9d7bbbdc5f40a7fb99c21f8ef
--
2.20.1

Comments

brian m. carlson Jan. 28, 2019, 4:52 p.m. UTC | #1
On Mon, Jan 28, 2019 at 03:24:59PM +0200, Arti Zirk wrote:
> +	# generate a standalone 'python http.server' script in $fqgitdir/gitweb
> +	# This asumes that python is in user's $PATH
> +	# This script is Python 2 and 3 compatible
> +	cat > "$fqgitdir/gitweb/gitweb.py" <<EOF
> +#!/usr/bin/env python

I will point out, that despite what the PEPs say, on Debian and
derivatives, "python" will always invoke Python 2, and never Python 3.

This is probably fine for now, but we'll need to reconsider it before
2020.
Arti Zirk Jan. 28, 2019, 5:48 p.m. UTC | #2
> I will point out, that despite what the PEPs say, on Debian and
> derivatives, "python" will always invoke Python 2, and never Python
> 3.
> 
> This is probably fine for now, but we'll need to reconsider it before
> 2020.

In this case it shouldn't be a problem because I have made sure that it
works in both cases.
Junio C Hamano Jan. 28, 2019, 6:27 p.m. UTC | #3
Arti Zirk <arti.zirk@gmail.com> writes:

> The generated wrapper is compatible with both Python 2 and 3.

;-)  

Yup, that obviously is the best way to settle "should we make it
clear that this would not work with Python 2?" question.
diff mbox series

Patch

diff --git a/Documentation/git-instaweb.txt b/Documentation/git-instaweb.txt
index e8ecdbf927..a54fe4401b 100644
--- a/Documentation/git-instaweb.txt
+++ b/Documentation/git-instaweb.txt
@@ -29,7 +29,8 @@  OPTIONS
 	The HTTP daemon command-line that will be executed.
 	Command-line options may be specified here, and the
 	configuration file will be added at the end of the command-line.
-	Currently apache2, lighttpd, mongoose, plackup and webrick are supported.
+	Currently apache2, lighttpd, mongoose, plackup, python and
+	webrick are supported.
 	(Default: lighttpd)

 -m::
diff --git a/git-instaweb.sh b/git-instaweb.sh
index eec264e630..7c55229773 100755
--- a/git-instaweb.sh
+++ b/git-instaweb.sh
@@ -67,6 +67,13 @@  resolve_full_httpd () {
 		httpd_only="${httpd%% *}" # cut on first space
 		return
 		;;
+	*python*)
+		# server is started by running via generated gitweb.py in
+		# $fqgitdir/gitweb
+		full_httpd="$fqgitdir/gitweb/gitweb.py"
+		httpd_only="${httpd%% *}" # cut on first space
+		return
+		;;
 	esac

 	httpd_only="$(echo $httpd | cut -f1 -d' ')"
@@ -110,7 +117,7 @@  start_httpd () {

 	# don't quote $full_httpd, there can be arguments to it (-f)
 	case "$httpd" in
-	*mongoose*|*plackup*)
+	*mongoose*|*plackup*|*python*)
 		#These servers don't have a daemon mode so we'll have to fork it
 		$full_httpd "$conf" &
 		#Save the pid before doing anything else (we'll print it later)
@@ -595,6 +602,121 @@  EOF
 	rm -f "$conf"
 }

+python_conf() {
+	# Python's builtin http.server and its CGI support is very limited.
+	# CGI handler is capable of running CGI script only from inside a directory.
+	# Trying to set cgi_directories=["/"] will add double slash to SCRIPT_NAME
+	# and that in turn breaks gitweb's relative link generation.
+
+	# create a simple web root where $fqgitdir/gitweb/$httpd_only is our root
+	mkdir -p "$fqgitdir/gitweb/$httpd_only/cgi-bin"
+	# Python http.server follows the symlinks
+	ln -sf "$root/gitweb.cgi" "$fqgitdir/gitweb/$httpd_only/cgi-bin/gitweb.cgi"
+	ln -sf "$root/static" "$fqgitdir/gitweb/$httpd_only/"
+
+	# generate a standalone 'python http.server' script in $fqgitdir/gitweb
+	# This asumes that python is in user's $PATH
+	# This script is Python 2 and 3 compatible
+	cat > "$fqgitdir/gitweb/gitweb.py" <<EOF
+#!/usr/bin/env python
+import os
+import sys
+
+# Open log file in line buffering mode
+accesslogfile = open("$fqgitdir/gitweb/access.log", 'a', buffering=1)
+errorlogfile = open("$fqgitdir/gitweb/error.log", 'a', buffering=1)
+
+# and replace our stdout and stderr with log files
+# also do a lowlevel duplicate of the logfile file descriptors so that
+# our CGI child process writes any stderr warning also to the log file
+_orig_stdout_fd = sys.stdout.fileno()
+sys.stdout.close()
+os.dup2(accesslogfile.fileno(), _orig_stdout_fd)
+sys.stdout = accesslogfile
+
+_orig_stderr_fd = sys.stderr.fileno()
+sys.stderr.close()
+os.dup2(errorlogfile.fileno(), _orig_stderr_fd)
+sys.stderr = errorlogfile
+
+from functools import partial
+
+if sys.version_info < (3, 0):  # Python 2
+	from CGIHTTPServer import CGIHTTPRequestHandler
+	from BaseHTTPServer import HTTPServer as ServerClass
+else:  # Python 3
+	from http.server import CGIHTTPRequestHandler
+	from http.server import HTTPServer as ServerClass
+
+
+# Those environment variables will be passed to the cgi script
+os.environ.update({
+	"GIT_EXEC_PATH": "$GIT_EXEC_PATH",
+	"GIT_DIR": "$GIT_DIR",
+	"GITWEB_CONFIG": "$GITWEB_CONFIG"
+})
+
+
+class GitWebRequestHandler(CGIHTTPRequestHandler):
+
+	def log_message(self, format, *args):
+		# Write access logs to stdout
+		sys.stdout.write("%s - - [%s] %s\n" %
+				(self.address_string(),
+				self.log_date_time_string(),
+				format%args))
+
+	def do_HEAD(self):
+		self.redirect_path()
+		CGIHTTPRequestHandler.do_HEAD(self)
+
+	def do_GET(self):
+		if self.path == "/":
+			self.send_response(303, "See Other")
+			self.send_header("Location", "/cgi-bin/gitweb.cgi")
+			self.end_headers()
+			return
+		self.redirect_path()
+		CGIHTTPRequestHandler.do_GET(self)
+
+	def do_POST(self):
+		self.redirect_path()
+		CGIHTTPRequestHandler.do_POST(self)
+
+	# rewrite path of every request that is not gitweb.cgi to out of cgi-bin
+	def redirect_path(self):
+		if not self.path.startswith("/cgi-bin/gitweb.cgi"):
+			self.path = self.path.replace("/cgi-bin/", "/")
+
+	# gitweb.cgi is the only thing that is ever going to be run here.
+	# Ignore everything else
+	def is_cgi(self):
+		result = False
+		if self.path.startswith('/cgi-bin/gitweb.cgi'):
+			result = CGIHTTPRequestHandler.is_cgi(self)
+		return result
+
+
+bind = "127.0.0.1"
+if "$local" == "true":
+	bind = "0.0.0.0"
+
+# Set our http root directory
+# This is a work around for a missing directory argument in older Python versions
+# as this was added to SimpleHTTPRequestHandler in Python 3.7
+os.chdir("$fqgitdir/gitweb/$httpd_only/")
+
+GitWebRequestHandler.protocol_version = "HTTP/1.0"
+httpd = ServerClass((bind, $port), GitWebRequestHandler)
+
+sa = httpd.socket.getsockname()
+print("Serving HTTP on", sa[0], "port", sa[1], "...")
+httpd.serve_forever()
+EOF
+
+	chmod a+x "$fqgitdir/gitweb/gitweb.py"
+}
+
 gitweb_conf() {
 	cat > "$fqgitdir/gitweb/gitweb_config.perl" <<EOF
 #!@@PERL@@
@@ -623,6 +745,9 @@  configure_httpd() {
 	*plackup*)
 		plackup_conf
 		;;
+	*python*)
+		python_conf
+		;;
 	*)
 		echo "Unknown httpd specified: $httpd"
 		exit 1