From 5036fdba37e9dea5f8fbb0f1cb5643c6d7327bfe Mon Sep 17 00:00:00 2001 From: okxlin <61420215+okxlin@users.noreply.github.com> Date: Tue, 28 Jul 2026 21:39:17 +0800 Subject: [PATCH] Add MicroBin application --- apps/microbin/2.1.0/.env.sample | 8 ++ apps/microbin/2.1.0/data.yml | 116 ++++++++++++++++++++ apps/microbin/2.1.0/docker-compose.yml | 46 ++++++++ apps/microbin/2.1.0/scripts/init.sh | 125 ++++++++++++++++++++++ apps/microbin/2.1.0/scripts/uninstall.sh | 5 + apps/microbin/2.1.0/scripts/upgrade.sh | 4 + apps/microbin/README.md | 52 +++++++++ apps/microbin/data.yml | 31 ++++++ apps/microbin/latest/.env.sample | 8 ++ apps/microbin/latest/data.yml | 116 ++++++++++++++++++++ apps/microbin/latest/docker-compose.yml | 46 ++++++++ apps/microbin/latest/scripts/init.sh | 125 ++++++++++++++++++++++ apps/microbin/latest/scripts/uninstall.sh | 5 + apps/microbin/latest/scripts/upgrade.sh | 4 + apps/microbin/logo-LICENSE.txt | 4 + apps/microbin/logo.png | Bin 0 -> 29394 bytes 16 files changed, 695 insertions(+) create mode 100644 apps/microbin/2.1.0/.env.sample create mode 100644 apps/microbin/2.1.0/data.yml create mode 100644 apps/microbin/2.1.0/docker-compose.yml create mode 100755 apps/microbin/2.1.0/scripts/init.sh create mode 100755 apps/microbin/2.1.0/scripts/uninstall.sh create mode 100755 apps/microbin/2.1.0/scripts/upgrade.sh create mode 100644 apps/microbin/README.md create mode 100644 apps/microbin/data.yml create mode 100644 apps/microbin/latest/.env.sample create mode 100644 apps/microbin/latest/data.yml create mode 100644 apps/microbin/latest/docker-compose.yml create mode 100755 apps/microbin/latest/scripts/init.sh create mode 100755 apps/microbin/latest/scripts/uninstall.sh create mode 100755 apps/microbin/latest/scripts/upgrade.sh create mode 100644 apps/microbin/logo-LICENSE.txt create mode 100644 apps/microbin/logo.png 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 0000000000000000000000000000000000000000..da0c331d7fed5346282c64c14fc97ca53cfdde2a GIT binary patch literal 29394 zcmeFZWmHsA+c10p5tI@GkVaDJk{m#h7%5Rg8l|MWYXk)q1sSAMK}0&FQ&Di}?(Rmq z;oWDrpZ9%!e(U>vv(_wNpS`c{ecnXjg&`9*K9BK3IwsSGk+=$lem-Mam~RhQhG11 zm-|K35KP%;{M_X*+2yocQ#L)QvTP*??cAp-Cgp}ovPrGooYeQ;ux{i+~41$ZA(NQBWw@6-i@?ACm!lC(&vw}2_ zz2i!7x^+gPv-fUM`v&57-%M7nu{)XgY?b>d4Vk+|&1>$T3_vB9929k&A&7?={@{3g zm3D(5MhN-fo`y%l%BW|;jH~bZk^9cwQABr|f0%L#(0hKB z*$VCa{7yO2?zys1e;d-tnSM|x&RY2sO@z1o)oHg692aK2-d|MWxd53=fwYn(^J@Gk{EL#yRN&;?F_FKEPJFdIq z5QO3p3^!dqE#(g0C=tfguWmH7k+zcsr)lj#|LT`o`;5`45wxXetNRRx%U^GV0*h0d zKV=zLcG@~MV(>g_-}NxDj|}`tb`3YknsEN{MKmFj58o<-|x>>hwLjTsp~Z45Wy?` z^q4_!^#KIcT`OZfN|iyk7XyN|CCLl`2>zv3E$$~4M@DhqU!Ng2<}W~y7seD#BX|S< zlpiAugl1aI+^NF#PZ14}O-EGS#aN@Wb3AAv$oOz1X{H`2)kAOZQ?&FfBk9Vcd_b}g zCM%+f2eWu13=e`@s(9HC9-*^_raus5(g#@);=KMYq9=?`tORw5w;Wz;3H zB)4c>-+$^|RZuzQxxmPja%IPSIa`_g)F{?e{m(S8ywT~OEDS1}B7h>wEc&2W_6se~ zcg(GQ)7wsJ?_&6|*j#GuSr6isoJ|189e##5)LAF z%!V#cA+jo}U-ttb=LoCVRx@@UFG^CZB6 z=p&F)QQsN=NdvaOS=<|(1x$IyUWO>O5JZ61?tSI-fI4AKvR@I`a4R&b2P=dyc@|3) zC?K@znF?k6mui{_Lg|Nu3%7uAM$I$|x{{!}@6Z!K(1e38D+X|3;$6kBv%{xMj#~j+ zIyWYnU95UZ2LMPPjE<-(2S)j@H<0my1`q^CQO@5@JRej^ziftGEIcy_gARPz=R^u$2w@|%p`mWO1ag~R$d5O zd~e_Y_cPHa=j?v2(Wj?=4tI+u+;v?03h?Mw=A_0Gl^i&gWmdW0j3E)Af_8gDfEn|W;@7+kKSYn7E4U5 zWfClE8vc}e07USMMdShSRA?cM2tD%1z9h+Lhq1aT6cwmpUvd~x#r&l7;&qTqp-lep z83UwWC1Pwqdxx+30ih@%Fi|sf7CJ#2OFauxDtU$-cxPoMeVVq1+!53ppA#bJdR+z! zo_^k9DWgtKldOw#h0ufgC=9ns5Oa3*tQx?s=zV}5SK8DIy(}qAH2e)vp=Gg@3(aG# z5+axxVmbSY7%MAUx>pgIilg>r%%+<0i2@FYLlA1r(Jnpovi{>8#!*0ZBhX5*G9CW@ z&i*=x868<{g09*x$8Ewy*s|!; z*&x_4uH?WLp@&YU=;htKOCcJjFwAJy0~_=otOjKoxsjWLUPZ2!y@48^%dP-f8OF~f zqu-y)D)OevX8Te98@bzeE&$b+S}f6{@y-vU%c@{Bgn9tPN+Wswwo`Xvj4;${WbbW^fUT+zvPVIsR)h`-8ly-X=%HXm3Hf?1-m z-U%Tlr;&!8^!6(mULRuxtYzy?6QQ%~DUK6LTc+%xC}L?ipR1z{9dA>cOkL zd3s{qtYe`SgiJWxG7uCp9+f2#~j=5BnYqwInTJN9dMGDr*__Te5d>lA?qq~SQd{jo}KpmMZMk#R1Nnc315>YVku>_QPt7|2w~*PSIuGpfCDZ6{k$|dOwbpf^9LzKhh@G^OaSw3FAwScSS<(z zEH7?m8mXFbe(s6dCht*yI^x?rOc1r=<{mQX3D&HxFSoETjMmDp-bUQOg|jM#0Ijce z{%HL*a;(-rXGqZN&IJ}v3v|CG0`{+!y+h?3O>g;jjSvtw16Yz3$y$j)*d%9*&s*c%;na;B^D5o-eA>=Vs|h?Tfim?)Y`pxmaELYCy<@%Gql<_Z)V^+5bc|{ER-P5Cg*s4Wa!g`HIk#aS*&cE3Lc^i z-PO`_DIMH<10}y$8@lF+==sK0@Ss3-0M*$PNMeHb@;oc=_Y?e%w=l0ke3%DOZ4-94D2t^Es2S_Ska@>wY=vPRLX)(U zB(Ltw430>UdF_o@T|Di~F06A#&iv7AgO};Hr+{Xf?hs_Gya4(JVI;jw34UN!E`qrP zFa&B!dPYj%p7Py-S7&viX2>$VHXpjdu1F;48V#><^un}QNSvh6kpW$SP}LtnJGwXz zDozxSlO8$(NS`jzr|VAW#VSE89NNV1!cGdVE()+!b7P{>+wPm5fUy*%RV62R@2ZVn zRm8JKED~L`^jlaOP}o9yk8Zh%xTny>JC^pvi7l{ShF7!*2&VHp`|@8{9l|0Qk%;WI z-`Hni<;)mIK-g}q-xt+Y!u`lMFqnU)eWq5clGElsFmm1anK>3_!4rM(q@&)AMXUy- z>osL5B)i*%V?V6qJj9ui4Hkz;^K)w1F~x!hpgM&3niIO|?e#^TkV%;^8K14!T_t{2cORq(7BEkc zV2yUd!B%neaM7>Ba+DkxMG$%<&UDp9iU;1mw97&R0{dw94ywDFdBWlr>@{^RVs|iV zYV&xi!=bSF625N{Wu-6k1JR;JVoha1z3MDZZzJx0;n)-_j2OEtf#SI&ita*YV38ov zo1;zAr#Ap7RAZU&6Zb1Iq9X)xxpX+ZAFxUjoE>1{jSU4$SNGce_yKOHB_lCFJ21TzyeGn+~=rS+t%&wL9FTK}iD- z&?aE}FzF5ssXNZuc={wiSVrM>M}IHgXOft~9(aacXTOO^fc*s(Sct$ZfsEPx`FO70 zQ}|36E3cGp`3QZ-jM@W-`*rQK4ePKQla4awVaRU7@)kr3k?tU2S=2i{1crL@$Zz`d z8eu*S*al^~Dq3#^);u+?9*m2g$z|ChoxKEawJZ?Ez`1$(=W)91Ao)Kdte|Cj=U6>= zex9bwUcw25(a5-qE(jmqql07BHo;>yu-GSMQm9{df)hdT^6YyoJ&&h zNIhEpWEAi~*fvTpq0W}gwel$cE^Ho~WFcd_8M}7QK@l@%3P^kY+DZMlA##Uvmt7EMMz(nu z`tNHn)fK?U7gW?L(kXGlPsY2RUBV~V3}N%lL2g#xqyc>b6k}-(V${0yX`)&Q^F+Cz zB*;<_Ur~~SEibH=x)qm4OcN_NX|X!;R4Xev3%7=;_e9=hT})p54=KEuU$ElIYPa43 zX|Q9Zv0ZV>((ZRGk<4+S40d#=g;ct*;XkAth6|sXeUl9vcaw4>QAUo=UM3DG&$<{bLpx^>}>5##UrD|6t9-!F_0aJluYyfO&O& z{cn!@dP|b8pI0!NJ6<~Ga49^Y%$$WimmX8V8C2w9=pRIoyh2(lZRI2|SmA86HdMtf z;_I(aQ*gH2BC4G$`Oi|7Zu64FRo|J>MYOCtxV6&{yrf(D+sTz zeJk&8#7(4dROH~o!k%-}bGstZdsk&Hs?t5(w=wkisI+u;O9YXSBXQC}TBMm1)uSCY zb9`-1y5gUa`0l>_pWxjx6XuhfG>h#GH|jkxmLX`CYY~{z_0dRsAAbeAGGD#Zord78 zy1we*6CX&X-LpmaY@?rW{{dP_LY!11J1;7s!p_dnp(#M$X)}N;$$ek?6GP^xH~W_T z_y}6f*Vb9ey}RBJ6R4A-rF1u!s_pAHt`;I{BBvSj=O**m808wD;Xa>D>-eoa5vhdW zlDD{^-BANR&g(N{PtSn6rdU`p&3rGo*9vpnTEThoHjy*Z45)0haoIjHnz7%Qt~vvwU*DMpGP6`UG1g*9UN_T@*-YD zLoVe{`a?3owj24()Fajnnpg13{|_QlHfViq6HhL3dTgX&J!tR)cb%h;?N3% z&)(MFj_}t}ecCI5GqFKGQX;}oHFv7-dV)8|Lnx!|IakrCe{ei2W<6b%sUT)5v8Ozf zxy;aV=Gf|+%L?k*c27WVF${GWeH_*#A8@lnTZu=~^4{3-vDsjmQ`wSZdTaXX(dJL< z^=8d}>4~X;M+MzsN%0~^WnArJ9hCs9+gj{jh1zZ7)1|Gw*&6NuK{bj41S#$y|Wy<(NY zp%860tZmBVYLWG`W8XoM$q-^_yKE2C*7gt43^m9N&?A)s-GBa?a z=xF&ibbX>zkMQ(gD9Ss6RvjbC2h*6~Uc331RwN`8hc<&-Vt01*%*R4%mmz+I6m_%k zIiU1lv)6O2VUP|=`a9+uaZu!6ECGgG1JyvDo2LG7rQsZ5ljFi7N*s7ne7u6+yqQiE zs_1XDy69iewR;dx-zIUIw=U67)SaQFWVo9xXIb+?bB<&Wabh3yr^XSu*^t8e-_o@~ zogEIS?q_^Im@IkEe2aC}RK*Q889e^!v(HWAH2b>wT^NLPI=!^D)n5C9--2Gkn@9hm zCiTswW`^3`jH<0o;Xeu}7BR; zn0fAThP4>i@(Ms5%zf^H)+=Lygudbm4rmk)A8yh;s7W( z60V~oKKo51Iz=8COX?M4VTl!d>%P#@ksT+1eDbFOemJyijyF+exrB)jD7{GaDC> z77QX$d)1DAhn!&9KhyB*&f?V_QqiqXsxhb&Cq^t(8W_=}Y`xLrt#(54Mu^E%wMEmpoVG<17)q zRAh;!1d(L1b0|%7a?K3EJ#^OGcQQYO5 zfu$DfHJD5V9LTR{q=rQ)D+xF0q3@33>4LDe9LjRLq9}UK6-;0m&@*oeURLdW>kEOp z7s_cGH{+#ihUk=_6bWu}+y$#F3Kd&Hx|@ZqE)~K?KLjWft5TNuQy!N`+}lwnzA*rSYe5anXSI)O_gB< z!HGe?;4bj`?_QsDmTf6{+ksy5m-s=+TF8uA+iB+2k4=qk{qFT%$F@y|`)oKX%!M82 z-S#yLZcKS&m?okjdruE&T{C2*3^X*)6`;|@>oXyeHb-a4jkT>cD>mRWyRq z;ka!!3lRMx&vOke+06O1gmjr+-nt?yCq6dA^gyzoN`|z?LexN^ZfSg&U~4jnaGQ5y4KjnrN9=R zV%^O-A~1%zh@@zz`g)7MeRzzoC# zeZPh;nT-(Y@gTX(j-l@ynv1rWCS`a)zSYKYx1k`RticXtx7Dx}_BR1XFAPe-b-VB= z2b5FCGoHC59UoMsz3fz_hhFzC*t+4l3R(1l>>B{8VzLSj)L{t&r9?PC>t+6Ty-nOL>5;6O_oSfDhedNv zO47NT3WJ^(AUbmZQK!IxuU)i>({(hvHZO`uEzW3%%0cnsp>_Gf3_d{9UABEanHgHh z7xM+z2T1_})hUy~wT2Fhw++?5iXgn}B`I&M;!q}6F`l#$Kr-wkJFFX{(GQ;*?*>`W zAw-lKX~|OdJieBTaM98HwOn2$#Y2&1?r0)#%yf%9IM~Y*wbd4~RH~m7RGWg}Nu=hI z{Z*T=9_rA!4@Ej~rIdP@MH)KyHFyFK=StDmcj@wJ#Dh`-UqN>^eDZpBK0WDgb=`x! znzq!g=Xqhil;erHCQbth+`bRdi3}JuU&^8*S`cOzZ@Nb&cA)5Q=-rA@xcr~z?k$^> z#Jj_|L)IWj|M_QZbeGH7V#M$Xm*Qz7=Q~d z1>#k=JyR9-sk`I*cy2ZgIn;3zkWySn=80$}TXc~CtMTm&kW0`N83j+d z@>0OB-F*8Cdm5U>FeOQ-uH_o;0>`|&cf9q5j#Xekw!X!tE};x6ka7BfosxoQac=2g z=e<(r52&&EqYF@rBoiu>nmT{JD7tk6kVCS2BsjD@N4(#80WyzUF999Lqbp2s<8k>G z+`Y)$djn1<~|Fw?)WgY*ui2n~3Vg3Qz3FAO1%b-vPHzv8D46E9inmqtjRwn2LQMv3FppI@; zJ!z3z=Yu8MVi(n3aC@?Sfe8{dF7C;s$DuE_IW_j%z;E6{cdvllB}$b`4$GUAP7J|NrX;iRGb5-X zJCQ`2yOg3(GbsJFeP8_wi zcp7SWmtDKsO!joHf?#t#xtd+ieTc4Zl4d1uR=C{&4O&#E{xtaR2W%cVJhJkvFIU={ zdO|rzCgtcZwc8sk0N!NK*#4r+Eq3fC%;1aSu0YC_4MUyB$@U1gv%OmBPiH4j+~4oN@wN55qAl%rvji7etx-CFLYG(xxs!7Bq1cew4FLXx1HKQZ#_Pm zv}fpRpIhx2!pIjwvfU5VxV80Uo-VusZ0@-63QD>94fG<()z1v`ymzV(`e{Te zXD7G{U2GWhe0Ha*HpP^je#h7ggI8~Ut69i=g-0#YUadnuM>PhX)nwg5AG)<}10^)%raR39Cm&0Nr#=U#PJ%O~gPTR+${STXD2dlNOEZ{p| zv?|}eN#y)TN6F@l09wGg4|Qn5gH{Q{5^;OK(?WPMb}*l3*Mx}Q(?C$D8PERuS#{XL zT7LZgkB;b&5v(U#Mu6pEGbQmHrBVJZFb#O8@4wu4EN$e;q3u6CiM7fXa-ec-mt${j zHv^-`qOjO#sme1P$Sd>;Vopg)94V!P0xPH}}c6pWFprUpZ{-dXUrxvz~!`XBEF)6&H-o%j|fc&=3G zyMdXgM^`{gVz)Ld5#?*35RDV$xWI?#<4&Ajs1u^0&Y10Sb#|UaZoxxMiuN6DT*&zD z<`a54VaWwex%B?&FjAknWwQCWc!su}QoGg?gD-X6I+dmz@W7)ejHbCrq-E!f1Cqf3 z&4E=LO75bdg-w|>uY|9$5xrQl zT}%K0lA|$?7Sq{0_w+&MmVEF96L6IQcRo753~jvnGUWnwieK=}C-Uts9PO$o&n2~b zJMYbnmRolxjZ|B<_vFRz&E%?7I*-?T7uohda9d3jT}i!&cT&tnwb=fv{6t4c{Nrn9 zc>2x=+E{Zi-+*H>wgXc~Ejkv);l8b7!{a2?S5u;*-6#*%jE(7vs z8zs!MPHS|*kig_1i#+&_{}C%t`%sCsQ7`=Dx(e7+Qb`S7U6lC>TUF_iEVyaZ0HN!@ zy$>v(|2BdD|GHs3O)>}ywr;P%=zDKPDJ!eaeTksZK#)Q*+ShQQ1!P17Ci>LbTFS8= zdrhro@DV+$x5p$xHaP)DP#pjfpC1udX^WCH_OlqmxQ{gZ;Di;S#gpKePnK z8zV0hd`F(bN>M1>vXQ z9#QC9PJBF=pS2JB3S|_xyci#N*xYOMmzM>%l@3qtjg?vKiU_ z(d1zFvW5!~Eg=(lH#N69;$X?WWHxJjFlwe|yBD1rO``Vt<&N`KOR9VkNPfL06!uO6 z<*Q=`VLbb_evP{$PW^2wgBHDNt_|{sTVMb@>pIWXI_@`l3RlwZok+-o8q`6qY-|zs zqeX(_6&4m!hhGUISU`!BUgO0=qfI;?bHlmN@s9(z-E1trXx?EEzdO@#5^~zNS2CY2 z;cmLDI$yY`3TJo~O0H(>cEh-=ntd55kJS@c-6~C&*FTz67>DPQ zVB!aE+3LYfqG%(yr!8m>F0u-jxK8`NQe+jgO19GEzCNC~E|o7)I$FsoGFE`tvoCn_ zW$!)d&MX*6hjS)XviQ5Up1_siP%$nP#tg8)-ag{A*9L!lj)NGg{Kp)OLUp7PK5(W_ z)q$1*9`ibMddPGfQs7s60em{?ykZg>CJZepu@uFKaI6`Xnll8v!9B7iX_^r zwf|`h*6+j|b|@>e#h0$bm!*Z4!!A({|J=9$|{(%;hcqqD)v_5Hie-Y3)1EbHW9zja8NLLgE?!q@p^uVi#s zT^>DbeleJZIO*)ORnzmjJlnynyI`q<|0{h3_t`7DyDV4GRJZ>A5Tskbtyj{wPDVo= z5b?Am?Z}hfcPvlaFg7Z8AS&mmYp*7mUQ=Y7cz=&4S|etjx~dN?P*CTz2C z^v+*LPU+#)V;lFguo@%(gZ3joxAH^7w_y#>-!-Huy2K}^4xe~xy!-w9-OgDJJM9xa z?FTt8^K}pX$J{=T58Y6=)_%bKGG9E9Ty=rK?dP=HVwYH|uy+IDNq;KM&R|r^BB)!b zjBx zU1xnFl=(3c4WYL01Ef%wmYioUeT7e4`un!KbodbJ(`UE}v9$F;rk{EaYN#+on}~xy z9lCuFyW8+tu0rX_3v|PEv$#-PNTv(M2q{uO`Sg9bEk5lRZP+3Q1hL$Z1FJH%izd!4 z(>m887bm*_9rIsc$O8?Zy{|8jjq^l@fOoMJ~G#u+~{FWzKu)zhq^nwn!1(uAV z=(d|Vf#2rhLvbLPUsyaVbqs3BDOFraJUCFo>vbEjP2!Ou`YWdzwY4~S)9(^2M=1ZA z1-K{dXBMxY0HSn>CU8I5_Vj7sII>y8W?!K8qn{IxfHi~y(33YEN{xX#dUn%Sei#z4N`qU zG|atNqACIJ7OlL-a!3_A9{nA_tH$CZuJ^mPrK|wvi9A>x)bpiEYV$-df-ahv9?L7= zCkoRS+i#Syyb59Ieio;mWDTPXl*f{wEXW1}TM5J>h`AF32pJ#*r|bAd^GVXN&t8FN z6I5ccK6-&SQ!fk7ICek4fmU8xC__-=-zHHq%?s92OjG9TQZ_7wXc;Jz>JVCvIK?=wb*_NM8 z@Cw%eK9-#4{N0y6wh2gHR|iK@0GXol-tNC$E~N2Gok5TrY~BvA=l9H3Kzr=kScVm~XvS5@m;t+PxkAi&eX>p_t<6F&;N63R z1UzVf0G5d3(vT?MmG4`abe?Vi^bHYq(a@mNP8_%E?|uK^MIfZ#u;omMme%$egVhI5 zES@rG5hw!-JvW+0%hk))N}R!^Ob3RCWgp;IUQ1+Q3e)DJopvCW@qk&MB%thI7>c5M zjUlh$H#vdc;@3ZZ1lj(Nsfs>U;LB`nwIP$)tlD>sAAr4t8CHP_V&_@NBB29c2|h5f zcQA~n?p)d0_A${m;T}qvc!wS)(OBQS1k+ydOMUV4UYv3x3U74tVfAwtU|{iZ%n=N^Yf!dkeFln6y8(kO+4ASL>s_ZGZ|S{2o14p>R%B<;>;@AA zvuzBW9j55aj~1wYCbUIe%gH8LYKiZ6;$wsuxnVoV#|!Dd=rZ&n>&C*Q*QV-bTPA6u z^RRAjvEy4ANX84cEMs;nYA=`Hy~#_Xz3;<@4fwA2x^9&&pewCMG57TEq19mOGtk>Vr=F zEFT?I5}#E+?MUvpLH&^iMFSS&JC_}K*KQ@(ip8(u$j63JdPNrRJ0`n{ zgiY?}Hc(pJ5#dBMzcl8^1hM!)#NGN&x?Rq|NRI0c;T8p}(8|#!;26 zf8uOZGwYw{kKOGxUM9W4MX1B6YU%Brj-&N)0;= zTgyO-gki&9ec5J~P)JWBu6_1sv#cL=ErrbW(_NoB2jc|9k2wANLG|ov(T%uuEO@uSB-K%wK#_7ky5lW!ew_w38NDg`v5}{%#r{r~BlJ6qXa1 z6&5@9;$8RRKiJTq%>J|0SXn*Y&!vYWQ7TJAEcQC|9UZX<+&r`^HuytN1VhP-v1O8> zo)K$tiY{6#9}~vc9u~Pw@|%gT)?GF87&xjw(Z;?Vjt^kWGCx}|;iDmU#oW2W}2z@<{2lvt_&Ji$*#o8GN zRVvS)DS(2KV$KO^v_}E%)R?#HBvdL6X2tQn%9^0Z*h&|2c$2zE|M@IVLUoo^Cq32p zBQSmgw_IPV5G!giX$VvA_!(?QshF9Sxpb~{#+?-*IB;mE@)4J}dm_CT8XLnVmq-<< z*|089$OK+cJO94`5RJG?81BjLkBvB?JHsy5S)KyfD23AHJR#-iJ8K#LA|kcQM|X)7 zF3ms`R|Mg$7GYa8NB{J>_+_-seak4jgGm-R|K@rEcVJZwo8T z@|ZuGO#I4l84f{QT%fBS_s`O2UpKN5zf9cxuWUQZ{(bkoC!0INjd#B%=HOD@I_E89 zsK}oaVueavSKgC*O-b*4bibvC&nJYnOVpn-{VsQl9b5c~Oo;05K3W;!uy{Sun?(=* zigXER!yt$PHYd>WEvKEt;MTQ)K3OxVt5kDBJIqGIg3n6)s!ag=_YzRut#_M}aqg3* z524?)G4IGL=BI8`>(nh*QPH034Hq=JOQ%ZdR%fGwdmr{!5mMS@hIch9KePzY;om)v z1XOp;TuQLLGF`nVL{%{=Z@&1mIo)b~qdYyeZt6T>1@6Yjd?VmV%q;j53s!MJW>^?g zN?(}xegJ;`b-qb9bfoB!yMd67oF{#^`UE2g(xZN-rgWwD;foyk9OopV;J`7*u$G&k zoP=iH5J2#Z?gzdVpZ@r`Tiy7cPcNJb9bEvCV1p3}i>Zx>&D!){&>}}2 zrTmPSOXER=aEWL)*o5u2A(nghLiNr>{n2hTv1a0z|J^WoC*TS;X(r6`)8St{4>*_R z94oi|E!K0Z8$sm6UFiT>reanh;#11I-!J$2u#%=-O+&>SN0q<`&^|)Fn^U>kG8SWjAO?jJO z5q|evmKXk&860lyxj#WlX%nJOW-Kaqq8XGmDCY6g@Uco|#K{%4`(duyBtnNrRrqdG z|76OvjYyFI3%ZUEPDYfi^kBxoXEQEslLq?uE!>1qELl`R{Bp5} z$8AIo0$8xdZ$Mb`v2X&9zA`rE(6mieLG0LB0Ti?l45Dp6n=^Q3A@7;XBNnyFqk0}W ziGPAAn1LHD5vyqb!I^qG#%vJS$fa{$nD%=`8aUY_zI8j~5{D_(#!49SxDkqy8kGZu zp+xD5wLj0T2>x;*W?Up4O_gEyD1V3QK39U{UyRGLh;t*D=%lsq*76(Ti*eBM=2-4vxmNU>wJ@yW5=BLBEbgq7Q zMgvDaeLXr$c!H8?AJyFYt(N&5WuU`|l$^Ma*XKnl6#vnB(l?L+VnSbDIJZ(artax3 zUNu2 z0ql?rbQy4^`Ga3yf^NXj6B(sCmNx+KkYkVWLD5PTFLqym2~2Z^gIlbmT{`Z{n{&O_ zJ@dY91vCdZGgIMd?o05M2b?jTb9T`IQ;?bupg&;oDZ*8V`&Ic(Fgye}9ihfND7HJ=Gg~e|Pub@)t;P{4w zz0;1lNV!p_HgxSxCXH7QEg@&eHBD_bjqs<+^wCP?I-tI-)YSd>8JPQo^iob@j$mqhy{I3`~sGut>{3k+-lu z8{KmG+k@Mhknk-*IVf2r#@m=*4n=~MDdVb{MVZT&(!Vb6e_Va{`$xxk?g2yb(x@ig z(j_vdUTGA7rIcB(`qVYW$Zj$X6QM?*1z7; zV3*EBVJp&CDhkLqz@&NNv~yHi`y^*NR5^ySL$K~r566foezk^R+D(;#{6Os z(LUS(lL9-< z0MMWR2Z$}wQL64y)tr4lO|n}|X!v7>Y$8!Vng@k^o+c{i@>Z&Pe6#k2a>ePOZa4q? zGRL1_D#zc=Zxh@YD~yd9jH_h^T#tgD|J>BFec{yi6_+5Z29f6P+wojHX!TRH`EAZX zYeL=gIM&s+@I`snbW=mr5IfBbP~S4Nf5g(HAtO1$V>zYxP$X+I3;xUQF4Asj10Ip} z_28>C@>w}6<$>Aes0;D%q$KGwcD61v6f5-Z7NemW?_|CtGVY9g7;j2$!yIu=wnTy{ zeKHtDZ8YWdcV)L3tUJ?tod_bd!OH zj($Olgaf4K_NnT3S58I}fUpz~_*zvNx2GVvS)}8pOTqwG@@a{Z6l{(=F%N2L^V$dhFpD-on}Z;);xib-E9o{KnRRgBW(S86KF+L` zcrK0`^tn1Di~*{<`80;uP(?OxkuXX_mYnYeDC0o2-OZm*QiFTzt9Sqx%W8Hayb))F z=e9`5x1WK<1+lk`8EN7@YJW3_H70!h*D3RG<6U7je&$`L^ zh*f-!!63CR!`p{yk^ZS_di)<-+8C|^3i@8f??8|{PTXF@hb~GT@T@*4C*&3*&wCfL z_W>+Y*B$zPS@1r-L;KdUSBoOTZD(=L4j!pf-6i!5QS&2J3VM0{(MK>!uhaF=V4l^o zdCqUCprQ!tW$+}57eRPrMr zj0UQ^=i%z~k1g6PKGYcb#G{s2m+!hJOVh3~=VwBhCVp~bwd>$j4ffB3R8>eW4}#xE zX8&R?=F`7}(njJcQtp#e->dvwhXaXTVvivkGX$wuAwR9UVHV(`xo+PlOOEH<+(s`BT;tzFnzrEfgOn(0YXcV&CGw7%b{?R|PiKDG z^tnoU>KCg-4!9qXx3OC5Wq%(xd|!yIdYd2Ef1QqQWsFNmqvH|9?e2Qc3G|E`lHDr= zM6bv`9yZ+U2JW{{hK+v*%;uys8()#qr+j2WpcVx|ZG|-j{Vo^uZnteMfP4sy{#Bj| zrhi}xTx8jNO@|HIN2i7Rwy8LN*R^51m6yLRLK6>`mvmx&Td%!j1$6xyiLGLo(o`Wg z7Ou8K`e+#c?(6#)m9s8DhbKY)wqbjdopyx~a-cW-{C=GO#_PC8ac1H{>Y#8Hw>*0e z2dG7P4~5b>5POZ3 z^Pbz)vJU}^i6AisFy&Gk^MjkQY%r*-9Jdhy(S6_8K<0n0Y%B-puhc4sDM=%qWiL|6 z>pLD(&6+!Q7JF=f$jydpJXXIh?1&^Ms@RG2g6&uF&Mr4u>vzEr*N#sgivWykxieEMlENXw$mC7JwfxARinwH z?RD&aE%a6$8r;+gA8(Da74zRPzFAtO`;BUz9fX_QikJQMK~_sKD?bKrIdrZFtO_#q z11rj$n-s{wsnpOAP}(xJ28Fo-Y*u;|N6}5E8BQJlFVX`xE)^f#H&JqvAVI zw9iU7ifz{gyh@{@HCa`68ms2v08<^)3Os2D!}FWf4aKzc&OL! zKb9QYqV5OOi}u-^!B7G9|g`+QwF7iNe@M zAvGGl=QI5N`Sp7B*R!AJob#M>-tYNv@Mka{4F`{XT_&A!dv3{YZ=2$@OSbB@HN=~Y zg}Y`%cS0Hn=MrE&;tGC#yk2JoJv?OJ{D=8XqTvrim8vHwRpKzzCWrT0bYJX&Q1aSM zVn;=PU!dg^{Yo`d|MEhSc?}og-&mqEW96160u?J==@xdbHcf?%FOOY5S3PheJigBY zG89IJ*_UI{+*O`tvYeJqdj(l4%4TzlA%1XtdufKdb@{_>jU)TqkgY^0H$JkTFgdo; z^{r=l*x^~t@oO8s4_d6B|P_3=ppWTr-v#jR@v7leci6OQpy+Nsunkj z;=z%EB3>*p4}5E{+a7uDjI=KlT9Zwgc2v~U%jJhGQ>VJC^DVl9J?4m?hIBpf=l)#7$WvxecR_L}llKgw3WURs_h4drJx6hR5bnJyx$IQq zONWU#mEiZ4d1i){rAPrDF6S5)$O<7N*x|y3kwf7v=UkyQh}~ce_YSH#1C%*Kl`f@U zI%jvrMK`1ld0A$9+3p3mt+%9~&L`gm@XmRo1j48}`*`$a-zAYal>LaA;=tj3aWna{ z_tnH&LPnfF<;orWOcr8=w^Ik82*rrc#5&IB9;a;LmQHCdbtNPJz2~@mz7Lu!IEr*Z zabI11^@!;Hzg5Nn)!>FZeb^CCQ~ zJdW}*!<7}f)uj^Ha60>Cf+yj>1;Bwzd>#+(8qhcoL1252iQ{2bp8YnNxhYWK$E@u& z!Ao{$!g58#Ttl;Lfo=Z?AQ%asep(jQHnPSk^pign=so{(s0op49}msd|8Jw9C5Rm; z?{PTcz!SJ%r(adsKnlW`e>7~9IiW1pN?HHumhJCi-x&=*FPMA7Ijdn~4wh(UHV<&CB;nOTCKhCo6!jU+8TKK};oHi^@7r>{tftRxN` zf~R}meNWS{D~CNi+0L%~`C)r2Q>U8beOD5tUQ0Y$pc3F*ER;opTTT7R%rf$RAN+oa z1BJVx)X&2+++%WM!HZ3e(sU0e?5>Qv2v`Djt+u2OF6x?aBVpjrJpG-EEtR2mfrA^y zl$OqZ*~a0@0Vvf+hIi7c^FqIaqNb~C~B(f;Wd3c5Yl?t9V_ zrtSCxdMy^5%HJhaP$6Xb*xzHi4RtN6iI6s5&~G=|^mCwQuQAsB5KoeloKKAA1W}E2 zqMgX*yitYCU%#C8*r9E&E5u+vubufPBZ5$>mcO~%ZU2o_o=3T z_f7M&$o}#*u^eR#3AaC1K9kYDKkszwx6Fi8rBbZGFvB3`Kv+2@`f&lW_2V5uuP z9D|#vS^Rg+3__8s9P+G1JnSs^Bf=?&X_l9I4s?enc0CI;D1LfsAgDybhwo4s6xx0k9iqd{!j~yS!#KhBZ=~o5S~407WE~6Uemk$ z3|7nI{uR@TPK}=+zsZK~Cx)W|wo1UMLFqgP_owY#Jg|}7@QQi}_Y9OgT19Pc;+|TQ z&yaR$LCGMx@YJ_!xZQzu1cuBcl|3N2aY2DHE%1OS6`>Ds*=$aTCuCC%D-O5A-uxZC z4JDoi)TP1=4eYtrOIZo5_LTJWB=h>nXKw@^=MR!)nmS(%O*Os$xS&|9CxBDkc@OiU z|GF1oxxXKm_OYb=2p*5rXF{;@AJ;D?Yc_nz9QjjnR`GoG%!_wq}H&c+Rsw$|T9-Lc24o2%1l%`&h6VO}3oo9X$x(2GPis z)Ttt+ft0E>K7BVw8pXG4g@f8&LkL8-6<0qkH}VQdoOtm)-U`?BilDcc>d>YpDLs17znAYjAmde{c5mS37+ zVG01u>4OHAD(m;3JjbP!TP^q0aMVMXe5J9ff3%V7*LmyPgg2=zEWSm+J+v^iM{*e} z(1cwpvF@a`b_2$Y$b_jOi~N`MT9LZc>V~S~fYOa^t2Ipk`wC!a`Nts6L%9YgDtOxl z%6`-C+JArM^>wHa==?phx>oPv?f;qWPu_Lw-3Y8waSP%~5tonNNVya|LSY_cf*j*Z zX=+-`bXAGVBI4sycr1V~bKk*1q*wa^M@62l5DU%Z={Qohs}YFD*lE`}Sn;6@9+K$q z+v65d6IE?BY;OokNB5Wy(nP^$AU=8{!fOCynZ&BFK8cprW}P;iSvHblqM}tbuqTH8owmTv?YIhm1f~JWR|SB7J{}` zwf+|_&ivPPPnD?;^J}i^5F<)|tkq|6((j3xu^UnTLEl0S=nMI>;q``YKoF8t`I`pN z8;}VqD+Qgr&~uK7;+_o}DqaC3x(n*#U!ZImB*n=-IGhxR8<4gF8XC|xx ZJZj1 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 literal 0 HcmV?d00001