Files
2026-07-13 13:23:39 +08:00

367 lines
13 KiB
TypeScript

import fs from 'node:fs';
import type { Server } from 'node:http';
import path from 'node:path';
import { setTimeout } from 'node:timers/promises';
import bodyParser from 'body-parser';
import { entries } from 'crawlee';
import type { Application } from 'express';
import express from 'express';
export const startExpressAppPromise = async (app: Application, port: number) => {
return new Promise<Server>((resolve) => {
const server = app.listen(port, () => resolve(server));
});
};
export const responseSamples = {
json: { foo: 'bar' },
xml:
'<?xml version="1.0" encoding="UTF-8"?>\n' +
'<items>\n' +
'<item>\n' +
' <url>https://apify.com</url>\n' +
' <title>Web Scraping, Data Extraction and Automation &#xb7; Apify</title>\n' +
'</item>\n' +
'</items>',
complexXml: fs.readFileSync(path.join(__dirname, 'data/complex.xml'), 'utf-8'),
image: fs.readFileSync(path.join(__dirname, 'data/apify.png')),
html: `<!doctype html>
<html>
<head>
<title>Example Domain</title>
<meta charset="utf-8">
<meta http-equiv="Content-type" content="text/html; charset=utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style type="text/css">
body {
background-color: #f0f0f2;
margin: 0;
padding: 0;
font-family: -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
}
div {
width: 600px;
margin: 5em auto;
padding: 2em;
background-color: #fdfdff;
border-radius: 0.5em;
box-shadow: 2px 3px 7px 2px rgba(0,0,0,0.02);
}
a:link, a:visited {
color: #38488f;
text-decoration: none;
}
@media (max-width: 700px) {
div {
margin: 0 auto;
width: auto;
}
}
</style>
</head>
<body>
<div>
<h1>Example Domain</h1>
<p>This domain is for use in illustrative examples in documents. You may use this
domain in literature without prior coordination or asking for permission.</p>
<p><a href="https://www.iana.org/domains/example">More information...</a></p>
</div>
</body>
</html>`,
resources: `
<html><body>
<link rel="stylesheet" type="text/css" href="/style.css">
<img src="/image.png">
<img src="/image.gif">
<script src="/script.js" defer="defer"></script>
</body></html>`,
cacheable: {
html: `
<!doctype html>
<html>
<head>
<title>Cacheable example website</title>
<meta charset="utf-8">
<meta http-equiv="Content-type" content="text/html; charset=utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="cacheable/style.css">
<script src="cacheable/script.js"></script>
</body>
</html>
`,
css: `
body {
background-color: #f0f0f2;
margin: 0;
padding: 0;
font-family: -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
}
`,
js: `
console.log('Hello world!');
`,
},
htmlWithOutsideRedirect: `
<html>
<head>
<title>Redirecting outside</title>
</head>
<body>
<a href="/special/redirect-outside">click me</a>
</body>
</html>`,
cloudflareBlocking: `
<!DOCTYPE html>
<head>
<title>Just a moment...</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=Edge">
<meta name="robots" content="noindex,nofollow">
<meta name="viewport" content="width=device-width,initial-scale=1">
<link href="/cdn-cgi/styles/challenges.css" rel="stylesheet">
<meta http-equiv="refresh" content="375">
<script src="/cdn-cgi/challenge-platform/h/g/orchestrate/chl_page/v1?ray=837fec16d9534138"></script><script src="https://challenges.cloudflare.com/turnstile/v0/g/74bd6362/api.js?onload=AudPIu1&amp;render=explicit" async="" defer="" crossorigin="anonymous"></script>
</head>
<body class="no-js">
<div class="main-wrapper" role="main">
<div class="main-content">
<h1 class="zone-name-title h1">dummypage.co</h1>
<h2 id="challenge-running" class="h2">Checking if the site connection is secure</h2>
<div id="challenge-stage" style="display: flex;">
<div id="turnstile-wrapper" class="captcha-prompt spacer">
<div><iframe style="border: medium; overflow: hidden; width: 300px; height: 65px;" src="https://challenges.cloudflare.com/cdn-cgi/challenge-platform/h/g/turnstile/if/ov2/av0/rcv0/0/o9un1/0x4AAAAAAADnPIDROrmt1Wwj/dark/normal" allow="cross-origin-isolated; fullscreen" sandbox="allow-same-origin allow-scripts allow-popups" id="cf-chl-widget-o9un1" tabindex="0" title="Widget containing a Cloudflare security challenge"></iframe><input type="hidden" name="cf-turnstile-response" id="cf-chl-widget-o9un1_response"></div>
</div>
</div>
<div id="challenge-spinner" class="spacer loading-spinner" style="display: none; visibility: hidden;">
<div class="lds-ring">
<div></div>
<div></div>
<div></div>
<div></div>
</div>
</div>
<div id="challenge-body-text" class="core-msg spacer">dummypage.co needs to review the security of your connection before proceeding.</div>
<div id="challenge-success" style="display: none;">
<div class="h2">
<span class="icon-wrapper">
</span>Connection is secure</div>
<div class="core-msg spacer">Proceeding...</div>
</div>
<noscript>
<div id="challenge-error-title">
<div class="h2">
<span class="icon-wrapper">
<div class="heading-icon warning-icon"></div>
</span>
<span id="challenge-error-text">Enable JavaScript and cookies to continue</span>
</div>
</div>
</noscript>
</div>
</div>
<div class="footer" role="contentinfo">
<div class="footer-inner">
<div class="clearfix diagnostic-wrapper">
<div class="ray-id">Ray ID: <code>837fec16d9534138</code></div>
</div>
<div class="text-center" id="footer-text">Performance &amp; security by <a rel="noopener noreferrer" href="https://www.cloudflare.com?utm_source=challenge&amp;utm_campaign=m" target="_blank">Cloudflare</a></div>
</div>
</div>
</body>
</html>`,
outsideIframe: `
<!DOCTYPE html>
<html>
<head>
<title>Outside iframe title</title>
</head>
<body>
<h1>Outside iframe</h1>
<iframe src="./inside-iframe"></iframe>
</body>
</html>`,
insideIframe: `
<!DOCTYPE html>
<html>
<head>
<title>In iframe</title>
</head>
<body>
<h1>In iframe</h1>
<p>Some content from inside of an iframe.</p>
</body>
</html>`,
shadowRoots: `
<html>
<body>
<div id="open-container">
<template shadowrootmode="open">
<p>[GOOD] This is inside the OPEN shadow DOM.</p>
<div>
<template shadowrootmode="open">
<p>[GOOD] This is inside of inside of the OPEN shadow DOM.</p>
</template>
</div>
</template>
</div>
<div id="closed-container">
<template shadowrootmode="closed">
<p>[BAD] This is inside the CLOSED shadow DOM.</p>
<div>
<template shadowrootmode="open">
<p>[BAD] This is inside of inside of the CLOSED shadow DOM.</p>
</template>
</div>
</template>
</div>
</body>
</html>`,
};
export async function runExampleComServer(): Promise<[Server, number]> {
const app = express();
app.use(
bodyParser.urlencoded({
extended: true,
}),
);
app.use(bodyParser.json());
const special = express.Router();
const cacheable = express.Router();
// "special" pages with debugging info and responses for use in tests
(() => {
special.get('/getRawHeaders', (req, res) => {
res.send(JSON.stringify(req.rawHeaders));
});
special.all('/getDebug', (req, res) => {
res.json({
headers: req.headers,
method: req.method,
bodyLength: +(req.headers['content-length'] ?? 0),
});
});
special.post('/mock', (req, res) => {
const { headers, statusCode, error = false, body } = req.body;
if (error) {
throw new Error(error);
}
entries(headers as Record<string, string>).forEach(([key, value]) => res.setHeader(key, value));
res.status(statusCode).send(body);
});
special.get('/headers', (req, res) => {
res.status(200).json({ headers: req.headers });
});
special.get('/invalidContentType', (_req, res) => {
res.send({ some: 'json' });
});
special.post('/jsonError', (_req, res) => {
res.status(500).json({ message: 'CUSTOM_ERROR' });
});
special.get('/mirror', (_req, res) => {
res.send('<html><head><title>Title</title></head><body>DATA</body></html>');
});
special.get('/html-type', (_req, res) => {
res.send(responseSamples.html);
});
special.get('/json-type', (_req, res) => {
res.json(responseSamples.json);
});
special.get('/xml-type', (_req, res) => {
res.type('application/xml');
res.send(responseSamples.xml);
});
special.get('/complex-xml', (_req, res) => {
res.type('application/xml');
res.send(responseSamples.complexXml);
});
special.get('/image-type', (_req, res) => {
res.type('image/png');
res.send(responseSamples.image);
});
special.get('/timeout', async (_req, res) => {
await setTimeout(32000);
res.type('html').send('<div>TEST</div>');
});
special.get('/resources', async (_req, res) => {
res.type('html').send(responseSamples.resources);
});
special.get('/redirect', (_req, res) => {
res.type('html').send(responseSamples.htmlWithOutsideRedirect);
});
special.get('/redirect-outside', (req, res) => {
res.redirect('https://example.com');
});
special.get('/cloudflareBlocking', async (_req, res) => {
res.type('html').status(403).send(responseSamples.cloudflareBlocking);
});
special.get('/outside-iframe', (_req, res) => {
res.type('html').send(responseSamples.outsideIframe);
});
special.get('/outside-iframe-csp', (_req, res) => {
res.setHeader('Content-Security-Policy', "require-trusted-types-for 'script'");
res.type('html').send(responseSamples.outsideIframe);
});
special.get('/inside-iframe', (_req, res) => {
res.type('html').send(responseSamples.insideIframe);
});
special.get('/shadow-root', (_req, res) => {
res.type('html').send(responseSamples.shadowRoots);
});
special.get('/html-entities', (_req, res) => {
res.type('html').send('&quot;&lt;&gt;"<>');
});
})();
// "cacheable" site with one page, scripts and stylesheets
(() => {
cacheable.get('/', (req, res) => {
res.send(responseSamples.cacheable.html);
});
cacheable.get('/style.css', (req, res) => {
res.type('text/css').send(responseSamples.cacheable.css);
});
cacheable.get('/script.js', (req, res) => {
res.type('application/javascript').send(responseSamples.cacheable.js);
});
})();
app.use('/special', special);
app.use('/cacheable', cacheable);
app.get('**/*', async (req, res) => {
await setTimeout(50);
res.send(responseSamples.html);
});
const server = await startExpressAppPromise(app, 0);
const { port } = server.address() as any;
return [server, port];
}