Files
1d04b4eb0f feat(docs): redesign documentation experience (#715)
* feat(docs): redesign documentation experience

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* fix(docs): preserve Config Studio state

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* fix(docs): clarify credential diagnostics

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* fix(docs): address review feedback

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: NewFuture <6290356+NewFuture@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-08-10 09:37:20 +08:00

464 lines
15 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 开发指南:如何实现一个新的 DNS Provider
本指南介绍如何基于不同的抽象基类,快速实现一个自定义的 DNS 服务商适配类,支持动态 DNS 记录的创建与更新。
## 📦 目录结构
```text
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特性选择合适的基类:
### 1. SimpleProvider - 简单DNS服务商
适用于只提供简单更新接口,不支持查询现有记录的DNS服务商。
**必须实现的方法:**
| 方法 | 说明 | 是否必须 |
|------|------|----------|
| `set_record(domain, value, record_type="A", ttl=None, line=None, **extra)` | **更新或创建DNS记录** | ✅ 必须 |
| `_validate()` | **验证认证信息** | ❌ 可选(有默认实现) |
**适用场景:**
- 只提供更新接口的DNS服务商(如HE.net)
- 不需要查询现有记录的简单场景
- 调试和测试用途
- 回调(Webhook)类型的DNS更新
### 2. BaseProvider - 完整DNS服务商 ⭐️ 推荐
适用于提供完整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必填) |
**内置功能:**
- ✅ SimpleProvider的所有功能
- 🎯 自动记录管理(查询→创建/更新的完整流程)
- 💾 缓存机制
- 📝 详细的操作日志和错误处理
**适用场景:**
- 提供完整REST API的DNS服务商(如Cloudflare、阿里云DNS)
- 需要查询现有记录状态的场景
- 支持精确的记录管理和状态跟踪
## 🔧 实现示例
### SimpleProvider 示例
适用于简单DNS服务商,参考现有实现:
- [`provider/he.py`](/ddns/provider/he.py): Hurricane Electric DNS更新
- [`provider/debug.py`](/ddns/provider/debug.py): 调试用途,打印IP地址
- [`provider/callback.py`](/ddns/provider/callback.py): 回调/Webhook类型DNS更新
> provider/mysimpleprovider.py
```python
# 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.token or len(self.token) < 16:
raise ValueError("Invalid API token format")
def set_record(self, domain, value, record_type="A", ttl=None, line=None, **extra):
# type: (str, str, str, int | None, str | None) -> bool
"""更新或创建DNS记录 https://doc.simpledns.com/update"""
# logic to update DNS record
```
### BaseProvider 示例
适用于标准DNS服务商,参考现有实现:
- [`provider/dnspod.py`](/ddns/provider/dnspod.py): POST 表单数据,无签名
- [`provider/cloudflare.py`](/ddns/provider/cloudflare.py): RESTful JSON,无签名
- [`provider/alidns.py`](/ddns/provider/alidns.py): POST 表单+sha256参数签名
- [`provider/huaweidns.py`](/ddns/provider/huaweidns.py): RESTful JSON,参数header签名
> provider/myprovider.py
```python
# 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信息 ZoneId https://doc.exmaple.com/api/query_zone"""
res = self._request("ZoneInfo", key=value...)
...
self.logger.debug("domain not found for: %s", domain)
return None
def _query_record(self, zone_id, subdomain, main_domain, record_type, line, extra):
# type: (str, str, str, str, str | None, dict) -> dict | None
"""查询记录信息 https://doc.exmaple.com/api/list_records"""
res = self._request("DescribeRecords", ZoneId=zone_id, Key=value...)
...
self.logger.warning("No record found for: %s", res)
return None
def _create_record(self, zone_id, subdomain, main_domain, value, record_type, ttl, line, extra):
# type: (str, str, str, str, str, int, str | None, dict) -> bool
"""创建新record https://doc.exmaple.com/api/create_record"""
res = self._request("CreateRecord", ZoneId=zone_id, DomainName=domain, OriginInfo=origin, **extra)
...
self.logger.error("Failed to create record: %s", res)
return False
def _update_record(self, zone_id, old_record, value, record_type, ttl, line, extra):
# type: (str, Any, str, str, int, str | None, dict) -> bool
"""更新record https://doc.exmaple.com/api/update_record"""
res = self._request("ModifyRecord", ZoneId=zone_id, DomainName=domain, OriginInfo=origin)
...
self.logger.error("Failed to update record: %s", res)
return False
```
---
## ✅ 开发最佳实践
### 选择合适的基类
1. **SimpleProvider** - 功能不完整的DNS服务商
- ✅ DNS服务商只提供更新API
- ✅ 不需要查询现有记录
2. **BaseProvider** - 适合标准和复杂场景
- ✅ DNS服务商提供完整查询,创建,修改 API
- ✅ 需要精确的记录状态管理
- ✅ 支持复杂的域名解析逻辑
### 通用开发建议
#### 🌐 HTTP请求处理
```python
# 使用内置的_http方法,自动处理代理、编码、日志
response = self._http("POST", path, params=params, headers=headers)
```
#### 🔒 格式验证
```python
def _validate(self):
"""认证信息验证示例"""
super(MyProvider, self)._validate()
# 检查API密钥格式
if not self.token or len(self.token) < 16:
raise ValueError("API token must be at least 16 characters")
```
#### 📝 日志记录
```python
if result:
self.logger.info("DNS record got: %s", result.get("id"))
return True
else:
self.logger.warning("DNS record update returned false")
```
---
## 🧪 测试和调试
### 单元测试
每个Provider都应该有完整的单元测试。项目提供统一的测试基类和工具:
```python
# 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):
"""测试基本初始化"""
```
### 运行测试
```bash
# 运行所有测试
python -m unittest discover tests -v
# 运行特定Provider测试
python -m unittest tests.test_provider_myprovider -v
# 运行特定测试方法
python tests/test_provider_myprovider.py
```
---
## 📚 更多资源和最佳实践
### 🏗️ 项目结构建议
```text
ddns/
├── provider/
│ ├── _base.py # 基类定义
│ ├── myprovider.py # 你的Provider实现
│ └── __init__.py # 导入和注册
tests/
├── base_test.py # 共享测试基类
├── test_provider_myprovider.py # 你的Provider测试
└── README.md # 测试指南
```
### 📖 参考实现
**SimpleProvider 参考:**
- [`provider/he.py`](/ddns/provider/he.py) - Hurricane Electric (简单表单提交)
- [`provider/debug.py`](/ddns/provider/debug.py) - 调试工具 (仅打印信息)
- [`provider/callback.py`](/ddns/provider/callback.py) - 回调/Webhook模式
**BaseProvider 参考:**
- [`provider/cloudflare.py`](/ddns/provider/cloudflare.py) - RESTful JSON API
- [`provider/alidns.py`](/ddns/provider/alidns.py) - POST+签名认证
- [`provider/dnspod.py`](/ddns/provider/dnspod.py) - POST表单数据提交
---
## 🔐 云服务商认证签名算法
对于需要签名认证的云服务商(如阿里云、华为云、腾讯云等),DDNS 提供了通用的 HMAC-SHA256 签名认证函数。
### 签名认证工具函数
#### `hmac_sha256_authorization()` - 通用签名生成器
通用的云服务商API认证签名生成函数,支持阿里云、华为云、腾讯云等多种云服务商。
使用HMAC-SHA256算法生成符合各云服务商规范的Authorization头部。
所有云服务商的差异通过模板参数传递,实现完全的服务商无关性。
```python
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签名值
### 各云服务商签名实现示例
#### 阿里云 (ACS3-HMAC-SHA256)
```python
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={{SignedHeaders}},Signature={{Signature}}"
)
signing_template = "ACS3-HMAC-SHA256\n{timestamp}\n{{HashedCanonicalRequest}}"
# 生成签名
authorization = hmac_sha256_authorization(
secret_key=self.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)
```
#### 腾讯云 (TC3-HMAC-SHA256)
```python
def _request(self, action, **params):
# 腾讯云需要派生密钥
derived_key = self._derive_signing_key(date, service, self.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={{SignedHeaders}}, Signature={{Signature}}"
)
signing_template = "TC3-HMAC-SHA256\n{timestamp}\n{date}/{service}/tc3_request\n{{HashedCanonicalRequest}}"
# 生成签名
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哈希计算
```python
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签名对象
```python
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() # 十六进制字符串格式
```
---
### 🛠️ 开发工具推荐
- 本地开发环境:VSCode
- 在线代码编辑器:GitHub Codespaces 或 github.dev
### 🎯 常见问题解决
1. **Q: 为什么选择SimpleProvider而不是BaseProvider?**
- A: 如果DNS服务商只提供更新API,没有查询API,选择SimpleProvider更简单高效
---
## 🎉 总结
### 快速检查清单
- [ ] 选择了合适的基类(`SimpleProvider` vs `BaseProvider`)
- [ ] 实现了所有必需的方法(GPT或者Copilot辅助)
- [ ] 添加了适当的错误处理和日志记录
- [ ] 编写了完整的单元测试(使用GPT或Copilot生成)
- [ ] 测试了各种边界情况和错误场景
- [ ] 更新了相关文档
## Happy Coding! 🚀