diff --git a/bin/handlers/PreviewRequestHandler.js b/bin/handlers/PreviewRequestHandler.js new file mode 100644 index 0000000..3beafd2 --- /dev/null +++ b/bin/handlers/PreviewRequestHandler.js @@ -0,0 +1,60 @@ +const {RequestHandler} = require('../server/RequestHandler'); +const {VirtualSequenceDiagram} = require('../../lib/sequence-diagram'); + +const UNSAFE_HTML = /[^a-zA-Z0-9 :;.,]/g; +const escChar = (c) => `&#x${c.charCodeAt(0).toString(16).padStart(4, '0')};`; +const escapeHTML = (v) => v.replaceAll(UNSAFE_HTML, escChar); + +class PreviewRequestHandler extends RequestHandler { + constructor(baseUrlPattern) { + super('GET', new RegExp(`^${baseUrlPattern}/?(.*)$`, 'i')); + this.info = `Rendering preview at ${baseUrlPattern}/`; + } + + handle(req, res, {match, pickEncoding, writeEncoded}) { + this.applyCommonHeaders(req, res); + res.setHeader('Content-Type', 'text/html; charset=utf-8'); + + const encoding = pickEncoding(); + const params = new URLSearchParams(match[1]); + const code = params.get('c'); + const encoded = code + .replaceAll(/[\r\n]+/g, '\n') + .split('\n') + .filter((ln) => ln !== '') + .map(encodeURIComponent) + .join('/'); + + let content = ''; + try { + new VirtualSequenceDiagram().process(code); + content = [ + '', + '', + '', + '', + '', + '', + ``, + '', + '', + ].join(''); + } catch(e) { + content = [ + '', + '', + '', + '', + '', + '', + `
${escapeHTML(String(e))}
`, + '', + '', + ].join(''); + } + + writeEncoded(encoding, content); + } +} + +module.exports = {PreviewRequestHandler}; diff --git a/bin/server.js b/bin/server.js index dfcd4a7..579438b 100755 --- a/bin/server.js +++ b/bin/server.js @@ -3,6 +3,7 @@ const {Server} = require('./server/Server'); const {StaticRequestHandler} = require('./server/StaticRequestHandler'); const {RenderRequestHandler} = require('./handlers/RenderRequestHandler'); +const {PreviewRequestHandler} = require('./handlers/PreviewRequestHandler'); const path = require('path'); const DEV = process.argv.includes('dev'); @@ -45,8 +46,9 @@ const statics = new StaticRequestHandler('') 'connect-src \'self\'', 'font-src \'self\' data:', 'img-src \'self\' blob:', - 'form-action \'none\'', - 'frame-ancestors \'none\'', + 'form-action \'self\'', + 'frame-ancestors \'self\'', + 'frame-src \'self\'', ].join('; ')) .addHeader('X-Content-Type-Options', 'nosniff') .addHeader('X-Frame-Options', 'DENY') @@ -96,8 +98,23 @@ const render = new RenderRequestHandler('/render') ].join('; ')) .addHeader('X-Content-Type-Options', 'nosniff'); +const preview = new PreviewRequestHandler('/preview') + .setCacheMaxAge(DEV ? 0 : RENDER_MAX_AGE) + .addHeader('Content-Security-Policy', [ + 'base-uri \'self\'', + 'default-src \'none\'', + 'style-src \'self\'', + 'connect-src \'none\'', + 'img-src \'self\'', + 'form-action \'none\'', + 'frame-ancestors \'self\'', + 'frame-src \'self\'', + ].join('; ')) + .addHeader('X-Content-Type-Options', 'nosniff'); + new Server() .addHandler(render) + .addHandler(preview) .addHandler(statics) .listen(PORT, HOSTNAME) .then((server) => server.printListeningInfo(process.stdout)); diff --git a/docs/ubuntu-nginx-installer.sh b/docs/ubuntu-nginx-installer.sh index dad2eca..0710bcc 100644 --- a/docs/ubuntu-nginx-installer.sh +++ b/docs/ubuntu-nginx-installer.sh @@ -121,6 +121,7 @@ cd - > /dev/null; EOF sudo tee /var/www/https/index.htm < /dev/null; + $DOMAIN diff --git a/index.html b/index.html index 559c106..09e8590 100644 --- a/index.html +++ b/index.html @@ -1,3 +1,4 @@ + @@ -9,7 +10,8 @@ connect-src 'self'; font-src 'self' data:; img-src 'self' blob:; - form-action 'none'; + frame-src 'self'; + form-action 'self'; "> @@ -78,7 +80,6 @@

Sequence Diagram Online Editor

Loading…

-
+ + diff --git a/library.htm b/library.htm index 285ef38..6a6be2b 100644 --- a/library.htm +++ b/library.htm @@ -1,3 +1,4 @@ + diff --git a/web/resources/preview.htm b/web/resources/preview.htm new file mode 100644 index 0000000..c9d26a5 --- /dev/null +++ b/web/resources/preview.htm @@ -0,0 +1,9 @@ + + + + + + +
← Press "Update" to see the diagram.
+ + diff --git a/web/styles/editor.css b/web/styles/editor.css index 9c1a6fe..9dff9f9 100644 --- a/web/styles/editor.css +++ b/web/styles/editor.css @@ -29,12 +29,6 @@ html, body { line-height: 1.3; } -#loader p.noscript { - position: relative; - top: -2.3em; - background: #FFFFFF; -} - #loader nav { margin: 80px 0 0; font-size: 0.8em; @@ -598,7 +592,7 @@ svg a:active, svg a:hover { } @media (prefers-color-scheme: dark) { - body, #loader p.noscript { + body { background: #222222; color: #EEEEEE; } diff --git a/web/styles/noscript.css b/web/styles/noscript.css new file mode 100644 index 0000000..836f326 --- /dev/null +++ b/web/styles/noscript.css @@ -0,0 +1,98 @@ +#loader { + display: none; +} + +main { + display: grid; + width: 100%; + height: 100%; + grid-template-columns: auto 1fr; + grid-template-rows: auto 1fr auto; +} + +.banner { + grid-column: 1 / 3; + padding: 10px 150px; + background: #FFFFCC; + text-align: center; + border-bottom: 1px solid #808080; +} + +nav { + position: absolute; + top: 0; + right: 0; + padding: 10px; + line-height: 1rem; + font-size: 0.9rem; + text-align: right; +} + +nav a { + padding: 0 10px; +} + +form { + margin: 0; + padding: 0; + position: relative; +} + +textarea { + width: 30vw; + height: 100%; + resize: horizontal; + min-width: 200px; + max-width: 80vw; + padding: 10px 50px 100px 10px; + white-space: pre; + box-sizing: border-box; + border: none; + border-right: 1px solid #808080; + background: #EEEEEE; +} + +button { + position: absolute; + top: 10px; + right: 10px; +} + +iframe { + width: 100%; + height: 100%; + border: none; +} + +a:link, a:visited { + color: #556688; + text-decoration: underline; +} + +a:hover, a:active { + color: #5577AA; + text-decoration: none; +} + +@media (prefers-color-scheme: dark) { + .banner { + background: #554400; + } + + textarea { + color: #FFEEDD; + background: #333333; + } + + iframe { + background: #111111; /* avoid flicker when reloading */ + } + + a:link, a:visited { + color: #99AAEE; + } + + a:hover, a:active { + color: #6699FF; + } +} diff --git a/web/styles/preview.css b/web/styles/preview.css new file mode 100644 index 0000000..a9236ed --- /dev/null +++ b/web/styles/preview.css @@ -0,0 +1,36 @@ +html, body { + margin: 0; + padding: 0; + height: 100%; +} + +body { + padding: 10px; + font-family: sans-serif; + background: white; + box-sizing: border-box; +} + +img { + width: 100%; + height: 100%; +} + +.error { + color: #800000; +} + +@media (prefers-color-scheme: dark) { + body { + background: #111111; + color: #DDDDDD; + } + + img { + filter: invert(1) hue-rotate(180deg); + } + + .error { + color: #FF8080; + } +}