2020-12-28 11:48:47 +00:00
// Allows displaying error stack traces with TypeScript file paths
2020-12-30 18:35:18 +00:00
require ( 'source-map-support' ) . install ( ) ;
2020-12-28 11:48:47 +00:00
import * as Koa from 'koa' ;
import * as fs from 'fs-extra' ;
2023-08-28 10:30:56 -03:00
import Logger , { LogLevel , LoggerWrapper , TargetType } from '@joplin/utils/Logger' ;
2023-05-17 18:16:29 +01:00
import config , { fullVersionString , initConfig , runningInDocker } from './config' ;
2024-05-11 16:12:54 +01:00
import { migrateLatest , waitForConnection , sqliteDefaultDir , latestMigration , needsMigration , migrateList , versionCheck , ConnectionCheckResult } from './db' ;
2021-06-24 09:25:58 +01:00
import { AppContext , Env , KoaNext } from './utils/types' ;
2020-12-28 11:48:47 +00:00
import FsDriverNode from '@joplin/lib/fs-driver-node' ;
2021-11-17 12:54:34 +00:00
import { getDeviceTimeDrift } from '@joplin/lib/ntp' ;
2020-12-30 23:50:44 +00:00
import routeHandler from './middleware/routeHandler' ;
2020-12-30 18:35:18 +00:00
import notificationHandler from './middleware/notificationHandler' ;
2020-12-30 23:50:44 +00:00
import ownerHandler from './middleware/ownerHandler' ;
2021-01-29 18:45:11 +00:00
import setupAppContext from './utils/setupAppContext' ;
2021-05-13 18:57:37 +02:00
import { initializeJoplinUtils } from './utils/joplinUtils' ;
import startServices from './utils/startServices' ;
2021-06-03 15:21:02 +02:00
import { credentialFile } from './utils/testing/testUtils' ;
2021-06-24 09:25:58 +01:00
import apiVersionHandler from './middleware/apiVersionHandler' ;
2021-09-23 15:56:40 +01:00
import clickJackingHandler from './middleware/clickJackingHandler' ;
2021-11-10 11:48:06 +00:00
import newModelFactory from './models/factory' ;
2021-10-27 19:29:54 +01:00
import setupCommands from './utils/setupCommands' ;
2021-11-01 19:20:36 +00:00
import { RouteResponseFormat , routeResponseFormat } from './utils/routeUtils' ;
2021-11-02 12:51:59 +00:00
import { parseEnv } from './env' ;
2023-08-05 18:26:12 +01:00
import { parseEnvFile } from '@joplin/utils/env' ;
2021-11-11 16:32:34 +00:00
import storageConnectionCheck from './utils/storageConnectionCheck' ;
2021-12-16 11:07:25 +01:00
import { setLocale } from '@joplin/lib/locale' ;
2023-08-04 10:57:54 +01:00
import initLib from '@joplin/lib/initLib' ;
2022-01-11 15:09:53 +00:00
import checkAdminHandler from './middleware/checkAdminHandler' ;
2024-03-20 03:52:42 -07:00
import ActionLogger from '@joplin/lib/utils/ActionLogger' ;
2021-10-23 17:51:44 +01:00
interface Argv {
env? : Env ;
pidfile? : string ;
envFile? : string ;
}
2021-10-01 19:35:27 +01:00
const nodeSqlite = require ( 'sqlite3' ) ;
2021-06-03 15:21:02 +02:00
const cors = require ( '@koa/cors' ) ;
2020-12-30 18:35:18 +00:00
const { shimInit } = require ( '@joplin/lib/shim-init-node.js' ) ;
2021-10-01 19:35:27 +01:00
shimInit ( { nodeSqlite } ) ;
2020-12-30 18:35:18 +00:00
2024-04-05 12:16:49 +01:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
2021-11-02 12:51:59 +00:00
const defaultEnvVariables : Record < Env , any > = {
2021-01-18 10:13:26 +00:00
dev : {
2021-06-17 11:21:37 +01:00
// To test with the Postgres database, uncomment DB_CLIENT below and
// comment out SQLITE_DATABASE. Then start the Postgres server using
2024-04-04 12:30:24 +01:00
// `docker compose --file docker-compose.db-dev.yml up`
2021-06-17 11:21:37 +01:00
// DB_CLIENT: 'pg',
2021-05-25 12:13:35 +02:00
SQLITE_DATABASE : ` ${ sqliteDefaultDir } /db-dev.sqlite ` ,
2021-01-18 10:13:26 +00:00
} ,
buildTypes : {
2021-05-25 12:13:35 +02:00
SQLITE_DATABASE : ` ${ sqliteDefaultDir } /db-buildTypes.sqlite ` ,
} ,
prod : {
SQLITE_DATABASE : ` ${ sqliteDefaultDir } /db-prod.sqlite ` ,
2021-01-18 10:13:26 +00:00
} ,
2020-12-28 11:48:47 +00:00
} ;
let appLogger_ : LoggerWrapper = null ;
function appLogger ( ) : LoggerWrapper {
2020-12-28 15:15:30 +00:00
if ( ! appLogger_ ) {
appLogger_ = Logger . create ( 'App' ) ;
}
2020-12-28 11:48:47 +00:00
return appLogger_ ;
}
2024-04-05 12:16:49 +01:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
2021-01-18 10:13:26 +00:00
function markPasswords ( o : Record < string , any > ) : Record < string , any > {
2021-11-09 16:05:42 +00:00
if ( ! o ) return o ;
2024-04-05 12:16:49 +01:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
2021-01-18 10:13:26 +00:00
const output : Record < string , any > = { } ;
for ( const k of Object . keys ( o ) ) {
2022-12-28 06:38:30 -08:00
if ( k . toLowerCase ( ) . includes ( 'password' ) || k . toLowerCase ( ) . includes ( 'secret' ) || k . toLowerCase ( ) . includes ( 'connectionstring' ) ) {
2021-01-18 10:13:26 +00:00
output [ k ] = '********' ;
} else {
output [ k ] = o [ k ] ;
}
}
return output ;
}
2024-04-05 12:16:49 +01:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
2021-05-25 11:49:47 +02:00
async function getEnvFilePath ( env : Env , argv : any ) : Promise < string > {
if ( argv . envFile ) return argv . envFile ;
if ( env === Env . Dev ) {
2021-06-03 15:21:02 +02:00
return credentialFile ( 'server.env' ) ;
2021-01-18 10:13:26 +00:00
}
2020-12-28 11:48:47 +00:00
2021-05-25 11:49:47 +02:00
return '' ;
}
async function main() {
2021-10-27 19:29:54 +01:00
const { selectedCommand , argv : yargsArgv } = await setupCommands ( ) ;
2024-04-05 12:16:49 +01:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
2021-10-27 19:29:54 +01:00
const argv : Argv = yargsArgv as any ;
const env : Env = argv . env as Env || Env . Prod ;
2021-05-25 11:49:47 +02:00
const envFilePath = await getEnvFilePath ( env , argv ) ;
2023-08-05 18:33:26 +01:00
let envFromFile : Record < string , string > = { } ;
try {
if ( envFilePath ) envFromFile = parseEnvFile ( envFilePath ) ;
} catch ( error ) {
error . message = ` Could not parse env file at ${ envFilePath } : ${ error . message } ` ;
throw error ;
}
2023-08-05 18:26:12 +01:00
const fullEnv = {
. . . envFromFile ,
. . . process . env ,
} ;
2021-05-25 11:49:47 +02:00
2021-06-06 19:14:12 +02:00
if ( ! defaultEnvVariables [ env ] ) throw new Error ( ` Invalid env: ${ env } ` ) ;
2021-01-18 10:13:26 +00:00
2023-08-05 18:26:12 +01:00
const envVariables = parseEnv ( fullEnv , defaultEnvVariables [ env ] ) ;
2021-06-06 19:14:12 +02:00
const app = new Koa ( ) ;
// Note: the order of middlewares is important. For example, ownerHandler
// loads the user, which is then used by notificationHandler. And finally
// routeHandler uses data from both previous middlewares. It would be good to
// layout these dependencies in code but not clear how to do this.
const corsAllowedDomains = [
'https://joplinapp.org' ,
] ;
2021-07-10 11:16:13 +01:00
if ( env === Env . Dev ) {
2021-07-31 14:42:56 +01:00
corsAllowedDomains . push ( 'http://localhost:8077' ) ;
2021-07-10 11:16:13 +01:00
}
2021-06-06 19:14:12 +02:00
function acceptOrigin ( origin : string ) : boolean {
const hostname = ( new URL ( origin ) ) . hostname ;
const userContentDomain = envVariables . USER_CONTENT_BASE_URL ? ( new URL ( envVariables . USER_CONTENT_BASE_URL ) ) . hostname : '' ;
if ( hostname === userContentDomain ) return true ;
const hostnameNoSub = hostname . split ( '.' ) . slice ( 1 ) . join ( '.' ) ;
2021-07-10 11:16:13 +01:00
// console.info('CORS check for origin', origin, 'Allowed domains', corsAllowedDomains);
2021-06-06 19:14:12 +02:00
if ( hostnameNoSub === userContentDomain ) return true ;
2021-07-10 11:16:13 +01:00
if ( corsAllowedDomains . includes ( origin ) ) return true ;
2021-06-06 19:14:12 +02:00
return false ;
}
2021-06-24 09:25:58 +01:00
// 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 ;
2021-07-24 17:45:30 +01:00
2021-11-01 19:20:36 +00:00
appLogger ( ) . error ( ` Middleware error on ${ ctx . path } : ` , error ) ;
const responseFormat = routeResponseFormat ( ctx ) ;
if ( responseFormat === RouteResponseFormat . Html ) {
// Since this is a low level error, rendering a view might fail too,
// so catch this and default to rendering JSON.
try {
ctx . response . set ( 'Content-Type' , 'text/html' ) ;
ctx . body = await ctx . joplin . services . mustache . renderView ( {
name : 'error' ,
title : 'Error' ,
path : 'index/error' ,
content : { error } ,
} ) ;
} catch ( anotherError ) {
ctx . response . set ( 'Content-Type' , 'application/json' ) ;
2022-01-14 10:14:43 +00:00
ctx . body = JSON . stringify ( { error : ` ${ error . message } (Check the server log for more information) ` } ) ;
2021-11-01 19:20:36 +00:00
}
} else {
ctx . response . set ( 'Content-Type' , 'application/json' ) ;
ctx . body = JSON . stringify ( { error : error.message } ) ;
2021-07-24 17:45:30 +01:00
}
2021-06-24 09:25:58 +01:00
}
} ) ;
2021-07-03 22:39:54 +01:00
// Creates the request-specific "joplin" context property.
app . use ( async ( ctx : AppContext , next : KoaNext ) = > {
ctx . joplin = {
. . . ctx . joplinBase ,
owner : null ,
notifications : [ ] ,
} ;
return next ( ) ;
} ) ;
2021-06-06 19:14:12 +02:00
app . use ( cors ( {
// https://github.com/koajs/cors/issues/52#issuecomment-413887382
origin : ( ctx : AppContext ) = > {
2022-12-26 11:55:41 +00:00
const origin = ctx . request . header . origin ;
2021-06-06 19:14:12 +02:00
if ( acceptOrigin ( origin ) ) {
return origin ;
} else {
// we can't return void, so let's return one of the valid domains
return corsAllowedDomains [ 0 ] ;
}
} ,
} ) ) ;
2021-07-03 22:39:54 +01:00
2021-06-24 09:25:58 +01:00
app . use ( apiVersionHandler ) ;
2021-06-06 19:14:12 +02:00
app . use ( ownerHandler ) ;
2022-01-11 15:09:53 +00:00
app . use ( checkAdminHandler ) ;
2021-06-06 19:14:12 +02:00
app . use ( notificationHandler ) ;
2021-09-23 15:56:40 +01:00
app . use ( clickJackingHandler ) ;
2021-06-06 19:14:12 +02:00
app . use ( routeHandler ) ;
await initConfig ( env , envVariables ) ;
2020-12-28 11:48:47 +00:00
await fs . mkdirp ( config ( ) . logDir ) ;
2021-01-29 18:45:11 +00:00
await fs . mkdirp ( config ( ) . tempDir ) ;
2020-12-28 11:48:47 +00:00
Logger . fsDriver_ = new FsDriverNode ( ) ;
const globalLogger = new Logger ( ) ;
2023-07-26 14:42:35 +01:00
const instancePrefix = config ( ) . INSTANCE_NAME ? ` ${ config ( ) . INSTANCE_NAME } : ` : '' ;
2020-12-28 15:15:30 +00:00
globalLogger . addTarget ( TargetType . Console , {
2023-08-28 10:30:56 -03:00
format : ( level : LogLevel , _prefix : string ) = > {
if ( level === LogLevel . Info ) return ` %(date_time)s: ${ instancePrefix } %(prefix)s: %(message)s ` ;
return ` %(date_time)s: ${ instancePrefix } [%(level)s] %(prefix)s: %(message)s ` ;
} ,
2020-12-28 15:15:30 +00:00
} ) ;
2020-12-28 11:48:47 +00:00
Logger . initializeGlobalLogger ( globalLogger ) ;
2023-08-04 10:57:54 +01:00
initLib ( globalLogger ) ;
2020-12-28 11:48:47 +00:00
2024-03-20 03:52:42 -07:00
// Don't log deletions made by the @joplin/lib API -- ActionLogger is
// designed for Joplin client use.
ActionLogger . enabled = false ;
2021-05-25 11:49:47 +02:00
if ( envFilePath ) appLogger ( ) . info ( ` Env variables were loaded from: ${ envFilePath } ` ) ;
2020-12-28 11:48:47 +00:00
const pidFile = argv . pidfile as string ;
if ( pidFile ) {
appLogger ( ) . info ( ` Writing PID to ${ pidFile } ... ` ) ;
fs . removeSync ( pidFile as string ) ;
fs . writeFileSync ( pidFile , ` ${ process . pid } ` ) ;
}
2021-08-14 17:49:01 +01:00
let runCommandAndExitApp = true ;
2021-10-27 19:29:54 +01:00
if ( selectedCommand ) {
const commandArgv = {
. . . argv ,
2024-04-05 12:16:49 +01:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
2021-10-27 19:29:54 +01:00
_ : ( argv as any ) . _ . slice ( ) ,
} ;
commandArgv . _ . splice ( 0 , 1 ) ;
if ( selectedCommand . commandName ( ) === 'db' ) {
await selectedCommand . run ( commandArgv , {
db : null ,
models : null ,
} ) ;
2021-10-23 17:51:44 +01:00
} else {
2021-10-27 19:29:54 +01:00
const connectionCheck = await waitForConnection ( config ( ) . database ) ;
2024-05-11 16:12:54 +01:00
const models = newModelFactory ( connectionCheck . connection , connectionCheck . connection , config ( ) ) ;
2021-10-27 19:29:54 +01:00
await selectedCommand . run ( commandArgv , {
db : connectionCheck.connection ,
models ,
} ) ;
2021-10-23 17:51:44 +01:00
}
2020-12-28 11:48:47 +00:00
} else {
2021-08-14 17:49:01 +01:00
runCommandAndExitApp = false ;
2023-05-17 18:16:29 +01:00
appLogger ( ) . info ( ` Starting server ${ fullVersionString ( config ( ) ) } ( ${ env } ) on port ${ config ( ) . port } and PID ${ process . pid } ... ` ) ;
2021-11-17 12:54:34 +00:00
2021-11-29 18:39:07 +00:00
if ( config ( ) . maxTimeDrift ) {
2023-05-10 12:50:48 +01:00
appLogger ( ) . info ( ` Checking for time drift using NTP server: ${ config ( ) . NTP_SERVER } ` ) ;
const timeDrift = await getDeviceTimeDrift ( config ( ) . NTP_SERVER ) ;
2021-11-29 18:39:07 +00:00
if ( Math . abs ( timeDrift ) > config ( ) . maxTimeDrift ) {
2021-12-16 10:53:28 +01:00
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, or set to 0 to disabled the check. ` ) ;
2021-11-29 18:39:07 +00:00
}
appLogger ( ) . info ( ` NTP time offset: ${ timeDrift } ms ` ) ;
2021-12-16 10:53:28 +01:00
} else {
appLogger ( ) . info ( 'Skipping NTP time check because MAX_TIME_DRIFT is 0.' ) ;
2021-11-17 12:54:34 +00:00
}
2024-05-11 16:12:54 +01:00
const printConnectionCheckInfo = ( connectionCheck : ConnectionCheckResult ) = > {
const connectionCheckLogInfo = { . . . connectionCheck } ;
delete connectionCheckLogInfo . connection ;
appLogger ( ) . info ( 'Connection check:' , connectionCheckLogInfo ) ;
} ;
2021-12-16 11:07:25 +01:00
setLocale ( 'en_GB' ) ;
2021-01-18 10:13:26 +00:00
appLogger ( ) . info ( 'Running in Docker:' , runningInDocker ( ) ) ;
appLogger ( ) . info ( 'Public base URL:' , config ( ) . baseUrl ) ;
2021-05-25 16:42:21 +02:00
appLogger ( ) . info ( 'API base URL:' , config ( ) . apiBaseUrl ) ;
appLogger ( ) . info ( 'User content base URL:' , config ( ) . userContentBaseUrl ) ;
2021-01-18 10:13:26 +00:00
appLogger ( ) . info ( 'Log dir:' , config ( ) . logDir ) ;
appLogger ( ) . info ( 'DB Config:' , markPasswords ( config ( ) . database ) ) ;
2021-11-02 12:51:59 +00:00
appLogger ( ) . info ( 'Mailer Config:' , markPasswords ( config ( ) . mailer ) ) ;
2021-11-09 16:05:42 +00:00
appLogger ( ) . info ( 'Content driver:' , markPasswords ( config ( ) . storageDriver ) ) ;
appLogger ( ) . info ( 'Content driver (fallback):' , markPasswords ( config ( ) . storageDriverFallback ) ) ;
2020-12-28 11:48:47 +00:00
appLogger ( ) . info ( 'Trying to connect to database...' ) ;
const connectionCheck = await waitForConnection ( config ( ) . database ) ;
2024-05-11 16:12:54 +01:00
printConnectionCheckInfo ( connectionCheck ) ;
2020-12-28 11:48:47 +00:00
2021-07-02 18:53:45 +01:00
const ctx = app . context as AppContext ;
2021-01-29 18:45:11 +00:00
2024-01-13 19:33:20 +00:00
await versionCheck ( connectionCheck . connection ) ;
2021-10-27 16:18:42 +01:00
if ( config ( ) . database . autoMigration ) {
appLogger ( ) . info ( 'Auto-migrating database...' ) ;
2022-10-21 11:45:07 +01:00
await migrateLatest ( connectionCheck . connection ) ;
appLogger ( ) . info ( 'Latest migration:' , await latestMigration ( connectionCheck . connection ) ) ;
2021-10-27 16:18:42 +01:00
} else {
2023-08-05 17:42:29 +01:00
if ( ! config ( ) . DB_ALLOW_INCOMPLETE_MIGRATIONS && ( await needsMigration ( connectionCheck . connection ) ) ) {
const list = await migrateList ( connectionCheck . connection , true ) ;
throw new Error ( ` One or more migrations need to be applied: \ n \ n ${ list } ` ) ;
}
2021-10-27 16:18:42 +01:00
appLogger ( ) . info ( 'Skipped database auto-migration.' ) ;
}
2020-12-28 11:48:47 +00:00
2024-05-11 16:12:54 +01:00
if ( config ( ) . DB_USE_SLAVE ) appLogger ( ) . info ( 'Using database replication - trying to connect to slave...' ) ;
const slaveConnectionCheck = config ( ) . DB_USE_SLAVE ? await waitForConnection ( config ( ) . databaseSlave ) : null ;
if ( slaveConnectionCheck ) {
printConnectionCheckInfo ( slaveConnectionCheck ) ;
appLogger ( ) . info ( ` Users with replication: ${ config ( ) . USERS_WITH_REPLICATION } ` ) ;
} else {
appLogger ( ) . info ( 'Not using database replication...' ) ;
}
await setupAppContext (
ctx ,
env ,
connectionCheck . connection ,
slaveConnectionCheck ? slaveConnectionCheck.connection : connectionCheck.connection ,
appLogger ,
) ;
2022-10-21 11:45:07 +01:00
await initializeJoplinUtils ( config ( ) , ctx . joplinBase . models , ctx . joplinBase . services . mustache ) ;
2021-11-11 16:32:34 +00:00
appLogger ( ) . info ( 'Performing main storage check...' ) ;
2024-05-11 16:12:54 +01:00
appLogger ( ) . info ( await storageConnectionCheck ( config ( ) . storageDriver , ctx . joplinBase . db , ctx . joplinBase . dbSlave , ctx . joplinBase . models ) ) ;
2021-11-11 16:32:34 +00:00
if ( config ( ) . storageDriverFallback ) {
appLogger ( ) . info ( 'Performing fallback storage check...' ) ;
2024-05-11 16:12:54 +01:00
appLogger ( ) . info ( await storageConnectionCheck ( config ( ) . storageDriverFallback , ctx . joplinBase . db , ctx . joplinBase . dbSlave , ctx . joplinBase . models ) ) ;
2021-11-11 16:32:34 +00:00
}
2021-11-20 15:19:25 +00:00
appLogger ( ) . info ( 'Starting services...' ) ;
2023-07-26 14:36:13 +01:00
await startServices ( config ( ) , ctx . joplinBase . services ) ;
2021-11-20 15:19:25 +00:00
2021-05-25 16:42:21 +02:00
appLogger ( ) . info ( ` Call this for testing: \` curl ${ config ( ) . apiBaseUrl } /api/ping \` ` ) ;
2020-12-28 11:48:47 +00:00
app . listen ( config ( ) . port ) ;
}
2021-08-14 17:49:01 +01:00
if ( runCommandAndExitApp ) process . exit ( 0 ) ;
2020-12-28 11:48:47 +00:00
}
2024-04-05 12:16:49 +01:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
2020-12-28 11:48:47 +00:00
main ( ) . catch ( ( error : any ) = > {
console . error ( error ) ;
process . exit ( 1 ) ;
} ) ;