@sjcrh/proteinpaint-server 2.216.0 → 2.217.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/package.json +4 -4
  2. package/src/app.js +1563 -1117
  3. package/src/serverconfig.js +132 -1
@@ -7,6 +7,7 @@ import fs from 'fs'
7
7
  import os from 'os'
8
8
  import path from 'path'
9
9
  import { fileURLToPath } from 'url'
10
+ import crypto from 'crypto'
10
11
 
11
12
  // import.meta.dirname is undefined when using docker dev environment
12
13
  // use __dirname and __filename global variable convention from commonjs
@@ -364,6 +365,128 @@ if (process.env.PP_MODE?.startsWith('container')) {
364
365
  })
365
366
  }
366
367
 
368
+ /*
369
+ The key for deriving the cache file names and the cachedir subdir name, kept module-local, not in serverconfig,
370
+ so that it is never part of a config dump or a response.
371
+
372
+ Set PP_CACHEID_CREDS to the same value on every instance that shares a cachedir, so that they derive the same names
373
+ and keep finding the existing cache files across restarts. When the server is started with container/envHelpers.mjs,
374
+ as in the container images, PP_CACHEID_CREDS_FILE may name a file with the value instead: envHelpers.mjs reads that
375
+ file and passes its content like the other <NAME>_CREDS values, so that it is not in the initial env of the server
376
+ process. This module does not read PP_CACHEID_CREDS_FILE, so without envHelpers.mjs it is ignored. When not set, a
377
+ random key is generated per process: the cache still works, but every restart or other instance misses on the
378
+ files that were written before.
379
+
380
+ Read here, after the handoff values are set in process.env above, and removed from process.env in every mode, so
381
+ that it is not inherited by a child process. Only native modules are imported by this file, since it is also
382
+ loaded unbundled from the published package, such as by genome/copyDataFilesFromRepo2Tp.js, besides its bundled
383
+ copy in app.js. Only the first loaded copy, which is the bundled one in a server process, reads the key and sets
384
+ up the cache subdir, see firstLoad below.
385
+ */
386
+ const cacheIdKeyValue = process.env.PP_CACHEID_CREDS
387
+ const hasPersistentCacheKey = !!cacheIdKeyValue
388
+ const cacheIdKey = cacheIdKeyValue ? Buffer.from(cacheIdKeyValue, 'utf8') : crypto.randomBytes(32)
389
+ delete process.env.PP_CACHEID_CREDS
390
+
391
+ /** Derive a 32-hex-char cacheId from the given object via
392
+ * HMAC-sha256(key, JSON.stringify([scope, args])). Truncation at 32 chars is safe
393
+ * for cache keys — collision probability is negligible at realistic cache sizes.
394
+ * Callers shape `args` to include only the fields whose identity
395
+ * determines the cache key, and must construct it with a stable key order
396
+ * (object literals do this naturally).
397
+ *
398
+ * `scope` is optional, and separates cacheIds for the same args, e.g. per user or session
399
+ * when the result depends on what the requester may access. Without a scope, identical
400
+ * args share one cacheId, which is the intended behavior for results that are the same for
401
+ * every requester. */
402
+ export function generateHash(args, scope = '') {
403
+ return crypto
404
+ .createHmac('sha256', cacheIdKey)
405
+ .update(JSON.stringify([scope, args]))
406
+ .digest('hex')
407
+ .slice(0, 32)
408
+ }
409
+
410
+ /*
411
+ true for the first loaded copy of this module in a process. A later copy, such as the unbundled file that a genome
412
+ file imports, finds no PP_CACHEID_CREDS since the first copy removed it, so it would derive a different cache
413
+ subdir. The flag only marks that the cache subdir is set up, and does not hold its name.
414
+ */
415
+ const firstLoadFlag = Symbol.for('proteinpaint.serverconfig.firstLoad')
416
+ const firstLoad = !globalThis[firstLoadFlag]
417
+ // configurable, so that a test can evaluate another first copy
418
+ if (firstLoad) Object.defineProperty(globalThis, firstLoadFlag, { value: true, configurable: true })
419
+
420
+ /*
421
+ Use an unlisted subdir of the configured cachedir, whose name is derived from the PP_CACHEID_CREDS key, so that the
422
+ cache files cannot be found by listing directories. On by default in a container, and may be set with
423
+ serverconfig.hideCachedir in any mode. The configured cachedir must then be owned by another user than the server
424
+ process, such as root, with mode 1733: the server user may create and use a subdir with a known name, but may not
425
+ list the entries or change the mode. A dir that is owned by the server user can always be listed by that user,
426
+ since the owner may change its mode.
427
+
428
+ All code that uses the path must copy serverconfig.cachedir to a module-local variable when it is loaded:
429
+ app.ts deletes serverconfig.cachedir before the server starts listening.
430
+ */
431
+ if (serverconfig.hideCachedir ?? process.env.PP_MODE?.startsWith('container')) {
432
+ // a later copy does not use the cache, and must not use the configured cachedir, which is the parent of the subdir
433
+ if (!firstLoad) delete serverconfig.cachedir
434
+ else {
435
+ const parent = serverconfig.cachedir
436
+ if (!parent) throw 'serverconfig.cachedir missing'
437
+ // a parent dir that is created here is owned by the server user, which the warning below reports
438
+ if (!fs.existsSync(parent)) fs.mkdirSync(parent, { recursive: true })
439
+ let parentEntries
440
+ try {
441
+ parentEntries = fs.readdirSync(parent)
442
+ console.warn(
443
+ `WARNING: serverconfig.cachedir='${parent}' can be listed by the server process, so the cache subdir name ` +
444
+ `is not hidden; the dir should be owned by another user, such as root, with mode 1733`
445
+ )
446
+ } catch (e) {
447
+ // EACCES is the expected result; another error is reported by the mkdirSync() below
448
+ }
449
+ if (!hasPersistentCacheKey) {
450
+ console.warn(
451
+ `WARNING: PP_CACHEID_CREDS is not set, so the cache subdir name is generated for this process only, ` +
452
+ `and the files cached by an earlier process are not found or removed`
453
+ )
454
+ }
455
+ // not a generateHash() value, since the HMAC input is not a JSON array
456
+ const dirName = crypto.createHmac('sha256', cacheIdKey).update('cachedir').digest('hex').slice(0, 32)
457
+ serverconfig.cachedir = path.join(parent, dirName)
458
+ // the name is not logged, so that it is not in any log output
459
+ fs.mkdirSync(serverconfig.cachedir, { recursive: true, mode: 0o700 })
460
+ if (parentEntries) moveEarlierCacheEntries(parent, parentEntries, dirName)
461
+ }
462
+ }
463
+ delete serverconfig.hideCachedir
464
+
465
+ /*
466
+ Moves the entries of the earlier cache layout, such as the saved sessions in massSession/, from the configured
467
+ cachedir into the derived subdir, where they are still found by the server and evicted by the cache monitor.
468
+ This is only possible while the configured cachedir can be listed, such as on the first start with the derived
469
+ subdir, before the dir owner and mode are changed. An entry with the name shape of a derived subdir, such as from
470
+ another instance with a different key, is not moved, and neither is an entry whose name already exists in the
471
+ derived subdir.
472
+ */
473
+ function moveEarlierCacheEntries(parent, entries, dirName) {
474
+ let moved = 0
475
+ for (const name of entries) {
476
+ if (name == dirName || /^[0-9a-f]{32}$/.test(name)) continue
477
+ const dest = path.join(parent, dirName, name)
478
+ if (fs.existsSync(dest)) continue
479
+ try {
480
+ fs.renameSync(path.join(parent, name), dest)
481
+ moved++
482
+ } catch (e) {
483
+ // such as EXDEV for a separately mounted entry, which then stays where it is
484
+ console.warn(`WARNING: unable to move the earlier cache entry '${name}' into the cache subdir: ${e.code || e}`)
485
+ }
486
+ }
487
+ if (moved) console.log(`moved ${moved} earlier cache entries into the cache subdir`)
488
+ }
489
+
367
490
  // when a mandatory setting is not defined in any ds, declare its default here
368
491
 
369
492
  if (process.argv.find(a => a == 'validate')) {
@@ -390,6 +513,13 @@ if (!serverconfig.backend_only && fs.existsSync(publicDir)) serverconfig.publicD
390
513
  const binDir = path.join(process.cwd(), './bin')
391
514
  if (!serverconfig.backend_only) serverconfig.binDir = binDir
392
515
 
516
+ // The proteinpaint-front package's own public/index.html and public/cards, which its init copies into public/ when
517
+ // missing there. The server serves these paths from this dir when public/ does not have them, such as a public/ mount
518
+ // that the init cannot write to (see app.middlewares.js). Auto-computed from cwd like binDir, never operator-set.
519
+ const frontPublicDir = path.join(process.cwd(), 'node_modules/@sjcrh/proteinpaint-front/public')
520
+ delete serverconfig.frontPublicDir
521
+ if (!serverconfig.backend_only && fs.existsSync(frontPublicDir)) serverconfig.frontPublicDir = frontPublicDir
522
+
393
523
  if (serverconfig.publicDir) {
394
524
  const defaultTarget = path.join(serverconfig.binpath, 'cards')
395
525
  if (!serverconfig.cards) {
@@ -410,7 +540,8 @@ if (fs.existsSync('./package.json')) {
410
540
  serverconfig.version = JSON.parse(pkg).version
411
541
  }
412
542
 
413
- if (!serverconfig.cache_snpgt) {
543
+ // a later copy may have no cachedir, see firstLoad above
544
+ if (!serverconfig.cache_snpgt && serverconfig.cachedir) {
414
545
  serverconfig.cache_snpgt = {
415
546
  dir: path.join(serverconfig.cachedir, 'snpgt'),
416
547
  fileNameRegexp: /[^\w]/, // client-provided cache file name matching with this are denied