mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-09-10 18:34:18 +08:00
🐤 Sync 2026-06-24 03:33:40
This commit is contained in:
@@ -2,14 +2,14 @@ include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-run
|
||||
PKG_VERSION:=1.0.0
|
||||
PKG_RELEASE:=4
|
||||
PKG_RELEASE:=5
|
||||
|
||||
LUCI_TITLE:=LuCI support for running makeself .run installers
|
||||
LUCI_DESCRIPTION:=Upload and execute makeself-generated .run installers from LuCI.
|
||||
LUCI_TITLE:=LuCI support for running installers and packages
|
||||
LUCI_DESCRIPTION:=Upload and execute .run, .sh, .ipk and .apk files from LuCI.
|
||||
LUCI_DEPENDS:=+luci-base +rpcd +libubox
|
||||
LUCI_PKGARCH:=all
|
||||
|
||||
PKG_MAINTAINER:=wukongdaily <2666180@gmail.com>
|
||||
PKG_MAINTAINER:=wukongdaily <wukongdaily@gmail.com>
|
||||
|
||||
# 定义版本号用于缓存刷新
|
||||
define Package/luci-app-run/postinst
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"zh": {
|
||||
"title": "Run安装器",
|
||||
"desc": "在路由器上上传并执行脚本或安装包,注意架构务必匹配。",
|
||||
"drop_tip": "拖入文件,或从电脑选择。",
|
||||
"choose_file": "选择 .run 或 .sh 文件",
|
||||
"choose_ipk": "选择 .ipk 包",
|
||||
"choose_apk": "选择 .apk 包",
|
||||
"execute": "执行",
|
||||
"clean_up": "清理",
|
||||
"upload_title": "上传文件",
|
||||
"log_title": "执行日志",
|
||||
"clean_done": "临时文件与日志已清理。",
|
||||
"only_supported": "仅支持 .run、.sh、.ipk 和 .apk 文件。",
|
||||
"prepare_upload": "准备上传:%s (%s)",
|
||||
"upload_failed": "上传失败。",
|
||||
"uploading": "正在上传 %s:%d%%",
|
||||
"upload_err": "上传请求失败。",
|
||||
"upload_invalid": "上传返回格式无效。",
|
||||
"upload_done": "上传完成:%s (%s)",
|
||||
"starting": "正在启动安装器...",
|
||||
"started": "安装器已启动,PID %d。",
|
||||
"running": "安装器正在运行。",
|
||||
"last_file": "上一次安装包:%s",
|
||||
"err_no_opkg": "您的系统不支持 ipk 包安装,请选择 apk 包。",
|
||||
"err_no_apk": "您的系统不支持 apk 包安装,请选择 ipk 包。"
|
||||
},
|
||||
"en": {
|
||||
"title": "Run Installer",
|
||||
"desc": "Upload and execute scripts or packages on this router, ensuring architecture compatibility.",
|
||||
"drop_tip": "Drop a file here, or choose one from your computer.",
|
||||
"choose_file": "Choose .run or .sh file",
|
||||
"choose_ipk": "Choose .ipk package",
|
||||
"choose_apk": "Choose .apk package",
|
||||
"execute": "Execute",
|
||||
"clean_up": "Clean up",
|
||||
"upload_title": "Upload file",
|
||||
"log_title": "Execution log",
|
||||
"clean_done": "Temporary files and logs were removed.",
|
||||
"only_supported": "Only .run, .sh, .ipk and .apk files are accepted.",
|
||||
"prepare_upload": "Preparing upload: %s (%s)",
|
||||
"upload_failed": "Upload failed.",
|
||||
"uploading": "Uploading %s: %d%%",
|
||||
"upload_err": "Upload request failed.",
|
||||
"upload_invalid": "Invalid upload response.",
|
||||
"upload_done": "Upload complete: %s (%s)",
|
||||
"starting": "Starting installer...",
|
||||
"started": "Installer started, PID %d.",
|
||||
"running": "Installer is running.",
|
||||
"last_file": "Last installer: %s",
|
||||
"err_no_opkg": "Your system does not support ipk package installation. Please use .apk packages instead.",
|
||||
"err_no_apk": "Your system does not support apk package installation. Please use .ipk packages instead."
|
||||
}
|
||||
}
|
||||
@@ -4,10 +4,37 @@
|
||||
'require ui';
|
||||
'require poll';
|
||||
|
||||
// ====================== 纯JSON国际化 · 中英双语 ======================
|
||||
const RUN_LANG = (function () {
|
||||
var I18N = null;
|
||||
|
||||
function loadI18N() {
|
||||
if (I18N)
|
||||
return Promise.resolve();
|
||||
|
||||
return new Promise(function (resolve, reject) {
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open('GET', '/luci-static/resources/view/run/i18n.json', true);
|
||||
xhr.onload = function () {
|
||||
if (xhr.status >= 200 && xhr.status < 300) {
|
||||
try {
|
||||
I18N = JSON.parse(xhr.responseText);
|
||||
resolve();
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
} else {
|
||||
reject(new Error('Failed to load i18n.json'));
|
||||
}
|
||||
};
|
||||
xhr.onerror = function () {
|
||||
reject(new Error('Network error loading i18n.json'));
|
||||
};
|
||||
xhr.send();
|
||||
});
|
||||
}
|
||||
|
||||
function getLang() {
|
||||
try {
|
||||
const m = document.cookie.match(/luci_lang=([a-zA-Z-]+)/);
|
||||
var m = document.cookie.match(/luci_lang=([a-zA-Z-]+)/);
|
||||
if (m) return m[1].substring(0, 2).toLowerCase();
|
||||
|
||||
if (window.L && L.env && L.env.lang)
|
||||
@@ -17,60 +44,17 @@ const RUN_LANG = (function () {
|
||||
} catch (e) {
|
||||
return 'zh';
|
||||
}
|
||||
})();
|
||||
|
||||
const I18N = {
|
||||
zh: {
|
||||
title: "Run安装器",
|
||||
desc: "在路由器上上传并执行 .run 安装包或 .sh 脚本,注意架构务必匹配。",
|
||||
drop_tip: "拖入一个 .run 或 .sh 文件,或从电脑选择。",
|
||||
choose_file: "选择 .run 或 .sh 文件",
|
||||
execute: "执行",
|
||||
clean_up: "清理",
|
||||
upload_title: "上传安装脚本 (.run / .sh)",
|
||||
log_title: "执行日志",
|
||||
clean_done: "临时文件与日志已清理。",
|
||||
only_run: "仅支持 .run 和 .sh 文件。",
|
||||
prepare_upload: "准备上传:%s (%s)",
|
||||
upload_failed: "上传失败。",
|
||||
uploading: "正在上传 %s:%d%%",
|
||||
upload_err: "上传请求失败。",
|
||||
upload_invalid: "上传返回格式无效。",
|
||||
upload_done: "上传完成:%s (%s)",
|
||||
starting: "正在启动安装器...",
|
||||
started: "安装器已启动,PID %d。",
|
||||
running: "安装器正在运行。",
|
||||
last_file: "上一次安装包:%s"
|
||||
},
|
||||
en: {
|
||||
title: "Run Installer",
|
||||
desc: "Upload and execute .run packages or .sh scripts on this router, ensuring architecture compatibility.",
|
||||
drop_tip: "Drop a .run or .sh file here, or choose one from your computer.",
|
||||
choose_file: "Choose .run or .sh file",
|
||||
execute: "Execute",
|
||||
clean_up: "Clean up",
|
||||
upload_title: "Upload installer script (.run / .sh)",
|
||||
log_title: "Execution log",
|
||||
clean_done: "Temporary files and logs were removed.",
|
||||
only_run: "Only .run and .sh files are accepted.",
|
||||
prepare_upload: "Preparing upload: %s (%s)",
|
||||
upload_failed: "Upload failed.",
|
||||
uploading: "Uploading %s: %d%%",
|
||||
upload_err: "Upload request failed.",
|
||||
upload_invalid: "Invalid upload response.",
|
||||
upload_done: "Upload complete: %s (%s)",
|
||||
starting: "Starting installer...",
|
||||
started: "Installer started, PID %d.",
|
||||
running: "Installer is running.",
|
||||
last_file: "Last installer: %s"
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function _(key) {
|
||||
const str = I18N[RUN_LANG]?.[key] || I18N.zh[key] || key;
|
||||
return str.format.apply(str, Array.prototype.slice.call(arguments, 1));
|
||||
if (!I18N)
|
||||
return key;
|
||||
|
||||
var lang = getLang();
|
||||
var str = I18N[lang]?.[key] || I18N.zh[key] || key;
|
||||
var args = Array.prototype.slice.call(arguments, 1);
|
||||
return str.format.apply(str, args);
|
||||
}
|
||||
// ====================================================================
|
||||
|
||||
var uploadStart = rpc.declare({
|
||||
object: 'luci-app-run',
|
||||
@@ -106,6 +90,11 @@ var getVersion = rpc.declare({
|
||||
method: 'version'
|
||||
});
|
||||
|
||||
var getCapabilities = rpc.declare({
|
||||
object: 'luci-app-run',
|
||||
method: 'capabilities'
|
||||
});
|
||||
|
||||
var readLog = rpc.declare({
|
||||
object: 'luci-app-run',
|
||||
method: 'read_log',
|
||||
@@ -146,23 +135,53 @@ return view.extend({
|
||||
logOffset: 0,
|
||||
currentUploadId: null,
|
||||
appVersion: 'unknown',
|
||||
capabilities: { opkg: 1, apk: 1 },
|
||||
|
||||
load: function () {
|
||||
var self = this;
|
||||
return getVersion().then(function (res) {
|
||||
if (res && res.version) {
|
||||
self.appVersion = res.version;
|
||||
// 检查版本是否变化,如果变化则强制刷新页面
|
||||
var storedVersion = localStorage.getItem('luci-app-run-version');
|
||||
if (storedVersion && storedVersion !== self.appVersion) {
|
||||
localStorage.setItem('luci-app-run-version', self.appVersion);
|
||||
window.location.reload(true);
|
||||
} else {
|
||||
localStorage.setItem('luci-app-run-version', self.appVersion);
|
||||
return loadI18N().then(function () {
|
||||
return getVersion().then(function (res) {
|
||||
if (res && res.version) {
|
||||
self.appVersion = res.version;
|
||||
var storedVersion = localStorage.getItem('luci-app-run-version');
|
||||
if (storedVersion && storedVersion !== self.appVersion) {
|
||||
localStorage.setItem('luci-app-run-version', self.appVersion);
|
||||
window.location.reload(true);
|
||||
} else {
|
||||
localStorage.setItem('luci-app-run-version', self.appVersion);
|
||||
}
|
||||
}
|
||||
}
|
||||
return getStatus().catch(function () {
|
||||
return {};
|
||||
return getCapabilities().then(function (cap) {
|
||||
if (cap) {
|
||||
self.capabilities = {
|
||||
opkg: cap.opkg || 0,
|
||||
apk: cap.apk || 0
|
||||
};
|
||||
}
|
||||
return getStatus().catch(function () {
|
||||
return {};
|
||||
});
|
||||
}).catch(function () {
|
||||
return getStatus().catch(function () {
|
||||
return {};
|
||||
});
|
||||
});
|
||||
}).catch(function () {
|
||||
return getCapabilities().then(function (cap) {
|
||||
if (cap) {
|
||||
self.capabilities = {
|
||||
opkg: cap.opkg || 0,
|
||||
apk: cap.apk || 0
|
||||
};
|
||||
}
|
||||
return getStatus().catch(function () {
|
||||
return {};
|
||||
});
|
||||
}).catch(function () {
|
||||
return getStatus().catch(function () {
|
||||
return {};
|
||||
});
|
||||
});
|
||||
});
|
||||
}).catch(function () {
|
||||
return getStatus().catch(function () {
|
||||
@@ -176,7 +195,19 @@ return view.extend({
|
||||
|
||||
var fileInput = E('input', {
|
||||
'type': 'file',
|
||||
accept: '.run,.sh,application/x-shellscript,application/octet-stream',
|
||||
accept: '.run,.sh,.ipk,.apk,application/x-shellscript,application/octet-stream',
|
||||
style: 'display:none'
|
||||
});
|
||||
|
||||
var ipkInput = E('input', {
|
||||
'type': 'file',
|
||||
accept: '.ipk',
|
||||
style: 'display:none'
|
||||
});
|
||||
|
||||
var apkInput = E('input', {
|
||||
'type': 'file',
|
||||
accept: '.apk',
|
||||
style: 'display:none'
|
||||
});
|
||||
|
||||
@@ -194,17 +225,36 @@ return view.extend({
|
||||
}, ['']);
|
||||
|
||||
var pickButton = E('button', {
|
||||
class: 'btn cbi-button cbi-button-apply',
|
||||
class: 'cbi-button cbi-button-apply run-btn',
|
||||
style: 'background:#333!important;background-color:#333!important;background-image:none!important;color:#fff!important;border-color:#333!important;box-shadow:none!important;text-shadow:none!important;opacity:1!important',
|
||||
click: function (ev) {
|
||||
ev.preventDefault();
|
||||
fileInput.click();
|
||||
}
|
||||
}, [_('choose_file')]);
|
||||
|
||||
var ipkButton = E('button', {
|
||||
class: 'cbi-button cbi-button-add run-btn',
|
||||
style: 'margin-left:10px;background:#2E7D32!important;background-color:#2E7D32!important;background-image:none!important;color:#fff!important;border-color:#2E7D32!important;box-shadow:none!important;text-shadow:none!important;opacity:1!important',
|
||||
click: function (ev) {
|
||||
ev.preventDefault();
|
||||
ipkInput.click();
|
||||
}
|
||||
}, [_('choose_ipk')]);
|
||||
|
||||
var apkButton = E('button', {
|
||||
class: 'cbi-button cbi-button-add run-btn',
|
||||
style: 'margin-left:10px;background:#1565C0!important;background-color:#1565C0!important;background-image:none!important;color:#fff!important;border-color:#1565C0!important;box-shadow:none!important;text-shadow:none!important;opacity:1!important',
|
||||
click: function (ev) {
|
||||
ev.preventDefault();
|
||||
apkInput.click();
|
||||
}
|
||||
}, [_('choose_apk')]);
|
||||
|
||||
var runButton = E('button', {
|
||||
class: 'btn cbi-button cbi-button-action',
|
||||
class: 'cbi-button cbi-button-action run-btn',
|
||||
disabled: true,
|
||||
style: 'min-width:140px;margin-left:15px;',
|
||||
style: 'min-width:140px;margin-left:15px;background:#7B1FA2!important;background-color:#7B1FA2!important;background-image:none!important;color:#fff!important;border-color:#7B1FA2!important;box-shadow:none!important;text-shadow:none!important;opacity:1!important',
|
||||
click: function (ev) {
|
||||
ev.preventDefault();
|
||||
self.startRun(runButton, state);
|
||||
@@ -212,8 +262,8 @@ return view.extend({
|
||||
}, [_('execute')]);
|
||||
|
||||
var cleanButton = E('button', {
|
||||
class: 'btn cbi-button cbi-button-reset',
|
||||
style: 'margin-left:35px;',
|
||||
class: 'cbi-button cbi-button-reset run-btn',
|
||||
style: 'margin-left:35px;background:#C62828!important;background-color:#C62828!important;background-image:none!important;color:#fff!important;border-color:#C62828!important;box-shadow:none!important;text-shadow:none!important;opacity:1!important',
|
||||
click: function (ev) {
|
||||
ev.preventDefault();
|
||||
cleanup().then(function (res) {
|
||||
@@ -251,9 +301,12 @@ return view.extend({
|
||||
}, [
|
||||
E('h3', [_('upload_title')]),
|
||||
E('p', [state]),
|
||||
E('p', [pickButton, runButton, cleanButton]),
|
||||
E('p', [pickButton, ipkButton, apkButton]),
|
||||
E('p', { style: 'margin-top:10px' }, [runButton, cleanButton]),
|
||||
progress,
|
||||
fileInput
|
||||
fileInput,
|
||||
ipkInput,
|
||||
apkInput
|
||||
]);
|
||||
|
||||
fileInput.addEventListener('change', function () {
|
||||
@@ -261,6 +314,16 @@ return view.extend({
|
||||
self.uploadFile(fileInput.files[0], progress, state, runButton);
|
||||
});
|
||||
|
||||
ipkInput.addEventListener('change', function () {
|
||||
if (ipkInput.files && ipkInput.files.length)
|
||||
self.uploadFile(ipkInput.files[0], progress, state, runButton);
|
||||
});
|
||||
|
||||
apkInput.addEventListener('change', function () {
|
||||
if (apkInput.files && apkInput.files.length)
|
||||
self.uploadFile(apkInput.files[0], progress, state, runButton);
|
||||
});
|
||||
|
||||
poll.add(function () {
|
||||
return self.refreshLog(log, state);
|
||||
}, 1);
|
||||
@@ -290,9 +353,8 @@ return view.extend({
|
||||
uploadFile: function (file, progress, state, runButton) {
|
||||
var self = this;
|
||||
|
||||
// 支持 .run 和 .sh 文件
|
||||
if (!file.name.match(/\.(run|sh)$/i)) {
|
||||
ui.addNotification(null, E('p', [_('only_run')]), 'danger');
|
||||
if (!file.name.match(/\.(run|sh|ipk|apk)$/i)) {
|
||||
ui.addNotification(null, E('p', [_('only_supported')]), 'danger');
|
||||
return Promise.reject();
|
||||
}
|
||||
|
||||
@@ -361,8 +423,15 @@ return view.extend({
|
||||
state.textContent = _('starting');
|
||||
|
||||
return runInstaller(this.currentUploadId).then(function (res) {
|
||||
if (res && res.error)
|
||||
throw new Error(res.error);
|
||||
if (res && res.error) {
|
||||
var errorMsg = res.error;
|
||||
if (res.error === 'ERR_NO_OPKG') {
|
||||
errorMsg = _('err_no_opkg');
|
||||
} else if (res.error === 'ERR_NO_APK') {
|
||||
errorMsg = _('err_no_apk');
|
||||
}
|
||||
throw new Error(errorMsg);
|
||||
}
|
||||
|
||||
self.logOffset = 0;
|
||||
state.textContent = _('started', res.pid);
|
||||
|
||||
@@ -115,7 +115,8 @@ method_list() {
|
||||
"offset": "Integer"
|
||||
},
|
||||
"cleanup": {},
|
||||
"version": {}
|
||||
"version": {},
|
||||
"capabilities": {}
|
||||
}
|
||||
JSON
|
||||
}
|
||||
@@ -132,8 +133,8 @@ upload_start() {
|
||||
local clean id token dir file
|
||||
clean="$(sanitize_name "$filename")"
|
||||
case "$clean" in
|
||||
*.run|*.sh) ;;
|
||||
*) json_error "Only .run and .sh files are accepted"; return ;;
|
||||
*.run|*.sh|*.ipk|*.apk) ;;
|
||||
*) json_error "Only .run, .sh, .ipk and .apk files are accepted"; return ;;
|
||||
esac
|
||||
|
||||
id="$(date +%s)_$$"
|
||||
@@ -225,14 +226,6 @@ run_installer() {
|
||||
name="$(cat "$dir/name" 2>/dev/null)"
|
||||
file="$dir/$name"
|
||||
|
||||
# 确保文件可执行(.run 和 .sh 都需要)
|
||||
chmod 700 "$file" 2>/dev/null || true
|
||||
|
||||
[ -x "$file" ] || {
|
||||
json_error "Uploaded file is not executable. Please check file permissions."
|
||||
return
|
||||
}
|
||||
|
||||
read_state
|
||||
if is_running "$PID"; then
|
||||
json_error "Another installer is already running"
|
||||
@@ -244,15 +237,56 @@ run_installer() {
|
||||
log="$work/output.log"
|
||||
mkdir -p "$work" || { json_error "Unable to create run directory"; return; }
|
||||
|
||||
(
|
||||
cd "$work" || exit 1
|
||||
printf 'luci-app-run: started %s\n' "$now"
|
||||
printf 'luci-app-run: executing %s\n\n' "$file"
|
||||
"$file"
|
||||
rc=$?
|
||||
printf '\nluci-app-run: exited with status %s at %s\n' "$rc" "$(date '+%Y-%m-%d %H:%M:%S')"
|
||||
exit "$rc"
|
||||
) > "$log" 2>&1 &
|
||||
case "$name" in
|
||||
*.ipk)
|
||||
if ! command -v opkg >/dev/null 2>&1; then
|
||||
json_error "ERR_NO_OPKG"
|
||||
return
|
||||
fi
|
||||
(
|
||||
cd "$work" || exit 1
|
||||
printf 'luci-app-run: started %s\n' "$now"
|
||||
printf 'luci-app-run: installing ipk package %s\n\n' "$file"
|
||||
opkg install "$file"
|
||||
rc=$?
|
||||
printf '\nluci-app-run: opkg exited with status %s at %s\n' "$rc" "$(date '+%Y-%m-%d %H:%M:%S')"
|
||||
exit "$rc"
|
||||
) > "$log" 2>&1 &
|
||||
;;
|
||||
*.apk)
|
||||
if ! command -v apk >/dev/null 2>&1; then
|
||||
json_error "ERR_NO_APK"
|
||||
return
|
||||
fi
|
||||
(
|
||||
cd "$work" || exit 1
|
||||
printf 'luci-app-run: started %s\n' "$now"
|
||||
printf 'luci-app-run: installing apk package %s\n\n' "$file"
|
||||
apk add --allow-untrusted "$file"
|
||||
rc=$?
|
||||
printf '\nluci-app-run: apk exited with status %s at %s\n' "$rc" "$(date '+%Y-%m-%d %H:%M:%S')"
|
||||
exit "$rc"
|
||||
) > "$log" 2>&1 &
|
||||
;;
|
||||
*)
|
||||
chmod 700 "$file" 2>/dev/null || true
|
||||
|
||||
[ -x "$file" ] || {
|
||||
json_error "Uploaded file is not executable. Please check file permissions."
|
||||
return
|
||||
}
|
||||
|
||||
(
|
||||
cd "$work" || exit 1
|
||||
printf 'luci-app-run: started %s\n' "$now"
|
||||
printf 'luci-app-run: executing %s\n\n' "$file"
|
||||
"$file"
|
||||
rc=$?
|
||||
printf '\nluci-app-run: exited with status %s at %s\n' "$rc" "$(date '+%Y-%m-%d %H:%M:%S')"
|
||||
exit "$rc"
|
||||
) > "$log" 2>&1 &
|
||||
;;
|
||||
esac
|
||||
|
||||
pid="$!"
|
||||
write_state "$pid" "$file" "$log" "$now"
|
||||
@@ -336,6 +370,20 @@ version() {
|
||||
json_dump
|
||||
}
|
||||
|
||||
capabilities() {
|
||||
local has_opkg=0
|
||||
local has_apk=0
|
||||
|
||||
command -v opkg >/dev/null 2>&1 && has_opkg=1
|
||||
command -v apk >/dev/null 2>&1 && has_apk=1
|
||||
|
||||
json_init
|
||||
json_add_int code 0
|
||||
json_add_int opkg "$has_opkg"
|
||||
json_add_int apk "$has_apk"
|
||||
json_dump
|
||||
}
|
||||
|
||||
case "$1" in
|
||||
list)
|
||||
method_list
|
||||
@@ -350,6 +398,7 @@ case "$1" in
|
||||
read_log) read_log ;;
|
||||
cleanup) cleanup ;;
|
||||
version) version ;;
|
||||
capabilities) capabilities ;;
|
||||
*) json_error "Unknown method" ;;
|
||||
esac
|
||||
;;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"luci-app-run": {
|
||||
"description": "Grant access to upload and execute .run installers",
|
||||
"description": "Grant access to upload and execute .run, .sh, .ipk and .apk files",
|
||||
"read": {
|
||||
"ubus": {
|
||||
"luci-app-run": [ "status", "read_log" ]
|
||||
|
||||
@@ -7,54 +7,54 @@ MAX_SIZE=$((256 * 1024 * 1024))
|
||||
umask 077
|
||||
|
||||
header() {
|
||||
printf 'Status: %s\r\n' "$1"
|
||||
printf 'Content-Type: application/json\r\n'
|
||||
printf 'Cache-Control: no-store\r\n'
|
||||
printf '\r\n'
|
||||
printf 'Status: %s\r\n' "$1"
|
||||
printf 'Content-Type: application/json\r\n'
|
||||
printf 'Cache-Control: no-store\r\n'
|
||||
printf '\r\n'
|
||||
}
|
||||
|
||||
json_escape() {
|
||||
printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g'
|
||||
printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g'
|
||||
}
|
||||
|
||||
fail() {
|
||||
header "${2:-400 Bad Request}"
|
||||
printf '{"code":1,"error":"%s"}\n' "$(json_escape "$1")"
|
||||
exit 0
|
||||
header "${2:-400 Bad Request}"
|
||||
printf '{"code":1,"error":"%s"}\n' "$(json_escape "$1")"
|
||||
exit 0
|
||||
}
|
||||
|
||||
ok() {
|
||||
header '200 OK'
|
||||
printf '{"code":0,"size":%s}\n' "$1"
|
||||
exit 0
|
||||
header '200 OK'
|
||||
printf '{"code":0,"size":%s}\n' "$1"
|
||||
exit 0
|
||||
}
|
||||
|
||||
urldecode() {
|
||||
local value="${1//+/ }"
|
||||
printf '%b' "${value//%/\\x}"
|
||||
local value="${1//+/ }"
|
||||
printf '%b' "${value//%/\\x}"
|
||||
}
|
||||
|
||||
query_value() {
|
||||
local key="$1" pair name value oldifs
|
||||
oldifs="$IFS"
|
||||
IFS='&'
|
||||
for pair in $QUERY_STRING; do
|
||||
name="${pair%%=*}"
|
||||
value="${pair#*=}"
|
||||
if [ "$name" = "$key" ]; then
|
||||
IFS="$oldifs"
|
||||
urldecode "$value"
|
||||
return
|
||||
fi
|
||||
done
|
||||
IFS="$oldifs"
|
||||
local key="$1" pair name value oldifs
|
||||
oldifs="$IFS"
|
||||
IFS='&'
|
||||
for pair in $QUERY_STRING; do
|
||||
name="${pair%%=*}"
|
||||
value="${pair#*=}"
|
||||
if [ "$name" = "$key" ]; then
|
||||
IFS="$oldifs"
|
||||
urldecode "$value"
|
||||
return
|
||||
fi
|
||||
done
|
||||
IFS="$oldifs"
|
||||
}
|
||||
|
||||
valid_id() {
|
||||
case "$1" in
|
||||
""|*[!A-Za-z0-9_-]*) return 1 ;;
|
||||
esac
|
||||
return 0
|
||||
case "$1" in
|
||||
""|*[!A-Za-z0-9_-]*) return 1 ;;
|
||||
esac
|
||||
return 0
|
||||
}
|
||||
|
||||
[ "$REQUEST_METHOD" = "POST" ] || fail "POST required" "405 Method Not Allowed"
|
||||
@@ -72,13 +72,17 @@ saved_token="$(cat "$dir/token" 2>/dev/null)"
|
||||
|
||||
name="$(cat "$dir/name" 2>/dev/null)"
|
||||
case "$name" in
|
||||
*.run)
|
||||
;;
|
||||
*.sh)
|
||||
;;
|
||||
*)
|
||||
fail "Invalid filename"
|
||||
;;
|
||||
*.run)
|
||||
;;
|
||||
*.sh)
|
||||
;;
|
||||
*.ipk)
|
||||
;;
|
||||
*.apk)
|
||||
;;
|
||||
*)
|
||||
fail "Invalid filename"
|
||||
;;
|
||||
esac
|
||||
|
||||
length="${CONTENT_LENGTH:-0}"
|
||||
@@ -91,8 +95,8 @@ tmp="$file.upload"
|
||||
cat > "$tmp" || fail "Unable to receive upload"
|
||||
actual="$(wc -c < "$tmp" 2>/dev/null)"
|
||||
[ "$actual" -eq "$length" ] 2>/dev/null || {
|
||||
rm -f "$tmp"
|
||||
fail "Upload size mismatch"
|
||||
rm -f "$tmp"
|
||||
fail "Upload size mismatch"
|
||||
}
|
||||
|
||||
mv "$tmp" "$file" || fail "Unable to save upload"
|
||||
|
||||
Reference in New Issue
Block a user