镜像自地址
https://github.com/JGZYES/ParlzPackageManger.git
已同步 2026-09-11 08:52:45 +08:00
feat(pmu): package upload/publish CLI + PHP service
Client src/pmu.c (independent binary, links out.c + sha256.c): - pmu register <email> <pwd>: local arithmetic human-verification, then POST register.php. - pmu login / logout / whoami: session token stored in ~/.pmu/config. - pmu ./x.pdm (or 'pmu publish x.pdm'): parses Package/Version/Architecture from the .pdm, computes sha256, uploads raw bytes with a Bearer token; the server auto-generates <pkg>.json and stores the .pdm; duplicate names are rejected. Server web/pmu/*.php: - lib.php: JSON responses, token auth, data store in web/pmu/data/*.json (gitignored). - register/login/logout/publish.php: account + session + upload + registry write (writes web/mirror/packages/<name>.json, copies .pdm, appends packages.json). - Duplicate package names -> 409; missing/invalid token -> 401. Also: .gitignore web/pmu/data/*.json; release.yml builds pmu per-platform.
这个提交包含在:
@@ -56,6 +56,9 @@ jobs:
|
|||||||
${{ matrix.cc }} -O2 -Wall -static -std=c11 -D_GNU_SOURCE -D_FORTIFY_SOURCE=0 -U_FORTIFY_SOURCE -DPMM_VERSION=\"$VER\" -o out/${{ matrix.bin }} \
|
${{ matrix.cc }} -O2 -Wall -static -std=c11 -D_GNU_SOURCE -D_FORTIFY_SOURCE=0 -U_FORTIFY_SOURCE -DPMM_VERSION=\"$VER\" -o out/${{ matrix.bin }} \
|
||||||
src/main.c src/json.c src/ini.c src/pmm.c src/http.c src/repo.c src/install.c \
|
src/main.c src/json.c src/ini.c src/pmm.c src/http.c src/repo.c src/install.c \
|
||||||
src/sha256.c src/sha1.c src/mirrors.c src/pdm.c src/out.c src/i18n.c
|
src/sha256.c src/sha1.c src/mirrors.c src/pdm.c src/out.c src/i18n.c
|
||||||
|
# -- pmu upload/publish client (also shipped per-platform)
|
||||||
|
${{ matrix.cc }} -O2 -Wall -static -std=c11 -D_GNU_SOURCE -D_FORTIFY_SOURCE=0 -U_FORTIFY_SOURCE -o out/pmu${{ matrix.ext }} \
|
||||||
|
src/pmu.c src/out.c src/sha256.c
|
||||||
|
|
||||||
- name: Package installers
|
- name: Package installers
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
+4
@@ -36,3 +36,7 @@
|
|||||||
web/.src/
|
web/.src/
|
||||||
web/git/
|
web/git/
|
||||||
web/releases/
|
web/releases/
|
||||||
|
|
||||||
|
# pmu upload service runtime state (accounts / sessions)
|
||||||
|
web/pmu/data/*.json
|
||||||
|
web/pmu/data/*.tmp
|
||||||
|
|||||||
@@ -0,0 +1,338 @@
|
|||||||
|
/* pmu.c — PMU : package upload / publish client for the PMM registry.
|
||||||
|
*
|
||||||
|
* pmu register <email> <password> create an account (local arithmetic captcha)
|
||||||
|
* pmu login <email> <password> log in, store a session token
|
||||||
|
* pmu logout revoke the stored token
|
||||||
|
* pmu whoami show logged-in email + server
|
||||||
|
* pmu ./foo.pdm publish a package (alias: pmu publish ./foo.pdm)
|
||||||
|
* pmu help | -h
|
||||||
|
*
|
||||||
|
* Talks to the PHP service under {server}/ (default https://pmm.parlz.com/pmu).
|
||||||
|
* Config is stored in ~/.pmu/config (Windows: D:\.pmu\config).
|
||||||
|
*/
|
||||||
|
#include "out.h"
|
||||||
|
#include "sha256.h"
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <time.h>
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
#include <direct.h>
|
||||||
|
#define PMU_MKDIR(p) _mkdir(p)
|
||||||
|
#else
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#define PMU_MKDIR(p) mkdir((p), 0755)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
static int g_no_color = 0;
|
||||||
|
|
||||||
|
static const char *pmu_server_default = "https://pmm.parlz.com/pmu";
|
||||||
|
|
||||||
|
/* ---- config dir / file: ~/.pmu (Windows D:\.pmu) ---- */
|
||||||
|
static void pmu_config_dir(char *buf, size_t sz) {
|
||||||
|
#ifdef _WIN32
|
||||||
|
snprintf(buf, sz, "D:\\.pmu");
|
||||||
|
#else
|
||||||
|
const char *home = getenv("HOME");
|
||||||
|
snprintf(buf, sz, "%s/.pmu", home && *home ? home : ".");
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
static char g_cfg[1400];
|
||||||
|
static char g_server[2048] = "";
|
||||||
|
static char g_token[128] = "";
|
||||||
|
static char g_email[256] = "";
|
||||||
|
|
||||||
|
static void cfg_init(void) {
|
||||||
|
char dir[1024];
|
||||||
|
pmu_config_dir(dir, sizeof(dir));
|
||||||
|
PMU_MKDIR(dir);
|
||||||
|
snprintf(g_cfg, sizeof(g_cfg), "%s/config", dir);
|
||||||
|
snprintf(g_server, sizeof(g_server), "%s", pmu_server_default);
|
||||||
|
FILE *f = fopen(g_cfg, "r");
|
||||||
|
if (f) {
|
||||||
|
char line[2400];
|
||||||
|
while (fgets(line, sizeof(line), f)) {
|
||||||
|
char *eq = strchr(line, '=');
|
||||||
|
if (!eq) continue;
|
||||||
|
char key[64]; size_t kl = (size_t)(eq - line);
|
||||||
|
while (kl && (line[kl-1] == ' ' || line[kl-1] == '\t')) kl--;
|
||||||
|
if (kl >= sizeof(key)) kl = sizeof(key) - 1;
|
||||||
|
memcpy(key, line, kl); key[kl] = 0;
|
||||||
|
char *val = eq + 1;
|
||||||
|
while (*val == ' ' || *val == '\t') val++;
|
||||||
|
char *e = val + strlen(val);
|
||||||
|
while (e > val && (e[-1] == '\n' || e[-1] == '\r' || e[-1] == ' ' || e[-1] == '\t')) *--e = 0;
|
||||||
|
if (strcmp(key, "server") == 0) snprintf(g_server, sizeof(g_server), "%s", val);
|
||||||
|
else if (strcmp(key, "token") == 0) snprintf(g_token, sizeof(g_token), "%s", val);
|
||||||
|
else if (strcmp(key, "email") == 0) snprintf(g_email, sizeof(g_email), "%s", val);
|
||||||
|
}
|
||||||
|
fclose(f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
static void cfg_save_token(const char *token, const char *email) {
|
||||||
|
snprintf(g_token, sizeof(g_token), "%s", token ? token : "");
|
||||||
|
snprintf(g_email, sizeof(g_email), "%s", email ? email : "");
|
||||||
|
FILE *f = fopen(g_cfg, "w");
|
||||||
|
if (!f) { pmm_error("无法写入 %s\n", g_cfg); return; }
|
||||||
|
fprintf(f, "server = %s\n", g_server);
|
||||||
|
fprintf(f, "token = %s\n", g_token);
|
||||||
|
fprintf(f, "email = %s\n", g_email);
|
||||||
|
fclose(f);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- HTTP POST via curl ---- */
|
||||||
|
static int http_post(const char *url, const char *auth, const char *body,
|
||||||
|
const char *file, char *out, size_t outs) {
|
||||||
|
char cmd[9000];
|
||||||
|
int i = 0;
|
||||||
|
if (file) { i += snprintf(cmd + i, sizeof(cmd) - (size_t)i, "curl -s -X POST %s --data-binary @'%s' '%s'",
|
||||||
|
auth ? auth : "", file, url); }
|
||||||
|
else { i += snprintf(cmd + i, sizeof(cmd) - (size_t)i, "curl -s -X POST %s -H 'Content-Type: application/json' --data-binary '%s' '%s'",
|
||||||
|
auth ? auth : "", body ? body : "", url); }
|
||||||
|
if (i < 0 || (size_t)i >= sizeof(cmd)) { pmm_error("请求过长\n"); return -1; }
|
||||||
|
FILE *f = popen(cmd, "r");
|
||||||
|
if (!f) return -1;
|
||||||
|
size_t got = fread(out, 1, outs - 1, f);
|
||||||
|
out[got] = '\0';
|
||||||
|
pclose(f);
|
||||||
|
return (int)got;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- tiny JSON getters (server returns {"ok":true,...,"error":".."}) ---- */
|
||||||
|
static const char *js_str(const char *body, const char *key) {
|
||||||
|
char pat[64];
|
||||||
|
snprintf(pat, sizeof(pat), "\"%s\":", key);
|
||||||
|
const char *p = strstr(body, pat);
|
||||||
|
if (!p) return NULL;
|
||||||
|
p += strlen(pat);
|
||||||
|
while (*p == ' ' || *p == '\t') p++;
|
||||||
|
if (*p != '"') return NULL;
|
||||||
|
p++;
|
||||||
|
const char *end = strchr(p, '"');
|
||||||
|
if (!end) return NULL;
|
||||||
|
return p; /* caller reads up to end (we return pointer; use length) */
|
||||||
|
}
|
||||||
|
static int js_ok(const char *body) { return strstr(body, "\"ok\":true") != NULL; }
|
||||||
|
static const char *js_err(const char *body) {
|
||||||
|
const char *p = strstr(body, "\"error\":");
|
||||||
|
if (!p) return NULL;
|
||||||
|
p += 8;
|
||||||
|
while (*p == ' ' || *p == '\t') p++;
|
||||||
|
if (*p == '"') p++;
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
static const char *js_val(const char *body, const char *key, char *buf, size_t sz) {
|
||||||
|
const char *p = js_str(body, key);
|
||||||
|
if (!p) return NULL;
|
||||||
|
const char *end = strchr(p, '"');
|
||||||
|
size_t n = end ? (size_t)(end - p) : strlen(p);
|
||||||
|
if (n >= sz) n = sz - 1;
|
||||||
|
memcpy(buf, p, n); buf[n] = 0;
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
static void js_err_print(const char *body) {
|
||||||
|
const char *e = js_err(body);
|
||||||
|
if (e) {
|
||||||
|
char buf[512]; size_t n = strlen(e);
|
||||||
|
const char *q = strchr(e, '"');
|
||||||
|
if (q && ((size_t)(q - e)) < n) n = (size_t)(q - e);
|
||||||
|
if (n >= sizeof(buf)) n = sizeof(buf) - 1;
|
||||||
|
memcpy(buf, e, n); buf[n] = 0;
|
||||||
|
pmm_error("%s\n", buf);
|
||||||
|
} else {
|
||||||
|
pmm_error("%s\n", body ? body : "(no response)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- local arithmetic human verification ---- */
|
||||||
|
static int captcha(void) {
|
||||||
|
srand((unsigned)time(NULL));
|
||||||
|
int a = rand() % 30 + 20; /* 20..49 */
|
||||||
|
int b = rand() % 10 + 3; /* 3..12 */
|
||||||
|
int op = rand() % 3; /* 0=+ 1=- 2=× */
|
||||||
|
int ans;
|
||||||
|
const char *sym;
|
||||||
|
if (op == 0) { ans = a + b; sym = "+"; }
|
||||||
|
else if (op == 1) { ans = a - b; sym = "-"; }
|
||||||
|
else { ans = a * b; sym = "\xc3\x97"; } /* × */
|
||||||
|
printf("人机验证: %d %s %d = ?\n答案: ", a, sym, b);
|
||||||
|
fflush(stdout);
|
||||||
|
char in[64];
|
||||||
|
if (!fgets(in, sizeof(in), stdin)) return 0;
|
||||||
|
if (atoi(in) != ans) { pmm_error("验证错误\n"); return 0; }
|
||||||
|
pmm_success("人机验证通过\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- parse Package/Version/Architecture out of a .pdm's control ---- */
|
||||||
|
static void pdm_meta(const char *path, char *pkg, size_t pkgsz,
|
||||||
|
char *ver, size_t versz, char *arch, size_t archsz) {
|
||||||
|
pkg[0] = ver[0] = arch[0] = 0;
|
||||||
|
char cmd[1800];
|
||||||
|
snprintf(cmd, sizeof(cmd), "tar -xOf '%s' control.tar.gz 2>/dev/null | tar -xzOf - pdm-control 2>/dev/null", path);
|
||||||
|
FILE *f = popen(cmd, "r");
|
||||||
|
if (!f) return;
|
||||||
|
char line[512];
|
||||||
|
while (fgets(line, sizeof(line), f)) {
|
||||||
|
char *e = line + strlen(line);
|
||||||
|
while (e > line && (e[-1] == '\n' || e[-1] == '\r')) *--e = 0;
|
||||||
|
if (strncmp(line, "Package:", 8) == 0 && !pkg[0]) snprintf(pkg, pkgsz, "%s", line + 8);
|
||||||
|
else if (strncmp(line, "Version:", 8) == 0 && !ver[0]) snprintf(ver, versz, "%s", line + 8);
|
||||||
|
else if (strncmp(line, "Architecture:", 13) == 0 && !arch[0]) snprintf(arch, archsz, "%s", line + 13);
|
||||||
|
}
|
||||||
|
pclose(f);
|
||||||
|
/* left-trim spaces */
|
||||||
|
for (char *s = pkg; *s && (*s == ' ' || *s == '\t'); s++) memmove(s, s + 1, strlen(s));
|
||||||
|
for (char *s = ver; *s && (*s == ' ' || *s == '\t'); s++) memmove(s, s + 1, strlen(s));
|
||||||
|
for (char *s = arch; *s && (*s == ' ' || *s == '\t'); s++) memmove(s, s + 1, strlen(s));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void usage(void) {
|
||||||
|
printf("pmu — 发布 PMM 包\n\n"
|
||||||
|
"用法:\n"
|
||||||
|
" pmu register <email> <password> 注册账号(本地算术人机验证)\n"
|
||||||
|
" pmu login <email> <password> 登录并保存 token\n"
|
||||||
|
" pmu logout 撤销并清除本地 token\n"
|
||||||
|
" pmu whoami 当前登录邮箱 + 服务器\n"
|
||||||
|
" pmu ./xxxx.pdm 发布包(自动生成 json)\n"
|
||||||
|
" pmu help | -h 帮助\n\n"
|
||||||
|
"服务端默认: %s (可用 pmu 配置 server= 覆盖)\n", pmu_server_default);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Detect the CPU architecture (used as the registry 'arch' field). */
|
||||||
|
static void detect_cpu_arch(char *out, size_t sz) {
|
||||||
|
out[0] = 0;
|
||||||
|
#ifdef _WIN32
|
||||||
|
const char *p = getenv("PROCESSOR_ARCHITECTURE");
|
||||||
|
if (p && (strcmp(p, "ARM64") == 0 || strcmp(p, "arm64") == 0)) snprintf(out, sz, "aarch64");
|
||||||
|
else snprintf(out, sz, "amd64");
|
||||||
|
#else
|
||||||
|
FILE *f = popen("uname -m 2>/dev/null", "r");
|
||||||
|
if (f) {
|
||||||
|
char m[64];
|
||||||
|
if (fgets(m, sizeof(m), f)) {
|
||||||
|
char *e = m + strlen(m);
|
||||||
|
while (e > m && (e[-1] == '\n' || e[-1] == '\r')) *--e = 0;
|
||||||
|
if (strcmp(m, "x86_64") == 0 || strcmp(m, "amd64") == 0) snprintf(out, sz, "amd64");
|
||||||
|
else if (strcmp(m, "aarch64") == 0 || strcmp(m, "arm64") == 0) snprintf(out, sz, "aarch64");
|
||||||
|
else snprintf(out, sz, "%s", m);
|
||||||
|
}
|
||||||
|
pclose(f);
|
||||||
|
}
|
||||||
|
if (!out[0]) snprintf(out, sz, "amd64");
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char **argv) {
|
||||||
|
g_no_color = getenv("PMM_NO_COLOR") ? 1 : 0;
|
||||||
|
cfg_init();
|
||||||
|
if (argc < 2) { usage(); return 1; }
|
||||||
|
|
||||||
|
if (strcmp(argv[1], "help") == 0 || strcmp(argv[1], "-h") == 0 || strcmp(argv[1], "--help") == 0) { usage(); return 0; }
|
||||||
|
|
||||||
|
char resp[8192];
|
||||||
|
char url[2300], auth[256] = "";
|
||||||
|
|
||||||
|
/* ---- register ---- */
|
||||||
|
if (strcmp(argv[1], "register") == 0) {
|
||||||
|
if (argc < 4) { pmm_error("用法: pmu register <email> <password>\n"); return 1; }
|
||||||
|
const char *email = argv[2], *pass = argv[3];
|
||||||
|
if (strchr(email, '\'') || strchr(pass, '\'')) { pmm_error("邮箱/密码不能含单引号\n"); return 1; }
|
||||||
|
if (!captcha()) return 1;
|
||||||
|
snprintf(url, sizeof(url), "%s/register.php", g_server);
|
||||||
|
char json[1024];
|
||||||
|
snprintf(json, sizeof(json), "{\"email\":\"%s\",\"password\":\"%s\"}", email, pass);
|
||||||
|
http_post(url, NULL, json, NULL, resp, sizeof(resp));
|
||||||
|
if (js_ok(resp)) { pmm_success("注册成功: %s\n", email); cfg_save_token("", email); }
|
||||||
|
else js_err_print(resp);
|
||||||
|
return js_ok(resp) ? 0 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- login ---- */
|
||||||
|
if (strcmp(argv[1], "login") == 0) {
|
||||||
|
if (argc < 4) { pmm_error("用法: pmu login <email> <password>\n"); return 1; }
|
||||||
|
const char *email = argv[2], *pass = argv[3];
|
||||||
|
if (strchr(email, '\'') || strchr(pass, '\'')) { pmm_error("邮箱/密码不能含单引号\n"); return 1; }
|
||||||
|
snprintf(url, sizeof(url), "%s/login.php", g_server);
|
||||||
|
char json[1024];
|
||||||
|
snprintf(json, sizeof(json), "{\"email\":\"%s\",\"password\":\"%s\"}", email, pass);
|
||||||
|
http_post(url, NULL, json, NULL, resp, sizeof(resp));
|
||||||
|
if (js_ok(resp)) {
|
||||||
|
char tok[256] = "";
|
||||||
|
js_val(resp, "token", tok, sizeof(tok));
|
||||||
|
cfg_save_token(tok, email);
|
||||||
|
pmm_success("登录成功: %s\n", email);
|
||||||
|
} else js_err_print(resp);
|
||||||
|
return js_ok(resp) ? 0 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- logout ---- */
|
||||||
|
if (strcmp(argv[1], "logout") == 0) {
|
||||||
|
if (g_token[0]) {
|
||||||
|
snprintf(auth, sizeof(auth), "-H 'Authorization: Bearer %s'", g_token);
|
||||||
|
snprintf(url, sizeof(url), "%s/logout.php", g_server);
|
||||||
|
http_post(url, auth, "{}", NULL, resp, sizeof(resp));
|
||||||
|
}
|
||||||
|
cfg_save_token("", "");
|
||||||
|
pmm_success("已登出\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- whoami ---- */
|
||||||
|
if (strcmp(argv[1], "whoami") == 0) {
|
||||||
|
pmm_info("服务器: %s\n", g_server);
|
||||||
|
pmm_info("邮箱: %s\n", g_email[0] ? g_email : "(未登录)");
|
||||||
|
pmm_info("token: %s\n", g_token[0] ? "(已登录)" : "(无)");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- publish ./x.pdm (or "publish") ---- */
|
||||||
|
const char *file = NULL;
|
||||||
|
if (strcmp(argv[1], "publish") == 0) file = argc > 2 ? argv[2] : NULL;
|
||||||
|
else if (strlen(argv[1]) > 4 && strcmp(argv[1] + strlen(argv[1]) - 4, ".pdm") == 0) file = argv[1];
|
||||||
|
if (file) {
|
||||||
|
if (!g_token[0]) { pmm_error("请先 pmu login\n"); return 1; }
|
||||||
|
char pkg[256], ver[128], arch[64];
|
||||||
|
pdm_meta(file, pkg, sizeof(pkg), ver, sizeof(ver), arch, sizeof(arch));
|
||||||
|
if (!pkg[0] || !ver[0]) { pmm_error("无法解析 %s 的 Package/Version\n", file); return 1; }
|
||||||
|
char hex[128];
|
||||||
|
if (pmm_sha256_file(file, hex) != 0) { pmm_error("无法计算 sha256\n"); return 1; }
|
||||||
|
/* URL-encode description (name/version/arch/os are safe charsets) */
|
||||||
|
char desc[512];
|
||||||
|
snprintf(desc, sizeof(desc), "%s %s", pkg, ver);
|
||||||
|
char enc[2100]; size_t ei = 0;
|
||||||
|
for (const char *p = desc; *p && ei + 4 < sizeof(enc); p++) {
|
||||||
|
if ((*p >= 'A' && *p <= 'Z') || (*p >= 'a' && *p <= 'z') || (*p >= '0' && *p <= '9') ||
|
||||||
|
*p == '-' || *p == '_' || *p == '.') enc[ei++] = *p;
|
||||||
|
else if (*p == ' ') { enc[ei++] = '%'; enc[ei++] = '2'; enc[ei++] = '0'; }
|
||||||
|
else { enc[ei++] = '%'; enc[ei++] = "0123456789ABCDEF"[((unsigned char)*p) >> 4];
|
||||||
|
enc[ei++] = "0123456789ABCDEF"[((unsigned char)*p) & 15]; }
|
||||||
|
}
|
||||||
|
enc[ei] = 0;
|
||||||
|
char *sp = enc; while (*sp == ' ') sp++; /* trim leading space */
|
||||||
|
/* os = the .pdm control's Architecture (pmm convention: linux/windows);
|
||||||
|
* arch = the CPU arch, detected separately. */
|
||||||
|
char os[16];
|
||||||
|
snprintf(os, sizeof(os), "%s", arch[0] ? arch : "linux");
|
||||||
|
if (strcmp(os, "windows") != 0 && strcmp(os, "macos") != 0 && strcmp(os, "linux") != 0)
|
||||||
|
snprintf(os, sizeof(os), "linux");
|
||||||
|
char cpuarch[16] = "";
|
||||||
|
detect_cpu_arch(cpuarch, sizeof(cpuarch));
|
||||||
|
snprintf(url, sizeof(url), "%s/publish.php?name=%s&version=%s&arch=%s&os=%s&description=%s",
|
||||||
|
g_server, pkg, ver, cpuarch[0] ? cpuarch : "amd64", os, sp);
|
||||||
|
snprintf(auth, sizeof(auth), "-H 'Authorization: Bearer %s'", g_token);
|
||||||
|
pmm_info("正在发布 %s %s ...\n", pkg, ver);
|
||||||
|
if (http_post(url, auth, NULL, file, resp, sizeof(resp)) <= 0) { pmm_error("发布失败(无响应)\n"); return 1; }
|
||||||
|
if (js_ok(resp)) {
|
||||||
|
char u[2200] = ""; js_val(resp, "url", u, sizeof(u));
|
||||||
|
pmm_success("发布成功: %s\n", u[0] ? u : file);
|
||||||
|
} else js_err_print(resp);
|
||||||
|
return js_ok(resp) ? 0 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
pmm_error("未知命令: %s (试试 pmu help)\n", argv[1]);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# deny direct access to web/pmu/data (user accounts / tokens / verify state)
|
||||||
|
Require all denied
|
||||||
+44
@@ -0,0 +1,44 @@
|
|||||||
|
<?php
|
||||||
|
/* web/pmu/lib.php — shared helpers for the PMU package-upload service.
|
||||||
|
* Multi-value state lives in web/pmu/data/*.json (gitignored). */
|
||||||
|
define('PMU_DATA', __DIR__ . '/data');
|
||||||
|
if (!is_dir(PMU_DATA)) @mkdir(PMU_DATA, 0777, true);
|
||||||
|
|
||||||
|
function pmu_json($arr) {
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
echo json_encode($arr, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
function pmu_ok($extra = []) { pmu_json(array_merge(['ok' => true], $extra)); }
|
||||||
|
function pmu_fail($msg, $code = 400) { http_response_code($code); pmu_json(['ok' => false, 'error' => $msg]); }
|
||||||
|
|
||||||
|
function pmu_read_json() {
|
||||||
|
$raw = file_get_contents('php://input');
|
||||||
|
$d = json_decode($raw ?: 'null', true);
|
||||||
|
return is_array($d) ? $d : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function pmu_load($file) {
|
||||||
|
$p = PMU_DATA . '/' . $file;
|
||||||
|
return is_file($p) ? (json_decode(file_get_contents($p), true) ?: []) : [];
|
||||||
|
}
|
||||||
|
function pmu_save($file, $arr) {
|
||||||
|
file_put_contents(PMU_DATA . '/' . $file,
|
||||||
|
json_encode($arr, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
|
||||||
|
}
|
||||||
|
|
||||||
|
function pmu_token() { return bin2hex(random_bytes(24)); }
|
||||||
|
|
||||||
|
/* Resolve the caller identity from a bearer token (Authorization: Bearer <tok>,
|
||||||
|
* or ?token=, or POST/token= for clients that can't set headers). */
|
||||||
|
function pmu_require_auth_token() {
|
||||||
|
$hdr = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
|
||||||
|
$tok = null;
|
||||||
|
if (preg_match('/Bearer\s+(.+)/i', $hdr, $m)) $tok = trim($m[1]);
|
||||||
|
if (!$tok) $tok = $_GET['token'] ?? ($_POST['token'] ?? null);
|
||||||
|
if (!$tok) pmu_fail('unauthorized', 401);
|
||||||
|
$s = pmu_load('sessions.json');
|
||||||
|
$email = $s[$tok] ?? null;
|
||||||
|
if (!$email) pmu_fail('invalid token', 401);
|
||||||
|
return [$tok, $email];
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<?php
|
||||||
|
/* web/pmu/login.php — verify credentials, issue a session token. */
|
||||||
|
define('PMM_SITE', 1);
|
||||||
|
require __DIR__ . '/lib.php';
|
||||||
|
|
||||||
|
$d = pmu_read_json();
|
||||||
|
$email = trim((string)($d['email'] ?? ''));
|
||||||
|
$pass = (string)($d['password'] ?? '');
|
||||||
|
$users = pmu_load('users.json');
|
||||||
|
if (!isset($users[$email]) || !password_verify($pass, $users[$email]['hash'])) pmu_fail('invalid credentials', 401);
|
||||||
|
|
||||||
|
$tok = pmu_token();
|
||||||
|
$s = pmu_load('sessions.json');
|
||||||
|
$s[$tok] = $email;
|
||||||
|
pmu_save('sessions.json', $s);
|
||||||
|
pmu_ok(['token' => $tok, 'email' => $email]);
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<?php
|
||||||
|
/* web/pmu/logout.php — revoke the caller's session token. */
|
||||||
|
define('PMM_SITE', 1);
|
||||||
|
require __DIR__ . '/lib.php';
|
||||||
|
|
||||||
|
[$tok, $email] = pmu_require_auth_token();
|
||||||
|
$s = pmu_load('sessions.json');
|
||||||
|
unset($s[$tok]);
|
||||||
|
pmu_save('sessions.json', $s);
|
||||||
|
pmu_ok(['logged_out_email' => $email]);
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
<?php
|
||||||
|
/* web/pmu/publish.php — upload a completed .pdm (raw body) and publish it to the
|
||||||
|
* mirror registry: writes/updates <pkg>.json, stores the .pdm under
|
||||||
|
* web/mirror/packages/<pkg>/, and appends the name to packages.json.
|
||||||
|
* Duplicate package names are rejected (409). Requires a valid bearer token. */
|
||||||
|
define('PMM_SITE', 1);
|
||||||
|
require __DIR__ . '/lib.php';
|
||||||
|
|
||||||
|
[$tok, $email] = pmu_require_auth_token();
|
||||||
|
|
||||||
|
$name = trim((string)($_GET['name'] ?? ''));
|
||||||
|
$ver = trim((string)($_GET['version'] ?? ''));
|
||||||
|
$arch = trim((string)($_GET['arch'] ?? 'amd64'));
|
||||||
|
$os = trim((string)($_GET['os'] ?? 'linux'));
|
||||||
|
$desc = trim((string)($_GET['description'] ?? $name));
|
||||||
|
|
||||||
|
if (!preg_match('/^[A-Za-z0-9][A-Za-z0-9._-]*$/', $name)) pmu_fail('invalid package name');
|
||||||
|
if ($ver === '') pmu_fail('missing version');
|
||||||
|
if ($arch === '') $arch = 'amd64';
|
||||||
|
if ($os !== 'windows' && $os !== 'linux' && $os !== 'macos') pmu_fail('invalid os');
|
||||||
|
|
||||||
|
$body = file_get_contents('php://input');
|
||||||
|
if (strlen($body) === 0) pmu_fail('empty upload body');
|
||||||
|
if (strlen($body) > 100 * 1024 * 1024) pmu_fail('package too large (>100 MB)');
|
||||||
|
|
||||||
|
$regDir = __DIR__ . '/../mirror/packages';
|
||||||
|
$pkgJson = $regDir . '/' . $name . '.json';
|
||||||
|
|
||||||
|
/* duplicate-name reject: the package name is already published */
|
||||||
|
if (is_file($pkgJson)) pmu_fail("package name '$name' already exists", 409);
|
||||||
|
|
||||||
|
$sha = hash('sha256', $body);
|
||||||
|
$file = $ver . '-' . $os . '-' . $arch . '.pdm';
|
||||||
|
$pkgDir = $regDir . '/' . $name;
|
||||||
|
if (!is_dir($pkgDir)) @mkdir($pkgDir, 0777, true);
|
||||||
|
file_put_contents($pkgDir . '/' . $file, $body);
|
||||||
|
|
||||||
|
$url = 'https://pmm.parlz.com/mirror/packages/' . $name . '/' . $file;
|
||||||
|
$meta = [
|
||||||
|
'name' => $name,
|
||||||
|
'version' => $ver,
|
||||||
|
'os' => $os,
|
||||||
|
'arch' => $arch,
|
||||||
|
'description' => $desc,
|
||||||
|
'variants' => [[
|
||||||
|
'name' => $name,
|
||||||
|
'version' => $ver,
|
||||||
|
'os' => $os,
|
||||||
|
'arch' => $arch,
|
||||||
|
'file' => $file,
|
||||||
|
'url' => $url,
|
||||||
|
'sha256' => $sha,
|
||||||
|
'description' => $desc,
|
||||||
|
]],
|
||||||
|
];
|
||||||
|
file_put_contents($pkgJson, json_encode($meta, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
|
||||||
|
|
||||||
|
/* append the name to the aggregate index if new */
|
||||||
|
$pk = $regDir . '/packages.json';
|
||||||
|
$list = is_file($pk) ? (json_decode(file_get_contents($pk), true) ?: []) : [];
|
||||||
|
if (!in_array($name, $list, true)) { $list[] = $name; file_put_contents($pk, json_encode($list, JSON_PRETTY_PRINT)); }
|
||||||
|
|
||||||
|
pmu_ok(['name' => $name, 'version' => $ver, 'file' => $file, 'url' => $url, 'sha256' => $sha]);
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<?php
|
||||||
|
/* web/pmu/register.php — create an account. Human verification is done client-side:
|
||||||
|
* the pmu client asks a local arithmetic question before calling here. */
|
||||||
|
define('PMM_SITE', 1);
|
||||||
|
require __DIR__ . '/lib.php';
|
||||||
|
|
||||||
|
$d = pmu_read_json();
|
||||||
|
$email = trim((string)($d['email'] ?? ''));
|
||||||
|
$pass = (string)($d['password'] ?? '');
|
||||||
|
if (filter_var($email, FILTER_VALIDATE_EMAIL) === false) pmu_fail('invalid email');
|
||||||
|
if (strlen($pass) < 6) pmu_fail('password too short (>= 6 chars)');
|
||||||
|
|
||||||
|
$users = pmu_load('users.json');
|
||||||
|
if (isset($users[$email])) pmu_fail('email already registered', 409);
|
||||||
|
|
||||||
|
$users[$email] = ['email' => $email, 'hash' => password_hash($pass, PASSWORD_DEFAULT), 'created' => time()];
|
||||||
|
pmu_save('users.json', $users);
|
||||||
|
pmu_ok(['email' => $email]);
|
||||||
在新工单中引用
屏蔽一个用户