You've already forked joplin
							
							
				mirror of
				https://github.com/laurent22/joplin.git
				synced 2025-10-31 00:07:48 +02:00 
			
		
		
		
	Server: Resolves #5738: Check for time drift when the server starts
This commit is contained in:
		| @@ -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 | ||||
|   | ||||
							
								
								
									
										6
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -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 | ||||
|   | ||||
							
								
								
									
										61
									
								
								packages/lib/ntp.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								packages/lib/ntp.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -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<Date> { | ||||
| 	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<number> { | ||||
| 	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<Date> { | ||||
| // 	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); | ||||
| // } | ||||
| @@ -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<Date> { | ||||
| 	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<Date> { | ||||
| 	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); | ||||
| } | ||||
| @@ -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; | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
							
								
								
									
										4
									
								
								packages/lib/vendor/ntp-client.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								packages/lib/vendor/ntp-client.js
									
									
									
									
										vendored
									
									
								
							| @@ -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) | ||||
|   | ||||
| @@ -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); | ||||
|   | ||||
| @@ -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, | ||||
| 	}; | ||||
| } | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
| @@ -160,6 +160,7 @@ export interface Config { | ||||
| 	storageDriver: StorageDriverConfig; | ||||
| 	storageDriverFallback: StorageDriverConfig; | ||||
| 	itemSizeHardLimit: number; | ||||
| 	maxTimeDrift: number; | ||||
| } | ||||
|  | ||||
| export enum HttpMethod { | ||||
|   | ||||
		Reference in New Issue
	
	Block a user