-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
boke0
committed
Feb 9, 2021
0 parents
commit 19f6b4d
Showing
14 changed files
with
577 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
__pycache__/ | ||
node_modules/ | ||
.cache | ||
*.egg-info/ | ||
*.egg | ||
.eggs/ | ||
.tmp/ | ||
.installed.cfg | ||
db.sqlite3 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
from mitama.app import Builder | ||
|
||
from .main import App | ||
|
||
|
||
class AppBuilder(Builder): | ||
app = App |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,119 @@ | ||
from mitama.app import Controller | ||
from mitama.app.http import Response | ||
from mitama.models import User, Group | ||
from .model import Repo | ||
from . import gitHttpBackend | ||
|
||
import git | ||
import os | ||
import shutil | ||
|
||
|
||
class RepoController(Controller): | ||
def handle(self, request): | ||
template = self.view.get_template("repo/list.html") | ||
repos = Repo.list() | ||
return Response.render(template, { | ||
'repos': repos | ||
}) | ||
def create(self, request): | ||
template = self.view.get_template("repo/create.html") | ||
nodes = [ | ||
*Group.list(), | ||
*User.list() | ||
] | ||
try: | ||
if request.method == 'POST': | ||
body = request.post() | ||
repo = Repo() | ||
repo.name = body['name'] | ||
repo.owner = body['owner'] | ||
repo.create() | ||
git.Repo.init( | ||
self.app.project_dir / ('repos/' + repo.name + '.git'), | ||
bare = True | ||
) | ||
return Response.redirect(self.app.convert_url('/'+repo.name)) | ||
except Exception as err: | ||
error = str(err) | ||
print(error) | ||
return Response.render(template, { | ||
'post': body, | ||
'error': error, | ||
'nodes': nodes | ||
}) | ||
return Response.render(template, { | ||
'post': dict(), | ||
'nodes': nodes | ||
}) | ||
def update(self, request): | ||
template = self.view.get_template("repo/update.html") | ||
repo = Repo.retrieve(name = request.params['repo']) | ||
try: | ||
if request.method == 'POST': | ||
body = request.post() | ||
name = repo.name | ||
repo.name = body['name'] | ||
repo.owner = body['owner'] | ||
repo.update() | ||
os.rename( | ||
self.app.project_dir / ('repos/' + name + '.git'), | ||
self.app.project_dir / ('repos/' + repo.name + '.git') | ||
) | ||
except Exception as err: | ||
error = str(err) | ||
return Response.render(template, { | ||
'repo': repo, | ||
'error': error | ||
}) | ||
return Response.render(template, { | ||
'repo': repo | ||
}) | ||
def delete(self, request): | ||
template = self.view.get_template("repo/delete.html") | ||
repo = Repo.retrieve(name = request.params['repo']) | ||
try: | ||
if request.method == 'POST': | ||
if not request.user.password_check(request.post()['password']): | ||
raise AuthorizationError('wrong password') | ||
shutil.rmtree(self.app.project_dir / ('repos/' + repo.name + '.git')) | ||
repo.delete() | ||
return Response.redirect(self.app.convert_url('/')) | ||
except Exception as err: | ||
error = str(err) | ||
return Response.render(template, { | ||
'repo': repo, | ||
'error': error | ||
}) | ||
return Response.render(template, { | ||
'repo': repo | ||
}) | ||
def retrieve(self, request): | ||
template = self.view.get_template("repo/retrieve.html") | ||
repo = Repo.retrieve(name = request.params['repo']) | ||
return Response.render(template, { | ||
'repo': repo | ||
}) | ||
|
||
class ProxyController(Controller): | ||
def handle(self, request): | ||
repo = Repo.retrieve(name = request.params['repo'][:-4]) | ||
if repo.owner._id != request.user._id and (isinstance(repo.owner, Group) and not repo.owner.is_in(request.user)): | ||
return Response(status=401, reason='Unauthorized', text='You are not the owner of the repository.') | ||
environ = dict(request.environ) | ||
environ['REQUEST_METHOD'] = request.method | ||
environ['PATH_INFO'] = self.app.revert_url(environ['PATH_INFO']) | ||
( | ||
status, | ||
reason, | ||
headers, | ||
body | ||
) = gitHttpBackend.wsgi_to_git_http_backend(environ, self.app.project_dir / 'repos') | ||
content_type = headers['Content-Type'] | ||
return Response( | ||
body = body, | ||
status = status, | ||
reason = reason, | ||
headers = headers, | ||
content_type = content_type | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,224 @@ | ||
"""Utility functions to invoke git-http-backend | ||
""" | ||
|
||
import subprocess | ||
import threading | ||
|
||
|
||
DEFAULT_CHUNK_SIZE = 0x8000 | ||
DEFAULT_MAX_HEADER_SIZE = 0X20000 # No header should ever be this large. | ||
# TODO: expose these sizes to API | ||
CRLF = b'\r\n' | ||
HEADER_END = CRLF * 2 | ||
|
||
|
||
def wsgi_to_git_http_backend(wsgi_environ, | ||
git_project_root, | ||
user=None): | ||
"""Convenience wrapper for how a WSGI application can use this | ||
module to handle a request. | ||
See build_cgi_environ regarding git_project_root and user. | ||
See run_git_http_backend for requirements for wsgi.input | ||
and wsgi.errors.""" | ||
cgi_environ = build_cgi_environ(wsgi_environ, git_project_root, user) | ||
input_stream = wsgi_environ['wsgi.input'] | ||
error_stream = wsgi_environ['wsgi.errors'] | ||
cgi_header, response_body_generator = run_git_http_backend( | ||
cgi_environ, input_stream, error_stream | ||
) | ||
status_line, list_of_headers = parse_cgi_header(cgi_header) | ||
status, reason = status_line.split(' ') | ||
return status, reason, list_of_headers, response_body_generator | ||
|
||
|
||
def run_git_http_backend(cgi_environ, input_stream, error_stream): | ||
"""Execute "git http-backend" as a CGI script, using the supplied | ||
environment and the file-like object input_stream. | ||
See build_cgi_environ() and git documentation for the requirements | ||
for cgi_environ . | ||
input_stream can be any object implementing the file protocol. Note | ||
that input_stream will be closed here. | ||
Any stderr generated by the git process will be piped to error_stream, | ||
which must have a file descriptor. | ||
Return (cgi_header, response_body_generator). The cgi_header is the | ||
string of raw headers returned by git ending with just one CRLF. The | ||
response sent back to the client will need an additional blank line | ||
separating this from the response body. | ||
Raise EnvironmentError (errno 1) if a CGI/HTTP header is not returned | ||
from git http-backend.""" | ||
input_length = int(cgi_environ.get('CONTENT_LENGTH', '') or 0) | ||
proc = subprocess.Popen( | ||
['git', 'http-backend'], | ||
bufsize=DEFAULT_CHUNK_SIZE, | ||
stdin=subprocess.PIPE, | ||
stdout=subprocess.PIPE, | ||
stderr=error_stream, | ||
env=cgi_environ | ||
) | ||
cgi_header, response_body_generator = _communicate_with_git( | ||
proc, input_stream, input_length | ||
) | ||
return cgi_header, response_body_generator | ||
|
||
|
||
def build_cgi_environ(wsgi_environ, git_project_root, user=None): | ||
"""Build a CGI environ from a WSGI environment: | ||
CONTENT_TYPE | ||
GIT_PROJECT_ROOT = directory containing bare repos | ||
PATH_INFO (if GIT_PROJECT_ROOT is set, otherwise PATH_TRANSLATED) | ||
QUERY_STRING | ||
REMOTE_USER | ||
REMOTE_ADDR | ||
REQUEST_METHOD | ||
The git_project_root parameter must point to a directory that contains | ||
the git bare repo designated by PATH_INFO. See the git documentation. | ||
The git repo (my-repo.git) is located at GIT_PROJECT_ROOT + PATH_INFO | ||
(if GIT_PROJECT_ROOT is defined) or at PATH_TRANSLATED. | ||
If REMOTE_USER is set in wsgi_environ, you should normally leave user | ||
alone. | ||
""" | ||
cgi_environ_ = dict(wsgi_environ) | ||
cgi_environ = dict() | ||
for key, value in cgi_environ_.items(): # NOT iteritems, due to "del" | ||
if isinstance(value, str): | ||
cgi_environ[key] = value | ||
cgi_environ['GIT_HTTP_EXPORT_ALL'] = '1' | ||
cgi_environ['GIT_PROJECT_ROOT'] = git_project_root | ||
if user: | ||
cgi_environ['REMOTE_USER'] = user | ||
cgi_environ.setdefault('REMOTE_USER', 'unknown') | ||
return cgi_environ | ||
|
||
|
||
def parse_cgi_header(cgi_header): | ||
"""Given the raw header returned by the CGI, return | ||
(status_line, list_of_headers). This adapts the CGI header | ||
to WSGI conventions.""" | ||
header_dict = {} | ||
raw_lines = cgi_header.split(CRLF) | ||
assert raw_lines[-1] == b'' | ||
for raw_line in raw_lines[:-1]: | ||
name, padded_value = raw_line.strip().split(b':', 1) | ||
value = padded_value.strip() | ||
header_dict[name.decode()] = value.decode() | ||
status_line = header_dict.pop('Status', None) or '200 OK' | ||
#list_of_headers = [(name, header_dict[name]) for name in names] | ||
#return status_line, list_of_headers | ||
return status_line, header_dict | ||
|
||
|
||
def _communicate_with_git(proc, input_stream, input_length): | ||
# Given a subprocess.Popen object: | ||
# * Start writing request data | ||
# * Start reading stdout and possibly stderr | ||
# * Extract the cgi_header | ||
# * Construct a generator for everything that comes after the header | ||
# * Return (cgi_header, response_body_generator) | ||
# (The generator is responsible for extracting all data and cleaning up.) | ||
# Raise EnvironmentError (errno 1) if header is not returned from proc. | ||
threading.Thread(target=_input_data_pump, | ||
args=(proc, input_stream, input_length)).start() | ||
chunks = [b''] # Dummy str at start helps here. | ||
header_end = None | ||
while not header_end: | ||
total_bytes_read = sum(map(len, chunks)) | ||
if total_bytes_read > DEFAULT_MAX_HEADER_SIZE: | ||
raise EnvironmentError( | ||
1, | ||
'Read %d bytes from "git http-backend" without ' | ||
'finding header boundary.' % total_bytes_read, | ||
) # TODO: Test this. | ||
chuck_data = proc.stdout.read(DEFAULT_CHUNK_SIZE) | ||
if not chuck_data: | ||
raise EnvironmentError( | ||
1, | ||
'Did not find header boundary in response ' | ||
'from "git http-backend".', | ||
) # TODO: Test this. | ||
chunks.append(chuck_data) | ||
# Search the two most recent chunks for the end of the header. | ||
# header_end -> (header_end_on_boundary, index_within_chunk) or None | ||
header_end = _find_header_end_in_2_chunks(*chunks[-2:]) | ||
header_end_on_boundary, index_within_chunk = header_end | ||
cgi_header, remainder = _separate_header( | ||
chunks, header_end_on_boundary, index_within_chunk | ||
) | ||
response_body_generator = _response_body_generator(remainder, proc) | ||
return cgi_header, response_body_generator | ||
|
||
|
||
def _input_data_pump(proc, input_stream, input_length): | ||
# Thread for feeding input to git | ||
# TODO: Currently using threads due to lack of universal standard for | ||
# async event loops in web applications. | ||
bytes_read = 0 | ||
while bytes_read < input_length: | ||
bytes_to_read = min(DEFAULT_CHUNK_SIZE, input_length - bytes_read) | ||
current_data = input_stream.read(bytes_to_read) | ||
bytes_read += len(current_data) | ||
proc.stdin.write(current_data) | ||
proc.stdin.close() | ||
|
||
|
||
def _find_header_end_in_2_chunks(chunk0, chunk1): | ||
# Search for the 4-byte HEADER_END in either the end of the first chunk | ||
# (with the 4-byte boundary stretching into the second chunk) or within | ||
# the second chunk starting at 0. | ||
# Return as (header_end_on_boundary, index_within_chunk). | ||
# Return None if header end not found. | ||
boundary_string = chunk0[-3:] + chunk1[:3] | ||
header_end = _search_str_for_header_end(boundary_string) | ||
if header_end != -1: | ||
return True, len(chunk0) - 3 + header_end | ||
header_end = _search_str_for_header_end(chunk1) | ||
if header_end != -1: | ||
return False, header_end | ||
return None | ||
|
||
|
||
def _search_str_for_header_end(data_str): | ||
"""Return index of header end or -1.""" | ||
return data_str.find(HEADER_END) | ||
|
||
|
||
def _separate_header(chunks, header_end_on_boundary, index_within_chunk): | ||
# Return header, remainder | ||
if header_end_on_boundary: | ||
# Header ends within chunks[-2] | ||
header_chunks = chunks[:-2] | ||
last_header_chunk = chunks[-2] | ||
body_start_index = (4 - (len(last_header_chunk) - index_within_chunk)) | ||
else: | ||
# Header ends within chunks[-1] | ||
header_chunks = chunks[:-1] | ||
last_header_chunk = chunks[-1] | ||
body_start_index = index_within_chunk + 4 | ||
header_chunks.append(last_header_chunk[:index_within_chunk]) | ||
header_chunks.append(CRLF) # Line end might have been split. | ||
header = b''.join(header_chunks) | ||
remainder = chunks[-1][body_start_index:] | ||
return header, remainder | ||
|
||
|
||
def _response_body_generator(remainder, proc): | ||
# The generator returned up the stack to the WSGI application. | ||
# Yields chunks of data from the subprocess output. | ||
yield remainder | ||
current_data = proc.stdout.read(DEFAULT_CHUNK_SIZE) | ||
while current_data: | ||
yield current_data | ||
current_data = proc.stdout.read(DEFAULT_CHUNK_SIZE) | ||
# TODO: Do we need this? | ||
while proc.poll() is None: | ||
yield b'' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
from mitama.app import App, Router | ||
from mitama.utils.controllers import static_files | ||
from mitama.utils.middlewares import BasicMiddleware, SessionMiddleware | ||
from mitama.app.method import view | ||
|
||
from .controller import RepoController, ProxyController | ||
|
||
|
||
class App(App): | ||
# name = 'MyApp' | ||
# description = 'This is my App.' | ||
router = Router( | ||
[ | ||
Router([ | ||
view("/<repo:re:(.*)\.git><path:path>", ProxyController), | ||
], middlewares = [BasicMiddleware]), | ||
Router([ | ||
view("/", RepoController), | ||
view("/create", RepoController, 'create'), | ||
view("/<repo>", RepoController, 'retrieve'), | ||
view("/<repo>/update", RepoController, 'update'), | ||
view("/<repo>/delete", RepoController, 'delete'), | ||
view("/static/<path:path>", static_files()), | ||
], middlewares = [SessionMiddleware]) | ||
] | ||
) |
Oops, something went wrong.