/** * Copyright 2023 Kapeta Inc. * SPDX-License-Identifier: BUSL-1.1 */ import _ from 'lodash'; import request from 'request'; import Path from 'path'; import { pathTemplateParser } from '../../utils/pathTemplateParser'; import { networkManager } from '../../networkManager'; import { socketManager } from '../../socketManager'; import { Request } from 'express'; import { ProxyRequestHandler, ProxyRequestInfo, SimpleRequest, SimpleResponse, StringMap } from '../../types'; import { Resource } from '@kapeta/schemas'; import { stringify } from 'qs'; import { proxyHttpRequest } from './web'; import { DSLConverters, HTTPTransport, RESTMethod, maskSensitiveData } from '@kapeta/kaplang-core'; import { EnrichedAsset } from '../../assetManager'; export function getRestMethodId(restResource: Resource, httpMethod: string, httpPath: string) { return _.findKey(restResource.spec.methods, (method) => { let methodType = method.method ? method.method.toUpperCase() : 'GET'; if (methodType.toUpperCase() !== httpMethod.toUpperCase()) { return false; } let path = method.path; if (restResource.spec.basePath) { path = Path.join(restResource.spec.basePath, path); } const pathTemplate = pathTemplateParser(path); return pathTemplate.matches(httpPath); }); } interface RESTMethodWithId extends RESTMethod { id: string; } /** * * @param req {Request} * @param opts {ProxyRequestInfo} * @return {{consumerMethod: *, providerMethod: *}} */ function resolveMethods(req: Request, opts: ProxyRequestInfo) { const consumerMethodId = getRestMethodId(opts.consumerResource, req.method, opts.consumerPath); if (!consumerMethodId) { throw new Error( `Consumer method not found for path "${req.method} ${opts.consumerPath}" in resource "${req.params.consumerInstanceId}::${req.params.consumerResourceName}` ); } const consumerMethod = _.cloneDeep(opts.consumerResource.spec.methods[consumerMethodId] as RESTMethodWithId); if (!consumerMethod) { throw new Error( `Consumer method not found for path "${req.method} ${opts.consumerPath}" in resource "${req.params.consumerInstanceId}::${req.params.consumerResourceName}` ); } consumerMethod.id = consumerMethodId; const providerMethodId = _.findKey(opts.connection.mapping, (mapping) => { return mapping.targetId === consumerMethodId; }); if (!providerMethodId) { throw new Error(`Connection contained no mapping for consumer method "${consumerMethodId}`); } const providerMethod = _.cloneDeep(opts.providerResource.spec.methods[providerMethodId] as RESTMethodWithId); if (!providerMethod) { throw new Error( `Provider method not found "${providerMethodId}" in resource "${opts.connection.provider.blockId}::${opts.connection.provider.resourceName}` ); } providerMethod.id = providerMethodId; return { consumerMethod, providerMethod, }; } function resolveEntitiesSource(blockAsset: EnrichedAsset) { return (blockAsset.data.spec?.entities?.source?.value as string) || ''; } const MASK_STRING = '*******'; function maskSimpleRequest(req: SimpleRequest, consumerMethod: RESTMethod, entitiesSource: string) { const maskedRequest = _.cloneDeep(req); Object.entries(consumerMethod.arguments || {}).forEach(([key, value]) => { if (value.transport === HTTPTransport.BODY && typeof maskedRequest.body === 'string') { try { maskedRequest.body = maskSensitiveData( maskedRequest.body, DSLConverters.fromSchemaType(value), entitiesSource, MASK_STRING ); } catch (error) { // Ignore errors masking the request body } } // TODO: Mask HEADER // TODO: Mask QUERY }); return maskedRequest; } function maskSimpleResponse(res: SimpleResponse, consumerMethod: RESTMethod, entitiesSource: string) { const maskedResponse = _.cloneDeep(res); Object.entries(consumerMethod.arguments || {}).forEach(([key, value]) => { // TODO: Mask HEADER }); try { if (typeof maskedResponse.body === 'string') { maskedResponse.body = maskSensitiveData( maskedResponse.body, DSLConverters.fromSchemaType(consumerMethod.responseType), entitiesSource, MASK_STRING ); } } catch (error) { // Ignore errors masking the response body } return maskedResponse; } export const proxyRestRequest: ProxyRequestHandler = (req, res, opts) => { if (_.isEmpty(opts.consumerResource.spec.methods) && _.isEmpty(opts.providerResource.spec.methods)) { // If there are no methods defined, we assume the user controls the path and we just proxy the raw request return proxyHttpRequest(req, res, opts); } const { consumerMethod, providerMethod } = resolveMethods(req, opts); const consumerEntitiesSource = resolveEntitiesSource(opts.consumerBlockAsset); const providerEntitiesSource = resolveEntitiesSource(opts.providerBlockAsset); const consumerPathTemplate = pathTemplateParser(consumerMethod.path); const providerPathTemplate = pathTemplateParser(providerMethod.path); const pathVariables = consumerPathTemplate.parse(opts.consumerPath); if (!pathVariables) { res.status(400).send({ error: `Path did not match any patterns: "${opts.consumerPath}"`, }); return; } let providerPath = providerPathTemplate.create(pathVariables); if (!providerPath.startsWith('/')) { providerPath = '/' + providerPath; } if (!_.isEmpty(req.query)) { providerPath += '?' + stringify(req.query, { arrayFormat: 'repeat' }); } const requestHeaders = _.clone(req.headers); delete requestHeaders['content-length']; delete requestHeaders['content-encoding']; delete requestHeaders['connection']; delete requestHeaders['host']; delete requestHeaders['origin']; console.log('Proxy request to provider: %s => %s [rest]', opts.consumerPath, opts.address + providerPath); const reqOpts: SimpleRequest = { method: providerMethod.method || 'GET', url: opts.address + providerPath, body: req.stringBody, headers: requestHeaders as StringMap, encoding: null, }; const traffic = networkManager.addRequest( req.params.systemId, opts.connection, maskSimpleRequest(reqOpts, consumerMethod, consumerEntitiesSource), consumerMethod.id, providerMethod.id ); socketManager.emit(traffic.connectionId, 'traffic_start', traffic); request(reqOpts, function (err, response, responseBody) { if (err) { traffic.asError(err); socketManager.emit(traffic.connectionId, 'traffic_end', traffic); res.status(500).send({ error: '' + err }); return; } const responseHeaders = _.clone(response.headers); delete responseHeaders['content-length']; delete responseHeaders['content-encoding']; delete responseHeaders['connection']; res.set(responseHeaders); res.status(response.statusCode); const simpleResponse = { code: response.statusCode, headers: response.headers as StringMap, body: responseBody, }; traffic.withResponse(maskSimpleResponse(simpleResponse, consumerMethod, consumerEntitiesSource)); socketManager.emit(traffic.connectionId, 'traffic_end', traffic); if (responseBody) { res.write(responseBody); } res.end(); }); };