// Copyright 2018 The Kubeflow Authors // // 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 // // http://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 express from 'express'; import proxy from 'http-proxy-middleware'; import { URL, URLSearchParams } from 'url'; import { HACK_FIX_HPM_PARTIAL_RESPONSE_HEADERS } from './consts'; export function _extractUrlFromReferer(proxyPrefix: string, referer = ''): string { const index = referer.indexOf(proxyPrefix); return index > -1 ? referer.substr(index + proxyPrefix.length) : ''; } export function _trimProxyPrefix(proxyPrefix: string, path: string): string { return path.indexOf(proxyPrefix) === 0 ? (path = path.substr(proxyPrefix.length)) : path; } export function _routePathWithReferer(proxyPrefix: string, path: string, referer = ''): string { // If a referer header is included, extract the referer URL, otherwise // just trim out the /_proxy/ prefix. Use the origin of the resulting URL. const proxiedUrlInReferer = _extractUrlFromReferer(proxyPrefix, referer); let decodedPath = decodeURIComponent(proxiedUrlInReferer || _trimProxyPrefix(proxyPrefix, path)); if (!decodedPath.startsWith('http://') && !decodedPath.startsWith('https://')) { decodedPath = 'http://' + decodedPath; } return new URL(decodedPath).origin; } export function _rewritePath(proxyPrefix: string, path: string, query: string): string { // Trim the proxy prefix if exists. It won't exist for any requests made // to absolute paths by the proxied resource. const querystring = new URLSearchParams(query).toString(); const decodedPath = decodeURIComponent(path); return _trimProxyPrefix(proxyPrefix, decodedPath) + (querystring && '?' + querystring); } export default (app: express.Application, apisPrefix: string) => { const proxyPrefix = apisPrefix + '/_proxy/'; app.use((req, _, next) => { // For any request that has a proxy referer header but no proxy prefix, // prepend the proxy prefix to it and redirect. if (req.headers.referer) { const refererUrl = _extractUrlFromReferer(proxyPrefix, req.headers.referer as string); if (refererUrl && req.url.indexOf(proxyPrefix) !== 0) { let proxiedUrl = decodeURIComponent( _extractUrlFromReferer(proxyPrefix, req.headers.referer as string), ); if (!proxiedUrl.startsWith('http://') && !proxiedUrl.startsWith('https://')) { proxiedUrl = 'http://' + proxiedUrl; } const proxiedOrigin = new URL(proxiedUrl).origin; req.url = proxyPrefix + encodeURIComponent(proxiedOrigin + req.url); } } next(); }); app.all( proxyPrefix + '*', proxy({ changeOrigin: true, logLevel: process.env.NODE_ENV === 'test' ? 'warn' : 'debug', target: 'http://127.0.0.1', router: (req: any) => { return _routePathWithReferer(proxyPrefix, req.path, req.headers.referer as string); }, pathRewrite: (_: any, req: any) => { return _rewritePath(proxyPrefix, req.path, req.query); }, headers: HACK_FIX_HPM_PARTIAL_RESPONSE_HEADERS, }), ); };