Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bddcee50da | |||
| a62e92922e | |||
| 34fc9ef534 | |||
| 9f29b39e11 | |||
| f41b7eed2d | |||
| 16a4cef212 |
@@ -1,12 +1,6 @@
|
|||||||
# jsdelivr cdn
|
# CDN
|
||||||
|
OneManager-php 程序 美化主题js
|
||||||
|
添加到<#body>下
|
||||||
|
<script src="//cdn.jsdelivr.net/gh/ldxw/CDN@v0.02.1/scfone-theme-js/odscf/scf.js"></script>
|
||||||
|
|
||||||
[](https://www.jsdelivr.com/package/gh/ldxw/cdn)
|
原作者地址:https://cdn.jsdelivr.net/gh/vcheckzen/CDN@0.02/odscf/logi.js
|
||||||
|
|
||||||
# 目录说明
|
|
||||||
- [图片壁纸](https://github.com/ldxw/cdn/tree/master/image) | image
|
|
||||||
- [logo](https://github.com/ldxw/cdn/tree/master/logo) | logo
|
|
||||||
- [静态文件](https://github.com/ldxw/cdn/tree/master/static) | static
|
|
||||||
|
|
||||||
|
|
||||||
# cdn链接
|
|
||||||
https://cdn.jsdelivr.net/gh/ldxw/cdn@master/
|
|
||||||
|
|||||||
@@ -1,173 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
set -e
|
|
||||||
|
|
||||||
# 自用创建上传证书
|
|
||||||
|
|
||||||
CERT_USER="3x-ui-cert"
|
|
||||||
CERT_DIR="/mnt/docker/docker-compose/dpanel/dpanel/compose/3x-ui/data/cert"
|
|
||||||
SFTP_ROOT="/srv/sftp/${CERT_USER}"
|
|
||||||
SFTP_DIR="${SFTP_ROOT}/cert"
|
|
||||||
|
|
||||||
echo "========== 3x-ui 证书 SFTP 用户一条龙配置 =========="
|
|
||||||
|
|
||||||
if [ "$(id -u)" != "0" ]; then
|
|
||||||
echo "错误:请使用 root 用户执行"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "1. 安装依赖..."
|
|
||||||
apt update
|
|
||||||
apt install -y openssh-server acl
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "2. 创建/检查 3x-ui 证书目录..."
|
|
||||||
mkdir -p "$CERT_DIR"
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "3. 创建/检查 SFTP 用户:$CERT_USER"
|
|
||||||
if id "$CERT_USER" >/dev/null 2>&1; then
|
|
||||||
echo "用户已存在:$CERT_USER"
|
|
||||||
else
|
|
||||||
useradd --badname -m -s /usr/sbin/nologin "$CERT_USER"
|
|
||||||
echo "请设置 $CERT_USER 的密码:"
|
|
||||||
passwd "$CERT_USER"
|
|
||||||
fi
|
|
||||||
|
|
||||||
CERT_UID="$(id -u "$CERT_USER")"
|
|
||||||
CERT_GID="$(id -g "$CERT_USER")"
|
|
||||||
|
|
||||||
echo "当前用户 UID:$CERT_UID"
|
|
||||||
echo "当前用户 GID:$CERT_GID"
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "4. 创建 SFTP 隔离目录..."
|
|
||||||
mkdir -p "$SFTP_DIR"
|
|
||||||
|
|
||||||
# Chroot 根目录必须 root 拥有,且不能给普通用户写权限
|
|
||||||
chown root:root /srv
|
|
||||||
chmod 755 /srv
|
|
||||||
|
|
||||||
mkdir -p /srv/sftp
|
|
||||||
chown root:root /srv/sftp
|
|
||||||
chmod 755 /srv/sftp
|
|
||||||
|
|
||||||
chown root:root "$SFTP_ROOT"
|
|
||||||
chmod 755 "$SFTP_ROOT"
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "5. 重新 bind 挂载证书目录..."
|
|
||||||
if mountpoint -q "$SFTP_DIR"; then
|
|
||||||
umount "$SFTP_DIR" 2>/dev/null || umount -l "$SFTP_DIR"
|
|
||||||
fi
|
|
||||||
|
|
||||||
mount --bind "$CERT_DIR" "$SFTP_DIR"
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "6. 修复 /etc/fstab 自动挂载..."
|
|
||||||
cp /etc/fstab "/etc/fstab.bak.$(date +%F-%H%M%S)"
|
|
||||||
|
|
||||||
TMP_FSTAB="$(mktemp)"
|
|
||||||
awk -v sftp="$SFTP_DIR" '$2 != sftp {print}' /etc/fstab > "$TMP_FSTAB"
|
|
||||||
cat "$TMP_FSTAB" > /etc/fstab
|
|
||||||
rm -f "$TMP_FSTAB"
|
|
||||||
|
|
||||||
echo "$CERT_DIR $SFTP_DIR none bind 0 0" >> /etc/fstab
|
|
||||||
systemctl daemon-reload 2>/dev/null || true
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "7. 给真实路径上级目录添加穿透权限..."
|
|
||||||
P="$(dirname "$CERT_DIR")"
|
|
||||||
while [ "$P" != "/" ]; do
|
|
||||||
setfacl -m u:${CERT_USER}:--x "$P" 2>/dev/null || true
|
|
||||||
setfacl -m m::rwx "$P" 2>/dev/null || true
|
|
||||||
P="$(dirname "$P")"
|
|
||||||
done
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "8. 修复 cert 目录本身权限..."
|
|
||||||
chgrp -R "$CERT_GID" "$CERT_DIR"
|
|
||||||
chmod -R g+rwX "$CERT_DIR"
|
|
||||||
find "$CERT_DIR" -type d -exec chmod g+s {} \;
|
|
||||||
|
|
||||||
# 给 SFTP 用户完整读写权限
|
|
||||||
setfacl -R -m u:${CERT_USER}:rwX "$CERT_DIR"
|
|
||||||
setfacl -R -m u:${CERT_UID}:rwX "$CERT_DIR"
|
|
||||||
setfacl -R -m m::rwx "$CERT_DIR"
|
|
||||||
|
|
||||||
# 默认继承权限:以后新建文件夹/文件也能写
|
|
||||||
find "$CERT_DIR" -type d -exec setfacl -m d:u:${CERT_USER}:rwx {} \;
|
|
||||||
find "$CERT_DIR" -type d -exec setfacl -m d:u:${CERT_UID}:rwx {} \;
|
|
||||||
find "$CERT_DIR" -type d -exec setfacl -m d:m::rwx {} \;
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "9. 可选给 UID 1000 读取权限,避免覆盖当前用户..."
|
|
||||||
if [ "$CERT_UID" != "1000" ]; then
|
|
||||||
setfacl -R -m u:1000:rX "$CERT_DIR" 2>/dev/null || true
|
|
||||||
find "$CERT_DIR" -type d -exec setfacl -m d:u:1000:rX {} \; 2>/dev/null || true
|
|
||||||
else
|
|
||||||
echo "当前 $CERT_USER 就是 UID 1000,跳过 u:1000:rX,避免覆盖写权限"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 最终再覆盖一次,确保 3x-ui-cert 一定是 rwx
|
|
||||||
setfacl -R -m u:${CERT_USER}:rwX "$CERT_DIR"
|
|
||||||
setfacl -R -m m::rwx "$CERT_DIR"
|
|
||||||
find "$CERT_DIR" -type d -exec setfacl -m d:u:${CERT_USER}:rwx {} \;
|
|
||||||
find "$CERT_DIR" -type d -exec setfacl -m d:m::rwx {} \;
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "10. 写入 SSH SFTP 限制配置..."
|
|
||||||
rm -f /etc/ssh/sshd_config.d/3x-ui-cert-sftp.conf
|
|
||||||
sed -i '/# BEGIN 3X_UI_CERT_SFTP/,/# END 3X_UI_CERT_SFTP/d' /etc/ssh/sshd_config
|
|
||||||
|
|
||||||
cat >>/etc/ssh/sshd_config <<'SSHEOF'
|
|
||||||
|
|
||||||
# BEGIN 3X_UI_CERT_SFTP
|
|
||||||
Match User 3x-ui-cert
|
|
||||||
ChrootDirectory /srv/sftp/3x-ui-cert
|
|
||||||
ForceCommand internal-sftp -d /cert
|
|
||||||
PasswordAuthentication yes
|
|
||||||
PubkeyAuthentication no
|
|
||||||
PermitTTY no
|
|
||||||
AllowTcpForwarding no
|
|
||||||
X11Forwarding no
|
|
||||||
AllowAgentForwarding no
|
|
||||||
# END 3X_UI_CERT_SFTP
|
|
||||||
SSHEOF
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "11. 检查并重启 SSH..."
|
|
||||||
sshd -t
|
|
||||||
systemctl restart ssh 2>/dev/null || systemctl restart sshd 2>/dev/null || service ssh restart
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "12. 测试 SFTP 实际路径写入..."
|
|
||||||
runuser -u "$CERT_USER" -- touch "$SFTP_DIR/.sftp_write_test"
|
|
||||||
rm -f "$SFTP_DIR/.sftp_write_test"
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "13. 测试新建域名目录和证书文件..."
|
|
||||||
TEST_DIR="$SFTP_DIR/.test-domain"
|
|
||||||
runuser -u "$CERT_USER" -- mkdir -p "$TEST_DIR"
|
|
||||||
runuser -u "$CERT_USER" -- touch "$TEST_DIR/fullchain.pem"
|
|
||||||
runuser -u "$CERT_USER" -- touch "$TEST_DIR/privkey.pem"
|
|
||||||
rm -rf "$TEST_DIR"
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "========== 配置完成 =========="
|
|
||||||
echo
|
|
||||||
echo "SFTP 用户:$CERT_USER"
|
|
||||||
echo "SFTP 端口:22"
|
|
||||||
echo "SFTP 上传目录:/cert"
|
|
||||||
echo "真实目录:$CERT_DIR"
|
|
||||||
echo
|
|
||||||
echo "第三方连接命令:"
|
|
||||||
echo "sftp -o PubkeyAuthentication=no -o PreferredAuthentications=password ${CERT_USER}@服务器IP"
|
|
||||||
echo
|
|
||||||
echo "当前 ACL:"
|
|
||||||
getfacl "$CERT_DIR"
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "重点确认应该看到:"
|
|
||||||
echo "user:${CERT_USER}:rwx"
|
|
||||||
echo "default:user:${CERT_USER}:rwx"
|
|
||||||
@@ -1,676 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
# Debian 12/13 Fail2Ban SSH 修复/安装脚本
|
|
||||||
#
|
|
||||||
# 功能:
|
|
||||||
# - 自动识别当前 SSH 客户端 IP,并加入 ignoreip,避免误封自己
|
|
||||||
# - 检测旧版 Fail2Ban,可选择修复、删除重装、仅检测、安全测试、紧急救援
|
|
||||||
# - 使用 systemd journal backend,避免 /var/log/auth.log 不存在
|
|
||||||
# - 使用 nftables 封禁
|
|
||||||
# - 启动后等待 fail2ban socket 和 sshd jail 真正 ready
|
|
||||||
# - 安全测试只封禁文档测试 IP 203.0.113.123,并立即解封
|
|
||||||
#
|
|
||||||
# 用法:
|
|
||||||
# bash setup_f2b_fixed.sh
|
|
||||||
#
|
|
||||||
# 可选:手动追加白名单 IP
|
|
||||||
# IGNORE_IPS="127.0.0.1/8 ::1 你的公网IP" bash setup_f2b_fixed.sh
|
|
||||||
#
|
|
||||||
# 救援模式:
|
|
||||||
# bash setup_f2b_fixed.sh --rescue
|
|
||||||
#
|
|
||||||
# 修复模式:
|
|
||||||
# bash setup_f2b_fixed.sh --repair
|
|
||||||
#
|
|
||||||
# 状态检测:
|
|
||||||
# bash setup_f2b_fixed.sh --status
|
|
||||||
#
|
|
||||||
# 安全封禁测试:
|
|
||||||
# bash setup_f2b_fixed.sh --safe-test
|
|
||||||
|
|
||||||
CONF_FILE="/etc/fail2ban/jail.d/99-sshd-hardening.local"
|
|
||||||
F2B_LOCAL="/etc/fail2ban/fail2ban.local"
|
|
||||||
TS="$(date +%Y%m%d-%H%M%S)"
|
|
||||||
BACKUP_DIR="/root/fail2ban-backup-${TS}"
|
|
||||||
|
|
||||||
DEFAULT_IGNORE_IPS="127.0.0.1/8 ::1"
|
|
||||||
IGNORE_IPS="${IGNORE_IPS:-$DEFAULT_IGNORE_IPS}"
|
|
||||||
PRESERVED_IGNORE_IPS=""
|
|
||||||
|
|
||||||
TEST_IP="203.0.113.123"
|
|
||||||
|
|
||||||
need_root() {
|
|
||||||
if [ "$(id -u)" -ne 0 ]; then
|
|
||||||
echo "请以 root 权限运行"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
dedupe_words() {
|
|
||||||
echo "$*" | tr ' ' '\n' | awk 'NF && !seen[$0]++' | paste -sd' ' -
|
|
||||||
}
|
|
||||||
|
|
||||||
get_ssh_client_ip() {
|
|
||||||
if [ -n "${SSH_CONNECTION:-}" ]; then
|
|
||||||
echo "$SSH_CONNECTION" | awk '{print $1}'
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -n "${SSH_CLIENT:-}" ]; then
|
|
||||||
echo "$SSH_CLIENT" | awk '{print $1}'
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
}
|
|
||||||
|
|
||||||
collect_old_ignoreips() {
|
|
||||||
if [ ! -d /etc/fail2ban ]; then
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
find /etc/fail2ban -type f \( -name "*.local" -o -name "*.conf" \) -print0 2>/dev/null |
|
|
||||||
while IFS= read -r -d '' file; do
|
|
||||||
awk '
|
|
||||||
/^[[:space:]]*ignoreip[[:space:]]*=/ {
|
|
||||||
val=$0
|
|
||||||
sub(/^[^=]*=/, "", val)
|
|
||||||
sub(/[[:space:]]*#.*/, "", val)
|
|
||||||
gsub(/^[[:space:]]+|[[:space:]]+$/, "", val)
|
|
||||||
if (val != "") print val
|
|
||||||
}
|
|
||||||
' "$file"
|
|
||||||
done | tr '\n' ' '
|
|
||||||
}
|
|
||||||
|
|
||||||
backup_fail2ban_config() {
|
|
||||||
if [ -d /etc/fail2ban ]; then
|
|
||||||
echo "正在备份旧配置到:$BACKUP_DIR"
|
|
||||||
mkdir -p "$BACKUP_DIR"
|
|
||||||
cp -a /etc/fail2ban/. "$BACKUP_DIR/" 2>/dev/null || true
|
|
||||||
echo "备份完成:$BACKUP_DIR"
|
|
||||||
else
|
|
||||||
echo "未发现 /etc/fail2ban,跳过备份。"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
detect_ssh_unit() {
|
|
||||||
if systemctl cat ssh.service >/dev/null 2>&1; then
|
|
||||||
echo "ssh.service"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
if systemctl cat sshd.service >/dev/null 2>&1; then
|
|
||||||
echo "sshd.service"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "ssh.service"
|
|
||||||
}
|
|
||||||
|
|
||||||
detect_ssh_ports() {
|
|
||||||
local sshd_bin=""
|
|
||||||
|
|
||||||
if command -v sshd >/dev/null 2>&1; then
|
|
||||||
sshd_bin="$(command -v sshd)"
|
|
||||||
elif [ -x /usr/sbin/sshd ]; then
|
|
||||||
sshd_bin="/usr/sbin/sshd"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -n "$sshd_bin" ]; then
|
|
||||||
"$sshd_bin" -T 2>/dev/null | awk '$1 == "port" {print $2}' | paste -sd, - || true
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
clear_fail2ban_nftables() {
|
|
||||||
if ! command -v nft >/dev/null 2>&1; then
|
|
||||||
echo "未检测到 nft 命令,跳过 nftables 清理。"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "正在清理 Fail2Ban 相关 nftables 表..."
|
|
||||||
|
|
||||||
nft list tables 2>/dev/null |
|
|
||||||
awk 'tolower($0) ~ /f2b|fail2ban/ {print $2, $3}' |
|
|
||||||
while read -r family table; do
|
|
||||||
if [ -n "${family:-}" ] && [ -n "${table:-}" ]; then
|
|
||||||
echo "删除 nftables 表:$family $table"
|
|
||||||
nft delete table "$family" "$table" 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
rescue_fail2ban() {
|
|
||||||
echo
|
|
||||||
echo "========== 紧急救援模式 =========="
|
|
||||||
echo "将停止并禁用 Fail2Ban,同时清理 Fail2Ban nftables 表。"
|
|
||||||
|
|
||||||
local ssh_client_ip
|
|
||||||
ssh_client_ip="$(get_ssh_client_ip)"
|
|
||||||
|
|
||||||
if command -v fail2ban-client >/dev/null 2>&1; then
|
|
||||||
if [ -n "$ssh_client_ip" ]; then
|
|
||||||
echo "尝试解封当前 SSH 客户端 IP:$ssh_client_ip"
|
|
||||||
fail2ban-client set sshd unbanip "$ssh_client_ip" 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "尝试执行全局解封..."
|
|
||||||
fail2ban-client unban --all 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "停止 Fail2Ban..."
|
|
||||||
systemctl stop fail2ban 2>/dev/null || true
|
|
||||||
|
|
||||||
echo "禁用 Fail2Ban 开机启动..."
|
|
||||||
systemctl disable fail2ban 2>/dev/null || true
|
|
||||||
|
|
||||||
echo "清理旧 socket 文件..."
|
|
||||||
rm -f /run/fail2ban/fail2ban.sock
|
|
||||||
rm -f /var/run/fail2ban/fail2ban.sock
|
|
||||||
|
|
||||||
clear_fail2ban_nftables
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "救援完成。"
|
|
||||||
echo "现在可以重新尝试 SSH 登录。"
|
|
||||||
echo "恢复后建议运行:"
|
|
||||||
echo " bash setup_f2b_fixed.sh --repair"
|
|
||||||
echo "=================================="
|
|
||||||
echo
|
|
||||||
}
|
|
||||||
|
|
||||||
wait_fail2ban_ready() {
|
|
||||||
local timeout="${1:-45}"
|
|
||||||
local i=0
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "正在等待 Fail2Ban 完全启动,最多等待 ${timeout} 秒..."
|
|
||||||
|
|
||||||
while [ "$i" -lt "$timeout" ]; do
|
|
||||||
if systemctl is-active --quiet fail2ban; then
|
|
||||||
if fail2ban-client ping >/dev/null 2>&1; then
|
|
||||||
if fail2ban-client status sshd >/dev/null 2>&1; then
|
|
||||||
echo "Fail2Ban 已启动,socket 正常,sshd jail 已加载。"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
sleep 1
|
|
||||||
i=$((i + 1))
|
|
||||||
done
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "Fail2Ban 未能在 ${timeout} 秒内完全 ready。"
|
|
||||||
echo
|
|
||||||
|
|
||||||
echo "systemd 状态:"
|
|
||||||
systemctl status fail2ban --no-pager -l || true
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "socket 检测:"
|
|
||||||
if [ -S /run/fail2ban/fail2ban.sock ]; then
|
|
||||||
echo "socket 存在:/run/fail2ban/fail2ban.sock"
|
|
||||||
elif [ -S /var/run/fail2ban/fail2ban.sock ]; then
|
|
||||||
echo "socket 存在:/var/run/fail2ban/fail2ban.sock"
|
|
||||||
else
|
|
||||||
echo "socket 不存在。"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "fail2ban-client ping:"
|
|
||||||
fail2ban-client ping || true
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "fail2ban-client status:"
|
|
||||||
fail2ban-client status || true
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "最近 Fail2Ban 日志:"
|
|
||||||
journalctl -u fail2ban -n 100 --no-pager || true
|
|
||||||
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
show_fail2ban_status() {
|
|
||||||
echo
|
|
||||||
echo "========== Fail2Ban 当前状态 =========="
|
|
||||||
|
|
||||||
if ! command -v fail2ban-client >/dev/null 2>&1; then
|
|
||||||
echo "未检测到 fail2ban-client。"
|
|
||||||
echo "======================================"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "[1/4] systemd 服务状态:"
|
|
||||||
if systemctl is-active --quiet fail2ban; then
|
|
||||||
echo "systemd: active"
|
|
||||||
else
|
|
||||||
echo "systemd: inactive 或 failed"
|
|
||||||
fi
|
|
||||||
|
|
||||||
systemctl status fail2ban --no-pager -l || true
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "[2/4] socket 检测:"
|
|
||||||
if [ -S /run/fail2ban/fail2ban.sock ]; then
|
|
||||||
echo "socket 存在:/run/fail2ban/fail2ban.sock"
|
|
||||||
elif [ -S /var/run/fail2ban/fail2ban.sock ]; then
|
|
||||||
echo "socket 存在:/var/run/fail2ban/fail2ban.sock"
|
|
||||||
else
|
|
||||||
echo "socket 不存在:/run/fail2ban/fail2ban.sock"
|
|
||||||
echo "这通常表示 Fail2Ban 还没初始化完成,或启动后又异常退出。"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "[3/4] fail2ban-client ping:"
|
|
||||||
if fail2ban-client ping >/dev/null 2>&1; then
|
|
||||||
fail2ban-client ping
|
|
||||||
else
|
|
||||||
echo "fail2ban-client 无法连接 socket。"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "[4/4] jail 状态:"
|
|
||||||
if fail2ban-client status >/dev/null 2>&1; then
|
|
||||||
fail2ban-client status
|
|
||||||
|
|
||||||
local jails
|
|
||||||
jails="$(fail2ban-client status 2>/dev/null | sed -n 's/.*Jail list:[[:space:]]*//p' | tr ',' ' ' | xargs || true)"
|
|
||||||
|
|
||||||
if [ -n "$jails" ]; then
|
|
||||||
for jail in $jails; do
|
|
||||||
echo
|
|
||||||
echo "---------- jail: $jail ----------"
|
|
||||||
fail2ban-client status "$jail" 2>/dev/null || true
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo "Fail2Ban 服务可能 active,但 client/socket 尚不可用。"
|
|
||||||
echo
|
|
||||||
echo "最近 Fail2Ban 日志:"
|
|
||||||
journalctl -u fail2ban -n 80 --no-pager || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "======================================"
|
|
||||||
echo
|
|
||||||
}
|
|
||||||
|
|
||||||
fix_fail2ban_startup() {
|
|
||||||
echo
|
|
||||||
echo "开始修复 Fail2Ban 启动/socket 状态..."
|
|
||||||
|
|
||||||
if ! command -v fail2ban-client >/dev/null 2>&1; then
|
|
||||||
echo "未检测到 fail2ban-client,请先安装 Fail2Ban。"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "停止 Fail2Ban..."
|
|
||||||
systemctl stop fail2ban 2>/dev/null || true
|
|
||||||
|
|
||||||
echo "清理旧 socket 文件..."
|
|
||||||
rm -f /run/fail2ban/fail2ban.sock
|
|
||||||
rm -f /var/run/fail2ban/fail2ban.sock
|
|
||||||
|
|
||||||
echo "测试配置..."
|
|
||||||
fail2ban-client -t
|
|
||||||
|
|
||||||
echo "重新启动 Fail2Ban..."
|
|
||||||
systemctl start fail2ban
|
|
||||||
|
|
||||||
wait_fail2ban_ready 45
|
|
||||||
|
|
||||||
echo "启动/socket 状态修复完成。"
|
|
||||||
}
|
|
||||||
|
|
||||||
install_packages() {
|
|
||||||
echo "正在安装 Fail2Ban 与 nftables..."
|
|
||||||
export DEBIAN_FRONTEND=noninteractive
|
|
||||||
|
|
||||||
apt-get update
|
|
||||||
apt-get install -y fail2ban nftables python3-systemd
|
|
||||||
|
|
||||||
echo "安装完成后先停止 Fail2Ban,等待写入安全配置后再启动..."
|
|
||||||
systemctl stop fail2ban 2>/dev/null || true
|
|
||||||
rm -f /run/fail2ban/fail2ban.sock
|
|
||||||
rm -f /var/run/fail2ban/fail2ban.sock
|
|
||||||
}
|
|
||||||
|
|
||||||
write_fail2ban_config() {
|
|
||||||
local ssh_client_ip
|
|
||||||
local old_ignore
|
|
||||||
local ssh_ports
|
|
||||||
local ssh_unit
|
|
||||||
local manual_ip=""
|
|
||||||
|
|
||||||
ssh_client_ip="$(get_ssh_client_ip)"
|
|
||||||
old_ignore="$(collect_old_ignoreips || true)"
|
|
||||||
|
|
||||||
if [ -n "$PRESERVED_IGNORE_IPS" ]; then
|
|
||||||
echo "检测到删除前保留的旧 ignoreip:$PRESERVED_IGNORE_IPS"
|
|
||||||
IGNORE_IPS="$IGNORE_IPS $PRESERVED_IGNORE_IPS"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -n "$old_ignore" ]; then
|
|
||||||
echo "检测到当前配置中的 ignoreip:$old_ignore"
|
|
||||||
IGNORE_IPS="$IGNORE_IPS $old_ignore"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -n "$ssh_client_ip" ]; then
|
|
||||||
echo "检测到当前 SSH 客户端 IP:$ssh_client_ip"
|
|
||||||
echo "会自动加入 ignoreip,避免误封当前登录来源。"
|
|
||||||
IGNORE_IPS="$IGNORE_IPS $ssh_client_ip"
|
|
||||||
else
|
|
||||||
echo "未检测到 SSH_CONNECTION,可能不是通过 SSH 执行。"
|
|
||||||
echo "强烈建议手动指定白名单,例如:"
|
|
||||||
echo ' IGNORE_IPS="127.0.0.1/8 ::1 你的公网IP" bash setup_f2b_fixed.sh'
|
|
||||||
echo
|
|
||||||
|
|
||||||
read -r -t 20 -p "请输入要追加的白名单 IP,留空跳过,20 秒后自动跳过:" manual_ip || true
|
|
||||||
manual_ip="${manual_ip:-}"
|
|
||||||
|
|
||||||
if [ -n "$manual_ip" ]; then
|
|
||||||
IGNORE_IPS="$IGNORE_IPS $manual_ip"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
IGNORE_IPS="$(dedupe_words "$IGNORE_IPS")"
|
|
||||||
|
|
||||||
ssh_ports="$(detect_ssh_ports)"
|
|
||||||
ssh_ports="${ssh_ports:-22}"
|
|
||||||
|
|
||||||
ssh_unit="$(detect_ssh_unit)"
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "SSH 端口:$ssh_ports"
|
|
||||||
echo "SSH systemd unit:$ssh_unit"
|
|
||||||
echo "最终 ignoreip:$IGNORE_IPS"
|
|
||||||
echo
|
|
||||||
|
|
||||||
mkdir -p /etc/fail2ban/jail.d
|
|
||||||
|
|
||||||
if [ -f "$CONF_FILE" ]; then
|
|
||||||
cp "$CONF_FILE" "${CONF_FILE}.bak.${TS}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -f "$F2B_LOCAL" ]; then
|
|
||||||
cp "$F2B_LOCAL" "${F2B_LOCAL}.bak.${TS}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "写入 SSH jail 配置:$CONF_FILE"
|
|
||||||
|
|
||||||
cat > "$CONF_FILE" <<EOF
|
|
||||||
[DEFAULT]
|
|
||||||
ignoreip = ${IGNORE_IPS}
|
|
||||||
|
|
||||||
# 使用 systemd journal,避免 /var/log/auth.log 不存在
|
|
||||||
backend = systemd
|
|
||||||
|
|
||||||
# 使用 nftables
|
|
||||||
banaction = nftables-multiport
|
|
||||||
banaction_allports = nftables-allports
|
|
||||||
|
|
||||||
# 避免反向 DNS 查询拖慢
|
|
||||||
usedns = no
|
|
||||||
|
|
||||||
# 基础封禁时间:10 天
|
|
||||||
bantime = 10d
|
|
||||||
|
|
||||||
# 统计窗口:1 小时
|
|
||||||
findtime = 1h
|
|
||||||
|
|
||||||
# 1 小时内失败 3 次即封禁
|
|
||||||
maxretry = 3
|
|
||||||
|
|
||||||
# 阶梯式封禁
|
|
||||||
bantime.increment = true
|
|
||||||
bantime.factor = 1
|
|
||||||
bantime.maxtime = 20w
|
|
||||||
bantime.rndtime = 10m
|
|
||||||
|
|
||||||
[sshd]
|
|
||||||
enabled = true
|
|
||||||
port = ${ssh_ports}
|
|
||||||
filter = sshd[mode=aggressive]
|
|
||||||
|
|
||||||
# Debian 通常是 ssh.service;部分系统可能是 sshd.service
|
|
||||||
journalmatch = _SYSTEMD_UNIT=${ssh_unit}
|
|
||||||
EOF
|
|
||||||
|
|
||||||
echo "写入 Fail2Ban 本地配置:$F2B_LOCAL"
|
|
||||||
|
|
||||||
cat > "$F2B_LOCAL" <<EOF
|
|
||||||
[Definition]
|
|
||||||
# IPv6 自动处理。写在 fail2ban.local,可避免 allowipv6 警告。
|
|
||||||
allowipv6 = auto
|
|
||||||
|
|
||||||
# 为阶梯式封禁保留更久历史记录
|
|
||||||
dbpurgeage = 180d
|
|
||||||
EOF
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "测试 Fail2Ban 配置..."
|
|
||||||
fail2ban-client -t
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "启用并重启 Fail2Ban..."
|
|
||||||
systemctl enable fail2ban
|
|
||||||
systemctl restart fail2ban
|
|
||||||
|
|
||||||
wait_fail2ban_ready 45
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "修复/安装完成。"
|
|
||||||
echo "当前 ignoreip:$IGNORE_IPS"
|
|
||||||
echo
|
|
||||||
}
|
|
||||||
|
|
||||||
safe_ban_test() {
|
|
||||||
echo
|
|
||||||
echo "========== 安全封禁测试 =========="
|
|
||||||
echo "测试 IP:$TEST_IP"
|
|
||||||
echo "说明:这是文档测试网段,不是你的真实 IP。测试后会立即解封。"
|
|
||||||
|
|
||||||
if ! command -v fail2ban-client >/dev/null 2>&1; then
|
|
||||||
echo "fail2ban-client 不存在,无法测试。"
|
|
||||||
echo "================================="
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! systemctl is-active --quiet fail2ban; then
|
|
||||||
echo "Fail2Ban 当前未运行,尝试启动..."
|
|
||||||
systemctl start fail2ban || true
|
|
||||||
wait_fail2ban_ready 45 || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! fail2ban-client status sshd >/dev/null 2>&1; then
|
|
||||||
echo "sshd jail 不存在或未启动,无法测试。"
|
|
||||||
echo "请先确认:fail2ban-client status sshd"
|
|
||||||
echo "================================="
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "执行测试封禁..."
|
|
||||||
fail2ban-client set sshd banip "$TEST_IP" || true
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "封禁后 sshd jail 状态:"
|
|
||||||
fail2ban-client status sshd || true
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "立即解封测试 IP..."
|
|
||||||
fail2ban-client set sshd unbanip "$TEST_IP" || true
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "解封后 sshd jail 状态:"
|
|
||||||
fail2ban-client status sshd || true
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "安全封禁测试完成。"
|
|
||||||
echo "================================="
|
|
||||||
echo
|
|
||||||
}
|
|
||||||
|
|
||||||
ask_safe_test() {
|
|
||||||
echo
|
|
||||||
local testyn=""
|
|
||||||
|
|
||||||
read -r -t 20 -p "是否进行安全封禁测试?只封测试 IP ${TEST_IP} 并立即解封。[y/N] " testyn || true
|
|
||||||
testyn="${testyn:-n}"
|
|
||||||
|
|
||||||
if [[ "$testyn" =~ ^[Yy]$ ]]; then
|
|
||||||
safe_ban_test
|
|
||||||
else
|
|
||||||
echo "已跳过安全封禁测试。"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
repair_current_install() {
|
|
||||||
echo
|
|
||||||
echo "准备修复当前 Fail2Ban 配置,不删除软件包..."
|
|
||||||
|
|
||||||
backup_fail2ban_config
|
|
||||||
|
|
||||||
echo "先停止 Fail2Ban,避免修复过程中误封当前连接..."
|
|
||||||
systemctl stop fail2ban 2>/dev/null || true
|
|
||||||
rm -f /run/fail2ban/fail2ban.sock
|
|
||||||
rm -f /var/run/fail2ban/fail2ban.sock
|
|
||||||
|
|
||||||
if ! command -v fail2ban-client >/dev/null 2>&1; then
|
|
||||||
install_packages
|
|
||||||
else
|
|
||||||
echo "确保 nftables 与 python3-systemd 已安装..."
|
|
||||||
export DEBIAN_FRONTEND=noninteractive
|
|
||||||
apt-get install -y nftables python3-systemd || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
write_fail2ban_config
|
|
||||||
}
|
|
||||||
|
|
||||||
remove_and_reinstall() {
|
|
||||||
echo
|
|
||||||
echo "准备删除旧版 Fail2Ban 并重新安装..."
|
|
||||||
|
|
||||||
PRESERVED_IGNORE_IPS="$(collect_old_ignoreips || true)"
|
|
||||||
if [ -n "$PRESERVED_IGNORE_IPS" ]; then
|
|
||||||
echo "已保存旧版 ignoreip:$PRESERVED_IGNORE_IPS"
|
|
||||||
fi
|
|
||||||
|
|
||||||
backup_fail2ban_config
|
|
||||||
|
|
||||||
echo "停止 Fail2Ban..."
|
|
||||||
systemctl stop fail2ban 2>/dev/null || true
|
|
||||||
|
|
||||||
echo "卸载 Fail2Ban..."
|
|
||||||
apt-get remove --purge -y fail2ban || true
|
|
||||||
|
|
||||||
echo "清理旧配置目录..."
|
|
||||||
rm -rf /etc/fail2ban
|
|
||||||
rm -f /run/fail2ban/fail2ban.sock
|
|
||||||
rm -f /var/run/fail2ban/fail2ban.sock
|
|
||||||
|
|
||||||
install_packages
|
|
||||||
write_fail2ban_config
|
|
||||||
}
|
|
||||||
|
|
||||||
main_menu() {
|
|
||||||
local ssh_client_ip
|
|
||||||
ssh_client_ip="$(get_ssh_client_ip)"
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "========== Fail2Ban SSH 修复/安装脚本 =========="
|
|
||||||
|
|
||||||
if [ -n "$ssh_client_ip" ]; then
|
|
||||||
echo "当前 SSH 客户端 IP:$ssh_client_ip"
|
|
||||||
echo "该 IP 会自动加入 ignoreip。"
|
|
||||||
else
|
|
||||||
echo "未检测到当前 SSH 客户端 IP。"
|
|
||||||
echo "建议运行时手动指定 IGNORE_IPS。"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "================================================"
|
|
||||||
echo
|
|
||||||
|
|
||||||
if command -v fail2ban-client >/dev/null 2>&1; then
|
|
||||||
echo "检测到系统已安装 Fail2Ban。"
|
|
||||||
echo
|
|
||||||
echo "请选择操作:"
|
|
||||||
echo " 1) 修复当前安装,不删除旧版,推荐"
|
|
||||||
echo " 2) 删除旧版 Fail2Ban 后重新安装"
|
|
||||||
echo " 3) 保留旧版,只检测状态,并可安全封禁测试"
|
|
||||||
echo " 4) 紧急救援:停止/禁用 Fail2Ban 并清理 Fail2Ban nftables 表"
|
|
||||||
echo " 5) 修复 Fail2Ban 启动/socket 状态"
|
|
||||||
echo " 0) 退出"
|
|
||||||
echo
|
|
||||||
|
|
||||||
local choice=""
|
|
||||||
read -r -p "请输入选项 [1/2/3/4/5/0],默认 1:" choice
|
|
||||||
choice="${choice:-1}"
|
|
||||||
|
|
||||||
case "$choice" in
|
|
||||||
1)
|
|
||||||
repair_current_install
|
|
||||||
show_fail2ban_status
|
|
||||||
ask_safe_test
|
|
||||||
;;
|
|
||||||
2)
|
|
||||||
remove_and_reinstall
|
|
||||||
show_fail2ban_status
|
|
||||||
ask_safe_test
|
|
||||||
;;
|
|
||||||
3)
|
|
||||||
show_fail2ban_status
|
|
||||||
ask_safe_test
|
|
||||||
;;
|
|
||||||
4)
|
|
||||||
rescue_fail2ban
|
|
||||||
;;
|
|
||||||
5)
|
|
||||||
fix_fail2ban_startup
|
|
||||||
show_fail2ban_status
|
|
||||||
;;
|
|
||||||
0)
|
|
||||||
echo "已退出。"
|
|
||||||
exit 0
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "无效选项,退出。"
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
else
|
|
||||||
echo "未检测到 Fail2Ban,将进行全新安装。"
|
|
||||||
install_packages
|
|
||||||
write_fail2ban_config
|
|
||||||
show_fail2ban_status
|
|
||||||
ask_safe_test
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
need_root
|
|
||||||
|
|
||||||
case "${1:-}" in
|
|
||||||
--rescue)
|
|
||||||
rescue_fail2ban
|
|
||||||
exit 0
|
|
||||||
;;
|
|
||||||
--repair)
|
|
||||||
repair_current_install
|
|
||||||
show_fail2ban_status
|
|
||||||
ask_safe_test
|
|
||||||
exit 0
|
|
||||||
;;
|
|
||||||
--status)
|
|
||||||
show_fail2ban_status
|
|
||||||
exit 0
|
|
||||||
;;
|
|
||||||
--safe-test)
|
|
||||||
safe_ban_test
|
|
||||||
exit 0
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
main_menu
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
button,hr,input{overflow:visible}audio,canvas,progress,video{display:inline-block}progress,sub,sup{vertical-align:baseline}html{font-family:sans-serif;line-height:1.15;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0} menu,article,aside,details,footer,header,nav,section{display:block}h1{font-size:2em;margin:.67em 0}figcaption,figure,main{display:block}figure{margin:1em 40px}hr{box-sizing:content-box;height:0}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}a{background-color:transparent;-webkit-text-decoration-skip:objects}a:active,a:hover{outline-width:0}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:bolder}dfn{font-style:italic}mark{background-color:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}audio:not([controls]){display:none;height:0}img{border-style:none}svg:not(:root){overflow:hidden}button,input,optgroup,select,textarea{font-family:sans-serif;font-size:100%;line-height:1.15;margin:0}button,input{}button,select{text-transform:none}[type=submit], [type=reset],button,html [type=button]{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:ButtonText dotted 1px}fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-cancel-button,[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}[hidden],template{display:none}/*# sourceMappingURL=normalize.min.css.map */
|
|
||||||
@@ -1,443 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="zh">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<title>白昼黑夜切换器</title>
|
|
||||||
<link
|
|
||||||
rel="stylesheet"
|
|
||||||
href="https://cdn.oplist.org/gh/ldxw/cdn@master/code/html/react-day-night-toggle-theme-animation/css/normalize-5.0.0.min.css"
|
|
||||||
/>
|
|
||||||
<style>
|
|
||||||
@import url("https://fonts.googleapis.com/css?family=Gloria+Hallelujah");
|
|
||||||
* {
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
body {
|
|
||||||
background: #7ec0ee;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
height: 100vh;
|
|
||||||
width: 100vw;
|
|
||||||
overflow: hidden;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: 3rem;
|
|
||||||
font-family: "Gloria Hallelujah", cursive;
|
|
||||||
font-weight: bold;
|
|
||||||
color: #fafafa;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="root">Loading...</div>
|
|
||||||
<!-- 修正CDN链接格式,删除多余空引号 -->
|
|
||||||
<script src="https://cdn.jsdmirror.cn/npm/react@16.3.0-alpha.1/umd/react.development.js"></script>
|
|
||||||
<script src="https://cdn.jsdmirror.cn/npm/react-dom@16.3.0-alpha.1/umd/react-dom.development.js"></script>
|
|
||||||
<script src="https://cdn.jsdmirror.cn/npm/styled-components@2.2.1/dist/styled-components.min.js"></script>
|
|
||||||
<script>
|
|
||||||
function _defineProperty(obj, key, value) {
|
|
||||||
if (key in obj) {
|
|
||||||
Object.defineProperty(obj, key, {
|
|
||||||
value: value,
|
|
||||||
enumerable: true,
|
|
||||||
configurable: true,
|
|
||||||
writable: true,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
obj[key] = value;
|
|
||||||
}
|
|
||||||
return obj;
|
|
||||||
}
|
|
||||||
function _extends() {
|
|
||||||
_extends =
|
|
||||||
Object.assign ||
|
|
||||||
function (target) {
|
|
||||||
for (var i = 1; i < arguments.length; i++) {
|
|
||||||
var source = arguments[i];
|
|
||||||
for (var key in source) {
|
|
||||||
if (Object.prototype.hasOwnProperty.call(source, key)) {
|
|
||||||
target[key] = source[key];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return target;
|
|
||||||
};
|
|
||||||
return _extends.apply(this, arguments);
|
|
||||||
}
|
|
||||||
const {
|
|
||||||
React,
|
|
||||||
ReactDOM,
|
|
||||||
styled: { default: styled, keyframes },
|
|
||||||
} = window;
|
|
||||||
|
|
||||||
const { createContext, Component } = React;
|
|
||||||
const { render } = ReactDOM;
|
|
||||||
|
|
||||||
const THEME_CONSTANTS = {
|
|
||||||
NIGHT: "night",
|
|
||||||
DAY: "day",
|
|
||||||
};
|
|
||||||
|
|
||||||
// 修正时间判断逻辑(原d<6语法错误,应取getHours())
|
|
||||||
const d = new Date();
|
|
||||||
const hours = d.getHours();
|
|
||||||
const defaultValue =
|
|
||||||
hours < 6 || hours > 19 ? THEME_CONSTANTS.NIGHT : THEME_CONSTANTS.DAY;
|
|
||||||
const ThemeContext = createContext(defaultValue);
|
|
||||||
|
|
||||||
const $night = "#111";
|
|
||||||
const $day = "#7ec0ee";
|
|
||||||
const $starColor = "#fefcd7";
|
|
||||||
const $sunColor = "#f9fa57";
|
|
||||||
const $size = 25;
|
|
||||||
const $transitionDuration = 0.75;
|
|
||||||
const $skyChangeDuration = 0.5;
|
|
||||||
const $positionX = 20;
|
|
||||||
const $positionY = 10;
|
|
||||||
|
|
||||||
const Container = styled.div`
|
|
||||||
background-color: ${(p) =>
|
|
||||||
p.theme === THEME_CONSTANTS.NIGHT ? $night : $day};
|
|
||||||
height: 100vh;
|
|
||||||
transition: background ${$skyChangeDuration} ease;
|
|
||||||
width: 100vw;
|
|
||||||
`;
|
|
||||||
|
|
||||||
// 修正BigStar组件,补充闭合花括号
|
|
||||||
const BigStar = styled.div`
|
|
||||||
border-radius: 100%;
|
|
||||||
height: ${$size}vh;
|
|
||||||
left: ${$positionX}vw;
|
|
||||||
position: absolute;
|
|
||||||
top: ${$positionY}vh;
|
|
||||||
transition: transform ${$transitionDuration}s ease 0s;
|
|
||||||
width: ${$size}vh;
|
|
||||||
z-index: 2;
|
|
||||||
`;
|
|
||||||
|
|
||||||
// 修正extend语法对齐
|
|
||||||
const Moon = BigStar.extend`
|
|
||||||
background-color: ${$starColor};
|
|
||||||
box-shadow: 0px 0px 50px #eee;
|
|
||||||
overflow: hidden;
|
|
||||||
transform: ${(p) =>
|
|
||||||
p.theme === THEME_CONSTANTS.NIGHT
|
|
||||||
? "scale(1)"
|
|
||||||
: "scale(0.5) translateX(-75vw) translateY(-120vh)"};
|
|
||||||
&:after {
|
|
||||||
box-shadow: inset 10px 10px 10px #bbb;
|
|
||||||
border-radius: 100%;
|
|
||||||
content: "";
|
|
||||||
display: block;
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Craters = styled.div`
|
|
||||||
background-color: #ddd;
|
|
||||||
border-radius: 100%;
|
|
||||||
opacity: 0.5;
|
|
||||||
position: absolute;
|
|
||||||
top: 15%;
|
|
||||||
left: 15%;
|
|
||||||
height: ${$size * 0.25}vh;
|
|
||||||
width: ${$size * 0.25}vh;
|
|
||||||
|
|
||||||
&:before {
|
|
||||||
background-color: #ddd;
|
|
||||||
content: "";
|
|
||||||
display: block;
|
|
||||||
border-radius: 100%;
|
|
||||||
height: 50%;
|
|
||||||
width: 50%;
|
|
||||||
position: absolute;
|
|
||||||
top: 110%;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:after {
|
|
||||||
background-color: #ddd;
|
|
||||||
content: "";
|
|
||||||
display: block;
|
|
||||||
border-radius: 100%;
|
|
||||||
height: 30%;
|
|
||||||
width: 30%;
|
|
||||||
position: absolute;
|
|
||||||
top: 80%;
|
|
||||||
right: 110%;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const CloudMove = keyframes`
|
|
||||||
0% {
|
|
||||||
transform: translateX(300%);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
opacity: .75;
|
|
||||||
}
|
|
||||||
25%, 75% {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: translateX(-300%);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Star = styled.div`
|
|
||||||
background: ${(p) =>
|
|
||||||
p.theme === THEME_CONSTANTS.NIGHT ? $starColor : $day};
|
|
||||||
border-radius: 100%;
|
|
||||||
height: ${(p) => p.size}px;
|
|
||||||
left: ${(p) => p.x}vw;
|
|
||||||
position: absolute;
|
|
||||||
top: ${(p) => p.y}vh;
|
|
||||||
opacity: ${(p) => (p.theme === THEME_CONSTANTS.NIGHT ? 1 : 0)};
|
|
||||||
transition: background ${$transitionDuration}s ease 0s,
|
|
||||||
opacity ${$transitionDuration}s ease 0s;
|
|
||||||
width: ${(p) => p.size}px;
|
|
||||||
z-index: 1;
|
|
||||||
`;
|
|
||||||
|
|
||||||
// 修正extend语法对齐
|
|
||||||
const Sun = BigStar.extend`
|
|
||||||
background-color: ${$sunColor};
|
|
||||||
box-shadow: 0px 0px 150px yellow;
|
|
||||||
transform: ${(p) =>
|
|
||||||
p.theme === THEME_CONSTANTS.NIGHT
|
|
||||||
? "scale(0.5) translateX(75vw) translateY(200vh)"
|
|
||||||
: "scale(1)"};
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Features = styled.div`
|
|
||||||
height: 50%;
|
|
||||||
width: 50%;
|
|
||||||
position: absolute;
|
|
||||||
right: 15%;
|
|
||||||
bottom: 15%;
|
|
||||||
transition: opacity 0.15s ease 0s;
|
|
||||||
transition-delay: 0.75s;
|
|
||||||
opacity: ${(p) => (p.show ? "1" : "0")};
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Blink = keyframes`
|
|
||||||
0%, 5%, 7%, 100% {
|
|
||||||
transform: scaleY(1);
|
|
||||||
}
|
|
||||||
6% {
|
|
||||||
transform: scaleY(0);
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Eye = styled.div`
|
|
||||||
border-radius: 100%;
|
|
||||||
height: 15%;
|
|
||||||
width: 15%;
|
|
||||||
background-color: black;
|
|
||||||
position: absolute;
|
|
||||||
top: 25%;
|
|
||||||
animation: ${Blink} 10s infinite 1s;
|
|
||||||
transform-origin: center;
|
|
||||||
|
|
||||||
&:after {
|
|
||||||
background-color: #fff;
|
|
||||||
border-radius: 100%;
|
|
||||||
content: "";
|
|
||||||
height: 40%;
|
|
||||||
width: 40%;
|
|
||||||
position: absolute;
|
|
||||||
top: 5%;
|
|
||||||
right: 30%;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const LeftEye = Eye.extend`
|
|
||||||
left: 25%;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const RightEye = Eye.extend`
|
|
||||||
left: 75%;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Smile = styled.div`
|
|
||||||
width: 55%;
|
|
||||||
height: 25%;
|
|
||||||
border-style: solid;
|
|
||||||
border-width: 4px;
|
|
||||||
border-top-color: transparent;
|
|
||||||
border-left-color: transparent;
|
|
||||||
border-right-color: transparent;
|
|
||||||
border-bottom-color: black;
|
|
||||||
border-radius: 100%;
|
|
||||||
position: absolute;
|
|
||||||
top: 35%;
|
|
||||||
left: 50%;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Face = ({ show }) =>
|
|
||||||
React.createElement(
|
|
||||||
Features,
|
|
||||||
{ show: show },
|
|
||||||
React.createElement(LeftEye, null),
|
|
||||||
React.createElement(RightEye, null),
|
|
||||||
React.createElement(Smile, null)
|
|
||||||
);
|
|
||||||
|
|
||||||
const ThemeToggleInput = styled.input`
|
|
||||||
cursor: pointer;
|
|
||||||
z-index: 3;
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
opacity: 0;
|
|
||||||
&:checked ~ div {
|
|
||||||
transform: translateX(20px);
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const ThemeToggleContainer = styled.div`
|
|
||||||
left: 10px;
|
|
||||||
position: fixed;
|
|
||||||
top: 10px;
|
|
||||||
width: 50px;
|
|
||||||
height: 30px;
|
|
||||||
border: 4px solid
|
|
||||||
${(p) => (p.theme === THEME_CONSTANTS.NIGHT ? "#fff" : "dodgerblue")};
|
|
||||||
border-radius: 30px;
|
|
||||||
transition: border-color 0.25s ease 0s;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const ThemeToggleSwitch = styled.div`
|
|
||||||
height: 20px;
|
|
||||||
width: 20px;
|
|
||||||
background: ${(p) =>
|
|
||||||
p.theme === THEME_CONSTANTS.NIGHT ? $starColor : $sunColor};
|
|
||||||
border-radius: 100%;
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
margin-top: -10px;
|
|
||||||
left: 1px;
|
|
||||||
transform: translateX(0px);
|
|
||||||
transition: background 0.25s ease 0s, transform 0.25s ease 0s;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const ThemeToggle = ({ onChange, checked }) =>
|
|
||||||
React.createElement(
|
|
||||||
ThemeContext.Consumer,
|
|
||||||
null,
|
|
||||||
(theme) =>
|
|
||||||
React.createElement(
|
|
||||||
ThemeToggleContainer,
|
|
||||||
{ theme: theme },
|
|
||||||
React.createElement(ThemeToggleInput, {
|
|
||||||
type: "checkbox",
|
|
||||||
onChange: onChange,
|
|
||||||
checked: checked,
|
|
||||||
title: "Toggle theme",
|
|
||||||
}),
|
|
||||||
React.createElement(ThemeToggleSwitch, { theme: theme })
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
const BackDrop = ({ stars }) =>
|
|
||||||
React.createElement(
|
|
||||||
ThemeContext.Consumer,
|
|
||||||
null,
|
|
||||||
(theme) =>
|
|
||||||
React.createElement(
|
|
||||||
Container,
|
|
||||||
{ theme: theme },
|
|
||||||
React.createElement(
|
|
||||||
Sun,
|
|
||||||
{ theme: theme },
|
|
||||||
React.createElement(Face, {
|
|
||||||
show: theme === THEME_CONSTANTS.DAY,
|
|
||||||
})
|
|
||||||
),
|
|
||||||
React.createElement(
|
|
||||||
Moon,
|
|
||||||
{ theme: theme },
|
|
||||||
React.createElement(Face, {
|
|
||||||
show: theme === THEME_CONSTANTS.NIGHT,
|
|
||||||
}),
|
|
||||||
React.createElement(Craters, null)
|
|
||||||
),
|
|
||||||
stars.map((star, idx) =>
|
|
||||||
React.createElement(
|
|
||||||
Star,
|
|
||||||
_extends({ key: `star--${idx}` }, star, { theme: theme })
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
const generateItems = (
|
|
||||||
numberOfItems = 5,
|
|
||||||
sizeLimit = 10,
|
|
||||||
animationDuration = 10
|
|
||||||
) => {
|
|
||||||
const items = [];
|
|
||||||
for (let i = 0; i < numberOfItems; i++) {
|
|
||||||
items.push({
|
|
||||||
animated: Math.random() > 0.75,
|
|
||||||
delay: Math.floor(Math.random() * animationDuration),
|
|
||||||
duration: Math.floor(Math.random() * animationDuration + 5),
|
|
||||||
size: Math.floor(Math.random() * sizeLimit + 1),
|
|
||||||
x: Math.floor(Math.random() * 100 + 1),
|
|
||||||
y: Math.floor(Math.random() * 100 + 1),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return items;
|
|
||||||
};
|
|
||||||
|
|
||||||
class App extends Component {
|
|
||||||
constructor(...args) {
|
|
||||||
super(...args);
|
|
||||||
_defineProperty(this, "state", {
|
|
||||||
theme: defaultValue,
|
|
||||||
});
|
|
||||||
_defineProperty(
|
|
||||||
this,
|
|
||||||
"toggle",
|
|
||||||
(e) => {
|
|
||||||
const { theme } = this.state;
|
|
||||||
this.setState({
|
|
||||||
theme:
|
|
||||||
theme === THEME_CONSTANTS.NIGHT
|
|
||||||
? THEME_CONSTANTS.DAY
|
|
||||||
: THEME_CONSTANTS.NIGHT,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 修正render方法定义(原写在constructor内,不符合React规范)
|
|
||||||
render() {
|
|
||||||
const { stars } = this.props;
|
|
||||||
const { theme } = this.state;
|
|
||||||
return React.createElement(
|
|
||||||
ThemeContext.Provider,
|
|
||||||
{ value: theme },
|
|
||||||
React.createElement(BackDrop, { stars: stars }),
|
|
||||||
React.createElement(ThemeToggle, {
|
|
||||||
checked: theme === THEME_CONSTANTS.NIGHT, // 修正checked判断逻辑
|
|
||||||
onChange: this.toggle,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_defineProperty(App, "defaultProps", { stars: generateItems(20, 4) });
|
|
||||||
render(
|
|
||||||
React.createElement(App, null),
|
|
||||||
document.querySelector("#root")
|
|
||||||
);
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
Before Width: | Height: | Size: 646 KiB |
|
Before Width: | Height: | Size: 487 KiB |
|
Before Width: | Height: | Size: 2.5 MiB |
|
Before Width: | Height: | Size: 572 KiB |
|
Before Width: | Height: | Size: 1.7 MiB |
|
Before Width: | Height: | Size: 135 KiB |
|
Before Width: | Height: | Size: 270 KiB |
|
Before Width: | Height: | Size: 2.6 MiB |
|
Before Width: | Height: | Size: 158 KiB |
|
Before Width: | Height: | Size: 138 KiB |
|
Before Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 223 KiB |
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 721 B |
|
Before Width: | Height: | Size: 721 B |
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 9.4 KiB |
|
Before Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 6.3 KiB |
|
Before Width: | Height: | Size: 6.3 KiB |
|
Before Width: | Height: | Size: 9.4 KiB |
|
Before Width: | Height: | Size: 9.4 KiB |
|
Before Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 6.5 KiB |
|
Before Width: | Height: | Size: 6.5 KiB |
|
Before Width: | Height: | Size: 557 B |
|
Before Width: | Height: | Size: 557 B |
|
Before Width: | Height: | Size: 8.0 KiB |
|
Before Width: | Height: | Size: 8.0 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 619 B |
|
Before Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 4.7 KiB |