惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

K
Kaspersky official blog
V
Vulnerabilities – Threatpost
P
Privacy & Cybersecurity Law Blog
罗磊的独立博客
C
Comments on: Blog
P
Privacy International News Feed
Attack and Defense Labs
Attack and Defense Labs
The Hacker News
The Hacker News
M
Microsoft Research Blog - Microsoft Research
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Latest news
Latest news
G
Google Developers Blog
D
Docker
博客园 - Franky
T
Tor Project blog
Vercel News
Vercel News
小众软件
小众软件
J
Java Code Geeks
B
Blog RSS Feed
Hacker News - Newest:
Hacker News - Newest: "LLM"
Simon Willison's Weblog
Simon Willison's Weblog
Google DeepMind News
Google DeepMind News
S
SegmentFault 最新的问题
F
Fox-IT International blog
N
News | PayPal Newsroom
Google DeepMind News
Google DeepMind News
WordPress大学
WordPress大学
L
Lohrmann on Cybersecurity
E
Exploit-DB.com RSS Feed
Spread Privacy
Spread Privacy
腾讯CDC
S
Securelist
Apple Machine Learning Research
Apple Machine Learning Research
Microsoft Azure Blog
Microsoft Azure Blog
Scott Helme
Scott Helme
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
T
Tailwind CSS Blog
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
A
Arctic Wolf
IT之家
IT之家
F
Full Disclosure
GbyAI
GbyAI
Last Week in AI
Last Week in AI
Stack Overflow Blog
Stack Overflow Blog
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
V
Visual Studio Blog
T
ThreatConnect
Y
Y Combinator Blog
A
About on SuperTechFans
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More

V2EX

[汽车] 比亚迪为天神之眼 A、B 城区智驾兜底 1 年 [分享创造] 周末撸了个赛博斗蛐蛐:让 AI 写 JS 心法去打武侠擂台 借钱给公户,公户再打到我私户,税收问题是不是不用关注? 用 AI 写生成长期运行的软件 有使用 aliyun 的 token plan 的吗?两小时用了 30%左右 [分享创造] 做了一个 AI 内容分享工具:把 ChatGPT / Claude 对话变成可公开访问网页 [推广] 零成本保号美国实体手机号! Talkatone 注册教程及虚拟号检测方法 [程序员] 推荐一下 OpenCode Go Token Plan 5$ 换 60$ DeepSeek/MiMo 额度 [分享创造] [分享] 想把分散的需求整理成可被联系的信息,做了个实验性平台 [技术栈] 发现个 grok 可以生成漏点(13)照片的提示词 速来!大饱眼福了! [分享创造] 程序员创业产品免费试用,一款带笔记、农历节气、节假日调休的清爽日历 让 Claude Code 写出符合规范的中文技术文档,一行命令搞定 [分享创造] tavafa2.0.0 支持 PS 图层编辑功能 [分享创造] 探讨:测试驱动开发(TDD)是否是目前提高 AI 编程准确率的唯一解? 正想求个稳定的机场,就发现有大佬建了这个节点,那就求推荐吧😉 一行命令给 Claude/Codex 加上 session 接力、文件锁、任务交接、凭据扫描、完成验证和知识记忆 [分享创造] 做了一个开源 SSH/SFTP 桌面工具,想解决人和 AI 共用远程上下文的问题 用 DeepSeek 做了个用量页面个小插件, 增加了几个维度的数据统计和计算 [体验分享] 从 TG、Signal 折腾一圈后,还是觉得 WhatsApp 最均衡 💔再见了!北京移动 18 元魔卡月底下架 [酷工作] 百度封控 / 渠道回传 / 无痕浏览 技术需求 [香港] 请问现在去香港还能开到银行卡吗 我开源了一个项目:把任何资料,安全的变成 AI 的上下文 我做了一个给超级个体 / OPC 用的 AI 智能体商业化平台 求推荐靠谱的海外 VPS [投资] 基金可以跑赢银行的贷款利率,那么基金一定靠谱么? 做了一个 Vibe coding 辅助小工具 claude|gpt 被封号的,或者还幸存的来 ai 时代的程序员 怪不得老板喜欢压迫员工,原来这么爽 [生活] 当系统判定“用户永远正确”时,老实人是不是只能认栽? [剧集] 凡人修仙传动画新年番 6 月 13 日上午 11 点开播! [推广] [追加福利!老板还没回] 偷偷再放 4 个独家 CDK, IP/500M 动态流量随缘自取,手慢无! 端午节准备去广州找个地方吃荔枝,大家有推荐的没 非招聘|佛系寻找远程同频开发者,先认识再合作 [问与答] 大伙儿推荐一个入口 IP 在境内的机场 币圈准备做复刻带单了,打不过就加入? 天塌了! Gemini 目前不支持你所在的地区。敬请期待! [问与答] 大佬们,有推荐比较好用的行李箱吗 我给 Claude Code 装了个“红绿灯”,再也不怕忘记确认状态了 Hermes Agent 通过 Webhook 收到消息后,再与用户进行交互会话,就分隔成两个会话了,丢失了上下文,如何解决? [Mac mini] QianPlayer — 给 macOS 写了一个原生视频播放器 年前决定戒烟到现在,顺便做了个小程序。 阿里百炼的自建 DeepSeek 限速是 TPM 1.2M,这限速是拍脑袋的吗? 想问一下上海拿工资的多少百分比租房? 换了个高刷 4k 显示器, c 口只有 15w [开源自荐] 悦心搜索 4.0,网盘搜索引擎,对接盘搜,快速搜索转存 [分享创造] [送会员] 搞了个专业文档转换, 翻译软件, 可一键批量翻译并保持格式,支持 PDF 等多种文档格式 原型设计是否可以直接让 AI 来做? [限时] 6 折招代理~阿里云国际|腾讯云国际 easy-tdx:接手停更的 pytdx,加了 CLI 和 30 个技术指标 [北京] 求租北京新能源指标 外包兼职(长期) [分享创造] 分享一个把微信步数变成修为的小程序:走路涨修为 求 codex、claude code 订阅账单每月$200 的,或者国内 coding 订阅,有偿 [生活] 鼻中隔偏曲术后第 9 天 Fractal Skills:给 AI Agent 一副不会过期的缰绳 vb 了一个图片工具箱,目前实现了拼图和切割图,大家看看怎么样,还花了 375 大洋买了个域名(10 年) 淘宝是不是发狂了?每天打开都要搞一个土鳖特效叫我立即领取 xx 元优惠券,实际上也没优惠什么 开启 Codex 桌宠 [上海] 上海有没有靠谱月嫂推荐? [问与答] 午休求救,要崩溃了 注册送 120 刀的周卡 分享一下我薅的站点 基于本地数据生成 ClaudeCode 热力图 小米大模型降智? 中转站免费 credit 就是电子鸡蛋 [问与答] 大家好,刚进 v 站,有没有大佬给我介绍 v 站的特色啊 阿里云 web 首页疑似会导致 Firefox 占用大量 CPU 资源 [AI Agent 智能体] 越来越怀疑,很多 Agent 现在根本进不了企业 [AI 独角兽团队] 内推直招 | 后端开发工程师,创业早期机会,升职加薪快 AI 写的代码你们是怎么保证质量的? 今天下午 Codex 每问个问题就报 429,大家都这样吗?是不是要出新模型了 [推广] 今天 pp 渠道死了,又是哀嚎遍野 claude 5 小时限额变少了,有没有同感? 安克创新咋了,一天 10 几个猎头狂推给我 Gps 坐标收藏夹 开发者平台,分成规则分享 [问与答] 麻醉是不是最接近死亡的体验? 这两天 Gemini 网页版开始胡说八道了吗? 关于海南求职的付费咨询 自建 VPS 推荐 [职场话题] 作为技术人如何和老板谈项目谈生意? claude code 工作中,切换不同中转站的 api 的不同模型,上下文记忆会丢失吗? [问与答] API 调用 chatgpt 的 这里是知乎吗? [VPS] 像搬瓦工、DMIT 等一般什么时候有优惠呢 [推广] Krill 福利加倍送,持续送,回贴就送,反正就是送~纯 pro 号池低至 0.13, image-2 免费用 , dp-v4-flash 官方 4 折! 大家拳皇和街霸玩的如何?做了一个帮助大家练招的训练工具 Crypto 交易所在线直招 实习生岗位开放 纯远程办公 BD 实习生/ 行研实习生/ 管培生 未来之星选拔计划 [华为] 大家怎么看这几天比较火的华为“韬定律芯片”逻辑折叠技术架构 [iPhone] 请问这个算是 iPhone 被 pdd 劫持了吗? 该走还是继续留 雨刮品牌-博世怎么样? [月末活动] 一个真正一目了然的自建 Codex 中转站 前端失业 2 个月了, 5 月份开始投简历,一共就约到 8 家 想做一个更轻的「友链网络」组件: LinkPals 被领导警告了 [AI Agent 智能体] 为什么我觉得 AI 真正的机会在“数字员工” 我自己感觉 codex 极大的扩展了个人的能力者不用说,但是用多了似乎也会有更多精神问题 把 10.8GB vLLM 镜像的 Pod Ready 从 4m35s 降到 14s: Hermes + SOCI lazy loading 实测
不用宝塔自动续签了,自己写个续签
lyxxxh2 · 2025-03-25 · via V2EX

之前

之前宝塔自动续签失效两次: 宝塔不能自动续签的 bug 修复

本以为已经好了,直到今天又失效,算你厉害,用不起。

https://i.imgur.com/lJfJ0d1.png

更新宝塔还是没用,坑爹。

我不理解: 比续签更复杂你们都能做,怎么到续签就出问题了。

不仅仅我一个人续签失败,挺多人都是这样。

我理解不了啊,你们是不是故意的???

通过 cursor 来写

给 ai 的:

我要自动续签 nginx 的证书,服务器是用的宝塔。
1. 有个 domains 变量,是一个列表
   域名有:
     - c.com
     - www.a.com b.com
     - a-admin.com v.xx.com ...
2. http 请求所有域名,根据域名证书是否小于 30 天,小于 30 天判定为过期。
3. 利用/home/xxx/acme.sh 来申请证书,使用阿里云的 DNS 解析。AccessKey:xxx  SecretKey:123456
4.  最后更新到 nginx 。

模型用的是 auto-select,给了屎一样的代码。

还说我 python 版低(我 3.12.3 ),也不知道用啥模型了,手动选择 3.7 才能用。

代码

改下配置就能用

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import ssl
import socket
import datetime
import subprocess
import os
import time
from typing import List, Tuple

# 域名列表
domains = [
    "a.com,www.a.com", 
    "admin.b.com,x.b.com",
    "c.com"
]

# 阿里云 DNS 配置
ALIYUN_ACCESS_KEY = "xxx"
ALIYUN_SECRET_KEY = "xx"

def check_cert_expiry(domain: str) -> Tuple[bool, int]:
    """
    检查证书是否过期
    返回: (是否过期, 剩余天数)
    对于多域名证书,检查每个域名并返回最短的剩余天数
    """
    try:
        # 处理多域名情况,逗号分隔的域名
        if ',' in domain:
            domains_list = domain.split(',')
            min_days_left = float('inf')  # 设置初始值为无穷大
            all_results = []
            
            # 检查每个域名
            for single_domain in domains_list:
                single_domain = single_domain.strip()
                expired, days = check_cert_expiry(single_domain)
                all_results.append((single_domain, expired, days))
                if days < min_days_left:
                    min_days_left = days
            
            # 打印所有域名的结果
            for single_domain, expired, days in all_results:
                print(f"  - 子域名 {single_domain} 剩余天数: {days}")
            
            # 如果最小天数小于 30 ,则需要续签
            return min_days_left < 30, min_days_left
            
        # 使用外部命令获取证书信息
        cmd = f"echo | openssl s_client -connect {domain}:443 -servername {domain} 2>/dev/null | openssl x509 -noout -dates"
        result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
        
        if result.returncode != 0:
            print(f"检查域名 {domain} 证书时出错: 无法连接或获取证书")
            return True, 0
        
        # 解析输出找到过期日期
        output = result.stdout
        not_after_line = [line for line in output.splitlines() if line.startswith('notAfter=')]
        
        if not not_after_line:
            print(f"检查域名 {domain} 证书时出错: 无法获取过期时间")
            return True, 0
            
        # 解析日期格式,例如: notAfter=May 30 12:00:00 2023 GMT
        date_str = not_after_line[0].split('=')[1]
        expires_date = datetime.datetime.strptime(date_str, '%b %d %H:%M:%S %Y %Z')
        days_left = (expires_date - datetime.datetime.now()).days
        
        print(f"域名 {domain} 证书到期日期: {expires_date.strftime('%Y-%m-%d')}, 剩余天数: {days_left}")
        return days_left < 30, days_left
    except Exception as e:
        print(f"检查域名 {domain} 证书时出错: {str(e)}")
        return True, 0  # 如果无法检查,默认为需要续签

def set_ali_env():
    """
    设置阿里云 DNS API 的环境变量
    """
    os.environ['Ali_Key'] = ALIYUN_ACCESS_KEY
    os.environ['Ali_Secret'] = ALIYUN_SECRET_KEY

def check_dns_record_exists(domain: str) -> bool:
    """
    检查指定域名的 DNS 验证记录是否存在
    """
    try:
        # 设置环境变量
        set_ali_env()
        
        # 验证记录的域名前缀
        acme_challenge = f"_acme-challenge.{domain}"
        
        # 使用阿里云 CLI 查询记录
        cmd = f"aliyun alidns DescribeDomainRecords --DomainName {domain.split('.')[-2]}.{domain.split('.')[-1]} --RRKeyWord _acme-challenge --Type TXT"
        result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
        
        # 检查输出中是否包含记录
        return acme_challenge in result.stdout
    except Exception as e:
        print(f"检查 DNS 记录时出错: {str(e)}")
        # 如果无法确定,假设记录存在,以确保安全
        return True

def renew_cert(domain: str) -> bool:
    """
    使用 acme.sh 续签证书
    支持多域名证书申请
    """
    try:
        # 先设置环境变量
        set_ali_env()
        
        acme_path = "/home/xxx/acme.sh"
        
        # 确保 acme.sh 有执行权限
        os.chmod(acme_path, 0o755)
        
        # 处理多域名情况
        domain_params = ""
        main_domain = ""
        if ',' in domain:
            domains_list = domain.split(',')
            main_domain = domains_list[0].strip()
            domain_params = f"-d {main_domain}"
            
            # 添加其他域名
            for alt_domain in domains_list[1:]:
                alt_domain = alt_domain.strip()
                domain_params += f" -d {alt_domain}"
        else:
            main_domain = domain
            domain_params = f"-d {domain}"
        
        # 检查并清理 DNS 记录
        needs_cleanup = False
        
        # 检查主域名
        if check_dns_record_exists(main_domain):
            print(f"域名 {main_domain} 存在 DNS 验证记录,需要清理")
            needs_cleanup = True
            # 清理主域名
            cleanup_cmd = f"{acme_path}/acme.sh --cleanup --domain {main_domain} --dns dns_ali"
            print(f"执行清理命令: {cleanup_cmd}")
            cleanup_process = subprocess.run(cleanup_cmd, shell=True, capture_output=True, text=True)
            print(f"清理结果: {cleanup_process.stdout}")
        else:
            print(f"域名 {main_domain} 不存在 DNS 验证记录,无需清理")
        
        # 检查其他域名
        if ',' in domain:
            for alt_domain in domain.split(',')[1:]:
                alt_domain = alt_domain.strip()
                if check_dns_record_exists(alt_domain):
                    print(f"域名 {alt_domain} 存在 DNS 验证记录,需要清理")
                    needs_cleanup = True
                    # 清理其他域名
                    alt_cleanup_cmd = f"{acme_path}/acme.sh --cleanup --domain {alt_domain} --dns dns_ali"
                    print(f"执行清理命令: {alt_cleanup_cmd}")
                    alt_cleanup_process = subprocess.run(alt_cleanup_cmd, shell=True, capture_output=True, text=True)
                    print(f"清理结果: {alt_cleanup_process.stdout}")
                else:
                    print(f"域名 {alt_domain} 不存在 DNS 验证记录,无需清理")
        
        # 如果进行了清理,等待 DNS 记录更新
        if needs_cleanup:
            print("等待 DNS 记录清理完成...")
            time.sleep(30)  # 等待 30 秒确保 DNS 记录已清理
        
        # 执行续签命令,明确指定使用 Let's Encrypt
        cmd = f"{acme_path}/acme.sh --issue --dns dns_ali {domain_params} --keylength 2048 --force --dnssleep 120 --server letsencrypt"
        print(f"执行命令: {cmd}")
        
        process = subprocess.Popen(
            cmd, 
            shell=True,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            text=True
        )
        
        # 获取输出
        stdout, stderr = process.communicate()
        
        if process.returncode == 0:
            print(f"续签输出: {stdout}")
            return True
        else:
            print(f"续签错误: {stderr}")
            
            # 如果仍然失败,尝试完全移除证书再重新申请
            if "DNS record already exists" in stderr:
                print("尝试完全移除证书后重新申请...")
                
                # 移除证书
                for d in domain.split(','):
                    d = d.strip()
                    remove_cmd = f"{acme_path}/acme.sh --remove -d {d}"
                    print(f"执行移除命令: {remove_cmd}")
                    subprocess.run(remove_cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
                
                # 再次等待
                print("等待 DNS 记录更新...")
                time.sleep(30)
                
                # 重新申请
                reissue_cmd = f"{acme_path}/acme.sh --issue --dns dns_ali {domain_params} --keylength 2048 --force --dnssleep 180 --server letsencrypt"
                print(f"执行重新申请命令: {reissue_cmd}")
                
                reissue_process = subprocess.Popen(
                    reissue_cmd, 
                    shell=True,
                    stdout=subprocess.PIPE,
                    stderr=subprocess.PIPE,
                    text=True
                )
                
                reissue_stdout, reissue_stderr = reissue_process.communicate()
                
                if reissue_process.returncode == 0:
                    print(f"重新申请成功: {reissue_stdout}")
                    return True
                else:
                    print(f"重新申请失败: {reissue_stderr}")
                    return False
            
            return False
            
    except Exception as e:
        print(f"续签域名 {domain} 证书时出错: {str(e)}")
        return False

def deploy_cert(domain: str) -> bool:
    """
    部署证书到 Nginx
    支持多域名证书部署
    """
    try:
        acme_path = "/home/xxx/acme.sh"
        
        # 处理多域名情况,使用第一个域名作为主域名
        main_domain = domain.split(',')[0].strip() if ',' in domain else domain
        
        # 证书安装路径
        nginx_cert_path = f"/www/server/panel/vhost/cert/{main_domain}"
        
        # 确保目录存在
        os.makedirs(nginx_cert_path, exist_ok=True)
        
        # 部署证书
        cmd = f"{acme_path}/acme.sh --install-cert -d {main_domain} " \
              f"--key-file {nginx_cert_path}/privkey.pem " \
              f"--fullchain-file {nginx_cert_path}/fullchain.pem " 
            #   f"\ --reloadcmd 'service nginx force-reload'"  利用宝塔重启,而不是 acme.sh 重启
        print(f"执行命令: {cmd}")
        
        process = subprocess.Popen(
            cmd, 
            shell=True,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            text=True
        )
        
        # 获取输出
        stdout, stderr = process.communicate()
        
        if process.returncode == 0:
            print(f"部署输出: {stdout}")
            return True
        else:
            print(f"部署错误: {stderr}")
            return False
            
    except Exception as e:
        print(f"部署域名 {domain} 证书时出错: {str(e)}")
        return False

def update_nginx():
    """
    更新 Nginx 配置并重启服务
    """
    try:
        # 使用宝塔命令重载 Nginx
        print("重载 Nginx 配置...")
        reload_cmd = "bt reload nginx"
        reload_process = subprocess.Popen(
            reload_cmd, 
            shell=True,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            text=True
        )
        
        reload_stdout, reload_stderr = reload_process.communicate()
        
        if reload_process.returncode != 0:
            print(f"Nginx 重载错误: {reload_stderr}")
            return False
            
        # 完全重启 Nginx 以确保证书生效
        print("重启 Nginx 服务...")
        restart_cmd = "bt restart nginx"
        restart_process = subprocess.Popen(
            restart_cmd, 
            shell=True,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            text=True
        )
        
        restart_stdout, restart_stderr = restart_process.communicate()
        
        if restart_process.returncode == 0:
            print(f"Nginx 重启成功: {restart_stdout}")
            return True
        else:
            print(f"Nginx 重启错误: {restart_stderr}")
            return False
    except Exception as e:
        print(f"更新和重启 Nginx 时出错: {str(e)}")
        return False

def main():
    print(f"开始检查证书状态 - {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
    domains_to_renew = []
    
    # 检查所有域名的证书状态
    for domain in domains:
        print(f"检查域名: {domain}")
        is_expired, days_left = check_cert_expiry(domain)
        if is_expired:
            print(f"域名 {domain} 证书将在 {days_left} 天后过期,需要续签")
            domains_to_renew.append(domain)
        else:
            print(f"域名 {domain} 证书还有 {days_left} 天过期,无需续签")
    
    if not domains_to_renew:
        print("所有证书都在有效期内,无需续签")
        return
    
    # 续签需要更新的证书
    renewed_domains = []
    for domain in domains_to_renew:
        print(f"\n 正在续签域名 {domain} 的证书...")
        if renew_cert(domain):
            print(f"域名 {domain} 证书续签成功")
            # 部署证书
            if deploy_cert(domain):
                print(f"域名 {domain} 证书部署成功")
                renewed_domains.append(domain)
            else:
                print(f"域名 {domain} 证书部署失败")
        else:
            print(f"域名 {domain} 证书续签失败")
            
    # 如果有证书被续签并部署,更新 Nginx 配置
    if renewed_domains:
        print("\n 正在更新 Nginx 配置...")
        if update_nginx():
            print("Nginx 配置更新成功")
        else:
            print("Nginx 配置更新失败")
    
    print(f"\n 证书续签任务完成 - {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
    print(f"已续签的域名: {', '.join(renewed_domains) if renewed_domains else '无'}")

def force_renew_all():
    """
    强制更新所有域名的证书,用于测试
    """
    print(f"开始强制更新所有证书 - {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
    
    # 续签所有域名的证书
    renewed_domains = []
    for domain in domains:
        print(f"\n 正在更新域名 {domain} 的证书...")
        if renew_cert(domain):
            print(f"域名 {domain} 证书更新成功")
            # 部署证书
            if deploy_cert(domain):
                print(f"域名 {domain} 证书部署成功")
                renewed_domains.append(domain)
            else:
                print(f"域名 {domain} 证书部署失败")
        else:
            print(f"域名 {domain} 证书更新失败")
    
    # 如果有证书被更新并部署,更新 Nginx 配置
    if renewed_domains:
        print("\n 正在更新 Nginx 配置...")
        if update_nginx():
            print("Nginx 配置更新成功")
        else:
            print("Nginx 配置更新失败")
    
    print(f"\n 证书更新任务完成 - {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
    print(f"已更新的域名: {', '.join(renewed_domains) if renewed_domains else '无'}")

if __name__ == "__main__":
    import sys
    if len(sys.argv) > 1 and sys.argv[1] == '--force':
        force_renew_all()
    else:
        main()