commit 105333dd6fa577400cc1ce5e193eeee3fa1c7a5f Author: yexinhao Date: Fri Feb 3 20:41:28 2023 +0800 feat: 给update.sh里的 dns 配上环境变量 diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml new file mode 100644 index 0000000..ec4510c --- /dev/null +++ b/.github/workflows/docker-publish.yml @@ -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 / + 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 }} diff --git a/.github/workflows/update-cert.yml b/.github/workflows/update-cert.yml new file mode 100644 index 0000000..7966717 --- /dev/null +++ b/.github/workflows/update-cert.yml @@ -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 }} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b6e4761 --- /dev/null +++ b/.gitignore @@ -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/ diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..b58b603 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,5 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..8d66637 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/jsLibraryMappings.xml b/.idea/jsLibraryMappings.xml new file mode 100644 index 0000000..d23208f --- /dev/null +++ b/.idea/jsLibraryMappings.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..28a804d --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..d239160 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/qcloud-ssl-cdn.iml b/.idea/qcloud-ssl-cdn.iml new file mode 100644 index 0000000..0c8867d --- /dev/null +++ b/.idea/qcloud-ssl-cdn.iml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..a892cbf --- /dev/null +++ b/Dockerfile @@ -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" ] \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE @@ -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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..c99346d --- /dev/null +++ b/README.md @@ -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证书 diff --git a/api/__init__.py b/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/api/cdn.py b/api/cdn.py new file mode 100644 index 0000000..1258da5 --- /dev/null +++ b/api/cdn.py @@ -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)) diff --git a/api/ecdn.py b/api/ecdn.py new file mode 100644 index 0000000..38653b6 --- /dev/null +++ b/api/ecdn.py @@ -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)) \ No newline at end of file diff --git a/api/get_client_profile.py b/api/get_client_profile.py new file mode 100644 index 0000000..9a5a333 --- /dev/null +++ b/api/get_client_profile.py @@ -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) \ No newline at end of file diff --git a/api/ssl.py b/api/ssl.py new file mode 100644 index 0000000..b0b23f9 --- /dev/null +++ b/api/ssl.py @@ -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 "" diff --git a/api/tools.py b/api/tools.py new file mode 100644 index 0000000..ef7868e --- /dev/null +++ b/api/tools.py @@ -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'(.*?)') + 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 diff --git a/config.example.py b/config.example.py new file mode 100644 index 0000000..abcb145 --- /dev/null +++ b/config.example.py @@ -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" diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh new file mode 100644 index 0000000..332e0f0 --- /dev/null +++ b/docker/entrypoint.sh @@ -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 \ No newline at end of file diff --git a/docker/update.sh b/docker/update.sh new file mode 100644 index 0000000..fd71f08 --- /dev/null +++ b/docker/update.sh @@ -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 diff --git a/main.py b/main.py new file mode 100644 index 0000000..4be6ae0 --- /dev/null +++ b/main.py @@ -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) + diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..a581319 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +tencentcloud-sdk-python==3.0.302 +requests==2.22.0 \ No newline at end of file diff --git a/urls.txt b/urls.txt new file mode 100644 index 0000000..4db6d7d --- /dev/null +++ b/urls.txt @@ -0,0 +1,2 @@ +https://blog.whuzfb.cn/img/me2.jpg +https://blog.whuzfb.cn/img/home-bg.jpg \ No newline at end of file