This commit is contained in:
cupcakearmy 2021-11-17 15:36:58 +01:00
parent fc79be588f
commit ad13a6f0c1
No known key found for this signature in database
GPG Key ID: 3235314B4D31232F
12 changed files with 244 additions and 37 deletions

5
.dockerignore Normal file
View File

@ -0,0 +1,5 @@
*
!src
!tsconfig.json
!package.json
!pnpm-lock.yaml

1
.gitignore vendored
View File

@ -2,3 +2,4 @@ node_modules
dist
assets
*.tsbuildinfo
morphus.*

23
Dockerfile Normal file
View File

@ -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" ]

View File

@ -8,3 +8,5 @@
- Multiple storage adapters (Local, S3, GCS)
- Caniuse based automatic formatting
- Highly performant
Allowed hosts -> crossorigin on img tag

View File

@ -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"
}

View File

@ -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:

View File

@ -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()

View File

@ -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

View File

@ -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)
}

8
src/fastify/hooks.ts Normal file
View File

@ -0,0 +1,8 @@
import { FastifyInstance } from 'fastify'
export function init(App: FastifyInstance) {
App.addHook('preHandler', (request, reply, done) => {
reply.header('Server', 'morphus')
done()
})
}

View File

@ -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: '*' })
}

View File

@ -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)
}
}