Support .headers files in devtools hosted mode server. This is similar to what WPT does (https://source.chromium.org/search?q=f:third_party%2Fblink%2Fweb_tests%2Fexternal%2Fwpt%2Fresources.*%5C.headers$) and is needed to support binary resources with custom headers, such as webbunles. Bug: 1182537 Change-Id: I0e089a9a3ff43cd5082d0cd20f7c89ceec42c60e Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/2944947 Commit-Queue: Danil Somsikov <dsv@chromium.org> Reviewed-by: Paul Lewis <aerotwist@chromium.org>
diff --git a/scripts/hosted_mode/server.js b/scripts/hosted_mode/server.js index af65765..b466aa0 100644 --- a/scripts/hosted_mode/server.js +++ b/scripts/hosted_mode/server.js
@@ -5,10 +5,13 @@ const https = require('https'); const path = require('path'); const parseURL = require('url').parse; +const promisify = require('util').promisify; const remoteDebuggingPort = parseInt(process.env.REMOTE_DEBUGGING_PORT, 10) || 9222; const port = parseInt(process.env.PORT, 10); const requestedPort = port || port === 0 ? port : 8090; +const readFile = promisify(fs.readFile); +const exists = promisify(fs.exists); let pathToOutTargetDir = __dirname; /** @@ -56,98 +59,121 @@ }); server.listen(requestedPort); -function requestHandler(request, response) { +async function requestHandler(request, response) { const filePath = unescape(parseURL(request.url).pathname); if (filePath === '/') { const landingURL = `http://localhost:${remoteDebuggingPort}#custom=true`; - sendResponse(200, `<html>Please go to <a href="${landingURL}">${landingURL}</a></html>`); + sendResponse(200, `<html>Please go to <a href="${landingURL}">${landingURL}</a></html>`, 'utf8'); return; } const absoluteFilePath = path.join(devtoolsFolder, filePath); if (!path.resolve(absoluteFilePath).startsWith(path.join(devtoolsFolder, '..'))) { console.log(`File requested (${absoluteFilePath}) is outside of devtools folder: ${devtoolsFolder}`); - sendResponse(403, `403 - Access denied. File requested is outside of devtools folder: ${devtoolsFolder}`); + sendResponse(403, `403 - Access denied. File requested is outside of devtools folder: ${devtoolsFolder}`, 'utf8'); return; } - fs.exists(absoluteFilePath, fsExistsCallback); - - function fsExistsCallback(fileExists) { - if (!fileExists) { - console.log(`Cannot find file ${absoluteFilePath}. Requested URL: ${filePath}`); - sendResponse(404, '404 - File not found'); - return; - } - - let encoding = 'utf8'; - if (absoluteFilePath.endsWith('.wasm') || absoluteFilePath.endsWith('.png') || absoluteFilePath.endsWith('.jpg') || - absoluteFilePath.endsWith('.avif')) { - encoding = 'binary'; - } - - fs.readFile(absoluteFilePath, encoding, readFileCallback); + const fileExists = await exists(absoluteFilePath); + if (!fileExists) { + console.log(`Cannot find file ${absoluteFilePath}. Requested URL: ${filePath}`); + sendResponse(404, '404 - File not found', 'utf8'); + return; } - function readFileCallback(err, file) { - if (err) { - console.log(`Unable to read local file ${absoluteFilePath}:`, err); - sendResponse(500, '500 - Internal Server Error'); - return; + let statusCode, data, headers; + const headersFileExists = await exists(absoluteFilePath + '.headers'); + if (headersFileExists) { + try { + const headersFile = await readFile(absoluteFilePath + '.headers', 'utf8'); + ({statusCode, headers} = parseRawResponse(headersFile)); + } catch (err) { + console.log(`Unable to read local file ${absoluteFilePath}.headers:`, err); + sendResponse(500, '500 - Internal Server Error', 'utf8'); } - sendResponse(200, file); } - function sendResponse(statusCode, data) { - const path = parseURL(request.url).pathname; + let encoding = 'utf8'; + if (absoluteFilePath.endsWith('.wasm') || absoluteFilePath.endsWith('.png') || absoluteFilePath.endsWith('.jpg') || + absoluteFilePath.endsWith('.avif')) { + encoding = 'binary'; + } - if (path.endsWith('.rawresponse')) { - sendRawResponse(data); - return; + try { + data = await readFile(absoluteFilePath, encoding); + if (absoluteFilePath.endsWith('.rawresponse')) { + ({statusCode, data, headers} = parseRawResponse(data)); } + sendResponse(statusCode || 200, data, encoding, headers); + } catch (err) { + console.log(`Unable to read local file ${absoluteFilePath}:`, err); + sendResponse(500, '500 - Internal Server Error', 'utf8'); + } - let encoding = 'utf8'; + function inferContentType(url) { + const path = parseURL(url).pathname; + if (path.endsWith('.js') || path.endsWith('.mjs')) { - response.setHeader('Content-Type', 'text/javascript; charset=utf-8'); - } else if (path.endsWith('.css')) { - response.setHeader('Content-Type', 'text/css; charset=utf-8'); - } else if (path.endsWith('.wasm')) { - response.setHeader('Content-Type', 'application/wasm'); - encoding = 'binary'; - } else if (path.endsWith('.svg')) { - response.setHeader('Content-Type', 'image/svg+xml; charset=utf-8'); - } else if (path.endsWith('.png')) { - response.setHeader('Content-Type', 'image/png'); - encoding = 'binary'; - } else if (path.endsWith('.jpg')) { - response.setHeader('Content-Type', 'image/jpg'); - encoding = 'binary'; - } else if (path.endsWith('.avif')) { - response.setHeader('Content-Type', 'image/avif'); - encoding = 'binary'; + return 'text/javascript; charset=utf-8'; } + if (path.endsWith('.css')) { + return 'text/css; charset=utf-8'; + } + if (path.endsWith('.wasm')) { + return 'application/wasm'; + } + if (path.endsWith('.svg')) { + return 'image/svg+xml; charset=utf-8'; + } + if (path.endsWith('.png')) { + return 'image/png'; + } + if (path.endsWith('.jpg')) { + return 'image/jpg'; + } + if (path.endsWith('.avif')) { + return 'image/avif'; + } + return null; + } + + function sendResponse(statusCode, data, encoding, headers) { + if (!headers) { + headers = new Map(); + } + if (!headers.get('Content-Type')) { + const inferredContentType = inferContentType(request.url); + if (inferredContentType) { + headers.set('Content-Type', inferredContentType); + } + } + headers.forEach((value, header) => { + response.setHeader(header, value); + }); response.writeHead(statusCode); - response.write(data, encoding); + if (data && encoding) { + response.write(data, encoding); + } response.end(); } - function sendRawResponse(data) { - const lines = data.split('\n'); + function parseRawResponse(rawResponse) { + const lines = rawResponse.split('\n'); let isHeader = true; let line = lines.shift(); const statusCode = parseInt(line, 10); + const headers = new Map(); + let data = ''; + while ((line = lines.shift()) !== undefined) { if (line.trim() === '') { isHeader = false; if (request.headers['if-none-match'] && response.getHeader('ETag') === request.headers['if-none-match']) { - response.writeHead(304); - response.end(); - return; + return {statusCode: 304}; } - response.writeHead(statusCode); continue; } @@ -155,12 +181,12 @@ const firstColon = line.indexOf(':'); let headerValue = line.substring(firstColon + 1).trim(); headerValue = headerValue.replace('$host_port', `${server.address().port}`); - response.setHeader(line.substring(0, firstColon), headerValue); + headers.set(line.substring(0, firstColon), headerValue); } else { - response.write(line); + data += line; } } - response.end(); + return {statusCode, data, headers}; } }