feat: 给update.sh里的 dns 配上环境变量
This commit is contained in:
commit
105333dd6f
71
.github/workflows/docker-publish.yml
vendored
Normal file
71
.github/workflows/docker-publish.yml
vendored
Normal file
@ -0,0 +1,71 @@
|
||||
name: Docker
|
||||
|
||||
# This workflow uses actions that are not certified by GitHub.
|
||||
# They are provided by a third-party and are governed by
|
||||
# separate terms of service, privacy policy, and support
|
||||
# documentation.
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '40 23 * * *'
|
||||
push:
|
||||
branches: [ main ]
|
||||
# Publish semver tags as releases.
|
||||
tags: [ 'v*.*.*' ]
|
||||
# pull_request:
|
||||
# branches: [ main ]
|
||||
|
||||
env:
|
||||
# Use docker.io for Docker Hub if empty
|
||||
REGISTRY: ghcr.io
|
||||
# github.repository as <account>/<repo>
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
# This is used to complete the identity challenge
|
||||
# with sigstore/fulcio when running outside of PRs.
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
# Workaround: https://github.com/docker/build-push-action/issues/461
|
||||
- name: Setup Docker buildx
|
||||
uses: docker/setup-buildx-action@79abd3f86f79a9d68a23c75a09a9a85889262adf
|
||||
|
||||
# Login against a Docker registry except on PR
|
||||
# https://github.com/docker/login-action
|
||||
- name: Log into registry
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@28218f9b04b4f3f62068d7b6ce6ca5b26e35336c
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# Extract metadata (tags, labels) for Docker
|
||||
# https://github.com/docker/metadata-action
|
||||
- name: Extract Docker metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
|
||||
# Build and push Docker image with Buildx (don't push on PR)
|
||||
# https://github.com/docker/build-push-action
|
||||
- name: Build and push Docker image
|
||||
id: build-and-push
|
||||
uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc
|
||||
with:
|
||||
context: .
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
45
.github/workflows/update-cert.yml
vendored
Normal file
45
.github/workflows/update-cert.yml
vendored
Normal file
@ -0,0 +1,45 @@
|
||||
name: Update Cert
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 10 1 * *'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Prepare Environment
|
||||
run: |
|
||||
curl https://get.acme.sh | sh -s email=my@example.com
|
||||
pip install -r requirements.txt
|
||||
|
||||
- name: Update Cert
|
||||
env:
|
||||
DP_Id: ${{ secrets.DP_Id }}
|
||||
DP_Key: ${{ secrets.DP_Key }}
|
||||
ACME_DNS_TYPE: ${{ secrets.ACME_DNS_TYPE }}
|
||||
ACME_DOMAIN: ${{ secrets.ACME_DOMAIN }}
|
||||
SECRETID: ${{ secrets.SECRETID }}
|
||||
SECRETKEY: ${{ secrets.SECRETKEY }}
|
||||
CDN_DOMAIN: ${{ secrets.CDN_DOMAIN }}
|
||||
CERT_HOME: /home/runner/.acme.sh
|
||||
ACME_HOME: /home/runner/.acme.sh
|
||||
WORK_DIR: .
|
||||
run: sh ./docker/update.sh
|
||||
|
||||
- name: Notification
|
||||
uses: monlor/bark-action@v3
|
||||
if: always()
|
||||
with:
|
||||
host: ${{ secrets.BARK_HOST}} # not required
|
||||
key: ${{ secrets.BARK_KEY }} # Your secret key
|
||||
title: Github Actions
|
||||
body: 'Your tencent cdn certs update ${{ job.status }}!'
|
||||
isArchive: 1
|
||||
url: 'github://github.com/${{ github.repository }}'
|
||||
group: Github
|
||||
icon: https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png
|
||||
copy: ${{ steps.meta.outputs.tags }}
|
129
.gitignore
vendored
Normal file
129
.gitignore
vendored
Normal file
@ -0,0 +1,129 @@
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
pip-wheel-metadata/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
.python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
5
.idea/.gitignore
vendored
Normal file
5
.idea/.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
5
.idea/inspectionProfiles/Project_Default.xml
Normal file
5
.idea/inspectionProfiles/Project_Default.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
</profile>
|
||||
</component>
|
6
.idea/jsLibraryMappings.xml
Normal file
6
.idea/jsLibraryMappings.xml
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="JavaScriptLibraryMappings">
|
||||
<includedPredefinedLibrary name="Node.js Core" />
|
||||
</component>
|
||||
</project>
|
6
.idea/misc.xml
Normal file
6
.idea/misc.xml
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="JavaScriptSettings">
|
||||
<option name="languageLevel" value="ES6" />
|
||||
</component>
|
||||
</project>
|
8
.idea/modules.xml
Normal file
8
.idea/modules.xml
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/qcloud-ssl-cdn.iml" filepath="$PROJECT_DIR$/.idea/qcloud-ssl-cdn.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
12
.idea/qcloud-ssl-cdn.iml
Normal file
12
.idea/qcloud-ssl-cdn.iml
Normal file
@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="WEB_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<excludeFolder url="file://$MODULE_DIR$/temp" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/tmp" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
6
.idea/vcs.xml
Normal file
6
.idea/vcs.xml
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
27
Dockerfile
Normal file
27
Dockerfile
Normal file
@ -0,0 +1,27 @@
|
||||
FROM python:alpine
|
||||
|
||||
LABEL AUTHOR="monlor"
|
||||
|
||||
ENV LANG="C.UTF-8"
|
||||
ENV TZ="Asia/Shanghai"
|
||||
ENV CERT_HOME="/data/certs"
|
||||
ENV WORK_DIR="/data"
|
||||
ENV ACME_HOME="/root/.acme.sh"
|
||||
|
||||
WORKDIR /data
|
||||
|
||||
COPY api /data/api
|
||||
COPY main.py /data/
|
||||
COPY requirements.txt /data/
|
||||
COPY docker/entrypoint.sh /
|
||||
COPY docker/update.sh /data
|
||||
|
||||
RUN apk update && apk add --no-cache curl openssl socat && \
|
||||
ln -sf /usr/share/zoneinfo/${TZ} /etc/localtime && \
|
||||
echo "${TZ}" > /etc/timezone && \
|
||||
pip install -r requirements.txt && \
|
||||
chmod +x /entrypoint.sh /data/update.sh && \
|
||||
curl https://get.acme.sh | sh -s email=my@example.com
|
||||
|
||||
VOLUME ["/data/certs"]
|
||||
ENTRYPOINT [ "/entrypoint.sh" ]
|
201
LICENSE
Normal file
201
LICENSE
Normal file
@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
94
README.md
Normal file
94
README.md
Normal file
@ -0,0 +1,94 @@
|
||||
## 腾讯云自动SSL证书上传及替换
|
||||
功能:
|
||||
* 把本地的SSL证书上传到腾讯云[SSL证书](https://console.cloud.tencent.com/ssl),并记录id
|
||||
* 为CDN服务更换指定id的SSL证书
|
||||
* 根据网址,批量预热URL
|
||||
|
||||
目的:
|
||||
* 把利用[acme.sh](https://github.com/acmesh-official/acme.sh)申请的`Let's Encrypt`证书上传到腾讯云
|
||||
* 由于多次申请`TrustAsia`的一年期免费单域名证书失败,所以准备使用`Let's Encrypt`证书
|
||||
* 该程序已将每一个步骤都实现:自动上传SSL并替换CDN的证书
|
||||
* 为了使网站访问更快,每天预热URL(可以单独抽出该函数,运行在[腾讯云函数](https://github.com/zfb132/auto_push_url))
|
||||
|
||||
|
||||
## 部署方式
|
||||
|
||||
### 使用 Docker 快速部署
|
||||
|
||||
每月 1 号凌晨 2 点定时执行证书更新
|
||||
|
||||
* `ACME_DNS_TYPE`: Acme 的 dns 类型,你可以选择你的 dns 类型并配置[环境变量密钥](https://github.com/acmesh-official/acme.sh/wiki/dnsapi)
|
||||
* `ACME_DOMAIN`: 你的顶级域名,例如:monlor.com,自动申请证书 monlor.com/*.monlor.com
|
||||
* `SECRETID`: 腾讯云 Secret Id
|
||||
* `SECRETKEY`: 腾讯云 Secret Key
|
||||
* `CDN_DOMAIN`: CDN 域名,多个域名用逗号分隔
|
||||
* `RUN_NOW`: 是否在 Docker 启动时执行程序
|
||||
|
||||
```bash
|
||||
docker run -d \
|
||||
--name qcloud-ssl-cdn \
|
||||
--restart=unless-stopped \
|
||||
-e DP_Id=xxx \
|
||||
-e DP_Key=xxx \
|
||||
-e ACME_DNS_TYPE=dns_dp \
|
||||
-e ACME_DOMAIN=monlor.com \
|
||||
-e SECRETID=xxx \
|
||||
-e SECRETKEY=xxx \
|
||||
-e CDN_DOMAIN=www.monlor.com \
|
||||
-e RUN_NOW=true \
|
||||
ghcr.io/monlor/qcloud-ssl-cdn:main
|
||||
```
|
||||
|
||||
#### 其他变量
|
||||
|
||||
* `ACME_ENABLED`: 是否启用 acme,不启用将证书映射到容器`/data/certs`目录
|
||||
* `PUSH_URLS`: CDN 刷新/预热地址,逗号分隔
|
||||
* `PUSH_URLS_PATH`: CDN 刷新/预热地址文件路径,文件映射到 Docker 容器,**路径不能是`/data/urls.txt`**
|
||||
|
||||
### 使用 GitHub Action 部署
|
||||
|
||||
Fork 此项目,配置以下 Github Action Secrets
|
||||
|
||||
* `ACME_DNS_TYPE`: Acme 的 dns 类型,你可以选择你的 dns 类型并配置[环境变量密钥](https://github.com/acmesh-official/acme.sh/wiki/dnsapi)
|
||||
* `ACME_DOMAIN`: 你的顶级域名,例如:monlor.com,自动申请证书 monlor.com/*.monlor.com
|
||||
* `SECRETID`: 腾讯云 Secret Id
|
||||
* `SECRETKEY`: 腾讯云 Secret Key
|
||||
* `CDN_DOMAIN`: CDN 域名,多个域名用逗号分隔
|
||||
* `BARK_HOST`: [Bark](https://github.com/Finb/Bark) 消息通知 Host
|
||||
* `BARK_KEY`: [Bark](https://github.com/Finb/Bark) 消息通知 Key
|
||||
|
||||
### 手动部署
|
||||
|
||||
#### 使用acme.sh申请证书
|
||||
[安装及简单使用](https://blog.whuzfb.cn/blog/2020/07/07/web_https/#3-%E5%AE%89%E8%A3%85acme%E8%87%AA%E5%8A%A8%E7%AD%BE%E5%8F%91%E8%AF%81%E4%B9%A6)
|
||||
对于本程序
|
||||
```bash
|
||||
# 腾讯云支持使用单域名和泛域名的证书,例如申请泛域名
|
||||
acme.sh --issue -d "whuzfb.cn" -d "*.whuzfb.cn" --dns dns_dp
|
||||
# 申请单域名
|
||||
# acme.sh --issue -d "blog.whuzfb.cn" --dns dns_dp
|
||||
```
|
||||
|
||||
#### 修改config.example.py参数
|
||||
根据注释修改每一项内容
|
||||
然后重命名为`config.py`
|
||||
|
||||
## 主要函数
|
||||
`ssl.get_cert_list(client)`:获取所有的SSL证书列表
|
||||
`ssl.get_cert_info(client, cert_id)`:根据id获取SSL证书的信息
|
||||
`ssl.get_cert_detail(client, cert_id)`:根据id获取SSL证书的详情
|
||||
`ssl.delete_cert(client, cert_id)`:删除指定id的SSL证书
|
||||
`ssl.upload_cert(client, local_cert_info)`:把本地的SSL证书上传到腾讯云,返回新证书的id
|
||||
|
||||
|
||||
`cdn.get_cdn_detail_info(client)`:获取所有CDN的详细信息,返回列表
|
||||
`cdn.get_cdn_url_push_info(client)`:查询CDN预热配额和每日可用量
|
||||
`cdn.update_cdn_url_push(client, urls)`:指定 URL 资源列表加载至 CDN 节点,支持指定加速区域预热;默认情况下境内、境外每日预热 URL 限额为各 1000 条,每次最多可提交 20 条
|
||||
`cdn.get_cdn_purge_url_info(client)`:查询CDN刷新配额和每日可用量
|
||||
`cdn.update_cdn_purge_url(client, urls)`:指定 URL 资源的刷新,支持指定加速区域刷新;默认情况下境内、境外每日刷新 URL 限额为各 10000 条,每次最多可提交 1000 条
|
||||
`cdn.update_cdn_ssl(client, domain, cert_id)`:为指定域名的CDN更换SSL证书
|
||||
|
||||
|
||||
`ecdn.get_ecdn_basic_info(client)`:获取所有ECDN(全球加速服务)的基本信息,返回列表
|
||||
`ecdn.get_ecdn_detail_info(client)`:获取所有ECDN的详细信息,返回列表
|
||||
`ecdn.update_ecdn_ssl(client, domain, cert_id)`:为指定域名的CDN的更换SSL证书
|
0
api/__init__.py
Normal file
0
api/__init__.py
Normal file
213
api/cdn.py
Normal file
213
api/cdn.py
Normal file
@ -0,0 +1,213 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
# author: 'zfb'
|
||||
# time: 2020-12-02 15:42
|
||||
import json
|
||||
|
||||
from datetime import datetime
|
||||
from tencentcloud.common.exception.tencent_cloud_sdk_exception import TencentCloudSDKException
|
||||
# 导入 cdn 产品模块的 models
|
||||
from tencentcloud.cdn.v20180606 import models
|
||||
|
||||
from api.get_client_profile import get_client_instance
|
||||
|
||||
def get_cdn_client_instance(id, key):
|
||||
'''获取cdn的实例,用于后面对cdn的各种操作
|
||||
'''
|
||||
client = get_client_instance(id, key, "cdn")
|
||||
return client
|
||||
|
||||
|
||||
def get_cdn_detail_info(client):
|
||||
'''获取所有CDN的详细信息,返回列表
|
||||
'''
|
||||
try:
|
||||
req = models.DescribeDomainsConfigRequest()
|
||||
# 参数列表为空:表示获取所有信息
|
||||
# 部分可选参数
|
||||
# Filters: Array Of DomainFilter, 查询条件过滤器,复杂类型
|
||||
# filter = DomainFilter()
|
||||
# filter.Name = "domain"
|
||||
# filter.Value = [domain]
|
||||
# filter.Fuzzy = False
|
||||
params = {}
|
||||
req.from_json_string(json.dumps(params))
|
||||
|
||||
resp = client.DescribeDomainsConfig(req)
|
||||
# print(resp.to_json_string())
|
||||
print("获取所有cdn详细信息成功")
|
||||
return resp.Domains
|
||||
|
||||
except TencentCloudSDKException as err:
|
||||
print(err)
|
||||
return []
|
||||
|
||||
def get_cdn_basic_info(client, domain_name):
|
||||
'''获取指定CDN的基本信息
|
||||
'''
|
||||
try:
|
||||
req = models.DescribeDomainsRequest()
|
||||
params = {
|
||||
"Limit": 1,
|
||||
"Filters": [
|
||||
{
|
||||
"Name": "domain",
|
||||
"Value": [ domain_name ],
|
||||
"Fuzzy": False
|
||||
}
|
||||
]
|
||||
}
|
||||
req.from_json_string(json.dumps(params))
|
||||
|
||||
resp = client.DescribeDomains(req)
|
||||
# print(resp.to_json_string())
|
||||
print("获取指定cdn基本信息成功")
|
||||
return resp.Domains
|
||||
|
||||
except TencentCloudSDKException as err:
|
||||
print(err)
|
||||
return []
|
||||
|
||||
def get_cdn_url_push_info(client):
|
||||
'''查询CDN预热配额和每日可用量
|
||||
'''
|
||||
try:
|
||||
req = models.DescribePushQuotaRequest()
|
||||
params = {}
|
||||
req.from_json_string(json.dumps(params))
|
||||
resp = client.DescribePushQuota(req)
|
||||
# print(resp.to_json_string())
|
||||
print("获取CDN预热配额和每日可用量信息成功")
|
||||
return resp.UrlPush
|
||||
|
||||
except TencentCloudSDKException as err:
|
||||
print(err)
|
||||
return []
|
||||
|
||||
|
||||
def update_cdn_url_push(client, urls, region):
|
||||
'''指定 URL 资源列表加载至 CDN 节点,支持指定加速区域预热
|
||||
默认情况下境内、境外每日预热 URL 限额为各 1000 条,每次最多可提交 20 条
|
||||
'''
|
||||
try:
|
||||
req = models.PushUrlsCacheRequest()
|
||||
params = {
|
||||
"Urls": urls,
|
||||
"Area": region,
|
||||
}
|
||||
req.from_json_string(json.dumps(params))
|
||||
resp = client.PushUrlsCache(req)
|
||||
print(resp.to_json_string())
|
||||
print("URL:{}预热成功".format(', '.join(urls)))
|
||||
return True
|
||||
|
||||
except TencentCloudSDKException as err:
|
||||
print(err)
|
||||
return False
|
||||
|
||||
|
||||
def get_cdn_purge_url_info(client):
|
||||
'''查询CDN刷新URL配额和每日可用量
|
||||
'''
|
||||
try:
|
||||
req = models.DescribePurgeQuotaRequest()
|
||||
params = {}
|
||||
req.from_json_string(json.dumps(params))
|
||||
resp = client.DescribePurgeQuota(req)
|
||||
# print(resp.to_json_string())
|
||||
print("获取CDN刷新URL配额和每日可用量信息成功")
|
||||
return resp.UrlPurge
|
||||
|
||||
except TencentCloudSDKException as err:
|
||||
print(err)
|
||||
return []
|
||||
|
||||
|
||||
def update_cdn_purge_url(client, urls, region):
|
||||
'''指定 URL 资源的刷新,支持指定加速区域刷新
|
||||
默认情况下境内、境外每日刷新 URL 限额为各 10000 条,每次最多可提交 1000 条
|
||||
'''
|
||||
try:
|
||||
req = models.PurgeUrlsCacheRequest()
|
||||
params = {
|
||||
"Urls": urls,
|
||||
"Area": region,
|
||||
"UrlEncode": True
|
||||
}
|
||||
req.from_json_string(json.dumps(params))
|
||||
resp = client.PurgeUrlsCache(req)
|
||||
print(resp.to_json_string())
|
||||
print("URL:{}刷新成功".format(', '.join(urls)))
|
||||
return True
|
||||
|
||||
except TencentCloudSDKException as err:
|
||||
print(err)
|
||||
return False
|
||||
|
||||
|
||||
def update_cdn_ssl(client, domain, cert_id):
|
||||
'''为指定域名的CDN更换SSL证书
|
||||
'''
|
||||
try:
|
||||
req = models.UpdateDomainConfigRequest()
|
||||
# 必选参数
|
||||
# Domain: String, 域名
|
||||
# 部分可选参数
|
||||
# Https: Https, Https 加速配置
|
||||
# 该类型详见 https://cloud.tencent.com/document/api/228/30987#Https
|
||||
timestr = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
params = {
|
||||
"Domain": domain,
|
||||
"Https": {
|
||||
"Switch": "on",
|
||||
"CertInfo": {
|
||||
"CertId": cert_id,
|
||||
"Message": "Auto update by api at {}".format(timestr)
|
||||
}
|
||||
}
|
||||
}
|
||||
req.from_json_string(json.dumps(params))
|
||||
|
||||
resp = client.UpdateDomainConfig(req)
|
||||
print(resp.to_json_string())
|
||||
print("成功更新域名为{0}的CDN的ssl证书为{1}".format(domain, cert_id))
|
||||
|
||||
except TencentCloudSDKException as err:
|
||||
print(err)
|
||||
exit("为CDN设置SSL证书{}出错".format(cert_id))
|
||||
|
||||
def update_cdn_https_options(client, domain, http2, hsts, age, hsts_subdomain, ocsp):
|
||||
'''为指定域名的CDN的HTTPS开启HTTP 2.0、HSTS、OCSP等多个可选项
|
||||
'''
|
||||
try:
|
||||
req = models.UpdateDomainConfigRequest()
|
||||
params = {
|
||||
"Domain": domain,
|
||||
"Https": {
|
||||
"Switch": "on"
|
||||
}
|
||||
}
|
||||
if http2:
|
||||
params["Https"]["Http2"] = "on"
|
||||
if hsts:
|
||||
params["Https"]["Hsts"] = {
|
||||
"Switch": "off",
|
||||
"MaxAge": 0,
|
||||
"IncludeSubDomains": "off"
|
||||
}
|
||||
params["Https"]["Hsts"]["Switch"] = "on"
|
||||
params["Https"]["Hsts"]["MaxAge"] = age
|
||||
if hsts_subdomain:
|
||||
params["Https"]["Hsts"]["IncludeSubDomains"] = "on"
|
||||
if ocsp:
|
||||
params["Https"]["OcspStapling"] = "on"
|
||||
|
||||
req.from_json_string(json.dumps(params))
|
||||
|
||||
resp = client.UpdateDomainConfig(req)
|
||||
print(resp.to_json_string())
|
||||
print("成功开启域名为{0}的CDN的HTTPS选项".format(domain))
|
||||
|
||||
except TencentCloudSDKException as err:
|
||||
print(err)
|
||||
exit("为{}的CDN开启HTTPS选项功能出错".format(domain))
|
79
api/ecdn.py
Normal file
79
api/ecdn.py
Normal file
@ -0,0 +1,79 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
# author: 'zfb'
|
||||
# time: 2020-12-02 15:50
|
||||
import json
|
||||
|
||||
from tencentcloud.common.exception.tencent_cloud_sdk_exception import TencentCloudSDKException
|
||||
# 导入 ecdn 产品模块的 models
|
||||
from tencentcloud.ecdn.v20191012 import models
|
||||
|
||||
from api.get_client_profile import get_client_instance
|
||||
|
||||
def get_ecdn_client_instance(id, key):
|
||||
'''获取ecdn的实例,用于后面对ecdn的各种操作
|
||||
'''
|
||||
client = get_client_instance(id, key, "ecdn")
|
||||
return client
|
||||
|
||||
|
||||
def get_ecdn_basic_info(client):
|
||||
'''获取所有ECDN的基本信息,返回列表
|
||||
'''
|
||||
try:
|
||||
req = models.DescribeDomainsRequest()
|
||||
params = {}
|
||||
req.from_json_string(json.dumps(params))
|
||||
resp = client.DescribeDomains(req)
|
||||
# print(resp.to_json_string())
|
||||
print("获取所有ecdn基本信息成功")
|
||||
return resp.Domains
|
||||
|
||||
except TencentCloudSDKException as err:
|
||||
print(err)
|
||||
return []
|
||||
|
||||
def get_ecdn_detail_info(client):
|
||||
'''获取所有ECDN的详细信息,返回列表
|
||||
'''
|
||||
try:
|
||||
req = models.DescribeDomainsConfigRequest()
|
||||
params = {}
|
||||
req.from_json_string(json.dumps(params))
|
||||
resp = client.DescribeDomainsConfig(req)
|
||||
# print(resp.to_json_string())
|
||||
print("获取所有ecdn详细信息成功")
|
||||
return resp.Domains
|
||||
|
||||
except TencentCloudSDKException as err:
|
||||
print(err)
|
||||
return []
|
||||
|
||||
def update_ecdn_ssl(client, domain, cert_id):
|
||||
'''为指定域名的CDN的更换SSL证书
|
||||
'''
|
||||
# 为ecdn更新证书,使用ecdn相关接口
|
||||
# https://console.cloud.tencent.com/api/explorer?Product=ecdn&Version=2019-10-12
|
||||
try:
|
||||
req = models.UpdateDomainConfigRequest()
|
||||
# 必选参数
|
||||
# Domain: String, 域名
|
||||
# 部分可选参数
|
||||
# Https: Https, Https 加速配置
|
||||
# 该类型详见 https://cloud.tencent.com/document/api/228/30987#Https
|
||||
params = {
|
||||
"Domain": domain,
|
||||
"Https": {
|
||||
"CertInfo": {
|
||||
"CertId": cert_id
|
||||
}
|
||||
}
|
||||
}
|
||||
req.from_json_string(json.dumps(params))
|
||||
resp = client.UpdateDomainConfig(req)
|
||||
print(resp.to_json_string())
|
||||
print("成功更新域名为{0}的CDN的ssl证书为{1}".format(domain, cert_id))
|
||||
|
||||
except TencentCloudSDKException as err:
|
||||
print(err)
|
||||
exit("为CDN设置SSL证书{}出错".format(cert_id))
|
52
api/get_client_profile.py
Normal file
52
api/get_client_profile.py
Normal file
@ -0,0 +1,52 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
# author: 'zfb'
|
||||
# time: 2020-12-02 15:17
|
||||
|
||||
from tencentcloud.common import credential
|
||||
# 导入可选配置类
|
||||
from tencentcloud.common.profile.client_profile import ClientProfile
|
||||
from tencentcloud.common.profile.http_profile import HttpProfile
|
||||
from tencentcloud.common.exception.tencent_cloud_sdk_exception import TencentCloudSDKException
|
||||
# 导入ssl产品模块的 client
|
||||
from tencentcloud.ssl.v20191205 import ssl_client
|
||||
# 导入cdn产品模块的 client
|
||||
from tencentcloud.cdn.v20180606 import cdn_client
|
||||
# 导入ecdn产品模块的 client
|
||||
from tencentcloud.ecdn.v20191012 import ecdn_client
|
||||
|
||||
def get_client_instance(id, key, product):
|
||||
'''获取指定endpoint的实例,用于后面对其的各种操作
|
||||
'''
|
||||
try:
|
||||
# 实例化一个认证对象,入参需要传入腾讯云账户 secretId,secretKey, 此处还需注意密钥对的保密
|
||||
cred = credential.Credential(id, key)
|
||||
|
||||
# 实例化一个 http 选项,可选
|
||||
httpProfile = HttpProfile()
|
||||
# post 请求 (默认为 post 请求)
|
||||
httpProfile.reqMethod = "POST"
|
||||
# 请求超时时间,单位为秒 (默认60秒)
|
||||
httpProfile.reqTimeout = 30
|
||||
# 不指定接入地域域名 (默认就近接入)
|
||||
httpProfile.endpoint = "{}.tencentcloudapi.com".format(product)
|
||||
|
||||
# 实例化一个 client 选项,可选
|
||||
clientProfile = ClientProfile()
|
||||
clientProfile.httpProfile = httpProfile
|
||||
# 实例化要请求产品的 client 对象,clientProfile 是可选的
|
||||
if product == "ssl":
|
||||
client = ssl_client.SslClient(cred, "", clientProfile)
|
||||
print("实例化一个ssl_client成功")
|
||||
elif product == "cdn":
|
||||
client = cdn_client.CdnClient(cred, "", clientProfile)
|
||||
print("实例化cdn client成功")
|
||||
elif product == "ecdn":
|
||||
client = ecdn_client.EcdnClient(cred, "", clientProfile)
|
||||
print("实例化ecdn client成功")
|
||||
else:
|
||||
exit("本程序仅支持ssl、cdn、ecdn")
|
||||
return client
|
||||
except TencentCloudSDKException as err:
|
||||
print(err)
|
||||
exit(-1)
|
145
api/ssl.py
Normal file
145
api/ssl.py
Normal file
@ -0,0 +1,145 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
# author: 'zfb'
|
||||
# time: 2020-12-02 15:02
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
from tencentcloud.common.exception.tencent_cloud_sdk_exception import TencentCloudSDKException
|
||||
# 导入 ssl 产品模块的 models
|
||||
from tencentcloud.ssl.v20191205 import models
|
||||
|
||||
from api.get_client_profile import get_client_instance
|
||||
|
||||
def get_ssl_client_instance(id, key):
|
||||
'''获取ssl的实例,用于后面对ssl的各种操作
|
||||
'''
|
||||
client = get_client_instance(id, key, "ssl")
|
||||
return client
|
||||
|
||||
|
||||
def get_cert_list(client):
|
||||
'''获取所有的SSL证书列表
|
||||
'''
|
||||
try:
|
||||
# 实例化一个 ssl 实例信息查询请求对象,每个接口都会对应一个 request 对象
|
||||
req = models.DescribeCertificatesRequest()
|
||||
# 可选参数列表
|
||||
# Offset: Integer, 分页偏移量,从0开始
|
||||
# Limit: Integer, 每页数量,默认20
|
||||
# SearchKey: String, 搜索关键词,可搜索证书 ID、备注名称、域名
|
||||
# CertificateType: String, 证书类型:CA = 客户端证书,SVR = 服务器证书
|
||||
# ProjectId: Integer, 项目 ID
|
||||
# ExpirationSort: String, 按到期时间排序:DESC = 降序, ASC = 升序
|
||||
# CertificateStatus: Array Of Integer, 证书状态
|
||||
# Deployable: Integer, 是否可部署,可选值:1 = 可部署,0 = 不可部署
|
||||
params = {}
|
||||
req.from_json_string(json.dumps(params))
|
||||
|
||||
# 通过 client 对象调用 DescribeCertificatesRequest 方法发起请求,请求方法名与请求对象对应
|
||||
# 返回的 resp 是一个 DescribeCertificatesResponse 类的实例,与请求对象对应
|
||||
resp = client.DescribeCertificates(req)
|
||||
# 输出 json 格式的字符串回包
|
||||
# print(resp.to_json_string())
|
||||
# 也可以取出单个值,通过官网接口文档或跳转到 response 对象的定义处查看返回字段的定义
|
||||
# print(resp.TotalCount)
|
||||
print("获取ssl证书列表成功")
|
||||
return resp.Certificates
|
||||
except TencentCloudSDKException as err:
|
||||
print(err)
|
||||
return []
|
||||
|
||||
|
||||
def get_cert_info(client, cert_id):
|
||||
'''根据id获取SSL证书的信息
|
||||
'''
|
||||
try:
|
||||
req = models.DescribeCertificateRequest()
|
||||
# 必选参数
|
||||
# CertificateId: String, 证书 ID
|
||||
params = {
|
||||
"CertificateId": cert_id
|
||||
}
|
||||
req.from_json_string(json.dumps(params))
|
||||
|
||||
resp = client.DescribeCertificate(req)
|
||||
# print(resp.to_json_string())
|
||||
print("获取ssl证书{}的信息成功".format(cert_id))
|
||||
return resp
|
||||
|
||||
except TencentCloudSDKException as err:
|
||||
print(err)
|
||||
exit("获取证书{}信息出错".format(cert_id))
|
||||
|
||||
|
||||
def get_cert_detail(client, cert_id):
|
||||
'''根据id获取SSL证书的详情
|
||||
'''
|
||||
try:
|
||||
req = models.DescribeCertificateDetailRequest()
|
||||
# 必选参数
|
||||
# CertificateId: String, 证书 ID
|
||||
params = {
|
||||
"CertificateId": cert_id
|
||||
}
|
||||
req.from_json_string(json.dumps(params))
|
||||
|
||||
resp = client.DescribeCertificateDetail(req)
|
||||
# print(resp.to_json_string())
|
||||
print("获取ssl证书{}的详细信息成功".format(cert_id))
|
||||
|
||||
except TencentCloudSDKException as err:
|
||||
print(err)
|
||||
exit("获取证书{}详细信息出错".format(cert_id))
|
||||
|
||||
|
||||
def delete_cert(client, cert_id):
|
||||
'''删除指定id的SSL证书(删除不存在的id会出现警告)
|
||||
'''
|
||||
try:
|
||||
req = models.DeleteCertificateRequest()
|
||||
# 必选参数
|
||||
# CertificateId: String, 证书 ID
|
||||
params = {
|
||||
"CertificateId": cert_id
|
||||
}
|
||||
req.from_json_string(json.dumps(params))
|
||||
|
||||
resp = client.DeleteCertificate(req)
|
||||
# print(resp.to_json_string())
|
||||
print("删除ssl证书{}成功".format(cert_id))
|
||||
|
||||
except TencentCloudSDKException as err:
|
||||
print(err)
|
||||
exit("删除证书{}出错".format(cert_id))
|
||||
|
||||
|
||||
def upload_cert(client, local_cert_info):
|
||||
'''把本地的SSL证书上传到腾讯云,返回新证书的id
|
||||
'''
|
||||
try:
|
||||
req = models.UploadCertificateRequest()
|
||||
# 必选参数
|
||||
# CertificatePublicKey: String, 证书公钥内容
|
||||
# CertificatePrivateKey: String, 私钥内容,证书类型为 SVR 时必填,为 CA 时可不填
|
||||
# 可选参数列表
|
||||
# CertificateType: String, 证书类型,默认 SVR。CA = 客户端证书,SVR = 服务器证书
|
||||
# Alias: String, 备注名称
|
||||
# ProjectId: Integer, 项目 ID
|
||||
timestr = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
params = {
|
||||
"CertificatePublicKey": local_cert_info["cer"],
|
||||
"CertificatePrivateKey": local_cert_info["key"],
|
||||
"CertificateType": local_cert_info["type"],
|
||||
"Alias": "Auto upload by api at {}".format(timestr)
|
||||
}
|
||||
req.from_json_string(json.dumps(params))
|
||||
|
||||
resp = client.UploadCertificate(req)
|
||||
# print(resp.to_json_string())
|
||||
print("上传ssl证书成功")
|
||||
return resp.CertificateId
|
||||
|
||||
except TencentCloudSDKException as err:
|
||||
print(err)
|
||||
return ""
|
85
api/tools.py
Normal file
85
api/tools.py
Normal file
@ -0,0 +1,85 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
# author: 'zfb'
|
||||
# time: 2020-12-02 15:45
|
||||
|
||||
def read_file(name):
|
||||
'''读取文件内容
|
||||
'''
|
||||
with open(name, 'r') as file:
|
||||
text = file.read()
|
||||
return text
|
||||
|
||||
|
||||
def chunks(l, n):
|
||||
for i in range(0, len(l), n):
|
||||
yield l[i:i + n]
|
||||
|
||||
|
||||
def resize_url_list(url_list, group_size):
|
||||
'''将一维列表按照指定长度分割
|
||||
'''
|
||||
url_chunks = list(chunks(url_list, group_size))
|
||||
results = []
|
||||
for i in range(len(url_chunks)):
|
||||
results.append(url_chunks[i])
|
||||
print("重置的URL列表个数{},每个列表包含文件数{}".format(len(results), group_size))
|
||||
return results
|
||||
|
||||
|
||||
def get_sitemap_urls(url):
|
||||
'''从给定的sitemap.xml文件获取链接
|
||||
'''
|
||||
import requests
|
||||
import re
|
||||
text = requests.get(url).text
|
||||
pattern = re.compile(r'<loc>(.*?)</loc>')
|
||||
results = re.findall(pattern, text)
|
||||
url_list = []
|
||||
for res in results:
|
||||
if not res.endswith("/"):
|
||||
res = res + "/"
|
||||
url_list.append(res)
|
||||
return url_list
|
||||
|
||||
|
||||
def get_urls_from_file(file_name):
|
||||
'''从给定的文件获取链接
|
||||
'''
|
||||
with open(file_name, 'r') as file:
|
||||
return [x.strip() for x in file.readlines()]
|
||||
|
||||
def generate_https(https):
|
||||
'''由于Https无法序列化,自己将其改为字典(已弃用)
|
||||
'''
|
||||
server_cert = {}
|
||||
server_cert["CertId"] = https.CertInfo.CertId
|
||||
server_cert["CertName"] = https.CertInfo.CertName
|
||||
server_cert["Certificate"] = https.CertInfo.Certificate
|
||||
server_cert["PrivateKey"] = https.CertInfo.PrivateKey
|
||||
server_cert["ExpireTime"] = https.CertInfo.ExpireTime
|
||||
server_cert["DeployTime"] = https.CertInfo.DeployTime
|
||||
server_cert["Message"] = https.CertInfo.Message
|
||||
|
||||
client_cert = {}
|
||||
client_cert["Certificate"] = https.ClientCertInfo.Certificate
|
||||
client_cert["CertName"] = https.ClientCertInfo.CertName
|
||||
client_cert["ExpireTime"] = https.ClientCertInfo.ExpireTime
|
||||
client_cert["DeployTime"] = https.ClientCertInfo.DeployTime
|
||||
|
||||
hsts = {}
|
||||
hsts["Switch"] = https.Hsts.Switch
|
||||
hsts["MaxAge"] = https.Hsts.MaxAge
|
||||
hsts["IncludeSubDomains"] = https.Hsts.IncludeSubDomains
|
||||
|
||||
res = {}
|
||||
res["Switch"] = https.Switch
|
||||
res["Http2"] = https.Http2
|
||||
res["OcspStapling"] = https.OcspStapling
|
||||
res["VerifyClient"] = https.VerifyClient
|
||||
res["Spdy"] = https.Spdy
|
||||
res["SslStatus"] = https.SslStatus
|
||||
res["CertInfo"] = server_cert
|
||||
res["ClientCertInfo"] = client_cert
|
||||
res["Hsts"] = https.Hsts
|
||||
return res
|
55
config.example.py
Normal file
55
config.example.py
Normal file
@ -0,0 +1,55 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
# author: 'zfb'
|
||||
# time: 2020-12-02 16:15
|
||||
|
||||
# 腾讯云支持使用单域名和泛域名的证书,例如
|
||||
# acme.sh --issue -d "whuzfb.cn" -d "*.whuzfb.cn" --dns dns_dp
|
||||
# acme.sh --issue -d "blog.whuzfb.cn" --dns dns_dp
|
||||
|
||||
# 使用ACME申请的SSL完整证书的本地存放路径
|
||||
CER_FILE = "/home/zfb/.acme.sh/whuzfb.cn/fullchain.cer"
|
||||
|
||||
# 使用ACME申请的SSL证书私钥的本地存放路径
|
||||
KEY_FILE = "/home/zfb/.acme.sh/whuzfb.cn/whuzfb.cn.key"
|
||||
|
||||
# CDN服务配置的域名(需要提前在腾讯云网页前端创建)
|
||||
# 如果ACME申请的证书为泛域名证书,且要配置多个CDN加速
|
||||
# CDN_DOMAIN = ["blog.whuzfb.cn", "blog2.whuzfb.cn", "web.whuzfb.cn"]
|
||||
CDN_DOMAIN = ["blog.whuzfb.cn"]
|
||||
|
||||
# 腾讯云:https://console.cloud.tencent.com/cam/capi
|
||||
SECRETID = "AKeee5555512345677777123456788889876"
|
||||
SECRETKEY = "A71234567890abcdefedcba098765432"
|
||||
|
||||
# 控制功能开关
|
||||
# 是否进行上传证书文件的操作(根据CER_FILE和KEY_FILE)
|
||||
UPLOAD_SSL = True
|
||||
# 以下为HTTPS额外功能
|
||||
# 是否开启HTTP2
|
||||
ENABLE_HTTP2 = True
|
||||
# 是否开启HSTS
|
||||
ENABLE_HSTS = True
|
||||
# 为HSTS设定最长过期时间(以秒为单位)
|
||||
HSTS_TIMEOUT_AGE = 1
|
||||
# HSTS包含子域名(仅对泛域名有效)
|
||||
HSTS_INCLUDE_SUBDOMAIN = True
|
||||
# 是否开启OCSP
|
||||
ENABLE_OCSP = True
|
||||
# 是否删除适用于CDN_DOMAIN域名下的其他所有证书
|
||||
# 满足以下条件:证书适用于CDN_DOMAIN、证书id不是本次使用的id
|
||||
DELETE_OLD_CERTS = True
|
||||
|
||||
# 是否进行为CDN_DOMAIN更换SSL证书的操作
|
||||
# 若UPDATE_SSL = True且UPLOAD_SSL = True,则CERT_ID可不设置,直接利用UPLOAD_SSL的证书
|
||||
UPDATE_SSL = True
|
||||
CERT_ID = ""
|
||||
# 是否进行预热URL的操作
|
||||
PUSH_URL = True
|
||||
# 是否进行刷新URL的操作
|
||||
PURGE_URL = True
|
||||
# 自定义的预热URL(默认会预热sitemap.xml的所有链接)文件路径
|
||||
# 该文件内,每行一个URL,例如
|
||||
# https://blog.whuzfb.cn/img/me2.jpg
|
||||
# https://blog.whuzfb.cn/img/home-bg.jpg
|
||||
URLS_FILE = "urls.txt"
|
17
docker/entrypoint.sh
Normal file
17
docker/entrypoint.sh
Normal file
@ -0,0 +1,17 @@
|
||||
#!/bin/sh
|
||||
|
||||
# 添加crontab配置
|
||||
cat > /var/spool/cron/crontabs/root <<-EOF
|
||||
0 2 1 * * /data/update.sh &> /data/run.log
|
||||
EOF
|
||||
# 启动crontab服务
|
||||
crond
|
||||
# 马上执行证书更新
|
||||
if [ ! -f /data/run.log ]; then
|
||||
touch /data/run.log
|
||||
fi
|
||||
if [ "${RUN_NOW:-"false"}" = "true" ]; then
|
||||
nohup /data/update.sh &> /data/run.log &
|
||||
fi
|
||||
|
||||
tail -f /data/run.log
|
75
docker/update.sh
Normal file
75
docker/update.sh
Normal file
@ -0,0 +1,75 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -u
|
||||
|
||||
if [ "${ACME_ENABLED:=true}" = "true" ]; then
|
||||
# 使用acme获取/更新证书
|
||||
${ACME_HOME}/acme.sh ${ACME_PARAMS:-} --force --issue --cert-home ${CERT_HOME} -d ${ACME_DOMAIN} -d *.${ACME_DOMAIN} --dns ${ACME_DNS_TYPE}
|
||||
fi
|
||||
|
||||
# 添加刷新url
|
||||
echo "${PUSH_URLS:-}" | tr ',' '\n' > ${WORK_DIR}/urls.txt
|
||||
if [ -n "${PUSH_URLS_PATH:-}" ]; then
|
||||
cat "${PUSH_URLS_PATH}" >> ${WORK_DIR}/urls.txt
|
||||
fi
|
||||
|
||||
# 写入腾讯云cdn更新配置
|
||||
cat>${WORK_DIR}/config.py<<-EOF
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
# author: 'zfb'
|
||||
# time: 2020-12-02 16:15
|
||||
|
||||
# 腾讯云支持使用单域名和泛域名的证书,例如
|
||||
# acme.sh --issue -d "whuzfb.cn" -d "*.whuzfb.cn" --dns dns_dp
|
||||
# acme.sh --issue -d "blog.whuzfb.cn" --dns dns_dp
|
||||
|
||||
# 使用ACME申请的SSL完整证书的本地存放路径
|
||||
CER_FILE = "${CERT_HOME}/${ACME_DOMAIN}/fullchain.cer"
|
||||
|
||||
# 使用ACME申请的SSL证书私钥的本地存放路径
|
||||
KEY_FILE = "${CERT_HOME}/${ACME_DOMAIN}/${ACME_DOMAIN}.key"
|
||||
|
||||
# CDN服务配置的域名(需要提前在腾讯云网页前端创建)
|
||||
# 如果ACME申请的证书为泛域名证书,且要配置多个CDN加速
|
||||
# CDN_DOMAIN = ["blog.whuzfb.cn", "blog2.whuzfb.cn", "web.whuzfb.cn"]
|
||||
CDN_DOMAIN = ["`echo ${CDN_DOMAIN} | sed -e 's/,/","/g'`"]
|
||||
|
||||
# 腾讯云:https://console.cloud.tencent.com/cam/capi
|
||||
SECRETID = "${SECRETID}"
|
||||
SECRETKEY = "${SECRETKEY}"
|
||||
|
||||
# 控制功能开关
|
||||
# 是否进行上传证书文件的操作(根据CER_FILE和KEY_FILE)
|
||||
UPLOAD_SSL = True
|
||||
# 以下为HTTPS额外功能
|
||||
# 是否开启HTTP2
|
||||
ENABLE_HTTP2 = True
|
||||
# 是否开启HSTS
|
||||
ENABLE_HSTS = True
|
||||
# 为HSTS设定最长过期时间(以秒为单位)
|
||||
HSTS_TIMEOUT_AGE = 31536000
|
||||
# HSTS包含子域名(仅对泛域名有效)
|
||||
HSTS_INCLUDE_SUBDOMAIN = True
|
||||
# 是否开启OCSP
|
||||
ENABLE_OCSP = True
|
||||
# 是否删除适用于CDN_DOMAIN域名下的其他所有证书
|
||||
# 满足以下条件:证书适用于CDN_DOMAIN、证书id不是本次使用的id
|
||||
DELETE_OLD_CERTS = True
|
||||
|
||||
# 是否进行为CDN_DOMAIN更换SSL证书的操作
|
||||
# 若UPDATE_SSL = True且UPLOAD_SSL = True,则CERT_ID可不设置,直接利用UPLOAD_SSL的证书
|
||||
UPDATE_SSL = True
|
||||
CERT_ID = ""
|
||||
# 是否进行预热URL的操作
|
||||
PUSH_URL = True
|
||||
# 是否进行刷新URL的操作
|
||||
PURGE_URL = True
|
||||
# 自定义的预热URL(默认会预热sitemap.xml的所有链接)文件路径
|
||||
# 该文件内,每行一个URL,例如
|
||||
# https://blog.whuzfb.cn/img/me2.jpg
|
||||
# https://blog.whuzfb.cn/img/home-bg.jpg
|
||||
URLS_FILE = "urls.txt"
|
||||
EOF
|
||||
# 更新CDN证书
|
||||
cd ${WORK_DIR} && python main.py
|
199
main.py
Normal file
199
main.py
Normal file
@ -0,0 +1,199 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
# author: 'zfb'
|
||||
# time: 2020-12-02 15:56
|
||||
from api import cdn, ecdn, ssl, tools
|
||||
import config
|
||||
|
||||
def run_config_ssl(id, key, cer_file, key_file):
|
||||
'''上传SSl证书到腾讯云SSL证书管理,返回新证书的id
|
||||
'''
|
||||
cert_info = {
|
||||
# Let's Encrypt 是通过中级 CA 机构颁发的证书,拿到的证书文件包含
|
||||
# whuzfb.cn.cer ca.cer
|
||||
# 需要人为地将服务器证书与中间证书拼接在一起(acme.sh已经进行拼接)
|
||||
# fullchain.cer
|
||||
"cer": tools.read_file(cer_file),
|
||||
"key": tools.read_file(key_file),
|
||||
"type": "CA"
|
||||
}
|
||||
ssl_client = ssl.get_ssl_client_instance(id, key)
|
||||
cert_list = ssl.get_cert_list(ssl_client)
|
||||
for cert in cert_list:
|
||||
# 获取每个证书的id
|
||||
cert_id = cert.CertificateId
|
||||
# 获取每个证书的信息
|
||||
# if domain == ssl.get_cert_info(ssl_client, cert_id).Domain:
|
||||
# # 删除证书
|
||||
# # delete_cert(client, cert_id)
|
||||
# # break
|
||||
# print(cert_id)
|
||||
# 上传证书,获取新证书的id
|
||||
id = ssl.upload_cert(ssl_client, cert_info)
|
||||
if len(id)>0:
|
||||
return id
|
||||
else:
|
||||
exit("获取新证书id失败")
|
||||
|
||||
|
||||
def run_config_cdn(id, key, domain, cert_id):
|
||||
'''该函数实现为CDN更新ssl证书的功能
|
||||
'''
|
||||
cdn_client = cdn.get_cdn_client_instance(id, key)
|
||||
cdns = cdn.get_cdn_detail_info(cdn_client)
|
||||
https = None
|
||||
for _cdn in cdns:
|
||||
if _cdn.Domain == domain:
|
||||
https = _cdn.Https
|
||||
break
|
||||
print(https)
|
||||
# generate_https(https)
|
||||
cdn.update_cdn_ssl(cdn_client, domain, cert_id)
|
||||
|
||||
def https_options_enabler(id, key, domain, http2, hsts, age, hsts_subdomain, ocsp):
|
||||
'''开启HTTPS配置中的部分选项
|
||||
'''
|
||||
cdn_client = cdn.get_cdn_client_instance(id, key)
|
||||
cdn.update_cdn_https_options(cdn_client, domain, http2, hsts, age, hsts_subdomain, ocsp)
|
||||
|
||||
def delete_old_ssls(id, key, cdn_domain, ignore_id):
|
||||
'''删除某个CDN的,除ignore_id以外的所有ssl证书
|
||||
'''
|
||||
ssl_client = ssl.get_ssl_client_instance(id, key)
|
||||
cert_list = ssl.get_cert_list(ssl_client)
|
||||
for cert in cert_list:
|
||||
cert_id = cert.CertificateId
|
||||
# 刚上传的这个证书不删除
|
||||
if cert_id == ignore_id:
|
||||
continue
|
||||
cert_info = ssl.get_cert_info(ssl_client, cert_id)
|
||||
cert_domain_and_alt_name = [cert_info.Domain] + cert_info.SubjectAltName
|
||||
matched = False
|
||||
# 判断域名匹配
|
||||
for cert_name in cert_domain_and_alt_name:
|
||||
if cert_name:
|
||||
# 判断主域名或多域名
|
||||
if cert_name == cdn_domain:
|
||||
matched = True
|
||||
break
|
||||
# 判断泛域名 m=['*','example.cn']
|
||||
m = cert_name.split('.', 1)
|
||||
n = cdn_domain.split('.', 1)
|
||||
if m[0] == "*" and m[1] == n[1]:
|
||||
matched = True
|
||||
break
|
||||
# 根据结果删除证书
|
||||
if matched:
|
||||
ssl.delete_cert(ssl_client, cert_id)
|
||||
|
||||
|
||||
def run_config_ecdn(id, key, domain, cert_id):
|
||||
'''全站加速网络:为指定域名的CDN更新SSL证书
|
||||
'''
|
||||
ecdn_client = ecdn.get_ecdn_client_instance(id, key)
|
||||
ecdn.get_ecdn_basic_info(ecdn_client)
|
||||
cdns = ecdn.get_ecdn_detail_info(ecdn_client)
|
||||
ecdn.update_ecdn_ssl(ecdn_client, domain, cert_id)
|
||||
|
||||
|
||||
def run_url_push(id, key, domain, urls_file):
|
||||
'''为CDN推送预热URL
|
||||
'''
|
||||
from time import sleep
|
||||
from os.path import isfile
|
||||
urls = []
|
||||
try:
|
||||
urls = tools.get_sitemap_urls("https://{}/sitemap.xml".format(domain))
|
||||
except Exception as e:
|
||||
print(repr(e))
|
||||
if isfile(urls_file):
|
||||
urls = urls + tools.get_urls_from_file(urls_file)
|
||||
cdn_client = cdn.get_cdn_client_instance(id, key)
|
||||
cdn_region = cdn.get_cdn_basic_info(cdn_client, domain)[0].Area
|
||||
# 预热URL支持area为global的参数,但是为了方便统计用量配额,手动分开刷新
|
||||
if cdn_region == 'global':
|
||||
cdn_region = ['mainland', 'overseas']
|
||||
else:
|
||||
cdn_region = [cdn_region]
|
||||
info = cdn.get_cdn_url_push_info(cdn_client)
|
||||
# 统计预热url数量
|
||||
cnt = 0
|
||||
# 根据加速域名配置的区域进行预热
|
||||
for i in info:
|
||||
if i.Area in cdn_region:
|
||||
grp_size = i.Batch
|
||||
available = i.Available
|
||||
print("正在对区域{0}进行url预热,剩余配额{1}条".format(i.Area, available))
|
||||
new_urls = tools.resize_url_list(urls, grp_size)
|
||||
for url_grp in new_urls:
|
||||
res = cdn.update_cdn_url_push(cdn_client, url_grp, i.Area)
|
||||
if res:
|
||||
cnt = cnt + len(url_grp)
|
||||
sleep(0.1)
|
||||
else:
|
||||
break
|
||||
print("成功预热{}个URL".format(cnt))
|
||||
|
||||
|
||||
def run_purge_url(id, key, domain, urls_file):
|
||||
'''为CDN推送刷新URL
|
||||
'''
|
||||
from time import sleep
|
||||
from os.path import isfile
|
||||
urls = []
|
||||
try:
|
||||
urls = tools.get_sitemap_urls("https://{}/sitemap.xml".format(domain))
|
||||
except Exception as e:
|
||||
print(repr(e))
|
||||
if isfile(urls_file):
|
||||
urls = urls + tools.get_urls_from_file(urls_file)
|
||||
cdn_client = cdn.get_cdn_client_instance(id, key)
|
||||
cdn_region = cdn.get_cdn_basic_info(cdn_client, domain)[0].Area
|
||||
# 刷新URL不支持area为global的参数
|
||||
if cdn_region == 'global':
|
||||
cdn_region = ['mainland', 'overseas']
|
||||
else:
|
||||
cdn_region = [cdn_region]
|
||||
info = cdn.get_cdn_purge_url_info(cdn_client)
|
||||
# 统计刷新url数量
|
||||
cnt = 0
|
||||
# 根据加速域名配置的区域进行刷新
|
||||
for i in info:
|
||||
if i.Area in cdn_region:
|
||||
grp_size = i.Batch
|
||||
available = i.Available
|
||||
print("正在对区域{0}进行url刷新,剩余配额{1}条".format(i.Area, available))
|
||||
new_urls = tools.resize_url_list(urls, grp_size)
|
||||
for url_grp in new_urls:
|
||||
res = cdn.update_cdn_purge_url(cdn_client, url_grp, i.Area)
|
||||
if res:
|
||||
cnt = cnt + len(url_grp)
|
||||
sleep(0.1)
|
||||
else:
|
||||
break
|
||||
print("成功刷新{}个URL".format(cnt))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
SECRETID = config.SECRETID
|
||||
SECRETKEY = config.SECRETKEY
|
||||
# 泛域名证书
|
||||
if config.UPLOAD_SSL:
|
||||
cert_id = run_config_ssl(SECRETID, SECRETKEY, config.CER_FILE, config.KEY_FILE)
|
||||
else:
|
||||
cert_id = config.CERT_ID
|
||||
for my_domain in config.CDN_DOMAIN:
|
||||
if config.UPDATE_SSL:
|
||||
run_config_cdn(SECRETID, SECRETKEY, my_domain, cert_id)
|
||||
if config.ENABLE_HSTS or config.ENABLE_OCSP or config.ENABLE_HTTP2:
|
||||
https_options_enabler(SECRETID, SECRETKEY, my_domain, config.ENABLE_HTTP2, config.ENABLE_HSTS,
|
||||
config.HSTS_TIMEOUT_AGE, config.HSTS_INCLUDE_SUBDOMAIN, config.ENABLE_OCSP)
|
||||
if config.DELETE_OLD_CERTS:
|
||||
delete_old_ssls(SECRETID, SECRETKEY, my_domain, cert_id)
|
||||
if config.PUSH_URL:
|
||||
run_url_push(SECRETID, SECRETKEY, my_domain, config.URLS_FILE)
|
||||
if config.PURGE_URL:
|
||||
run_purge_url(SECRETID, SECRETKEY, my_domain, config.URLS_FILE)
|
||||
# ecdn是全球加速服务,与CDN不同,本账号没有开通该功能
|
||||
# run_config_ecdn(SECRETID, SECRETKEY, my_domain, cert_id)
|
||||
|
2
requirements.txt
Normal file
2
requirements.txt
Normal file
@ -0,0 +1,2 @@
|
||||
tencentcloud-sdk-python==3.0.302
|
||||
requests==2.22.0
|
Loading…
Reference in New Issue
Block a user