diff options
Diffstat (limited to 'data/extensions/https-everywhere-eff@eff.org/components/ssl-observatory.js')
-rw-r--r-- | data/extensions/https-everywhere-eff@eff.org/components/ssl-observatory.js | 945 |
1 files changed, 945 insertions, 0 deletions
diff --git a/data/extensions/https-everywhere-eff@eff.org/components/ssl-observatory.js b/data/extensions/https-everywhere-eff@eff.org/components/ssl-observatory.js new file mode 100644 index 0000000..76cc116 --- /dev/null +++ b/data/extensions/https-everywhere-eff@eff.org/components/ssl-observatory.js @@ -0,0 +1,945 @@ +const Ci = Components.interfaces; +const Cc = Components.classes; +const Cr = Components.results; + +const CI = Components.interfaces; +const CC = Components.classes; +const CR = Components.results; + +// Log levels +let VERB=1; +let DBUG=2; +let INFO=3; +let NOTE=4; +let WARN=5; + +let BASE_REQ_SIZE=4096; +let TIMEOUT = 60000; +let MAX_OUTSTANDING = 20; // Max # submission XHRs in progress +let MAX_DELAYED = 32; // Max # XHRs are waiting around to be sent or retried + +let ASN_PRIVATE = -1; // Do not record the ASN this cert was seen on +let ASN_IMPLICIT = -2; // ASN can be learned from connecting IP +let ASN_UNKNOWABLE = -3; // Cert was seen in the absence of [trustworthy] Internet access + +// XXX: We should make the _observatory tree relative. +let LLVAR="extensions.https_everywhere.LogLevel"; + +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); +Components.utils.import("resource://gre/modules/ctypes.jsm"); + +// Alias to reduce the number of spurious warnings from amo-validator. +let tcypes = ctypes; + +const OS = Cc['@mozilla.org/observer-service;1'].getService(CI.nsIObserverService); + +const SERVICE_CTRID = "@eff.org/ssl-observatory;1"; +const SERVICE_ID=Components.ID("{0f9ab521-986d-4ad8-9c1f-6934e195c15c}"); +const SERVICE_NAME = "Anonymously Submits SSL certificates to EFF for security auditing."; +const LOADER = CC["@mozilla.org/moz/jssubscript-loader;1"].getService(CI.mozIJSSubScriptLoader); + +const _INCLUDED = {}; + +const INCLUDE = function(name) { + if (arguments.length > 1) + for (var j = 0, len = arguments.length; j < len; j++) + INCLUDE(arguments[j]); + else if (!_INCLUDED[name]) { + try { + LOADER.loadSubScript("chrome://https-everywhere/content/code/" + + name + ".js"); + _INCLUDED[name] = true; + } catch(e) { + dump("INCLUDE " + name + ": " + e + "\n"); + } + } +} + +INCLUDE('Root-CAs'); +INCLUDE('sha256'); +INCLUDE('X509ChainWhitelist'); +INCLUDE('NSS'); + +function SSLObservatory() { + this.prefs = CC["@mozilla.org/preferences-service;1"] + .getService(CI.nsIPrefBranch); + + try { + // Check for torbutton + var tor_logger_component = CC["@torproject.org/torbutton-logger;1"]; + if (tor_logger_component) { + this.tor_logger = + tor_logger_component.getService(CI.nsISupports).wrappedJSObject; + this.torbutton_installed = true; + } + } catch(e) { + this.torbutton_installed = false; + } + + this.HTTPSEverywhere = CC["@eff.org/https-everywhere;1"] + .getService(Components.interfaces.nsISupports) + .wrappedJSObject; + + /* The proxy test result starts out null until the test is attempted. + * This is for UI notification purposes */ + this.proxy_test_successful = null; + this.proxy_test_callback = null; + this.cto_url = "https://check.torproject.org/?TorButton=true"; + // a regexp to match the above URL + this.cto_regexp = RegExp("^https://check\\.torproject\\.org/"); + + this.public_roots = root_ca_hashes; + + // Clear these on cookies-cleared observer event + this.already_submitted = {}; + this.delayed_submissions = {}; + OS.addObserver(this, "cookie-changed", false); + + // Figure out the url to submit to + this.submit_host = null; + this.findSubmissionTarget(); + + // Used to track current number of pending requests to the server + this.current_outstanding_requests = 0; + + // Generate nonce to append to url, to catch in nsIProtocolProxyFilter + // and to protect against CSRF + this.csrf_nonce = "#"+Math.random().toString()+Math.random().toString(); + + this.compatJSON = Cc["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON); + + // Register observer + OS.addObserver(this, "http-on-examine-response", false); + + // Register protocolproxyfilter + this.pps = CC["@mozilla.org/network/protocol-proxy-service;1"] + .getService(CI.nsIProtocolProxyService); + + this.pps.registerFilter(this, 0); + this.wrappedJSObject = this; + + this.client_asn = ASN_PRIVATE; + if (this.myGetBoolPref("send_asn")) + this.setupASNWatcher(); + + try { + NSS.initialize(""); + } catch(e) { + this.log(WARN, "Failed to initialize NSS component:" + e); + } + + this.testProxySettings(); + + this.log(DBUG, "Loaded observatory component!"); +} + +SSLObservatory.prototype = { + // QueryInterface implementation, e.g. using the generateQI helper + QueryInterface: XPCOMUtils.generateQI( + [ CI.nsIObserver, + CI.nsIProtocolProxyFilter, + //CI.nsIWifiListener, + CI.nsIBadCertListener2]), + + wrappedJSObject: null, // Initialized by constructor + + // properties required for XPCOM registration: + classDescription: SERVICE_NAME, + classID: SERVICE_ID, + contractID: SERVICE_CTRID, + + // https://developer.mozilla.org/En/How_to_check_the_security_state_of_an_XMLHTTPRequest_over_SSL + getSSLCert: function(channel) { + try { + // Do we have a valid channel argument? + if (!channel instanceof Ci.nsIChannel) { + return null; + } + var secInfo = channel.securityInfo; + + // Print general connection security state + if (secInfo instanceof Ci.nsITransportSecurityInfo) { + secInfo.QueryInterface(Ci.nsITransportSecurityInfo); + } else { + return null; + } + + if (secInfo instanceof Ci.nsISSLStatusProvider) { + return secInfo.QueryInterface(Ci.nsISSLStatusProvider). + SSLStatus.QueryInterface(Ci.nsISSLStatus).serverCert; + } + return null; + } catch(err) { + return null; + } + }, + + findSubmissionTarget: function() { + // Compute the URL that the Observatory will currently submit to + var host = this.prefs.getCharPref("extensions.https_everywhere._observatory.server_host"); + // Rebuild the regexp iff the host has changed + if (host != this.submit_host) { + this.submit_host = host; + this.submit_url = "https://" + host + "/submit_cert"; + this.submission_regexp = RegExp("^" + this.regExpEscape(this.submit_url)); + } + }, + + regExpEscape: function(s) { + // Borrowed from the Closure Library, + // https://closure-library.googlecode.com/svn/docs/closure_goog_string_string.js.source.html + return String(s).replace(/([-()\[\]{}+?*.$\^|,:#<!\\])/g, '\\$1').replace(/\x08/g, '\\x08'); + }, + + notifyCertProblem: function(socketInfo, status, targetSite) { + this.log(NOTE, "cert warning for " + targetSite); + if (targetSite == "observatory.eff.org") { + this.log(WARN, "Surpressing observatory warning"); + return true; + } + return false; + }, + + setupASNWatcher: function() { + this.getClientASN(); + this.max_ap = null; + + // we currently do not actually do *any* ASN watching from the client + // (in other words, the db will not have ASNs for certs submitted + // through Tor, even if the user checks the "send ASN" option) + // all of this code for guessing at changes in our public IP via WiFi hints + // is therefore disabled + /* + // Observe network changes to get new ASNs + OS.addObserver(this, "network:offline-status-changed", false); + var pref_service = Cc["@mozilla.org/preferences-service;1"] + .getService(Ci.nsIPrefBranchInternal); + var proxy_branch = pref_service.QueryInterface(Ci.nsIPrefBranchInternal); + proxy_branch.addObserver("network.proxy", this, false); + + try { + var wifi_service = Cc["@mozilla.org/wifi/monitor;1"].getService(Ci.nsIWifiMonitor); + wifi_service.startWatching(this); + } catch(e) { + this.log(INFO, "Failed to register ASN change monitor: "+e); + }*/ + }, + + stopASNWatcher: function() { + this.client_asn = ASN_PRIVATE; + /* + // unhook the observers we registered above + OS.removeObserver(this, "network:offline-status-changed"); + var pref_service = Cc["@mozilla.org/preferences-service;1"] + .getService(Ci.nsIPrefBranchInternal); + var proxy_branch = pref_service.QueryInterface(Ci.nsIPrefBranchInternal); + proxy_branch.removeObserver(this, "network.proxy"); + try { + var wifi_service = Cc["@mozilla.org/wifi/monitor;1"].getService(Ci.nsIWifiMonitor); + wifi_service.stopWatching(this); + } catch(e) { + this.log(WARN, "Failed to stop wifi state monitor: "+e); + }*/ + }, + + getClientASN: function() { + // Fetch a new client ASN.. + if (!this.myGetBoolPref("send_asn")) { + this.client_asn = ASN_PRIVATE; + return; + } + else if (!this.torbutton_installed) { + this.client_asn = ASN_IMPLICIT; + return; + } + // XXX As a possible base case: the user is running Tor, is not using + // bridges, and has send_asn enabled: should we ping an eff.org URL to + // learn our ASN? + return; + }, + + /* + // Wifi status listener + onChange: function(accessPoints) { + try { + var max_ap = accessPoints[0].mac; + } catch(e) { + return null; // accessPoints[0] is undefined + } + var max_signal = accessPoints[0].signal; + var old_max_present = false; + for (var i=0; i<accessPoints.length; i++) { + if (accessPoints[i].mac == this.max_ap) { + old_max_present = true; + } + if (accessPoints[i].signal > max_signal) { + max_ap = accessPoints[i].mac; + max_signal = accessPoints[i].signal; + } + } + this.max_ap = max_ap; + if (!old_max_present) { + this.log(INFO, "Old access point is out of range. Getting new ASN"); + this.getClientASN(); + } else { + this.log(DBUG, "Old access point is still in range."); + } + }, + + // Wifi status listener + onError: function(value) { + // XXX: Do we care? + this.log(NOTE, "ASN change observer got an error: "+value); + this.getClientASN(); + }, + */ + + // Calculate the MD5 fingerprint for a cert. This is the fingerprint of the + // DER-encoded form, same as the result of + // openssl x509 -md5 -fingerprint -noout + // We use this because the SSL Observatory depends in many places on a special + // fingerprint which is the concatenation of MD5+SHA1, and the MD5 fingerprint + // is no longer available on the cert object. + // Implementation cribbed from + // https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Reference/Interface/nsICryptoHash + md5Fingerprint: function(cert) { + var len = new Object(); + var derData = cert.getRawDER(len); + var ch = CC["@mozilla.org/security/hash;1"].createInstance(CI.nsICryptoHash); + ch.init(ch.MD5); + ch.update(derData,derData.length); + var h = ch.finish(false); + + function toHexString(charCode) { + return ("0" + charCode.toString(16)).slice(-2); + } + return [toHexString(h.charCodeAt(i)) for (i in h)].join("").toUpperCase(); + }, + + ourFingerprint: function(cert) { + // Calculate our custom fingerprint from an nsIX509Cert + return (this.md5Fingerprint(cert)+cert.sha1Fingerprint).replace(":", "", "g"); + }, + + observe: function(subject, topic, data) { + if (topic == "cookie-changed" && data == "cleared") { + this.already_submitted = {}; + this.delayed_submissions = {}; + this.log(INFO, "Cookies were cleared. Purging list of pending and already submitted certs"); + return; + } + + if (topic == "nsPref:changed") { + // XXX: We somehow need to only call this once. Right now, we'll make + // like 3 calls to getClientASN().. The only thing I can think + // of is a timer... + if (data == "network.proxy.ssl" || data == "network.proxy.ssl_port" || + data == "network.proxy.socks" || data == "network.proxy.socks_port") { + this.log(INFO, "Proxy settings have changed. Getting new ASN"); + this.getClientASN(); + } + return; + } + + if (topic == "network:offline-status-changed" && data == "online") { + this.log(INFO, "Browser back online. Getting new ASN."); + this.getClientASN(); + return; + } + + if ("http-on-examine-response" == topic) { + + if (!this.observatoryActive()) return; + + var host_ip = "-1"; + var httpchannelinternal = subject.QueryInterface(Ci.nsIHttpChannelInternal); + try { + host_ip = httpchannelinternal.remoteAddress; + } catch(e) { + this.log(INFO, "Could not get server IP address."); + } + subject.QueryInterface(Ci.nsIHttpChannel); + var certchain = this.getSSLCert(subject); + if (certchain) { + var chainEnum = certchain.getChain(); + var chainArray = []; + var chainArrayFpStr = ''; + var fps = []; + for(var i = 0; i < chainEnum.length; i++) { + var cert = chainEnum.queryElementAt(i, Ci.nsIX509Cert); + chainArray.push(cert); + var fp = this.ourFingerprint(cert); + fps.push(fp); + chainArrayFpStr = chainArrayFpStr + fp; + } + var chain_hash = sha256_digest(chainArrayFpStr).toUpperCase(); + this.log(INFO, "SHA-256 hash of cert chain for "+new String(subject.URI.host)+" is "+ chain_hash); + + if(!this.myGetBoolPref("use_whitelist")) { + this.log(WARN, "Not using whitelist to filter cert chains."); + } + else if (this.isChainWhitelisted(chain_hash)) { + this.log(INFO, "This cert chain is whitelisted. Not submitting."); + return; + } + else { + this.log(INFO, "Cert chain is NOT whitelisted. Proceeding with submission."); + } + + if (subject.URI.port == -1) { + this.submitChain(chainArray, fps, new String(subject.URI.host), subject, host_ip, false); + } else { + this.submitChain(chainArray, fps, subject.URI.host+":"+subject.URI.port, subject, host_ip, false); + } + } + } + }, + + observatoryActive: function() { + + if (!this.myGetBoolPref("enabled")) + return false; + + if (this.torbutton_installed && this.proxy_test_successful) { + // Allow Tor users to choose if they want to submit + // during tor and/or non-tor + if (this.myGetBoolPref("submit_during_tor") && + this.prefs.getBoolPref("extensions.torbutton.tor_enabled")) + return true; + + if (this.myGetBoolPref("submit_during_nontor") && + !this.prefs.getBoolPref("extensions.torbutton.tor_enabled")) + return true; + + return false; + } + + if (this.proxy_test_successful) { + return true; + } else if (this.myGetBoolPref("use_custom_proxy")) { + // no torbutton; the custom proxy is probably the user opting to + // submit certs without strong anonymisation. Because the + // anonymisation is weak, we avoid submitting during private browsing + // mode. + try { + var pbs = CC["@mozilla.org/privatebrowsing;1"].getService(CI.nsIPrivateBrowsingService); + if (pbs.privateBrowsingEnabled) return false; + } catch (e) { /* seamonkey or old firefox */ } + + return true; + } + + return false; + }, + + myGetBoolPref: function(prefstring) { + // syntactic sugar + return this.prefs.getBoolPref ("extensions.https_everywhere._observatory." + prefstring); + }, + + isChainWhitelisted: function(chainhash) { + if (X509ChainWhitelist == null) { + this.log(WARN, "Could not find whitelist of popular certificate chains, so ignoring whitelist"); + return false; + } + if (X509ChainWhitelist[chainhash] != null) { + return true; + } + return false; + }, + + findRootInChain: function(certArray) { + // Return the position in the chain Array of the/a root CA + var rootidx = -1; + var nextInChain = certArray[0].issuer; + for (var i = 0; i < certArray.length; i++) { + // Find the next cert in the valid chain + if (certArray[i].equals(nextInChain)) { + if (certArray[i].issuerName == certArray[i].subjectName) { + // All X509 root certs are self-signed + this.log(INFO, "Got root cert at position: "+i); + rootidx = i; + break; + } else { + // This is an intermediate CA cert; keep looking for the root + nextInChain = certArray[i].issuer; + } + } + } + return rootidx; + }, + + processConvergenceChain: function(chain) { + // Make sure the chain we're working with is sane, even if Convergence is + // present. + + // Convergence currently performs MITMs against the Firefox in order to + // get around https://bugzilla.mozilla.org/show_bug.cgi?id=644640. The + // end-entity cert produced by Convergence contains a copy of the real + // end-entity cert inside an X509v3 extension. We extract this and send + // it rather than the Convergence certs. + var convergence = Components.classes['@thoughtcrime.org/convergence;1']; + if (!convergence) return null; + convergence = convergence.getService().wrappedJSObject; + if (!convergence || !convergence.enabled) return null; + + this.log(INFO, "Convergence uses its own internal root certs; not submitting those"); + + //this.log(WARN, convergence.certificateStatus.getVerificiationStatus(chain.certArray[0])); + try { + var certInfo = this.extractRealLeafFromConveregenceLeaf(chain.certArray[0]); + var b64Cert = certInfo["certificate"]; + var certDB = Cc["@mozilla.org/security/x509certdb;1"].getService(Ci.nsIX509CertDB); + chain.leaf = certDB.constructX509FromBase64(b64Cert); + chain.certArray = [chain.leaf]; + chain.fps = [this.ourFingerprint(chain.leaf)]; + } catch (e) { + this.log(WARN, "Failed to extract leaf cert from Convergence cert " + e); + chain.certArray = chain.certArray.slice(0,1); + chain.fps = chain.fps.slice(0,1); + } + + }, + + extractRealLeafFromConveregenceLeaf: function(certificate) { + // Copied from Convergence's CertificateStatus.getVerificiationStatus + var len = {}; + var derEncoding = certificate.getRawDER(len); + + var derItem = NSS.types.SECItem(); + derItem.data = NSS.lib.ubuffer(derEncoding); + derItem.len = len.value; + + var completeCertificate = NSS.lib.CERT_DecodeDERCertificate(derItem.address(), 1, null); + + var extItem = NSS.types.SECItem(); + var status = NSS.lib.CERT_FindCertExtension(completeCertificate, + NSS.lib.SEC_OID_NS_CERT_EXT_COMMENT, + extItem.address()); + if (status != -1) { + var encoded = ''; + var asArray = tcypes.cast(extItem.data, tcypes.ArrayType(tcypes.unsigned_char, extItem.len).ptr).contents; + var marker = false; + + for (var i=0;i<asArray.length;i++) { + if (marker) { + encoded += String.fromCharCode(asArray[i]); + } else if (asArray[i] == 0x00) { + marker = true; + } + } + + return JSON.parse(encoded); + } + }, + + shouldSubmit: function(chain, domain) { + // Return true if we should submit this chain to the SSL Observatory + var rootidx = this.findRootInChain(chain.certArray); + var ss= false; + + if (chain.leaf.issuerName == chain.leaf.subjectName) + ss = true; + + if (!this.myGetBoolPref("self_signed") && ss) { + this.log(INFO, "Not submitting self-signed cert for " + domain); + return false; + } + + if (!ss && !this.myGetBoolPref("alt_roots")) { + if (rootidx == -1) { + // A cert with an unknown/absent Issuer. Out of caution, don't submit these + this.log(INFO, "Cert for " + domain + " issued by unknown CA " + + chain.leaf.issuerName + " (not submitting due to settings)"); + return false; + } else if (!(chain.fps[rootidx] in this.public_roots)) { + // A cert with a known but non-public Issuer + this.log(INFO, "Got a private root cert. Ignoring domain " + +domain+" with root "+chain.fps[rootidx]); + return false; + } + } + + if (chain.fps[0] in this.already_submitted) { + this.log(INFO, "Already submitted cert for "+domain+". Ignoring"); + return false; + } + return true; + }, + + submitChain: function(certArray, fps, domain, channel, host_ip, resubmitting) { + var base64Certs = []; + // Put all this chain data in one object so that it can be modified by + // subroutines if required + var c = {}; c.certArray = certArray; c.fps = fps; c.leaf = certArray[0]; + this.processConvergenceChain(c); + if (!this.shouldSubmit(c,domain)) return; + + // only try to submit now if there aren't too many outstanding requests + if (this.current_outstanding_requests > MAX_OUTSTANDING) { + this.log(WARN, "Too many outstanding requests ("+this.current_outstanding_requests+"), not submitting"); + + // if there are too many current requests but not too many + // delayed/pending ones, then delay this one + if (Object.keys(this.delayed_submissions).length < MAX_DELAYED) + if (!(c.fps[0] in this.delayed_submissions)) { + this.log(WARN, "Planning to retry submission..."); + let retry = function() { this.submitChain(certArray, fps, domain, channel, host_ip, true); }; + this.delayed_submissions[c.fps[0]] = retry; + } + return; + } + + for (var i = 0; i < c.certArray.length; i++) { + var len = new Object(); + var derData = c.certArray[i].getRawDER(len); + let result = ""; + for (let j = 0, dataLength = derData.length; j < dataLength; ++j) + result += String.fromCharCode(derData[j]); + base64Certs.push(btoa(result)); + } + + var reqParams = []; + reqParams.push("domain="+domain); + reqParams.push("server_ip="+host_ip); + if (this.myGetBoolPref("testing")) { + reqParams.push("testing=1"); + // The server can compute these, but they're a nice test suite item! + reqParams.push("fplist="+this.compatJSON.encode(c.fps)); + } + reqParams.push("certlist="+this.compatJSON.encode(base64Certs)); + + if (resubmitting) { + reqParams.push("client_asn="+ASN_UNKNOWABLE); + } + else { + reqParams.push("client_asn="+this.client_asn); + } + + if (this.myGetBoolPref("priv_dns")) { + reqParams.push("private_opt_in=1"); + } + else { + reqParams.push("private_opt_in=0"); + } + + var params = reqParams.join("&") + "&padding=0"; + var tot_len = BASE_REQ_SIZE; + + this.log(INFO, "Submitting cert for "+domain); + this.log(DBUG, "submit_cert params: "+params); + + // Pad to exp scale. This is done because the distribution of cert sizes + // is almost certainly pareto, and definitely not uniform. + for (tot_len = BASE_REQ_SIZE; tot_len < params.length; tot_len*=2); + + while (params.length != tot_len) { + params += "0"; + } + + var that = this; // We have neither SSLObservatory nor this in scope in the lambda + + var HTTPSEverywhere = CC["@eff.org/https-everywhere;1"] + .getService(Components.interfaces.nsISupports) + .wrappedJSObject; + var win = null; + if (channel) { + var browser = this.HTTPSEverywhere.getBrowserForChannel(channel); + if (browser) { + var win = browser.contentWindow; + } + } + var req = this.buildRequest(params); + req.timeout = TIMEOUT; + + req.onreadystatechange = function(evt) { + if (req.readyState == 4) { + // pop off one outstanding request + that.current_outstanding_requests -= 1; + that.log(DBUG, "Popping one off of outstanding requests, current num is: "+that.current_outstanding_requests); + + if (req.status == 200) { + that.log(INFO, "Successful cert submission"); + if (!that.prefs.getBoolPref("extensions.https_everywhere._observatory.cache_submitted") && + c.fps[0] in that.already_submitted) { + delete that.already_submitted[c.fps[0]]; + } + + // Retry up to two previously failed submissions + let n = 0; + for (let fp in that.delayed_submissions) { + that.log(WARN, "Retrying a submission..."); + that.delayed_submissions[fp](); + delete that.delayed_submissions[fp]; + if (++n >= 2) break; + } + } else if (req.status == 403) { + that.log(WARN, "The SSL Observatory has issued a warning about this certificate for " + domain); + try { + var warningObj = JSON.parse(req.responseText); + if (win) that.warnUser(warningObj, win, c.certArray[0]); + } catch(e) { + that.log(WARN, "Failed to process SSL Observatory cert warnings :( " + e); + that.log(WARN, req.responseText); + } + } else { + // Submission failed + if (c.fps[0] in that.already_submitted) { + delete that.already_submitted[c.fps[0]]; + } + try { + that.log(WARN, "Cert submission failure "+req.status+": "+req.responseText); + } catch(e) { + that.log(WARN, "Cert submission failure and exception: "+e); + } + // If we don't have too many delayed submissions, and this isn't + // (somehow?) one of them, then plan to retry this submission later + if (Object.keys(that.delayed_submissions).length < MAX_DELAYED && + c.fps[0] in that.delayed_submissions) { + that.log(WARN, "Planning to retry submission..."); + let retry = function() { that.submitChain(certArray, fps, domain, channel, host_ip, true); }; + that.delayed_submissions[c.fps[0]] = retry; + } + } + } + }; + + // Cache this here to prevent multiple submissions for all the content elements. + that.already_submitted[c.fps[0]] = true; + + // add one to current outstanding request number + that.current_outstanding_requests += 1; + that.log(DBUG, "Adding outstanding request, current num is: "+that.current_outstanding_requests); + req.send(params); + }, + + buildRequest: function(params) { + var req = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"] + .createInstance(Ci.nsIXMLHttpRequest); + + // We do this again in case the user altered about:config + this.findSubmissionTarget(); + req.open("POST", this.submit_url+this.csrf_nonce, true); + + // Send the proper header information along with the request + // Do not set gzip header.. It will ruin the padding + req.setRequestHeader("X-Privacy-Info", "EFF SSL Observatory: https://www.eff.org/r.22c"); + req.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); + req.setRequestHeader("Content-length", params.length); + req.setRequestHeader("Connection", "close"); + // Need to clear useragent and other headers.. + req.setRequestHeader("User-Agent", ""); + req.setRequestHeader("Accept", ""); + req.setRequestHeader("Accept-Language", ""); + req.setRequestHeader("Accept-Encoding", ""); + req.setRequestHeader("Accept-Charset", ""); + return req; + }, + + warnUser: function(warningObj, win, cert) { + var aWin = CC['@mozilla.org/appshell/window-mediator;1'] + .getService(CI.nsIWindowMediator) + .getMostRecentWindow('navigator:browser'); + aWin.openDialog("chrome://https-everywhere/content/observatory-warning.xul", + "","chrome,centerscreen", warningObj, win, cert); + }, + + registerProxyTestNotification: function(callback_fcn) { + if (this.proxy_test_successful != null) { + /* Proxy test already ran. Callback immediately. */ + callback_fcn(this.proxy_test_successful); + this.proxy_test_callback = null; + return; + } else { + this.proxy_test_callback = callback_fcn; + } + }, + + testProxySettings: function() { + /* Plan: + * 1. Launch an async XMLHttpRequest to check.tp.o with magic nonce + * 3. Filter the nonce in protocolProxyFilter to use proxy settings + * 4. Async result function sets test result status based on check.tp.o + */ + this.proxy_test_successful = null; + + try { + var req = Components.classes["@mozilla.org/xmlextras/xmlhttprequest;1"] + .createInstance(Components.interfaces.nsIXMLHttpRequest); + var url = this.cto_url + this.csrf_nonce; + req.open('GET', url, true); + req.channel.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE; + req.overrideMimeType("text/xml"); + var that = this; // Scope gymnastics for async callback + req.onreadystatechange = function (oEvent) { + if (req.readyState === 4) { + that.proxy_test_successful = false; + + if(req.status == 200) { + if(!req.responseXML) { + that.log(INFO, "Tor check failed: No XML returned by check service."); + return; + } + + var result = req.responseXML.getElementById('TorCheckResult'); + if(result===null) { + that.log(INFO, "Tor check failed: Non-XML returned by check service."); + } else if(typeof(result.target) == 'undefined' + || result.target === null) { + that.log(INFO, "Tor check failed: Busted XML returned by check service."); + } else if(result.target === "success") { + that.log(INFO, "Tor check succeeded."); + that.proxy_test_successful = true; + } else { + that.log(INFO, "Tor check failed: "+result.target); + } + } else { + that.log(INFO, "Tor check failed: HTTP Error "+req.status); + } + + /* Notify the UI of the test result */ + if (that.proxy_test_callback) { + that.proxy_test_callback(that.proxy_test_successful); + that.proxy_test_callback = null; + } + } + }; + req.send(null); + } catch(e) { + this.proxy_test_successful = false; + if(e.result == 0x80004005) { // NS_ERROR_FAILURE + this.log(INFO, "Tor check failed: Proxy not running."); + } + this.log(INFO, "Tor check failed: Internal error: "+e); + if (this.proxy_test_callback) { + this.proxy_test_callback(this.proxy_test_successful); + this.proxy_test_callback = null; + } + } + }, + + getProxySettings: function(testingForTor) { + // This may be called either for an Observatory submission, or during a test to see if Tor is + // present. The testingForTor argument is true in the latter case. + var proxy_settings = ["direct", "", 0]; + this.log(INFO,"in getProxySettings()"); + var custom_proxy_type = this.prefs.getCharPref("extensions.https_everywhere._observatory.proxy_type"); + if (this.torbutton_installed && this.myGetBoolPref("use_tor_proxy")) { + this.log(INFO,"CASE: use_tor_proxy"); + // extract torbutton proxy settings + proxy_settings[0] = "http"; + proxy_settings[1] = this.prefs.getCharPref("extensions.torbutton.https_proxy"); + proxy_settings[2] = this.prefs.getIntPref("extensions.torbutton.https_port"); + + if (proxy_settings[2] == 0) { + proxy_settings[0] = "socks"; + proxy_settings[1] = this.prefs.getCharPref("extensions.torbutton.socks_host"); + proxy_settings[2] = this.prefs.getIntPref("extensions.torbutton.socks_port"); + } + /* Regarding the test below: + * + * custom_proxy_type == "direct" is indicative of the user having selected "submit certs even if + * Tor is not available", rather than true custom Tor proxy settings. So in that case, there's + * not much point probing to see if the direct proxy is actually a Tor connection, and + * localhost:9050 is a better bet. People whose networks send all traffc through Tor can just + * tell the Observatory to submit certs without Tor. + */ + } else if (this.myGetBoolPref("use_custom_proxy") && !(testingForTor && custom_proxy_type == "direct")) { + this.log(INFO,"CASE: use_custom_proxy"); + proxy_settings[0] = custom_proxy_type; + proxy_settings[1] = this.prefs.getCharPref("extensions.https_everywhere._observatory.proxy_host"); + proxy_settings[2] = this.prefs.getIntPref("extensions.https_everywhere._observatory.proxy_port"); + } else { + /* Take a guess at default tor proxy settings */ + this.log(INFO,"CASE: try localhost:9050"); + proxy_settings[0] = "socks"; + proxy_settings[1] = "localhost"; + proxy_settings[2] = 9050; + } + this.log(INFO, "Using proxy: " + proxy_settings); + return proxy_settings; + }, + + applyFilter: function(aProxyService, inURI, aProxy) { + + try { + if (inURI instanceof Ci.nsIURI) { + var aURI = inURI.QueryInterface(Ci.nsIURI); + if (!aURI) this.log(WARN, "Failed to QI to nsIURI!"); + } else { + this.log(WARN, "applyFilter called without URI"); + } + } catch (e) { + this.log(WARN, "EXPLOSION: " + e); + } + + var isSubmission = this.submission_regexp.test(aURI.spec); + var testingForTor = this.cto_regexp.test(aURI.spec); + + if (isSubmission || testingForTor) { + if (aURI.path.search(this.csrf_nonce+"$") != -1) { + + this.log(INFO, "Got observatory url + nonce: "+aURI.spec); + var proxy_settings = null; + var proxy = null; + + // Send it through tor by creating an nsIProxy instance + // for the torbutton proxy settings. + try { + proxy_settings = this.getProxySettings(testingForTor); + proxy = this.pps.newProxyInfo(proxy_settings[0], proxy_settings[1], + proxy_settings[2], + Ci.nsIProxyInfo.TRANSPARENT_PROXY_RESOLVES_HOST, + 0xFFFFFFFF, null); + } catch(e) { + this.log(WARN, "Error specifying proxy for observatory: "+e); + } + + this.log(INFO, "Specifying proxy: "+proxy); + + // TODO: Use new identity or socks u/p to ensure we get a unique + // tor circuit for this request + return proxy; + } + } + return aProxy; + }, + + // [optional] an array of categories to register this component in. + // Hack to cause us to get instantiate early + _xpcom_categories: [ { category: "profile-after-change" }, ], + + encString: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/', + encStringS: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_', + + log: function(level, str) { + var econsole = CC["@mozilla.org/consoleservice;1"] + .getService(CI.nsIConsoleService); + try { + var threshold = this.prefs.getIntPref(LLVAR); + } catch (e) { + econsole.logStringMessage( "SSL Observatory: Failed to read about:config LogLevel"); + threshold = WARN; + } + if (level >= threshold) { + var levelName = ["", "VERB", "DBUG", "INFO", "NOTE", "WARN"][level]; + var prefix = "SSL Observatory " + levelName + ": "; + // dump() prints to browser stdout. That's sometimes undesireable, + // so only do it when a pref is set (running from test.sh enables + // this pref). + if (this.prefs.getBoolPref("extensions.https_everywhere.log_to_stdout")) { + dump(prefix + str + "\n"); + } + econsole.logStringMessage(prefix + str); + } + } +}; + +/** +* XPCOMUtils.generateNSGetFactory was introduced in Mozilla 2 (Firefox 4). +* XPCOMUtils.generateNSGetModule is for Mozilla 1.9.2 (Firefox 3.6). +*/ +if (XPCOMUtils.generateNSGetFactory) + var NSGetFactory = XPCOMUtils.generateNSGetFactory([SSLObservatory]); +else + var NSGetModule = XPCOMUtils.generateNSGetModule([SSLObservatory]); |