#!/usr/bin/env bash # WIKI: https://httpsok.com/doc/ # 此脚本仅支持 bash,不支持 posix sh。 # 如果遇到类似 Syntax error: "(" unexpected (expecting "fi") 的问题, # 请尝试运行 "bash -version" 检查版本。 # 请访问 WIKI 寻找解决方案。 ################################################ NGINX_BIN=nginx # 定义 NGINX 可执行文件的名称 # NGINX_CONFIG=/etc/nginx/nginx.conf # NGINX 配置文件路径(已注释) # NGINX_CONFIG_HOME=/etc/nginx # NGINX 配置目录(已注释) ################################################## VER=1.0.0 # 定义脚本版本号 PROJECT_NAME="httpssl" # 定义项目名称 PROJECT_ENTRY="httpssl.sh" # 定义项目入口脚本名称 PROJECT_HOME="$HOME/.httpssl" # 定义项目主目录路径 PROJECT_BACKUPS="$HOME/.httpssl/backups" # 定义项目备份目录路径 PROJECT_ENTRY_BIN="$PROJECT_HOME/$PROJECT_ENTRY" # 定义项目入口脚本的完整路径 PROJECT_TOKEN_FILE="$PROJECT_HOME/token" # 定义项目令牌文件路径 HTTPSSL_UUID_FILE="$PROJECT_HOME/uuid" # 定义项目 UUID 文件路径 PROJECT_LOG_FILE="$PROJECT_HOME/$PROJECT_NAME.log" # 定义项目日志文件路径 HTTPSSL_TOKEN="" # 初始化 HTTPSSL 令牌变量 HTTPSSL_UUID="" # 初始化 HTTPSSL UUID 变量 HTTPSSL_HOME_URL="https://iyoucloud.com/" # 定义 HTTPSSL 官网 URL BASE_API_URL="https://api.httpsok.com/v1/nginx" # 定义 HTTPSSL API 基础 URL SCRIPT_URL="https://httpssl.iyoucloud.com/" # 定义获取脚本的 URL latest_code="" # 初始化最新代码变量 preparse="" # 初始化预解析变量 OS="" # 初始化操作系统变量 NGINX_VERSION="" # 初始化 NGINX 版本变量 MODE="normal" # 初始化模式变量,默认为 normal # 将字符串转换为大写 _upper_case() { tr '[a-z]' '[A-Z]' } # 将字符串转换为小写 _lower_case() { tr '[A-Z]' '[a-z]' } # 检查字符串是否以指定子字符串开头 _startswith() { _str="$1" _sub="$2" echo "$_str" | grep -- "^$_sub" >/dev/null 2>&1 } # 检查字符串是否以指定子字符串结尾 _endswith() { _str="$1" _sub="$2" echo "$_str" | grep -- "$_sub\$" >/dev/null 2>&1 } # 检查字符串是否包含指定子字符串 _contains() { _str="$1" _sub="$2" echo "$_str" | grep -- "$_sub" >/dev/null 2>&1 } # 获取当前时间的时间戳(秒) _time() { date -u "+%s" } # 计算数学表达式的值 _math() { _m_opts="$@" printf "%s" "$(($_m_opts))" } # 输出错误信息到标准错误 _err() { echo -e "\033[31m$(date +"%F %T") $@\033[0m" 1>&2 } # 输出信息到标准错误 _info() { echo -e "$(date +"%F %T") $@" 1>&2 } # 输出成功信息到标准错误 _suc() { echo -e "\033[32m$(date +"%F %T") $@\033[0m" 1>&2 } # 生成随机的 MD5 哈希值 _random_md5() { head -c 32 /dev/urandom | md5sum | awk '{print $1}' } TRACE_ID=$(_random_md5) # 生成随机的 TRACE_ID # 检查命令是否存在 _exists() { cmd="$1" if [ -z "$cmd" ]; then _usage "Usage: _exists cmd" return 1 fi if eval type type >/dev/null 2>&1; then eval type "$cmd" >/dev/null 2>&1 elif command >/dev/null 2>&1; then command -v "$cmd" >/dev/null 2>&1 else which "$cmd" >/dev/null 2>&1 fi ret="$?" return $ret } # 显示欢迎信息 showWelcome() { echo echo -e "\033[1;36mHttpssl 让 SSL 变得简单。 $HTTPSSL_HOME_URL \033[0m" echo -e "\033[1;36m版本: $VER\033[0m" echo -e "\033[1;36mTraceID: $TRACE_ID\033[0m" echo } # 创建目录(如果不存在) _mkdirs() { _dir="$1" if [ ! "$_dir" = "" ]; then if [ ! -d "$_dir" ]; then mkdir -p "$_dir" && _suc "成功创建目录 $_dir。" fi fi } # 初始化路径,创建必要的目录 _initpath() { _mkdirs "$PROJECT_HOME" _mkdirs "$PROJECT_BACKUPS" } # 当未检测到 NGINX 时的处理 _no_nginx_here(){ echo _err "无法检测到 nginx\n" _err "请确认您的系统上已成功安装 nginx" _detected_is_root_run echo echo exit } # 检测是否以 root 用户运行 _detected_is_root_run(){ YELLOW='\033[1;33m' NC='\033[0m' # 无颜色 if [ "$(id -u)" -ne 0 ]; then echo -e "${YELLOW}检测到以非 root 用户运行,建议以 root 用户运行。${NC}\n" fi } _init_params() { # 如果变量 OS 已经被设置,则直接返回 if [ "$OS" != "" ]; then return 0 fi # 检查并确定操作系统类型 if [ -f /etc/os-release ]; then # 从 /etc/os-release 文件中获取操作系统的名称 OS=$(grep 'PRETTY_NAME' /etc/os-release | awk -F '=' '{print $2}' | tr -d '"') elif [ -f /etc/redhat-release ]; then # 从 /etc/redhat-release 文件中获取操作系统的名称 OS=$(cat /etc/redhat-release) elif [ -f /etc/alpine-release ]; then # 如果存在 /etc/alpine-release 文件,则操作系统为 Alpine OS="alpine" else # 如果以上文件都不存在,则表示操作系统不受支持,输出错误信息并退出 _err "Unsupported operating system" exit 1 fi # 检查 NGINX_BIN 是否已设置,如果未设置,则尝试找到 nginx 可执行文件 $NGINX_BIN -V > /dev/null 2>&1 if [ $? -ne 0 ]; then echo "no nginx in PATH, find the nginx" # 使用 ps 命令查找正在运行的 nginx 进程的 PID pid=$(ps -e | grep nginx | grep -v 'grep' | head -n 1 | awk '{print $1}') if [ -n "$pid" ]; then # 使用 readlink 获取 nginx 可执行文件的实际路径 NGINX_BIN=$(readlink -f /proc/"$pid"/exe) # 再次验证找到的 nginx 可执行文件是否有效 $NGINX_BIN -V > /dev/null 2>&1 if [ $? -ne 0 ]; then _no_nginx_here else echo "Nginx executable path: $NGINX_BIN" fi else _no_nginx_here fi fi # 获取 nginx 的版本信息 NGINX_VERSION=$($NGINX_BIN -v 2>&1 | awk -F ': ' '{print $2}' | head -n 1 | head -c 20) # 如果用户未设置 NGINX_CONFIG,则尝试自动检测 if [ -z "$NGINX_CONFIG" ]; then # 使用 ps 命令查找 nginx 主进程的命令行参数,获取配置文件路径 NGINX_CONFIG=$(ps -eo pid,cmd | grep nginx | grep master | grep '\-c' | awk -F '-c' '{print $2}' | sed 's/ //g') fi # 修正 NGINX_CONFIG 仅为 nginx.conf 的情况 if [ -z "$NGINX_CONFIG" ] || [ "$NGINX_CONFIG" = "nginx.conf" ]; then # 使用 nginx -t 命令测试配置文件,并从输出中提取配置文件路径 NGINX_CONFIG=$($NGINX_BIN -t 2>&1 | grep 'configuration' | head -n 1 | awk -F 'file' '{print $2}' | awk '{print $1}' ) fi # 如果 NGINX_CONFIG_HOME 未设置,则根据 NGINX_CONFIG 设置 if [ -z "$NGINX_CONFIG_HOME" ]; then NGINX_CONFIG_HOME=$(dirname "$NGINX_CONFIG") fi # 初始化 httpssl 参数 _init_httpssl_params # 输出系统信息 _info "os-name: $OS" _info "version: $NGINX_VERSION" _info "nginx-config: $NGINX_CONFIG" _info "nginx-config-home: $NGINX_CONFIG_HOME" _info "nginx-bin: $NGINX_BIN" _info "httpssl-uuid: $HTTPSSL_UUID" # 如果 NGINX_CONFIG_HOME 为当前目录,则提示用户手动设置 if [ "$NGINX_CONFIG_HOME" = "." ]; then echo "" echo "" echo -e "\033[31m获取nginx配置文件失败, 请您根据实际情况,手动设置\033[0m" echo "" echo -e "\033[1;36m修改文件 $PROJECT_ENTRY_BIN\033[0m" echo "" echo "################################################" echo "# 配置示例 " echo "NGINX_CONFIG=/etc/nginx/nginx.conf" echo "NGINX_CONFIG_HOME=/etc/nginx" echo "##################################################" echo "" exit 0 fi # 显示欢迎信息 showWelcome } # 初始化 HTTPSSL 参数 _init_httpssl_params() { # 如果 HTTPSSL_UUID 已经存在,则无需重新初始化,直接返回 if [ "$HTTPSSL_UUID" != "" ]; then return 0 fi # 如果存在 HTTPSSL_UUID_FILE 文件,则读取其中的 UUID if [ -f "$HTTPSSL_UUID_FILE" ]; then HTTPSSL_UUID=$(cat "$HTTPSSL_UUID_FILE") fi # 如果成功从文件中读取到 HTTPSSL_UUID,则返回 if [ "$HTTPSSL_UUID" != "" ]; then # _info "从 $HTTPSSL_UUID_FILE 加载 HTTPSSL_UUID: $HTTPSSL_UUID" return 0 fi # 初始化所需的目录路径 _initpath # 尝试从系统的产品 UUID 文件中获取 UUID if [ -f "/sys/class/dmi/id/product_uuid" ]; then HTTPSSL_UUID=$(cat /sys/class/dmi/id/product_uuid) if [ "$HTTPSSL_UUID" != "" ]; then # 将获取到的 UUID 保存到 HTTPSSL_UUID_FILE 文件中 echo "$HTTPSSL_UUID" > "$HTTPSSL_UUID_FILE" # _info "从 product_uuid 保存 HTTPSSL_UUID 到 $HTTPSSL_UUID_FILE: $HTTPSSL_UUID" return 0 fi fi # 如果无法从系统文件中获取 UUID,则生成一个随机的 MD5 值作为 UUID HTTPSSL_UUID=$(_random_md5) # 将生成的 UUID 保存到 HTTPSSL_UUID_FILE 文件中 echo "$HTTPSSL_UUID" > "$HTTPSSL_UUID_FILE" # _info "保存 HTTPSSL_UUID 到 $HTTPSSL_UUID_FILE: $HTTPSSL_UUID" } # 初始化 HTTP 请求的头部信息 _inithttp() { # 初始化 HTTPSSL 参数,确保 HTTPSSL_UUID 已经被设置 _init_httpssl_params # 设置 HTTP 请求的头部信息 _H0="Content-Type: text/plain" _H1="httpsok-token: $HTTPSSL_TOKEN" _H2="httpsok-version: $VER" _H3="os-name: $OS" _H4="nginx-version: $NGINX_VERSION" _H5="nginx-config-home: $NGINX_CONFIG_HOME" _H6="nginx-config: $NGINX_CONFIG" _H7="trace-id: $TRACE_ID" _H8="mode: $MODE" _H9="httpsok-uuid: $HTTPSSL_UUID" } # 发送 POST 请求 _post() { # 初始化 HTTP 请求头部信息 _inithttp # 构建完整的请求 URL url="${BASE_API_URL}$1" # 请求的主体内容 body="$2" # 使用 curl 命令发送 POST 请求 curl -s -X POST -H "$_H0" -H "$_H1" -H "$_H2" -H "$_H3" -H "$_H4" -H "$_H5" -H "$_H6" -H "$_H7" -H "$_H8" -H "$_H9" --data-binary "$body" "$url" } # 发送带有文件的 POST 请求 _post2() { # 初始化 HTTP 请求头部信息 _inithttp # 构建完整的请求 URL url="${BASE_API_URL}$1" # 要发送的文件名 fiename="$2" # 使用 curl 命令发送 POST 请求,文件作为二进制数据发送 curl -s -X POST -H "$_H0" -H "$_H1" -H "$_H2" -H "$_H3" -H "$_H4" -H "$_H5" -H "$_H6" -H "$_H7" -H "$_H8" -H "$_H9" --data-binary "@$fiename" "$url" } # 发送 GET 请求 _get() { # 初始化 HTTP 请求头部信息 _inithttp # 构建完整的请求 URL url="${BASE_API_URL}$1" # 使用 curl 命令发送 GET 请求 curl -s -H "$_H0" -H "$_H1" -H "$_H2" -H "$_H3" -H "$_H4" -H "$_H5" -H "$_H6" -H "$_H7" -H "$_H8" -H "$_H9" "$url" } # 上传证书和密钥文件 _upload() { # 初始化 HTTP 请求头部信息 _inithttp # 构建完整的上传 URL,包括查询参数 code url="${BASE_API_URL}/upload?code=$1" # 设置要上传的证书文件 _F1="cert=@\"$2\"" # 设置要上传的证书密钥文件 _F2="certKey=@\"$3\"" # 使用 curl 命令发送带有文件的 POST 请求,采用 multipart/form-data 格式 curl -s -X POST -H "Content-Type: multipart/form-data" -H "$_H1" -H "$_H2" -H "$_H3" -H "$_H4" -H "$_H5" -H "$_H6" -H "$_H7" -H "$_H8" -H "$_H9" -F $_F1 -F $_F2 "$url" 2>&1 } # 发送 PUT 请求 _put() { # 初始化 HTTP 请求头部信息 _inithttp # 构建完整的请求 URL url="${BASE_API_URL}$1" # 请求的主体内容 body="$2" # 使用 curl 命令发送 PUT 请求 curl -s -X PUT -H "$_H0" -H "$_H1" -H "$_H2" -H "$_H3" -H "$_H4" -H "$_H5" -H "$_H6" -H "$_H7" -H "$_H8" -H "$_H9" --data-binary "$body" "$url" } # 远程记录日志 _remote_log() { # 日志类型 type="$1" # 日志代码 code="$2" # 日志消息 msg="$3" # 发送 PUT 请求记录日志 _put "/log/$type?code=$code" "$msg" } # 通知远程服务器操作已完成 _done() { # 获取当前时间戳 _t=$(_time) # 发送 GET 请求通知操作已完成 _get "/done?t=$_t" } # 通知远程服务器卸载操作 _remote_uninstall(){ # 加载令牌 _load_token _t=$(_time) _get "/uninstall?t=$_t" } # _create_file 函数用于创建文件或备份已存在的文件 _create_file() { local _code="$1" # 第一个参数,表示操作码 local file_path="$2" # 第二个参数,表示文件路径 # 如果文件不存在,则创建该文件 if [ ! -e "$file_path" ]; then dir_path=$(dirname "$file_path") # 获取文件所在目录路径 mkdir -p "$dir_path" # 创建目录路径 touch "$file_path" # 创建空文件 _suc "$_code" "File created: $file_path" # 输出成功信息 else # 如果文件已存在,则备份该文件 if [ -f "$file_path" ]; then _filename=${file_path##*/} # 获取文件名 _date=$(date +"%Y%m%d%H%M%S") # 获取当前日期时间,格式为YYYYMMDDHHMMSS backup_file_path="$PROJECT_BACKUPS/$_filename.$_date" # 备份文件路径 # 如果备份文件已存在,输出错误信息;否则,进行备份 if [ -f "$backup_file_path" ]; then _err "$_code" "Backup file already exists: $backup_file_path" # 输出错误信息 else mv "$file_path" "$backup_file_path" # 移动原文件到备份路径 _suc "$_code" "Backup the file $file_path to $backup_file_path" # 输出成功信息 fi fi fi } # _check 函数用于检查证书的状态,并根据状态执行相应的操作 _check() { depth=$1 # 第一个参数,表示检查的深度或尝试次数 code=$2 # 第二个参数,表示证书的代码或标识符 cert_file=$3 # 第三个参数,表示证书文件的路径 cert_key_file=$4 # 第四个参数,表示证书密钥文件的路径 url="/check?code=$code" # 构造用于检查证书状态的URL # 如果尝试次数已用完,输出错误信息 if [ $depth -le 0 ]; then _err "The maximum number of attempts exceeded" # 输出错误信息 else resp=$(_get "$url") # 发送GET请求,获取响应 status=$(echo "$resp" | head -n 1) # 获取响应的第一行,表示状态码 # 根据状态码执行不同的操作 case $status in "1") # 状态码为1,表示有新的证书已更新 md5_line=$(echo "$resp" | awk 'NR==2') # 获取响应的第二行,包含MD5校验值 cert_file_md5=$(echo $md5_line | awk -F ',' '{print $1}') # 提取证书文件的MD5值 cert_key_file_md5=$(echo $md5_line | awk -F ',' '{print $2}') # 提取证书密钥文件的MD5值 tmp_cert_file="/tmp/$code.cer" # 临时证书文件路径 tmp_cert_key_file="/tmp/$code.key" # 临时证书密钥文件路径 # 下载新的证书和密钥文件到临时路径 _get "/cert/$code.cer" > "$tmp_cert_file" && _get "/cert/$code.key" > "$tmp_cert_key_file" # 计算下载的临时文件的MD5值 tmp_cert_md5=$(md5sum "$tmp_cert_file" | awk '{print $1}') tmp_cert_key_md5=$(md5sum "$tmp_cert_key_file" | awk '{print $1}') # 比较MD5值,确保文件完整性和一致性 if [ "$cert_file_md5" = "$tmp_cert_md5" ] && [ "$cert_key_file_md5" = "$tmp_cert_key_md5" ]; then # 如果本地证书文件不存在,需要创建文件 _create_file "$code" "$cert_file" && _create_file "$code" "$cert_key_file" mv "$tmp_cert_file" "$cert_file" && mv "$tmp_cert_key_file" "$cert_key_file" # 替换旧的证书和密钥文件 _suc "$code $cert_file New cert updated(证书更新成功)" # 输出成功信息 _remote_log "cert-updated-success" "$code" "New cert updated(证书更新成功)" # 记录远程日志 echo "latest_code $code" # 输出最新的代码或标识符 else _err "$code $cert_file New cert update failed (md5 not match)" # 输出错误信息 _remote_log "cert-updated-failed" "$code" "New cert update failed (md5 not match): cert_file_md5=$cert_file_md5,tmp_cert_md5=$tmp_cert_md5,cert_key_file_md5=$cert_key_file_md5,tmp_cert_key_md5=$tmp_cert_key_md5" # 记录远程日志 fi ;; "2") # 状态码为2,表示证书正在处理中 _info "$code $cert_file Processing, please just wait(正在处理中,请稍等)..." # 输出信息 sleep 10 # 等待10秒 _check $((depth - 1)) "$code" "$cert_file" "$cert_key_file" # 递归调用_check函数,尝试次数减1 ;; "3") # 状态码为3,表示证书有效 _info "$code $cert_file Cert valid(证书有效)" # 输出信息 ;; "12") # 状态码为12,表示DNS CNAME无效 _err "$code $cert_file DNS CNAME invalid(DNS检测不通过)" # 输出错误信息 ;; "13") # 状态码为13,表示请求非法 _err "$code $cert_file code invalid(非法请求)" # 输出错误信息 ;; *) # 其他未知状态码,输出响应内容 _err "$code $cert_file $resp" # 输出错误信息 ;; esac fi } # 保存令牌到指定文件 _save_token() { _token="$1" # 获取第一个参数作为令牌 _check_token "$_token" # 检查令牌的有效性 _initpath # 初始化路径 if [ ! "$_token" = "" ]; then # 如果令牌不为空 echo "$_token" > "$PROJECT_TOKEN_FILE" # 将令牌写入到项目的令牌文件中 _suc "save token $_token to $PROJECT_TOKEN_FILE" # 输出成功信息 fi } # 从文件中加载令牌 _load_token() { if [ "$HTTPSSL_TOKEN" = "" ]; then # 如果当前令牌为空 HTTPSSL_TOKEN=$(cat "$PROJECT_TOKEN_FILE") # 从项目的令牌文件中读取令牌 fi } # 显示当前的令牌 _show_token() { _load_token # 加载令牌 echo -e "Your token is: \033[1;36m$HTTPSSL_TOKEN\033[0m" # 输出令牌,使用颜色格式化 } # 检查令牌的有效性 _check_token() { _token="$1" # 获取第一个参数作为令牌 if [ ! "$_token" = "" ]; then # 如果令牌不为空 HTTPSSL_TOKEN="$_token" # 设置全局令牌变量 fi if [ "$HTTPSSL_TOKEN" = "" ]; then # 如果全局令牌仍为空 _err "httpssl's token can not be empty" # 输出错误信息 exit 4 # 退出脚本,返回状态码4 fi _init_params # 初始化参数 status=$(_get "/status") # 发送GET请求检查令牌状态 # _info "status >> $status" if [ "$status" != "ok" ]; then # 如果状态不是“ok” # echo -e "\033[1;36mTraceID: $TRACE_ID\033[0m" _err "Invalid token: \033[1;36m$HTTPSSL_TOKEN\033[0m" # 输出错误信息,指示令牌无效 _info "Please copy your token from '$HTTPSSL_HOME_URL'" # 提示用户从指定URL获取令牌 echo _err "$status" # 输出状态信息 exit 4 # 退出脚本,返回状态码4 fi return 0 # 返回成功状态 } # 限制最大嵌套级别 _include_max_calls=20 # 最大允许的嵌套调用次数 _include_global_count=0 # 当前嵌套调用计数 __process_include() { # echo "-_include_global_count: $_include_global_count --------------------------------------------------" # echo 递归调用时,递归深度加1 ((_include_global_count++)) # 如果递归深度超过最大限制,输出警告并返回 if [ $_include_global_count -gt $_include_max_calls ]; then echo "#######################################################" echo "##### 警告:达到最大递归深度限制。" echo "#######################################################" cat /dev/stdin # 输出当前的输入内容 return 0 fi # 使用awk处理输入的内容 tmp=$(cat /dev/stdin | awk -v NGINX_CONFIG=">$NGINX_CONFIG:" -v NGINX_CONFIG_HOME="$NGINX_CONFIG_HOME" '{ original = $0 # Remove leading and trailing whitespace characters from each line gsub(/^[[:space:]]+|[[:space:]]+$/, "") sub(/^[\t ]*|[\t ]*$/,"") # Ignore the lines at the beginning of the file if ($0 ~ /^>/) { print original next } # Determines if it starts with #, and if it does, ignores it if ($0 ~ /^#/) { # Replace the include in the comment # gsub("include", "import") print original next } # Determine whether include is included if ($0 ~ /^include /) { # Ignore mime.types if($0 ~ /mime\.types;/){ print "#import " original next } # print "#import " $2 print "#import " original # system("cat " $2) # print ">> " # ls /etc/nginx/conf.d/*.conf | xargs -I {} sed "s|^|{}:|" {} # system("ls " $2 " | xargs -I {} sed \"s|^|{}:|\" {}") # /etc/nginx/conf.d/*.conf; # Replace ; gsub(/;/, "") # /etc/nginx/conf.d/*.conf # Resolved to an include file # print $2 # Deal with relative path issues if (substr($2, 1, 1) != "/") { $2 = NGINX_CONFIG_HOME "/" $2 } # print "# " original # The second way # find . -maxdepth 1 -print0 | xargs -0 command # system("find " $2 " -maxdepth 1 -print0 | xargs -0 -I {} sed \"s|^|>{}:|\" {}") # just backup # system("ls -1 " $2 " | xargs -I {} sed \"s|^|>{}:|\" {}") # If the last line of the file is not newline, the file will not be parsed correctly # system("ls -1 " $2 " 2>/dev/null | xargs -I {} cat {} ") # Using sh has security implications deprecated # system("ls -1 " $2 " 2>/dev/null | xargs -I {} sh -c \"cat {} && echo\" ") # # system("ls -1 " $2 " 2>/dev/null | xargs -I {} sed -n \"$p\" {} ") # xargs -I {} awk -v FP="{}" "BEGIN{ print \"# include \" FP } {print} END{print ""} " {} #system("ls -1 " $2 " 2>/dev/null | xargs -I {} awk \" {print} END {print \"\" } \" {} ") # # cmd = "ls -1 " $2 " 2>/dev/null | xargs -I {} awk \" {print} END {print \"\\\"\"\n\"\\\"\" } \" {} " # cmd = "ls -1 " $2 " 2>/dev/null | xargs -I {} awk \"BEGIN {print \"\"{}\"\" } {print} END {print \n } \" {} " # OK Add a newline character to the end of the file # cmd = "ls -1 " $2 " 2>/dev/null | xargs -I {} awk \" {print} END {print \"\\n\" } \" {} " # OK Add a newline character to the end of the file # cmd = "ls -1 " $2 " 2>/dev/null | xargs -I {} awk '\'' {print} '\'' {} " # OK add config file path #cmd = "ls -1 " $2 " 2>/dev/null | xargs -I GG awk '\'' BEGIN {print \"#included GG;\" } {print} '\'' GG " #cmd = "ls -1 " $2 " 2>/dev/null | xargs -I {} awk '\'' BEGIN {print \"#included {};\" } {print} '\'' {} " cmd = "ls -1 " $2 " 2>/dev/null | xargs -I {} awk '\'' BEGIN {print \"#included-begin {};\" } {print} END{ print \"#included-end {};\" } '\'' {} " # print cmd system(cmd) print "" next } print original }' ) if echo "$tmp" | grep -v '#' | grep -q "include"; then # 如果文件内容中包含 'include'(不包含注释行 '#),则进行递归调用 echo "$tmp" | __process_include else # 如果没有包含 'include',则结束递归调用并输出内容 echo "$tmp" fi } _preparse() { _init_params # 初始化参数 # 读取 NGINX 配置文件并进行处理,得到处理后的配置文本 config_text=$(cat $NGINX_CONFIG | __process_include ) # 临时文件名,用于存储预处理后的配置文本 tmp_name="/tmp/2nLN3ZspTMGifYtO.tmp" # 将处理后的配置文本写入临时文件 echo "$config_text" > $tmp_name # 调用 _post2 函数进行后续处理,并将结果存储在 preparse 变量中 preparse=$(_post2 "/preparse" "$tmp_name") # 删除临时文件 rm -rf "$tmp_name" > /dev/null 2>&1 # 如果 preparse 为空,表示处理失败,返回错误码 4 if [ "$preparse" = "" ]; then return 4 fi # 如果处理成功,返回 0 return 0 } _upload_certs() { # 从 preparse 中逐行读取数据,并进行证书上传处理 while read row do # 如果当前行为空,跳过 if [ "$row" = "" ]; then continue fi # 将当前行通过逗号分隔,提取出证书代码、证书文件路径和证书私钥文件路径 code=$(echo $row | awk -F ',' '{print $1}') cert_file=$(echo $row | awk -F ',' '{print $2}') cert_key_file=$(echo $row | awk -F ',' '{print $3}') # 调用 _upload 函数上传证书,并检查返回状态 up_status=$(_upload "$code" "$cert_file" "$cert_key_file") # 如果上传失败,跳过当前行继续处理下一行 if [ "$up_status" != "ok" ]; then continue fi done <&1) # 如果测试失败,记录日志并显示错误信息 if [ $? != 0 ]; then _remote_log "nginx-test-failed" "$latest_code" "$msg" echo _err "Nginx test failed(测试失败). \n\n$msg" else # 如果测试成功,执行 Nginx 重载 msg=$($NGINX_BIN -s reload 2>&1) # 如果重载成功,记录日志并显示成功信息 if [ $? -eq 0 ]; then _remote_log "nginx-reload-success" "$latest_code" "Nginx reload success(重载成功)." echo _suc "Nginx reload success(重载成功)." else # 检查 Nginx 是否正在运行 show_msg=$msg pid=$(ps -e | grep nginx | grep -v 'grep' | head -n 1 | awk '{print $1}') # 如果没有找到 Nginx 进程,提示服务未启动 if [ -z "$pid" ]; then msg="Nginx is not started(服务未启动). \n\n$msg" show_msg="\033[33mNginx is not started. You can run \"service nginx start\" to start the Nginx(请重启服务). \033[31m\n\n$show_msg" fi # 记录重载失败日志并显示错误信息 _remote_log "nginx-reload-failed" "$latest_code" "$msg" echo _err "Nginx reload failed(重载失败). \n\n$show_msg" fi fi ) } # 输出当前项目版本 version() { echo "$PROJECT_ENTRY v$VER" } # 检测配置文件,如果没有指定环境变量 PROFILE,则自动检测 _detect_profile() { # 如果指定了 PROFILE 且该文件存在,直接输出 PROFILE if [ -n "$PROFILE" -a -f "$PROFILE" ]; then echo "$PROFILE" return fi # 初始化检测到的配置文件变量 DETECTED_PROFILE='' # 获取当前 shell 类型(bash 或 zsh) SHELLTYPE="$(basename "/$SHELL")" # 根据 shell 类型检查对应的配置文件 if [ "$SHELLTYPE" = "bash" ]; then # 如果是 bash,优先检测 .bashrc 文件,如果不存在则检测 .bash_profile 文件 if [ -f "$HOME/.bashrc" ]; then DETECTED_PROFILE="$HOME/.bashrc" elif [ -f "$HOME/.bash_profile" ]; then DETECTED_PROFILE="$HOME/.bash_profile" fi elif [ "$SHELLTYPE" = "zsh" ]; then # 如果是 zsh,检测 .zshrc 文件 DETECTED_PROFILE="$HOME/.zshrc" fi # 如果没有找到配置文件,再次检查 .profile 或其他可能的配置文件 if [ -z "$DETECTED_PROFILE" ]; then if [ -f "$HOME/.profile" ]; then DETECTED_PROFILE="$HOME/.profile" elif [ -f "$HOME/.bashrc" ]; then DETECTED_PROFILE="$HOME/.bashrc" elif [ -f "$HOME/.bash_profile" ]; then DETECTED_PROFILE="$HOME/.bash_profile" elif [ -f "$HOME/.zshrc" ]; then DETECTED_PROFILE="$HOME/.zshrc" fi fi # 如果仍然没有找到配置文件,检查系统级别的配置文件 if [ -z "$DETECTED_PROFILE" ]; then if [ -f "/etc/profile" ]; then DETECTED_PROFILE="/etc/profile" fi fi # 输出检测到的配置文件路径 echo "$DETECTED_PROFILE" } # 用于显示文件的最后几个字节,防止 tail 报错 _tail_c() { tail -c "$1" 2>/dev/null || tail -"$1"c } # 设置配置文件中的选项值 _setopt() { # 配置文件路径 __conf="$1" # 配置选项 __opt="$2" # 配置选项与值之间的分隔符 __sep="$3" # 配置值 __val="$4" # 配置结束符 __end="$5" # 如果未指定选项,输出使用说明 if [ -z "$__opt" ]; then _usage usage: _setopt '"file" "opt" "=" "value" [";"]' return fi # 如果配置文件不存在,则创建该文件 if [ ! -f "$__conf" ]; then touch "$__conf" fi # 如果配置文件非空,添加换行符 if [ -n "$(_tail_c 1 <"$__conf")" ]; then echo >>"$__conf" fi # 检查配置文件中是否已经包含该选项 if grep -n "^$__opt$__sep" "$__conf" >/dev/null; then # 如果值中包含 "&" 或 "|",则进行转义 if _contains "$__val" "&"; then __val="$(echo "$__val" | sed 's/&/\\&/g')" fi if _contains "$__val" "|"; then __val="$(echo "$__val" | sed 's/|/\\|/g')" fi # 修改配置文件中该选项的值 text="$(cat "$__conf")" printf -- "%s\n" "$text" | sed "s|^$__opt$__sep.*$|$__opt$__sep$__val$__end|" >"$__conf" # 如果配置文件中包含以 '#' 开头的选项(注释),则修改该选项 elif grep -n "^#$__opt$__sep" "$__conf" >/dev/null; then if _contains "$__val" "&"; then __val="$(echo "$__val" | sed 's/&/\\&/g')" fi if _contains "$__val" "|"; then __val="$(echo "$__val" | sed 's/|/\\|/g')" fi # 修改注释中的选项值 text="$(cat "$__conf")" printf -- "%s\n" "$text" | sed "s|^#$__opt$__sep.*$|$__opt$__sep$__val$__end|" >"$__conf" # 如果配置文件中没有该选项,则直接添加新的选项 else echo "$__opt$__sep$__val$__end" >>"$__conf" fi } # 安装函数,安装项目并设置相关配置 _install() { # 输出安装开始的信息 _info "Installing $PROJECT_NAME." # 初始化路径 _initpath # 使用 curl 下载脚本并赋予可执行权限 curl -s "$SCRIPT_URL" > "$PROJECT_ENTRY_BIN" && chmod +x "$PROJECT_ENTRY_BIN" # 如果下载并设置权限成功,则输出成功信息,否则输出失败信息并退出 if [ -x "$PROJECT_ENTRY_BIN" ] ; then _suc "Install $PROJECT_NAME complete." else _err "Install $PROJECT_NAME failed." exit 4 fi # 安装定时任务 installcronjob # 安装别名 installAlais } # 安装定时任务的函数 installcronjob() { # 设置 crontab 命令 _CRONTAB="crontab" # 获取当前时间 _t=$(_time) # 随机生成分钟和小时,确保在9点到17点之间 random_minute=$(_math $_t % 60) random_hour=$(_math $_t % 9 + 9 ) # 9 ~ 17 # 检查 crontab 是否存在 if ! _exists "$_CRONTAB" ; then _err "$_CRONTAB not exits\ncrontab定时任务不存在,请安装。参考文档:https://httpsok.com/doc/faq/crontab.html \n" return 4 fi # 检查 crontab 中是否已存在该项目的定时任务 if ! $_CRONTAB -l | grep "$PROJECT_ENTRY" > /dev/null; then _info "Installing cron job." # 检查操作系统类型,调整 crontab 命令的参数 if _exists uname && uname -a | grep SunOS >/dev/null; then _CRONTAB_STDIN="$_CRONTAB --" else _CRONTAB_STDIN="$_CRONTAB -" fi # 将新的定时任务添加到 crontab $_CRONTAB -l | { cat echo "$random_minute $random_hour * * * '$PROJECT_ENTRY_BIN' -m -r >> '$PROJECT_LOG_FILE' 2>&1" } | $_CRONTAB_STDIN # 输出成功信息 _suc "Install cron job complete." fi # 如果安装定时任务失败,输出错误信息并提示用户手动添加定时任务 if [ "$?" != "0" ]; then _err "Install cron job failed. You can add cronjob by yourself." _err "Or you can add cronjob by yourself:" _err "$random_minute $random_hour * * * '$PROJECT_ENTRY_BIN' -m -r >> '$PROJECT_LOG_FILE' 2>&1" return 1 fi } # 安装别名的函数 installAlais() { # 设置别名配置文件路径 _envfile="$PROJECT_ENTRY_BIN.env" # 创建别名配置文件,内容为设置项目别名 echo "alias $PROJECT_ENTRY=\"$PROJECT_ENTRY_BIN\"" > "$_envfile" # 输出安装别名的信息 _info "Installing alias" # 获取用户的 shell 配置文件 _profile="$(_detect_profile)" # 如果找到了配置文件,添加别名到该文件中 if [ "$_profile" ]; then _info "Found profile: $_profile" _info "Installing alias to '$_profile'" _setopt "$_profile" ". \"$_envfile\"" _suc "OK, Close and reopen your terminal to start using $PROJECT_NAME" else # 如果没有找到配置文件,提示用户手动配置 _err "No profile is found, you will need to go into $PROJECT_HOME to use $PROJECT_NAME" fi } # 卸载函数,卸载项目并清理相关文件 _uninstall() { # 输出卸载开始的信息 _info "Uninstalling httpssl." # 执行远程卸载操作 _remote_uninstall # 卸载定时任务 uninstallcronjob # 卸载别名 _uninstallalias # 删除项目目录 if [ -d "$PROJECT_HOME" ]; then _info "Removing directory $PROJECT_HOME" rm -rf "$PROJECT_HOME" fi # 输出卸载完成的信息 _suc "Uninstall httpssl complete." # 显示欢迎信息 showWelcome # 提供重新安装的提示 echo "If your need install httpssl agian. Please see $HTTPSSL_HOME_URL . curl -s $SCRIPT_URL | bash -s 'your token' " } # 卸载别名的函数 _uninstallalias() { # 设置别名配置文件路径 _envfile="$PROJECT_ENTRY_BIN.env" # 获取用户的 shell 配置文件 _profile="$(_detect_profile)" # 如果找到配置文件,执行卸载别名操作 if [ "$_profile" ]; then _info "Uninstalling alias from: '$_profile'" # 输出卸载别名信息 # 读取配置文件内容 text="$(cat "$_profile")" # 使用 sed 命令删除配置文件中与别名相关的行 echo "$text" | sed "s|^.*\"$_envfile\"$||" >"$_profile" fi } # 卸载定时任务的函数 uninstallcronjob() { # 设置 crontab 命令 _CRONTAB="crontab" # 检查 crontab 是否存在 if ! _exists "$_CRONTAB" ; then _err "$_CRONTAB not exits" # 如果 crontab 不存在,输出错误信息 return 4 fi # 获取当前 crontab 中与项目相关的任务 cr="$($_CRONTAB -l | grep "$PROJECT_ENTRY")" # 如果找到定时任务,则进行删除 if [ "$cr" ]; then _info "Removing cron job" # 输出正在移除定时任务的信息 # 检查操作系统类型,调整 crontab 命令的参数 if _exists uname && uname -a | grep SunOS >/dev/null; then # 如果是 SunOS 系统,使用不同的命令删除任务 $_CRONTAB -l | sed "/$PROJECT_ENTRY/d" | $_CRONTAB -- else # 否则在标准系统中删除任务 $_CRONTAB -l | sed "/$PROJECT_ENTRY/d" | $_CRONTAB - fi # 输出定时任务删除成功的提示信息 _suc "Remove cron job complete." fi } # 执行主程序的函数 _run() { # 加载 token 配置并检查有效性 _load_token _check_token # 如果无法预处理,输出错误信息并返回 if ! _preparse ; then _err "No SSL certificate was detected(未检测到SSL证书).\n " _info "Please refer to resolve the issue(查看文档,解决此问题). https://httpsok.com/doc/faq/nossl.html " echo "" return 4 fi # 输出 SSL 证书检测的提示信息 _info "Checking SSL certificate, please wait a moment(证书检测中请稍等...)." echo # 上传证书、检查 DNS 和证书、重新加载 nginx _upload_certs _check_dns _check_certs _reload_nginx # 完成后执行的操作 _done echo "" } # 处理传入参数的函数 _process() { # 遍历传入的所有参数 while [ ${#} -gt 0 ]; do case "${1}" in # 如果是帮助命令,显示帮助信息并返回 --help | -h) show_help return ;; # 如果是版本命令,显示版本信息并返回 --version | -v) version return ;; # 如果是运行命令,调用 _run 函数执行 --run | -r) _run ;; # 如果是设置命令,保存 token 并安装项目 --setup | -s) _save_token "$2" _install _run return ;; # 如果是 token 命令,保存或显示 token --token | -t) if [ "$2" ]; then _save_token "$2" else _show_token fi return ;; # 如果是模式命令,设置模式为 crontab --mode | -m) MODE="crontab" ;; # 如果是安装命令,调用 _install 函数安装项目 --install | -i) _install return ;; # 如果是卸载命令,调用 _uninstall 函数卸载项目 --uninstall | -u) _uninstall return ;; # 处理其他未知参数,输出错误信息 *) echo "Unknown parameter : $1 $2" return 1 ;; esac # 移动到下一个参数 shift 1 done } show_help() { # 显示帮助信息 echo "Usage: $PROJECT_ENTRY ... [parameters ...] Commands: -h, --help 显示此帮助信息。 -v, --version 显示版本信息。 -r, --run 运行 $PROJECT_NAME -s, --setup 安装并运行(推荐第一次使用时选择)。 -t, --token 设置或显示令牌。 -m, --mode 设置模式为 normal 或 crontab。 -i, --install 安装 $PROJECT_NAME 到你的系统中。 -u, --uninstall 卸载 $PROJECT_NAME 在你的系统中。 " showWelcome } main() { # 如果没有参数传入,显示帮助信息并退出 [ -z "$1" ] && show_help && return # 如果第一个参数以 '-' 开头,调用 _process 处理后续参数 if _startswith "$1" '-'; then _process "$@"; else # 否则默认执行 --setup 操作 _process --setup "$@"; fi } # 调用 main 函数,传入所有参数 main "$@"