mike23415 commited on
Commit
4716751
Β·
verified Β·
1 Parent(s): beb9cca

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +507 -50
app.py CHANGED
@@ -6,68 +6,525 @@ import uuid
6
  import os
7
  import io
8
  import base64
9
- from PIL import Image
10
  import time
 
 
 
 
 
 
 
11
 
12
  app = Flask(__name__)
13
  CORS(app)
14
 
15
- # In-memory store: { id: { data, image (optional), expire_at, view_once } }
16
- SECRETS = {}
17
- MAX_IMAGE_SIZE = 300 * 1024 # 300 KB
 
18
 
19
- @app.route("/api/store", methods=["POST"])
20
- def store():
21
- form = request.form
22
- data = form.get("data")
23
- ttl = int(form.get("ttl", 300))
24
- view_once = form.get("view_once") == "true"
25
-
26
- # Handle image if present
27
- image_file = request.files.get("image")
28
- image_data = None
29
-
30
- if image_file:
31
- img_bytes = image_file.read()
32
- if len(img_bytes) > MAX_IMAGE_SIZE:
33
- image = Image.open(io.BytesIO(img_bytes))
34
- image.thumbnail((1024, 1024)) # Resize for safety
35
- output = io.BytesIO()
36
- image.save(output, format="JPEG", optimize=True, quality=70)
37
- image_data = base64.b64encode(output.getvalue()).decode("utf-8")
38
- else:
39
- image_data = base64.b64encode(img_bytes).decode("utf-8")
40
-
41
- sid = str(uuid.uuid4())
42
- SECRETS[sid] = {
43
- "data": data,
44
- "image": image_data,
45
- "expire_at": time.time() + ttl,
46
- "view_once": view_once
47
- }
48
- return jsonify({"id": sid})
49
 
50
- @app.route("/api/fetch/<sid>")
51
- def fetch(sid):
52
- secret = SECRETS.get(sid)
53
- if not secret:
54
- return jsonify({"error": "Not found"}), 404
55
- if time.time() > secret["expire_at"]:
56
- del SECRETS[sid]
57
- return jsonify({"error": "Expired"}), 410
58
 
59
- response = {"data": secret["data"]}
60
- if secret.get("image"):
61
- response["image"] = secret["image"]
 
 
 
 
 
62
 
63
- if secret["view_once"]:
64
- del SECRETS[sid]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
 
66
- return jsonify(response)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
67
 
68
  @app.route("/")
69
  def index():
70
- return "Sharelock Flask backend is running."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71
 
72
  if __name__ == "__main__":
73
- app.run(host="0.0.0.0", port=7860)
 
 
 
 
 
 
 
 
 
 
 
 
 
6
  import os
7
  import io
8
  import base64
 
9
  import time
10
+ import json
11
+ import hashlib
12
+ import qrcode
13
+ from PIL import Image
14
+ import requests
15
+ import random
16
+ import string
17
 
18
  app = Flask(__name__)
19
  CORS(app)
20
 
21
+ # In-memory storage
22
+ SECRETS = {} # { id: { data, file_data, file_type, expire_at, view_once, theme, analytics, etc. } }
23
+ SHORT_LINKS = {} # { short_id: full_id }
24
+ ANALYTICS = {} # { secret_id: [analytics_entries] }
25
 
26
+ # Configuration
27
+ MAX_FILE_SIZE = 5 * 1024 * 1024 # 5MB
28
+ ALLOWED_EXTENSIONS = {
29
+ 'image': ['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg'],
30
+ 'video': ['mp4', 'avi', 'mov', 'wmv', 'flv', 'webm'],
31
+ 'audio': ['mp3', 'wav', 'ogg', 'aac', 'm4a'],
32
+ 'document': ['pdf', 'txt', 'doc', 'docx', 'rtf', 'odt']
33
+ }
34
+
35
+ def get_file_type(filename):
36
+ """Determine file type based on extension"""
37
+ if not filename:
38
+ return 'unknown'
39
+
40
+ ext = filename.rsplit('.', 1)[1].lower() if '.' in filename else ''
41
+
42
+ for file_type, extensions in ALLOWED_EXTENSIONS.items():
43
+ if ext in extensions:
44
+ return file_type
45
+
46
+ return 'unknown'
 
 
 
 
 
 
 
 
 
47
 
48
+ def generate_short_id():
49
+ """Generate a short, unique ID"""
50
+ return ''.join(random.choices(string.ascii_lowercase + string.digits, k=6))
 
 
 
 
 
51
 
52
+ def get_client_ip(request):
53
+ """Get client IP address"""
54
+ if request.headers.get('X-Forwarded-For'):
55
+ return request.headers.get('X-Forwarded-For').split(',')[0].strip()
56
+ elif request.headers.get('X-Real-IP'):
57
+ return request.headers.get('X-Real-IP')
58
+ else:
59
+ return request.remote_addr
60
 
61
+ def get_location_info(ip):
62
+ """Get location information from IP (mock implementation)"""
63
+ # In production, use a real geolocation service like ipapi.co, ipstack.com, etc.
64
+ try:
65
+ # Mock data - replace with real API call
66
+ if ip == '127.0.0.1' or ip.startswith('192.168.'):
67
+ return {
68
+ 'country': 'Local',
69
+ 'city': 'Local',
70
+ 'region': 'Local',
71
+ 'timezone': 'Local'
72
+ }
73
+
74
+ # Example with ipapi.co (uncomment for production)
75
+ # response = requests.get(f'https://ipapi.co/{ip}/json/', timeout=5)
76
+ # if response.status_code == 200:
77
+ # data = response.json()
78
+ # return {
79
+ # 'country': data.get('country_name', 'Unknown'),
80
+ # 'city': data.get('city', 'Unknown'),
81
+ # 'region': data.get('region', 'Unknown'),
82
+ # 'timezone': data.get('timezone', 'Unknown')
83
+ # }
84
+
85
+ return {
86
+ 'country': 'Unknown',
87
+ 'city': 'Unknown',
88
+ 'region': 'Unknown',
89
+ 'timezone': 'Unknown'
90
+ }
91
+ except:
92
+ return {
93
+ 'country': 'Unknown',
94
+ 'city': 'Unknown',
95
+ 'region': 'Unknown',
96
+ 'timezone': 'Unknown'
97
+ }
98
 
99
+ def generate_qr_code(data):
100
+ """Generate QR code for the given data"""
101
+ qr = qrcode.QRCode(
102
+ version=1,
103
+ error_correction=qrcode.constants.ERROR_CORRECT_L,
104
+ box_size=10,
105
+ border=4,
106
+ )
107
+ qr.add_data(data)
108
+ qr.make(fit=True)
109
+
110
+ img = qr.make_image(fill_color="black", back_color="white")
111
+
112
+ # Convert to base64
113
+ buffer = io.BytesIO()
114
+ img.save(buffer, format='PNG')
115
+ img_str = base64.b64encode(buffer.getvalue()).decode()
116
+
117
+ return f"data:image/png;base64,{img_str}"
118
+
119
+ def record_access(secret_id, request):
120
+ """Record access analytics"""
121
+ ip = get_client_ip(request)
122
+ location = get_location_info(ip)
123
+ user_agent = request.headers.get('User-Agent', '')
124
+
125
+ # Determine device type
126
+ device_type = 'desktop'
127
+ if any(mobile in user_agent.lower() for mobile in ['mobile', 'android', 'iphone', 'ipad']):
128
+ device_type = 'mobile'
129
+
130
+ analytics_entry = {
131
+ 'timestamp': time.time(),
132
+ 'ip': ip,
133
+ 'location': location,
134
+ 'user_agent': user_agent,
135
+ 'device_type': device_type,
136
+ 'referer': request.headers.get('Referer', ''),
137
+ 'accept_language': request.headers.get('Accept-Language', '')
138
+ }
139
+
140
+ if secret_id not in ANALYTICS:
141
+ ANALYTICS[secret_id] = []
142
+
143
+ ANALYTICS[secret_id].append(analytics_entry)
144
+
145
+ return analytics_entry
146
 
147
  @app.route("/")
148
  def index():
149
+ """Health check endpoint"""
150
+ return jsonify({
151
+ "status": "running",
152
+ "service": "Sharelock Backend",
153
+ "version": "2.0.0",
154
+ "features": [
155
+ "End-to-end encryption",
156
+ "File uploads (5MB max)",
157
+ "QR code generation",
158
+ "Analytics tracking",
159
+ "Short URLs",
160
+ "Self-destruct messages"
161
+ ]
162
+ })
163
+
164
+ @app.route("/api/store", methods=["POST"])
165
+ def store():
166
+ """Store encrypted secret with enhanced features"""
167
+ try:
168
+ form = request.form
169
+ data = form.get("data")
170
+
171
+ if not data:
172
+ return jsonify({"error": "Data is required"}), 400
173
+
174
+ # Parse parameters
175
+ ttl = int(form.get("ttl", 300))
176
+ view_once = form.get("view_once", "false").lower() == "true"
177
+ delay_seconds = int(form.get("delay_seconds", 0))
178
+ theme = form.get("theme", "default")
179
+ password_hint = form.get("password_hint", "")
180
+
181
+ # Handle file upload
182
+ file_data = None
183
+ file_type = None
184
+ file_name = None
185
+
186
+ if 'file' in request.files:
187
+ file = request.files['file']
188
+ if file and file.filename:
189
+ # Check file size
190
+ file.seek(0, os.SEEK_END)
191
+ file_size = file.tell()
192
+ file.seek(0)
193
+
194
+ if file_size > MAX_FILE_SIZE:
195
+ return jsonify({"error": f"File too large. Max size: {MAX_FILE_SIZE/1024/1024:.1f}MB"}), 400
196
+
197
+ # Process file
198
+ file_name = secure_filename(file.filename)
199
+ file_type = get_file_type(file_name)
200
+
201
+ if file_type == 'unknown':
202
+ return jsonify({"error": "File type not supported"}), 400
203
+
204
+ # Read and encode file
205
+ file_content = file.read()
206
+ file_data = base64.b64encode(file_content).decode('utf-8')
207
+
208
+ # Generate IDs
209
+ secret_id = str(uuid.uuid4())
210
+ short_id = generate_short_id()
211
+
212
+ # Ensure short_id is unique
213
+ while short_id in SHORT_LINKS:
214
+ short_id = generate_short_id()
215
+
216
+ # Store secret
217
+ SECRETS[secret_id] = {
218
+ "data": data,
219
+ "file_data": file_data,
220
+ "file_type": file_type,
221
+ "file_name": file_name,
222
+ "expire_at": time.time() + ttl,
223
+ "view_once": view_once,
224
+ "delay_seconds": delay_seconds,
225
+ "theme": theme,
226
+ "password_hint": password_hint,
227
+ "created_at": time.time(),
228
+ "creator_ip": get_client_ip(request),
229
+ "access_count": 0
230
+ }
231
+
232
+ # Store short link mapping
233
+ SHORT_LINKS[short_id] = secret_id
234
+
235
+ # Generate QR code
236
+ base_url = request.host_url.rstrip('/')
237
+ secret_url = f"{base_url}/tools/sharelock?id={secret_id}"
238
+ qr_code = generate_qr_code(secret_url)
239
+
240
+ return jsonify({
241
+ "id": secret_id,
242
+ "short_id": short_id,
243
+ "short_url": f"{base_url}/s/{short_id}",
244
+ "qr_code": qr_code,
245
+ "expires_at": SECRETS[secret_id]["expire_at"],
246
+ "has_file": file_data is not None
247
+ })
248
+
249
+ except Exception as e:
250
+ return jsonify({"error": str(e)}), 500
251
+
252
+ @app.route("/api/fetch/<secret_id>")
253
+ def fetch(secret_id):
254
+ """Fetch and decrypt secret with analytics"""
255
+ try:
256
+ # Check if it's a short link
257
+ if secret_id in SHORT_LINKS:
258
+ secret_id = SHORT_LINKS[secret_id]
259
+
260
+ secret = SECRETS.get(secret_id)
261
+ if not secret:
262
+ return jsonify({"error": "Secret not found"}), 404
263
+
264
+ # Check expiration
265
+ if time.time() > secret["expire_at"]:
266
+ # Clean up expired secret
267
+ if secret_id in SECRETS:
268
+ del SECRETS[secret_id]
269
+ # Clean up short link
270
+ for short_id, full_id in list(SHORT_LINKS.items()):
271
+ if full_id == secret_id:
272
+ del SHORT_LINKS[short_id]
273
+ return jsonify({"error": "Secret has expired"}), 410
274
+
275
+ # Record access analytics
276
+ analytics_entry = record_access(secret_id, request)
277
+
278
+ # Increment access count
279
+ secret["access_count"] += 1
280
+
281
+ # Prepare response
282
+ response = {
283
+ "data": secret["data"],
284
+ "theme": secret.get("theme", "default"),
285
+ "delay_seconds": secret.get("delay_seconds", 0),
286
+ "password_hint": secret.get("password_hint", ""),
287
+ "access_count": secret["access_count"]
288
+ }
289
+
290
+ # Include file data if present
291
+ if secret.get("file_data"):
292
+ response["file_data"] = secret["file_data"]
293
+ response["file_type"] = secret.get("file_type", "unknown")
294
+ response["file_name"] = secret.get("file_name", "unknown")
295
+
296
+ # Handle view-once deletion
297
+ if secret["view_once"]:
298
+ # Schedule deletion after delay
299
+ delay = secret.get("delay_seconds", 0)
300
+ if delay > 0:
301
+ # In a production environment, you'd use a task queue like Celery
302
+ # For now, we'll delete immediately after response
303
+ pass
304
+
305
+ # Delete the secret
306
+ del SECRETS[secret_id]
307
+
308
+ # Clean up short link
309
+ for short_id, full_id in list(SHORT_LINKS.items()):
310
+ if full_id == secret_id:
311
+ del SHORT_LINKS[short_id]
312
+ break
313
+
314
+ return jsonify(response)
315
+
316
+ except Exception as e:
317
+ return jsonify({"error": str(e)}), 500
318
+
319
+ @app.route("/api/analytics/<secret_id>")
320
+ def get_analytics(secret_id):
321
+ """Get analytics for a specific secret"""
322
+ try:
323
+ # Verify secret exists or existed
324
+ if secret_id not in SECRETS and secret_id not in ANALYTICS:
325
+ return jsonify({"error": "Secret not found"}), 404
326
+
327
+ analytics_data = ANALYTICS.get(secret_id, [])
328
+
329
+ # Format analytics for frontend
330
+ formatted_analytics = []
331
+ for entry in analytics_data:
332
+ formatted_analytics.append({
333
+ "timestamp": entry["timestamp"],
334
+ "datetime": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(entry["timestamp"])),
335
+ "ip": entry["ip"],
336
+ "location": entry["location"],
337
+ "device_type": entry["device_type"],
338
+ "user_agent": entry["user_agent"][:100] + "..." if len(entry["user_agent"]) > 100 else entry["user_agent"]
339
+ })
340
+
341
+ return jsonify({
342
+ "secret_id": secret_id,
343
+ "total_accesses": len(formatted_analytics),
344
+ "analytics": formatted_analytics
345
+ })
346
+
347
+ except Exception as e:
348
+ return jsonify({"error": str(e)}), 500
349
+
350
+ @app.route("/api/secrets")
351
+ def list_secrets():
352
+ """List all active secrets (for dashboard)"""
353
+ try:
354
+ current_time = time.time()
355
+ active_secrets = []
356
+
357
+ for secret_id, secret in SECRETS.items():
358
+ if current_time <= secret["expire_at"]:
359
+ # Find short link
360
+ short_id = None
361
+ for s_id, full_id in SHORT_LINKS.items():
362
+ if full_id == secret_id:
363
+ short_id = s_id
364
+ break
365
+
366
+ active_secrets.append({
367
+ "id": secret_id,
368
+ "short_id": short_id,
369
+ "created_at": secret["created_at"],
370
+ "expires_at": secret["expire_at"],
371
+ "view_once": secret["view_once"],
372
+ "has_file": secret.get("file_data") is not None,
373
+ "file_type": secret.get("file_type"),
374
+ "theme": secret.get("theme", "default"),
375
+ "access_count": secret.get("access_count", 0),
376
+ "preview": secret["data"][:100] + "..." if len(secret["data"]) > 100 else secret["data"]
377
+ })
378
+
379
+ return jsonify({
380
+ "secrets": active_secrets,
381
+ "total": len(active_secrets)
382
+ })
383
+
384
+ except Exception as e:
385
+ return jsonify({"error": str(e)}), 500
386
+
387
+ @app.route("/api/delete/<secret_id>", methods=["DELETE"])
388
+ def delete_secret(secret_id):
389
+ """Manually delete a secret"""
390
+ try:
391
+ if secret_id not in SECRETS:
392
+ return jsonify({"error": "Secret not found"}), 404
393
+
394
+ # Delete secret
395
+ del SECRETS[secret_id]
396
+
397
+ # Clean up short link
398
+ for short_id, full_id in list(SHORT_LINKS.items()):
399
+ if full_id == secret_id:
400
+ del SHORT_LINKS[short_id]
401
+ break
402
+
403
+ return jsonify({"message": "Secret deleted successfully"})
404
+
405
+ except Exception as e:
406
+ return jsonify({"error": str(e)}), 500
407
+
408
+ @app.route("/s/<short_id>")
409
+ def redirect_short_link(short_id):
410
+ """Redirect short link to full URL"""
411
+ if short_id not in SHORT_LINKS:
412
+ return jsonify({"error": "Short link not found"}), 404
413
+
414
+ secret_id = SHORT_LINKS[short_id]
415
+ base_url = request.host_url.rstrip('/')
416
+ return f"""
417
+ <!DOCTYPE html>
418
+ <html>
419
+ <head>
420
+ <title>Sharelock - Redirecting...</title>
421
+ <meta http-equiv="refresh" content="0;url={base_url}/tools/sharelock?id={secret_id}">
422
+ </head>
423
+ <body>
424
+ <p>Redirecting to secure message...</p>
425
+ <p>If you are not redirected automatically, <a href="{base_url}/tools/sharelock?id={secret_id}">click here</a>.</p>
426
+ </body>
427
+ </html>
428
+ """
429
+
430
+ @app.route("/api/qr/<secret_id>")
431
+ def get_qr_code(secret_id):
432
+ """Generate QR code for a secret"""
433
+ try:
434
+ if secret_id not in SECRETS:
435
+ return jsonify({"error": "Secret not found"}), 404
436
+
437
+ base_url = request.host_url.rstrip('/')
438
+ secret_url = f"{base_url}/tools/sharelock?id={secret_id}"
439
+ qr_code = generate_qr_code(secret_url)
440
+
441
+ return jsonify({"qr_code": qr_code})
442
+
443
+ except Exception as e:
444
+ return jsonify({"error": str(e)}), 500
445
+
446
+ @app.route("/api/stats")
447
+ def get_stats():
448
+ """Get overall statistics"""
449
+ try:
450
+ total_secrets = len(SECRETS)
451
+ total_accesses = sum(len(analytics) for analytics in ANALYTICS.values())
452
+
453
+ # Count by file type
454
+ file_types = {}
455
+ for secret in SECRETS.values():
456
+ file_type = secret.get("file_type", "text")
457
+ file_types[file_type] = file_types.get(file_type, 0) + 1
458
+
459
+ # Count by theme
460
+ themes = {}
461
+ for secret in SECRETS.values():
462
+ theme = secret.get("theme", "default")
463
+ themes[theme] = themes.get(theme, 0) + 1
464
+
465
+ return jsonify({
466
+ "total_secrets": total_secrets,
467
+ "total_accesses": total_accesses,
468
+ "file_types": file_types,
469
+ "themes": themes,
470
+ "active_short_links": len(SHORT_LINKS)
471
+ })
472
+
473
+ except Exception as e:
474
+ return jsonify({"error": str(e)}), 500
475
+
476
+ @app.route("/api/cleanup", methods=["POST"])
477
+ def cleanup_expired():
478
+ """Clean up expired secrets"""
479
+ try:
480
+ current_time = time.time()
481
+ expired_count = 0
482
+
483
+ # Find expired secrets
484
+ expired_secrets = []
485
+ for secret_id, secret in SECRETS.items():
486
+ if current_time > secret["expire_at"]:
487
+ expired_secrets.append(secret_id)
488
+
489
+ # Delete expired secrets
490
+ for secret_id in expired_secrets:
491
+ del SECRETS[secret_id]
492
+ expired_count += 1
493
+
494
+ # Clean up short links
495
+ for short_id, full_id in list(SHORT_LINKS.items()):
496
+ if full_id == secret_id:
497
+ del SHORT_LINKS[short_id]
498
+ break
499
+
500
+ return jsonify({
501
+ "message": f"Cleaned up {expired_count} expired secrets",
502
+ "expired_count": expired_count
503
+ })
504
+
505
+ except Exception as e:
506
+ return jsonify({"error": str(e)}), 500
507
+
508
+ # Error handlers
509
+ @app.errorhandler(404)
510
+ def not_found(error):
511
+ return jsonify({"error": "Endpoint not found"}), 404
512
+
513
+ @app.errorhandler(500)
514
+ def internal_error(error):
515
+ return jsonify({"error": "Internal server error"}), 500
516
 
517
  if __name__ == "__main__":
518
+ print("πŸ” Sharelock Backend Starting...")
519
+ print("πŸ“Š Features enabled:")
520
+ print(" βœ… End-to-end encryption")
521
+ print(" βœ… File uploads (5MB max)")
522
+ print(" βœ… QR code generation")
523
+ print(" βœ… Analytics tracking")
524
+ print(" βœ… Short URLs")
525
+ print(" βœ… Self-destruct messages")
526
+ print(" βœ… Multiple themes")
527
+ print(" βœ… Password hints")
528
+ print("πŸš€ Server running on http://0.0.0.0:7860")
529
+
530
+ app.run(host="0.0.0.0", port=7860, debug=True)