roundabout,
created on Friday, 3 November 2023, 14:17:52 (1699021072),
received on Wednesday, 31 July 2024, 06:54:38 (1722408878)
Author identity: vlad <vlad.muntoiu@gmail.com>
748a0e9dfde00a93d3ae0f24caa4c368b75987bc
config.py
@@ -1,8 +1,16 @@
import os from dotenv import load_dotenv load_dotenv("secrets.env")DB_PASSWORD = os.environ.get("DB_PASSWORD") REPOS_PATH = "./repos" BASE_DOMAIN = "localhost" HASHING_ROUNDS = 16 RESERVED_NAMES = ("git", "settings", "logout", "accounts",) locking = FalsefolderIcon = "mdi:folder" unknownIcon = "mdi:file"
@@ -102,4 +110,3 @@ def matchIcon(name):
return "mdi:book-information-variant" if name.startswith(".gitignore"): return "simple-icons:git"
gitHTTP.py
@@ -1,31 +1,134 @@
from gitme import appimport uuid from gitme import app, User, db, bcryptimport os import shutilimport config import flask import git import subprocess from flask_httpauth import HTTPBasicAuth auth = HTTPBasicAuth() @auth.verify_password def verifyPassword(username, password): user = User.query.filter_by(username=username).first() if user and bcrypt.check_password_hash(user.passwordHashed, password): flask.session["username"] = user.username flask.g.user = username return True return False @app.route("/git/<username>/<repository>/<path:subpath>", methods=["PROPFIND"]) def gitList(username, repository, subpath): serverRepoLocation = os.path.join(config.REPOS_PATH, username, repository, ".git", subpath) if not os.path.exists(serverRepoLocation): flask.abort(404) text = """<?xml version="1.0" encoding="utf-8" ?> <D:multistatus xmlns:D="DAV:"> """ for file in os.listdir(serverRepoLocation): text += f""" <D:response> <D:href>/git/{username}/{repository}/</D:href> """ if os.path.isdir(file): text += """ <D:propstat> <D:prop> <D:resourcetype> <D:collection/> </D:resourcetype> </D:prop> <D:status>HTTP/1.1 200 OK</D:status> </D:propstat> """ else: text += """ <D:propstat> <D:prop/> <D:status>HTTP/1.1 200 OK</D:status> </D:propstat> """ text += """ </D:response>""" text += """ </D:multistatus>""" return text @app.route("/git/<username>/<repository>/", methods=["GET", "POST"]) def gitRoot(username, repository): serverRepoLocation = os.path.join(config.REPOS_PATH, os.path.join(username, repository)) app.logger.info(f"Loading {serverRepoLocation}") if not os.path.exists(serverRepoLocation): app.logger.error(f"Cannot load {serverRepoLocation}") flask.abort(404) repo = git.Repo(serverRepoLocation)@app.route("/git/<username>/<repository>/<path:git_path>", methods=["GET", "POST"]) def git_http_backend(username, repository, git_path):repo_path = os.path.join(config.REPOS_PATH, username, repository, ".git")repo = git.Repo(repo_path)git_cmd = repo.git@auth.login_required def gitInfo(username, repository, git_path=""): serverRepoLocation = os.path.join(config.REPOS_PATH, os.path.join(username, repository)) app.logger.info(f"Loading {serverRepoLocation}") if not os.path.exists(serverRepoLocation): app.logger.error(f"Cannot load {serverRepoLocation}") flask.abort(404) repo = git.Repo(serverRepoLocation)if flask.request.method == "GET": result = git_cmd.upload_pack()method = "upload-pack" result = repo.git.upload_pack()elif flask.request.method == "POST": result = git_cmd.receive_pack()method = "receive-pack" if flask.request.headers.get("Content-Type") == "application/x-git-receive-pack-request": result = repo.git.receive_pack() else: result = "Not supported"response = flask.Response(result, content_type="application/x-git-" + git_cmd[-1])response = flask.Response(result, content_type=f"application/x-git-{method}-result")return response @app.route("/git/<username>/<repository>/<path:git_path>", methods=["MKCOL"]) def gitDummyMkCol(username, repository, git_path): return "", 200 @app.route("/git/<username>/<repository>/info/", methods=["MKCOL"]) def gitMakeInfo(username, repository): serverRepoLocation = os.path.join(config.REPOS_PATH, os.path.join(username, repository)) app.logger.info(f"Loading {serverRepoLocation}") if not os.path.exists(serverRepoLocation): app.logger.error(f"Cannot load {serverRepoLocation}") flask.abort(404) if not os.path.exists(os.path.join(serverRepoLocation, ".git", "info")): os.makedirs(os.path.join(serverRepoLocation, ".git", "info")) return "", 200 @app.route("/git/<username>/<repository>/info/refs") def gitInfoRefs(username, repository): serverRepoLocation = os.path.join(config.REPOS_PATH, os.path.join(username, repository)) app.logger.info(f"Loading {serverRepoLocation}") if not os.path.exists(serverRepoLocation):
@@ -39,27 +142,26 @@ def gitInfoRefs(username, repository):
app.logger.warning(text) return flask.Response(text, content_type="application/x-git-upload-pack-result")return flask.Response(text, content_type=f"application/x-{flask.request.args.get('service')}-result")@app.route("/git/<username>/<repository>/objects/info/alternates", methods=["GET"]) @auth.login_requireddef gitInfoAlternates(username, repository): # Handle the request and provide the responsetext = "#Alternate object stores\n" return flask.Response(text, content_type="text/plain") @app.route("/git/<username>/<repository>/objects/info/http-alternates") @auth.login_requireddef gitHttpAlternates(username, repository): # Return an empty responsereturn flask.Response("", content_type="text/plain") @app.route("/git/<username>/<repository>/HEAD") @auth.login_requireddef gitHead(username, repository): serverRepoLocation = os.path.join(config.REPOS_PATH, os.path.join(username, repository)) app.logger.info(f"Loading {serverRepoLocation}") if not os.path.exists(serverRepoLocation):
@@ -67,62 +169,173 @@ def gitHead(username, repository):
flask.abort(404) repo = git.Repo(serverRepoLocation) text = f"ref: {repo.head.ref}\n" return flask.Response(text, content_type="text/plain") @app.route("/git/<username>/<repository>/objects/info/packs") def gitInfoPacks(username, repository): repo_path = os.path.join(config.REPOS_PATH, username, repository)serverRepoLocation = os.path.join(config.REPOS_PATH, username, repository, ".git") packs = [f for f in os.listdir(os.path.join(serverRepoLocation, "objects", "pack")) if f.endswith(".pack")] text = "\n".join(packs) + "\n" return flask.Response(text, content_type="text/plain")# List the pack files in the repositorypacks = [f for f in os.listdir(os.path.join(repo_path, "objects", "pack")) if f.endswith(".pack")]@app.route("/git/<username>/<repository>/objects/<path:objectPath>") @auth.login_required def gitObject(username, repository, objectPath): serverRepoLocation = os.path.join(config.REPOS_PATH, os.path.join(username, repository)) objectFullPath = os.path.join(serverRepoLocation, ".git/objects", objectPath)# Generate a response with the list of pack filesresponse_text = "\n".join(packs) + "\n"if not os.path.exists(objectFullPath): return flask.Response("Object not found", status=404, content_type="text/plain")return flask.Response(response_text, content_type="text/plain")with open(objectFullPath, "rb") as f: objectData = f.read()return flask.Response(objectData, content_type="application/octet-stream") @app.route("/git/<username>/<repository>/objects/<path:object_path>")def gitObject(username, repository, object_path):repo_path = os.path.join(config.REPOS_PATH, username, repository)object_full_path = os.path.join(repo_path, ".git/objects", object_path)# Check if the object file existsif not os.path.exists(object_full_path):return flask.Response("Object not found", status=404, content_type="text/plain")@app.route("/git/<username>/<repository>/<path:itemPath>", methods=["PUT"]) # @auth.login_required def gitPut(username, repository, itemPath): serverRepoLocation = os.path.join(config.REPOS_PATH, os.path.join(username, repository)) itemFullPath = os.path.join(serverRepoLocation, ".git", itemPath)# Serve the object as binary datawith open(object_full_path, "rb") as object_file:object_data = object_file.read()if not os.path.exists(os.path.dirname(itemFullPath)): os.makedirs(os.path.dirname(itemFullPath))return flask.Response(object_data, content_type="application/octet-stream")with open(itemFullPath, "wb") as f: f.write(flask.request.data) return "", 200 @app.route("/git/<username>/<repository>/<path:itemPath>", methods=["MOVE"]) # @auth.login_required def gitMove(username, repository, itemPath): serverRepoLocation = os.path.join(config.REPOS_PATH, username, repository) itemFullPath = os.path.join(serverRepoLocation, ".git", itemPath) newPath = os.path.join(serverRepoLocation, ".git", flask.request.headers["destination"].split("/", 6)[-1]) app.logger.info(f"move {itemPath} to {newPath}") shutil.move(itemFullPath, newPath) return "", 200@app.route("/git/<username>/<repository>/git-upload-pack", methods=["POST"]) @auth.login_requireddef gitUploadPack(username, repository): repoPath = "/path/to/repos/{}/{}/".format(username, repository)cmd = ["git", "--git-dir", repoPath, "upload-pack"]serverRepoLocation = os.path.join(config.REPOS_PATH, os.path.join(username, repository)) cmd = ["git", "--git-dir", serverRepoLocation, "upload-pack"]gitProcess = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) # Forward the Git process input and outputinputData = flask.request.get_data() output = gitProcess.communicate(input=inputData) return flask.Response(output[0], content_type="application/x-git-upload-pack-result") @app.route("/git/<username>/<repository>/git-receive-pack", methods=["POST"]) @auth.login_requireddef gitReceivePack(username, repository): repoPath = "/path/to/repos/{}/{}/".format(username, repository)cmd = ["git", "--git-dir", repoPath, "receive-pack"]serverRepoLocation = os.path.join(config.REPOS_PATH, username, repository)gitProcess = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)proc = subprocess.Popen(['git', '--work-tree', serverRepoLocation, 'receive-pack'], stdin=subprocess.PIPE, stdout=subprocess.PIPE) proc.stdin.write(flask.request.get_data()) proc.stdin.close() response = proc.stdout.read()# Forward the Git process input and outputinputData = flask.request.get_data()output = gitProcess.communicate(input=inputData)return flask.Response(output[0], content_type="application/x-git-receive-pack-result")return flask.Response(response, content_type='application/x-git-receive-pack-result') @app.route("/git/<username>/<repository>/", methods=["PROPFIND"]) def gitPropFind(username, repository): serverRepoLocation = os.path.join(config.REPOS_PATH, username, repository) if not os.path.exists(serverRepoLocation): flask.abort(404) repo = git.Repo(serverRepoLocation) branches = "" for branch in repo.heads: branches += f""" <ns0:branch xmlns="DAV:"> <ns0:name>{branch.name}</ns0:name> </ns0:branch> """ response = f"""<?xml version="1.0" encoding="UTF-8"?> <D:multistatus xmlns:D="DAV:"> <D:response> <D:href>/git/{username}/{repository}/</D:href> <D:propstat> <D:status>HTTP/1.1 200 OK</D:status> <D:prop> <D:displayname>{repository}</D:displayname> <D:resourcetype><D:collection/></D:resourcetype> <D:supportedlock> <D:lockentry> <D:lockscope><D:exclusive/></D:lockscope> <D:locktype><D:write/></D:locktype> </D:lockentry> </D:supportedlock> </D:prop> </D:propstat> </D:response> </D:multistatus> """ return flask.Response(response, content_type="application/xml") locks = {} @app.route("/git/<username>/<repository>/<path:subpath>", methods=["LOCK", "UNLOCK"]) def gitLock(username, repository, subpath): if flask.request.method == "LOCK": scope = flask.request.args.get("lockscope") type_ = flask.request.args.get("locktype") if username + "/" + repository in locks: return "Lock already exists", 423 lockToken = uuid.uuid4().hex if config.locking: locks[username + "/" + repository] = { "scope": scope, "type": type_, "token": lockToken } text = f"""<?xml version="1.0" encoding="UTF-8"?> <D:prop> <D:lockdiscovery> <D:activelock> <D:lockscope><D:exclusive/></D:lockscope> <D:locktype><D:write/></D:locktype> <D:depth>Infinity</D:depth> <D:owner> <D:href>https://{config.BASE_DOMAIN}/{username}</D:href> </D:owner> <D:timeout>Second-600</D:timeout> <D:locktoken> <D:href>opaquelocktoken:{lockToken}</D:href> </D:locktoken> </D:activelock> </D:lockdiscovery> </D:prop> """ return text elif flask.request.method == "UNLOCK": lockToken = flask.request.headers.get("Lock-Token") if config.locking and locks[username + "/" + repository]["token"] == lockToken: del locks[username + "/" + repository] return ""
gitme.py
@@ -1,16 +1,49 @@
import os import flask from flask_sqlalchemy import SQLAlchemyimport git import mimetypes import magic from markupsafe import escapefrom flask_bcrypt import Bcrypt from markupsafe import escape, Markup from flask_migrate import Migrateimport config app = flask.Flask(__name__) import gitHTTPmime = magic.Magic(mime=True)from flask_httpauth import HTTPBasicAuth auth = HTTPBasicAuth() app.config["SQLALCHEMY_DATABASE_URI"] = f"postgresql://root:{config.DB_PASSWORD}@localhost/gitme" app.config["SECRET_KEY"] = config.DB_PASSWORD app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False db = SQLAlchemy(app) bcrypt = Bcrypt(app) migrate = Migrate(app, db) def onlyChars(string, chars): for i in string: if i not in chars: return False return True with app.app_context(): class User(db.Model): username = db.Column(db.String(32), unique=True, nullable=False, primary_key=True) displayName = db.Column(db.String(128), unique=False, nullable=True) passwordHashed = db.Column(db.String(60), nullable=False) email = db.Column(db.String(254), nullable=True) def __init__(self, username, password, email=None, displayName=None): self.username = username self.passwordHashed = bcrypt.generate_password_hash(password, config.HASHING_ROUNDS).decode("utf-8") self.email = email import gitHTTPdef humanSize(value, decimals=2, scale=1024, units=("B", "kiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB", "RiB", "QiB")):
@@ -27,8 +60,8 @@ def humanSize(value, decimals=2, scale=1024, units=("B", "kiB", "MiB", "GiB", "T
def guessMIME(path): if os.path.isdir(path): mimetype = "inode/directory" elif mime.from_file(path):mimetype = mime.from_file(path)elif magic.from_file(path, mime=True): mimetype = magic.from_file(path, mime=True)else: mimetype = "application/octet-stream" return mimetype
@@ -40,14 +73,83 @@ def convertToHTML(path):
return contents @app.context_processor def default(): username = flask.session.get("username") return {"loggedInUser": username} @app.route("/") def main(): return flask.render_template("home.html", title="gitme") @app.route("/settings/") def main(): return flask.render_template("user-settings.html", title="gitme") @app.route("/accounts/", methods=["GET", "POST"]) def login(): return flask.render_template("login.html", title="gitme")if flask.request.method == "GET": return flask.render_template("login.html", title="gitme") else: if "login" in flask.request.form: username = flask.request.form["username"] password = flask.request.form["password"] user = User.query.filter_by(username=username).first() if user and bcrypt.check_password_hash(user.passwordHashed, password): flask.session["username"] = user.username flask.flash(Markup(f"<iconify-icon icon='mdi:account'></iconify-icon>Successfully logged in as {username}"), category="success") return flask.redirect("/", code=303) elif not user: flask.flash(Markup("<iconify-icon icon='mdi:account-question'></iconify-icon>User not found"), category="alert") return flask.render_template("login.html", title="gitme") else: flask.flash(Markup("<iconify-icon icon='mdi:account-question'></iconify-icon>Invalid password"), category="error") return flask.render_template("login.html", title="gitme") if "signup" in flask.request.form: username = flask.request.form["username"] password = flask.request.form["password"] password2 = flask.request.form["password2"] email = flask.request.form.get("email") email2 = flask.request.form.get("email2") # repeat email is a honeypot name = flask.request.form.get("name") if not onlyChars(username, "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_"): flask.flash(Markup("<iconify-icon icon='mdi:account-error'></iconify-icon>Usernames may only contain Latin alphabet, numbers, '-' and '_'"), category="error") return flask.render_template("login.html", title="gitme") if username in config.RESERVED_NAMES: flask.flash(Markup(f"<iconify-icon icon='mdi:account-error'></iconify-icon>Sorry, {username} is a system path"), category="error") return flask.render_template("login.html", title="gitme") userCheck = User.query.filter_by(username=username).first() if userCheck: flask.flash(Markup(f"<iconify-icon icon='mdi:account-error'></iconify-icon>The username {username} is taken"), category="error") return flask.render_template("login.html", title="gitme") if password2 != password: flask.flash(Markup("<iconify-icon icon='mdi:key-alert'></iconify-icon>Make sure the passwords match"), category="error") return flask.render_template("login.html", title="gitme") user = User(username, password, email, name) os.makedirs(os.path.join(config.REPOS_PATH, username)) db.session.add(user) db.session.commit() flask.session["username"] = user.username flask.flash(Markup(f"<iconify-icon icon='mdi:account'></iconify-icon>Successfully created and logged in as {username}"), category="success") return flask.redirect("/", code=303) @app.route("/logout") def logout(): flask.session.clear() flask.flash(Markup(f"<iconify-icon icon='mdi:account'></iconify-icon>Successfully logged out"), category="info") return flask.redirect("/", code=303)@app.route("/<username>/")
@@ -163,8 +265,8 @@ def repositoryTree(username, repository, branch, subpath):
icon = specialIcon elif os.path.isdir(path): icon = config.folderIcon elif mimetypes.guess_type(path)[0] in config.fileIcons:icon = config.fileIcons[mimetypes.guess_type(path)[0]]elif guessMIME(path)[0] in config.fileIcons: icon = config.fileIcons[guessMIME(path)[0]]else: icon = config.unknownIcon
@@ -181,6 +283,7 @@ def repositoryTree(username, repository, branch, subpath):
current=branch, mode=mode, mimetype=mimetype, detailedtype=magic.from_file(path),size=size, icon=icon, subpath=os.path.join("/", subpath),
post-receive
@@ -0,0 +1,16 @@
#!/bin/bash while read oldrev newrev refname do # Check if the refname starts with "refs/heads/" if [[ $refname == refs/heads/* ]]; then # Extract the branch name from refname branchname="${refname#refs/heads/}" # Change to the repository's working tree cd /path/to/your/repo/working/tree # Update the working tree for the branch git checkout -f "$branchname" fi done
static/style.css
@@ -82,7 +82,7 @@ button, input, .button, select {
text-decoration: none; } #repo-tabs, #global-nav > ul {#repo-tabs, #global-nav > ul, #home-tabs {padding: 0; margin: 0; }
@@ -94,4 +94,12 @@ button, input, .button, select {
/* for now */ #repo-table > * > tr > :is(td, th):nth-child(1) { display: none; } .password-error { background: var(--color-error); color: var(--color-error-text); padding: 8px; gap: 4px; border-radius: 2px;}
templates/default.html
@@ -24,8 +24,13 @@
{% block breadcrumbs %}{% endblock %} </ul> <x-hbox style="--gap: 2ch;"> <a href="/accounts">Log in or sign up</a><a href="/settings">Preferences</a>{% if loggedInUser %} <a href="/{{ loggedInUser }}">{{ loggedInUser }}</a> <a href="/logout">Logout</a> <a href="/settings">Preferences</a> {% else %} <a href="/accounts">Log in or sign up</a> {% endif %}</x-hbox> </nav> {% block nav %}{% endblock %}
@@ -33,6 +38,33 @@
<main> {% block content %}{% endblock %} </main> {% with messages = get_flashed_messages(with_categories=true) %} <ol class="toast-container"> {% for category, message in messages %} <li style=" {% if category %} background-color: {% if category == 'error' %}var(--color-error) {% elif category == 'alert' %}var(--color-alert) {% elif category == 'info' %}var(--color-info) {% elif category == 'success' %}var(--color-success) {% endif %}; color: {% if category == 'error' %}var(--color-error-text) {% elif category == 'alert' %}var(--color-alert-text) {% elif category == 'info' %}var(--color-info-text) {% elif category == 'success' %}var(--color-success-text) {% endif %}; {% endif %}" > {{ message }} <x-buttonbox> <button class="button-flat" onclick="removeToast()" style="color: inherit !important;">Close</button> </x-buttonbox> </li> {% endfor %} </ol> {% endwith %}</body> <script src="/static/ripples.js"></script> <script src="/static/efficient-ui/dialogs.js"></script>
templates/file-view.html
@@ -7,6 +7,8 @@
</x-buttonbox> </x-hbox> {{ mimetype }} • {{ size[0] }} {{ size[1] }} <br> {{ detailedtype }}{% if mode == "text" %} <pre>{{ contents }}</pre>
templates/login.html
@@ -12,17 +12,18 @@
<x-notebook> <label><input type="radio" name="login-tabs" checked>Log in</label> <x-tab> <form><form method="post" id="login"> <input type="hidden" name="login" value="login"><div class="card"> <section> <x-vbox> <x-vbox class="nopad"> <label for="login-username">Username</label> <input id="login-username" required><input id="login-username" name="username" required></x-vbox> <x-vbox class="nopad"> <label for="login-password">Password</label> <input id="login-password" type="password" required><input id="login-password" name="password" type="password" required></x-vbox> <button style="width: 100%;" id="login-submit" type="submit">Log in</button> </x-vbox>
@@ -73,36 +74,37 @@
</x-tab> <label><input type="radio" name="login-tabs">Sign up</label> <x-tab> <form><form method="post" id="signup"> <input type="hidden" name="signup" value="signup"><div class="card"> <section> <x-vbox> <x-vbox class="nopad"> <label for="signup-username">Wanted username</label> <input id="signup-username" required><input id="signup-username" name="username" required></x-vbox> <x-vbox class="nopad"> <label for="signup-password">Password</label> <input id="signup-password" type="password" required><input id="signup-password" name="password" type="password" required></x-vbox> <x-vbox class="nopad"> <label for="signup-password-2">Repeat password</label> <input id="signup-password-2" type="password" required><input id="signup-password-2" name="password2" type="password" required></x-vbox> <x-vbox class="nopad"> <label for="signup-email">Email (recommended)</label> <input id="signup-email" type="email"><input id="signup-email" name="email" type="email"></x-vbox> <x-vbox class="nopad" style="display: none;"> <label for="signup-email-2">Repeat email (recommended)</label> <input id="signup-email-2" type="email"><input id="signup-email-2" name="email2" type="email"></x-vbox> <x-vbox class="nopad"> <label for="signup-name">Friendly name (optional)</label> <input id="signup-name"><input id="signup-name" name="name"></x-vbox> <label> <input id="signup-terms" type="checkbox" required><input id="signup-terms" type="checkbox" name="tos" required>I accept the <a href="/help/policies">policies listed here</a> </label> <button style="width: 100%;" id="signup-submit" type="submit">Sign up</button>