Skip to content

Commit

Permalink
[init]
Browse files Browse the repository at this point in the history
  • Loading branch information
boke0 committed Feb 9, 2021
0 parents commit 19f6b4d
Show file tree
Hide file tree
Showing 14 changed files with 577 additions and 0 deletions.
9 changes: 9 additions & 0 deletions .gitignore
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
7 changes: 7 additions & 0 deletions izanami/__init__.py
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
119 changes: 119 additions & 0 deletions izanami/controller.py
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
)
224 changes: 224 additions & 0 deletions izanami/gitHttpBackend.py
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''
26 changes: 26 additions & 0 deletions izanami/main.py
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])
]
)
Loading

0 comments on commit 19f6b4d

Please sign in to comment.