pygrack.py
202 lines
| 7.0 KiB
| text/x-python
|
PythonLexer
r2382 | import os | |||
import socket | ||||
import logging | ||||
r2875 | import traceback | |||
r2382 | ||||
from webob import Request, Response, exc | ||||
r3376 | import rhodecode | |||
r3797 | from rhodecode.lib.vcs import subprocessio | |||
r2382 | ||||
log = logging.getLogger(__name__) | ||||
class FileWrapper(object): | ||||
def __init__(self, fd, content_length): | ||||
self.fd = fd | ||||
self.content_length = content_length | ||||
self.remain = content_length | ||||
def read(self, size): | ||||
if size <= self.remain: | ||||
try: | ||||
data = self.fd.read(size) | ||||
except socket.error: | ||||
raise IOError(self) | ||||
self.remain -= size | ||||
elif self.remain: | ||||
data = self.fd.read(self.remain) | ||||
self.remain = 0 | ||||
else: | ||||
data = None | ||||
return data | ||||
def __repr__(self): | ||||
return '<FileWrapper %s len: %s, read: %s>' % ( | ||||
self.fd, self.content_length, self.content_length - self.remain | ||||
) | ||||
class GitRepository(object): | ||||
git_folder_signature = set(['config', 'head', 'info', 'objects', 'refs']) | ||||
commands = ['git-upload-pack', 'git-receive-pack'] | ||||
r2726 | def __init__(self, repo_name, content_path, extras): | |||
r2382 | files = set([f.lower() for f in os.listdir(content_path)]) | |||
if not (self.git_folder_signature.intersection(files) | ||||
== self.git_folder_signature): | ||||
raise OSError('%s missing git signature' % content_path) | ||||
self.content_path = content_path | ||||
self.valid_accepts = ['application/x-%s-result' % | ||||
c for c in self.commands] | ||||
self.repo_name = repo_name | ||||
r2726 | self.extras = extras | |||
r2382 | ||||
def _get_fixedpath(self, path): | ||||
""" | ||||
Small fix for repo_path | ||||
:param path: | ||||
""" | ||||
return path.split(self.repo_name, 1)[-1].strip('/') | ||||
def inforefs(self, request, environ): | ||||
""" | ||||
WSGI Response producer for HTTP GET Git Smart | ||||
HTTP /info/refs request. | ||||
""" | ||||
r2726 | git_command = request.GET.get('service') | |||
r2382 | if git_command not in self.commands: | |||
log.debug('command %s not allowed' % git_command) | ||||
return exc.HTTPMethodNotAllowed() | ||||
# note to self: | ||||
# please, resist the urge to add '\n' to git capture and increment | ||||
# line count by 1. | ||||
# The code in Git client not only does NOT need '\n', but actually | ||||
# blows up if you sprinkle "flush" (0000) as "0001\n". | ||||
# It reads binary, per number of bytes specified. | ||||
# if you do add '\n' as part of data, count it. | ||||
r2579 | server_advert = '# service=%s' % git_command | |||
packet_len = str(hex(len(server_advert) + 4)[2:].rjust(4, '0')).lower() | ||||
r3376 | _git_path = rhodecode.CONFIG.get('git_path', 'git') | |||
r2382 | try: | |||
out = subprocessio.SubprocessIOChunker( | ||||
r3376 | r'%s %s --stateless-rpc --advertise-refs "%s"' % ( | |||
|
r4116 | _git_path, git_command[4:], self.content_path), | ||
r2382 | starting_values=[ | |||
r2579 | packet_len + server_advert + '0000' | |||
r2382 | ] | |||
) | ||||
except EnvironmentError, e: | ||||
r2875 | log.error(traceback.format_exc()) | |||
r2382 | raise exc.HTTPExpectationFailed() | |||
resp = Response() | ||||
resp.content_type = 'application/x-%s-advertisement' % str(git_command) | ||||
r2579 | resp.charset = None | |||
r2382 | resp.app_iter = out | |||
return resp | ||||
def backend(self, request, environ): | ||||
""" | ||||
WSGI Response producer for HTTP POST Git Smart HTTP requests. | ||||
Reads commands and data from HTTP POST's body. | ||||
returns an iterator obj with contents of git command's | ||||
response to stdout | ||||
""" | ||||
|
r4116 | _git_path = rhodecode.CONFIG.get('git_path', 'git') | ||
r2382 | git_command = self._get_fixedpath(request.path_info) | |||
if git_command not in self.commands: | ||||
log.debug('command %s not allowed' % git_command) | ||||
return exc.HTTPMethodNotAllowed() | ||||
if 'CONTENT_LENGTH' in environ: | ||||
inputstream = FileWrapper(environ['wsgi.input'], | ||||
request.content_length) | ||||
else: | ||||
inputstream = environ['wsgi.input'] | ||||
try: | ||||
r2402 | gitenv = os.environ | |||
# forget all configs | ||||
gitenv['GIT_CONFIG_NOGLOBAL'] = '1' | ||||
opts = dict( | ||||
r2406 | env=gitenv, | |||
|
r4116 | cwd=self.content_path, | ||
r2402 | ) | |||
|
r4116 | cmd = r'%s %s --stateless-rpc "%s"' % (_git_path, git_command[4:], | ||
self.content_path), | ||||
r2875 | log.debug('handling cmd %s' % cmd) | |||
r2382 | out = subprocessio.SubprocessIOChunker( | |||
r2875 | cmd, | |||
r2402 | inputstream=inputstream, | |||
**opts | ||||
) | ||||
r2382 | except EnvironmentError, e: | |||
r2875 | log.error(traceback.format_exc()) | |||
r2382 | raise exc.HTTPExpectationFailed() | |||
if git_command in [u'git-receive-pack']: | ||||
# updating refs manually after each push. | ||||
# Needed for pre-1.7.0.4 git clients using regular HTTP mode. | ||||
|
r4116 | from rhodecode.lib.vcs import get_repo | ||
from dulwich.server import update_server_info | ||||
repo = get_repo(self.content_path) | ||||
if repo: | ||||
update_server_info(repo._repo) | ||||
r2382 | ||||
resp = Response() | ||||
resp.content_type = 'application/x-%s-result' % git_command.encode('utf8') | ||||
r2581 | resp.charset = None | |||
r2382 | resp.app_iter = out | |||
return resp | ||||
def __call__(self, environ, start_response): | ||||
request = Request(environ) | ||||
_path = self._get_fixedpath(request.path_info) | ||||
if _path.startswith('info/refs'): | ||||
app = self.inforefs | ||||
elif [a for a in self.valid_accepts if a in request.accept]: | ||||
app = self.backend | ||||
try: | ||||
resp = app(request, environ) | ||||
except exc.HTTPException, e: | ||||
resp = e | ||||
r2875 | log.error(traceback.format_exc()) | |||
r2382 | except Exception, e: | |||
r2875 | log.error(traceback.format_exc()) | |||
r2382 | resp = exc.HTTPInternalServerError() | |||
return resp(environ, start_response) | ||||
class GitDirectory(object): | ||||
r2726 | def __init__(self, repo_root, repo_name, extras): | |||
r2382 | repo_location = os.path.join(repo_root, repo_name) | |||
if not os.path.isdir(repo_location): | ||||
raise OSError(repo_location) | ||||
self.content_path = repo_location | ||||
self.repo_name = repo_name | ||||
self.repo_location = repo_location | ||||
r2726 | self.extras = extras | |||
r2382 | ||||
def __call__(self, environ, start_response): | ||||
content_path = self.content_path | ||||
try: | ||||
r2726 | app = GitRepository(self.repo_name, content_path, self.extras) | |||
r2382 | except (AssertionError, OSError): | |||
r2764 | content_path = os.path.join(content_path, '.git') | |||
if os.path.isdir(content_path): | ||||
app = GitRepository(self.repo_name, content_path, self.extras) | ||||
r2382 | else: | |||
r2594 | return exc.HTTPNotFound()(environ, start_response) | |||
r2382 | return app(environ, start_response) | |||
r2726 | def make_wsgi_app(repo_name, repo_root, extras): | |||
|
r4116 | from dulwich.web import LimitedInputFilter, GunzipFilter | ||
app = GitDirectory(repo_root, repo_name, extras) | ||||
return GunzipFilter(LimitedInputFilter(app)) | ||||