本指南介绍如何基于不同的抽象基类,快速实现一个自定义的 DNS 服务商适配类,支持动态 DNS 记录的创建与更新。
ddns/
├── provider/
│ ├── _base.py # 抽象基类 SimpleProvider 和 BaseProvider,签名认证函数
│ └── myprovider.py # 你的新服务商实现
tests/
├── base_test.py # 共享测试工具和基类
├── test_provider_*.py # 各个Provider的单元测试文件
├── test_module_*.py # 其他测试
└── README.md # 测试指南
doc/dev/
└── provider.md # Provider开发指南 (本文档)
DDNS 提供两种抽象基类,根据DNS服务商的API特性选择合适的基类:
适用于只提供简单更新接口,不支持查询现有记录的DNS服务商。
必须实现的方法:
方法 | 说明 | 是否必须 |
---|---|---|
set_record(domain, value, record_type="A", ttl=None, line=None, **extra) |
更新或创建DNS记录 | ✅ 必须 |
_validate() |
验证认证信息 | ❌ 可选(有默认实现) |
适用场景:
适用于提供完整CRUD操作的标准DNS服务商API。
必须实现的方法:
方法 | 说明 | 是否必须 |
---|---|---|
_query_zone_id(domain) |
查询主域名的Id (zone_id) | ✅ 必须 |
_query_record(zone_id, subdomain, main_domain, record_type, line=None, extra=None) |
查询当前 DNS 记录 | ✅ 必须 |
_create_record(zone_id, subdomain, main_domain, value, record_type, ttl=None, line=None, extra=None) |
创建新记录 | ✅ 必须 |
_update_record(zone_id, old_record, value, record_type, ttl=None, line=None, extra=None) |
更新现有记录 | ✅ 必须 |
_validate() |
验证认证信息 | ❌ 可选(有默认id和token必填) |
内置功能:
适用场景:
适用于简单DNS服务商,参考现有实现:
provider/he.py
: Hurricane Electric DNS更新provider/debug.py
: 调试用途,打印IP地址provider/callback.py
: 回调/Webhook类型DNS更新provider/mysimpleprovider.py
# coding=utf-8
"""
自定义简单 DNS 服务商示例
@author: YourGithubUsername
"""
from ._base import SimpleProvider, TYPE_FORM
class MySimpleProvider(SimpleProvider):
"""
示例SimpleProvider实现
支持简单的DNS记录更新,适用于大多数简单DNS API
"""
API = 'https://api.simpledns.com'
content_type = TYPE_FORM # 或 TYPE_JSON
decode_response = False # 如果返回纯文本而非JSON,设为False
def _validate(self):
"""验证认证信息(可选重写)"""
super(MySimpleProvider, self)._validate()
# 添加特定的验证逻辑,如检查API密钥格式
if not self.auth_token or len(self.auth_token) < 16:
raise ValueError("Invalid API token format")
def set_record(self, domain, value, record_type="A", ttl=None, line=None, **extra):
"""更新DNS记录 - 必须实现"""
# logic to update DNS record
适用于标准DNS服务商,参考现有实现:
provider/dnspod.py
: POST 表单数据,无签名provider/cloudflare.py
: RESTful JSON,无签名provider/alidns.py
: POST 表单+sha256参数签名provider/huaweidns.py
: RESTful JSON,参数header签名provider/myprovider.py
# coding=utf-8
"""
自定义标准 DNS 服务商示例
@author: YourGithubUsername
"""
from ._base import BaseProvider, TYPE_JSON, hmac_sha256_authorization, sha256_hash
class MyProvider(BaseProvider):
"""
示例BaseProvider实现
适用于提供完整CRUD API的DNS服务商
"""
API = 'https://api.exampledns.com'
content_type = TYPE_JSON # 或 TYPE_FORM
def _query_zone_id(self, domain):
# type: (str) -> str | None
"""查询主域名的Zone ID"""
# 精确查找 或者 list匹配
def _query_record(self, zone_id, subdomain, main_domain, record_type, line=None, extra=None):
# type: (str, str, str, str, str | None, dict | None) -> Any
"""查询现有DNS记录"""
def _create_record(self, zone_id, subdomain, main_domain, value, record_type, ttl=None, line=None, extra=None):
# type: (str, str, str, str, str, int | str | None, str | None, dict | None) -> bool
"""创建新的DNS记录"""
def _update_record(self, zone_id, old_record, value, record_type, ttl=None, line=None, extra=None):
# type: (str, dict, str, str, int | str | None, str | None, dict | None) -> bool
"""更新现有DNS记录"""
def _request(self, action, **params):
# type: (str, **(str | int | bytes | bool | None)) -> dict
"""[推荐]封装通用请求逻辑,处理认证和公共参数"""
# 构建请求参数
request_params = {
"Action": action,
"Version": "2023-01-01",
"AccessKeyId": self.auth_id,
**{k: v for k, v in params.items() if v is not None}
}
res = self._http("POST", "/", params=request_params, headers=headers)
return res.get("data", {})
# 使用内置的_http方法,自动处理代理、编码、日志
response = self._http("POST", path, params=params, headers=headers)
def _validate(self):
"""认证信息验证示例"""
super(MyProvider, self)._validate()
# 检查API密钥格式
if not self.auth_token or len(self.auth_token) < 16:
raise ValueError("API token must be at least 16 characters")
if result:
self.logger.info("DNS record got: %s", result.get("id"))
return True
else:
self.logger.warning("DNS record update returned false")
每个Provider都应该有完整的单元测试。项目提供统一的测试基类和工具:
# tests/test_provider_myprovider.py
from base_test import BaseProviderTestCase, unittest, patch, MagicMock
from ddns.provider.myprovider import MyProvider
class TestMyProvider(BaseProviderTestCase):
def setUp(self):
super(TestMyProvider, self).setUp()
# Provider特定的setup
def test_init_with_basic_config(self):
"""测试基本初始化"""
# 运行所有测试
python -m unittest discover tests -v
# 运行特定Provider测试
python -m unittest tests.test_provider_myprovider -v
# 运行特定测试方法
python tests/test_provider_myprovider.py
ddns/
├── provider/
│ ├── _base.py # 基类定义
│ ├── myprovider.py # 你的Provider实现
│ └── __init__.py # 导入和注册
tests/
├── base_test.py # 共享测试基类
├── test_provider_myprovider.py # 你的Provider测试
└── README.md # 测试指南
SimpleProvider 参考:
provider/he.py
- Hurricane Electric (简单表单提交)provider/debug.py
- 调试工具 (仅打印信息)provider/callback.py
- 回调/Webhook模式BaseProvider 参考:
provider/cloudflare.py
- RESTful JSON APIprovider/alidns.py
- POST+签名认证provider/dnspod.py
- POST表单数据提交对于需要签名认证的云服务商(如阿里云、华为云、腾讯云等),DDNS 提供了通用的 HMAC-SHA256 签名认证函数。
hmac_sha256_authorization()
- 通用签名生成器通用的云服务商API认证签名生成函数,支持阿里云、华为云、腾讯云等多种云服务商。 使用HMAC-SHA256算法生成符合各云服务商规范的Authorization头部。 所有云服务商的差异通过模板参数传递,实现完全的服务商无关性。
from ddns.provider._base import hmac_sha256_authorization, sha256_hash
# 通用签名函数调用示例
authorization = hmac_sha256_authorization(
secret_key=secret_key, # 签名密钥(已派生处理)
method="POST", # HTTP方法
path="/v1/domains/records", # API路径
query="limit=20&offset=0", # 查询字符串
headers=request_headers, # 请求头部字典
body_hash=sha256_hash(request_body), # 请求体哈希
signing_string_format=signing_template, # 待签名字符串模板
authorization_format=auth_template # Authorization头部模板
)
函数参数说明:
参数 | 类型 | 说明 |
---|---|---|
secret_key |
str \| bytes |
签名密钥,已经过密钥派生处理 |
method |
str |
HTTP请求方法 (GET, POST, etc.) |
path |
str |
API请求路径 |
query |
str |
URL查询字符串 |
headers |
dict[str, str] |
HTTP请求头部 |
body_hash |
str |
请求体的SHA256哈希值 |
signing_string_format |
str |
待签名字符串模板,包含 {HashedCanonicalRequest} 占位符 |
authorization_format |
str |
Authorization头部模板,包含 {SignedHeaders} , {Signature} 占位符 |
模板变量:
{HashedCanonicalRequest}
- 规范请求的SHA256哈希值{SignedHeaders}
- 按字母顺序排列的签名头部列表{Signature}
- 最终的HMAC-SHA256签名值def _request(self, action, **params):
# 构建请求头部
headers = {
"host": "alidns.aliyuncs.com",
"x-acs-action": action,
"x-acs-content-sha256": sha256_hash(body),
"x-acs-date": timestamp,
"x-acs-signature-nonce": nonce,
"x-acs-version": "2015-01-09"
}
# 阿里云签名模板
auth_template = (
"ACS3-HMAC-SHA256 Credential={access_key},"
"SignedHeaders=,Signature="
)
signing_template = "ACS3-HMAC-SHA256\n{timestamp}\n"
# 生成签名
authorization = hmac_sha256_authorization(
secret_key=self.auth_token,
method="POST",
path="/",
query=query_string,
headers=headers,
body_hash=sha256_hash(body),
signing_string_format=signing_template,
authorization_format=auth_template
)
headers["authorization"] = authorization
return self._http("POST", "/", body=body, headers=headers)
def _request(self, action, **params):
# 腾讯云需要派生密钥
derived_key = self._derive_signing_key(date, service, self.auth_token)
# 构建请求头部
headers = {
"content-type": "application/json",
"host": "dnspod.tencentcloudapi.com",
"x-tc-action": action,
"x-tc-timestamp": timestamp,
"x-tc-version": "2021-03-23"
}
# 腾讯云签名模板
auth_template = (
"TC3-HMAC-SHA256 Credential={secret_id}/{date}/{service}/tc3_request, "
"SignedHeaders=, Signature="
)
signing_template = "TC3-HMAC-SHA256\n{timestamp}\n{date}/{service}/tc3_request\n"
# 生成签名
authorization = hmac_sha256_authorization(
secret_key=derived_key, # 注意:使用派生密钥
method="POST",
path="/",
query="",
headers=headers,
body_hash=sha256_hash(body),
signing_string_format=signing_template,
authorization_format=auth_template
)
headers["authorization"] = authorization
return self._http("POST", "/", body=body, headers=headers)
sha256_hash()
- SHA256哈希计算from ddns.provider._base import sha256_hash
# 计算字符串的SHA256哈希
hash_value = sha256_hash("request body content")
# 计算字节数据的SHA256哈希
hash_value = sha256_hash(b"binary data")
hmac_sha256()
- HMAC-SHA256签名对象from ddns.provider._base import hmac_sha256
# 生成HMAC-SHA256字节签名
# 获取 HMAC 对象,可调用 .digest() 获取字节或 .hexdigest() 获取十六进制字符串
hmac_obj = hmac_sha256("secret_key", "message_to_sign")
signature_bytes = hmac_obj.digest() # 字节格式
signature_hex = hmac_obj.hexdigest() # 十六进制字符串格式
SimpleProvider
vs BaseProvider
)