Network Indicator

显示当前页面连接IP 和SPDY、HTTP/2

当前为 2016-01-01 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Network Indicator
  3. // @version 0.0.8
  4. // @compatibility FF34+
  5. // @description 显示当前页面连接IP 和SPDY、HTTP/2
  6. // @include main
  7. // @namespace https://greasyfork.org/users/25642
  8. // ==/UserScript==
  9.  
  10. 'use strict';
  11.  
  12. if (location == 'chrome://browser/content/browser.xul') {
  13.  
  14. const AUTO_POPUP = 600; //鼠标悬停图标上自动弹出面板的延时,非负整数,单位毫秒。0为禁用。
  15.  
  16. const DEBUG = false; //调试开关
  17. const GET_LOCAL_IP = true; //是否启用获取显示内(如果有)外网IP。基于WebRTC,
  18. //如无法显示,请确保about:config中的media.peerconnection.enabled的值为true,
  19. //或者将上面的 “DEBUG”的值改为true,重启FF,打开浏览器控制台(ctrl+shift+j),
  20. //弹出面板后,将有关输出适宜打ma,复制发给我看看。
  21. //还有可能会被AdBlock, Ghostery等扩展阻止。
  22. //若关闭则只显示外网IP
  23.  
  24. const HTML_NS = 'http://www.w3.org/1999/xhtml';
  25. const XUL_PAGE = 'data:application/vnd.mozilla.xul+xml;charset=utf-8,<window%20id="win"/>';
  26. let promise = {};
  27. try{
  28. Cu.import('resource://gre/modules/PromiseUtils.jsm', promise);
  29. promise = promise.PromiseUtils;
  30. }catch(ex){
  31. Cu.import('resource://gre/modules/Promise.jsm', promise);
  32. promise = promise.Promise;
  33. }
  34. let HiddenFrame = function() {};
  35. HiddenFrame.prototype = {
  36. _frame: null,
  37. _deferred: null,
  38. _retryTimerId: null,
  39. get hiddenDOMDocument() {
  40. return Services.appShell.hiddenDOMWindow.document;
  41. },
  42. get isReady() {
  43. return this.hiddenDOMDocument.readyState === 'complete';
  44. },
  45. get() {
  46. if (!this._deferred) {
  47. this._deferred = promise.defer();
  48. this._create();
  49. }
  50. return this._deferred.promise;
  51. },
  52. destroy() {
  53. clearTimeout(this._retryTimerId);
  54. if (this._frame) {
  55. if (!Cu.isDeadWrapper(this._frame)) {
  56. this._frame.removeEventListener('load', this, true);
  57. this._frame.remove();
  58. }
  59. this._frame = null;
  60. this._deferred = null;
  61. }
  62. },
  63. handleEvent() {
  64. let contentWindow = this._frame.contentWindow;
  65. if (contentWindow.location.href === XUL_PAGE) {
  66. this._frame.removeEventListener('load', this, true);
  67. this._deferred.resolve(contentWindow);
  68. } else {
  69. contentWindow.location = XUL_PAGE;
  70. }
  71. },
  72. _create() {
  73. if (this.isReady) {
  74. let doc = this.hiddenDOMDocument;
  75. this._frame = doc.createElementNS(HTML_NS, 'iframe');
  76. this._frame.addEventListener('load', this, true);
  77. doc.documentElement.appendChild(this._frame);
  78. } else {
  79. this._retryTimerId = setTimeout(this._create.bind(this), 0);
  80. }
  81. }
  82. };
  83.  
  84. let networkIndicator = {
  85.  
  86. autoPopup: AUTO_POPUP,
  87.  
  88. _getLocalIP: GET_LOCAL_IP,
  89.  
  90. init(){
  91. if(this.icon) return;
  92. this.setStyle();
  93. this.icon.addEventListener('click', this, false);
  94. if(this.autoPopup){
  95. this.icon.addEventListener('mouseenter', this, false);
  96. this.icon.addEventListener('mouseleave', this, false);
  97. }
  98. ['dblclick', 'mouseover', 'mouseout', 'command', 'contextmenu'].forEach(event => {
  99. this.panel.addEventListener(event, this, false);
  100. });
  101. gBrowser.tabContainer.addEventListener('TabSelect', this, false);
  102. ['content-document-global-created', 'inner-window-destroyed', 'outer-window-destroyed',
  103. 'http-on-examine-cached-response', 'http-on-examine-response'].forEach(topic => {
  104. Services.obs.addObserver(this, topic, false);
  105. });
  106. },
  107.  
  108. _icon: null,
  109. _panel: null,
  110.  
  111. get icon (){
  112. if(!this._icon){
  113. this._icon = document.getElementById('NetworkIndicator-icon') ||
  114. this.createElement('image', {id: 'NetworkIndicator-icon', class: 'urlbar-icon'},
  115. [document.getElementById('urlbar-icons')]);
  116. return false;
  117. }
  118. return this._icon;
  119. },
  120.  
  121. get panel (){
  122. if(!this._panel){
  123. let cE = this.createElement;
  124. this._panel = document.getElementById('NetworkIndicator-panel') ||
  125. cE('panel', {
  126. id: 'NetworkIndicator-panel',
  127. type: 'arrow'
  128. }, document.getElementById('mainPopupSet'));
  129. this._panel._contextMenu = cE('menupopup', {id: 'NetworkIndicator-contextMenu'}, this._panel);
  130. cE('menuitem', {label: '复制全部'}, this._panel._contextMenu)._command = 'copyAll';
  131. cE('menuitem', {label: '复制选中'}, this._panel._contextMenu)._command = 'copySelection';
  132. this._panel._list = cE('ul', {}, cE('vbox', {context: 'NetworkIndicator-contextMenu'}, this._panel));
  133. }
  134. return this._panel;
  135. },
  136.  
  137. currentBrowserPanel: new WeakMap(),
  138. _panelNeedUpdate: false,
  139.  
  140. observe(subject, topic, data) {
  141. if(topic == 'http-on-examine-response' || topic == 'http-on-examine-cached-response'){
  142. this.onExamineResponse(subject, topic);
  143. }else if(topic == 'inner-window-destroyed'){
  144. let innerID = subject.QueryInterface(Ci.nsISupportsPRUint64).data;
  145. delete this.recordInner[innerID];
  146. if(this.getWinId().currentInnerWindowID != innerID){
  147. this._panelNeedUpdate = true;
  148. this.updateState();
  149. }
  150. }else if(topic == 'outer-window-destroyed'){
  151. let outerID = subject.QueryInterface(Ci.nsISupportsPRUint64).data,
  152. cwId = this.getWinId();
  153. delete this.recordOuter[outerID];
  154. if(cwId.outerWindowID != outerID){
  155. this._panelNeedUpdate = true;
  156. this.updateState();
  157. //从一般网页后退到无网络请求的页面(例如about:xxx)应关闭面板。
  158. if(!this.recordInner[cwId.currentInnerWindowID])
  159. this.panel.hidePopup && this.panel.hidePopup();
  160. }
  161. }else if(topic == 'content-document-global-created'){
  162. let domWinUtils = subject.top
  163. .QueryInterface(Ci.nsIInterfaceRequestor)
  164. .getInterface(Ci.nsIDOMWindowUtils),
  165. outerID = domWinUtils.outerWindowID,
  166. innerID = domWinUtils.currentInnerWindowID,
  167. ro = this.recordOuter[outerID];
  168. if(!ro) return;
  169. let mainHost = ro.pop(),
  170. ri = this.recordInner[innerID];
  171. //标记主域名
  172. mainHost.isMainHost = true;
  173. this.recordInner[innerID] = [mainHost];
  174. delete this.recordOuter[outerID];
  175. }
  176. },
  177.  
  178. //记录缓存对象
  179. recordOuter: {},
  180. recordInner: {},
  181.  
  182. onExamineResponse(subject, topic) {
  183. let channel = subject.QueryInterface(Ci.nsIHttpChannel),
  184. nc = channel.notificationCallbacks || channel.loadGroup && channel.loadGroup.notificationCallbacks,
  185. domWinUtils = null,
  186. domWindow = null;
  187. if(!nc || (channel.loadFlags & Ci.nsIChannel.LOAD_REQUESTMASK) == 5120){
  188. //前进后退读取Cache需更新panel
  189. return this._panelNeedUpdate = topic == 'http-on-examine-cached-response';
  190. }
  191. try{
  192. domWindow = nc.getInterface(Ci.nsIDOMWindow);
  193. domWinUtils = domWindow.top
  194. .QueryInterface(Ci.nsIInterfaceRequestor)
  195. .getInterface(Ci.nsIDOMWindowUtils);
  196. }catch(ex){
  197. //XHR响应处理
  198. let ww = null;
  199. try{
  200. ww = subject.notificationCallbacks.getInterface(Ci.nsILoadContext);
  201. }catch(ex1){
  202. try{
  203. ww = subject.loadGroup.notificationCallbacks.getInterface(Ci.nsILoadContext);
  204. }catch(ex2){}
  205. }
  206. if(!ww) return;
  207. try{domWindow = ww.associatedWindow;}catch(ex3){}
  208. domWinUtils = this.getWinId(ww.topFrameElement);
  209. }
  210.  
  211. let isMainHost = (channel.loadFlags & Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI
  212. && domWindow && domWindow == domWindow.top);
  213.  
  214. //排除ChromeWindow的、unload等事件触发的请求响应
  215. if(!domWinUtils || (channel.loadFlags == 640 && !subject.loadGroup)
  216. || domWindow instanceof Ci.nsIDOMChromeWindow
  217. || (!isMainHost && channel.loadInfo && channel.loadInfo.loadingDocument
  218. && channel.loadInfo.loadingDocument.ownerGlobal === null)
  219. ) return;
  220.  
  221. let outerID = domWinUtils.outerWindowID,
  222. innerID = domWinUtils.currentInnerWindowID,
  223. newentry = Object.create(null),
  224. cwId = this.getWinId();
  225.  
  226. newentry.host = channel.URI.asciiHost;
  227. newentry.scheme = channel.URI.scheme;
  228. //newentry.url = channel.URI.asciiSpec;
  229. channel.remoteAddress && (newentry.ip = channel.remoteAddress);
  230.  
  231. channel.QueryInterface(Ci.nsIHttpChannelInternal);
  232. try{
  233. //获取响应头的服务器、SPDY、HTTP/2信息
  234. channel.visitResponseHeaders({
  235. visitHeader(name, value){
  236. let lowerName = name.toLowerCase();
  237. if (lowerName == 'server') {
  238. newentry.server = value
  239. }else if(lowerName == 'x-firefox-spdy'){
  240. newentry.spdy = value
  241. }
  242. }
  243. });
  244. }catch(ex){}
  245.  
  246. if(isMainHost){
  247. newentry.url = channel.URI.asciiSpec;
  248. outerID && (this.recordOuter[outerID] || (this.recordOuter[outerID] = [])).push(newentry);
  249. if(this.panel.state != 'closed'){
  250. if(cwId.outerWindowID == outerID){
  251. if(this.panel.hasAttribute('overflowY'))
  252. this.panel.removeAttribute('overflowY');
  253. let list = this.panel._list;
  254. while(list.hasChildNodes())
  255. list.removeChild(list.lastChild);
  256. list._minWidth = 0;
  257. }
  258. }
  259. }else{
  260. innerID && (this.recordInner[innerID] || (this.recordInner[innerID] = [])).push(newentry);
  261. //newentry.loadFlags = channel.loadFlags
  262. }
  263.  
  264. //更新图标状态
  265. if(cwId.outerWindowID == outerID || cwId.currentInnerWindowID == innerID)
  266. this.updateState(cwId);
  267.  
  268. //当且仅当主动点击打开显示面板时才查询IP位置、更新面板信息。
  269. //避免每次刷新页面都请求查询网站的IP,以减少暴露隐私的可能、性能消耗。
  270. if(this.panel.state != 'closed' && (cwId.outerWindowID == outerID || cwId.currentInnerWindowID == innerID)){
  271. //标记下次点击显示时是否需更新面板内容
  272. if(this._panelNeedUpdate = !(this.recordInner[cwId.currentInnerWindowID] || [{}]).some(re => re.isMainHost))
  273. this.panel.hidePopup(); //类似about:addons页面情况下,刷新时必须关闭面板,避免计数叠加。
  274.  
  275. this.dnsDetect(newentry, isMainHost);
  276. }else{
  277. this._panelNeedUpdate = true;
  278. }
  279. },
  280.  
  281. _nsIDNSService: Cc['@mozilla.org/network/dns-service;1'].createInstance(Ci.nsIDNSService),
  282.  
  283. _nsIClipboardHelper: Cc['@mozilla.org/widget/clipboardhelper;1'].getService(Ci.nsIClipboardHelper),
  284.  
  285. dnsDetect(obj, isMainHost){
  286. if(obj.ip) return this.updatePanel(obj, isMainHost);
  287. this._nsIDNSService.asyncResolve(obj.host, this._nsIDNSService.RESOLVE_BYPASS_CACHE, {
  288. onLookupComplete: (request, records, status) => {
  289. if (!Components.isSuccessCode(status)) return;
  290. obj.ip = records.getNextAddrAsString();
  291. this.updatePanel(obj, isMainHost);
  292. }
  293. }, null);
  294. },
  295.  
  296. updatePanel(record, isMainHost){
  297. let cE = this.createElement,
  298. list = this.panel._list,
  299. li = list.querySelector(`li[ucni-ip="${record.ip}"]`),
  300. p = null;
  301.  
  302. if(!li){//不存在相同的IP
  303. let fragment = document.createDocumentFragment(),
  304. ipSpan = null;
  305. li = cE('li', {'ucni-ip': record.ip}, fragment);
  306. cE('p', {class: 'ucni-ip', text: record.ip + '\n'}, ipSpan = cE('span', {}, li));
  307. // + '\n' 复制时增加换行格式
  308. p = cE('p', {class: 'ucni-host', host: record.host, scheme: record.scheme, counter: 1, text: record.host + '\n'}, cE('span', {}, li));
  309. p._connCounter = 1;
  310. p._connScheme = [record.scheme];
  311. if(isMainHost){
  312. //标记主域名
  313. li.classList.add('ucni-MainHost');
  314. //主域名重排列至首位
  315. list.insertBefore(fragment, list.firstChild);
  316. //更新主域名 IP位置
  317. this.updateMainInfo(record, list);
  318. }else{
  319. list.appendChild(fragment);
  320. //不存在相同的IP且非主域名
  321. this.setTooltip(li, record);
  322. }
  323.  
  324. //调整容器宽度以适应IP长度
  325. let minWidth = record.ip.length - record.ip.split(/:|\./).length / 2 + 1;
  326. if(list._minWidth && minWidth > list._minWidth){
  327. Array.prototype.forEach.call(list.querySelectorAll('li>span:first-child'), span => {
  328. if(!span._width || span._width < minWidth)
  329. span.style.minWidth = `${span._width = list._minWidth = minWidth}ch`;;
  330. });
  331. }else{
  332. if(!list._minWidth) list._minWidth = minWidth;
  333. ipSpan.style.minWidth = `${ipSpan._width = list._minWidth}ch`;
  334. }
  335. }else{//相同的IP
  336. p = li.querySelector(`.ucni-host[host="${record.host}"]`);
  337. if(!p){//同IP不同的域名
  338. p = cE('p', {class: 'ucni-host', host: record.host, scheme: record.scheme, counter: 1, text: record.host + '\n'}, li.querySelector('.ucni-host').parentNode);
  339. p._connCounter = 1;
  340. p._connScheme = [record.scheme];
  341. }else{//同IP同域名
  342. p.setAttribute('counter', ++p._connCounter); //计数+1
  343.  
  344. if(p._connScheme.every(s => s != record.scheme)){
  345. //同IP同域名不同的协议
  346. p._connScheme.push(record.scheme);
  347. p.setAttribute('scheme', p._connScheme.join(' '));
  348. }
  349. }
  350. if(isMainHost){
  351. li.classList.add('ucni-MainHost');
  352. if(list.firstChild != li){
  353. list.insertBefore(li, list.firstChild);
  354. li.lastChild.insertBefore(p, li.lastChild.firstChild);
  355. }
  356. this.updateMainInfo(record, list);
  357. }
  358. }
  359.  
  360. if(this.panel.popupBoxObject.height > 500 && !this.panel.hasAttribute('overflowY')){
  361. this.panel.setAttribute('overflowY', true);
  362. }
  363.  
  364. if(record.spdy && (!p.spdy || p.spdy.every(s => s != record.spdy))){
  365. (p.spdy || (p.spdy = [])).push(record.spdy);
  366. p.setAttribute('spdy', p.spdy.join(' '));
  367. }
  368.  
  369. this.setTooltip(p, {
  370. counter: p._connCounter,
  371. server: record.server,
  372. scheme: p._connScheme || [record.scheme],
  373. spdy: p.spdy
  374. });
  375. },
  376.  
  377. updateMainInfo(obj, list) {
  378. if(obj.location){
  379. if(list.querySelector('#ucni-mplocation')) return;
  380. let cE = this.createElement,
  381. fm = document.createDocumentFragment(),
  382. li = cE('li', {id: 'ucni-mplocation'}, fm),
  383. timeStamp = new Date().getTime(),
  384. text = ['所在地', '服务器', '内网IP', '外网IP'],
  385. info = [];
  386. let setMainInfo = info => {
  387. let location = this.localAndPublicIPs.publicLocation;
  388. if(this.localAndPublicIPs._public){
  389. info.push({value: this.localAndPublicIPs._public[0], text: text[3]});
  390. location = this.localAndPublicIPs._public[1];
  391. }
  392. for(let i of info){
  393. if(!i.value) continue;
  394. let label = cE('label', {text: i.value + '\n'}, cE('span', {text: i.text + ': '}, cE('p', {}, li)).parentNode);
  395. if(i.text === text[3]) this.setTooltip(label, { ip: i.value, location: location });
  396. }
  397. list.insertBefore(fm, list.firstChild);
  398. //同时更新第一个(主域名)tooltip
  399. this.setTooltip(list.querySelector('.ucni-MainHost'), obj);
  400. };
  401.  
  402. info.push({value: obj.location, text: text[0]});
  403. obj.server && info.push({value: obj.server, text: text[1]});
  404.  
  405. if(this._getLocalIP && !this.localAndPublicIPs){
  406. (new Promise(this.getLocalAndPublicIPs)).then(reslut => {
  407. this.localAndPublicIPs = reslut;
  408. info.push({value: reslut.localIP, text: text[2]});
  409. info.push({value: reslut.publicIP, text: text[3]});
  410. setMainInfo(info);
  411. }, () => {setMainInfo(info);}).catch(() => {
  412. setMainInfo(info);
  413. });
  414. }else{
  415. if(this.localAndPublicIPs){
  416. info.push({value: this.localAndPublicIPs.localIP, text: text[2]});
  417. info.push({value: this.localAndPublicIPs.publicIP, text: text[3]});
  418. }
  419. setMainInfo(info);
  420. }
  421. }else{
  422. this.queryLocation(obj.ip, result => {
  423. obj.location = result.location;
  424. this.localAndPublicIPs = this.localAndPublicIPs || {};
  425. //如果不使用WebRCT方式获取内外网IP
  426. if(!this._getLocalIP){
  427. this.localAndPublicIPs.publicIP = result.publicIP;
  428. this.localAndPublicIPs.publicLocation = result.publicLocation;
  429. }else{
  430. this.localAndPublicIPs._public = [result.publicIP, result.publicLocation];
  431. }
  432. this.updateMainInfo(obj, list);
  433. });
  434. }
  435. },
  436.  
  437. queryLocation(ip, callback){
  438. let req = Cc['@mozilla.org/xmlextras/xmlhttprequest;1']
  439. .createInstance(Ci.nsIXMLHttpRequest),
  440. url = '', regex = null;
  441. if(ip.indexOf(':') > -1){
  442. regex = [/id="Span1">(?:IPv\d[^:]+:\s*)?([^<]+)(?=<br)/i,
  443. /"cz_ip">([^<]+)/i, /"cz_addr">(?:IPv\d[^:]+:\s*)?([^<]+)/i];
  444. url = `http://ip.ipv6home.cn/?ip=${ip}`;
  445. }else{
  446. regex = [/"InputIPAddrMessage">([^<]+)/i, /"cz_ip">([^<]+)/i, /"cz_addr">([^<]+)/i];
  447. url = `http://www.cz88.net/ip/index.aspx?ip=${ip}`;
  448. }
  449. req.open('GET', url, true);
  450. req.send(null);
  451. req.timeout = 10000;
  452. req.ontimeout = req.onerror = () => {
  453. callback({ip: ip, location: 'ERR 查询过程中出错,请重试。'});
  454. };
  455. req.onload = () => {
  456. if (req.status == 200) {
  457. let match = regex.map(r => req.responseText.match(r)[1]
  458. .replace(/^[^>]+>(?:IPv\d[^:]+:\s*)?|\s*CZ88.NET.*/g, ''));
  459. try{
  460. callback({
  461. ip: ip, location: match[0],
  462. publicIP: match[1], publicLocation: match[2]
  463. });
  464. }catch(ex){ req.onerror();}
  465. }
  466. };
  467. },
  468.  
  469. localAndPublicIPs: null,
  470.  
  471. getLocalAndPublicIPs(resolve, reject){
  472. let hiddenFrame = new HiddenFrame(),
  473. _RTCtimeout = null,
  474. _failedTimeout = null;
  475.  
  476. //chrome环境下会抛出异常
  477. hiddenFrame.get().then(window => {
  478. let RTCPeerConnection = window.RTCPeerConnection
  479. || window.mozRTCPeerConnection;
  480.  
  481. if(!RTCPeerConnection) {
  482. hiddenFrame.destroy();
  483. hiddenFrame = null;
  484. if(DEBUG) {
  485. console.log('%cNetwork Indicator:\n',
  486. 'color:red; font-size:120%; background-color:#ccc;',
  487. 'WebRTC功能不可用!'
  488. );
  489. }
  490. return reject();
  491. }
  492. let pc = new RTCPeerConnection(undefined, {
  493. optional: [{RtpDataChannels: true}]
  494. }), onResolve = ips => {
  495. clearTimeout(_failedTimeout);
  496. hiddenFrame.destroy();
  497. hiddenFrame = null;
  498. resolve(ips);
  499. }, ip = {}, debug = [];
  500.  
  501. let regex1 = /(?:[a-z\d]+[\:\.]+){2,}[a-z\d]+/i,
  502. regex2 = /UDP \d+ ([\da-z\.\:]+).+srflx raddr ([\da-z\.\:]+)/i;
  503. //内网IPv4,应该没有用IPv6的吧
  504. let lcRegex = /^(192\.168\.|169\.254\.|10\.|172\.(1[6-9]|2\d|3[01]))/;
  505. pc.onicecandidate = ice => {
  506. if(!ice.candidate) return;
  507. let _ip1 = ice.candidate.candidate.match(regex1),
  508. _ip2 = ice.candidate.candidate.match(regex2);
  509.  
  510. if(DEBUG) debug.push(ice.candidate.candidate);
  511.  
  512. if(Array.isArray(_ip1)){
  513. clearTimeout(_RTCtimeout);
  514. if(Array.isArray(_ip2) && _ip2.length === 3)
  515. return onResolve({publicIP: _ip2[1], localIP: _ip2[2]});
  516.  
  517. ip[lcRegex.test(_ip1[0]) ? 'localIP' : 'publicIP'] = _ip1[0];
  518. _RTCtimeout = setTimeout(()=>{
  519. onResolve(ip);
  520. }, 1000);
  521. }
  522. };
  523.  
  524.  
  525. //5s超时
  526. _failedTimeout = setTimeout(()=>{
  527. if(DEBUG) {
  528. console.log('%cNetwork Indicator:\n',
  529. 'color:red; font-size:120%; background-color:#ccc;',
  530. debug.join('\n')
  531. );
  532. }
  533. reject();
  534. hiddenFrame.destroy();
  535. hiddenFrame = null;
  536. }, 5000);
  537.  
  538. pc.createOffer(result => { pc.setLocalDescription(result);}, () => {});
  539. pc.createDataChannel('');
  540. });
  541. },
  542.  
  543. updateState(cwId = this.getWinId()){
  544. let records = this.recordInner[cwId.currentInnerWindowID] || [],
  545. state = this.getStateBySpdyVer((records.filter(re => re.isMainHost)[0] || {}).spdy),
  546. subDocsState = (records.filter(re => !re.isMainHost) || [{}]).map(re => this.getStateBySpdyVer(re.spdy));
  547. if(state == 0 && subDocsState.some(st => st != 0))
  548. state = subDocsState.some(st => st == 7) ? 2 : 1;
  549.  
  550. state = ['unknown', 'subSpdy', 'subHttp2', 'active', 'spdy2', 'spdy3', 'spdy31', 'http2'][state];
  551. if(this.icon.spdyState != state){
  552. this.icon.setAttribute('state', this.icon.spdyState = state);
  553. }
  554. },
  555.  
  556. getStateBySpdyVer(version = '0'){
  557. let state = 3;
  558. if(version === '0'){
  559. state = 0;
  560. }else if(version === '2'){
  561. state = 4;
  562. }else if(version === '3'){
  563. state = 5;
  564. }else if(version === '3.1'){
  565. state = 6;
  566. }else if(/^h2/.test(version)){
  567. state = 7;
  568. }
  569. return state;
  570. },
  571.  
  572. openPopup(event){
  573. if(event.button !== 0) return;
  574. event.view.clearTimeout(this.panel._showPanelTimeout);
  575. let currentBrowser = this.currentBrowserPanel.get(this.panel);
  576. if(gBrowser.selectedBrowser != currentBrowser || this._panelNeedUpdate){
  577. let list = this.panel._list,
  578. cwId = this.getWinId(),
  579. ri = this.recordInner[cwId.currentInnerWindowID];
  580. if(!ri) return;
  581.  
  582. if(this.panel.hasAttribute('overflowY'))
  583. this.panel.removeAttribute('overflowY');
  584. while(list.hasChildNodes())
  585. list.removeChild(list.lastChild);
  586. list._minWidth = 0;
  587.  
  588. let noneMainHost = !ri.some(re => re.isMainHost);
  589. ri.forEach((record, index) => {
  590. //类似about:addons无主域名的情况
  591. if(index == 0 && noneMainHost)
  592. record.isMainHost = true;
  593. this.dnsDetect(record, record.isMainHost);
  594. });
  595.  
  596. this.currentBrowserPanel.set(this.panel, gBrowser.selectedBrowser);
  597. //更新完毕
  598. this._panelNeedUpdate = false;
  599. }
  600.  
  601. //弹出面板
  602. let position = (this.icon.boxObject.y < (window.outerHeight / 2)) ?
  603. 'bottomcenter top' : 'topcenter bottom';
  604. position += (this.icon.boxObject.x < (window.innerWidth / 2)) ?
  605. 'left' : 'right';
  606. this.panel.openPopup(this.icon, position, 0, 0, false, false);
  607. },
  608.  
  609. updataLocation(event){
  610. let target = event.target;
  611. while(!target.hasAttribute('ucni-ip')){
  612. if(target == this.panel) return;
  613. target = target.parentNode;
  614. }
  615. let currentBrowser = this.currentBrowserPanel.get(this.panel),
  616. cwId = this.getWinId(),
  617. ri = this.recordInner[cwId.currentInnerWindowID];
  618. if(target.matches('li[ucni-ip]')){
  619. this.queryLocation(target.getAttribute('ucni-ip'), result => {
  620. //刷新所有同IP的location
  621. ri.forEach(record => {
  622. if(result.ip == record.ip){
  623. record.location = result.location;
  624. let text = this.setTooltip(target, record);
  625. if(event.altKey){
  626. this._nsIClipboardHelper.copyString(text);
  627. }
  628. }
  629. });
  630. });
  631. }
  632. },
  633.  
  634. highlightHosts(event){
  635. let host = event.target.getAttribute('host');
  636. if(!host) return;
  637. Array.prototype.forEach.call(this.panel._list.querySelectorAll(`p[host="${host}"]`), p => {
  638. let hover = p.classList.contains('ucni-hover');
  639. if(event.type === 'mouseover' ? !hover : hover) p.classList.toggle('ucni-hover');
  640. });
  641. },
  642.  
  643. setTooltip(target, obj){
  644. let text = [];
  645. if(obj.counter){
  646. text.push('连接数: ' + obj.counter);
  647. obj.scheme && obj.scheme.length && text.push('Scheme: ' + obj.scheme.join(', '));
  648. obj.spdy && obj.spdy.length && text.push('SPDY: ' + obj.spdy.join(', '));
  649. }else{
  650. text.push('所在地: ' + (obj.location || '双击获取, + Alt键同时复制。'));
  651. obj.server && text.push('服务器: ' + obj.server);
  652. obj.ip && text.push('IP地址: ' + obj.ip);
  653. }
  654. text = text.join('\n');
  655. target.setAttribute('tooltiptext', text);
  656.  
  657. return text;
  658. },
  659.  
  660. handleEvent(event){
  661. switch(event.type){
  662. case 'TabSelect':
  663. this.panel.hidePopup();
  664. this.updateState();
  665. break;
  666. case 'dblclick':
  667. let info = this.panel._list.querySelector('#ucni-mplocation > p:last-child');
  668. if(info && info.contains(event.originalTarget)){
  669. let publicIP = info.childNodes[1].textContent.trim();
  670. if(/^[a-z\.\:\d]+$/i.test(publicIP)){
  671. this.queryLocation(publicIP, result => {
  672. this.localAndPublicIPs.publicLocation = result.location;
  673. let text = this.setTooltip(info.childNodes[1], result);
  674. if(event.altKey)
  675. this._nsIClipboardHelper.copyString(text);
  676. });
  677. }
  678. }else{
  679. this.updataLocation(event);
  680. }
  681. break;
  682. case 'mouseover':
  683. case 'mouseout':
  684. this.highlightHosts(event);
  685. break;
  686. case 'mouseenter':
  687. case 'mouseleave':
  688. event.view.clearTimeout(this.panel._showPanelTimeout);
  689. if(event.type === 'mouseenter'){
  690. this.panel._showPanelTimeout =
  691. event.view.setTimeout(this.openPopup.bind(this, event), this.autoPopup);
  692. }
  693. break;
  694. case 'command':
  695. this.onContextMenuCommand(event);
  696. break;
  697. case 'contextmenu':
  698. this.panel.focus();
  699. let selection = event.view.getSelection();
  700. this.panel._contextMenu.childNodes[1].setAttribute('hidden',
  701. !this.panel.contains(selection.anchorNode) || selection.toString().trim() === '');
  702. break;
  703. default:
  704. this.openPopup(event);
  705. }
  706. },
  707.  
  708. getWinId(browser = gBrowser.selectedBrowser){
  709. if(!browser) return {};
  710. let windowUtils = browser.contentWindow
  711. .QueryInterface(Ci.nsIInterfaceRequestor)
  712. .getInterface(Ci.nsIDOMWindowUtils);
  713. return {
  714. currentInnerWindowID: windowUtils.currentInnerWindowID,
  715. outerWindowID: windowUtils.outerWindowID
  716. };
  717. },
  718.  
  719. onContextMenuCommand(event){
  720. switch(event.originalTarget._command){
  721. case 'copyAll':
  722. this._nsIClipboardHelper.copyString(this.panel._list.textContent.trim());
  723. break;
  724. case 'copySelection':
  725. this._nsIClipboardHelper.copyString(event.view.getSelection()
  726. .toString().replace(/(?:\r\n)+/g, '\n').trim());
  727. break;
  728. }
  729. },
  730.  
  731. createElement(name, attr, parent){
  732. let ns = '', e = null;
  733. if(!~['ul', 'li', 'span', 'p'].indexOf(name)){
  734. ns = 'http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul';
  735. }else{
  736. ns = 'http://www.w3.org/1999/xhtml';
  737. name = 'html:' + name;
  738. }
  739. e = document.createElementNS(ns , name);
  740. if(attr){
  741. for (let i in attr) {
  742. if(i == 'text')
  743. e.textContent = attr[i];
  744. else
  745. e.setAttribute(i, attr[i]);
  746. }
  747. }
  748. if(parent){
  749. if(Array.isArray(parent)){
  750. (parent.length == 2) ?
  751. parent[0].insertBefore(e, parent[1]) :
  752. parent[0].insertBefore(e, parent[0].firstChild);
  753. }else{
  754. parent.appendChild(e);
  755. }
  756. }
  757. return e;
  758. },
  759.  
  760. setStyle(){
  761. let sss = Cc['@mozilla.org/content/style-sheet-service;1'].getService(Ci.nsIStyleSheetService);
  762. sss.loadAndRegisterSheet(Services.io.newURI('data:text/css,' + encodeURIComponent(`
  763. @-moz-document url("chrome://browser/content/browser.xul"){
  764. #NetworkIndicator-icon{
  765. visibility: visible !important;
  766. list-style-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAAAQCAMAAADphoe6AAABRFBMVEUAAAAAzhgAAPMAzhgAzyiUlJT/fQAAzxYAj84AzhgAzhgAzRgAzhkAzhcAzhgAWN4AzhgAzhgAzhgAzhkAzxcAzhoAzRcAk9D/fQBjs2MAj84AAPOUlJSUlJSTk5P/fQAAj84AAPP/fQAAzxgAzhkAzRkAAPMAzRgAzhcAzRcAj82Tk5P/fQAAAPKUlJQAAPUA1BGUlJT/fQAAAPMAj86Tk5MAjs7/fQAAjs6UlJQAAPIAAPMAjs6UlJQAkM4AAPSWlpabm5sAj87/fQAAAPMAkM4AAPMAj86UlJT/fQAAAPL/fQAAkM//fQD/fQAAj86UlJQAAPP/fQAAAPMAkM+UlJT/fQD/fQAAjc7/fQAAAPMAj83/fQD/fQD/fQCTk5P/fQCUlJT/fQCVlZX/fQAAkc6UlJT/fQAAAPL/fQCSkpIAAACm8FOxAAAAa3RSTlMAv79+Bb+/E7+8pkpCLLMFrJmLazcmHBMSAr2pqX4qBX58fHpycG5hWU1LS0srFhAOvLyzs7OmpnBwSkJCQiwWEAasrJmZi4uLi4CAenFrZWVdQjc3NzctJiYhHByzspqamJh5YVlZWU0rHiQB3/AAAAHvSURBVEjHzdNXk9owFAVgSSAhYIHt9tIhCUmoobN0tvfed9N79P/fM06i6N5kJg/mJefJ35wZj3w8Iv9Lvp0k4omJ8f1xupWOGN/l9mJ7wA9H++39lPHygifg8bo3WeW8mgTuSBkdAucYYyXgQyFqX4EfU+oPTeFxk/NVYKshZQe4UmcsB2y/EuII2BemdGEKkwLn8QlwXspWBLjLWAy6KEQ7BRykNOCdwuQ154XyZWGsvSll/v4ib2lvMNYdnX+paL8RovhwVbS15ygNLi8+f+HWySqvxps8sfLLw6iMrjXkgf7oEmMsVmcZ7euaqK2/FFk9QshP/bNh6vG6NNnhTuJl7W3pZM3S3mJOYiPtXeFk3daep05mfW698ulkkOB8oB35eNxPR6P93z497WUY62mnPn84ywpxpu199nTGQ+mMK5t7mEDuyDRyjmWQD0WW6Dh/7gn1uLBJOd4cQ1uthgVdidUr0HZb2YoopfS9CoT1oMC4x/75rE0u+YDAXMg+8jnrIV8JReALFs2gyOofRgd4v0NQDraxM1vY2V2l4AKP5k1njHtsvED5bZLAWJs3yKONO2T73a2zgNL2zS2Z0vjvXhmjBZIFgjLMY5e62NdFoghYIBTUDTLusX8saA4wISg3EezSH75NYS953fo7l/NVa/IOl+4AAAAASUVORK5CYII=");
  767. -moz-image-region: rect(0px 16px 16px 0px);
  768. }
  769. #NetworkIndicator-icon[state=subSpdy] {
  770. -moz-image-region: rect(0px 32px 16px 16px);
  771. }
  772. #NetworkIndicator-icon[state=subHttp2] {
  773. -moz-image-region: rect(0px 48px 16px 32px);
  774. }
  775. #NetworkIndicator-icon[state=http2] {
  776. -moz-image-region: rect(0px 64px 16px 48px);
  777. }
  778. #NetworkIndicator-icon[state=active] {
  779. -moz-image-region: rect(0px 80px 16px 64px);
  780. }
  781. #NetworkIndicator-icon[state=spdy2] {
  782. -moz-image-region: rect(0px 96px 16px 80px);
  783. }
  784. #NetworkIndicator-icon[state=spdy3] {
  785. -moz-image-region: rect(0px 112px 16px 96px);
  786. }
  787. #NetworkIndicator-icon[state=spdy31] {
  788. -moz-image-region: rect(0px 128px 16px 112px);
  789. }
  790.  
  791. #NetworkIndicator-panel :-moz-any(ul, li, span, p){
  792. margin:0;
  793. padding:0;
  794. }
  795. #NetworkIndicator-panel :-moz-any(p, label){
  796. -moz-user-focus: normal;
  797. -moz-user-select: text;
  798. cursor: text!important;
  799. }
  800. #NetworkIndicator-panel .panel-arrowcontent{
  801. margin: 0;
  802. padding:5px !important;
  803. }
  804. #NetworkIndicator-panel #ucni-mplocation{
  805. flex-direction: column;
  806. }
  807. #NetworkIndicator-panel #ucni-mplocation>p{
  808. display: flex;
  809. }
  810. #NetworkIndicator-panel p.ucni-ip{
  811. font: bold 90%/1.5rem Helvetica, Arial !important;
  812. color: #2553B8;
  813. }
  814. #NetworkIndicator-panel #ucni-mplocation>p>:-moz-any(span, label){
  815. color: #666;
  816. font-size:90%;
  817. font-weight:bold;
  818. }
  819. #NetworkIndicator-panel #ucni-mplocation>p>label{
  820. color:#0055CC!important;
  821. flex:1!important;
  822. text-align: center!important;
  823. padding:0!important;
  824. margin:0 0 0 1ch!important;
  825. max-width:23em!important;
  826. }
  827.  
  828. #NetworkIndicator-panel li:nth-child(2n-1){
  829. background: #eee;
  830. }
  831. #NetworkIndicator-panel li:not(#ucni-mplocation):hover{
  832. background-color: #ccc;
  833. }
  834. #NetworkIndicator-panel p.ucni-host,
  835. #NetworkIndicator-panel li{
  836. display:flex;
  837. }
  838. #NetworkIndicator-panel li>span:last-child{
  839. flex: 1;
  840. }
  841.  
  842. #NetworkIndicator-panel p[scheme="http"]{
  843. color:#629BED;
  844. }
  845. #NetworkIndicator-panel p[scheme="https"]{
  846. color:#479900;
  847. text-shadow:0 0 1px #BDD700;
  848. }
  849. #NetworkIndicator-panel p[scheme~="https"][scheme~="http"]{
  850. color:#7A62ED;
  851. font-weight: bold;
  852. }
  853. #NetworkIndicator-panel p[scheme="https"]{
  854. color:#00CC00;
  855. }
  856. #NetworkIndicator-panel p.ucni-host[spdy]::after,
  857. #NetworkIndicator-panel p.ucni-host[counter]::before{
  858. content: attr(spdy);
  859. color: #FFF;
  860. font-weight: bold;
  861. font-size:75%;
  862. display: block;
  863. top:1px;
  864. background: #6080DF;
  865. border-radius: 3px;
  866. float: right;
  867. padding: 0 2px;
  868. margin: 3px 0 2px;
  869. }
  870. #NetworkIndicator-panel p.ucni-host[counter]::before{
  871. float: left;
  872. background: #FF9900;
  873. content: attr(counter);
  874. }
  875. #NetworkIndicator-panel p.ucni-hover:not(:hover){
  876. text-decoration:underline wavy orange;
  877. }
  878. #NetworkIndicator-panel p.ucni-host.ucni-hover{
  879. color: blue;
  880. text-shadow:0 0 1px rgba(0, 0, 255, .4);
  881. }
  882.  
  883. #NetworkIndicator-panel[overflowY] .panel-arrowcontent{
  884. height: 400px!important;
  885. overflow-y: scroll;
  886. }
  887. #NetworkIndicator-panel[overflowY] ul{
  888. position: relative;
  889. }
  890. #NetworkIndicator-panel[overflowY] #ucni-mplocation{
  891. position: sticky;
  892. top:-5px;
  893. margin-top: -5px;
  894. border-top:5px #FFF solid;
  895. }
  896. }`), null, null), sss.AGENT_SHEET);
  897. }
  898. };
  899.  
  900. networkIndicator.init();
  901. }