Restore certificate checking for API syncing errors

Closes #864

This adds a 'channel' property to Zotero.HTTP.UnexpectedStatusException,
because the 'channel' property of the XHR can be garbage-collected
before handling, and the channel's 'securityInfo' property is necessary
to detect certificate errors.
This commit is contained in:
Dan Stillman 2015-12-09 04:07:48 -05:00
parent 01fddc9bb9
commit 18349b2232
5 changed files with 139 additions and 35 deletions

View file

@ -12,6 +12,7 @@ Zotero.HTTP = new function() {
this.UnexpectedStatusException = function(xmlhttp, msg) {
this.xmlhttp = xmlhttp;
this.status = xmlhttp.status;
this.channel = xmlhttp.channel;
this.message = msg;
// Hide password from debug output

View file

@ -754,40 +754,7 @@ Zotero.Sync.Server = new function () {
function _checkResponse(xmlhttp, noReloadOnFailure) {
if (!xmlhttp.responseText) {
var channel = xmlhttp.channel;
// Check SSL cert
if (channel) {
var secInfo = channel.securityInfo;
if (secInfo instanceof Ci.nsITransportSecurityInfo) {
secInfo.QueryInterface(Ci.nsITransportSecurityInfo);
if ((secInfo.securityState & Ci.nsIWebProgressListener.STATE_IS_INSECURE) == Ci.nsIWebProgressListener.STATE_IS_INSECURE) {
var url = channel.name;
var ios = Components.classes["@mozilla.org/network/io-service;1"].
getService(Components.interfaces.nsIIOService);
try {
var uri = ios.newURI(url, null, null);
var host = uri.host;
}
catch (e) {
Zotero.debug(e);
}
var kbURL = 'https://zotero.org/support/kb/ssl_certificate_error';
_error(Zotero.getString('sync.storage.error.webdav.sslCertificateError', host) + "\n\n"
+ Zotero.getString('general.seeForMoreInformation', kbURL),
false, noReloadOnFailure);
}
else if ((secInfo.securityState & Ci.nsIWebProgressListener.STATE_IS_BROKEN) == Ci.nsIWebProgressListener.STATE_IS_BROKEN) {
_error(Zotero.getString('sync.error.sslConnectionError'), false, noReloadOnFailure);
}
}
}
if (xmlhttp.status === 0) {
_error(Zotero.getString('sync.error.checkConnection'), false, noReloadOnFailure);
}
_error(Zotero.getString('sync.error.emptyResponseServer') + Zotero.getString('general.tryAgainLater'),
false, noReloadOnFailure);
}
if (!xmlhttp.responseXML || !xmlhttp.responseXML.childNodes[0] ||
xmlhttp.responseXML.childNodes[0].tagName != 'response' ||

View file

@ -524,6 +524,7 @@ Zotero.Sync.APIClient.prototype = {
catch (e) {
tries++;
if (e instanceof Zotero.HTTP.UnexpectedStatusException) {
this._checkConnection(e.xmlhttp, e.channel);
//this._checkRetry(e.xmlhttp);
if (e.is5xx()) {
@ -566,6 +567,70 @@ Zotero.Sync.APIClient.prototype = {
},
/**
* Check connection for certificate errors, interruptions, and empty responses and
* throw an appropriate error
*/
_checkConnection: function (xmlhttp, channel) {
const Ci = Components.interfaces;
if (!xmlhttp.responseText) {
let msg = null;
let dialogButtonText = null;
let dialogButtonCallback = null;
// Check SSL cert
if (channel) {
let secInfo = channel.securityInfo;
if (secInfo instanceof Ci.nsITransportSecurityInfo) {
secInfo.QueryInterface(Ci.nsITransportSecurityInfo);
if ((secInfo.securityState & Ci.nsIWebProgressListener.STATE_IS_INSECURE)
== Ci.nsIWebProgressListener.STATE_IS_INSECURE) {
let url = channel.name;
let ios = Components.classes["@mozilla.org/network/io-service;1"]
.getService(Components.interfaces.nsIIOService);
try {
var uri = ios.newURI(url, null, null);
var host = uri.host;
}
catch (e) {
Zotero.debug(e);
}
let kbURL = 'https://www.zotero.org/support/kb/ssl_certificate_error';
msg = Zotero.getString('sync.storage.error.webdav.sslCertificateError', host);
dialogButtonText = Zotero.getString('general.moreInformation');
dialogButtonCallback = function () {
let wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]
.getService(Components.interfaces.nsIWindowMediator);
let win = wm.getMostRecentWindow("navigator:browser");
win.ZoteroPane.loadURI(kbURL, { metaKey: true, shiftKey: true });
};
}
else if ((secInfo.securityState & Ci.nsIWebProgressListener.STATE_IS_BROKEN)
== Ci.nsIWebProgressListener.STATE_IS_BROKEN) {
msg = Zotero.getString('sync.error.sslConnectionError');
}
}
}
if (!msg && xmlhttp.status === 0) {
msg = Zotero.getString('sync.error.checkConnection');
}
if (!msg) {
msg = Zotero.getString('sync.error.emptyResponseServer')
+ Zotero.getString('general.tryAgainLater');
}
throw new Zotero.Error(
msg,
0,
{
dialogButtonText,
dialogButtonCallback
}
);
}
},
_checkBackoff: function (xmlhttp) {
var backoff = xmlhttp.getResponseHeader("Backoff");
if (backoff) {

View file

@ -710,7 +710,7 @@ function setHTTPResponse(server, baseURL, response, responses) {
response = responses[topic][key];
}
var responseArray = [response.status || 200, {}, ""];
var responseArray = [response.status !== undefined ? response.status : 200, {}, ""];
if (response.json) {
responseArray[1]["Content-Type"] = "application/json";
responseArray[2] = JSON.stringify(response.json);

View file

@ -0,0 +1,71 @@
"use strict";
describe("Zotero.Sync.APIClient", function () {
Components.utils.import("resource://zotero/config.js");
var apiKey = Zotero.Utilities.randomString(24);
var baseURL = "http://local.zotero/";
var server, client;
function setResponse(response) {
setHTTPResponse(server, baseURL, response, {});
}
before(function () {
Components.utils.import("resource://zotero/concurrentCaller.js");
var caller = new ConcurrentCaller(1);
caller.setLogger(msg => Zotero.debug(msg));
caller.stopOnError = true;
caller.onError = function (e) {
Zotero.logError(e);
if (e.fatal) {
caller.stop();
throw e;
}
};
Zotero.HTTP.mock = sinon.FakeXMLHttpRequest;
client = new Zotero.Sync.APIClient({
baseURL,
apiVersion: ZOTERO_CONFIG.API_VERSION,
apiKey,
caller
})
})
beforeEach(function () {
server = sinon.fakeServer.create();
server.autoRespond = true;
})
after(function () {
Zotero.HTTP.mock = null;
})
describe("#_checkConnection()", function () {
it("should catch an error with an empty response", function* () {
setResponse({
method: "GET",
url: "error",
status: 500,
text: ""
})
var e = yield getPromiseError(client.makeRequest("GET", baseURL + "error"));
assert.ok(e);
assert.isTrue(e.message.startsWith(Zotero.getString('sync.error.emptyResponseServer')));
})
it("should catch an interrupted connection", function* () {
setResponse({
method: "GET",
url: "empty",
status: 0,
text: ""
})
var e = yield getPromiseError(client.makeRequest("GET", baseURL + "empty"));
assert.ok(e);
assert.equal(e.message, Zotero.getString('sync.error.checkConnection'));
})
})
})