用函数式思维写 shell 之 config 脚本
shell 练习:用函数式思维写 shell,以服务器安全-封锁 ip/解封 ip 作为示例。
config 脚本代码
bash
#!/bin/bash
# desc: 生成配置文件
# date: 2023-12-04 15:00
# author: echoxu
# 功能: 生成可自定义的配置文件
. lib/common
. lib/blacklist
. lib/nginx
. lib/firewall
# 设置默认值:当输入为空时
get_config_file_section_default_value(){
section_name=$1
section_value=''
case "$section_name" in
FilePath)
section_value=${HOME}/.server_secure.conf
;;
nginxlog_path)
section_value=/usr/share/nginx/logs/Access.log
;;
securelog_path)
section_value=/var/log/secure
;;
limit_count)
section_value=0
;;
blacklist_path)
section_value=${HOME}/.blacklist
;;
whitelist_path)
section_value=${HOME}/.whitelist
;;
server_secure_log_path)
section_value=${HOME}/server_secure.log
;;
block_ip_type)
section_value=firewalld
;;
*)
;;
esac
echo $section_value
}
# 检查封禁 ip 的方式, 1 为防火墙 2 为 Nginx host deny file
valid_block_ip_type_from_input(){
block_ip_type=$1
case "$block_ip_type" in
1)
block_ip_type=firewalld
;;
2)
block_ip_type=nginx
;;
*)
return 106
;;
esac
echo $block_ip_type
}
# 数据校验:当输入不为空时
valid_data_from_input(){
valid_data_name=$1
valid_section_value=$2
case "$valid_data_name" in
FilePath|nginxlog_path|securelog_path|blacklist_path|whitelist_path|server_secure_log_path)
real_path=$(get_real_path $section_value)
is_path_result=$(vaild_is_path_from_input $real_path)
code=$?
if [ $code != 0 ];then
return $code # 校验不通过继续往上层调用返回错误码
else
echo $is_path_result
fi
;;
limit_count)
is_number_result=$(vaild_is_number_from_input $valid_section_value)
code=$?
if [ $code != 0 ];then
return $code
else
echo $is_number_result
fi
;;
block_ip_type)
is_correct_input=$(valid_block_ip_type_from_input $valid_section_value)
code=$?
if [ $code != 0 ];then
return $code
else
echo $is_correct_input
fi
;;
*)
;;
esac
}
# 中间函数
config_file_add_section_handler(){
section_name=$1
section_value=$2
if [ -z "$section_value" ];then
section_value=$(get_config_file_section_default_value $section_name)
else
valid_result=$(valid_data_from_input $section_name $section_value)
code=$?
if [ $code != 0 ];then
return $code
else
section_value=$valid_result
fi
fi
echo $section_value
}
# 配置文件添加数据
config_file_add_section(){
section_name=$1
section_value=$2
section_handler_result=$(config_file_add_section_handler $section_name $section_value)
code=$?
if [ $code != 0 ];then
case "$code" in
100)
err_code $code "$values_from_args 是一个目录路径,请输入正确的文件路径..."
exit 1
;;
104)
err_code $code "$values_from_args 文件不存在,请先创建它..."
exit 1
;;
101)
err_code $code "抱歉,你无法对 $values_from_args 进行读写操作..."
exit 1
;;
102)
err_code $code "你输入的: $values_from_args 不是一个数字..."
exit 1
;;
106)
err_code $code "你输入的: $values_from_args 不在当前设定的区间范围内,请输入 1 或者 2..."
exit 1
;;
esac
else
section_value=$section_handler_result
fi
# 保存配置文件路径
if [[ $section_name == "FilePath" ]];then
echo "FilePath=$section_value" > $config_path_temp
fi
get_config_path_result=$(get_config_path)
echo "$section_name=$section_value" >> $get_config_path_result
}
# 设置配置文件参数
set_config_path(){
while true
do
echo "该命令将创建一个配置文件,在其中存储配置信息。"
read -p "请输入配置文件路径 (回车将使用 ~/.blascklist.conf 作为默认配置文件路径):" config_path
config_file_add_section FilePath $config_path
get_config_path_result=$(get_config_path)
cat /dev/null > $get_config_path_result
read -p "请输入 nginx 日志路径 (回车将使用 /usr/share/nginx/logs/Access.log 作为路径): " nginxlog_path
config_file_add_section nginxlog_path $nginxlog_path
read -p "请输入 secure 日志路径 (回车将使用 /var/log/secure 作为路径): " securelog_path
config_file_add_section securelog_path $securelog_path
read -p "请输入 limit (回车将使用 0 作为其值): " limit_count
config_file_add_section limit_count $limit_count
read -p "请输入 黑名单 文件路径 (回车将使用 ~/.blacklist 作为路径): " blacklist_path
config_file_add_section blacklist_path $blacklist_path
read -p "请输入 白名单 文件路径 (回车将使用 ~/.whitelist 作为路径): " whitelist_path
config_file_add_section whitelist_path $whitelist_path
read -p "请输入 黑名单/白名单 操作日志路径 (回车将使用 ~/server_secure.log 作为路径): " server_secure_log_path
config_file_add_section server_secure_log_path $server_secure_log_path
echo && echo -e "${Tip}: 请仔细阅读并选择使用何种方案来封禁 ip"
echo -e "${Tip}: 使用防火墙方案不用重启 Nginx,但误封操作可能导致 SSH 连接失败"
echo -e "${Tip}: 使用 Ningx Host Deny 方案需要重启 Nginx,但不会影响 SSH 连接"
echo && echo -e "${Red_font_prefix}${Font_color_suffix}${Green_font_prefix}1.使用 Firewwall${Font_color_suffix}"
echo -e "${Green_font_prefix}2.使用 Nginx Host Deny${Font_color_suffix}" && echo
read -p "请输入 [1-2] 来选择封禁 ip 的方案 (回车将使用 防火墙 来封禁 ip): " block_ip_type
config_file_add_section block_ip_type $block_ip_type
break
done
echo && echo -e "你使用了 ${Green_font_prefix}[$get_config_path_result]${Font_color_suffix} 作为配置文件路径" && echo
}
# 打印配置文件路径
print_config_file_path(){
config_path=$(get_config_path)
# 用于接收被调用方的返回值并将此返回值继续返回给调用方
# 当函数调用层数过多(超过两层)时再继续用 echo 作为函数返回值的话会导致最里层的调用函数的错误信息无法正确打印
code=$?
if [ $code != 0 ];then
return $code
fi
echo $config_path
}
# handler 函数
block_ip_type_change_handler(){
block_ip_type_input=$1
block_ip_type_after_change=''
if [ "$block_ip_type_input" = "firewalld" ];then
block_ip_type_after_change=nginx
fi
if [ "$block_ip_type_input" = "nginx" ];then
block_ip_type_after_change=firewalld
fi
echo $block_ip_type_after_change
}
# 数据迁移: 将防火墙数据或 nginx host deny 数据迁移并导入
migrate_data(){
block_ip_type_input=$1
log_path=$(get_section_value_from_config_path server_secure_log_path)
code=$?
print_return_code_message_of_get_section_path $code
if [ ! -f $log_path ];then
err_code 104 "操作日志记录文件: $log_path 不存在..."
exit 1
fi
# firewalld->nginx
if [ "$block_ip_type_input" = "firewalld" ];then
nginx_deny_file_path=$(get_nginx_host_deny_path)
# 有备份需求可开启
# datatime=`date +'%F-%T'`
# sudo cp $nginx_deny_file_path $nginx_deny_file_path-${datatime}
firewall_ip=$(firewall_op_with_get)
cat $nginx_deny_file_path |awk -F ' ' '{print $2}'|awk -F ';' '{print $1}' > /tmp/format_nginx_deny_file.txt
cat $firewall_ip /tmp/format_nginx_deny_file.txt |sort|uniq -d > /tmp/firewall_nginx_common.txt # 两个文件共同拥有的内容
cat $firewall_ip /tmp/firewall_nginx_common.txt |sort|uniq -u > /tmp/firewall_exclude_common.txt # 除去共同部分,只在 firewalld 中独有的,也就是我们需要添加进 nginx deny file 中的
if [ `cat /tmp/firewall_exclude_common.txt|wc -l` = 0 ];then
return 208
fi
# 读取文件优先使用 read line 不要使用 cat, cat 速度比较慢
while read line || [ -n "$line" ]
do
sudo echo "deny $line;" >> $nginx_deny_file_path
echo -e "${Succeed}: IP: $line 已添加进 Nginx host deny 中."
echo "$(date +'%F-%T') 已封禁入侵ip(f2n+): $line" >> $log_path
done < "/tmp/firewall_exclude_common.txt"
md5sum $nginx_deny_file_path |awk '{print $1}' > /tmp/.nginx_deny_md5
reload_nginx
fi
# nginx->firewalld
if [ "$block_ip_type_input" = "nginx" ];then
nginx_host_deny_ip_file=$(get_nginx_host_deny_path)
blacklist_path=$(get_section_value_from_config_path blacklist_path)
code=$?
print_return_code_message_of_get_section_path $code
if [ ! -f $blacklist_path ];then
err_code 104 "黑名单文件: $blacklist_path 不存在..."
exit 1
fi
# 有备份需求可开启
# datatime=`date +'%F-%T'`
# cp $blacklist_path $blacklist_path-${datatime}
firewall_ip=$(firewall_op_with_get)
# 使 firewalld 和 blacklist 数据一致(在该脚本未上线之前,firewalld 数据是要多余 blacklist )
cat $blacklist_path $firewall_ip |sort|uniq -d > /tmp/blacklist_firewall_common.txt
cat $firewall_ip /tmp/blacklist_firewall_common.txt |sort|uniq -u > /tmp/firewall_exclude_blcak_common.txt # 获取 firewalld 新增的内容
while read line || [ -n "$line" ]
do
if [ -z $line ];then
echo "黑名单和防火墙数据一致,无需同步..."
else
echo $line >> $blacklist_path
echo -e "${Info}: IP: $line 已添加进黑名单列表中."
fi
done < "/tmp/firewall_exclude_blcak_common.txt"
cat $nginx_host_deny_ip_file |awk -F ' ' '{print $2}'|awk -F ';' '{print $1}' > /tmp/format_nginx_deny_file.txt
cat $firewall_ip /tmp/format_nginx_deny_file.txt |sort|uniq -d > /tmp/firewall_nginx_common.txt
cat /tmp/format_nginx_deny_file.txt /tmp/firewall_nginx_common.txt |sort|uniq -u > /tmp/nginx_exclude_firewall_common.txt # 获取 nginx deny file 新增的内容
if [ `cat /tmp/nginx_exclude_firewall_common.txt|wc -l` = 0 ];then
return 208
fi
while read line || [ -n "$line" ]
do
# 这里不使用 firewall_op_with_add $line 是因为这个函数要校验是否在防火墙中存在,这个非常耗时
sudo /usr/bin/firewall-cmd --permanent --add-rich-rule="rule family=ipv4 source address=$line drop" > /dev/null 2>&1
echo -e "${Succeed}: IP: $line 已添加进防火墙列表中."
echo $line >> $blacklist_path
echo -e "${Succeed}: IP: $line 已添加进黑名单列表中."
echo "$(date +'%F-%T') 已封禁入侵ip(n2f+): $line" >> $log_path
done < "/tmp/nginx_exclude_firewall_common.txt"
md5sum $blacklist_path |awk '{print $1}' > /tmp/.blacklist_md5
firewalld_reload
fi
}
# 更换现有的封禁 ip 的方案, 如 firewalld 换为 nginx
# todo: 是否删除原来 firewall 中的数据
block_ip_type_change(){
block_ip_type_res=$(get_section_value_from_config_path block_ip_type)
code=$?
print_return_code_message_of_get_section_path $code
echo -e "${Info}: 当前封禁 ip 的方案为: $block_ip_type_res"
res_after_change=$(block_ip_type_change_handler $block_ip_type_res)
echo -e "${Tip}: 你确定要更改为使用 $res_after_change 方案吗 (y/n)?"
read -p "请输入 (y/n)" answer
case $answer in
y|Y|yes)
conf_path_res=$(get_config_path)
echo -e "${Succeed}: 操作已确认,修改后的封禁 ip 方案为 : $res_after_change"
echo -e "${Tip}: 正在进行数据迁移操作,请耐心等待..." && echo
# echo "开始时间:$(date +'%F-%T')"
migrate_data $block_ip_type_res
code=$?
if [ $code = 208 ];then
echo -e "${Tip}: $block_ip_type_res 未新增数据,$res_after_change 列表无需更新..."
fi
# echo "结束时间:$(date +'%F-%T')"
rm -rf /tmp/blacklist_firewall_common.txt /tmp/firewall_exclude_blcak_common.txt /tmp/firewall_exclude_common.txt
rm -rf /tmp/format_nginx_deny_file.txt /tmp/firewall_nginx_common.txt /tmp/nginx_exclude_firewall_common.txt
rm -rf /tmp/firewall_rules_ip*
sed -i 's/block_ip_type='"${block_ip_type_res}"'/block_ip_type='"${res_after_change}"'/' $conf_path_res
echo -e "${Succeed}: 已将原方案:$block_ip_type_res 中的数据迁移到 $res_after_change 里"
;;
n|N|no)
echo -e "${Info}: 取消操作,当前封禁 ip 的方案为: $block_ip_type_res"
;;
*)
echo -e "${Error}: 请输入 y/n"
;;
esac
}
# 配置文件路由函数
config_router(){
flags=$1
case "$flags" in
config|-c)
set_config_path
;;
find|-f)
result=$(print_config_file_path)
code=$?
if [ $code != 0 ];then
err_code $code "没有找到配置文件, 请先用 $0 -c 来创建它..."
else
echo -e "${Info}: 配置文件路径为:$result"
fi
;;
migrate|-m)
block_ip_type_change
;;
*)
echo -e "${Error}: Usage: $0 -c|-f|-m"
echo -e "${Info}: -c: 生成配置文件"
echo -e "${Info}: -f: 查找配置文件路径"
echo -e "${Info}: -m: 变更封禁 ip 的方案"
return 1
;;
esac
}
config_router $1