Skip to content

Middleware for SCITT + ORAS #2

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file.
580 changes: 580 additions & 0 deletions src/gitatp/aiohttp_middleware/atproto_index.py

Large diffs are not rendered by default.

126 changes: 126 additions & 0 deletions src/gitatp/aiohttp_middleware/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import abc
import sys
import asyncio
import pathlib

import aiohttp.web

from ..util import git_subprocess

class AioHTTPGitHTTPBackend:
@abc.abstractmethod
async def on_startup(self, app):
pass

@abc.abstractmethod
async def on_cleanup(self, app):
pass

@abc.abstractmethod
async def pre_git_http_backend(self, request, namespace, repo_name, local_repo_path):
pass

@abc.abstractmethod
async def git_receive_pack(self, request, namespace, repo_name, local_repo_path, push_options):
pass

def make_middleware(self):
@aiohttp.web.middleware
async def middleware(request, handler):
nonlocal self
if (
request.path.endswith("/info/refs")
or request.path.endswith("git-upload-pack")
or request.path.endswith("git-receive-pack")
):
return await self.git_http_backend(request)
return await handler(request)
return middleware

async def git_http_backend(self, request):
for find_git_url_path_end in [
"/info/refs",
"/git-upload-pack",
"/git-receive-pack",
]:
if request.path.endswith(find_git_url_path_end):
git_path = request.path[
request.path.index(find_git_url_path_end):
]
path_components = request.path[
:request.path.index(find_git_url_path_end)
].split("/")
namespace = path_components[-2]
repo_name = path_components[-1]
break

if repo_name.endswith(".git"):
repo_name = repo_name[:-4]

local_repo_path = pathlib.Path(self.git_project_root, namespace, f"{repo_name}.git")

# Create repo if it should exist
await self.pre_git_http_backend(request, namespace, repo_name, local_repo_path)

# TODO Replace file with original after proc.wait()
await (
await asyncio.create_subprocess_exec(
"git", "config", "receive.advertisePushOptions", "true",
cwd=str(local_repo_path),
)
).wait()

path_info = f"{repo_name}.git{git_path}"
print(f"path_info: {namespace}/{path_info}")
env = {
"GIT_PROJECT_ROOT": str(local_repo_path.parent),
"GIT_HTTP_EXPORT_ALL": "1",
"PATH_INFO": f"/{path_info}",
"REMOTE_USER": request.remote or "",
"REMOTE_ADDR": request.transport.get_extra_info("peername")[0],
"REQUEST_METHOD": request.method,
"QUERY_STRING": request.query_string,
"CONTENT_TYPE": request.headers.get("Content-Type", ""),
}

# Copy relevant HTTP headers to environment variables
for header in ("Content-Type", "User-Agent", "Accept-Encoding", "Pragma"):
header_value = request.headers.get(header)
if header_value:
env["HTTP_" + header.upper().replace("-", "_")] = header_value

# Prepare the subprocess to run git http-backend
proc = await asyncio.create_subprocess_exec(
"git", "http-backend",
env=env,
stdin=asyncio.subprocess.PIPE,
stdout=asyncio.subprocess.PIPE,
stderr=sys.stderr, # Output stderr to the server's stderr
)

# Push options are parsed from git client upload pack
push_options = {}

# Create a StreamResponse to send data back to the client
response = aiohttp.web.StreamResponse()

# Run the read and write tasks concurrently
await asyncio.gather(
git_subprocess.write_to_git(proc.stdin, request, response, push_options),
git_subprocess.read_from_git(proc.stdout, request, response),
proc.wait(),
)

push_options = git_subprocess.PushOptions(**push_options)

# Handle push events (git-receive-pack)
if path_info.endswith("git-receive-pack"):
await self.git_receive_pack(
request,
namespace,
repo_name,
local_repo_path,
push_options,
)

return response
89 changes: 80 additions & 9 deletions src/gitatp/cli.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import asyncio

import argparse
import configparser
import textwrap
import pathlib
import subprocess

from .git_http_backend import *
from .aiohttp_middleware.atproto_index import (
AioHTTPGitHTTPBackendATProtoConfig,
AioHTTPGitHTTPBackendATProto,
)

import magic
import keyring
import snoop

def file_contents_bytes_to_markdown(file_path: str, content: bytes) -> str:
Expand Down Expand Up @@ -91,26 +96,92 @@ def render_content(namespace: str, repo_name: str, ref: str, path: str) -> HTMLR
)

# Step 2: Define an aiohttp app and adapt the FastAPI app
async def init_aiohttp_app():
aiohttp_app = web.Application()
async def init_aiohttp_app(middlewares):
aiohttp_app = web.Application(
middlewares=[
middleware.make_middleware()
for middleware in middlewares
],
)

# Create ASGIResource which handle rendering
asgi_resource = ASGIResource(fastapi_app)
# asgi_resource = ASGIResource(fastapi_app)

# Register routes
aiohttp_app.router.add_route("*", "/{namespace}/{repo}.git/{path:.*}", handle_git_backend_request)
# aiohttp_app.router.add_route("*", "/{namespace}/{repo}.git/{path:.*}", handle_git_backend_request)

# Register resource
aiohttp_app.router.register_resource(asgi_resource)
# aiohttp_app.router.register_resource(asgi_resource)

# Mount startup and shutdown events from aiohttp to ASGI app
asgi_resource.lifespan_mount(aiohttp_app)
# asgi_resource.lifespan_mount(aiohttp_app)

return aiohttp_app

def make_parser():
parser = argparse.ArgumentParser(prog='atproto-git', usage='%(prog)s [options]')
parser.add_argument('--repos-directory', required=True, dest="repos_directory", help='directory for local copies of git repos')

config = configparser.ConfigParser()
config.read(str(pathlib.Path("~", ".gitconfig").expanduser()))

try:
atproto_handle = config["user"]["atproto"]
except Exception as e:
raise Exception(f"You must run: $ git config --global user.atproto $USER.atproto-pds.fqdn.example.com") from e
try:
atproto_email = config["user"]["email"]
except Exception as e:
raise Exception(f"You must run: $ git config --global user.email [email protected]") from e

atproto_handle_username = atproto_handle.split(".")[0]
atproto_base_url = "https://" + ".".join(atproto_handle.split(".")[1:])
keyring_atproto_password = ".".join(["password", atproto_handle])

try:
atproto_password = keyring.get_password(
atproto_email,
keyring_atproto_password,
)
except Exception as e:
raise Exception(f"You must run: $ python -m keyring set {atproto_email} {keyring_atproto_password}") from e

parser.add_argument(
'--atproto-base-url',
dest="atproto_base_url",
default=atproto_base_url,
)
parser.add_argument(
'--atproto-handle',
dest="atproto_handle",
default=atproto_handle,
)
parser.add_argument(
'--atproto-password',
dest="atproto_password",
default=atproto_password,
)

return parser

def main() -> None:
loop = asyncio.get_event_loop()
aiohttp_app = loop.run_until_complete(init_aiohttp_app())

parser = make_parser()
args = parser.parse_args()

atproto_config = AioHTTPGitHTTPBackendATProtoConfig(**vars(args))
atproto_middleware = AioHTTPGitHTTPBackendATProto(atproto_config)

middlewares = [
atproto_middleware,
]

aiohttp_app = loop.run_until_complete(init_aiohttp_app(middlewares))

for middleware in middlewares:
aiohttp_app.on_startup.append(middleware.on_startup)
aiohttp_app.on_cleanup.append(middleware.on_cleanup)

# Start the server
web.run_app(aiohttp_app, host="0.0.0.0", port=8080)
Loading