diff --git a/config.ts b/config.ts index 17cb4db8ba..211a295e51 100644 --- a/config.ts +++ b/config.ts @@ -1,5 +1,6 @@ import type { ConfigLoader } from '@balena/pinejs'; import * as balenaModel from './src/balena'; +import { getFileUploadHandler } from './src/fileupload-handler'; export = { models: [balenaModel], @@ -25,4 +26,5 @@ export = { ], }, ], + webResourceHandler: getFileUploadHandler(), } as ConfigLoader.Config; diff --git a/config/confd/conf.d/cloudront-pk.pem.toml b/config/confd/conf.d/cloudront-pk.pem.toml new file mode 100644 index 0000000000..8127f27590 --- /dev/null +++ b/config/confd/conf.d/cloudront-pk.pem.toml @@ -0,0 +1,7 @@ +[template] +src = "cloudfront-pk.pem.tmpl" +dest = "/etc/ssl/private/cloudfront-pk.pem" +keys = [ + "WEBRESOURCES_CLOUDFRONT_PRIVATEKEY", +] +mode = "0400" diff --git a/config/confd/conf.d/env.toml b/config/confd/conf.d/env.toml index e7d28b0fc6..d31a19a488 100644 --- a/config/confd/conf.d/env.toml +++ b/config/confd/conf.d/env.toml @@ -53,5 +53,13 @@ keys = [ "VPN_PORT", "VPN_SERVICE_API_KEY", "VPN_GUEST_API_KEY", - "VPN_CONNECT_PROXY_PORT" + "VPN_CONNECT_PROXY_PORT", + "WEBRESOURCES_S3_ACCESS_KEY", + "WEBRESOURCES_S3_SECRET_KEY", + "WEBRESOURCES_S3_REGION", + "WEBRESOURCES_S3_HOST", + "WEBRESOURCES_S3_BUCKET", + "WEBRESOURCES_S3_MAX_FILESIZE", + "WEBRESOURCES_CLOUDFRONT_PUBLICKEY", + "WEBRESOURCES_CLOUDFRONT_HOST" ] diff --git a/config/confd/templates/cloudfront-pk.pem.tmpl b/config/confd/templates/cloudfront-pk.pem.tmpl new file mode 100644 index 0000000000..068ad8bd9f --- /dev/null +++ b/config/confd/templates/cloudfront-pk.pem.tmpl @@ -0,0 +1 @@ +{{base64Decode (getenv "WEBRESOURCES_CLOUDFRONT_PRIVATEKEY" "")}} diff --git a/config/confd/templates/env.tmpl b/config/confd/templates/env.tmpl index f6284dba7f..3f73a75635 100644 --- a/config/confd/templates/env.tmpl +++ b/config/confd/templates/env.tmpl @@ -79,3 +79,11 @@ VPN_SERVICE_API_KEY={{getenv "VPN_SERVICE_API_KEY"}} {{if getenv "VPN_GUEST_API_KEY"}}VPN_GUEST_API_KEY={{getenv "VPN_GUEST_API_KEY"}}{{end}} {{if getenv "AUTH_RESINOS_REGISTRY_CODE"}}AUTH_RESINOS_REGISTRY_CODE={{getenv "AUTH_RESINOS_REGISTRY_CODE"}}{{end}} {{if getenv "BROTLI_COMPRESSION_QUALITY"}}BROTLI_COMPRESSION_QUALITY={{getenv "BROTLI_COMPRESSION_QUALITY"}}{{end}} +{{if getenv "WEBRESOURCES_S3_ACCESS_KEY"}}WEBRESOURCES_S3_ACCESS_KEY={{getenv "WEBRESOURCES_S3_ACCESS_KEY"}}{{end}} +{{if getenv "WEBRESOURCES_S3_SECRET_KEY"}}WEBRESOURCES_S3_SECRET_KEY={{getenv "WEBRESOURCES_S3_SECRET_KEY"}}{{end}} +{{if getenv "WEBRESOURCES_S3_REGION"}}WEBRESOURCES_S3_REGION={{getenv "WEBRESOURCES_S3_REGION"}}{{end}} +{{if getenv "WEBRESOURCES_S3_HOST"}}WEBRESOURCES_S3_HOST={{getenv "WEBRESOURCES_S3_HOST"}}{{end}} +{{if getenv "WEBRESOURCES_S3_BUCKET"}}WEBRESOURCES_S3_BUCKET={{getenv "WEBRESOURCES_S3_BUCKET"}}{{end}} +{{if getenv "WEBRESOURCES_S3_MAX_FILESIZE"}}WEBRESOURCES_S3_MAX_FILESIZE={{getenv "WEBRESOURCES_S3_MAX_FILESIZE"}}{{end}} +{{if getenv "WEBRESOURCES_CLOUDFRONT_PUBLICKEY"}}WEBRESOURCES_CLOUDFRONT_PUBLICKEY={{getenv "WEBRESOURCES_CLOUDFRONT_PUBLICKEY"}}{{end}} +{{if getenv "WEBRESOURCES_CLOUDFRONT_HOST"}}WEBRESOURCES_CLOUDFRONT_HOST={{getenv "WEBRESOURCES_CLOUDFRONT_HOST"}}{{end}} diff --git a/package-lock.json b/package-lock.json index 58f608cc71..cd8f9c3eaf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "@balena/es-version": "^1.0.2", "@balena/node-metrics-gatherer": "^6.0.3", "@balena/pinejs": "^15.3.0", + "@balena/pinejs-webresource-cloudfront": "^0.0.3", "@sentry/node": "^7.49.0", "@types/basic-auth": "^1.1.3", "@types/bluebird": "^3.5.38", @@ -467,6 +468,17 @@ "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==", "optional": true }, + "node_modules/@aws-sdk/cloudfront-signer": { + "version": "3.398.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/cloudfront-signer/-/cloudfront-signer-3.398.0.tgz", + "integrity": "sha512-fyUty9SNI3oiOSvgVcK0S2OmihawzqWCR5TdcZ2EWbpiLk0V94U5BaKoIKu6jYY+57OgGjr/vUDrDideT/0cMw==", + "dependencies": { + "@smithy/url-parser": "^2.0.5" + }, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@aws-sdk/config-resolver": { "version": "3.200.0", "resolved": "https://registry.npmjs.org/@aws-sdk/config-resolver/-/config-resolver-3.200.0.tgz", @@ -2937,9 +2949,9 @@ } }, "node_modules/@balena/pinejs": { - "version": "15.3.0", - "resolved": "https://registry.npmjs.org/@balena/pinejs/-/pinejs-15.3.0.tgz", - "integrity": "sha512-FxXXRIJZgPapPYVOM0/F46BmNwr6FRgOhRoOXu8QgpXMPodT0ufvngATiDAhngTn3BnfcWVanntNUJh82h0Zbg==", + "version": "15.3.2", + "resolved": "https://registry.npmjs.org/@balena/pinejs/-/pinejs-15.3.2.tgz", + "integrity": "sha512-Zn/aYDu00UNbE88dtXkj9URZT6X9LsRgMCjV9GojDJ2mO5ti0Rd42uGKORVi6O+7C/RoM/0BSXT/SiH88uPpBg==", "dependencies": { "@balena/abstract-sql-compiler": "^9.0.3", "@balena/abstract-sql-to-typescript": "^2.1.0", @@ -3006,6 +3018,16 @@ "serve-static": "^1.15.0" } }, + "node_modules/@balena/pinejs-webresource-cloudfront": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/@balena/pinejs-webresource-cloudfront/-/pinejs-webresource-cloudfront-0.0.3.tgz", + "integrity": "sha512-DgJSovaoVlbpBIMDDpXyRBFnIR7RPY5E3ImO7enVKsaO1P8UXM+BS4JjZMMJhwV068jA6AkL4evcziISoHcewg==", + "dependencies": { + "@aws-sdk/cloudfront-signer": "^3.398.0", + "@balena/pinejs": "^15.3.2", + "memoizee": "^0.4.15" + } + }, "node_modules/@balena/sbvr-parser": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/@balena/sbvr-parser/-/sbvr-parser-1.4.3.tgz", @@ -3663,12 +3685,11 @@ "optional": true }, "node_modules/@smithy/querystring-parser": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-2.0.1.tgz", - "integrity": "sha512-h+e7k1z+IvI2sSbUBG9Aq46JsgLl4UqIUl6aigAlRBj+P6ocNXpM6Yn1vMBw5ijtXeZbYpd1YvCxwDgdw3jhmg==", - "optional": true, + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-2.0.5.tgz", + "integrity": "sha512-C2stCULH0r54KBksv3AWcN8CLS3u9+WsEW8nBrvctrJ5rQTNa1waHkffpVaiKvcW2nP0aIMBPCobD/kYf/q9mA==", "dependencies": { - "@smithy/types": "^2.0.2", + "@smithy/types": "^2.2.2", "tslib": "^2.5.0" }, "engines": { @@ -3676,10 +3697,9 @@ } }, "node_modules/@smithy/querystring-parser/node_modules/tslib": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.1.tgz", - "integrity": "sha512-t0hLfiEKfMUoqhG+U1oid7Pva4bbDPHYfJNiB7BiIjRkj1pyC++4N3huJfqY6aRH6VTB0rvtzQwjM4K6qpfOig==", - "optional": true + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, "node_modules/@smithy/signature-v4": { "version": "2.0.1", @@ -3728,10 +3748,9 @@ "optional": true }, "node_modules/@smithy/types": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@smithy/types/-/types-2.0.2.tgz", - "integrity": "sha512-wcymEjIXQ9+NEfE5Yt5TInAqe1o4n+Nh+rh00AwoazppmUt8tdo6URhc5gkDcOYrcvlDVAZE7uG69nDpEGUKxw==", - "optional": true, + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-2.2.2.tgz", + "integrity": "sha512-4PS0y1VxDnELGHGgBWlDksB2LJK8TG8lcvlWxIsgR+8vROI7Ms8h1P4FQUx+ftAX2QZv5g1CJCdhdRmQKyonyw==", "dependencies": { "tslib": "^2.5.0" }, @@ -3742,25 +3761,22 @@ "node_modules/@smithy/types/node_modules/tslib": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.1.tgz", - "integrity": "sha512-t0hLfiEKfMUoqhG+U1oid7Pva4bbDPHYfJNiB7BiIjRkj1pyC++4N3huJfqY6aRH6VTB0rvtzQwjM4K6qpfOig==", - "optional": true + "integrity": "sha512-t0hLfiEKfMUoqhG+U1oid7Pva4bbDPHYfJNiB7BiIjRkj1pyC++4N3huJfqY6aRH6VTB0rvtzQwjM4K6qpfOig==" }, "node_modules/@smithy/url-parser": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-2.0.1.tgz", - "integrity": "sha512-NpHVOAwddo+OyyIoujDL9zGL96piHWrTNXqltWmBvlUoWgt1HPyBuKs6oHjioyFnNZXUqveTOkEEq0U5w6Uv8A==", - "optional": true, + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-2.0.5.tgz", + "integrity": "sha512-OdMBvZhpckQSkugCXNJQCvqJ71wE7Ftxce92UOQLQ9pwF6hoS5PLL7wEfpnuEXtStzBqJYkzu1C1ZfjuFGOXAA==", "dependencies": { - "@smithy/querystring-parser": "^2.0.1", - "@smithy/types": "^2.0.2", + "@smithy/querystring-parser": "^2.0.5", + "@smithy/types": "^2.2.2", "tslib": "^2.5.0" } }, "node_modules/@smithy/url-parser/node_modules/tslib": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.1.tgz", - "integrity": "sha512-t0hLfiEKfMUoqhG+U1oid7Pva4bbDPHYfJNiB7BiIjRkj1pyC++4N3huJfqY6aRH6VTB0rvtzQwjM4K6qpfOig==", - "optional": true + "integrity": "sha512-t0hLfiEKfMUoqhG+U1oid7Pva4bbDPHYfJNiB7BiIjRkj1pyC++4N3huJfqY6aRH6VTB0rvtzQwjM4K6qpfOig==" }, "node_modules/@smithy/util-base64": { "version": "2.0.0", diff --git a/package.json b/package.json index 7078aa51ef..0e9d44fe3f 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "@balena/es-version": "^1.0.2", "@balena/node-metrics-gatherer": "^6.0.3", "@balena/pinejs": "^15.3.0", + "@balena/pinejs-webresource-cloudfront": "^0.0.3", "@sentry/node": "^7.49.0", "@types/basic-auth": "^1.1.3", "@types/bluebird": "^3.5.38", diff --git a/src/fileupload-handler.ts b/src/fileupload-handler.ts new file mode 100644 index 0000000000..1d1210dfc2 --- /dev/null +++ b/src/fileupload-handler.ts @@ -0,0 +1,85 @@ +import { webResourceHandler } from '@balena/pinejs'; +import { + CloudFrontHandler, + CloudFrontHandlerProps, +} from '@balena/pinejs-webresource-cloudfront'; +import { + WEBRESOURCES_S3_ACCESS_KEY, + WEBRESOURCES_S3_SECRET_KEY, + WEBRESOURCES_S3_REGION, + WEBRESOURCES_S3_HOST, + WEBRESOURCES_S3_BUCKET, + WEBRESOURCES_S3_MAX_FILESIZE, + WEBRESOURCES_CLOUDFRONT_PRIVATEKEY, + WEBRESOURCES_CLOUDFRONT_PUBLICKEY, + WEBRESOURCES_CLOUDFRONT_HOST, +} from './lib/config'; + +const getS3Config = (): webResourceHandler.S3HandlerProps | undefined => { + if ( + WEBRESOURCES_S3_ACCESS_KEY != null && + WEBRESOURCES_S3_SECRET_KEY != null && + WEBRESOURCES_S3_REGION != null && + WEBRESOURCES_S3_HOST != null && + WEBRESOURCES_S3_BUCKET != null + ) { + return { + accessKey: WEBRESOURCES_S3_ACCESS_KEY, + secretKey: WEBRESOURCES_S3_SECRET_KEY, + region: WEBRESOURCES_S3_REGION, + endpoint: WEBRESOURCES_S3_HOST, + bucket: WEBRESOURCES_S3_BUCKET, + maxSize: WEBRESOURCES_S3_MAX_FILESIZE, + }; + } +}; + +const getCloudfrontConfig = (): CloudFrontHandlerProps | undefined => { + const s3Config = getS3Config(); + if ( + s3Config != null && + WEBRESOURCES_CLOUDFRONT_PRIVATEKEY != null && + WEBRESOURCES_CLOUDFRONT_PUBLICKEY != null && + WEBRESOURCES_CLOUDFRONT_HOST != null + ) { + return { + cfPublicKeyId: WEBRESOURCES_CLOUDFRONT_PUBLICKEY, + cfSecretKey: WEBRESOURCES_CLOUDFRONT_PRIVATEKEY, + cfDistributionDomain: WEBRESOURCES_CLOUDFRONT_HOST, + ...s3Config, + }; + } +}; + +let handler: webResourceHandler.WebResourceHandler | undefined; +export const getFileUploadHandler = () => { + if (handler == null) { + const cfConfig = getCloudfrontConfig(); + if (cfConfig != null) { + handler = new CloudFrontHandler(cfConfig); + console.log('Successfully initialised webresource CloudFront handler.'); + console.log({ + region: cfConfig.region, + endpoint: cfConfig.endpoint, + bucket: cfConfig.bucket, + cFhost: cfConfig.cfDistributionDomain, + }); + return handler; + } + + const s3Config = getS3Config(); + if (s3Config != null) { + handler = new webResourceHandler.S3Handler(s3Config); + console.log('Successfully initialised webresource S3 handler.'); + console.log({ + region: s3Config.region, + endpoint: s3Config.endpoint, + bucket: s3Config.bucket, + }); + return handler; + } + + console.log('No webresource handler loaded.'); + } + return handler; +}; diff --git a/src/lib/config.ts b/src/lib/config.ts index 8e45afd510..39b14ae29c 100644 --- a/src/lib/config.ts +++ b/src/lib/config.ts @@ -415,6 +415,29 @@ export const IGNORE_FROZEN_DEVICE_PERMISSIONS = boolVar( false, ); +export const WEBRESOURCES_S3_HOST = optionalVar('WEBRESOURCES_S3_HOST'); +export const WEBRESOURCES_S3_REGION = optionalVar('WEBRESOURCES_S3_REGION'); +export const WEBRESOURCES_S3_ACCESS_KEY = optionalVar( + 'WEBRESOURCES_S3_ACCESS_KEY', +); +export const WEBRESOURCES_S3_SECRET_KEY = optionalVar( + 'WEBRESOURCES_S3_SECRET_KEY', +); +export const WEBRESOURCES_S3_BUCKET = optionalVar('WEBRESOURCES_S3_BUCKET'); +export const WEBRESOURCES_S3_MAX_FILESIZE = intVar( + 'WEBRESOURCES_S3_MAX_FILESIZE', + 10000000, +); +export const WEBRESOURCES_CLOUDFRONT_PRIVATEKEY = optionalVar( + 'WEBRESOURCES_CLOUDFRONT_PRIVATEKEY', +); +export const WEBRESOURCES_CLOUDFRONT_PUBLICKEY = optionalVar( + 'WEBRESOURCES_CLOUDFRONT_PUBLICKEY', +); +export const WEBRESOURCES_CLOUDFRONT_HOST = optionalVar( + 'WEBRESOURCES_CLOUDFRONT_HOST', +); + /** * Splits an env var in the format of `${username}:${password}` * into a RedisAuth object. Auth is optional, so this can return