git_http.py
Python script, ASCII text executable
1import uuid 2from models import * 3from app import app, db, bcrypt 4from misc_utils import * 5from common import git_command 6import os 7import shutil 8import config 9import flask 10import git 11import subprocess 12from flask_httpauth import HTTPBasicAuth 13import zlib 14import re 15import datetime 16 17auth = HTTPBasicAuth(realm=config.AUTH_REALM) 18 19auth_required = flask.Response("Unauthorized Access", 401, 20{"WWW-Authenticate": 'Basic realm="Login Required"'}) 21 22 23@auth.verify_password 24def verify_password(username, password): 25user = User.query.filter_by(username=username).first() 26 27if user and bcrypt.check_password_hash(user.password_hashed, password): 28flask.g.user = username 29return True 30 31return False 32 33 34def get_commit_identity(identity, pusher, repo): 35email = identity.rpartition("<")[2].rpartition(">")[0].strip() 36# If the email is not valid, attribute the commit to the pusher. 37if not email: 38return pusher 39email_users = User.query.filter_by(email=email).all() 40# If no user has the email, attribute the commit to the pusher. 41if not email_users: 42return pusher 43 44# If only one user has the email, attribute the commit to them. 45if len(email_users) == 1: 46return email_users[0] 47 48# If it's ambiguous, attribute the commit to an user with a higher permission level. 49for user in email_users: 50if repo.owner == user: 51return user 52 53for user in email_users: 54relationship = RepoAccess.query.filter_by(user=user, repo=repo).first() 55if relationship.permission_level == 2: 56return user 57 58for user in email_users: 59relationship = RepoAccess.query.filter_by(user=user, repo=repo).first() 60if relationship.permission_level == 1: 61return user 62 63# If no user has a higher permission level, attribute the commit to the pusher :( 64return pusher 65 66 67@app.route("/<username>/<repository>/git-upload-pack", methods=["POST"]) 68@app.route("/git/<username>/<repository>/git-upload-pack", methods=["POST"]) 69@auth.login_required(optional=True) 70def git_upload_pack(username, repository): 71if auth.current_user() is None and not get_visibility(username, repository): 72return auth_required 73if not (get_visibility(username, repository) or get_permission_level(flask.g.user, username, 74repository) is not None): 75flask.abort(403) 76 77server_repo_location = os.path.join(config.REPOS_PATH, username, repository, ".git") 78text = git_command(server_repo_location, flask.request.data, "upload-pack", 79"--stateless-rpc", ".") 80 81return flask.Response(text, content_type="application/x-git-upload-pack-result") 82 83 84@app.route("/<username>/<repository>/git-receive-pack", methods=["POST"]) 85@app.route("/git/<username>/<repository>/git-receive-pack", methods=["POST"]) 86@auth.login_required 87def git_receive_pack(username, repository): 88if not get_permission_level(flask.g.user, username, repository): 89flask.abort(403) 90 91server_repo_location = os.path.join(config.REPOS_PATH, username, repository, ".git") 92text = git_command(server_repo_location, flask.request.data, "receive-pack", 93"--stateless-rpc", ".") 94 95if flask.request.data == b"0000": 96return flask.Response("", content_type="application/x-git-receive-pack-result") 97 98push_info = flask.request.data.split(b"\x00")[0].decode() 99if not push_info: 100return flask.Response(text, content_type="application/x-git-receive-pack-result") 101 102old_sha, new_sha, ref = push_info[4:].split() # discard first 4 characters, used for line length which we don't need 103 104if old_sha == "0" * 40: 105commits_list = subprocess.check_output(["git", "rev-list", new_sha], 106cwd=server_repo_location).decode().strip().split("\n") 107else: 108commits_list = subprocess.check_output(["git", "rev-list", f"{old_sha}..{new_sha}"], 109cwd=server_repo_location).decode().strip().split("\n") 110 111for sha in reversed(commits_list): 112info = git_command(server_repo_location, None, "show", "-s", 113"--format='%H%n%at%n%cn <%ce>%n%B'", sha).decode() 114 115sha, time, identity, body = info.split("\n", 3) 116login = flask.g.user 117 118if not Commit.query.filter_by(identifier=f"/{username}/{repository}/{sha}").first(): 119logged_in_user = User.query.filter_by(username=login).first() 120user = get_commit_identity(identity, logged_in_user, db.session.get(Repo, f"/{username}/{repository}")) 121repo = Repo.query.filter_by(route=f"/{username}/{repository}").first() 122 123commit = Commit(sha, user, repo, time, body, identity) 124 125db.session.add(commit) 126db.session.commit() 127 128if ref.startswith("refs/heads/"): # if the push is to a branch 129ref = ref.rpartition("/")[2] # get the branch name only 130repo_data = db.session.get(Repo, f"/{username}/{repository}") 131if ref == repo_data.site_branch: 132# Update the site 133from celery_tasks import copy_site 134copy_site.delay(repo_data.route) 135 136return flask.Response(text, content_type="application/x-git-receive-pack-result") 137 138 139@app.route("/<username>/<repository>/info/refs", methods=["GET", "POST"]) 140@app.route("/git/<username>/<repository>/info/refs", methods=["GET", "POST"]) 141@auth.login_required(optional=True) 142def git_info_refs(username, repository): 143server_repo_location = os.path.join(config.REPOS_PATH, username, repository, ".git") 144 145repo = git.Repo(server_repo_location) 146repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 147if not repo_data.default_branch: 148if repo.heads: 149repo_data.default_branch = repo.heads[0].name 150repo.git.checkout("-f", repo_data.default_branch) 151 152if auth.current_user() is None and ( 153not get_visibility(username, repository) or flask.request.args.get( 154"service") == "git-receive-pack"): 155return auth_required 156try: 157if not (get_visibility(username, repository) or get_permission_level(flask.g.user, 158username, 159repository) is not None): 160flask.abort(403) 161except AttributeError: 162return auth_required 163 164service = flask.request.args.get("service") 165 166if service.startswith("git"): 167service = service[4:] 168else: 169flask.abort(403) 170 171if service == "receive-pack": 172try: 173if not get_permission_level(flask.g.user, username, repository): 174flask.abort(403) 175except AttributeError: 176return auth_required 177 178service_line = f"# service=git-{service}\n" 179service_line = (f"{len(service_line) + 4:04x}" + service_line).encode() 180 181if service == "upload-pack": 182text = service_line + b"0000" + git_command(server_repo_location, None, "upload-pack", 183"--stateless-rpc", 184"--advertise-refs", 185"--http-backend-info-refs", ".") 186elif service == "receive-pack": 187refs = git_command(server_repo_location, None, "receive-pack", 188"--http-backend-info-refs", ".") 189text = service_line + b"0000" + refs 190else: 191flask.abort(403) 192 193response = flask.Response(text, content_type=f"application/x-git-{service}-advertisement") 194response.headers["Cache-Control"] = "no-cache" 195 196return response 197