From 0d35b3e6548038461aa0515a177ff98e2b36b49a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Fri, 29 Aug 2025 22:04:29 +0200 Subject: [PATCH] poprawki --- app.py | 64 +++++++++++----- static/css/main.css | 6 +- static/js/main.js | 181 ++++++++++++++++++++++++++++++++++++-------- templates/form.html | 11 ++- 4 files changed, 205 insertions(+), 57 deletions(-) diff --git a/app.py b/app.py index 775278a..e41e863 100644 --- a/app.py +++ b/app.py @@ -73,20 +73,7 @@ def track_request_data(): @app.after_request -def add_cache_headers(response): - if request.path.startswith("/static/"): - - response.headers.pop("Content-Disposition", None) - - if request.path.endswith((".css", ".js")): - response.headers["Cache-Control"] = "public, max-age=31536000, immutable" - else: - response.headers["Cache-Control"] = "public, max-age=86400" - return response - - -@app.after_request -def after_request(response): +def finalize_response(response): elapsed = time.perf_counter() - g.start_time redis_client.incrbyfloat("stats:processing_time_total", elapsed) redis_client.incr("stats:processing_time_count") @@ -102,6 +89,29 @@ def after_request(response): redis_client.set("stats:processing_time_max", elapsed) except Exception: redis_client.set("stats:processing_time_max", elapsed) + + path = request.path or "/" + + if response.status_code >= 400: + response.headers["Cache-Control"] = "no-store" + response.headers["Pragma"] = "no-cache" + response.headers["Expires"] = "0" + return response + + if path.startswith("/static/"): + response.headers.pop("Content-Disposition", None) + if path.endswith((".css", ".js")): + response.headers["Cache-Control"] = "public, max-age=31536000, immutable" + else: + response.headers["Cache-Control"] = "public, max-age=86400" + return response + + if path == "/": + response.headers["Cache-Control"] = "private, no-store" + response.headers["Pragma"] = "no-cache" + response.headers["Expires"] = "0" + return response + return response @@ -319,6 +329,14 @@ def add_recent_convert(): redis_client.ltrim("recent_converts", 0, 99) +def validate_ip(value: str) -> str: + v = (value or "").strip() + try: + return str(ipaddress.ip_address(v)) + except Exception: + raise ValueError("Invalid IP address") + + @app.route("/favicon.ico", methods=["GET"]) def favicon(): return Response(status=204) @@ -329,7 +347,11 @@ def index(): generated_link = None recent_links = get_recent_links() url_param = request.args.get("url", config.DEFAULT_SOURCE_URL) - target_ip = request.args.get("ip", "127.0.0.1") + raw_ip = request.args.get("ip", "127.0.0.1") + try: + target_ip = validate_ip(raw_ip) + except ValueError: + target_ip = "127.0.0.1" if url_param: try: @@ -437,7 +459,15 @@ def convert(): redis_client.incr("stats:errors_400") abort(400) - target_ip = request.args.get("ip", "127.0.0.1") + try: + target_ip = validate_ip(request.args.get("ip", "127.0.0.1")) + except ValueError: + if debug_mode: + d("Bad parametr ?ip") + return debug_response(status=400) + redis_client.incr("stats:errors_400") + abort(400, description="Invalid IP") + if debug_mode: d(f"URL (encoded): {encoded_url}") d(f"URL (decoded): {decoded_url}") @@ -561,7 +591,7 @@ def convert_head(): abort(400) decoded_url = unquote(encoded_url) validate_and_normalize_url(decoded_url) - target_ip = request.args.get("ip", "127.0.0.1") + target_ip = validate_ip(request.args.get("ip", "127.0.0.1")) etag = build_etag(None, None, target_ip) resp = Response(status=200) resp.headers.update(cache_headers(etag, None)) diff --git a/static/css/main.css b/static/css/main.css index bdf6e09..fd62534 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -41,8 +41,8 @@ body { margin: 0; font-family: ui-sans-serif, system-ui, "Segoe UI", Roboto, Arial, sans-serif; background: - radial-gradient(1200px 600px at 10% -10%, rgba(91, 157, 255, .08), transparent 60%), - radial-gradient(900px 500px at 110% 0%, rgba(123, 212, 255, .10), transparent 60%), + /*radial-gradient(1200px 600px at 10% -10%, rgba(91, 157, 255, .08), transparent 60%), + radial-gradient(900px 500px at 110% 0%, rgba(123, 212, 255, .10), transparent 60%),*/ var(--bg); color: var(--text); } @@ -89,7 +89,7 @@ body { } .card { - background: linear-gradient(180deg, var(--card), color-mix(in srgb, var(--card) 80%, #000 20%)); + background: linear-gradient(180deg, var(--card), color-mix(in srgb, var(--card) 90%, #000 10%)); border: 1px solid var(--border); border-radius: 16px; box-shadow: var(--shadow); diff --git a/static/js/main.js b/static/js/main.js index c73c70d..e1c0a44 100644 --- a/static/js/main.js +++ b/static/js/main.js @@ -2,7 +2,14 @@ const $ = (q, c = document) => c.querySelector(q); const $$ = (q, c = document) => Array.from(c.querySelectorAll(q)); - const setTheme = (t) => { document.documentElement.setAttribute('data-theme', t); try { localStorage.setItem('theme', t) } catch { } }; + + // --- theme --- + const setTheme = (t) => { + document.documentElement.setAttribute('data-theme', t); + try { localStorage.setItem('theme', t) } catch { } + }; + window.setTheme = setTheme; + const toast = (msg) => { const el = $('#toast'); if (!el) return; el.textContent = msg; el.classList.add('show'); @@ -10,6 +17,52 @@ }; const host = () => `${location.protocol}//${location.host}`; + // --- IP validators --- + function isValidIPv4(ip) { + if (!/^\d{1,3}(?:\.\d{1,3}){3}$/.test(ip)) return false; + return ip.split('.').every(oct => { + if (oct.length > 1 && oct[0] === '0') return oct === '0'; + const n = Number(oct); + return n >= 0 && n <= 255; + }); + } + + function isValidIPv6(ip) { + let work = ip; + const idx = work.lastIndexOf(':'); + if (idx !== -1) { + const tail = work.slice(idx + 1); + if (/^\d{1,3}(?:\.\d{1,3}){3}$/.test(tail)) { + if (!isValidIPv4(tail)) return false; + work = work.slice(0, idx) + ':0:0'; + } + } + if (work.split('::').length > 2) return false; + const hasCompress = work.includes('::'); + const parts = work.split(':').filter(Boolean); + if ((!hasCompress && parts.length !== 8) || (hasCompress && parts.length > 7)) return false; + return parts.every(g => /^[0-9a-fA-F]{1,4}$/.test(g)); + } + + function isValidIP(ip) { + const v = (ip || '').trim(); + return isValidIPv4(v) || isValidIPv6(v); + } + + // --- URL helpers --- + function normalizeUrlMaybe(v) { + const raw = (v || '').trim(); + if (!raw) return ''; + try { + const test = raw.includes('://') ? raw : `https://${raw}`; + const u = new URL(test); + if (u.protocol !== 'http:' && u.protocol !== 'https:') throw new Error('scheme'); + return u.toString(); + } catch { + return ''; + } + } + function buildLink(url, ip) { if (!url || !ip) return ''; try { @@ -34,32 +87,75 @@ const out = $('#generated-link'); const openBtn = $('#open-link'); - function updatePreview() { - const link = buildLink(urlInput.value.trim(), ipInput.value.trim()); - out.value = link || ''; - if (link) { - openBtn.setAttribute('href', link); - openBtn.setAttribute('aria-disabled', 'false'); - } else { - openBtn.setAttribute('href', '#'); - openBtn.setAttribute('aria-disabled', 'true'); - } - $('.result-box')?.setAttribute('data-state', link ? 'ready' : 'empty'); + function showError(input, msg) { + const id = input.getAttribute('id'); + const box = document.querySelector(`.error[data-error-for="${id}"]`); + if (box) box.textContent = msg || ''; + input.setAttribute('aria-invalid', msg ? 'true' : 'false'); } + function updatePreview() { + const rawUrl = (urlInput?.value || '').trim(); + const ip = (ipInput?.value || '').trim(); + + if (!ip || !isValidIP(ip)) { + if (out) out.value = ''; + if (openBtn) { + openBtn.setAttribute('href', '#'); + openBtn.setAttribute('aria-disabled', 'true'); + } + $('.result-box')?.setAttribute('data-state', 'empty'); + return; + } + + const normalized = normalizeUrlMaybe(rawUrl); // poprawny http/https lub '' + const guessed = rawUrl ? (rawUrl.includes('://') ? rawUrl : `https://${rawUrl}`) : ''; + const previewUrl = normalized || guessed; + + if (!previewUrl) { + if (out) out.value = ''; + if (openBtn) { + openBtn.setAttribute('href', '#'); + openBtn.setAttribute('aria-disabled', 'true'); + } + $('.result-box')?.setAttribute('data-state', 'empty'); + return; + } + + const link = buildLink(previewUrl, ip); + if (out) out.value = link; + + const ok = !!normalized; + if (openBtn) { + if (ok) { + openBtn.setAttribute('href', link); + openBtn.setAttribute('aria-disabled', 'false'); + } else { + openBtn.setAttribute('href', '#'); + openBtn.setAttribute('aria-disabled', 'true'); + } + } + $('.result-box')?.setAttribute('data-state', ok ? 'ready' : 'empty'); + } + + // live update ['input', 'change', 'blur'].forEach(evt => { urlInput?.addEventListener(evt, updatePreview); ipInput?.addEventListener(evt, updatePreview); }); + // presets ipPreset?.addEventListener('change', () => { const v = ipPreset.value; if (!v) return; if (v !== 'custom') ipInput.value = v; ipInput.focus(); + const ok = isValidIP(ipInput.value.trim()); + showError(ipInput, ok ? '' : 'Invalid IP address'); updatePreview(); }); + // event delegation document.addEventListener('click', (e) => { let t = e.target; @@ -74,7 +170,6 @@ btn.classList.add('copied'); setTimeout(() => btn.classList.remove('copied'), 1200); toast('Link copied'); }).catch(() => { - // Fallback const range = document.createRange(); range.selectNodeContents(el); const selObj = getSelection(); selObj.removeAllRanges(); selObj.addRange(range); try { document.execCommand('copy'); toast('Link copied'); } catch { } @@ -85,15 +180,19 @@ if (t.closest('[data-action="copy-text"]')) { e.preventDefault(); const btn = t.closest('[data-action="copy-text"]'); - const text = btn.getAttribute('data-text') || ''; + let text = btn.getAttribute('data-text') || ''; if (!text) return; + if (text.startsWith('/')) text = host() + text; navigator.clipboard?.writeText(text).then(() => toast('Copied')); } if (t.closest('[data-action="clear"]')) { e.preventDefault(); urlInput.value = ''; - + if (ipInput) ipInput.value = ''; + if (ipPreset) ipPreset.value = ''; + showError(urlInput, ''); + showError(ipInput, ''); updatePreview(); urlInput.focus(); } @@ -104,33 +203,33 @@ const panel = $('#' + (btn.getAttribute('aria-controls') || '')); if (!panel) return; const expanded = btn.getAttribute('aria-expanded') === 'true'; - btn.setAttribute('aria-expanded', expanded ? 'false' : 'true'); - panel.style.display = expanded ? 'none' : ''; + const next = !expanded; + btn.setAttribute('aria-expanded', next ? 'true' : 'false'); + panel.hidden = !next; + panel.style.display = next ? '' : 'none'; + const newLabel = next ? 'Collapse' : 'Expand'; + btn.textContent = newLabel; + btn.setAttribute('aria-label', newLabel); } }); - function showError(input, msg) { - const id = input.getAttribute('id'); - const box = document.querySelector(`.error[data-error-for="${id}"]`); - if (box) box.textContent = msg || ''; - input.setAttribute('aria-invalid', msg ? 'true' : 'false'); - } - + // field-level validation urlInput?.addEventListener('blur', () => { - const v = urlInput.value.trim(); - if (!v) return showError(urlInput, ''); - try { new URL(v); showError(urlInput, ''); } - catch { showError(urlInput, 'Invalid URL'); } + const raw = urlInput.value.trim(); + if (!raw) return showError(urlInput, ''); + const normalized = normalizeUrlMaybe(raw); + showError(urlInput, normalized ? '' : 'Invalid URL'); }); ipInput?.addEventListener('blur', () => { const v = ipInput.value.trim(); if (!v) return showError(ipInput, ''); - const ok = /^\b\d{1,3}(?:\.\d{1,3}){3}\b$/.test(v); - showError(ipInput, ok ? '' : 'Invalid IPv4 address'); + const ok = isValidIP(v); + showError(ipInput, ok ? '' : 'Invalid IP address'); + if (!ok) $('.result-box')?.setAttribute('data-state', 'empty'); }); - + // init (preview + recent-list sync) (function init() { const serverLink = out?.value?.trim(); if (serverLink) { @@ -141,14 +240,34 @@ } })(); + document.addEventListener('DOMContentLoaded', () => { + const btn = document.querySelector('[data-action="collapse"]'); + const panelId = btn?.getAttribute('aria-controls') || ''; + const panel = panelId ? document.getElementById(panelId) : null; + if (!btn || !panel) return; + + const expanded = btn.getAttribute('aria-expanded') === 'true'; + panel.hidden = !expanded; + // usuń/ustaw inline display spójnie z hidden + panel.style.display = expanded ? '' : 'none'; + + const label = expanded ? 'Collapse' : 'Expand'; + btn.textContent = label; + btn.setAttribute('aria-label', label); + }); + + form?.addEventListener('submit', (e) => e.preventDefault()); + document.addEventListener('keydown', (e) => { if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'c') { const text = out?.value?.trim(); if (!text) return; navigator.clipboard?.writeText(text).then(() => toast('Link copied')); } }); + })(); +// --- theme color sync (poza IIFE) --- function updateThemeColor() { const meta = document.querySelector('meta[name="theme-color"]'); if (!meta) return; diff --git a/templates/form.html b/templates/form.html index 9885f4a..f032121 100644 --- a/templates/form.html +++ b/templates/form.html @@ -53,11 +53,10 @@
- + Common choices: 0.0.0.0, 127.0.0.1, - or your device IP. + your device IP, supports IPv4 and IPv6.
@@ -68,12 +67,12 @@ +
-
@@ -119,7 +118,7 @@ {{ link_data[0]|datetimeformat }}