Compare commits

..

21 Commits

Author SHA1 Message Date
Mateusz Gruszczyński
46d986ce80 zmiany w js i html 2025-08-31 11:21:58 +02:00
gru
ec07782444 Update static/css/main.css 2025-08-31 10:44:04 +02:00
gru
69adc272ff Update static/css/main.css 2025-08-30 18:18:29 +02:00
gru
68e45a7477 Update static/css/main.css 2025-08-30 18:16:15 +02:00
gru
da4f9907ae Update static/css/main.css 2025-08-30 18:15:10 +02:00
gru
fec9d9c5dd Update static/css/main.css 2025-08-30 18:11:46 +02:00
gru
6c58d7f524 Update static/css/main.css 2025-08-30 18:11:01 +02:00
gru
ae98702806 Update static/css/main.css 2025-08-30 18:10:24 +02:00
gru
163df6233b Update static/css/main.css 2025-08-30 18:08:55 +02:00
gru
78cf869354 Update static/css/main.css 2025-08-30 18:06:54 +02:00
gru
79b7ea77f4 Update app.py 2025-08-30 17:59:25 +02:00
gru
6347a96db8 Update app.py 2025-08-30 09:54:53 +02:00
gru
94d5130470 Merge pull request 'refactor' (#1) from refactor into master
Reviewed-on: #1
2025-08-30 00:19:49 +02:00
gru
aaea5cdeef Update app.py 2025-08-29 23:57:39 +02:00
gru
a2febd82c1 Update templates/form.html 2025-08-29 23:55:32 +02:00
gru
e065f5892d Update app.py 2025-08-29 23:49:32 +02:00
gru
1afaea2d00 Update app.py 2025-08-29 23:47:57 +02:00
gru
809b1168e1 Update templates/form.html 2025-08-29 23:45:23 +02:00
Mateusz Gruszczyński
259d716e0f poprawki 2025-08-29 22:36:41 +02:00
Mateusz Gruszczyński
43c9a7006c poprawki 2025-08-29 22:32:22 +02:00
Mateusz Gruszczyński
0419ca1d9d poprawki 2025-08-29 22:24:36 +02:00
4 changed files with 104 additions and 37 deletions

59
app.py
View File

@@ -6,6 +6,7 @@ import time
import json import json
import hashlib import hashlib
import ipaddress import ipaddress
import hmac, ipaddress
from datetime import datetime from datetime import datetime
from urllib.parse import urlparse, quote, unquote, urljoin from urllib.parse import urlparse, quote, unquote, urljoin
from functools import wraps from functools import wraps
@@ -94,8 +95,6 @@ def finalize_response(response):
if response.status_code >= 400: if response.status_code >= 400:
response.headers["Cache-Control"] = "no-store" response.headers["Cache-Control"] = "no-store"
response.headers["Pragma"] = "no-cache"
response.headers["Expires"] = "0"
return response return response
if path.startswith("/static/"): if path.startswith("/static/"):
@@ -108,8 +107,6 @@ def finalize_response(response):
if path == "/": if path == "/":
response.headers["Cache-Control"] = "private, no-store" response.headers["Cache-Control"] = "private, no-store"
response.headers["Pragma"] = "no-cache"
response.headers["Expires"] = "0"
return response return response
return response return response
@@ -279,16 +276,34 @@ def track_url_request(url):
def add_recent_link(url, target_ip): def add_recent_link(url, target_ip):
ts = datetime.now().isoformat() ts = datetime.now().isoformat()
link_data = f"{ts}|{url}|{target_ip}" new_item = f"{ts}|{url}|{target_ip}"
key = "recent_links"
current = redis_client.lrange(key, 0, -1)
filtered = []
for raw in current:
try:
s = raw.decode()
parts = s.split("|")
if len(parts) >= 3 and parts[1] == url and parts[2] == target_ip:
continue
except Exception:
pass
filtered.append(raw)
with redis_client.pipeline() as pipe: with redis_client.pipeline() as pipe:
pipe.lpush("recent_links", link_data) pipe.delete(key)
pipe.ltrim("recent_links", 0, 9) pipe.lpush(key, new_item)
if filtered:
pipe.rpush(key, *filtered[:99])
pipe.ltrim(key, 0, 99)
pipe.execute() pipe.execute()
redis_client.incr("stats:recent_links_added") redis_client.incr("stats:recent_links_added")
def get_recent_links(): def get_recent_links():
links = redis_client.lrange("recent_links", 0, 9) links = redis_client.lrange("recent_links", 0, 99)
out = [] out = []
for link in links: for link in links:
parts = link.decode().split("|") parts = link.decode().split("|")
@@ -344,31 +359,11 @@ def favicon():
@app.route("/", methods=["GET"]) @app.route("/", methods=["GET"])
def index(): def index():
generated_link = None
recent_links = get_recent_links() recent_links = get_recent_links()
url_param = request.args.get("url", config.DEFAULT_SOURCE_URL)
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:
normalized = validate_and_normalize_url(unquote(url_param))
encoded = quote(normalized, safe="")
generated_link = urljoin(
request.host_url, f"convert?url={encoded}&ip={target_ip}"
)
add_recent_link(normalized, target_ip)
recent_links = get_recent_links()
except Exception as e:
app.logger.error(f"Error processing URL: {str(e)}")
try: try:
return render_template( return render_template(
"form.html", "form.html",
generated_link=generated_link,
recent_links=recent_links, recent_links=recent_links,
client_ip=get_client_ip(), client_ip=get_client_ip(),
user_agent=request.headers.get("User-Agent", "Unknown"), user_agent=request.headers.get("User-Agent", "Unknown"),
@@ -376,7 +371,6 @@ def index():
except Exception: except Exception:
return jsonify( return jsonify(
{ {
"generated_link": generated_link,
"recent_links": recent_links, "recent_links": recent_links,
"client_ip": get_client_ip(), "client_ip": get_client_ip(),
"user_agent": request.headers.get("User-Agent", "Unknown"), "user_agent": request.headers.get("User-Agent", "Unknown"),
@@ -387,7 +381,7 @@ def index():
@app.route("/convert") @app.route("/convert")
@limiter.limit(config.RATE_LIMIT_CONVERT) @limiter.limit(config.RATE_LIMIT_CONVERT)
def convert(): def convert():
import hmac, ipaddress
def is_private_client_ip() -> bool: def is_private_client_ip() -> bool:
ip = get_client_ip() ip = get_client_ip()
@@ -436,8 +430,8 @@ def convert():
return resp return resp
try: try:
redis_client.incr("stats:convert_requests")
add_recent_convert() add_recent_convert()
redis_client.incr("stats:convert_requests")
if debug_mode: if debug_mode:
d("Start /convert w trybie debug") d("Start /convert w trybie debug")
@@ -515,11 +509,13 @@ def convert():
if debug_mode: if debug_mode:
d("Upstream 304 zwracam 304") d("Upstream 304 zwracam 304")
r.close() r.close()
add_recent_link(normalized_url, target_ip)
return debug_response(status=304) return debug_response(status=304)
resp = Response(status=304) resp = Response(status=304)
resp.headers.update(cache_headers(etag, r.headers.get("Last-Modified"))) resp.headers.update(cache_headers(etag, r.headers.get("Last-Modified")))
resp.direct_passthrough = True resp.direct_passthrough = True
r.close() r.close()
add_recent_link(normalized_url, target_ip)
return resp return resp
up_etag = r.headers.get("ETag") up_etag = r.headers.get("ETag")
@@ -566,6 +562,7 @@ def convert():
resp.headers.update(cache_headers(etag, up_lm)) resp.headers.update(cache_headers(etag, up_lm))
resp.direct_passthrough = True resp.direct_passthrough = True
redis_client.incr("stats:conversions_success") redis_client.incr("stats:conversions_success")
add_recent_link(normalized_url, target_ip)
return resp return resp
except requests.exceptions.RequestException as e: except requests.exceptions.RequestException as e:

View File

@@ -194,7 +194,8 @@ select:focus {
.form-actions { .form-actions {
display: flex; display: flex;
gap: 10px; gap: 10px;
align-items: center align-items: center;
justify-content: flex-end;
} }
/* Result */ /* Result */
@@ -324,14 +325,20 @@ select:focus {
color: #fff; color: #fff;
box-shadow: 0 10px 20px color-mix(in srgb, var(--brand) 35%, transparent); box-shadow: 0 10px 20px color-mix(in srgb, var(--brand) 35%, transparent);
transition: transform .04s ease, filter .15s ease, box-shadow .15s ease; transition: transform .04s ease, filter .15s ease, box-shadow .15s ease;
display: inline-block;
text-decoration: none;
display: inline-block;
} }
.btn:hover { .btn:hover {
filter: brightness(1.05) transition: transform 0.15s ease;
display: inline-block;
transform: scale(1.05);
} }
.btn:active { .btn:active {
transform: translateY(1px) transform: scale(0.95);
} }
.btn.outline { .btn.outline {
@@ -357,6 +364,30 @@ select:focus {
border-radius: 14px border-radius: 14px
} }
a.btn:hover {
text-decoration: none;
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
pointer-events: none;
}
a.btn[aria-disabled="true"] {
opacity: .5;
cursor: not-allowed;
pointer-events: none;
filter: none;
transform: none;
}
a.btn[aria-disabled="true"]:hover {
filter: none;
transform: none;
text-decoration: none;
}
/* Toast */ /* Toast */
#toast { #toast {
position: fixed; position: fixed;
@@ -661,6 +692,16 @@ select:focus {
background: var(--bg-elev) background: var(--bg-elev)
} }
.result-box .hint {
display: block;
text-align: center;
font-size: .9rem;
color: var(--muted);
opacity: 0.5;
opacity: 1.1;
text-shadow: 0 0 4px rgba(123, 212, 255, 0.4);
}
#error-dump { #error-dump {
margin: 0; margin: 0;
padding: 12px; padding: 12px;
@@ -673,6 +714,7 @@ select:focus {
overflow-wrap: anywhere; overflow-wrap: anywhere;
} }
@media (max-width:720px) { @media (max-width:720px) {
.error-card { .error-card {
padding: 12px padding: 12px
@@ -685,4 +727,18 @@ select:focus {
#error-dump { #error-dump {
max-height: 300px max-height: 300px
} }
}
.result-box {
margin-top: 14px;
padding: 12px;
border: 1px dashed var(--border);
border-radius: 12px;
background: var(--bg-elev);
box-shadow: 0 0 8px 2px color-mix(in srgb, var(--brand) 50%, transparent 50%);
transition: box-shadow 0.3s ease;
}
.result-box:hover {
box-shadow: 0 0 12px 4px color-mix(in srgb, var(--brand) 70%, transparent 30%);
} }

View File

@@ -86,6 +86,7 @@
const ipPreset = $('#ip-preset'); const ipPreset = $('#ip-preset');
const out = $('#generated-link'); const out = $('#generated-link');
const openBtn = $('#open-link'); const openBtn = $('#open-link');
const copyBtn = $('#copy-btn');
function showError(input, msg) { function showError(input, msg) {
const id = input.getAttribute('id'); const id = input.getAttribute('id');
@@ -94,6 +95,7 @@
input.setAttribute('aria-invalid', msg ? 'true' : 'false'); input.setAttribute('aria-invalid', msg ? 'true' : 'false');
} }
function updatePreview() { function updatePreview() {
const rawUrl = (urlInput?.value || '').trim(); const rawUrl = (urlInput?.value || '').trim();
const ip = (ipInput?.value || '').trim(); const ip = (ipInput?.value || '').trim();
@@ -103,12 +105,14 @@
if (openBtn) { if (openBtn) {
openBtn.setAttribute('href', '#'); openBtn.setAttribute('href', '#');
openBtn.setAttribute('aria-disabled', 'true'); openBtn.setAttribute('aria-disabled', 'true');
openBtn.setAttribute('disabled', 'true');
} }
if (copyBtn) copyBtn.setAttribute('disabled', 'true');
$('.result-box')?.setAttribute('data-state', 'empty'); $('.result-box')?.setAttribute('data-state', 'empty');
return; return;
} }
const normalized = normalizeUrlMaybe(rawUrl); // poprawny http/https lub '' const normalized = normalizeUrlMaybe(rawUrl);
const guessed = rawUrl ? (rawUrl.includes('://') ? rawUrl : `https://${rawUrl}`) : ''; const guessed = rawUrl ? (rawUrl.includes('://') ? rawUrl : `https://${rawUrl}`) : '';
const previewUrl = normalized || guessed; const previewUrl = normalized || guessed;
@@ -117,7 +121,9 @@
if (openBtn) { if (openBtn) {
openBtn.setAttribute('href', '#'); openBtn.setAttribute('href', '#');
openBtn.setAttribute('aria-disabled', 'true'); openBtn.setAttribute('aria-disabled', 'true');
openBtn.setAttribute('disabled', 'true');
} }
if (copyBtn) copyBtn.setAttribute('disabled', 'true');
$('.result-box')?.setAttribute('data-state', 'empty'); $('.result-box')?.setAttribute('data-state', 'empty');
return; return;
} }
@@ -130,11 +136,14 @@
if (ok) { if (ok) {
openBtn.setAttribute('href', link); openBtn.setAttribute('href', link);
openBtn.setAttribute('aria-disabled', 'false'); openBtn.setAttribute('aria-disabled', 'false');
openBtn.removeAttribute('disabled');
} else { } else {
openBtn.setAttribute('href', '#'); openBtn.setAttribute('href', '#');
openBtn.setAttribute('aria-disabled', 'true'); openBtn.setAttribute('aria-disabled', 'true');
openBtn.setAttribute('disabled', 'true');
} }
} }
if (copyBtn) copyBtn.toggleAttribute('disabled', !ok);
$('.result-box')?.setAttribute('data-state', ok ? 'ready' : 'empty'); $('.result-box')?.setAttribute('data-state', ok ? 'ready' : 'empty');
} }

View File

@@ -83,12 +83,17 @@
<input id="generated-link" type="text" value="{{ generated_link or '' }}" readonly <input id="generated-link" type="text" value="{{ generated_link or '' }}" readonly
placeholder="Link will appear here…"> placeholder="Link will appear here…">
<div class="result-buttons"> <div class="result-buttons">
<button class="btn" type="button" data-action="copy" data-target="#generated-link">Copy</button>
<button class="btn" type="button" id="copy-btn" data-action="copy"
data-target="#generated-link">Copy</button>
<a class="btn outline" id="open-link" href="{{ generated_link or '#' }}" target="_blank" <a class="btn outline" id="open-link" href="{{ generated_link or '#' }}" target="_blank"
rel="noopener" aria-disabled="{{ 'false' if generated_link else 'true' }}">Open</a> rel="noopener" aria-disabled="{{ 'false' if generated_link else 'true' }}">Open</a>
</div> </div>
</div> </div>
<small class="hint">The preview updates live while you type.</small> <small class="hint">Paste this link in your Mikrotik (IP -> DNS -> Adlist) or other DNS server /
ad blocking tool</small>
</div> </div>
</section> </section>