Coder Social home page Coder Social logo

sharonkoch / amphtml_demo Goto Github PK

View Code? Open in Web Editor NEW

This project forked from ampproject/amphtml

1.0 0.0 0.0 927.64 MB

The AMP web component framework.

Home Page: https://amp.dev

License: Apache License 2.0

Shell 0.08% JavaScript 79.91% C++ 7.55% Python 0.28% C 0.01% Go 0.03% TypeScript 0.55% CSS 2.22% HTML 9.10% Yacc 0.12% Starlark 0.15%

amphtml_demo's Introduction

AMP ⚡

⚡⚡⚡

Build Status GitHub Release Commits

Tooling

Percy Prettier Codecov Renovate

AMP is a web component framework for easily creating user-first websites, stories, ads, emails and more.

AMP is an open source project, and we'd love your help making it better!

Want to know more about AMP?

Having a problem using AMP?

Want to help make AMP better?

Other useful information

amphtml_demo's People

Contributors

renovate-bot avatar cramforce avatar rsimha avatar erwinmombay avatar alanorozco avatar jridgewell avatar lannka avatar zhouyx avatar dvoytenko avatar gmajoulet avatar samouri avatar danielrozenberg avatar renovate[bot] avatar caroqliu avatar calebcordry avatar aghassemi avatar enriqe avatar honeybadgerdontcare avatar cathyxz avatar cvializ avatar powdercloud avatar rcebulko avatar powerivq avatar gregable avatar mszylkowski avatar estherkim avatar processprocess avatar keithwrightbos avatar newmuis avatar mkhatib avatar

Stargazers

 avatar

amphtml_demo's Issues

intersection-observer-0.12.0.tgz: 1 vulnerabilities (highest severity is: 9.8)

Vulnerable Library - intersection-observer-0.12.0.tgz

A polyfill for IntersectionObserver

Library home page: https://registry.npmjs.org/intersection-observer/-/intersection-observer-0.12.0.tgz

Path to dependency file: /package.json

Path to vulnerable library: /node_modules/intersection-observer/package.json

Found in HEAD commit: 93fe5c7ae5045a3bc62ee5f2042db9f6f63ae4ba

Vulnerabilities

CVE Severity CVSS Dependency Type Fixed in (intersection-observer version) Remediation Possible**
MSC-2024-8267 Critical 9.8 intersection-observer-0.12.0.tgz Direct N/A

**In some cases, Remediation PR cannot be created automatically for a vulnerability despite the availability of remediation

Details

MSC-2024-8267

Vulnerable Library - intersection-observer-0.12.0.tgz

A polyfill for IntersectionObserver

Library home page: https://registry.npmjs.org/intersection-observer/-/intersection-observer-0.12.0.tgz

Path to dependency file: /package.json

Path to vulnerable library: /node_modules/intersection-observer/package.json

Dependency Hierarchy:

  • intersection-observer-0.12.0.tgz (Vulnerable Library)

Found in HEAD commit: 93fe5c7ae5045a3bc62ee5f2042db9f6f63ae4ba

Found in base branch: main

Vulnerability Details

A malicious Polyfill reference has been identified in this package. The issue is located in the file "package\intersection-observer-test.html".
To address this security concern, we recommend taking one of two actions: either remove the affected file completely or replace the suspicious reference with a trusted alternative. Reliable Polyfill sources include Cloudflare (https://cdnjs.cloudflare.com/polyfill) and Fastly (https://community.fastly.com/t/new-options-for-polyfill-io-users/2540).
Mend Note: For more detailed information about the Polyfill supply chain attack and its widespread impact, you can refer to our comprehensive blog post at https://www.mend.io/blog/more-than-100k-sites-impacted-by-polyfill-supply-chain-attack/.

Publish Date: 2024-07-04

URL: MSC-2024-8267

CVSS 3 Score Details (9.8)

Base Score Metrics:

  • Exploitability Metrics:
    • Attack Vector: Network
    • Attack Complexity: Low
    • Privileges Required: None
    • User Interaction: None
    • Scope: Unchanged
  • Impact Metrics:
    • Confidentiality Impact: High
    • Integrity Impact: High
    • Availability Impact: High

For more information on CVSS3 Scores, click here.

vulcanize-1.14.12.tgz: 1 vulnerabilities (highest severity is: 5.3)

Vulnerable Library - vulcanize-1.14.12.tgz

Path to dependency file: /validator/js/webui/package.json

Path to vulnerable library: /validator/js/webui/node_modules/word-wrap/package.json

Found in HEAD commit: 93fe5c7ae5045a3bc62ee5f2042db9f6f63ae4ba

Vulnerabilities

CVE Severity CVSS Dependency Type Fixed in (vulcanize version) Remediation Possible**
CVE-2023-26115 Medium 5.3 word-wrap-1.2.3.tgz Transitive 1.15.0

**In some cases, Remediation PR cannot be created automatically for a vulnerability despite the availability of remediation

Details

CVE-2023-26115

Vulnerable Library - word-wrap-1.2.3.tgz

Wrap words to a specified length.

Library home page: https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz

Path to dependency file: /validator/js/webui/package.json

Path to vulnerable library: /validator/js/webui/node_modules/word-wrap/package.json

Dependency Hierarchy:

  • vulcanize-1.14.12.tgz (Root Library)
    • hydrolysis-1.25.0.tgz
      • escodegen-1.14.3.tgz
        • optionator-0.8.3.tgz
          • word-wrap-1.2.3.tgz (Vulnerable Library)

Found in HEAD commit: 93fe5c7ae5045a3bc62ee5f2042db9f6f63ae4ba

Found in base branch: main

Vulnerability Details

All versions of the package word-wrap are vulnerable to Regular Expression Denial of Service (ReDoS) due to the usage of an insecure regular expression within the result variable.

Publish Date: 2023-06-22

URL: CVE-2023-26115

CVSS 3 Score Details (5.3)

Base Score Metrics:

  • Exploitability Metrics:
    • Attack Vector: Network
    • Attack Complexity: Low
    • Privileges Required: None
    • User Interaction: None
    • Scope: Unchanged
  • Impact Metrics:
    • Confidentiality Impact: None
    • Integrity Impact: None
    • Availability Impact: Low

For more information on CVSS3 Scores, click here.

Suggested Fix

Type: Upgrade version

Origin: GHSA-j8xg-fqg3-53r7

Release Date: 2023-06-22

Fix Resolution (word-wrap): 1.2.4

Direct dependency fix Resolution (vulcanize): 1.15.0

⛑️ Automatic Remediation will be attempted for this issue.


⛑️Automatic Remediation will be attempted for this issue.

Dependency Dashboard

This issue lists Renovate updates and detected dependencies. Read the Dependency Dashboard docs to learn more.

This repository currently has no open or pending branches.

Detected dependencies

npm
build-system/tasks/e2e/package.json
build-system/tasks/storybook/env/amp/package.json
build-system/tasks/storybook/env/preact/package.json
build-system/tasks/storybook/env/react/package.json
build-system/tasks/storybook/package.json
build-system/tasks/visual-diff/package.json
extensions/amp-access/0.1/iframe-api/package.json
extensions/amp-viewer-integration/0.1/messaging/package.json
package.json
third_party/amp-toolbox-cache-url/package.json
validator/js/gulpjs/package.json
validator/js/nodejs/package.json
validator/js/webui/package.json
validator/package.json

Code Security Report: 44 high severity findings, 115 total findings

Code Security Report

Scan Metadata

Latest Scan: 2024-08-19 04:37am
Total Findings: 115 | New Findings: 0 | Resolved Findings: 1
Tested Project Files: 3891
Detected Programming Languages: 4 (Python, C/C++ (Beta), Go, JavaScript / TypeScript*)

  • Check this box to manually trigger a scan

Most Relevant Findings

The list below presents the 10 most relevant findings that need your attention. To view information on the remaining findings, navigate to the Mend Application.

Automatic Remediation Available (10)

SeverityVulnerability TypeCWEFileData FlowsDate
HighPath/Directory Traversal

CWE-22

app.js:1601

12024-08-19 04:08am
Vulnerable Code

const max = serveMode === 'default' ? '.max' : '';
const localPath = `${pc.cwd()}/dist/v0/analytics-vendors/${vendor}${max}.json`;
try {
const file = await fs.promises.readFile(localPath);

1 Data Flow/s detected

async (req, res) => {

const {vendor} = req.params;

const localPath = `${pc.cwd()}/dist/v0/analytics-vendors/${vendor}${max}.json`;

const file = await fs.promises.readFile(localPath);

⛑️ Remediation Suggestion

--- original
+++ remediated
@@ -0,0 +0,0 @@
'use strict';
/**
* @fileoverview Creates an http server to handle static
* files and list directories for use with the amp live server
*/
const argv = require('minimist')(process.argv.slice(2));
const bacon = require('baconipsum');
const bodyParser = require('body-parser');
const cors = require('./amp-cors');
const devDashboard = require('./app-index');
const express = require('express');
const formidable = require('formidable');
const fs = require('fs');
const jsdom = require('jsdom');
const path = require('path');
const upload = require('multer')();
const pc = process;
const autocompleteEmailData = require('./autocomplete-test-data');
const header = require('connect-header');
const runVideoTestBench = require('./app-video-testbench');
const {
getServeMode,
isRtvMode,
replaceUrls,
toInaboxDocument,
} = require('./app-utils');
const {
getVariableRequest,
runVariableSubstitution,
saveVariableRequest,
saveVariables,
} = require('./variable-substitution');
const {
recaptchaFrameRequestHandler,
recaptchaRouter,
} = require('./recaptcha-router');
const {logWithoutTimestamp} = require('../common/logging');
const {log} = require('../common/logging');
const {red} = require('kleur/colors');
const {renderShadowViewer} = require('./shadow-viewer');
/**
* Respond with content received from a URL when SERVE_MODE is "cdn".
* @param {express.Response} res
* @param {string} cdnUrl
* @return {Promise<boolean>}
*/
async function passthroughServeModeCdn(res, cdnUrl) {
if (SERVE_MODE !== 'cdn') {
return false;
}
try {
const response = await fetch(cdnUrl);
res.status(response.status);
res.send(await response.text());
} catch (e) {
log(red('ERROR:'), e);
res.status(500);
res.end();
}
return true;
}
const app = express();
const TEST_SERVER_PORT = argv.port || 8000;
let SERVE_MODE = getServeMode();
app.use(bodyParser.json());
app.use(bodyParser.text());
// Middleware is executed in order, so this must be at the top.
// TODO(#24333): Migrate all server URL handlers to new-server/router and
// deprecate app.js.
app.use(require('./new-server/router'));
app.use(require('./routes/a4a-envelopes'));
app.use('/amp4test', require('./amp4test').app);
app.use('/analytics', require('./routes/analytics'));
app.use('/list/', require('./routes/list'));
app.use('/test', require('./routes/test'));
if (argv.coverage) {
app.use('/coverage', require('istanbul-middleware').createHandler());
}
// Built binaries should be fetchable from other origins, i.e. Storybook.
app.use(header({'Access-Control-Allow-Origin': '*'}));
// Append ?csp=1 to the URL to turn on the CSP header.
// TODO: shall we turn on CSP all the time?
app.use((req, res, next) => {
if (req.query.csp) {
res.set({
'content-security-policy':
"default-src * blob: data:; script-src https://cdn.ampproject.org/rtv/ https://cdn.ampproject.org/v0.js https://cdn.ampproject.org/v0/ https://cdn.ampproject.org/viewer/ http://localhost:8000 https://localhost:8000; object-src 'none'; style-src 'unsafe-inline' https://cdn.ampproject.org/rtv/ https://cdn.materialdesignicons.com https://cloud.typography.com https://fast.fonts.net https://fonts.googleapis.com https://maxcdn.bootstrapcdn.com https://p.typekit.net https://use.fontawesome.com https://use.typekit.net https://cdnjs.cloudflare.com/ajax/libs/font-awesome/; report-uri https://csp-collector.appspot.com/csp/amp",
});
}
next();
});
/**
*
* @param {string} serveMode
* @return {boolean}
*/
function isValidServeMode(serveMode) {
return (
['default', 'minified', 'cdn', 'esm'].includes(serveMode) ||
isRtvMode(serveMode)
);
}
/**
*
* @param {string} serveMode
*/
function setServeMode(serveMode) {
SERVE_MODE = serveMode;
}
app.get('/serve_mode=:mode', (req, res) => {
const newMode = req.params.mode;
if (isValidServeMode(newMode)) {
setServeMode(newMode);
res.send(`<h2>Serve mode changed to ${newMode}</h2>`);
} else {
const info = '<h2>Serve mode ' + newMode + ' is not supported. </h2>';
res.status(400).send(info);
}
});
if (argv._.includes('integration') && !argv.nobuild) {
setServeMode('minified');
}
if (!(argv._.includes('unit') || argv._.includes('integration'))) {
// Dev dashboard routes break test scaffolding since they're global.
devDashboard.installExpressMiddleware(app);
}
// Changes the current serve mode via query param
// e.g. /serve_mode_change?mode=(default|minified|cdn|<RTV_NUMBER>)
// (See ./app-index/settings.js)
app.get('/serve_mode_change', (req, res) => {
const {mode} = req.query;
if (isValidServeMode(mode)) {
setServeMode(mode);
res.json({ok: true});
return;
}
res.status(400).json({ok: false});
});
// Redirects to a proxied document with optional mode through query params.
//
// Mode can be one of:
// - '/', empty string, or unset for an unwrapped doc
// - '/a4a/' for an AMP4ADS wrapper
// - '/a4a-3p/' for a 3P AMP4ADS wrapper
// - '/inabox/' for an AMP inabox wrapper
// - '/shadow/' for a shadow-wrapped document
//
// Examples:
// - /proxy/?url=hello.com 👉 /proxy/s/hello.com
// - /proxy/?url=hello.com?mode=/shadow/ 👉 /shadow/proxy/s/hello.com
// - /proxy/?url=https://hello.com 👉 /proxy/s/hello.com
// - /proxy/?url=https://www.google.com/amp/s/hello.com 👉 /proxy/s/hello.com
// - /proxy/?url=hello.com/canonical 👉 /proxy/s/hello.com/amp
//
// This passthrough is useful to generate the URL from <form> values,
// (See ./app-index/proxy-form.js)
app.get('/proxy', async (req, res, next) => {
const {mode, url} = req.query;
const urlSuffixClearPrefixReStr =
'^https?://((www.)?google.(com?|[a-z]{2}|com?.[a-z]{2}|cat)/amp/s/)?';
const urlSuffix = url.replace(new RegExp(urlSuffixClearPrefixReStr, 'i'), '');
try {
const ampdocUrl = await requestAmphtmlDocUrl(urlSuffix);
const ampdocUrlSuffix = ampdocUrl.replace(/^https?:\/\//, '');
const modePrefix = (mode || '').replace(/\/$/, '');
const proxyUrl = `${modePrefix}/proxy/s/${ampdocUrlSuffix}`;
res.redirect(proxyUrl);
} catch ({message}) {
logWithoutTimestamp(`ERROR: ${message}`);
next();
}
});
/**
* Resolves an AMPHTML URL from a canonical URL. If AMPHTML is canonical, same
* URL is returned.
* @param {string} urlSuffix URL without protocol or google.com/amp/s/...
* @param {string=} protocol 'https' or 'http'. 'https' retries using 'http'.
* @return {!Promise<string>}
*/
async function requestAmphtmlDocUrl(urlSuffix, protocol = 'https') {
const defaultUrl = `${protocol}://${urlSuffix}`;
logWithoutTimestamp(`Fetching URL: ${defaultUrl}`);
const response = await fetch(defaultUrl);
if (!response.ok) {
if (protocol == 'https') {
return requestAmphtmlDocUrl(urlSuffix, 'http');
}
throw new Error(`Status: ${response.status}`);
}
const {window} = new jsdom.JSDOM(await response.text());
const linkRelAmphtml = window.document.querySelector('link[rel=amphtml]');
const amphtmlUrl = linkRelAmphtml && linkRelAmphtml.getAttribute('href');
return amphtmlUrl || defaultUrl;
}
/*
* Intercept Recaptcha frame for,
* integration tests. Using this to mock
* out the recaptcha api.
*/
app.get('/dist.3p/*/recaptcha.*html', recaptchaFrameRequestHandler);
app.use('/recaptcha', recaptchaRouter);
// Deprecate usage of .min.html/.max.html
app.get(
[
'/examples/*.(min|max).html',
'/test/manual/*.(min|max).html',
'/test/fixtures/e2e/*/*.(min|max).html',
],
(req, res) => {
const filePath = req.url;
res.send(generateInfo(filePath));
}
);
app.use('/pwa', (req, res) => {
let file;
let contentType;
if (!req.url || req.path == '/') {
// pwa.html
contentType = 'text/html';
file = '/examples/pwa/pwa.html';
} else if (req.url == '/pwa.js') {
// pwa.js
contentType = 'application/javascript';
file = '/examples/pwa/pwa.js';
} else if (req.url == '/pwa-sw.js') {
// pwa.js
contentType = 'application/javascript';
file = '/examples/pwa/pwa-sw.js';
} else if (req.url == '/ampdoc-shell') {
// pwa-ampdoc-shell.html
contentType = 'text/html';
file = '/examples/pwa/pwa-ampdoc-shell.html';
} else {
// Redirect to the underlying resource.
// TODO(dvoytenko): would be nicer to do forward instead of redirect.
res.writeHead(302, {'Location': req.url});
res.end();
return;
}
res.statusCode = 200;
res.setHeader('Content-Type', contentType);
fs.promises.readFile(pc.cwd() + file).then((file) => {
res.end(file);
});
});
app.use('/api/show', (_req, res) => {
res.json({
showNotification: true,
});
});
app.use('/api/dont-show', (_req, res) => {
res.json({
showNotification: false,
});
});
app.use('/api/echo/query', (req, res) => {
res.json(JSON.parse(req.query.data));
});
app.use('/api/echo/post', (req, res) => {
res.setHeader('Content-Type', 'application/json');
res.end(req.body);
});
app.use('/api/ping', (_req, res) => {
res.status(204).end();
});
app.use('/form/html/post', (req, res) => {
cors.assertCors(req, res, ['POST']);
const form = new formidable.IncomingForm();
form.parse(req, (_err, fields) => {
res.setHeader('Content-Type', 'text/html');
if (fields['email'] == '[email protected]') {
res.statusCode = 500;
res.end(`
<h1 style="color:red;">Sorry ${fields['name']}!</h1>
<p>The email ${fields['email']} is already subscribed!</p>
`);
} else {
res.end(`
<h1>Thanks ${fields['name']}!</h1>
<p>Please make sure to confirm your email ${fields['email']}</p>
`);
}
});
});
app.use('/form/redirect-to/post', (req, res) => {
cors.assertCors(req, res, ['POST'], ['AMP-Redirect-To']);
res.setHeader('AMP-Redirect-To', 'https://google.com');
res.end('{}');
});
app.use('/form/echo-json/post', (req, res) => {
cors.assertCors(req, res, ['POST']);
const form = new formidable.IncomingForm();
const fields = Object.create(null);
form.on('field', function (name, value) {
if (!(name in fields)) {
fields[name] = value;
return;
}
const realName = name;
if (realName in fields) {
if (!Array.isArray(fields[realName])) {
fields[realName] = [fields[realName]];
}
} else {
fields[realName] = [];
}
fields[realName].push(value);
});
form.parse(req, () => {
res.setHeader('Content-Type', 'application/json; charset=utf-8');
if (fields['email'] == '[email protected]') {
res.statusCode = 500;
}
res.end(JSON.stringify(fields));
});
});
app.use('/form/json/poll1', (req, res) => {
cors.assertCors(req, res, ['POST']);
const form = new formidable.IncomingForm();
form.parse(req, () => {
res.setHeader('Content-Type', 'application/json');
res.end(
JSON.stringify({
result: [
{
answer: 'Penguins',
percentage: new Array(77),
},
{
answer: 'Ostriches',
percentage: new Array(8),
},
{
answer: 'Kiwis',
percentage: new Array(14),
},
{
answer: 'Wekas',
percentage: new Array(1),
},
],
})
);
});
});
app.post('/form/json/upload', upload.fields([{name: 'myFile'}]), (req, res) => {
cors.assertCors(req, res, ['POST']);
const myFile = req.files['myFile'];
if (!myFile) {
res.json({message: 'No file data received'});
return;
}
const fileData = myFile[0];
const contents = fileData.buffer.toString();
res.json({message: contents});
});
app.use('/form/search-html/get', (_req, res) => {
res.setHeader('Content-Type', 'text/html');
res.end(`
<h1>Here's results for your search<h1>
<ul>
<li>Result 1</li>
<li>Result 2</li>
<li>Result 3</li>
</ul>
`);
});
app.use('/form/search-json/get', (req, res) => {
cors.assertCors(req, res, ['GET']);
res.json({
term: req.query.term,
additionalFields: req.query.additionalFields,
results: [{title: 'Result 1'}, {title: 'Result 2'}, {title: 'Result 3'}],
});
});
const autocompleteColors = [
'red',
'orange',
'yellow',
'green',
'blue',
'purple',
'pink',
'black',
'white',
];
app.use('/form/autocomplete/query', (req, res) => {
const query = req.query.q;
if (!query) {
res.json({items: autocompleteColors});
} else {
const lowerCaseQuery = query.toLowerCase();
const filtered = autocompleteColors.filter((l) =>
l.toLowerCase().includes(lowerCaseQuery)
);
res.json({items: filtered});
}
});
app.use('/form/autocomplete/error', (_req, res) => {
res.status(500).end();
});
app.use('/form/mention/query', (req, res) => {
const query = req.query.q;
if (!query) {
res.json({items: autocompleteEmailData});
return;
}
const lowerCaseQuery = query.toLowerCase().trim();
const filtered = autocompleteEmailData.filter((l) =>
l.toLowerCase().startsWith(lowerCaseQuery)
);
res.json({items: filtered});
});
app.use('/form/verify-search-json/post', (req, res) => {
cors.assertCors(req, res, ['POST']);
const form = new formidable.IncomingForm();
form.parse(req, (_err, fields) => {
res.setHeader('Content-Type', 'application/json; charset=utf-8');
const errors = [];
if (!fields.phone.match(/^650/)) {
errors.push({name: 'phone', message: 'Phone must start with 650'});
}
if (fields.name !== 'Frank') {
errors.push({name: 'name', message: 'Please set your name to be Frank'});
}
if (fields.error === 'true') {
errors.push({message: 'You asked for an error, you get an error.'});
}
if (fields.city !== 'Mountain View' || fields.zip !== '94043') {
errors.push({
name: 'city',
message: "City doesn't match zip (Mountain View and 94043)",
});
}
if (errors.length === 0) {
res.end(
JSON.stringify({
results: [
{title: 'Result 1'},
{title: 'Result 2'},
{title: 'Result 3'},
],
committed: true,
})
);
} else {
res.statusCode = 400;
res.end(JSON.stringify({verifyErrors: errors}));
}
});
});
/**
* Fetches an AMP document from the AMP proxy and replaces JS
* URLs, so that they point to localhost.
*
* @param {express.Request} req
* @param {express.Response} res
* @param {string} mode
* @return {Promise<void>}
*/
async function proxyToAmpProxy(req, res, mode) {
const url =
'https://cdn.ampproject.org/' +
(req.query['amp_js_v'] ? 'v' : 'c') +
req.url;
logWithoutTimestamp('Fetching URL: ' + url);
const urlResponse = await fetch(url);
let body = await urlResponse.text();
body = body
// Unversion URLs.
.replace(
/https\:\/\/cdn\.ampproject\.org\/rtv\/\d+\//g,
'https://cdn.ampproject.org/'
)
// <base> href pointing to the proxy, so that images, etc. still work.
.replace('<head>', '<head><base href="https://cdn.ampproject.org/">');
const inabox = req.query['inabox'];
const urlPrefix = getUrlPrefix(req);
if (req.query['mraid']) {
body = body
.replace(
'</head>',
'<script async host-service="amp-mraid" src="https://cdn.ampproject.org/v0/amp-mraid-0.1.js">' +
'</script>' +
'</head>'
)
// Change cdnUrl from the default so amp-mraid requests the (mock)
// mraid.js from the local server. In a real environment this doesn't
// matter as the local environment would intercept this request.
.replace(
'<head>',
' <head>' +
' <script>' +
' window.AMP_CONFIG = {' +
` cdnUrl: "${urlPrefix}",` +
' };' +
' </script>'
);
}
if (inabox) {
body = toInaboxDocument(body);
// Allow CORS requests for A4A.
const origin = req.headers.origin || urlPrefix;
cors.enableCors(req, res, origin);
}
body = replaceUrls(mode, body, urlPrefix);
res.status(urlResponse.status).send(body);
}
let itemCtr = 2;
const doctype = '<!doctype html>\n';
const liveListDocs = Object.create(null);
app.use('/examples/live-list-update(-reverse)?.amp.html', (req, res, next) => {
const mode = SERVE_MODE;
let liveListDoc = liveListDocs[req.baseUrl];
if (mode != 'minified' && mode != 'default') {
// Only handle compile(prev min)/default (prev max) mode
next();
return;
}
// When we already have state in memory and user refreshes page, we flush
// the dom we maintain on the server.
if (!('amp_latest_update_time' in req.query) && liveListDoc) {
let outerHTML = liveListDoc.documentElement./*OK*/ outerHTML;
outerHTML = replaceUrls(mode, outerHTML);
res.send(`${doctype}${outerHTML}`);
return;
}
if (!liveListDoc) {
const liveListUpdateFullPath = `${pc.cwd()}${req.baseUrl}`;
logWithoutTimestamp('liveListUpdateFullPath', liveListUpdateFullPath);
const liveListFile = fs.readFileSync(liveListUpdateFullPath);
liveListDoc = liveListDocs[req.baseUrl] = new jsdom.JSDOM(
liveListFile
).window.document;
liveListDoc.ctr = 0;
}
const liveList = liveListDoc.querySelector('#my-live-list');
const perPage = Number(liveList.getAttribute('data-max-items-per-page'));
const items = liveList.querySelector('[items]');
const pagination = liveListDoc.querySelector('#my-live-list [pagination]');
const item1 = liveList.querySelector('#list-item-1');
if (liveListDoc.ctr != 0) {
if (Math.random() < 0.8) {
// Always run a replace on the first item
liveListReplace(item1);
if (Math.random() < 0.5) {
liveListTombstone(liveList);
}
if (Math.random() < 0.8) {
liveListInsert(liveList, item1);
}
pagination.textContent = '';
const liveChildren = [].slice
.call(items.children)
.filter((x) => !x.hasAttribute('data-tombstone'));
const pageCount = Math.ceil(liveChildren.length / perPage);
const pageListItems = Array.apply(null, Array(pageCount))
.map((_, i) => `<li>${i + 1}</li>`)
.join('');
const newPagination =
'<nav aria-label="amp live list pagination">' +
`<ul class="pagination">${pageListItems}</ul>` +
'</nav>';
pagination./*OK*/ innerHTML = newPagination;
} else {
// Sometimes we want an empty response to simulate no changes.
res.send(`${doctype}<html></html>`);
return;
}
}
let outerHTML = liveListDoc.documentElement./*OK*/ outerHTML;
outerHTML = replaceUrls(mode, outerHTML);
liveListDoc.ctr++;
res.send(`${doctype}${outerHTML}`);
});
/**
* @param {Element} item
*/
function liveListReplace(item) {
item.setAttribute('data-update-time', Date.now().toString());
const itemContents = item.querySelectorAll('.content');
itemContents[0].textContent = Math.floor(Math.random() * 10).toString();
itemContents[1].textContent = Math.floor(Math.random() * 10).toString();
}
/**
* @param {Element} liveList
* @param {Element} node
*/
function liveListInsert(liveList, node) {
const iterCount = Math.floor(Math.random() * 2) + 1;
logWithoutTimestamp(`inserting ${iterCount} item(s)`);
for (let i = 0; i < iterCount; i++) {
/**
* TODO(#28387) this type cast may be hiding a bug.
* @type {Element}
*/
const child = /** @type {*} */ (node.cloneNode(true));
child.setAttribute('id', `list-item-${itemCtr++}`);
child.setAttribute('data-sort-time', Date.now().toString());
liveList.querySelector('[items]')?.appendChild(child);
}
}
/**
* @param {Element} liveList
*/
function liveListTombstone(liveList) {
const tombstoneId = Math.floor(Math.random() * itemCtr);
logWithoutTimestamp(`trying to tombstone #list-item-${tombstoneId}`);
// We can tombstone any list item except item-1 since we always do a
// replace example on item-1.
if (tombstoneId != 1) {
const item = liveList./*OK*/ querySelector(`#list-item-${tombstoneId}`);
if (item) {
item.setAttribute('data-tombstone', '');
}
}
}
/**
* Generate a random number between min and max
* Value is inclusive of both min and max values.
*
* @param {number} min
* @param {number} max
* @return {number}
*/
function range(min, max) {
const values = Array.apply(null, new Array(max - min + 1)).map(
(_, i) => min + i
);
return values[Math.round(Math.random() * (max - min))];
}
/**
* Returns the result of a coin flip, true or false
*
* @return {boolean}
*/
function flip() {
return !!Math.floor(Math.random() * 2);
}
/**
* @return {string}
*/
function getLiveBlogItem() {
const now = Date.now();
// Generate a 3 to 7 worded headline
const headline = bacon(range(3, 7));
const numOfParagraphs = range(1, 2);
const body = Array.apply(null, new Array(numOfParagraphs))
.map(() => {
return `<p>${bacon(range(50, 90))}</p>`;
})
.join('\n');
const img = `<amp-img
src="${
flip()
? 'https://placekitten.com/300/350'
: 'https://baconmockup.com/300/350'
}"
layout="responsive"
height="300" width="350">
</amp-img>`;
return `<!doctype html>
<html amp><body>
<amp-live-list id="live-blog-1">
<div items>
<div id="live-blog-item-${now}" data-sort-time="${now}">
<h3 class="headline">
<a href="#live-blog-item-${now}">${headline}</a>
</h3>
<div class="author">
<div class="byline">
<p>
by <span itemscope itemtype="http://schema.org/Person"
itemprop="author"><b>Lorem Ipsum</b>
<a class="mailto" href="mailto:lorem.ipsum@">
lorem.ipsum@</a></span>
</p>
<p class="brand">PublisherName News Reporter<p>
<p><span itemscope itemtype="http://schema.org/Date"
itemprop="Date">
${new Date(now).toString().replace(/ GMT.*$/, '')}
<span></p>
</div>
</div>
<div class="article-body">${body}</div>
${img}
<div class="social-box">
<amp-social-share type="facebook"
data-param-text="Hello world"
data-param-href="https://example.test/?ref=URL"
data-param-app_id="145634995501895"></amp-social-share>
<amp-social-share type="twitter"></amp-social-share>
</div>
</div>
</div>
</amp-live-list></body></html>`;
}
/**
* @return {string}
*/
function getLiveBlogItemWithBindAttributes() {
const now = Date.now();
// Generate a 3 to 7 worded headline
const numOfParagraphs = range(1, 2);
const body = Array.apply(null, new Array(numOfParagraphs))
.map(() => {
return `<p>${bacon(range(50, 90))}</p>`;
})
.join('\n');
return `<!doctype html>
<html amp><body>
<amp-live-list id="live-blog-1">
<div items>
<div id="live-blog-item-${now}" data-sort-time="${now}">
<div class="article-body">
${body}
<p> As you can see, bacon is far superior to
<b><span [text]='favoriteFood'>everything!</span></b>!</p>
</div>
</div>
</div>
</amp-live-list></body></html>`;
}
app.use(
'/examples/live-blog(-non-floating-button)?.amp.html',
(req, res, next) => {
if ('amp_latest_update_time' in req.query) {
res.setHeader('Content-Type', 'text/html');
res.end(getLiveBlogItem());
return;
}
next();
}
);
app.use('/examples/bind/live-list.amp.html', (req, res, next) => {
if ('amp_latest_update_time' in req.query) {
res.setHeader('Content-Type', 'text/html');
res.end(getLiveBlogItemWithBindAttributes());
return;
}
next();
});
app.use('/impression-proxy/', (req, res) => {
cors.assertCors(req, res, ['GET']);
// Fake response with the following optional fields:
// location: The Url the that server would have sent redirect to w/o ALP
// tracking_url: URL that should be requested to track click
// gclid: The conversion tracking value
const body = {
'location': 'localhost:8000/examples/?gclid=1234&foo=bar&example=123',
'tracking_url': 'tracking_url',
'gclid': '1234',
};
res.send(body);
// Or fake response with status 204 if viewer replaceUrl is provided
});
/**
* Acts in a similar fashion to /serve_mode_change. Saves
* analytics requests via /run-variable-substitution, and
* then returns the encoded/substituted/replaced request
* via /get-variable-request.
*/
// Saves the variables input to be used in run-variable-substitution
app.get('/save-variables', saveVariables);
// Creates an iframe with amp-analytics. Analytics request
// uses save-variable-request as its endpoint.
app.get('/run-variable-substitution', runVariableSubstitution);
// Saves the analytics request to the dev server.
app.get('/save-variable-request', saveVariableRequest);
// Returns the saved analytics request.
app.get('/get-variable-request', getVariableRequest);
let forcePromptOnNext = false;
app.post('/get-consent-v1/', (req, res) => {
cors.assertCors(req, res, ['POST']);
const body = {
'promptIfUnknown': true,
'purposeConsentRequired': ['purpose-foo', 'purpose-bar'],
'forcePromptOnNext': forcePromptOnNext,
'sharedData': {
'tfua': true,
'coppa': true,
},
};
res.json(body);
});
app.get('/get-consent-v1-set/', (req, res) => {
cors.assertCors(req, res, ['GET']);
const value = req.query['forcePromptOnNext'];
if (value == 'false' || value == '0') {
forcePromptOnNext = false;
} else {
forcePromptOnNext = true;
}
res.json({});
res.end();
});
app.post('/get-consent-no-prompt/', (req, res) => {
cors.assertCors(req, res, ['POST']);
const body = {};
res.json(body);
});
app.post('/check-consent', (req, res) => {
cors.assertCors(req, res, ['POST']);
const response = {
'consentRequired': req.query.consentRequired === 'true',
'consentStateValue': req.query.consentStateValue,
'consentString': req.query.consentString,
'expireCache': req.query.expireCache === 'true',
};
if (req.query.consentMetadata) {
response['consentMetadata'] = JSON.parse(
req.query.consentMetadata.replace(/'/g, '"')
);
}
res.json(response);
});
// Proxy with local JS.
// Example:
// http://localhost:8000/proxy/s/www.washingtonpost.com/amphtml/news/post-politics/wp/2016/02/21/bernie-sanders-says-lower-turnout-contributed-to-his-nevada-loss-to-hillary-clinton/
app.use('/proxy/', (req, res) => proxyToAmpProxy(req, res, SERVE_MODE));
// Nest the response in an iframe.
// Example:
// http://localhost:8000/iframe/examples/ads.amp.html
app.get('/iframe/*', (req, res) => {
// Returns an html blob with an iframe pointing to the url after /iframe/.
res.send(`<!doctype html>
<html style="width:100%; height:100%;">
<body style="width:98%; height:98%;">
<iframe src="${req.url.substr(7)}"
style="width:100%; height:100%;">
</iframe>
</body>
</html>`);
});
app.get('/a4a_template/*', (req, res) => {
cors.assertCors(req, res, ['GET']);
const match = /^\/a4a_template\/([a-z-]+)\/(\d+)$/.exec(req.path);
if (!match) {
res.status(404);
res.end('Invalid path: ' + req.path);
return;
}
const filePath =
`${pc.cwd()}/extensions/amp-ad-network-${match[1]}-impl/` +
`0.1/data/${match[2]}.template`;
fs.promises
.readFile(filePath)
.then((file) => {
res.setHeader('Content-Type', 'application/json');
res.setHeader('AMP-template-amp-creative', 'amp-mustache');
res.end(file);
})
.catch(() => {
res.status(404);
res.end('Not found: ' + filePath);
});
});
// Returns a document that echoes any post messages received from parent.
// An optional `message` query param can be appended for an initial post
// message sent on document load.
// Example:
// http://localhost:8000/iframe-echo-message?message=${payload}
app.get('/iframe-echo-message', (req, res) => {
const {message} = req.query;
res.send(
`<!doctype html>
<body style="background-color: yellow">
<script>
if (${message}) {
echoMessage(${message});
}
window.addEventListener('message', function(event) {
echoMessage(event.data);
});
function echoMessage(message) {
parent.postMessage(message, '*');
}
</script>
</body>
</html>`
);
});
/**
* Append ?sleep=5 to any included JS file in examples to emulate delay in
* loading that file. This allows you to test issues with your extension being
* late to load and testing user interaction with your element before your code
* loads.
*
* Example delay loading amp-form script by 5 seconds:
* <script async custom-element="amp-form"
* src="https://cdn.ampproject.org/v0/amp-form-0.1.js?sleep=5"></script>
*/
app.use(['/dist/v0/amp-*.(m?js)', '/dist/amp*.(m?js)'], (req, _res, next) => {
const sleep = parseInt(req.query.sleep || 0, 10) * 1000;
setTimeout(next, sleep);
});
/**
* Disable caching for extensions if the --no_caching_extensions flag is used.
*/
app.get(['/dist/v0/amp-*.(m?js)'], (_req, res, next) => {
if (argv.no_caching_extensions) {
res.header('Cache-Control', 'no-store');
}
next();
});
/**
* Video testbench endpoint
*/
app.get('/test/manual/amp-video.amp.html', runVideoTestBench);
app.get(
[
'/examples/(**/)?*.html',
'/test/manual/(**/)?*.html',
'/test/fixtures/e2e/(**/)?*.html',
'/test/fixtures/performance/(**/)?*.html',
],
(req, res, next) => {
const filePath = req.path;
const mode = SERVE_MODE;
const inabox = req.query['inabox'];
const stream = Number(req.query['stream']);
const componentVersion = req.query['componentVersion'];
const urlPrefix = getUrlPrefix(req);
fs.promises
.readFile(pc.cwd() + filePath, 'utf8')
.then((file) => {
if (req.query['amp_js_v']) {
file = addViewerIntegrationScript(req.query['amp_js_v'], file);
}
if (req.query['mraid']) {
file = file
.replace(
'</head>',
'<script async host-service="amp-mraid" src="https://cdn.ampproject.org/v0/amp-mraid-0.1.js">' +
'</script>' +
'</head>'
)
.replace(
'<head>',
' <head>' +
' <script>' +
' window.AMP_CONFIG = {' +
` cdnUrl: "${urlPrefix}",` +
' };' +
' </script>'
);
}
file = file.replace(/__TEST_SERVER_PORT__/g, TEST_SERVER_PORT);
if (componentVersion) {
file = file.replace(/-latest.js/g, `-${componentVersion}.js`);
}
if (inabox) {
file = toInaboxDocument(file);
// Allow CORS requests for A4A.
if (req.headers.origin) {
cors.enableCors(req, res, req.headers.origin);
}
}
file = replaceUrls(mode, file);
const ampExperimentsOptIn = req.query['exp'];
if (ampExperimentsOptIn) {
file = file.replace(
'<head>',
`<head><meta name="amp-experiments-opt-in" content="${ampExperimentsOptIn}">`
);
}
// Extract amp-ad for the given 'type' specified in URL query.
if (req.path.indexOf('/examples/ads.amp.html') == 0 && req.query.type) {
const ads =
file.match(
elementExtractor('(amp-ad|amp-embed)', req.query.type)
) ?? [];
file = file.replace(
/<body>[\s\S]+<\/body>/m,
'<body>' + ads.join('') + '</body>'
);
}
// Extract amp-analytics for the given 'type' specified in URL query.
if (
req.path.indexOf('/examples/analytics-vendors.amp.html') == 0 &&
req.query.type
) {
const analytics =
file.match(elementExtractor('amp-analytics', req.query.type)) ?? [];
file = file.replace(
/<div id="container">[\s\S]+<\/div>/m,
'<div id="container">' + analytics.join('') + '</div>'
);
}
// Extract amp-consent for the given 'type' specified in URL query.
if (
req.path.indexOf('/examples/amp-consent/cmp-vendors.amp.html') == 0 &&
req.query.type
) {
const consent =
file.match(elementExtractor('amp-consent', req.query.type)) ?? [];
file = file.replace(
/<div id="container">[\s\S]+<\/div>/m,
'<div id="container">' + consent.join('') + '</div>'
);
}
if (stream > 0) {
res.writeHead(200, {'Content-Type': 'text/html'});
let pos = 0;
const writeChunk = function () {
const chunk = file.substring(
pos,
Math.min(pos + stream, file.length)
);
res.write(chunk);
pos += stream;
if (pos < file.length) {
setTimeout(writeChunk, 500);
} else {
res.end();
}
};
writeChunk();
} else {
res.send(file);
}
})
.catch(() => {
next();
});
}
);
/**
* @param {string} string
* @return {string}
*/
function escapeRegExp(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
/**
* @param {string} tagName
* @param {string} type
* @return {RegExp}
*/
function elementExtractor(tagName, type) {
type = escapeRegExp(type);
return new RegExp(
`<${tagName}[\\s][^>]*['"]${type}['"][^>]*>([\\s\\S]+?)</${tagName}>`,
'gm'
);
}
// Data for example: http://localhost:8000/examples/bind/xhr.amp.html
app.use('/bind/form/get', (req, res) => {
cors.assertCors(req, res, ['GET']);
res.json({
bindXhrResult: 'I was fetched from the server!',
});
});
// Data for example: http://localhost:8000/examples/bind/ecommerce.amp.html
app.use('/bind/ecommerce/sizes', (req, res) => {
cors.assertCors(req, res, ['GET']);
setTimeout(() => {
const prices = {
'0': {
'sizes': {
'XS': 8.99,
'S': 9.99,
},
},
'1': {
'sizes': {
'S': 10.99,
'M': 12.99,
'L': 14.99,
},
},
'2': {
'sizes': {
'L': 11.99,
'XL': 13.99,
},
},
'3': {
'sizes': {
'M': 7.99,
'L': 9.99,
'XL': 11.99,
},
},
'4': {
'sizes': {
'XS': 8.99,
'S': 10.99,
'L': 15.99,
},
},
'5': {
'sizes': {
'S': 8.99,
'L': 14.99,
'XL': 11.99,
},
},
'6': {
'sizes': {
'XS': 8.99,
'S': 9.99,
'M': 12.99,
},
},
'7': {
'sizes': {
'M': 10.99,
'L': 11.99,
},
},
};
const object = {};
object[req.query.shirt] = prices[req.query.shirt];
res.json(object);
}, 1000); // Simulate network delay.
});
/**
* Simulates a publisher's metering state store.
* (amp-subscriptions)
* @type {{[ampReaderId: string]: {}}}
*/
const meteringStateStore = {};
// Simulate a publisher's entitlements API.
// (amp-subscriptions)
app.use('/subscription/:id/entitlements', (req, res) => {
cors.assertCors(req, res, ['GET']);
// Create entitlements response.
const source = 'local' + req.params.id;
const granted = req.params.id > 0;
const grantReason = granted ? 'SUBSCRIBER' : 'NOT_SUBSCRIBER';
const decryptedDocumentKey = decryptDocumentKey(req.query.crypt);
const response = {
source,
granted,
grantReason,
data: {
login: true,
},
decryptedDocumentKey,
};
// Store metering state, if possible.
const ampReaderId = req.query.rid;
if (ampReaderId && req.query.meteringState) {
// Parse metering state from encoded Base64 string.
const encodedMeteringState = req.query.meteringState;
const decodedMeteringState = Buffer.from(
encodedMeteringState,
'base64'
).toString();
const meteringState = JSON.parse(decodedMeteringState);
// Store metering state.
meteringStateStore[ampReaderId] = meteringState;
}
// Add metering state to response, if possible.
if (meteringStateStore[ampReaderId]) {
response.metering = {
state: meteringStateStore[ampReaderId],
};
}
res.json(response);
});
// Simulate a publisher's SKU map API.
// (amp-subscriptions)
app.use('/subscriptions/skumap', (req, res) => {
cors.assertCors(req, res, ['GET']);
res.json({
'subscribe.google.com': {
'subscribeButtonSimple': {
'sku': 'basic',
},
'subscribeButtonCarousel': {
'carouselOptions': {
'skus': ['basic', 'premium_monthly'],
},
},
},
});
});
// Simulate a publisher's pingback API.
// (amp-subscriptions)
app.use('/subscription/pingback', (req, res) => {
cors.assertCors(req, res, ['POST']);
res.json({
done: true,
});
});
/*
Simulate a publisher's account registration API.
The `amp-subscriptions-google` extension sends this API a POST request.
The request body looks like:
{
"googleSignInDetails": {
// This signed JWT contains information from Google Sign-In
"idToken": "...JWT from Google Sign-In...",
// Some useful fields from the `idToken`, pre-parsed for convenience
"name": "Jane Smith",
"givenName": "Jane",
"familyName": "Smith",
"imageUrl": "https://imageurl",
"email": "[email protected]"
},
// Associate this ID with the registration. Use it to look up metering state
// for future entitlements requests
// https://github.com/ampproject/amphtml/blob/main/extensions/amp-subscriptions/amp-subscriptions.md#combining-the-amp-reader-id-with-publisher-cookies
"ampReaderId": "amp-s0m31d3nt1f13r"
}
(amp-subscriptions-google)
*/
app.use('/subscription/register', (req, res) => {
cors.assertCors(req, res, ['POST']);
// Generate a new ID for this metering state.
const meteringStateId = 'ppid' + Math.round(Math.random() * 99999999);
// Define registration timestamp.
//
// For demo purposes, set timestamp to 30 seconds ago.
// This causes Metering Toast to show immediately,
// which helps engineers test metering.
const registrationTimestamp = Math.round(Date.now() / 1000) - 30000;
// Store metering state.
//
// For demo purposes, just save this in memory.
// Production systems should persist this.
meteringStateStore[req.body.ampReaderId] = {
id: meteringStateId,
standardAttributes: {
// eslint-disable-next-line local/camelcase
registered_user: {
timestamp: registrationTimestamp, // In seconds.
},
},
};
res.json({
metering: {
state: meteringStateStore[req.body.ampReaderId],
},
});
});
// Simulated adzerk ad server and AMP cache CDN.
app.get('/adzerk/*', (req, res) => {
cors.assertCors(req, res, ['GET'], ['AMP-template-amp-creative']);
const match = /\/(\d+)/.exec(req.path);
if (!match || !match[1]) {
res.status(404);
res.end('Invalid path: ' + req.path);
return;
}
const filePath =
pc.cwd() + '/extensions/amp-ad-network-adzerk-impl/0.1/data/' + match[1];
fs.promises
.readFile(filePath)
.then((file) => {
res.setHeader('Content-Type', 'application/json');
res.setHeader('AMP-Ad-Template-Extension', 'amp-mustache');
res.setHeader('AMP-Ad-Response-Type', 'template');
res.end(file);
})
.catch(() => {
res.status(404);
res.end('Not found: ' + filePath);
});
});
app.get('/dist/*.mjs', (req, res, next) => {
// Allow CORS access control explicitly for mjs files
cors.enableCors(req, res);
next();
});
/*
* Serve extension scripts and their source maps.
*/
app.get(
['/dist/rtv/*/v0/*.(m?js)', '/dist/rtv/*/v0/*.(m?js).map'],
async (req, res, next) => {
const mode = SERVE_MODE;
const fileName = path.basename(req.path).replace('.max.', '.');
let filePath = 'https://cdn.ampproject.org/v0/' + fileName;
if (await passthroughServeModeCdn(res, filePath)) {
return;
}
const isJsMap = filePath.endsWith('.map');
if (isJsMap) {
filePath = filePath.replace(/\.(m?js)\.map$/, '.$1');
}
filePath = replaceUrls(mode, filePath);
req.url = filePath + (isJsMap ? '.map' : '');
next();
}
);
/**
* Handle amp-story translation file requests with an rtv path.
* We need to make sure we only handle the amp-story requests since this
* can affect other tests with json requests.
*/
app.get('/dist/rtv/*/v0/amp-story*.json', async (req, _res, next) => {
const fileName = path.basename(req.path);
let filePath = 'https://cdn.ampproject.org/v0/' + fileName;
filePath = replaceUrls(SERVE_MODE, filePath);
req.url = filePath;
next();
});
if (argv.coverage === 'live') {
app.get('/dist/amp.js', async (req, res) => {
const ampJs = await fs.promises.readFile(`${pc.cwd()}${req.path}`);
res.setHeader('Content-Type', 'text/javascript');
res.setHeader('Access-Control-Allow-Origin', '*');
// Append an unload handler that reports coverage information each time you
// leave a page.
res.end(`${ampJs};
window.addEventListener('beforeunload', (evt) => {
const COV_REPORT_URL = 'http://localhost:${TEST_SERVER_PORT}/coverage/client';
console.info('POSTing code coverage to', COV_REPORT_URL);
const xhr = new XMLHttpRequest();
xhr.open('POST', COV_REPORT_URL, true);
xhr.setRequestHeader('Content-type', 'application/json');
xhr.send(JSON.stringify(window.__coverage__));
// Required by Chrome
evt.returnValue = '';
return null;
});`);
});
}
app.get('/dist/ww.(m?js)', async (req, res, next) => {
// Special case for entry point script url. Use minified for testing
const mode = SERVE_MODE;
const fileName = path.basename(req.path);
if (await passthroughServeModeCdn(res, fileName)) {
return;
}
if (mode == 'default') {
req.url = req.url.replace(/\.(m?js)$/, '.max.$1');
}
next();
});
app.get('/dist/iframe-transport-client-lib.(m?js)', (req, _res, next) => {
req.url = req.url.replace(/dist/, 'dist.3p/current');
next();
});
app.get('/dist/amp-inabox-host.(m?js)', (req, _res, next) => {
const mode = SERVE_MODE;
if (mode != 'default') {
req.url = req.url.replace('amp-inabox-host', 'amp4ads-host-v0');
}
next();
});
app.get('/mraid.js', (req, _res, next) => {
req.url = req.url.replace('mraid.js', 'examples/mraid/mraid.js');
next();
});
/**
* Shadow viewer. Fetches shadow runtime from cdn by default.
* Setting the param useLocal=1 will load the runtime from the local build.
*/
app.use('/shadow/', (req, res) => {
const {url} = req;
const isProxyUrl = /^\/proxy\//.test(url);
const baseHref = isProxyUrl
? 'https://cdn.ampproject.org/'
: `${path.dirname(url)}/`;
const viewerHtml = renderShadowViewer({
src: '//' + req.hostname + '/' + req.url.replace(/^\//, ''),
baseHref,
});
if (!req.query.useLocal) {
res.end(viewerHtml);
return;
}
res.end(replaceUrls(SERVE_MODE, viewerHtml));
});
app.use('/mraid/', (req, res) => {
res.redirect(req.url + '?inabox=1&mraid=1');
});
/**
* @param {string} ampJsVersionString
* @param {string} file
* @return {string}
*/
function addViewerIntegrationScript(ampJsVersionString, file) {
const ampJsVersion = parseFloat(ampJsVersionString);
if (!ampJsVersion) {
return file;
}
let viewerScript;
// eslint-disable-next-line local/no-es2015-number-props
if (Number.isInteger(ampJsVersion)) {
// Viewer integration script from gws, such as
// https://cdn.ampproject.org/viewer/google/v7.js
viewerScript =
'<script async src="https://cdn.ampproject.org/viewer/google/v' +
ampJsVersion +
'.js"></script>';
} else {
// Viewer integration script from runtime, such as
// https://cdn.ampproject.org/v0/amp-viewer-integration-0.1.js
viewerScript =
'<script async ' +
'src="https://cdn.ampproject.org/v0/amp-viewer-integration-' +
ampJsVersion +
'.js" data-amp-report-test="viewer-integr.js"></script>';
}
file = file.replace('</head>', viewerScript + '</head>');
return file;
}
/**
* @param {express.Request} req
* @return {string}
*/
function getUrlPrefix(req) {
return req.protocol + '://' + req.headers.host;
}
/**
* @param {string} filePath
* @return {string}
*/
function generateInfo(filePath) {
const mode = SERVE_MODE;
filePath = filePath.substr(0, filePath.length - 9) + '.html';
return (
'<h2>Please note that .min/.max is no longer supported</h2>' +
'<h3>Current serving mode is ' +
mode +
'</h3>' +
'<h3>Please go to <a href= ' +
filePath +
'>Unversioned Link</a> to view the page<h3>' +
'<h3></h3>' +
'<h3><a href = /serve_mode=default>' +
'Change to DEFAULT mode (unminified JS)</a></h3>' +
'<h3><a href = /serve_mode=minified>' +
'Change to COMPILED mode (minified JS)</a></h3>' +
'<h3><a href = /serve_mode=cdn>' +
'Change to CDN mode (prod JS)</a></h3>'
);
}
/**
* @param {string} encryptedDocumentKey
* @return {?string}
*/
function decryptDocumentKey(encryptedDocumentKey) {
if (!encryptedDocumentKey) {
return null;
}
const cryptoStart = 'ENCRYPT(';
if (!encryptedDocumentKey.includes(cryptoStart, 0)) {
return null;
}
let jsonString = encryptedDocumentKey.replace(cryptoStart, '');
jsonString = jsonString.substring(0, jsonString.length - 1);
const parsedJson = JSON.parse(jsonString);
if (!parsedJson) {
return null;
}
return parsedJson.key;
}
// serve local vendor config JSON files
app.use(
'(/dist)?/rtv/*/v0/analytics-vendors/:vendor.json',
async (req, res) => {
const {vendor} = req.params;
const serveMode = SERVE_MODE;
const cdnUrl = `https://cdn.ampproject.org/v0/analytics-vendors/${vendor}.json`;
if (await passthroughServeModeCdn(res, cdnUrl)) {
return;
}
const max = serveMode === 'default' ? '.max' : '';
const localPath = `${pc.cwd()}/dist/v0/analytics-vendors/${vendor}${max}.json`;
@@ -1599,0 +1599,6 @@
+ const resolvedPath = path.resolve(localPath);
+ const expectedDir = path.resolve(pc.cwd(), 'dist/v0/analytics-vendors');
+ if (!resolvedPath.startsWith(expectedDir)) {
+ res.status(400).end('Invalid path');
+ return;
+ }
try {
const file = await fs.promises.readFile(localPath);
res.setHeader('Content-Type', 'application/json');
res.end(file);
} catch (_) {
res.status(404);
res.end('Not found: ' + localPath);
}
}
);
module.exports = app;

  • Create Pull Request

Remediation feedback:

  • 👍 Like
  • 👎 Dislike
Secure Code Warrior Training Material

● Training

   ▪ Secure Code Warrior Path/Directory Traversal Training

● Videos

   ▪ Secure Code Warrior Path/Directory Traversal Video

● Further Reading

   ▪ OWASP Path Traversal

   ▪ OWASP Input Validation Cheat Sheet

 
HighPath/Directory Traversal

CWE-22

app.js:1005

12024-08-19 04:08am
Vulnerable Code

const mode = SERVE_MODE;
const inabox = req.query['inabox'];
const stream = Number(req.query['stream']);
const componentVersion = req.query['componentVersion'];
const urlPrefix = getUrlPrefix(req);
fs.promises

1 Data Flow/s detected

(req, res, next) => {

const filePath = req.path;

.readFile(pc.cwd() + filePath, 'utf8')

⛑️ Remediation Suggestion

--- original
+++ remediated
@@ -0,0 +0,0 @@
'use strict';
/**
* @fileoverview Creates an http server to handle static
* files and list directories for use with the amp live server
*/
const argv = require('minimist')(process.argv.slice(2));
const bacon = require('baconipsum');
const bodyParser = require('body-parser');
const cors = require('./amp-cors');
const devDashboard = require('./app-index');
const express = require('express');
const formidable = require('formidable');
const fs = require('fs');
const jsdom = require('jsdom');
const path = require('path');
const upload = require('multer')();
const pc = process;
const autocompleteEmailData = require('./autocomplete-test-data');
const header = require('connect-header');
const runVideoTestBench = require('./app-video-testbench');
const {
getServeMode,
isRtvMode,
replaceUrls,
toInaboxDocument,
} = require('./app-utils');
const {
getVariableRequest,
runVariableSubstitution,
saveVariableRequest,
saveVariables,
} = require('./variable-substitution');
const {
recaptchaFrameRequestHandler,
recaptchaRouter,
} = require('./recaptcha-router');
const {logWithoutTimestamp} = require('../common/logging');
const {log} = require('../common/logging');
const {red} = require('kleur/colors');
const {renderShadowViewer} = require('./shadow-viewer');
/**
* Respond with content received from a URL when SERVE_MODE is "cdn".
* @param {express.Response} res
* @param {string} cdnUrl
* @return {Promise<boolean>}
*/
async function passthroughServeModeCdn(res, cdnUrl) {
if (SERVE_MODE !== 'cdn') {
return false;
}
try {
const response = await fetch(cdnUrl);
res.status(response.status);
res.send(await response.text());
} catch (e) {
log(red('ERROR:'), e);
res.status(500);
res.end();
}
return true;
}
const app = express();
const TEST_SERVER_PORT = argv.port || 8000;
let SERVE_MODE = getServeMode();
app.use(bodyParser.json());
app.use(bodyParser.text());
// Middleware is executed in order, so this must be at the top.
// TODO(#24333): Migrate all server URL handlers to new-server/router and
// deprecate app.js.
app.use(require('./new-server/router'));
app.use(require('./routes/a4a-envelopes'));
app.use('/amp4test', require('./amp4test').app);
app.use('/analytics', require('./routes/analytics'));
app.use('/list/', require('./routes/list'));
app.use('/test', require('./routes/test'));
if (argv.coverage) {
app.use('/coverage', require('istanbul-middleware').createHandler());
}
// Built binaries should be fetchable from other origins, i.e. Storybook.
app.use(header({'Access-Control-Allow-Origin': '*'}));
// Append ?csp=1 to the URL to turn on the CSP header.
// TODO: shall we turn on CSP all the time?
app.use((req, res, next) => {
if (req.query.csp) {
res.set({
'content-security-policy':
"default-src * blob: data:; script-src https://cdn.ampproject.org/rtv/ https://cdn.ampproject.org/v0.js https://cdn.ampproject.org/v0/ https://cdn.ampproject.org/viewer/ http://localhost:8000 https://localhost:8000; object-src 'none'; style-src 'unsafe-inline' https://cdn.ampproject.org/rtv/ https://cdn.materialdesignicons.com https://cloud.typography.com https://fast.fonts.net https://fonts.googleapis.com https://maxcdn.bootstrapcdn.com https://p.typekit.net https://use.fontawesome.com https://use.typekit.net https://cdnjs.cloudflare.com/ajax/libs/font-awesome/; report-uri https://csp-collector.appspot.com/csp/amp",
});
}
next();
});
/**
*
* @param {string} serveMode
* @return {boolean}
*/
function isValidServeMode(serveMode) {
return (
['default', 'minified', 'cdn', 'esm'].includes(serveMode) ||
isRtvMode(serveMode)
);
}
/**
*
* @param {string} serveMode
*/
function setServeMode(serveMode) {
SERVE_MODE = serveMode;
}
app.get('/serve_mode=:mode', (req, res) => {
const newMode = req.params.mode;
if (isValidServeMode(newMode)) {
setServeMode(newMode);
res.send(`<h2>Serve mode changed to ${newMode}</h2>`);
} else {
const info = '<h2>Serve mode ' + newMode + ' is not supported. </h2>';
res.status(400).send(info);
}
});
if (argv._.includes('integration') && !argv.nobuild) {
setServeMode('minified');
}
if (!(argv._.includes('unit') || argv._.includes('integration'))) {
// Dev dashboard routes break test scaffolding since they're global.
devDashboard.installExpressMiddleware(app);
}
// Changes the current serve mode via query param
// e.g. /serve_mode_change?mode=(default|minified|cdn|<RTV_NUMBER>)
// (See ./app-index/settings.js)
app.get('/serve_mode_change', (req, res) => {
const {mode} = req.query;
if (isValidServeMode(mode)) {
setServeMode(mode);
res.json({ok: true});
return;
}
res.status(400).json({ok: false});
});
// Redirects to a proxied document with optional mode through query params.
//
// Mode can be one of:
// - '/', empty string, or unset for an unwrapped doc
// - '/a4a/' for an AMP4ADS wrapper
// - '/a4a-3p/' for a 3P AMP4ADS wrapper
// - '/inabox/' for an AMP inabox wrapper
// - '/shadow/' for a shadow-wrapped document
//
// Examples:
// - /proxy/?url=hello.com 👉 /proxy/s/hello.com
// - /proxy/?url=hello.com?mode=/shadow/ 👉 /shadow/proxy/s/hello.com
// - /proxy/?url=https://hello.com 👉 /proxy/s/hello.com
// - /proxy/?url=https://www.google.com/amp/s/hello.com 👉 /proxy/s/hello.com
// - /proxy/?url=hello.com/canonical 👉 /proxy/s/hello.com/amp
//
// This passthrough is useful to generate the URL from <form> values,
// (See ./app-index/proxy-form.js)
app.get('/proxy', async (req, res, next) => {
const {mode, url} = req.query;
const urlSuffixClearPrefixReStr =
'^https?://((www.)?google.(com?|[a-z]{2}|com?.[a-z]{2}|cat)/amp/s/)?';
const urlSuffix = url.replace(new RegExp(urlSuffixClearPrefixReStr, 'i'), '');
try {
const ampdocUrl = await requestAmphtmlDocUrl(urlSuffix);
const ampdocUrlSuffix = ampdocUrl.replace(/^https?:\/\//, '');
const modePrefix = (mode || '').replace(/\/$/, '');
const proxyUrl = `${modePrefix}/proxy/s/${ampdocUrlSuffix}`;
res.redirect(proxyUrl);
} catch ({message}) {
logWithoutTimestamp(`ERROR: ${message}`);
next();
}
});
/**
* Resolves an AMPHTML URL from a canonical URL. If AMPHTML is canonical, same
* URL is returned.
* @param {string} urlSuffix URL without protocol or google.com/amp/s/...
* @param {string=} protocol 'https' or 'http'. 'https' retries using 'http'.
* @return {!Promise<string>}
*/
async function requestAmphtmlDocUrl(urlSuffix, protocol = 'https') {
const defaultUrl = `${protocol}://${urlSuffix}`;
logWithoutTimestamp(`Fetching URL: ${defaultUrl}`);
const response = await fetch(defaultUrl);
if (!response.ok) {
if (protocol == 'https') {
return requestAmphtmlDocUrl(urlSuffix, 'http');
}
throw new Error(`Status: ${response.status}`);
}
const {window} = new jsdom.JSDOM(await response.text());
const linkRelAmphtml = window.document.querySelector('link[rel=amphtml]');
const amphtmlUrl = linkRelAmphtml && linkRelAmphtml.getAttribute('href');
return amphtmlUrl || defaultUrl;
}
/*
* Intercept Recaptcha frame for,
* integration tests. Using this to mock
* out the recaptcha api.
*/
app.get('/dist.3p/*/recaptcha.*html', recaptchaFrameRequestHandler);
app.use('/recaptcha', recaptchaRouter);
// Deprecate usage of .min.html/.max.html
app.get(
[
'/examples/*.(min|max).html',
'/test/manual/*.(min|max).html',
'/test/fixtures/e2e/*/*.(min|max).html',
],
(req, res) => {
const filePath = req.url;
res.send(generateInfo(filePath));
}
);
app.use('/pwa', (req, res) => {
let file;
let contentType;
if (!req.url || req.path == '/') {
// pwa.html
contentType = 'text/html';
file = '/examples/pwa/pwa.html';
} else if (req.url == '/pwa.js') {
// pwa.js
contentType = 'application/javascript';
file = '/examples/pwa/pwa.js';
} else if (req.url == '/pwa-sw.js') {
// pwa.js
contentType = 'application/javascript';
file = '/examples/pwa/pwa-sw.js';
} else if (req.url == '/ampdoc-shell') {
// pwa-ampdoc-shell.html
contentType = 'text/html';
file = '/examples/pwa/pwa-ampdoc-shell.html';
} else {
// Redirect to the underlying resource.
// TODO(dvoytenko): would be nicer to do forward instead of redirect.
res.writeHead(302, {'Location': req.url});
res.end();
return;
}
res.statusCode = 200;
res.setHeader('Content-Type', contentType);
fs.promises.readFile(pc.cwd() + file).then((file) => {
res.end(file);
});
});
app.use('/api/show', (_req, res) => {
res.json({
showNotification: true,
});
});
app.use('/api/dont-show', (_req, res) => {
res.json({
showNotification: false,
});
});
app.use('/api/echo/query', (req, res) => {
res.json(JSON.parse(req.query.data));
});
app.use('/api/echo/post', (req, res) => {
res.setHeader('Content-Type', 'application/json');
res.end(req.body);
});
app.use('/api/ping', (_req, res) => {
res.status(204).end();
});
app.use('/form/html/post', (req, res) => {
cors.assertCors(req, res, ['POST']);
const form = new formidable.IncomingForm();
form.parse(req, (_err, fields) => {
res.setHeader('Content-Type', 'text/html');
if (fields['email'] == '[email protected]') {
res.statusCode = 500;
res.end(`
<h1 style="color:red;">Sorry ${fields['name']}!</h1>
<p>The email ${fields['email']} is already subscribed!</p>
`);
} else {
res.end(`
<h1>Thanks ${fields['name']}!</h1>
<p>Please make sure to confirm your email ${fields['email']}</p>
`);
}
});
});
app.use('/form/redirect-to/post', (req, res) => {
cors.assertCors(req, res, ['POST'], ['AMP-Redirect-To']);
res.setHeader('AMP-Redirect-To', 'https://google.com');
res.end('{}');
});
app.use('/form/echo-json/post', (req, res) => {
cors.assertCors(req, res, ['POST']);
const form = new formidable.IncomingForm();
const fields = Object.create(null);
form.on('field', function (name, value) {
if (!(name in fields)) {
fields[name] = value;
return;
}
const realName = name;
if (realName in fields) {
if (!Array.isArray(fields[realName])) {
fields[realName] = [fields[realName]];
}
} else {
fields[realName] = [];
}
fields[realName].push(value);
});
form.parse(req, () => {
res.setHeader('Content-Type', 'application/json; charset=utf-8');
if (fields['email'] == '[email protected]') {
res.statusCode = 500;
}
res.end(JSON.stringify(fields));
});
});
app.use('/form/json/poll1', (req, res) => {
cors.assertCors(req, res, ['POST']);
const form = new formidable.IncomingForm();
form.parse(req, () => {
res.setHeader('Content-Type', 'application/json');
res.end(
JSON.stringify({
result: [
{
answer: 'Penguins',
percentage: new Array(77),
},
{
answer: 'Ostriches',
percentage: new Array(8),
},
{
answer: 'Kiwis',
percentage: new Array(14),
},
{
answer: 'Wekas',
percentage: new Array(1),
},
],
})
);
});
});
app.post('/form/json/upload', upload.fields([{name: 'myFile'}]), (req, res) => {
cors.assertCors(req, res, ['POST']);
const myFile = req.files['myFile'];
if (!myFile) {
res.json({message: 'No file data received'});
return;
}
const fileData = myFile[0];
const contents = fileData.buffer.toString();
res.json({message: contents});
});
app.use('/form/search-html/get', (_req, res) => {
res.setHeader('Content-Type', 'text/html');
res.end(`
<h1>Here's results for your search<h1>
<ul>
<li>Result 1</li>
<li>Result 2</li>
<li>Result 3</li>
</ul>
`);
});
app.use('/form/search-json/get', (req, res) => {
cors.assertCors(req, res, ['GET']);
res.json({
term: req.query.term,
additionalFields: req.query.additionalFields,
results: [{title: 'Result 1'}, {title: 'Result 2'}, {title: 'Result 3'}],
});
});
const autocompleteColors = [
'red',
'orange',
'yellow',
'green',
'blue',
'purple',
'pink',
'black',
'white',
];
app.use('/form/autocomplete/query', (req, res) => {
const query = req.query.q;
if (!query) {
res.json({items: autocompleteColors});
} else {
const lowerCaseQuery = query.toLowerCase();
const filtered = autocompleteColors.filter((l) =>
l.toLowerCase().includes(lowerCaseQuery)
);
res.json({items: filtered});
}
});
app.use('/form/autocomplete/error', (_req, res) => {
res.status(500).end();
});
app.use('/form/mention/query', (req, res) => {
const query = req.query.q;
if (!query) {
res.json({items: autocompleteEmailData});
return;
}
const lowerCaseQuery = query.toLowerCase().trim();
const filtered = autocompleteEmailData.filter((l) =>
l.toLowerCase().startsWith(lowerCaseQuery)
);
res.json({items: filtered});
});
app.use('/form/verify-search-json/post', (req, res) => {
cors.assertCors(req, res, ['POST']);
const form = new formidable.IncomingForm();
form.parse(req, (_err, fields) => {
res.setHeader('Content-Type', 'application/json; charset=utf-8');
const errors = [];
if (!fields.phone.match(/^650/)) {
errors.push({name: 'phone', message: 'Phone must start with 650'});
}
if (fields.name !== 'Frank') {
errors.push({name: 'name', message: 'Please set your name to be Frank'});
}
if (fields.error === 'true') {
errors.push({message: 'You asked for an error, you get an error.'});
}
if (fields.city !== 'Mountain View' || fields.zip !== '94043') {
errors.push({
name: 'city',
message: "City doesn't match zip (Mountain View and 94043)",
});
}
if (errors.length === 0) {
res.end(
JSON.stringify({
results: [
{title: 'Result 1'},
{title: 'Result 2'},
{title: 'Result 3'},
],
committed: true,
})
);
} else {
res.statusCode = 400;
res.end(JSON.stringify({verifyErrors: errors}));
}
});
});
/**
* Fetches an AMP document from the AMP proxy and replaces JS
* URLs, so that they point to localhost.
*
* @param {express.Request} req
* @param {express.Response} res
* @param {string} mode
* @return {Promise<void>}
*/
async function proxyToAmpProxy(req, res, mode) {
const url =
'https://cdn.ampproject.org/' +
(req.query['amp_js_v'] ? 'v' : 'c') +
req.url;
logWithoutTimestamp('Fetching URL: ' + url);
const urlResponse = await fetch(url);
let body = await urlResponse.text();
body = body
// Unversion URLs.
.replace(
/https\:\/\/cdn\.ampproject\.org\/rtv\/\d+\//g,
'https://cdn.ampproject.org/'
)
// <base> href pointing to the proxy, so that images, etc. still work.
.replace('<head>', '<head><base href="https://cdn.ampproject.org/">');
const inabox = req.query['inabox'];
const urlPrefix = getUrlPrefix(req);
if (req.query['mraid']) {
body = body
.replace(
'</head>',
'<script async host-service="amp-mraid" src="https://cdn.ampproject.org/v0/amp-mraid-0.1.js">' +
'</script>' +
'</head>'
)
// Change cdnUrl from the default so amp-mraid requests the (mock)
// mraid.js from the local server. In a real environment this doesn't
// matter as the local environment would intercept this request.
.replace(
'<head>',
' <head>' +
' <script>' +
' window.AMP_CONFIG = {' +
` cdnUrl: "${urlPrefix}",` +
' };' +
' </script>'
);
}
if (inabox) {
body = toInaboxDocument(body);
// Allow CORS requests for A4A.
const origin = req.headers.origin || urlPrefix;
cors.enableCors(req, res, origin);
}
body = replaceUrls(mode, body, urlPrefix);
res.status(urlResponse.status).send(body);
}
let itemCtr = 2;
const doctype = '<!doctype html>\n';
const liveListDocs = Object.create(null);
app.use('/examples/live-list-update(-reverse)?.amp.html', (req, res, next) => {
const mode = SERVE_MODE;
let liveListDoc = liveListDocs[req.baseUrl];
if (mode != 'minified' && mode != 'default') {
// Only handle compile(prev min)/default (prev max) mode
next();
return;
}
// When we already have state in memory and user refreshes page, we flush
// the dom we maintain on the server.
if (!('amp_latest_update_time' in req.query) && liveListDoc) {
let outerHTML = liveListDoc.documentElement./*OK*/ outerHTML;
outerHTML = replaceUrls(mode, outerHTML);
res.send(`${doctype}${outerHTML}`);
return;
}
if (!liveListDoc) {
const liveListUpdateFullPath = `${pc.cwd()}${req.baseUrl}`;
logWithoutTimestamp('liveListUpdateFullPath', liveListUpdateFullPath);
const liveListFile = fs.readFileSync(liveListUpdateFullPath);
liveListDoc = liveListDocs[req.baseUrl] = new jsdom.JSDOM(
liveListFile
).window.document;
liveListDoc.ctr = 0;
}
const liveList = liveListDoc.querySelector('#my-live-list');
const perPage = Number(liveList.getAttribute('data-max-items-per-page'));
const items = liveList.querySelector('[items]');
const pagination = liveListDoc.querySelector('#my-live-list [pagination]');
const item1 = liveList.querySelector('#list-item-1');
if (liveListDoc.ctr != 0) {
if (Math.random() < 0.8) {
// Always run a replace on the first item
liveListReplace(item1);
if (Math.random() < 0.5) {
liveListTombstone(liveList);
}
if (Math.random() < 0.8) {
liveListInsert(liveList, item1);
}
pagination.textContent = '';
const liveChildren = [].slice
.call(items.children)
.filter((x) => !x.hasAttribute('data-tombstone'));
const pageCount = Math.ceil(liveChildren.length / perPage);
const pageListItems = Array.apply(null, Array(pageCount))
.map((_, i) => `<li>${i + 1}</li>`)
.join('');
const newPagination =
'<nav aria-label="amp live list pagination">' +
`<ul class="pagination">${pageListItems}</ul>` +
'</nav>';
pagination./*OK*/ innerHTML = newPagination;
} else {
// Sometimes we want an empty response to simulate no changes.
res.send(`${doctype}<html></html>`);
return;
}
}
let outerHTML = liveListDoc.documentElement./*OK*/ outerHTML;
outerHTML = replaceUrls(mode, outerHTML);
liveListDoc.ctr++;
res.send(`${doctype}${outerHTML}`);
});
/**
* @param {Element} item
*/
function liveListReplace(item) {
item.setAttribute('data-update-time', Date.now().toString());
const itemContents = item.querySelectorAll('.content');
itemContents[0].textContent = Math.floor(Math.random() * 10).toString();
itemContents[1].textContent = Math.floor(Math.random() * 10).toString();
}
/**
* @param {Element} liveList
* @param {Element} node
*/
function liveListInsert(liveList, node) {
const iterCount = Math.floor(Math.random() * 2) + 1;
logWithoutTimestamp(`inserting ${iterCount} item(s)`);
for (let i = 0; i < iterCount; i++) {
/**
* TODO(#28387) this type cast may be hiding a bug.
* @type {Element}
*/
const child = /** @type {*} */ (node.cloneNode(true));
child.setAttribute('id', `list-item-${itemCtr++}`);
child.setAttribute('data-sort-time', Date.now().toString());
liveList.querySelector('[items]')?.appendChild(child);
}
}
/**
* @param {Element} liveList
*/
function liveListTombstone(liveList) {
const tombstoneId = Math.floor(Math.random() * itemCtr);
logWithoutTimestamp(`trying to tombstone #list-item-${tombstoneId}`);
// We can tombstone any list item except item-1 since we always do a
// replace example on item-1.
if (tombstoneId != 1) {
const item = liveList./*OK*/ querySelector(`#list-item-${tombstoneId}`);
if (item) {
item.setAttribute('data-tombstone', '');
}
}
}
/**
* Generate a random number between min and max
* Value is inclusive of both min and max values.
*
* @param {number} min
* @param {number} max
* @return {number}
*/
function range(min, max) {
const values = Array.apply(null, new Array(max - min + 1)).map(
(_, i) => min + i
);
return values[Math.round(Math.random() * (max - min))];
}
/**
* Returns the result of a coin flip, true or false
*
* @return {boolean}
*/
function flip() {
return !!Math.floor(Math.random() * 2);
}
/**
* @return {string}
*/
function getLiveBlogItem() {
const now = Date.now();
// Generate a 3 to 7 worded headline
const headline = bacon(range(3, 7));
const numOfParagraphs = range(1, 2);
const body = Array.apply(null, new Array(numOfParagraphs))
.map(() => {
return `<p>${bacon(range(50, 90))}</p>`;
})
.join('\n');
const img = `<amp-img
src="${
flip()
? 'https://placekitten.com/300/350'
: 'https://baconmockup.com/300/350'
}"
layout="responsive"
height="300" width="350">
</amp-img>`;
return `<!doctype html>
<html amp><body>
<amp-live-list id="live-blog-1">
<div items>
<div id="live-blog-item-${now}" data-sort-time="${now}">
<h3 class="headline">
<a href="#live-blog-item-${now}">${headline}</a>
</h3>
<div class="author">
<div class="byline">
<p>
by <span itemscope itemtype="http://schema.org/Person"
itemprop="author"><b>Lorem Ipsum</b>
<a class="mailto" href="mailto:lorem.ipsum@">
lorem.ipsum@</a></span>
</p>
<p class="brand">PublisherName News Reporter<p>
<p><span itemscope itemtype="http://schema.org/Date"
itemprop="Date">
${new Date(now).toString().replace(/ GMT.*$/, '')}
<span></p>
</div>
</div>
<div class="article-body">${body}</div>
${img}
<div class="social-box">
<amp-social-share type="facebook"
data-param-text="Hello world"
data-param-href="https://example.test/?ref=URL"
data-param-app_id="145634995501895"></amp-social-share>
<amp-social-share type="twitter"></amp-social-share>
</div>
</div>
</div>
</amp-live-list></body></html>`;
}
/**
* @return {string}
*/
function getLiveBlogItemWithBindAttributes() {
const now = Date.now();
// Generate a 3 to 7 worded headline
const numOfParagraphs = range(1, 2);
const body = Array.apply(null, new Array(numOfParagraphs))
.map(() => {
return `<p>${bacon(range(50, 90))}</p>`;
})
.join('\n');
return `<!doctype html>
<html amp><body>
<amp-live-list id="live-blog-1">
<div items>
<div id="live-blog-item-${now}" data-sort-time="${now}">
<div class="article-body">
${body}
<p> As you can see, bacon is far superior to
<b><span [text]='favoriteFood'>everything!</span></b>!</p>
</div>
</div>
</div>
</amp-live-list></body></html>`;
}
app.use(
'/examples/live-blog(-non-floating-button)?.amp.html',
(req, res, next) => {
if ('amp_latest_update_time' in req.query) {
res.setHeader('Content-Type', 'text/html');
res.end(getLiveBlogItem());
return;
}
next();
}
);
app.use('/examples/bind/live-list.amp.html', (req, res, next) => {
if ('amp_latest_update_time' in req.query) {
res.setHeader('Content-Type', 'text/html');
res.end(getLiveBlogItemWithBindAttributes());
return;
}
next();
});
app.use('/impression-proxy/', (req, res) => {
cors.assertCors(req, res, ['GET']);
// Fake response with the following optional fields:
// location: The Url the that server would have sent redirect to w/o ALP
// tracking_url: URL that should be requested to track click
// gclid: The conversion tracking value
const body = {
'location': 'localhost:8000/examples/?gclid=1234&foo=bar&example=123',
'tracking_url': 'tracking_url',
'gclid': '1234',
};
res.send(body);
// Or fake response with status 204 if viewer replaceUrl is provided
});
/**
* Acts in a similar fashion to /serve_mode_change. Saves
* analytics requests via /run-variable-substitution, and
* then returns the encoded/substituted/replaced request
* via /get-variable-request.
*/
// Saves the variables input to be used in run-variable-substitution
app.get('/save-variables', saveVariables);
// Creates an iframe with amp-analytics. Analytics request
// uses save-variable-request as its endpoint.
app.get('/run-variable-substitution', runVariableSubstitution);
// Saves the analytics request to the dev server.
app.get('/save-variable-request', saveVariableRequest);
// Returns the saved analytics request.
app.get('/get-variable-request', getVariableRequest);
let forcePromptOnNext = false;
app.post('/get-consent-v1/', (req, res) => {
cors.assertCors(req, res, ['POST']);
const body = {
'promptIfUnknown': true,
'purposeConsentRequired': ['purpose-foo', 'purpose-bar'],
'forcePromptOnNext': forcePromptOnNext,
'sharedData': {
'tfua': true,
'coppa': true,
},
};
res.json(body);
});
app.get('/get-consent-v1-set/', (req, res) => {
cors.assertCors(req, res, ['GET']);
const value = req.query['forcePromptOnNext'];
if (value == 'false' || value == '0') {
forcePromptOnNext = false;
} else {
forcePromptOnNext = true;
}
res.json({});
res.end();
});
app.post('/get-consent-no-prompt/', (req, res) => {
cors.assertCors(req, res, ['POST']);
const body = {};
res.json(body);
});
app.post('/check-consent', (req, res) => {
cors.assertCors(req, res, ['POST']);
const response = {
'consentRequired': req.query.consentRequired === 'true',
'consentStateValue': req.query.consentStateValue,
'consentString': req.query.consentString,
'expireCache': req.query.expireCache === 'true',
};
if (req.query.consentMetadata) {
response['consentMetadata'] = JSON.parse(
req.query.consentMetadata.replace(/'/g, '"')
);
}
res.json(response);
});
// Proxy with local JS.
// Example:
// http://localhost:8000/proxy/s/www.washingtonpost.com/amphtml/news/post-politics/wp/2016/02/21/bernie-sanders-says-lower-turnout-contributed-to-his-nevada-loss-to-hillary-clinton/
app.use('/proxy/', (req, res) => proxyToAmpProxy(req, res, SERVE_MODE));
// Nest the response in an iframe.
// Example:
// http://localhost:8000/iframe/examples/ads.amp.html
app.get('/iframe/*', (req, res) => {
// Returns an html blob with an iframe pointing to the url after /iframe/.
res.send(`<!doctype html>
<html style="width:100%; height:100%;">
<body style="width:98%; height:98%;">
<iframe src="${req.url.substr(7)}"
style="width:100%; height:100%;">
</iframe>
</body>
</html>`);
});
app.get('/a4a_template/*', (req, res) => {
cors.assertCors(req, res, ['GET']);
const match = /^\/a4a_template\/([a-z-]+)\/(\d+)$/.exec(req.path);
if (!match) {
res.status(404);
res.end('Invalid path: ' + req.path);
return;
}
const filePath =
`${pc.cwd()}/extensions/amp-ad-network-${match[1]}-impl/` +
`0.1/data/${match[2]}.template`;
fs.promises
.readFile(filePath)
.then((file) => {
res.setHeader('Content-Type', 'application/json');
res.setHeader('AMP-template-amp-creative', 'amp-mustache');
res.end(file);
})
.catch(() => {
res.status(404);
res.end('Not found: ' + filePath);
});
});
// Returns a document that echoes any post messages received from parent.
// An optional `message` query param can be appended for an initial post
// message sent on document load.
// Example:
// http://localhost:8000/iframe-echo-message?message=${payload}
app.get('/iframe-echo-message', (req, res) => {
const {message} = req.query;
res.send(
`<!doctype html>
<body style="background-color: yellow">
<script>
if (${message}) {
echoMessage(${message});
}
window.addEventListener('message', function(event) {
echoMessage(event.data);
});
function echoMessage(message) {
parent.postMessage(message, '*');
}
</script>
</body>
</html>`
);
});
/**
* Append ?sleep=5 to any included JS file in examples to emulate delay in
* loading that file. This allows you to test issues with your extension being
* late to load and testing user interaction with your element before your code
* loads.
*
* Example delay loading amp-form script by 5 seconds:
* <script async custom-element="amp-form"
* src="https://cdn.ampproject.org/v0/amp-form-0.1.js?sleep=5"></script>
*/
app.use(['/dist/v0/amp-*.(m?js)', '/dist/amp*.(m?js)'], (req, _res, next) => {
const sleep = parseInt(req.query.sleep || 0, 10) * 1000;
setTimeout(next, sleep);
});
/**
* Disable caching for extensions if the --no_caching_extensions flag is used.
*/
app.get(['/dist/v0/amp-*.(m?js)'], (_req, res, next) => {
if (argv.no_caching_extensions) {
res.header('Cache-Control', 'no-store');
}
next();
});
/**
* Video testbench endpoint
*/
app.get('/test/manual/amp-video.amp.html', runVideoTestBench);
app.get(
[
'/examples/(**/)?*.html',
'/test/manual/(**/)?*.html',
'/test/fixtures/e2e/(**/)?*.html',
'/test/fixtures/performance/(**/)?*.html',
],
(req, res, next) => {
const filePath = req.path;
const mode = SERVE_MODE;
const inabox = req.query['inabox'];
const stream = Number(req.query['stream']);
const componentVersion = req.query['componentVersion'];
const urlPrefix = getUrlPrefix(req);
@@ -1005,0 +1005,5 @@
+ const resolvedPath = path.resolve(pc.cwd(), filePath);
+ const expectedDir = path.resolve(pc.cwd()); // TODO: Change this to your expected directory
+ if (!resolvedPath.startsWith(expectedDir)) {
+ throw new Error('Invalid path');
+ }
fs.promises
.readFile(pc.cwd() + filePath, 'utf8')
.then((file) => {
if (req.query['amp_js_v']) {
file = addViewerIntegrationScript(req.query['amp_js_v'], file);
}
if (req.query['mraid']) {
file = file
.replace(
'</head>',
'<script async host-service="amp-mraid" src="https://cdn.ampproject.org/v0/amp-mraid-0.1.js">' +
'</script>' +
'</head>'
)
.replace(
'<head>',
' <head>' +
' <script>' +
' window.AMP_CONFIG = {' +
` cdnUrl: "${urlPrefix}",` +
' };' +
' </script>'
);
}
file = file.replace(/__TEST_SERVER_PORT__/g, TEST_SERVER_PORT);
if (componentVersion) {
file = file.replace(/-latest.js/g, `-${componentVersion}.js`);
}
if (inabox) {
file = toInaboxDocument(file);
// Allow CORS requests for A4A.
if (req.headers.origin) {
cors.enableCors(req, res, req.headers.origin);
}
}
file = replaceUrls(mode, file);
const ampExperimentsOptIn = req.query['exp'];
if (ampExperimentsOptIn) {
file = file.replace(
'<head>',
`<head><meta name="amp-experiments-opt-in" content="${ampExperimentsOptIn}">`
);
}
// Extract amp-ad for the given 'type' specified in URL query.
if (req.path.indexOf('/examples/ads.amp.html') == 0 && req.query.type) {
const ads =
file.match(
elementExtractor('(amp-ad|amp-embed)', req.query.type)
) ?? [];
file = file.replace(
/<body>[\s\S]+<\/body>/m,
'<body>' + ads.join('') + '</body>'
);
}
// Extract amp-analytics for the given 'type' specified in URL query.
if (
req.path.indexOf('/examples/analytics-vendors.amp.html') == 0 &&
req.query.type
) {
const analytics =
file.match(elementExtractor('amp-analytics', req.query.type)) ?? [];
file = file.replace(
/<div id="container">[\s\S]+<\/div>/m,
'<div id="container">' + analytics.join('') + '</div>'
);
}
// Extract amp-consent for the given 'type' specified in URL query.
if (
req.path.indexOf('/examples/amp-consent/cmp-vendors.amp.html') == 0 &&
req.query.type
) {
const consent =
file.match(elementExtractor('amp-consent', req.query.type)) ?? [];
file = file.replace(
/<div id="container">[\s\S]+<\/div>/m,
'<div id="container">' + consent.join('') + '</div>'
);
}
if (stream > 0) {
res.writeHead(200, {'Content-Type': 'text/html'});
let pos = 0;
const writeChunk = function () {
const chunk = file.substring(
pos,
Math.min(pos + stream, file.length)
);
res.write(chunk);
pos += stream;
if (pos < file.length) {
setTimeout(writeChunk, 500);
} else {
res.end();
}
};
writeChunk();
} else {
res.send(file);
}
})
.catch(() => {
next();
});
}
);
/**
* @param {string} string
* @return {string}
*/
function escapeRegExp(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
/**
* @param {string} tagName
* @param {string} type
* @return {RegExp}
*/
function elementExtractor(tagName, type) {
type = escapeRegExp(type);
return new RegExp(
`<${tagName}[\\s][^>]*['"]${type}['"][^>]*>([\\s\\S]+?)</${tagName}>`,
'gm'
);
}
// Data for example: http://localhost:8000/examples/bind/xhr.amp.html
app.use('/bind/form/get', (req, res) => {
cors.assertCors(req, res, ['GET']);
res.json({
bindXhrResult: 'I was fetched from the server!',
});
});
// Data for example: http://localhost:8000/examples/bind/ecommerce.amp.html
app.use('/bind/ecommerce/sizes', (req, res) => {
cors.assertCors(req, res, ['GET']);
setTimeout(() => {
const prices = {
'0': {
'sizes': {
'XS': 8.99,
'S': 9.99,
},
},
'1': {
'sizes': {
'S': 10.99,
'M': 12.99,
'L': 14.99,
},
},
'2': {
'sizes': {
'L': 11.99,
'XL': 13.99,
},
},
'3': {
'sizes': {
'M': 7.99,
'L': 9.99,
'XL': 11.99,
},
},
'4': {
'sizes': {
'XS': 8.99,
'S': 10.99,
'L': 15.99,
},
},
'5': {
'sizes': {
'S': 8.99,
'L': 14.99,
'XL': 11.99,
},
},
'6': {
'sizes': {
'XS': 8.99,
'S': 9.99,
'M': 12.99,
},
},
'7': {
'sizes': {
'M': 10.99,
'L': 11.99,
},
},
};
const object = {};
object[req.query.shirt] = prices[req.query.shirt];
res.json(object);
}, 1000); // Simulate network delay.
});
/**
* Simulates a publisher's metering state store.
* (amp-subscriptions)
* @type {{[ampReaderId: string]: {}}}
*/
const meteringStateStore = {};
// Simulate a publisher's entitlements API.
// (amp-subscriptions)
app.use('/subscription/:id/entitlements', (req, res) => {
cors.assertCors(req, res, ['GET']);
// Create entitlements response.
const source = 'local' + req.params.id;
const granted = req.params.id > 0;
const grantReason = granted ? 'SUBSCRIBER' : 'NOT_SUBSCRIBER';
const decryptedDocumentKey = decryptDocumentKey(req.query.crypt);
const response = {
source,
granted,
grantReason,
data: {
login: true,
},
decryptedDocumentKey,
};
// Store metering state, if possible.
const ampReaderId = req.query.rid;
if (ampReaderId && req.query.meteringState) {
// Parse metering state from encoded Base64 string.
const encodedMeteringState = req.query.meteringState;
const decodedMeteringState = Buffer.from(
encodedMeteringState,
'base64'
).toString();
const meteringState = JSON.parse(decodedMeteringState);
// Store metering state.
meteringStateStore[ampReaderId] = meteringState;
}
// Add metering state to response, if possible.
if (meteringStateStore[ampReaderId]) {
response.metering = {
state: meteringStateStore[ampReaderId],
};
}
res.json(response);
});
// Simulate a publisher's SKU map API.
// (amp-subscriptions)
app.use('/subscriptions/skumap', (req, res) => {
cors.assertCors(req, res, ['GET']);
res.json({
'subscribe.google.com': {
'subscribeButtonSimple': {
'sku': 'basic',
},
'subscribeButtonCarousel': {
'carouselOptions': {
'skus': ['basic', 'premium_monthly'],
},
},
},
});
});
// Simulate a publisher's pingback API.
// (amp-subscriptions)
app.use('/subscription/pingback', (req, res) => {
cors.assertCors(req, res, ['POST']);
res.json({
done: true,
});
});
/*
Simulate a publisher's account registration API.
The `amp-subscriptions-google` extension sends this API a POST request.
The request body looks like:
{
"googleSignInDetails": {
// This signed JWT contains information from Google Sign-In
"idToken": "...JWT from Google Sign-In...",
// Some useful fields from the `idToken`, pre-parsed for convenience
"name": "Jane Smith",
"givenName": "Jane",
"familyName": "Smith",
"imageUrl": "https://imageurl",
"email": "[email protected]"
},
// Associate this ID with the registration. Use it to look up metering state
// for future entitlements requests
// https://github.com/ampproject/amphtml/blob/main/extensions/amp-subscriptions/amp-subscriptions.md#combining-the-amp-reader-id-with-publisher-cookies
"ampReaderId": "amp-s0m31d3nt1f13r"
}
(amp-subscriptions-google)
*/
app.use('/subscription/register', (req, res) => {
cors.assertCors(req, res, ['POST']);
// Generate a new ID for this metering state.
const meteringStateId = 'ppid' + Math.round(Math.random() * 99999999);
// Define registration timestamp.
//
// For demo purposes, set timestamp to 30 seconds ago.
// This causes Metering Toast to show immediately,
// which helps engineers test metering.
const registrationTimestamp = Math.round(Date.now() / 1000) - 30000;
// Store metering state.
//
// For demo purposes, just save this in memory.
// Production systems should persist this.
meteringStateStore[req.body.ampReaderId] = {
id: meteringStateId,
standardAttributes: {
// eslint-disable-next-line local/camelcase
registered_user: {
timestamp: registrationTimestamp, // In seconds.
},
},
};
res.json({
metering: {
state: meteringStateStore[req.body.ampReaderId],
},
});
});
// Simulated adzerk ad server and AMP cache CDN.
app.get('/adzerk/*', (req, res) => {
cors.assertCors(req, res, ['GET'], ['AMP-template-amp-creative']);
const match = /\/(\d+)/.exec(req.path);
if (!match || !match[1]) {
res.status(404);
res.end('Invalid path: ' + req.path);
return;
}
const filePath =
pc.cwd() + '/extensions/amp-ad-network-adzerk-impl/0.1/data/' + match[1];
fs.promises
.readFile(filePath)
.then((file) => {
res.setHeader('Content-Type', 'application/json');
res.setHeader('AMP-Ad-Template-Extension', 'amp-mustache');
res.setHeader('AMP-Ad-Response-Type', 'template');
res.end(file);
})
.catch(() => {
res.status(404);
res.end('Not found: ' + filePath);
});
});
app.get('/dist/*.mjs', (req, res, next) => {
// Allow CORS access control explicitly for mjs files
cors.enableCors(req, res);
next();
});
/*
* Serve extension scripts and their source maps.
*/
app.get(
['/dist/rtv/*/v0/*.(m?js)', '/dist/rtv/*/v0/*.(m?js).map'],
async (req, res, next) => {
const mode = SERVE_MODE;
const fileName = path.basename(req.path).replace('.max.', '.');
let filePath = 'https://cdn.ampproject.org/v0/' + fileName;
if (await passthroughServeModeCdn(res, filePath)) {
return;
}
const isJsMap = filePath.endsWith('.map');
if (isJsMap) {
filePath = filePath.replace(/\.(m?js)\.map$/, '.$1');
}
filePath = replaceUrls(mode, filePath);
req.url = filePath + (isJsMap ? '.map' : '');
next();
}
);
/**
* Handle amp-story translation file requests with an rtv path.
* We need to make sure we only handle the amp-story requests since this
* can affect other tests with json requests.
*/
app.get('/dist/rtv/*/v0/amp-story*.json', async (req, _res, next) => {
const fileName = path.basename(req.path);
let filePath = 'https://cdn.ampproject.org/v0/' + fileName;
filePath = replaceUrls(SERVE_MODE, filePath);
req.url = filePath;
next();
});
if (argv.coverage === 'live') {
app.get('/dist/amp.js', async (req, res) => {
const ampJs = await fs.promises.readFile(`${pc.cwd()}${req.path}`);
res.setHeader('Content-Type', 'text/javascript');
res.setHeader('Access-Control-Allow-Origin', '*');
// Append an unload handler that reports coverage information each time you
// leave a page.
res.end(`${ampJs};
window.addEventListener('beforeunload', (evt) => {
const COV_REPORT_URL = 'http://localhost:${TEST_SERVER_PORT}/coverage/client';
console.info('POSTing code coverage to', COV_REPORT_URL);
const xhr = new XMLHttpRequest();
xhr.open('POST', COV_REPORT_URL, true);
xhr.setRequestHeader('Content-type', 'application/json');
xhr.send(JSON.stringify(window.__coverage__));
// Required by Chrome
evt.returnValue = '';
return null;
});`);
});
}
app.get('/dist/ww.(m?js)', async (req, res, next) => {
// Special case for entry point script url. Use minified for testing
const mode = SERVE_MODE;
const fileName = path.basename(req.path);
if (await passthroughServeModeCdn(res, fileName)) {
return;
}
if (mode == 'default') {
req.url = req.url.replace(/\.(m?js)$/, '.max.$1');
}
next();
});
app.get('/dist/iframe-transport-client-lib.(m?js)', (req, _res, next) => {
req.url = req.url.replace(/dist/, 'dist.3p/current');
next();
});
app.get('/dist/amp-inabox-host.(m?js)', (req, _res, next) => {
const mode = SERVE_MODE;
if (mode != 'default') {
req.url = req.url.replace('amp-inabox-host', 'amp4ads-host-v0');
}
next();
});
app.get('/mraid.js', (req, _res, next) => {
req.url = req.url.replace('mraid.js', 'examples/mraid/mraid.js');
next();
});
/**
* Shadow viewer. Fetches shadow runtime from cdn by default.
* Setting the param useLocal=1 will load the runtime from the local build.
*/
app.use('/shadow/', (req, res) => {
const {url} = req;
const isProxyUrl = /^\/proxy\//.test(url);
const baseHref = isProxyUrl
? 'https://cdn.ampproject.org/'
: `${path.dirname(url)}/`;
const viewerHtml = renderShadowViewer({
src: '//' + req.hostname + '/' + req.url.replace(/^\//, ''),
baseHref,
});
if (!req.query.useLocal) {
res.end(viewerHtml);
return;
}
res.end(replaceUrls(SERVE_MODE, viewerHtml));
});
app.use('/mraid/', (req, res) => {
res.redirect(req.url + '?inabox=1&mraid=1');
});
/**
* @param {string} ampJsVersionString
* @param {string} file
* @return {string}
*/
function addViewerIntegrationScript(ampJsVersionString, file) {
const ampJsVersion = parseFloat(ampJsVersionString);
if (!ampJsVersion) {
return file;
}
let viewerScript;
// eslint-disable-next-line local/no-es2015-number-props
if (Number.isInteger(ampJsVersion)) {
// Viewer integration script from gws, such as
// https://cdn.ampproject.org/viewer/google/v7.js
viewerScript =
'<script async src="https://cdn.ampproject.org/viewer/google/v' +
ampJsVersion +
'.js"></script>';
} else {
// Viewer integration script from runtime, such as
// https://cdn.ampproject.org/v0/amp-viewer-integration-0.1.js
viewerScript =
'<script async ' +
'src="https://cdn.ampproject.org/v0/amp-viewer-integration-' +
ampJsVersion +
'.js" data-amp-report-test="viewer-integr.js"></script>';
}
file = file.replace('</head>', viewerScript + '</head>');
return file;
}
/**
* @param {express.Request} req
* @return {string}
*/
function getUrlPrefix(req) {
return req.protocol + '://' + req.headers.host;
}
/**
* @param {string} filePath
* @return {string}
*/
function generateInfo(filePath) {
const mode = SERVE_MODE;
filePath = filePath.substr(0, filePath.length - 9) + '.html';
return (
'<h2>Please note that .min/.max is no longer supported</h2>' +
'<h3>Current serving mode is ' +
mode +
'</h3>' +
'<h3>Please go to <a href= ' +
filePath +
'>Unversioned Link</a> to view the page<h3>' +
'<h3></h3>' +
'<h3><a href = /serve_mode=default>' +
'Change to DEFAULT mode (unminified JS)</a></h3>' +
'<h3><a href = /serve_mode=minified>' +
'Change to COMPILED mode (minified JS)</a></h3>' +
'<h3><a href = /serve_mode=cdn>' +
'Change to CDN mode (prod JS)</a></h3>'
);
}
/**
* @param {string} encryptedDocumentKey
* @return {?string}
*/
function decryptDocumentKey(encryptedDocumentKey) {
if (!encryptedDocumentKey) {
return null;
}
const cryptoStart = 'ENCRYPT(';
if (!encryptedDocumentKey.includes(cryptoStart, 0)) {
return null;
}
let jsonString = encryptedDocumentKey.replace(cryptoStart, '');
jsonString = jsonString.substring(0, jsonString.length - 1);
const parsedJson = JSON.parse(jsonString);
if (!parsedJson) {
return null;
}
return parsedJson.key;
}
// serve local vendor config JSON files
app.use(
'(/dist)?/rtv/*/v0/analytics-vendors/:vendor.json',
async (req, res) => {
const {vendor} = req.params;
const serveMode = SERVE_MODE;
const cdnUrl = `https://cdn.ampproject.org/v0/analytics-vendors/${vendor}.json`;
if (await passthroughServeModeCdn(res, cdnUrl)) {
return;
}
const max = serveMode === 'default' ? '.max' : '';
const localPath = `${pc.cwd()}/dist/v0/analytics-vendors/${vendor}${max}.json`;
try {
const file = await fs.promises.readFile(localPath);
res.setHeader('Content-Type', 'application/json');
res.end(file);
} catch (_) {
res.status(404);
res.end('Not found: ' + localPath);
}
}
);
module.exports = app;

  • Create Pull Request

Remediation feedback:

  • 👍 Like
  • 👎 Dislike
Secure Code Warrior Training Material

● Training

   ▪ Secure Code Warrior Path/Directory Traversal Training

● Videos

   ▪ Secure Code Warrior Path/Directory Traversal Video

● Further Reading

   ▪ OWASP Path Traversal

   ▪ OWASP Input Validation Cheat Sheet

 
HighPath/Directory Traversal

CWE-22

recaptcha-router.js:19

12024-08-19 04:08am
Vulnerable Code

};
`;
const recaptchaFrameRequestHandler = (req, res, next) => {
if (argv._.includes('unit') || argv._.includes('integration')) {
fs.promises.readFile(pc.cwd() + req.path, 'utf8').then((file) => {

1 Data Flow/s detected

app.get('/dist.3p/*/recaptcha.*html', recaptchaFrameRequestHandler);

const recaptchaFrameRequestHandler = (req, res, next) => {

fs.promises.readFile(pc.cwd() + req.path, 'utf8').then((file) => {

⛑️ Remediation Suggestion

--- original
+++ remediated
@@ -1,0 +1,1 @@
+const path = require('path')
const argv = require('minimist')(process.argv.slice(2));
const cors = require('./amp-cors');
const pc = process;
const fs = require('fs');
const multer = require('multer');
const recaptchaRouter = require('express').Router();
const upload = multer();
const recaptchaMock = `
window.grecaptcha = {
ready: (callback) => callback(),
execute: () => Promise.resolve('recaptcha-mock')
};
`;
const recaptchaFrameRequestHandler = (req, res, next) => {
if (argv._.includes('unit') || argv._.includes('integration')) {
@@ -19,1 +20,6 @@
- fs.promises.readFile(pc.cwd() + req.path, 'utf8').then((file) => {
+ const resolvedPath = path.resolve(pc.cwd(), req.path);
+ const expectedDir = path.resolve(pc.cwd()); // TODO: Change this to your expected directory
+ if (!resolvedPath.startsWith(expectedDir)) {
+ throw new Error('Invalid path');
+ }
+ fs.promises.readFile(resolvedPath, 'utf8').then((file) => {
file = file.replace(
/initRecaptcha\(.*\)/g,
'initRecaptcha("/recaptcha/mock.js?sitekey=")'
);
res.end(file);
});
} else {
next();
}
};
recaptchaRouter.get('/mock.js', (_req, res) => {
res.end(recaptchaMock);
});
recaptchaRouter.post('/submit', upload.array(), (req, res) => {
cors.enableCors(req, res);
const responseJson = {
message: 'Success!',
};
Object.keys(req.body).forEach((bodyKey) => {
responseJson[bodyKey] = req.body[bodyKey];
});
const containsRecaptchaInResponse = Object.keys(responseJson).some(
(responseJsonKey) => {
return responseJsonKey.toLowerCase().includes('recaptcha');
}
);
if (containsRecaptchaInResponse) {
res.status(200).json(responseJson);
} else {
res.status(400).json({
message: 'Did not include a recaptcha token',
});
}
});
module.exports = {
recaptchaFrameRequestHandler,
recaptchaRouter,
};

  • Create Pull Request

Remediation feedback:

  • 👍 Like
  • 👎 Dislike
Secure Code Warrior Training Material

● Training

   ▪ Secure Code Warrior Path/Directory Traversal Training

● Videos

   ▪ Secure Code Warrior Path/Directory Traversal Video

● Further Reading

   ▪ OWASP Path Traversal

   ▪ OWASP Input Validation Cheat Sheet

 
HighCross-Site Scripting

CWE-79

app.js:1355

12024-08-19 04:08am
Vulnerable Code

app.get('/adzerk/*', (req, res) => {
cors.assertCors(req, res, ['GET'], ['AMP-template-amp-creative']);
const match = /\/(\d+)/.exec(req.path);
if (!match || !match[1]) {
res.status(404);
res.end('Invalid path: ' + req.path);

1 Data Flow/s detected

app.get('/adzerk/*', (req, res) => {

res.end('Invalid path: ' + req.path);

⛑️ Remediation Suggestion

--- original
+++ remediated
@@ -1,0 +1,1 @@
+const escape = require('escape-html');
'use strict';
/**
* @fileoverview Creates an http server to handle static
* files and list directories for use with the amp live server
*/
const argv = require('minimist')(process.argv.slice(2));
const bacon = require('baconipsum');
const bodyParser = require('body-parser');
const cors = require('./amp-cors');
const devDashboard = require('./app-index');
const express = require('express');
const formidable = require('formidable');
const fs = require('fs');
const jsdom = require('jsdom');
const path = require('path');
const upload = require('multer')();
const pc = process;
const autocompleteEmailData = require('./autocomplete-test-data');
const header = require('connect-header');
const runVideoTestBench = require('./app-video-testbench');
const {
getServeMode,
isRtvMode,
replaceUrls,
toInaboxDocument,
} = require('./app-utils');
const {
getVariableRequest,
runVariableSubstitution,
saveVariableRequest,
saveVariables,
} = require('./variable-substitution');
const {
recaptchaFrameRequestHandler,
recaptchaRouter,
} = require('./recaptcha-router');
const {logWithoutTimestamp} = require('../common/logging');
const {log} = require('../common/logging');
const {red} = require('kleur/colors');
const {renderShadowViewer} = require('./shadow-viewer');
/**
* Respond with content received from a URL when SERVE_MODE is "cdn".
* @param {express.Response} res
* @param {string} cdnUrl
* @return {Promise<boolean>}
*/
async function passthroughServeModeCdn(res, cdnUrl) {
if (SERVE_MODE !== 'cdn') {
return false;
}
try {
const response = await fetch(cdnUrl);
res.status(response.status);
res.send(await response.text());
} catch (e) {
log(red('ERROR:'), e);
res.status(500);
res.end();
}
return true;
}
const app = express();
const TEST_SERVER_PORT = argv.port || 8000;
let SERVE_MODE = getServeMode();
app.use(bodyParser.json());
app.use(bodyParser.text());
// Middleware is executed in order, so this must be at the top.
// TODO(#24333): Migrate all server URL handlers to new-server/router and
// deprecate app.js.
app.use(require('./new-server/router'));
app.use(require('./routes/a4a-envelopes'));
app.use('/amp4test', require('./amp4test').app);
app.use('/analytics', require('./routes/analytics'));
app.use('/list/', require('./routes/list'));
app.use('/test', require('./routes/test'));
if (argv.coverage) {
app.use('/coverage', require('istanbul-middleware').createHandler());
}
// Built binaries should be fetchable from other origins, i.e. Storybook.
app.use(header({'Access-Control-Allow-Origin': '*'}));
// Append ?csp=1 to the URL to turn on the CSP header.
// TODO: shall we turn on CSP all the time?
app.use((req, res, next) => {
if (req.query.csp) {
res.set({
'content-security-policy':
"default-src * blob: data:; script-src https://cdn.ampproject.org/rtv/ https://cdn.ampproject.org/v0.js https://cdn.ampproject.org/v0/ https://cdn.ampproject.org/viewer/ http://localhost:8000 https://localhost:8000; object-src 'none'; style-src 'unsafe-inline' https://cdn.ampproject.org/rtv/ https://cdn.materialdesignicons.com https://cloud.typography.com https://fast.fonts.net https://fonts.googleapis.com https://maxcdn.bootstrapcdn.com https://p.typekit.net https://use.fontawesome.com https://use.typekit.net https://cdnjs.cloudflare.com/ajax/libs/font-awesome/; report-uri https://csp-collector.appspot.com/csp/amp",
});
}
next();
});
/**
*
* @param {string} serveMode
* @return {boolean}
*/
function isValidServeMode(serveMode) {
return (
['default', 'minified', 'cdn', 'esm'].includes(serveMode) ||
isRtvMode(serveMode)
);
}
/**
*
* @param {string} serveMode
*/
function setServeMode(serveMode) {
SERVE_MODE = serveMode;
}
app.get('/serve_mode=:mode', (req, res) => {
const newMode = req.params.mode;
if (isValidServeMode(newMode)) {
setServeMode(newMode);
res.send(`<h2>Serve mode changed to ${newMode}</h2>`);
} else {
const info = '<h2>Serve mode ' + newMode + ' is not supported. </h2>';
res.status(400).send(info);
}
});
if (argv._.includes('integration') && !argv.nobuild) {
setServeMode('minified');
}
if (!(argv._.includes('unit') || argv._.includes('integration'))) {
// Dev dashboard routes break test scaffolding since they're global.
devDashboard.installExpressMiddleware(app);
}
// Changes the current serve mode via query param
// e.g. /serve_mode_change?mode=(default|minified|cdn|<RTV_NUMBER>)
// (See ./app-index/settings.js)
app.get('/serve_mode_change', (req, res) => {
const {mode} = req.query;
if (isValidServeMode(mode)) {
setServeMode(mode);
res.json({ok: true});
return;
}
res.status(400).json({ok: false});
});
// Redirects to a proxied document with optional mode through query params.
//
// Mode can be one of:
// - '/', empty string, or unset for an unwrapped doc
// - '/a4a/' for an AMP4ADS wrapper
// - '/a4a-3p/' for a 3P AMP4ADS wrapper
// - '/inabox/' for an AMP inabox wrapper
// - '/shadow/' for a shadow-wrapped document
//
// Examples:
// - /proxy/?url=hello.com 👉 /proxy/s/hello.com
// - /proxy/?url=hello.com?mode=/shadow/ 👉 /shadow/proxy/s/hello.com
// - /proxy/?url=https://hello.com 👉 /proxy/s/hello.com
// - /proxy/?url=https://www.google.com/amp/s/hello.com 👉 /proxy/s/hello.com
// - /proxy/?url=hello.com/canonical 👉 /proxy/s/hello.com/amp
//
// This passthrough is useful to generate the URL from <form> values,
// (See ./app-index/proxy-form.js)
app.get('/proxy', async (req, res, next) => {
const {mode, url} = req.query;
const urlSuffixClearPrefixReStr =
'^https?://((www.)?google.(com?|[a-z]{2}|com?.[a-z]{2}|cat)/amp/s/)?';
const urlSuffix = url.replace(new RegExp(urlSuffixClearPrefixReStr, 'i'), '');
try {
const ampdocUrl = await requestAmphtmlDocUrl(urlSuffix);
const ampdocUrlSuffix = ampdocUrl.replace(/^https?:\/\//, '');
const modePrefix = (mode || '').replace(/\/$/, '');
const proxyUrl = `${modePrefix}/proxy/s/${ampdocUrlSuffix}`;
res.redirect(proxyUrl);
} catch ({message}) {
logWithoutTimestamp(`ERROR: ${message}`);
next();
}
});
/**
* Resolves an AMPHTML URL from a canonical URL. If AMPHTML is canonical, same
* URL is returned.
* @param {string} urlSuffix URL without protocol or google.com/amp/s/...
* @param {string=} protocol 'https' or 'http'. 'https' retries using 'http'.
* @return {!Promise<string>}
*/
async function requestAmphtmlDocUrl(urlSuffix, protocol = 'https') {
const defaultUrl = `${protocol}://${urlSuffix}`;
logWithoutTimestamp(`Fetching URL: ${defaultUrl}`);
const response = await fetch(defaultUrl);
if (!response.ok) {
if (protocol == 'https') {
return requestAmphtmlDocUrl(urlSuffix, 'http');
}
throw new Error(`Status: ${response.status}`);
}
const {window} = new jsdom.JSDOM(await response.text());
const linkRelAmphtml = window.document.querySelector('link[rel=amphtml]');
const amphtmlUrl = linkRelAmphtml && linkRelAmphtml.getAttribute('href');
return amphtmlUrl || defaultUrl;
}
/*
* Intercept Recaptcha frame for,
* integration tests. Using this to mock
* out the recaptcha api.
*/
app.get('/dist.3p/*/recaptcha.*html', recaptchaFrameRequestHandler);
app.use('/recaptcha', recaptchaRouter);
// Deprecate usage of .min.html/.max.html
app.get(
[
'/examples/*.(min|max).html',
'/test/manual/*.(min|max).html',
'/test/fixtures/e2e/*/*.(min|max).html',
],
(req, res) => {
const filePath = req.url;
res.send(generateInfo(filePath));
}
);
app.use('/pwa', (req, res) => {
let file;
let contentType;
if (!req.url || req.path == '/') {
// pwa.html
contentType = 'text/html';
file = '/examples/pwa/pwa.html';
} else if (req.url == '/pwa.js') {
// pwa.js
contentType = 'application/javascript';
file = '/examples/pwa/pwa.js';
} else if (req.url == '/pwa-sw.js') {
// pwa.js
contentType = 'application/javascript';
file = '/examples/pwa/pwa-sw.js';
} else if (req.url == '/ampdoc-shell') {
// pwa-ampdoc-shell.html
contentType = 'text/html';
file = '/examples/pwa/pwa-ampdoc-shell.html';
} else {
// Redirect to the underlying resource.
// TODO(dvoytenko): would be nicer to do forward instead of redirect.
res.writeHead(302, {'Location': req.url});
res.end();
return;
}
res.statusCode = 200;
res.setHeader('Content-Type', contentType);
fs.promises.readFile(pc.cwd() + file).then((file) => {
res.end(file);
});
});
app.use('/api/show', (_req, res) => {
res.json({
showNotification: true,
});
});
app.use('/api/dont-show', (_req, res) => {
res.json({
showNotification: false,
});
});
app.use('/api/echo/query', (req, res) => {
res.json(JSON.parse(req.query.data));
});
app.use('/api/echo/post', (req, res) => {
res.setHeader('Content-Type', 'application/json');
res.end(req.body);
});
app.use('/api/ping', (_req, res) => {
res.status(204).end();
});
app.use('/form/html/post', (req, res) => {
cors.assertCors(req, res, ['POST']);
const form = new formidable.IncomingForm();
form.parse(req, (_err, fields) => {
res.setHeader('Content-Type', 'text/html');
if (fields['email'] == '[email protected]') {
res.statusCode = 500;
res.end(`
<h1 style="color:red;">Sorry ${fields['name']}!</h1>
<p>The email ${fields['email']} is already subscribed!</p>
`);
} else {
res.end(`
<h1>Thanks ${fields['name']}!</h1>
<p>Please make sure to confirm your email ${fields['email']}</p>
`);
}
});
});
app.use('/form/redirect-to/post', (req, res) => {
cors.assertCors(req, res, ['POST'], ['AMP-Redirect-To']);
res.setHeader('AMP-Redirect-To', 'https://google.com');
res.end('{}');
});
app.use('/form/echo-json/post', (req, res) => {
cors.assertCors(req, res, ['POST']);
const form = new formidable.IncomingForm();
const fields = Object.create(null);
form.on('field', function (name, value) {
if (!(name in fields)) {
fields[name] = value;
return;
}
const realName = name;
if (realName in fields) {
if (!Array.isArray(fields[realName])) {
fields[realName] = [fields[realName]];
}
} else {
fields[realName] = [];
}
fields[realName].push(value);
});
form.parse(req, () => {
res.setHeader('Content-Type', 'application/json; charset=utf-8');
if (fields['email'] == '[email protected]') {
res.statusCode = 500;
}
res.end(JSON.stringify(fields));
});
});
app.use('/form/json/poll1', (req, res) => {
cors.assertCors(req, res, ['POST']);
const form = new formidable.IncomingForm();
form.parse(req, () => {
res.setHeader('Content-Type', 'application/json');
res.end(
JSON.stringify({
result: [
{
answer: 'Penguins',
percentage: new Array(77),
},
{
answer: 'Ostriches',
percentage: new Array(8),
},
{
answer: 'Kiwis',
percentage: new Array(14),
},
{
answer: 'Wekas',
percentage: new Array(1),
},
],
})
);
});
});
app.post('/form/json/upload', upload.fields([{name: 'myFile'}]), (req, res) => {
cors.assertCors(req, res, ['POST']);
const myFile = req.files['myFile'];
if (!myFile) {
res.json({message: 'No file data received'});
return;
}
const fileData = myFile[0];
const contents = fileData.buffer.toString();
res.json({message: contents});
});
app.use('/form/search-html/get', (_req, res) => {
res.setHeader('Content-Type', 'text/html');
res.end(`
<h1>Here's results for your search<h1>
<ul>
<li>Result 1</li>
<li>Result 2</li>
<li>Result 3</li>
</ul>
`);
});
app.use('/form/search-json/get', (req, res) => {
cors.assertCors(req, res, ['GET']);
res.json({
term: req.query.term,
additionalFields: req.query.additionalFields,
results: [{title: 'Result 1'}, {title: 'Result 2'}, {title: 'Result 3'}],
});
});
const autocompleteColors = [
'red',
'orange',
'yellow',
'green',
'blue',
'purple',
'pink',
'black',
'white',
];
app.use('/form/autocomplete/query', (req, res) => {
const query = req.query.q;
if (!query) {
res.json({items: autocompleteColors});
} else {
const lowerCaseQuery = query.toLowerCase();
const filtered = autocompleteColors.filter((l) =>
l.toLowerCase().includes(lowerCaseQuery)
);
res.json({items: filtered});
}
});
app.use('/form/autocomplete/error', (_req, res) => {
res.status(500).end();
});
app.use('/form/mention/query', (req, res) => {
const query = req.query.q;
if (!query) {
res.json({items: autocompleteEmailData});
return;
}
const lowerCaseQuery = query.toLowerCase().trim();
const filtered = autocompleteEmailData.filter((l) =>
l.toLowerCase().startsWith(lowerCaseQuery)
);
res.json({items: filtered});
});
app.use('/form/verify-search-json/post', (req, res) => {
cors.assertCors(req, res, ['POST']);
const form = new formidable.IncomingForm();
form.parse(req, (_err, fields) => {
res.setHeader('Content-Type', 'application/json; charset=utf-8');
const errors = [];
if (!fields.phone.match(/^650/)) {
errors.push({name: 'phone', message: 'Phone must start with 650'});
}
if (fields.name !== 'Frank') {
errors.push({name: 'name', message: 'Please set your name to be Frank'});
}
if (fields.error === 'true') {
errors.push({message: 'You asked for an error, you get an error.'});
}
if (fields.city !== 'Mountain View' || fields.zip !== '94043') {
errors.push({
name: 'city',
message: "City doesn't match zip (Mountain View and 94043)",
});
}
if (errors.length === 0) {
res.end(
JSON.stringify({
results: [
{title: 'Result 1'},
{title: 'Result 2'},
{title: 'Result 3'},
],
committed: true,
})
);
} else {
res.statusCode = 400;
res.end(JSON.stringify({verifyErrors: errors}));
}
});
});
/**
* Fetches an AMP document from the AMP proxy and replaces JS
* URLs, so that they point to localhost.
*
* @param {express.Request} req
* @param {express.Response} res
* @param {string} mode
* @return {Promise<void>}
*/
async function proxyToAmpProxy(req, res, mode) {
const url =
'https://cdn.ampproject.org/' +
(req.query['amp_js_v'] ? 'v' : 'c') +
req.url;
logWithoutTimestamp('Fetching URL: ' + url);
const urlResponse = await fetch(url);
let body = await urlResponse.text();
body = body
// Unversion URLs.
.replace(
/https\:\/\/cdn\.ampproject\.org\/rtv\/\d+\//g,
'https://cdn.ampproject.org/'
)
// <base> href pointing to the proxy, so that images, etc. still work.
.replace('<head>', '<head><base href="https://cdn.ampproject.org/">');
const inabox = req.query['inabox'];
const urlPrefix = getUrlPrefix(req);
if (req.query['mraid']) {
body = body
.replace(
'</head>',
'<script async host-service="amp-mraid" src="https://cdn.ampproject.org/v0/amp-mraid-0.1.js">' +
'</script>' +
'</head>'
)
// Change cdnUrl from the default so amp-mraid requests the (mock)
// mraid.js from the local server. In a real environment this doesn't
// matter as the local environment would intercept this request.
.replace(
'<head>',
' <head>' +
' <script>' +
' window.AMP_CONFIG = {' +
` cdnUrl: "${urlPrefix}",` +
' };' +
' </script>'
);
}
if (inabox) {
body = toInaboxDocument(body);
// Allow CORS requests for A4A.
const origin = req.headers.origin || urlPrefix;
cors.enableCors(req, res, origin);
}
body = replaceUrls(mode, body, urlPrefix);
res.status(urlResponse.status).send(body);
}
let itemCtr = 2;
const doctype = '<!doctype html>\n';
const liveListDocs = Object.create(null);
app.use('/examples/live-list-update(-reverse)?.amp.html', (req, res, next) => {
const mode = SERVE_MODE;
let liveListDoc = liveListDocs[req.baseUrl];
if (mode != 'minified' && mode != 'default') {
// Only handle compile(prev min)/default (prev max) mode
next();
return;
}
// When we already have state in memory and user refreshes page, we flush
// the dom we maintain on the server.
if (!('amp_latest_update_time' in req.query) && liveListDoc) {
let outerHTML = liveListDoc.documentElement./*OK*/ outerHTML;
outerHTML = replaceUrls(mode, outerHTML);
res.send(`${doctype}${outerHTML}`);
return;
}
if (!liveListDoc) {
const liveListUpdateFullPath = `${pc.cwd()}${req.baseUrl}`;
logWithoutTimestamp('liveListUpdateFullPath', liveListUpdateFullPath);
const liveListFile = fs.readFileSync(liveListUpdateFullPath);
liveListDoc = liveListDocs[req.baseUrl] = new jsdom.JSDOM(
liveListFile
).window.document;
liveListDoc.ctr = 0;
}
const liveList = liveListDoc.querySelector('#my-live-list');
const perPage = Number(liveList.getAttribute('data-max-items-per-page'));
const items = liveList.querySelector('[items]');
const pagination = liveListDoc.querySelector('#my-live-list [pagination]');
const item1 = liveList.querySelector('#list-item-1');
if (liveListDoc.ctr != 0) {
if (Math.random() < 0.8) {
// Always run a replace on the first item
liveListReplace(item1);
if (Math.random() < 0.5) {
liveListTombstone(liveList);
}
if (Math.random() < 0.8) {
liveListInsert(liveList, item1);
}
pagination.textContent = '';
const liveChildren = [].slice
.call(items.children)
.filter((x) => !x.hasAttribute('data-tombstone'));
const pageCount = Math.ceil(liveChildren.length / perPage);
const pageListItems = Array.apply(null, Array(pageCount))
.map((_, i) => `<li>${i + 1}</li>`)
.join('');
const newPagination =
'<nav aria-label="amp live list pagination">' +
`<ul class="pagination">${pageListItems}</ul>` +
'</nav>';
pagination./*OK*/ innerHTML = newPagination;
} else {
// Sometimes we want an empty response to simulate no changes.
res.send(`${doctype}<html></html>`);
return;
}
}
let outerHTML = liveListDoc.documentElement./*OK*/ outerHTML;
outerHTML = replaceUrls(mode, outerHTML);
liveListDoc.ctr++;
res.send(`${doctype}${outerHTML}`);
});
/**
* @param {Element} item
*/
function liveListReplace(item) {
item.setAttribute('data-update-time', Date.now().toString());
const itemContents = item.querySelectorAll('.content');
itemContents[0].textContent = Math.floor(Math.random() * 10).toString();
itemContents[1].textContent = Math.floor(Math.random() * 10).toString();
}
/**
* @param {Element} liveList
* @param {Element} node
*/
function liveListInsert(liveList, node) {
const iterCount = Math.floor(Math.random() * 2) + 1;
logWithoutTimestamp(`inserting ${iterCount} item(s)`);
for (let i = 0; i < iterCount; i++) {
/**
* TODO(#28387) this type cast may be hiding a bug.
* @type {Element}
*/
const child = /** @type {*} */ (node.cloneNode(true));
child.setAttribute('id', `list-item-${itemCtr++}`);
child.setAttribute('data-sort-time', Date.now().toString());
liveList.querySelector('[items]')?.appendChild(child);
}
}
/**
* @param {Element} liveList
*/
function liveListTombstone(liveList) {
const tombstoneId = Math.floor(Math.random() * itemCtr);
logWithoutTimestamp(`trying to tombstone #list-item-${tombstoneId}`);
// We can tombstone any list item except item-1 since we always do a
// replace example on item-1.
if (tombstoneId != 1) {
const item = liveList./*OK*/ querySelector(`#list-item-${tombstoneId}`);
if (item) {
item.setAttribute('data-tombstone', '');
}
}
}
/**
* Generate a random number between min and max
* Value is inclusive of both min and max values.
*
* @param {number} min
* @param {number} max
* @return {number}
*/
function range(min, max) {
const values = Array.apply(null, new Array(max - min + 1)).map(
(_, i) => min + i
);
return values[Math.round(Math.random() * (max - min))];
}
/**
* Returns the result of a coin flip, true or false
*
* @return {boolean}
*/
function flip() {
return !!Math.floor(Math.random() * 2);
}
/**
* @return {string}
*/
function getLiveBlogItem() {
const now = Date.now();
// Generate a 3 to 7 worded headline
const headline = bacon(range(3, 7));
const numOfParagraphs = range(1, 2);
const body = Array.apply(null, new Array(numOfParagraphs))
.map(() => {
return `<p>${bacon(range(50, 90))}</p>`;
})
.join('\n');
const img = `<amp-img
src="${
flip()
? 'https://placekitten.com/300/350'
: 'https://baconmockup.com/300/350'
}"
layout="responsive"
height="300" width="350">
</amp-img>`;
return `<!doctype html>
<html amp><body>
<amp-live-list id="live-blog-1">
<div items>
<div id="live-blog-item-${now}" data-sort-time="${now}">
<h3 class="headline">
<a href="#live-blog-item-${now}">${headline}</a>
</h3>
<div class="author">
<div class="byline">
<p>
by <span itemscope itemtype="http://schema.org/Person"
itemprop="author"><b>Lorem Ipsum</b>
<a class="mailto" href="mailto:lorem.ipsum@">
lorem.ipsum@</a></span>
</p>
<p class="brand">PublisherName News Reporter<p>
<p><span itemscope itemtype="http://schema.org/Date"
itemprop="Date">
${new Date(now).toString().replace(/ GMT.*$/, '')}
<span></p>
</div>
</div>
<div class="article-body">${body}</div>
${img}
<div class="social-box">
<amp-social-share type="facebook"
data-param-text="Hello world"
data-param-href="https://example.test/?ref=URL"
data-param-app_id="145634995501895"></amp-social-share>
<amp-social-share type="twitter"></amp-social-share>
</div>
</div>
</div>
</amp-live-list></body></html>`;
}
/**
* @return {string}
*/
function getLiveBlogItemWithBindAttributes() {
const now = Date.now();
// Generate a 3 to 7 worded headline
const numOfParagraphs = range(1, 2);
const body = Array.apply(null, new Array(numOfParagraphs))
.map(() => {
return `<p>${bacon(range(50, 90))}</p>`;
})
.join('\n');
return `<!doctype html>
<html amp><body>
<amp-live-list id="live-blog-1">
<div items>
<div id="live-blog-item-${now}" data-sort-time="${now}">
<div class="article-body">
${body}
<p> As you can see, bacon is far superior to
<b><span [text]='favoriteFood'>everything!</span></b>!</p>
</div>
</div>
</div>
</amp-live-list></body></html>`;
}
app.use(
'/examples/live-blog(-non-floating-button)?.amp.html',
(req, res, next) => {
if ('amp_latest_update_time' in req.query) {
res.setHeader('Content-Type', 'text/html');
res.end(getLiveBlogItem());
return;
}
next();
}
);
app.use('/examples/bind/live-list.amp.html', (req, res, next) => {
if ('amp_latest_update_time' in req.query) {
res.setHeader('Content-Type', 'text/html');
res.end(getLiveBlogItemWithBindAttributes());
return;
}
next();
});
app.use('/impression-proxy/', (req, res) => {
cors.assertCors(req, res, ['GET']);
// Fake response with the following optional fields:
// location: The Url the that server would have sent redirect to w/o ALP
// tracking_url: URL that should be requested to track click
// gclid: The conversion tracking value
const body = {
'location': 'localhost:8000/examples/?gclid=1234&foo=bar&example=123',
'tracking_url': 'tracking_url',
'gclid': '1234',
};
res.send(body);
// Or fake response with status 204 if viewer replaceUrl is provided
});
/**
* Acts in a similar fashion to /serve_mode_change. Saves
* analytics requests via /run-variable-substitution, and
* then returns the encoded/substituted/replaced request
* via /get-variable-request.
*/
// Saves the variables input to be used in run-variable-substitution
app.get('/save-variables', saveVariables);
// Creates an iframe with amp-analytics. Analytics request
// uses save-variable-request as its endpoint.
app.get('/run-variable-substitution', runVariableSubstitution);
// Saves the analytics request to the dev server.
app.get('/save-variable-request', saveVariableRequest);
// Returns the saved analytics request.
app.get('/get-variable-request', getVariableRequest);
let forcePromptOnNext = false;
app.post('/get-consent-v1/', (req, res) => {
cors.assertCors(req, res, ['POST']);
const body = {
'promptIfUnknown': true,
'purposeConsentRequired': ['purpose-foo', 'purpose-bar'],
'forcePromptOnNext': forcePromptOnNext,
'sharedData': {
'tfua': true,
'coppa': true,
},
};
res.json(body);
});
app.get('/get-consent-v1-set/', (req, res) => {
cors.assertCors(req, res, ['GET']);
const value = req.query['forcePromptOnNext'];
if (value == 'false' || value == '0') {
forcePromptOnNext = false;
} else {
forcePromptOnNext = true;
}
res.json({});
res.end();
});
app.post('/get-consent-no-prompt/', (req, res) => {
cors.assertCors(req, res, ['POST']);
const body = {};
res.json(body);
});
app.post('/check-consent', (req, res) => {
cors.assertCors(req, res, ['POST']);
const response = {
'consentRequired': req.query.consentRequired === 'true',
'consentStateValue': req.query.consentStateValue,
'consentString': req.query.consentString,
'expireCache': req.query.expireCache === 'true',
};
if (req.query.consentMetadata) {
response['consentMetadata'] = JSON.parse(
req.query.consentMetadata.replace(/'/g, '"')
);
}
res.json(response);
});
// Proxy with local JS.
// Example:
// http://localhost:8000/proxy/s/www.washingtonpost.com/amphtml/news/post-politics/wp/2016/02/21/bernie-sanders-says-lower-turnout-contributed-to-his-nevada-loss-to-hillary-clinton/
app.use('/proxy/', (req, res) => proxyToAmpProxy(req, res, SERVE_MODE));
// Nest the response in an iframe.
// Example:
// http://localhost:8000/iframe/examples/ads.amp.html
app.get('/iframe/*', (req, res) => {
// Returns an html blob with an iframe pointing to the url after /iframe/.
res.send(`<!doctype html>
<html style="width:100%; height:100%;">
<body style="width:98%; height:98%;">
<iframe src="${req.url.substr(7)}"
style="width:100%; height:100%;">
</iframe>
</body>
</html>`);
});
app.get('/a4a_template/*', (req, res) => {
cors.assertCors(req, res, ['GET']);
const match = /^\/a4a_template\/([a-z-]+)\/(\d+)$/.exec(req.path);
if (!match) {
res.status(404);
res.end('Invalid path: ' + req.path);
return;
}
const filePath =
`${pc.cwd()}/extensions/amp-ad-network-${match[1]}-impl/` +
`0.1/data/${match[2]}.template`;
fs.promises
.readFile(filePath)
.then((file) => {
res.setHeader('Content-Type', 'application/json');
res.setHeader('AMP-template-amp-creative', 'amp-mustache');
res.end(file);
})
.catch(() => {
res.status(404);
res.end('Not found: ' + filePath);
});
});
// Returns a document that echoes any post messages received from parent.
// An optional `message` query param can be appended for an initial post
// message sent on document load.
// Example:
// http://localhost:8000/iframe-echo-message?message=${payload}
app.get('/iframe-echo-message', (req, res) => {
const {message} = req.query;
res.send(
`<!doctype html>
<body style="background-color: yellow">
<script>
if (${message}) {
echoMessage(${message});
}
window.addEventListener('message', function(event) {
echoMessage(event.data);
});
function echoMessage(message) {
parent.postMessage(message, '*');
}
</script>
</body>
</html>`
);
});
/**
* Append ?sleep=5 to any included JS file in examples to emulate delay in
* loading that file. This allows you to test issues with your extension being
* late to load and testing user interaction with your element before your code
* loads.
*
* Example delay loading amp-form script by 5 seconds:
* <script async custom-element="amp-form"
* src="https://cdn.ampproject.org/v0/amp-form-0.1.js?sleep=5"></script>
*/
app.use(['/dist/v0/amp-*.(m?js)', '/dist/amp*.(m?js)'], (req, _res, next) => {
const sleep = parseInt(req.query.sleep || 0, 10) * 1000;
setTimeout(next, sleep);
});
/**
* Disable caching for extensions if the --no_caching_extensions flag is used.
*/
app.get(['/dist/v0/amp-*.(m?js)'], (_req, res, next) => {
if (argv.no_caching_extensions) {
res.header('Cache-Control', 'no-store');
}
next();
});
/**
* Video testbench endpoint
*/
app.get('/test/manual/amp-video.amp.html', runVideoTestBench);
app.get(
[
'/examples/(**/)?*.html',
'/test/manual/(**/)?*.html',
'/test/fixtures/e2e/(**/)?*.html',
'/test/fixtures/performance/(**/)?*.html',
],
(req, res, next) => {
const filePath = req.path;
const mode = SERVE_MODE;
const inabox = req.query['inabox'];
const stream = Number(req.query['stream']);
const componentVersion = req.query['componentVersion'];
const urlPrefix = getUrlPrefix(req);
fs.promises
.readFile(pc.cwd() + filePath, 'utf8')
.then((file) => {
if (req.query['amp_js_v']) {
file = addViewerIntegrationScript(req.query['amp_js_v'], file);
}
if (req.query['mraid']) {
file = file
.replace(
'</head>',
'<script async host-service="amp-mraid" src="https://cdn.ampproject.org/v0/amp-mraid-0.1.js">' +
'</script>' +
'</head>'
)
.replace(
'<head>',
' <head>' +
' <script>' +
' window.AMP_CONFIG = {' +
` cdnUrl: "${urlPrefix}",` +
' };' +
' </script>'
);
}
file = file.replace(/__TEST_SERVER_PORT__/g, TEST_SERVER_PORT);
if (componentVersion) {
file = file.replace(/-latest.js/g, `-${componentVersion}.js`);
}
if (inabox) {
file = toInaboxDocument(file);
// Allow CORS requests for A4A.
if (req.headers.origin) {
cors.enableCors(req, res, req.headers.origin);
}
}
file = replaceUrls(mode, file);
const ampExperimentsOptIn = req.query['exp'];
if (ampExperimentsOptIn) {
file = file.replace(
'<head>',
`<head><meta name="amp-experiments-opt-in" content="${ampExperimentsOptIn}">`
);
}
// Extract amp-ad for the given 'type' specified in URL query.
if (req.path.indexOf('/examples/ads.amp.html') == 0 && req.query.type) {
const ads =
file.match(
elementExtractor('(amp-ad|amp-embed)', req.query.type)
) ?? [];
file = file.replace(
/<body>[\s\S]+<\/body>/m,
'<body>' + ads.join('') + '</body>'
);
}
// Extract amp-analytics for the given 'type' specified in URL query.
if (
req.path.indexOf('/examples/analytics-vendors.amp.html') == 0 &&
req.query.type
) {
const analytics =
file.match(elementExtractor('amp-analytics', req.query.type)) ?? [];
file = file.replace(
/<div id="container">[\s\S]+<\/div>/m,
'<div id="container">' + analytics.join('') + '</div>'
);
}
// Extract amp-consent for the given 'type' specified in URL query.
if (
req.path.indexOf('/examples/amp-consent/cmp-vendors.amp.html') == 0 &&
req.query.type
) {
const consent =
file.match(elementExtractor('amp-consent', req.query.type)) ?? [];
file = file.replace(
/<div id="container">[\s\S]+<\/div>/m,
'<div id="container">' + consent.join('') + '</div>'
);
}
if (stream > 0) {
res.writeHead(200, {'Content-Type': 'text/html'});
let pos = 0;
const writeChunk = function () {
const chunk = file.substring(
pos,
Math.min(pos + stream, file.length)
);
res.write(chunk);
pos += stream;
if (pos < file.length) {
setTimeout(writeChunk, 500);
} else {
res.end();
}
};
writeChunk();
} else {
res.send(file);
}
})
.catch(() => {
next();
});
}
);
/**
* @param {string} string
* @return {string}
*/
function escapeRegExp(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
/**
* @param {string} tagName
* @param {string} type
* @return {RegExp}
*/
function elementExtractor(tagName, type) {
type = escapeRegExp(type);
return new RegExp(
`<${tagName}[\\s][^>]*['"]${type}['"][^>]*>([\\s\\S]+?)</${tagName}>`,
'gm'
);
}
// Data for example: http://localhost:8000/examples/bind/xhr.amp.html
app.use('/bind/form/get', (req, res) => {
cors.assertCors(req, res, ['GET']);
res.json({
bindXhrResult: 'I was fetched from the server!',
});
});
// Data for example: http://localhost:8000/examples/bind/ecommerce.amp.html
app.use('/bind/ecommerce/sizes', (req, res) => {
cors.assertCors(req, res, ['GET']);
setTimeout(() => {
const prices = {
'0': {
'sizes': {
'XS': 8.99,
'S': 9.99,
},
},
'1': {
'sizes': {
'S': 10.99,
'M': 12.99,
'L': 14.99,
},
},
'2': {
'sizes': {
'L': 11.99,
'XL': 13.99,
},
},
'3': {
'sizes': {
'M': 7.99,
'L': 9.99,
'XL': 11.99,
},
},
'4': {
'sizes': {
'XS': 8.99,
'S': 10.99,
'L': 15.99,
},
},
'5': {
'sizes': {
'S': 8.99,
'L': 14.99,
'XL': 11.99,
},
},
'6': {
'sizes': {
'XS': 8.99,
'S': 9.99,
'M': 12.99,
},
},
'7': {
'sizes': {
'M': 10.99,
'L': 11.99,
},
},
};
const object = {};
object[req.query.shirt] = prices[req.query.shirt];
res.json(object);
}, 1000); // Simulate network delay.
});
/**
* Simulates a publisher's metering state store.
* (amp-subscriptions)
* @type {{[ampReaderId: string]: {}}}
*/
const meteringStateStore = {};
// Simulate a publisher's entitlements API.
// (amp-subscriptions)
app.use('/subscription/:id/entitlements', (req, res) => {
cors.assertCors(req, res, ['GET']);
// Create entitlements response.
const source = 'local' + req.params.id;
const granted = req.params.id > 0;
const grantReason = granted ? 'SUBSCRIBER' : 'NOT_SUBSCRIBER';
const decryptedDocumentKey = decryptDocumentKey(req.query.crypt);
const response = {
source,
granted,
grantReason,
data: {
login: true,
},
decryptedDocumentKey,
};
// Store metering state, if possible.
const ampReaderId = req.query.rid;
if (ampReaderId && req.query.meteringState) {
// Parse metering state from encoded Base64 string.
const encodedMeteringState = req.query.meteringState;
const decodedMeteringState = Buffer.from(
encodedMeteringState,
'base64'
).toString();
const meteringState = JSON.parse(decodedMeteringState);
// Store metering state.
meteringStateStore[ampReaderId] = meteringState;
}
// Add metering state to response, if possible.
if (meteringStateStore[ampReaderId]) {
response.metering = {
state: meteringStateStore[ampReaderId],
};
}
res.json(response);
});
// Simulate a publisher's SKU map API.
// (amp-subscriptions)
app.use('/subscriptions/skumap', (req, res) => {
cors.assertCors(req, res, ['GET']);
res.json({
'subscribe.google.com': {
'subscribeButtonSimple': {
'sku': 'basic',
},
'subscribeButtonCarousel': {
'carouselOptions': {
'skus': ['basic', 'premium_monthly'],
},
},
},
});
});
// Simulate a publisher's pingback API.
// (amp-subscriptions)
app.use('/subscription/pingback', (req, res) => {
cors.assertCors(req, res, ['POST']);
res.json({
done: true,
});
});
/*
Simulate a publisher's account registration API.
The `amp-subscriptions-google` extension sends this API a POST request.
The request body looks like:
{
"googleSignInDetails": {
// This signed JWT contains information from Google Sign-In
"idToken": "...JWT from Google Sign-In...",
// Some useful fields from the `idToken`, pre-parsed for convenience
"name": "Jane Smith",
"givenName": "Jane",
"familyName": "Smith",
"imageUrl": "https://imageurl",
"email": "[email protected]"
},
// Associate this ID with the registration. Use it to look up metering state
// for future entitlements requests
// https://github.com/ampproject/amphtml/blob/main/extensions/amp-subscriptions/amp-subscriptions.md#combining-the-amp-reader-id-with-publisher-cookies
"ampReaderId": "amp-s0m31d3nt1f13r"
}
(amp-subscriptions-google)
*/
app.use('/subscription/register', (req, res) => {
cors.assertCors(req, res, ['POST']);
// Generate a new ID for this metering state.
const meteringStateId = 'ppid' + Math.round(Math.random() * 99999999);
// Define registration timestamp.
//
// For demo purposes, set timestamp to 30 seconds ago.
// This causes Metering Toast to show immediately,
// which helps engineers test metering.
const registrationTimestamp = Math.round(Date.now() / 1000) - 30000;
// Store metering state.
//
// For demo purposes, just save this in memory.
// Production systems should persist this.
meteringStateStore[req.body.ampReaderId] = {
id: meteringStateId,
standardAttributes: {
// eslint-disable-next-line local/camelcase
registered_user: {
timestamp: registrationTimestamp, // In seconds.
},
},
};
res.json({
metering: {
state: meteringStateStore[req.body.ampReaderId],
},
});
});
// Simulated adzerk ad server and AMP cache CDN.
app.get('/adzerk/*', (req, res) => {
cors.assertCors(req, res, ['GET'], ['AMP-template-amp-creative']);
const match = /\/(\d+)/.exec(req.path);
if (!match || !match[1]) {
res.status(404);
@@ -1355,1 +1356,1 @@
- res.end('Invalid path: ' + req.path);
+ res.end('Invalid path: ' + escape(req.path));
return;
}
const filePath =
pc.cwd() + '/extensions/amp-ad-network-adzerk-impl/0.1/data/' + match[1];
fs.promises
.readFile(filePath)
.then((file) => {
res.setHeader('Content-Type', 'application/json');
res.setHeader('AMP-Ad-Template-Extension', 'amp-mustache');
res.setHeader('AMP-Ad-Response-Type', 'template');
res.end(file);
})
.catch(() => {
res.status(404);
res.end('Not found: ' + filePath);
});
});
app.get('/dist/*.mjs', (req, res, next) => {
// Allow CORS access control explicitly for mjs files
cors.enableCors(req, res);
next();
});
/*
* Serve extension scripts and their source maps.
*/
app.get(
['/dist/rtv/*/v0/*.(m?js)', '/dist/rtv/*/v0/*.(m?js).map'],
async (req, res, next) => {
const mode = SERVE_MODE;
const fileName = path.basename(req.path).replace('.max.', '.');
let filePath = 'https://cdn.ampproject.org/v0/' + fileName;
if (await passthroughServeModeCdn(res, filePath)) {
return;
}
const isJsMap = filePath.endsWith('.map');
if (isJsMap) {
filePath = filePath.replace(/\.(m?js)\.map$/, '.$1');
}
filePath = replaceUrls(mode, filePath);
req.url = filePath + (isJsMap ? '.map' : '');
next();
}
);
/**
* Handle amp-story translation file requests with an rtv path.
* We need to make sure we only handle the amp-story requests since this
* can affect other tests with json requests.
*/
app.get('/dist/rtv/*/v0/amp-story*.json', async (req, _res, next) => {
const fileName = path.basename(req.path);
let filePath = 'https://cdn.ampproject.org/v0/' + fileName;
filePath = replaceUrls(SERVE_MODE, filePath);
req.url = filePath;
next();
});
if (argv.coverage === 'live') {
app.get('/dist/amp.js', async (req, res) => {
const ampJs = await fs.promises.readFile(`${pc.cwd()}${req.path}`);
res.setHeader('Content-Type', 'text/javascript');
res.setHeader('Access-Control-Allow-Origin', '*');
// Append an unload handler that reports coverage information each time you
// leave a page.
res.end(`${ampJs};
window.addEventListener('beforeunload', (evt) => {
const COV_REPORT_URL = 'http://localhost:${TEST_SERVER_PORT}/coverage/client';
console.info('POSTing code coverage to', COV_REPORT_URL);
const xhr = new XMLHttpRequest();
xhr.open('POST', COV_REPORT_URL, true);
xhr.setRequestHeader('Content-type', 'application/json');
xhr.send(JSON.stringify(window.__coverage__));
// Required by Chrome
evt.returnValue = '';
return null;
});`);
});
}
app.get('/dist/ww.(m?js)', async (req, res, next) => {
// Special case for entry point script url. Use minified for testing
const mode = SERVE_MODE;
const fileName = path.basename(req.path);
if (await passthroughServeModeCdn(res, fileName)) {
return;
}
if (mode == 'default') {
req.url = req.url.replace(/\.(m?js)$/, '.max.$1');
}
next();
});
app.get('/dist/iframe-transport-client-lib.(m?js)', (req, _res, next) => {
req.url = req.url.replace(/dist/, 'dist.3p/current');
next();
});
app.get('/dist/amp-inabox-host.(m?js)', (req, _res, next) => {
const mode = SERVE_MODE;
if (mode != 'default') {
req.url = req.url.replace('amp-inabox-host', 'amp4ads-host-v0');
}
next();
});
app.get('/mraid.js', (req, _res, next) => {
req.url = req.url.replace('mraid.js', 'examples/mraid/mraid.js');
next();
});
/**
* Shadow viewer. Fetches shadow runtime from cdn by default.
* Setting the param useLocal=1 will load the runtime from the local build.
*/
app.use('/shadow/', (req, res) => {
const {url} = req;
const isProxyUrl = /^\/proxy\//.test(url);
const baseHref = isProxyUrl
? 'https://cdn.ampproject.org/'
: `${path.dirname(url)}/`;
const viewerHtml = renderShadowViewer({
src: '//' + req.hostname + '/' + req.url.replace(/^\//, ''),
baseHref,
});
if (!req.query.useLocal) {
res.end(viewerHtml);
return;
}
res.end(replaceUrls(SERVE_MODE, viewerHtml));
});
app.use('/mraid/', (req, res) => {
res.redirect(req.url + '?inabox=1&mraid=1');
});
/**
* @param {string} ampJsVersionString
* @param {string} file
* @return {string}
*/
function addViewerIntegrationScript(ampJsVersionString, file) {
const ampJsVersion = parseFloat(ampJsVersionString);
if (!ampJsVersion) {
return file;
}
let viewerScript;
// eslint-disable-next-line local/no-es2015-number-props
if (Number.isInteger(ampJsVersion)) {
// Viewer integration script from gws, such as
// https://cdn.ampproject.org/viewer/google/v7.js
viewerScript =
'<script async src="https://cdn.ampproject.org/viewer/google/v' +
ampJsVersion +
'.js"></script>';
} else {
// Viewer integration script from runtime, such as
// https://cdn.ampproject.org/v0/amp-viewer-integration-0.1.js
viewerScript =
'<script async ' +
'src="https://cdn.ampproject.org/v0/amp-viewer-integration-' +
ampJsVersion +
'.js" data-amp-report-test="viewer-integr.js"></script>';
}
file = file.replace('</head>', viewerScript + '</head>');
return file;
}
/**
* @param {express.Request} req
* @return {string}
*/
function getUrlPrefix(req) {
return req.protocol + '://' + req.headers.host;
}
/**
* @param {string} filePath
* @return {string}
*/
function generateInfo(filePath) {
const mode = SERVE_MODE;
filePath = filePath.substr(0, filePath.length - 9) + '.html';
return (
'<h2>Please note that .min/.max is no longer supported</h2>' +
'<h3>Current serving mode is ' +
mode +
'</h3>' +
'<h3>Please go to <a href= ' +
filePath +
'>Unversioned Link</a> to view the page<h3>' +
'<h3></h3>' +
'<h3><a href = /serve_mode=default>' +
'Change to DEFAULT mode (unminified JS)</a></h3>' +
'<h3><a href = /serve_mode=minified>' +
'Change to COMPILED mode (minified JS)</a></h3>' +
'<h3><a href = /serve_mode=cdn>' +
'Change to CDN mode (prod JS)</a></h3>'
);
}
/**
* @param {string} encryptedDocumentKey
* @return {?string}
*/
function decryptDocumentKey(encryptedDocumentKey) {
if (!encryptedDocumentKey) {
return null;
}
const cryptoStart = 'ENCRYPT(';
if (!encryptedDocumentKey.includes(cryptoStart, 0)) {
return null;
}
let jsonString = encryptedDocumentKey.replace(cryptoStart, '');
jsonString = jsonString.substring(0, jsonString.length - 1);
const parsedJson = JSON.parse(jsonString);
if (!parsedJson) {
return null;
}
return parsedJson.key;
}
// serve local vendor config JSON files
app.use(
'(/dist)?/rtv/*/v0/analytics-vendors/:vendor.json',
async (req, res) => {
const {vendor} = req.params;
const serveMode = SERVE_MODE;
const cdnUrl = `https://cdn.ampproject.org/v0/analytics-vendors/${vendor}.json`;
if (await passthroughServeModeCdn(res, cdnUrl)) {
return;
}
const max = serveMode === 'default' ? '.max' : '';
const localPath = `${pc.cwd()}/dist/v0/analytics-vendors/${vendor}${max}.json`;
try {
const file = await fs.promises.readFile(localPath);
res.setHeader('Content-Type', 'application/json');
res.end(file);
} catch (_) {
res.status(404);
res.end('Not found: ' + localPath);
}
}
);
module.exports = app;

  • Create Pull Request

Remediation feedback:

  • 👍 Like
  • 👎 Dislike
Secure Code Warrior Training Material

● Training

   ▪ Secure Code Warrior Cross-Site Scripting Training

● Videos

   ▪ Secure Code Warrior Cross-Site Scripting Video

 
HighCross-Site Scripting

CWE-79

app.js:901

12024-08-19 04:08am
Vulnerable Code

// Nest the response in an iframe.
// Example:
// http://localhost:8000/iframe/examples/ads.amp.html
app.get('/iframe/*', (req, res) => {
// Returns an html blob with an iframe pointing to the url after /iframe/.
res.send(`<!doctype html>

1 Data Flow/s detected

app.get('/iframe/*', (req, res) => {

<iframe src="${req.url.substr(7)}"

res.send(`<!doctype html>

⛑️ Remediation Suggestion

--- original
+++ remediated
@@ -1,0 +1,1 @@
+const escape = require('escape-html');
'use strict';
/**
* @fileoverview Creates an http server to handle static
* files and list directories for use with the amp live server
*/
const argv = require('minimist')(process.argv.slice(2));
const bacon = require('baconipsum');
const bodyParser = require('body-parser');
const cors = require('./amp-cors');
const devDashboard = require('./app-index');
const express = require('express');
const formidable = require('formidable');
const fs = require('fs');
const jsdom = require('jsdom');
const path = require('path');
const upload = require('multer')();
const pc = process;
const autocompleteEmailData = require('./autocomplete-test-data');
const header = require('connect-header');
const runVideoTestBench = require('./app-video-testbench');
const {
getServeMode,
isRtvMode,
replaceUrls,
toInaboxDocument,
} = require('./app-utils');
const {
getVariableRequest,
runVariableSubstitution,
saveVariableRequest,
saveVariables,
} = require('./variable-substitution');
const {
recaptchaFrameRequestHandler,
recaptchaRouter,
} = require('./recaptcha-router');
const {logWithoutTimestamp} = require('../common/logging');
const {log} = require('../common/logging');
const {red} = require('kleur/colors');
const {renderShadowViewer} = require('./shadow-viewer');
/**
* Respond with content received from a URL when SERVE_MODE is "cdn".
* @param {express.Response} res
* @param {string} cdnUrl
* @return {Promise<boolean>}
*/
async function passthroughServeModeCdn(res, cdnUrl) {
if (SERVE_MODE !== 'cdn') {
return false;
}
try {
const response = await fetch(cdnUrl);
res.status(response.status);
res.send(await response.text());
} catch (e) {
log(red('ERROR:'), e);
res.status(500);
res.end();
}
return true;
}
const app = express();
const TEST_SERVER_PORT = argv.port || 8000;
let SERVE_MODE = getServeMode();
app.use(bodyParser.json());
app.use(bodyParser.text());
// Middleware is executed in order, so this must be at the top.
// TODO(#24333): Migrate all server URL handlers to new-server/router and
// deprecate app.js.
app.use(require('./new-server/router'));
app.use(require('./routes/a4a-envelopes'));
app.use('/amp4test', require('./amp4test').app);
app.use('/analytics', require('./routes/analytics'));
app.use('/list/', require('./routes/list'));
app.use('/test', require('./routes/test'));
if (argv.coverage) {
app.use('/coverage', require('istanbul-middleware').createHandler());
}
// Built binaries should be fetchable from other origins, i.e. Storybook.
app.use(header({'Access-Control-Allow-Origin': '*'}));
// Append ?csp=1 to the URL to turn on the CSP header.
// TODO: shall we turn on CSP all the time?
app.use((req, res, next) => {
if (req.query.csp) {
res.set({
'content-security-policy':
"default-src * blob: data:; script-src https://cdn.ampproject.org/rtv/ https://cdn.ampproject.org/v0.js https://cdn.ampproject.org/v0/ https://cdn.ampproject.org/viewer/ http://localhost:8000 https://localhost:8000; object-src 'none'; style-src 'unsafe-inline' https://cdn.ampproject.org/rtv/ https://cdn.materialdesignicons.com https://cloud.typography.com https://fast.fonts.net https://fonts.googleapis.com https://maxcdn.bootstrapcdn.com https://p.typekit.net https://use.fontawesome.com https://use.typekit.net https://cdnjs.cloudflare.com/ajax/libs/font-awesome/; report-uri https://csp-collector.appspot.com/csp/amp",
});
}
next();
});
/**
*
* @param {string} serveMode
* @return {boolean}
*/
function isValidServeMode(serveMode) {
return (
['default', 'minified', 'cdn', 'esm'].includes(serveMode) ||
isRtvMode(serveMode)
);
}
/**
*
* @param {string} serveMode
*/
function setServeMode(serveMode) {
SERVE_MODE = serveMode;
}
app.get('/serve_mode=:mode', (req, res) => {
const newMode = req.params.mode;
if (isValidServeMode(newMode)) {
setServeMode(newMode);
res.send(`<h2>Serve mode changed to ${newMode}</h2>`);
} else {
const info = '<h2>Serve mode ' + newMode + ' is not supported. </h2>';
res.status(400).send(info);
}
});
if (argv._.includes('integration') && !argv.nobuild) {
setServeMode('minified');
}
if (!(argv._.includes('unit') || argv._.includes('integration'))) {
// Dev dashboard routes break test scaffolding since they're global.
devDashboard.installExpressMiddleware(app);
}
// Changes the current serve mode via query param
// e.g. /serve_mode_change?mode=(default|minified|cdn|<RTV_NUMBER>)
// (See ./app-index/settings.js)
app.get('/serve_mode_change', (req, res) => {
const {mode} = req.query;
if (isValidServeMode(mode)) {
setServeMode(mode);
res.json({ok: true});
return;
}
res.status(400).json({ok: false});
});
// Redirects to a proxied document with optional mode through query params.
//
// Mode can be one of:
// - '/', empty string, or unset for an unwrapped doc
// - '/a4a/' for an AMP4ADS wrapper
// - '/a4a-3p/' for a 3P AMP4ADS wrapper
// - '/inabox/' for an AMP inabox wrapper
// - '/shadow/' for a shadow-wrapped document
//
// Examples:
// - /proxy/?url=hello.com 👉 /proxy/s/hello.com
// - /proxy/?url=hello.com?mode=/shadow/ 👉 /shadow/proxy/s/hello.com
// - /proxy/?url=https://hello.com 👉 /proxy/s/hello.com
// - /proxy/?url=https://www.google.com/amp/s/hello.com 👉 /proxy/s/hello.com
// - /proxy/?url=hello.com/canonical 👉 /proxy/s/hello.com/amp
//
// This passthrough is useful to generate the URL from <form> values,
// (See ./app-index/proxy-form.js)
app.get('/proxy', async (req, res, next) => {
const {mode, url} = req.query;
const urlSuffixClearPrefixReStr =
'^https?://((www.)?google.(com?|[a-z]{2}|com?.[a-z]{2}|cat)/amp/s/)?';
const urlSuffix = url.replace(new RegExp(urlSuffixClearPrefixReStr, 'i'), '');
try {
const ampdocUrl = await requestAmphtmlDocUrl(urlSuffix);
const ampdocUrlSuffix = ampdocUrl.replace(/^https?:\/\//, '');
const modePrefix = (mode || '').replace(/\/$/, '');
const proxyUrl = `${modePrefix}/proxy/s/${ampdocUrlSuffix}`;
res.redirect(proxyUrl);
} catch ({message}) {
logWithoutTimestamp(`ERROR: ${message}`);
next();
}
});
/**
* Resolves an AMPHTML URL from a canonical URL. If AMPHTML is canonical, same
* URL is returned.
* @param {string} urlSuffix URL without protocol or google.com/amp/s/...
* @param {string=} protocol 'https' or 'http'. 'https' retries using 'http'.
* @return {!Promise<string>}
*/
async function requestAmphtmlDocUrl(urlSuffix, protocol = 'https') {
const defaultUrl = `${protocol}://${urlSuffix}`;
logWithoutTimestamp(`Fetching URL: ${defaultUrl}`);
const response = await fetch(defaultUrl);
if (!response.ok) {
if (protocol == 'https') {
return requestAmphtmlDocUrl(urlSuffix, 'http');
}
throw new Error(`Status: ${response.status}`);
}
const {window} = new jsdom.JSDOM(await response.text());
const linkRelAmphtml = window.document.querySelector('link[rel=amphtml]');
const amphtmlUrl = linkRelAmphtml && linkRelAmphtml.getAttribute('href');
return amphtmlUrl || defaultUrl;
}
/*
* Intercept Recaptcha frame for,
* integration tests. Using this to mock
* out the recaptcha api.
*/
app.get('/dist.3p/*/recaptcha.*html', recaptchaFrameRequestHandler);
app.use('/recaptcha', recaptchaRouter);
// Deprecate usage of .min.html/.max.html
app.get(
[
'/examples/*.(min|max).html',
'/test/manual/*.(min|max).html',
'/test/fixtures/e2e/*/*.(min|max).html',
],
(req, res) => {
const filePath = req.url;
res.send(generateInfo(filePath));
}
);
app.use('/pwa', (req, res) => {
let file;
let contentType;
if (!req.url || req.path == '/') {
// pwa.html
contentType = 'text/html';
file = '/examples/pwa/pwa.html';
} else if (req.url == '/pwa.js') {
// pwa.js
contentType = 'application/javascript';
file = '/examples/pwa/pwa.js';
} else if (req.url == '/pwa-sw.js') {
// pwa.js
contentType = 'application/javascript';
file = '/examples/pwa/pwa-sw.js';
} else if (req.url == '/ampdoc-shell') {
// pwa-ampdoc-shell.html
contentType = 'text/html';
file = '/examples/pwa/pwa-ampdoc-shell.html';
} else {
// Redirect to the underlying resource.
// TODO(dvoytenko): would be nicer to do forward instead of redirect.
res.writeHead(302, {'Location': req.url});
res.end();
return;
}
res.statusCode = 200;
res.setHeader('Content-Type', contentType);
fs.promises.readFile(pc.cwd() + file).then((file) => {
res.end(file);
});
});
app.use('/api/show', (_req, res) => {
res.json({
showNotification: true,
});
});
app.use('/api/dont-show', (_req, res) => {
res.json({
showNotification: false,
});
});
app.use('/api/echo/query', (req, res) => {
res.json(JSON.parse(req.query.data));
});
app.use('/api/echo/post', (req, res) => {
res.setHeader('Content-Type', 'application/json');
res.end(req.body);
});
app.use('/api/ping', (_req, res) => {
res.status(204).end();
});
app.use('/form/html/post', (req, res) => {
cors.assertCors(req, res, ['POST']);
const form = new formidable.IncomingForm();
form.parse(req, (_err, fields) => {
res.setHeader('Content-Type', 'text/html');
if (fields['email'] == '[email protected]') {
res.statusCode = 500;
res.end(`
<h1 style="color:red;">Sorry ${fields['name']}!</h1>
<p>The email ${fields['email']} is already subscribed!</p>
`);
} else {
res.end(`
<h1>Thanks ${fields['name']}!</h1>
<p>Please make sure to confirm your email ${fields['email']}</p>
`);
}
});
});
app.use('/form/redirect-to/post', (req, res) => {
cors.assertCors(req, res, ['POST'], ['AMP-Redirect-To']);
res.setHeader('AMP-Redirect-To', 'https://google.com');
res.end('{}');
});
app.use('/form/echo-json/post', (req, res) => {
cors.assertCors(req, res, ['POST']);
const form = new formidable.IncomingForm();
const fields = Object.create(null);
form.on('field', function (name, value) {
if (!(name in fields)) {
fields[name] = value;
return;
}
const realName = name;
if (realName in fields) {
if (!Array.isArray(fields[realName])) {
fields[realName] = [fields[realName]];
}
} else {
fields[realName] = [];
}
fields[realName].push(value);
});
form.parse(req, () => {
res.setHeader('Content-Type', 'application/json; charset=utf-8');
if (fields['email'] == '[email protected]') {
res.statusCode = 500;
}
res.end(JSON.stringify(fields));
});
});
app.use('/form/json/poll1', (req, res) => {
cors.assertCors(req, res, ['POST']);
const form = new formidable.IncomingForm();
form.parse(req, () => {
res.setHeader('Content-Type', 'application/json');
res.end(
JSON.stringify({
result: [
{
answer: 'Penguins',
percentage: new Array(77),
},
{
answer: 'Ostriches',
percentage: new Array(8),
},
{
answer: 'Kiwis',
percentage: new Array(14),
},
{
answer: 'Wekas',
percentage: new Array(1),
},
],
})
);
});
});
app.post('/form/json/upload', upload.fields([{name: 'myFile'}]), (req, res) => {
cors.assertCors(req, res, ['POST']);
const myFile = req.files['myFile'];
if (!myFile) {
res.json({message: 'No file data received'});
return;
}
const fileData = myFile[0];
const contents = fileData.buffer.toString();
res.json({message: contents});
});
app.use('/form/search-html/get', (_req, res) => {
res.setHeader('Content-Type', 'text/html');
res.end(`
<h1>Here's results for your search<h1>
<ul>
<li>Result 1</li>
<li>Result 2</li>
<li>Result 3</li>
</ul>
`);
});
app.use('/form/search-json/get', (req, res) => {
cors.assertCors(req, res, ['GET']);
res.json({
term: req.query.term,
additionalFields: req.query.additionalFields,
results: [{title: 'Result 1'}, {title: 'Result 2'}, {title: 'Result 3'}],
});
});
const autocompleteColors = [
'red',
'orange',
'yellow',
'green',
'blue',
'purple',
'pink',
'black',
'white',
];
app.use('/form/autocomplete/query', (req, res) => {
const query = req.query.q;
if (!query) {
res.json({items: autocompleteColors});
} else {
const lowerCaseQuery = query.toLowerCase();
const filtered = autocompleteColors.filter((l) =>
l.toLowerCase().includes(lowerCaseQuery)
);
res.json({items: filtered});
}
});
app.use('/form/autocomplete/error', (_req, res) => {
res.status(500).end();
});
app.use('/form/mention/query', (req, res) => {
const query = req.query.q;
if (!query) {
res.json({items: autocompleteEmailData});
return;
}
const lowerCaseQuery = query.toLowerCase().trim();
const filtered = autocompleteEmailData.filter((l) =>
l.toLowerCase().startsWith(lowerCaseQuery)
);
res.json({items: filtered});
});
app.use('/form/verify-search-json/post', (req, res) => {
cors.assertCors(req, res, ['POST']);
const form = new formidable.IncomingForm();
form.parse(req, (_err, fields) => {
res.setHeader('Content-Type', 'application/json; charset=utf-8');
const errors = [];
if (!fields.phone.match(/^650/)) {
errors.push({name: 'phone', message: 'Phone must start with 650'});
}
if (fields.name !== 'Frank') {
errors.push({name: 'name', message: 'Please set your name to be Frank'});
}
if (fields.error === 'true') {
errors.push({message: 'You asked for an error, you get an error.'});
}
if (fields.city !== 'Mountain View' || fields.zip !== '94043') {
errors.push({
name: 'city',
message: "City doesn't match zip (Mountain View and 94043)",
});
}
if (errors.length === 0) {
res.end(
JSON.stringify({
results: [
{title: 'Result 1'},
{title: 'Result 2'},
{title: 'Result 3'},
],
committed: true,
})
);
} else {
res.statusCode = 400;
res.end(JSON.stringify({verifyErrors: errors}));
}
});
});
/**
* Fetches an AMP document from the AMP proxy and replaces JS
* URLs, so that they point to localhost.
*
* @param {express.Request} req
* @param {express.Response} res
* @param {string} mode
* @return {Promise<void>}
*/
async function proxyToAmpProxy(req, res, mode) {
const url =
'https://cdn.ampproject.org/' +
(req.query['amp_js_v'] ? 'v' : 'c') +
req.url;
logWithoutTimestamp('Fetching URL: ' + url);
const urlResponse = await fetch(url);
let body = await urlResponse.text();
body = body
// Unversion URLs.
.replace(
/https\:\/\/cdn\.ampproject\.org\/rtv\/\d+\//g,
'https://cdn.ampproject.org/'
)
// <base> href pointing to the proxy, so that images, etc. still work.
.replace('<head>', '<head><base href="https://cdn.ampproject.org/">');
const inabox = req.query['inabox'];
const urlPrefix = getUrlPrefix(req);
if (req.query['mraid']) {
body = body
.replace(
'</head>',
'<script async host-service="amp-mraid" src="https://cdn.ampproject.org/v0/amp-mraid-0.1.js">' +
'</script>' +
'</head>'
)
// Change cdnUrl from the default so amp-mraid requests the (mock)
// mraid.js from the local server. In a real environment this doesn't
// matter as the local environment would intercept this request.
.replace(
'<head>',
' <head>' +
' <script>' +
' window.AMP_CONFIG = {' +
` cdnUrl: "${urlPrefix}",` +
' };' +
' </script>'
);
}
if (inabox) {
body = toInaboxDocument(body);
// Allow CORS requests for A4A.
const origin = req.headers.origin || urlPrefix;
cors.enableCors(req, res, origin);
}
body = replaceUrls(mode, body, urlPrefix);
res.status(urlResponse.status).send(body);
}
let itemCtr = 2;
const doctype = '<!doctype html>\n';
const liveListDocs = Object.create(null);
app.use('/examples/live-list-update(-reverse)?.amp.html', (req, res, next) => {
const mode = SERVE_MODE;
let liveListDoc = liveListDocs[req.baseUrl];
if (mode != 'minified' && mode != 'default') {
// Only handle compile(prev min)/default (prev max) mode
next();
return;
}
// When we already have state in memory and user refreshes page, we flush
// the dom we maintain on the server.
if (!('amp_latest_update_time' in req.query) && liveListDoc) {
let outerHTML = liveListDoc.documentElement./*OK*/ outerHTML;
outerHTML = replaceUrls(mode, outerHTML);
res.send(`${doctype}${outerHTML}`);
return;
}
if (!liveListDoc) {
const liveListUpdateFullPath = `${pc.cwd()}${req.baseUrl}`;
logWithoutTimestamp('liveListUpdateFullPath', liveListUpdateFullPath);
const liveListFile = fs.readFileSync(liveListUpdateFullPath);
liveListDoc = liveListDocs[req.baseUrl] = new jsdom.JSDOM(
liveListFile
).window.document;
liveListDoc.ctr = 0;
}
const liveList = liveListDoc.querySelector('#my-live-list');
const perPage = Number(liveList.getAttribute('data-max-items-per-page'));
const items = liveList.querySelector('[items]');
const pagination = liveListDoc.querySelector('#my-live-list [pagination]');
const item1 = liveList.querySelector('#list-item-1');
if (liveListDoc.ctr != 0) {
if (Math.random() < 0.8) {
// Always run a replace on the first item
liveListReplace(item1);
if (Math.random() < 0.5) {
liveListTombstone(liveList);
}
if (Math.random() < 0.8) {
liveListInsert(liveList, item1);
}
pagination.textContent = '';
const liveChildren = [].slice
.call(items.children)
.filter((x) => !x.hasAttribute('data-tombstone'));
const pageCount = Math.ceil(liveChildren.length / perPage);
const pageListItems = Array.apply(null, Array(pageCount))
.map((_, i) => `<li>${i + 1}</li>`)
.join('');
const newPagination =
'<nav aria-label="amp live list pagination">' +
`<ul class="pagination">${pageListItems}</ul>` +
'</nav>';
pagination./*OK*/ innerHTML = newPagination;
} else {
// Sometimes we want an empty response to simulate no changes.
res.send(`${doctype}<html></html>`);
return;
}
}
let outerHTML = liveListDoc.documentElement./*OK*/ outerHTML;
outerHTML = replaceUrls(mode, outerHTML);
liveListDoc.ctr++;
res.send(`${doctype}${outerHTML}`);
});
/**
* @param {Element} item
*/
function liveListReplace(item) {
item.setAttribute('data-update-time', Date.now().toString());
const itemContents = item.querySelectorAll('.content');
itemContents[0].textContent = Math.floor(Math.random() * 10).toString();
itemContents[1].textContent = Math.floor(Math.random() * 10).toString();
}
/**
* @param {Element} liveList
* @param {Element} node
*/
function liveListInsert(liveList, node) {
const iterCount = Math.floor(Math.random() * 2) + 1;
logWithoutTimestamp(`inserting ${iterCount} item(s)`);
for (let i = 0; i < iterCount; i++) {
/**
* TODO(#28387) this type cast may be hiding a bug.
* @type {Element}
*/
const child = /** @type {*} */ (node.cloneNode(true));
child.setAttribute('id', `list-item-${itemCtr++}`);
child.setAttribute('data-sort-time', Date.now().toString());
liveList.querySelector('[items]')?.appendChild(child);
}
}
/**
* @param {Element} liveList
*/
function liveListTombstone(liveList) {
const tombstoneId = Math.floor(Math.random() * itemCtr);
logWithoutTimestamp(`trying to tombstone #list-item-${tombstoneId}`);
// We can tombstone any list item except item-1 since we always do a
// replace example on item-1.
if (tombstoneId != 1) {
const item = liveList./*OK*/ querySelector(`#list-item-${tombstoneId}`);
if (item) {
item.setAttribute('data-tombstone', '');
}
}
}
/**
* Generate a random number between min and max
* Value is inclusive of both min and max values.
*
* @param {number} min
* @param {number} max
* @return {number}
*/
function range(min, max) {
const values = Array.apply(null, new Array(max - min + 1)).map(
(_, i) => min + i
);
return values[Math.round(Math.random() * (max - min))];
}
/**
* Returns the result of a coin flip, true or false
*
* @return {boolean}
*/
function flip() {
return !!Math.floor(Math.random() * 2);
}
/**
* @return {string}
*/
function getLiveBlogItem() {
const now = Date.now();
// Generate a 3 to 7 worded headline
const headline = bacon(range(3, 7));
const numOfParagraphs = range(1, 2);
const body = Array.apply(null, new Array(numOfParagraphs))
.map(() => {
return `<p>${bacon(range(50, 90))}</p>`;
})
.join('\n');
const img = `<amp-img
src="${
flip()
? 'https://placekitten.com/300/350'
: 'https://baconmockup.com/300/350'
}"
layout="responsive"
height="300" width="350">
</amp-img>`;
return `<!doctype html>
<html amp><body>
<amp-live-list id="live-blog-1">
<div items>
<div id="live-blog-item-${now}" data-sort-time="${now}">
<h3 class="headline">
<a href="#live-blog-item-${now}">${headline}</a>
</h3>
<div class="author">
<div class="byline">
<p>
by <span itemscope itemtype="http://schema.org/Person"
itemprop="author"><b>Lorem Ipsum</b>
<a class="mailto" href="mailto:lorem.ipsum@">
lorem.ipsum@</a></span>
</p>
<p class="brand">PublisherName News Reporter<p>
<p><span itemscope itemtype="http://schema.org/Date"
itemprop="Date">
${new Date(now).toString().replace(/ GMT.*$/, '')}
<span></p>
</div>
</div>
<div class="article-body">${body}</div>
${img}
<div class="social-box">
<amp-social-share type="facebook"
data-param-text="Hello world"
data-param-href="https://example.test/?ref=URL"
data-param-app_id="145634995501895"></amp-social-share>
<amp-social-share type="twitter"></amp-social-share>
</div>
</div>
</div>
</amp-live-list></body></html>`;
}
/**
* @return {string}
*/
function getLiveBlogItemWithBindAttributes() {
const now = Date.now();
// Generate a 3 to 7 worded headline
const numOfParagraphs = range(1, 2);
const body = Array.apply(null, new Array(numOfParagraphs))
.map(() => {
return `<p>${bacon(range(50, 90))}</p>`;
})
.join('\n');
return `<!doctype html>
<html amp><body>
<amp-live-list id="live-blog-1">
<div items>
<div id="live-blog-item-${now}" data-sort-time="${now}">
<div class="article-body">
${body}
<p> As you can see, bacon is far superior to
<b><span [text]='favoriteFood'>everything!</span></b>!</p>
</div>
</div>
</div>
</amp-live-list></body></html>`;
}
app.use(
'/examples/live-blog(-non-floating-button)?.amp.html',
(req, res, next) => {
if ('amp_latest_update_time' in req.query) {
res.setHeader('Content-Type', 'text/html');
res.end(getLiveBlogItem());
return;
}
next();
}
);
app.use('/examples/bind/live-list.amp.html', (req, res, next) => {
if ('amp_latest_update_time' in req.query) {
res.setHeader('Content-Type', 'text/html');
res.end(getLiveBlogItemWithBindAttributes());
return;
}
next();
});
app.use('/impression-proxy/', (req, res) => {
cors.assertCors(req, res, ['GET']);
// Fake response with the following optional fields:
// location: The Url the that server would have sent redirect to w/o ALP
// tracking_url: URL that should be requested to track click
// gclid: The conversion tracking value
const body = {
'location': 'localhost:8000/examples/?gclid=1234&foo=bar&example=123',
'tracking_url': 'tracking_url',
'gclid': '1234',
};
res.send(body);
// Or fake response with status 204 if viewer replaceUrl is provided
});
/**
* Acts in a similar fashion to /serve_mode_change. Saves
* analytics requests via /run-variable-substitution, and
* then returns the encoded/substituted/replaced request
* via /get-variable-request.
*/
// Saves the variables input to be used in run-variable-substitution
app.get('/save-variables', saveVariables);
// Creates an iframe with amp-analytics. Analytics request
// uses save-variable-request as its endpoint.
app.get('/run-variable-substitution', runVariableSubstitution);
// Saves the analytics request to the dev server.
app.get('/save-variable-request', saveVariableRequest);
// Returns the saved analytics request.
app.get('/get-variable-request', getVariableRequest);
let forcePromptOnNext = false;
app.post('/get-consent-v1/', (req, res) => {
cors.assertCors(req, res, ['POST']);
const body = {
'promptIfUnknown': true,
'purposeConsentRequired': ['purpose-foo', 'purpose-bar'],
'forcePromptOnNext': forcePromptOnNext,
'sharedData': {
'tfua': true,
'coppa': true,
},
};
res.json(body);
});
app.get('/get-consent-v1-set/', (req, res) => {
cors.assertCors(req, res, ['GET']);
const value = req.query['forcePromptOnNext'];
if (value == 'false' || value == '0') {
forcePromptOnNext = false;
} else {
forcePromptOnNext = true;
}
res.json({});
res.end();
});
app.post('/get-consent-no-prompt/', (req, res) => {
cors.assertCors(req, res, ['POST']);
const body = {};
res.json(body);
});
app.post('/check-consent', (req, res) => {
cors.assertCors(req, res, ['POST']);
const response = {
'consentRequired': req.query.consentRequired === 'true',
'consentStateValue': req.query.consentStateValue,
'consentString': req.query.consentString,
'expireCache': req.query.expireCache === 'true',
};
if (req.query.consentMetadata) {
response['consentMetadata'] = JSON.parse(
req.query.consentMetadata.replace(/'/g, '"')
);
}
res.json(response);
});
// Proxy with local JS.
// Example:
// http://localhost:8000/proxy/s/www.washingtonpost.com/amphtml/news/post-politics/wp/2016/02/21/bernie-sanders-says-lower-turnout-contributed-to-his-nevada-loss-to-hillary-clinton/
app.use('/proxy/', (req, res) => proxyToAmpProxy(req, res, SERVE_MODE));
// Nest the response in an iframe.
// Example:
// http://localhost:8000/iframe/examples/ads.amp.html
app.get('/iframe/*', (req, res) => {
// Returns an html blob with an iframe pointing to the url after /iframe/.
res.send(`<!doctype html>
<html style="width:100%; height:100%;">
<body style="width:98%; height:98%;">
@@ -904,1 +905,1 @@
- <iframe src="${req.url.substr(7)}"
+ <iframe src="${escape(req.url.substr(7))}"
style="width:100%; height:100%;">
</iframe>
</body>
</html>`);
});
app.get('/a4a_template/*', (req, res) => {
cors.assertCors(req, res, ['GET']);
const match = /^\/a4a_template\/([a-z-]+)\/(\d+)$/.exec(req.path);
if (!match) {
res.status(404);
res.end('Invalid path: ' + req.path);
return;
}
const filePath =
`${pc.cwd()}/extensions/amp-ad-network-${match[1]}-impl/` +
`0.1/data/${match[2]}.template`;
fs.promises
.readFile(filePath)
.then((file) => {
res.setHeader('Content-Type', 'application/json');
res.setHeader('AMP-template-amp-creative', 'amp-mustache');
res.end(file);
})
.catch(() => {
res.status(404);
res.end('Not found: ' + filePath);
});
});
// Returns a document that echoes any post messages received from parent.
// An optional `message` query param can be appended for an initial post
// message sent on document load.
// Example:
// http://localhost:8000/iframe-echo-message?message=${payload}
app.get('/iframe-echo-message', (req, res) => {
const {message} = req.query;
res.send(
`<!doctype html>
<body style="background-color: yellow">
<script>
if (${message}) {
echoMessage(${message});
}
window.addEventListener('message', function(event) {
echoMessage(event.data);
});
function echoMessage(message) {
parent.postMessage(message, '*');
}
</script>
</body>
</html>`
);
});
/**
* Append ?sleep=5 to any included JS file in examples to emulate delay in
* loading that file. This allows you to test issues with your extension being
* late to load and testing user interaction with your element before your code
* loads.
*
* Example delay loading amp-form script by 5 seconds:
* <script async custom-element="amp-form"
* src="https://cdn.ampproject.org/v0/amp-form-0.1.js?sleep=5"></script>
*/
app.use(['/dist/v0/amp-*.(m?js)', '/dist/amp*.(m?js)'], (req, _res, next) => {
const sleep = parseInt(req.query.sleep || 0, 10) * 1000;
setTimeout(next, sleep);
});
/**
* Disable caching for extensions if the --no_caching_extensions flag is used.
*/
app.get(['/dist/v0/amp-*.(m?js)'], (_req, res, next) => {
if (argv.no_caching_extensions) {
res.header('Cache-Control', 'no-store');
}
next();
});
/**
* Video testbench endpoint
*/
app.get('/test/manual/amp-video.amp.html', runVideoTestBench);
app.get(
[
'/examples/(**/)?*.html',
'/test/manual/(**/)?*.html',
'/test/fixtures/e2e/(**/)?*.html',
'/test/fixtures/performance/(**/)?*.html',
],
(req, res, next) => {
const filePath = req.path;
const mode = SERVE_MODE;
const inabox = req.query['inabox'];
const stream = Number(req.query['stream']);
const componentVersion = req.query['componentVersion'];
const urlPrefix = getUrlPrefix(req);
fs.promises
.readFile(pc.cwd() + filePath, 'utf8')
.then((file) => {
if (req.query['amp_js_v']) {
file = addViewerIntegrationScript(req.query['amp_js_v'], file);
}
if (req.query['mraid']) {
file = file
.replace(
'</head>',
'<script async host-service="amp-mraid" src="https://cdn.ampproject.org/v0/amp-mraid-0.1.js">' +
'</script>' +
'</head>'
)
.replace(
'<head>',
' <head>' +
' <script>' +
' window.AMP_CONFIG = {' +
` cdnUrl: "${urlPrefix}",` +
' };' +
' </script>'
);
}
file = file.replace(/__TEST_SERVER_PORT__/g, TEST_SERVER_PORT);
if (componentVersion) {
file = file.replace(/-latest.js/g, `-${componentVersion}.js`);
}
if (inabox) {
file = toInaboxDocument(file);
// Allow CORS requests for A4A.
if (req.headers.origin) {
cors.enableCors(req, res, req.headers.origin);
}
}
file = replaceUrls(mode, file);
const ampExperimentsOptIn = req.query['exp'];
if (ampExperimentsOptIn) {
file = file.replace(
'<head>',
`<head><meta name="amp-experiments-opt-in" content="${ampExperimentsOptIn}">`
);
}
// Extract amp-ad for the given 'type' specified in URL query.
if (req.path.indexOf('/examples/ads.amp.html') == 0 && req.query.type) {
const ads =
file.match(
elementExtractor('(amp-ad|amp-embed)', req.query.type)
) ?? [];
file = file.replace(
/<body>[\s\S]+<\/body>/m,
'<body>' + ads.join('') + '</body>'
);
}
// Extract amp-analytics for the given 'type' specified in URL query.
if (
req.path.indexOf('/examples/analytics-vendors.amp.html') == 0 &&
req.query.type
) {
const analytics =
file.match(elementExtractor('amp-analytics', req.query.type)) ?? [];
file = file.replace(
/<div id="container">[\s\S]+<\/div>/m,
'<div id="container">' + analytics.join('') + '</div>'
);
}
// Extract amp-consent for the given 'type' specified in URL query.
if (
req.path.indexOf('/examples/amp-consent/cmp-vendors.amp.html') == 0 &&
req.query.type
) {
const consent =
file.match(elementExtractor('amp-consent', req.query.type)) ?? [];
file = file.replace(
/<div id="container">[\s\S]+<\/div>/m,
'<div id="container">' + consent.join('') + '</div>'
);
}
if (stream > 0) {
res.writeHead(200, {'Content-Type': 'text/html'});
let pos = 0;
const writeChunk = function () {
const chunk = file.substring(
pos,
Math.min(pos + stream, file.length)
);
res.write(chunk);
pos += stream;
if (pos < file.length) {
setTimeout(writeChunk, 500);
} else {
res.end();
}
};
writeChunk();
} else {
res.send(file);
}
})
.catch(() => {
next();
});
}
);
/**
* @param {string} string
* @return {string}
*/
function escapeRegExp(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
/**
* @param {string} tagName
* @param {string} type
* @return {RegExp}
*/
function elementExtractor(tagName, type) {
type = escapeRegExp(type);
return new RegExp(
`<${tagName}[\\s][^>]*['"]${type}['"][^>]*>([\\s\\S]+?)</${tagName}>`,
'gm'
);
}
// Data for example: http://localhost:8000/examples/bind/xhr.amp.html
app.use('/bind/form/get', (req, res) => {
cors.assertCors(req, res, ['GET']);
res.json({
bindXhrResult: 'I was fetched from the server!',
});
});
// Data for example: http://localhost:8000/examples/bind/ecommerce.amp.html
app.use('/bind/ecommerce/sizes', (req, res) => {
cors.assertCors(req, res, ['GET']);
setTimeout(() => {
const prices = {
'0': {
'sizes': {
'XS': 8.99,
'S': 9.99,
},
},
'1': {
'sizes': {
'S': 10.99,
'M': 12.99,
'L': 14.99,
},
},
'2': {
'sizes': {
'L': 11.99,
'XL': 13.99,
},
},
'3': {
'sizes': {
'M': 7.99,
'L': 9.99,
'XL': 11.99,
},
},
'4': {
'sizes': {
'XS': 8.99,
'S': 10.99,
'L': 15.99,
},
},
'5': {
'sizes': {
'S': 8.99,
'L': 14.99,
'XL': 11.99,
},
},
'6': {
'sizes': {
'XS': 8.99,
'S': 9.99,
'M': 12.99,
},
},
'7': {
'sizes': {
'M': 10.99,
'L': 11.99,
},
},
};
const object = {};
object[req.query.shirt] = prices[req.query.shirt];
res.json(object);
}, 1000); // Simulate network delay.
});
/**
* Simulates a publisher's metering state store.
* (amp-subscriptions)
* @type {{[ampReaderId: string]: {}}}
*/
const meteringStateStore = {};
// Simulate a publisher's entitlements API.
// (amp-subscriptions)
app.use('/subscription/:id/entitlements', (req, res) => {
cors.assertCors(req, res, ['GET']);
// Create entitlements response.
const source = 'local' + req.params.id;
const granted = req.params.id > 0;
const grantReason = granted ? 'SUBSCRIBER' : 'NOT_SUBSCRIBER';
const decryptedDocumentKey = decryptDocumentKey(req.query.crypt);
const response = {
source,
granted,
grantReason,
data: {
login: true,
},
decryptedDocumentKey,
};
// Store metering state, if possible.
const ampReaderId = req.query.rid;
if (ampReaderId && req.query.meteringState) {
// Parse metering state from encoded Base64 string.
const encodedMeteringState = req.query.meteringState;
const decodedMeteringState = Buffer.from(
encodedMeteringState,
'base64'
).toString();
const meteringState = JSON.parse(decodedMeteringState);
// Store metering state.
meteringStateStore[ampReaderId] = meteringState;
}
// Add metering state to response, if possible.
if (meteringStateStore[ampReaderId]) {
response.metering = {
state: meteringStateStore[ampReaderId],
};
}
res.json(response);
});
// Simulate a publisher's SKU map API.
// (amp-subscriptions)
app.use('/subscriptions/skumap', (req, res) => {
cors.assertCors(req, res, ['GET']);
res.json({
'subscribe.google.com': {
'subscribeButtonSimple': {
'sku': 'basic',
},
'subscribeButtonCarousel': {
'carouselOptions': {
'skus': ['basic', 'premium_monthly'],
},
},
},
});
});
// Simulate a publisher's pingback API.
// (amp-subscriptions)
app.use('/subscription/pingback', (req, res) => {
cors.assertCors(req, res, ['POST']);
res.json({
done: true,
});
});
/*
Simulate a publisher's account registration API.
The `amp-subscriptions-google` extension sends this API a POST request.
The request body looks like:
{
"googleSignInDetails": {
// This signed JWT contains information from Google Sign-In
"idToken": "...JWT from Google Sign-In...",
// Some useful fields from the `idToken`, pre-parsed for convenience
"name": "Jane Smith",
"givenName": "Jane",
"familyName": "Smith",
"imageUrl": "https://imageurl",
"email": "[email protected]"
},
// Associate this ID with the registration. Use it to look up metering state
// for future entitlements requests
// https://github.com/ampproject/amphtml/blob/main/extensions/amp-subscriptions/amp-subscriptions.md#combining-the-amp-reader-id-with-publisher-cookies
"ampReaderId": "amp-s0m31d3nt1f13r"
}
(amp-subscriptions-google)
*/
app.use('/subscription/register', (req, res) => {
cors.assertCors(req, res, ['POST']);
// Generate a new ID for this metering state.
const meteringStateId = 'ppid' + Math.round(Math.random() * 99999999);
// Define registration timestamp.
//
// For demo purposes, set timestamp to 30 seconds ago.
// This causes Metering Toast to show immediately,
// which helps engineers test metering.
const registrationTimestamp = Math.round(Date.now() / 1000) - 30000;
// Store metering state.
//
// For demo purposes, just save this in memory.
// Production systems should persist this.
meteringStateStore[req.body.ampReaderId] = {
id: meteringStateId,
standardAttributes: {
// eslint-disable-next-line local/camelcase
registered_user: {
timestamp: registrationTimestamp, // In seconds.
},
},
};
res.json({
metering: {
state: meteringStateStore[req.body.ampReaderId],
},
});
});
// Simulated adzerk ad server and AMP cache CDN.
app.get('/adzerk/*', (req, res) => {
cors.assertCors(req, res, ['GET'], ['AMP-template-amp-creative']);
const match = /\/(\d+)/.exec(req.path);
if (!match || !match[1]) {
res.status(404);
res.end('Invalid path: ' + req.path);
return;
}
const filePath =
pc.cwd() + '/extensions/amp-ad-network-adzerk-impl/0.1/data/' + match[1];
fs.promises
.readFile(filePath)
.then((file) => {
res.setHeader('Content-Type', 'application/json');
res.setHeader('AMP-Ad-Template-Extension', 'amp-mustache');
res.setHeader('AMP-Ad-Response-Type', 'template');
res.end(file);
})
.catch(() => {
res.status(404);
res.end('Not found: ' + filePath);
});
});
app.get('/dist/*.mjs', (req, res, next) => {
// Allow CORS access control explicitly for mjs files
cors.enableCors(req, res);
next();
});
/*
* Serve extension scripts and their source maps.
*/
app.get(
['/dist/rtv/*/v0/*.(m?js)', '/dist/rtv/*/v0/*.(m?js).map'],
async (req, res, next) => {
const mode = SERVE_MODE;
const fileName = path.basename(req.path).replace('.max.', '.');
let filePath = 'https://cdn.ampproject.org/v0/' + fileName;
if (await passthroughServeModeCdn(res, filePath)) {
return;
}
const isJsMap = filePath.endsWith('.map');
if (isJsMap) {
filePath = filePath.replace(/\.(m?js)\.map$/, '.$1');
}
filePath = replaceUrls(mode, filePath);
req.url = filePath + (isJsMap ? '.map' : '');
next();
}
);
/**
* Handle amp-story translation file requests with an rtv path.
* We need to make sure we only handle the amp-story requests since this
* can affect other tests with json requests.
*/
app.get('/dist/rtv/*/v0/amp-story*.json', async (req, _res, next) => {
const fileName = path.basename(req.path);
let filePath = 'https://cdn.ampproject.org/v0/' + fileName;
filePath = replaceUrls(SERVE_MODE, filePath);
req.url = filePath;
next();
});
if (argv.coverage === 'live') {
app.get('/dist/amp.js', async (req, res) => {
const ampJs = await fs.promises.readFile(`${pc.cwd()}${req.path}`);
res.setHeader('Content-Type', 'text/javascript');
res.setHeader('Access-Control-Allow-Origin', '*');
// Append an unload handler that reports coverage information each time you
// leave a page.
res.end(`${ampJs};
window.addEventListener('beforeunload', (evt) => {
const COV_REPORT_URL = 'http://localhost:${TEST_SERVER_PORT}/coverage/client';
console.info('POSTing code coverage to', COV_REPORT_URL);
const xhr = new XMLHttpRequest();
xhr.open('POST', COV_REPORT_URL, true);
xhr.setRequestHeader('Content-type', 'application/json');
xhr.send(JSON.stringify(window.__coverage__));
// Required by Chrome
evt.returnValue = '';
return null;
});`);
});
}
app.get('/dist/ww.(m?js)', async (req, res, next) => {
// Special case for entry point script url. Use minified for testing
const mode = SERVE_MODE;
const fileName = path.basename(req.path);
if (await passthroughServeModeCdn(res, fileName)) {
return;
}
if (mode == 'default') {
req.url = req.url.replace(/\.(m?js)$/, '.max.$1');
}
next();
});
app.get('/dist/iframe-transport-client-lib.(m?js)', (req, _res, next) => {
req.url = req.url.replace(/dist/, 'dist.3p/current');
next();
});
app.get('/dist/amp-inabox-host.(m?js)', (req, _res, next) => {
const mode = SERVE_MODE;
if (mode != 'default') {
req.url = req.url.replace('amp-inabox-host', 'amp4ads-host-v0');
}
next();
});
app.get('/mraid.js', (req, _res, next) => {
req.url = req.url.replace('mraid.js', 'examples/mraid/mraid.js');
next();
});
/**
* Shadow viewer. Fetches shadow runtime from cdn by default.
* Setting the param useLocal=1 will load the runtime from the local build.
*/
app.use('/shadow/', (req, res) => {
const {url} = req;
const isProxyUrl = /^\/proxy\//.test(url);
const baseHref = isProxyUrl
? 'https://cdn.ampproject.org/'
: `${path.dirname(url)}/`;
const viewerHtml = renderShadowViewer({
src: '//' + req.hostname + '/' + req.url.replace(/^\//, ''),
baseHref,
});
if (!req.query.useLocal) {
res.end(viewerHtml);
return;
}
res.end(replaceUrls(SERVE_MODE, viewerHtml));
});
app.use('/mraid/', (req, res) => {
res.redirect(req.url + '?inabox=1&mraid=1');
});
/**
* @param {string} ampJsVersionString
* @param {string} file
* @return {string}
*/
function addViewerIntegrationScript(ampJsVersionString, file) {
const ampJsVersion = parseFloat(ampJsVersionString);
if (!ampJsVersion) {
return file;
}
let viewerScript;
// eslint-disable-next-line local/no-es2015-number-props
if (Number.isInteger(ampJsVersion)) {
// Viewer integration script from gws, such as
// https://cdn.ampproject.org/viewer/google/v7.js
viewerScript =
'<script async src="https://cdn.ampproject.org/viewer/google/v' +
ampJsVersion +
'.js"></script>';
} else {
// Viewer integration script from runtime, such as
// https://cdn.ampproject.org/v0/amp-viewer-integration-0.1.js
viewerScript =
'<script async ' +
'src="https://cdn.ampproject.org/v0/amp-viewer-integration-' +
ampJsVersion +
'.js" data-amp-report-test="viewer-integr.js"></script>';
}
file = file.replace('</head>', viewerScript + '</head>');
return file;
}
/**
* @param {express.Request} req
* @return {string}
*/
function getUrlPrefix(req) {
return req.protocol + '://' + req.headers.host;
}
/**
* @param {string} filePath
* @return {string}
*/
function generateInfo(filePath) {
const mode = SERVE_MODE;
filePath = filePath.substr(0, filePath.length - 9) + '.html';
return (
'<h2>Please note that .min/.max is no longer supported</h2>' +
'<h3>Current serving mode is ' +
mode +
'</h3>' +
'<h3>Please go to <a href= ' +
filePath +
'>Unversioned Link</a> to view the page<h3>' +
'<h3></h3>' +
'<h3><a href = /serve_mode=default>' +
'Change to DEFAULT mode (unminified JS)</a></h3>' +
'<h3><a href = /serve_mode=minified>' +
'Change to COMPILED mode (minified JS)</a></h3>' +
'<h3><a href = /serve_mode=cdn>' +
'Change to CDN mode (prod JS)</a></h3>'
);
}
/**
* @param {string} encryptedDocumentKey
* @return {?string}
*/
function decryptDocumentKey(encryptedDocumentKey) {
if (!encryptedDocumentKey) {
return null;
}
const cryptoStart = 'ENCRYPT(';
if (!encryptedDocumentKey.includes(cryptoStart, 0)) {
return null;
}
let jsonString = encryptedDocumentKey.replace(cryptoStart, '');
jsonString = jsonString.substring(0, jsonString.length - 1);
const parsedJson = JSON.parse(jsonString);
if (!parsedJson) {
return null;
}
return parsedJson.key;
}
// serve local vendor config JSON files
app.use(
'(/dist)?/rtv/*/v0/analytics-vendors/:vendor.json',
async (req, res) => {
const {vendor} = req.params;
const serveMode = SERVE_MODE;
const cdnUrl = `https://cdn.ampproject.org/v0/analytics-vendors/${vendor}.json`;
if (await passthroughServeModeCdn(res, cdnUrl)) {
return;
}
const max = serveMode === 'default' ? '.max' : '';
const localPath = `${pc.cwd()}/dist/v0/analytics-vendors/${vendor}${max}.json`;
try {
const file = await fs.promises.readFile(localPath);
res.setHeader('Content-Type', 'application/json');
res.end(file);
} catch (_) {
res.status(404);
res.end('Not found: ' + localPath);
}
}
);
module.exports = app;

  • Create Pull Request

Remediation feedback:

  • 👍 Like
  • 👎 Dislike
Secure Code Warrior Training Material

● Training

   ▪ Secure Code Warrior Cross-Site Scripting Training

● Videos

   ▪ Secure Code Warrior Cross-Site Scripting Video

 
HighCross-Site Scripting

CWE-79

app.js:916

12024-08-19 04:08am
Vulnerable Code

app.get('/a4a_template/*', (req, res) => {
cors.assertCors(req, res, ['GET']);
const match = /^\/a4a_template\/([a-z-]+)\/(\d+)$/.exec(req.path);
if (!match) {
res.status(404);
res.end('Invalid path: ' + req.path);

1 Data Flow/s detected

app.get('/a4a_template/*', (req, res) => {

res.end('Invalid path: ' + req.path);

⛑️ Remediation Suggestion

--- original
+++ remediated
@@ -1,0 +1,1 @@
+var escape = require('escape-html');
'use strict';
/**
* @fileoverview Creates an http server to handle static
* files and list directories for use with the amp live server
*/
const argv = require('minimist')(process.argv.slice(2));
const bacon = require('baconipsum');
const bodyParser = require('body-parser');
const cors = require('./amp-cors');
const devDashboard = require('./app-index');
const express = require('express');
const formidable = require('formidable');
const fs = require('fs');
const jsdom = require('jsdom');
const path = require('path');
const upload = require('multer')();
const pc = process;
const autocompleteEmailData = require('./autocomplete-test-data');
const header = require('connect-header');
const runVideoTestBench = require('./app-video-testbench');
const {
getServeMode,
isRtvMode,
replaceUrls,
toInaboxDocument,
} = require('./app-utils');
const {
getVariableRequest,
runVariableSubstitution,
saveVariableRequest,
saveVariables,
} = require('./variable-substitution');
const {
recaptchaFrameRequestHandler,
recaptchaRouter,
} = require('./recaptcha-router');
const {logWithoutTimestamp} = require('../common/logging');
const {log} = require('../common/logging');
const {red} = require('kleur/colors');
const {renderShadowViewer} = require('./shadow-viewer');
/**
* Respond with content received from a URL when SERVE_MODE is "cdn".
* @param {express.Response} res
* @param {string} cdnUrl
* @return {Promise<boolean>}
*/
async function passthroughServeModeCdn(res, cdnUrl) {
if (SERVE_MODE !== 'cdn') {
return false;
}
try {
const response = await fetch(cdnUrl);
res.status(response.status);
res.send(await response.text());
} catch (e) {
log(red('ERROR:'), e);
res.status(500);
res.end();
}
return true;
}
const app = express();
const TEST_SERVER_PORT = argv.port || 8000;
let SERVE_MODE = getServeMode();
app.use(bodyParser.json());
app.use(bodyParser.text());
// Middleware is executed in order, so this must be at the top.
// TODO(#24333): Migrate all server URL handlers to new-server/router and
// deprecate app.js.
app.use(require('./new-server/router'));
app.use(require('./routes/a4a-envelopes'));
app.use('/amp4test', require('./amp4test').app);
app.use('/analytics', require('./routes/analytics'));
app.use('/list/', require('./routes/list'));
app.use('/test', require('./routes/test'));
if (argv.coverage) {
app.use('/coverage', require('istanbul-middleware').createHandler());
}
// Built binaries should be fetchable from other origins, i.e. Storybook.
app.use(header({'Access-Control-Allow-Origin': '*'}));
// Append ?csp=1 to the URL to turn on the CSP header.
// TODO: shall we turn on CSP all the time?
app.use((req, res, next) => {
if (req.query.csp) {
res.set({
'content-security-policy':
"default-src * blob: data:; script-src https://cdn.ampproject.org/rtv/ https://cdn.ampproject.org/v0.js https://cdn.ampproject.org/v0/ https://cdn.ampproject.org/viewer/ http://localhost:8000 https://localhost:8000; object-src 'none'; style-src 'unsafe-inline' https://cdn.ampproject.org/rtv/ https://cdn.materialdesignicons.com https://cloud.typography.com https://fast.fonts.net https://fonts.googleapis.com https://maxcdn.bootstrapcdn.com https://p.typekit.net https://use.fontawesome.com https://use.typekit.net https://cdnjs.cloudflare.com/ajax/libs/font-awesome/; report-uri https://csp-collector.appspot.com/csp/amp",
});
}
next();
});
/**
*
* @param {string} serveMode
* @return {boolean}
*/
function isValidServeMode(serveMode) {
return (
['default', 'minified', 'cdn', 'esm'].includes(serveMode) ||
isRtvMode(serveMode)
);
}
/**
*
* @param {string} serveMode
*/
function setServeMode(serveMode) {
SERVE_MODE = serveMode;
}
app.get('/serve_mode=:mode', (req, res) => {
const newMode = req.params.mode;
if (isValidServeMode(newMode)) {
setServeMode(newMode);
res.send(`<h2>Serve mode changed to ${newMode}</h2>`);
} else {
const info = '<h2>Serve mode ' + newMode + ' is not supported. </h2>';
res.status(400).send(info);
}
});
if (argv._.includes('integration') && !argv.nobuild) {
setServeMode('minified');
}
if (!(argv._.includes('unit') || argv._.includes('integration'))) {
// Dev dashboard routes break test scaffolding since they're global.
devDashboard.installExpressMiddleware(app);
}
// Changes the current serve mode via query param
// e.g. /serve_mode_change?mode=(default|minified|cdn|<RTV_NUMBER>)
// (See ./app-index/settings.js)
app.get('/serve_mode_change', (req, res) => {
const {mode} = req.query;
if (isValidServeMode(mode)) {
setServeMode(mode);
res.json({ok: true});
return;
}
res.status(400).json({ok: false});
});
// Redirects to a proxied document with optional mode through query params.
//
// Mode can be one of:
// - '/', empty string, or unset for an unwrapped doc
// - '/a4a/' for an AMP4ADS wrapper
// - '/a4a-3p/' for a 3P AMP4ADS wrapper
// - '/inabox/' for an AMP inabox wrapper
// - '/shadow/' for a shadow-wrapped document
//
// Examples:
// - /proxy/?url=hello.com 👉 /proxy/s/hello.com
// - /proxy/?url=hello.com?mode=/shadow/ 👉 /shadow/proxy/s/hello.com
// - /proxy/?url=https://hello.com 👉 /proxy/s/hello.com
// - /proxy/?url=https://www.google.com/amp/s/hello.com 👉 /proxy/s/hello.com
// - /proxy/?url=hello.com/canonical 👉 /proxy/s/hello.com/amp
//
// This passthrough is useful to generate the URL from <form> values,
// (See ./app-index/proxy-form.js)
app.get('/proxy', async (req, res, next) => {
const {mode, url} = req.query;
const urlSuffixClearPrefixReStr =
'^https?://((www.)?google.(com?|[a-z]{2}|com?.[a-z]{2}|cat)/amp/s/)?';
const urlSuffix = url.replace(new RegExp(urlSuffixClearPrefixReStr, 'i'), '');
try {
const ampdocUrl = await requestAmphtmlDocUrl(urlSuffix);
const ampdocUrlSuffix = ampdocUrl.replace(/^https?:\/\//, '');
const modePrefix = (mode || '').replace(/\/$/, '');
const proxyUrl = `${modePrefix}/proxy/s/${ampdocUrlSuffix}`;
res.redirect(proxyUrl);
} catch ({message}) {
logWithoutTimestamp(`ERROR: ${message}`);
next();
}
});
/**
* Resolves an AMPHTML URL from a canonical URL. If AMPHTML is canonical, same
* URL is returned.
* @param {string} urlSuffix URL without protocol or google.com/amp/s/...
* @param {string=} protocol 'https' or 'http'. 'https' retries using 'http'.
* @return {!Promise<string>}
*/
async function requestAmphtmlDocUrl(urlSuffix, protocol = 'https') {
const defaultUrl = `${protocol}://${urlSuffix}`;
logWithoutTimestamp(`Fetching URL: ${defaultUrl}`);
const response = await fetch(defaultUrl);
if (!response.ok) {
if (protocol == 'https') {
return requestAmphtmlDocUrl(urlSuffix, 'http');
}
throw new Error(`Status: ${response.status}`);
}
const {window} = new jsdom.JSDOM(await response.text());
const linkRelAmphtml = window.document.querySelector('link[rel=amphtml]');
const amphtmlUrl = linkRelAmphtml && linkRelAmphtml.getAttribute('href');
return amphtmlUrl || defaultUrl;
}
/*
* Intercept Recaptcha frame for,
* integration tests. Using this to mock
* out the recaptcha api.
*/
app.get('/dist.3p/*/recaptcha.*html', recaptchaFrameRequestHandler);
app.use('/recaptcha', recaptchaRouter);
// Deprecate usage of .min.html/.max.html
app.get(
[
'/examples/*.(min|max).html',
'/test/manual/*.(min|max).html',
'/test/fixtures/e2e/*/*.(min|max).html',
],
(req, res) => {
const filePath = req.url;
res.send(generateInfo(filePath));
}
);
app.use('/pwa', (req, res) => {
let file;
let contentType;
if (!req.url || req.path == '/') {
// pwa.html
contentType = 'text/html';
file = '/examples/pwa/pwa.html';
} else if (req.url == '/pwa.js') {
// pwa.js
contentType = 'application/javascript';
file = '/examples/pwa/pwa.js';
} else if (req.url == '/pwa-sw.js') {
// pwa.js
contentType = 'application/javascript';
file = '/examples/pwa/pwa-sw.js';
} else if (req.url == '/ampdoc-shell') {
// pwa-ampdoc-shell.html
contentType = 'text/html';
file = '/examples/pwa/pwa-ampdoc-shell.html';
} else {
// Redirect to the underlying resource.
// TODO(dvoytenko): would be nicer to do forward instead of redirect.
res.writeHead(302, {'Location': req.url});
res.end();
return;
}
res.statusCode = 200;
res.setHeader('Content-Type', contentType);
fs.promises.readFile(pc.cwd() + file).then((file) => {
res.end(file);
});
});
app.use('/api/show', (_req, res) => {
res.json({
showNotification: true,
});
});
app.use('/api/dont-show', (_req, res) => {
res.json({
showNotification: false,
});
});
app.use('/api/echo/query', (req, res) => {
res.json(JSON.parse(req.query.data));
});
app.use('/api/echo/post', (req, res) => {
res.setHeader('Content-Type', 'application/json');
res.end(req.body);
});
app.use('/api/ping', (_req, res) => {
res.status(204).end();
});
app.use('/form/html/post', (req, res) => {
cors.assertCors(req, res, ['POST']);
const form = new formidable.IncomingForm();
form.parse(req, (_err, fields) => {
res.setHeader('Content-Type', 'text/html');
if (fields['email'] == '[email protected]') {
res.statusCode = 500;
res.end(`
<h1 style="color:red;">Sorry ${fields['name']}!</h1>
<p>The email ${fields['email']} is already subscribed!</p>
`);
} else {
res.end(`
<h1>Thanks ${fields['name']}!</h1>
<p>Please make sure to confirm your email ${fields['email']}</p>
`);
}
});
});
app.use('/form/redirect-to/post', (req, res) => {
cors.assertCors(req, res, ['POST'], ['AMP-Redirect-To']);
res.setHeader('AMP-Redirect-To', 'https://google.com');
res.end('{}');
});
app.use('/form/echo-json/post', (req, res) => {
cors.assertCors(req, res, ['POST']);
const form = new formidable.IncomingForm();
const fields = Object.create(null);
form.on('field', function (name, value) {
if (!(name in fields)) {
fields[name] = value;
return;
}
const realName = name;
if (realName in fields) {
if (!Array.isArray(fields[realName])) {
fields[realName] = [fields[realName]];
}
} else {
fields[realName] = [];
}
fields[realName].push(value);
});
form.parse(req, () => {
res.setHeader('Content-Type', 'application/json; charset=utf-8');
if (fields['email'] == '[email protected]') {
res.statusCode = 500;
}
res.end(JSON.stringify(fields));
});
});
app.use('/form/json/poll1', (req, res) => {
cors.assertCors(req, res, ['POST']);
const form = new formidable.IncomingForm();
form.parse(req, () => {
res.setHeader('Content-Type', 'application/json');
res.end(
JSON.stringify({
result: [
{
answer: 'Penguins',
percentage: new Array(77),
},
{
answer: 'Ostriches',
percentage: new Array(8),
},
{
answer: 'Kiwis',
percentage: new Array(14),
},
{
answer: 'Wekas',
percentage: new Array(1),
},
],
})
);
});
});
app.post('/form/json/upload', upload.fields([{name: 'myFile'}]), (req, res) => {
cors.assertCors(req, res, ['POST']);
const myFile = req.files['myFile'];
if (!myFile) {
res.json({message: 'No file data received'});
return;
}
const fileData = myFile[0];
const contents = fileData.buffer.toString();
res.json({message: contents});
});
app.use('/form/search-html/get', (_req, res) => {
res.setHeader('Content-Type', 'text/html');
res.end(`
<h1>Here's results for your search<h1>
<ul>
<li>Result 1</li>
<li>Result 2</li>
<li>Result 3</li>
</ul>
`);
});
app.use('/form/search-json/get', (req, res) => {
cors.assertCors(req, res, ['GET']);
res.json({
term: req.query.term,
additionalFields: req.query.additionalFields,
results: [{title: 'Result 1'}, {title: 'Result 2'}, {title: 'Result 3'}],
});
});
const autocompleteColors = [
'red',
'orange',
'yellow',
'green',
'blue',
'purple',
'pink',
'black',
'white',
];
app.use('/form/autocomplete/query', (req, res) => {
const query = req.query.q;
if (!query) {
res.json({items: autocompleteColors});
} else {
const lowerCaseQuery = query.toLowerCase();
const filtered = autocompleteColors.filter((l) =>
l.toLowerCase().includes(lowerCaseQuery)
);
res.json({items: filtered});
}
});
app.use('/form/autocomplete/error', (_req, res) => {
res.status(500).end();
});
app.use('/form/mention/query', (req, res) => {
const query = req.query.q;
if (!query) {
res.json({items: autocompleteEmailData});
return;
}
const lowerCaseQuery = query.toLowerCase().trim();
const filtered = autocompleteEmailData.filter((l) =>
l.toLowerCase().startsWith(lowerCaseQuery)
);
res.json({items: filtered});
});
app.use('/form/verify-search-json/post', (req, res) => {
cors.assertCors(req, res, ['POST']);
const form = new formidable.IncomingForm();
form.parse(req, (_err, fields) => {
res.setHeader('Content-Type', 'application/json; charset=utf-8');
const errors = [];
if (!fields.phone.match(/^650/)) {
errors.push({name: 'phone', message: 'Phone must start with 650'});
}
if (fields.name !== 'Frank') {
errors.push({name: 'name', message: 'Please set your name to be Frank'});
}
if (fields.error === 'true') {
errors.push({message: 'You asked for an error, you get an error.'});
}
if (fields.city !== 'Mountain View' || fields.zip !== '94043') {
errors.push({
name: 'city',
message: "City doesn't match zip (Mountain View and 94043)",
});
}
if (errors.length === 0) {
res.end(
JSON.stringify({
results: [
{title: 'Result 1'},
{title: 'Result 2'},
{title: 'Result 3'},
],
committed: true,
})
);
} else {
res.statusCode = 400;
res.end(JSON.stringify({verifyErrors: errors}));
}
});
});
/**
* Fetches an AMP document from the AMP proxy and replaces JS
* URLs, so that they point to localhost.
*
* @param {express.Request} req
* @param {express.Response} res
* @param {string} mode
* @return {Promise<void>}
*/
async function proxyToAmpProxy(req, res, mode) {
const url =
'https://cdn.ampproject.org/' +
(req.query['amp_js_v'] ? 'v' : 'c') +
req.url;
logWithoutTimestamp('Fetching URL: ' + url);
const urlResponse = await fetch(url);
let body = await urlResponse.text();
body = body
// Unversion URLs.
.replace(
/https\:\/\/cdn\.ampproject\.org\/rtv\/\d+\//g,
'https://cdn.ampproject.org/'
)
// <base> href pointing to the proxy, so that images, etc. still work.
.replace('<head>', '<head><base href="https://cdn.ampproject.org/">');
const inabox = req.query['inabox'];
const urlPrefix = getUrlPrefix(req);
if (req.query['mraid']) {
body = body
.replace(
'</head>',
'<script async host-service="amp-mraid" src="https://cdn.ampproject.org/v0/amp-mraid-0.1.js">' +
'</script>' +
'</head>'
)
// Change cdnUrl from the default so amp-mraid requests the (mock)
// mraid.js from the local server. In a real environment this doesn't
// matter as the local environment would intercept this request.
.replace(
'<head>',
' <head>' +
' <script>' +
' window.AMP_CONFIG = {' +
` cdnUrl: "${urlPrefix}",` +
' };' +
' </script>'
);
}
if (inabox) {
body = toInaboxDocument(body);
// Allow CORS requests for A4A.
const origin = req.headers.origin || urlPrefix;
cors.enableCors(req, res, origin);
}
body = replaceUrls(mode, body, urlPrefix);
res.status(urlResponse.status).send(body);
}
let itemCtr = 2;
const doctype = '<!doctype html>\n';
const liveListDocs = Object.create(null);
app.use('/examples/live-list-update(-reverse)?.amp.html', (req, res, next) => {
const mode = SERVE_MODE;
let liveListDoc = liveListDocs[req.baseUrl];
if (mode != 'minified' && mode != 'default') {
// Only handle compile(prev min)/default (prev max) mode
next();
return;
}
// When we already have state in memory and user refreshes page, we flush
// the dom we maintain on the server.
if (!('amp_latest_update_time' in req.query) && liveListDoc) {
let outerHTML = liveListDoc.documentElement./*OK*/ outerHTML;
outerHTML = replaceUrls(mode, outerHTML);
res.send(`${doctype}${outerHTML}`);
return;
}
if (!liveListDoc) {
const liveListUpdateFullPath = `${pc.cwd()}${req.baseUrl}`;
logWithoutTimestamp('liveListUpdateFullPath', liveListUpdateFullPath);
const liveListFile = fs.readFileSync(liveListUpdateFullPath);
liveListDoc = liveListDocs[req.baseUrl] = new jsdom.JSDOM(
liveListFile
).window.document;
liveListDoc.ctr = 0;
}
const liveList = liveListDoc.querySelector('#my-live-list');
const perPage = Number(liveList.getAttribute('data-max-items-per-page'));
const items = liveList.querySelector('[items]');
const pagination = liveListDoc.querySelector('#my-live-list [pagination]');
const item1 = liveList.querySelector('#list-item-1');
if (liveListDoc.ctr != 0) {
if (Math.random() < 0.8) {
// Always run a replace on the first item
liveListReplace(item1);
if (Math.random() < 0.5) {
liveListTombstone(liveList);
}
if (Math.random() < 0.8) {
liveListInsert(liveList, item1);
}
pagination.textContent = '';
const liveChildren = [].slice
.call(items.children)
.filter((x) => !x.hasAttribute('data-tombstone'));
const pageCount = Math.ceil(liveChildren.length / perPage);
const pageListItems = Array.apply(null, Array(pageCount))
.map((_, i) => `<li>${i + 1}</li>`)
.join('');
const newPagination =
'<nav aria-label="amp live list pagination">' +
`<ul class="pagination">${pageListItems}</ul>` +
'</nav>';
pagination./*OK*/ innerHTML = newPagination;
} else {
// Sometimes we want an empty response to simulate no changes.
res.send(`${doctype}<html></html>`);
return;
}
}
let outerHTML = liveListDoc.documentElement./*OK*/ outerHTML;
outerHTML = replaceUrls(mode, outerHTML);
liveListDoc.ctr++;
res.send(`${doctype}${outerHTML}`);
});
/**
* @param {Element} item
*/
function liveListReplace(item) {
item.setAttribute('data-update-time', Date.now().toString());
const itemContents = item.querySelectorAll('.content');
itemContents[0].textContent = Math.floor(Math.random() * 10).toString();
itemContents[1].textContent = Math.floor(Math.random() * 10).toString();
}
/**
* @param {Element} liveList
* @param {Element} node
*/
function liveListInsert(liveList, node) {
const iterCount = Math.floor(Math.random() * 2) + 1;
logWithoutTimestamp(`inserting ${iterCount} item(s)`);
for (let i = 0; i < iterCount; i++) {
/**
* TODO(#28387) this type cast may be hiding a bug.
* @type {Element}
*/
const child = /** @type {*} */ (node.cloneNode(true));
child.setAttribute('id', `list-item-${itemCtr++}`);
child.setAttribute('data-sort-time', Date.now().toString());
liveList.querySelector('[items]')?.appendChild(child);
}
}
/**
* @param {Element} liveList
*/
function liveListTombstone(liveList) {
const tombstoneId = Math.floor(Math.random() * itemCtr);
logWithoutTimestamp(`trying to tombstone #list-item-${tombstoneId}`);
// We can tombstone any list item except item-1 since we always do a
// replace example on item-1.
if (tombstoneId != 1) {
const item = liveList./*OK*/ querySelector(`#list-item-${tombstoneId}`);
if (item) {
item.setAttribute('data-tombstone', '');
}
}
}
/**
* Generate a random number between min and max
* Value is inclusive of both min and max values.
*
* @param {number} min
* @param {number} max
* @return {number}
*/
function range(min, max) {
const values = Array.apply(null, new Array(max - min + 1)).map(
(_, i) => min + i
);
return values[Math.round(Math.random() * (max - min))];
}
/**
* Returns the result of a coin flip, true or false
*
* @return {boolean}
*/
function flip() {
return !!Math.floor(Math.random() * 2);
}
/**
* @return {string}
*/
function getLiveBlogItem() {
const now = Date.now();
// Generate a 3 to 7 worded headline
const headline = bacon(range(3, 7));
const numOfParagraphs = range(1, 2);
const body = Array.apply(null, new Array(numOfParagraphs))
.map(() => {
return `<p>${bacon(range(50, 90))}</p>`;
})
.join('\n');
const img = `<amp-img
src="${
flip()
? 'https://placekitten.com/300/350'
: 'https://baconmockup.com/300/350'
}"
layout="responsive"
height="300" width="350">
</amp-img>`;
return `<!doctype html>
<html amp><body>
<amp-live-list id="live-blog-1">
<div items>
<div id="live-blog-item-${now}" data-sort-time="${now}">
<h3 class="headline">
<a href="#live-blog-item-${now}">${headline}</a>
</h3>
<div class="author">
<div class="byline">
<p>
by <span itemscope itemtype="http://schema.org/Person"
itemprop="author"><b>Lorem Ipsum</b>
<a class="mailto" href="mailto:lorem.ipsum@">
lorem.ipsum@</a></span>
</p>
<p class="brand">PublisherName News Reporter<p>
<p><span itemscope itemtype="http://schema.org/Date"
itemprop="Date">
${new Date(now).toString().replace(/ GMT.*$/, '')}
<span></p>
</div>
</div>
<div class="article-body">${body}</div>
${img}
<div class="social-box">
<amp-social-share type="facebook"
data-param-text="Hello world"
data-param-href="https://example.test/?ref=URL"
data-param-app_id="145634995501895"></amp-social-share>
<amp-social-share type="twitter"></amp-social-share>
</div>
</div>
</div>
</amp-live-list></body></html>`;
}
/**
* @return {string}
*/
function getLiveBlogItemWithBindAttributes() {
const now = Date.now();
// Generate a 3 to 7 worded headline
const numOfParagraphs = range(1, 2);
const body = Array.apply(null, new Array(numOfParagraphs))
.map(() => {
return `<p>${bacon(range(50, 90))}</p>`;
})
.join('\n');
return `<!doctype html>
<html amp><body>
<amp-live-list id="live-blog-1">
<div items>
<div id="live-blog-item-${now}" data-sort-time="${now}">
<div class="article-body">
${body}
<p> As you can see, bacon is far superior to
<b><span [text]='favoriteFood'>everything!</span></b>!</p>
</div>
</div>
</div>
</amp-live-list></body></html>`;
}
app.use(
'/examples/live-blog(-non-floating-button)?.amp.html',
(req, res, next) => {
if ('amp_latest_update_time' in req.query) {
res.setHeader('Content-Type', 'text/html');
res.end(getLiveBlogItem());
return;
}
next();
}
);
app.use('/examples/bind/live-list.amp.html', (req, res, next) => {
if ('amp_latest_update_time' in req.query) {
res.setHeader('Content-Type', 'text/html');
res.end(getLiveBlogItemWithBindAttributes());
return;
}
next();
});
app.use('/impression-proxy/', (req, res) => {
cors.assertCors(req, res, ['GET']);
// Fake response with the following optional fields:
// location: The Url the that server would have sent redirect to w/o ALP
// tracking_url: URL that should be requested to track click
// gclid: The conversion tracking value
const body = {
'location': 'localhost:8000/examples/?gclid=1234&foo=bar&example=123',
'tracking_url': 'tracking_url',
'gclid': '1234',
};
res.send(body);
// Or fake response with status 204 if viewer replaceUrl is provided
});
/**
* Acts in a similar fashion to /serve_mode_change. Saves
* analytics requests via /run-variable-substitution, and
* then returns the encoded/substituted/replaced request
* via /get-variable-request.
*/
// Saves the variables input to be used in run-variable-substitution
app.get('/save-variables', saveVariables);
// Creates an iframe with amp-analytics. Analytics request
// uses save-variable-request as its endpoint.
app.get('/run-variable-substitution', runVariableSubstitution);
// Saves the analytics request to the dev server.
app.get('/save-variable-request', saveVariableRequest);
// Returns the saved analytics request.
app.get('/get-variable-request', getVariableRequest);
let forcePromptOnNext = false;
app.post('/get-consent-v1/', (req, res) => {
cors.assertCors(req, res, ['POST']);
const body = {
'promptIfUnknown': true,
'purposeConsentRequired': ['purpose-foo', 'purpose-bar'],
'forcePromptOnNext': forcePromptOnNext,
'sharedData': {
'tfua': true,
'coppa': true,
},
};
res.json(body);
});
app.get('/get-consent-v1-set/', (req, res) => {
cors.assertCors(req, res, ['GET']);
const value = req.query['forcePromptOnNext'];
if (value == 'false' || value == '0') {
forcePromptOnNext = false;
} else {
forcePromptOnNext = true;
}
res.json({});
res.end();
});
app.post('/get-consent-no-prompt/', (req, res) => {
cors.assertCors(req, res, ['POST']);
const body = {};
res.json(body);
});
app.post('/check-consent', (req, res) => {
cors.assertCors(req, res, ['POST']);
const response = {
'consentRequired': req.query.consentRequired === 'true',
'consentStateValue': req.query.consentStateValue,
'consentString': req.query.consentString,
'expireCache': req.query.expireCache === 'true',
};
if (req.query.consentMetadata) {
response['consentMetadata'] = JSON.parse(
req.query.consentMetadata.replace(/'/g, '"')
);
}
res.json(response);
});
// Proxy with local JS.
// Example:
// http://localhost:8000/proxy/s/www.washingtonpost.com/amphtml/news/post-politics/wp/2016/02/21/bernie-sanders-says-lower-turnout-contributed-to-his-nevada-loss-to-hillary-clinton/
app.use('/proxy/', (req, res) => proxyToAmpProxy(req, res, SERVE_MODE));
// Nest the response in an iframe.
// Example:
// http://localhost:8000/iframe/examples/ads.amp.html
app.get('/iframe/*', (req, res) => {
// Returns an html blob with an iframe pointing to the url after /iframe/.
res.send(`<!doctype html>
<html style="width:100%; height:100%;">
<body style="width:98%; height:98%;">
<iframe src="${req.url.substr(7)}"
style="width:100%; height:100%;">
</iframe>
</body>
</html>`);
});
app.get('/a4a_template/*', (req, res) => {
cors.assertCors(req, res, ['GET']);
const match = /^\/a4a_template\/([a-z-]+)\/(\d+)$/.exec(req.path);
if (!match) {
res.status(404);
@@ -916,1 +917,1 @@
- res.end('Invalid path: ' + req.path);
+ res.end('Invalid path: ' + escape(req.path));
return;
}
const filePath =
`${pc.cwd()}/extensions/amp-ad-network-${match[1]}-impl/` +
`0.1/data/${match[2]}.template`;
fs.promises
.readFile(filePath)
.then((file) => {
res.setHeader('Content-Type', 'application/json');
res.setHeader('AMP-template-amp-creative', 'amp-mustache');
res.end(file);
})
.catch(() => {
res.status(404);
res.end('Not found: ' + filePath);
});
});
// Returns a document that echoes any post messages received from parent.
// An optional `message` query param can be appended for an initial post
// message sent on document load.
// Example:
// http://localhost:8000/iframe-echo-message?message=${payload}
app.get('/iframe-echo-message', (req, res) => {
const {message} = req.query;
res.send(
`<!doctype html>
<body style="background-color: yellow">
<script>
if (${message}) {
echoMessage(${message});
}
window.addEventListener('message', function(event) {
echoMessage(event.data);
});
function echoMessage(message) {
parent.postMessage(message, '*');
}
</script>
</body>
</html>`
);
});
/**
* Append ?sleep=5 to any included JS file in examples to emulate delay in
* loading that file. This allows you to test issues with your extension being
* late to load and testing user interaction with your element before your code
* loads.
*
* Example delay loading amp-form script by 5 seconds:
* <script async custom-element="amp-form"
* src="https://cdn.ampproject.org/v0/amp-form-0.1.js?sleep=5"></script>
*/
app.use(['/dist/v0/amp-*.(m?js)', '/dist/amp*.(m?js)'], (req, _res, next) => {
const sleep = parseInt(req.query.sleep || 0, 10) * 1000;
setTimeout(next, sleep);
});
/**
* Disable caching for extensions if the --no_caching_extensions flag is used.
*/
app.get(['/dist/v0/amp-*.(m?js)'], (_req, res, next) => {
if (argv.no_caching_extensions) {
res.header('Cache-Control', 'no-store');
}
next();
});
/**
* Video testbench endpoint
*/
app.get('/test/manual/amp-video.amp.html', runVideoTestBench);
app.get(
[
'/examples/(**/)?*.html',
'/test/manual/(**/)?*.html',
'/test/fixtures/e2e/(**/)?*.html',
'/test/fixtures/performance/(**/)?*.html',
],
(req, res, next) => {
const filePath = req.path;
const mode = SERVE_MODE;
const inabox = req.query['inabox'];
const stream = Number(req.query['stream']);
const componentVersion = req.query['componentVersion'];
const urlPrefix = getUrlPrefix(req);
fs.promises
.readFile(pc.cwd() + filePath, 'utf8')
.then((file) => {
if (req.query['amp_js_v']) {
file = addViewerIntegrationScript(req.query['amp_js_v'], file);
}
if (req.query['mraid']) {
file = file
.replace(
'</head>',
'<script async host-service="amp-mraid" src="https://cdn.ampproject.org/v0/amp-mraid-0.1.js">' +
'</script>' +
'</head>'
)
.replace(
'<head>',
' <head>' +
' <script>' +
' window.AMP_CONFIG = {' +
` cdnUrl: "${urlPrefix}",` +
' };' +
' </script>'
);
}
file = file.replace(/__TEST_SERVER_PORT__/g, TEST_SERVER_PORT);
if (componentVersion) {
file = file.replace(/-latest.js/g, `-${componentVersion}.js`);
}
if (inabox) {
file = toInaboxDocument(file);
// Allow CORS requests for A4A.
if (req.headers.origin) {
cors.enableCors(req, res, req.headers.origin);
}
}
file = replaceUrls(mode, file);
const ampExperimentsOptIn = req.query['exp'];
if (ampExperimentsOptIn) {
file = file.replace(
'<head>',
`<head><meta name="amp-experiments-opt-in" content="${ampExperimentsOptIn}">`
);
}
// Extract amp-ad for the given 'type' specified in URL query.
if (req.path.indexOf('/examples/ads.amp.html') == 0 && req.query.type) {
const ads =
file.match(
elementExtractor('(amp-ad|amp-embed)', req.query.type)
) ?? [];
file = file.replace(
/<body>[\s\S]+<\/body>/m,
'<body>' + ads.join('') + '</body>'
);
}
// Extract amp-analytics for the given 'type' specified in URL query.
if (
req.path.indexOf('/examples/analytics-vendors.amp.html') == 0 &&
req.query.type
) {
const analytics =
file.match(elementExtractor('amp-analytics', req.query.type)) ?? [];
file = file.replace(
/<div id="container">[\s\S]+<\/div>/m,
'<div id="container">' + analytics.join('') + '</div>'
);
}
// Extract amp-consent for the given 'type' specified in URL query.
if (
req.path.indexOf('/examples/amp-consent/cmp-vendors.amp.html') == 0 &&
req.query.type
) {
const consent =
file.match(elementExtractor('amp-consent', req.query.type)) ?? [];
file = file.replace(
/<div id="container">[\s\S]+<\/div>/m,
'<div id="container">' + consent.join('') + '</div>'
);
}
if (stream > 0) {
res.writeHead(200, {'Content-Type': 'text/html'});
let pos = 0;
const writeChunk = function () {
const chunk = file.substring(
pos,
Math.min(pos + stream, file.length)
);
res.write(chunk);
pos += stream;
if (pos < file.length) {
setTimeout(writeChunk, 500);
} else {
res.end();
}
};
writeChunk();
} else {
res.send(file);
}
})
.catch(() => {
next();
});
}
);
/**
* @param {string} string
* @return {string}
*/
function escapeRegExp(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
/**
* @param {string} tagName
* @param {string} type
* @return {RegExp}
*/
function elementExtractor(tagName, type) {
type = escapeRegExp(type);
return new RegExp(
`<${tagName}[\\s][^>]*['"]${type}['"][^>]*>([\\s\\S]+?)</${tagName}>`,
'gm'
);
}
// Data for example: http://localhost:8000/examples/bind/xhr.amp.html
app.use('/bind/form/get', (req, res) => {
cors.assertCors(req, res, ['GET']);
res.json({
bindXhrResult: 'I was fetched from the server!',
});
});
// Data for example: http://localhost:8000/examples/bind/ecommerce.amp.html
app.use('/bind/ecommerce/sizes', (req, res) => {
cors.assertCors(req, res, ['GET']);
setTimeout(() => {
const prices = {
'0': {
'sizes': {
'XS': 8.99,
'S': 9.99,
},
},
'1': {
'sizes': {
'S': 10.99,
'M': 12.99,
'L': 14.99,
},
},
'2': {
'sizes': {
'L': 11.99,
'XL': 13.99,
},
},
'3': {
'sizes': {
'M': 7.99,
'L': 9.99,
'XL': 11.99,
},
},
'4': {
'sizes': {
'XS': 8.99,
'S': 10.99,
'L': 15.99,
},
},
'5': {
'sizes': {
'S': 8.99,
'L': 14.99,
'XL': 11.99,
},
},
'6': {
'sizes': {
'XS': 8.99,
'S': 9.99,
'M': 12.99,
},
},
'7': {
'sizes': {
'M': 10.99,
'L': 11.99,
},
},
};
const object = {};
object[req.query.shirt] = prices[req.query.shirt];
res.json(object);
}, 1000); // Simulate network delay.
});
/**
* Simulates a publisher's metering state store.
* (amp-subscriptions)
* @type {{[ampReaderId: string]: {}}}
*/
const meteringStateStore = {};
// Simulate a publisher's entitlements API.
// (amp-subscriptions)
app.use('/subscription/:id/entitlements', (req, res) => {
cors.assertCors(req, res, ['GET']);
// Create entitlements response.
const source = 'local' + req.params.id;
const granted = req.params.id > 0;
const grantReason = granted ? 'SUBSCRIBER' : 'NOT_SUBSCRIBER';
const decryptedDocumentKey = decryptDocumentKey(req.query.crypt);
const response = {
source,
granted,
grantReason,
data: {
login: true,
},
decryptedDocumentKey,
};
// Store metering state, if possible.
const ampReaderId = req.query.rid;
if (ampReaderId && req.query.meteringState) {
// Parse metering state from encoded Base64 string.
const encodedMeteringState = req.query.meteringState;
const decodedMeteringState = Buffer.from(
encodedMeteringState,
'base64'
).toString();
const meteringState = JSON.parse(decodedMeteringState);
// Store metering state.
meteringStateStore[ampReaderId] = meteringState;
}
// Add metering state to response, if possible.
if (meteringStateStore[ampReaderId]) {
response.metering = {
state: meteringStateStore[ampReaderId],
};
}
res.json(response);
});
// Simulate a publisher's SKU map API.
// (amp-subscriptions)
app.use('/subscriptions/skumap', (req, res) => {
cors.assertCors(req, res, ['GET']);
res.json({
'subscribe.google.com': {
'subscribeButtonSimple': {
'sku': 'basic',
},
'subscribeButtonCarousel': {
'carouselOptions': {
'skus': ['basic', 'premium_monthly'],
},
},
},
});
});
// Simulate a publisher's pingback API.
// (amp-subscriptions)
app.use('/subscription/pingback', (req, res) => {
cors.assertCors(req, res, ['POST']);
res.json({
done: true,
});
});
/*
Simulate a publisher's account registration API.
The `amp-subscriptions-google` extension sends this API a POST request.
The request body looks like:
{
"googleSignInDetails": {
// This signed JWT contains information from Google Sign-In
"idToken": "...JWT from Google Sign-In...",
// Some useful fields from the `idToken`, pre-parsed for convenience
"name": "Jane Smith",
"givenName": "Jane",
"familyName": "Smith",
"imageUrl": "https://imageurl",
"email": "[email protected]"
},
// Associate this ID with the registration. Use it to look up metering state
// for future entitlements requests
// https://github.com/ampproject/amphtml/blob/main/extensions/amp-subscriptions/amp-subscriptions.md#combining-the-amp-reader-id-with-publisher-cookies
"ampReaderId": "amp-s0m31d3nt1f13r"
}
(amp-subscriptions-google)
*/
app.use('/subscription/register', (req, res) => {
cors.assertCors(req, res, ['POST']);
// Generate a new ID for this metering state.
const meteringStateId = 'ppid' + Math.round(Math.random() * 99999999);
// Define registration timestamp.
//
// For demo purposes, set timestamp to 30 seconds ago.
// This causes Metering Toast to show immediately,
// which helps engineers test metering.
const registrationTimestamp = Math.round(Date.now() / 1000) - 30000;
// Store metering state.
//
// For demo purposes, just save this in memory.
// Production systems should persist this.
meteringStateStore[req.body.ampReaderId] = {
id: meteringStateId,
standardAttributes: {
// eslint-disable-next-line local/camelcase
registered_user: {
timestamp: registrationTimestamp, // In seconds.
},
},
};
res.json({
metering: {
state: meteringStateStore[req.body.ampReaderId],
},
});
});
// Simulated adzerk ad server and AMP cache CDN.
app.get('/adzerk/*', (req, res) => {
cors.assertCors(req, res, ['GET'], ['AMP-template-amp-creative']);
const match = /\/(\d+)/.exec(req.path);
if (!match || !match[1]) {
res.status(404);
res.end('Invalid path: ' + req.path);
return;
}
const filePath =
pc.cwd() + '/extensions/amp-ad-network-adzerk-impl/0.1/data/' + match[1];
fs.promises
.readFile(filePath)
.then((file) => {
res.setHeader('Content-Type', 'application/json');
res.setHeader('AMP-Ad-Template-Extension', 'amp-mustache');
res.setHeader('AMP-Ad-Response-Type', 'template');
res.end(file);
})
.catch(() => {
res.status(404);
res.end('Not found: ' + filePath);
});
});
app.get('/dist/*.mjs', (req, res, next) => {
// Allow CORS access control explicitly for mjs files
cors.enableCors(req, res);
next();
});
/*
* Serve extension scripts and their source maps.
*/
app.get(
['/dist/rtv/*/v0/*.(m?js)', '/dist/rtv/*/v0/*.(m?js).map'],
async (req, res, next) => {
const mode = SERVE_MODE;
const fileName = path.basename(req.path).replace('.max.', '.');
let filePath = 'https://cdn.ampproject.org/v0/' + fileName;
if (await passthroughServeModeCdn(res, filePath)) {
return;
}
const isJsMap = filePath.endsWith('.map');
if (isJsMap) {
filePath = filePath.replace(/\.(m?js)\.map$/, '.$1');
}
filePath = replaceUrls(mode, filePath);
req.url = filePath + (isJsMap ? '.map' : '');
next();
}
);
/**
* Handle amp-story translation file requests with an rtv path.
* We need to make sure we only handle the amp-story requests since this
* can affect other tests with json requests.
*/
app.get('/dist/rtv/*/v0/amp-story*.json', async (req, _res, next) => {
const fileName = path.basename(req.path);
let filePath = 'https://cdn.ampproject.org/v0/' + fileName;
filePath = replaceUrls(SERVE_MODE, filePath);
req.url = filePath;
next();
});
if (argv.coverage === 'live') {
app.get('/dist/amp.js', async (req, res) => {
const ampJs = await fs.promises.readFile(`${pc.cwd()}${req.path}`);
res.setHeader('Content-Type', 'text/javascript');
res.setHeader('Access-Control-Allow-Origin', '*');
// Append an unload handler that reports coverage information each time you
// leave a page.
res.end(`${ampJs};
window.addEventListener('beforeunload', (evt) => {
const COV_REPORT_URL = 'http://localhost:${TEST_SERVER_PORT}/coverage/client';
console.info('POSTing code coverage to', COV_REPORT_URL);
const xhr = new XMLHttpRequest();
xhr.open('POST', COV_REPORT_URL, true);
xhr.setRequestHeader('Content-type', 'application/json');
xhr.send(JSON.stringify(window.__coverage__));
// Required by Chrome
evt.returnValue = '';
return null;
});`);
});
}
app.get('/dist/ww.(m?js)', async (req, res, next) => {
// Special case for entry point script url. Use minified for testing
const mode = SERVE_MODE;
const fileName = path.basename(req.path);
if (await passthroughServeModeCdn(res, fileName)) {
return;
}
if (mode == 'default') {
req.url = req.url.replace(/\.(m?js)$/, '.max.$1');
}
next();
});
app.get('/dist/iframe-transport-client-lib.(m?js)', (req, _res, next) => {
req.url = req.url.replace(/dist/, 'dist.3p/current');
next();
});
app.get('/dist/amp-inabox-host.(m?js)', (req, _res, next) => {
const mode = SERVE_MODE;
if (mode != 'default') {
req.url = req.url.replace('amp-inabox-host', 'amp4ads-host-v0');
}
next();
});
app.get('/mraid.js', (req, _res, next) => {
req.url = req.url.replace('mraid.js', 'examples/mraid/mraid.js');
next();
});
/**
* Shadow viewer. Fetches shadow runtime from cdn by default.
* Setting the param useLocal=1 will load the runtime from the local build.
*/
app.use('/shadow/', (req, res) => {
const {url} = req;
const isProxyUrl = /^\/proxy\//.test(url);
const baseHref = isProxyUrl
? 'https://cdn.ampproject.org/'
: `${path.dirname(url)}/`;
const viewerHtml = renderShadowViewer({
src: '//' + req.hostname + '/' + req.url.replace(/^\//, ''),
baseHref,
});
if (!req.query.useLocal) {
res.end(viewerHtml);
return;
}
res.end(replaceUrls(SERVE_MODE, viewerHtml));
});
app.use('/mraid/', (req, res) => {
res.redirect(req.url + '?inabox=1&mraid=1');
});
/**
* @param {string} ampJsVersionString
* @param {string} file
* @return {string}
*/
function addViewerIntegrationScript(ampJsVersionString, file) {
const ampJsVersion = parseFloat(ampJsVersionString);
if (!ampJsVersion) {
return file;
}
let viewerScript;
// eslint-disable-next-line local/no-es2015-number-props
if (Number.isInteger(ampJsVersion)) {
// Viewer integration script from gws, such as
// https://cdn.ampproject.org/viewer/google/v7.js
viewerScript =
'<script async src="https://cdn.ampproject.org/viewer/google/v' +
ampJsVersion +
'.js"></script>';
} else {
// Viewer integration script from runtime, such as
// https://cdn.ampproject.org/v0/amp-viewer-integration-0.1.js
viewerScript =
'<script async ' +
'src="https://cdn.ampproject.org/v0/amp-viewer-integration-' +
ampJsVersion +
'.js" data-amp-report-test="viewer-integr.js"></script>';
}
file = file.replace('</head>', viewerScript + '</head>');
return file;
}
/**
* @param {express.Request} req
* @return {string}
*/
function getUrlPrefix(req) {
return req.protocol + '://' + req.headers.host;
}
/**
* @param {string} filePath
* @return {string}
*/
function generateInfo(filePath) {
const mode = SERVE_MODE;
filePath = filePath.substr(0, filePath.length - 9) + '.html';
return (
'<h2>Please note that .min/.max is no longer supported</h2>' +
'<h3>Current serving mode is ' +
mode +
'</h3>' +
'<h3>Please go to <a href= ' +
filePath +
'>Unversioned Link</a> to view the page<h3>' +
'<h3></h3>' +
'<h3><a href = /serve_mode=default>' +
'Change to DEFAULT mode (unminified JS)</a></h3>' +
'<h3><a href = /serve_mode=minified>' +
'Change to COMPILED mode (minified JS)</a></h3>' +
'<h3><a href = /serve_mode=cdn>' +
'Change to CDN mode (prod JS)</a></h3>'
);
}
/**
* @param {string} encryptedDocumentKey
* @return {?string}
*/
function decryptDocumentKey(encryptedDocumentKey) {
if (!encryptedDocumentKey) {
return null;
}
const cryptoStart = 'ENCRYPT(';
if (!encryptedDocumentKey.includes(cryptoStart, 0)) {
return null;
}
let jsonString = encryptedDocumentKey.replace(cryptoStart, '');
jsonString = jsonString.substring(0, jsonString.length - 1);
const parsedJson = JSON.parse(jsonString);
if (!parsedJson) {
return null;
}
return parsedJson.key;
}
// serve local vendor config JSON files
app.use(
'(/dist)?/rtv/*/v0/analytics-vendors/:vendor.json',
async (req, res) => {
const {vendor} = req.params;
const serveMode = SERVE_MODE;
const cdnUrl = `https://cdn.ampproject.org/v0/analytics-vendors/${vendor}.json`;
if (await passthroughServeModeCdn(res, cdnUrl)) {
return;
}
const max = serveMode === 'default' ? '.max' : '';
const localPath = `${pc.cwd()}/dist/v0/analytics-vendors/${vendor}${max}.json`;
try {
const file = await fs.promises.readFile(localPath);
res.setHeader('Content-Type', 'application/json');
res.end(file);
} catch (_) {
res.status(404);
res.end('Not found: ' + localPath);
}
}
);
module.exports = app;

  • Create Pull Request

Remediation feedback:

  • 👍 Like
  • 👎 Dislike
Secure Code Warrior Training Material

● Training

   ▪ Secure Code Warrior Cross-Site Scripting Training

● Videos

   ▪ Secure Code Warrior Cross-Site Scripting Video

 
HighCross-Site Scripting

CWE-79

app.js:942

12024-08-19 04:08am
Vulnerable Code

// message sent on document load.
// Example:
// http://localhost:8000/iframe-echo-message?message=${payload}
app.get('/iframe-echo-message', (req, res) => {
const {message} = req.query;
res.send(

1 Data Flow/s detected

app.get('/iframe-echo-message', (req, res) => {

const {message} = req.query;

`<!doctype html>

⛑️ Remediation Suggestion

--- original
+++ remediated
@@ -1,0 +1,1 @@
+const escape = require('escape-html');
'use strict';
/**
* @fileoverview Creates an http server to handle static
* files and list directories for use with the amp live server
*/
const argv = require('minimist')(process.argv.slice(2));
const bacon = require('baconipsum');
const bodyParser = require('body-parser');
const cors = require('./amp-cors');
const devDashboard = require('./app-index');
const express = require('express');
const formidable = require('formidable');
const fs = require('fs');
const jsdom = require('jsdom');
const path = require('path');
const upload = require('multer')();
const pc = process;
const autocompleteEmailData = require('./autocomplete-test-data');
const header = require('connect-header');
const runVideoTestBench = require('./app-video-testbench');
const {
getServeMode,
isRtvMode,
replaceUrls,
toInaboxDocument,
} = require('./app-utils');
const {
getVariableRequest,
runVariableSubstitution,
saveVariableRequest,
saveVariables,
} = require('./variable-substitution');
const {
recaptchaFrameRequestHandler,
recaptchaRouter,
} = require('./recaptcha-router');
const {logWithoutTimestamp} = require('../common/logging');
const {log} = require('../common/logging');
const {red} = require('kleur/colors');
const {renderShadowViewer} = require('./shadow-viewer');
/**
* Respond with content received from a URL when SERVE_MODE is "cdn".
* @param {express.Response} res
* @param {string} cdnUrl
* @return {Promise<boolean>}
*/
async function passthroughServeModeCdn(res, cdnUrl) {
if (SERVE_MODE !== 'cdn') {
return false;
}
try {
const response = await fetch(cdnUrl);
res.status(response.status);
res.send(await response.text());
} catch (e) {
log(red('ERROR:'), e);
res.status(500);
res.end();
}
return true;
}
const app = express();
const TEST_SERVER_PORT = argv.port || 8000;
let SERVE_MODE = getServeMode();
app.use(bodyParser.json());
app.use(bodyParser.text());
// Middleware is executed in order, so this must be at the top.
// TODO(#24333): Migrate all server URL handlers to new-server/router and
// deprecate app.js.
app.use(require('./new-server/router'));
app.use(require('./routes/a4a-envelopes'));
app.use('/amp4test', require('./amp4test').app);
app.use('/analytics', require('./routes/analytics'));
app.use('/list/', require('./routes/list'));
app.use('/test', require('./routes/test'));
if (argv.coverage) {
app.use('/coverage', require('istanbul-middleware').createHandler());
}
// Built binaries should be fetchable from other origins, i.e. Storybook.
app.use(header({'Access-Control-Allow-Origin': '*'}));
// Append ?csp=1 to the URL to turn on the CSP header.
// TODO: shall we turn on CSP all the time?
app.use((req, res, next) => {
if (req.query.csp) {
res.set({
'content-security-policy':
"default-src * blob: data:; script-src https://cdn.ampproject.org/rtv/ https://cdn.ampproject.org/v0.js https://cdn.ampproject.org/v0/ https://cdn.ampproject.org/viewer/ http://localhost:8000 https://localhost:8000; object-src 'none'; style-src 'unsafe-inline' https://cdn.ampproject.org/rtv/ https://cdn.materialdesignicons.com https://cloud.typography.com https://fast.fonts.net https://fonts.googleapis.com https://maxcdn.bootstrapcdn.com https://p.typekit.net https://use.fontawesome.com https://use.typekit.net https://cdnjs.cloudflare.com/ajax/libs/font-awesome/; report-uri https://csp-collector.appspot.com/csp/amp",
});
}
next();
});
/**
*
* @param {string} serveMode
* @return {boolean}
*/
function isValidServeMode(serveMode) {
return (
['default', 'minified', 'cdn', 'esm'].includes(serveMode) ||
isRtvMode(serveMode)
);
}
/**
*
* @param {string} serveMode
*/
function setServeMode(serveMode) {
SERVE_MODE = serveMode;
}
app.get('/serve_mode=:mode', (req, res) => {
const newMode = req.params.mode;
if (isValidServeMode(newMode)) {
setServeMode(newMode);
res.send(`<h2>Serve mode changed to ${newMode}</h2>`);
} else {
const info = '<h2>Serve mode ' + newMode + ' is not supported. </h2>';
res.status(400).send(info);
}
});
if (argv._.includes('integration') && !argv.nobuild) {
setServeMode('minified');
}
if (!(argv._.includes('unit') || argv._.includes('integration'))) {
// Dev dashboard routes break test scaffolding since they're global.
devDashboard.installExpressMiddleware(app);
}
// Changes the current serve mode via query param
// e.g. /serve_mode_change?mode=(default|minified|cdn|<RTV_NUMBER>)
// (See ./app-index/settings.js)
app.get('/serve_mode_change', (req, res) => {
const {mode} = req.query;
if (isValidServeMode(mode)) {
setServeMode(mode);
res.json({ok: true});
return;
}
res.status(400).json({ok: false});
});
// Redirects to a proxied document with optional mode through query params.
//
// Mode can be one of:
// - '/', empty string, or unset for an unwrapped doc
// - '/a4a/' for an AMP4ADS wrapper
// - '/a4a-3p/' for a 3P AMP4ADS wrapper
// - '/inabox/' for an AMP inabox wrapper
// - '/shadow/' for a shadow-wrapped document
//
// Examples:
// - /proxy/?url=hello.com 👉 /proxy/s/hello.com
// - /proxy/?url=hello.com?mode=/shadow/ 👉 /shadow/proxy/s/hello.com
// - /proxy/?url=https://hello.com 👉 /proxy/s/hello.com
// - /proxy/?url=https://www.google.com/amp/s/hello.com 👉 /proxy/s/hello.com
// - /proxy/?url=hello.com/canonical 👉 /proxy/s/hello.com/amp
//
// This passthrough is useful to generate the URL from <form> values,
// (See ./app-index/proxy-form.js)
app.get('/proxy', async (req, res, next) => {
const {mode, url} = req.query;
const urlSuffixClearPrefixReStr =
'^https?://((www.)?google.(com?|[a-z]{2}|com?.[a-z]{2}|cat)/amp/s/)?';
const urlSuffix = url.replace(new RegExp(urlSuffixClearPrefixReStr, 'i'), '');
try {
const ampdocUrl = await requestAmphtmlDocUrl(urlSuffix);
const ampdocUrlSuffix = ampdocUrl.replace(/^https?:\/\//, '');
const modePrefix = (mode || '').replace(/\/$/, '');
const proxyUrl = `${modePrefix}/proxy/s/${ampdocUrlSuffix}`;
res.redirect(proxyUrl);
} catch ({message}) {
logWithoutTimestamp(`ERROR: ${message}`);
next();
}
});
/**
* Resolves an AMPHTML URL from a canonical URL. If AMPHTML is canonical, same
* URL is returned.
* @param {string} urlSuffix URL without protocol or google.com/amp/s/...
* @param {string=} protocol 'https' or 'http'. 'https' retries using 'http'.
* @return {!Promise<string>}
*/
async function requestAmphtmlDocUrl(urlSuffix, protocol = 'https') {
const defaultUrl = `${protocol}://${urlSuffix}`;
logWithoutTimestamp(`Fetching URL: ${defaultUrl}`);
const response = await fetch(defaultUrl);
if (!response.ok) {
if (protocol == 'https') {
return requestAmphtmlDocUrl(urlSuffix, 'http');
}
throw new Error(`Status: ${response.status}`);
}
const {window} = new jsdom.JSDOM(await response.text());
const linkRelAmphtml = window.document.querySelector('link[rel=amphtml]');
const amphtmlUrl = linkRelAmphtml && linkRelAmphtml.getAttribute('href');
return amphtmlUrl || defaultUrl;
}
/*
* Intercept Recaptcha frame for,
* integration tests. Using this to mock
* out the recaptcha api.
*/
app.get('/dist.3p/*/recaptcha.*html', recaptchaFrameRequestHandler);
app.use('/recaptcha', recaptchaRouter);
// Deprecate usage of .min.html/.max.html
app.get(
[
'/examples/*.(min|max).html',
'/test/manual/*.(min|max).html',
'/test/fixtures/e2e/*/*.(min|max).html',
],
(req, res) => {
const filePath = req.url;
res.send(generateInfo(filePath));
}
);
app.use('/pwa', (req, res) => {
let file;
let contentType;
if (!req.url || req.path == '/') {
// pwa.html
contentType = 'text/html';
file = '/examples/pwa/pwa.html';
} else if (req.url == '/pwa.js') {
// pwa.js
contentType = 'application/javascript';
file = '/examples/pwa/pwa.js';
} else if (req.url == '/pwa-sw.js') {
// pwa.js
contentType = 'application/javascript';
file = '/examples/pwa/pwa-sw.js';
} else if (req.url == '/ampdoc-shell') {
// pwa-ampdoc-shell.html
contentType = 'text/html';
file = '/examples/pwa/pwa-ampdoc-shell.html';
} else {
// Redirect to the underlying resource.
// TODO(dvoytenko): would be nicer to do forward instead of redirect.
res.writeHead(302, {'Location': req.url});
res.end();
return;
}
res.statusCode = 200;
res.setHeader('Content-Type', contentType);
fs.promises.readFile(pc.cwd() + file).then((file) => {
res.end(file);
});
});
app.use('/api/show', (_req, res) => {
res.json({
showNotification: true,
});
});
app.use('/api/dont-show', (_req, res) => {
res.json({
showNotification: false,
});
});
app.use('/api/echo/query', (req, res) => {
res.json(JSON.parse(req.query.data));
});
app.use('/api/echo/post', (req, res) => {
res.setHeader('Content-Type', 'application/json');
res.end(req.body);
});
app.use('/api/ping', (_req, res) => {
res.status(204).end();
});
app.use('/form/html/post', (req, res) => {
cors.assertCors(req, res, ['POST']);
const form = new formidable.IncomingForm();
form.parse(req, (_err, fields) => {
res.setHeader('Content-Type', 'text/html');
if (fields['email'] == '[email protected]') {
res.statusCode = 500;
res.end(`
<h1 style="color:red;">Sorry ${fields['name']}!</h1>
<p>The email ${fields['email']} is already subscribed!</p>
`);
} else {
res.end(`
<h1>Thanks ${fields['name']}!</h1>
<p>Please make sure to confirm your email ${fields['email']}</p>
`);
}
});
});
app.use('/form/redirect-to/post', (req, res) => {
cors.assertCors(req, res, ['POST'], ['AMP-Redirect-To']);
res.setHeader('AMP-Redirect-To', 'https://google.com');
res.end('{}');
});
app.use('/form/echo-json/post', (req, res) => {
cors.assertCors(req, res, ['POST']);
const form = new formidable.IncomingForm();
const fields = Object.create(null);
form.on('field', function (name, value) {
if (!(name in fields)) {
fields[name] = value;
return;
}
const realName = name;
if (realName in fields) {
if (!Array.isArray(fields[realName])) {
fields[realName] = [fields[realName]];
}
} else {
fields[realName] = [];
}
fields[realName].push(value);
});
form.parse(req, () => {
res.setHeader('Content-Type', 'application/json; charset=utf-8');
if (fields['email'] == '[email protected]') {
res.statusCode = 500;
}
res.end(JSON.stringify(fields));
});
});
app.use('/form/json/poll1', (req, res) => {
cors.assertCors(req, res, ['POST']);
const form = new formidable.IncomingForm();
form.parse(req, () => {
res.setHeader('Content-Type', 'application/json');
res.end(
JSON.stringify({
result: [
{
answer: 'Penguins',
percentage: new Array(77),
},
{
answer: 'Ostriches',
percentage: new Array(8),
},
{
answer: 'Kiwis',
percentage: new Array(14),
},
{
answer: 'Wekas',
percentage: new Array(1),
},
],
})
);
});
});
app.post('/form/json/upload', upload.fields([{name: 'myFile'}]), (req, res) => {
cors.assertCors(req, res, ['POST']);
const myFile = req.files['myFile'];
if (!myFile) {
res.json({message: 'No file data received'});
return;
}
const fileData = myFile[0];
const contents = fileData.buffer.toString();
res.json({message: contents});
});
app.use('/form/search-html/get', (_req, res) => {
res.setHeader('Content-Type', 'text/html');
res.end(`
<h1>Here's results for your search<h1>
<ul>
<li>Result 1</li>
<li>Result 2</li>
<li>Result 3</li>
</ul>
`);
});
app.use('/form/search-json/get', (req, res) => {
cors.assertCors(req, res, ['GET']);
res.json({
term: req.query.term,
additionalFields: req.query.additionalFields,
results: [{title: 'Result 1'}, {title: 'Result 2'}, {title: 'Result 3'}],
});
});
const autocompleteColors = [
'red',
'orange',
'yellow',
'green',
'blue',
'purple',
'pink',
'black',
'white',
];
app.use('/form/autocomplete/query', (req, res) => {
const query = req.query.q;
if (!query) {
res.json({items: autocompleteColors});
} else {
const lowerCaseQuery = query.toLowerCase();
const filtered = autocompleteColors.filter((l) =>
l.toLowerCase().includes(lowerCaseQuery)
);
res.json({items: filtered});
}
});
app.use('/form/autocomplete/error', (_req, res) => {
res.status(500).end();
});
app.use('/form/mention/query', (req, res) => {
const query = req.query.q;
if (!query) {
res.json({items: autocompleteEmailData});
return;
}
const lowerCaseQuery = query.toLowerCase().trim();
const filtered = autocompleteEmailData.filter((l) =>
l.toLowerCase().startsWith(lowerCaseQuery)
);
res.json({items: filtered});
});
app.use('/form/verify-search-json/post', (req, res) => {
cors.assertCors(req, res, ['POST']);
const form = new formidable.IncomingForm();
form.parse(req, (_err, fields) => {
res.setHeader('Content-Type', 'application/json; charset=utf-8');
const errors = [];
if (!fields.phone.match(/^650/)) {
errors.push({name: 'phone', message: 'Phone must start with 650'});
}
if (fields.name !== 'Frank') {
errors.push({name: 'name', message: 'Please set your name to be Frank'});
}
if (fields.error === 'true') {
errors.push({message: 'You asked for an error, you get an error.'});
}
if (fields.city !== 'Mountain View' || fields.zip !== '94043') {
errors.push({
name: 'city',
message: "City doesn't match zip (Mountain View and 94043)",
});
}
if (errors.length === 0) {
res.end(
JSON.stringify({
results: [
{title: 'Result 1'},
{title: 'Result 2'},
{title: 'Result 3'},
],
committed: true,
})
);
} else {
res.statusCode = 400;
res.end(JSON.stringify({verifyErrors: errors}));
}
});
});
/**
* Fetches an AMP document from the AMP proxy and replaces JS
* URLs, so that they point to localhost.
*
* @param {express.Request} req
* @param {express.Response} res
* @param {string} mode
* @return {Promise<void>}
*/
async function proxyToAmpProxy(req, res, mode) {
const url =
'https://cdn.ampproject.org/' +
(req.query['amp_js_v'] ? 'v' : 'c') +
req.url;
logWithoutTimestamp('Fetching URL: ' + url);
const urlResponse = await fetch(url);
let body = await urlResponse.text();
body = body
// Unversion URLs.
.replace(
/https\:\/\/cdn\.ampproject\.org\/rtv\/\d+\//g,
'https://cdn.ampproject.org/'
)
// <base> href pointing to the proxy, so that images, etc. still work.
.replace('<head>', '<head><base href="https://cdn.ampproject.org/">');
const inabox = req.query['inabox'];
const urlPrefix = getUrlPrefix(req);
if (req.query['mraid']) {
body = body
.replace(
'</head>',
'<script async host-service="amp-mraid" src="https://cdn.ampproject.org/v0/amp-mraid-0.1.js">' +
'</script>' +
'</head>'
)
// Change cdnUrl from the default so amp-mraid requests the (mock)
// mraid.js from the local server. In a real environment this doesn't
// matter as the local environment would intercept this request.
.replace(
'<head>',
' <head>' +
' <script>' +
' window.AMP_CONFIG = {' +
` cdnUrl: "${urlPrefix}",` +
' };' +
' </script>'
);
}
if (inabox) {
body = toInaboxDocument(body);
// Allow CORS requests for A4A.
const origin = req.headers.origin || urlPrefix;
cors.enableCors(req, res, origin);
}
body = replaceUrls(mode, body, urlPrefix);
res.status(urlResponse.status).send(body);
}
let itemCtr = 2;
const doctype = '<!doctype html>\n';
const liveListDocs = Object.create(null);
app.use('/examples/live-list-update(-reverse)?.amp.html', (req, res, next) => {
const mode = SERVE_MODE;
let liveListDoc = liveListDocs[req.baseUrl];
if (mode != 'minified' && mode != 'default') {
// Only handle compile(prev min)/default (prev max) mode
next();
return;
}
// When we already have state in memory and user refreshes page, we flush
// the dom we maintain on the server.
if (!('amp_latest_update_time' in req.query) && liveListDoc) {
let outerHTML = liveListDoc.documentElement./*OK*/ outerHTML;
outerHTML = replaceUrls(mode, outerHTML);
res.send(`${doctype}${outerHTML}`);
return;
}
if (!liveListDoc) {
const liveListUpdateFullPath = `${pc.cwd()}${req.baseUrl}`;
logWithoutTimestamp('liveListUpdateFullPath', liveListUpdateFullPath);
const liveListFile = fs.readFileSync(liveListUpdateFullPath);
liveListDoc = liveListDocs[req.baseUrl] = new jsdom.JSDOM(
liveListFile
).window.document;
liveListDoc.ctr = 0;
}
const liveList = liveListDoc.querySelector('#my-live-list');
const perPage = Number(liveList.getAttribute('data-max-items-per-page'));
const items = liveList.querySelector('[items]');
const pagination = liveListDoc.querySelector('#my-live-list [pagination]');
const item1 = liveList.querySelector('#list-item-1');
if (liveListDoc.ctr != 0) {
if (Math.random() < 0.8) {
// Always run a replace on the first item
liveListReplace(item1);
if (Math.random() < 0.5) {
liveListTombstone(liveList);
}
if (Math.random() < 0.8) {
liveListInsert(liveList, item1);
}
pagination.textContent = '';
const liveChildren = [].slice
.call(items.children)
.filter((x) => !x.hasAttribute('data-tombstone'));
const pageCount = Math.ceil(liveChildren.length / perPage);
const pageListItems = Array.apply(null, Array(pageCount))
.map((_, i) => `<li>${i + 1}</li>`)
.join('');
const newPagination =
'<nav aria-label="amp live list pagination">' +
`<ul class="pagination">${pageListItems}</ul>` +
'</nav>';
pagination./*OK*/ innerHTML = newPagination;
} else {
// Sometimes we want an empty response to simulate no changes.
res.send(`${doctype}<html></html>`);
return;
}
}
let outerHTML = liveListDoc.documentElement./*OK*/ outerHTML;
outerHTML = replaceUrls(mode, outerHTML);
liveListDoc.ctr++;
res.send(`${doctype}${outerHTML}`);
});
/**
* @param {Element} item
*/
function liveListReplace(item) {
item.setAttribute('data-update-time', Date.now().toString());
const itemContents = item.querySelectorAll('.content');
itemContents[0].textContent = Math.floor(Math.random() * 10).toString();
itemContents[1].textContent = Math.floor(Math.random() * 10).toString();
}
/**
* @param {Element} liveList
* @param {Element} node
*/
function liveListInsert(liveList, node) {
const iterCount = Math.floor(Math.random() * 2) + 1;
logWithoutTimestamp(`inserting ${iterCount} item(s)`);
for (let i = 0; i < iterCount; i++) {
/**
* TODO(#28387) this type cast may be hiding a bug.
* @type {Element}
*/
const child = /** @type {*} */ (node.cloneNode(true));
child.setAttribute('id', `list-item-${itemCtr++}`);
child.setAttribute('data-sort-time', Date.now().toString());
liveList.querySelector('[items]')?.appendChild(child);
}
}
/**
* @param {Element} liveList
*/
function liveListTombstone(liveList) {
const tombstoneId = Math.floor(Math.random() * itemCtr);
logWithoutTimestamp(`trying to tombstone #list-item-${tombstoneId}`);
// We can tombstone any list item except item-1 since we always do a
// replace example on item-1.
if (tombstoneId != 1) {
const item = liveList./*OK*/ querySelector(`#list-item-${tombstoneId}`);
if (item) {
item.setAttribute('data-tombstone', '');
}
}
}
/**
* Generate a random number between min and max
* Value is inclusive of both min and max values.
*
* @param {number} min
* @param {number} max
* @return {number}
*/
function range(min, max) {
const values = Array.apply(null, new Array(max - min + 1)).map(
(_, i) => min + i
);
return values[Math.round(Math.random() * (max - min))];
}
/**
* Returns the result of a coin flip, true or false
*
* @return {boolean}
*/
function flip() {
return !!Math.floor(Math.random() * 2);
}
/**
* @return {string}
*/
function getLiveBlogItem() {
const now = Date.now();
// Generate a 3 to 7 worded headline
const headline = bacon(range(3, 7));
const numOfParagraphs = range(1, 2);
const body = Array.apply(null, new Array(numOfParagraphs))
.map(() => {
return `<p>${bacon(range(50, 90))}</p>`;
})
.join('\n');
const img = `<amp-img
src="${
flip()
? 'https://placekitten.com/300/350'
: 'https://baconmockup.com/300/350'
}"
layout="responsive"
height="300" width="350">
</amp-img>`;
return `<!doctype html>
<html amp><body>
<amp-live-list id="live-blog-1">
<div items>
<div id="live-blog-item-${now}" data-sort-time="${now}">
<h3 class="headline">
<a href="#live-blog-item-${now}">${headline}</a>
</h3>
<div class="author">
<div class="byline">
<p>
by <span itemscope itemtype="http://schema.org/Person"
itemprop="author"><b>Lorem Ipsum</b>
<a class="mailto" href="mailto:lorem.ipsum@">
lorem.ipsum@</a></span>
</p>
<p class="brand">PublisherName News Reporter<p>
<p><span itemscope itemtype="http://schema.org/Date"
itemprop="Date">
${new Date(now).toString().replace(/ GMT.*$/, '')}
<span></p>
</div>
</div>
<div class="article-body">${body}</div>
${img}
<div class="social-box">
<amp-social-share type="facebook"
data-param-text="Hello world"
data-param-href="https://example.test/?ref=URL"
data-param-app_id="145634995501895"></amp-social-share>
<amp-social-share type="twitter"></amp-social-share>
</div>
</div>
</div>
</amp-live-list></body></html>`;
}
/**
* @return {string}
*/
function getLiveBlogItemWithBindAttributes() {
const now = Date.now();
// Generate a 3 to 7 worded headline
const numOfParagraphs = range(1, 2);
const body = Array.apply(null, new Array(numOfParagraphs))
.map(() => {
return `<p>${bacon(range(50, 90))}</p>`;
})
.join('\n');
return `<!doctype html>
<html amp><body>
<amp-live-list id="live-blog-1">
<div items>
<div id="live-blog-item-${now}" data-sort-time="${now}">
<div class="article-body">
${body}
<p> As you can see, bacon is far superior to
<b><span [text]='favoriteFood'>everything!</span></b>!</p>
</div>
</div>
</div>
</amp-live-list></body></html>`;
}
app.use(
'/examples/live-blog(-non-floating-button)?.amp.html',
(req, res, next) => {
if ('amp_latest_update_time' in req.query) {
res.setHeader('Content-Type', 'text/html');
res.end(getLiveBlogItem());
return;
}
next();
}
);
app.use('/examples/bind/live-list.amp.html', (req, res, next) => {
if ('amp_latest_update_time' in req.query) {
res.setHeader('Content-Type', 'text/html');
res.end(getLiveBlogItemWithBindAttributes());
return;
}
next();
});
app.use('/impression-proxy/', (req, res) => {
cors.assertCors(req, res, ['GET']);
// Fake response with the following optional fields:
// location: The Url the that server would have sent redirect to w/o ALP
// tracking_url: URL that should be requested to track click
// gclid: The conversion tracking value
const body = {
'location': 'localhost:8000/examples/?gclid=1234&foo=bar&example=123',
'tracking_url': 'tracking_url',
'gclid': '1234',
};
res.send(body);
// Or fake response with status 204 if viewer replaceUrl is provided
});
/**
* Acts in a similar fashion to /serve_mode_change. Saves
* analytics requests via /run-variable-substitution, and
* then returns the encoded/substituted/replaced request
* via /get-variable-request.
*/
// Saves the variables input to be used in run-variable-substitution
app.get('/save-variables', saveVariables);
// Creates an iframe with amp-analytics. Analytics request
// uses save-variable-request as its endpoint.
app.get('/run-variable-substitution', runVariableSubstitution);
// Saves the analytics request to the dev server.
app.get('/save-variable-request', saveVariableRequest);
// Returns the saved analytics request.
app.get('/get-variable-request', getVariableRequest);
let forcePromptOnNext = false;
app.post('/get-consent-v1/', (req, res) => {
cors.assertCors(req, res, ['POST']);
const body = {
'promptIfUnknown': true,
'purposeConsentRequired': ['purpose-foo', 'purpose-bar'],
'forcePromptOnNext': forcePromptOnNext,
'sharedData': {
'tfua': true,
'coppa': true,
},
};
res.json(body);
});
app.get('/get-consent-v1-set/', (req, res) => {
cors.assertCors(req, res, ['GET']);
const value = req.query['forcePromptOnNext'];
if (value == 'false' || value == '0') {
forcePromptOnNext = false;
} else {
forcePromptOnNext = true;
}
res.json({});
res.end();
});
app.post('/get-consent-no-prompt/', (req, res) => {
cors.assertCors(req, res, ['POST']);
const body = {};
res.json(body);
});
app.post('/check-consent', (req, res) => {
cors.assertCors(req, res, ['POST']);
const response = {
'consentRequired': req.query.consentRequired === 'true',
'consentStateValue': req.query.consentStateValue,
'consentString': req.query.consentString,
'expireCache': req.query.expireCache === 'true',
};
if (req.query.consentMetadata) {
response['consentMetadata'] = JSON.parse(
req.query.consentMetadata.replace(/'/g, '"')
);
}
res.json(response);
});
// Proxy with local JS.
// Example:
// http://localhost:8000/proxy/s/www.washingtonpost.com/amphtml/news/post-politics/wp/2016/02/21/bernie-sanders-says-lower-turnout-contributed-to-his-nevada-loss-to-hillary-clinton/
app.use('/proxy/', (req, res) => proxyToAmpProxy(req, res, SERVE_MODE));
// Nest the response in an iframe.
// Example:
// http://localhost:8000/iframe/examples/ads.amp.html
app.get('/iframe/*', (req, res) => {
// Returns an html blob with an iframe pointing to the url after /iframe/.
res.send(`<!doctype html>
<html style="width:100%; height:100%;">
<body style="width:98%; height:98%;">
<iframe src="${req.url.substr(7)}"
style="width:100%; height:100%;">
</iframe>
</body>
</html>`);
});
app.get('/a4a_template/*', (req, res) => {
cors.assertCors(req, res, ['GET']);
const match = /^\/a4a_template\/([a-z-]+)\/(\d+)$/.exec(req.path);
if (!match) {
res.status(404);
res.end('Invalid path: ' + req.path);
return;
}
const filePath =
`${pc.cwd()}/extensions/amp-ad-network-${match[1]}-impl/` +
`0.1/data/${match[2]}.template`;
fs.promises
.readFile(filePath)
.then((file) => {
res.setHeader('Content-Type', 'application/json');
res.setHeader('AMP-template-amp-creative', 'amp-mustache');
res.end(file);
})
.catch(() => {
res.status(404);
res.end('Not found: ' + filePath);
});
});
// Returns a document that echoes any post messages received from parent.
// An optional `message` query param can be appended for an initial post
// message sent on document load.
// Example:
// http://localhost:8000/iframe-echo-message?message=${payload}
app.get('/iframe-echo-message', (req, res) => {
@@ -941,1 +942,1 @@
- const {message} = req.query;
+ const message = escape(req.query.message);
res.send(
`<!doctype html>
<body style="background-color: yellow">
<script>
if (${message}) {
echoMessage(${message});
}
window.addEventListener('message', function(event) {
echoMessage(event.data);
});
function echoMessage(message) {
parent.postMessage(message, '*');
}
</script>
</body>
</html>`
);
});
/**
* Append ?sleep=5 to any included JS file in examples to emulate delay in
* loading that file. This allows you to test issues with your extension being
* late to load and testing user interaction with your element before your code
* loads.
*
* Example delay loading amp-form script by 5 seconds:
* <script async custom-element="amp-form"
* src="https://cdn.ampproject.org/v0/amp-form-0.1.js?sleep=5"></script>
*/
app.use(['/dist/v0/amp-*.(m?js)', '/dist/amp*.(m?js)'], (req, _res, next) => {
const sleep = parseInt(req.query.sleep || 0, 10) * 1000;
setTimeout(next, sleep);
});
/**
* Disable caching for extensions if the --no_caching_extensions flag is used.
*/
app.get(['/dist/v0/amp-*.(m?js)'], (_req, res, next) => {
if (argv.no_caching_extensions) {
res.header('Cache-Control', 'no-store');
}
next();
});
/**
* Video testbench endpoint
*/
app.get('/test/manual/amp-video.amp.html', runVideoTestBench);
app.get(
[
'/examples/(**/)?*.html',
'/test/manual/(**/)?*.html',
'/test/fixtures/e2e/(**/)?*.html',
'/test/fixtures/performance/(**/)?*.html',
],
(req, res, next) => {
const filePath = req.path;
const mode = SERVE_MODE;
const inabox = req.query['inabox'];
const stream = Number(req.query['stream']);
const componentVersion = req.query['componentVersion'];
const urlPrefix = getUrlPrefix(req);
fs.promises
.readFile(pc.cwd() + filePath, 'utf8')
.then((file) => {
if (req.query['amp_js_v']) {
file = addViewerIntegrationScript(req.query['amp_js_v'], file);
}
if (req.query['mraid']) {
file = file
.replace(
'</head>',
'<script async host-service="amp-mraid" src="https://cdn.ampproject.org/v0/amp-mraid-0.1.js">' +
'</script>' +
'</head>'
)
.replace(
'<head>',
' <head>' +
' <script>' +
' window.AMP_CONFIG = {' +
` cdnUrl: "${urlPrefix}",` +
' };' +
' </script>'
);
}
file = file.replace(/__TEST_SERVER_PORT__/g, TEST_SERVER_PORT);
if (componentVersion) {
file = file.replace(/-latest.js/g, `-${componentVersion}.js`);
}
if (inabox) {
file = toInaboxDocument(file);
// Allow CORS requests for A4A.
if (req.headers.origin) {
cors.enableCors(req, res, req.headers.origin);
}
}
file = replaceUrls(mode, file);
const ampExperimentsOptIn = req.query['exp'];
if (ampExperimentsOptIn) {
file = file.replace(
'<head>',
`<head><meta name="amp-experiments-opt-in" content="${ampExperimentsOptIn}">`
);
}
// Extract amp-ad for the given 'type' specified in URL query.
if (req.path.indexOf('/examples/ads.amp.html') == 0 && req.query.type) {
const ads =
file.match(
elementExtractor('(amp-ad|amp-embed)', req.query.type)
) ?? [];
file = file.replace(
/<body>[\s\S]+<\/body>/m,
'<body>' + ads.join('') + '</body>'
);
}
// Extract amp-analytics for the given 'type' specified in URL query.
if (
req.path.indexOf('/examples/analytics-vendors.amp.html') == 0 &&
req.query.type
) {
const analytics =
file.match(elementExtractor('amp-analytics', req.query.type)) ?? [];
file = file.replace(
/<div id="container">[\s\S]+<\/div>/m,
'<div id="container">' + analytics.join('') + '</div>'
);
}
// Extract amp-consent for the given 'type' specified in URL query.
if (
req.path.indexOf('/examples/amp-consent/cmp-vendors.amp.html') == 0 &&
req.query.type
) {
const consent =
file.match(elementExtractor('amp-consent', req.query.type)) ?? [];
file = file.replace(
/<div id="container">[\s\S]+<\/div>/m,
'<div id="container">' + consent.join('') + '</div>'
);
}
if (stream > 0) {
res.writeHead(200, {'Content-Type': 'text/html'});
let pos = 0;
const writeChunk = function () {
const chunk = file.substring(
pos,
Math.min(pos + stream, file.length)
);
res.write(chunk);
pos += stream;
if (pos < file.length) {
setTimeout(writeChunk, 500);
} else {
res.end();
}
};
writeChunk();
} else {
res.send(file);
}
})
.catch(() => {
next();
});
}
);
/**
* @param {string} string
* @return {string}
*/
function escapeRegExp(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
/**
* @param {string} tagName
* @param {string} type
* @return {RegExp}
*/
function elementExtractor(tagName, type) {
type = escapeRegExp(type);
return new RegExp(
`<${tagName}[\\s][^>]*['"]${type}['"][^>]*>([\\s\\S]+?)</${tagName}>`,
'gm'
);
}
// Data for example: http://localhost:8000/examples/bind/xhr.amp.html
app.use('/bind/form/get', (req, res) => {
cors.assertCors(req, res, ['GET']);
res.json({
bindXhrResult: 'I was fetched from the server!',
});
});
// Data for example: http://localhost:8000/examples/bind/ecommerce.amp.html
app.use('/bind/ecommerce/sizes', (req, res) => {
cors.assertCors(req, res, ['GET']);
setTimeout(() => {
const prices = {
'0': {
'sizes': {
'XS': 8.99,
'S': 9.99,
},
},
'1': {
'sizes': {
'S': 10.99,
'M': 12.99,
'L': 14.99,
},
},
'2': {
'sizes': {
'L': 11.99,
'XL': 13.99,
},
},
'3': {
'sizes': {
'M': 7.99,
'L': 9.99,
'XL': 11.99,
},
},
'4': {
'sizes': {
'XS': 8.99,
'S': 10.99,
'L': 15.99,
},
},
'5': {
'sizes': {
'S': 8.99,
'L': 14.99,
'XL': 11.99,
},
},
'6': {
'sizes': {
'XS': 8.99,
'S': 9.99,
'M': 12.99,
},
},
'7': {
'sizes': {
'M': 10.99,
'L': 11.99,
},
},
};
const object = {};
object[req.query.shirt] = prices[req.query.shirt];
res.json(object);
}, 1000); // Simulate network delay.
});
/**
* Simulates a publisher's metering state store.
* (amp-subscriptions)
* @type {{[ampReaderId: string]: {}}}
*/
const meteringStateStore = {};
// Simulate a publisher's entitlements API.
// (amp-subscriptions)
app.use('/subscription/:id/entitlements', (req, res) => {
cors.assertCors(req, res, ['GET']);
// Create entitlements response.
const source = 'local' + req.params.id;
const granted = req.params.id > 0;
const grantReason = granted ? 'SUBSCRIBER' : 'NOT_SUBSCRIBER';
const decryptedDocumentKey = decryptDocumentKey(req.query.crypt);
const response = {
source,
granted,
grantReason,
data: {
login: true,
},
decryptedDocumentKey,
};
// Store metering state, if possible.
const ampReaderId = req.query.rid;
if (ampReaderId && req.query.meteringState) {
// Parse metering state from encoded Base64 string.
const encodedMeteringState = req.query.meteringState;
const decodedMeteringState = Buffer.from(
encodedMeteringState,
'base64'
).toString();
const meteringState = JSON.parse(decodedMeteringState);
// Store metering state.
meteringStateStore[ampReaderId] = meteringState;
}
// Add metering state to response, if possible.
if (meteringStateStore[ampReaderId]) {
response.metering = {
state: meteringStateStore[ampReaderId],
};
}
res.json(response);
});
// Simulate a publisher's SKU map API.
// (amp-subscriptions)
app.use('/subscriptions/skumap', (req, res) => {
cors.assertCors(req, res, ['GET']);
res.json({
'subscribe.google.com': {
'subscribeButtonSimple': {
'sku': 'basic',
},
'subscribeButtonCarousel': {
'carouselOptions': {
'skus': ['basic', 'premium_monthly'],
},
},
},
});
});
// Simulate a publisher's pingback API.
// (amp-subscriptions)
app.use('/subscription/pingback', (req, res) => {
cors.assertCors(req, res, ['POST']);
res.json({
done: true,
});
});
/*
Simulate a publisher's account registration API.
The `amp-subscriptions-google` extension sends this API a POST request.
The request body looks like:
{
"googleSignInDetails": {
// This signed JWT contains information from Google Sign-In
"idToken": "...JWT from Google Sign-In...",
// Some useful fields from the `idToken`, pre-parsed for convenience
"name": "Jane Smith",
"givenName": "Jane",
"familyName": "Smith",
"imageUrl": "https://imageurl",
"email": "[email protected]"
},
// Associate this ID with the registration. Use it to look up metering state
// for future entitlements requests
// https://github.com/ampproject/amphtml/blob/main/extensions/amp-subscriptions/amp-subscriptions.md#combining-the-amp-reader-id-with-publisher-cookies
"ampReaderId": "amp-s0m31d3nt1f13r"
}
(amp-subscriptions-google)
*/
app.use('/subscription/register', (req, res) => {
cors.assertCors(req, res, ['POST']);
// Generate a new ID for this metering state.
const meteringStateId = 'ppid' + Math.round(Math.random() * 99999999);
// Define registration timestamp.
//
// For demo purposes, set timestamp to 30 seconds ago.
// This causes Metering Toast to show immediately,
// which helps engineers test metering.
const registrationTimestamp = Math.round(Date.now() / 1000) - 30000;
// Store metering state.
//
// For demo purposes, just save this in memory.
// Production systems should persist this.
meteringStateStore[req.body.ampReaderId] = {
id: meteringStateId,
standardAttributes: {
// eslint-disable-next-line local/camelcase
registered_user: {
timestamp: registrationTimestamp, // In seconds.
},
},
};
res.json({
metering: {
state: meteringStateStore[req.body.ampReaderId],
},
});
});
// Simulated adzerk ad server and AMP cache CDN.
app.get('/adzerk/*', (req, res) => {
cors.assertCors(req, res, ['GET'], ['AMP-template-amp-creative']);
const match = /\/(\d+)/.exec(req.path);
if (!match || !match[1]) {
res.status(404);
res.end('Invalid path: ' + req.path);
return;
}
const filePath =
pc.cwd() + '/extensions/amp-ad-network-adzerk-impl/0.1/data/' + match[1];
fs.promises
.readFile(filePath)
.then((file) => {
res.setHeader('Content-Type', 'application/json');
res.setHeader('AMP-Ad-Template-Extension', 'amp-mustache');
res.setHeader('AMP-Ad-Response-Type', 'template');
res.end(file);
})
.catch(() => {
res.status(404);
res.end('Not found: ' + filePath);
});
});
app.get('/dist/*.mjs', (req, res, next) => {
// Allow CORS access control explicitly for mjs files
cors.enableCors(req, res);
next();
});
/*
* Serve extension scripts and their source maps.
*/
app.get(
['/dist/rtv/*/v0/*.(m?js)', '/dist/rtv/*/v0/*.(m?js).map'],
async (req, res, next) => {
const mode = SERVE_MODE;
const fileName = path.basename(req.path).replace('.max.', '.');
let filePath = 'https://cdn.ampproject.org/v0/' + fileName;
if (await passthroughServeModeCdn(res, filePath)) {
return;
}
const isJsMap = filePath.endsWith('.map');
if (isJsMap) {
filePath = filePath.replace(/\.(m?js)\.map$/, '.$1');
}
filePath = replaceUrls(mode, filePath);
req.url = filePath + (isJsMap ? '.map' : '');
next();
}
);
/**
* Handle amp-story translation file requests with an rtv path.
* We need to make sure we only handle the amp-story requests since this
* can affect other tests with json requests.
*/
app.get('/dist/rtv/*/v0/amp-story*.json', async (req, _res, next) => {
const fileName = path.basename(req.path);
let filePath = 'https://cdn.ampproject.org/v0/' + fileName;
filePath = replaceUrls(SERVE_MODE, filePath);
req.url = filePath;
next();
});
if (argv.coverage === 'live') {
app.get('/dist/amp.js', async (req, res) => {
const ampJs = await fs.promises.readFile(`${pc.cwd()}${req.path}`);
res.setHeader('Content-Type', 'text/javascript');
res.setHeader('Access-Control-Allow-Origin', '*');
// Append an unload handler that reports coverage information each time you
// leave a page.
res.end(`${ampJs};
window.addEventListener('beforeunload', (evt) => {
const COV_REPORT_URL = 'http://localhost:${TEST_SERVER_PORT}/coverage/client';
console.info('POSTing code coverage to', COV_REPORT_URL);
const xhr = new XMLHttpRequest();
xhr.open('POST', COV_REPORT_URL, true);
xhr.setRequestHeader('Content-type', 'application/json');
xhr.send(JSON.stringify(window.__coverage__));
// Required by Chrome
evt.returnValue = '';
return null;
});`);
});
}
app.get('/dist/ww.(m?js)', async (req, res, next) => {
// Special case for entry point script url. Use minified for testing
const mode = SERVE_MODE;
const fileName = path.basename(req.path);
if (await passthroughServeModeCdn(res, fileName)) {
return;
}
if (mode == 'default') {
req.url = req.url.replace(/\.(m?js)$/, '.max.$1');
}
next();
});
app.get('/dist/iframe-transport-client-lib.(m?js)', (req, _res, next) => {
req.url = req.url.replace(/dist/, 'dist.3p/current');
next();
});
app.get('/dist/amp-inabox-host.(m?js)', (req, _res, next) => {
const mode = SERVE_MODE;
if (mode != 'default') {
req.url = req.url.replace('amp-inabox-host', 'amp4ads-host-v0');
}
next();
});
app.get('/mraid.js', (req, _res, next) => {
req.url = req.url.replace('mraid.js', 'examples/mraid/mraid.js');
next();
});
/**
* Shadow viewer. Fetches shadow runtime from cdn by default.
* Setting the param useLocal=1 will load the runtime from the local build.
*/
app.use('/shadow/', (req, res) => {
const {url} = req;
const isProxyUrl = /^\/proxy\//.test(url);
const baseHref = isProxyUrl
? 'https://cdn.ampproject.org/'
: `${path.dirname(url)}/`;
const viewerHtml = renderShadowViewer({
src: '//' + req.hostname + '/' + req.url.replace(/^\//, ''),
baseHref,
});
if (!req.query.useLocal) {
res.end(viewerHtml);
return;
}
res.end(replaceUrls(SERVE_MODE, viewerHtml));
});
app.use('/mraid/', (req, res) => {
res.redirect(req.url + '?inabox=1&mraid=1');
});
/**
* @param {string} ampJsVersionString
* @param {string} file
* @return {string}
*/
function addViewerIntegrationScript(ampJsVersionString, file) {
const ampJsVersion = parseFloat(ampJsVersionString);
if (!ampJsVersion) {
return file;
}
let viewerScript;
// eslint-disable-next-line local/no-es2015-number-props
if (Number.isInteger(ampJsVersion)) {
// Viewer integration script from gws, such as
// https://cdn.ampproject.org/viewer/google/v7.js
viewerScript =
'<script async src="https://cdn.ampproject.org/viewer/google/v' +
ampJsVersion +
'.js"></script>';
} else {
// Viewer integration script from runtime, such as
// https://cdn.ampproject.org/v0/amp-viewer-integration-0.1.js
viewerScript =
'<script async ' +
'src="https://cdn.ampproject.org/v0/amp-viewer-integration-' +
ampJsVersion +
'.js" data-amp-report-test="viewer-integr.js"></script>';
}
file = file.replace('</head>', viewerScript + '</head>');
return file;
}
/**
* @param {express.Request} req
* @return {string}
*/
function getUrlPrefix(req) {
return req.protocol + '://' + req.headers.host;
}
/**
* @param {string} filePath
* @return {string}
*/
function generateInfo(filePath) {
const mode = SERVE_MODE;
filePath = filePath.substr(0, filePath.length - 9) + '.html';
return (
'<h2>Please note that .min/.max is no longer supported</h2>' +
'<h3>Current serving mode is ' +
mode +
'</h3>' +
'<h3>Please go to <a href= ' +
filePath +
'>Unversioned Link</a> to view the page<h3>' +
'<h3></h3>' +
'<h3><a href = /serve_mode=default>' +
'Change to DEFAULT mode (unminified JS)</a></h3>' +
'<h3><a href = /serve_mode=minified>' +
'Change to COMPILED mode (minified JS)</a></h3>' +
'<h3><a href = /serve_mode=cdn>' +
'Change to CDN mode (prod JS)</a></h3>'
);
}
/**
* @param {string} encryptedDocumentKey
* @return {?string}
*/
function decryptDocumentKey(encryptedDocumentKey) {
if (!encryptedDocumentKey) {
return null;
}
const cryptoStart = 'ENCRYPT(';
if (!encryptedDocumentKey.includes(cryptoStart, 0)) {
return null;
}
let jsonString = encryptedDocumentKey.replace(cryptoStart, '');
jsonString = jsonString.substring(0, jsonString.length - 1);
const parsedJson = JSON.parse(jsonString);
if (!parsedJson) {
return null;
}
return parsedJson.key;
}
// serve local vendor config JSON files
app.use(
'(/dist)?/rtv/*/v0/analytics-vendors/:vendor.json',
async (req, res) => {
const {vendor} = req.params;
const serveMode = SERVE_MODE;
const cdnUrl = `https://cdn.ampproject.org/v0/analytics-vendors/${vendor}.json`;
if (await passthroughServeModeCdn(res, cdnUrl)) {
return;
}
const max = serveMode === 'default' ? '.max' : '';
const localPath = `${pc.cwd()}/dist/v0/analytics-vendors/${vendor}${max}.json`;
try {
const file = await fs.promises.readFile(localPath);
res.setHeader('Content-Type', 'application/json');
res.end(file);
} catch (_) {
res.status(404);
res.end('Not found: ' + localPath);
}
}
);
module.exports = app;

  • Create Pull Request

Remediation feedback:

  • 👍 Like
  • 👎 Dislike
Secure Code Warrior Training Material

● Training

   ▪ Secure Code Warrior Cross-Site Scripting Training

● Videos

   ▪ Secure Code Warrior Cross-Site Scripting Video

 
HighCross-Site Scripting

CWE-79

amp4test.js:126

12024-08-19 04:08am
Vulnerable Code

}
const key = req.params.id;
log('SERVER-LOG [WITHDRAW]: ' + key);
const result = bank[req.params.bid][key];
if (typeof result === 'function') {
return res

1 Data Flow/s detected

app.use('/request-bank/:bid/withdraw/:id/', (req, res) => {

const key = req.params.id;

.send(`another client is withdrawing this ID [${key}]`);

⛑️ Remediation Suggestion

--- original
+++ remediated
@@ -1,0 +1,1 @@
+const escape = require('escape-html');
'use strict';
const app = require('express').Router();
const cors = require('./amp-cors');
const minimist = require('minimist');
const argv = minimist(process.argv.slice(2));
const path = require('path');
const upload = require('multer')();
const {getServeMode, replaceUrls} = require('./app-utils');
const {renderShadowViewer} = require('./shadow-viewer');
const CUSTOM_TEMPLATES = ['amp-mustache'];
const SERVE_MODE = getServeMode();
/**
* Logs the given messages to the console when --verbose is specified.
* @param {*} messages
*/
function log(...messages) {
if (argv.verbose) {
console.log.apply(console, messages);
}
}
app.use('/compose-doc', function (req, res) {
res.setHeader('X-XSS-Protection', '0');
const {body, css, experiments, extensions, spec} = req.query;
const frameHtml =
SERVE_MODE == 'minified'
? 'dist.3p/current-min/frame.html'
: 'dist.3p/current/frame.max.html';
let experimentsBlock = '';
if (experiments) {
const string = `"${experiments.split(',').join('","')}"`;
// TODO: Why is setting localDev necessary?
// `allow-doc-opt-in` enables any experiment to be enabled via doc opt-in.
experimentsBlock = `<script>
window.AMP_CONFIG = window.AMP_CONFIG || {"localDev": true};
window.AMP_CONFIG['allow-doc-opt-in'] = (window.AMP_CONFIG['allow-doc-opt-in'] || []).concat([${string}]);
</script>
<meta name="amp-experiments-opt-in" content="${experiments}">`;
}
// TODO: Do we need to inject amp-3p-iframe-src for non-ad tests?
const head = `${experimentsBlock}
<meta name="amp-3p-iframe-src" content="http://localhost:9876/${frameHtml}">`;
const doc = composeDocument({
body,
css,
extensions: extensions ? extensions.split(',') : '',
head,
spec,
});
res.cookie('test-cookie', 'test');
res.send(doc);
});
app.use('/compose-html', function (req, res) {
res.setHeader('X-XSS-Protection', '0');
res.send(`
<!doctype html>
<html>
<head>
<title>NON-AMP TEST</title>
<meta name="viewport" content="width=device-width,minimum-scale=1,initial-scale=1">
</head>
<body>
${req.query.body}
</body>
</html>
`);
});
app.use('/compose-shadow', function (req, res) {
const {docUrl} = req.query;
const viewerHtml = renderShadowViewer({
src: docUrl.replace(/^\//, ''),
baseHref: path.dirname(req.url),
});
res.send(replaceUrls(SERVE_MODE, viewerHtml));
});
/**
* A server side temporary request storage which is useful for testing
* browser sent HTTP requests.
*/
const bank = {};
/**
* Deposit a request. An ID has to be specified. Will override previous request
* if the same ID already exists.
*/
app.use('/request-bank/:bid/deposit/:id/', upload.array(), (req, res) => {
cors.enableCors(req, res);
if (!bank[req.params.bid]) {
bank[req.params.bid] = {};
}
const key = req.params.id;
log('SERVER-LOG [DEPOSIT]: ', key);
if (typeof bank[req.params.bid][key] === 'function') {
bank[req.params.bid][key](req);
} else {
bank[req.params.bid][key] = req;
}
res.end();
});
/**
* Withdraw a request. If the request of the given ID is already in the bank,
* return it immediately. Otherwise wait until it gets deposited
* The same request cannot be withdrawn twice at the same time.
*/
app.use('/request-bank/:bid/withdraw/:id/', (req, res) => {
cors.enableCors(req, res);
if (!bank[req.params.bid]) {
bank[req.params.bid] = {};
}
const key = req.params.id;
log('SERVER-LOG [WITHDRAW]: ' + key);
const result = bank[req.params.bid][key];
if (typeof result === 'function') {
return res
.status(500)
@@ -128,1 +129,1 @@
- .send(`another client is withdrawing this ID [${key}]`);
+ .send(`another client is withdrawing this ID [${escape(key)}]`);
}
const callback = function (result) {
if (result === undefined) {
// This happens when tearDown is called but no request
// of given ID has been received yet.
@@ -134,1 +135,1 @@
- res.status(404).send(`Request of given ID not found: [${key}]`);
+ res.status(404).send(`Request of given ID not found: [${escape(key)}]`);
} else {
res.json({
headers: result.headers,
body: result.body,
url: result.url,
});
}
delete bank[req.params.bid][key];
};
if (result) {
callback(result);
} else {
bank[req.params.bid][key] = callback;
}
});
/**
* Clean up all pending withdraw & deposit requests.
*/
app.use('/request-bank/:bid/teardown/', (req, res) => {
log('SERVER-LOG [TEARDOWN]');
const b = bank[req.params.bid];
for (const id in b) {
const callback = b[id];
if (typeof callback === 'function') {
// Respond 404 to pending request.
callback();
}
delete b[id];
}
res.end();
});
/**
* Serves a fake ad for test-amp-ad-fake.js
*/
app.get('/a4a/:bid', (req, res) => {
cors.enableCors(req, res);
const {bid} = req.params;
const body = `
<a href=https://amp.dev target=_blank>
<amp-img alt="AMP Ad" height=250 src=//localhost:9876/amp4test/request-bank/${bid}/deposit/image width=300></amp-img>
</a>
<amp-pixel src="//localhost:9876/amp4test/request-bank/${bid}/deposit/pixel/foo?cid=CLIENT_ID(a)"></amp-pixel>
<amp-analytics>
<script type="application/json">
{
"requests": {
"pageview": "//localhost:9876/amp4test/request-bank/${bid}/deposit/analytics/bar"
},
"triggers": {
"pageview": {
"on": "visible",
"request": "pageview",
"extraUrlParams": {
"timestamp": "\${timestamp}",
"title": "\${title}",
"ampdocUrl": "\${ampdocUrl}",
"canonicalUrl": "\${canonicalUrl}",
"cid": "\${clientId(a)}",
"img": "\${htmlAttr(amp-img,src)}",
"navTiming": "\${navTiming(requestStart,requestStart)}",
"navType": "\${navType}",
"navRedirectCount": "\${navRedirectCount}",
"sourceUrl": "\${sourceUrl}",
"cookie": "\${cookie(test-cookie)}"
}
}
}
}
</script>
</amp-analytics>`;
const doc = composeDocument({
spec: 'amp4ads',
body,
css: 'body { background-color: #f4f4f4; }',
extensions: ['amp-analytics'],
});
res.cookie('test-cookie', 'test');
res.send(doc);
});
/**
* @param {{body: string, css?: string|undefined, extensions: Array<string>|undefined, head?: string|undefined, spec?: string|undefined, mode?: string|undefined}} config
* @return {string}
*/
function composeDocument(config) {
const {body, css, extensions, head, mode, spec} = config;
const m = mode || SERVE_MODE;
const cdn = m === 'cdn';
const minified = m === 'minified';
const cssTag = css ? `<style amp-custom>${css}</style>` : '';
// Set link[rel=canonical], CSS boilerplate and runtime <script> depending
// on the AMP spec.
let canonical, boilerplate, runtime;
const amp = spec || 'amp';
switch (amp) {
case 'amp':
canonical = '<link rel="canonical" href="http://nonblocking.io" />';
boilerplate =
'<style amp-boilerplate>body{-webkit-animation:-amp-start 8s steps(1,end) 0s 1 normal both;-moz-animation:-amp-start 8s steps(1,end) 0s 1 normal both;-ms-animation:-amp-start 8s steps(1,end) 0s 1 normal both;animation:-amp-start 8s steps(1,end) 0s 1 normal both}@-webkit-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-moz-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-ms-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-o-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}</style><noscript><style amp-boilerplate>body{-webkit-animation:none;-moz-animation:none;-ms-animation:none;animation:none}</style></noscript>';
runtime = cdn
? 'https://cdn.ampproject.org/v0.js'
: `/dist/${minified ? 'v0' : 'amp'}.js`;
break;
case 'amp4ads':
canonical = '';
boilerplate =
'<style amp4ads-boilerplate>body{visibility:hidden}</style>';
runtime = cdn
? 'https://cdn.ampproject.org/amp4ads-v0.js'
: `/dist/${minified ? 'amp4ads-v0' : 'amp-inabox'}.js`;
break;
case 'amp4email':
canonical = '';
boilerplate =
'<style amp4email-boilerplate>body{visibility:hidden}</style>';
runtime = cdn
? 'https://cdn.ampproject.org/v0.js'
: `/dist/${minified ? 'v0' : 'amp'}.js`;
break;
default:
throw new Error('Unrecognized AMP spec: ' + spec);
}
const runtimeScript = `<script async src="${runtime}"></script>`;
// Generate extension <script> markup.
let extensionScripts = '';
if (extensions) {
extensionScripts = extensions
.map((extension) => {
const tuple = extension.split(':');
const name = tuple[0];
const version = tuple[1] || '0.1';
const src = cdn
? `https://cdn.ampproject.org/v0/${name}-${version}.js`
: `/dist/v0/${name}-${version}.${minified ? '' : 'max.'}js`;
const type = CUSTOM_TEMPLATES.includes(name)
? 'custom-template'
: 'custom-element';
return `<script async ${type}="${name}" src="${src}"></script>`;
})
.join('\n');
}
const topHalfOfHtml = `<!doctype html>
<html ${amp}>
<head>
<title>AMP TEST</title>
<meta charset="utf-8">
${canonical}
<meta name="viewport" content="width=device-width,minimum-scale=1,initial-scale=1">
${head || ''}
${boilerplate}
${runtimeScript}
${extensionScripts}
${cssTag}
</head>`;
// To enable A4A FIE, a <script amp-ad-metadata> tag must exist.
let ampAdMeta = '';
if (amp === 'amp4ads') {
// `ampRuntimeUtf16CharOffsets` is used to cut out all runtime scripts,
// which are not needed in FIE mode.
const start = topHalfOfHtml.indexOf(runtimeScript);
let end = start + runtimeScript.length;
let customElements = [],
extensionsMap = [];
if (extensions) {
end = topHalfOfHtml.indexOf(extensionScripts) + extensionScripts.length;
// Filter out extensions that are not custom elements, e.g. amp-mustache.
customElements = extensions.filter((e) => !CUSTOM_TEMPLATES.includes(e));
extensionsMap = customElements.map((ce) => {
return {
'custom-element': ce,
// TODO: Should this be a local URL i.e. /dist/v0/...?
'src': `https://cdn.ampproject.org/v0/${ce}-0.1.js`,
};
});
}
ampAdMeta = `<script amp-ad-metadata type=application/json>
{
"ampRuntimeUtf16CharOffsets": [ ${start}, ${end} ],
"customElementExtensions": ${JSON.stringify(customElements)},
"extensions": ${JSON.stringify(extensionsMap)}
}
</script>`;
}
return `${topHalfOfHtml}
<body>
${body}
${ampAdMeta}
</body>
</html>`;
}
module.exports = {
app,
log,
};

  • Create Pull Request

Remediation feedback:

  • 👍 Like
  • 👎 Dislike
Secure Code Warrior Training Material

● Training

   ▪ Secure Code Warrior Cross-Site Scripting Training

● Videos

   ▪ Secure Code Warrior Cross-Site Scripting Video

 
HighCross-Site Scripting

CWE-79

amp4test.js:64

12024-08-19 04:08am
Vulnerable Code

res.send(doc);
});
app.use('/compose-html', function (req, res) {
res.setHeader('X-XSS-Protection', '0');
res.send(`

1 Data Flow/s detected

app.use('/compose-html', function (req, res) {

⛑️ Remediation Suggestion

--- original
+++ remediated
@@ -1,0 +1,1 @@
+const escape = require('escape-html');
'use strict';
const app = require('express').Router();
const cors = require('./amp-cors');
const minimist = require('minimist');
const argv = minimist(process.argv.slice(2));
const path = require('path');
const upload = require('multer')();
const {getServeMode, replaceUrls} = require('./app-utils');
const {renderShadowViewer} = require('./shadow-viewer');
const CUSTOM_TEMPLATES = ['amp-mustache'];
const SERVE_MODE = getServeMode();
/**
* Logs the given messages to the console when --verbose is specified.
* @param {*} messages
*/
function log(...messages) {
if (argv.verbose) {
console.log.apply(console, messages);
}
}
app.use('/compose-doc', function (req, res) {
res.setHeader('X-XSS-Protection', '0');
const {body, css, experiments, extensions, spec} = req.query;
const frameHtml =
SERVE_MODE == 'minified'
? 'dist.3p/current-min/frame.html'
: 'dist.3p/current/frame.max.html';
let experimentsBlock = '';
if (experiments) {
const string = `"${experiments.split(',').join('","')}"`;
// TODO: Why is setting localDev necessary?
// `allow-doc-opt-in` enables any experiment to be enabled via doc opt-in.
experimentsBlock = `<script>
window.AMP_CONFIG = window.AMP_CONFIG || {"localDev": true};
window.AMP_CONFIG['allow-doc-opt-in'] = (window.AMP_CONFIG['allow-doc-opt-in'] || []).concat([${string}]);
</script>
<meta name="amp-experiments-opt-in" content="${experiments}">`;
}
// TODO: Do we need to inject amp-3p-iframe-src for non-ad tests?
const head = `${experimentsBlock}
<meta name="amp-3p-iframe-src" content="http://localhost:9876/${frameHtml}">`;
const doc = composeDocument({
body,
css,
extensions: extensions ? extensions.split(',') : '',
head,
spec,
});
res.cookie('test-cookie', 'test');
res.send(doc);
});
app.use('/compose-html', function (req, res) {
res.setHeader('X-XSS-Protection', '0');
@@ -64,1 +65,1 @@
- res.send(`
+ res.send(`
<!doctype html>
<html>
<head>
@@ -68,2 +69,2 @@
- <title>NON-AMP TEST</title>
- <meta name="viewport" content="width=device-width,minimum-scale=1,initial-scale=1">
+<title>NON-AMP TEST</title>
+<meta name="viewport" content="width=device-width,minimum-scale=1,initial-scale=1">
</head>
<body>
@@ -72,1 +73,1 @@
-${req.query.body}
+${escape(req.query.body)}
</body>
</html>
@@ -75,1 +76,1 @@
- `);
+`);
});
app.use('/compose-shadow', function (req, res) {
const {docUrl} = req.query;
const viewerHtml = renderShadowViewer({
src: docUrl.replace(/^\//, ''),
baseHref: path.dirname(req.url),
});
res.send(replaceUrls(SERVE_MODE, viewerHtml));
});
/**
* A server side temporary request storage which is useful for testing
* browser sent HTTP requests.
*/
const bank = {};
/**
* Deposit a request. An ID has to be specified. Will override previous request
* if the same ID already exists.
*/
app.use('/request-bank/:bid/deposit/:id/', upload.array(), (req, res) => {
cors.enableCors(req, res);
if (!bank[req.params.bid]) {
bank[req.params.bid] = {};
}
const key = req.params.id;
log('SERVER-LOG [DEPOSIT]: ', key);
if (typeof bank[req.params.bid][key] === 'function') {
bank[req.params.bid][key](req);
} else {
bank[req.params.bid][key] = req;
}
res.end();
});
/**
* Withdraw a request. If the request of the given ID is already in the bank,
* return it immediately. Otherwise wait until it gets deposited
* The same request cannot be withdrawn twice at the same time.
*/
app.use('/request-bank/:bid/withdraw/:id/', (req, res) => {
cors.enableCors(req, res);
if (!bank[req.params.bid]) {
bank[req.params.bid] = {};
}
const key = req.params.id;
log('SERVER-LOG [WITHDRAW]: ' + key);
const result = bank[req.params.bid][key];
if (typeof result === 'function') {
return res
.status(500)
.send(`another client is withdrawing this ID [${key}]`);
}
const callback = function (result) {
if (result === undefined) {
// This happens when tearDown is called but no request
// of given ID has been received yet.
res.status(404).send(`Request of given ID not found: [${key}]`);
} else {
res.json({
headers: result.headers,
body: result.body,
url: result.url,
});
}
delete bank[req.params.bid][key];
};
if (result) {
callback(result);
} else {
bank[req.params.bid][key] = callback;
}
});
/**
* Clean up all pending withdraw & deposit requests.
*/
app.use('/request-bank/:bid/teardown/', (req, res) => {
log('SERVER-LOG [TEARDOWN]');
const b = bank[req.params.bid];
for (const id in b) {
const callback = b[id];
if (typeof callback === 'function') {
// Respond 404 to pending request.
callback();
}
delete b[id];
}
res.end();
});
/**
* Serves a fake ad for test-amp-ad-fake.js
*/
app.get('/a4a/:bid', (req, res) => {
cors.enableCors(req, res);
const {bid} = req.params;
const body = `
<a href=https://amp.dev target=_blank>
<amp-img alt="AMP Ad" height=250 src=//localhost:9876/amp4test/request-bank/${bid}/deposit/image width=300></amp-img>
</a>
<amp-pixel src="//localhost:9876/amp4test/request-bank/${bid}/deposit/pixel/foo?cid=CLIENT_ID(a)"></amp-pixel>
<amp-analytics>
<script type="application/json">
{
"requests": {
"pageview": "//localhost:9876/amp4test/request-bank/${bid}/deposit/analytics/bar"
},
"triggers": {
"pageview": {
"on": "visible",
"request": "pageview",
"extraUrlParams": {
"timestamp": "\${timestamp}",
"title": "\${title}",
"ampdocUrl": "\${ampdocUrl}",
"canonicalUrl": "\${canonicalUrl}",
"cid": "\${clientId(a)}",
"img": "\${htmlAttr(amp-img,src)}",
"navTiming": "\${navTiming(requestStart,requestStart)}",
"navType": "\${navType}",
"navRedirectCount": "\${navRedirectCount}",
"sourceUrl": "\${sourceUrl}",
"cookie": "\${cookie(test-cookie)}"
}
}
}
}
</script>
</amp-analytics>`;
const doc = composeDocument({
spec: 'amp4ads',
body,
css: 'body { background-color: #f4f4f4; }',
extensions: ['amp-analytics'],
});
res.cookie('test-cookie', 'test');
res.send(doc);
});
/**
* @param {{body: string, css?: string|undefined, extensions: Array<string>|undefined, head?: string|undefined, spec?: string|undefined, mode?: string|undefined}} config
* @return {string}
*/
function composeDocument(config) {
const {body, css, extensions, head, mode, spec} = config;
const m = mode || SERVE_MODE;
const cdn = m === 'cdn';
const minified = m === 'minified';
const cssTag = css ? `<style amp-custom>${css}</style>` : '';
// Set link[rel=canonical], CSS boilerplate and runtime <script> depending
// on the AMP spec.
let canonical, boilerplate, runtime;
const amp = spec || 'amp';
switch (amp) {
case 'amp':
canonical = '<link rel="canonical" href="http://nonblocking.io" />';
boilerplate =
'<style amp-boilerplate>body{-webkit-animation:-amp-start 8s steps(1,end) 0s 1 normal both;-moz-animation:-amp-start 8s steps(1,end) 0s 1 normal both;-ms-animation:-amp-start 8s steps(1,end) 0s 1 normal both;animation:-amp-start 8s steps(1,end) 0s 1 normal both}@-webkit-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-moz-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-ms-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-o-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}</style><noscript><style amp-boilerplate>body{-webkit-animation:none;-moz-animation:none;-ms-animation:none;animation:none}</style></noscript>';
runtime = cdn
? 'https://cdn.ampproject.org/v0.js'
: `/dist/${minified ? 'v0' : 'amp'}.js`;
break;
case 'amp4ads':
canonical = '';
boilerplate =
'<style amp4ads-boilerplate>body{visibility:hidden}</style>';
runtime = cdn
? 'https://cdn.ampproject.org/amp4ads-v0.js'
: `/dist/${minified ? 'amp4ads-v0' : 'amp-inabox'}.js`;
break;
case 'amp4email':
canonical = '';
boilerplate =
'<style amp4email-boilerplate>body{visibility:hidden}</style>';
runtime = cdn
? 'https://cdn.ampproject.org/v0.js'
: `/dist/${minified ? 'v0' : 'amp'}.js`;
break;
default:
throw new Error('Unrecognized AMP spec: ' + spec);
}
const runtimeScript = `<script async src="${runtime}"></script>`;
// Generate extension <script> markup.
let extensionScripts = '';
if (extensions) {
extensionScripts = extensions
.map((extension) => {
const tuple = extension.split(':');
const name = tuple[0];
const version = tuple[1] || '0.1';
const src = cdn
? `https://cdn.ampproject.org/v0/${name}-${version}.js`
: `/dist/v0/${name}-${version}.${minified ? '' : 'max.'}js`;
const type = CUSTOM_TEMPLATES.includes(name)
? 'custom-template'
: 'custom-element';
return `<script async ${type}="${name}" src="${src}"></script>`;
})
.join('\n');
}
const topHalfOfHtml = `<!doctype html>
<html ${amp}>
<head>
<title>AMP TEST</title>
<meta charset="utf-8">
${canonical}
<meta name="viewport" content="width=device-width,minimum-scale=1,initial-scale=1">
${head || ''}
${boilerplate}
${runtimeScript}
${extensionScripts}
${cssTag}
</head>`;
// To enable A4A FIE, a <script amp-ad-metadata> tag must exist.
let ampAdMeta = '';
if (amp === 'amp4ads') {
// `ampRuntimeUtf16CharOffsets` is used to cut out all runtime scripts,
// which are not needed in FIE mode.
const start = topHalfOfHtml.indexOf(runtimeScript);
let end = start + runtimeScript.length;
let customElements = [],
extensionsMap = [];
if (extensions) {
end = topHalfOfHtml.indexOf(extensionScripts) + extensionScripts.length;
// Filter out extensions that are not custom elements, e.g. amp-mustache.
customElements = extensions.filter((e) => !CUSTOM_TEMPLATES.includes(e));
extensionsMap = customElements.map((ce) => {
return {
'custom-element': ce,
// TODO: Should this be a local URL i.e. /dist/v0/...?
'src': `https://cdn.ampproject.org/v0/${ce}-0.1.js`,
};
});
}
ampAdMeta = `<script amp-ad-metadata type=application/json>
{
"ampRuntimeUtf16CharOffsets": [ ${start}, ${end} ],
"customElementExtensions": ${JSON.stringify(customElements)},
"extensions": ${JSON.stringify(extensionsMap)}
}
</script>`;
}
return `${topHalfOfHtml}
<body>
${body}
${ampAdMeta}
</body>
</html>`;
}
module.exports = {
app,
log,
};

  • Create Pull Request

Remediation feedback:

  • 👍 Like
  • 👎 Dislike
Secure Code Warrior Training Material

● Training

   ▪ Secure Code Warrior Cross-Site Scripting Training

● Videos

   ▪ Secure Code Warrior Cross-Site Scripting Video

 
HighCross-Site Scripting

CWE-79

amp4test.js:134

12024-08-19 04:08am
Vulnerable Code

}
const callback = function (result) {
if (result === undefined) {
// This happens when tearDown is called but no request
// of given ID has been received yet.
res.status(404).send(`Request of given ID not found: [${key}]`);

1 Data Flow/s detected

app.use('/request-bank/:bid/withdraw/:id/', (req, res) => {

const key = req.params.id;

const callback = function (result) {

res.status(404).send(`Request of given ID not found: [${key}]`);

⛑️ Remediation Suggestion

--- original
+++ remediated
@@ -1,0 +1,1 @@
+const escape = require('escape-html');
'use strict';
const app = require('express').Router();
const cors = require('./amp-cors');
const minimist = require('minimist');
const argv = minimist(process.argv.slice(2));
const path = require('path');
const upload = require('multer')();
const {getServeMode, replaceUrls} = require('./app-utils');
const {renderShadowViewer} = require('./shadow-viewer');
const CUSTOM_TEMPLATES = ['amp-mustache'];
const SERVE_MODE = getServeMode();
/**
* Logs the given messages to the console when --verbose is specified.
* @param {*} messages
*/
function log(...messages) {
if (argv.verbose) {
console.log.apply(console, messages);
}
}
app.use('/compose-doc', function (req, res) {
res.setHeader('X-XSS-Protection', '0');
const {body, css, experiments, extensions, spec} = req.query;
const frameHtml =
SERVE_MODE == 'minified'
? 'dist.3p/current-min/frame.html'
: 'dist.3p/current/frame.max.html';
let experimentsBlock = '';
if (experiments) {
const string = `"${experiments.split(',').join('","')}"`;
// TODO: Why is setting localDev necessary?
// `allow-doc-opt-in` enables any experiment to be enabled via doc opt-in.
experimentsBlock = `<script>
window.AMP_CONFIG = window.AMP_CONFIG || {"localDev": true};
window.AMP_CONFIG['allow-doc-opt-in'] = (window.AMP_CONFIG['allow-doc-opt-in'] || []).concat([${string}]);
</script>
<meta name="amp-experiments-opt-in" content="${experiments}">`;
}
// TODO: Do we need to inject amp-3p-iframe-src for non-ad tests?
const head = `${experimentsBlock}
<meta name="amp-3p-iframe-src" content="http://localhost:9876/${frameHtml}">`;
const doc = composeDocument({
body,
css,
extensions: extensions ? extensions.split(',') : '',
head,
spec,
});
res.cookie('test-cookie', 'test');
res.send(doc);
});
app.use('/compose-html', function (req, res) {
res.setHeader('X-XSS-Protection', '0');
res.send(`
<!doctype html>
<html>
<head>
<title>NON-AMP TEST</title>
<meta name="viewport" content="width=device-width,minimum-scale=1,initial-scale=1">
</head>
<body>
${req.query.body}
</body>
</html>
`);
});
app.use('/compose-shadow', function (req, res) {
const {docUrl} = req.query;
const viewerHtml = renderShadowViewer({
src: docUrl.replace(/^\//, ''),
baseHref: path.dirname(req.url),
});
res.send(replaceUrls(SERVE_MODE, viewerHtml));
});
/**
* A server side temporary request storage which is useful for testing
* browser sent HTTP requests.
*/
const bank = {};
/**
* Deposit a request. An ID has to be specified. Will override previous request
* if the same ID already exists.
*/
app.use('/request-bank/:bid/deposit/:id/', upload.array(), (req, res) => {
cors.enableCors(req, res);
if (!bank[req.params.bid]) {
bank[req.params.bid] = {};
}
const key = req.params.id;
log('SERVER-LOG [DEPOSIT]: ', key);
if (typeof bank[req.params.bid][key] === 'function') {
bank[req.params.bid][key](req);
} else {
bank[req.params.bid][key] = req;
}
res.end();
});
/**
* Withdraw a request. If the request of the given ID is already in the bank,
* return it immediately. Otherwise wait until it gets deposited
* The same request cannot be withdrawn twice at the same time.
*/
app.use('/request-bank/:bid/withdraw/:id/', (req, res) => {
cors.enableCors(req, res);
if (!bank[req.params.bid]) {
bank[req.params.bid] = {};
}
const key = req.params.id;
log('SERVER-LOG [WITHDRAW]: ' + key);
const result = bank[req.params.bid][key];
if (typeof result === 'function') {
return res
.status(500)
@@ -128,1 +129,1 @@
- .send(`another client is withdrawing this ID [${key}]`);
+ .send(`another client is withdrawing this ID [${escape(key)}]`);
}
const callback = function (result) {
if (result === undefined) {
// This happens when tearDown is called but no request
// of given ID has been received yet.
@@ -134,1 +135,1 @@
- res.status(404).send(`Request of given ID not found: [${key}]`);
+ res.status(404).send(`Request of given ID not found: [${escape(key)}]`);
} else {
res.json({
headers: result.headers,
body: result.body,
url: result.url,
});
}
delete bank[req.params.bid][key];
};
if (result) {
callback(result);
} else {
bank[req.params.bid][key] = callback;
}
});
/**
* Clean up all pending withdraw & deposit requests.
*/
app.use('/request-bank/:bid/teardown/', (req, res) => {
log('SERVER-LOG [TEARDOWN]');
const b = bank[req.params.bid];
for (const id in b) {
const callback = b[id];
if (typeof callback === 'function') {
// Respond 404 to pending request.
callback();
}
delete b[id];
}
res.end();
});
/**
* Serves a fake ad for test-amp-ad-fake.js
*/
app.get('/a4a/:bid', (req, res) => {
cors.enableCors(req, res);
const {bid} = req.params;
const body = `
<a href=https://amp.dev target=_blank>
<amp-img alt="AMP Ad" height=250 src=//localhost:9876/amp4test/request-bank/${bid}/deposit/image width=300></amp-img>
</a>
<amp-pixel src="//localhost:9876/amp4test/request-bank/${bid}/deposit/pixel/foo?cid=CLIENT_ID(a)"></amp-pixel>
<amp-analytics>
<script type="application/json">
{
"requests": {
"pageview": "//localhost:9876/amp4test/request-bank/${bid}/deposit/analytics/bar"
},
"triggers": {
"pageview": {
"on": "visible",
"request": "pageview",
"extraUrlParams": {
"timestamp": "\${timestamp}",
"title": "\${title}",
"ampdocUrl": "\${ampdocUrl}",
"canonicalUrl": "\${canonicalUrl}",
"cid": "\${clientId(a)}",
"img": "\${htmlAttr(amp-img,src)}",
"navTiming": "\${navTiming(requestStart,requestStart)}",
"navType": "\${navType}",
"navRedirectCount": "\${navRedirectCount}",
"sourceUrl": "\${sourceUrl}",
"cookie": "\${cookie(test-cookie)}"
}
}
}
}
</script>
</amp-analytics>`;
const doc = composeDocument({
spec: 'amp4ads',
body,
css: 'body { background-color: #f4f4f4; }',
extensions: ['amp-analytics'],
});
res.cookie('test-cookie', 'test');
res.send(doc);
});
/**
* @param {{body: string, css?: string|undefined, extensions: Array<string>|undefined, head?: string|undefined, spec?: string|undefined, mode?: string|undefined}} config
* @return {string}
*/
function composeDocument(config) {
const {body, css, extensions, head, mode, spec} = config;
const m = mode || SERVE_MODE;
const cdn = m === 'cdn';
const minified = m === 'minified';
const cssTag = css ? `<style amp-custom>${css}</style>` : '';
// Set link[rel=canonical], CSS boilerplate and runtime <script> depending
// on the AMP spec.
let canonical, boilerplate, runtime;
const amp = spec || 'amp';
switch (amp) {
case 'amp':
canonical = '<link rel="canonical" href="http://nonblocking.io" />';
boilerplate =
'<style amp-boilerplate>body{-webkit-animation:-amp-start 8s steps(1,end) 0s 1 normal both;-moz-animation:-amp-start 8s steps(1,end) 0s 1 normal both;-ms-animation:-amp-start 8s steps(1,end) 0s 1 normal both;animation:-amp-start 8s steps(1,end) 0s 1 normal both}@-webkit-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-moz-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-ms-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-o-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}</style><noscript><style amp-boilerplate>body{-webkit-animation:none;-moz-animation:none;-ms-animation:none;animation:none}</style></noscript>';
runtime = cdn
? 'https://cdn.ampproject.org/v0.js'
: `/dist/${minified ? 'v0' : 'amp'}.js`;
break;
case 'amp4ads':
canonical = '';
boilerplate =
'<style amp4ads-boilerplate>body{visibility:hidden}</style>';
runtime = cdn
? 'https://cdn.ampproject.org/amp4ads-v0.js'
: `/dist/${minified ? 'amp4ads-v0' : 'amp-inabox'}.js`;
break;
case 'amp4email':
canonical = '';
boilerplate =
'<style amp4email-boilerplate>body{visibility:hidden}</style>';
runtime = cdn
? 'https://cdn.ampproject.org/v0.js'
: `/dist/${minified ? 'v0' : 'amp'}.js`;
break;
default:
throw new Error('Unrecognized AMP spec: ' + spec);
}
const runtimeScript = `<script async src="${runtime}"></script>`;
// Generate extension <script> markup.
let extensionScripts = '';
if (extensions) {
extensionScripts = extensions
.map((extension) => {
const tuple = extension.split(':');
const name = tuple[0];
const version = tuple[1] || '0.1';
const src = cdn
? `https://cdn.ampproject.org/v0/${name}-${version}.js`
: `/dist/v0/${name}-${version}.${minified ? '' : 'max.'}js`;
const type = CUSTOM_TEMPLATES.includes(name)
? 'custom-template'
: 'custom-element';
return `<script async ${type}="${name}" src="${src}"></script>`;
})
.join('\n');
}
const topHalfOfHtml = `<!doctype html>
<html ${amp}>
<head>
<title>AMP TEST</title>
<meta charset="utf-8">
${canonical}
<meta name="viewport" content="width=device-width,minimum-scale=1,initial-scale=1">
${head || ''}
${boilerplate}
${runtimeScript}
${extensionScripts}
${cssTag}
</head>`;
// To enable A4A FIE, a <script amp-ad-metadata> tag must exist.
let ampAdMeta = '';
if (amp === 'amp4ads') {
// `ampRuntimeUtf16CharOffsets` is used to cut out all runtime scripts,
// which are not needed in FIE mode.
const start = topHalfOfHtml.indexOf(runtimeScript);
let end = start + runtimeScript.length;
let customElements = [],
extensionsMap = [];
if (extensions) {
end = topHalfOfHtml.indexOf(extensionScripts) + extensionScripts.length;
// Filter out extensions that are not custom elements, e.g. amp-mustache.
customElements = extensions.filter((e) => !CUSTOM_TEMPLATES.includes(e));
extensionsMap = customElements.map((ce) => {
return {
'custom-element': ce,
// TODO: Should this be a local URL i.e. /dist/v0/...?
'src': `https://cdn.ampproject.org/v0/${ce}-0.1.js`,
};
});
}
ampAdMeta = `<script amp-ad-metadata type=application/json>
{
"ampRuntimeUtf16CharOffsets": [ ${start}, ${end} ],
"customElementExtensions": ${JSON.stringify(customElements)},
"extensions": ${JSON.stringify(extensionsMap)}
}
</script>`;
}
return `${topHalfOfHtml}
<body>
${body}
${ampAdMeta}
</body>
</html>`;
}
module.exports = {
app,
log,
};

  • Create Pull Request

Remediation feedback:

  • 👍 Like
  • 👎 Dislike
Secure Code Warrior Training Material

● Training

   ▪ Secure Code Warrior Cross-Site Scripting Training

● Videos

   ▪ Secure Code Warrior Cross-Site Scripting Video

Findings Overview

Severity Vulnerability Type CWE Language Count
High Path/Directory Traversal CWE-22 JavaScript / TypeScript* 3
High Cross-Site Scripting CWE-79 JavaScript / TypeScript* 18
High Server Side Request Forgery CWE-918 JavaScript / TypeScript* 4
High Integer Overflow CWE-190 C/C++ (Beta) 1
High Cross-Site Scripting CWE-79 Go 6
High Origin Validation Error CWE-346 JavaScript / TypeScript* 4
High DOM Based Cross-Site Scripting CWE-79 JavaScript / TypeScript* 6
High Server Side Request Forgery CWE-918 Go 2
Medium Regex Denial of Service (ReDoS) CWE-1333 JavaScript / TypeScript* 9
Medium Hidden HTML Input CWE-472 Python 41
Medium Out of Buffer Bounds Read CWE-125 C/C++ (Beta) 2
Medium Miscellaneous Dangerous Functions CWE-676 Python 4
Low Improper Input Validation CWE-20 JavaScript / TypeScript* 1
Low Cookie Without 'HttpOnly' Flag CWE-1004 JavaScript / TypeScript* 3
Low Log Forging CWE-117 JavaScript / TypeScript* 2
Low Sensitive Cookie Without Secure CWE-614 JavaScript / TypeScript* 3
Low Unvalidated/Open Redirect CWE-601 JavaScript / TypeScript* 4
Low Log Forging CWE-117 Go 2

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.