Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e9e49a8d87 | |||
| 82375b31b6 | |||
| e270f0f569 | |||
| 06a38db443 | |||
| 1985b75b96 | |||
| 5c8ec75c13 | |||
| 58029b4ad9 | |||
| d6f0bccf8f | |||
| b7c59ad5db | |||
| 76ac67e53a | |||
| 5d13f87340 | |||
| 0dcfd7c3f7 | |||
| 4fe8c46b91 | |||
| 16a3694d63 | |||
| eec9114bf0 | |||
| b101b09a33 | |||
| 7e4d9dee01 | |||
| f53d3e7a92 | |||
| 4738bc869d | |||
| eea5278b94 | |||
| 656cb40077 | |||
| 13ae6e9c09 | |||
| 9f0a4f33e7 | |||
| a6a3bd5859 | |||
| 40811d95b4 | |||
| 28d3368e59 | |||
| cc242fe773 |
@@ -1,3 +1,12 @@
|
||||
# jsdelivr CDN
|
||||
# jsdelivr cdn
|
||||
|
||||
[](https://www.jsdelivr.com/package/gh/ldxw/CDN)
|
||||
[](https://www.jsdelivr.com/package/gh/ldxw/cdn)
|
||||
|
||||
# 目录说明
|
||||
- [图片壁纸](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/
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
#!/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"
|
||||
@@ -0,0 +1,676 @@
|
||||
#!/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
|
||||
@@ -0,0 +1 @@
|
||||
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 */
|
||||
@@ -0,0 +1,443 @@
|
||||
<!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>
|
||||
|
After Width: | Height: | Size: 351 KiB |
|
After Width: | Height: | Size: 646 KiB |
|
After Width: | Height: | Size: 487 KiB |
|
After Width: | Height: | Size: 2.5 MiB |
|
After Width: | Height: | Size: 572 KiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 135 KiB |
|
After Width: | Height: | Size: 270 KiB |
|
After Width: | Height: | Size: 2.6 MiB |
|
After Width: | Height: | Size: 158 KiB |
|
After Width: | Height: | Size: 138 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 223 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 66 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 721 B |
|
After Width: | Height: | Size: 721 B |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 9.4 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 5.8 KiB |
|
After Width: | Height: | Size: 5.8 KiB |
|
After Width: | Height: | Size: 6.3 KiB |
|
After Width: | Height: | Size: 6.3 KiB |
|
After Width: | Height: | Size: 9.4 KiB |
|
After Width: | Height: | Size: 9.4 KiB |
|
After Width: | Height: | Size: 4.8 KiB |
|
After Width: | Height: | Size: 4.8 KiB |
|
After Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 6.2 KiB |
|
After Width: | Height: | Size: 6.2 KiB |
|
After Width: | Height: | Size: 6.5 KiB |
|
After Width: | Height: | Size: 6.5 KiB |
|
After Width: | Height: | Size: 557 B |
|
After Width: | Height: | Size: 557 B |
|
After Width: | Height: | Size: 8.0 KiB |
|
After Width: | Height: | Size: 8.0 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 619 B |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 4.6 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 4.6 KiB |
|
After Width: | Height: | Size: 4.6 KiB |