├── .gitignore ├── Makefile ├── README.md ├── bootstrap.js ├── chrome.manifest ├── chrome ├── content │ └── indicator.jsm ├── locale │ └── en-US │ │ └── overlay.dtd └── skin │ ├── icon-http2-active.png │ ├── icon-spdy-active.png │ ├── icon-spdy-subactive.png │ └── overlay.css ├── exclude.lst ├── icon.png ├── install.rdf └── vendor ├── icon-http2-active.svg ├── icon-spdy-active.svg ├── icon-spdy-subactive.svg └── icon.svg /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.xpi 3 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: 2 | zip moz-spdy-indicator.xpi * -r chrome --exclude @exclude.lst 3 | 4 | clean: 5 | rm *.xpi 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Firefox extension to add an HTTP/2 and SPDY support indicator in the address bar. 2 | 3 | **This extension is [now on AMO](https://addons.mozilla.org/en-US/firefox/addon/spdy-indicator/).** 4 | 5 | Inspired by http://www.devthought.com/2012/03/10/chrome-spdy-indicator/ 6 | 7 | Address bar icon by http://fontawesome.io/ - [SIL OFL 1.1](http://scripts.sil.org/OFL) license. 8 | 9 | ## License 10 | 11 | Copyright © 2012-2015 Cheng Sun <\_@chengsun.uk> 12 | 13 | Licensed under the [WTFPL version 2](http://www.wtfpl.net/txt/copying/). 14 | -------------------------------------------------------------------------------- /bootstrap.js: -------------------------------------------------------------------------------- 1 | const {classes: Cc, interfaces: Ci, utils: Cu} = Components; 2 | Cu.import("resource://gre/modules/Services.jsm"); 3 | 4 | function startup(data, reason) { 5 | Cu.import("chrome://spdyindicator/content/indicator.jsm"); 6 | SPDYManager.startup(); 7 | } 8 | 9 | function shutdown(data, reason) { 10 | if (reason == APP_SHUTDOWN) return; 11 | 12 | Cu.import("chrome://spdyindicator/content/indicator.jsm"); 13 | SPDYManager.shutdown(); 14 | Cu.unload("chrome://spdyindicator/content/indicator.jsm"); 15 | } 16 | 17 | function install(data, reason) { 18 | // o hai there new friend! :D 19 | } 20 | function uninstall(data, reason) { 21 | // y u do this to me :( 22 | } 23 | -------------------------------------------------------------------------------- /chrome.manifest: -------------------------------------------------------------------------------- 1 | content spdyindicator chrome/content/ 2 | locale spdyindicator en-US chrome/locale/en-US/ 3 | skin spdyindicator default chrome/skin/ 4 | -------------------------------------------------------------------------------- /chrome/content/indicator.jsm: -------------------------------------------------------------------------------- 1 | var EXPORTED_SYMBOLS = ["SPDYManager"]; 2 | 3 | const {classes: Cc, interfaces: Ci, utils: Cu} = Components; 4 | Cu.import("resource://gre/modules/Services.jsm"); 5 | 6 | // this is kludgy: in order to prevent unbounded memory growth, 7 | // we limit the number of SPDY-requested prePaths to this number 8 | const SPDYREQUESTS_MAXSIZE = 20; 9 | 10 | function getLoadContext(request) { 11 | let loadContext = null; 12 | try { 13 | loadContext = request.QueryInterface(Ci.nsIChannel) 14 | .notificationCallbacks 15 | .getInterface(Ci.nsILoadContext); 16 | } catch (e) { 17 | try { 18 | loadContext = request.loadGroup 19 | .notificationCallbacks 20 | .getInterface(Ci.nsILoadContext); 21 | } catch (e) {} 22 | } 23 | 24 | return loadContext; 25 | } 26 | 27 | // get the chrome window from a content window 28 | function getChromeWindow(domWindow) { 29 | return domWindow.QueryInterface(Ci.nsIInterfaceRequestor) 30 | .getInterface(Ci.nsIWebNavigation) 31 | .QueryInterface(Ci.nsIDocShellTreeItem) 32 | .rootTreeItem 33 | .QueryInterface(Ci.nsIInterfaceRequestor) 34 | .getInterface(Ci.nsIDOMWindow); 35 | } 36 | 37 | const prefBranchName = "extensions.spdyindicator."; 38 | var SPDYManager = { 39 | // private 40 | indicators: [], 41 | branch: Services.prefs.getBranch(prefBranchName), 42 | 43 | createIndicator: function (window) { 44 | let indicator = new SPDYIndicator(window); 45 | indicator.start(); 46 | this.indicators.push(indicator); 47 | return indicator; 48 | }, 49 | 50 | newWindowListener: function (subject, topic, data) { 51 | let window = subject.QueryInterface(Ci.nsIDOMWindow); 52 | switch (topic) { 53 | case "domwindowopened": 54 | let self = this; 55 | window.addEventListener('load', function onLoad() { 56 | window.removeEventListener('load', onLoad, false); 57 | if (window.document.documentElement.getAttribute('windowtype') === "navigator:browser") { 58 | self.createIndicator(window); 59 | } 60 | }, false); 61 | break; 62 | case "domwindowclosed": 63 | for (let i in this.indicators) { 64 | if (this.indicators[i].window === window) { 65 | this.indicators[i].stop(); 66 | this.indicators.splice(i, 1); 67 | break; 68 | } 69 | } 70 | break; 71 | } 72 | }, 73 | 74 | setDefaultPrefs: function () { 75 | let defaultBranch = Services.prefs.getDefaultBranch(prefBranchName); 76 | defaultBranch.setIntPref("minShowState", 2); 77 | defaultBranch.setBoolPref("showDebug", false); 78 | }, 79 | 80 | observe: function (subject, topic, data) { 81 | switch (topic) { 82 | case "http-on-examine-response": 83 | case "http-on-examine-cached-response": 84 | case "http-on-examine-merged-response": 85 | subject.QueryInterface(Ci.nsIHttpChannel); 86 | 87 | // make sure we are requested via SPDY 88 | let spdyHeader = null; 89 | try { 90 | spdyHeader = subject.getResponseHeader("X-Firefox-Spdy"); 91 | } catch (e) {} 92 | if (!spdyHeader || !spdyHeader.length) return; 93 | 94 | // find the browser which this request originated from 95 | let loadContext = getLoadContext(subject); 96 | if (!loadContext) return; 97 | 98 | let browser = loadContext.topFrameElement; 99 | let domWindow = null; 100 | 101 | if (browser) { 102 | domWindow = browser.contentWindow 103 | .QueryInterface(Ci.nsISupports); 104 | } else { 105 | try { 106 | domWindow = loadContext.associatedWindow; 107 | } catch (e) {} 108 | } 109 | 110 | if (!domWindow) return; 111 | let window = getChromeWindow(domWindow); 112 | 113 | // notify the appropriate indicator 114 | let indicator = null; 115 | for (let i in this.indicators) { 116 | if (this.indicators[i].window === window) { 117 | indicator = this.indicators[i]; 118 | break; 119 | } 120 | } 121 | if (!indicator) { 122 | debug("Could not find indicator from chrome window for request"); 123 | } else { 124 | indicator.spdyRequested(domWindow, browser, subject.URI, spdyHeader); 125 | } 126 | break; 127 | } 128 | }, 129 | 130 | // used by bootstrap.js 131 | startup: function (browser) { 132 | this.setDefaultPrefs(); 133 | 134 | // add response header observers 135 | Services.obs.addObserver(this, "http-on-examine-response", false); 136 | Services.obs.addObserver(this, "http-on-examine-merged-response", false); 137 | Services.obs.addObserver(this, "http-on-examine-cached-response", false); 138 | 139 | // add indicators for existing windows 140 | let browserEnum = Services.wm.getEnumerator("navigator:browser"); 141 | while (browserEnum.hasMoreElements()) { 142 | this.createIndicator(browserEnum.getNext()); 143 | } 144 | // listen for window open/close events 145 | Services.ww.registerNotification(this.newWindowListener.bind(this)); 146 | }, 147 | 148 | shutdown: function () { 149 | // remove response header observers 150 | Services.obs.removeObserver(this, "http-on-examine-response"); 151 | Services.obs.removeObserver(this, "http-on-examine-merged-response"); 152 | Services.obs.removeObserver(this, "http-on-examine-cached-response"); 153 | 154 | // stop and destroy indicators for existing windows 155 | for (let i in this.indicators) { 156 | this.indicators[i].stop(); 157 | } 158 | this.indicators = []; 159 | // stop listening for window open/close events 160 | Services.ww.unregisterNotification(this.newWindowListener); 161 | }, 162 | 163 | // used by SPDYIndicator 164 | indicatorStates: [ 165 | { 166 | name: "unknown", 167 | tooltip: "SPDY state unknown", 168 | }, { 169 | name: "inactive", 170 | tooltip: "SPDY is inactive", 171 | }, { 172 | name: "subactive", 173 | tooltip: "HTTP/2 or SPDY is active for some sub-documents included in the top-level document", 174 | }, { 175 | name: "spdy", 176 | tooltip: "SPDY is active for the top-level document", 177 | }, { 178 | name: "spdy2", 179 | tooltip: "SPDY 2 is active for the top-level document", 180 | }, { 181 | name: "spdy3", 182 | tooltip: "SPDY 3 is active for the top-level document", 183 | }, { 184 | name: "spdy31", 185 | tooltip: "SPDY 3.1 is active for the top-level document", 186 | }, { 187 | name: "http2", 188 | tooltip: "HTTP/2 is active for the top-level document", 189 | } 190 | ], 191 | 192 | getMinShowState: function () { 193 | return this.branch.getIntPref("minShowState"); 194 | }, 195 | getShowDebug: function () { 196 | return this.branch.getBoolPref("showDebug"); 197 | } 198 | }; 199 | 200 | function debug(s) { 201 | if (SPDYManager.getShowDebug()) { 202 | try { 203 | throw new Error("dummy"); 204 | } catch (e) { 205 | var stack = e.stack.split('\n'); 206 | stack.splice(0, 1); 207 | Services.console.logStringMessage("SPDYIndicator: " + s + "\n" + stack.join('\n')); 208 | } 209 | } 210 | } 211 | 212 | function SPDYIndicator(window) { 213 | this.window = window; 214 | this.browser = window.QueryInterface(Ci.nsIInterfaceRequestor) 215 | .getInterface(Ci.nsIDOMWindow) 216 | .gBrowser; 217 | this._update_bound = this.update.bind(this); 218 | this.tabProgressListener.onLocationChange = this.tabProgressListener.onLocationChange.bind(this); 219 | debug("SPDYIndicator created"); 220 | } 221 | 222 | SPDYIndicator.prototype = { 223 | piStylesheet: null, 224 | 225 | start: function () { 226 | // insert our stylesheet 227 | this.piStylesheet = this.window.document.createProcessingInstruction('xml-stylesheet', 228 | 'href="chrome://spdyindicator/skin/overlay.css" type="text/css"'); 229 | this.window.document.insertBefore(this.piStylesheet, this.window.document.firstChild); 230 | 231 | // create icon 232 | let spdyIndicator = this.window.document.createElement('image'); 233 | spdyIndicator.id = 'spdyindicator-icon'; 234 | spdyIndicator.className = 'urlbar-icon'; 235 | 236 | // insert icon in urlbar 237 | let urlbarIcons = this.window.document.getElementById('urlbar-icons'); 238 | urlbarIcons.insertBefore(spdyIndicator, urlbarIcons.firstChild); 239 | 240 | // add browser event listeners 241 | this.browser.addEventListener("pageshow", this._update_bound, true); 242 | this.browser.addEventListener("select", this._update_bound, false); 243 | 244 | // add tab location listener 245 | this.browser.addTabsProgressListener(this.tabProgressListener); 246 | 247 | debug("SPDYIndicator started"); 248 | this.update(); 249 | }, 250 | 251 | stop: function () { 252 | // remove stylesheet 253 | if (this.piStylesheet.parentNode) { 254 | this.piStylesheet.parentNode.removeChild(this.piStylesheet); 255 | } else { 256 | debug("SPDYIndicator could not find stylesheet"); 257 | } 258 | 259 | 260 | // remove icon 261 | let spdyIndicator = this.window.document.getElementById('spdyindicator-icon'); 262 | if (spdyIndicator.parentNode) { 263 | spdyIndicator.parentNode.removeChild(spdyIndicator); 264 | } else { 265 | debug("SPDYIndicator could not find icon"); 266 | } 267 | 268 | // remove browser event listeners 269 | this.browser.removeEventListener("pageshow", this._update_bound, true); 270 | this.browser.removeEventListener("select", this._update_bound, false); 271 | 272 | // remove tab location listener 273 | this.browser.removeTabsProgressListener(this.tabProgressListener); 274 | 275 | debug("SPDYIndicator stopped"); 276 | }, 277 | 278 | getState: function (browser) { 279 | return browser.getUserData("__spdyindicator_state") || 0; 280 | }, 281 | setState: function (browser, newState) { 282 | browser.setUserData("__spdyindicator_state", newState, null); 283 | this.updateIndicator(browser); 284 | }, 285 | updateState: function (browser, newState) { 286 | let oldState = this.getState(browser); 287 | if (newState > oldState) 288 | this.setState(browser, newState); 289 | }, 290 | 291 | isTopLevelSPDY: function (browser) { 292 | let spdyRequests = browser.getUserData("__spdyindicator_spdyrequests"); 293 | let currentPath = browser.getUserData("__spdyindicator_path") || 294 | browser.currentURI.prePath; 295 | return (spdyRequests && spdyRequests.indexOf(currentPath) !== -1); 296 | }, 297 | 298 | updateStateForSPDY: function (browser) { 299 | let spdyVersions = browser.getUserData("__spdyindicator_spdyversions"); 300 | let currentPath = browser.getUserData("__spdyindicator_path") || 301 | browser.currentURI.prePath; 302 | 303 | let state = 3; 304 | if (spdyVersions && spdyVersions.hasOwnProperty(currentPath)) { 305 | let version = spdyVersions[currentPath]; 306 | if (version.match(/^h2/)) { 307 | state = 7; 308 | } else if (version === "3.1") { 309 | state = 6; 310 | } else if (version === "3") { 311 | state = 5; 312 | } else if (version === "2") { 313 | state = 4; 314 | } 315 | } 316 | this.updateState(browser, state); 317 | }, 318 | 319 | update: function () { 320 | this.updateIndicator(this.browser.selectedBrowser); 321 | }, 322 | 323 | updateIndicator: function(browser) { 324 | // change indicator state 325 | let indicator = this.window.document.getElementById("spdyindicator-icon"); 326 | if (indicator) { 327 | let state = this.getState(browser); 328 | let indicatorState = SPDYManager.indicatorStates[state]; 329 | indicator.setAttribute("hidden", state < SPDYManager.getMinShowState()); 330 | indicator.setAttribute("state", indicatorState.name); 331 | indicator.setAttribute("tooltiptext", indicatorState.tooltip); 332 | } else { 333 | debug("SPDYIndicator could not find icon element"); 334 | } 335 | }, 336 | _update_bound: null, 337 | 338 | spdyRequested: function (domWindow, browser, uri, version) { 339 | debug("Requested " + uri.asciiSpec); 340 | debug("SPDY Version " + version); 341 | 342 | if (!browser) { 343 | browser = this.browser.getBrowserForDocument(domWindow.top.document); 344 | } 345 | if (!browser) return; 346 | 347 | let spdyRequests = browser.getUserData("__spdyindicator_spdyrequests") || []; 348 | let spdyVersions = browser.getUserData("__spdyindicator_spdyversions") || {}; 349 | if (spdyRequests.indexOf(uri.prePath) === -1) { 350 | spdyRequests.push(uri.prePath); 351 | spdyVersions[uri.prePath] = version; 352 | } 353 | if (spdyRequests.length > SPDYREQUESTS_MAXSIZE) { 354 | let splices = spdyRequests.splice(0, spdyRequests.length - SPDYREQUESTS_MAXSIZE); 355 | splices.forEach(function (element) { 356 | if (spdyVersions.hasOwnProperty(element)) { 357 | delete spdyVersions[element]; 358 | } 359 | }); 360 | } 361 | browser.setUserData("__spdyindicator_spdyrequests", spdyRequests, null); 362 | browser.setUserData("__spdyindicator_spdyversions", spdyVersions, null); 363 | 364 | if (this.isTopLevelSPDY(browser)) { 365 | this.updateStateForSPDY(browser); 366 | } else { 367 | this.updateState(browser, 2); 368 | } 369 | }, 370 | 371 | tabProgressListener: { 372 | onLocationChange: function (browser, webProgress, request, URI, flags) { 373 | browser.setUserData("__spdyindicator_path", URI.prePath, null); 374 | this.setState(browser, 0); 375 | if (this.isTopLevelSPDY(browser)) { 376 | this.updateStateForSPDY(browser); 377 | } 378 | }, 379 | onProgressChange: function () {}, 380 | onSecurityChange: function () {}, 381 | onStateChange: function () {}, 382 | onStatusChange: function () {} 383 | }, 384 | }; 385 | 386 | // vim: filetype=javascript 387 | -------------------------------------------------------------------------------- /chrome/locale/en-US/overlay.dtd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /chrome/skin/icon-http2-active.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chengsun/moz-spdy-indicator/fccfc3ca26a94f1b4ce25772dfdcb32ce48f89e9/chrome/skin/icon-http2-active.png -------------------------------------------------------------------------------- /chrome/skin/icon-spdy-active.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chengsun/moz-spdy-indicator/fccfc3ca26a94f1b4ce25772dfdcb32ce48f89e9/chrome/skin/icon-spdy-active.png -------------------------------------------------------------------------------- /chrome/skin/icon-spdy-subactive.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chengsun/moz-spdy-indicator/fccfc3ca26a94f1b4ce25772dfdcb32ce48f89e9/chrome/skin/icon-spdy-subactive.png -------------------------------------------------------------------------------- /chrome/skin/overlay.css: -------------------------------------------------------------------------------- 1 | #spdyindicator-icon[state=http2] { 2 | list-style-image: url("chrome://spdyindicator/skin/icon-http2-active.png"); 3 | } 4 | #spdyindicator-icon[state^=spdy] { 5 | list-style-image: url("chrome://spdyindicator/skin/icon-spdy-active.png"); 6 | } 7 | #spdyindicator-icon[state=subactive] { 8 | list-style-image: url("chrome://spdyindicator/skin/icon-spdy-subactive.png"); 9 | } 10 | #spdyindicator-icon { 11 | cursor: default; 12 | height: 16px; 13 | } 14 | -------------------------------------------------------------------------------- /exclude.lst: -------------------------------------------------------------------------------- 1 | exclude.lst 2 | Makefile 3 | README.md 4 | .DS_Store 5 | *.xpi 6 | vendor/* 7 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chengsun/moz-spdy-indicator/fccfc3ca26a94f1b4ce25772dfdcb32ce48f89e9/icon.png -------------------------------------------------------------------------------- /install.rdf: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | spdyindicator@chengsun.github.com 6 | 2.4 7 | 2 8 | true 9 | 10 | 11 | 12 | {ec8030f7-c20a-464f-9b0e-13a3a9e97384} 13 | 10.* 14 | 56.* 15 | 16 | 17 | 18 | HTTP/2 and SPDY indicator 19 | An indicator showing HTTP/2 and SPDY support in the address bar. 20 | Cheng Sun 21 | http://github.com/chengsun/moz-spdy-indicator 22 | 23 | 24 | -------------------------------------------------------------------------------- /vendor/icon-http2-active.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /vendor/icon-spdy-active.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /vendor/icon-spdy-subactive.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /vendor/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 13 | 15 | 18 | 22 | 26 | 31 | 37 | 42 | 43 | 46 | 50 | 54 | 59 | 65 | 70 | 71 | 72 | 74 | 75 | 77 | image/svg+xml 78 | 80 | 81 | 82 | 83 | 84 | 87 | 95 | HTTP 2 109 | SPDY 123 | 124 | 128 | 129 | --------------------------------------------------------------------------------