Skip to content

Commit

Permalink
v2.1 Beta
Browse files Browse the repository at this point in the history
1. 修复 Python3.9 下邮件发送问题
2. 新增自定义证书路径配置,从上个版本更新的用户请在 `cert.db` 中,
   为 `domain` 表添加 `cert_path(VARCHAR(255),is_nullable:YES)`
   和 `private_key_path(VARCHAR(255),is_nullable:YES)`
3. 使用 configparse 管理配置文件,更新时请将 `config-template.ini` 复制一份改名为 `config.ini` 并重新配置
  • Loading branch information
0xJacky committed Aug 27, 2021
1 parent aebc17d commit 3f6eaa6
Show file tree
Hide file tree
Showing 11 changed files with 192 additions and 81 deletions.
6 changes: 4 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
*.pyc
settings.py
cert.db
test.py
cert/
.idea
.idea
venv
.DS_Store
config.ini
6 changes: 6 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
FROM python:latest
VOLUME ['/app', '/cert']
WORKDIR /app
RUN pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple
COPY ./sources.list /etc/apt/sources.list
RUN apt update -y && apt install cron -y
82 changes: 47 additions & 35 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
# CDN Cert
自动将 Let's encrypt 续签后的证书推送到阿里云 CDN
自动将 Let's encrypt,或其他网站证书推送到阿里云 CDN

## v2.1 Beta 更新日志
更新于 2021 年 8 月 27 日
1. 修复 Python3.9 下邮件发送问题
2. 新增自定义证书路径配置,从上个版本更新的用户请在 `cert.db` 中,
`domain` 表添加 `cert_path(VARCHAR(255),is_nullable:YES)`
`private_key_path(VARCHAR(255),is_nullable:YES)`
3. 使用 configparse 管理配置文件,更新时请将 `config-template.ini` 复制一份改名为 `config.ini` 并重新配置
4. 支持 Docker 部署 (TODO)

## v2 更新日志
更新于 2019 年 7 月 8 日
Expand All @@ -18,52 +27,56 @@

使用 SQLite3 做为数据库,并支持阿里云邮件推送服务,如有更新可以将推送结果发送到您的邮箱。

## 配置环境
## 手动配置环境
1. 准备
```
git clone https://github.com/0xJacky/cdn_cert.git "CDN Cert"
git clone https://github.com/0xJacky/cdn_cert.git
pip3 install -r requirements.txt
```
2. 配置

`settings-template.py` 复制一份并命名为 `settings.py`

打开 `settings.py` 配置 Let's encrypt 证书目录,邮件发送账户等,在 `settings-simple.py`
中,我提供了基于 certbot 和 acme.sh 管理证书的配置模板,请根据需求进行注释或解除注释
`config-template.ini` 复制一份并命名为 `config-template.ini`

##### 2018.6.7 更新日志(important!)
#### config.ini 配置说明

请注意,更新完本版本后务必重新配置 `settings-template.py`
| 配置项 | 默认值 | 说明 |
| -------------------------------- | --------------------- | ------------------------------------------------------------ |
| database.Path | cert.db | 指定数据库存储的地址,相对路径 |
| letencrypt.Path | /cert/ssl | Let's encrypt 证书目录 |
| letencrypt.ServerCertificateName | fullchain.cer | 如果您使用的是 Let's encrypt 官方的 certbot 则无需修改此项 |
| letencrypt.PrivateKeyName | {{ domain_name }}.key | PrivkeyName 提供变量 {{ domain_name }}<br />例如,使用 acme.sh 管理证书的用户,生成的私钥名称与域名相同,则应该设置为 PrivkeyName = {{ domain_name }}.key |
| mail.Host | smtpdm.aliyun.com | # 邮件反馈设置 - 阿里云邮件推送服务 |
| mail.Port | 465 | 发信端口,除 80 端口外默认使用 SSL |
| mail.UserName | - | 发件人地址,通过控制台创建的发件人地址 |
| mail.PassWord | - | 发件人密码,通过控制台创建的发件人密码 |
| mail.From | - | 发件人昵称 |
| mail.To | - | 收件人地址,支持多个收件人,最多30个,以 `,` 分割 |

本次更新增加了 `ServerCertificateName``PrivkeyName` 两个变量,以适应 acme.sh 生成证书的路径

`PrivkeyName` 提供变量 `{{ domain_name }}`

例如,使用 acme.sh 管理证书的用户,生成的私钥名称与域名相同,则应该设置为 `PrivkeyName = '{{ domain_name }}.key'`

## 使用方法

1. 用法 `-h/ --help`
```
python3 cdncert.py -h
usage: cdncert.py [-h] [-f] [-o ONLY] [-a {domain,user}] [-d {domain,user}]
[-ls {domains,users}] [-v]
CDN Cert - Automatically push the new certificates to CDN
optional arguments:
-h, --help show this help message and exit
-f, --force force update
-o ONLY, --only ONLY update only, use it after -f/--force
-a {domain,user}, --add {domain,user}
add [domain/user] to database
-d {domain,user}, --delete {domain,user}
delete [domain/user] from database
-ls {domains,users}, --list {domains,users}
print all [domains/users] from database
-v, --verbosity increase output verbosity
```
usage: cdncert.py [-h] [-f] [-o ONLY] [-a {domain,user}] [-e {domain,user}] [-d {domain,user}] [-ls {domains,users}] [-v]
CDN Cert - Automatically push the new certificates to CDN
optional arguments:
-h, --help show this help message and exit
-f, --force force update
-o ONLY, --only ONLY update only, use it after -f/--force
-a {domain,user}, --add {domain,user}
add [domain/user] to database
-e {domain,user}, --edit {domain,user}
edit [domain/user] in database
-d {domain,user}, --delete {domain,user}
delete [domain/user] from database
-ls {domains,users}, --list {domains,users}
print all [domains/users] from database
-v, --verbosity increase output verbosity
```
2. 添加用户信息 `-a user`

![image][image-1]
Expand Down Expand Up @@ -97,12 +110,11 @@ pip3 install -r requirements.txt

![image][image-9]

11. 定时配置
11. 定时配置(Docker 无需配置)
```
crontab -e
# 每天 3:30 执行
30 3 * * * python3 /home/cdn_cert/update.py
30 3 * * * python3 /home/cdn_cert/cdncert.py
```

### LICENSE 版权声明
Expand Down
13 changes: 11 additions & 2 deletions cdncert.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,11 @@
parser.add_argument('-f', '--force', action="store_true", help='force update')
parser.add_argument('-o', '--only', action="store", default=None, help='update only, use it after -f/--force')
parser.add_argument('-a', '--add', action="store", choices=['domain', 'user'], help='add [domain/user] to database')
parser.add_argument('-d', '--delete', action="store", choices=['domain', 'user'], help='delete [domain/user] from database')
parser.add_argument('-ls', '--list', action="store", choices=['domains', 'users'], help='print all [domains/users] from database')
parser.add_argument('-e', '--edit', action="store", choices=['domain', 'user'], help='edit [domain/user] in database')
parser.add_argument('-d', '--delete', action="store", choices=['domain', 'user'], help='delete [domain/user] from '
'database')
parser.add_argument('-ls', '--list', action="store", choices=['domains', 'users'], help='print all [domains/users] '
'from database')
parser.add_argument("-v", "--verbosity", action="store_true", help="increase output verbosity")
args = parser.parse_args()

Expand All @@ -28,6 +31,12 @@
Core.add_user()
elif args.add == 'domain':
Core.add_domain()
elif args.edit:
if args.edit == 'user':
pass
# Core.update_user()
elif args.edit == 'domain':
Core.update_domain()
elif args.delete:
if args.delete == 'user':
Core.delete_user()
Expand Down
15 changes: 15 additions & 0 deletions config-template.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
[database]
Path=cert.db

[letencrypt]
Path=/cert/ssl
ServerCertificateName=fullchain.cer
PrivateKeyName = {{ domain_name }}.key

[mail]
Host=smtpdm.aliyun.com
Port=465
UserName=
PassWord=
From=
To=
61 changes: 52 additions & 9 deletions core.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import hashlib
import json
import settings
from pathlib import Path
from aliyunsdkcore.client import AcsClient
from aliyunsdkcdn.request.v20180510.SetDomainServerCertificateRequest import SetDomainServerCertificateRequest
from prettytable import PrettyTable
Expand All @@ -31,7 +32,7 @@ def do(self, force=False, only=''):
else:
domains = db.get_all_domain()
for domain in domains:
queue += (domain.domain, )
queue += (domain.domain,)
self.push(force=force, queue=queue)

# 获取文件 md5
Expand All @@ -41,7 +42,8 @@ def md5sum(path):
file = open(path, 'rb')
return hashlib.md5(file.read()).hexdigest()

def add_user(self):
@staticmethod
def add_user():
name = input('Please input the user name\n')

if db.has_user(name):
Expand All @@ -63,7 +65,24 @@ def add_domain(self):
if db.has_user(user) is not True:
exit('\033[1;31mNo record of %s.\033[0m' % user)

db.add_domain(domain, user)
sure = input('Set your own server certificate and private key path: [y/n]')
if sure == 'y':
while True:
cert_path = input('Server certificate path:\n')
if Path(cert_path).exists():
break
else:
print('\033[1;31mFile not found, please input again.\033[0m')

while True:
private_key_path = input('Private key path:\n')
if Path(private_key_path).exists():
break
else:
print('\033[1;31mFile not found, please input again.\033[0m')
db.add_domain(domain, user, cert_path, private_key_path)
else:
db.add_domain(domain, user)

@staticmethod
def get_all_domain():
Expand Down Expand Up @@ -100,7 +119,24 @@ def update_domain(self):
if db.has_user(user) is not True:
exit('\033[1;31mNo record of this user.\033[0m')

db.update_domain(domain, user=user)
sure = input('Set your own server certificate and private key path: [y/n]')
if sure == 'y':
while True:
cert_path = input('Server certificate path:\n')
if Path(cert_path).exists():
break
else:
print('\033[1;31mFile not found, please input again.\033[0m')

while True:
private_key_path = input('Private key path:\n')
if Path(private_key_path).exists():
break
else:
print('\033[1;31mFile not found, please input again.\033[0m')
db.update_domain(domain, user=user, cert_path=cert_path, private_key_path=private_key_path)
else:
db.update_domain(domain, user=user)

def delete_domain(self):
self.get_all_domain()
Expand Down Expand Up @@ -129,17 +165,24 @@ def delete_user(self):
def push(self, force, queue=()):
msg = {}
for domain in queue:
ServerCertificatePath = os.path.join(settings.LiveCert, domain,
settings.ServerCertificateName) # 安全证书路径

PrivateKeyPath = os.path.join(settings.LiveCert, domain,
settings.PrivkeyName.replace('{{ domain_name }}', domain)) # 私钥路径
info = db.get_domain(domain)
user = db.get_user(info.user)
store_md5 = info.md5
currect_md5 = self.md5sum(PrivateKeyPath)
if not currect_md5 == store_md5 or force is True:

# 自定义路径
if info.cert_path and info.private_key_path:
ServerCertificatePath = info.cert_path
PrivateKeyPath = info.private_key_path

current_md5 = self.md5sum(PrivateKeyPath)
if not current_md5 == store_md5 or force is True:
try:
client = AcsClient(user.access_key_id, user.access_key_secret, 'cn-hangzhou')
ServerCertificatePath = os.path.join(settings.LiveCert, domain,
settings.ServerCertificateName) # 安全证书路径
ServerCertificate = open(ServerCertificatePath, 'r').read()
PrivateKey = open(PrivateKeyPath, 'r').read()
CertName = domain + '_' + datetime.datetime.now().strftime("%Y%m%d_%H%M%S") # 证书名称
Expand All @@ -157,7 +200,7 @@ def push(self, force, queue=()):
RequestId = json.loads(response.decode('utf-8'))['RequestId']
result = "Push successfully\nRequestId: " + str(RequestId)

db.update_domain(domain, currect_md5)
db.update_domain(domain, current_md5)
except Exception as e:
result = e.get_error_code() if hasattr(e, 'get_error_code') else e
msg[domain] = result
Expand Down
6 changes: 6 additions & 0 deletions cron.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
CRIPT=$(readlink -f "$0")
SCRIPTPATH=$(dirname "$SCRIPT")

cd "$SCRIPTPATH"

/usr/bin/python3 cdncert.py > cdncert.log 2>&1
39 changes: 21 additions & 18 deletions database.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/usr/bin/python
# -*- coding:utf-8 -*-
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import create_engine, Table, Column, String, Integer, MetaData
from sqlalchemy import create_engine, Column, String, Integer
from sqlalchemy.orm import sessionmaker
from settings import DB_PATH

Expand All @@ -14,6 +14,8 @@ class Domain(db):
id = Column(Integer, primary_key=True)
domain = Column(String(255))
md5 = Column(String(32))
cert_path = Column(String(255))
private_key_path = Column(String(255))
user = Column(Integer)


Expand All @@ -26,7 +28,7 @@ class User(db):


class Database(object):
def __init__(self, verbosity = False):
def __init__(self, verbosity=False):
engine.echo = verbosity
db.metadata.create_all(engine)
self.sessionmaker = sessionmaker(bind=engine)
Expand All @@ -37,16 +39,17 @@ def add_user(self, name, access_key_id, access_key_secret):
self.session.add(User(name=name, access_key_id=access_key_id, access_key_secret=access_key_secret))
self.session.commit()
print("\033[1;32mUser %s added successfully\033[0m" % name)
except Exception:
print(Exception)
except Exception as e:
print(e)

def add_domain(self, domain, user):
def add_domain(self, domain, user, cert_path=None, private_key_path=None):
try:
self.session.add(Domain(domain=domain, user=user))
self.session.add(Domain(domain=domain, user=user,
cert_path=cert_path, private_key_path=private_key_path))
self.session.commit()
print("\033[1;32mDomain %s added successfully\033[0m" % domain)
except Exception:
print(Exception)
except Exception as e:
print(e)

def get_domain(self, domain):
return self.session.query(Domain).filter(Domain.domain == domain).first()
Expand All @@ -72,16 +75,20 @@ def get_user(self, name):
def get_all_user(self):
return self.session.query(User).all()

def update_domain(self, domain, md5, user=None):
def update_domain(self, domain, md5, user=None, cert_path=None, private_key_path=None):
try:
domain = self.session.query(Domain).filter(Domain.domain == domain).first()
domain.md5 = md5
if user is not None:
domain.user = user
print("\033[1;32mDomain %s updated successfully\033[0m" % domain)
if cert_path is not None:
domain.cert_path=cert_path
if private_key_path is not None:
domain.private_key_path=private_key_path
self.session.commit()
except Exception:
print(Exception)
print("\033[1;32mDomain %s updated successfully\033[0m" % domain.domain)
except Exception as e:
print(e)

def delete_domain(self, domain):
self.session.query(Domain).filter(Domain.domain == domain).delete()
Expand All @@ -93,9 +100,5 @@ def delete_user(self, name):
self.session.query(User).filter(User.name == name).delete()
self.session.commit()
print("\033[1;32mUser %s deleted successfully\033[0m" % name)
except Exception:
print(Exception)




except Exception as e:
print(e)
Loading

0 comments on commit 3f6eaa6

Please sign in to comment.