Get far on package manager

This commit is contained in:
2025-04-29 05:05:34 -05:00
parent 53c027ef63
commit 4c32bd9b99
21 changed files with 1176 additions and 0 deletions

View File

@@ -0,0 +1,621 @@
func DB.Open()
{
var path = DB.working / "data.db";
return Sqlite.Open(path);
}
/*func DB.GetPackageDetails(name)
{
DB.Lock();
var dbCon = DB.Open();
Sqlite.Close(dbCon);
DB.Unlock();
}*/
func DB.CanUploadPackagePrefix(userId, packageName)
{
var prefix = packageName.Split(".",true,2);
DB.Lock();
var dbCon = DB.Open();
var exec = Sqlite.Exec(dbCon, $"SELECT * FROM reserved_prefixes WHERE prefix = {Sqlite.Escape(prefix)};");
Sqlite.Close(dbCon);
DB.Unlock();
if(TypeOf(exec) == "List" && exec.Length > 0) return ParseLong(exec[0].accountId) == userId;
return true;
}
func DB.GetUserInfo(userId)
{
DB.Lock();
var dbCon = DB.Open();
var exec = Sqlite.Exec(dbCon, $"SELECT * FROM sessions WHERE id = {userId};");
Sqlite.Close(dbCon);
DB.Unlock();
if(TypeOf(exec) == "List" && exec.Length == 1)
{
var data = exec[0];
data.flags = ParseLong(data.flags);
return data;
}
return null;
}
func DB.PackageExists(userId,pkgInfo)
{
var statusCode = 4;
DB.Lock();
var dbCon = DB.Open();
var exec = Sqlite.Exec(dbCon,$"SELECT * from packages WHERE packageName = {Sqlite.Escape(pkgInfo.Name)};");
if(TypeOf(exec) == "List")
{
if(exec.Length == 1)
{
var pkgId = exec[0].id;
if(ParseLong(exec[0].accountId) != userId)
statusCode=4;
else
{
exec = Sqlite.Exec(dbCon,$"SELECT * from versions WHERE packageId = {pkgId} AND version = {pkgInfo.Version.VersionInt};");
if(TypeOf(exec) == "List")
{
if(exec.Length == 1)
{
if(pkgInfo.Version.Stage == "dev")
statusCode=3;
else
statusCode=2;
}
else statusCode=1;
}
}
}
else statusCode=0;
}
Sqlite.Close(dbCon);
DB.Unlock();
return statusCode;
}
func DB.AddPackage(userId,pkgInfo)
{
DB.Lock();
var dbCon = DB.Open();
Sqlite.Exec(dbCon,$"INSERT INTO packages (packageName, accountId) VALUES ({Sqlite.Escape(pkgInfo.Name)}, {userId});");
Sqlite.Close(dbCon);
DB.Unlock();
}
func DB.UpdateVersion(pkgInfo)
{
DB.Lock();
var dbCon = DB.Open();
var exec = Sqlite.Exec(dbCon,$"SELECT * FROM packages WHERE packageName = {Sqlite.Escape(pkgInfo.Name)};");
var pkgId = 0;
if(TypeOf(exec) == "List" && exec.Length == 1)
{
pkgId = exec[0].id;
}
var version = pkgInfo.Version.VersionInt;
var info = Json.Decode(pkgInfo.Info);
var description = info.description;
var type = info.type;
var maintainer = info.maintainer;
var homepage = info.homepage;
var repo = info.repo;
var license = info.license;
if(TypeOf(description) != "String") description="";
if(TypeOf(type) != "String") type="";
if(TypeOf(maintainer) != "String") maintainer="";
if(TypeOf(homepage) != "String") homepage="";
if(TypeOf(repo) != "String") repo="";
if(TypeOf(license) != "String") license="";
//CREATE TABLE IF NOT EXISTS versions (id INTEGER PRIMARY KEY AUTOINCREMENT, packageId INTEGER, version INTEGER, description TEXT, type TEXT, maintainer TEXT, homepage TEXT, repo TEXT, license TEXT);
//VALUES ({pkgId},{version},{Sqlite.Escape(description)},{Sqlite.Escape(type)},{Sqlite.Escape(maintainer)},{Sqlite.Escape(homepage)},{Sqlite.Escape(repo)},{Sqlite.Escape(license)});
Sqlite.Exec(dbCon,$"UPDATE versions SET description = {Sqlite.Escape(description)}, type = {Sqlite.Escape(type)}, maintainer = {Sqlite.Escape(maintainer)}, homepage = {Sqlite.Escape(homepage)}, repo = {Sqlite.Escape(repo)}, license = {Sqlite.Escape(license)}, uploadTime = {Time.Now} WHERE packageId = {pkgId} AND version = {version};");
Sqlite.Close(dbCon);
DB.Unlock();
}
func DB.AddVersion(pkgInfo)
{
DB.Lock();
var dbCon = DB.Open();
var exec = Sqlite.Exec(dbCon,$"SELECT * FROM packages WHERE packageName = {Sqlite.Escape(pkgInfo.Name)};");
var pkgId = 0;
if(TypeOf(exec) == "List" && exec.Length == 1)
{
pkgId = exec[0].id;
}
var version = pkgInfo.Version.VersionInt;
var info = Json.Decode(pkgInfo.Info);
var description = info.description;
var type = info.type;
var maintainer = info.maintainer;
var homepage = info.homepage;
var repo = info.repo;
var license = info.license;
if(TypeOf(description) != "String") description="";
if(TypeOf(type) != "String") type="";
if(TypeOf(maintainer) != "String") maintainer="";
if(TypeOf(homepage) != "String") homepage="";
if(TypeOf(repo) != "String") repo="";
if(TypeOf(license) != "String") license="";
//CREATE TABLE IF NOT EXISTS versions (id INTEGER PRIMARY KEY AUTOINCREMENT, packageId INTEGER, version INTEGER, description TEXT, type TEXT, maintainer TEXT, homepage TEXT, repo TEXT, license TEXT);
Sqlite.Exec(dbCon,$"INSERT INTO versions (packageId,version,description,type,maintainer,homepage,repo,license,uploadTime) VALUES ({pkgId},{version},{Sqlite.Escape(description)},{Sqlite.Escape(type)},{Sqlite.Escape(maintainer)},{Sqlite.Escape(homepage)},{Sqlite.Escape(repo)},{Sqlite.Escape(license)},{Time.Now});");
Sqlite.Close(dbCon);
DB.Unlock();
}
func DB.GetPackageIcon(name, version)
{
if(TypeOf(name) != "String") name = "";
if(TypeOf(version) != "String") version = "";
var file = DB.working / "Packages" / name / $"{name}-{version}.crvm";
if(FS.Local.FileExists(file) && name.Length > 0 && version.Length > 0)
{
var strm = FS.Local.OpenFile(file,"rb");
if(strm != null)
{
var exec = VM.LoadExecutable(strm);
strm.Close();
if(exec != null)
{
if(exec.Icon != null)
{
return exec.Icon;
}
}
}
}
return embed("crosslang.png");
}
func DB.UploadPackage(userId, filePath)
{
if(!FS.Local.FileExists(filePath)) return {Success=false, Reason = "File does not exist"};
var userInfo = DB.GetUserInfo(userId);
if(userInfo == null) return { Success=false, Reason = "User does not exist"};
if((userInfo.flags & DB.FLAG_VERIFIED) == 0) return { Success=false, Reason = "User is not verified"};
var strm = FS.Local.OpenFile(filePath,"rb");
var failed=false;
var reason = "";
try {
var pkgInfo = VM.LoadExecutable(strm);
}catch(ex) {
if(ex.Type == "NativeException")
reason = ex.Text;
failed=true;
}
strm.Close();
if(failed) return {Success=false, Reason = reason};
if(!DB.CanUploadPackagePrefix(userId,pkgInfo.Name)) return { Success = false, Reason = "You can't upload a package with that prefix."};
switch(DB.PackageExists(userId, pkgInfo))
{
case 0:
//package name does not exist
DB.AddPackage(userId,pkgInfo);
case 1:
DB.AddVersion(pkgInfo);
FS.Local.CreateDirectory(DB.working / "Packages" / pkgInfo.Name);
FS.Local.MoveFile(filePath, DB.working / "Packages" / pkgInfo.Name / $"{pkgInfo.Name}-{pkgInfo.Version}.crvm");
//package version does not exist
break;
case 2:
return { Success = false, Reason = "Package already exists and is not a dev package." };
case 3:
//update package version (it exists but is dev)
DB.UpdateVersion(pkgInfo);
FS.Local.MoveFile(filePath, DB.working / "Packages" / pkgInfo.Name / $"{pkgInfo.Name}-{pkgInfo.Version}.crvm");
break;
case 4:
//package is not yours
return { Success = false, Reason = "You don't own the package."};
}
return {Success=true};
}
func DB.GetUserIdFromSession(session)
{
DB.Lock();
var dbCon = DB.Open();
var exec = Sqlite.Exec(dbCon,$"SELECT * FROM sessions WHERE key = {Sqlite.Escape(session)};");
Sqlite.Close(dbCon);
DB.Unlock();
if(TypeOf(exec) == "List" && exec.Length == 1) return ParseLong(exec[0].accountId);
return -1;
}
func DB.GetSession(ctx)
{
var cookie = ctx.RequestHeaders.TryGetFirst("Cookie");
if(TypeOf(cookie) == "String")
{
each(var part : cookie.Split("; "))
{
if(part.Length > 0)
{
var cookieKV = part.Split("=",true,2);
if(cookieKV.Length == 2 && cookieKV[0] == "Session")
{
var session = cookieKV[1];
var sessionId = DB.GetUserIdFromSession(session);
if(sessionId != -1)
return session;
}
}
}
}
return null;
}
func DB.CreateSession(userId)
{
DB.Lock();
var dbCon = DB.Open();
var rand = Net.Http.UrlEncode(Crypto.Base64Encode(Crypto.RandomBytes(32, "CPKG")));
Sqlite.Exec(dbCon, $"INSERT INTO sessions (accountId,key) VALUES ({userId},{Sqlite.Escape(rand)});");
Sqlite.Close(dbCon);
DB.Unlock();
return rand;
}
func DB.CreateCSRF(ctx)
{
var session = DB.GetSession(ctx);
if(session)
{
var csrf = Crypto.Base64Encode(Crypto.RandomBytes(32, "CPKG"));
var expires = Time.Now + 600;
DB.Lock();
DB.csrf.Add({
Token = csrf,
Expires = expires,
Session = session
});
DB.Unlock();
return csrf;
}
return "";
}
func DB.VerifyCSRF(session,csrf)
{
if(TypeOf(csrf) != "String") return false;
if(csrf.Length == 0) return false;
csrf = csrf.Replace("\n","").Replace("\r","\n").Replace(" ","");
if(csrf.Length == 0) return false;
var retVal = false;
var time = Time.Now;
DB.Lock();
var _oldCSRF = DB.csrf;
DB.csrf = [];
//This will be inefficient but hopefully there won't be too many csrf tokens at any given time (we try to delete the expired ones)
each(var csrfItem : _oldCSRF)
{
if(time >= csrfItem.Expires) continue;
if(csrfItem.Token == csrf && time < csrfItem.Expires && session == csrfItem.Session)
{
retVal=true;
continue;
}
DB.csrf.Add(csrfItem);
}
DB.Unlock();
return retVal;
}
func DB.GetLatestVersion(name)
{
var sql = $"SELECT * FROM packages p join versions v on p.id = v.packageId and v.version = (SELECT MAX(version) FROM versions WHERE packageId = v.packageId) and p.packageName = {Sqlite.Escape(name)};";
DB.Lock();
var dbCon = DB.Open();
var res = Sqlite.Exec(dbCon, sql);
DB.Unlock();
if(TypeOf(res) == "List" && res.Length > 0)
{
return Version.FromLong(ParseLong(res[0].version)).ToString();
}
return null;
}
func DB.GetPackages($page)
{
if(TypeOf(page) != "Long") page = 1;
var sql = "SELECT * FROM packages p join versions v on p.id = v.packageId and v.version = (SELECT MAX(version) FROM versions WHERE packageId = v.packageId);";
DB.Lock();
var dbCon = DB.Open();
var res = Sqlite.Exec(dbCon, sql);
Sqlite.Close(dbCon);
DB.Unlock();
var res2 = [];
each(var item : res)
{
res2.Add({
id = item.packageId,
name = item.packageName,
version = Version.FromLong(ParseLong(item.version)).ToString(),
homepage = item.homepage,
repo = item.repo,
type = item.type,
maintainer = item.maintainer,
description = item.description
});
}
return res2;
}
func DB.Lock()
{
DB.mtx.Lock();
}
func DB.Unlock()
{
DB.mtx.Unlock();
}
func DB.GetUniqueNumber()
{
DB.Lock();
var unum = DB.u_n_counter++;
DB.Unlock();
return unum;
}
func DB.Init(working)
{
DB.working = Path.FromString(working);
DB.csrf = [];
DB.u_n_counter=0;
FS.Local.CreateDirectory(DB.working);
FS.Local.CreateDirectory(DB.working / "Temp");
FS.Local.CreateDirectory(DB.working / "Packages");
each(var f : FS.Local.EnumeratePaths(DB.working / "Temp"))
{
if(FS.Local.FileExists(f)) FS.Local.DeleteFile(f);
}
DB.mtx = Mutex();
var p = DB.working / "conf.json";
if(FS.Local.FileExists(p))
DB.Config = Json.Decode(FS.ReadAllText(FS.Local, p));
else
DB.Config = {
AllowRegister=false,
};
var dbCon = DB.Open();
Sqlite.Exec(dbCon,"CREATE TABLE IF NOT EXISTS packages (id INTEGER PRIMARY KEY AUTOINCREMENT, packageName TEXT UNIQUE, accountId INTEGER);");
Sqlite.Exec(dbCon,"CREATE TABLE IF NOT EXISTS versions (id INTEGER PRIMARY KEY AUTOINCREMENT, packageId INTEGER, version INTEGER, description TEXT, type TEXT, maintainer TEXT, homepage TEXT, repo TEXT, license TEXT, uploadTime INTEGER);");
Sqlite.Exec(dbCon,"CREATE TABLE IF NOT EXISTS accounts (id INTEGER PRIMARY KEY AUTOINCREMENT, email TEXT UNIQUE, accountName TEXT UNIQUE, password_hash TEXT, password_salt TEXT, motto TEXT, verifyKey TEXT UNIQUE, flags INTEGER);");
Sqlite.Exec(dbCon,"CREATE TABLE IF NOT EXISTS sessions (id INTEGER PRIMARY KEY AUTOINCREMENT, accountId INTEGER, key STRING UNIQUE);");
Sqlite.Exec(dbCon,"CREATE TABLE IF NOT EXISTS reserved_prefixes (id INTEGER PRIMARY KEY AUTOINCREMENT, accountId INTEGER, prefix STRING UNIQUE);");
Sqlite.Close(dbCon);
}
DB.FLAG_ADMIN = 0b00000010;
DB.FLAG_VERIFIED = 0b00000001;
DB.FLAG_RESETPASS = 0b00001000;
DB.FLAG_VERIFY = 0b00000100;
DB.ITTR = 35000;
func DB.GetAccountId(email, password)
{
DB.Lock();
var dbCon = DB.Open();
var exec = Sqlite.Exec(dbCon,$"SELECT * FROM accounts WHERE email = {Sqlite.Escape(email)};");
Sqlite.Close(dbCon);
DB.Unlock();
var correct=false;
if(TypeOf(exec) == "List" && exec.Length > 0)
{
var salt = Crypto.Base64Decode(exec[0].password_salt);
var hash = Crypto.PBKDF2(password, salt, DB.ITTR,64,384);
var hashStr = Crypto.Base64Encode(hash);
if(exec[0].password_hash == hashStr)
{
return ParseLong(exec[0].id);
}
}
return -1;
}
func DB.SendVerifyEmail(email,name, verify_hash_str)
{
var emailText = $"
<h1>Hello {Net.Http.HtmlEncode(name)}</h1>
<span>To verify your account go to <a href=\"{DB.Config.Prefix.TrimEnd('/')}/verify?code={Net.Http.UrlEncode(verify_hash_str)}\">{DB.Config.Prefix.TrimEnd('/')}/verify?code={Net.Http.UrlEncode(verify_hash_str)}</a></span>
";
var data = {
server = DB.Config.MailConfig.Server,
auth = DB.Config.MailConfig.Auth,
domain = DB.Config.MailConfig.Domain,
from = DB.Config.MailConfig.From,
to = email.Replace(">","").Replace("\n",""),
subject = "Verify your account on CPKG",
body = {
type = "text/html",
data = emailText
}
};
Net.Smtp.Send(data);
}
func DB.CreateUser(email, name, password)
{
var res = {Success = true, Redirect="/"};
DB.Lock();
var dbCon = DB.Open();
if(Sqlite.Exec(dbCon, "SELECT * FROM accounts LIMIT 1;").Length == 0)
{
//create the admin
var salt = Crypto.RandomBytes(32, "CPKG");
var hash = Crypto.PBKDF2(password, salt, DB.ITTR,64,384);
var r = Sqlite.Exec(dbCon,$"INSERT INTO accounts (email, accountName, password_hash, password_salt, flags) values ({Sqlite.Escape(email)},{Sqlite.Escape(name)},{Sqlite.Escape(Crypto.Base64Encode(hash))},{Sqlite.Escape(Crypto.Base64Encode(salt))},{DB.FLAG_ADMIN|DB.FLAG_VERIFIED});");
if(TypeOf(r) == "String") res = {Success = false, Reason = r};
}
else
{
var exec = Sqlite.Exec(dbCon,$"SELECT * FROM accounts WHERE email = {Sqlite.Escape(email)};");
if(TypeOf(exec) == "List" && exec.Length > 0)
{
exec = Sqlite.Exec(dbCon,$"SELECT * FROM accounts WHERE accountName = {Sqlite.Escape(name)};");
if(TypeOf(exec) == "List" && exec.Length > 0)
{
res = {Success=false, Reason = "Email and Name already exists"};
}
else
{
res = {Success=false, Reason = "Email already exists"};
}
}
else {
exec = Sqlite.Exec(dbCon,$"SELECT * FROM accounts WHERE accountName = {Sqlite.Escape(name)};");
if(TypeOf(exec) == "List" && exec.Length > 0)
{
res = {Success=false, Reason = "Name already exists"};
}
else if(DB.Config.AllowRegister)
{
//email and name already exists
var salt = Crypto.RandomBytes(32, "CPKG");
var hash = Crypto.PBKDF2(password, salt, DB.ITTR,64,384);
var verify_hash = Crypto.RandomBytes(32, "CPKG");
var verify_hash_str = Crypto.Base64Encode(verify_hash);
var r = Sqlite.Exec(dbCon,$"INSERT INTO accounts (email, accountName, password_hash, password_salt, verifyKey, flags) values ({Sqlite.Escape(email)},{Sqlite.Escape(name)},{Sqlite.Escape(Crypto.Base64Encode(hash))},{Sqlite.Escape(Crypto.Base64Encode(salt))},{Sqlite.Escape(verify_hash_str)},{DB.FLAG_VERIFY});");
if(TypeOf(r) == "String") res = {Success = false, Reason = r};
if(DB.Config.MailConfig)
DB.SendVerifyEmail(email,name, verify_hash_str);
res = {Success=true, Redirect="/check_email"};
}
else
{
res = {Success=false, Reason="Registration is disabled on this server"};
}
}
}
Sqlite.Close(dbCon);
DB.Unlock();
return res;
}
func DB.GetPackageVersions(name)
{
var sql = $"SELECT * FROM packages p inner join versions v on p.id = v.packageId inner join accounts a on p.accountId = a.id WHERE p.packageName = {Sqlite.Escape(name)} ORDER BY v.version DESC;";
var dbCon = DB.Open();
var res = Sqlite.Exec(dbCon, sql);
Sqlite.Close(dbCon);
DB.Unlock();
var list = [];
each(var item : res)
{
var version = Version.FromLong(ParseLong(item.version)).ToString();
var uploadTime = ParseLong(item.uploadTime);
list.Add({
version,
download = $"./api/v1/download?name={Net.Http.UrlEncode(name)}&version={Net.Http.UrlEncode(version)}",
accountName = item.accountName,
description = item.description,
maintainer = item.maintainer,
type = item.type,
repo = item.repo,
homepage = item.homepage,
license = item.license,
uploadTime,
uploadDate=Time.UTCUsSlashDate(uploadTime)
});
}
return list;
}
func DB.QueryPackages(q, offset, limit)
{
if(TypeOf(offset) != "Long") offset = 0;
if(TypeOf(limit) != "Long") limit = 20;
var q2 = Sqlite.Escape($"%{q}%");
var sql = $"SELECT * FROM packages p inner join versions v on p.id = v.packageId and (p.packageName LIKE {q2} OR v.description LIKE {q2}) and v.version = (SELECT MAX(version) FROM versions WHERE packageId = v.packageId) INNER JOIN accounts a on a.id = p.accountId LIMIT {limit} OFFSET {offset};";
DB.Lock();
var dbCon = DB.Open();
var res = Sqlite.Exec(dbCon, sql);
Sqlite.Close(dbCon);
DB.Unlock();
var res2=[];
each(var item : res)
{
var uploadTime = ParseLong(item.uploadTime);
res2.Add({
accountName = item.accountName,
description = item.description,
homepage = item.homepage,
license = item.license,
maintainer = item.maintainer,
packageName = item.packageName,
uploadTime,
uploadDate = Time.UTCUsSlashDate(uploadTime),
repo = item.repo,
type = item.type,
version = Version.FromLong(ParseLong(item.version)).ToString()
});
}
return res2;
}