[aur-dev] [PATCH/RFC 0/5] Use Git repositories to store AUR packages
This patch series adds Git support to the AUR. The scripts are not thoroughly tested and there is no documentation as of now. First comments and reviews are welcome, though! If you want to test this, note that the scripts currently require a patched sshd(8) [1]. I am trying to get this upstream. [1] https://github.com/ScottDuckworth/openssh-akcenv Lukas Fleischer (5): Add support for adding SSH public keys to profiles Add basic Git authentication/authorization scripts Add update hook template Use Git repositories to store packages Add public clone URLs to package details UPGRADING | 10 +- schema/aur-schema.sql | 1 + scripts/git-integration/config.sample | 17 + scripts/git-integration/git-auth.py | 41 + scripts/git-integration/git-serve.py | 106 ++ scripts/git-integration/git-update.py | 334 ++++++ web/html/account.php | 7 +- web/html/pkgsubmit.php | 495 -------- web/lib/Archive/PEAR.php | 1063 ------------------ web/lib/Archive/PEAR5.php | 33 - web/lib/Archive/Tar.php | 1993 --------------------------------- web/lib/acctfuncs.inc.php | 78 +- web/lib/aurjson.class.php | 1 - web/lib/config.inc.php.proto | 4 +- web/lib/pkgbasefuncs.inc.php | 39 - web/lib/pkgbuild-parser.inc.php | 139 --- web/lib/pkgfuncs.inc.php | 195 ---- web/lib/routing.inc.php | 1 - web/template/account_edit_form.php | 5 + web/template/header.php | 1 - web/template/pkg_details.php | 10 +- web/template/pkgbase_details.php | 10 +- 22 files changed, 602 insertions(+), 3981 deletions(-) create mode 100644 scripts/git-integration/config.sample create mode 100755 scripts/git-integration/git-auth.py create mode 100755 scripts/git-integration/git-serve.py create mode 100755 scripts/git-integration/git-update.py delete mode 100644 web/html/pkgsubmit.php delete mode 100644 web/lib/Archive/PEAR.php delete mode 100644 web/lib/Archive/PEAR5.php delete mode 100644 web/lib/Archive/Tar.php delete mode 100644 web/lib/pkgbuild-parser.inc.php -- 2.0.0
Users can now add an SSH public key on the account edit page. This will later be used to authenticate users via SSH. Signed-off-by: Lukas Fleischer <archlinux@cryptocrack.de> --- UPGRADING | 10 ++++- schema/aur-schema.sql | 1 + web/html/account.php | 7 ++-- web/lib/acctfuncs.inc.php | 78 ++++++++++++++++++++++++++++++++++---- web/template/account_edit_form.php | 5 +++ 5 files changed, 90 insertions(+), 11 deletions(-) diff --git a/UPGRADING b/UPGRADING index 863fde3..2e1b806 100644 --- a/UPGRADING +++ b/UPGRADING @@ -1,6 +1,15 @@ Upgrading ========= +From 3.1.0 to 4.0.0 +------------------- + +1. Add a field for the SSH public key to the Users table: + +---- +ALTER TABLE Users ADD COLUMN SSHPubKey VARCHAR(4096) NULL DEFAULT NULL; +---- + From 3.0.0 to 3.1.0 ------------------- @@ -16,7 +25,6 @@ ALTER TABLE Licenses MODIFY Name VARCHAR(255) NOT NULL; ALTER TABLE Groups MODIFY Name VARCHAR(255) NOT NULL; ALTER TABLE PackageDepends MODIFY DepCondition VARCHAR(255); ALTER TABLE PackageRelations MODIFY RelCondition VARCHAR(255); ----- From 2.3.1 to 3.0.0 ------------------- diff --git a/schema/aur-schema.sql b/schema/aur-schema.sql index 0efae93..750ee21 100644 --- a/schema/aur-schema.sql +++ b/schema/aur-schema.sql @@ -32,6 +32,7 @@ CREATE TABLE Users ( LangPreference VARCHAR(5) NOT NULL DEFAULT 'en', IRCNick VARCHAR(32) NOT NULL DEFAULT '', PGPKey VARCHAR(40) NULL DEFAULT NULL, + SSHPubKey VARCHAR(4096) NULL DEFAULT NULL, LastLogin BIGINT UNSIGNED NOT NULL DEFAULT 0, LastLoginIPAddress INTEGER UNSIGNED NOT NULL DEFAULT 0, InactivityTS BIGINT UNSIGNED NOT NULL DEFAULT 0, diff --git a/web/html/account.php b/web/html/account.php index 47cf6d2..aef240a 100644 --- a/web/html/account.php +++ b/web/html/account.php @@ -52,7 +52,7 @@ if (isset($_COOKIE["AURSID"])) { display_account_form($atype, "UpdateAccount", $row["Username"], $row["AccountTypeID"], $row["Suspended"], $row["Email"], "", "", $row["RealName"], $row["LangPreference"], - $row["IRCNick"], $row["PGPKey"], + $row["IRCNick"], $row["PGPKey"], $row["SSHPubKey"], $row["InactivityTS"] ? 1 : 0, $row["ID"]); } else { print __("You do not have permission to edit this account."); @@ -82,7 +82,8 @@ if (isset($_COOKIE["AURSID"])) { in_request("U"), in_request("T"), in_request("S"), in_request("E"), in_request("P"), in_request("C"), in_request("R"), in_request("L"), in_request("I"), - in_request("K"), in_request("J"), in_request("ID")); + in_request("K"), in_request("PK"), in_request("J"), + in_request("ID")); } } else { if ($atype == "Trusted User" || $atype == "Developer") { @@ -113,7 +114,7 @@ if (isset($_COOKIE["AURSID"])) { # display the account request form # print __("Use this form to create an account."); - display_account_form("", "NewAccount", "", "", "", "", "", "", "", $LANG); + display_account_form("", "NewAccount", "", "", "", "", "", "", "", "", $LANG); } } diff --git a/web/lib/acctfuncs.inc.php b/web/lib/acctfuncs.inc.php index 06d4311..d7578a8 100644 --- a/web/lib/acctfuncs.inc.php +++ b/web/lib/acctfuncs.inc.php @@ -54,13 +54,14 @@ function html_format_pgp_fingerprint($fingerprint) { * @param string $L The language preference of the displayed user * @param string $I The IRC nickname of the displayed user * @param string $K The PGP key fingerprint of the displayed user + * @param string $PK The SSH public key of the displayed user * @param string $J The inactivity status of the displayed user * @param string $UID The user ID of the displayed user * * @return void */ -function display_account_form($UTYPE,$A,$U="",$T="",$S="", - $E="",$P="",$C="",$R="",$L="",$I="",$K="",$J="", $UID=0) { +function display_account_form($UTYPE,$A,$U="",$T="",$S="",$E="",$P="",$C="", + $R="",$L="",$I="",$K="",$PK="",$J="", $UID=0) { global $SUPPORTED_LANGS; include("account_edit_form.php"); @@ -84,13 +85,14 @@ function display_account_form($UTYPE,$A,$U="",$T="",$S="", * @param string $L The language preference of the user * @param string $I The IRC nickname of the user * @param string $K The PGP fingerprint of the user + * @param string $PK The SSH public key of the user * @param string $J The inactivity status of the user * @param string $UID The user ID of the modified account * * @return string|void Return void if successful, otherwise return error */ -function process_account_form($UTYPE,$TYPE,$A,$U="",$T="",$S="",$E="", - $P="",$C="",$R="",$L="",$I="",$K="",$J="",$UID=0) { +function process_account_form($UTYPE,$TYPE,$A,$U="",$T="",$S="",$E="",$P="", + $C="",$R="",$L="",$I="",$K="",$PK="",$J="",$UID=0) { global $SUPPORTED_LANGS, $AUR_LOCATION; $error = ''; @@ -143,6 +145,15 @@ function process_account_form($UTYPE,$TYPE,$A,$U="",$T="",$S="",$E="", $error = __("The PGP key fingerprint is invalid."); } + if (!$error && !empty($PK)) { + if (valid_ssh_pubkey($PK)) { + $tokens = explode(" ", $PK); + $PK = $tokens[0] . " " . $tokens[1]; + } else { + $error = __("The SSH public key is invalid."); + } + } + if (($UTYPE == "User" && $T > 1) || ($UTYPE == "Trusted User" && $T > 2)) { $error = __("Cannot increase account permissions."); } @@ -185,11 +196,29 @@ function process_account_form($UTYPE,$TYPE,$A,$U="",$T="",$S="",$E="", "<strong>", htmlspecialchars($E,ENT_QUOTES), "</strong>"); } } + if (!$error) { + /* + * Check whether the SSH public key is available. + * TODO: Fix race condition. + */ + $q = "SELECT COUNT(*) FROM Users "; + $q.= "WHERE SSHPubKey = " . $dbh->quote($PK); + if ($TYPE == "edit") { + $q.= " AND ID != " . intval($UID); + } + $result = $dbh->query($q); + $row = $result->fetch(PDO::FETCH_NUM); + + if ($row[0]) { + $error = __("The SSH public key, %s%s%s, is already in use.", + "<strong>", htmlspecialchars($PK, ENT_QUOTES), "</strong>"); + } + } if ($error) { print "<ul class='errorlist'><li>".$error."</li></ul>\n"; display_account_form($UTYPE, $A, $U, $T, $S, $E, "", "", - $R, $L, $I, $K, $J, $UID); + $R, $L, $I, $K, $PK, $J, $UID); return; } @@ -211,11 +240,13 @@ function process_account_form($UTYPE,$TYPE,$A,$U="",$T="",$S="",$E="", $L = $dbh->quote($L); $I = $dbh->quote($I); $K = $dbh->quote(str_replace(" ", "", $K)); + $PK = $dbh->quote($PK); $q = "INSERT INTO Users (AccountTypeID, Suspended, "; $q.= "InactivityTS, Username, Email, Passwd, Salt, "; - $q.= "RealName, LangPreference, IRCNick, PGPKey) "; + $q.= "RealName, LangPreference, IRCNick, PGPKey, "; + $q.= "SSHPubKey) "; $q.= "VALUES (1, 0, 0, $U, $E, $P, $salt, $R, $L, "; - $q.= "$I, $K)"; + $q.= "$I, $K, $PK)"; $result = $dbh->exec($q); if (!$result) { print __("Error trying to create account, %s%s%s.", @@ -283,6 +314,7 @@ function process_account_form($UTYPE,$TYPE,$A,$U="",$T="",$S="",$E="", $q.= ", LangPreference = " . $dbh->quote($L); $q.= ", IRCNick = " . $dbh->quote($I); $q.= ", PGPKey = " . $dbh->quote(str_replace(" ", "", $K)); + $q.= ", SSHPubKey = " . $dbh->quote($PK); $q.= ", InactivityTS = " . $inactivity_ts; $q.= " WHERE ID = ".intval($UID); $result = $dbh->exec($q); @@ -797,6 +829,38 @@ function valid_pgp_fingerprint($fingerprint) { } /** + * Determine if the SSH public key is valid + * + * @param string $pubkey SSH public key to check + * + * @return bool True if the SSH public key is valid, otherwise false + */ +function valid_ssh_pubkey($pubkey) { + $valid_prefixes = array( + "ssh-rsa", "ssh-dss", "ecdsa-sha2-nistp256", + "ecdsa-sha2-nistp384", "ecdsa-sha2-nistp521", "ssh-ed25519" + ); + + $has_valid_prefix = false; + foreach ($valid_prefixes as $prefix) { + if (strpos($pubkey, $prefix . " ") === 0) { + $has_valid_prefix = true; + break; + } + } + if (!$has_valid_prefix) { + return false; + } + + $tokens = explode(" ", $pubkey); + if (empty($tokens[1])) { + return false; + } + + return (base64_encode(base64_decode($tokens[1], true)) == $tokens[1]); +} + +/** * Determine if the user account has been suspended * * @param string $id The ID of user to check if suspended diff --git a/web/template/account_edit_form.php b/web/template/account_edit_form.php index 30b26fd..767b227 100644 --- a/web/template/account_edit_form.php +++ b/web/template/account_edit_form.php @@ -93,6 +93,11 @@ </p> <p> + <label for="id_ssh"><?= __("SSH Public Key") ?>:</label> + <textarea name="PK" id="id_ssh" rows="5" cols="30"><?= htmlspecialchars($PK) ?></textarea> + </p> + + <p> <label for="id_language"><?= __("Language") ?>:</label> <select name="L" id="id_language"> <?php -- 2.0.0
This adds two scripts to be used together with Git over SSH: * git-auth.py is supposed to be used as AuthorizedKeysCommand. It checks whether the public key belongs to any AUR user and invokes git-serve.py, passing the name of the corresponding user as a command line argument, if any. * git-serve.py is a wrapper around git-shell(1) that checks whether the user passed as command line argument has access to the Git repository that a push operation writes to. Signed-off-by: Lukas Fleischer <archlinux@cryptocrack.de> --- scripts/git-integration/config.sample | 16 ++++++ scripts/git-integration/git-auth.py | 41 ++++++++++++++ scripts/git-integration/git-serve.py | 104 ++++++++++++++++++++++++++++++++++ 3 files changed, 161 insertions(+) create mode 100644 scripts/git-integration/config.sample create mode 100755 scripts/git-integration/git-auth.py create mode 100755 scripts/git-integration/git-serve.py diff --git a/scripts/git-integration/config.sample b/scripts/git-integration/config.sample new file mode 100644 index 0000000..7e53abd --- /dev/null +++ b/scripts/git-integration/config.sample @@ -0,0 +1,16 @@ +[database] +host = localhost +name = AUR +user = aur +password = aur + +[auth] +key-prefixes = ssh-rsa ssh-dss ecdsa-sha2-nistp256 ecdsa-sha2-nistp384 ecdsa-sha2-nistp521 ssh-ed25519 +username-regex = [a-zA-Z0-9]+[.\-_]?[a-zA-Z0-9]+$ +git-serve-cmd = /srv/http/aur/scripts/git-integration/git-serve.py +ssh-options = no-port-forwarding,no-X11-forwarding,no-pty + +[serve] +repo-base = /pub/git/ +repo-regex = [a-z0-9][a-z0-9.+_-]*$ +git-shell-cmd = /usr/bin/git-shell diff --git a/scripts/git-integration/git-auth.py b/scripts/git-integration/git-auth.py new file mode 100755 index 0000000..fe9aec1 --- /dev/null +++ b/scripts/git-integration/git-auth.py @@ -0,0 +1,41 @@ +#!/usr/bin/python3 + +import configparser +import mysql.connector +import os +import re + +config = configparser.RawConfigParser() +config.read(os.path.dirname(os.path.realpath(__file__)) + "/config") + +aur_db_host = config.get('database', 'host') +aur_db_name = config.get('database', 'name') +aur_db_user = config.get('database', 'user') +aur_db_pass = config.get('database', 'password') + +key_prefixes = config.get('auth', 'key-prefixes').split() +username_regex = config.get('auth', 'username-regex') +git_serve_cmd = config.get('auth', 'git-serve-cmd') +ssh_opts = config.get('auth', 'ssh-options') + +pubkey = os.environ.get("SSH_KEY") +valid_prefixes = tuple(p + " " for p in key_prefixes) +if pubkey is None or not pubkey.startswith(valid_prefixes): + exit(1) + +db = mysql.connector.connect(host=aur_db_host, user=aur_db_user, + passwd=aur_db_pass, db=aur_db_name, + buffered=True) + +cur = db.cursor() +cur.execute("SELECT Username FROM Users WHERE SSHPubKey = %s " + + "AND Suspended = 0", (pubkey,)) + +if cur.rowcount != 1: + exit(1) + +user = cur.fetchone()[0] +if not re.match(username_regex, user): + exit(1) + +print('command="%s %s",%s %s' % (git_serve_cmd, user, ssh_opts, pubkey)) diff --git a/scripts/git-integration/git-serve.py b/scripts/git-integration/git-serve.py new file mode 100755 index 0000000..345c7ce --- /dev/null +++ b/scripts/git-integration/git-serve.py @@ -0,0 +1,104 @@ +#!/usr/bin/python3 + +import configparser +import mysql.connector +import os +import pygit2 +import re +import shlex +import sys + +config = configparser.RawConfigParser() +config.read(os.path.dirname(os.path.realpath(__file__)) + "/config") + +aur_db_host = config.get('database', 'host') +aur_db_name = config.get('database', 'name') +aur_db_user = config.get('database', 'user') +aur_db_pass = config.get('database', 'password') + +repo_base_path = config.get('serve', 'repo-base') +repo_regex = config.get('serve', 'repo-regex') +git_shell_cmd = config.get('serve', 'git-shell-cmd') + +def repo_path_validate(path): + if not path.startswith(repo_base_path): + return False + if not path.endswith('.git/'): + return False + repo = path[len(repo_base_path):-5] + return re.match(repo_regex, repo) + +def repo_path_get_pkgbase(path): + pkgbase = path.rstrip('/').rpartition('/')[2] + if pkgbase.endswith('.git'): + pkgbase = pkgbase[:-4] + return pkgbase + +def setup_repo(repo, user): + if not re.match(repo_regex, repo): + die('invalid repository name: %s' % (repo)) + + db = mysql.connector.connect(host=aur_db_host, user=aur_db_user, + passwd=aur_db_pass, db=aur_db_name) + cur = db.cursor() + + cur.execute("SELECT COUNT(*) FROM PackageBases WHERE Name = %s ", [repo]) + if cur.fetchone()[0] > 0: + die('package base already exists: %s' % (repo)) + + cur.execute("SELECT ID FROM Users WHERE Username = %s ", [user]) + userid = cur.fetchone()[0] + if userid == 0: + die('unknown user: %s' % (user)) + + cur.execute("INSERT INTO PackageBases (Name, SubmittedTS, ModifiedTS, " + + "SubmitterUID) VALUES (%s, UNIX_TIMESTAMP(), " + + "UNIX_TIMESTAMP(), %s)", [repo, userid]) + + db.commit() + db.close() + + repo_path = repo_base_path + '/' + repo + '.git/' + pygit2.init_repository(repo_path, True) + +def check_permissions(pkgbase, user): + db = mysql.connector.connect(host=aur_db_host, user=aur_db_user, + passwd=aur_db_pass, db=aur_db_name, + buffered=True) + cur = db.cursor() + + cur.execute("SELECT COUNT(*) FROM PackageBases INNER JOIN Users " + + "ON Users.ID = PackageBases.MaintainerUID OR " + + "PackageBases.MaintainerUID IS NULL WHERE " + + "Name = %s AND Username = %s", [pkgbase, user]) + return cur.fetchone()[0] > 0 + +def die(msg): + sys.stderr.write("%s\n" % (msg)) + exit(1) + +user = sys.argv[1] +cmd = os.environ.get("SSH_ORIGINAL_COMMAND") +if not cmd: + die('no command specified') +cmdargv = shlex.split(cmd) +action = cmdargv[0] + +if action == 'git-upload-pack' or action == 'git-receive-pack': + path = cmdargv[1] + if not repo_path_validate(path): + die('invalid path: %s' % (path)) + pkgbase = repo_path_get_pkgbase(path) + if action == 'git-receive-pack': + if not check_permissions(pkgbase, user): + die('permission denied: %s' % (user)) + os.environ["AUR_USER"] = user + os.environ["AUR_GIT_DIR"] = path + os.environ["AUR_PKGBASE"] = pkgbase + os.execl(git_shell_cmd, git_shell_cmd, '-c', cmd) +elif action == 'setup-repo': + if len(cmdargv) < 2: + die('missing repository name') + setup_repo(cmdargv[1], user) +else: + die('invalid command: %s' % (action)) -- 2.0.0
This adds a script that can be used as an update hook to check all commits for validity and to regenerate the package details page before updating a named ref. Signed-off-by: Lukas Fleischer <archlinux@cryptocrack.de> --- scripts/git-integration/config.sample | 1 + scripts/git-integration/git-serve.py | 2 + scripts/git-integration/git-update.py | 334 ++++++++++++++++++++++++++++++++++ 3 files changed, 337 insertions(+) create mode 100755 scripts/git-integration/git-update.py diff --git a/scripts/git-integration/config.sample b/scripts/git-integration/config.sample index 7e53abd..39f68e4 100644 --- a/scripts/git-integration/config.sample +++ b/scripts/git-integration/config.sample @@ -13,4 +13,5 @@ ssh-options = no-port-forwarding,no-X11-forwarding,no-pty [serve] repo-base = /pub/git/ repo-regex = [a-z0-9][a-z0-9.+_-]*$ +git-update-hook = /srv/http/aur/scripts/git-integration/git-update.py git-shell-cmd = /usr/bin/git-shell diff --git a/scripts/git-integration/git-serve.py b/scripts/git-integration/git-serve.py index 345c7ce..84d8dc6 100755 --- a/scripts/git-integration/git-serve.py +++ b/scripts/git-integration/git-serve.py @@ -18,6 +18,7 @@ aur_db_pass = config.get('database', 'password') repo_base_path = config.get('serve', 'repo-base') repo_regex = config.get('serve', 'repo-regex') +git_update_hook = config.get('serve', 'git-update-hook') git_shell_cmd = config.get('serve', 'git-shell-cmd') def repo_path_validate(path): @@ -60,6 +61,7 @@ def setup_repo(repo, user): repo_path = repo_base_path + '/' + repo + '.git/' pygit2.init_repository(repo_path, True) + os.symlink(git_update_hook, repo_path + 'hooks/update') def check_permissions(pkgbase, user): db = mysql.connector.connect(host=aur_db_host, user=aur_db_user, diff --git a/scripts/git-integration/git-update.py b/scripts/git-integration/git-update.py new file mode 100755 index 0000000..64606b8 --- /dev/null +++ b/scripts/git-integration/git-update.py @@ -0,0 +1,334 @@ +#!/usr/bin/python3 + +from copy import copy, deepcopy +import configparser +import mysql.connector +import os +import pygit2 +import re +import sys + +config = configparser.RawConfigParser() +config.read(os.path.dirname(os.path.realpath(__file__)) + "/config") + +aur_db_host = config.get('database', 'host') +aur_db_name = config.get('database', 'name') +aur_db_user = config.get('database', 'user') +aur_db_pass = config.get('database', 'password') + +MULTIVALUED_ATTRS = set([ + 'arch', + 'groups', + 'makedepends', + 'checkdepends', + 'optdepends', + 'depends', + 'provides', + 'conflicts', + 'replaces', + 'options', + 'license', + 'source', + 'noextract', + 'backup', +]) + +def IsMultiValued(attr): + return attr in MULTIVALUED_ATTRS + +class AurInfo(object): + def __init__(self): + self._pkgbase = {} + self._packages = {} + + def GetPackageNames(self): + return self._packages.keys() + + def GetMergedPackage(self, pkgname): + package = deepcopy(self._pkgbase) + package['pkgname'] = pkgname + for k, v in self._packages.get(pkgname).items(): + package[k] = deepcopy(v) + return package + + def AddPackage(self, pkgname): + self._packages[pkgname] = {} + return self._packages[pkgname] + + def GetPkgbase(self): + if 'pkgname' in self._pkgbase: + return self._pkgbase['pkgname'] + else: + return None + + def SetPkgbase(self, pkgbasename): + self._pkgbase = {'pkgname' : pkgbasename} + return self._pkgbase + + def Save(self, db, cur, user): + # Obtain package base ID and previous maintainer. + cur.execute("SELECT ID, MaintainerUID FROM PackageBases " + "WHERE Name = %s", [self._pkgbase['pkgname']]) + (pkgbase_id, maintainer_uid) = cur.fetchone() + was_orphan = not maintainer_uid + + # Obtain the user ID of the new maintainer. + cur.execute("SELECT ID FROM Users WHERE Username = %s", [user]) + user_id = int(cur.fetchone()[0]) + + # Update package base details and delete current packages. + cur.execute("UPDATE PackageBases SET ModifiedTS = UNIX_TIMESTAMP(), " + + "MaintainerUID = %s, PackagerUID = %s, " + + "OutOfDateTS = NULL WHERE ID = %s", + [user_id, user_id, pkgbase_id]) + cur.execute("DELETE FROM Packages WHERE PackageBaseID = %s", + [pkgbase_id]) + + for pkgname in self._packages.keys(): + pkginfo = self.GetMergedPackage(pkgname) + + if 'epoch' in pkginfo and pkginfo['epoch'] > 0: + ver = '%d:%s-%s' % (pkginfo['epoch'], pkginfo['pkgver'], + pkginfo['pkgrel']) + else: + ver = '%s-%s' % (pkginfo['pkgver'], pkginfo['pkgrel']) + + # Create a new package. + cur.execute("INSERT INTO Packages (PackageBaseID, Name, " + + "Version, Description, URL) " + + "VALUES (%s, %s, %s, %s, %s)", + [pkgbase_id, pkginfo['pkgname'], ver, + pkginfo['pkgdesc'], pkginfo['url']]) + db.commit() + pkgid = cur.lastrowid + + # Add package sources. + for source in pkginfo['source']: + cur.execute("INSERT INTO PackageSources (PackageID, Source) " + + "VALUES (%s, %s)", [pkgid, source]) + + # Add package dependencies. + for deptype in ('depends', 'makedepends', + 'checkdepends', 'optdepends'): + if not deptype in pkginfo: + continue + cur.execute("SELECT ID FROM DependencyTypes WHERE Name = %s", + [deptype]) + deptypeid = cur.fetchone()[0] + for dep in pkginfo[deptype]: + depname = re.sub(r'(<|=|>).*', '', dep) + depcond = dep[len(depname):] + cur.execute("INSERT INTO PackageDepends (PackageID, " + + "DepTypeID, DepName, DepCondition) " + + "VALUES (%s, %s, %s, %s)", [pkgid, deptypeid, + depname, depcond]) + + # Add package relations (conflicts, provides, replaces). + for reltype in ('conflicts', 'provides', 'replaces'): + if not reltype in pkginfo: + continue + cur.execute("SELECT ID FROM RelationTypes WHERE Name = %s", + [reltype]) + reltypeid = cur.fetchone()[0] + for rel in pkginfo[reltype]: + relname = re.sub(r'(<|=|>).*', '', rel) + relcond = rel[len(relname):] + cur.execute("INSERT INTO PackageRelations (PackageID, " + + "RelTypeID, RelName, RelCondition) " + + "VALUES (%s, %s, %s, %s)", [pkgid, reltypeid, + relname, relcond]) + + # Add package licenses. + if 'license' in pkginfo: + for license in pkginfo['license']: + cur.execute("SELECT ID FROM Licenses WHERE Name = %s", + [license]) + if cur.rowcount == 1: + licenseid = cur.fetchone()[0] + else: + cur.execute("INSERT INTO Licenses (Name) VALUES (%s)", + [license]) + db.commit() + licenseid = cur.lastrowid + cur.execute("INSERT INTO PackageLicenses (PackageID, " + + "LicenseID) VALUES (%s, %s)", + [pkgid, licenseid]) + + # Add package groups. + if 'groups' in pkginfo: + for group in pkginfo['groups']: + cur.execute("SELECT ID FROM Groups WHERE Name = %s", + [group]) + if cur.rowcount == 1: + groupid = cur.fetchone()[0] + else: + cur.execute("INSERT INTO Groups (Name) VALUES (%s)", + [group]) + db.commit() + groupid = cur.lastrowid + cur.execute("INSERT INTO PackageGroups (PackageID, " + "GroupID) VALUES (%s, %s)", [pkgid, groupid]) + + # Add user to notification list on adoption. + if was_orphan: + cur.execute("INSERT INTO CommentNotify (PackageBaseID, UserID) " + + "VALUES (%s, %s)", [pkgbase_id, user_id]) + + db.commit() + +class ECatcherInterface(object): + def Catch(self, lineno, error): + raise NotImplementedError + + +class CollectionECatcher(ECatcherInterface): + def __init__(self): + self._errors = [] + + def Catch(self, lineno, error): + self._errors.append((lineno, error)) + + def HasErrors(self): + return len(self._errors) > 0 + + def Errors(self): + return copy(self._errors) + + +def ParseAurinfoFromIterable(iterable, ecatcher=None): + aurinfo = AurInfo() + + if ecatcher is None: + ecatcher = StderrECatcher() + + current_package = None + lineno = 0 + + for line in iterable: + lineno += 1 + + if not line.strip(): + # end of package + current_package = None + continue + + if not line.startswith('\t'): + # start of new package + try: + key, value = map(lambda s: s.strip(), line.split('=', 1)) + except ValueError: + ecatcher.Catch(lineno, 'unexpected header format in section=%s' % + current_package['pkgname']) + continue + + if key == 'pkgbase': + current_package = aurinfo.SetPkgbase(value) + else: + current_package = aurinfo.AddPackage(value) + else: + # package attribute + if current_package is None: + ecatcher.Catch(lineno, 'package attribute found outside of ' + 'a package section') + continue + + try: + key, value = map(lambda s: s.strip(), line.split('=', 1)) + except ValueError: + ecatcher.Catch(lineno, 'unexpected attribute format in ' + 'section=%s' % current_package['pkgname']) + + if IsMultiValued(key): + if not current_package.get(key): + current_package[key] = [] + current_package[key].append(value) + else: + if not current_package.get(key): + current_package[key] = value + else: + ecatcher.Catch(lineno, 'overwriting attribute ' + '%s: %s -> %s' % (key, current_package[key], + value)) + + return aurinfo + +def die(msg): + sys.stderr.write("error: %s\n" % (msg)) + exit(1) + +def die_commit(msg, commit): + sys.stderr.write("error: The following error " + + "occurred when parsing commit\n") + sys.stderr.write("error: %s:\n" % (commit)) + sys.stderr.write("error: %s\n" % (msg)) + exit(1) + +if len(sys.argv) != 4: + die("invalid arguments") + +refname = sys.argv[1] +sha1_old = sys.argv[2] +sha1_new = sys.argv[3] + +user = os.environ.get("AUR_USER") +pkgbase = os.environ.get("AUR_PKGBASE") +git_dir = os.environ.get("AUR_GIT_DIR") + +if refname != "refs/heads/master": + die("pushing to a branch other than master is restricted") + +repo = pygit2.Repository(git_dir) +walker = repo.walk(sha1_new, pygit2.GIT_SORT_TOPOLOGICAL) +if sha1_old != "0000000000000000000000000000000000000000": + walker.hide(sha1_old) + +for commit in walker: + if not '.AURINFO' in commit.tree: + die_commit("missing .AURINFO", commit.id) + + for treeobj in commit.tree: + if repo[treeobj.id].size > 100000: + die_commit("maximum blob size (100kB) exceeded", commit.id) + + aurinfo_raw = repo[commit.tree['.AURINFO'].id].data.decode() + ecatcher = CollectionECatcher() + aurinfo = ParseAurinfoFromIterable(aurinfo_raw.split('\n'), ecatcher) + errors = ecatcher.Errors() + if errors: + sys.stderr.write("error: The following errors occurred " + "when parsing .AURINFO in commit\n") + sys.stderr.write("error: %s:\n" % (commit.id)) + for error in errors: + sys.stderr.write("error: line %d: %s\n" % error) + exit(1) + + if aurinfo.GetPkgbase() != pkgbase: + die_commit('invalid pkgbase: %s' % (aurinfo.GetPkgbase()), commit.id) + + for pkgname in aurinfo.GetPackageNames(): + pkginfo = aurinfo.GetMergedPackage(pkgname) + + if not re.match(r'[a-z0-9][a-z0-9\.+_-]*$', pkginfo['pkgname']): + die_commit('invalid package name: %s' % (pkginfo['pkgname']), + commit.id) + + if not re.match(r'(?:http|ftp)s?://.*', pkginfo['url']): + die_commit('invalid URL: %s' % (pkginfo['url']), commit.id) + + for field in ('pkgname', 'pkgdesc', 'url'): + if len(pkginfo[field]) > 255: + die_commit('%s field too long: %s' % (field, pkginfo[field]), + commit.id) + +aurinfo_raw = repo[repo[sha1_new].tree['.AURINFO'].id].data.decode() +ecatcher = CollectionECatcher() +aurinfo = ParseAurinfoFromIterable(aurinfo_raw.split('\n'), ecatcher) + +db = mysql.connector.connect(host=aur_db_host, user=aur_db_user, + passwd=aur_db_pass, db=aur_db_name, + buffered=True) +cur = db.cursor() + +aurinfo.Save(db, cur, user) + +db.close() -- 2.0.0
Signed-off-by: Lukas Fleischer <archlinux@cryptocrack.de> --- web/lib/config.inc.php.proto | 1 + web/template/pkg_details.php | 4 ++++ web/template/pkgbase_details.php | 4 ++++ 3 files changed, 9 insertions(+) diff --git a/web/lib/config.inc.php.proto b/web/lib/config.inc.php.proto index cff3924..ee639eb 100644 --- a/web/lib/config.inc.php.proto +++ b/web/lib/config.inc.php.proto @@ -10,6 +10,7 @@ define( "AUR_db_pass", "aur" ); # Configuration of directories where things live define( "CGIT_URI", "https://git.aur.archlinux.org/" ); +define( "GIT_CLONE_URL", "git://git.aur.archlinux.org/pub/git/%s.git/" ); define( "USERNAME_MIN_LEN", 3 ); define( "USERNAME_MAX_LEN", 16 ); diff --git a/web/template/pkg_details.php b/web/template/pkg_details.php index 4fa6da9..31102bf 100644 --- a/web/template/pkg_details.php +++ b/web/template/pkg_details.php @@ -132,6 +132,10 @@ $sources = pkg_sources($row["ID"]); <table id="pkginfo"> <tr> + <th><?= __('Git Clone URL') . ': ' ?></th> + <td><?= sprintf(GIT_CLONE_URL, htmlspecialchars($row['BaseName'])) ?></td> + </tr> + <tr> <th><?= __('Package Base') . ': ' ?></th> <td class="wrap"><a href="<?= htmlspecialchars(get_pkgbase_uri($row['BaseName']), ENT_QUOTES); ?>"><?= htmlspecialchars($row['BaseName']); ?></a></td> </tr> diff --git a/web/template/pkgbase_details.php b/web/template/pkgbase_details.php index b9a35f5..2ad5cb5 100644 --- a/web/template/pkgbase_details.php +++ b/web/template/pkgbase_details.php @@ -107,6 +107,10 @@ $pkgs = pkgbase_get_pkgnames($base_id); <table id="pkginfo"> <tr> + <th><?= __('Git Clone URL') . ': ' ?></th> + <td><?= sprintf(GIT_CLONE_URL, htmlspecialchars($row['Name'])) ?></td> + </tr> + <tr> <th><?= __('Category') . ': ' ?></th> <?php if ($SID && ($uid == $row["MaintainerUID"] || -- 2.0.0
Hi guys, author of Aura (a package manager / AUR helper) here. Before this got too far along I thought I'd bring up what another AUR helper dev, my users, and myself have been discussing over the past little while. We've been paying close attention to the AUR 3.x development as of late. Version 2 of the RPC is a godsend, and the upcoming git integration sounds great. We've discussed the following: 1. RPC V2: It is yet unenforced. It is clearly better to have all dependency metadata available publicly, rather than rely on shoddy, insecure Bash parsing/execution done in different ways by every AUR helper, but the fact remains that the most popular AUR helpers don't enforce AUR 3. Aura 2 (in development) will, with support for dep resolution of all non-AUR 3 compliant packages dropped. We believe this is the way forward, but we face a new problem: 2. The AUR is filled with dead packages. It could use a reboot, rejecting all non-AUR 3 uploads. This would then ensure all packages provide full information via the RPC, and AUR helpers can safely resolve dependencies. The question is, how to reboot? a. Delete every package? Pros: All the long-orphaned packages would be gone forever. Cons: Current ownership of packages would be lost. b. Flag every package out-of-date? Pros: Everyone would/should become aware of the issue, and upload AUR 3 compliant versions of the packages they maintain. Cons: There's nothing stopping them from just hitting the "unflag out-of-date" button to circumvent this. c. Only move non-orphaned packages to the new git-based AUR? Pros: Would clear away the cruft and leave only real packages. Cons: ? If nothing else, (c.) might give us the greatest hope for a revitalized AUR. The equivalent to (b.) could be accomplished if enough AUR helper users collectively flagged non-AUR 3 compliant packages out of date. As v2 of the RPC has been released, it would be a waste if its use wasn't brought to the mainstream. That said, it might be difficult to do so if the state of the AUR remains as it is. On 17 June 2014 11:22, Lukas Fleischer <archlinux@cryptocrack.de> wrote:
This patch series adds Git support to the AUR. The scripts are not thoroughly tested and there is no documentation as of now. First comments and reviews are welcome, though!
If you want to test this, note that the scripts currently require a patched sshd(8) [1]. I am trying to get this upstream.
[1] https://github.com/ScottDuckworth/openssh-akcenv
Lukas Fleischer (5): Add support for adding SSH public keys to profiles Add basic Git authentication/authorization scripts Add update hook template Use Git repositories to store packages Add public clone URLs to package details
UPGRADING | 10 +- schema/aur-schema.sql | 1 + scripts/git-integration/config.sample | 17 + scripts/git-integration/git-auth.py | 41 + scripts/git-integration/git-serve.py | 106 ++ scripts/git-integration/git-update.py | 334 ++++++ web/html/account.php | 7 +- web/html/pkgsubmit.php | 495 -------- web/lib/Archive/PEAR.php | 1063 ------------------ web/lib/Archive/PEAR5.php | 33 - web/lib/Archive/Tar.php | 1993 --------------------------------- web/lib/acctfuncs.inc.php | 78 +- web/lib/aurjson.class.php | 1 - web/lib/config.inc.php.proto | 4 +- web/lib/pkgbasefuncs.inc.php | 39 - web/lib/pkgbuild-parser.inc.php | 139 --- web/lib/pkgfuncs.inc.php | 195 ---- web/lib/routing.inc.php | 1 - web/template/account_edit_form.php | 5 + web/template/header.php | 1 - web/template/pkg_details.php | 10 +- web/template/pkgbase_details.php | 10 +- 22 files changed, 602 insertions(+), 3981 deletions(-) create mode 100644 scripts/git-integration/config.sample create mode 100755 scripts/git-integration/git-auth.py create mode 100755 scripts/git-integration/git-serve.py create mode 100755 scripts/git-integration/git-update.py delete mode 100644 web/html/pkgsubmit.php delete mode 100644 web/lib/Archive/PEAR.php delete mode 100644 web/lib/Archive/PEAR5.php delete mode 100644 web/lib/Archive/Tar.php delete mode 100644 web/lib/pkgbuild-parser.inc.php
-- 2.0.0
On Tue, 17 Jun 2014 at 22:18:54, Colin Woodbury wrote:
[...] 2. The AUR is filled with dead packages. It could use a reboot, rejecting all non-AUR 3 uploads. This would then ensure all packages provide full information via the RPC, and AUR helpers can safely resolve dependencies. The question is, how to reboot? [...] c. Only move non-orphaned packages to the new git-based AUR? Pros: Would clear away the cruft and leave only real packages. Cons: ?
If nothing else, (c.) might give us the greatest hope for a revitalized AUR. The equivalent to (b.) could be accomplished if enough AUR helper users collectively flagged non-AUR 3 compliant packages out of date. [...]
I like that idea. We had the discussion of how to migrate the AUR to Git repositories in another thread and there was no consensus. Let me suggest a slightly modified version of your plan: When AUR 4.0.0 is released, we create an empty Git repository for each package that exists in the AUR at that time. Submitter, maintainer, votes, comments and everything else that is stored in the AUR database is retained, so no one can take over someone else's packages. People can then commit the current version of their PKGBUILD and push into the empty repository. Note that there is a Git hook that checks whether .AURINFO is available before updating the refs on the server. This ensures that source packages without metadata are going to be rejected. People that already used Git repositories for their AUR packages before can rewrite their commits to include metadata and then import the complete history. Git repositories that are still empty after one year will be deleted (including the corresponding packages). The migration will probably start a couple of weeks before the actual release (with a second setup of the new AUR release, while the "old" AUR still runs under the old domain) to avoid a period of time with almost no packages available. What do you think of that?
I think that sounds great! We seem to win on all fronts: - Old packages are given a chance but deleted if proven "dead". - Everyone is forced to upload an AUR 3+ compliant package. - There is overlap between the new and old AUR, to give all AUR helpers a grace period to alter their functionality and handle the new APIs. On 17 June 2014 14:07, Lukas Fleischer <archlinux@cryptocrack.de> wrote:
On Tue, 17 Jun 2014 at 22:18:54, Colin Woodbury wrote:
[...] 2. The AUR is filled with dead packages. It could use a reboot, rejecting all non-AUR 3 uploads. This would then ensure all packages provide full information via the RPC, and AUR helpers can safely resolve dependencies. The question is, how to reboot? [...] c. Only move non-orphaned packages to the new git-based AUR? Pros: Would clear away the cruft and leave only real packages. Cons: ?
If nothing else, (c.) might give us the greatest hope for a revitalized AUR. The equivalent to (b.) could be accomplished if enough AUR helper users collectively flagged non-AUR 3 compliant packages out of date. [...]
I like that idea. We had the discussion of how to migrate the AUR to Git repositories in another thread and there was no consensus. Let me suggest a slightly modified version of your plan:
When AUR 4.0.0 is released, we create an empty Git repository for each package that exists in the AUR at that time. Submitter, maintainer, votes, comments and everything else that is stored in the AUR database is retained, so no one can take over someone else's packages. People can then commit the current version of their PKGBUILD and push into the empty repository. Note that there is a Git hook that checks whether .AURINFO is available before updating the refs on the server. This ensures that source packages without metadata are going to be rejected. People that already used Git repositories for their AUR packages before can rewrite their commits to include metadata and then import the complete history.
Git repositories that are still empty after one year will be deleted (including the corresponding packages).
The migration will probably start a couple of weeks before the actual release (with a second setup of the new AUR release, while the "old" AUR still runs under the old domain) to avoid a period of time with almost no packages available.
What do you think of that?
Am Dienstag, den 17.06.2014, 23:07 +0200 schrieb Lukas Fleischer:
The migration will probably start a couple of weeks before the actual release (with a second setup of the new AUR release, while the "old" AUR still runs under the old domain) to avoid a period of time with almost no packages available.
What do you think of that?
+1 :)
On Tue, Jun 17, 2014 at 11:07 PM, Lukas Fleischer <archlinux@cryptocrack.de> wrote:
What do you think of that?
Sounds perfect to me as well :) As Colin stated, I don't see any drawback in this plan.
participants (4)
-
Colin Woodbury
-
Lukas Fleischer
-
Rémy Marquis
-
Stefan Tatschner