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