1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-12-24 10:27:10 +02:00

All, Server: Add support for X-API-MIN-VERSION header

This commit is contained in:
Laurent Cozic 2021-06-24 09:25:58 +01:00
parent 752d118e5d
commit 51f3c0016e
7 changed files with 116 additions and 1 deletions

View File

@ -142,6 +142,7 @@ export default class JoplinServerApi {
}
if (sessionId) headers['X-API-AUTH'] = sessionId;
headers['X-API-MIN-VERSION'] = '2.1.4';
const fetchOptions: any = {};
fetchOptions.headers = headers;

View File

@ -2436,6 +2436,11 @@
"integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
"dev": true
},
"compare-versions": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-3.6.0.tgz",
"integrity": "sha512-W6Af2Iw1z4CB7q4uU4hv646dW9GQuBM+YpC0UvUCWSD8w90SJjp+ujJuXaEMtAXBtSqGfMPuFOVn4/+FlaqfBA=="
},
"component-emitter": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz",

View File

@ -24,6 +24,7 @@
"bcryptjs": "^2.4.3",
"bulma": "^0.9.1",
"bulma-prefers-dark": "^0.1.0-beta.0",
"compare-versions": "^3.6.0",
"dayjs": "^1.9.8",
"formidable": "^1.2.2",
"fs-extra": "^8.1.0",

View File

@ -8,7 +8,7 @@ import Logger, { LoggerWrapper, TargetType } from '@joplin/lib/Logger';
import config, { initConfig, runningInDocker, EnvVariables } from './config';
import { createDb, dropDb } from './tools/dbTools';
import { dropTables, connectDb, disconnectDb, migrateDb, waitForConnection, sqliteDefaultDir } from './db';
import { AppContext, Env } from './utils/types';
import { AppContext, Env, KoaNext } from './utils/types';
import FsDriverNode from '@joplin/lib/fs-driver-node';
import routeHandler from './middleware/routeHandler';
import notificationHandler from './middleware/notificationHandler';
@ -17,6 +17,7 @@ import setupAppContext from './utils/setupAppContext';
import { initializeJoplinUtils } from './utils/joplinUtils';
import startServices from './utils/startServices';
import { credentialFile } from './utils/testing/testUtils';
import apiVersionHandler from './middleware/apiVersionHandler';
const cors = require('@koa/cors');
const nodeEnvFile = require('node-env-file');
@ -119,6 +120,18 @@ async function main() {
return false;
}
// This is used to catch any low level error thrown from a middleware. It
// won't deal with errors from routeHandler, which catches and handles its
// own errors.
app.use(async (ctx: AppContext, next: KoaNext) => {
try {
await next();
} catch (error) {
ctx.status = error.httpCode || 500;
ctx.body = JSON.stringify({ error: error.message });
}
});
app.use(cors({
// https://github.com/koajs/cors/issues/52#issuecomment-413887382
origin: (ctx: AppContext) => {
@ -132,6 +145,7 @@ async function main() {
}
},
}));
app.use(apiVersionHandler);
app.use(ownerHandler);
app.use(notificationHandler);
app.use(routeHandler);

View File

@ -0,0 +1,57 @@
import config from '../config';
import { ErrorPreconditionFailed } from '../utils/errors';
import { beforeAllDb, afterAllTests, beforeEachDb, koaAppContext, koaNext, expectNotThrow, expectHttpError } from '../utils/testing/testUtils';
import apiVersionHandler from './apiVersionHandler';
describe('apiVersionHandler', function() {
beforeAll(async () => {
await beforeAllDb('apiVersionHandler');
});
afterAll(async () => {
await afterAllTests();
});
beforeEach(async () => {
await beforeEachDb();
});
test('should work if no version header is provided', async function() {
const context = await koaAppContext({});
await expectNotThrow(async () => apiVersionHandler(context, koaNext));
});
test('should work if the header version number is lower than the server version', async function() {
config().appVersion = '2.1.0';
const context = await koaAppContext({
request: {
method: 'GET',
url: '/api/ping',
headers: {
'x-api-min-version': '2.0.0',
},
},
});
await expectNotThrow(async () => apiVersionHandler(context, koaNext));
});
test('should not work if the header version number is greater than the server version', async function() {
config().appVersion = '2.1.0';
const context = await koaAppContext({
request: {
method: 'GET',
url: '/api/ping',
headers: {
'x-api-min-version': '2.2.0',
},
},
});
await expectHttpError(async () => apiVersionHandler(context, koaNext), ErrorPreconditionFailed.httpCode);
});
});

View File

@ -0,0 +1,27 @@
import { AppContext, KoaNext } from '../utils/types';
import { isApiRequest } from '../utils/requestUtils';
import config from '../config';
import { ErrorPreconditionFailed } from '../utils/errors';
const compareVersions = require('compare-versions');
export default async function(ctx: AppContext, next: KoaNext): Promise<void> {
if (!isApiRequest(ctx)) return;
const appVersion = config().appVersion;
const minVersion = ctx.headers['x-api-min-version'];
// For now we don't require this header to be set to keep compatibility with
// older clients.
if (!minVersion) return next();
const diff = compareVersions(appVersion, minVersion);
// We only throw an error if the client requires a version of Joplin Server
// that's ahead of what's installed. This is mostly to automatically notify
// those who self-host so that they know they need to upgrade Joplin Server.
if (diff < 0) {
throw new ErrorPreconditionFailed(`Joplin Server v${minVersion} is required but v${appVersion} is installed. Please upgrade Joplin Server.`);
}
return next();
}

View File

@ -51,6 +51,16 @@ export class ErrorBadRequest extends ApiError {
}
export class ErrorPreconditionFailed extends ApiError {
public static httpCode: number = 412;
public constructor(message: string = 'Precondition Failed') {
super(message, ErrorPreconditionFailed.httpCode);
Object.setPrototypeOf(this, ErrorPreconditionFailed.prototype);
}
}
export class ErrorUnprocessableEntity extends ApiError {
public static httpCode: number = 422;