diff --git a/apps/microbin/2.1.0/.env.sample b/apps/microbin/2.1.0/.env.sample new file mode 100644 index 000000000..1d646b850 --- /dev/null +++ b/apps/microbin/2.1.0/.env.sample @@ -0,0 +1,8 @@ +CONTAINER_NAME=microbin +PANEL_APP_BIND_ADDRESS=127.0.0.1 +PANEL_APP_PORT_HTTP=8080 +MICROBIN_ADMIN_USERNAME=admin +MICROBIN_ADMIN_PASSWORD=generate +MICROBIN_MAX_FILE_SIZE_ENCRYPTED_MB=256 +MICROBIN_MAX_FILE_SIZE_UNENCRYPTED_MB=256 +APP_DATA_DIR=./data diff --git a/apps/microbin/2.1.0/data.yml b/apps/microbin/2.1.0/data.yml new file mode 100644 index 000000000..5e2ff380e --- /dev/null +++ b/apps/microbin/2.1.0/data.yml @@ -0,0 +1,116 @@ +additionalProperties: + formFields: + - default: 127.0.0.1 + edit: true + envKey: PANEL_APP_BIND_ADDRESS + labelEn: Bind Address + labelZh: 绑定地址 + label: + en: Bind Address + zh: 绑定地址 + zh-Hant: 綁定位址 + ja: バインドアドレス + ko: 바인드 주소 + ru: Адрес привязки + ms: Alamat ikatan + pt-br: Endereco de vinculacao + required: true + type: text + - default: 8080 + edit: true + envKey: PANEL_APP_PORT_HTTP + labelEn: HTTP Port + labelZh: HTTP 端口 + label: + en: HTTP Port + zh: HTTP 端口 + zh-Hant: HTTP 連接埠 + ja: HTTP ポート + ko: HTTP 포트 + ru: HTTP-порт + ms: Port HTTP + pt-br: Porta HTTP + required: true + rule: paramPort + type: number + - default: admin + edit: true + envKey: MICROBIN_ADMIN_USERNAME + labelEn: Administrator Username + labelZh: 管理员用户名 + label: + en: Administrator Username + zh: 管理员用户名 + zh-Hant: 管理員使用者名稱 + ja: 管理者ユーザー名 + ko: 관리자 사용자 이름 + ru: Имя администратора + ms: Nama pengguna pentadbir + pt-br: Nome de usuario do administrador + required: true + rule: paramCommon + type: text + - default: generate + edit: true + envKey: MICROBIN_ADMIN_PASSWORD + labelEn: Administrator Password + labelZh: 管理员密码 + label: + en: Administrator Password + zh: 管理员密码 + zh-Hant: 管理員密碼 + ja: 管理者パスワード + ko: 관리자 비밀번호 + ru: Пароль администратора + ms: Kata laluan pentadbir + pt-br: Senha do administrador + required: true + type: password + - default: 256 + edit: true + envKey: MICROBIN_MAX_FILE_SIZE_ENCRYPTED_MB + labelEn: Encrypted File Limit (MB) + labelZh: 加密文件上限 (MB) + label: + en: Encrypted File Limit (MB) + zh: 加密文件上限 (MB) + zh-Hant: 加密檔案上限 (MB) + ja: 暗号化ファイル上限 (MB) + ko: 암호화 파일 제한 (MB) + ru: Лимит зашифрованного файла (МБ) + ms: Had fail disulitkan (MB) + pt-br: Limite de arquivo criptografado (MB) + required: true + type: number + - default: 256 + edit: true + envKey: MICROBIN_MAX_FILE_SIZE_UNENCRYPTED_MB + labelEn: Unencrypted File Limit (MB) + labelZh: 未加密文件上限 (MB) + label: + en: Unencrypted File Limit (MB) + zh: 未加密文件上限 (MB) + zh-Hant: 未加密檔案上限 (MB) + ja: 非暗号化ファイル上限 (MB) + ko: 암호화되지 않은 파일 제한 (MB) + ru: Лимит незашифрованного файла (МБ) + ms: Had fail tidak disulitkan (MB) + pt-br: Limite de arquivo nao criptografado (MB) + required: true + type: number + - default: ./data + edit: true + envKey: APP_DATA_DIR + labelEn: Data Directory + labelZh: 数据目录 + label: + en: Data Directory + zh: 数据目录 + zh-Hant: 資料目錄 + ja: データディレクトリ + ko: 데이터 디렉터리 + ru: Каталог данных + ms: Direktori data + pt-br: Diretorio de dados + required: true + type: text diff --git a/apps/microbin/2.1.0/docker-compose.yml b/apps/microbin/2.1.0/docker-compose.yml new file mode 100644 index 000000000..a073d0be3 --- /dev/null +++ b/apps/microbin/2.1.0/docker-compose.yml @@ -0,0 +1,46 @@ +services: + microbin: + image: danielszabo99/microbin:2.1.0@sha256:34fe4ac99b65cc633975ae72ca6fc84eab15f73a95b2e03b98312374063ee5d7 + container_name: ${CONTAINER_NAME} + restart: unless-stopped + user: "65534:65534" + networks: + - 1panel-network + ports: + - "${PANEL_APP_BIND_ADDRESS}:${PANEL_APP_PORT_HTTP}:8080" + environment: + - MICROBIN_BIND=0.0.0.0 + - MICROBIN_PORT=8080 + - MICROBIN_DATA_DIR=/app/microbin_data + - MICROBIN_ADMIN_USERNAME=${MICROBIN_ADMIN_USERNAME} + - MICROBIN_ADMIN_PASSWORD=${MICROBIN_ADMIN_PASSWORD} + - MICROBIN_MAX_FILE_SIZE_ENCRYPTED_MB=${MICROBIN_MAX_FILE_SIZE_ENCRYPTED_MB} + - MICROBIN_MAX_FILE_SIZE_UNENCRYPTED_MB=${MICROBIN_MAX_FILE_SIZE_UNENCRYPTED_MB} + - MICROBIN_PRIVATE=true + - MICROBIN_NO_LISTING=true + - MICROBIN_ENABLE_READONLY=true + - MICROBIN_QR=true + - MICROBIN_DISABLE_TELEMETRY=true + - MICROBIN_DISABLE_UPDATE_CHECKING=true + - MICROBIN_LIST_SERVER=false + volumes: + - "${APP_DATA_DIR}:/app/microbin_data" + read_only: true + tmpfs: + - /tmp:mode=1777,size=64m + cap_drop: + - ALL + security_opt: + - no-new-privileges:true + healthcheck: + test: ["CMD", "bash", "-c", "exec 3<>/dev/tcp/127.0.0.1/8080 && printf 'GET / HTTP/1.0\\r\\nHost: localhost\\r\\n\\r\\n' >&3 && grep -q '200 OK' <&3"] + interval: 15s + timeout: 5s + retries: 8 + start_period: 20s + labels: + createdBy: "Apps" + +networks: + 1panel-network: + external: true diff --git a/apps/microbin/2.1.0/scripts/init.sh b/apps/microbin/2.1.0/scripts/init.sh new file mode 100755 index 000000000..a0d74c722 --- /dev/null +++ b/apps/microbin/2.1.0/scripts/init.sh @@ -0,0 +1,125 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)" +ENV_FILE="${ENV_FILE:-${ROOT_DIR}/.env}" + +fail() { + printf '%s\n' "$1" >&2 + exit 1 +} + +read_env_value() { + local key="$1" + local value + value="$(sed -n "s/^${key}=//p" "$ENV_FILE" | tail -n 1)" + case "$value" in + \"*\") value="${value#\"}"; value="${value%\"}" ;; + \'*\') value="${value#\'}"; value="${value%\'}" ;; + esac + printf '%s\n' "$value" +} + +set_env_value() { + local key="$1" + local value="$2" + local temp_file + temp_file="$(mktemp "${ROOT_DIR}/.microbin-env.tmp.XXXXXX")" + awk -v key="$key" -v value="$value" ' + BEGIN { written = 0 } + $0 ~ "^" key "=" { + if (!written) { + print key "=" value + written = 1 + } + next + } + { print } + END { if (!written) print key "=" value } + ' "$ENV_FILE" > "$temp_file" + chmod 600 "$temp_file" + mv -f -- "$temp_file" "$ENV_FILE" +} + +validate_ipv4() { + local value="$1" + local octet + local -a octets + [[ "$value" =~ ^[0-9]{1,3}(\.[0-9]{1,3}){3}$ ]] || fail "PANEL_APP_BIND_ADDRESS must be an IPv4 address" + IFS=. read -r -a octets <<< "$value" + for octet in "${octets[@]}"; do + ((10#$octet <= 255)) || fail "PANEL_APP_BIND_ADDRESS contains an invalid IPv4 octet" + done +} + +validate_positive_integer() { + local key="$1" + local value="$2" + local maximum="$3" + [[ "$value" =~ ^[1-9][0-9]*$ ]] || fail "$key must be a positive integer" + ((10#$value <= maximum)) || fail "$key must not exceed $maximum" +} + +generate_alphanumeric() { + local length="$1" + local material + material="$(openssl rand -base64 96 | tr -dc A-Za-z0-9)" + [[ ${#material} -ge $length ]] || fail "unable to generate sufficient random material" + printf '%s\n' "${material:0:length}" +} + +[[ -f "$ENV_FILE" ]] || fail "$ENV_FILE not found" +[[ ! -L "$ENV_FILE" ]] || fail "$ENV_FILE must not be a symbolic link" +[[ "$(id -u)" -eq 0 ]] || fail "MicroBin init must run as root" +command -v openssl >/dev/null 2>&1 || fail "openssl is required to generate credentials" + +validate_ipv4 "$(read_env_value PANEL_APP_BIND_ADDRESS)" +validate_positive_integer PANEL_APP_PORT_HTTP "$(read_env_value PANEL_APP_PORT_HTTP)" 65535 + +admin_username="$(read_env_value MICROBIN_ADMIN_USERNAME)" +[[ "$admin_username" =~ ^[A-Za-z0-9][A-Za-z0-9_.-]{0,63}$ ]] || fail "MICROBIN_ADMIN_USERNAME is invalid" + +admin_password="$(read_env_value MICROBIN_ADMIN_PASSWORD)" +if [[ -z "$admin_password" || "$admin_password" == generate ]]; then + admin_password="$(generate_alphanumeric 48)" +fi +[[ "$admin_password" =~ ^[A-Za-z0-9]{32,128}$ ]] || + fail "MICROBIN_ADMIN_PASSWORD must contain 32 to 128 alphanumeric characters" + +validate_positive_integer MICROBIN_MAX_FILE_SIZE_ENCRYPTED_MB "$(read_env_value MICROBIN_MAX_FILE_SIZE_ENCRYPTED_MB)" 4096 +validate_positive_integer MICROBIN_MAX_FILE_SIZE_UNENCRYPTED_MB "$(read_env_value MICROBIN_MAX_FILE_SIZE_UNENCRYPTED_MB)" 4096 + +data_raw="$(read_env_value APP_DATA_DIR)" +[[ -n "$data_raw" && "$data_raw" != /* ]] || fail "APP_DATA_DIR must be a non-empty relative path" +case "$data_raw" in + *$'\n'* | *$'\r'* | *\\* | *:* | *'$'* | *'#'* | *'"'* | *"'"*) + fail "APP_DATA_DIR contains unsupported characters" + ;; +esac + +relative_data="${data_raw#./}" +[[ -n "$relative_data" ]] || fail "APP_DATA_DIR must not resolve to the version root" +current="$ROOT_DIR" +IFS=/ read -r -a components <<< "$relative_data" +for component in "${components[@]}"; do + [[ -n "$component" && "$component" != . && "$component" != .. ]] || fail "APP_DATA_DIR contains traversal" + current="$current/$component" + [[ ! -L "$current" ]] || fail "APP_DATA_DIR must not contain symbolic-link components" +done + +data_dir="$(realpath -m -- "$ROOT_DIR/$relative_data")" +case "$data_dir" in + "$ROOT_DIR"/*) ;; + *) fail "APP_DATA_DIR must stay inside the application version directory" ;; +esac + +install -d -m 0750 -- "$data_dir" +resolved_data="$(realpath -e -- "$data_dir")" +case "$resolved_data" in + "$ROOT_DIR"/*) ;; + *) fail "APP_DATA_DIR resolves outside the application version directory" ;; +esac +chown -R 65534:65534 -- "$resolved_data" + +set_env_value MICROBIN_ADMIN_PASSWORD "$admin_password" +chmod 600 "$ENV_FILE" diff --git a/apps/microbin/2.1.0/scripts/uninstall.sh b/apps/microbin/2.1.0/scripts/uninstall.sh new file mode 100755 index 000000000..9365d9925 --- /dev/null +++ b/apps/microbin/2.1.0/scripts/uninstall.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Persistent data is intentionally retained for recovery or reinstallation. +exit 0 diff --git a/apps/microbin/2.1.0/scripts/upgrade.sh b/apps/microbin/2.1.0/scripts/upgrade.sh new file mode 100755 index 000000000..2a53d75f3 --- /dev/null +++ b/apps/microbin/2.1.0/scripts/upgrade.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +set -euo pipefail + +exit 0 diff --git a/apps/microbin/README.md b/apps/microbin/README.md new file mode 100644 index 000000000..3145d11b7 --- /dev/null +++ b/apps/microbin/README.md @@ -0,0 +1,52 @@ +# MicroBin + +## 产品介绍 + +MicroBin 是使用 Rust 编写的轻量自托管分享服务,可发布文本、文件和短链接,并支持有效期、只读内容、二维码和管理界面。 + +## 主要功能 + +- 文本、文件和短链接分享 +- 有效期、只读内容和二维码 +- SQLite 持久化与管理界面 + +## 访问说明 + +安装后通过 `http://<服务器 IP>:<端口>` 访问。默认只绑定 `127.0.0.1`,并关闭公开列表、遥测和更新检查。远程访问应通过 HTTPS 反向代理发布。管理员界面使用安装时配置的用户名和密码。 + +## 数据持久化 + +`APP_DATA_DIR` 保存 SQLite 数据库、文本记录和附件。卸载不会删除此目录,请定期独立备份。 + +## 安全与部署风险 + +- 官方镜像默认以 root 运行;本应用强制使用 `65534:65534`,并已验证文本、附件、重启和持久化流程。容器使用只读根文件系统、丢弃全部 Linux capabilities,并启用 `no-new-privileges`。 +- 当前官方镜像的新鲜双架构扫描包含基础系统漏洞。MicroBin 二进制只动态链接 libc、libm 和 libgcc,不链接系统 OpenSSL,也不调用镜像中的 Perl、Archive::Tar、Storable、gzip、mount、libcap 或 ncurses 工具。32 位 OpenSSL Critical 不适用于支持的 amd64/arm64 架构。 +- 这些结论只覆盖官方入口和默认配置,并不表示镜像中没有漏洞。镜像摘要、入口或运行用户发生变化后必须重新扫描。 + +## Introduction + +MicroBin is a lightweight self-hosted sharing service written in Rust. It publishes text, files, and short URLs with expiration, read-only content, QR codes, and an administration interface. + +## Features + +- Text, file, and short URL sharing +- Expiration, read-only content, and QR codes +- SQLite persistence and an administration interface + +## Usage Notes + +- Access the service at `http://:`. It binds to `127.0.0.1` by default and disables public listing, telemetry, and update checks. Use an HTTPS reverse proxy for remote access. +- `APP_DATA_DIR` stores the SQLite database, text records, and attachments. Uninstallation preserves this directory. +- The container is forced to UID/GID `65534:65534`, uses a read-only root filesystem, drops all Linux capabilities, and enables `no-new-privileges`. + +## Security Note + +Fresh scans of the official images contain base-system vulnerabilities. The MicroBin binary dynamically links only libc, libm, and libgcc; it neither links system OpenSSL nor invokes the image's Perl, Archive::Tar, Storable, gzip, mount, libcap, or ncurses tooling. The 32-bit-only OpenSSL Critical does not apply to the supported amd64/arm64 images. These are default-entrypoint reachability exceptions, not claims that the image is vulnerability-free. Any digest, entrypoint, or runtime-user change requires a fresh review. + +## References + +- Project: +- Stable release: +- Official Compose: +- License: (BSD-3-Clause) diff --git a/apps/microbin/data.yml b/apps/microbin/data.yml new file mode 100644 index 000000000..24736f989 --- /dev/null +++ b/apps/microbin/data.yml @@ -0,0 +1,31 @@ +name: MicroBin +tags: + - 工具 +title: 轻量自托管的文本与文件分享服务 +description: 轻量自托管的文本与文件分享服务 +additionalProperties: + key: microbin + name: MicroBin + tags: + - Tool + shortDescZh: 轻量自托管的文本与文件分享服务 + shortDescEn: Lightweight self-hosted text and file sharing service + description: + en: Lightweight self-hosted text and file sharing service + zh: 轻量自托管的文本与文件分享服务 + zh-Hant: 輕量自架式文字與檔案分享服務 + ja: 軽量なセルフホスト型テキスト・ファイル共有サービス + ko: 가벼운 셀프 호스팅 텍스트 및 파일 공유 서비스 + ru: Легковесный сервис для самостоятельного обмена текстом и файлами + ms: Perkhidmatan perkongsian teks dan fail hos kendiri yang ringan + pt-br: Servico leve e auto-hospedado de compartilhamento de textos e arquivos + type: website + crossVersionUpdate: true + limit: 0 + recommend: 0 + website: https://microbin.eu/ + github: https://github.com/szabodanika/microbin + document: https://microbin.eu/documentation/ + architectures: + - amd64 + - arm64 diff --git a/apps/microbin/latest/.env.sample b/apps/microbin/latest/.env.sample new file mode 100644 index 000000000..1d646b850 --- /dev/null +++ b/apps/microbin/latest/.env.sample @@ -0,0 +1,8 @@ +CONTAINER_NAME=microbin +PANEL_APP_BIND_ADDRESS=127.0.0.1 +PANEL_APP_PORT_HTTP=8080 +MICROBIN_ADMIN_USERNAME=admin +MICROBIN_ADMIN_PASSWORD=generate +MICROBIN_MAX_FILE_SIZE_ENCRYPTED_MB=256 +MICROBIN_MAX_FILE_SIZE_UNENCRYPTED_MB=256 +APP_DATA_DIR=./data diff --git a/apps/microbin/latest/data.yml b/apps/microbin/latest/data.yml new file mode 100644 index 000000000..5e2ff380e --- /dev/null +++ b/apps/microbin/latest/data.yml @@ -0,0 +1,116 @@ +additionalProperties: + formFields: + - default: 127.0.0.1 + edit: true + envKey: PANEL_APP_BIND_ADDRESS + labelEn: Bind Address + labelZh: 绑定地址 + label: + en: Bind Address + zh: 绑定地址 + zh-Hant: 綁定位址 + ja: バインドアドレス + ko: 바인드 주소 + ru: Адрес привязки + ms: Alamat ikatan + pt-br: Endereco de vinculacao + required: true + type: text + - default: 8080 + edit: true + envKey: PANEL_APP_PORT_HTTP + labelEn: HTTP Port + labelZh: HTTP 端口 + label: + en: HTTP Port + zh: HTTP 端口 + zh-Hant: HTTP 連接埠 + ja: HTTP ポート + ko: HTTP 포트 + ru: HTTP-порт + ms: Port HTTP + pt-br: Porta HTTP + required: true + rule: paramPort + type: number + - default: admin + edit: true + envKey: MICROBIN_ADMIN_USERNAME + labelEn: Administrator Username + labelZh: 管理员用户名 + label: + en: Administrator Username + zh: 管理员用户名 + zh-Hant: 管理員使用者名稱 + ja: 管理者ユーザー名 + ko: 관리자 사용자 이름 + ru: Имя администратора + ms: Nama pengguna pentadbir + pt-br: Nome de usuario do administrador + required: true + rule: paramCommon + type: text + - default: generate + edit: true + envKey: MICROBIN_ADMIN_PASSWORD + labelEn: Administrator Password + labelZh: 管理员密码 + label: + en: Administrator Password + zh: 管理员密码 + zh-Hant: 管理員密碼 + ja: 管理者パスワード + ko: 관리자 비밀번호 + ru: Пароль администратора + ms: Kata laluan pentadbir + pt-br: Senha do administrador + required: true + type: password + - default: 256 + edit: true + envKey: MICROBIN_MAX_FILE_SIZE_ENCRYPTED_MB + labelEn: Encrypted File Limit (MB) + labelZh: 加密文件上限 (MB) + label: + en: Encrypted File Limit (MB) + zh: 加密文件上限 (MB) + zh-Hant: 加密檔案上限 (MB) + ja: 暗号化ファイル上限 (MB) + ko: 암호화 파일 제한 (MB) + ru: Лимит зашифрованного файла (МБ) + ms: Had fail disulitkan (MB) + pt-br: Limite de arquivo criptografado (MB) + required: true + type: number + - default: 256 + edit: true + envKey: MICROBIN_MAX_FILE_SIZE_UNENCRYPTED_MB + labelEn: Unencrypted File Limit (MB) + labelZh: 未加密文件上限 (MB) + label: + en: Unencrypted File Limit (MB) + zh: 未加密文件上限 (MB) + zh-Hant: 未加密檔案上限 (MB) + ja: 非暗号化ファイル上限 (MB) + ko: 암호화되지 않은 파일 제한 (MB) + ru: Лимит незашифрованного файла (МБ) + ms: Had fail tidak disulitkan (MB) + pt-br: Limite de arquivo nao criptografado (MB) + required: true + type: number + - default: ./data + edit: true + envKey: APP_DATA_DIR + labelEn: Data Directory + labelZh: 数据目录 + label: + en: Data Directory + zh: 数据目录 + zh-Hant: 資料目錄 + ja: データディレクトリ + ko: 데이터 디렉터리 + ru: Каталог данных + ms: Direktori data + pt-br: Diretorio de dados + required: true + type: text diff --git a/apps/microbin/latest/docker-compose.yml b/apps/microbin/latest/docker-compose.yml new file mode 100644 index 000000000..cba23d605 --- /dev/null +++ b/apps/microbin/latest/docker-compose.yml @@ -0,0 +1,46 @@ +services: + microbin: + image: danielszabo99/microbin:latest@sha256:6660e5ccad0d764fa3c0032464ffb8f4b4f28c92a2eb9e39202b94cdc5b68909 + container_name: ${CONTAINER_NAME} + restart: unless-stopped + user: "65534:65534" + networks: + - 1panel-network + ports: + - "${PANEL_APP_BIND_ADDRESS}:${PANEL_APP_PORT_HTTP}:8080" + environment: + - MICROBIN_BIND=0.0.0.0 + - MICROBIN_PORT=8080 + - MICROBIN_DATA_DIR=/app/microbin_data + - MICROBIN_ADMIN_USERNAME=${MICROBIN_ADMIN_USERNAME} + - MICROBIN_ADMIN_PASSWORD=${MICROBIN_ADMIN_PASSWORD} + - MICROBIN_MAX_FILE_SIZE_ENCRYPTED_MB=${MICROBIN_MAX_FILE_SIZE_ENCRYPTED_MB} + - MICROBIN_MAX_FILE_SIZE_UNENCRYPTED_MB=${MICROBIN_MAX_FILE_SIZE_UNENCRYPTED_MB} + - MICROBIN_PRIVATE=true + - MICROBIN_NO_LISTING=true + - MICROBIN_ENABLE_READONLY=true + - MICROBIN_QR=true + - MICROBIN_DISABLE_TELEMETRY=true + - MICROBIN_DISABLE_UPDATE_CHECKING=true + - MICROBIN_LIST_SERVER=false + volumes: + - "${APP_DATA_DIR}:/app/microbin_data" + read_only: true + tmpfs: + - /tmp:mode=1777,size=64m + cap_drop: + - ALL + security_opt: + - no-new-privileges:true + healthcheck: + test: ["CMD", "bash", "-c", "exec 3<>/dev/tcp/127.0.0.1/8080 && printf 'GET / HTTP/1.0\\r\\nHost: localhost\\r\\n\\r\\n' >&3 && grep -q '200 OK' <&3"] + interval: 15s + timeout: 5s + retries: 8 + start_period: 20s + labels: + createdBy: "Apps" + +networks: + 1panel-network: + external: true diff --git a/apps/microbin/latest/scripts/init.sh b/apps/microbin/latest/scripts/init.sh new file mode 100755 index 000000000..a0d74c722 --- /dev/null +++ b/apps/microbin/latest/scripts/init.sh @@ -0,0 +1,125 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)" +ENV_FILE="${ENV_FILE:-${ROOT_DIR}/.env}" + +fail() { + printf '%s\n' "$1" >&2 + exit 1 +} + +read_env_value() { + local key="$1" + local value + value="$(sed -n "s/^${key}=//p" "$ENV_FILE" | tail -n 1)" + case "$value" in + \"*\") value="${value#\"}"; value="${value%\"}" ;; + \'*\') value="${value#\'}"; value="${value%\'}" ;; + esac + printf '%s\n' "$value" +} + +set_env_value() { + local key="$1" + local value="$2" + local temp_file + temp_file="$(mktemp "${ROOT_DIR}/.microbin-env.tmp.XXXXXX")" + awk -v key="$key" -v value="$value" ' + BEGIN { written = 0 } + $0 ~ "^" key "=" { + if (!written) { + print key "=" value + written = 1 + } + next + } + { print } + END { if (!written) print key "=" value } + ' "$ENV_FILE" > "$temp_file" + chmod 600 "$temp_file" + mv -f -- "$temp_file" "$ENV_FILE" +} + +validate_ipv4() { + local value="$1" + local octet + local -a octets + [[ "$value" =~ ^[0-9]{1,3}(\.[0-9]{1,3}){3}$ ]] || fail "PANEL_APP_BIND_ADDRESS must be an IPv4 address" + IFS=. read -r -a octets <<< "$value" + for octet in "${octets[@]}"; do + ((10#$octet <= 255)) || fail "PANEL_APP_BIND_ADDRESS contains an invalid IPv4 octet" + done +} + +validate_positive_integer() { + local key="$1" + local value="$2" + local maximum="$3" + [[ "$value" =~ ^[1-9][0-9]*$ ]] || fail "$key must be a positive integer" + ((10#$value <= maximum)) || fail "$key must not exceed $maximum" +} + +generate_alphanumeric() { + local length="$1" + local material + material="$(openssl rand -base64 96 | tr -dc A-Za-z0-9)" + [[ ${#material} -ge $length ]] || fail "unable to generate sufficient random material" + printf '%s\n' "${material:0:length}" +} + +[[ -f "$ENV_FILE" ]] || fail "$ENV_FILE not found" +[[ ! -L "$ENV_FILE" ]] || fail "$ENV_FILE must not be a symbolic link" +[[ "$(id -u)" -eq 0 ]] || fail "MicroBin init must run as root" +command -v openssl >/dev/null 2>&1 || fail "openssl is required to generate credentials" + +validate_ipv4 "$(read_env_value PANEL_APP_BIND_ADDRESS)" +validate_positive_integer PANEL_APP_PORT_HTTP "$(read_env_value PANEL_APP_PORT_HTTP)" 65535 + +admin_username="$(read_env_value MICROBIN_ADMIN_USERNAME)" +[[ "$admin_username" =~ ^[A-Za-z0-9][A-Za-z0-9_.-]{0,63}$ ]] || fail "MICROBIN_ADMIN_USERNAME is invalid" + +admin_password="$(read_env_value MICROBIN_ADMIN_PASSWORD)" +if [[ -z "$admin_password" || "$admin_password" == generate ]]; then + admin_password="$(generate_alphanumeric 48)" +fi +[[ "$admin_password" =~ ^[A-Za-z0-9]{32,128}$ ]] || + fail "MICROBIN_ADMIN_PASSWORD must contain 32 to 128 alphanumeric characters" + +validate_positive_integer MICROBIN_MAX_FILE_SIZE_ENCRYPTED_MB "$(read_env_value MICROBIN_MAX_FILE_SIZE_ENCRYPTED_MB)" 4096 +validate_positive_integer MICROBIN_MAX_FILE_SIZE_UNENCRYPTED_MB "$(read_env_value MICROBIN_MAX_FILE_SIZE_UNENCRYPTED_MB)" 4096 + +data_raw="$(read_env_value APP_DATA_DIR)" +[[ -n "$data_raw" && "$data_raw" != /* ]] || fail "APP_DATA_DIR must be a non-empty relative path" +case "$data_raw" in + *$'\n'* | *$'\r'* | *\\* | *:* | *'$'* | *'#'* | *'"'* | *"'"*) + fail "APP_DATA_DIR contains unsupported characters" + ;; +esac + +relative_data="${data_raw#./}" +[[ -n "$relative_data" ]] || fail "APP_DATA_DIR must not resolve to the version root" +current="$ROOT_DIR" +IFS=/ read -r -a components <<< "$relative_data" +for component in "${components[@]}"; do + [[ -n "$component" && "$component" != . && "$component" != .. ]] || fail "APP_DATA_DIR contains traversal" + current="$current/$component" + [[ ! -L "$current" ]] || fail "APP_DATA_DIR must not contain symbolic-link components" +done + +data_dir="$(realpath -m -- "$ROOT_DIR/$relative_data")" +case "$data_dir" in + "$ROOT_DIR"/*) ;; + *) fail "APP_DATA_DIR must stay inside the application version directory" ;; +esac + +install -d -m 0750 -- "$data_dir" +resolved_data="$(realpath -e -- "$data_dir")" +case "$resolved_data" in + "$ROOT_DIR"/*) ;; + *) fail "APP_DATA_DIR resolves outside the application version directory" ;; +esac +chown -R 65534:65534 -- "$resolved_data" + +set_env_value MICROBIN_ADMIN_PASSWORD "$admin_password" +chmod 600 "$ENV_FILE" diff --git a/apps/microbin/latest/scripts/uninstall.sh b/apps/microbin/latest/scripts/uninstall.sh new file mode 100755 index 000000000..9365d9925 --- /dev/null +++ b/apps/microbin/latest/scripts/uninstall.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Persistent data is intentionally retained for recovery or reinstallation. +exit 0 diff --git a/apps/microbin/latest/scripts/upgrade.sh b/apps/microbin/latest/scripts/upgrade.sh new file mode 100755 index 000000000..2a53d75f3 --- /dev/null +++ b/apps/microbin/latest/scripts/upgrade.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +set -euo pipefail + +exit 0 diff --git a/apps/microbin/logo-LICENSE.txt b/apps/microbin/logo-LICENSE.txt new file mode 100644 index 000000000..601fc6bce --- /dev/null +++ b/apps/microbin/logo-LICENSE.txt @@ -0,0 +1,4 @@ +Source: https://github.com/szabodanika/microbin/blob/937a24b1d6f5d5908a2af4e1524a8267756d9cef/templates/assets/logo-square.png +License: BSD-3-Clause +License text: https://github.com/szabodanika/microbin/blob/937a24b1d6f5d5908a2af4e1524a8267756d9cef/LICENSE +Copyright (c) 2022-2023, Daniel Szabo diff --git a/apps/microbin/logo.png b/apps/microbin/logo.png new file mode 100644 index 000000000..da0c331d7 Binary files /dev/null and b/apps/microbin/logo.png differ