老实说在想到这个解决方案之前, 超级吓, 因为不解决这个问题, 就意味着项目无法实现预期的功能.
Adobe AIR中跨域的问题貌似主要是因为安全沙箱(security sandbox)不同导致的, 官方提供了两个相关的解决方案, 一个是使用iframe的sandboxRoot和documentRoot属性, 如:
<iframe
src="http://www.example.com/local/ui.html"
sandboxRoot="http://www.example.com/local/"
documentRoot="app:/sandbox/">
</iframe>
我花了很长很长的时间才理解这两个属性到底指什么, 一方面是因为官方文档说得不够清楚, 我一开始一直以为app:/sandbox/是表示应用沙箱的固定用法, 一方面也是因为相关资料相对较少.
这个方法使得跨域页面中的内容(不包括iframe的src, 但包括iframe加载的内容里的各种东西, 甚至是XMLHttpRequest), 如果其URL在sandboxRoot指定的URL之下, 则并不会请求该URL在互联网上的位置, 而是请求documentRoot所指定的文件目录中的内容. 比如按上面的例子, 如果iframe加载的页面中有一个脚本的URL是http://www.example.com/local/sample.js, 则实际会加载的则是app:/sandbox/sample.js, 这里的documentRoot可以是任意目录.
但是这个方案似乎只能说是加载下文件, 并不能让脚本获取iframe的内容. 另外则有两个bridge(桥), 可以实现脚本的交互. 一个是parentSandboxBridge, 另一个是childSandboxBridge. 这两个bridge都是iframe.contentWindow的属性, 指向一个对象, 这样双方就可以通过这个对象下的属性或者方法进行信息交换了. 但需要补充的是, 不要企图传document这类的对象, 只能传传自己的(非严格界定, 但是大体意思大家应该懂). 官网上也提醒道, 如果要在iframe页面加载过程中让其中的脚本访问parentSandboxBridge, 则要在iframe中的脚本执行前就给iframe绑上parentSandboxBridge. 这时可以使用 “dominitialize” 事件.
这样一来, 跨域交互看起来终于是可行的了. 但是… 我做的是一个采集程序, 对方如果不调用我的bridge, 即使我搭好了又有什么意义呢?
后来才明白前面提到的sandboxRoot和documentRoot的意义. 对于现在多数的网页, 里面一般都会有脚本, 但那天看Google Adwords Keyword Tool的时候差点就伤心了, 从上面往下, 眼看着全是内嵌的脚本… 终于… 发现一个cues.js文件, 可能是GWT相关的信息初始化文件, 其URL是: https://adwords.google.com/cues/cues.js 于是, 将sandboxRoot设置为https://adwords.google.com/cues/, documentRoot设置为app-storage:/, 再把经过更改的cues.js文件放到app-storage:/, 这样一来, 当网页加载完成, 我安排在cues.js里的 “木马” 也就可以执行他的任务了~
如果要替换的JS文件是静态的, 那完全也可以更改后放到app:/目录(只读, 可以用trick写入, 但有兼容问题), 我放到app-storage:/是因为cues.js是动态生成的, 悲催吧…
虽然不能说是一个完美的解决方案, 但至少解决了自己的问题. 不过, 要是真遇上一个页面里没有脚本或者全是内嵌脚本的时候… 就伤了. 有人会问为什么要用那么低级的方式, 不直接发HTTP请求来解决? 老实说我也觉得蛮低级. 但Google Adwords Keyword Tool是用GWT开发的, 里面用来交换数据的格式并非常见的application/x-www-form-urlencoded, 而是text/gwt-rpc, 因为时间紧, 不能保证这是能轻松破解的(同样, 资料极少), 于是退而求其次了.