mirror of
https://github.com/zotero/zotero.git
synced 2026-08-28 05:25:31 +00:00
Local API: Don't validate Zotero-Server-ID for file uploads
Some checks are pending
Some checks are pending
https://github.com/urschrei/pyzotero/issues/344#issuecomment-5108140508
This commit is contained in:
parent
caeffa9f41
commit
681c48f1c2
2 changed files with 78 additions and 3 deletions
|
|
@ -241,6 +241,10 @@ const exportFormats = new Map([
|
|||
* Base class for all local API endpoints. Implements pre- and post-processing steps.
|
||||
*/
|
||||
class LocalAPIEndpoint {
|
||||
// Whether write-method requests to this endpoint must provide a Zotero-Server-ID
|
||||
// header. Endpoints that validate a different secret can set this to false.
|
||||
requireServerIDOnWrite = true;
|
||||
|
||||
async init(requestData) {
|
||||
if (!Zotero.Prefs.get('httpServer.localAPI.enabled')) {
|
||||
return this.makeResponse(403, 'text/plain', 'Local API is not enabled');
|
||||
|
|
@ -611,7 +615,7 @@ class LocalAPIEndpoint {
|
|||
* the request is rejected with 412 (Precondition Failed).
|
||||
* - Write requests (POST, PUT, PATCH, DELETE) must provide the header, or
|
||||
* they're rejected with 428 (Precondition Required). Read requests may
|
||||
* omit it.
|
||||
* omit it, as may endpoints that set requireServerIDOnWrite to false.
|
||||
*
|
||||
* Returns null when the request may proceed, or an HTTP response array when
|
||||
* it should be rejected.
|
||||
|
|
@ -621,7 +625,8 @@ class LocalAPIEndpoint {
|
|||
_validateServerID(requestData) {
|
||||
let provided = requestData.headers.get('Zotero-Server-ID');
|
||||
if (!provided) {
|
||||
if (['POST', 'PUT', 'PATCH', 'DELETE'].includes(requestData.method)) {
|
||||
if (this.requireServerIDOnWrite
|
||||
&& ['POST', 'PUT', 'PATCH', 'DELETE'].includes(requestData.method)) {
|
||||
return this.makeResponse(428, 'text/plain', 'Zotero-Server-ID not provided');
|
||||
}
|
||||
return null;
|
||||
|
|
@ -1279,7 +1284,8 @@ Zotero.Server.Endpoints["/api/groups/:groupID/items/:itemKey/file/view/url"] = Z
|
|||
Zotero.Server.LocalAPI.UploadReceiver = class extends LocalAPIEndpoint {
|
||||
supportedMethods = ['POST'];
|
||||
|
||||
// Required so the post-write block doesn't insist on a library write check
|
||||
// Overridden so the write block doesn't run: no API key or library write check
|
||||
// applies here, and the response isn't a data object
|
||||
async _initInternal(requestData) {
|
||||
try {
|
||||
if (!Zotero.Prefs.get('httpServer.localAPI.enabled')) {
|
||||
|
|
|
|||
|
|
@ -2198,6 +2198,75 @@ describe("Local API Server", function () {
|
|||
assert.equal(status, 404);
|
||||
});
|
||||
|
||||
it("shouldn't require a server ID on the upload request", async function () {
|
||||
// Web API clients post the bytes to S3, not to Zotero, so they strip
|
||||
// Zotero-specific headers from this request -- it has to work without one
|
||||
let content = "Upload sent without a Zotero-Server-ID header";
|
||||
let bytes = new TextEncoder().encode(content);
|
||||
let tmpPath = PathUtils.join(Zotero.getTempDirectory().path, 'localapi-no-server-id.bin');
|
||||
await IOUtils.write(tmpPath, bytes);
|
||||
let md5 = await Zotero.Utilities.Internal.md5Async(tmpPath);
|
||||
await IOUtils.remove(tmpPath);
|
||||
|
||||
let { response: authResp } = await apiPost(
|
||||
`/users/0/items/${attachment.key}/file`,
|
||||
{
|
||||
body: `md5=${md5}&filename=no-server-id.bin`
|
||||
+ `&filesize=${bytes.length}&mtime=${Date.now()}`,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'If-None-Match': '*',
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
let uploadXhr = await Zotero.HTTP.request('POST', authResp.url, {
|
||||
headers: {
|
||||
'Zotero-Allowed-Request': '1',
|
||||
'Content-Type': 'application/octet-stream',
|
||||
},
|
||||
body: content,
|
||||
successCodes: [201],
|
||||
responseType: 'text'
|
||||
});
|
||||
assert.equal(uploadXhr.status, 201);
|
||||
|
||||
// And the upload is usable, so the flow completes as normal
|
||||
let { status } = await apiPost(
|
||||
`/users/0/items/${attachment.key}/file`,
|
||||
{
|
||||
body: `upload=${authResp.uploadKey}`,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'If-None-Match': '*',
|
||||
},
|
||||
successCodes: [204]
|
||||
}
|
||||
);
|
||||
assert.equal(status, 204);
|
||||
assert.equal(
|
||||
await Zotero.Utilities.Internal.md5Async(await attachment.getFilePathAsync()),
|
||||
md5
|
||||
);
|
||||
});
|
||||
|
||||
it("should reject an upload with a mismatched server ID with 412", async function () {
|
||||
let { status } = await Zotero.HTTP.request(
|
||||
'POST', apiRoot + '/local/uploads/badkey999',
|
||||
{
|
||||
headers: {
|
||||
'Zotero-Allowed-Request': '1',
|
||||
'Content-Type': 'application/octet-stream',
|
||||
'Zotero-Server-ID': 'wrongServerID',
|
||||
},
|
||||
body: 'whatever',
|
||||
successCodes: [412],
|
||||
responseType: 'text'
|
||||
}
|
||||
);
|
||||
assert.equal(status, 412);
|
||||
});
|
||||
|
||||
it("should reject PATCH partial upload with 405", async function () {
|
||||
let { status } = await apiPatch(
|
||||
`/users/0/items/${attachment.key}/file?algorithm=xdelta&upload=foo`,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue