~edwargix/git.sr.ht

2e5d1e3817968fd59ce6a225ed66ca0900774761 — Drew DeVault 6 years ago 1048906
Improve performance of git push
5 files changed, 156 insertions(+), 79 deletions(-)

M gitsrht-keys
M gitsrht-shell
M gitsrht/app.py
A gitsrht/blueprints/internal.py
M gitsrht/service.py
M gitsrht-keys => gitsrht-keys +42 -26
@@ 1,48 1,64 @@
#!/usr/bin/env python3
import json
import os
import sys
import requests
from scmsrht.redis import redis
from srht.api import get_results
from srht.config import cfg, get_origin
from srht.database import DbSession
db = DbSession(cfg("git.sr.ht", "connection-string"))
from gitsrht.types import User, SSHKey
db.init()
from uuid import uuid4

sys.stderr.write(str(sys.argv) + "\n")
key_type = sys.argv[3]
b64key = sys.argv[4]

user = (User.query.join(SSHKey)
        .filter(SSHKey.key.ilike(f"%{b64key}%"))).one_or_none()
if not user:
    from gitsrht.service import oauth_service
    # Fall back to meta.sr.ht first
    meta_origin = get_origin("meta.sr.ht")
    r = requests.get(f"{meta_origin}/api/ssh-key/{b64key}")
    if r.status_code == 200:
        username = r.json()["owner"]["name"]
        user = User.query.filter(User.username == username).one_or_none()
        try:
            # Attempt to pull down keys for next time
            keys_url = f"{meta_origin}/api/user/ssh-keys"
            for key in get_results(keys_url, user.oauth_token):
                oauth_service.ensure_user_sshkey(user, key)
            db.session.commit()
        except:
            pass
if not user:
user_id = username = None

cache = redis.get(f"git.sr.ht.ssh-keys.{b64key}")
if cache:
    cache = json.loads(cache.decode())
    user_id = cache["user_id"]
    username = cache["username"]
else:
    from srht.database import DbSession
    db = DbSession(cfg("git.sr.ht", "connection-string"))
    from gitsrht.types import User, SSHKey
    db.init()

    user = (User.query.join(SSHKey)
            .filter(SSHKey.key.ilike(f"%{b64key}%"))).one_or_none()
    if not user:
        from gitsrht.service import oauth_service
        # Fall back to meta.sr.ht first
        meta_origin = get_origin("meta.sr.ht")
        r = requests.get(f"{meta_origin}/api/ssh-key/{b64key}")
        if r.status_code == 200:
            username = r.json()["owner"]["name"]
            user = User.query.filter(User.username == username).one_or_none()
            try:
                # Attempt to pull down keys for next time
                keys_url = f"{meta_origin}/api/user/ssh-keys"
                for key in get_results(keys_url, user.oauth_token):
                    oauth_service.ensure_user_sshkey(user, key)
                db.session.commit()
            except:
                pass
    if user:
        user_id = user.id
        username = user.username

if not user_id:
    sys.stderr.write("Unknown public key")
    sys.exit(0)

default_shell = os.path.join(os.path.dirname(sys.argv[0]), "gitsrht-shell")
shell = cfg("git.sr.ht", "shell", default=default_shell)
keys = ("command=\"{} '{}' '{}'\",".format(shell, user.id, b64key) +
keys = ("command=\"{} '{}' '{}' '{}'\",".format(
        shell, user_id, username, b64key) +
    "no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty," +
    "environment=\"SRHT_UID={}\",environment=\"SRHT_PUSH={}\"".format(
        user.id, str(uuid4())) +
    " {} {} {}".format(key_type, b64key, user.username) + "\n")
        user_id, str(uuid4())) +
    " {} {} {}".format(key_type, b64key, username) + "\n")
print(keys)
sys.stderr.write(keys)
sys.exit(0)

M gitsrht-shell => gitsrht-shell +36 -52
@@ 8,23 8,19 @@ try:
except Exception as ex:
    sys.stderr.write("Unable to open log for writing\n")
    sys.stderr.write(str(ex) + "\n")
import json
import requests
import shlex
from datetime import datetime
from srht.config import cfg
from srht.config import cfg, get_origin
from srht.crypto import sign_payload
from srht.validation import Validation
from srht.database import DbSession
db = DbSession(cfg("git.sr.ht", "connection-string"))
from scmsrht.access import has_access, UserAccess
from gitsrht.types import User, Repository, RepoVisibility, Redirect
from gitsrht.repos import GitRepoApi
db.init()

def log(s, *args):
    sys.stderr.write("{} {}\n".format(datetime.now().isoformat(),
        s.format(*args) if isinstance(s, str) else str(s)))

origin = cfg("git.sr.ht", "origin")
origin = get_origin("git.sr.ht")
repos = cfg("git.sr.ht", "repos")

_cmd = os.environ.get("SSH_ORIGINAL_COMMAND")


@@ 33,20 29,17 @@ if not _cmd:
if len(sys.argv) < 2:
    log("Error: expected 2 arguments from SSH")
    sys.exit(1)
user_id = sys.argv[1]
ssh_key = sys.argv[2]
user_id = int(sys.argv[1])
username = sys.argv[2]
ssh_key = sys.argv[3]

user = User.query.filter(User.id == user_id).first()
if not user:
    log("Unknown user ID {}", user_id)
    sys.exit(1)
log("User: {}", user.username)
log("User: {}", username)

cmd = shlex.split(_cmd)
valid_commands = ["git-receive-pack", "git-upload-pack", "git-upload-archive"]
if len(cmd) < 1 or not cmd[0] in valid_commands:
    log("Not permitting unacceptable command")
    print("Hi {}! You've successfully authenticated, ".format(user.username) +
    print("Hi {}! You've successfully authenticated, ".format(username) +
        "but I do not provide an interactive shell. Bye!")
    sys.exit(128)
os.chdir(repos)


@@ 55,40 48,31 @@ if not path.startswith(repos):
    path = os.path.join(repos, path)
cmd[-1] = path

repo = Repository.query.filter(Repository.path == path).first()
if not repo:
    repo = Redirect.query.filter(Redirect.path == path).first()
    if repo:
        repo = repo.new_repo
        sys.stderr.write("\n\t\033[93mNOTICE\033[0m\n")
        sys.stderr.write("\tThis repository has moved:\n")
        # TODO: orgs
        sys.stderr.write("\t{}/~{}/{}\n".format(
            origin, repo.owner.username, repo.name))
        sys.stderr.write("\tPlease update your remote.\n\n")
        sys.exit(128)

    _path, repo_name = os.path.split(path)
    owner = os.path.basename(_path)
    if "~" + user.username != owner:
        sys.exit(128)

    valid = Validation({ "name": repo_name })
    repo_api = GitRepoApi()
    repo = repo_api.create_repo(valid, user)
    if not valid.ok:
        sys.exit(128)
    repo.visibility = RepoVisibility.autocreated
    db.session.commit()

import gitsrht.types as tables
if cmd[0] == "git-receive-pack":
    if not has_access(repo, UserAccess.write, user, tables=tables):
        sys.exit(128)
# Delegate to web application for validation
payload = {
    "path": path,
    "user_id": user_id,
    # 2 is write, 1 is read
    "access": 2 if cmd[0] == "git-receive-pack" else 1,
}
payload = json.dumps(payload)
headers = {
    "Content-Type": "application/json",
}
headers.update(sign_payload(payload))
r = requests.post(f"{origin}/internal/push-check",
        data=payload, headers=headers)
sys.stderr.write(r.text + "\n")
response = r.json()
if r.status_code == 302:
    print("\n\t\033[93mNOTICE\033[0m\n")
    print("\tThis repository has moved:\n")
    print(f"\t{response['redirect']}\n")
    print("\tPlease update your remote.\n\n")
    sys.exit(128)
elif r.status_code == 200:
    log("Executing {}", " ".join(cmd))
    sys.stderr.close()
    os.execvp(cmd[0], cmd)
else:
    if not has_access(repo, UserAccess.read, user, tables=tables):
        sys.exit(128)

log("Executing {}", " ".join(cmd))
sys.stderr.close()
os.execvp(cmd[0], cmd)
    sys.exit(128)

M gitsrht/app.py => gitsrht/app.py +2 -0
@@ 23,10 23,12 @@ class GitApp(ScmSrhtFlask):
                repo_api=GitRepoApi(), oauth_service=oauth_service)

        from gitsrht.blueprints.api import data
        from gitsrht.blueprints.internal import internal
        from gitsrht.blueprints.repo import repo
        from gitsrht.blueprints.stats import stats

        self.register_blueprint(data)
        self.register_blueprint(internal)
        self.register_blueprint(repo)
        self.register_blueprint(stats)
        self.register_blueprint(webhooks_notify)

A gitsrht/blueprints/internal.py => gitsrht/blueprints/internal.py +62 -0
@@ 0,0 1,62 @@
"""
This blueprint is used internally by gitsrht-shell to speed up git pushes, by
taking advantage of the database connection already established by the web app.
"""

import base64
from flask import Blueprint, request
from srht.config import get_origin
from scmsrht.access import has_access, UserAccess
from scmsrht.urls import get_clone_urls
from gitsrht.repos import GitRepoApi
from gitsrht.types import User, Repository, RepoVisibility, Redirect
from srht.crypto import verify_request_signature
from srht.database import db
from srht.flask import csrf_bypass
from srht.validation import Validation

internal = Blueprint("internal", __name__)

@csrf_bypass
@internal.route("/internal/push-check", methods=["POST"])
def push_check():
    verify_request_signature(request)
    valid = Validation(request)
    path = valid.require("path")
    user_id = valid.require("user_id", cls=int)
    access = valid.require("access", cls=int)
    if not valid.ok:
        return valid.response
    access = UserAccess(access)
    user = User.query.filter(User.id == user_id).one()

    repo = Repository.query.filter(Repository.path == path).first()
    if not repo:
        redir = Redirect.query.filter(Redirect.path == path).first()
        if redir:
            origin = get_origin("git.sr.ht", external=True)
            repo = redir.new_repo
            # TODO: orgs
            return {
                "redirect": 'git@{origin}:{repo.owner.username}/{repo.name}'
            }, 302

        # Autocreate this repo
        _path, repo_name = os.path.split(path)
        owner = os.path.basename(_path)
        if "~" + user.username != owner:
            return { }, 401

        valid = Validation({ "name": repo_name })
        repo_api = GitRepoApi()
        repo = repo_api.create_repo(valid, user)
        if not valid.ok:
            sys.exit(128)
        repo.visibility = RepoVisibility.autocreated
        db.session.commit()
        return { }, 200

    if not has_access(repo, access, user):
        return { }, 401

    return { }, 200

M gitsrht/service.py => gitsrht/service.py +14 -1
@@ 1,5 1,6 @@
from flask import Blueprint, request, url_for
from gitsrht.types import User, OAuthToken, SSHKey
from scmsrht.redis import redis
from scmsrht.service import scm_scopes
from srht.api import get_results
from srht.database import db


@@ 41,6 42,15 @@ class GitOAuthService(AbstractOAuthService):
        key.key = meta_key["key"]
        key.fingerprint = meta_key["fingerprint"]
        db.session.add(key)
        b64key = meta_key["key"].split(" ")
        if len(b64key) > 3:
            return True
        b64key = b64key[1]
        cache = {
            "user_id": user.id,
            "username": user.username,
        }
        redis.set(f"git.sr.ht.ssh-keys.{b64key}", json.dumps(cache))
        return True

    def ensure_meta_webhooks(self, user, webhooks):


@@ 68,7 78,6 @@ webhooks_notify = Blueprint("webhooks.notify", __name__)
def notify_keys():
    payload = json.loads(request.data.decode('utf-8'))
    event = request.headers.get("X-Webhook-Event")
    # TODO: Regenerate authorized_keys
    if event == "ssh-key:add":
        user = User.query.filter(
                User.username == payload["owner"]["name"]).one_or_none()


@@ 78,6 87,10 @@ def notify_keys():
    elif event == "ssh-key:remove":
        key = SSHKey.query.filter(
                SSHKey.meta_id == payload["id"]).one_or_none()
        b64key = key.key.split(" ")
        if len(b64key) >= 2:
            b64key = b64key[1]
            redis.delete(f"git.sr.ht.ssh-keys.{b64key}")
        if key:
            db.session.delete(key)
            db.session.commit()