Spaces:
Sleeping
Sleeping
Add IP address, full referrer URL, and improved decryption fallback
Browse files- .gitignore +1 -1
- app.py +15 -2
- encryption.py +34 -15
- models.py +1 -0
- templates/index.html +14 -7
.gitignore
CHANGED
|
@@ -45,4 +45,4 @@ build/
|
|
| 45 |
*.egg-info/
|
| 46 |
|
| 47 |
# Private keys (keep in repo but be careful)
|
| 48 |
-
|
|
|
|
| 45 |
*.egg-info/
|
| 46 |
|
| 47 |
# Private keys (keep in repo but be careful)
|
| 48 |
+
PRIVATE_KEY.pem
|
app.py
CHANGED
|
@@ -122,6 +122,7 @@ async def get_data(
|
|
| 122 |
"id": item.id,
|
| 123 |
"user_id": item.user_id,
|
| 124 |
"refer_url": item.refer_url,
|
|
|
|
| 125 |
"json_data": item.json_data,
|
| 126 |
"created_at": item.created_at.isoformat() if item.created_at else None
|
| 127 |
}
|
|
@@ -188,8 +189,18 @@ async def blink(
|
|
| 188 |
# Store with error information
|
| 189 |
decrypted_results = [{"error": str(e), "raw_encrypted": encrypted_data[:100]}]
|
| 190 |
|
| 191 |
-
# Get referer URL from headers
|
| 192 |
-
refer_url = request.headers.get("referer")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 193 |
|
| 194 |
# Store each decrypted result as separate records
|
| 195 |
records_created = 0
|
|
@@ -197,6 +208,7 @@ async def blink(
|
|
| 197 |
blink_record = BlinkData(
|
| 198 |
user_id=user_id,
|
| 199 |
refer_url=refer_url,
|
|
|
|
| 200 |
json_data=json_data
|
| 201 |
)
|
| 202 |
db.add(blink_record)
|
|
@@ -207,6 +219,7 @@ async def blink(
|
|
| 207 |
blink_record = BlinkData(
|
| 208 |
user_id=user_id,
|
| 209 |
refer_url=refer_url,
|
|
|
|
| 210 |
json_data={"encrypted_length": len(encrypted_data)}
|
| 211 |
)
|
| 212 |
db.add(blink_record)
|
|
|
|
| 122 |
"id": item.id,
|
| 123 |
"user_id": item.user_id,
|
| 124 |
"refer_url": item.refer_url,
|
| 125 |
+
"ip_address": item.ip_address,
|
| 126 |
"json_data": item.json_data,
|
| 127 |
"created_at": item.created_at.isoformat() if item.created_at else None
|
| 128 |
}
|
|
|
|
| 189 |
# Store with error information
|
| 190 |
decrypted_results = [{"error": str(e), "raw_encrypted": encrypted_data[:100]}]
|
| 191 |
|
| 192 |
+
# Get referer URL from headers (full URL, not just origin)
|
| 193 |
+
refer_url = request.headers.get("referer")
|
| 194 |
+
|
| 195 |
+
# Get client IP address
|
| 196 |
+
# Check X-Forwarded-For header first (for proxies/load balancers)
|
| 197 |
+
forwarded_for = request.headers.get("x-forwarded-for")
|
| 198 |
+
if forwarded_for:
|
| 199 |
+
# X-Forwarded-For can contain multiple IPs, take the first one
|
| 200 |
+
ip_address = forwarded_for.split(",")[0].strip()
|
| 201 |
+
else:
|
| 202 |
+
# Fall back to direct client IP
|
| 203 |
+
ip_address = request.client.host if request.client else None
|
| 204 |
|
| 205 |
# Store each decrypted result as separate records
|
| 206 |
records_created = 0
|
|
|
|
| 208 |
blink_record = BlinkData(
|
| 209 |
user_id=user_id,
|
| 210 |
refer_url=refer_url,
|
| 211 |
+
ip_address=ip_address,
|
| 212 |
json_data=json_data
|
| 213 |
)
|
| 214 |
db.add(blink_record)
|
|
|
|
| 219 |
blink_record = BlinkData(
|
| 220 |
user_id=user_id,
|
| 221 |
refer_url=refer_url,
|
| 222 |
+
ip_address=ip_address,
|
| 223 |
json_data={"encrypted_length": len(encrypted_data)}
|
| 224 |
)
|
| 225 |
db.add(blink_record)
|
encryption.py
CHANGED
|
@@ -21,21 +21,36 @@ _private_key = None
|
|
| 21 |
|
| 22 |
def load_private_key():
|
| 23 |
"""
|
| 24 |
-
Load the RSA private key from
|
| 25 |
Caches the key for subsequent calls.
|
| 26 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
Returns:
|
| 28 |
-
RSA private key object
|
| 29 |
-
|
| 30 |
-
Raises:
|
| 31 |
-
FileNotFoundError: If private key file doesn't exist
|
| 32 |
-
ValueError: If private key is invalid
|
| 33 |
"""
|
| 34 |
global _private_key
|
| 35 |
|
| 36 |
if _private_key is not None:
|
| 37 |
return _private_key
|
| 38 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
try:
|
| 40 |
with open(PRIVATE_KEY_PATH, "rb") as key_file:
|
| 41 |
_private_key = serialization.load_pem_private_key(
|
|
@@ -46,11 +61,12 @@ def load_private_key():
|
|
| 46 |
logger.info(f"Successfully loaded private key from {PRIVATE_KEY_PATH}")
|
| 47 |
return _private_key
|
| 48 |
except FileNotFoundError:
|
| 49 |
-
logger.
|
| 50 |
-
raise
|
| 51 |
except Exception as e:
|
| 52 |
-
logger.
|
| 53 |
-
|
|
|
|
|
|
|
| 54 |
|
| 55 |
|
| 56 |
def decrypt_data(encrypted_base64: str) -> Optional[Any]:
|
|
@@ -61,15 +77,17 @@ def decrypt_data(encrypted_base64: str) -> Optional[Any]:
|
|
| 61 |
encrypted_base64: Base64 URL-safe encoded encrypted string
|
| 62 |
|
| 63 |
Returns:
|
| 64 |
-
Decrypted data parsed as JSON,
|
| 65 |
-
|
| 66 |
-
Raises:
|
| 67 |
-
ValueError: If decryption fails
|
| 68 |
"""
|
| 69 |
try:
|
| 70 |
# Load the private key
|
| 71 |
private_key = load_private_key()
|
| 72 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
# Decode base64 URL-safe encoded data
|
| 74 |
# Add padding if necessary
|
| 75 |
padded = encrypted_base64 + '=' * (4 - len(encrypted_base64) % 4)
|
|
@@ -98,7 +116,8 @@ def decrypt_data(encrypted_base64: str) -> Optional[Any]:
|
|
| 98 |
|
| 99 |
except Exception as e:
|
| 100 |
logger.error(f"Decryption failed: {e}")
|
| 101 |
-
|
|
|
|
| 102 |
|
| 103 |
|
| 104 |
def decrypt_multiple_blocks(encrypted_data: str, block_size: int = 344) -> list[Any]:
|
|
|
|
| 21 |
|
| 22 |
def load_private_key():
|
| 23 |
"""
|
| 24 |
+
Load the RSA private key from environment variable or PEM file.
|
| 25 |
Caches the key for subsequent calls.
|
| 26 |
|
| 27 |
+
Priority:
|
| 28 |
+
1. PRIVATE_KEY environment variable (PEM content)
|
| 29 |
+
2. PRIVATE_KEY_PATH file
|
| 30 |
+
|
| 31 |
Returns:
|
| 32 |
+
RSA private key object, or None if not available
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
"""
|
| 34 |
global _private_key
|
| 35 |
|
| 36 |
if _private_key is not None:
|
| 37 |
return _private_key
|
| 38 |
|
| 39 |
+
# Try loading from environment variable first
|
| 40 |
+
private_key_pem = os.getenv("PRIVATE_KEY")
|
| 41 |
+
if private_key_pem:
|
| 42 |
+
try:
|
| 43 |
+
_private_key = serialization.load_pem_private_key(
|
| 44 |
+
private_key_pem.encode(),
|
| 45 |
+
password=None,
|
| 46 |
+
backend=default_backend()
|
| 47 |
+
)
|
| 48 |
+
logger.info("Successfully loaded private key from PRIVATE_KEY env variable")
|
| 49 |
+
return _private_key
|
| 50 |
+
except Exception as e:
|
| 51 |
+
logger.warning(f"Failed to load private key from env: {e}")
|
| 52 |
+
|
| 53 |
+
# Try loading from file
|
| 54 |
try:
|
| 55 |
with open(PRIVATE_KEY_PATH, "rb") as key_file:
|
| 56 |
_private_key = serialization.load_pem_private_key(
|
|
|
|
| 61 |
logger.info(f"Successfully loaded private key from {PRIVATE_KEY_PATH}")
|
| 62 |
return _private_key
|
| 63 |
except FileNotFoundError:
|
| 64 |
+
logger.warning(f"Private key file not found: {PRIVATE_KEY_PATH}")
|
|
|
|
| 65 |
except Exception as e:
|
| 66 |
+
logger.warning(f"Failed to load private key from file: {e}")
|
| 67 |
+
|
| 68 |
+
logger.warning("No private key available - encrypted data will not be decrypted")
|
| 69 |
+
return None
|
| 70 |
|
| 71 |
|
| 72 |
def decrypt_data(encrypted_base64: str) -> Optional[Any]:
|
|
|
|
| 77 |
encrypted_base64: Base64 URL-safe encoded encrypted string
|
| 78 |
|
| 79 |
Returns:
|
| 80 |
+
Decrypted data parsed as JSON, encrypted data if no key available, or None on error
|
|
|
|
|
|
|
|
|
|
| 81 |
"""
|
| 82 |
try:
|
| 83 |
# Load the private key
|
| 84 |
private_key = load_private_key()
|
| 85 |
|
| 86 |
+
# If no private key, return the encrypted data as-is
|
| 87 |
+
if private_key is None:
|
| 88 |
+
logger.warning("No private key - returning encrypted data")
|
| 89 |
+
return {"encrypted_data": encrypted_base64, "decryption_status": "no_key_available"}
|
| 90 |
+
|
| 91 |
# Decode base64 URL-safe encoded data
|
| 92 |
# Add padding if necessary
|
| 93 |
padded = encrypted_base64 + '=' * (4 - len(encrypted_base64) % 4)
|
|
|
|
| 116 |
|
| 117 |
except Exception as e:
|
| 118 |
logger.error(f"Decryption failed: {e}")
|
| 119 |
+
# Return encrypted data on failure
|
| 120 |
+
return {"encrypted_data": encrypted_base64, "decryption_error": str(e)}
|
| 121 |
|
| 122 |
|
| 123 |
def decrypt_multiple_blocks(encrypted_data: str, block_size: int = 344) -> list[Any]:
|
models.py
CHANGED
|
@@ -22,6 +22,7 @@ class BlinkData(Base):
|
|
| 22 |
id = Column(Integer, primary_key=True, autoincrement=True, index=True)
|
| 23 |
user_id = Column(String(20), index=True, nullable=False)
|
| 24 |
refer_url = Column(Text, nullable=True)
|
|
|
|
| 25 |
json_data = Column(JSON, nullable=True)
|
| 26 |
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
| 27 |
|
|
|
|
| 22 |
id = Column(Integer, primary_key=True, autoincrement=True, index=True)
|
| 23 |
user_id = Column(String(20), index=True, nullable=False)
|
| 24 |
refer_url = Column(Text, nullable=True)
|
| 25 |
+
ip_address = Column(String(45), nullable=True) # IPv6 can be up to 45 chars
|
| 26 |
json_data = Column(JSON, nullable=True)
|
| 27 |
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
| 28 |
|
templates/index.html
CHANGED
|
@@ -1,5 +1,6 @@
|
|
| 1 |
<!DOCTYPE html>
|
| 2 |
<html lang="en">
|
|
|
|
| 3 |
<head>
|
| 4 |
<meta charset="UTF-8">
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
@@ -186,7 +187,9 @@
|
|
| 186 |
}
|
| 187 |
|
| 188 |
@keyframes spin {
|
| 189 |
-
to {
|
|
|
|
|
|
|
| 190 |
}
|
| 191 |
|
| 192 |
.empty-state {
|
|
@@ -203,6 +206,7 @@
|
|
| 203 |
}
|
| 204 |
</style>
|
| 205 |
</head>
|
|
|
|
| 206 |
<body>
|
| 207 |
<div class="container">
|
| 208 |
<header>
|
|
@@ -228,13 +232,14 @@
|
|
| 228 |
<th>ID</th>
|
| 229 |
<th>User ID</th>
|
| 230 |
<th>Refer URL</th>
|
|
|
|
| 231 |
<th>JSON Data</th>
|
| 232 |
<th>Created At</th>
|
| 233 |
</tr>
|
| 234 |
</thead>
|
| 235 |
<tbody id="tableBody">
|
| 236 |
<tr>
|
| 237 |
-
<td colspan="
|
| 238 |
<div class="loading">
|
| 239 |
<div class="spinner"></div>
|
| 240 |
Loading data...
|
|
@@ -269,11 +274,11 @@
|
|
| 269 |
|
| 270 |
function renderTable(items) {
|
| 271 |
const tbody = document.getElementById('tableBody');
|
| 272 |
-
|
| 273 |
if (items.length === 0) {
|
| 274 |
tbody.innerHTML = `
|
| 275 |
<tr>
|
| 276 |
-
<td colspan="
|
| 277 |
<div class="empty-state">
|
| 278 |
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
| 279 |
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4" />
|
|
@@ -291,6 +296,7 @@
|
|
| 291 |
<td>${item.id}</td>
|
| 292 |
<td class="user-id">${item.user_id}</td>
|
| 293 |
<td class="refer-url" title="${item.refer_url || ''}">${item.refer_url || '-'}</td>
|
|
|
|
| 294 |
<td><pre class="json-data">${JSON.stringify(item.json_data, null, 2)}</pre></td>
|
| 295 |
<td class="timestamp">${new Date(item.created_at).toLocaleString()}</td>
|
| 296 |
</tr>
|
|
@@ -309,10 +315,10 @@
|
|
| 309 |
currentPage = page;
|
| 310 |
const data = await fetchData(page);
|
| 311 |
totalRecords = data.total;
|
| 312 |
-
|
| 313 |
document.getElementById('totalRecords').textContent = data.total.toLocaleString();
|
| 314 |
document.getElementById('uniqueUsers').textContent = data.unique_users.toLocaleString();
|
| 315 |
-
|
| 316 |
renderTable(data.items);
|
| 317 |
updatePagination();
|
| 318 |
}
|
|
@@ -334,4 +340,5 @@
|
|
| 334 |
loadPage(1);
|
| 335 |
</script>
|
| 336 |
</body>
|
| 337 |
-
|
|
|
|
|
|
| 1 |
<!DOCTYPE html>
|
| 2 |
<html lang="en">
|
| 3 |
+
|
| 4 |
<head>
|
| 5 |
<meta charset="UTF-8">
|
| 6 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
|
|
| 187 |
}
|
| 188 |
|
| 189 |
@keyframes spin {
|
| 190 |
+
to {
|
| 191 |
+
transform: rotate(360deg);
|
| 192 |
+
}
|
| 193 |
}
|
| 194 |
|
| 195 |
.empty-state {
|
|
|
|
| 206 |
}
|
| 207 |
</style>
|
| 208 |
</head>
|
| 209 |
+
|
| 210 |
<body>
|
| 211 |
<div class="container">
|
| 212 |
<header>
|
|
|
|
| 232 |
<th>ID</th>
|
| 233 |
<th>User ID</th>
|
| 234 |
<th>Refer URL</th>
|
| 235 |
+
<th>IP Address</th>
|
| 236 |
<th>JSON Data</th>
|
| 237 |
<th>Created At</th>
|
| 238 |
</tr>
|
| 239 |
</thead>
|
| 240 |
<tbody id="tableBody">
|
| 241 |
<tr>
|
| 242 |
+
<td colspan="6">
|
| 243 |
<div class="loading">
|
| 244 |
<div class="spinner"></div>
|
| 245 |
Loading data...
|
|
|
|
| 274 |
|
| 275 |
function renderTable(items) {
|
| 276 |
const tbody = document.getElementById('tableBody');
|
| 277 |
+
|
| 278 |
if (items.length === 0) {
|
| 279 |
tbody.innerHTML = `
|
| 280 |
<tr>
|
| 281 |
+
<td colspan="6">
|
| 282 |
<div class="empty-state">
|
| 283 |
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
| 284 |
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4" />
|
|
|
|
| 296 |
<td>${item.id}</td>
|
| 297 |
<td class="user-id">${item.user_id}</td>
|
| 298 |
<td class="refer-url" title="${item.refer_url || ''}">${item.refer_url || '-'}</td>
|
| 299 |
+
<td class="ip-address">${item.ip_address || '-'}</td>
|
| 300 |
<td><pre class="json-data">${JSON.stringify(item.json_data, null, 2)}</pre></td>
|
| 301 |
<td class="timestamp">${new Date(item.created_at).toLocaleString()}</td>
|
| 302 |
</tr>
|
|
|
|
| 315 |
currentPage = page;
|
| 316 |
const data = await fetchData(page);
|
| 317 |
totalRecords = data.total;
|
| 318 |
+
|
| 319 |
document.getElementById('totalRecords').textContent = data.total.toLocaleString();
|
| 320 |
document.getElementById('uniqueUsers').textContent = data.unique_users.toLocaleString();
|
| 321 |
+
|
| 322 |
renderTable(data.items);
|
| 323 |
updatePagination();
|
| 324 |
}
|
|
|
|
| 340 |
loadPage(1);
|
| 341 |
</script>
|
| 342 |
</body>
|
| 343 |
+
|
| 344 |
+
</html>
|