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
|
@ -12,6 +12,7 @@ Zotero.HTTP = new function() {
|
||||||
this.UnexpectedStatusException = function(xmlhttp, msg) {
|
this.UnexpectedStatusException = function(xmlhttp, msg) {
|
||||||
this.xmlhttp = xmlhttp;
|
this.xmlhttp = xmlhttp;
|
||||||
this.status = xmlhttp.status;
|
this.status = xmlhttp.status;
|
||||||
|
this.channel = xmlhttp.channel;
|
||||||
this.message = msg;
|
this.message = msg;
|
||||||
|
|
||||||
// Hide password from debug output
|
// Hide password from debug output
|
||||||
|
|
|
@ -754,40 +754,7 @@ Zotero.Sync.Server = new function () {
|
||||||
|
|
||||||
|
|
||||||
function _checkResponse(xmlhttp, noReloadOnFailure) {
|
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] ||
|
if (!xmlhttp.responseXML || !xmlhttp.responseXML.childNodes[0] ||
|
||||||
xmlhttp.responseXML.childNodes[0].tagName != 'response' ||
|
xmlhttp.responseXML.childNodes[0].tagName != 'response' ||
|
||||||
|
|
|
@ -524,6 +524,7 @@ Zotero.Sync.APIClient.prototype = {
|
||||||
catch (e) {
|
catch (e) {
|
||||||
tries++;
|
tries++;
|
||||||
if (e instanceof Zotero.HTTP.UnexpectedStatusException) {
|
if (e instanceof Zotero.HTTP.UnexpectedStatusException) {
|
||||||
|
this._checkConnection(e.xmlhttp, e.channel);
|
||||||
//this._checkRetry(e.xmlhttp);
|
//this._checkRetry(e.xmlhttp);
|
||||||
|
|
||||||
if (e.is5xx()) {
|
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) {
|
_checkBackoff: function (xmlhttp) {
|
||||||
var backoff = xmlhttp.getResponseHeader("Backoff");
|
var backoff = xmlhttp.getResponseHeader("Backoff");
|
||||||
if (backoff) {
|
if (backoff) {
|
||||||
|
|
|
@ -710,7 +710,7 @@ function setHTTPResponse(server, baseURL, response, responses) {
|
||||||
response = responses[topic][key];
|
response = responses[topic][key];
|
||||||
}
|
}
|
||||||
|
|
||||||
var responseArray = [response.status || 200, {}, ""];
|
var responseArray = [response.status !== undefined ? response.status : 200, {}, ""];
|
||||||
if (response.json) {
|
if (response.json) {
|
||||||
responseArray[1]["Content-Type"] = "application/json";
|
responseArray[1]["Content-Type"] = "application/json";
|
||||||
responseArray[2] = JSON.stringify(response.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 New Issue
Block a user