http-proxy-middleware#createProxyMiddleware TypeScript Examples

The following examples show how to use http-proxy-middleware#createProxyMiddleware. You can vote up the ones you like or vote down the ones you don't like, and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: resolver.ts    From one-platform with MIT License 7 votes vote down vote up
function resolver(req: Request, res: Response, next: NextFunction): void {
  const proxy = createProxyMiddleware({
    target: SPASHIP_ROUTER_HOST,
    secure: useSecureSSL,
    changeOrigin: true,
    toProxy: true,
    ignorePath: false,
    headers: {
      'X-OP-Authenticated': req.oidc.isAuthenticated() ? 'true' : 'false',
      ...(req.oidc.isAuthenticated() && {
        'X-OP-Auth-Token': req.oidc.accessToken?.access_token,
      }),
    },
    logProvider: logger.info,
  });

  proxy(req, res, next);
}
Example #2
Source File: resolver.ts    From one-platform with MIT License 6 votes vote down vote up
export default function resolver(
  req: Request,
  res: Response,
  next: NextFunction
): void {
  const url = req.params?.url as string;
  let targetUrl: URL;
  const query = new URLSearchParams(req.query as Record<string, string>);

  try {
    targetUrl = new URL(url);
    targetUrl.search = query.toString();
  } catch (error) {
    res.status(403).json({ error: 'Invalid proxy url' });
    return;
  }

  const proxy = createProxyMiddleware({
    target: targetUrl.toString(),
    secure: useSecureSSL,
    changeOrigin: true,
    ignorePath: true,
  });
  proxy(req, res, next);
}
Example #3
Source File: proxyMiddleware.ts    From react-typescript-boilerplate with MIT License 6 votes vote down vote up
export default function proxyMiddleware(server: Express): void {
    Object.entries(proxyTable).forEach(([path, options]) => {
        const from = path;
        const to = options.target as string;
        console.log(`proxy ${link(from)} ${chalk.green('->')} ${link(to)}`);

        if (!options.logLevel) options.logLevel = 'warn';
        server.use(path, createProxyMiddleware(options));
    });
    process.stdout.write('\n');
}
Example #4
Source File: proxy.ts    From the-fake-backend with ISC License 6 votes vote down vote up
/**
 * Add a httpProxy to a proxy properties.
 *
 * @param proxy The proxy properties object
 * @param basePath The server basePath
 * @return The proxy with the proxy middleware
 */
function buildProxy(proxy: ProxyProperties, basePath?: string): Proxy {
  const { appendBasePath, name, host, onProxyReq, onProxyRes } = proxy;

  return {
    host,
    name,
    handler: createProxyMiddleware({
      ...proxy,
      target: host,
      pathRewrite: (path) =>
        appendBasePath ? path : path.replace(basePath || '', ''),
      changeOrigin: true,
      onProxyReq: (proxyReq, req, res, options) => {
        proxy.onProxyReq?.(proxyReq, req, res, options);
        fixRequestBody(proxyReq, req);
      },
      onProxyRes,
    }),
  };
}
Example #5
Source File: index.ts    From ace with GNU Affero General Public License v3.0 6 votes vote down vote up
ipcMain.on('load-project', (event, arg) => {
    if (server) server.close(() => console.log('server closed'));
    statics.push(arg);
    server = express();
    server.use(cors());
    statics.forEach((s) => server.use(express.static(s)));
    server.use(createProxyMiddleware({
        target: 'http://localhost:9696/',
        router: (req) => req.header('X-Ace-ProxyTarget'),
        changeOrigin: false,
    }));
    server = server.listen(PORT);
});
Example #6
Source File: app.ts    From crossfeed with Creative Commons Zero v1.0 Universal 6 votes vote down vote up
matomoProxy = createProxyMiddleware({
  target: process.env.MATOMO_URL,
  headers: { HTTP_X_FORWARDED_URI: '/matomo' },
  pathRewrite: function (path, req) {
    return path.replace(/^\/matomo/, '');
  },
  onProxyReq: function (proxyReq, req, res) {
    // Only pass the MATOMO_SESSID cookie to Matomo.
    if (!proxyReq.getHeader('Cookie')) return;
    const cookies = cookie.parse(proxyReq.getHeader('Cookie'));
    const newCookies = cookie.serialize(
      'MATOMO_SESSID',
      String(cookies['MATOMO_SESSID'])
    );
    proxyReq.setHeader('Cookie', newCookies);
  },
  onProxyRes: function (proxyRes, req, res) {
    // Remove transfer-encoding: chunked responses, because API Gateway doesn't
    // support chunked encoding.
    if (proxyRes.headers['transfer-encoding'] === 'chunked') {
      proxyRes.headers['transfer-encoding'] = '';
    }
  }
})
Example #7
Source File: server.ts    From bitcoin-s-ts with MIT License 6 votes vote down vote up
wsProxy = createProxyMiddleware({
  target: Config.walletServerWs,
  ws: true,
  changeOrigin: true, // doesn't seem to matter locally
  pathRewrite: {
    [`^${Config.wsRoot}`]: '',
  },
  proxyTimeout: WS_PROXY_TIMEOUT,
  auth: `${Config.serverUser}:${Config.serverPassword}`,
  // onOpen: () => {
  //   console.debug('onOpen()')
  // },
  // onProxyReqWs: () => {
  //   console.debug('onProxyReqWs()')
  // },
  // Currently setting login info at UI
  // onProxyReq: (proxyReq: http.ClientRequest, req: http.IncomingMessage, res: http.ServerResponse, options/*: httpProxy.ServerOptions*/) => {
  //   console.debug('onProxyReq() ws', proxyReq.getHeader('Authorization'))
  //   // If we have a user and password set, add a Basic auth header for them
  //   // Backend server will ignore if it does not currently have a password set
  //   if (Config.serverUser && Config.serverPassword) {
  //     proxyReq.setHeader('Authorization', Config.serverAuthHeader)
  //   }
  // },
  onError: (err: Error, req: Request, res: Response) => {
    Logger.error('websocket onError', err, res.statusCode, res.statusMessage)
    // (<any>err).code === ECONNRESET
    // [2021-12-17T15:43:20.234Z error: websocket onError read ECONNRESET
    // [HPM] Error occurred while proxying request localhost:4200 to undefined [ECONNRESET] (https://nodejs.org/api/errors.html#errors_common_system_errors)
  }
})
Example #8
Source File: server.ts    From bitcoin-s-ts with MIT License 6 votes vote down vote up
// 10 seconds
app.use(Config.apiRoot, verifyAuth, createProxyMiddleware({
  target: Config.oracleServerUrl,
  changeOrigin: true,
  pathRewrite: {
    [`^${Config.apiRoot}`]: '',
  },
  proxyTimeout: PROXY_TIMEOUT,
  onProxyReq: (proxyReq: http.ClientRequest, req: http.IncomingMessage, res: http.ServerResponse, options/*: httpProxy.ServerOptions*/) => {
    // console.debug('onProxyReq() ws')
    // If we have a user and password set, add a Basic auth header for them
    // Backend server will ignore if it does not currently have a password set
    if (Config.serverUser && Config.serverPassword) {
      proxyReq.setHeader('Authorization', 
        'Basic ' + Buffer.from(Config.serverUser + ':' + Config.serverPassword).toString('base64'))
    }
  },
  onError: (err: Error, req: Request, res: Response) => {
    // Handle oracleServer is unavailable
    if (err && (<any>err).code === ECONNREFUSED) {
      res.writeHead(500, 'oracleServer connection refused').end()
    } else {
      Logger.error('onError', err, res.statusCode, res.statusMessage)
    }
  }
}))
Example #9
Source File: server.ts    From bitcoin-s-ts with MIT License 6 votes vote down vote up
// Proxy calls to this server to Mempool API
function createMempoolProxy(agent?: any /* SocksProxyAgent */) {
  const root = (agent ? Config.torProxyRoot : '') + Config.mempoolRoot
  app.use(root, createProxyMiddleware({
    agent,
    target: Config.mempoolUrl,
    changeOrigin: true,
    pathRewrite: {
      [`^${root}`]: '',
    },
    proxyTimeout: MEMPOOL_PROXY_TIMEOUT,
    onProxyReq: (proxyReq: http.ClientRequest, req: http.IncomingMessage, res: http.ServerResponse, options/*: httpProxy.ServerOptions*/) => {
      if (!agent) { // this throws error with 'agent' set
        proxy.removeFrontendHeaders(proxyReq)
      }

      // console.debug('onProxyReq() req headers:', req.headers)
      // console.debug('onProxyReq() proxyReq headers:', proxyReq.getHeaders())
      // console.debug('onProxyReq() res headers:', res.getHeaders())
    },
    onError: proxy.getProxyErrorHandler('mempool', agent),
  }))
}
Example #10
Source File: server.ts    From bitcoin-s-ts with MIT License 6 votes vote down vote up
// Proxy calls to this server to Blockstream API
function createBlockstreamProxy(agent?: any /* SocksProxyAgent */) {
  const root = (agent ? Config.torProxyRoot : '') + Config.blockstreamRoot
  app.use(root, createProxyMiddleware({
    agent,
    target: Config.blockstreamUrl,
    changeOrigin: true,
    pathRewrite: {
      [`^${root}`]: '',
    },
    proxyTimeout: BLOCKSTREAM_PROXY_TIMEOUT,
    onProxyReq: (proxyReq: http.ClientRequest, req: http.IncomingMessage, res: http.ServerResponse, options/*: httpProxy.ServerOptions*/) => {
      if (!agent) { // this throws error with 'agent' set
        proxy.removeFrontendHeaders(proxyReq)
      }

      // console.debug('onProxyReq() req headers:', req.headers)
      // console.debug('onProxyReq() proxyReq headers:', proxyReq.getHeaders())
      // console.debug('onProxyReq() res headers:', res.getHeaders())
    },
    onError: proxy.getProxyErrorHandler('blockstream', agent),
  }))
}
Example #11
Source File: server.ts    From bitcoin-s-ts with MIT License 6 votes vote down vote up
// Proxy calls to this server on to Oracle Explorer
function createOracleExplorerProxy(agent?: any /* SocksProxyAgent */) {
  const root = (agent ? Config.torProxyRoot : '') + Config.oracleExplorerRoot
  app.use(root, createProxyMiddleware({
    // target: oracleExplorerUrl,
    agent,
    router: proxy.hostRouter, // Dynamic target
    changeOrigin: true,
    pathRewrite: {
      [`^${root}`]: '',
    },
    proxyTimeout: EXPLORER_PROXY_TIMEOUT,
    onProxyReq: (proxyReq: http.ClientRequest, req: http.IncomingMessage, res: http.ServerResponse, options/*: httpProxy.ServerOptions*/) => {
      if (!agent) { // this throws error with 'agent' set
        // Use HOST_OVERRIDE_HEADER value to set underlying oracle explorer proxyReq host header
        const host = req.headers[HOST_OVERRIDE_HEADER] || Config.oracleExplorerHost
        proxyReq.setHeader('host', host)
        proxyReq.removeHeader(HOST_OVERRIDE_HEADER)
        // Remove unnecessary headers
        proxy.removeFrontendHeaders(proxyReq)
        res.removeHeader('x-powered-by')
      }
      
      // console.debug('onProxyReq() req headers:', req.headers)
      // console.debug('onProxyReq() proxyReq headers:', proxyReq.getHeaders())
      // console.debug('onProxyReq() res headers:', res.getHeaders())
    },
    onError: proxy.getProxyErrorHandler('oracleExplorer', agent),
  }))
}
Example #12
Source File: router.ts    From backstage with Apache License 2.0 6 votes vote down vote up
/**
 * Create the Airbrake Router, used for making API calls to the Airbrake API.
 *
 * @public
 *
 * @param options - Router options
 */
export async function createRouter(
  options: RouterOptions,
): Promise<express.Router> {
  const { logger } = options;

  const router = Router();
  router.use(express.json());

  router.get('/health', (_, response) => {
    logger.info('PONG!');
    response.send({ status: 'ok' });
  });

  router.use(
    '/api',
    createProxyMiddleware({
      target: 'https://api.airbrake.io/api',
      changeOrigin: true,
      pathRewrite: generateAirbrakePathRewrite(options),
    }),
  );

  router.use(errorHandler());
  return router;
}
Example #13
Source File: resolver.ts    From one-platform with MIT License 6 votes vote down vote up
export default function resolver(
  req: Request,
  res: Response,
  next: NextFunction
): void {
  const { uid, role, rhatUUID } = res.locals.user;

  /* Adding additional roles */
  role.push('user:' + uid, 'user:' + rhatUUID, 'op-users');

  const token = createHmac('sha1', COUCHDB_SECRET as string)
    .update(uid) // lgtm[js/weak-cryptographic-algorithm]
    .digest('hex');

  const proxy = createProxyMiddleware({
    target: COUCHDB_HOST,
    secure: useSecureSSL,
    changeOrigin: true,
    headers: {
      'X-Auth-CouchDB-UserName': uid,
      'X-Auth-CouchDB-Roles': role.join(','),
      'X-Auth-CouchDB-Token': token,
    },
    pathRewrite: {
      ['^/api/couchdb']: '',
    },
  });
  proxy(req, res, next);
}
Example #14
Source File: server.ts    From bitcoin-s-ts with MIT License 5 votes vote down vote up
app.use(Config.apiRoot, verifyAuth, createProxyMiddleware({
  target: Config.walletServerUrl,
  changeOrigin: true,
  pathRewrite: {
    [`^${Config.apiRoot}`]: '',
  },
  // auth: `${Config.serverUser}:${Config.serverPassword}`, // Does not work to get auth set against backend
  proxyTimeout: PROXY_TIMEOUT,
  onProxyReq: (proxyReq: http.ClientRequest, req: http.IncomingMessage, res: http.ServerResponse, options: any /* : httpProxy.ServerOptions */) => {
    // console.debug('onProxyReq() ws')
    // If we have a user and password set, add a Basic auth header for them
    // Backend server will ignore if it does not currently have a password set
    if (Config.serverUser && Config.serverPassword) {
      proxyReq.setHeader('Authorization', Config.serverAuthHeader)
    }
  },
  onError: (err: Error, req: Request, res: Response) => {
    // Handle server is unavailable
    if (err && (<any>err).code === ECONNREFUSED) {
      res.writeHead(500, 'appServer connection refused').end()
    } else {
      Logger.error('onError', err, res.statusCode, res.statusMessage)
    }
  }
}))
Example #15
Source File: router.test.ts    From backstage with Apache License 2.0 5 votes vote down vote up
jest.mock('http-proxy-middleware', () => ({
  createProxyMiddleware: jest.fn(() => () => undefined),
}));
Example #16
Source File: router.test.ts    From backstage with Apache License 2.0 5 votes vote down vote up
mockCreateProxyMiddleware = createProxyMiddleware as jest.MockedFunction<
  typeof createProxyMiddleware
>
Example #17
Source File: dev-server.ts    From nebula-dashboard with Apache License 2.0 5 votes vote down vote up
app.use('/api-metrics/*', createProxyMiddleware({
  target: getTargetUrl(proxy.prometheus.target),
  pathRewrite: {
    '/api-metrics': '/api/v1',
  },
  changeOrigin: true,
}))
Example #18
Source File: dev-server.ts    From nebula-dashboard with Apache License 2.0 5 votes vote down vote up
app.use('/api-nebula/*', createProxyMiddleware({
  target: getTargetUrl(proxy.gateway.target),
  pathRewrite: {
    '/api-nebula': '/api',
  },
  changeOrigin: true,
}));
Example #19
Source File: router.ts    From backstage with Apache License 2.0 4 votes vote down vote up
// Creates a proxy middleware, possibly with defaults added on top of the
// given config.
export function buildMiddleware(
  pathPrefix: string,
  logger: Logger,
  route: string,
  config: string | ProxyConfig,
): RequestHandler {
  const fullConfig =
    typeof config === 'string' ? { target: config } : { ...config };

  // Validate that target is a valid URL.
  if (typeof fullConfig.target !== 'string') {
    throw new Error(`Proxy target must be a string`);
  }
  try {
    // eslint-disable-next-line no-new
    new URL(fullConfig.target! as string);
  } catch {
    throw new Error(
      `Proxy target is not a valid URL: ${fullConfig.target ?? ''}`,
    );
  }

  // Default is to do a path rewrite that strips out the proxy's path prefix
  // and the rest of the route.
  if (fullConfig.pathRewrite === undefined) {
    let routeWithSlash = route.endsWith('/') ? route : `${route}/`;

    if (!pathPrefix.endsWith('/') && !routeWithSlash.startsWith('/')) {
      // Need to insert a / between pathPrefix and routeWithSlash
      routeWithSlash = `/${routeWithSlash}`;
    } else if (pathPrefix.endsWith('/') && routeWithSlash.startsWith('/')) {
      // Never expect this to happen at this point in time as
      // pathPrefix is set using `getExternalBaseUrl` which "Returns the
      // external HTTP base backend URL for a given plugin,
      // **without a trailing slash.**". But in case this changes in future, we
      // need to drop a / on either pathPrefix or routeWithSlash
      routeWithSlash = routeWithSlash.substring(1);
    }

    // The ? makes the slash optional for the rewrite, so that a base path without an ending slash
    // will also be matched (e.g. '/sample' and then requesting just '/api/proxy/sample' without an
    // ending slash). Otherwise the target gets called with the full '/api/proxy/sample' path
    // appended.
    fullConfig.pathRewrite = {
      [`^${pathPrefix}${routeWithSlash}?`]: '/',
    };
  }

  // Default is to update the Host header to the target
  if (fullConfig.changeOrigin === undefined) {
    fullConfig.changeOrigin = true;
  }

  // Attach the logger to the proxy config
  fullConfig.logProvider = () => logger;

  // Only return the allowed HTTP headers to not forward unwanted secret headers
  const requestHeaderAllowList = new Set<string>(
    [
      // allow all safe headers
      ...safeForwardHeaders,

      // allow all headers that are set by the proxy
      ...((fullConfig.headers && Object.keys(fullConfig.headers)) || []),

      // allow all configured headers
      ...(fullConfig.allowedHeaders || []),
    ].map(h => h.toLocaleLowerCase()),
  );

  // Use the custom middleware filter to do two things:
  //  1. Remove any headers not in the allow list to stop them being forwarded
  //  2. Only permit the allowed HTTP methods if configured
  //
  // We are filtering the proxy request headers here rather than in
  // `onProxyReq` because when global-agent is enabled then `onProxyReq`
  // fires _after_ the agent has already sent the headers to the proxy
  // target, causing a ERR_HTTP_HEADERS_SENT crash
  const filter = (_pathname: string, req: http.IncomingMessage): boolean => {
    const headerNames = Object.keys(req.headers);
    headerNames.forEach(h => {
      if (!requestHeaderAllowList.has(h.toLocaleLowerCase())) {
        delete req.headers[h];
      }
    });

    return fullConfig?.allowedMethods?.includes(req.method!) ?? true;
  };
  // Makes http-proxy-middleware logs look nicer and include the mount path
  filter.toString = () => route;

  // Only forward the allowed HTTP headers to not forward unwanted secret headers
  const responseHeaderAllowList = new Set<string>(
    [
      // allow all safe headers
      ...safeForwardHeaders,

      // allow all configured headers
      ...(fullConfig.allowedHeaders || []),
    ].map(h => h.toLocaleLowerCase()),
  );

  // only forward the allowed headers in backend->client
  fullConfig.onProxyRes = (proxyRes: http.IncomingMessage) => {
    const headerNames = Object.keys(proxyRes.headers);

    headerNames.forEach(h => {
      if (!responseHeaderAllowList.has(h.toLocaleLowerCase())) {
        delete proxyRes.headers[h];
      }
    });
  };

  return createProxyMiddleware(filter, fullConfig);
}
Example #20
Source File: router.test.ts    From backstage with Apache License 2.0 4 votes vote down vote up
describe('buildMiddleware', () => {
  const logger = getVoidLogger();

  beforeEach(() => {
    mockCreateProxyMiddleware.mockClear();
  });

  it('accepts strings prefixed by /', async () => {
    buildMiddleware('/proxy', logger, '/test', 'http://mocked');

    expect(createProxyMiddleware).toHaveBeenCalledTimes(1);

    const [filter, fullConfig] = mockCreateProxyMiddleware.mock.calls[0] as [
      (pathname: string, req: Partial<http.IncomingMessage>) => boolean,
      Options,
    ];
    expect(filter('', { method: 'GET', headers: {} })).toBe(true);
    expect(filter('', { method: 'POST', headers: {} })).toBe(true);
    expect(filter('', { method: 'PUT', headers: {} })).toBe(true);
    expect(filter('', { method: 'PATCH', headers: {} })).toBe(true);
    expect(filter('', { method: 'DELETE', headers: {} })).toBe(true);

    expect(fullConfig.pathRewrite).toEqual({ '^/proxy/test/?': '/' });
    expect(fullConfig.changeOrigin).toBe(true);
    expect(fullConfig.logProvider!(logger)).toBe(logger);
  });

  it('accepts routes not prefixed with / when path is not suffixed with /', async () => {
    buildMiddleware('/proxy', logger, 'test', 'http://mocked');

    expect(createProxyMiddleware).toHaveBeenCalledTimes(1);

    const [filter, fullConfig] = mockCreateProxyMiddleware.mock.calls[0] as [
      (pathname: string, req: Partial<http.IncomingMessage>) => boolean,
      Options,
    ];
    expect(filter('', { method: 'GET', headers: {} })).toBe(true);
    expect(filter('', { method: 'POST', headers: {} })).toBe(true);
    expect(filter('', { method: 'PUT', headers: {} })).toBe(true);
    expect(filter('', { method: 'PATCH', headers: {} })).toBe(true);
    expect(filter('', { method: 'DELETE', headers: {} })).toBe(true);

    expect(fullConfig.pathRewrite).toEqual({ '^/proxy/test/?': '/' });
    expect(fullConfig.changeOrigin).toBe(true);
    expect(fullConfig.logProvider!(logger)).toBe(logger);
  });

  it('accepts routes prefixed with / when path is suffixed with /', async () => {
    buildMiddleware('/proxy/', logger, '/test', 'http://mocked');

    expect(createProxyMiddleware).toHaveBeenCalledTimes(1);

    const [filter, fullConfig] = mockCreateProxyMiddleware.mock.calls[0] as [
      (pathname: string, req: Partial<http.IncomingMessage>) => boolean,
      Options,
    ];
    expect(filter('', { method: 'GET', headers: {} })).toBe(true);
    expect(filter('', { method: 'POST', headers: {} })).toBe(true);
    expect(filter('', { method: 'PUT', headers: {} })).toBe(true);
    expect(filter('', { method: 'PATCH', headers: {} })).toBe(true);
    expect(filter('', { method: 'DELETE', headers: {} })).toBe(true);

    expect(fullConfig.pathRewrite).toEqual({ '^/proxy/test/?': '/' });
    expect(fullConfig.changeOrigin).toBe(true);
    expect(fullConfig.logProvider!(logger)).toBe(logger);
  });

  it('limits allowedMethods', async () => {
    buildMiddleware('/proxy', logger, '/test', {
      target: 'http://mocked',
      allowedMethods: ['GET', 'DELETE'],
    });

    expect(createProxyMiddleware).toHaveBeenCalledTimes(1);

    const [filter, fullConfig] = mockCreateProxyMiddleware.mock.calls[0] as [
      (pathname: string, req: Partial<http.IncomingMessage>) => boolean,
      Options,
    ];
    expect(filter('', { method: 'GET', headers: {} })).toBe(true);
    expect(filter('', { method: 'POST', headers: {} })).toBe(false);
    expect(filter('', { method: 'PUT', headers: {} })).toBe(false);
    expect(filter('', { method: 'PATCH', headers: {} })).toBe(false);
    expect(filter('', { method: 'DELETE', headers: {} })).toBe(true);

    expect(fullConfig.pathRewrite).toEqual({ '^/proxy/test/?': '/' });
    expect(fullConfig.changeOrigin).toBe(true);
    expect(fullConfig.logProvider!(logger)).toBe(logger);
  });

  it('permits default headers', async () => {
    buildMiddleware('/proxy', logger, '/test', {
      target: 'http://mocked',
    });

    expect(createProxyMiddleware).toHaveBeenCalledTimes(1);

    const [filter] = mockCreateProxyMiddleware.mock.calls[0] as [
      (pathname: string, req: Partial<http.IncomingMessage>) => boolean,
    ];

    const testHeaders = {
      'cache-control': 'mocked',
      'content-language': 'mocked',
      'content-length': 'mocked',
      'content-type': 'mocked',
      expires: 'mocked',
      'last-modified': 'mocked',
      pragma: 'mocked',
      host: 'mocked',
      accept: 'mocked',
      'accept-language': 'mocked',
      'user-agent': 'mocked',
      cookie: 'mocked',
    } as Partial<http.IncomingHttpHeaders>;
    const expectedHeaders = {
      ...testHeaders,
    } as Partial<http.IncomingHttpHeaders>;
    delete expectedHeaders.cookie;

    expect(testHeaders).toBeDefined();
    expect(expectedHeaders).toBeDefined();
    expect(testHeaders).not.toEqual(expectedHeaders);
    expect(filter).toBeDefined();

    filter!('', { method: 'GET', headers: testHeaders });

    expect(testHeaders).toEqual(expectedHeaders);
  });

  it('permits default and configured headers', async () => {
    buildMiddleware('/proxy', logger, '/test', {
      target: 'http://mocked',
      headers: {
        Authorization: 'my-token',
      },
    });

    expect(createProxyMiddleware).toHaveBeenCalledTimes(1);

    const [filter] = mockCreateProxyMiddleware.mock.calls[0] as [
      (pathname: string, req: Partial<http.IncomingMessage>) => boolean,
    ];

    const testHeaders = {
      authorization: 'mocked',
      cookie: 'mocked',
    } as Partial<http.IncomingHttpHeaders>;
    const expectedHeaders = {
      ...testHeaders,
    } as Partial<http.IncomingHttpHeaders>;
    delete expectedHeaders.cookie;

    expect(testHeaders).toBeDefined();
    expect(expectedHeaders).toBeDefined();
    expect(testHeaders).not.toEqual(expectedHeaders);
    expect(filter).toBeDefined();

    filter!('', { method: 'GET', headers: testHeaders });

    expect(testHeaders).toEqual(expectedHeaders);
  });

  it('permits configured headers', async () => {
    buildMiddleware('/proxy', logger, '/test', {
      target: 'http://mocked',
      allowedHeaders: ['authorization', 'cookie'],
    });

    expect(createProxyMiddleware).toHaveBeenCalledTimes(1);

    const [filter] = mockCreateProxyMiddleware.mock.calls[0] as [
      (pathname: string, req: Partial<http.IncomingMessage>) => boolean,
    ];

    const testHeaders = {
      authorization: 'mocked',
      cookie: 'mocked',
      'x-auth-request-user': 'mocked',
    } as Partial<http.IncomingHttpHeaders>;
    const expectedHeaders = {
      ...testHeaders,
    } as Partial<http.IncomingHttpHeaders>;
    delete expectedHeaders['x-auth-request-user'];

    expect(testHeaders).toBeDefined();
    expect(expectedHeaders).toBeDefined();
    expect(testHeaders).not.toEqual(expectedHeaders);
    expect(filter).toBeDefined();

    filter!('', { method: 'GET', headers: testHeaders });

    expect(testHeaders).toEqual(expectedHeaders);
  });

  it('responds default headers', async () => {
    buildMiddleware('/proxy', logger, '/test', {
      target: 'http://mocked',
    });

    expect(createProxyMiddleware).toHaveBeenCalledTimes(1);

    const config = mockCreateProxyMiddleware.mock.calls[0][1] as Options;

    const testClientResponse = {
      headers: {
        'cache-control': 'value',
        'content-language': 'value',
        'content-length': 'value',
        'content-type': 'value',
        expires: 'value',
        'last-modified': 'value',
        pragma: 'value',
        'set-cookie': ['value'],
      },
    } as Partial<http.IncomingMessage>;

    expect(config).toBeDefined();
    expect(config.onProxyRes).toBeDefined();

    config.onProxyRes!(
      testClientResponse as http.IncomingMessage,
      {} as Request,
      {} as Response,
    );

    expect(Object.keys(testClientResponse.headers!)).toEqual([
      'cache-control',
      'content-language',
      'content-length',
      'content-type',
      'expires',
      'last-modified',
      'pragma',
    ]);
  });

  it('responds configured headers', async () => {
    buildMiddleware('/proxy', logger, '/test', {
      target: 'http://mocked',
      allowedHeaders: ['set-cookie'],
    });

    expect(createProxyMiddleware).toHaveBeenCalledTimes(1);

    const config = mockCreateProxyMiddleware.mock.calls[0][1] as Options;

    const testClientResponse = {
      headers: {
        'set-cookie': [],
        'x-auth-request-user': 'asd',
      },
    } as Partial<http.IncomingMessage>;

    expect(config).toBeDefined();
    expect(config.onProxyRes).toBeDefined();

    config.onProxyRes!(
      testClientResponse as http.IncomingMessage,
      {} as Request,
      {} as Response,
    );

    expect(Object.keys(testClientResponse.headers!)).toEqual(['set-cookie']);
  });

  it('rejects malformed target URLs', async () => {
    expect(() =>
      buildMiddleware('/proxy', logger, '/test', 'backstage.io'),
    ).toThrowError(/Proxy target is not a valid URL/);
    expect(() =>
      buildMiddleware('/proxy', logger, '/test', { target: 'backstage.io' }),
    ).toThrowError(/Proxy target is not a valid URL/);
  });
});
Example #21
Source File: proxy.ts    From erda-ui with GNU Affero General Public License v3.0 4 votes vote down vote up
createProxyService = (app: INestApplication) => {
  const onError = (err, req, res: Response, target) => {
    const reqMsg = `${req.url} to ${target}`;
    try {
      if (!res.writableEnded) {
        const errMsg = `Error occurred while proxying request ${reqMsg}: ${err.message}`;
        res.end();
        logger.warn(errMsg, err);
      } else {
        logger.warn(`Response is ended before error handler while proxying request ${reqMsg}`);
      }
    } catch (e) {
      logger.error(`Error happens while handling proxy exception for request ${reqMsg}: ${e}`);
    }
  };
  const wsProxy = createProxyMiddleware(
    (pathname: string, req: Request) => {
      return req.headers.upgrade === 'websocket' && wsPathRegex.some((regex) => regex.test(pathname));
    },
    {
      target: API_URL,
      ws: true,
      changeOrigin: !isProd,
      xfwd: true,
      secure: false,
      pathRewrite: replaceApiOrgPath,
      onProxyReqWs: (proxyReq, req: Request, socket) => {
        if (isProd) {
          const { query } = qs.parseUrl(req.url);
          proxyReq.setHeader('org', query?.wsOrg);
        }
        socket.on('error', (error) => {
          logger.warn('Websocket error:', error); // add error handler to prevent server crash https://github.com/chimurai/http-proxy-middleware/issues/463#issuecomment-676630189
        });
      },
      onError,
    },
  );
  app.use(
    createProxyMiddleware(
      (pathname: string) => {
        return !!pathname.match('^/static/admin');
      },
      {
        target: ENTERPRISE_UI_URL,
        changeOrigin: !isProd,
        secure: false,
      },
    ),
  );
  app.use(
    createProxyMiddleware(
      (pathname: string) => {
        return !!pathname.match('^/static/fdp');
      },
      {
        target: FDP_UI_URL,
        changeOrigin: !isProd,
        secure: false,
        pathRewrite: { '^/static/fdp': '' },
        onError,
      },
    ),
  );
  app.use(wsProxy);
  app.use(
    createProxyMiddleware(
      (pathname: string) => {
        return !!pathname.match('^/api/uc');
      },
      {
        target: UC_API_URL,
        changeOrigin: true,
        secure: false,
        pathRewrite: (api) => (isProd ? api.replace('/api/uc', '') : api),
        onError,
      },
    ),
  );
  app.use(
    createProxyMiddleware(
      (pathname: string) => {
        return !!pathname.match('^/api');
      },
      {
        target: API_URL,
        changeOrigin: !isProd,
        xfwd: true,
        secure: false,
        pathRewrite: replaceApiOrgPath,
        headers: {
          Connection: 'keep-alive', // try fix error: write after end
        },
        onProxyReq: (proxyReq, req: Request) => {
          if (!isProd) {
            proxyReq.setHeader('referer', API_URL);
          } else {
            const org = extractOrg(req.originalUrl); // api/files not append org to path,org not exist in this condition
            if (org) {
              proxyReq.setHeader('org', org);
            }
          }
        },
        onError,
      },
    ),
  );
  app.use(
    createProxyMiddleware(
      (pathname: string, req: Request) => {
        if (pathname.startsWith('/wb/')) {
          return true;
        }
        const userAgent = req.headers['user-agent'];
        if (userAgent.toLowerCase().includes('git')) {
          // compatible with JGit
          return /[^/]*\/dop/.test(pathname);
        }
        return false;
      },
      {
        target: GITTAR_URL,
        changeOrigin: !isProd,
        onError,
      },
    ),
  );
  app.use(
    '/metadata.json',
    createProxyMiddleware({
      target: API_URL,
      changeOrigin: !isProd,
      onError,
    }),
  );
  return wsProxy;
}
Example #22
Source File: core-node-rpc-proxy.ts    From stacks-blockchain-api with GNU General Public License v3.0 4 votes vote down vote up
export function createCoreNodeRpcProxyRouter(db: DataStore): express.Router {
  const router = express.Router();
  router.use(cors());

  const stacksNodeRpcEndpoint = GetStacksNodeProxyEndpoint();

  logger.info(`/v2/* proxying to: ${stacksNodeRpcEndpoint}`);

  // Note: while keep-alive may result in some performance improvements with the stacks-node http server,
  // it can also cause request distribution issues when proxying to a pool of stacks-nodes. See:
  // https://github.com/hirosystems/stacks-blockchain-api/issues/756
  const httpAgent = new Agent({
    // keepAlive: true,
    keepAlive: false, // `false` is the default -- set it explicitly for readability anyway.
    // keepAliveMsecs: 60000,
    maxSockets: 200,
    maxTotalSockets: 400,
  });

  const PROXY_CACHE_CONTROL_FILE_ENV_VAR = 'STACKS_API_PROXY_CACHE_CONTROL_FILE';
  let proxyCacheControlFile = '.proxy-cache-control.json';
  if (process.env[PROXY_CACHE_CONTROL_FILE_ENV_VAR]) {
    proxyCacheControlFile = process.env[PROXY_CACHE_CONTROL_FILE_ENV_VAR] as string;
    logger.info(`Using ${proxyCacheControlFile}`);
  }
  const cacheControlFileWatcher = chokidar.watch(proxyCacheControlFile, {
    persistent: false,
    useFsEvents: false,
    ignoreInitial: true,
  });
  let pathCacheOptions = new Map<RegExp, string | null>();

  const updatePathCacheOptions = () => {
    try {
      const configContent: { paths: Record<string, string> } = jsoncParser.parse(
        fs.readFileSync(proxyCacheControlFile, 'utf8')
      );
      pathCacheOptions = new Map(
        Object.entries(configContent.paths).map(([k, v]) => [RegExp(k), v])
      );
    } catch (error) {
      pathCacheOptions.clear();
      logger.error(`Error reading changes from ${proxyCacheControlFile}`, error);
    }
  };
  updatePathCacheOptions();
  cacheControlFileWatcher.on('all', (eventName, path, stats) => {
    updatePathCacheOptions();
  });

  const getCacheControlHeader = (statusCode: number, url: string): string | null => {
    if (statusCode < 200 || statusCode > 299) {
      return null;
    }
    for (const [regexp, cacheControl] of pathCacheOptions.entries()) {
      if (cacheControl && regexp.test(url)) {
        return cacheControl;
      }
    }
    return null;
  };

  /**
   * Check for any extra endpoints that have been configured for performing a "multicast" for a tx submission.
   */
  async function getExtraTxPostEndpoints(): Promise<string[] | false> {
    const STACKS_API_EXTRA_TX_ENDPOINTS_FILE_ENV_VAR = 'STACKS_API_EXTRA_TX_ENDPOINTS_FILE';
    const extraEndpointsEnvVar = process.env[STACKS_API_EXTRA_TX_ENDPOINTS_FILE_ENV_VAR];
    if (!extraEndpointsEnvVar) {
      return false;
    }
    const filePath = path.resolve(REPO_DIR, extraEndpointsEnvVar);
    let fileContents: string;
    try {
      fileContents = await fs.promises.readFile(filePath, { encoding: 'utf8' });
    } catch (error) {
      logError(`Error reading ${STACKS_API_EXTRA_TX_ENDPOINTS_FILE_ENV_VAR}: ${error}`, error);
      return false;
    }
    const endpoints = fileContents
      .split(/\r?\n/)
      .map(r => r.trim())
      .filter(r => !r.startsWith('#') && r.length !== 0);
    if (endpoints.length === 0) {
      return false;
    }
    return endpoints;
  }

  /**
   * Reads an http request stream into a Buffer.
   */
  async function readRequestBody(req: express.Request, maxSizeBytes = Infinity): Promise<Buffer> {
    return new Promise((resolve, reject) => {
      let resultBuffer: Buffer = Buffer.alloc(0);
      req.on('data', chunk => {
        if (!Buffer.isBuffer(chunk)) {
          reject(
            new Error(
              `Expected request body chunks to be Buffer, received ${chunk.constructor.name}`
            )
          );
          req.destroy();
          return;
        }
        resultBuffer = resultBuffer.length === 0 ? chunk : Buffer.concat([resultBuffer, chunk]);
        if (resultBuffer.byteLength >= maxSizeBytes) {
          reject(new Error(`Request body exceeded max byte size`));
          req.destroy();
          return;
        }
      });
      req.on('end', () => {
        if (!req.complete) {
          return reject(
            new Error('The connection was terminated while the message was still being sent')
          );
        }
        resolve(resultBuffer);
      });
      req.on('error', error => reject(error));
    });
  }

  /**
   * Logs a transaction broadcast event alongside the current block height.
   */
  async function logTxBroadcast(response: string): Promise<void> {
    try {
      const blockHeightQuery = await db.getCurrentBlockHeight();
      if (!blockHeightQuery.found) {
        return;
      }
      const blockHeight = blockHeightQuery.result;
      // Strip wrapping double quotes (if any)
      const txId = response.replace(/^"(.*)"$/, '$1');
      logger.info('Transaction broadcasted', {
        txid: `0x${txId}`,
        first_broadcast_at_stacks_height: blockHeight,
      });
    } catch (error) {
      logError(`Error logging tx broadcast: ${error}`, error);
    }
  }

  router.post(
    '/transactions',
    asyncHandler(async (req, res, next) => {
      const extraEndpoints = await getExtraTxPostEndpoints();
      if (!extraEndpoints) {
        next();
        return;
      }
      const endpoints = [
        // The primary proxy endpoint (the http response from this one will be returned to the client)
        `http://${stacksNodeRpcEndpoint}/v2/transactions`,
      ];
      endpoints.push(...extraEndpoints);
      logger.info(`Overriding POST /v2/transactions to multicast to ${endpoints.join(',')}}`);
      const maxBodySize = 10_000_000; // 10 MB max POST body size
      const reqBody = await readRequestBody(req, maxBodySize);
      const reqHeaders: string[][] = [];
      for (let i = 0; i < req.rawHeaders.length; i += 2) {
        reqHeaders.push([req.rawHeaders[i], req.rawHeaders[i + 1]]);
      }
      const postFn = async (endpoint: string) => {
        const reqOpts: RequestInit = {
          method: 'POST',
          agent: httpAgent,
          body: reqBody,
          headers: reqHeaders,
        };
        const proxyResult = await fetch(endpoint, reqOpts);
        return proxyResult;
      };

      // Here's were we "multicast" the `/v2/transaction` POST, by concurrently sending the http request to all configured endpoints.
      const results = await Promise.allSettled(endpoints.map(endpoint => postFn(endpoint)));

      // Only the first (non-extra) endpoint http response is proxied back through to the client, so ensure any errors from requests
      // to the extra endpoints are logged.
      results.slice(1).forEach(p => {
        if (p.status === 'rejected') {
          logError(`Error during POST /v2/transaction to extra endpoint: ${p.reason}`, p.reason);
        } else {
          if (!p.value.ok) {
            logError(
              `Response ${p.value.status} during POST /v2/transaction to extra endpoint ${p.value.url}`
            );
          }
        }
      });

      // Proxy the result of the (non-extra) http response back to the client.
      const mainResult = results[0];
      if (mainResult.status === 'rejected') {
        logError(
          `Error in primary POST /v2/transaction proxy: ${mainResult.reason}`,
          mainResult.reason
        );
        res.status(500).json({ error: mainResult.reason });
      } else {
        const proxyResp = mainResult.value;
        res.status(proxyResp.status);
        proxyResp.headers.forEach((value, name) => {
          res.setHeader(name, value);
        });
        if (proxyResp.status === 200) {
          // Log the transaction id broadcast, but clone the `Response` first before parsing its body
          // so we don't mess up the original response's `ReadableStream` pointers.
          const parsedTxId: string = await proxyResp.clone().text();
          await logTxBroadcast(parsedTxId);
        }
        await pipelineAsync(proxyResp.body, res);
      }
    })
  );

  const proxyOptions: Options = {
    agent: httpAgent,
    target: `http://${stacksNodeRpcEndpoint}`,
    changeOrigin: true,
    selfHandleResponse: true,
    onProxyRes: responseInterceptor(async (responseBuffer, proxyRes, req, res) => {
      if (req.url !== undefined) {
        const header = getCacheControlHeader(res.statusCode, req.url);
        if (header) {
          res.setHeader('Cache-Control', header);
        }
        const url = new URL(req.url, `http://${req.headers.host}`);
        if (url.pathname === '/v2/transactions' && res.statusCode === 200) {
          await logTxBroadcast(responseBuffer.toString());
        }
      }
      return responseBuffer;
    }),
    onError: (error, req, res) => {
      const msg =
        (error as any).code === 'ECONNREFUSED'
          ? 'core node unresponsive'
          : 'cannot connect to core node';
      res
        .writeHead(502, { 'Content-Type': 'application/json' })
        .end(JSON.stringify({ message: msg, error: error }));
    },
  };

  const stacksNodeRpcProxy = createProxyMiddleware(proxyOptions);

  router.use(stacksNodeRpcProxy);

  return router;
}