mirror of
https://github.com/donlon/cloudflare-error-page.git
synced 2025-12-24 17:29:16 +00:00
Compare commits
11 Commits
javascript
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
59c1f0dcfb | ||
|
|
f90fa3501b | ||
|
|
3d470d55a4 | ||
|
|
d6cf4bbedb | ||
|
|
48169fd3fa | ||
|
|
ae8849f39f | ||
|
|
f50f81afd1 | ||
|
|
d39409c0ca | ||
|
|
8be87415fe | ||
|
|
fb52f6d523 | ||
|
|
5a633b5958 |
15
.gitignore
vendored
15
.gitignore
vendored
@@ -1,14 +1,13 @@
|
|||||||
.vscode/
|
.vscode/
|
||||||
|
.DS_Store
|
||||||
build/
|
|
||||||
*.egg-info/
|
|
||||||
__pycache__/
|
|
||||||
dist/
|
|
||||||
|
|
||||||
node_modules/
|
|
||||||
|
|
||||||
.venv/
|
.venv/
|
||||||
venv/
|
venv/
|
||||||
ttt/
|
|
||||||
|
node_modules/
|
||||||
|
build/
|
||||||
|
dist/
|
||||||
|
*.egg-info/
|
||||||
|
__pycache__/
|
||||||
|
|
||||||
instance/
|
instance/
|
||||||
|
|||||||
50
README.md
50
README.md
@@ -8,7 +8,7 @@ This project creates customized error pages that mimics the well-known Cloudflar
|
|||||||
|
|
||||||
## Online Editor
|
## Online Editor
|
||||||
|
|
||||||
Here's an online editor to create customized error pages. Try it out [here](https://virt.moe/cferr/editor/).
|
Here's an online editor to create customized error pages and example server apps. Try it out [here](https://virt.moe/cferr/editor/).
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -66,12 +66,52 @@ You can also see live demo [here](https://virt.moe/cferr/examples/default).
|
|||||||
|
|
||||||
A demo server using Flask is also available in [flask_demo.py](examples/flask_demo.py).
|
A demo server using Flask is also available in [flask_demo.py](examples/flask_demo.py).
|
||||||
|
|
||||||
### Node.js/NPM
|
### JavaScript/NodeJS
|
||||||
|
|
||||||
A Node.js package is available in [nodejs](nodejs) folder. However currently it supports only Node.js but not web browsers,
|
Install the `cloudflare-error-page` package using npm:
|
||||||
and we plan to refactor it into a shared package, so it can work in both environments.
|
|
||||||
|
|
||||||
(Thanks [@junduck](https://github.com/junduck) for creating this.)
|
``` Bash
|
||||||
|
npm install cloudflare-error-page
|
||||||
|
```
|
||||||
|
|
||||||
|
The following example demonstrates how to create a simple Express server and return the error page to visitors.
|
||||||
|
|
||||||
|
``` JavaScript
|
||||||
|
import express from 'express';
|
||||||
|
import { render as render_cf_error_page } from 'cloudflare-error-page';
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
const port = 3000;
|
||||||
|
|
||||||
|
// Define a route for GET requests to the root URL
|
||||||
|
app.get('/', (req, res) => {
|
||||||
|
res.status(500).send(render_cf_error_page({
|
||||||
|
title: "Internal server error",
|
||||||
|
// Browser status is ok
|
||||||
|
browser_status: {
|
||||||
|
status: 'ok',
|
||||||
|
},
|
||||||
|
// Cloudflare status is error
|
||||||
|
cloudflare_status: {
|
||||||
|
status: 'error',
|
||||||
|
status_text: 'Error',
|
||||||
|
},
|
||||||
|
// Host status is also ok
|
||||||
|
host_status: {
|
||||||
|
status: 'ok',
|
||||||
|
location: 'example.com',
|
||||||
|
},
|
||||||
|
error_source: "cloudflare",
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start the server and listen on the specified port
|
||||||
|
app.listen(port, () => {
|
||||||
|
console.log(`Example app listening at http://localhost:${port}`);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
(Thanks [@junduck](https://github.com/junduck) for creating the original NodeJS version.)
|
||||||
|
|
||||||
### PHP
|
### PHP
|
||||||
|
|
||||||
|
|||||||
@@ -170,8 +170,10 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
min-height: 700px !important;
|
min-height: 700px !important;
|
||||||
|
max-height: 1200px !important;
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
font-size: 0.8em !important;
|
font-size: 0.8em !important;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.save-as-dialog__buttons {
|
.save-as-dialog__buttons {
|
||||||
@@ -236,6 +238,7 @@
|
|||||||
<option value="empty">Empty</option>
|
<option value="empty">Empty</option>
|
||||||
<option value="catastrophic">Catastrophic failure</option>
|
<option value="catastrophic">Catastrophic failure</option>
|
||||||
<option value="working">Server working</option>
|
<option value="working">Server working</option>
|
||||||
|
<option value="teapot">Teapot</option>
|
||||||
<option value="consensual">Myth of consensual</option>
|
<option value="consensual">Myth of consensual</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
@@ -473,7 +476,7 @@
|
|||||||
<button type="button" id="btnOpen" class="btn btn-sm btn-primary">Preview in new tab</button>
|
<button type="button" id="btnOpen" class="btn btn-sm btn-primary">Preview in new tab</button>
|
||||||
<!-- Button trigger modal -->
|
<!-- Button trigger modal -->
|
||||||
<button type="button" class="btn btn-sm btn-primary" data-bs-toggle="modal" data-bs-target="#saveAsDialog">
|
<button type="button" class="btn btn-sm btn-primary" data-bs-toggle="modal" data-bs-target="#saveAsDialog">
|
||||||
Save as...
|
Save as & Examples ...
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -530,10 +533,10 @@
|
|||||||
Python Example
|
Python Example
|
||||||
</button>
|
</button>
|
||||||
<button type="button" data-type="js" class="list-group-item list-group-item-action">
|
<button type="button" data-type="js" class="list-group-item list-group-item-action">
|
||||||
NodeJS Example
|
JavaScript Example
|
||||||
</button>
|
</button>
|
||||||
<button type="button" data-type="static" class="list-group-item list-group-item-action">
|
<button type="button" data-type="static" class="list-group-item list-group-item-action">
|
||||||
Static Page
|
Static Webpage
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex gap-1 save-as-dialog__buttons">
|
<div class="d-flex gap-1 save-as-dialog__buttons">
|
||||||
@@ -545,8 +548,9 @@
|
|||||||
Save
|
Save
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<textarea id="saveAsDialogCode" class="form-control save-as-dialog__code" spellcheck="false"
|
<div class="border rounded save-as-dialog__code">
|
||||||
readonly></textarea>
|
<pre id="saveAsDialogCode" class="px-2 py-1 h-100"></pre>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
"@types/ejs": "^3.1.5",
|
"@types/ejs": "^3.1.5",
|
||||||
"@types/html-minifier-terser": "^7.0.2",
|
"@types/html-minifier-terser": "^7.0.2",
|
||||||
"@types/node": "^24.10.2",
|
"@types/node": "^24.10.2",
|
||||||
|
"@types/prismjs": "^1.26.5",
|
||||||
"html-minifier-terser": "^7.2.0",
|
"html-minifier-terser": "^7.2.0",
|
||||||
"prettier": "3.7.4",
|
"prettier": "3.7.4",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
@@ -22,6 +23,8 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bootstrap": "^5.3.8",
|
"bootstrap": "^5.3.8",
|
||||||
"ejs": "^3.1.10"
|
"cloudflare-error-page": "../../javascript",
|
||||||
|
"ejs": "^3.1.10",
|
||||||
|
"prismjs": "^1.30.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import ejs from 'ejs';
|
import ejs from 'ejs';
|
||||||
|
import { ErrorPageParams } from 'cloudflare-error-page';
|
||||||
|
|
||||||
import jsTemplate from './js.ejs?raw';
|
import jsTemplate from './js.ejs?raw';
|
||||||
import jsonTemplate from './json.ejs?raw';
|
import jsonTemplate from './json.ejs?raw';
|
||||||
@@ -6,21 +7,98 @@ import pythonTemplate from './python.ejs?raw';
|
|||||||
|
|
||||||
interface CodeGen {
|
interface CodeGen {
|
||||||
name: string;
|
name: string;
|
||||||
generate(params: any): string;
|
language: string;
|
||||||
|
generate(params: ErrorPageParams): string;
|
||||||
}
|
}
|
||||||
|
|
||||||
class EjsCodeGen implements CodeGen {
|
class EjsCodeGen implements CodeGen {
|
||||||
name: string;
|
name: string;
|
||||||
|
language: string;
|
||||||
private template: ejs.TemplateFunction;
|
private template: ejs.TemplateFunction;
|
||||||
constructor(name: string, templateContent: any) {
|
constructor(name: string, language: string, templateContent: string) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
|
this.language = language;
|
||||||
this.template = ejs.compile(templateContent);
|
this.template = ejs.compile(templateContent);
|
||||||
}
|
}
|
||||||
generate(params: any): string {
|
protected prepareTemplateArgs(params: ErrorPageParams): Record<string, any> {
|
||||||
return this.template({ params });
|
return {};
|
||||||
|
}
|
||||||
|
generate(params: ErrorPageParams): string {
|
||||||
|
const moreArgs = this.prepareTemplateArgs(params);
|
||||||
|
return this.template({ params, ...moreArgs });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const jsCodeGen = new EjsCodeGen('NodeJS Example', jsTemplate);
|
function getErrorCode(error_code?: string | number) {
|
||||||
export const jsonCodeGen = new EjsCodeGen('JSON', jsonTemplate);
|
const errorCode = error_code || '';
|
||||||
export const pythonCodeGen = new EjsCodeGen('Python Example', pythonTemplate);
|
return /\d{3}/.test(errorCode + '') ? errorCode : 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
class JSCodeGen extends EjsCodeGen {
|
||||||
|
constructor(templateContent: string) {
|
||||||
|
super('JavaScript Example', 'javascript', templateContent);
|
||||||
|
}
|
||||||
|
private formatJSArgs(params: ErrorPageParams): string {
|
||||||
|
params = { ...params };
|
||||||
|
const rayIdKey = Math.random() + '';
|
||||||
|
const clientIpKey = Math.random() + '';
|
||||||
|
params.ray_id = rayIdKey;
|
||||||
|
params.client_ip = clientIpKey;
|
||||||
|
const paramsArg = JSON.stringify(params, null, 2)
|
||||||
|
.replace(`"${rayIdKey}"`, "(req.get('Cf-Ray') ?? '').substring(0, 16)")
|
||||||
|
.replace(`"${clientIpKey}"`, "req.get('X-Forwarded-For') || req.socket.remoteAddress");
|
||||||
|
return paramsArg;
|
||||||
|
}
|
||||||
|
protected prepareTemplateArgs(params: ErrorPageParams): Record<string, any> {
|
||||||
|
return {
|
||||||
|
errorCode: getErrorCode(params.error_code),
|
||||||
|
// TODO: format to JS-style object (key w/o parens)
|
||||||
|
indentedParams: this.formatJSArgs(params).replaceAll('\n', '\n '),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PythonCodeGen extends EjsCodeGen {
|
||||||
|
constructor(templateContent: string) {
|
||||||
|
super('Python Example', 'python', templateContent);
|
||||||
|
}
|
||||||
|
private formatPythonArgs(params: ErrorPageParams): string {
|
||||||
|
// Covert the parameters to Python format object
|
||||||
|
params = { ...params };
|
||||||
|
const randomKey = Math.random() + '';
|
||||||
|
const rayIdKey = Math.random() + '';
|
||||||
|
const clientIpKey = Math.random() + '';
|
||||||
|
params.ray_id = rayIdKey;
|
||||||
|
params.client_ip = clientIpKey;
|
||||||
|
const paramsArg = JSON.stringify(
|
||||||
|
params,
|
||||||
|
(key, value) => {
|
||||||
|
if (typeof value === 'boolean') {
|
||||||
|
return randomKey + value.toString();
|
||||||
|
} else if (value === null) {
|
||||||
|
return randomKey + 'null';
|
||||||
|
} else {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
4
|
||||||
|
)
|
||||||
|
.replace(`"${randomKey}true"`, 'True')
|
||||||
|
.replace(`"${randomKey}false"`, 'False')
|
||||||
|
.replace(`"${randomKey}null"`, 'None')
|
||||||
|
.replace(`"${rayIdKey}"`, 'request.headers.get("Cf-Ray", "")[:16]')
|
||||||
|
.replace(`"${clientIpKey}"`, 'request.headers.get("X-Forwarded-For") or request.remote_addr');
|
||||||
|
return paramsArg;
|
||||||
|
}
|
||||||
|
protected prepareTemplateArgs(params: ErrorPageParams): Record<string, any> {
|
||||||
|
return {
|
||||||
|
errorCode: getErrorCode(params.error_code),
|
||||||
|
// TODO: format to JS-style object (key w/o parens)
|
||||||
|
indentedParams: this.formatPythonArgs(params).replaceAll('\n', '\n '),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const jsCodeGen = new JSCodeGen(jsTemplate);
|
||||||
|
export const jsonCodeGen = new EjsCodeGen('JSON', 'json', jsonTemplate);
|
||||||
|
export const pythonCodeGen = new PythonCodeGen(pythonTemplate);
|
||||||
|
|||||||
@@ -1,13 +1,21 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/*
|
||||||
|
* JavaScript Example: Render and serve the Cloudflare error page using Express server
|
||||||
|
*
|
||||||
|
* Prerequisits:
|
||||||
|
* npm install express cloudflare-error-page
|
||||||
|
*/
|
||||||
|
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
import { render as render_cf_error_page } from 'cloudflare-error-page';
|
import { render as render_cf_error_page } from 'cloudflare-error-page';
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const port = 3000;
|
const port = 3000;
|
||||||
|
|
||||||
|
<%# TODO: use the library as middleware? _%>
|
||||||
// Define a route for GET requests to the root URL
|
// Define a route for GET requests to the root URL
|
||||||
<%# TODO: format to JS-style object (key w/o parens) _%>
|
|
||||||
app.get('/', (req, res) => {
|
app.get('/', (req, res) => {
|
||||||
res.status(500).send(render_cf_error_page(<%-JSON.stringify(params, null, 2).replaceAll('\n', '\n ')%>));
|
res.status(<%= errorCode %>).send(render_cf_error_page(<%- indentedParams %>));
|
||||||
});
|
});
|
||||||
|
|
||||||
// Start the server and listen on the specified port
|
// Start the server and listen on the specified port
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
<%-JSON.stringify(params, null, 4)%>
|
<%- JSON.stringify(params, null, 4) %>
|
||||||
|
|||||||
@@ -1,29 +1,22 @@
|
|||||||
<%
|
#!/usr/bin/env python3
|
||||||
// Covert the parameters to Python format object
|
#
|
||||||
const randomKey = Math.random() + ''
|
# Python Example: Render and serve the Cloudflare error page using Flask server
|
||||||
const paramsArg = JSON.stringify(params, (key, value) => {
|
#
|
||||||
if (typeof value === 'boolean') {
|
# Prerequisits:
|
||||||
return randomKey + value.toString()
|
# pip install flask cloudflare-error-page
|
||||||
} else if (value === null) {
|
#
|
||||||
return randomKey + 'null'
|
|
||||||
} else {
|
from flask import Flask, request
|
||||||
return value
|
|
||||||
}
|
|
||||||
}, 4)
|
|
||||||
.replace(`"${randomKey}true"`, 'True')
|
|
||||||
.replace(`"${randomKey}false"`, 'False')
|
|
||||||
.replace(`"${randomKey}null"`, 'None')
|
|
||||||
_%>
|
|
||||||
from flask import Flask
|
|
||||||
from cloudflare_error_page import render as render_cf_error_page
|
from cloudflare_error_page import render as render_cf_error_page
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
<%# TODO: use the library as middleware? _%>
|
||||||
# Define a route for GET requests to the root URL
|
# Define a route for GET requests to the root URL
|
||||||
@app.route('/')
|
@app.route("/")
|
||||||
def index():
|
def index():
|
||||||
# Render the error page
|
# Render the error page
|
||||||
return render_cf_error_page(<%- paramsArg.replaceAll('\n', '\n ') %>), 500
|
return render_cf_error_page(<%- indentedParams %>), <%= errorCode %>
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == "__main__":
|
||||||
app.run(debug=True, port=5000)
|
app.run(debug=True, port=5000)
|
||||||
|
|||||||
@@ -5,17 +5,19 @@
|
|||||||
- inputs call render() on change
|
- inputs call render() on change
|
||||||
- "Open in new tab" opens the rendered HTML in a new window using a blob URL
|
- "Open in new tab" opens the rendered HTML in a new window using a blob URL
|
||||||
*/
|
*/
|
||||||
import ejs from 'ejs';
|
|
||||||
import templateContent from './template.ejs?raw';
|
|
||||||
|
|
||||||
import 'bootstrap/js/src/modal.js';
|
import 'bootstrap/js/src/modal.js';
|
||||||
import Popover from 'bootstrap/js/src/popover.js';
|
import Popover from 'bootstrap/js/src/popover.js';
|
||||||
|
import Prism from 'prismjs';
|
||||||
|
import 'prismjs/components/prism-json.js';
|
||||||
|
import 'prismjs/components/prism-python.js';
|
||||||
|
import { render as render_cf_error_page } from 'cloudflare-error-page';
|
||||||
|
|
||||||
import 'bootstrap/dist/css/bootstrap.min.css';
|
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||||
|
import 'prismjs/themes/prism.css';
|
||||||
|
|
||||||
import { jsCodeGen, jsonCodeGen, pythonCodeGen } from './codegen';
|
import { jsCodeGen, jsonCodeGen, pythonCodeGen } from './codegen';
|
||||||
|
|
||||||
let template = ejs.compile(templateContent);
|
|
||||||
|
|
||||||
// can be changed if specified by '?from=<name>'
|
// can be changed if specified by '?from=<name>'
|
||||||
let initialConfig = {
|
let initialConfig = {
|
||||||
title: 'Internal server error',
|
title: 'Internal server error',
|
||||||
@@ -101,6 +103,31 @@ const PRESETS = {
|
|||||||
what_happened: 'This site is still working. And it looks great.',
|
what_happened: 'This site is still working. And it looks great.',
|
||||||
what_can_i_do: 'Visit the site before it crashes someday.',
|
what_can_i_do: 'Visit the site before it crashes someday.',
|
||||||
},
|
},
|
||||||
|
teapot: {
|
||||||
|
title: "I'm a teapot",
|
||||||
|
error_code: '418',
|
||||||
|
more_information: {
|
||||||
|
text: 'rfc2324',
|
||||||
|
link: 'https://www.rfc-editor.org/rfc/rfc2324',
|
||||||
|
},
|
||||||
|
browser_status: {
|
||||||
|
status: 'ok',
|
||||||
|
location: 'You',
|
||||||
|
status_text: 'Working',
|
||||||
|
},
|
||||||
|
cloudflare_status: {
|
||||||
|
status: 'ok',
|
||||||
|
status_text: 'Working',
|
||||||
|
},
|
||||||
|
host_status: {
|
||||||
|
status: 'ok',
|
||||||
|
location: 'Teapot',
|
||||||
|
status_text: 'Working',
|
||||||
|
},
|
||||||
|
error_source: 'host',
|
||||||
|
what_happened: "The server can not brew coffee for it is a teapot.",
|
||||||
|
what_can_i_do: 'Please try a different coffee machine.',
|
||||||
|
},
|
||||||
consensual: {
|
consensual: {
|
||||||
title: 'The Myth Of "Consensual" Internet',
|
title: 'The Myth Of "Consensual" Internet',
|
||||||
error_code: 'lmao',
|
error_code: 'lmao',
|
||||||
@@ -255,24 +282,9 @@ function readConfig() {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
function formatUtcTimestamp() {
|
|
||||||
const d = new Date();
|
|
||||||
|
|
||||||
const year = d.getUTCFullYear();
|
|
||||||
const month = String(d.getUTCMonth() + 1).padStart(2, '0');
|
|
||||||
const day = String(d.getUTCDate()).padStart(2, '0');
|
|
||||||
|
|
||||||
const hours = String(d.getUTCHours()).padStart(2, '0');
|
|
||||||
const minutes = String(d.getUTCMinutes()).padStart(2, '0');
|
|
||||||
const seconds = String(d.getUTCSeconds()).padStart(2, '0');
|
|
||||||
|
|
||||||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds} UTC`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderEjs(params) {
|
function renderEjs(params) {
|
||||||
return template({
|
return render_cf_error_page(params, false);
|
||||||
params: params,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Basic render: build HTML string from config and put into iframe.srcdoc */
|
/* Basic render: build HTML string from config and put into iframe.srcdoc */
|
||||||
@@ -280,9 +292,8 @@ function render() {
|
|||||||
const cfg = readConfig();
|
const cfg = readConfig();
|
||||||
window.lastCfg = cfg;
|
window.lastCfg = cfg;
|
||||||
|
|
||||||
cfg.time = formatUtcTimestamp();
|
// TODO: input box for Ray ID / Client IP?
|
||||||
cfg.ray_id = '0123456789abcdef';
|
cfg.ray_id = '0123456789abcdef';
|
||||||
cfg.client_ip = '1.1.1.1';
|
|
||||||
if (Number.isNaN(Number(cfg.error_code))) {
|
if (Number.isNaN(Number(cfg.error_code))) {
|
||||||
cfg.html_title = cfg.title || 'Internal server error';
|
cfg.html_title = cfg.title || 'Internal server error';
|
||||||
}
|
}
|
||||||
@@ -305,8 +316,8 @@ function render() {
|
|||||||
let lastRenderedHtml = '';
|
let lastRenderedHtml = '';
|
||||||
function openInNewTab() {
|
function openInNewTab() {
|
||||||
if (!lastRenderedHtml) render();
|
if (!lastRenderedHtml) render();
|
||||||
const wnd = window.open()
|
const wnd = window.open();
|
||||||
wnd.document.documentElement.innerHTML = lastRenderedHtml
|
wnd.document.documentElement.innerHTML = lastRenderedHtml;
|
||||||
}
|
}
|
||||||
|
|
||||||
function createShareableLink() {
|
function createShareableLink() {
|
||||||
@@ -445,15 +456,21 @@ function updateSaveAsDialog(e) {
|
|||||||
}
|
}
|
||||||
const params = { ...lastCfg };
|
const params = { ...lastCfg };
|
||||||
delete params.time;
|
delete params.time;
|
||||||
|
delete params.ray_id;
|
||||||
|
let language;
|
||||||
if (codegen) {
|
if (codegen) {
|
||||||
saveAsContent = codegen.generate(params);
|
saveAsContent = codegen.generate(params);
|
||||||
|
language = codegen.language;
|
||||||
} else if (saveAsType == 'static') {
|
} else if (saveAsType == 'static') {
|
||||||
render() // rerender the page
|
render(); // rerender the page
|
||||||
saveAsContent = lastRenderedHtml;
|
saveAsContent = lastRenderedHtml;
|
||||||
|
language = 'html';
|
||||||
} else {
|
} else {
|
||||||
throw new Error('unexpected saveAsType=' + saveAsType)
|
throw new Error('unexpected saveAsType=' + saveAsType);
|
||||||
}
|
}
|
||||||
$('saveAsDialogCode').value = saveAsContent;
|
const html = Prism.highlight(saveAsContent, Prism.languages[language], language);
|
||||||
|
|
||||||
|
$('saveAsDialogCode').innerHTML = html;
|
||||||
$('saveAsDialogCode').scrollTop = 0;
|
$('saveAsDialogCode').scrollTop = 0;
|
||||||
|
|
||||||
document.querySelectorAll('#saveAsDialogTypes button').forEach((element) => {
|
document.querySelectorAll('#saveAsDialogTypes button').forEach((element) => {
|
||||||
@@ -475,32 +492,37 @@ document.querySelectorAll('#saveAsDialogTypes button').forEach((element) => {
|
|||||||
|
|
||||||
const saveAsDialogCopyPopover = new Popover($('saveAsDialogCopyBtn'));
|
const saveAsDialogCopyPopover = new Popover($('saveAsDialogCopyBtn'));
|
||||||
$('saveAsDialogCopyBtn').addEventListener('click', (e) => {
|
$('saveAsDialogCopyBtn').addEventListener('click', (e) => {
|
||||||
const field = $('saveAsDialogCode');
|
navigator.clipboard.writeText(saveAsContent).then(() => {
|
||||||
field.select();
|
|
||||||
// field.setSelectionRange(0, field.value.length);
|
|
||||||
navigator.clipboard.writeText(field.value).then(() => {
|
|
||||||
saveAsDialogCopyPopover.show();
|
saveAsDialogCopyPopover.show();
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
saveAsDialogCopyPopover.hide();
|
saveAsDialogCopyPopover.hide();
|
||||||
}, 2000);
|
}, 2000);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
function toSnakeCase(str, separator = '_') {
|
||||||
|
return str
|
||||||
|
.toLowerCase()
|
||||||
|
.normalize('NFKC')
|
||||||
|
.replace(/[^\p{L}\p{N}]+/gu, '_') // keep letters & numbers from ALL languages
|
||||||
|
.replace(/^_+|_+$/g, '')
|
||||||
|
.replaceAll('_', separator);
|
||||||
|
}
|
||||||
|
const saveAsExtensions = {
|
||||||
|
js: 'js',
|
||||||
|
json: 'json',
|
||||||
|
python: 'py',
|
||||||
|
static: 'html',
|
||||||
|
};
|
||||||
$('saveAsDialogSaveBtn').addEventListener('click', (e) => {
|
$('saveAsDialogSaveBtn').addEventListener('click', (e) => {
|
||||||
let saveName = '';
|
let name = lastCfg.title || 'Internal server error';
|
||||||
switch (saveAsType) {
|
switch (saveAsType) {
|
||||||
case 'js':
|
case 'js':
|
||||||
saveName = 'cf-error-page-example.js';
|
|
||||||
break;
|
|
||||||
case 'json':
|
|
||||||
saveName = 'cf-error-page-params.json';
|
|
||||||
break;
|
|
||||||
case 'python':
|
case 'python':
|
||||||
saveName = 'cf_error_page_example.py';
|
name += ' example';
|
||||||
break;
|
break;
|
||||||
case 'static':
|
|
||||||
saveName = 'cf_error_page.html';
|
|
||||||
break;
|
|
||||||
// TODO: name output files using page title
|
|
||||||
}
|
}
|
||||||
|
const separator = saveAsType == 'python' ? '_' : '-';
|
||||||
|
const ext = saveAsExtensions[saveAsType] ?? txt;
|
||||||
|
const saveName = `${toSnakeCase(name, separator)}.${ext}`;
|
||||||
saveFile(saveAsContent, saveName);
|
saveFile(saveAsContent, saveName);
|
||||||
});
|
});
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -13,6 +13,9 @@ export default defineConfig(({ mode }) => {
|
|||||||
minify: true,
|
minify: true,
|
||||||
sourcemap: true,
|
sourcemap: true,
|
||||||
},
|
},
|
||||||
|
resolve: {
|
||||||
|
dedupe: ['ejs'],
|
||||||
|
},
|
||||||
server: {
|
server: {
|
||||||
port: 3000,
|
port: 3000,
|
||||||
proxy: {
|
proxy: {
|
||||||
|
|||||||
@@ -310,6 +310,11 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
undici-types "~7.16.0"
|
undici-types "~7.16.0"
|
||||||
|
|
||||||
|
"@types/prismjs@^1.26.5":
|
||||||
|
version "1.26.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/prismjs/-/prismjs-1.26.5.tgz#72499abbb4c4ec9982446509d2f14fb8483869d6"
|
||||||
|
integrity sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==
|
||||||
|
|
||||||
acorn@^8.15.0:
|
acorn@^8.15.0:
|
||||||
version "8.15.0"
|
version "8.15.0"
|
||||||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.15.0.tgz#a360898bc415edaac46c8241f6383975b930b816"
|
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.15.0.tgz#a360898bc415edaac46c8241f6383975b930b816"
|
||||||
@@ -392,6 +397,11 @@ clean-css@~5.3.2:
|
|||||||
dependencies:
|
dependencies:
|
||||||
source-map "~0.6.0"
|
source-map "~0.6.0"
|
||||||
|
|
||||||
|
cloudflare-error-page@../../javascript:
|
||||||
|
version "0.1.0"
|
||||||
|
dependencies:
|
||||||
|
ejs "^3.1.10"
|
||||||
|
|
||||||
commander@^10.0.0:
|
commander@^10.0.0:
|
||||||
version "10.0.1"
|
version "10.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/commander/-/commander-10.0.1.tgz#881ee46b4f77d1c1dccc5823433aa39b022cbe06"
|
resolved "https://registry.yarnpkg.com/commander/-/commander-10.0.1.tgz#881ee46b4f77d1c1dccc5823433aa39b022cbe06"
|
||||||
@@ -613,6 +623,11 @@ prettier@3.7.4:
|
|||||||
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.7.4.tgz#d2f8335d4b1cec47e1c8098645411b0c9dff9c0f"
|
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.7.4.tgz#d2f8335d4b1cec47e1c8098645411b0c9dff9c0f"
|
||||||
integrity sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==
|
integrity sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==
|
||||||
|
|
||||||
|
prismjs@^1.30.0:
|
||||||
|
version "1.30.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.30.0.tgz#d9709969d9d4e16403f6f348c63553b19f0975a9"
|
||||||
|
integrity sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==
|
||||||
|
|
||||||
readdirp@~3.6.0:
|
readdirp@~3.6.0:
|
||||||
version "3.6.0"
|
version "3.6.0"
|
||||||
resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7"
|
resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7"
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ npm install cloudflare-error-page
|
|||||||
Or install from GitHub:
|
Or install from GitHub:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install git+https://github.com/donlon/cloudflare-error-page.git#main:nodejs
|
npm install git+https://github.com/donlon/cloudflare-error-page.git#javascriptnodejs
|
||||||
```
|
```
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
@@ -34,7 +34,7 @@ fs.writeFileSync('error.html', errorPage);
|
|||||||
|
|
||||||
## API Reference
|
## API Reference
|
||||||
|
|
||||||
### `render(params: ErrorPageParams, allowHtml?: boolean): string`
|
### `render(params: ErrorPageParams, allowHtml?: boolean, moreArgs?: { [name: string]: any; }): string`
|
||||||
|
|
||||||
Generates an HTML error page based on the provided parameters.
|
Generates an HTML error page based on the provided parameters.
|
||||||
|
|
||||||
@@ -42,13 +42,14 @@ Generates an HTML error page based on the provided parameters.
|
|||||||
|
|
||||||
- `params`: An object containing error page configuration
|
- `params`: An object containing error page configuration
|
||||||
- `allowHtml` (optional): Whether to allow HTML in `what_happened` and `what_can_i_do` fields. Default: `true`
|
- `allowHtml` (optional): Whether to allow HTML in `what_happened` and `what_can_i_do` fields. Default: `true`
|
||||||
|
- `moreArgs` (optional): More arguments passed to the ejs template
|
||||||
|
|
||||||
#### ErrorPageParams Interface
|
#### ErrorPageParams Interface
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
interface ErrorPageParams {
|
interface ErrorPageParams {
|
||||||
// Basic information
|
// Basic information
|
||||||
error_code?: number; // Default: 500
|
error_code?: string | number; // Default: 500
|
||||||
title?: string; // Default: 'Internal server error'
|
title?: string; // Default: 'Internal server error'
|
||||||
html_title?: string; // Default: '{error_code}: {title}'
|
html_title?: string; // Default: '{error_code}: {title}'
|
||||||
time?: string; // Auto-generated if not provided
|
time?: string; // Auto-generated if not provided
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export interface CreatorInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ErrorPageParams {
|
export interface ErrorPageParams {
|
||||||
error_code?: number;
|
error_code?: string | number;
|
||||||
title?: string;
|
title?: string;
|
||||||
html_title?: string;
|
html_title?: string;
|
||||||
time?: string;
|
time?: string;
|
||||||
@@ -53,7 +53,6 @@ export interface ErrorPageParams {
|
|||||||
|
|
||||||
// Load EJS template
|
// Load EJS template
|
||||||
export const baseTemplate: ejs.TemplateFunction = ejs.compile(templateString);
|
export const baseTemplate: ejs.TemplateFunction = ejs.compile(templateString);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate random hex string for ray-id
|
* Generate random hex string for ray-id
|
||||||
*/
|
*/
|
||||||
@@ -70,7 +69,7 @@ function genHexString(digits: number): string {
|
|||||||
* Render a customized Cloudflare error page
|
* Render a customized Cloudflare error page
|
||||||
* @param params - The parameters for the error page
|
* @param params - The parameters for the error page
|
||||||
* @param allowHtml - Whether to allow HTML in what_happened and what_can_i_do fields (default: true)
|
* @param allowHtml - Whether to allow HTML in what_happened and what_can_i_do fields (default: true)
|
||||||
* @param moreArgs - More arguments passed to ejs template
|
* @param moreArgs - More arguments passed to the ejs template
|
||||||
* @returns The rendered HTML string
|
* @returns The rendered HTML string
|
||||||
*/
|
*/
|
||||||
export function render(
|
export function render(
|
||||||
|
|||||||
@@ -4,9 +4,9 @@
|
|||||||
<!--[if IE 8]> <html class="no-js ie8 oldie" lang="en-US"> <![endif]-->
|
<!--[if IE 8]> <html class="no-js ie8 oldie" lang="en-US"> <![endif]-->
|
||||||
<!--[if gt IE 8]><!--> <html class="no-js" lang="en-US"> <!--<![endif]-->
|
<!--[if gt IE 8]><!--> <html class="no-js" lang="en-US"> <!--<![endif]-->
|
||||||
<head>
|
<head>
|
||||||
<% const error_code = params.error_code || 500; %>
|
<% const error_code = params.error_code || 500; -%>
|
||||||
<% const title = params.title || 'Internal server error'; %>
|
<% const title = params.title || 'Internal server error'; -%>
|
||||||
<% const html_title = params.html_title || (error_code + ': ' + title); %>
|
<% const html_title = params.html_title || (error_code + ': ' + title); -%>
|
||||||
<title><%= html_title %></title>
|
<title><%= html_title %></title>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
<meta name="robots" content="noindex, nofollow" />
|
<meta name="robots" content="noindex, nofollow" />
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||||
<style>
|
<style>
|
||||||
<%- html_style %>
|
<%- html_style -%>
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -25,18 +25,18 @@
|
|||||||
<span class="inline-block"><%= title %></span>
|
<span class="inline-block"><%= title %></span>
|
||||||
<span class="code-label">Error code <%= error_code %></span>
|
<span class="code-label">Error code <%= error_code %></span>
|
||||||
</h1>
|
</h1>
|
||||||
<% const more_info = params.more_information || {}; %>
|
<%_ const more_info = params.more_information || {}; -%>
|
||||||
<% if (!more_info.hidden) { %>
|
<%_ if (!more_info.hidden) { -%>
|
||||||
<div>
|
<div>
|
||||||
Visit <a href="<%= more_info.link || 'https://www.cloudflare.com/' %>" target="_blank" rel="noopener noreferrer"><%= more_info.text || 'cloudflare.com' %></a> for <%= more_info.for || 'more information' %>.
|
Visit <a href="<%= more_info.link || 'https://www.cloudflare.com/' %>" target="_blank" rel="noopener noreferrer"><%= more_info.text || 'cloudflare.com' %></a> for <%= more_info.for || 'more information' %>.
|
||||||
</div>
|
</div>
|
||||||
<% } %>
|
<%_ } -%>
|
||||||
<div class="<%= more_info.hidden ? '' : 'mt-3' %>"><%= params.time %></div>
|
<div class="<%= more_info.hidden ? '' : 'mt-3' %>"><%= params.time %></div>
|
||||||
</header>
|
</header>
|
||||||
<div class="my-8 bg-gradient-gray">
|
<div class="my-8 bg-gradient-gray">
|
||||||
<div class="w-240 lg:w-full mx-auto">
|
<div class="w-240 lg:w-full mx-auto">
|
||||||
<div class="clearfix md:px-8">
|
<div class="clearfix md:px-8">
|
||||||
<%
|
<%_
|
||||||
const items = [
|
const items = [
|
||||||
{ id: 'browser', icon: 'browser', default_location: 'You', default_name: 'Browser' },
|
{ id: 'browser', icon: 'browser', default_location: 'You', default_name: 'Browser' },
|
||||||
{ id: 'cloudflare', icon: 'cloud', default_location: 'San Francisco', default_name: 'Cloudflare' },
|
{ id: 'cloudflare', icon: 'cloud', default_location: 'San Francisco', default_name: 'Cloudflare' },
|
||||||
@@ -55,7 +55,7 @@
|
|||||||
}
|
}
|
||||||
const status_text = item.status_text || (status === 'ok' ? 'Working' : 'Error');
|
const status_text = item.status_text || (status === 'ok' ? 'Working' : 'Error');
|
||||||
const is_error_source = params.error_source === id;
|
const is_error_source = params.error_source === id;
|
||||||
%>
|
-%>
|
||||||
<div id="cf-<%= id %>-status" class="<%= is_error_source ? 'cf-error-source' : '' %> relative w-1/3 md:w-full py-15 md:p-0 md:py-8 md:text-left md:border-solid md:border-0 md:border-b md:border-gray-400 overflow-hidden float-left md:float-none text-center">
|
<div id="cf-<%= id %>-status" class="<%= is_error_source ? 'cf-error-source' : '' %> relative w-1/3 md:w-full py-15 md:p-0 md:py-8 md:text-left md:border-solid md:border-0 md:border-b md:border-gray-400 overflow-hidden float-left md:float-none text-center">
|
||||||
<div class="relative mb-10 md:m-0">
|
<div class="relative mb-10 md:m-0">
|
||||||
<span class="cf-icon-<%= icon %> block md:hidden h-20 bg-center bg-no-repeat"></span>
|
<span class="cf-icon-<%= icon %> block md:hidden h-20 bg-center bg-no-repeat"></span>
|
||||||
@@ -65,7 +65,7 @@
|
|||||||
<h3 class="md:inline-block mt-3 md:mt-0 text-2xl text-gray-600 font-light leading-1.3" <%- (item.name || default_name) === 'Cloudflare' ? 'style="color: #2f7bbf;"' : '' %>><%= item.name || default_name %></h3>
|
<h3 class="md:inline-block mt-3 md:mt-0 text-2xl text-gray-600 font-light leading-1.3" <%- (item.name || default_name) === 'Cloudflare' ? 'style="color: #2f7bbf;"' : '' %>><%= item.name || default_name %></h3>
|
||||||
<span class="leading-1.3 text-2xl" style="color: <%= text_color %>"><%= status_text %></span>
|
<span class="leading-1.3 text-2xl" style="color: <%= text_color %>"><%= status_text %></span>
|
||||||
</div>
|
</div>
|
||||||
<% }); %>
|
<%_ }); -%>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -95,12 +95,11 @@
|
|||||||
</span>
|
</span>
|
||||||
<% const perf_sec_by = params.perf_sec_by || {}; %>
|
<% const perf_sec_by = params.perf_sec_by || {}; %>
|
||||||
<span class="cf-footer-item sm:block sm:mb-1"><span>Performance & security by</span> <a rel="noopener noreferrer" href="<%= perf_sec_by.link || 'https://www.cloudflare.com/' %>" id="brand_link" target="_blank"><%= perf_sec_by.text || 'Cloudflare' %></a></span>
|
<span class="cf-footer-item sm:block sm:mb-1"><span>Performance & security by</span> <a rel="noopener noreferrer" href="<%= perf_sec_by.link || 'https://www.cloudflare.com/' %>" id="brand_link" target="_blank"><%= perf_sec_by.text || 'Cloudflare' %></a></span>
|
||||||
|
<%_ const creator_info = params.creator_info || {}; _%>
|
||||||
<% const creator_info = params.creator_info || {}; %>
|
<%_ if (!(creator_info.hidden ?? true)) { _%>
|
||||||
<% if (!(creator_info.hidden ?? true)) { %>
|
|
||||||
<span class="cf-footer-separator sm:hidden">•</span>
|
<span class="cf-footer-separator sm:hidden">•</span>
|
||||||
<span class="cf-footer-item sm:block sm:mb-1">Created with <a href="<%= creator_info.link %>" target="_blank"><%= creator_info.text %></a></span>
|
<span class="cf-footer-item sm:block sm:mb-1">Created with <a href="<%= creator_info.link %>" target="_blank"><%= creator_info.text %></a></span>
|
||||||
<% } %>
|
<%_ } -%>
|
||||||
</p>
|
</p>
|
||||||
</div><!-- /.error-footer -->
|
</div><!-- /.error-footer -->
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user