diff --git a/.eslintignore b/.eslintignore index 956f8e577..7e298e2c8 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1176,9 +1176,9 @@ packages/lib/models/utils/paginationToSql.js.map packages/lib/models/utils/types.d.ts packages/lib/models/utils/types.js packages/lib/models/utils/types.js.map -packages/lib/ntpDate.d.ts -packages/lib/ntpDate.js -packages/lib/ntpDate.js.map +packages/lib/ntp.d.ts +packages/lib/ntp.js +packages/lib/ntp.js.map packages/lib/onedrive-api.d.ts packages/lib/onedrive-api.js packages/lib/onedrive-api.js.map diff --git a/.gitignore b/.gitignore index 426b5cc07..25f7354ce 100644 --- a/.gitignore +++ b/.gitignore @@ -1159,9 +1159,9 @@ packages/lib/models/utils/paginationToSql.js.map packages/lib/models/utils/types.d.ts packages/lib/models/utils/types.js packages/lib/models/utils/types.js.map -packages/lib/ntpDate.d.ts -packages/lib/ntpDate.js -packages/lib/ntpDate.js.map +packages/lib/ntp.d.ts +packages/lib/ntp.js +packages/lib/ntp.js.map packages/lib/onedrive-api.d.ts packages/lib/onedrive-api.js packages/lib/onedrive-api.js.map diff --git a/packages/lib/ntp.ts b/packages/lib/ntp.ts new file mode 100644 index 000000000..840a785f1 --- /dev/null +++ b/packages/lib/ntp.ts @@ -0,0 +1,61 @@ +import shim from './shim'; +const ntpClient_ = require('./vendor/ntp-client'); + +const server = { + domain: 'pool.ntp.org', + port: 123, +}; + +function ntpClient() { + ntpClient_.dgram = shim.dgram(); + return ntpClient_; +} + +export async function getNetworkTime(): Promise { + return new Promise(function(resolve: Function, reject: Function) { + ntpClient().getNetworkTime(server.domain, server.port, function(error: any, date: Date) { + if (error) { + reject(error); + return; + } + + resolve(date); + }); + }); +} + +export async function getDeviceTimeDrift(): Promise { + let ntpTime: Date = null; + try { + ntpTime = await getNetworkTime(); + } catch (error) { + error.message = `Cannot retrieve the network time: ${error.message}`; + throw error; + } + + return ntpTime.getTime() - Date.now(); +} + +// export default async function(): Promise { +// if (shouldSyncTime()) { +// const release = await fetchingTimeMutex.acquire(); + +// try { +// if (shouldSyncTime()) { +// const date = await networkTime(); +// nextSyncTime = Date.now() + 60 * 1000; +// timeOffset = date.getTime() - Date.now(); +// } +// } catch (error) { +// logger.warn('Could not get NTP time - falling back to device time:', error); +// // Fallback to device time since +// // most of the time it's actually correct +// nextSyncTime = Date.now() + 20 * 1000; +// timeOffset = 0; +// } finally { +// release(); +// } +// } + +// return new Date(Date.now() + timeOffset); +// } diff --git a/packages/lib/ntpDate.ts b/packages/lib/ntpDate.ts deleted file mode 100644 index d78cf7a7e..000000000 --- a/packages/lib/ntpDate.ts +++ /dev/null @@ -1,59 +0,0 @@ -const ntpClient = require('./vendor/ntp-client'); -import Logger from './Logger'; -const Mutex = require('async-mutex').Mutex; - -let nextSyncTime = 0; -let timeOffset = 0; -let logger = new Logger(); - -const fetchingTimeMutex = new Mutex(); - -const server = { - domain: 'pool.ntp.org', - port: 123, -}; - -async function networkTime(): Promise { - return new Promise(function(resolve: Function, reject: Function) { - ntpClient.getNetworkTime(server.domain, server.port, function(error: any, date: Date) { - if (error) { - reject(error); - return; - } - - resolve(date); - }); - }); -} - -function shouldSyncTime() { - return !nextSyncTime || Date.now() > nextSyncTime; -} - -export function setLogger(v: any) { - logger = v; -} - -export default async function(): Promise { - if (shouldSyncTime()) { - const release = await fetchingTimeMutex.acquire(); - - try { - if (shouldSyncTime()) { - const date = await networkTime(); - nextSyncTime = Date.now() + 60 * 1000; - timeOffset = date.getTime() - Date.now(); - } - } catch (error) { - logger.warn('Could not get NTP time - falling back to device time:', error); - // Fallback to device time since - // most of the time it's actually correct - nextSyncTime = Date.now() + 20 * 1000; - timeOffset = 0; - } finally { - release(); - } - } - - return new Date(Date.now() + timeOffset); -} diff --git a/packages/lib/shim-init-node.js b/packages/lib/shim-init-node.js index 0e89a3815..960982016 100644 --- a/packages/lib/shim-init-node.js +++ b/packages/lib/shim-init-node.js @@ -16,6 +16,7 @@ const https = require('https'); const toRelative = require('relative'); const timers = require('timers'); const zlib = require('zlib'); +const dgram = require('dgram'); function fileExists(filePath) { try { @@ -93,6 +94,10 @@ function shimInit(options = null) { return shim.fsDriver_; }; + shim.dgram = () => { + return dgram; + }; + if (options.React) { shim.react = () => { return options.React; diff --git a/packages/lib/shim.ts b/packages/lib/shim.ts index c360d061c..e65d732a7 100644 --- a/packages/lib/shim.ts +++ b/packages/lib/shim.ts @@ -333,6 +333,10 @@ const shim = { return react_; }, + dgram: () => { + throw new Error('Not implemented'); + }, + platformSupportsKeyChain: () => { // keytar throws an error when system keychain is not present; even // when keytar itself is installed. try/catch to ensure system diff --git a/packages/lib/vendor/ntp-client.js b/packages/lib/vendor/ntp-client.js index 235d48d7b..7706eadba 100644 --- a/packages/lib/vendor/ntp-client.js +++ b/packages/lib/vendor/ntp-client.js @@ -44,8 +44,8 @@ const Buffer = require('buffer').Buffer; if (!exports.dgram) throw new Error('dgram package has not been set!!'); - var client = exports.dgram.createSocket("udp4"), - ntpData = new Buffer(48); + var client = exports.dgram.createSocket("udp4"); + var ntpData = Buffer.alloc(48); // new Buffer(48); // RFC 2030 -> LI = 0 (no warning, 2 bits), VN = 3 (IPv4 only, 3 bits), Mode = 3 (Client Mode, 3 bits) -> 1 byte // -> rtol(LI, 6) ^ rotl(VN, 3) ^ rotl(Mode, 0) diff --git a/packages/server/src/app.ts b/packages/server/src/app.ts index caa9026c9..ea145b268 100644 --- a/packages/server/src/app.ts +++ b/packages/server/src/app.ts @@ -8,6 +8,7 @@ import config, { initConfig, runningInDocker } from './config'; import { migrateLatest, waitForConnection, sqliteDefaultDir, latestMigration } from './db'; import { AppContext, Env, KoaNext } from './utils/types'; import FsDriverNode from '@joplin/lib/fs-driver-node'; +import { getDeviceTimeDrift } from '@joplin/lib/ntp'; import routeHandler from './middleware/routeHandler'; import notificationHandler from './middleware/notificationHandler'; import ownerHandler from './middleware/ownerHandler'; @@ -249,6 +250,13 @@ async function main() { runCommandAndExitApp = false; appLogger().info(`Starting server v${config().appVersion} (${env}) on port ${config().port} and PID ${process.pid}...`); + + const timeDrift = await getDeviceTimeDrift(); + if (Math.abs(timeDrift) > config().maxTimeDrift) { + throw new Error(`The device time drift is ${timeDrift}ms (Max allowed: ${config().maxTimeDrift}ms) - cannot continue as it could cause data loss and conflicts on the sync clients. You may increase env var MAX_TIME_DRIFT to pass the check.`); + } + + appLogger().info(`NTP time offset: ${timeDrift}ms`); appLogger().info('Running in Docker:', runningInDocker()); appLogger().info('Public base URL:', config().baseUrl); appLogger().info('API base URL:', config().apiBaseUrl); diff --git a/packages/server/src/config.ts b/packages/server/src/config.ts index dc8239297..ebf648aaa 100644 --- a/packages/server/src/config.ts +++ b/packages/server/src/config.ts @@ -134,6 +134,7 @@ export async function initConfig(envType: Env, env: EnvVariables, overrides: any storageDriver: parseStorageDriverConnectionString(env.STORAGE_DRIVER), storageDriverFallback: parseStorageDriverConnectionString(env.STORAGE_DRIVER_FALLBACK), itemSizeHardLimit: 250000000, // Beyond this the Postgres driver will crash the app + maxTimeDrift: env.MAX_TIME_DRIFT, ...overrides, }; } diff --git a/packages/server/src/env.ts b/packages/server/src/env.ts index dda8f2caa..4cb625abe 100644 --- a/packages/server/src/env.ts +++ b/packages/server/src/env.ts @@ -16,6 +16,7 @@ const defaultEnvValues: EnvVariables = { ERROR_STACK_TRACES: false, COOKIES_SECURE: false, RUNNING_IN_DOCKER: false, + MAX_TIME_DRIFT: 10, // ================================================== // URL config @@ -85,6 +86,7 @@ export interface EnvVariables { ERROR_STACK_TRACES: boolean; COOKIES_SECURE: boolean; RUNNING_IN_DOCKER: boolean; + MAX_TIME_DRIFT: number; APP_BASE_URL: string; USER_CONTENT_BASE_URL: string; diff --git a/packages/server/src/utils/types.ts b/packages/server/src/utils/types.ts index 6911e4b59..b5e07b090 100644 --- a/packages/server/src/utils/types.ts +++ b/packages/server/src/utils/types.ts @@ -160,6 +160,7 @@ export interface Config { storageDriver: StorageDriverConfig; storageDriverFallback: StorageDriverConfig; itemSizeHardLimit: number; + maxTimeDrift: number; } export enum HttpMethod {