// Copyright Abridged, Inc. 2021,2024. All Rights Reserved. // Node module: @collabland/aws // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT import {IncomingMessage, ServerResponse} from 'http'; import {AWS_TRACE_ID, Segment} from './types.js'; export class ReqResInfo { segment: Partial; constructor( private xrayHttp: XRayHttp, req: IncomingMessage, ) { this.init(req); } init(req: IncomingMessage) { const forwarded = !!req.headers['x-forwarded-for']; let url; // eslint-disable-next-line @typescript-eslint/no-explicit-any const connection = req.connection as any; if (connection) url = (connection.secure || connection.encrypted ? 'https://' : 'http://') + ((req.headers['host'] ?? '') + (req.url ?? '')); const request: Record = { method: req.method ?? '', user_agent: req.headers['user-agent'] ?? '', client_ip: this.getClientIp(req) ?? '', url: url ?? '', }; if (forwarded) { request.x_forwarded_for = forwarded; } const amznTraceHeader = this.xrayHttp.processTraceData( req.headers[AWS_TRACE_ID], ); const name = req.headers['host'] ?? 'api.collab.land'; this.segment = { name, root: amznTraceHeader.root, parent: amznTraceHeader.parent, http: { request, }, }; } getClientIp(req: IncomingMessage) { let clientIp: string | undefined = undefined; if (req.headers['x-forwarded-for']) clientIp = ((req.headers['x-forwarded-for'] as string) ?? '').split( ',', )[0]; else if (req.connection?.remoteAddress) clientIp = req.connection.remoteAddress; else if (req.socket?.remoteAddress) clientIp = req.socket.remoteAddress; return clientIp; } /** * Closes the local and automatically captures the response data. * @param res - The response object from the HTTP/HTTPS call. */ close(res: ServerResponse) { const response: Record = { status: res.statusCode || '', }; if (res.getHeader('content-length')) { response.content_length = res.getHeader('content-length'); } (this.segment.http as Record).response = response; } } export class XRayHttp { /** * Splits out the data from the trace id format. Used by the middleware. * @param {String} traceData - The additional trace data (typically in * req.headers.x-amzn-trace-id). */ processTraceData(traceData?: string | string[]) { const amznTraceData: Record = {}; if (typeof traceData !== 'string') return amznTraceData; const reservedKeywords = ['root', 'parent', 'sampled', 'self']; let remainingBytes = 256; traceData.split(';').forEach(header => { if (!header) return; const pair = header.split('='); if (pair[0] && pair[1]) { const key = pair[0].trim().toLowerCase(); const value = pair[1].trim().toLowerCase(); const reserved = reservedKeywords.indexOf(key) !== -1; if (reserved) { amznTraceData[key] = value; } else if ( !reserved && remainingBytes - (key.length + value.length) >= 0 ) { amznTraceData[key] = value; remainingBytes -= key.length + value.length; } } }); return amznTraceData; } /** * Splits out the 'x-amzn-trace-id' header params from the incoming request. * @param {http.IncomingMessage|https.IncomingMessage} req - The request * object from the incoming call. */ getTraceData(req: IncomingMessage) { let amznTraceHeader: Record = {}; if (req.headers[AWS_TRACE_ID]) { amznTraceHeader = this.processTraceData(req.headers[AWS_TRACE_ID]); } return amznTraceHeader; } /** * Traces the request/response cycle of an http IncomingMessage/ServerResponse * pair. Creates a segment, adds the core request / response data adding * throttling / error / fault flags based on the response status code. * @param req - The incoming request. * @param res - The server response. */ traceRequest(req: IncomingMessage) { const reqResInfo = new ReqResInfo(new XRayHttp(), req); return reqResInfo; } traceResponse(reqResInfo: ReqResInfo, res: ServerResponse) { reqResInfo.close(res); return reqResInfo; } }