From ad13a6f0c150bdd8f5a52062e187010e918ee0cc Mon Sep 17 00:00:00 2001 From: cupcakearmy Date: Wed, 17 Nov 2021 15:36:58 +0100 Subject: [PATCH] progress --- .dockerignore | 5 ++ .gitignore | 1 + Dockerfile | 23 +++++++ README.md | 2 + package.json | 2 + pnpm-lock.yaml | 133 ++++++++++++++++++++++++++++++++++++++ src/config.ts | 39 ++++++++--- src/controllers/image.ts | 6 +- src/controllers/index.ts | 11 +++- src/fastify/hooks.ts | 8 +++ src/fastify/middleware.ts | 8 +++ src/index.ts | 43 ++++++------ 12 files changed, 244 insertions(+), 37 deletions(-) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 src/fastify/hooks.ts create mode 100644 src/fastify/middleware.ts diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..9575226 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,5 @@ +* +!src +!tsconfig.json +!package.json +!pnpm-lock.yaml diff --git a/.gitignore b/.gitignore index 8af382c..2c67709 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ node_modules dist assets *.tsbuildinfo +morphus.* diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..d7e2725 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,23 @@ +FROM node:16-alpine as builder + +WORKDIR /app + +COPY package.json pnpm-lock.yaml ./ +RUN npm exec pnpm i --frozen-lockfile + +COPY . . +RUN npm exec pnpm run build +RUN ls -hal + +FROM node:16-alpine + +WORKDIR /app +COPY package.json pnpm-lock.yaml ./ +RUN npm exec pnpm i --frozen-lockfile --prod + +COPY --from=builder /app/dist ./dist + +ENV ASSETS=/data +ENV ADDRESS=0.0.0.0 +EXPOSE 80 +CMD [ "node", "dist/src" ] diff --git a/README.md b/README.md index bcb19af..cc585f8 100644 --- a/README.md +++ b/README.md @@ -8,3 +8,5 @@ - Multiple storage adapters (Local, S3, GCS) - Caniuse based automatic formatting - Highly performant + +Allowed hosts -> crossorigin on img tag diff --git a/package.json b/package.json index d023c9e..11930ae 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "caniuse-db": "^1.0.30001280", "class-validator": "^0.13.1", "convict": "^6.2.1", + "convict-format-with-validator": "^6.2.0", "device-detector-js": "^3.0.0", "fastify": "^3.23.1", "fastify-caching": "^6.1.0", @@ -30,6 +31,7 @@ "flat": "^5.0.2", "js-yaml": "^4.1.0", "ms": "^2.1.3", + "pino-pretty": "^7.2.0", "sharp": "^0.29.3", "under-pressure": "^5.8.0" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1663d90..11ff39d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10,6 +10,7 @@ specifiers: caniuse-db: ^1.0.30001280 class-validator: ^0.13.1 convict: ^6.2.1 + convict-format-with-validator: ^6.2.0 device-detector-js: ^3.0.0 fastify: ^3.23.1 fastify-caching: ^6.1.0 @@ -18,6 +19,7 @@ specifiers: flat: ^5.0.2 js-yaml: ^4.1.0 ms: ^2.1.3 + pino-pretty: ^7.2.0 sharp: ^0.29.3 ts-node-dev: ^1.1.8 typescript: ^4.4.4 @@ -27,6 +29,7 @@ dependencies: caniuse-db: 1.0.30001280 class-validator: 0.13.1 convict: 6.2.1 + convict-format-with-validator: 6.2.0 device-detector-js: 3.0.0 fastify: 3.23.1 fastify-caching: 6.1.0 @@ -35,6 +38,7 @@ dependencies: flat: 5.0.2 js-yaml: 4.1.0 ms: 2.1.3 + pino-pretty: 7.2.0 sharp: 0.29.3 under-pressure: 5.8.0 @@ -131,6 +135,13 @@ packages: engines: {node: '>=0.10.0'} dev: false + /ansi-styles/3.2.1: + resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} + engines: {node: '>=4'} + dependencies: + color-convert: 1.9.3 + dev: false + /anymatch/3.1.2: resolution: {integrity: sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==} engines: {node: '>= 8'} @@ -162,6 +173,16 @@ packages: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} dev: false + /args/5.0.1: + resolution: {integrity: sha512-1kqmFCFsPffavQFGt8OxJdIcETti99kySRUPMpOhaGjL6mRJn8HFU1OxKY5bMqfZKUwTQc1mZkAjmGYaVOHFtQ==} + engines: {node: '>= 6.0.0'} + dependencies: + camelcase: 5.0.0 + chalk: 2.4.2 + leven: 2.1.0 + mri: 1.1.4 + dev: false + /atomic-sleep/1.0.0: resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} engines: {node: '>=8.0.0'} @@ -247,6 +268,11 @@ packages: engines: {node: '>=0.2.0'} dev: false + /camelcase/5.0.0: + resolution: {integrity: sha512-faqwZqnWxbxn+F1d399ygeamQNy3lPp/H9H6rNrqYh4FSVCtcY+3cub1MxA8o9mDd55mM8Aghuu/kuyYA6VTsA==} + engines: {node: '>=6'} + dev: false + /caniuse-db/1.0.30001280: resolution: {integrity: sha512-b22HvM+u7BBIIG2O1K7dZC2UGVfgnQEM27tDqHRJCaDW5mkQ5/dW+DPRJAmt9xF8fryLN8fEjk6UygMohzzWYA==} dev: false @@ -257,6 +283,15 @@ packages: traverse: 0.3.9 dev: false + /chalk/2.4.2: + resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} + engines: {node: '>=4'} + dependencies: + ansi-styles: 3.2.1 + escape-string-regexp: 1.0.5 + supports-color: 5.5.0 + dev: false + /chokidar/3.5.2: resolution: {integrity: sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ==} engines: {node: '>= 8.10.0'} @@ -294,6 +329,12 @@ packages: engines: {node: '>=0.10.0'} dev: false + /color-convert/1.9.3: + resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} + dependencies: + color-name: 1.1.3 + dev: false + /color-convert/2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -301,6 +342,10 @@ packages: color-name: 1.1.4 dev: false + /color-name/1.1.3: + resolution: {integrity: sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=} + dev: false + /color-name/1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} dev: false @@ -319,6 +364,10 @@ packages: color-string: 1.6.0 dev: false + /colorette/2.0.16: + resolution: {integrity: sha512-hUewv7oMjCp+wkBv5Rm0v87eJhq4woh5rSR+42YSQJKecCqgIqNkZ6lAlQms/BwHPJA5NKMRlpxPRv0n8HQW6g==} + dev: false + /concat-map/0.0.1: resolution: {integrity: sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=} @@ -326,6 +375,13 @@ packages: resolution: {integrity: sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=} dev: false + /convict-format-with-validator/6.2.0: + resolution: {integrity: sha512-2LIL3yEZY27M13UHLIP4mGivusP9h2M+X4mYsRBLexwUp8+0sgVk2MgB2b2bnQwkn293lkbkxgdevzn0nZdyzQ==} + engines: {node: '>=6'} + dependencies: + validator: 13.7.0 + dev: false + /convict/6.2.1: resolution: {integrity: sha512-Mn4AJiYkR3TAZH1Xm/RU7gFS/0kM5TBSAQDry8y40Aez0ASY+3boUhv+3QE5XbOXiXM2JjdhkKve3IsBvWCibQ==} engines: {node: '>=6'} @@ -347,6 +403,10 @@ packages: resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} dev: true + /dateformat/4.6.3: + resolution: {integrity: sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==} + dev: false + /debug/4.3.2: resolution: {integrity: sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==} engines: {node: '>=6.0'} @@ -437,6 +497,11 @@ packages: once: 1.4.0 dev: false + /escape-string-regexp/1.0.5: + resolution: {integrity: sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=} + engines: {node: '>=0.8.0'} + dev: false + /expand-template/2.0.3: resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} engines: {node: '>=6'} @@ -653,6 +718,11 @@ packages: resolution: {integrity: sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==} dev: false + /has-flag/3.0.0: + resolution: {integrity: sha1-tdRU3CGZriJWmfNGfloH87lVuv0=} + engines: {node: '>=4'} + dev: false + /has-unicode/2.0.1: resolution: {integrity: sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=} dev: false @@ -763,6 +833,11 @@ packages: resolution: {integrity: sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=} dev: false + /joycon/3.0.1: + resolution: {integrity: sha512-SJcJNBg32dGgxhPtM0wQqxqV0ax9k/9TaUskGDSJkSFSQOEWWvQ3zzWdGQRIUry2j1zA5+ReH13t0Mf3StuVZA==} + engines: {node: '>=10'} + dev: false + /js-yaml/4.1.0: resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} hasBin: true @@ -778,6 +853,11 @@ packages: resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} dev: false + /leven/2.1.0: + resolution: {integrity: sha1-wuep93IJTe6dNCAq6KzORoeHVYA=} + engines: {node: '>=0.10.0'} + dev: false + /libphonenumber-js/1.9.42: resolution: {integrity: sha512-UBtU0ylpZPKPT8NLIyQJWj/DToMFxmo3Fm5m6qDc0LATvf0SY0qUhaurCEvukAB9Fo+Ia2Anjzqwoupaa64fXg==} dev: false @@ -863,6 +943,11 @@ packages: hasBin: true dev: true + /mri/1.1.4: + resolution: {integrity: sha512-6y7IjGPm8AzlvoUrwAaw1tLnUBudaS3752vcd8JtrpGGQn+rXIe63LFVHm/YMwtqAuh+LJPCFdlLYPWM1nYn6w==} + engines: {node: '>=4'} + dev: false + /ms/2.1.2: resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} dev: false @@ -941,6 +1026,31 @@ packages: engines: {node: '>=8.6'} dev: true + /pino-abstract-transport/0.5.0: + resolution: {integrity: sha512-+KAgmVeqXYbTtU2FScx1XS3kNyfZ5TrXY07V96QnUSFqo2gAqlvmaxH67Lj7SWazqsMabf+58ctdTcBgnOLUOQ==} + dependencies: + duplexify: 4.1.2 + split2: 4.1.0 + dev: false + + /pino-pretty/7.2.0: + resolution: {integrity: sha512-pkZhaF1JiyQt4BRqkLANYWuZTxavmuXh3OHsb8goeQasTFgNdzOECXkZWyPYrA0YMRa8zKoVsCzeYz9lI2NYwA==} + hasBin: true + dependencies: + args: 5.0.1 + colorette: 2.0.16 + dateformat: 4.6.3 + fast-safe-stringify: 2.1.1 + joycon: 3.0.1 + pino-abstract-transport: 0.5.0 + pump: 3.0.0 + readable-stream: 3.6.0 + rfdc: 1.3.0 + secure-json-parse: 2.4.0 + sonic-boom: 2.3.1 + strip-json-comments: 3.1.1 + dev: false + /pino-std-serializers/3.2.0: resolution: {integrity: sha512-EqX4pwDPrt3MuOAAUBMU0Tk5kR/YcCM5fNPEzgCO2zJ5HfX0vbiH9HbJglnyeQsN96Kznae6MWD47pZB5avTrg==} dev: false @@ -1179,6 +1289,12 @@ packages: flatstr: 1.0.12 dev: false + /sonic-boom/2.3.1: + resolution: {integrity: sha512-o0vJPsRiCW5Q0EmRKjNiiYGy2DqSXcxk4mY9vIBSPwmkH/e/vJ2Tq8EECd5NTiO77x8vlVN+ykDjRQJTqf7eKg==} + dependencies: + atomic-sleep: 1.0.0 + dev: false + /source-map-support/0.5.20: resolution: {integrity: sha512-n1lZZ8Ve4ksRqizaBQgxXDgKwttHDhyfQjA6YZZn8+AroHbsIz+JjwxQDxbp+7y5OYCI8t1Yk7etjD9CRd2hIw==} dependencies: @@ -1191,6 +1307,11 @@ packages: engines: {node: '>=0.10.0'} dev: true + /split2/4.1.0: + resolution: {integrity: sha512-VBiJxFkxiXRlUIeyMQi8s4hgvKCSjtknJv/LVYbrgALPwf5zSKmEwV9Lst25AkvMDnvxODugjdl6KZgwKM1WYQ==} + engines: {node: '>= 10.x'} + dev: false + /stream-shift/1.0.1: resolution: {integrity: sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==} dev: false @@ -1242,6 +1363,18 @@ packages: resolution: {integrity: sha1-PFMZQukIwml8DsNEhYwobHygpgo=} engines: {node: '>=0.10.0'} + /strip-json-comments/3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + dev: false + + /supports-color/5.5.0: + resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} + engines: {node: '>=4'} + dependencies: + has-flag: 3.0.0 + dev: false + /tar-fs/2.1.1: resolution: {integrity: sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==} dependencies: diff --git a/src/config.ts b/src/config.ts index a40ab33..1b0141e 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,6 +1,9 @@ +import type { FastifyInstance } from 'fastify' import convict from 'convict' import yaml from 'js-yaml' +convict.addFormat(require('convict-format-with-validator').ipaddress) + export enum StorageType { Local = 'local', // S3 = 's3', @@ -36,6 +39,20 @@ function formatNullableStringOrRegexpArray(values: any) { convict.addParser({ extension: ['yml', 'yaml'], parse: (s) => yaml.load(s, { schema: Schema }) }) const config = convict({ + // Server + port: { + doc: 'The port to bind.', + format: 'port', + default: 80, + env: 'PORT', + }, + address: { + doc: 'The address to bind.', + format: 'ipaddress', + default: '127.0.0.1', + env: 'ADDRESS', + }, + // Security allowedDomains: { doc: 'The domains that are allowed to be used as image sources', @@ -89,16 +106,18 @@ for (const file of ['morphus.yaml', 'morphus.yaml', 'morphus.json']) { } catch {} } -try { - config.validate({ allowed: 'strict' }) -} catch (e) { - if (e instanceof Error) { - console.error(e.message) - } else { - console.error(e) +export function init(App: FastifyInstance) { + try { + config.validate({ allowed: 'strict' }) + App.log.info(config.toString()) + } catch (e) { + if (e instanceof Error) { + App.log.error(e.message) + } else { + App.log.error(e) + } + process.exit(1) } - process.exit(1) } -export const Config = config.get() -console.debug(Config) +export const Config = config.get() diff --git a/src/controllers/image.ts b/src/controllers/image.ts index e52e8df..1d9f40f 100644 --- a/src/controllers/image.ts +++ b/src/controllers/image.ts @@ -177,9 +177,9 @@ export const image: RouteHandlerMethod = async (request, reply) => { } if (Config.allowedHosts) { - const host = request.headers.host - console.debug('Testing host', host, Config.allowedHosts) - if (!host || !testForPrefixOrRegexp(host, Config.allowedHosts)) return ForbiddenError(reply, 'host not allowed') + const origin = request.headers.origin + if (!origin || !testForPrefixOrRegexp(origin, Config.allowedHosts)) + return ForbiddenError(reply, 'origin not allowed') } // @ts-ignore diff --git a/src/controllers/index.ts b/src/controllers/index.ts index b5ef511..78196d3 100644 --- a/src/controllers/index.ts +++ b/src/controllers/index.ts @@ -1,2 +1,9 @@ -export * from './image' -export * from './version' +import { FastifyInstance } from 'fastify' + +import { image } from './image' +import { version } from './version' + +export function init(App: FastifyInstance) { + App.get('/api/image', image) + App.get('/version', version) +} diff --git a/src/fastify/hooks.ts b/src/fastify/hooks.ts new file mode 100644 index 0000000..f7337cc --- /dev/null +++ b/src/fastify/hooks.ts @@ -0,0 +1,8 @@ +import { FastifyInstance } from 'fastify' + +export function init(App: FastifyInstance) { + App.addHook('preHandler', (request, reply, done) => { + reply.header('Server', 'morphus') + done() + }) +} diff --git a/src/fastify/middleware.ts b/src/fastify/middleware.ts new file mode 100644 index 0000000..233c136 --- /dev/null +++ b/src/fastify/middleware.ts @@ -0,0 +1,8 @@ +import { FastifyInstance } from 'fastify' + +export function init(App: FastifyInstance) { + App.register(require('under-pressure')) + App.register(require('fastify-caching')) + App.register(require('fastify-compress'), { global: true }) + App.register(require('fastify-cors'), { origin: '*' }) +} diff --git a/src/index.ts b/src/index.ts index 3bbd236..60b8d52 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,35 +1,34 @@ -// Require the framework and instantiate it import fastify from 'fastify' -import compress from 'fastify-compress' -import cors from 'fastify-cors' -import underPressure from 'under-pressure' -import './config' -import { version } from './controllers' -import { image } from './controllers/image' -import { init } from './storage' +import { Config, init as initConfig } from './config' +import { init as initRoutes } from './controllers' +import { init as initStorage } from './storage' +import { init as initMiddleware } from './fastify/middleware' +import { init as initHooks } from './fastify/hooks' -init() +export const App = fastify({ logger: { prettyPrint: true } }) -const app = fastify({ logger: true }) -app.register(underPressure) -app.register(require('fastify-caching')) -app.register(compress, { global: true }) -app.register(cors, { origin: true }) +// Internal +initConfig(App) +initStorage() -app.addHook('preHandler', (request, reply, done) => { - reply.header('Server', 'morphus') - done() +// Fastify +initMiddleware(App) +initHooks(App) +initRoutes(App) + +process.on('SIGINT', async function () { + App.log.info('Stopping server') + // Close with 2s timeout + await Promise.race([App.close(), new Promise((resolve) => setTimeout(resolve, 2000))]) + process.exit() }) -app.get('/api/image', image) -app.get('/version', version) - async function start() { try { - await app.listen(3000) + await App.listen(Config.port, Config.address) } catch (err) { - app.log.error(err) + App.log.error(err) process.exit(1) } }