2021-06-22 20:57:04 +02:00
|
|
|
import Setting from './models/Setting';
|
2023-07-27 17:05:56 +02:00
|
|
|
import Logger from '@joplin/utils/Logger';
|
2021-06-22 20:57:04 +02:00
|
|
|
import Api, { RequestFile } from './services/rest/Api';
|
|
|
|
import ApiResponse from './services/rest/ApiResponse';
|
2019-07-29 15:43:53 +02:00
|
|
|
const urlParser = require('url');
|
2020-11-05 18:58:23 +02:00
|
|
|
const { randomClipperPort, startPort } = require('./randomClipperPort');
|
2018-05-25 14:30:27 +02:00
|
|
|
const enableServerDestroy = require('server-destroy');
|
2018-09-28 20:24:57 +02:00
|
|
|
const multiparty = require('multiparty');
|
2018-05-16 15:16:14 +02:00
|
|
|
|
2021-06-22 20:57:04 +02:00
|
|
|
export enum StartState {
|
|
|
|
Idle = 'idle',
|
|
|
|
Starting = 'starting',
|
|
|
|
Started = 'started',
|
|
|
|
}
|
|
|
|
|
|
|
|
export default class ClipperServer {
|
|
|
|
|
|
|
|
private logger_: Logger;
|
|
|
|
private startState_: StartState = StartState.Idle;
|
2024-04-05 13:16:49 +02:00
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
2021-06-22 20:57:04 +02:00
|
|
|
private server_: any = null;
|
|
|
|
private port_: number = null;
|
|
|
|
private api_: Api = null;
|
2023-06-30 11:30:29 +02:00
|
|
|
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
2021-06-22 20:57:04 +02:00
|
|
|
private dispatch_: Function;
|
|
|
|
|
|
|
|
private static instance_: ClipperServer = null;
|
|
|
|
|
2023-03-06 16:22:01 +02:00
|
|
|
public constructor() {
|
2018-05-23 13:14:38 +02:00
|
|
|
this.logger_ = new Logger();
|
2018-05-25 14:30:27 +02:00
|
|
|
}
|
|
|
|
|
2023-03-06 16:22:01 +02:00
|
|
|
public static instance() {
|
2018-05-25 14:30:27 +02:00
|
|
|
if (this.instance_) return this.instance_;
|
|
|
|
this.instance_ = new ClipperServer();
|
|
|
|
return this.instance_;
|
2018-05-23 13:14:38 +02:00
|
|
|
}
|
|
|
|
|
2021-06-22 20:57:04 +02:00
|
|
|
public get api(): Api {
|
|
|
|
return this.api_;
|
|
|
|
}
|
|
|
|
|
2024-04-05 13:16:49 +02:00
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
2023-03-06 16:22:01 +02:00
|
|
|
public initialize(actionApi: any = null) {
|
2020-07-23 21:04:04 +02:00
|
|
|
this.api_ = new Api(() => {
|
|
|
|
return Setting.value('api.token');
|
2024-04-05 13:16:49 +02:00
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
2021-06-22 20:57:04 +02:00
|
|
|
}, (action: any) => { this.dispatch(action); }, actionApi);
|
2020-07-23 21:04:04 +02:00
|
|
|
}
|
|
|
|
|
2023-03-06 16:22:01 +02:00
|
|
|
public setLogger(l: Logger) {
|
2018-05-23 13:14:38 +02:00
|
|
|
this.logger_ = l;
|
|
|
|
}
|
|
|
|
|
2023-03-06 16:22:01 +02:00
|
|
|
public logger() {
|
2018-05-23 13:14:38 +02:00
|
|
|
return this.logger_;
|
|
|
|
}
|
|
|
|
|
2023-06-30 11:30:29 +02:00
|
|
|
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
2023-03-06 16:22:01 +02:00
|
|
|
public setDispatch(d: Function) {
|
2018-05-25 14:30:27 +02:00
|
|
|
this.dispatch_ = d;
|
|
|
|
}
|
|
|
|
|
2024-04-05 13:16:49 +02:00
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
2023-03-06 16:22:01 +02:00
|
|
|
public dispatch(action: any) {
|
2018-05-25 14:30:27 +02:00
|
|
|
if (!this.dispatch_) throw new Error('dispatch not set!');
|
|
|
|
this.dispatch_(action);
|
|
|
|
}
|
|
|
|
|
2023-03-06 16:22:01 +02:00
|
|
|
public setStartState(v: StartState) {
|
2018-05-25 14:30:27 +02:00
|
|
|
if (this.startState_ === v) return;
|
|
|
|
this.startState_ = v;
|
|
|
|
this.dispatch({
|
|
|
|
type: 'CLIPPER_SERVER_SET',
|
|
|
|
startState: v,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2023-03-06 16:22:01 +02:00
|
|
|
public setPort(v: number) {
|
2018-05-25 14:30:27 +02:00
|
|
|
if (this.port_ === v) return;
|
|
|
|
this.port_ = v;
|
|
|
|
this.dispatch({
|
|
|
|
type: 'CLIPPER_SERVER_SET',
|
|
|
|
port: v,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2023-03-06 16:22:01 +02:00
|
|
|
public async findAvailablePort() {
|
2018-05-25 12:08:22 +02:00
|
|
|
const tcpPortUsed = require('tcp-port-used');
|
|
|
|
|
|
|
|
let state = null;
|
|
|
|
for (let i = 0; i < 10000; i++) {
|
2018-05-26 12:18:54 +02:00
|
|
|
state = randomClipperPort(state, Setting.value('env'));
|
2018-05-25 12:08:22 +02:00
|
|
|
const inUse = await tcpPortUsed.check(state.port);
|
|
|
|
if (!inUse) return state.port;
|
|
|
|
}
|
|
|
|
|
2019-07-29 15:43:53 +02:00
|
|
|
throw new Error('All potential ports are in use or not available.');
|
2018-05-25 12:08:22 +02:00
|
|
|
}
|
|
|
|
|
2023-03-06 16:22:01 +02:00
|
|
|
public async isRunning() {
|
2019-09-08 18:16:45 +02:00
|
|
|
const tcpPortUsed = require('tcp-port-used');
|
|
|
|
const port = Setting.value('api.port') ? Setting.value('api.port') : startPort(Setting.value('env'));
|
|
|
|
const inUse = await tcpPortUsed.check(port);
|
|
|
|
return inUse ? port : 0;
|
|
|
|
}
|
|
|
|
|
2023-03-06 16:22:01 +02:00
|
|
|
public async start() {
|
2018-05-25 14:30:27 +02:00
|
|
|
this.setPort(null);
|
|
|
|
|
2021-06-22 20:57:04 +02:00
|
|
|
this.setStartState(StartState.Starting);
|
2018-05-25 12:08:22 +02:00
|
|
|
|
2019-09-08 18:16:45 +02:00
|
|
|
const settingPort = Setting.value('api.port');
|
|
|
|
|
2018-05-25 12:08:22 +02:00
|
|
|
try {
|
2019-09-08 18:16:45 +02:00
|
|
|
const p = settingPort ? settingPort : await this.findAvailablePort();
|
2018-05-25 14:30:27 +02:00
|
|
|
this.setPort(p);
|
2018-05-25 12:08:22 +02:00
|
|
|
} catch (error) {
|
2021-06-22 20:57:04 +02:00
|
|
|
this.setStartState(StartState.Idle);
|
2018-05-25 12:08:22 +02:00
|
|
|
this.logger().error(error);
|
2023-08-01 16:38:59 +02:00
|
|
|
return null;
|
2018-05-23 15:25:59 +02:00
|
|
|
}
|
2018-05-16 15:16:14 +02:00
|
|
|
|
2018-05-25 14:30:27 +02:00
|
|
|
this.server_ = require('http').createServer();
|
2018-05-16 15:16:14 +02:00
|
|
|
|
2024-04-05 13:16:49 +02:00
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
2021-06-22 20:57:04 +02:00
|
|
|
this.server_.on('request', async (request: any, response: any) => {
|
2024-04-05 13:16:49 +02:00
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
2021-06-22 20:57:04 +02:00
|
|
|
const writeCorsHeaders = (code: any, contentType = 'application/json', additionalHeaders: any = null) => {
|
2023-06-01 13:02:36 +02:00
|
|
|
const headers = {
|
|
|
|
|
|
|
|
'Content-Type': contentType,
|
|
|
|
'Access-Control-Allow-Origin': '*',
|
|
|
|
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS, PUT, PATCH, DELETE',
|
|
|
|
'Access-Control-Allow-Headers': 'X-Requested-With,content-type',
|
|
|
|
...(additionalHeaders ? additionalHeaders : {}),
|
|
|
|
};
|
2018-09-30 11:15:46 +02:00
|
|
|
response.writeHead(code, headers);
|
2019-07-29 15:43:53 +02:00
|
|
|
};
|
2018-05-16 15:16:14 +02:00
|
|
|
|
2024-04-05 13:16:49 +02:00
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
2021-06-22 20:57:04 +02:00
|
|
|
const writeResponseJson = (code: any, object: any) => {
|
2018-05-16 15:16:14 +02:00
|
|
|
writeCorsHeaders(code);
|
|
|
|
response.write(JSON.stringify(object));
|
|
|
|
response.end();
|
2019-07-29 15:43:53 +02:00
|
|
|
};
|
2018-05-16 15:16:14 +02:00
|
|
|
|
2024-04-05 13:16:49 +02:00
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
2021-06-22 20:57:04 +02:00
|
|
|
const writeResponseText = (code: any, text: any) => {
|
2018-05-26 16:46:57 +02:00
|
|
|
writeCorsHeaders(code, 'text/plain');
|
2018-05-25 12:08:22 +02:00
|
|
|
response.write(text);
|
|
|
|
response.end();
|
2019-07-29 15:43:53 +02:00
|
|
|
};
|
2018-05-25 12:08:22 +02:00
|
|
|
|
2024-04-05 13:16:49 +02:00
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
2021-06-22 20:57:04 +02:00
|
|
|
const writeResponseInstance = (code: any, instance: any) => {
|
2019-07-29 15:43:53 +02:00
|
|
|
if (instance.type === 'attachment') {
|
|
|
|
const filename = instance.attachmentFilename ? instance.attachmentFilename : 'file';
|
|
|
|
writeCorsHeaders(code, instance.contentType ? instance.contentType : 'application/octet-stream', {
|
2019-09-19 23:51:18 +02:00
|
|
|
'Content-disposition': `attachment; filename=${filename}`,
|
2019-07-29 15:43:53 +02:00
|
|
|
'Content-Length': instance.body.length,
|
|
|
|
});
|
|
|
|
response.end(instance.body);
|
|
|
|
} else {
|
|
|
|
throw new Error('Not implemented');
|
|
|
|
}
|
|
|
|
};
|
2018-09-30 11:15:46 +02:00
|
|
|
|
2024-04-05 13:16:49 +02:00
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
2021-06-22 20:57:04 +02:00
|
|
|
const writeResponse = (code: any, response: any) => {
|
2018-09-30 11:15:46 +02:00
|
|
|
if (response instanceof ApiResponse) {
|
2019-07-29 15:43:53 +02:00
|
|
|
writeResponseInstance(code, response);
|
2018-09-30 11:15:46 +02:00
|
|
|
} else if (typeof response === 'string') {
|
2018-09-27 10:14:05 +02:00
|
|
|
writeResponseText(code, response);
|
2020-06-20 13:03:22 +02:00
|
|
|
} else if (response === null || response === undefined) {
|
|
|
|
writeResponseText(code, '');
|
2018-09-27 10:14:05 +02:00
|
|
|
} else {
|
|
|
|
writeResponseJson(code, response);
|
|
|
|
}
|
2019-07-29 15:43:53 +02:00
|
|
|
};
|
2018-05-16 15:16:14 +02:00
|
|
|
|
2019-09-19 23:51:18 +02:00
|
|
|
this.logger().info(`Request: ${request.method} ${request.url}`);
|
2018-05-16 15:16:14 +02:00
|
|
|
|
2018-09-27 10:14:05 +02:00
|
|
|
const url = urlParser.parse(request.url, true);
|
2018-05-26 16:46:57 +02:00
|
|
|
|
2024-04-05 13:16:49 +02:00
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
2021-06-22 20:57:04 +02:00
|
|
|
const execRequest = async (request: any, body = '', files: RequestFile[] = []) => {
|
2018-09-27 10:14:05 +02:00
|
|
|
try {
|
2018-09-28 20:24:57 +02:00
|
|
|
const response = await this.api_.route(request.method, url.pathname, url.query, body, files);
|
2020-06-20 13:03:22 +02:00
|
|
|
writeResponse(200, response);
|
2018-09-27 10:14:05 +02:00
|
|
|
} catch (error) {
|
2018-10-04 09:05:22 +02:00
|
|
|
this.logger().error(error);
|
2019-02-24 14:24:55 +02:00
|
|
|
const httpCode = error.httpCode ? error.httpCode : 500;
|
|
|
|
const msg = [];
|
|
|
|
if (httpCode >= 500) msg.push('Internal Server Error');
|
|
|
|
if (error.message) msg.push(error.message);
|
2020-01-20 04:19:57 +02:00
|
|
|
if (error.stack) msg.push(`\n\n${error.stack}`);
|
2019-02-24 14:24:55 +02:00
|
|
|
|
|
|
|
writeResponse(httpCode, { error: msg.join(': ') });
|
2018-05-26 16:46:57 +02:00
|
|
|
}
|
2019-07-29 15:43:53 +02:00
|
|
|
};
|
2018-09-23 19:03:11 +02:00
|
|
|
|
2018-09-28 20:24:57 +02:00
|
|
|
const contentType = request.headers['content-type'] ? request.headers['content-type'] : '';
|
|
|
|
|
2018-09-27 10:14:05 +02:00
|
|
|
if (request.method === 'OPTIONS') {
|
|
|
|
writeCorsHeaders(200);
|
|
|
|
response.end();
|
|
|
|
} else {
|
2018-09-28 20:24:57 +02:00
|
|
|
if (contentType.indexOf('multipart/form-data') === 0) {
|
2019-07-29 15:43:53 +02:00
|
|
|
const form = new multiparty.Form();
|
2018-09-28 20:24:57 +02:00
|
|
|
|
2024-04-05 13:16:49 +02:00
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
2023-02-20 17:02:29 +02:00
|
|
|
form.parse(request, (error: any, fields: any, files: any) => {
|
2019-07-29 15:43:53 +02:00
|
|
|
if (error) {
|
2018-09-28 20:24:57 +02:00
|
|
|
writeResponse(error.httpCode ? error.httpCode : 500, error.message);
|
|
|
|
return;
|
|
|
|
} else {
|
2021-06-22 20:57:04 +02:00
|
|
|
void execRequest(request, fields && fields.props && fields.props.length ? fields.props[0] : '', files && files.data ? files.data : []);
|
2018-09-28 20:24:57 +02:00
|
|
|
}
|
2019-07-29 15:43:53 +02:00
|
|
|
});
|
2018-05-16 15:16:14 +02:00
|
|
|
} else {
|
2018-10-04 09:05:22 +02:00
|
|
|
if (request.method === 'POST' || request.method === 'PUT') {
|
2018-09-28 20:24:57 +02:00
|
|
|
let body = '';
|
|
|
|
|
2024-04-05 13:16:49 +02:00
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
2021-06-22 20:57:04 +02:00
|
|
|
request.on('data', (data: any) => {
|
2018-09-28 20:24:57 +02:00
|
|
|
body += data;
|
|
|
|
});
|
|
|
|
|
|
|
|
request.on('end', async () => {
|
2021-06-22 20:57:04 +02:00
|
|
|
void execRequest(request, body);
|
2018-09-28 20:24:57 +02:00
|
|
|
});
|
|
|
|
} else {
|
2021-06-22 20:57:04 +02:00
|
|
|
void execRequest(request);
|
2018-09-28 20:24:57 +02:00
|
|
|
}
|
2018-05-16 15:16:14 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2018-05-25 14:30:27 +02:00
|
|
|
enableServerDestroy(this.server_);
|
|
|
|
|
2019-09-19 23:51:18 +02:00
|
|
|
this.logger().info(`Starting Clipper server on port ${this.port_}`);
|
2018-05-25 14:30:27 +02:00
|
|
|
|
2018-06-01 17:44:16 +02:00
|
|
|
this.server_.listen(this.port_, '127.0.0.1');
|
2018-05-25 14:30:27 +02:00
|
|
|
|
2021-06-22 20:57:04 +02:00
|
|
|
this.setStartState(StartState.Started);
|
2019-09-08 18:16:45 +02:00
|
|
|
|
|
|
|
// We return an empty promise that never resolves so that it's possible to `await` the server indefinitely.
|
|
|
|
// This is used only in command-server.js
|
|
|
|
return new Promise(() => {});
|
2018-05-25 14:30:27 +02:00
|
|
|
}
|
2018-05-16 15:16:14 +02:00
|
|
|
|
2023-03-06 16:22:01 +02:00
|
|
|
public async stop() {
|
2018-05-25 14:30:27 +02:00
|
|
|
this.server_.destroy();
|
|
|
|
this.server_ = null;
|
2021-06-22 20:57:04 +02:00
|
|
|
this.setStartState(StartState.Idle);
|
2018-05-25 14:30:27 +02:00
|
|
|
this.setPort(null);
|
2018-05-16 15:16:14 +02:00
|
|
|
}
|
|
|
|
}
|