Compare commits
21 Commits
7b6dd96d68
...
master
Author | SHA1 | Date | |
---|---|---|---|
![]() |
46d986ce80 | ||
ec07782444 | |||
69adc272ff | |||
68e45a7477 | |||
da4f9907ae | |||
fec9d9c5dd | |||
6c58d7f524 | |||
ae98702806 | |||
163df6233b | |||
78cf869354 | |||
79b7ea77f4 | |||
6347a96db8 | |||
94d5130470 | |||
aaea5cdeef | |||
a2febd82c1 | |||
e065f5892d | |||
1afaea2d00 | |||
809b1168e1 | |||
![]() |
259d716e0f | ||
![]() |
43c9a7006c | ||
![]() |
0419ca1d9d |
59
app.py
59
app.py
@@ -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:
|
||||||
|
@@ -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%);
|
||||||
}
|
}
|
@@ -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');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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>
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user