需求
阿里云提供了开放的接口修改解析,只要使用了阿里DNS,无论付费免费都可以使用脚本修改
而对于动态IP地址,这个接口太有用了,只需要一个程序...等等,好AliDDNS程序应该是
- 可以集成在某个软件的
- 小巧,便于在嵌入式中使用
- 轻量无依赖,不要运行的使用
openssl: Command not found
综上,我结合了Nginx-NJS
开发了njs-aliddns,方便、简洁、无惧空间小
为什么njs都搭上ddns了
我观察aliddns很久了,发现网上的项目大多是这几个语言写的
- sh(需要
openssl
tr
等光猫缺少的命令) - php(PHP运行环境太大)
- javascript(node 10.x,node体积太大)
- c# (10m,基于官方SDK)
- go (10m,基于官方SDK)
本来momentPHP用得好好的,换成Nginx+vList5后找不到替代项目了,一咬牙自己写!
思路
这个折磨了我好久,快1个月了吧,因为js_peroidic
太坑了
- 报错不写入日志,还以为是逻辑错误看了好久
- 需要写入
location
块,我本来很不理解 - 难以调试,一个typeo(拼写错误)都可以检查一天
- 阿里官方的SDK存在错误,就是
encodeURL()
不完整
期间多次翻阅官方文档,写到我都开始怀疑自己了才成功。客官,点个星呗
我的项目参考了 yyqian/aliyun-ddns,但是无法套用,因为我使用了WebAPI
CryptoSubtle
(天哪,网上怎么没有使用示例和解说?用CryptoJS
着迷了吧)ngx.fetch
(喂官方,为什么不叫作globalThis.fetch
呢,差点还以为nginx连fetch都没有)
还有,文档在 这里 ,这个项目的链接已经失效了。是一个PDF
接口封装
由于我有 写AliDDNS的经验,因此我迅速抽象了一个接口
interface DescribeDomainRecordsResult{
Status: string | 'Enable',
Type: 'A' | 'AAAA' | 'CNAME' | 'MX',
Remark: string,
TTL: number,
RecordId: number,
Priority: number,
RR: string,
DomainName: string,
Weight: number,
Value: string,
Line: 'default' | string,
Locked: boolean,
CreateTimestamp: number,
UpdateTimestamp: number
}
编码请求
使用请求参数构造规范化的请求字符串(Canonicalized Query String)
太简单啦,上代码
let tmp = '', query = '';
const keys = Object.keys(param).sort();
for (let i = 0 ; i < keys.length ; i ++)
tmp += '&' + encode(keys[i]) + '=' + encode(param[keys[i]]),
query +='&' + keys[i] + '=' + param[keys[i]] ;
对每个请求参数的名称和值进行编码。名称和值要使用UTF-8字符集进行URL编码
,URL编码的编码规则是:
- 对于字符 A-Z、a-z、0-9以及字符"-"、"_"、"."、"~"不编码;
- 对于其他字符编码成"%XY"的格式,其中XY是字符对应ASCII码的16进制表示。比
如英文的双引号(")对应的编码就是%22- 对于扩展的UTF-8字符,编码成"%XY%ZA…"的格式;
需要说明的是英文空格( )要被编码是%20,而不是加号(+)。
这里我使用了自己写的转换函数,在任意JS运行环境都可以使用
const encode = (data: string) => (typeof data == 'string' ? data : new String(data)).replace(
/[^a-zA-Z0-9-_.~]/g,
(data) => '%' + data.charCodeAt(0).toString(16).padStart(2, '0').toUpperCase()
);
按照RFC2104的定义,使用上面的用于签名的字符串计算签名HMAC值。
注意:计算签名时使用的Key就是用户持有的Access Key Secret并加上一个"&"字符
(ASCII:38),使用的哈希算法是SHA1。
这里使用了CryptoSubtle,就是crypto.subtle.*
。使用很简单
const key = await crypto.subtle.importKey('raw',
config.accessSec + '&',
{
"name": "HMAC",
"hash": "SHA-1"
},
true,
['sign']
),
_key = await crypto.subtle.sign("HMAC", key, 'GET&%2F&' + encode(tmp.substring(1)));
按照Base64编码规则把上面的HMAC值编码成字符串,即得到签名值(Signature)。
const result = Buffer.from(_key).toString('base64');
注意:这里不能使用atob
,会报错非ASCII字符。
本来都打算使用 js-base64 了,结果扒出了核心代码,我都看笑了
https://github.com/dankogai/js-base64/blob/main/base64.ts#L63
接下来请求!简单吗?
await ngx.fetch(
'http://alidns.aliyuncs.com/?' +
'Signature=' + encodeURIComponent(sig) + query
)
注意 千万千万别忘记了encodeURIComponent()
,否则会报错
于是,代码封装如下
/**
* 向AliAPI发送请求
* @param param 请求对象
* @param config 配置
* @returns 返回的数据
*/
async function request(param: Record<string,any>, config: Config) {
// 合并参数
const DEFAULT = {
'AccessKeyId': config.accessKey,
'Format': 'JSON',
'SignatureMethod': 'HMAC-SHA1',
'SignatureNonce': Math.floor(Math.random() * 0xffffffff) .toString(36),
'SignatureVersion': '1.0',
'Timestamp': new Date().toISOString().replace(/\..+$/, 'Z'),
'Version': '2015-01-09'
} as Record<string,any>;
for (const key in DEFAULT) {
if(key in param) continue;
param[key] = DEFAULT[key];
}
// 编码
let tmp = '', query = '';
const keys = Object.keys(param).sort();
for (let i = 0 ; i < keys.length ; i ++)
tmp += '&' + encode(keys[i]) + '=' + encode(param[keys[i]]),
query +='&' + keys[i] + '=' + param[keys[i]] ;
const key = await crypto.subtle.importKey('raw',
config.accessSec + '&',
{
"name": "HMAC",
"hash": "SHA-1"
},
true,
['sign']
),
_key = await crypto.subtle.sign("HMAC", key, 'GET&%2F&' + encode(tmp.substring(1))),
sig = Buffer.from(_key).toString('base64');
// 请求
const res = await ngx.fetch(
'http://alidns.aliyuncs.com/?' +
'Signature=' + encodeURIComponent(sig) + query
), res_json = await res.json() as Record<string,any>;
if(!res.ok || 'Message' in res_json)
throw ngx.log(ngx.ERR, 'AliDDNS RequestError: E_' + res_json['Code'] + ': ' + res_json['Message'] + '\n URL:' + 'http://alidns.aliyuncs.com/?' +
'Signature=' + sig + query) ;
return res_json;
}
核心代码
重要的写完了,接下来就是原理了
找到缓存,不存在就不耻下问
const dat = ngx.shared[shared].get('ddns') as string; if(dat) var record = JSON.parse(dat) as DescribeDomainRecordsResult; else{ var record = await getRecord(config); ngx.shared[shared].set('ddns', JSON.stringify(record)); ngx.log(ngx.INFO, "GET Record succeed"); }
请求IP API,获取地址
const ipaddr = await ngx.fetch(config.ipapi); if (!ipaddr.ok) return ngx.log(ngx.ERR, `AliDDNS Fatal: IPApi(${config.ipapi}) returns with ${ipaddr.status}`); const addr = await ipaddr.text();
- 判断IP是否不同
是的话就更新
await request({ 'Action': 'UpdateDomainRecord', 'RR': config.dprefix, 'RecordId': record.RecordId, 'TTL': record.TTL, 'Type': record.Type, 'Value': addr, 'Version': '2015-01-09' }, config); ngx.log(ngx.INFO, `AliDDNS: ${config.dprefix}.${config.domain} Redirected to ${addr}`);
程序返回,更新缓存
record['UpdateTimestamp'] = Date.now(); record['Value'] = addr; shared && shared in ngx.shared && ngx.shared[shared].set('ddns', JSON.stringify(record));
- API则是使用缓存格式化输出
部署指南
写了这么多,小白:啊吧啊吧,我:好吧,当我没说,直接去看看开源
首先,下载我们 编译好的版本
然后打开nginx.conf
,随机找一个幸运server
块,添加这些代码
js_import ddns.js;
location @ddns{
js_var $shared_zone [你的shareZone名称];
js_var $ddns_key [账号的accessKey];
js_var $ddns_sec [账号的accessKeySecret];
js_var $ddns_domain [你购买的域名名称];
js_var $ddns_prefix [你想要修改的域名前缀,不要前缀就填写@];
js_var $ddns_type [解析类型, IPV4填"A", IPV6填"AAAA"];
js_var $ddns_ipapi [获取IP地址的接口,建议IPV4填"https://4.ipw.cn",IPV6填"https://6.ipw.cn"];
js_periodic ddns.main interval=300s;
}
如果你不知道何为shareZone,请和我一起操作
在http{}
块下添加这些
js_shared_dict_zone zone=njs:1m type=string;
这样,你的shareZone就叫做njs
如果你想要像我一样 【可视化】,那么与我一起在你想要的location{}
块中填写
js_var $shared_zone [你的shareZone名称];
js_content ddns.status;
bingo! 重启Nginx,看看日志里有没有你喜欢的内容
2024/07/28 11:22:25 [info] 4285#0: js: GET Record succeed
2024/07/28 11:22:25 [info] 4285#0: js: AliDDNS: cloud.imzlh.top Redirected to 2409:8a28:6...
结束!Enjoy!
本文由 zlh 创作,采用 知识共享署名4.0 国际许可协议进行许可。
本站文章除注明转载/出处外,均为本站原创或翻译,转载前请务必署名。