光猫折腾/njs妙用: 基于阿里DNS的动态解析DDNS

in 前后端开发 with 0 comment

AliDDNS! but njs

需求

阿里云提供了开放的接口修改解析,只要使用了阿里DNS,无论付费免费都可以使用脚本修改
而对于动态IP地址,这个接口太有用了,只需要一个程序...等等,好AliDDNS程序应该是

综上,我结合了Nginx-NJS开发了njs-aliddns,方便、简洁、无惧空间小

为什么njs都搭上ddns了

我观察aliddns很久了,发现网上的项目大多是这几个语言写的

本来momentPHP用得好好的,换成Nginx+vList5后找不到替代项目了,一咬牙自己写!

思路

这个折磨了我好久,快1个月了吧,因为js_peroidic太坑了

期间多次翻阅官方文档,写到我都开始怀疑自己了才成功。客官,点个星呗
我的项目参考了 yyqian/aliyun-ddns,但是无法套用,因为我使用了WebAPI

还有,文档在 这里 ,这个项目的链接已经失效了。是一个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;
}

核心代码

重要的写完了,接下来就是原理了

部署指南

写了这么多,小白:啊吧啊吧,我:好吧,当我没说,直接去看看开源
首先,下载我们 编译好的版本
然后打开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!

Responses