Network Indicator

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

Vous devrez installer une extension telle que Tampermonkey, Greasemonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Userscripts pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension de gestionnaire de script utilisateur pour installer ce script.

(J'ai déjà un gestionnaire de scripts utilisateur, laissez-moi l'installer !)

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

(J'ai déjà un gestionnaire de style utilisateur, laissez-moi l'installer!)

// ==UserScript==
// @name			Network Indicator
// @version			0.0.8
// @compatibility	FF34+
// @description		显示当前页面连接IP 和SPDY、HTTP/2
// @include			main
// @namespace https://greasyfork.org/users/25642
// ==/UserScript==

'use strict';

if (location == 'chrome://browser/content/browser.xul') {

	const AUTO_POPUP = 600; //鼠标悬停图标上自动弹出面板的延时,非负整数,单位毫秒。0为禁用。

	const DEBUG = false; //调试开关
	const GET_LOCAL_IP = true; //是否启用获取显示内(如果有)外网IP。基于WebRTC,
								//如无法显示,请确保about:config中的media.peerconnection.enabled的值为true,
								//或者将上面的 “DEBUG”的值改为true,重启FF,打开浏览器控制台(ctrl+shift+j),
								//弹出面板后,将有关输出适宜打ma,复制发给我看看。
								//还有可能会被AdBlock, Ghostery等扩展阻止。
								//若关闭则只显示外网IP

	const HTML_NS = 'http://www.w3.org/1999/xhtml';
	const XUL_PAGE = 'data:application/vnd.mozilla.xul+xml;charset=utf-8,<window%20id="win"/>';
	let promise = {};
	try{
		Cu.import('resource://gre/modules/PromiseUtils.jsm', promise);
		promise = promise.PromiseUtils;
	}catch(ex){
		Cu.import('resource://gre/modules/Promise.jsm', promise);
		promise = promise.Promise;
	}
	let HiddenFrame = function() {};
	HiddenFrame.prototype = {
		_frame: null,
		_deferred: null,
		_retryTimerId: null,
		get hiddenDOMDocument() {
			return Services.appShell.hiddenDOMWindow.document;
		},
		get isReady() {
			return this.hiddenDOMDocument.readyState === 'complete';
		},
		get() {
			if (!this._deferred) {
				this._deferred = promise.defer();
				this._create();
			}
			return this._deferred.promise;
		},
		destroy() {
			clearTimeout(this._retryTimerId);
			if (this._frame) {
				if (!Cu.isDeadWrapper(this._frame)) {
					this._frame.removeEventListener('load', this, true);
					this._frame.remove();
				}
				this._frame = null;
				this._deferred = null;
			}
		},
		handleEvent() {
			let contentWindow = this._frame.contentWindow;
			if (contentWindow.location.href === XUL_PAGE) {
				this._frame.removeEventListener('load', this, true);
				this._deferred.resolve(contentWindow);
			} else {
				contentWindow.location = XUL_PAGE;
			}
		},
		_create() {
			if (this.isReady) {
				let doc = this.hiddenDOMDocument;
				this._frame = doc.createElementNS(HTML_NS, 'iframe');
				this._frame.addEventListener('load', this, true);
				doc.documentElement.appendChild(this._frame);
			} else {
				this._retryTimerId = setTimeout(this._create.bind(this), 0);
			}
		}
	};

	let networkIndicator = {

		autoPopup: AUTO_POPUP,

		_getLocalIP: GET_LOCAL_IP,

		init(){
			if(this.icon) return;
			this.setStyle();
			this.icon.addEventListener('click', this, false);
			if(this.autoPopup){
				this.icon.addEventListener('mouseenter', this, false);
				this.icon.addEventListener('mouseleave', this, false);
			}
			['dblclick', 'mouseover', 'mouseout', 'command', 'contextmenu'].forEach(event => {
				this.panel.addEventListener(event, this, false);
			});
			gBrowser.tabContainer.addEventListener('TabSelect', this, false);
			['content-document-global-created', 'inner-window-destroyed', 'outer-window-destroyed',
			 'http-on-examine-cached-response', 'http-on-examine-response'].forEach(topic => {
				Services.obs.addObserver(this, topic, false);
			});
		},

		_icon: null,
		_panel: null,

		get icon (){
			if(!this._icon){
				this._icon = document.getElementById('NetworkIndicator-icon') || 
					this.createElement('image', {id: 'NetworkIndicator-icon', class: 'urlbar-icon'},
						[document.getElementById('urlbar-icons')]);
				return false;
			}
			return this._icon;
		},

		get panel (){
			if(!this._panel){
				let cE = this.createElement;
				this._panel = document.getElementById('NetworkIndicator-panel') || 
					cE('panel', {
						id: 'NetworkIndicator-panel',
						type: 'arrow'
					}, document.getElementById('mainPopupSet'));
				this._panel._contextMenu = cE('menupopup', {id: 'NetworkIndicator-contextMenu'}, this._panel);
				cE('menuitem', {label: '复制全部'}, this._panel._contextMenu)._command = 'copyAll';
				cE('menuitem', {label: '复制选中'}, this._panel._contextMenu)._command = 'copySelection';
				this._panel._list = cE('ul', {}, cE('vbox', {context: 'NetworkIndicator-contextMenu'}, this._panel));
			}
			return this._panel;
		},

		currentBrowserPanel: new WeakMap(),
		_panelNeedUpdate: false,

		observe(subject, topic, data) {
			if(topic == 'http-on-examine-response' || topic == 'http-on-examine-cached-response'){
				this.onExamineResponse(subject, topic);
			}else if(topic == 'inner-window-destroyed'){
				let innerID = subject.QueryInterface(Ci.nsISupportsPRUint64).data;
				delete this.recordInner[innerID];
				if(this.getWinId().currentInnerWindowID != innerID){
					this._panelNeedUpdate = true;
					this.updateState();
				}
			}else if(topic == 'outer-window-destroyed'){
				let outerID = subject.QueryInterface(Ci.nsISupportsPRUint64).data,
					cwId = this.getWinId();
				delete this.recordOuter[outerID];
				if(cwId.outerWindowID != outerID){
					this._panelNeedUpdate = true;
					this.updateState();
					//从一般网页后退到无网络请求的页面(例如about:xxx)应关闭面板。
					if(!this.recordInner[cwId.currentInnerWindowID])
						this.panel.hidePopup && this.panel.hidePopup();
				}
			}else if(topic == 'content-document-global-created'){
				let domWinUtils = subject.top
									.QueryInterface(Ci.nsIInterfaceRequestor)
									.getInterface(Ci.nsIDOMWindowUtils),
					outerID = domWinUtils.outerWindowID,
					innerID = domWinUtils.currentInnerWindowID,
					ro = this.recordOuter[outerID];
				if(!ro) return;
				let mainHost = ro.pop(),
					ri = this.recordInner[innerID];
				//标记主域名
				mainHost.isMainHost = true;
				this.recordInner[innerID] = [mainHost];
				delete this.recordOuter[outerID];
			}
		},

		//记录缓存对象
		recordOuter: {},
		recordInner: {},

		onExamineResponse(subject, topic) {
			let channel = subject.QueryInterface(Ci.nsIHttpChannel),
				nc = channel.notificationCallbacks || channel.loadGroup && channel.loadGroup.notificationCallbacks,
				domWinUtils = null,
				domWindow = null;
			if(!nc || (channel.loadFlags & Ci.nsIChannel.LOAD_REQUESTMASK) == 5120){
				//前进后退读取Cache需更新panel
				return this._panelNeedUpdate = topic == 'http-on-examine-cached-response';
			}
			try{
					domWindow = nc.getInterface(Ci.nsIDOMWindow);
					domWinUtils = domWindow.top
									.QueryInterface(Ci.nsIInterfaceRequestor)
									.getInterface(Ci.nsIDOMWindowUtils);
			}catch(ex){
				//XHR响应处理
				let ww = null;
				try{
					ww = subject.notificationCallbacks.getInterface(Ci.nsILoadContext);
				}catch(ex1){
					try{
						ww = subject.loadGroup.notificationCallbacks.getInterface(Ci.nsILoadContext);
					}catch(ex2){}
				}
				if(!ww) return;
				try{domWindow = ww.associatedWindow;}catch(ex3){}
				domWinUtils = this.getWinId(ww.topFrameElement);
			}

			let isMainHost = (channel.loadFlags & Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI
					&& domWindow && domWindow == domWindow.top);

			//排除ChromeWindow的、unload等事件触发的请求响应
			if(!domWinUtils || (channel.loadFlags == 640 && !subject.loadGroup)
				|| domWindow instanceof Ci.nsIDOMChromeWindow
				|| (!isMainHost && channel.loadInfo && channel.loadInfo.loadingDocument
					&& channel.loadInfo.loadingDocument.ownerGlobal === null)
			) return;

			let outerID = domWinUtils.outerWindowID,
				innerID = domWinUtils.currentInnerWindowID,
				newentry = Object.create(null),
				cwId = this.getWinId();

			newentry.host = channel.URI.asciiHost;
			newentry.scheme = channel.URI.scheme;
			//newentry.url = channel.URI.asciiSpec;
			channel.remoteAddress && (newentry.ip = channel.remoteAddress);

			channel.QueryInterface(Ci.nsIHttpChannelInternal);
			try{
				//获取响应头的服务器、SPDY、HTTP/2信息
				channel.visitResponseHeaders({
					visitHeader(name, value){
						let lowerName = name.toLowerCase();
						if (lowerName == 'server') {
							newentry.server = value
						}else if(lowerName == 'x-firefox-spdy'){
							newentry.spdy = value
						}
					}
				});
			}catch(ex){}

			if(isMainHost){
				newentry.url = channel.URI.asciiSpec;
				outerID && (this.recordOuter[outerID] || (this.recordOuter[outerID] = [])).push(newentry);
				if(this.panel.state != 'closed'){
					if(cwId.outerWindowID == outerID){
						if(this.panel.hasAttribute('overflowY'))
							this.panel.removeAttribute('overflowY');
						let list = this.panel._list;
						while(list.hasChildNodes())
							list.removeChild(list.lastChild);
						list._minWidth = 0;
					}
				}
			}else{
				innerID && (this.recordInner[innerID] || (this.recordInner[innerID] = [])).push(newentry);
				//newentry.loadFlags = channel.loadFlags
			}

			//更新图标状态
			if(cwId.outerWindowID == outerID || cwId.currentInnerWindowID == innerID)
				this.updateState(cwId);

			//当且仅当主动点击打开显示面板时才查询IP位置、更新面板信息。
			//避免每次刷新页面都请求查询网站的IP,以减少暴露隐私的可能、性能消耗。
			if(this.panel.state != 'closed' && (cwId.outerWindowID == outerID || cwId.currentInnerWindowID == innerID)){
				//标记下次点击显示时是否需更新面板内容
				if(this._panelNeedUpdate = !(this.recordInner[cwId.currentInnerWindowID] || [{}]).some(re => re.isMainHost))
					this.panel.hidePopup(); //类似about:addons页面情况下,刷新时必须关闭面板,避免计数叠加。

				this.dnsDetect(newentry, isMainHost);
			}else{
				this._panelNeedUpdate = true;
			}
		},

		_nsIDNSService: Cc['@mozilla.org/network/dns-service;1'].createInstance(Ci.nsIDNSService),

		_nsIClipboardHelper: Cc['@mozilla.org/widget/clipboardhelper;1'].getService(Ci.nsIClipboardHelper),

		dnsDetect(obj, isMainHost){
			if(obj.ip) return this.updatePanel(obj, isMainHost);
			this._nsIDNSService.asyncResolve(obj.host, this._nsIDNSService.RESOLVE_BYPASS_CACHE, {
				onLookupComplete: (request, records, status) => {
					if (!Components.isSuccessCode(status)) return;
					obj.ip = records.getNextAddrAsString();
					this.updatePanel(obj, isMainHost);
				}
			}, null);
		},

		updatePanel(record, isMainHost){
			let cE = this.createElement,
				list = this.panel._list,
				li = list.querySelector(`li[ucni-ip="${record.ip}"]`),
				p = null;

			if(!li){//不存在相同的IP
				let fragment = document.createDocumentFragment(),
					ipSpan = null;
				li = cE('li', {'ucni-ip': record.ip}, fragment);
				cE('p', {class: 'ucni-ip', text: record.ip + '\n'}, ipSpan = cE('span', {}, li));
				// + '\n' 复制时增加换行格式
				p = cE('p', {class: 'ucni-host', host: record.host, scheme: record.scheme, counter: 1, text: record.host + '\n'}, cE('span', {}, li));
				p._connCounter = 1;
				p._connScheme = [record.scheme];
				if(isMainHost){
					//标记主域名
					li.classList.add('ucni-MainHost');
					//主域名重排列至首位
					list.insertBefore(fragment, list.firstChild);
					//更新主域名 IP位置
					this.updateMainInfo(record, list);
				}else{
					list.appendChild(fragment);
					//不存在相同的IP且非主域名
					this.setTooltip(li, record);
				}

				//调整容器宽度以适应IP长度
				let minWidth = record.ip.length - record.ip.split(/:|\./).length / 2 + 1;
				if(list._minWidth && minWidth > list._minWidth){
					Array.prototype.forEach.call(list.querySelectorAll('li>span:first-child'), span => {
						if(!span._width || span._width < minWidth)
							span.style.minWidth =  `${span._width = list._minWidth = minWidth}ch`;;
					});
				}else{
					if(!list._minWidth) list._minWidth = minWidth;
					ipSpan.style.minWidth = `${ipSpan._width = list._minWidth}ch`;
				}
			}else{//相同的IP
				p = li.querySelector(`.ucni-host[host="${record.host}"]`);
				if(!p){//同IP不同的域名
					p = cE('p', {class: 'ucni-host', host: record.host, scheme: record.scheme, counter: 1, text: record.host + '\n'}, li.querySelector('.ucni-host').parentNode);
					p._connCounter = 1;
					p._connScheme = [record.scheme];
				}else{//同IP同域名
					p.setAttribute('counter', ++p._connCounter); //计数+1

					if(p._connScheme.every(s => s != record.scheme)){
						//同IP同域名不同的协议
						p._connScheme.push(record.scheme);
						p.setAttribute('scheme', p._connScheme.join(' '));
					}
				}
				if(isMainHost){
					li.classList.add('ucni-MainHost');
					if(list.firstChild != li){
						list.insertBefore(li, list.firstChild);
						li.lastChild.insertBefore(p, li.lastChild.firstChild);
					}
					this.updateMainInfo(record, list);
				}
			}

			if(this.panel.popupBoxObject.height > 500 && !this.panel.hasAttribute('overflowY')){
				this.panel.setAttribute('overflowY', true);
			}

			if(record.spdy && (!p.spdy || p.spdy.every(s => s != record.spdy))){
				(p.spdy || (p.spdy = [])).push(record.spdy);
				p.setAttribute('spdy', p.spdy.join(' '));
			}

			this.setTooltip(p, {
				counter: p._connCounter,
				server: record.server,
				scheme: p._connScheme || [record.scheme],
				spdy: p.spdy
			});
		},

		updateMainInfo(obj, list) {
			if(obj.location){
				if(list.querySelector('#ucni-mplocation')) return;
				let cE = this.createElement,
					fm = document.createDocumentFragment(),
					li = cE('li', {id: 'ucni-mplocation'}, fm),
					timeStamp = new Date().getTime(),
					text = ['所在地', '服务器', '内网IP', '外网IP'],
					info = [];
				let setMainInfo = info => {
					let location = this.localAndPublicIPs.publicLocation;
					if(this.localAndPublicIPs._public){
						info.push({value: this.localAndPublicIPs._public[0], text: text[3]});
						location = this.localAndPublicIPs._public[1];
					}
					for(let i of info){
						if(!i.value) continue;
						let label = cE('label', {text: i.value + '\n'}, cE('span', {text: i.text + ': '}, cE('p', {}, li)).parentNode);
						if(i.text === text[3]) this.setTooltip(label, { ip: i.value, location: location });
					}
					list.insertBefore(fm, list.firstChild);
					//同时更新第一个(主域名)tooltip
					this.setTooltip(list.querySelector('.ucni-MainHost'), obj);
				};

				info.push({value: obj.location, text: text[0]});
				obj.server && info.push({value: obj.server, text: text[1]});

				if(this._getLocalIP && !this.localAndPublicIPs){
					(new Promise(this.getLocalAndPublicIPs)).then(reslut => {
						this.localAndPublicIPs = reslut;
						info.push({value: reslut.localIP, text: text[2]});
						info.push({value: reslut.publicIP, text: text[3]});
						setMainInfo(info);
					}, () => {setMainInfo(info);}).catch(() => {
						setMainInfo(info);
					});
				}else{
					if(this.localAndPublicIPs){
						info.push({value: this.localAndPublicIPs.localIP, text: text[2]});
						info.push({value: this.localAndPublicIPs.publicIP, text: text[3]});
					}
					setMainInfo(info);
				}
			}else{
				this.queryLocation(obj.ip, result => {
					obj.location = result.location;
					this.localAndPublicIPs = this.localAndPublicIPs || {};
					//如果不使用WebRCT方式获取内外网IP
					if(!this._getLocalIP){
						this.localAndPublicIPs.publicIP = result.publicIP;
						this.localAndPublicIPs.publicLocation = result.publicLocation;
					}else{
						this.localAndPublicIPs._public = [result.publicIP, result.publicLocation];
					}
					this.updateMainInfo(obj, list);
				});
			}
		},

		queryLocation(ip, callback){
			let req = Cc['@mozilla.org/xmlextras/xmlhttprequest;1']
						.createInstance(Ci.nsIXMLHttpRequest),
				url = '', regex = null;
			if(ip.indexOf(':') > -1){
				regex = [/id="Span1">(?:IPv\d[^:]+:\s*)?([^<]+)(?=<br)/i,
						/"cz_ip">([^<]+)/i, /"cz_addr">(?:IPv\d[^:]+:\s*)?([^<]+)/i];
				url = `http://ip.ipv6home.cn/?ip=${ip}`;
			}else{
				regex = [/"InputIPAddrMessage">([^<]+)/i, /"cz_ip">([^<]+)/i, /"cz_addr">([^<]+)/i];
				url = `http://www.cz88.net/ip/index.aspx?ip=${ip}`;
			}
			req.open('GET', url, true);
			req.send(null);
			req.timeout = 10000;
			req.ontimeout = req.onerror = () => {
				callback({ip: ip, location: 'ERR 查询过程中出错,请重试。'});
			};
			req.onload = () => {
				if (req.status == 200) {
					let match = regex.map(r => req.responseText.match(r)[1]
							.replace(/^[^>]+>(?:IPv\d[^:]+:\s*)?|\s*CZ88.NET.*/g, ''));
					try{
						callback({
							ip: ip, location: match[0],
							publicIP: match[1], publicLocation: match[2]
						});
					}catch(ex){ req.onerror();}
				}
			};
		},

		localAndPublicIPs: null,

		getLocalAndPublicIPs(resolve, reject){
			let hiddenFrame = new HiddenFrame(),
				_RTCtimeout = null,
				_failedTimeout = null;

			//chrome环境下会抛出异常
			hiddenFrame.get().then(window => {
				let RTCPeerConnection = window.RTCPeerConnection
					|| window.mozRTCPeerConnection;

				if(!RTCPeerConnection) {
					hiddenFrame.destroy();
					hiddenFrame = null;
					if(DEBUG) {
						console.log('%cNetwork Indicator:\n', 
							'color:red; font-size:120%; background-color:#ccc;',
							'WebRTC功能不可用!'
						);
					}
					return reject();
				}
				let pc = new RTCPeerConnection(undefined, {
					optional: [{RtpDataChannels: true}]
				}), onResolve = ips => {
					clearTimeout(_failedTimeout);
					hiddenFrame.destroy();
					hiddenFrame = null;
					resolve(ips);
				}, ip = {}, debug = [];

				let regex1 = /(?:[a-z\d]+[\:\.]+){2,}[a-z\d]+/i,
					regex2 = /UDP \d+ ([\da-z\.\:]+).+srflx raddr ([\da-z\.\:]+)/i;
				//内网IPv4,应该没有用IPv6的吧
				let lcRegex = /^(192\.168\.|169\.254\.|10\.|172\.(1[6-9]|2\d|3[01]))/;
				pc.onicecandidate = ice => {
					if(!ice.candidate) return;
					let _ip1 = ice.candidate.candidate.match(regex1),
						_ip2 = ice.candidate.candidate.match(regex2);

					if(DEBUG) debug.push(ice.candidate.candidate);

					if(Array.isArray(_ip1)){
						clearTimeout(_RTCtimeout);
						if(Array.isArray(_ip2) && _ip2.length === 3)
							return onResolve({publicIP: _ip2[1], localIP: _ip2[2]});

						ip[lcRegex.test(_ip1[0]) ? 'localIP' : 'publicIP'] = _ip1[0];
						
						_RTCtimeout = setTimeout(()=>{
							onResolve(ip);
						}, 1000);
					}
				};


				//5s超时
				_failedTimeout = setTimeout(()=>{
					if(DEBUG) {
						console.log('%cNetwork Indicator:\n', 
							'color:red; font-size:120%; background-color:#ccc;',
							debug.join('\n')
						);
					}
					reject();
					hiddenFrame.destroy();
					hiddenFrame = null;
				}, 5000);

				pc.createOffer(result => { pc.setLocalDescription(result);}, () => {});
				pc.createDataChannel('');
			});
		},

		updateState(cwId = this.getWinId()){
			let records = this.recordInner[cwId.currentInnerWindowID] || [],
				state = this.getStateBySpdyVer((records.filter(re => re.isMainHost)[0] || {}).spdy),
				subDocsState = (records.filter(re => !re.isMainHost) || [{}]).map(re => this.getStateBySpdyVer(re.spdy));
			if(state == 0 && subDocsState.some(st => st != 0))
				state = subDocsState.some(st => st == 7) ? 2 : 1;

			state = ['unknown', 'subSpdy', 'subHttp2', 'active', 'spdy2', 'spdy3', 'spdy31', 'http2'][state];
			if(this.icon.spdyState != state){
				this.icon.setAttribute('state', this.icon.spdyState = state);
			}
		},

		getStateBySpdyVer(version = '0'){
			let state = 3;
			if(version === '0'){
				state = 0;
			}else if(version === '2'){
				state = 4;
			}else if(version === '3'){
				state = 5;
			}else if(version === '3.1'){
				state = 6;
			}else if(/^h2/.test(version)){
				state = 7;
			}
			return state;
		},

		openPopup(event){
			if(event.button !== 0) return;
			event.view.clearTimeout(this.panel._showPanelTimeout);
			let currentBrowser = this.currentBrowserPanel.get(this.panel);
			if(gBrowser.selectedBrowser != currentBrowser || this._panelNeedUpdate){
				let list = this.panel._list,
					cwId = this.getWinId(),
					ri = this.recordInner[cwId.currentInnerWindowID];
				if(!ri) return;

				if(this.panel.hasAttribute('overflowY'))
						this.panel.removeAttribute('overflowY');
				while(list.hasChildNodes())
					list.removeChild(list.lastChild);
				list._minWidth = 0;

				let noneMainHost = !ri.some(re => re.isMainHost);
				ri.forEach((record, index) => {
					//类似about:addons无主域名的情况
					if(index == 0 && noneMainHost)
						record.isMainHost = true;
					this.dnsDetect(record, record.isMainHost);
				});

				this.currentBrowserPanel.set(this.panel, gBrowser.selectedBrowser);
				//更新完毕
				this._panelNeedUpdate = false;
			}

			//弹出面板
			let position = (this.icon.boxObject.y < (window.outerHeight / 2)) ?
					'bottomcenter top' : 'topcenter bottom';
			position += (this.icon.boxObject.x < (window.innerWidth / 2)) ?
								'left' : 'right';
			this.panel.openPopup(this.icon, position, 0, 0, false, false);
		},

		updataLocation(event){
			let target = event.target;
			while(!target.hasAttribute('ucni-ip')){
				if(target == this.panel) return;
				target = target.parentNode;
			}
			let currentBrowser = this.currentBrowserPanel.get(this.panel),
				cwId = this.getWinId(),
				ri = this.recordInner[cwId.currentInnerWindowID];
			if(target.matches('li[ucni-ip]')){
				this.queryLocation(target.getAttribute('ucni-ip'), result => {
					//刷新所有同IP的location
					ri.forEach(record => {
						if(result.ip == record.ip){
							record.location = result.location;
							let text = this.setTooltip(target, record);
							if(event.altKey){
								this._nsIClipboardHelper.copyString(text);
							}
						}
					});
				});
			}
		},

		highlightHosts(event){
			let host = event.target.getAttribute('host');
			if(!host) return;
			Array.prototype.forEach.call(this.panel._list.querySelectorAll(`p[host="${host}"]`), p => {
				let hover = p.classList.contains('ucni-hover');
				if(event.type === 'mouseover' ? !hover : hover) p.classList.toggle('ucni-hover');
			});
		},

		setTooltip(target, obj){
			let text = [];
			if(obj.counter){
				text.push('连接数:   ' + obj.counter);
				obj.scheme && obj.scheme.length && text.push('Scheme:   ' + obj.scheme.join(', '));
				obj.spdy && obj.spdy.length && text.push('SPDY:    ' + obj.spdy.join(', '));
			}else{
				text.push('所在地:   ' + (obj.location || '双击获取, + Alt键同时复制。'));
				obj.server && text.push('服务器:   ' + obj.server);
				obj.ip && text.push('IP地址:   ' + obj.ip);
			}
			text = text.join('\n');
			target.setAttribute('tooltiptext', text);

			return text;
		},

		handleEvent(event){
			switch(event.type){
				case 'TabSelect':
					this.panel.hidePopup();
					this.updateState();
					break;
				case 'dblclick':
					let info = this.panel._list.querySelector('#ucni-mplocation > p:last-child');
					if(info && info.contains(event.originalTarget)){
						let publicIP = info.childNodes[1].textContent.trim();
						if(/^[a-z\.\:\d]+$/i.test(publicIP)){
							this.queryLocation(publicIP, result => {
								this.localAndPublicIPs.publicLocation = result.location;
								let text = this.setTooltip(info.childNodes[1], result);
								if(event.altKey)
									this._nsIClipboardHelper.copyString(text);
							});
						}
					}else{
						this.updataLocation(event);
					}
					break;
				case 'mouseover':
				case 'mouseout':
					this.highlightHosts(event);
					break;
				case 'mouseenter':
				case 'mouseleave':
					event.view.clearTimeout(this.panel._showPanelTimeout);
					if(event.type === 'mouseenter'){
						this.panel._showPanelTimeout =
							event.view.setTimeout(this.openPopup.bind(this, event), this.autoPopup);
					}
					break;
				case 'command':
					this.onContextMenuCommand(event);
					break;
				case 'contextmenu':
					this.panel.focus();
					let selection = event.view.getSelection();
					this.panel._contextMenu.childNodes[1].setAttribute('hidden', 
						!this.panel.contains(selection.anchorNode) || selection.toString().trim() === '');
					break;
				default:
					this.openPopup(event);
			}
		},

		getWinId(browser = gBrowser.selectedBrowser){
			if(!browser) return {};
			let windowUtils = browser.contentWindow
								.QueryInterface(Ci.nsIInterfaceRequestor)
								.getInterface(Ci.nsIDOMWindowUtils);
			return {
				currentInnerWindowID: windowUtils.currentInnerWindowID,
				outerWindowID: windowUtils.outerWindowID
			};
		},

		onContextMenuCommand(event){
			switch(event.originalTarget._command){
				case 'copyAll':
					this._nsIClipboardHelper.copyString(this.panel._list.textContent.trim());
					break;
				case 'copySelection':
					this._nsIClipboardHelper.copyString(event.view.getSelection()
						.toString().replace(/(?:\r\n)+/g, '\n').trim());
					break;
			}
		},

		createElement(name, attr, parent){
			let ns = '', e = null;
			if(!~['ul', 'li', 'span', 'p'].indexOf(name)){
				ns = 'http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul';
			}else{
				ns = 'http://www.w3.org/1999/xhtml';
				name = 'html:' + name;
			}
			e = document.createElementNS(ns , name);
			if(attr){
				for (let i in attr) {
					if(i == 'text')
						e.textContent = attr[i];
					else
						e.setAttribute(i, attr[i]);
				}
			}
			if(parent){
				if(Array.isArray(parent)){
					(parent.length == 2) ? 
						parent[0].insertBefore(e, parent[1]) :
						parent[0].insertBefore(e, parent[0].firstChild);
				}else{
					parent.appendChild(e);
				}
			}
			return e;
		},

		setStyle(){
			let sss = Cc['@mozilla.org/content/style-sheet-service;1'].getService(Ci.nsIStyleSheetService);
			sss.loadAndRegisterSheet(Services.io.newURI('data:text/css,' + encodeURIComponent(`
			@-moz-document url("chrome://browser/content/browser.xul"){
				#NetworkIndicator-icon{
					visibility: visible !important;
					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=");
					-moz-image-region: rect(0px 16px 16px 0px);
				}
				#NetworkIndicator-icon[state=subSpdy] {
					-moz-image-region: rect(0px 32px 16px 16px);
				}
				#NetworkIndicator-icon[state=subHttp2] {
					-moz-image-region: rect(0px 48px 16px 32px);
				}
				#NetworkIndicator-icon[state=http2] {
					-moz-image-region: rect(0px 64px 16px 48px);
				}
				#NetworkIndicator-icon[state=active] {
					-moz-image-region: rect(0px 80px 16px 64px);
				}
				#NetworkIndicator-icon[state=spdy2] {
					-moz-image-region: rect(0px 96px 16px 80px);
				}
				#NetworkIndicator-icon[state=spdy3] {
					-moz-image-region: rect(0px 112px 16px 96px);
				}
				#NetworkIndicator-icon[state=spdy31] {
					-moz-image-region: rect(0px 128px 16px 112px);
				}

				#NetworkIndicator-panel :-moz-any(ul, li, span, p){
					margin:0;
					padding:0;
				}
				#NetworkIndicator-panel :-moz-any(p, label){
					-moz-user-focus: normal;
					-moz-user-select: text;
					cursor: text!important;
				}
				#NetworkIndicator-panel .panel-arrowcontent{
					margin: 0;
					padding:5px !important;
				}
				#NetworkIndicator-panel #ucni-mplocation{
					flex-direction: column;
				}
				#NetworkIndicator-panel #ucni-mplocation>p{
					display: flex;
				}
				#NetworkIndicator-panel p.ucni-ip{
					font: bold 90%/1.5rem Helvetica, Arial !important;
					color: #2553B8;
				}
				#NetworkIndicator-panel #ucni-mplocation>p>:-moz-any(span, label){
					color: #666;
					font-size:90%;
					font-weight:bold;
				}
				#NetworkIndicator-panel #ucni-mplocation>p>label{
					color:#0055CC!important;
					flex:1!important;
					text-align: center!important;
					padding:0!important;
					margin:0 0 0 1ch!important;
					max-width:23em!important;
				}

				#NetworkIndicator-panel li:nth-child(2n-1){
					background: #eee;
				}
				#NetworkIndicator-panel li:not(#ucni-mplocation):hover{
					background-color: #ccc;
				}
				#NetworkIndicator-panel p.ucni-host,
				#NetworkIndicator-panel li{
					display:flex;
				}
				#NetworkIndicator-panel li>span:last-child{
					flex: 1;
				}

				#NetworkIndicator-panel p[scheme="http"]{
					color:#629BED;
				}
				#NetworkIndicator-panel p[scheme="https"]{
					color:#479900;
					text-shadow:0 0 1px #BDD700;
				}
				#NetworkIndicator-panel p[scheme~="https"][scheme~="http"]{
					color:#7A62ED;
					font-weight: bold;
				}
				#NetworkIndicator-panel p[scheme="https"]{
					color:#00CC00;
				}
				#NetworkIndicator-panel p.ucni-host[spdy]::after,
				#NetworkIndicator-panel p.ucni-host[counter]::before{
					content: attr(spdy);
					color: #FFF;
					font-weight: bold;
					font-size:75%;
					display: block;
					top:1px;
					background: #6080DF;
					border-radius: 3px;
					float: right;
					padding: 0 2px;
					margin: 3px 0 2px;
				}
				#NetworkIndicator-panel p.ucni-host[counter]::before{
					float: left;
					background: #FF9900;
					content: attr(counter);
				}
				#NetworkIndicator-panel p.ucni-hover:not(:hover){
					text-decoration:underline wavy orange;
				}
				#NetworkIndicator-panel p.ucni-host.ucni-hover{
					color: blue;
					text-shadow:0 0 1px rgba(0, 0, 255, .4);
				}

				#NetworkIndicator-panel[overflowY] .panel-arrowcontent{
					height: 400px!important;
					overflow-y: scroll;
				}
				#NetworkIndicator-panel[overflowY] ul{
					position: relative;
				}
				#NetworkIndicator-panel[overflowY] #ucni-mplocation{
					position: sticky;
					top:-5px;
					margin-top: -5px;
					border-top:5px #FFF solid;
				}
			}`), null, null), sss.AGENT_SHEET);
		}
	};

	networkIndicator.init();
}