/** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you 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. */ const zlib = require('zlib'); // eslint-disable-next-line import/no-extraneous-dependencies const parsedArgs = require('yargs').argv; const { supersetPort = 8088, superset: supersetUrl = null } = parsedArgs; const backend = (supersetUrl || `http://localhost:${supersetPort}`).replace( '//+$/', '', ); // strip ending backslash let manifest; function isHTML(res) { const CONTENT_TYPE_HEADER = 'content-type'; const contentType = res.getHeader ? res.getHeader(CONTENT_TYPE_HEADER) : res.headers[CONTENT_TYPE_HEADER]; return contentType.includes('text/html'); } function toDevHTML(originalHtml) { let html = originalHtml.replace( /(\s*)([\s\S]*)(<\/title>)/i, '$1[DEV] $2 $3', ); if (manifest) { const loaded = new Set(); // replace bundled asset files, HTML comment tags generated by Jinja macros // in superset/templates/superset/partials/asset_bundle.html html = html.replace( /<!-- Bundle (css|js) (.*?) START -->[\s\S]*?<!-- Bundle \1 \2 END -->/gi, (match, assetType, bundleName) => { if (bundleName in manifest.entrypoints) { return `<!-- DEV bundle: ${bundleName} ${assetType} START -->\n ${( manifest.entrypoints[bundleName][assetType] || [] ) .filter(chunkFilePath => { if (loaded.has(chunkFilePath)) { return false; } loaded.add(chunkFilePath); return true; }) .map(chunkFilePath => assetType === 'css' ? `<link rel="stylesheet" type="text/css" href="${chunkFilePath}" />` : `<script src="${chunkFilePath}"></script>`, ) .join( '\n ', )}\n <!-- DEV bundle: ${bundleName} ${assetType} END -->`; } return match; }, ); } return html; } function copyHeaders(originalResponse, response) { response.statusCode = originalResponse.statusCode; response.statusMessage = originalResponse.statusMessage; if (response.setHeader) { let keys = Object.keys(originalResponse.headers); if (isHTML(originalResponse)) { keys = keys.filter( key => key !== 'content-encoding' && key !== 'content-length', ); } keys.forEach(key => { let value = originalResponse.headers[key]; if (key === 'set-cookie') { // remove cookie domain value = Array.isArray(value) ? value : [value]; value = value.map(x => x.replace(/Domain=[^;]+?/i, '')); } else if (key === 'location') { // set redirects to use local URL value = (value || '').replace(backend, ''); } response.setHeader(key, value); }); } else { response.headers = originalResponse.headers; } } /** * Manipulate HTML server response to replace asset files with * local webpack-dev-server build. */ function processHTML(proxyResponse, response) { let body = Buffer.from([]); let originalResponse = proxyResponse; let uncompress; const responseEncoding = originalResponse.headers['content-encoding']; // decode GZIP response if (responseEncoding === 'gzip') { uncompress = zlib.createGunzip(); } else if (responseEncoding === 'br') { uncompress = zlib.createBrotliDecompress(); } else if (responseEncoding === 'deflate') { uncompress = zlib.createInflate(); } if (uncompress) { originalResponse.pipe(uncompress); originalResponse = uncompress; } originalResponse .on('data', data => { body = Buffer.concat([body, data]); }) .on('error', error => { // eslint-disable-next-line no-console console.error(error); response.end(`Error fetching proxied request: ${error.message}`); }) .on('end', () => { response.end(toDevHTML(body.toString())); }); } module.exports = newManifest => { manifest = newManifest; return { context: '/', target: backend, hostRewrite: true, changeOrigin: true, cookieDomainRewrite: '', // remove cookie domain selfHandleResponse: true, // so that the onProxyRes takes care of sending the response onProxyRes(proxyResponse, request, response) { try { copyHeaders(proxyResponse, response); if (isHTML(response)) { processHTML(proxyResponse, response); } else { proxyResponse.pipe(response); } response.flushHeaders(); } catch (e) { response.setHeader('content-type', 'text/plain'); response.write(`Error requesting ${request.path} from proxy:\n\n`); response.end(e.stack); } }, }; };