九月的第一天,重要的是开学了,莘莘学子们返回校园,而我下面说的,则是次要的,呵呵。
是这样的,想换掉旧的 ddns 服务商,打算试试一个海外的。作为一站长,使用 ddns 是司空见惯的事情。
没想到服务商却没有提供客户端 ,希望只是暂时的,日后会提供。好在服务商有一个 Web 开放接口,于是拟作一个 DDNS 客户端,通过这个 Web Service 接口,采用 HTTP Basic Auth 认证,提交一字段 ip 即可刷新 name server 的 ip 了,简单可行,当然还要提交域名和密码,都少不了的。默认账户的密码即为域名之密码,但出于安全方面的考虑,不建议直接使用账户,而是随后在后台去分配一个 hashkey 作为每一个域名其单独的密码,这样为每个绑定的域名设定一对一的关联密码是比较好的做法,以策于安全。进一步地说,每个域名修改其 ddns 时,所提交的 hashkey 都可以不相同的。域名、hash 密码、Web Serivce、新 IP 组合的情况如下 url:http://foo.bar.com:BKlud7MtMyJs2lne@dns.abc.net/update?hostname=foo.bar.com&myip=61.140.176.162
假设这个 dns.abc.net 便是服务商提供个 Service 入口。理论情况下,在一个任意可输入url的地方,只要是url“输入框”发送这个 url 都可达成同步ip的任务,根本小事一桩。不过服务商没有提供一个标准的客户端,还是会让初次使用的用户感到不方便,如果不熟悉 url 发送新 ip,那么标准客户端对于他们来说不可或缺的,依然需要客户端来完成动态更新域名的任务,并不是“多此一举”。估计服务商最终应该会提供一个标准的客户端。然而希望即使有客户端以后,亦要保留这个基于 Web Service 的提交方式。实际上,我认为通过 HTTP Auth 认证已经是非常好的做法。盖客户端之任务,无非就是检测当前服务器的 IP 然后更新。该 IP 固然非固定 IP,并非长久租用而固定不变的。尽管可以一直维持 1 ~ 2日,但掉线的时候总不会通知你,因此你所做的,首先就要检测住这个 IP 的情况,保证一旦遭到改变,你这个客户端要知道。第二步,你要把这新 IP 告诉给远方的 Name Server。NameSever 收到修改IP的消息,便会在 TTLS 时间内,修改绑定的 IP,更新缓存。
对比于提供“客户端”的方案,提供 WebService+HTTP BasicAuth 的方式显得更精简、更安全。我们所“花费的代价”,却就是写写一个监视 IP、发送 IP 的这么一个工具。
该工具逻辑很简单,咱按照这个思路,山寨一个 ddns。敲定一个基于 MS HTA 应用程序。就是这么简单的一个工具,我想当然地以为不消二十分钟便能完成。可是这只是想当然,内部原因就是因为自己拖拉的行事作风,贻害工作甚深。为改正,也是作个人累积,现笔记如下:
一、如何得知自己的IP? & XHR的建立
这个好办,包括 ipconfig 命令的很多工具都可以查到。再不是就是利用提供 ip 所在地服务的网站查询非局域网的 ip。此方案采用了这种较为保险的方法。然而这里笔者甘愿冒当“小白”提出一个问题,——你用肉眼看的话不用说就可以看到网站的ip,那是四组数字不假,机器怎么看?就要可以读取、分析 Response 实际响应内容的工具或函数。所谓自动化过程,简而言之,就是让机器去代替人们干的活。
如无意外,一般涉及 url 相关的任务,就会联想到 HTTPRequest、WebRequest 和 XMLHttpRequest 等的类库提供的模块。其中XMLHttpRequest 的广谱性最强,能快速投入使用。实践过程中,先要确定的是版本的选择,因为发现系统库中有多种版本的 XHR(下简称 XHR)并存的现象,故所以,按照高版本为先的原则,通过遍历 progid 标识数组,而且使用 try…… catch…… 规避抛出的异常的方式获取 XHR 对象。
我不是太确定 try 对于性能有多大影响,在我的 js 库中,有一段引用网友的分析,,那是在注释部分。
/** * 返回XHR对象。 * 注意 url 必须以http开头的完整路径。 * @param {String} referer 定义 HTTP-REFERER 变量,请求方说明,例如 http://www.163.com/ * @return {MSXML.ServerXMLHTTP} */ $.xhr = function (asyncCallback, referer){ var xhr = typeof XMLHttpRequest != 'undefined' && new XMLHttpRequest(); if(!xhr){ var activeX = [ 'MSXML2.ServerXMLHTTP.5.0' ,'MSXML2.ServerXMLHTTP.3.0' ,'MSXML2.ServerXMLHTTP' ,'MSXML2.XMLHTTP.3.0' ,'MSXML2.XMLHTTP' ,'Microsoft.XMLHTTP' ]; // try 是写在 for 循环里面的, // 每当遇到一个try语句,异常的框架就放到堆栈上面,直到所有的try语句都完成。 // 如果下一级的try语句没有对某种异常进行处理,堆栈就会展开,直到遇到有处理这种异常的try语句。 // 这是十分浪费性能的做法,但暂时又找不到恰当的解决方式,于是采用了这种方法。 for (var i = 0, j = activeX.length; i < j; ++i) { try { xhr = new ActiveXObject(activeX[i]); break; } catch (e){ } } } if(!xhr){ throw '没有XHR组件!'; } if(activeX[i].indexOf('Server') != -1){ // 设置超时 // Timeouts in ms for parts of communication: resolve, connect, send (per packet), receive (per packet) xhr.setTimeouts(30000, 30000, 30000, 30000); // 忽略 SSL Ignore all SSL errors xhr.setOption(2, 13056); } // if ( method == "POST" ) { // objXmlHttp.setRequestHeader( "Content-Type", "application/x-www-form-urlencoded" ); // } referer && Http.setRequestHeader("Referer", referer); if(asyncCallback && typeof(asyncCallback) == 'function'){ xhr.onreadystatechange = function(){ if (xhr.readyState == 0){ }else if (xhr.readyState == 1){ }else if (xhr.readyState == 2){ }else if (xhr.readyState == 3){ }else if (xhr.readyState == 4 && xhr.status == 200){ asyncCallback(xhr); }else if(xhr.readyState == 4 && xhr.status != 200){ // Non-OK HTTP response var text = "Http Error: {0} {1}\nFailed to grab page data from: {2}"; text = text.format(xhr.Status, objXmlHttp.StatusText, url); throw text; } } } return xhr; }Edk.js(主页: http://code.google.com/p/naturaljs/)的包涵盖常用的 MS 元件,包括数据库连接的、文件读写的等等,并且进行了浅浅一层的封装,但不过多的 over-engine 的设计,从而适当呈现。关于 XHR 对象本身,必须指出,除了浏览器 XHR 对象外,微软还提供了 ServerXHR 针对服务端使用的 XHR 元件。Edk 添加了其 progid。个人印象,Server XHR 主要增加了“超时(timeout)”的功能。虽然浏览器无法使用上 Server-side XHR,不过据小弟所知,通过 setTimeout() 变通的方法配合起来,就可以为 XHR 请求时带来超时的功能,Ext 库中就有类似的实现,并且把页面的计时器从一个普通的方法调用抽象使之变为一个个实例对象去控制。以上属于题外话了,有点扯远了。 Ddns工具采用 edk.js 默认的超时参数。一般来说网站是稳定可靠的,超时就无须担心了。这里准备了几个可查询的ip服务站点:
- http://www.j4.com.tw/james/remoip.php
- http://www.ip38.com
- http://whatismyipaddress.com/
ip = ip.match(/(\d+\.){3}\d+/); if(ip == null){ throw '查询ip的服务端有问题了。'; }else{ ip = ip[0]; }
二、业务逻辑
这里业务,若说“业务”则太过于“隆重”,口味过重了,嘿嘿,提一提吧。现在已知的新的 ip,作为输入条件。if(_ip == null && ip != _ip){ // 第一次取得 IP 地址,记录下来 ddnsClient._ip = ip; logger.innerHTML += '初始化'; }else if(_ip != null && ip != _ip){ // 发生了变化!!! ADSL IP changed // 这里更新 ip…… ddnsClient._ip = ip; }else if(_ip != null && ip == _ip){ if(logger.innerHTML.length > 1000) { logger.innerHTML = ''; // 清空 innerHTML } logger.innerHTML += '<br/>于 {0} 发生过检查,当前记录ip{1},查询ip{2}。ip 没有变化。'.format(new Date, _ip, ip); }
_ip 即 ddnsClient._ip,就是一静态 String,保存上次更新了的 ip,与当前获取的 ip 相比较,若没变化则,ip 稳定,若变化了则更新 ip。
三、提交ip更新
注意没有,XHR 很好地完成了查询 ip的任务,对吧?然而,后面再需要 XHR 来发送 Name Server Ip的时候,我却遇到了稍微棘手的问题,这也是我在这儿耗费相当时间的原因。首先,用XHR发送到Web Service站点,设置了帐户和域名,返回的却是一堆莫名其妙的数据,好像也不是文本数据,看不出什么结果来,好了,因为我在 HTA 程序中,总是可以通过 Window.open() 新窗口吧,不料,死不放心的 ie 安全机制总会弹出帐号、密码的登录框,点击保存起来也没用。每次都如临大敌地、不胜其烦的问一次,确认一次,需要人工操作,那还叫什么全自动操作啊?
我知道,这是浏览器的 HTTP 认证,其他浏览器不知会不会这么烦——不过我也不想试了,因为这儿我想到了 Curl,*nix 下著名的 curl 工具。有木有 Window 移植版?有!——立刻下了回来,CMD命令模式下curl –help 看看帮助用法先,——这是我多年来的习惯。一看果然很详尽,curl 功能强大真不是盖的。光看帮助就罗列一大堆的用法,比较全面地发挥了一串 url 为输入条件而能够做的的事情。而此时此刻的我,只是需要一个 HTTP GET 命令。好办!默认就是,其他什么参数也不用,请看:
curl http://foo.bar.com:BKlud7MtMyJs2lne@dns.abc.net/update?hostname=foo.bar.com&myip=61.140.176.162嘿,一看屏幕,是不是行啦?没错,显示 good 说明可以啦。liunx的就是好!不由得感谢一下自由的 Linux,Win 就是死板,哈哈!好了,接下的事情似乎就顺理成章了,通过 wscript.run调用可执行程序,然后捕获结果。怎么捕获呢?头脑中第一个念头就是DOS的重定义输出符号 > ,改变默认的屏幕输出,改成为文件输出,换句话说,就是将 curl 请求回来的结果保存文件,然后 HTA 里面打开磁盘文件。文件名任意,就叫 ddns.log 吧。因为服务端返回 good / badauth + ip 似乎太儿戏,我想了想,curl 不是支持 trace 的吗?好看看 trace,我选择文本的—trace-ascii,跟踪结果如下:
== Info: About to connect() to dyn.dns.he.net port 80 (#0)
== Info: Trying 184.105.242.3... == Info: connected
== Info: Connected to dyn.dns.he.net (184.105.242.3) port 80 (#0)
== Info: Server auth using Basic with user 'forum.ajaxjs.com'
=> Send header, 248 bytes (0xf8)
0000: GET /nic/update?hostname=forum.ajaxjs.com^&myip=61.140.152.2 HTT
0040: P/1.1
0047: Authorization: Basic Zm9ydW0uYWpheGpzLmNvbTpCS2x1ZDdNdE15SnMybG5
0087: l
008a: User-Agent: curl/7.17.0 (i586-pc-mingw32msvc) libcurl/7.17.0 zli
00ca: b/1.2.2
00d3: Host: dyn.dns.he.net
00e9: Accept: */*
00f6:
<= Recv header, 16 bytes (0x10)
0000: HTTP/1.1 200 OK.
<= Recv header, 36 bytes (0x24)
0000: Date: Tue, 30 Aug 2011 12:31:37 GMT.
<= Recv header, 26 bytes (0x1a)
0000: Server: dns.he.net v0.0.1.
<= Recv header, 43 bytes (0x2b)
0000: Email: DNS Administrator <dnsadmin@he.net>.
<= Recv header, 41 bytes (0x29)
0000: Cache-Control: no-cache, must-revalidate.
<= Recv header, 39 bytes (0x27)
0000: Expires: Wed, 29 Aug 2012 12:31:37 GMT.
<= Recv header, 19 bytes (0x13)
0000: Content-Length: 18.
<= Recv header, 24 bytes (0x18)
0000: Content-Type: text/html.
<= Recv header, 1 bytes (0x1)
0000: .
<= Recv data, 18 bytes (0x12)
0000: nochg 61.140.152.2
== Info: Connection #0 to host dyn.dns.he.net left intact
== Info: Closing connection #0
没事闲着可分析这日志文件,哈哈。当前只关注倒数第三行的那行,表示修改了 ip 与否。
现在是“0000: nochg 61.140.152.2”,提交的ip跟原来的一致,无须作改变。程序中用正则 /Recv data[\s\S]*0000:(.*)/; 抠出了这行。我一般喜欢用正则,而不是分析字符串。
其实以上过程还是有点颇费周折的地方,就是开始试的时候,出来一堆乱七八糟的字符,什么没有这命令云云,断不是正常反馈的结果,于是我再查 help,再拼命的试,几次下来,还是那错误,我内心再次陷入低潮……老大……不过就是简单的命令行调用嘛,何必这样折磨我 ~5555。及后我逐个字符分析,慢慢分析,咦,,有个“&”(在url的querystring 参数处),原来就是这个大于字符,在 DOS中 必须转义为^&,否则另外一个意思……自问熟悉 DOS 的日子也不短了,竟连这个……
四、无总结的小结
虽然是一个简单到家的工具玩意,可我因实力不济,兼之我又是十分多心的人,事情多,横跨八、九月间才能完成此拙文,十分窘的说,无论写代码、写文章都像挤牙膏似的,超时又超时。哎,总之~不管如何,终于弄成了下面不算样子的东西,几天下来的测试,大体情况还凑合吧。
理论上XHR与Curl的作用是无异的吧,但其实质这次如果不是 XHR 在 HTTP Basic Auth 有点问题,也不会出动到 XHR 的代替品 Curl.exe。采集的时候,*nix 系的人应该会用 curl 来做吗?
下次要做个监视网站的,不用那么密地,10分钟 check 一次。
更新 XHR for IE:
/** * 返回XHR对象。 * 注意 url 必须以http开头的完整路径。 * @param {Object} cfg * @cfg {String} referer 定义 HTTP-REFERER 变量,请求方说明,例如 http://www.163.com/ */ $$.request = function (cfg) { var isNative = typeof XMLHttpRequest != 'undefined'; if (!isNative) { var progId, progIds = [ 'MSXML2.ServerXMLHTTP.5.0' , 'MSXML2.ServerXMLHTTP.3.0' , 'MSXML2.ServerXMLHTTP' , 'MSXML2.XMLHTTP.6.0' , 'MSXML2.XMLHTTP.3.0' , 'MSXML2.XMLHTTP' , 'Microsoft.XMLHTTP' ]; // try 是写在 for 循环里面的, // 每当遇到一个 try 语句,异常的框架就放到堆栈上面,直到所有的try语句都完成。 // 如果下一级的 try 语句没有对某种异常进行处理,堆栈就会展开,直到遇到有处理这种异常的 try 语句。 // 这是十分浪费性能的做法,但暂时又找不到恰当的解决方式,于是不得已采用了这种 Hack 的方法。 for (var i = 0, j = progIds.length; i < j; ++i) { try { progId = progIds[i]; new ActiveXObject(progId); // no referer at all break; } catch (e) { } } } if (!isNative && !progId) { throw '没有XHR组件!'; } $$.request = function (cfg) { var xhr = isNative ? new XMLHttpRequest() : new ActiveXObject(progId); if (!xhr) { throw '系统创建XHR对象失败!'; } if (!cfg || !cfg.url) { throw '不符合最低参数之要求!'; } var method = (cfg.isPost || cfg.post) ? "POST" : "GET"; if (cfg.method) { method = cfg.method.toUpperCase(); } var asyncCallback = cfg.fn; var isAysc; isAysc = typeof cfg.isAysc == 'undefined' ? true /* 默认是异步的 */ : cfg.isAysc; isAysc = isAysc && asyncCallback && typeof (asyncCallback) == 'function'; isAysc = !!isAysc; // ff 必须输入 Boolean,ie则没那么严格。 xhr.open(method, cfg.url, isAysc); if (isAysc) { // 为协调 ie,将 onreadystatechange 置于 open() 后。 xhr.onreadystatechange = function () { switch (xhr.readyState) { case 1: break; case 2: break; case 3: break; case 4: asyncCallback(xhr); // 避免 ie 内存泄露 $$.isIE && typeof setTimeout != 'undefined' && setTimeout(function () { xhr.onreadystatechange = new Function(); xhr = null; delete xhr; }, 0); break; default: throw '通讯有问题!'; } } } method == 'POST' && xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); cfg.referer && xhr.setRequestHeader("Referer", cfg.referer); cfg.contentType && xhr.setRequestHeader("Content-Type", cfg.contentType); // you may set xhr.setRequestHeader("Accept", "text/json"); // 可接受文档格式,注意,这里默认为 text/json。 if (cfg.accept && cfg.accept != 'text/json') { xhr.setRequestHeader("Accept", cfg.accept); } else { xhr.setRequestHeader("Accept", 'text/json'); } /* For Serverside xhr ONLY */ if (!isNative && progId.indexOf('Server') != -1) { // 为服务端 XHR 的设置 xhr.setTimeouts(30000, 30000, 30000, 30000); // 设置超时 Timeouts in ms for parts of communication: resolve, connect, send (per packet), receive (per packet) xhr.setOption(2, 13056); // 忽略 SSL Ignore all SSL errors } xhr.send(cfg.post || null); if (!isAysc) return xhr; } return $$.request(cfg); }
服务端 XHR:
;(function(){ /** * 统一编码为UTF-8。 */ Response.Charset = "utf-8"; Session.CodePage = 65001; Session.Timeout = 200; Server.ScriptTimeout = 100; /** * 获取来自表单的各个字段的数据。HTTP FORM所提交的数据乃是key/value配对的结构。 * 本函数是把这些key/value集合转化成为JavaScript对象。这是post包最核心的函数。 * 对各个字段有decodeURI()、unescape()、$$.getPerm()自动类型转化的功能。 * @param {Boolean/String} isAllRequest true表示为返回QueryString和Form的Request对象集合。 * 如果指定form则返回表单的hash值,如果指定 QueryString 则返回URL上面的参数hash。 * @return {Object} 客户端提交的数据或查询参数。 */ function every(collection){ var count = collection.count; var obj = { count : count // 字段总数 }; var emu = new Enumerator(collection); var key, v; while(count > 0 && !emu.atEnd()){ key = emu.item().toString(); v = collection(key)(); // MS ASP这里好奇怪,不加()不能返回字符串 obj[key] = $$.getPrimitives(v); // 进行自动类型转换。 emu.moveNext(); } return obj; } this.QueryString = function(){ return every(Request.QueryString); } this.Form = function(){ return every(Request.Form); } this.ServerRequest = function(){ var method, url, params; url = Request.QueryString('url')(); url = decodeURI(url); // Response.write('请求url:' + url); var xmlhttp; xmlhttp = new ActiveXObject("Msxml2.ServerXMLHTTP.6.0"); xmlhttp.open(method, url, false); // 设置超时时间,注意参数顺序 // setTimeouts的参数顺序也是固定的,按顺序为: // 域名解析超时时间(resolveTimeout)、连接超时时间(connectTimeout)、数据发送超时时间(sendTimeout)、数据接收超时时间(receiveTimeout) xmlhttp.setTimeouts(2000, 2000, 2000, 10000); try { xmlhttp.send(null); }catch(e) { Response.Write('发生异常:' + e.message + '<br/>'); // 判断是否为超时错误 if(e.number == -2147012894) { var step = ''; // 判断超时错误发生所在的阶段 switch(xmlhttp.readyState) { case 1: step = "解析域名或连接远程服务器" break; case 2: step = "发送请求"; break; case 3: step = "接收数据"; break; default: step = "未知阶段"; } Response.Write("在 " + step + " 时发生超时错误"); } Response.End(); } try{ eval("var JSON = " + xmlhttp.responseText + ";"); }catch(e){ Response.write('请求完毕,但解析 JSON 错误! 响应内容为:' + xmlhttp.responseText); } } }).call(bf);