cloudrun-malware-scanner/gcs-proxy-server.ts (109 lines of code) (raw):

/* * Copyright 2022 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import * as process from 'node:process'; import {GoogleAuth} from 'google-auth-library'; import {logger} from './logger.js'; import {readAndVerifyConfig} from './config.js'; import * as httpProxy from 'http-proxy'; import {Storage} from '@google-cloud/storage'; import {name as packageName, version as packageVersion} from './package.json'; import {IncomingMessage, ServerResponse, ClientRequest} from 'http'; import {Socket} from 'node:net'; const TOKEN_REFRESH_THRESHOLD_MILLIS = 60000; const googleAuth = new GoogleAuth(); let accessToken: string | null | undefined; let accessTokenRefreshTimeout: NodeJS.Timeout | null = null; let clamCvdMirrorBucket = 'uninitialized'; async function accessTokenRefresh() { if (accessTokenRefreshTimeout) { clearTimeout(accessTokenRefreshTimeout); accessTokenRefreshTimeout = null; } const client = await googleAuth.getClient(); if ( !client.credentials?.expiry_date || client.credentials.expiry_date <= new Date().getTime() + TOKEN_REFRESH_THRESHOLD_MILLIS ) { accessToken = await googleAuth.getAccessToken(); logger.info( `Refreshed Access token; expires at ${new Date( client.credentials.expiry_date!, // Non-null assertion is safe here due to the check above ).toISOString()}`, ); } const nextCheckDate = new Date( client.credentials.expiry_date! - TOKEN_REFRESH_THRESHOLD_MILLIS, // Non-null assertion is safe here due to the check above ); logger.debug( `Next access token refresh check at ${nextCheckDate.toISOString()}`, ); accessTokenRefreshTimeout = setTimeout( // eslint-disable-next-line @typescript-eslint/no-misused-promises accessTokenRefresh, nextCheckDate.getTime() - new Date().getTime(), ); } function handleProxyError( err: Error, req: IncomingMessage, res: ServerResponse | Socket, // eslint-disable-next-line @typescript-eslint/no-unused-vars _target?: httpProxy.ProxyTargetUrl, ): void { logger.error( `Failed to proxy to GCS for path ${req.url}, returning code 500: ${err}`, ); (res as ServerResponse).writeHead(500, { 'Content-Type': 'text/plain', }); res.end(`Failed to proxy to GCS: internal error\n`); } function handleProxyReq( proxyReq: ClientRequest, req: IncomingMessage, res: ServerResponse, ): void { if (proxyReq.path?.startsWith(`/${clamCvdMirrorBucket}/`)) { logger.info(`Proxying request for ${proxyReq.path} to GCS`); proxyReq.setHeader('Authorization', 'Bearer ' + accessToken); } else { logger.error(`Denying Proxy request for ${proxyReq.path} to GCS - 403`); res.writeHead(403, { 'Content-Type': 'text/plain', }); res.end('Failed to proxy to GCS - unauthorzied path: status 403\n'); } } function setupGcsReverseProxy() { const proxy = httpProxy.createProxyServer({ target: 'https://storage.googleapis.com/', changeOrigin: true, autoRewrite: true, secure: true, ws: false, }); proxy.on('proxyReq', handleProxyReq); proxy.on('error', handleProxyError); const PROXY_PORT = parseInt(process.env.PROXY_PORT || '8888', 10); proxy.listen(PROXY_PORT, 'localhost'); logger.info( `GCS authenticating reverse proxy listenting on port ${PROXY_PORT} for requests to ${clamCvdMirrorBucket}`, ); } async function run(): Promise<void> { let configFile; if (process.argv.length >= 3) { configFile = process.argv[2]; } else { configFile = './config.json'; } const storage = new Storage({ userAgent: `cloud-solutions/${packageName}-usage-v${packageVersion}`, }); const config = await readAndVerifyConfig(configFile, storage); clamCvdMirrorBucket = config.ClamCvdMirrorBucket; await accessTokenRefresh(); setupGcsReverseProxy(); } // Start the service, exiting on error. run().catch((e) => { logger.fatal(e); logger.fatal('Exiting'); process.exit(1); });