mirror of
https://github.com/donlon/cloudflare-error-page.git
synced 2025-12-19 14:59:28 +00:00
add simple editor UI and backend
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -5,6 +5,10 @@ build/
|
||||
__pycache__/
|
||||
dist/
|
||||
|
||||
node_modules/
|
||||
|
||||
.venv/
|
||||
venv/
|
||||
ttt/
|
||||
|
||||
instance/
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import html
|
||||
import os
|
||||
import secrets
|
||||
from datetime import datetime, timezone
|
||||
@@ -18,7 +19,7 @@ def get_resources_folder() -> str:
|
||||
return os.path.join(os.path.dirname(os.path.abspath(__file__)), 'resources')
|
||||
|
||||
|
||||
def render(params: dict, use_cdn: bool=True) -> str:
|
||||
def render(params: dict, allow_html: bool=True, use_cdn: bool=True, show_creator: bool=False) -> str:
|
||||
"""
|
||||
Render a customized Cloudflare error page.
|
||||
"""
|
||||
@@ -28,6 +29,9 @@ def render(params: dict, use_cdn: bool=True) -> str:
|
||||
params['time'] = utc_now.strftime("%Y-%m-%d %H:%M:%S UTC")
|
||||
if not params.get('ray_id'):
|
||||
params['ray_id'] = secrets.token_hex(8)
|
||||
if not allow_html:
|
||||
params['what_happened'] = html.escape(params.get('what_happened', ''))
|
||||
params['what_can_i_do'] = html.escape(params.get('what_can_i_do', ''))
|
||||
|
||||
template = env.get_template("error.html")
|
||||
return template.render(params=params, resources_use_cdn=use_cdn)
|
||||
return template.render(params=params, resources_use_cdn=use_cdn, show_creator=show_creator)
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
{% set default_name = 'Host' -%}
|
||||
{% endif %}
|
||||
{% set item = params.get(item_id + '_status', {}) -%}
|
||||
{% set status = item.get('status', 'ok') -%}
|
||||
{% set status = item.status or 'ok' -%}
|
||||
{% if item.status_text_color %}
|
||||
{% set text_color = item.status_text_color -%}
|
||||
{% elif status == 'ok' %}
|
||||
@@ -58,7 +58,7 @@
|
||||
{% elif status == 'error' %}
|
||||
{% set text_color = '#bd2426' -%} {# text-red-error #}
|
||||
{% endif %}
|
||||
{% set status_text = item.get('status_text', 'Working' if status == 'ok' else 'Not Working') -%}
|
||||
{% set status_text = item.status_text or ('Working' if status == 'ok' else 'Not Working') -%}
|
||||
<div id="cf-{{item_id}}-status" class="{{'cf-error-source' if params.error_source == item_id else ''}} 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">
|
||||
<span class="cf-icon-{{icon}} block md:hidden h-20 bg-center bg-no-repeat"></span>
|
||||
@@ -96,8 +96,13 @@
|
||||
<span class="hidden" id="cf-footer-ip">{{ params.client_ip or '1.1.1.1' }}</span>
|
||||
<span class="cf-footer-separator sm:hidden">•</span>
|
||||
</span>
|
||||
{% set perf_sec_by = params.get('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 or 'https://www.cloudflare.com/'}}" id="brand_link" target="_blank">{{perf_sec_by.get('text', 'Cloudflare')}}</a></span>
|
||||
{% set perf_sec_by = params.perf_sec_by or {} %}
|
||||
<span class="cf-footer-item sm:block sm:mb-1"><span>Performance & security by</span> <a rel="noopener noreferrer" href="{{perf_sec_by.link or 'https://www.cloudflare.com/'}}" id="brand_link" target="_blank">{{perf_sec_by.text or 'Cloudflare'}}</a></span>
|
||||
|
||||
{% if show_creator %}
|
||||
<span class="cf-footer-separator sm:hidden">•</span>
|
||||
<span class="cf-footer-item sm:block sm:mb-1">Created by <a href="https://virt.moe/cloudflare-error-page/editor/" target="_blank">CF Error Page Editor</a></span>
|
||||
{% endif %}
|
||||
</p>
|
||||
</div><!-- /.error-footer -->
|
||||
</div>
|
||||
|
||||
783
editor/resources/index.html
Normal file
783
editor/resources/index.html
Normal file
@@ -0,0 +1,783 @@
|
||||
<!-- SPDX-License-Identifier: MIT -->
|
||||
<!--
|
||||
!!!
|
||||
!!! Note: This file is vibely generated, and could be very hard to maintain.
|
||||
!!!
|
||||
-->
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<title>Cloudflare Error Page Editor</title>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/ejs@3.1.10/ejs.min.js"></script>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
|
||||
<script>
|
||||
(async function () {
|
||||
/*
|
||||
Editor logic
|
||||
- initialConfig: provided default config (from your JSON)
|
||||
- render(): placeholder that generates HTML and writes to iframe.srcdoc
|
||||
- inputs call render() on change
|
||||
- "Open in new tab" opens the rendered HTML in a new window using a blob URL
|
||||
*/
|
||||
let template;
|
||||
let postponeRendering = false;
|
||||
|
||||
fetch('template.ejs')
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
alert('failed to fetch template');
|
||||
}
|
||||
return response.text(); // Returns a Promise
|
||||
})
|
||||
.then(templateContent => {
|
||||
template = ejs.compile(templateContent)
|
||||
if (postponeRendering) {
|
||||
render()
|
||||
}
|
||||
})
|
||||
|
||||
const initialConfig = {
|
||||
"html_title": "cloudflare.com | 500: Internal server error",
|
||||
"title": "Internal server error",
|
||||
"error_code": 500,
|
||||
|
||||
"more_information": {
|
||||
"hidden": false,
|
||||
"text": "cloudflare.com",
|
||||
"link": "https://youtube.com/watch?v=dQw4w9WgXcQ"
|
||||
},
|
||||
|
||||
"browser_status": {
|
||||
"status": "ok",
|
||||
"location": "You",
|
||||
"name": "Browser",
|
||||
"status_text": "Working"
|
||||
},
|
||||
"cloudflare_status": {
|
||||
"status": "error",
|
||||
"location": "Cloud",
|
||||
"name": "Cloudflare",
|
||||
"status_text": "Not Working"
|
||||
},
|
||||
"host_status": {
|
||||
"status": "ok",
|
||||
"location": "The Site",
|
||||
"name": "Host",
|
||||
"status_text": "Working"
|
||||
},
|
||||
"error_source": "host",
|
||||
"what_happened": "There is an internal server error on Cloudflare's network.",
|
||||
"what_can_i_do": "Please try again in a few minutes.",
|
||||
"perf_sec_by": {
|
||||
"text": "Cloudflare",
|
||||
"link": "https://youtube.com/watch?v=dQw4w9WgXcQ"
|
||||
}
|
||||
};
|
||||
|
||||
// Demo presets (content brief — replace or expand as needed)
|
||||
const PRESETS = {
|
||||
"default": structuredClone(initialConfig),
|
||||
"empty": {
|
||||
"error_code": 500,
|
||||
},
|
||||
"catastrophic": {
|
||||
"title": "Catastrophic infrastructure failure",
|
||||
"error_code": 500,
|
||||
"more_information": {
|
||||
"text": "cloudflare.com",
|
||||
"link": "https://youtube.com/watch?v=dQw4w9WgXcQ"
|
||||
},
|
||||
"browser_status": {
|
||||
"status": "error",
|
||||
"status_text": "Out of Memory"
|
||||
},
|
||||
"cloudflare_status": {
|
||||
"status": "error",
|
||||
"location": "Everywhere",
|
||||
"status_text": "Not Working"
|
||||
},
|
||||
"host_status": {
|
||||
"status": "error",
|
||||
"location": "example.com",
|
||||
"status_text": "On Fire"
|
||||
},
|
||||
"error_source": "cloudflare",
|
||||
"what_happened": "There is a catastrophic failure.",
|
||||
"what_can_i_do": "Please try again in a few years.",
|
||||
"perf_sec_by": {
|
||||
"text": "Cloudflare",
|
||||
"link": "https://youtube.com/watch?v=dQw4w9WgXcQ"
|
||||
}
|
||||
},
|
||||
"working": {
|
||||
"title": "Web server is working",
|
||||
"error_code": 200,
|
||||
"more_information": {
|
||||
"hidden": true
|
||||
},
|
||||
"browser_status": {
|
||||
"status": "ok",
|
||||
"status_text": "Seems Working"
|
||||
},
|
||||
"cloudflare_status": {
|
||||
"status": "ok",
|
||||
"status_text": "Often Working"
|
||||
},
|
||||
"host_status": {
|
||||
"status": "ok",
|
||||
"location": "example.com",
|
||||
"status_text": "Just Working"
|
||||
},
|
||||
"error_source": "host",
|
||||
"what_happened": "This site is still working. And it looks great.",
|
||||
"what_can_i_do": "Visit the site before it crashes someday."
|
||||
},
|
||||
};
|
||||
|
||||
/* Utilities */
|
||||
function $(id) { return document.getElementById(id); }
|
||||
|
||||
/* Fill form from config */
|
||||
function loadConfig(cfg) {
|
||||
$('html_title').value = cfg.html_title ?? '';
|
||||
$('title').value = cfg.title ?? '';
|
||||
$('error_code').value = cfg.error_code ?? '';
|
||||
|
||||
$('more_hidden').checked = !!(cfg.more_information && cfg.more_information.hidden);
|
||||
$('more_text').value = cfg.more_information?.text ?? '';
|
||||
$('more_link').value = cfg.more_information?.link ?? '';
|
||||
|
||||
$('browser_status').value = cfg.browser_status?.status ?? 'ok';
|
||||
$('browser_location').value = cfg.browser_status?.location ?? '';
|
||||
$('browser_name').value = cfg.browser_status?.name ?? '';
|
||||
$('browser_status_text').value = cfg.browser_status?.status_text ?? '';
|
||||
|
||||
$('cloudflare_status').value = cfg.cloudflare_status?.status ?? 'ok';
|
||||
$('cloudflare_location').value = cfg.cloudflare_status?.location ?? '';
|
||||
$('cloudflare_name').value = cfg.cloudflare_status?.name ?? '';
|
||||
$('cloudflare_status_text').value = cfg.cloudflare_status?.status_text ?? '';
|
||||
|
||||
$('host_status').value = cfg.host_status?.status ?? 'ok';
|
||||
$('host_location').value = cfg.host_status?.location ?? '';
|
||||
$('host_name').value = cfg.host_status?.name ?? '';
|
||||
$('host_status_text').value = cfg.host_status?.status_text ?? '';
|
||||
|
||||
if (cfg.error_source === 'browser') $('err_browser').checked = true;
|
||||
else if (cfg.error_source === 'cloudflare') $('err_cloudflare').checked = true;
|
||||
else $('err_host').checked = true;
|
||||
|
||||
$('what_happened').value = cfg.what_happened ?? '';
|
||||
$('what_can_i_do').value = cfg.what_can_i_do ?? '';
|
||||
|
||||
$('perf_text').value = cfg.perf_sec_by?.text ?? '';
|
||||
$('perf_link').value = cfg.perf_sec_by?.link ?? '';
|
||||
}
|
||||
|
||||
/* Read config from form inputs */
|
||||
function readConfig() {
|
||||
return {
|
||||
html_title: $('html_title').value,
|
||||
title: $('title').value,
|
||||
error_code: Number($('error_code').value) || 0,
|
||||
more_information: {
|
||||
hidden: !!$('more_hidden').checked,
|
||||
text: $('more_text').value,
|
||||
link: $('more_link').value
|
||||
},
|
||||
browser_status: {
|
||||
status: $('browser_status').value,
|
||||
location: $('browser_location').value,
|
||||
name: $('browser_name').value,
|
||||
status_text: $('browser_status_text').value
|
||||
},
|
||||
cloudflare_status: {
|
||||
status: $('cloudflare_status').value,
|
||||
location: $('cloudflare_location').value,
|
||||
name: $('cloudflare_name').value,
|
||||
status_text: $('cloudflare_status_text').value
|
||||
},
|
||||
host_status: {
|
||||
status: $('host_status').value,
|
||||
location: $('host_location').value,
|
||||
name: $('host_name').value,
|
||||
status_text: $('host_status_text').value
|
||||
},
|
||||
error_source: (document.querySelector('input[name="error_source"]:checked') || { value: 'host' }).value,
|
||||
what_happened: $('what_happened').value,
|
||||
what_can_i_do: $('what_can_i_do').value,
|
||||
perf_sec_by: {
|
||||
text: $('perf_text').value,
|
||||
link: $('perf_link').value
|
||||
}
|
||||
};
|
||||
}
|
||||
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, use_cdn = true) {
|
||||
return template({
|
||||
params: params,
|
||||
resources_use_cdn: use_cdn
|
||||
})
|
||||
}
|
||||
|
||||
/* Basic render: build HTML string from config and put into iframe.srcdoc */
|
||||
function render() {
|
||||
const cfg = readConfig();
|
||||
window.lastCfg = cfg
|
||||
|
||||
if (!template) {
|
||||
postponeRendering = true
|
||||
return
|
||||
}
|
||||
|
||||
cfg.time = formatUtcTimestamp()
|
||||
cfg.ray_id = '0123456789abcdef'
|
||||
cfg.client_ip = '1.1.1.1'
|
||||
|
||||
let pageHtml = renderEjs(cfg)
|
||||
// Write into iframe
|
||||
const iframe = $('previewFrame');
|
||||
iframe.srcdoc = pageHtml;
|
||||
|
||||
// store last rendered HTML for "open in new tab"
|
||||
lastRenderedHtml = pageHtml;
|
||||
updateStatusBlockStyles();
|
||||
}
|
||||
|
||||
/* Open in new tab: create blob and open */
|
||||
let lastRenderedHtml = '';
|
||||
function openInNewTab() {
|
||||
if (!lastRenderedHtml) render();
|
||||
const blob = new Blob([lastRenderedHtml], { type: 'text/html' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
window.open(url, '_blank', 'noopener');
|
||||
// revoke after some time to avoid memory leak
|
||||
setTimeout(() => URL.revokeObjectURL(url), 15_000);
|
||||
}
|
||||
|
||||
function createShareableLink() {
|
||||
$('shareLink').value = ''
|
||||
fetch('../s/create', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
'parameters': window.lastCfg
|
||||
})
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
alert('failed to create link');
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(result => {
|
||||
if (result.status != 'ok') {
|
||||
alert('failed to create link');
|
||||
return
|
||||
}
|
||||
$('shareLink').value = result.url
|
||||
})
|
||||
}
|
||||
function exportJSON() {
|
||||
let content = JSON.stringify(lastCfg)
|
||||
const file = new File([content], 'cloudflare-error-page-params.json', {
|
||||
type: 'text/plain',
|
||||
})
|
||||
const link = document.createElement('a')
|
||||
const url = URL.createObjectURL(file)
|
||||
|
||||
link.href = url
|
||||
link.download = file.name
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
|
||||
document.body.removeChild(link)
|
||||
window.URL.revokeObjectURL(url)
|
||||
}
|
||||
|
||||
/* Wire up events */
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// initialize form values from initialConfig
|
||||
loadConfig(initialConfig);
|
||||
render();
|
||||
|
||||
// On preset change, load preset and render
|
||||
$('presetSelect').addEventListener('change', e => {
|
||||
const p = e.target.value;
|
||||
if (PRESETS[p]) loadConfig(PRESETS[p]);
|
||||
render();
|
||||
});
|
||||
|
||||
// Render / Open button handlers
|
||||
// $('btnRender').addEventListener('click', e => { e.preventDefault(); render(); });
|
||||
$('btnOpen').addEventListener('click', e => { e.preventDefault(); openInNewTab(); });
|
||||
$('btnShare').addEventListener('click', e => { e.preventDefault(); createShareableLink() });
|
||||
$('btnExport').addEventListener('click', e => { e.preventDefault(); exportJSON() });
|
||||
|
||||
$('btnCopyLink').addEventListener('click', () => {
|
||||
const field = $('shareLink');
|
||||
field.select();
|
||||
field.setSelectionRange(0, field.value.length);
|
||||
navigator.clipboard.writeText(field.value).then(() => {
|
||||
// No notification required unless you want one
|
||||
});
|
||||
});
|
||||
|
||||
// Input change -> render
|
||||
const inputs = document.querySelectorAll('#editorForm input, #editorForm textarea, #editorForm select');
|
||||
inputs.forEach(inp => {
|
||||
inp.addEventListener('input', debounce(() => {
|
||||
// Update status block color classes for quick visual feedback in the editor
|
||||
render();
|
||||
}, 250));
|
||||
// for radio change events (error_source)
|
||||
if (inp.type === 'radio') inp.addEventListener('change', () => { render(); });
|
||||
});
|
||||
});
|
||||
|
||||
/* Update status block colors based on selected status and error_source */
|
||||
function updateStatusBlockStyles() {
|
||||
const browserOk = $('browser_status').value === 'ok';
|
||||
const cfOk = $('cloudflare_status').value === 'ok';
|
||||
const hostOk = $('host_status').value === 'ok';
|
||||
|
||||
setBlockClass('block_browser', browserOk ? 'status-ok' : 'status-error');
|
||||
setBlockClass('block_cloudflare', cfOk ? 'status-ok' : 'status-error');
|
||||
setBlockClass('block_host', hostOk ? 'status-ok' : 'status-error');
|
||||
}
|
||||
|
||||
function setBlockClass(id, cls) {
|
||||
const el = $(id);
|
||||
if (!el) return;
|
||||
el.classList.remove('status-ok', 'status-error');
|
||||
el.classList.add(cls);
|
||||
}
|
||||
|
||||
/* Simple debounce */
|
||||
function debounce(fn, wait) {
|
||||
let t;
|
||||
return (...args) => {
|
||||
clearTimeout(t);
|
||||
t = setTimeout(() => fn(...args), wait);
|
||||
};
|
||||
}
|
||||
})()
|
||||
</script>
|
||||
<style>
|
||||
/* Layout: editor + preview */
|
||||
.app {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
height: 100vh;
|
||||
padding: 0.8rem;
|
||||
}
|
||||
|
||||
/* On md and up, arrange horizontally: editor left, preview right */
|
||||
@media (min-width: 768px) {
|
||||
.app {
|
||||
flex-direction: row;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.editor {
|
||||
flex: 0 0;
|
||||
min-width: 380px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.preview {
|
||||
flex: 1 1 48%;
|
||||
/* max-width: 48%; */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
/* On small screens use stacked layout: editor then iframe */
|
||||
.editor {
|
||||
background: #fff;
|
||||
border: 1px solid #e3e6ea;
|
||||
border-radius: .5rem;
|
||||
padding: 0.8rem;
|
||||
}
|
||||
|
||||
.preview {
|
||||
background: #fff;
|
||||
border: 1px solid #e3e6ea;
|
||||
border-radius: .5rem;
|
||||
padding: 0.8rem;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
/* Compact form: label and control same row */
|
||||
.form-row {
|
||||
display: flex;
|
||||
gap: .3rem;
|
||||
align-items: center;
|
||||
margin-bottom: .6rem;
|
||||
}
|
||||
|
||||
.form-row>label {
|
||||
min-width: 6rem;
|
||||
max-width: 12rem;
|
||||
margin-bottom: 0;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.form-row>.control {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* Status block styling */
|
||||
.status-block {
|
||||
border: 1px solid #cfeadd;
|
||||
border-radius: .375rem;
|
||||
padding: .75rem;
|
||||
margin-bottom: .5rem;
|
||||
}
|
||||
|
||||
.status-ok {
|
||||
background: #e9f7ef;
|
||||
border: 1px solid #cfeadd;
|
||||
}
|
||||
|
||||
.status-error {
|
||||
background: #fff5f5;
|
||||
border: 1px solid #f3c2c2;
|
||||
}
|
||||
|
||||
/* Iframe styling */
|
||||
iframe.preview-frame {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 1px solid #ddd;
|
||||
flex: 1 1 auto;
|
||||
min-height: 360px;
|
||||
border-radius: .375rem;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, .06);
|
||||
}
|
||||
|
||||
/* Controls toolbar */
|
||||
.toolbar {
|
||||
display: flex;
|
||||
gap: .5rem;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: .75rem;
|
||||
}
|
||||
|
||||
/* Compact textarea resizing */
|
||||
textarea.compact {
|
||||
min-height: 80px;
|
||||
resize: vertical;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body class="bg-light">
|
||||
|
||||
<div class="container-fluid h-100">
|
||||
<div class="app">
|
||||
|
||||
<!-- Editor column -->
|
||||
<div class="editor">
|
||||
<h5 class="form-row">Cloudflare Error Page Editor</h5>
|
||||
<hr>
|
||||
<div class="form-row mb-3">
|
||||
<label for="presetSelect">Preset:</label>
|
||||
<select id="presetSelect" class="form-select form-select-sm">
|
||||
<option value="default">Default</option>
|
||||
<option value="empty">Empty</option>
|
||||
<option value="catastrophic">Catastrophic failure</option>
|
||||
<option value="working">Server working</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<form id="editorForm" class="needs-validation" novalidate>
|
||||
<!-- Basic properties -->
|
||||
<div class="mb-3">
|
||||
<!-- <h6 class="mb-2">Page</h6> -->
|
||||
|
||||
<div class="form-row">
|
||||
<label for="html_title">HTML Title</label>
|
||||
<div class="control">
|
||||
<input id="html_title" class="form-control form-control-sm"
|
||||
placeholder="cloudflare.com | 500: Internal server error" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="title">Title</label>
|
||||
<div class="control">
|
||||
<input id="title" class="form-control form-control-sm" placeholder="Internal server error" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="error_code">Error Code</label>
|
||||
<div class="control">
|
||||
<input id="error_code" type="number" class="form-control form-control-sm" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- More information -->
|
||||
<div class="mt-3 mb-3">
|
||||
<h6 class="mb-2">Visit ... for more information</h6>
|
||||
|
||||
<div class="status-block">
|
||||
<div class="form-row">
|
||||
<label>Hidden</label>
|
||||
<div class="control d-flex align-items-center gap-2">
|
||||
<input id="more_hidden" class="form-check-input" type="checkbox" />
|
||||
<!-- <small class="text-muted">If checked, the "visit ..." line is hidden</small> -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="more_text">Text</label>
|
||||
<div class="control">
|
||||
<input id="more_text" class="form-control form-control-sm" placeholder="cloudflare.com" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="more_link">Link</label>
|
||||
<div class="control">
|
||||
<input id="more_link" class="form-control form-control-sm"
|
||||
placeholder="https://www.cloudflare.com/" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Status blocks -->
|
||||
<div class="mb-3">
|
||||
<h6 class="mb-2">Status</h6>
|
||||
|
||||
<!-- Browser -->
|
||||
<div id="block_browser" class="status-block status-ok">
|
||||
<div class="d-flex justify-content-between align-items-start mb-2">
|
||||
<strong>Browser</strong>
|
||||
<div>
|
||||
<input class="form-check-input" type="radio" name="error_source" id="err_browser" value="browser" />
|
||||
<label for="err_browser" class="ms-1 small">Error here</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="browser_status">Status</label>
|
||||
<div class="control">
|
||||
<select id="browser_status" class="form-select form-select-sm">
|
||||
<option value="ok">Ok</option>
|
||||
<option value="error">Error</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="browser_location">Location</label>
|
||||
<div class="control">
|
||||
<input id="browser_location" class="form-control form-control-sm" placeholder="Browser" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="browser_name">Name</label>
|
||||
<div class="control">
|
||||
<input id="browser_name" class="form-control form-control-sm" placeholder="You" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="browser_status_text">Status text</label>
|
||||
<div class="control">
|
||||
<input id="browser_status_text" class="form-control form-control-sm" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Cloudflare -->
|
||||
<div id="block_cloudflare" class="status-block status-error">
|
||||
<div class="d-flex justify-content-between align-items-start mb-2">
|
||||
<strong>Cloudflare</strong>
|
||||
<div>
|
||||
<input class="form-check-input" type="radio" name="error_source" id="err_cloudflare"
|
||||
value="cloudflare" />
|
||||
<label for="err_cloudflare" class="ms-1 small">Error here</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="cloudflare_status">Status</label>
|
||||
<div class="control">
|
||||
<select id="cloudflare_status" class="form-select form-select-sm">
|
||||
<option value="ok">Ok</option>
|
||||
<option value="error">Error</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="cloudflare_location">Location</label>
|
||||
<div class="control">
|
||||
<input id="cloudflare_location" class="form-control form-control-sm" placeholder="Cloud" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="cloudflare_name">Name</label>
|
||||
<div class="control">
|
||||
<input id="cloudflare_name" class="form-control form-control-sm" placeholder="Cloudflare" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="cloudflare_status_text">Status text</label>
|
||||
<div class="control">
|
||||
<input id="cloudflare_status_text" class="form-control form-control-sm" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Host -->
|
||||
<div id="block_host" class="status-block status-ok">
|
||||
<div class="d-flex justify-content-between align-items-start mb-2">
|
||||
<strong>Host</strong>
|
||||
<div>
|
||||
<input class="form-check-input" type="radio" name="error_source" id="err_host" value="host" />
|
||||
<label for="err_host" class="ms-1 small">Error here</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="host_status">Status</label>
|
||||
<div class="control">
|
||||
<select id="host_status" class="form-select form-select-sm">
|
||||
<option value="ok">Ok</option>
|
||||
<option value="error">Error</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="host_location">Location</label>
|
||||
<div class="control">
|
||||
<input id="host_location" class="form-control form-control-sm" placeholder="The site" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="host_name">Name</label>
|
||||
<div class="control">
|
||||
<input id="host_name" class="form-control form-control-sm" placeholder="Host" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="host_status_text">Status text</label>
|
||||
<div class="control">
|
||||
<input id="host_status_text" class="form-control form-control-sm" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label for="what_happened" class="fw-semibold">What happened?</label>
|
||||
<div class="control">
|
||||
<textarea id="what_happened" class="form-control compact" placeholder="There is an internal server error on Cloudflare's network.
|
||||
"></textarea>
|
||||
</div>
|
||||
<!-- </div> -->
|
||||
|
||||
<!-- <div class=""> -->
|
||||
<label for="what_can_i_do" class="fw-semibold mt-2">What can I do?</label>
|
||||
<div class="control">
|
||||
<textarea id="what_can_i_do" class="form-control compact"
|
||||
placeholder="Please try again in a few minutes."></textarea>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<h6 class="form-row">Performance & security by ...</h6>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="perf_text">Text</label>
|
||||
<div class="control">
|
||||
<input id="perf_text" class="form-control form-control-sm" placeholder="Cloudflare" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="perf_link">Link</label>
|
||||
<div class="control">
|
||||
<input id="perf_link" class="form-control form-control-sm" placeholder="https://www.cloudflare.com/" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- </div> -->
|
||||
|
||||
<div class="d-flex gap-2 mt-2 mb-2">
|
||||
<!-- <button id="btnRender" class="btn btn-sm btn-primary">Render</button> -->
|
||||
<button id="btnOpen" class="btn btn-sm btn-primary">Preview in new tab</button>
|
||||
<button id="btnExport" class="btn btn-sm btn-primary">Export JSON</button>
|
||||
</div>
|
||||
|
||||
<button id="btnShare" class="btn btn-sm btn-primary">Create shareable link</button>
|
||||
<div class="mt-2">
|
||||
<div class="input-group input-group-sm">
|
||||
<input id="shareLink" class="form-control" readonly />
|
||||
<button id="btnCopyLink" class="btn btn-outline-secondary" type="button">Copy</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="fw-semibold mt-2 text-center">>> Star this project on
|
||||
<a href="https://github.com/donlon/cloudflare-error-page" target="_blank">Github</a> ⭐
|
||||
</div>
|
||||
|
||||
<div class="mt-2" style="font-size: 0.9em;">You can also embed the error page into your own website. See
|
||||
<a href="https://github.com/donlon/cloudflare-error-page#quickstart" target="_blank">Quickstart</a> in the
|
||||
homepage.
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Preview column -->
|
||||
<div class="preview">
|
||||
<div class="d-flex justify-content-between align-items-center mb-1">
|
||||
<h6><strong>Preview</strong></h6>
|
||||
<div class="text-muted small">Preview updates automatically on change</div>
|
||||
</div>
|
||||
|
||||
<iframe id="previewFrame" class="preview-frame" sandbox="allow-same-origin allow-popups allow-forms"></iframe>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- JS: Bootstrap bundle -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
118
editor/resources/template.ejs
Normal file
118
editor/resources/template.ejs
Normal file
@@ -0,0 +1,118 @@
|
||||
<%
|
||||
let resources_cdn = resources_use_cdn ? 'https://cloudflare.com' : '';
|
||||
%>
|
||||
<!DOCTYPE html>
|
||||
<!--[if lt IE 7]> <html class="no-js ie6 oldie" lang="en-US"> <![endif]-->
|
||||
<!--[if IE 7]> <html class="no-js ie7 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]-->
|
||||
<head>
|
||||
<%
|
||||
let error_code = params.error_code || 500;
|
||||
let title = params.title || 'Internal server error';
|
||||
let html_title_output = params.html_title || (error_code + ': ' + title);
|
||||
%>
|
||||
<title><%= html_title_output %></title>
|
||||
<meta charset="UTF-8" />
|
||||
<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 rel="stylesheet" id="cf_styles-css" href="<%= resources_cdn %>/cdn-cgi/styles/main.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="cf-wrapper">
|
||||
<div id="cf-error-details" class="p-0">
|
||||
<header class="mx-auto pt-10 lg:pt-6 lg:px-8 w-240 lg:w-full mb-8">
|
||||
<h1 class="inline-block sm:block sm:mb-2 font-light text-60 lg:text-4xl text-black-dark leading-tight mr-2">
|
||||
<span class="inline-block"><%= title %></span>
|
||||
<span class="code-label">Error code <%= error_code %></span>
|
||||
</h1>
|
||||
<% let more_info = params.more_information || {}; %>
|
||||
<% if (!more_info.hidden) { %>
|
||||
<div>
|
||||
Visit <a href="<%= more_info.link || 'https://www.cloudflare.com/' %>" target="_blank" rel="noopener noreferrer"><%= more_info.text || 'cloudflare.com' %></a> for more information.
|
||||
</div>
|
||||
<% } %>
|
||||
<div class="<%= more_info.hidden ? '' : 'mt-3' %>"><%= params.time %></div>
|
||||
</header>
|
||||
<div class="my-8 bg-gradient-gray">
|
||||
<div class="w-240 lg:w-full mx-auto">
|
||||
<div class="clearfix md:px-8">
|
||||
<% for (let item_id of ['browser', 'cloudflare', 'host']) { %>
|
||||
<%
|
||||
let icon, default_location, default_name, text_color, status_text;
|
||||
|
||||
if (item_id === 'browser') {
|
||||
icon = 'browser';
|
||||
default_location = 'You';
|
||||
default_name = 'Browser';
|
||||
} else if (item_id === 'cloudflare') {
|
||||
icon = 'cloud';
|
||||
default_location = 'San Francisco';
|
||||
default_name = 'Cloudflare';
|
||||
} else {
|
||||
icon = 'server';
|
||||
default_location = 'example.com';
|
||||
default_name = 'Host';
|
||||
}
|
||||
|
||||
let item = params[item_id + '_status'] || {};
|
||||
let status = item.status || 'ok';
|
||||
|
||||
if (item.status_text_color) {
|
||||
text_color = item.status_text_color;
|
||||
} else if (status === 'ok') {
|
||||
text_color = '#9bca3e'; // text-green-success
|
||||
} else if (status === 'error') {
|
||||
text_color = '#bd2426'; // text-red-error
|
||||
}
|
||||
|
||||
status_text = item.status_text || (status === 'ok' ? 'Working' : 'Not Working');
|
||||
%>
|
||||
<div id="cf-<%= item_id %>-status" class="<% if (params.error_source === item_id) { %>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">
|
||||
<span class="cf-icon-<%= icon %> block md:hidden h-20 bg-center bg-no-repeat"></span>
|
||||
<span class="cf-icon-<%= status %> w-12 h-12 absolute left-1/2 md:left-auto md:right-0 md:top-0 -ml-6 -bottom-4"></span>
|
||||
</div>
|
||||
<span class="md:block w-full truncate"><%= item.location || default_location %></span>
|
||||
<h3 class="md:inline-block mt-3 md:mt-0 text-2xl text-gray-600 font-light leading-1.3"><%= item.name || default_name %></h3>
|
||||
<span class="leading-1.3 text-2xl" style="color: <%= text_color %>"><%= status_text %></span>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="w-240 lg:w-full mx-auto mb-8 lg:px-8">
|
||||
<div class="clearfix">
|
||||
<div class="w-1/2 md:w-full float-left pr-6 md:pb-10 md:pr-0 leading-relaxed">
|
||||
<h2 class="text-3xl font-normal leading-1.3 mb-4">What happened?</h2>
|
||||
<%= (params.what_happened || 'There is an internal server error on Cloudflare\'s network.') %>
|
||||
</div>
|
||||
<div class="w-1/2 md:w-full float-left leading-relaxed">
|
||||
<h2 class="text-3xl font-normal leading-1.3 mb-4">What can I do?</h2>
|
||||
<%= (params.what_can_i_do || 'Please try again in a few minutes.') %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="cf-error-footer cf-wrapper w-240 lg:w-full py-10 sm:py-4 sm:px-8 mx-auto text-center sm:text-left border-solid border-0 border-t border-gray-300">
|
||||
<p class="text-13">
|
||||
<span class="cf-footer-item sm:block sm:mb-1">Cloudflare Ray ID: <strong class="font-semibold"><%= params.ray_id %></strong></span>
|
||||
<span class="cf-footer-separator sm:hidden">•</span>
|
||||
<span id="cf-footer-item-ip" class="cf-footer-item hidden sm:block sm:mb-1">
|
||||
Your IP:
|
||||
<button type="button" id="cf-footer-ip-reveal" class="cf-footer-ip-reveal-btn">Click to reveal</button>
|
||||
<span class="hidden" id="cf-footer-ip"><%= params.client_ip || '1.1.1.1' %></span>
|
||||
<span class="cf-footer-separator sm:hidden">•</span>
|
||||
</span>
|
||||
<% let 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>
|
||||
</p>
|
||||
</div><!-- /.error-footer -->
|
||||
</div>
|
||||
</div>
|
||||
<script>(function(){function d(){var b=a.getElementById("cf-footer-item-ip"),c=a.getElementById("cf-footer-ip-reveal");b&&"classList"in b&&(b.classList.remove("hidden"),c.addEventListener("click",function(){c.classList.add("hidden");a.getElementById("cf-footer-ip").classList.remove("hidden")}))}var a=document;document.addEventListener&&a.addEventListener("DOMContentLoaded",d)})();</script>
|
||||
</body>
|
||||
</html>
|
||||
106
editor/server/__init__.py
Normal file
106
editor/server/__init__.py
Normal file
@@ -0,0 +1,106 @@
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
import os
|
||||
import secrets
|
||||
import string
|
||||
import sys
|
||||
|
||||
from flask import Flask, request
|
||||
from flask_limiter import Limiter
|
||||
from flask_limiter.util import get_remote_address
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from sqlalchemy import event
|
||||
from sqlalchemy.orm import DeclarativeBase
|
||||
|
||||
root_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), '../../')
|
||||
sys.path.append(root_dir)
|
||||
from cloudflare_error_page import render as render_cf_error_page
|
||||
|
||||
class Base(DeclarativeBase):
|
||||
pass
|
||||
|
||||
db: SQLAlchemy = SQLAlchemy(model_class=Base, session_options={
|
||||
# 'autobegin': False,
|
||||
# 'expire_on_commit': False,
|
||||
})
|
||||
|
||||
limiter: Limiter = Limiter(
|
||||
key_func=get_remote_address, # Uses client's IP address by default
|
||||
default_limits=["200 per day", "50 per hour"] # Global default limits
|
||||
)
|
||||
|
||||
def generate_secret(length=32) -> str:
|
||||
characters = string.ascii_letters + string.digits # A-Z, a-z, 0-9
|
||||
return ''.join(secrets.choice(characters) for _ in range(length))
|
||||
|
||||
|
||||
def create_app(test_config=None) -> Flask:
|
||||
instance_path = os.getenv('INSTANCE_PATH', None)
|
||||
if instance_path is not None:
|
||||
os.makedirs(instance_path, exist_ok=True)
|
||||
app = Flask(__name__,
|
||||
instance_path=instance_path,
|
||||
instance_relative_config=True
|
||||
)
|
||||
app.json.ensure_ascii = False
|
||||
app.json.mimetype = "application/json; charset=utf-8"
|
||||
secret_key = os.getenv('SECRET_KEY', '')
|
||||
if secret_key:
|
||||
app.secret_key = secret_key
|
||||
else:
|
||||
print('Using generated secret')
|
||||
app.secret_key = generate_secret()
|
||||
app.config["SQLALCHEMY_DATABASE_URI"] = os.getenv('DATABASE_URI', 'sqlite:///example.db')
|
||||
url_prefix = os.getenv('URL_PREFIX', '')
|
||||
|
||||
|
||||
from . import models
|
||||
from . import examples
|
||||
from . import editor
|
||||
from . import shared
|
||||
|
||||
if app.config["SQLALCHEMY_DATABASE_URI"].startswith('sqlite'):
|
||||
app.config["SQLALCHEMY_ENGINE_OPTIONS"] = {
|
||||
'isolation_level': 'SERIALIZABLE',
|
||||
# "execution_options": {"autobegin": False}
|
||||
}
|
||||
db.init_app(app)
|
||||
limiter.init_app(app)
|
||||
|
||||
|
||||
with app.app_context():
|
||||
db.create_all()
|
||||
# if db.engine.dialect.name == 'sqlite':
|
||||
# @event.listens_for(db.engine, "connect")
|
||||
# def enable_foreign_keys(dbapi_connection, connection_record):
|
||||
# cursor = dbapi_connection.cursor()
|
||||
# cursor.execute("PRAGMA foreign_keys=ON;")
|
||||
# cursor.close()
|
||||
|
||||
@app.route('/health')
|
||||
def health():
|
||||
return '', 204
|
||||
|
||||
app.register_blueprint(editor.bp, url_prefix=f'{url_prefix}/editor')
|
||||
app.register_blueprint(examples.bp, url_prefix=f'{url_prefix}/examples')
|
||||
app.register_blueprint(shared.bp, url_prefix=f'{url_prefix}/s')
|
||||
|
||||
return app
|
||||
|
||||
|
||||
def get_common_cf_template_params():
|
||||
# Get real Ray ID from Cloudflare header
|
||||
ray_id = request.headers.get('Cf-Ray')
|
||||
if ray_id:
|
||||
ray_id = ray_id[:16]
|
||||
# Get real client ip from Cloudflare header or request.remote_addr
|
||||
client_ip = request.headers.get('X-Forwarded-For')
|
||||
if not client_ip:
|
||||
client_ip = request.remote_addr
|
||||
return {
|
||||
'ray_id': ray_id,
|
||||
'client_ip': client_ip,
|
||||
}
|
||||
|
||||
|
||||
__all__ = ['create_app', 'db', 'get_common_cf_template_params', 'render_cf_error_page']
|
||||
21
editor/server/editor.py
Normal file
21
editor/server/editor.py
Normal file
@@ -0,0 +1,21 @@
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
import os
|
||||
|
||||
from flask import (
|
||||
Blueprint,
|
||||
send_from_directory,
|
||||
)
|
||||
|
||||
from . import get_common_cf_template_params, render_cf_error_page
|
||||
|
||||
root_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), '../../')
|
||||
res_folder = os.path.join(root_dir, 'editor/resources')
|
||||
|
||||
bp = Blueprint('editor', __name__, url_prefix='/')
|
||||
|
||||
|
||||
@bp.route('/', defaults={'path': 'index.html'})
|
||||
@bp.route('/<path:path>')
|
||||
def index(path: str):
|
||||
return send_from_directory(res_folder, path)
|
||||
58
editor/server/examples.py
Normal file
58
editor/server/examples.py
Normal file
@@ -0,0 +1,58 @@
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
import os
|
||||
import re
|
||||
|
||||
from flask import (
|
||||
Blueprint,
|
||||
json,
|
||||
abort,
|
||||
redirect,
|
||||
)
|
||||
|
||||
from . import get_common_cf_template_params, render_cf_error_page
|
||||
|
||||
root_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), '../../')
|
||||
examples_dir = os.path.join(root_dir, 'examples')
|
||||
|
||||
bp = Blueprint('examples', __name__, url_prefix='/')
|
||||
|
||||
|
||||
param_cache: dict[str, dict] = {}
|
||||
|
||||
def get_page_params(name: str) -> dict:
|
||||
name = re.sub(r'[^\w]', '', name)
|
||||
params = param_cache.get(name)
|
||||
if params is not None:
|
||||
return params
|
||||
try:
|
||||
with open(os.path.join(examples_dir, f'{name}.json')) as f:
|
||||
params = json.load(f)
|
||||
param_cache[name] = params
|
||||
return params
|
||||
except Exception as _:
|
||||
return None
|
||||
|
||||
|
||||
@bp.route('/', defaults={'name': 'default'})
|
||||
@bp.route('/<path:name>')
|
||||
def index(name: str):
|
||||
name = os.path.basename(name) # keep only the base name
|
||||
lower_name = name.lower()
|
||||
print(lower_name, name)
|
||||
if name != lower_name:
|
||||
return redirect(lower_name)
|
||||
else:
|
||||
name = lower_name
|
||||
|
||||
params = get_page_params(name)
|
||||
if params is None:
|
||||
abort(404)
|
||||
|
||||
params = {
|
||||
**params,
|
||||
**get_common_cf_template_params(),
|
||||
}
|
||||
|
||||
# Render the error page
|
||||
return render_cf_error_page(params, use_cdn=True), 500
|
||||
19
editor/server/models.py
Normal file
19
editor/server/models.py
Normal file
@@ -0,0 +1,19 @@
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
from sqlalchemy import (
|
||||
Column,
|
||||
DateTime,
|
||||
Integer,
|
||||
JSON,
|
||||
String,
|
||||
func,
|
||||
)
|
||||
|
||||
from . import db
|
||||
|
||||
|
||||
class Item(db.Model):
|
||||
id = Column(Integer, primary_key=True, autoincrement=True, nullable=False)
|
||||
name = Column(String(255), unique=True, nullable=False, index=True)
|
||||
params = Column(JSON, nullable=False)
|
||||
time_created = Column(DateTime(timezone=True), server_default=func.now())
|
||||
69
editor/server/shared.py
Normal file
69
editor/server/shared.py
Normal file
@@ -0,0 +1,69 @@
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
import random
|
||||
import string
|
||||
|
||||
from flask import (
|
||||
Blueprint,
|
||||
request,
|
||||
abort,
|
||||
jsonify,
|
||||
url_for,
|
||||
)
|
||||
|
||||
from . import get_common_cf_template_params, render_cf_error_page
|
||||
from . import db
|
||||
from . import limiter
|
||||
from . import models
|
||||
|
||||
# root_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), '../../')
|
||||
# examples_dir = os.path.join(root_dir, 'examples')
|
||||
|
||||
bp = Blueprint('shared', __name__, url_prefix='/')
|
||||
|
||||
|
||||
rand_charset = string.ascii_letters + string.digits
|
||||
|
||||
def get_rand_name(digits=8):
|
||||
return ''.join(random.choice(rand_charset) for _ in range(digits))
|
||||
|
||||
|
||||
@bp.post('/create')
|
||||
@limiter.limit("20 per minute")
|
||||
@limiter.limit("500 per hour")
|
||||
def create():
|
||||
if len(request.data) > 4096:
|
||||
abort(413)
|
||||
params = request.json['parameters'] # throws KeyError
|
||||
# TODO: strip unused params
|
||||
try:
|
||||
item = models.Item()
|
||||
item.name = get_rand_name()
|
||||
item.params = params
|
||||
db.session.add(item)
|
||||
db.session.commit()
|
||||
except:
|
||||
db.session.rollback()
|
||||
return jsonify({
|
||||
'status': 'failed',
|
||||
})
|
||||
return jsonify({
|
||||
'status': 'ok',
|
||||
'name': item.name,
|
||||
'url': request.host_url[:-1] + url_for('shared.get', name=item.name),
|
||||
# TODO: better way to handle this
|
||||
})
|
||||
|
||||
|
||||
@bp.get('/<name>')
|
||||
def get(name: str):
|
||||
item = db.session.query(models.Item).filter_by(name=name).first()
|
||||
if not item:
|
||||
return abort(404)
|
||||
params = item.params
|
||||
params = {
|
||||
**params,
|
||||
**get_common_cf_template_params(),
|
||||
}
|
||||
# TODO: cache
|
||||
return render_cf_error_page(params=params, allow_html=False, use_cdn=True, show_creator=True), 200
|
||||
Reference in New Issue
Block a user