launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #18228
[Merge] lp:~cjwatson/turnip/cgit into lp:turnip
Colin Watson has proposed merging lp:~cjwatson/turnip/cgit into lp:turnip.
Commit message:
Proxy non-git requests through to an appropriately-configured cgit.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~cjwatson/turnip/cgit/+merge/254606
Proxy non-git requests through to an appropriately-configured cgit. This is a stopgap until we have a proper integrated code browser.
This branch does not itself do user separation, but it supports it. I'll submit a separate branch of the turnip charm that takes care of that.
--
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~cjwatson/turnip/cgit into lp:turnip.
=== modified file 'config.yaml'
--- config.yaml 2015-03-30 11:11:55 +0000
+++ config.yaml 2015-03-30 16:57:54 +0000
@@ -9,4 +9,10 @@
smart_ssh_port: 9422
repo_store: /var/tmp/git.launchpad.dev
turnip_log_dir: ./
+<<<<<<< TREE
virtinfo_endpoint: http://xmlrpc-private.launchpad.dev:8087/git
+=======
+virtinfo_endpoint: http://localhost:6543/githosting
+cgit_exec_path: /usr/lib/cgit/cgit.cgi
+cgit_data_path: /usr/share/cgit
+>>>>>>> MERGE-SOURCE
=== modified file 'httpserver.tac'
--- httpserver.tac 2015-02-18 22:53:47 +0000
+++ httpserver.tac 2015-03-30 16:57:54 +0000
@@ -19,7 +19,8 @@
smarthttp_site = server.Site(
SmartHTTPFrontendResource(b'localhost',
config.get('pack_virt_port'),
- config.get('virtinfo_endpoint')))
+ config.get('virtinfo_endpoint'),
+ config.get('repo_store')))
return internet.TCPServer(config.get('smart_http_port'), smarthttp_site)
application = service.Application("Turnip SmartHTTP Service")
=== modified file 'turnip/pack/http.py'
--- turnip/pack/http.py 2015-03-26 03:50:48 +0000
+++ turnip/pack/http.py 2015-03-30 16:57:54 +0000
@@ -5,6 +5,9 @@
)
from cStringIO import StringIO
+import os.path
+import tempfile
+import textwrap
import sys
import zlib
@@ -17,6 +20,8 @@
http,
resource,
server,
+ static,
+ twcgi,
)
from turnip.pack.git import (
@@ -290,30 +295,156 @@
return b''
+class DirectoryWithoutListings(static.File):
+ """A static directory resource without support for directory listings."""
+
+ def directoryListing(self):
+ return self.childNotFound
+
+
+class RobotsResource(static.Data):
+ """HTTP resource to serve our robots.txt."""
+
+ robots_txt = textwrap.dedent("""\
+ User-agent: *
+ Disallow: /
+ """).encode('US-ASCII')
+
+ def __init__(self):
+ static.Data.__init__(self, self.robots_txt, 'text/plain')
+
+
+class CGitScriptResource(twcgi.CGIScript):
+ """HTTP resource to run cgit."""
+
+ def __init__(self, root):
+ twcgi.CGIScript.__init__(self, root.cgit_exec_path)
+ self.root = root
+ self.cgit_config = None
+
+ def _error(self, request, message, code=http.INTERNAL_SERVER_ERROR):
+ request.setResponseCode(code)
+ request.setHeader(b'Content-Type', b'text/plain')
+ request.write(message)
+ request.finish()
+
+ def _finished(self, ignored):
+ if self.cgit_config is not None:
+ self.cgit_config.close()
+
+ def _translatePathCallback(self, translated, env, request,
+ *args, **kwargs):
+ if 'path' not in translated:
+ self._error(
+ request, b'translatePath response did not include path')
+ return
+ repo_url = request.path.rstrip('/')
+ repo_path = '%s/%s' % (self.root.repo_store, translated['path'])
+ trailing = translated.get('trailing')
+ if trailing:
+ if not trailing.startswith('/'):
+ trailing = '/' + trailing
+ if not repo_url.endswith(trailing):
+ self._error(
+ request,
+ b'translatePath returned inconsistent response: '
+ b'"%s" does not end with "%s"' % (
+ repo_url.encode('UTF-8'), trailing.encode('UTF-8')))
+ return
+ repo_url = repo_url[:-len(trailing)]
+ repo_url = repo_url.strip('/')
+ request.notifyFinish().addBoth(self._finished)
+ self.cgit_config = tempfile.NamedTemporaryFile(
+ mode='w+', prefix='turnip-cgit-')
+ os.chmod(self.cgit_config.name, 0o644)
+ fmt = {'repo_url': repo_url, 'repo_path': repo_path}
+ print(textwrap.dedent("""\
+ # XXX clone-prefix
+ css=/static/cgit.css
+ enable-http-clone=0
+ enable-index-owner=0
+ logo=/static/cgit.png
+
+ repo.url={repo_url}
+ repo.path={repo_path}
+ """).format(**fmt), file=self.cgit_config)
+ self.cgit_config.flush()
+ env["CGIT_CONFIG"] = self.cgit_config.name
+ env["PATH_INFO"] = "/%s%s" % (repo_url, trailing)
+ env["SCRIPT_NAME"] = "/"
+ twcgi.CGIScript.runProcess(self, env, request, *args, **kwargs)
+
+ def _translatePathErrback(self, failure, request):
+ e = failure.value
+ if e.faultCode in (1, 290):
+ error_code = http.NOT_FOUND
+ elif e.faultCode in (2, 310):
+ error_code = http.FORBIDDEN
+ elif e.faultCode in (3, 410):
+ # XXX cjwatson 2015-03-30: should be UNAUTHORIZED, but we
+ # don't implement that yet
+ error_code = http.FORBIDDEN
+ else:
+ error_code = http.INTERNAL_SERVER_ERROR
+ self._error(request, e.faultString, code=error_code)
+
+ def runProcess(self, env, request, *args, **kwargs):
+ proxy = xmlrpc.Proxy(self.root.virtinfo_endpoint, allowNone=True)
+ # XXX cjwatson 2015-03-30: authentication
+ d = proxy.callRemote(
+ b'translatePath', request.path, b'read', None, False)
+ d.addErrback(self._translatePathErrback, request)
+ d.addCallback(
+ self._translatePathCallback, env, request, *args, **kwargs)
+ return server.NOT_DONE_YET
+
+
class SmartHTTPFrontendResource(resource.Resource):
"""HTTP resource to translate Git smart HTTP requests to pack protocol."""
allowed_services = frozenset((b'git-upload-pack', b'git-receive-pack'))
- def __init__(self, backend_host, backend_port, virtinfo_endpoint):
+ def __init__(self, backend_host, backend_port, virtinfo_endpoint,
+ repo_store, cgit_exec_path=None, cgit_data_path=None):
resource.Resource.__init__(self)
self.backend_host = backend_host
self.backend_port = backend_port
self.virtinfo_endpoint = virtinfo_endpoint
+ # XXX cjwatson 2015-03-30: Knowing about the store path here
+ # violates turnip's layering and may cause scaling problems later,
+ # but for now cgit needs direct filesystem access.
+ self.repo_store = repo_store
+ self.cgit_exec_path = cgit_exec_path
self.putChild('', SmartHTTPRootResource())
+ if cgit_data_path is not None:
+ static_resource = DirectoryWithoutListings(
+ cgit_data_path, defaultType='text/plain')
+ self.putChild('static', static_resource)
+ self.putChild(
+ 'favicon.ico',
+ static.File(os.path.join(cgit_data_path, 'favicon.ico')))
+
+ @staticmethod
+ def _isGitRequest(request):
+ content_type = request.getHeader(b'Content-Type')
+ if content_type is None:
+ return False
+ return content_type.startswith(b'application/x-git-')
def getChild(self, path, request):
- if request.path.endswith(b'/info/refs'):
- # /PATH/TO/REPO/info/refs
- return SmartHTTPRefsResource(
- self, request.path[:-len(b'/info/refs')])
- try:
- # /PATH/TO/REPO/SERVICE
- path, service = request.path.rsplit(b'/', 1)
- except ValueError:
- path = request.path
- service = None
- if service in self.allowed_services:
- return SmartHTTPCommandResource(self, service, path)
-
+ if self._isGitRequest(request):
+ if request.path.endswith(b'/info/refs'):
+ # /PATH/TO/REPO/info/refs
+ return SmartHTTPRefsResource(
+ self, request.path[:-len(b'/info/refs')])
+ try:
+ # /PATH/TO/REPO/SERVICE
+ path, service = request.path.rsplit(b'/', 1)
+ except ValueError:
+ path = request.path
+ service = None
+ if service in self.allowed_services:
+ return SmartHTTPCommandResource(self, service, path)
+ elif self.cgit_exec_path is not None:
+ return CGitScriptResource(self)
return resource.NoResource(b'No such resource')
=== modified file 'turnip/pack/tests/test_functional.py'
--- turnip/pack/tests/test_functional.py 2015-03-30 09:19:41 +0000
+++ turnip/pack/tests/test_functional.py 2015-03-30 16:57:54 +0000
@@ -345,7 +345,7 @@
# virtinfo servers started by the mixin.
frontend_site = server.Site(
SmartHTTPFrontendResource(
- b'localhost', self.virt_port, self.virtinfo_url))
+ b'localhost', self.virt_port, self.virtinfo_url, self.root))
self.frontend_listener = reactor.listenTCP(0, frontend_site)
self.port = self.frontend_listener.getHost().port
=== modified file 'turnipserver.py'
--- turnipserver.py 2015-03-30 09:19:41 +0000
+++ turnipserver.py 2015-03-30 16:57:54 +0000
@@ -31,6 +31,8 @@
PACK_BACKEND_PORT = config.get('pack_backend_port')
REPO_STORE = config.get('repo_store')
VIRTINFO_ENDPOINT = config.get('virtinfo_endpoint')
+CGIT_EXEC_PATH = config.get('cgit_exec_path')
+CGIT_DATA_PATH = config.get('cgit_data_path')
# turnipserver.py is preserved for convenience in development, services
# in production are run in separate processes.
@@ -54,7 +56,9 @@
PackFrontendFactory('localhost',
PACK_VIRT_PORT))
smarthttp_site = server.Site(
- SmartHTTPFrontendResource(b'localhost', PACK_VIRT_PORT, VIRTINFO_ENDPOINT))
+ SmartHTTPFrontendResource(
+ b'localhost', PACK_VIRT_PORT, VIRTINFO_ENDPOINT, REPO_STORE,
+ cgit_exec_path=CGIT_EXEC_PATH, cgit_data_path=CGIT_DATA_PATH))
reactor.listenTCP(config.get('smart_http_port'), smarthttp_site)
smartssh_service = SmartSSHService(
b'localhost', PACK_VIRT_PORT, config.get('authentication_endpoint'),
Follow ups