Files
op-packages/luci-app-run/root/usr/libexec/rpcd/luci-app-run
T

533 lines
13 KiB
Bash
Executable File

#!/bin/sh
. /usr/share/libubox/jshn.sh
BASE_DIR="/tmp/luci-app-run"
UPLOAD_DIR="$BASE_DIR/uploads"
RUN_DIR="$BASE_DIR/runs"
STATE_FILE="$BASE_DIR/state"
MAX_SIZE=$((256 * 1024 * 1024))
umask 077
mkdir -p "$UPLOAD_DIR" "$RUN_DIR"
reply() {
printf '%s\n' "$1"
}
json_error() {
local msg="$1"
json_init
json_add_int code 1
json_add_string error "$msg"
json_dump
}
json_ok() {
json_init
json_add_int code 0
[ -n "$1" ] && json_add_string message "$1"
json_dump
}
sanitize_name() {
local name="$1"
name="${name##*/}"
name="${name##*\\}"
printf '%s' "$name" | tr -c 'A-Za-z0-9._-' '_'
}
valid_id() {
case "$1" in
""|*[!A-Za-z0-9_-]*) return 1 ;;
esac
return 0
}
load_input() {
local input
input="$(cat)"
json_load "$input" >/dev/null 2>&1
}
get_string() {
json_get_var "$1" "$1"
}
get_int() {
json_get_var "$1" "$1"
}
new_token() {
local token
token="$(hexdump -n 16 -e '16/1 "%02x"' /dev/urandom 2>/dev/null)"
[ -n "$token" ] || token="$(date +%s)_$$_$(awk 'BEGIN{srand();print int(rand()*1000000)}')"
printf '%s' "$token" | tr -c 'A-Za-z0-9_' '_'
}
session_dir() {
printf '%s/%s' "$UPLOAD_DIR" "$1"
}
write_state() {
local pid="$1"
local file="$2"
local log="$3"
local started="$4"
local type="$5"
{
printf 'PID=%s\n' "$pid"
printf 'FILE=%s\n' "$file"
printf 'LOG=%s\n' "$log"
printf 'STARTED=%s\n' "$started"
printf 'TYPE=%s\n' "$type"
} > "$STATE_FILE"
}
read_state() {
[ -f "$STATE_FILE" ] && . "$STATE_FILE"
}
is_running() {
local pid="$1"
[ -n "$pid" ] || return 1
[ -d "/proc/$pid" ] || return 1
local cmdline
cmdline="$(cat "/proc/$pid/cmdline" 2>/dev/null | tr '\0' ' ')"
echo "$cmdline" | grep -q "luci-app-run" && return 0
# Check for .run files (must be followed by space or end of string)
case "$cmdline" in
*".run "*) return 0 ;;
*".run") return 0 ;;
esac
# Check for .sh files (must be followed by space or end of string)
case "$cmdline" in
*".sh "*) return 0 ;;
*".sh") return 0 ;;
esac
return 1
}
method_list() {
cat <<'JSON'
{
"upload_start": {
"filename": "String",
"size": "Integer"
},
"upload_chunk": {
"id": "String",
"data": "String",
"index": "Integer"
},
"upload_finish": {
"id": "String"
},
"run": {
"id": "String",
"args": "String"
},
"status": {},
"read_log": {
"offset": "Integer"
},
"download_run": {
"url": "String"
},
"cleanup": {},
"version": {},
"capabilities": {}
}
JSON
}
upload_start() {
load_input
get_string filename
get_int size
[ -n "$filename" ] || { json_error "Missing filename"; return; }
[ -n "$size" ] || size=0
[ "$size" -le "$MAX_SIZE" ] 2>/dev/null || { json_error "File is larger than 256 MiB"; return; }
local clean id token dir file
clean="$(sanitize_name "$filename")"
case "$clean" in
*.run|*.sh|*.ipk|*.apk) ;;
*) json_error "Only .run, .sh, .ipk and .apk files are accepted"; return ;;
esac
id="$(date +%s)_$$"
token="$(new_token)"
dir="$(session_dir "$id")"
file="$dir/$clean"
mkdir -p "$dir" || { json_error "Unable to create upload directory"; return; }
: > "$file" || { json_error "Unable to create upload file"; return; }
printf '%s\n' "$clean" > "$dir/name"
printf '%s\n' "$size" > "$dir/size"
printf '%s\n' "$token" > "$dir/token"
json_init
json_add_int code 0
json_add_string id "$id"
json_add_string token "$token"
json_add_string filename "$clean"
json_dump
}
upload_chunk() {
load_input
get_string id
get_string data
get_int index
valid_id "$id" || { json_error "Invalid upload id"; return; }
[ -n "$data" ] || { json_error "Missing chunk data"; return; }
local dir name file
dir="$(session_dir "$id")"
[ -d "$dir" ] || { json_error "Unknown upload id"; return; }
name="$(cat "$dir/name" 2>/dev/null)"
file="$dir/$name"
printf '%s' "$data" | base64 -d >> "$file" 2>/dev/null || {
json_error "Unable to decode upload chunk"
return
}
json_init
json_add_int code 0
json_add_int index "$index"
json_add_int received "$(wc -c < "$file" 2>/dev/null)"
json_dump
}
upload_finish() {
load_input
get_string id
valid_id "$id" || { json_error "Invalid upload id"; return; }
local dir name file expected actual
dir="$(session_dir "$id")"
[ -d "$dir" ] || { json_error "Unknown upload id"; return; }
name="$(cat "$dir/name" 2>/dev/null)"
expected="$(cat "$dir/size" 2>/dev/null)"
file="$dir/$name"
actual="$(wc -c < "$file" 2>/dev/null)"
[ -s "$file" ] || { json_error "Uploaded file is empty"; return; }
if [ -n "$expected" ] && [ "$expected" -gt 0 ] 2>/dev/null; then
[ "$actual" -eq "$expected" ] 2>/dev/null || {
json_error "Upload size mismatch"
return
}
fi
chmod 700 "$file" || { json_error "Unable to chmod uploaded file"; return; }
json_init
json_add_int code 0
json_add_string id "$id"
json_add_string filename "$name"
json_add_int size "$actual"
json_dump
}
download_run() {
load_input
get_string url
[ -n "$url" ] || { json_error "Missing URL"; return; }
local filename id dir file log work pid now
# Extract filename from URL - get everything after the last /
filename="${url##*/}"
# Remove query parameters if any
filename="${filename%%\?*}"
case "$filename" in
*.run) ;;
*) json_error "Only .run files are supported for download"; return ;;
esac
read_state
if [ -n "$PID" ] && ! is_running "$PID"; then
rm -f "$STATE_FILE"
unset PID
fi
if is_running "$PID"; then
json_error "Another installer is already running"
return
fi
id="download_$(date +%s)"
dir="$RUN_DIR/$id"
file="$dir/$filename"
log="$dir/output.log"
mkdir -p "$dir" || { json_error "Unable to create download directory"; return; }
now="$(date '+%Y-%m-%d %H:%M:%S')"
(
cd "$dir" || exit 1
printf 'luci-app-run: started %s\n' "$now"
printf 'luci-app-run: downloading %s\n\n' "$url"
if command -v curl >/dev/null 2>&1; then
curl -L -o "$filename" "$url"
elif command -v wget >/dev/null 2>&1; then
wget -O "$filename" "$url"
else
printf 'luci-app-run: error: neither curl nor wget is available\n'
exit 1
fi
rc=$?
if [ "$rc" -ne 0 ]; then
printf '\nluci-app-run: download failed with status %s at %s\n' "$rc" "$(date '+%Y-%m-%d %H:%M:%S')"
exit "$rc"
fi
printf '\nluci-app-run: download completed at %s\n' "$(date '+%Y-%m-%d %H:%M:%S')"
printf '\nluci-app-run: executing %s\n\n' "$file"
chmod 700 "$file" 2>/dev/null || true
"$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 &
pid="$!"
write_state "$pid" "$file" "$log" "$now" "download"
json_init
json_add_int code 0
json_add_int pid "$pid"
json_add_string log "$log"
json_add_string filename "$filename"
json_dump
}
run_installer() {
load_input
get_string id
get_string args
valid_id "$id" || { json_error "Invalid upload id"; return; }
local dir name file log work pid now
dir="$(session_dir "$id")"
[ -d "$dir" ] || { json_error "Unknown upload id"; return; }
name="$(cat "$dir/name" 2>/dev/null)"
file="$dir/$name"
read_state
if [ -n "$PID" ] && ! is_running "$PID"; then
rm -f "$STATE_FILE"
unset PID
fi
if is_running "$PID"; then
json_error "Another installer is already running"
return
fi
now="$(date '+%Y-%m-%d %H:%M:%S')"
work="$RUN_DIR/$id"
log="$work/output.log"
mkdir -p "$work" || { json_error "Unable to create run directory"; return; }
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 %s\n\n' "$file" "$args"
"$file" $args
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" "upload"
json_init
json_add_int code 0
json_add_int pid "$pid"
json_add_string log "$log"
json_dump
}
status() {
read_state
json_init
json_add_int code 0
if is_running "$PID"; then
json_add_boolean running 1
else
json_add_boolean running 0
fi
[ -n "$PID" ] && json_add_int pid "$PID"
[ -n "$FILE" ] && json_add_string file "$FILE"
[ -n "$LOG" ] && json_add_string log "$LOG"
[ -n "$STARTED" ] && json_add_string started "$STARTED"
json_dump
}
read_log() {
load_input
get_int offset
[ -n "$offset" ] || offset=0
read_state
[ -n "$LOG" ] && [ -f "$LOG" ] || {
json_init
json_add_int code 0
json_add_string data ""
json_add_int offset 0
json_add_boolean running 0
json_dump
return
}
local size data start
size="$(wc -c < "$LOG" 2>/dev/null)"
[ "$offset" -le "$size" ] 2>/dev/null || offset=0
start=$((offset + 1))
data="$(tail -c +"$start" "$LOG" 2>/dev/null)"
json_init
json_add_int code 0
json_add_string data "$data"
json_add_int offset "$size"
if is_running "$PID"; then
json_add_boolean running 1
else
json_add_boolean running 0
fi
json_dump
}
cleanup() {
read_state
if is_running "$PID"; then
json_error "Cannot clean up while installer is running"
return
fi
rm -rf "$UPLOAD_DIR"
mkdir -p "$UPLOAD_DIR"
# Keep state file but clear PID and FILE, preserve LOG path
if [ -n "$LOG" ]; then
echo "PID=" > "$STATE_FILE"
echo "FILE=" >> "$STATE_FILE"
echo "LOG=$LOG" >> "$STATE_FILE"
echo "STARTED=" >> "$STATE_FILE"
else
> "$STATE_FILE"
fi
json_ok "Cleaned"
}
version() {
local ver="unknown"
[ -f "/tmp/luci-app-run.version" ] && ver="$(cat /tmp/luci-app-run.version)"
json_init
json_add_int code 0
json_add_string version "$ver"
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
;;
call)
case "$2" in
upload_start) upload_start ;;
upload_chunk) upload_chunk ;;
upload_finish) upload_finish ;;
run) run_installer ;;
download_run) download_run ;;
status) status ;;
read_log) read_log ;;
cleanup) cleanup ;;
version) version ;;
capabilities) capabilities ;;
*) json_error "Unknown method" ;;
esac
;;
*)
reply '{}'
;;
esac