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:
parent
01fddc9bb9
commit
18349b2232
5 changed files with 139 additions and 35 deletions
|
@ -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
|
||||
|
|
|
@ -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' ||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
|
|
71
test/tests/syncAPIClientTest.js
Normal file
71
test/tests/syncAPIClientTest.js
Normal 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'));
|
||||
})
|
||||
})
|
||||
})
|
Loading…
Reference in a new issue