Yak基础插件案例——CDN检测

作者:@奶权师傅

关于CDN

介绍

要谈CDN,就得先从CDN以及CDN的配置先说起。

内容分发网络(英语:Content Delivery Network或Content Distribution Network,缩写:CDN)是指一种透过互联网互相连接的电脑网络系统,利用最靠近每位用户的服务器,更快、更可靠地将音乐、图片、视频、应用程序及其他文件发送给用户,来提供高性能、可扩展性及低成本的网络内容传递给用户。——来自维基百科

优点

CDN的总承载量取决于其网络节点的数量决定,通常会比单一骨干最大的带宽还要大。这使得CDN可以承载的用户数量比起传统的单一服务器多。假设现在把一台有100Gbps处理能力的服务器放在只有10Gbps带宽的数据中心,那么这时候带宽就成为了架构上了瓶颈。但是如果放到十个有10Gbps的地点,整个系统的处理能力就可以达到10*10Gbps。同时,将服务器放到不同地点还有其他好处,例如:异地备援、隐藏真实IP等。

配置

配置CDN一般有两种方式:

第一种

CDN厂商提供一个域名,给自己需要接入CDN的域名添加一个cname,指向CDN厂商提供的域名。这种域名一般会有个随机前缀。

第二种

把需要接入CDN的域名的NS记录指向CDN厂商的DNS服务器IP。

检测CDN

知道了配置原理后,我们可以很容易得到几种检测的思路。

CNAME指纹

配置中说的第一种方法是去配置一个CNAME记录为CDN厂商提供的域名,这个域名通常会有一个前缀,后缀一般都是固定的。找了两个加载js库的CDN加速服务,看一下他们的CNAME记录。

可以看到不同的CDN厂商的域名一般会有个共同点,就是带有cdn/dns等字眼(不一定),这就相当于CDN厂商的一个指纹。我们只需要维护一个常见CDN厂商的CNAME指纹字典,可以去里面查目标的CNAME记录是否为某个CDN厂商的域名。但是这种方式的缺点十分明显,就是维护指纹的成本很高,如果某个CDN厂商加了新的域名,那就需要重新添加指纹了。

IP段

我们都知道CDN厂商一般会有很多个节点,而这些节点一般是是在一些IP段里面。所以我们可以维护一个常见CDN厂商的IP段列表。

https://github.com/al0ne/Vxscan/blob/master/lib/iscdn.py

如果目标域名的A记录在IP段列表中,那么我们可以暂时认为其接入了CDN。但是这样做也是明显有跟上面一样的缺点的,CDN厂商添加节点的行为会使得我们的脚本出现误报/漏报。

ASN号

ASN介绍

自治系统或自治域(英文:Autonomous system, AS)是指在互联网中,一个或多个实体管辖下的所有IP网络和路由器的组合,它们对互联网执行共同的路由策略。——来自维基百科

我们可以整理出常见CDN厂商的ASN号列表,如果目标域名A记录IP的ASN号在列表中,那么我们也可以暂时认为其接入了CDN。

但是还是有明显的误报/漏报问题,因为谁也不知道会不会突然冒出一个新的CDN厂商被我们遇到,或者某些家大业大的大厂自己实现了CDN的接入,没有使用CDN厂商的服务。

多地区ping

上面介绍的三种方式的优点就是速度快,需要检测的域名非常多时优势很大。基本就是DNS查一下然后进行各种类型指纹的匹配就行,但缺点就是误报/漏报率高。想要降低误报/漏报率可以将以上的方式做一下结合,例如精灵师傅的OneForAll子域名工具就结合了上面提到的方式。但是再怎么样也只是能将误报/漏报率降到最低,达不到零误报/漏报。

前面介绍到CDN会根据地区返回不同的IP。那么我们可以准备很多个地区的服务器,同时对该域名执行ping操作。看看返回的IP是否相同,达到判断是否接入CDN的目的。

但是这样成本非常高,好在网上有现成的服务可以使用。例如:站长之家多地区ping

但是在官网使用该服务一次只会请求一部分监测点进行ping操作,效率不高。所以我打算使用yaklang利用其探测点,重新写一个并发的版本。

插件编写

其实这个插件本质上是一个“爬虫”,我们需要去分析一下站长之家的请求流程。

大概流程如下:首先会发送一个POST请求到https://ping.chinaz.com/,获取监测点的guid,以及enkey、checkType等参数,作为后续请求的参数。

接着带着上一步获取到的参数发送一个POST请求到https://ping.chinaz.com/iframe.ashx?t=ping

如果成功则返回格式如下

 {state:1,msg:'',result:{ip:'36.152.44.96',ipaddress:'中国江苏南京 移动',responsetime:'13毫秒',ttl:'51',bytes:'32'}}  

反之则返回格式如下

{state:0,msg:''}

将其中需要的信息提取出来就行。

编写获取初始化参数的函数

经过测试,其实后续的回调接口只需要获取enkey与checkType以及监测点的guid,所以我们发送对应的POST请求后使用正则表达式提取出需要的参数即可。(在写完插件的第二天V1师傅加了个Xpath库,可以更容易提取出参数了,所以这里最优解是用Xpath库。)

// 字典转url query格式,带urlencode  
dict2UrlQueryWithUrlEncode := func(d) {  
    s := make([]string)  
    for k, v := range d {  
        s = append(s, sprintf("%v=%v", k, codec.EscapeQueryUrl(v)))  
    }  
    return str.Join(s, "&")  
}  
  
// 获取初始化配置  
getInitConfig := func() {  
    datas := dict2UrlQueryWithUrlEncode({  
        "host": "Example Domain",  
        "linetype": "电信,多线,联通,移动,其他",  
    })  
    // 请求接口获取配置  
    res, err := http.Request("POST", "多个地点Ping服务器,网站测速 - 站长工具", http.body(datas), http.header("Content-Type", "application/x-www-form-urlencoded"))  
    die(err)  
    resBody := string(http.GetAllBody(res))  
  
    // <div id="0e519c9d-dab8-480c-a372-c72480dd133a" class="row listw tc clearfix" linetype="1" state="0" trycount="0">  
            // <div class="col-2" name="city" serveruroup="0" data-company="[网锐]微端BGP200M/1200/月,本周特价 - 湖南网锐网络">江苏宿迁[电信]</div>  
    // 获取监测点UUID  
    r1, _ := re.Compile(`<div id="([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})" class="row listw tc clearfix"[\s\S]+?<div class="col-2" name="city"[\s\S]+?>(\S+)</div>`)  
    result := r1.FindAllStringSubmatch(resBody, -1)  
    pingServerInfoList := make([]map[string]string)  
    for _, i := range result {  
        pingServerInfoList = append(pingServerInfoList, {  
            "name": i[2],  
            "guid": i[1],  
        })  
    }  
  
    // <input type="hidden" id="enkey" value="OT5JUx9bX5INGvYSBT087i8pZeO7y9et" />  
    // 获取enkey参数  
    r2, _ := re.Compile(`<input type="hidden" id="enkey" value="(\S+)" />`)  
    enkey := r2.FindStringSubmatch(resBody)  
    if len(enkey) == 0 {  
        die("get enkey failure")  
    }  
    enkey := enkey[1]  
  
    // <input type="hidden" id="checktype" value="0" />  
    // 获取checkType参数  
    r3, _ := re.Compile(`<input type="hidden" id="checktype" value="(\d+?)" />`)  
    checkType := r3.FindStringSubmatch(resBody)  
    if len(checkType) == 0 {  
        die("get checkType failure")  
    }  
    checkType := checkType[1]  
  
    return pingServerInfoList, enkey, checkType  
}  

编写执行监测点ping操作函数

这里需要注意的点是,目标返回的是一个JSONP格式的数据,往JSONP回调函数里面丢的是一个js的对象,不是一个标准的JSON格式,所以需要对其进行一些字符串操作,使其能被转为一个map[string]var格式的数据。且因为我们需要启动goroutine进行并发操作,所以我是使用了一个Channel来在多个goroutine中安全地操作数据。

// 字典转url query格式,带urlencode  
dict2UrlQueryWithUrlEncode := func(d) {  
    s := make([]string)  
    for k, v := range d {  
        s = append(s, sprintf("%v=%v", k, codec.EscapeQueryUrl(v)))  
    }  
    return str.Join(s, "&")  
}  
// 转为合法json,并反序列化  
convertLegalJson := func(s) {  
    // 去掉括号  
    s = str.ReplaceAll(s, "({", "{")  
    s = str.ReplaceAll(s, "})", "}")  
      
    // 加引号,改单引号  
    s = str.ReplaceAll(s, `state:`, `"state":`)  
    s = str.ReplaceAll(s, `msg:`, `"msg":`)  
    s = str.ReplaceAll(s, `result:`, `"result":`)  
    s = str.ReplaceAll(s, `ip:`, `"ip":`)  
    s = str.ReplaceAll(s, `ipaddress:`, `"ipaddress":`)  
    s = str.ReplaceAll(s, `responsetime:`, `"responsetime":`)  
    s = str.ReplaceAll(s, `ttl:`, `"ttl":`)  
    s = str.ReplaceAll(s, `bytes:`, `"bytes":`)  
    s = str.ReplaceAll(s, `'`, `"`)  
  
    // 反序列化  
    d, err := json.New(s)  
    die(err)  
    return d.Value()  
}  
// 让监测点开始ping操作  
ping := func(serverInfo, enkey, checkType, target, results) {  
    datas := dict2UrlQueryWithUrlEncode({  
        "guid": serverInfo["guid"],  
        "host": target,  
        "ishost": "0",  
        "isipv6": "0",  
        "encode": enkey,  
        "checktype": checkType,  
    })  
    res, err := http.Request("POST", "多个地点ping[iframe.ashx]服务器-网站测速-站长工具", http.body(datas), http.header("Content-Type", "application/x-www-form-urlencoded"), http.timeout(20))  
    // 请求失败也返回ping失败的结果  
    if err != nil {  
        results <- {"state": 0, "msg": ""}  
        return  
    }  
    resBody := string(http.GetAllBody(res))  
    // 失败 ({state:0,msg:''})  
    // 成功 ({state:1,msg:'',result:{ip:'110.242.68.4',ipaddress:'中国河北保定顺平县 联通',responsetime:'20毫秒',ttl:'52',bytes:'32'}})  
  
    // 处理结果  
    pingInfo := convertLegalJson(resBody)  
    pingInfo["cityname"] = serverInfo["name"]  
    if pingInfo["state"] == float64(1) {  
        if str.Contains(pingInfo["result"]["responsetime"], "超时") {  
            pingInfo["result"]["responsetime"] = "超时"  
        }  
        if str.Contains(pingInfo["result"]["ttl"], "超时") {  
            pingInfo["result"]["ttl"] = "超时"  
        }  
        if pingInfo["result"]["bytes"] == "" {  
            pingInfo["result"]["bytes"] = "-"  
        }  
        pingInfo["result"]["ipaddress"] = str.Join(str.Fields(pingInfo["result"]["ipaddress"]), " ")  
    }  
    results <- pingInfo  
}  
 

编写监测逻辑与图像化输出

第一步

初始化与yakit的连接,并解析外部参数(即需要检测的域名)。因为使用了str.ParseStringToHosts(),所以target参数可以使用,进行分割以支持多个目标。

yakit.AutoInitYakit()  
  
// 解析参数  
targets := cli.String("target", cli.setRequired(true))  
targetList := str.ParseStringToHosts(targets)  

第二步

调用func getInitConfig()拿到所有需要的参数

pingServerInfoList, enkey, checkType := getInitConfig()  
serverNum := len(pingServerInfoList)  
printf("初始化成功,共获取到%v个监测点\n", serverNum)  

第三步

遍历targetList拿到每一个target,初始化一个当前target的表格,并声明一个用于收集结果的Channel。再遍历探测点信息pingServerInfoList,启动goroutine并发执行func ping()。

for targetIndex, target := range targetList {  
    yakit.EnableTable(target, ["监测点", "响应IP", "IP归属地", "响应时间", "TTL", "数据包大小"])  
    results := make(chan var)  
  
    // YAK 中编写"多线程/多并发"应用 | Yak Program Language  
    submitTask := func(param...) {  
        go ping(param...)  
    }  
    for _, serverInfo := range pingServerInfoList {  
        submitTask(serverInfo, enkey, checkType, target, results)  
    }  
}  

这里还有一个关于在循环中goroutine启动时定义域的一个坑点。需要使用一个trick去化解。即我们在循环中不直接启动goroutine,而是在循环中调用一个同步函数,在该函数中再开启goroutine执行异步任务。具体可以看官网这个链接:https://www.yaklang.io/docs/newforyak/concurrent

第四步

从通道中拿到结果,将结果做一系列处理(如统计返回IP数量、进度条、表格等)和最重要的CDN判断后用yakit库进行输出。

for targetIndex, target := range targetList {  
    yakit.EnableTable(target, ["监测点", "响应IP", "IP归属地", "响应时间", "TTL", "数据包大小"])  
    results := make(chan var)  
  
    // YAK 中编写"多线程/多并发"应用 | Yak Program Language  
    submitTask := func(param...) {  
        go ping(param...)  
    }  
    for _, serverInfo := range pingServerInfoList {  
        submitTask(serverInfo, enkey, checkType, target, results)  
    }  
    // println(<- results)  
    ips := make(map[string]int)  
    for i := 0; i < serverNum; i++ {  
        data := <- results  
     // dump(data)  
  
        // 总进度条  
        // yakit.SetProgress(( float64(i+1)*(float64(targetIndex+1)/float64(len(targetList))) )/float64(serverNum))  
        yakit.SetProgress( float64(i+1)*float64(targetIndex+1) / (float64(len(targetList)) * float64(serverNum)) )  
        // 子任务进度条  
        yakit.SetProgressEx(target, float64(i+1)/float64(serverNum))  
  
        // 统计结果、处理表格输出  
        if data["state"] == float64(1) {  
            if data["result"]["ip"] != "" {  
                if ips[data["result"]["ip"]] == undefined {  
                    ips[data["result"]["ip"]] = 1  
                }else {  
                    ips[data["result"]["ip"]]++  
                }  
                // 成功的探测点输出表格  
                tableData := make(map[string]var)  
                tableData["监测点"] = data["cityname"]  
                tableData["响应IP"] = data["result"]["ip"]  
                tableData["IP归属地"] = data["result"]["ipaddress"]  
                tableData["响应时间"] = data["result"]["responsetime"]  
                tableData["TTL"] = data["result"]["ttl"]  
                tableData["数据包大小"] = data["result"]["bytes"]  
                yakit.Output(yakit.TableData(target, tableData))  
            }  
        }  
        // printf("\r正在执行ping操作,当前:%v/%v个,成功:%v/%v个,失败:%v/%v个,总进度:%.2f%%", i+1, serverNum, success, serverNum, failure, serverNum, 100*(float64(i+1)/float64(serverNum)))  
    }  
    println()  
    if len(ips) > 1 {  
        yakit.StatusCard(sprintf("%v:IS CDN", target), "是", target)  
        yakit.StatusCard(sprintf("%v:IP Number", target), len(ips), target)  
        // println("监测点返回不同IP,可能存在CDN")  
    }else {  
        for ip := range ips {  
            yakit.StatusCard(sprintf("%v:IS CDN", target), "否", target)  
            yakit.StatusCard(sprintf("%v:IP Address", target), ip, target)  
            // printf("所有监测点返回IP一致,为%v\n", ip)  
        }  
    }  
    // break  
}  

最终插件效果

(其实还可以再给每个域名都分配一个goroutine,让每个目标间的监测也是异步进行的。但是考虑到可能会对接口产生较大的压力,所以就没这样做了。

插件地址

如果大家对这个CDN判断插件感兴趣的话,可以直接在插件仓库中导入米斯特的第三方yakit-store插件库。地址为:https://github.com/Acmesec/yakit-store

更新后点击头部的刷新按钮

就可以在插件仓库或者基础安全工具中看到插件啦!

Yak官方资源

Yak 语言官方教程:
https://yaklang.com/docs/intro/
Yakit 视频教程:
https://space.bilibili.com/437503777
Github下载地址:
https://github.com/yaklang/yakit
Yakit官网下载地址:
https://yaklang.com/
Yakit安装文档:
https://yaklang.com/products/download_and_install
Yakit使用文档:
https://yaklang.com/products/intro/
常见问题速查:
https://yaklang.com/products/FAQ

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值