qrtunnel 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,93 @@
1
+ Metadata-Version: 2.4
2
+ Name: qrtunnel
3
+ Version: 0.1.0
4
+ Summary: Cross-platform file sharing via QR code with ngrok and SSH tunneling
5
+ Author-email: Ani <ani@example.com>
6
+ License: MIT
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: Programming Language :: Python :: 3.6
9
+ Classifier: Programming Language :: Python :: 3.7
10
+ Classifier: Programming Language :: Python :: 3.8
11
+ Classifier: Programming Language :: Python :: 3.9
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Requires-Python: >=3.6
17
+ Description-Content-Type: text/markdown
18
+ Requires-Dist: qrcode[pil]
19
+ Requires-Dist: pyngrok
20
+
21
+ # QRTunnel v0.1
22
+
23
+ Cross-platform file sharing via SSH reverse tunneling and QR codes. Allows sharing files with mobile devices anywhere in the world, even behind NAT/firewalls.
24
+
25
+ ## Features
26
+
27
+ * **Simple File Sharing:** Share one or more files directly from your command line.
28
+ * **Secure Tunnels:** Utilizes ngrok for secure, public HTTPS tunnels, even behind NATs and firewalls.
29
+ * **No-Auth Alternative:** For Mac/Linux users, an SSH-based tunnel (localhost.run) is available, requiring no ngrok account.
30
+ * **QR Code Display:** Generates a scannable QR code in your terminal for easy access on mobile devices.
31
+ * **Web Interface:** Provides a simple web page for recipients to download shared files, individually or as a ZIP archive.
32
+ * **Ngrok Authtoken Management:** Interactive setup and status check for your ngrok authentication token.
33
+
34
+ ## Installation
35
+
36
+ 1. **Clone the repository:**
37
+ ```bash
38
+ git clone https://github.com/your_username/qrtunnel.git
39
+ cd qrtunnel
40
+ ```
41
+ 2. **Install dependencies:**
42
+ ```bash
43
+ pip install pyngrok qrcode[pil]
44
+ ```
45
+
46
+ ## Usage
47
+
48
+ ### Basic Sharing
49
+
50
+ To share one or more files:
51
+
52
+ ```bash
53
+ python qr.py <file_path1> [<file_path2> ...]
54
+ ```
55
+
56
+ Example:
57
+ ```bash
58
+ python qr.py mydocument.pdf myimage.jpg
59
+ ```
60
+
61
+ This will start a local HTTP server, create a public tunnel (using ngrok by default), and display a QR code. Scan the QR code with your phone to access the files.
62
+
63
+ ### Ngrok Authentication Setup
64
+
65
+ `qrtunnel` uses ngrok for reliable public tunnels. The first time you use it, or if you need to update your token, you'll be prompted to set up your ngrok authtoken. You can also do this manually:
66
+
67
+ ```bash
68
+ python qr.py --setup
69
+ ```
70
+
71
+ Follow the on-screen instructions to get and save your ngrok authtoken.
72
+
73
+ ### Check Ngrok Status
74
+
75
+ To check if your ngrok authtoken is configured:
76
+
77
+ ```bash
78
+ python qr.py --status
79
+ ```
80
+
81
+ ### No-Auth Sharing (Mac/Linux Only)
82
+
83
+ If you're on Mac or Linux and prefer not to use an ngrok account, you can use the `--noauth` flag. This will attempt to create an SSH tunnel via `localhost.run`.
84
+
85
+ ```bash
86
+ python qr.py <file_path1> [<file_path2> ...] --noauth
87
+ ```
88
+
89
+ **Note:** This option is not supported on Windows.
90
+
91
+ ### Quitting the Server
92
+
93
+ The server will run until you press `q` in the terminal or use `Ctrl+C`.
@@ -0,0 +1,73 @@
1
+ # QRTunnel v0.1
2
+
3
+ Cross-platform file sharing via SSH reverse tunneling and QR codes. Allows sharing files with mobile devices anywhere in the world, even behind NAT/firewalls.
4
+
5
+ ## Features
6
+
7
+ * **Simple File Sharing:** Share one or more files directly from your command line.
8
+ * **Secure Tunnels:** Utilizes ngrok for secure, public HTTPS tunnels, even behind NATs and firewalls.
9
+ * **No-Auth Alternative:** For Mac/Linux users, an SSH-based tunnel (localhost.run) is available, requiring no ngrok account.
10
+ * **QR Code Display:** Generates a scannable QR code in your terminal for easy access on mobile devices.
11
+ * **Web Interface:** Provides a simple web page for recipients to download shared files, individually or as a ZIP archive.
12
+ * **Ngrok Authtoken Management:** Interactive setup and status check for your ngrok authentication token.
13
+
14
+ ## Installation
15
+
16
+ 1. **Clone the repository:**
17
+ ```bash
18
+ git clone https://github.com/your_username/qrtunnel.git
19
+ cd qrtunnel
20
+ ```
21
+ 2. **Install dependencies:**
22
+ ```bash
23
+ pip install pyngrok qrcode[pil]
24
+ ```
25
+
26
+ ## Usage
27
+
28
+ ### Basic Sharing
29
+
30
+ To share one or more files:
31
+
32
+ ```bash
33
+ python qr.py <file_path1> [<file_path2> ...]
34
+ ```
35
+
36
+ Example:
37
+ ```bash
38
+ python qr.py mydocument.pdf myimage.jpg
39
+ ```
40
+
41
+ This will start a local HTTP server, create a public tunnel (using ngrok by default), and display a QR code. Scan the QR code with your phone to access the files.
42
+
43
+ ### Ngrok Authentication Setup
44
+
45
+ `qrtunnel` uses ngrok for reliable public tunnels. The first time you use it, or if you need to update your token, you'll be prompted to set up your ngrok authtoken. You can also do this manually:
46
+
47
+ ```bash
48
+ python qr.py --setup
49
+ ```
50
+
51
+ Follow the on-screen instructions to get and save your ngrok authtoken.
52
+
53
+ ### Check Ngrok Status
54
+
55
+ To check if your ngrok authtoken is configured:
56
+
57
+ ```bash
58
+ python qr.py --status
59
+ ```
60
+
61
+ ### No-Auth Sharing (Mac/Linux Only)
62
+
63
+ If you're on Mac or Linux and prefer not to use an ngrok account, you can use the `--noauth` flag. This will attempt to create an SSH tunnel via `localhost.run`.
64
+
65
+ ```bash
66
+ python qr.py <file_path1> [<file_path2> ...] --noauth
67
+ ```
68
+
69
+ **Note:** This option is not supported on Windows.
70
+
71
+ ### Quitting the Server
72
+
73
+ The server will run until you press `q` in the terminal or use `Ctrl+C`.
@@ -0,0 +1,33 @@
1
+ [build-system]
2
+ requires = ["setuptools"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "qrtunnel"
7
+ version = "0.1.0"
8
+ description = "Cross-platform file sharing via QR code with ngrok and SSH tunneling"
9
+ readme = "README.md"
10
+ requires-python = ">=3.6"
11
+ license = {text = "MIT"}
12
+ authors = [
13
+ { name = "Ani", email = "ani@example.com" }
14
+ ]
15
+ classifiers = [
16
+ "Programming Language :: Python :: 3",
17
+ "Programming Language :: Python :: 3.6",
18
+ "Programming Language :: Python :: 3.7",
19
+ "Programming Language :: Python :: 3.8",
20
+ "Programming Language :: Python :: 3.9",
21
+ "Programming Language :: Python :: 3.10",
22
+ "Programming Language :: Python :: 3.11",
23
+ "Operating System :: OS Independent",
24
+ "License :: OSI Approved :: MIT License",
25
+ ]
26
+
27
+ dependencies = [
28
+ "qrcode[pil]",
29
+ "pyngrok",
30
+ ]
31
+
32
+ [project.scripts]
33
+ qrtunnel = "qr:main"
qrtunnel-0.1.0/qr.py ADDED
@@ -0,0 +1,809 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ qrtunnel: Simple cross-platform file sharing via QR code with ngrok authentication.
4
+ Usage: qrtunnel <file_path1> [<file_path2> ...]
5
+
6
+ Dependencies:
7
+ pip install pyngrok qrcode[pil]
8
+ """
9
+
10
+ import os
11
+ import sys
12
+ import threading
13
+ import time
14
+ import argparse
15
+ import platform
16
+ import subprocess
17
+ import re
18
+ import json
19
+ from http.server import HTTPServer, BaseHTTPRequestHandler
20
+ from urllib.parse import urlparse
21
+ from pathlib import Path
22
+
23
+
24
+ def getch():
25
+ """Reads a single character from stdin without echoing or requiring Enter."""
26
+
27
+ if platform.system() == 'Windows':
28
+ try:
29
+ import msvcrt
30
+ if msvcrt.kbhit():
31
+ return msvcrt.getch().decode('utf-8', errors='ignore')
32
+ return None
33
+ except:
34
+ return None
35
+ else:
36
+ try:
37
+ import select
38
+ import sys
39
+ import tty
40
+ import termios
41
+
42
+ # Check if there's input available (non-blocking)
43
+ rlist, _, _ = select.select([sys.stdin], [], [], 0)
44
+ if rlist:
45
+ fd = sys.stdin.fileno()
46
+ old_settings = termios.tcgetattr(fd)
47
+ try:
48
+ tty.setraw(sys.stdin.fileno())
49
+ ch = sys.stdin.read(1)
50
+ finally:
51
+ termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
52
+ return ch
53
+ return None
54
+ except:
55
+ return None
56
+
57
+
58
+ class Config:
59
+ """Configuration constants"""
60
+ LOCAL_PORT = 8000
61
+ CONFIG_DIR = Path.home() / ".qrtunnel"
62
+ CONFIG_FILE = CONFIG_DIR / "config.json"
63
+
64
+
65
+ class FileShareHandler(BaseHTTPRequestHandler):
66
+ """HTTP request handler for file sharing"""
67
+
68
+ file_paths = None
69
+
70
+ def log_message(self, format, *args):
71
+ """Suppress default logging"""
72
+ pass
73
+
74
+ def do_GET(self):
75
+ """Handle GET requests"""
76
+ parsed_path = urlparse(self.path)
77
+
78
+ if parsed_path.path == '/download':
79
+ self.serve_files_as_zip()
80
+ else:
81
+ self.send_download_page()
82
+
83
+ def send_download_page(self):
84
+ """Send HTML page with a download button"""
85
+ file_list_html = "".join(f"<li>{os.path.basename(p)}</li>" for p in self.file_paths)
86
+
87
+ html = f"""<!DOCTYPE html>
88
+ <html>
89
+ <head>
90
+ <meta charset="UTF-8">
91
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
92
+ <title>qrtunnel - File Download</title>
93
+ <style>
94
+ * {{ margin: 0; padding: 0; box-sizing: border-box; }}
95
+ body {{
96
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
97
+ background: #1a1a2e;
98
+ min-height: 100vh;
99
+ display: flex;
100
+ align-items: center;
101
+ justify-content: center;
102
+ padding: 20px;
103
+ color: #eee;
104
+ }}
105
+ .container {{
106
+ background: #16213e;
107
+ border-radius: 8px;
108
+ padding: 40px;
109
+ box-shadow: 0 4px 20px rgba(0,0,0,0.4);
110
+ max-width: 480px;
111
+ width: 100%;
112
+ }}
113
+ .header {{
114
+ text-align: center;
115
+ margin-bottom: 32px;
116
+ padding-bottom: 24px;
117
+ border-bottom: 1px solid #2a3a5e;
118
+ }}
119
+ h1 {{
120
+ font-size: 24px;
121
+ font-weight: 600;
122
+ margin-bottom: 8px;
123
+ color: #fff;
124
+ }}
125
+ .subtitle {{
126
+ font-size: 14px;
127
+ color: #888;
128
+ }}
129
+ .file-section {{
130
+ margin-bottom: 32px;
131
+ }}
132
+ .file-section-title {{
133
+ font-size: 12px;
134
+ text-transform: uppercase;
135
+ letter-spacing: 1px;
136
+ color: #666;
137
+ margin-bottom: 12px;
138
+ }}
139
+ .file-list {{
140
+ list-style: none;
141
+ background: #0f0f1a;
142
+ border-radius: 6px;
143
+ border: 1px solid #2a3a5e;
144
+ }}
145
+ .file-list li {{
146
+ padding: 12px 16px;
147
+ font-size: 14px;
148
+ font-family: 'SF Mono', 'Consolas', monospace;
149
+ border-bottom: 1px solid #2a3a5e;
150
+ color: #ccc;
151
+ }}
152
+ .file-list li:last-child {{
153
+ border-bottom: none;
154
+ }}
155
+ .download-button {{
156
+ display: block;
157
+ width: 100%;
158
+ padding: 16px 24px;
159
+ background: #4361ee;
160
+ color: #fff;
161
+ border: none;
162
+ border-radius: 6px;
163
+ font-size: 15px;
164
+ font-weight: 500;
165
+ cursor: pointer;
166
+ text-decoration: none;
167
+ text-align: center;
168
+ transition: background 0.2s ease;
169
+ }}
170
+ .download-button:hover {{
171
+ background: #3a56d4;
172
+ }}
173
+ .download-button:active {{
174
+ background: #324bc2;
175
+ }}
176
+ .footer {{
177
+ text-align: center;
178
+ margin-top: 24px;
179
+ font-size: 12px;
180
+ color: #555;
181
+ }}
182
+ </style>
183
+ </head>
184
+ <body>
185
+ <div class="container">
186
+ <div class="header">
187
+ <h1>Files Ready</h1>
188
+ <p class="subtitle">Download as ZIP archive</p>
189
+ </div>
190
+ <div class="file-section">
191
+ <p class="file-section-title">Files ({len(self.file_paths)})</p>
192
+ <ul class="file-list">
193
+ {file_list_html}
194
+ </ul>
195
+ </div>
196
+ <a href="/download" class="download-button">Download All</a>
197
+ <p class="footer">qrtunnel</p>
198
+ </div>
199
+ </body>
200
+ </html>"""
201
+
202
+ self.send_response(200)
203
+ self.send_header('Content-type', 'text/html')
204
+ self.send_header('Content-Length', len(html.encode()))
205
+ self.end_headers()
206
+ self.wfile.write(html.encode())
207
+
208
+ def serve_files_as_zip(self):
209
+ """Create a ZIP archive of all files and serve it"""
210
+ try:
211
+ import zipfile
212
+ from io import BytesIO
213
+
214
+ zip_buffer = BytesIO()
215
+ with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zipf:
216
+ for file_path in self.file_paths:
217
+ zipf.write(file_path, os.path.basename(file_path))
218
+
219
+ zip_buffer.seek(0)
220
+ zip_data = zip_buffer.getvalue()
221
+
222
+ self.send_response(200)
223
+ self.send_header('Content-type', 'application/zip')
224
+ self.send_header('Content-Disposition', 'attachment; filename="files.zip"')
225
+ self.send_header('Content-Length', len(zip_data))
226
+ self.end_headers()
227
+ self.wfile.write(zip_data)
228
+
229
+ print(f"✓ Files served as ZIP to {self.client_address[0]}")
230
+ except Exception as e:
231
+ print(f"✗ Error creating or serving ZIP file: {e}")
232
+ self.send_error(500, "Internal server error")
233
+
234
+
235
+ class NgrokAuth:
236
+ """Manages ngrok authentication"""
237
+
238
+ def __init__(self):
239
+ self.config_dir = Config.CONFIG_DIR
240
+ self.config_file = Config.CONFIG_FILE
241
+
242
+ def ensure_config_dir(self):
243
+ """Create config directory if it doesn't exist"""
244
+ self.config_dir.mkdir(parents=True, exist_ok=True)
245
+
246
+ def load_config(self):
247
+ """Load configuration from file"""
248
+ if self.config_file.exists():
249
+ try:
250
+ with open(self.config_file, 'r') as f:
251
+ return json.load(f)
252
+ except:
253
+ return {}
254
+ return {}
255
+
256
+ def save_config(self, config):
257
+ """Save configuration to file"""
258
+ self.ensure_config_dir()
259
+ with open(self.config_file, 'w') as f:
260
+ json.dump(config, f, indent=2)
261
+
262
+ def get_authtoken(self):
263
+ """Get ngrok authtoken from config"""
264
+ config = self.load_config()
265
+ return config.get('ngrok_authtoken')
266
+
267
+ def save_authtoken(self, token):
268
+ """Save ngrok authtoken to config"""
269
+ config = self.load_config()
270
+ config['ngrok_authtoken'] = token
271
+ self.save_config(config)
272
+
273
+ def setup_ngrok_account(self):
274
+ """Interactive setup for ngrok account"""
275
+ print("\n" + "="*60)
276
+ print("NGROK ACCOUNT SETUP")
277
+ print("="*60)
278
+ print("\nNgrok is a reliable tunneling service that works on all platforms.")
279
+ print("\n🔑 To get your ngrok authtoken:")
280
+ print(" 1. Visit: https://dashboard.ngrok.com/signup")
281
+ print(" 2. Sign up for a FREE account (email required)")
282
+ print(" 3. Copy your authtoken from: https://dashboard.ngrok.com/get-started/your-authtoken")
283
+
284
+ # Show no-auth alternative on Mac/Linux
285
+ if platform.system() != 'Windows':
286
+ print("\n" + "-"*60)
287
+ print("💡 ALTERNATIVE: No Sign-up Required!")
288
+ print("-"*60)
289
+ print("If you don't want to sign up for ngrok, you can use the")
290
+ print("--noauth flag which uses SSH tunneling (localhost.run):")
291
+ print("\n qrtunnel <files> --noauth")
292
+ print("\nThis requires no authentication or sign-up!")
293
+ print("-"*60)
294
+
295
+ print("\n" + "="*60)
296
+
297
+ choice = input("\nDo you have an ngrok authtoken? (y/n): ").strip().lower()
298
+
299
+ if choice == 'y':
300
+ print("\n📋 Paste your ngrok authtoken below:")
301
+ authtoken = input("Authtoken: ").strip()
302
+
303
+ if authtoken and len(authtoken) > 20:
304
+ self.save_authtoken(authtoken)
305
+ print("\n✓ Authtoken saved successfully!")
306
+ print(f" Config location: {self.config_file}")
307
+ return authtoken
308
+ else:
309
+ print("\n✗ Invalid authtoken. Please try again.")
310
+ return None
311
+ else:
312
+ print("\n[OPTIONS]:")
313
+ print(" 1. Sign up at: https://dashboard.ngrok.com/signup")
314
+ print(" 2. Run 'qrtunnel --setup' after you get your authtoken")
315
+ if platform.system() != 'Windows':
316
+ print(" 3. OR use: qrtunnel <files> --noauth (no sign-up needed!)")
317
+ return None
318
+
319
+ def verify_token(self, token):
320
+ """Verify ngrok token by attempting to set it"""
321
+ try:
322
+ from pyngrok import ngrok, conf
323
+ ngrok.set_auth_token(token)
324
+ return True
325
+ except Exception as e:
326
+ print(f"✗ Token verification failed: {e}")
327
+ return False
328
+
329
+
330
+ class NgrokTunnel:
331
+ """Ngrok tunnel with authentication"""
332
+
333
+ def __init__(self, local_port, auth_manager):
334
+ self.local_port = local_port
335
+ self.auth_manager = auth_manager
336
+ self.public_url = None
337
+ self.tunnel = None
338
+ self.name = "ngrok"
339
+
340
+ def start(self):
341
+ """Start ngrok tunnel with authentication"""
342
+ try:
343
+ from pyngrok import ngrok, conf
344
+
345
+ print(f"\n[*] Starting ngrok tunnel...")
346
+
347
+ # Get authtoken
348
+ authtoken = self.auth_manager.get_authtoken()
349
+
350
+ if not authtoken:
351
+ print("[!] No ngrok authtoken found")
352
+
353
+ # Show helpful message about --noauth alternative
354
+ if platform.system() != 'Windows':
355
+ print("\n" + "="*60)
356
+ print("💡 TIP: You can skip ngrok sign-up!")
357
+ print("="*60)
358
+ print("\nRestart the program with --noauth flag to use SSH tunneling")
359
+ print("(localhost.run) which requires NO authentication or sign-up:")
360
+ print("\n qrtunnel <your_files> --noauth")
361
+ print("\nOr continue below to set up ngrok (requires free account).")
362
+ print("="*60 + "\n")
363
+
364
+ authtoken = self.auth_manager.setup_ngrok_account()
365
+
366
+ if not authtoken:
367
+ print("[!] Cannot start ngrok without authtoken")
368
+ if platform.system() != 'Windows':
369
+ print("\n💡 Remember: You can use --noauth to skip authentication!")
370
+ print(" Example: qrtunnel myfile.pdf --noauth")
371
+ return False
372
+
373
+ # Set authtoken
374
+ try:
375
+ ngrok.set_auth_token(authtoken)
376
+ except Exception as e:
377
+ print(f"[!] Error setting authtoken: {e}")
378
+ print("[*] Your saved token might be invalid. Let's set it up again.")
379
+ authtoken = self.auth_manager.setup_ngrok_account()
380
+ if not authtoken:
381
+ return False
382
+ ngrok.set_auth_token(authtoken)
383
+
384
+ # Configure ngrok
385
+ conf.get_default().log_level = "ERROR"
386
+
387
+ # Start tunnel
388
+ print("[*] Establishing tunnel...")
389
+ self.tunnel = ngrok.connect(self.local_port, bind_tls=True)
390
+ self.public_url = self.tunnel.public_url
391
+
392
+ # Ensure HTTPS
393
+ if self.public_url.startswith('http://'):
394
+ self.public_url = self.public_url.replace('http://', 'https://')
395
+
396
+ print(f"✓ Tunnel established: {self.public_url}")
397
+ return True
398
+
399
+ except ImportError:
400
+ print("✗ Error: pyngrok is not installed")
401
+ print(" Install with: pip install pyngrok")
402
+ return False
403
+ except Exception as e:
404
+ error_msg = str(e).lower()
405
+
406
+ if 'authtoken' in error_msg or 'unauthorized' in error_msg or 'invalid' in error_msg:
407
+ print(f"✗ Authentication error: {e}")
408
+ print("\n[*] Your authtoken might be invalid or expired.")
409
+
410
+ # Show --noauth alternative
411
+ if platform.system() != 'Windows':
412
+ print("\n" + "="*60)
413
+ print("💡 ALTERNATIVE: Skip authentication entirely!")
414
+ print("="*60)
415
+ print("\nYou can restart with --noauth to use SSH tunneling:")
416
+ print("\n qrtunnel <your_files> --noauth")
417
+ print("\nNo sign-up or authentication required!")
418
+ print("="*60)
419
+
420
+ print("\n[*] Or let's set up your ngrok authtoken again...")
421
+ authtoken = self.auth_manager.setup_ngrok_account()
422
+ if authtoken:
423
+ # Try one more time with new token
424
+ try:
425
+ from pyngrok import ngrok
426
+ ngrok.set_auth_token(authtoken)
427
+ self.tunnel = ngrok.connect(self.local_port, bind_tls=True)
428
+ self.public_url = self.tunnel.public_url
429
+ if self.public_url.startswith('http://'):
430
+ self.public_url = self.public_url.replace('http://', 'https://')
431
+ print(f"✓ Tunnel established: {self.public_url}")
432
+ return True
433
+ except Exception as retry_error:
434
+ print(f"✗ Still failed: {retry_error}")
435
+ if platform.system() != 'Windows':
436
+ print("\n💡 Try restarting with: qrtunnel <files> --noauth")
437
+ return False
438
+ return False
439
+ else:
440
+ print(f"✗ Error starting ngrok: {e}")
441
+ return False
442
+
443
+ def stop(self):
444
+ """Stop ngrok tunnel"""
445
+ if self.tunnel:
446
+ try:
447
+ from pyngrok import ngrok
448
+ ngrok.disconnect(self.tunnel.public_url)
449
+ print("\n[*] Ngrok tunnel closed")
450
+ except:
451
+ pass
452
+
453
+
454
+ class SSHTunnel:
455
+ """SSH-based tunnel (localhost.run - no auth required)"""
456
+
457
+ def __init__(self, local_port):
458
+ self.local_port = local_port
459
+ self.process = None
460
+ self.public_url = None
461
+ self.name = "localhost.run"
462
+ self.output_thread = None
463
+ self.url_found = threading.Event()
464
+
465
+ def check_ssh(self):
466
+ """Check if SSH is available"""
467
+ try:
468
+ subprocess.run(['ssh', '-V'], capture_output=True, timeout=2)
469
+ return True
470
+ except:
471
+ return False
472
+
473
+ def _read_output(self):
474
+ """Read output from SSH process in background thread"""
475
+ url_pattern = re.compile(r'https://[a-zA-Z0-9.-]+\.lhr\.life')
476
+
477
+ try:
478
+ while self.process and self.process.poll() is None:
479
+ line = self.process.stdout.readline()
480
+ if not line:
481
+ time.sleep(0.1)
482
+ continue
483
+
484
+ line = line.strip()
485
+ if line:
486
+ # Look for URL in the output
487
+ match = url_pattern.search(line)
488
+ if match and not self.public_url:
489
+ self.public_url = match.group(0)
490
+ self.url_found.set()
491
+ except:
492
+ pass
493
+
494
+ def start(self):
495
+ """Start SSH tunnel"""
496
+ if not self.check_ssh():
497
+ print(f"[!] SSH not available, skipping {self.name}")
498
+ return False
499
+
500
+ print(f"[*] Trying {self.name} (no auth required)...")
501
+
502
+ cmd = [
503
+ 'ssh',
504
+ '-o', 'StrictHostKeyChecking=no',
505
+ '-o', 'UserKnownHostsFile=/dev/null',
506
+ '-o', 'ServerAliveInterval=60',
507
+ '-o', 'ConnectTimeout=15',
508
+ '-o', 'LogLevel=ERROR',
509
+ '-T',
510
+ '-R', f'80:localhost:{self.local_port}',
511
+ 'nokey@localhost.run'
512
+ ]
513
+
514
+ try:
515
+ self.process = subprocess.Popen(
516
+ cmd,
517
+ stdout=subprocess.PIPE,
518
+ stderr=subprocess.STDOUT,
519
+ stdin=subprocess.PIPE,
520
+ text=True,
521
+ bufsize=1
522
+ )
523
+
524
+ # Start background thread to read output
525
+ self.output_thread = threading.Thread(target=self._read_output, daemon=True)
526
+ self.output_thread.start()
527
+
528
+ # Wait for URL with timeout
529
+ if self.url_found.wait(timeout=20):
530
+ print(f"✓ Connected via {self.name}: {self.public_url}")
531
+ return True
532
+ else:
533
+ print(f"[!] {self.name} timeout - no URL received")
534
+ self.stop()
535
+ return False
536
+
537
+ except Exception as e:
538
+ print(f"[!] {self.name} error: {e}")
539
+ self.stop()
540
+ return False
541
+
542
+ def stop(self):
543
+ """Stop SSH tunnel"""
544
+ if self.process:
545
+ try:
546
+ self.process.terminate()
547
+ self.process.wait(timeout=3)
548
+ except:
549
+ try:
550
+ self.process.kill()
551
+ except:
552
+ pass
553
+ self.process = None
554
+ print("\n[*] SSH tunnel closed")
555
+
556
+
557
+ class TunnelManager:
558
+ """Manages tunnel services"""
559
+
560
+ def __init__(self, local_port, noauth=False):
561
+ self.local_port = local_port
562
+ self.active_tunnel = None
563
+ self.public_url = None
564
+ self.auth_manager = NgrokAuth()
565
+ self.noauth = noauth
566
+
567
+ def start(self):
568
+ """Start tunnel based on mode"""
569
+ print("\n" + "="*60)
570
+ print("ESTABLISHING PUBLIC TUNNEL")
571
+ print("="*60)
572
+
573
+ if self.noauth:
574
+ # Try SSH tunnel first (localhost.run)
575
+ ssh_tunnel = SSHTunnel(self.local_port)
576
+ if ssh_tunnel.start():
577
+ self.active_tunnel = ssh_tunnel
578
+ self.public_url = ssh_tunnel.public_url
579
+ print("="*60)
580
+ return True
581
+ else:
582
+ print("\n[!] No-auth SSH tunnel failed. Falling back to ngrok...")
583
+ print("="*60)
584
+
585
+ # Use ngrok (default or fallback)
586
+ ngrok_tunnel = NgrokTunnel(self.local_port, self.auth_manager)
587
+ if ngrok_tunnel.start():
588
+ self.active_tunnel = ngrok_tunnel
589
+ self.public_url = ngrok_tunnel.public_url
590
+ print("="*60)
591
+ return True
592
+
593
+ print("="*60)
594
+ print("\n✗ All tunnel services failed")
595
+ print("\n[SOLUTIONS]:")
596
+ if platform.system() != 'Windows':
597
+ print(" 1. 🚀 EASIEST: Restart with --noauth (no sign-up required!)")
598
+ print(" Example: qrtunnel <your_files> --noauth")
599
+ print()
600
+ print(" 2. Make sure you have a valid ngrok authtoken")
601
+ print(" 3. Run: qrtunnel --setup (to configure ngrok)")
602
+ print(" 4. Check your internet connection")
603
+ print(" 5. Check your firewall settings")
604
+ return False
605
+
606
+ def stop(self):
607
+ """Stop active tunnel"""
608
+ if self.active_tunnel:
609
+ self.active_tunnel.stop()
610
+
611
+
612
+ def generate_qr_code(url):
613
+ """Generate and display QR code in terminal"""
614
+ try:
615
+ import qrcode
616
+
617
+ qr = qrcode.QRCode(
618
+ version=1,
619
+ error_correction=qrcode.constants.ERROR_CORRECT_L,
620
+ box_size=1,
621
+ border=2,
622
+ )
623
+ qr.add_data(url)
624
+ qr.make(fit=True)
625
+
626
+ print("\n" + "="*60)
627
+ print("SCAN THIS QR CODE TO ACCESS THE FILES:")
628
+ print("="*60)
629
+ qr.print_ascii(invert=True)
630
+ print("="*60)
631
+ print(f"\n🌐 URL: {url}")
632
+ print("="*60 + "\n")
633
+
634
+ except ImportError:
635
+ print("\n" + "="*60)
636
+ print("⚠️ QR code library not installed")
637
+ print("Install with: pip install qrcode[pil]")
638
+ print("="*60)
639
+ print(f"\n🌐 URL: {url}")
640
+ print("="*60 + "\n")
641
+
642
+
643
+ def format_size(bytes):
644
+ """Format bytes to human-readable size"""
645
+ for unit in ['B', 'KB', 'MB', 'GB']:
646
+ if bytes < 1024.0:
647
+ return f"{bytes:.1f} {unit}"
648
+ bytes /= 1024.0
649
+ return f"{bytes:.1f} TB"
650
+
651
+
652
+ def main():
653
+ """Main function"""
654
+ parser = argparse.ArgumentParser(
655
+ description="qrtunnel: Simple cross-platform file sharing via QR code with ngrok.",
656
+ usage="qrtunnel <file_path1> [<file_path2> ...] [options]"
657
+ )
658
+ parser.add_argument('file_paths', nargs='*', help='One or more paths to the files you want to share.')
659
+ parser.add_argument('--setup', action='store_true', help='Set up or reconfigure ngrok authtoken')
660
+ parser.add_argument('--status', action='store_true', help='Check authentication status')
661
+ parser.add_argument('--noauth', action='store_true', help='Use SSH tunnel (localhost.run) without authentication (Mac/Linux only)')
662
+
663
+ args = parser.parse_args()
664
+
665
+ # Handle setup mode
666
+ if args.setup:
667
+ auth = NgrokAuth()
668
+ token = auth.setup_ngrok_account()
669
+ if token:
670
+ print("\n✓ Setup complete! You can now use qrtunnel to share files.")
671
+ else:
672
+ print("\n✗ Setup incomplete. Please try again.")
673
+ sys.exit(0 if token else 1)
674
+
675
+ # Handle status check
676
+ if args.status:
677
+ auth = NgrokAuth()
678
+ token = auth.get_authtoken()
679
+ print("\n" + "="*60)
680
+ print("AUTHENTICATION STATUS")
681
+ print("="*60)
682
+ if token:
683
+ masked_token = token[:8] + "..." + token[-4:] if len(token) > 12 else "***"
684
+ print(f"✓ Ngrok authtoken found: {masked_token}")
685
+ print(f" Config location: {auth.config_file}")
686
+ else:
687
+ print("✗ No ngrok authtoken configured")
688
+ print("\nTo set up ngrok:")
689
+ print(" 1. Run: qrtunnel --setup")
690
+ print(" 2. Or visit: https://dashboard.ngrok.com/get-started/your-authtoken")
691
+ print("="*60 + "\n")
692
+ sys.exit(0 if token else 1)
693
+
694
+ # Handle --noauth on Windows
695
+ noauth_mode = args.noauth
696
+ if noauth_mode and platform.system() == 'Windows':
697
+ print("\n" + "="*60)
698
+ print("⚠️ WARNING: --noauth is not supported on Windows")
699
+ print("="*60)
700
+ print("\nThe --noauth option uses SSH tunneling via localhost.run,")
701
+ print("which is not reliably supported on Windows.")
702
+ print("\nProceeding with ngrok instead...")
703
+ print("="*60)
704
+ noauth_mode = False
705
+
706
+ # Validate that we have files to share
707
+ if not args.file_paths:
708
+ parser.print_help()
709
+ sys.exit(1)
710
+
711
+ file_paths = args.file_paths
712
+
713
+ # Validate files
714
+ for file_path in file_paths:
715
+ if not os.path.exists(file_path):
716
+ print(f"✗ Error: File '{file_path}' not found")
717
+ sys.exit(1)
718
+
719
+ if not os.path.isfile(file_path):
720
+ print(f"✗ Error: '{file_path}' is not a file")
721
+ sys.exit(1)
722
+
723
+ # Display banner
724
+ print("\n" + "="*60)
725
+ print("qrtunnel - Simple File Sharing")
726
+ print(f"Platform: {platform.system()} {platform.release()}")
727
+ if noauth_mode:
728
+ print("Mode: No-auth (SSH tunnel via localhost.run)")
729
+ else:
730
+ print("Mode: ngrok (authenticated)")
731
+ print("="*60)
732
+ print("Files to be shared:")
733
+ for file_path in file_paths:
734
+ size = os.path.getsize(file_path)
735
+ print(f" - {os.path.basename(file_path)} ({format_size(size)})")
736
+ print("="*60)
737
+
738
+ # Set up handler with file paths
739
+ FileShareHandler.file_paths = file_paths
740
+
741
+ # Start HTTP server
742
+ try:
743
+ server = HTTPServer(('localhost', Config.LOCAL_PORT), FileShareHandler)
744
+ except OSError as e:
745
+ print(f"\n✗ Error: Could not bind to port {Config.LOCAL_PORT}")
746
+ print(f" {e}")
747
+ sys.exit(1)
748
+
749
+ server_thread = threading.Thread(target=server.serve_forever, daemon=True)
750
+ server_thread.start()
751
+ print(f"\n✓ HTTP server started on localhost:{Config.LOCAL_PORT}")
752
+
753
+ # Start tunnel
754
+ tunnel_manager = TunnelManager(Config.LOCAL_PORT, noauth=noauth_mode)
755
+
756
+ if not tunnel_manager.start():
757
+ server.shutdown()
758
+ sys.exit(1)
759
+
760
+ # Generate and display QR code
761
+ generate_qr_code(tunnel_manager.public_url)
762
+
763
+ print("[*] Server is running. Press 'q' to quit, or Ctrl+C to stop.\n")
764
+
765
+ # Set terminal to raw mode for immediate character reading
766
+ if platform.system() != 'Windows':
767
+ import tty
768
+ import termios
769
+ fd = sys.stdin.fileno()
770
+ old_settings = termios.tcgetattr(fd)
771
+ try:
772
+ tty.setcbreak(fd) # Use cbreak mode instead of raw
773
+
774
+ try:
775
+ while True:
776
+ import select
777
+ if select.select([sys.stdin], [], [], 0.1)[0]:
778
+ char = sys.stdin.read(1)
779
+ if char and char.lower() == 'q':
780
+ print("\n[*] 'q' pressed. Shutting down...")
781
+ break
782
+ except KeyboardInterrupt:
783
+ print("\n[*] Ctrl+C pressed. Shutting down...")
784
+ finally:
785
+ termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
786
+ tunnel_manager.stop()
787
+ server.shutdown()
788
+ print("[*] Server stopped. Goodbye!")
789
+ else:
790
+ # Windows version
791
+ import msvcrt
792
+ try:
793
+ while True:
794
+ if msvcrt.kbhit():
795
+ char = msvcrt.getch().decode('utf-8', errors='ignore')
796
+ if char and char.lower() == 'q':
797
+ print("\n[*] 'q' pressed. Shutting down...")
798
+ break
799
+ time.sleep(0.1)
800
+ except KeyboardInterrupt:
801
+ print("\n[*] Ctrl+C pressed. Shutting down...")
802
+ finally:
803
+ tunnel_manager.stop()
804
+ server.shutdown()
805
+ print("[*] Server stopped. Goodbye!")
806
+
807
+
808
+ if __name__ == '__main__':
809
+ main()
@@ -0,0 +1,93 @@
1
+ Metadata-Version: 2.4
2
+ Name: qrtunnel
3
+ Version: 0.1.0
4
+ Summary: Cross-platform file sharing via QR code with ngrok and SSH tunneling
5
+ Author-email: Ani <ani@example.com>
6
+ License: MIT
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: Programming Language :: Python :: 3.6
9
+ Classifier: Programming Language :: Python :: 3.7
10
+ Classifier: Programming Language :: Python :: 3.8
11
+ Classifier: Programming Language :: Python :: 3.9
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Requires-Python: >=3.6
17
+ Description-Content-Type: text/markdown
18
+ Requires-Dist: qrcode[pil]
19
+ Requires-Dist: pyngrok
20
+
21
+ # QRTunnel v0.1
22
+
23
+ Cross-platform file sharing via SSH reverse tunneling and QR codes. Allows sharing files with mobile devices anywhere in the world, even behind NAT/firewalls.
24
+
25
+ ## Features
26
+
27
+ * **Simple File Sharing:** Share one or more files directly from your command line.
28
+ * **Secure Tunnels:** Utilizes ngrok for secure, public HTTPS tunnels, even behind NATs and firewalls.
29
+ * **No-Auth Alternative:** For Mac/Linux users, an SSH-based tunnel (localhost.run) is available, requiring no ngrok account.
30
+ * **QR Code Display:** Generates a scannable QR code in your terminal for easy access on mobile devices.
31
+ * **Web Interface:** Provides a simple web page for recipients to download shared files, individually or as a ZIP archive.
32
+ * **Ngrok Authtoken Management:** Interactive setup and status check for your ngrok authentication token.
33
+
34
+ ## Installation
35
+
36
+ 1. **Clone the repository:**
37
+ ```bash
38
+ git clone https://github.com/your_username/qrtunnel.git
39
+ cd qrtunnel
40
+ ```
41
+ 2. **Install dependencies:**
42
+ ```bash
43
+ pip install pyngrok qrcode[pil]
44
+ ```
45
+
46
+ ## Usage
47
+
48
+ ### Basic Sharing
49
+
50
+ To share one or more files:
51
+
52
+ ```bash
53
+ python qr.py <file_path1> [<file_path2> ...]
54
+ ```
55
+
56
+ Example:
57
+ ```bash
58
+ python qr.py mydocument.pdf myimage.jpg
59
+ ```
60
+
61
+ This will start a local HTTP server, create a public tunnel (using ngrok by default), and display a QR code. Scan the QR code with your phone to access the files.
62
+
63
+ ### Ngrok Authentication Setup
64
+
65
+ `qrtunnel` uses ngrok for reliable public tunnels. The first time you use it, or if you need to update your token, you'll be prompted to set up your ngrok authtoken. You can also do this manually:
66
+
67
+ ```bash
68
+ python qr.py --setup
69
+ ```
70
+
71
+ Follow the on-screen instructions to get and save your ngrok authtoken.
72
+
73
+ ### Check Ngrok Status
74
+
75
+ To check if your ngrok authtoken is configured:
76
+
77
+ ```bash
78
+ python qr.py --status
79
+ ```
80
+
81
+ ### No-Auth Sharing (Mac/Linux Only)
82
+
83
+ If you're on Mac or Linux and prefer not to use an ngrok account, you can use the `--noauth` flag. This will attempt to create an SSH tunnel via `localhost.run`.
84
+
85
+ ```bash
86
+ python qr.py <file_path1> [<file_path2> ...] --noauth
87
+ ```
88
+
89
+ **Note:** This option is not supported on Windows.
90
+
91
+ ### Quitting the Server
92
+
93
+ The server will run until you press `q` in the terminal or use `Ctrl+C`.
@@ -0,0 +1,9 @@
1
+ README.md
2
+ pyproject.toml
3
+ qr.py
4
+ qrtunnel.egg-info/PKG-INFO
5
+ qrtunnel.egg-info/SOURCES.txt
6
+ qrtunnel.egg-info/dependency_links.txt
7
+ qrtunnel.egg-info/entry_points.txt
8
+ qrtunnel.egg-info/requires.txt
9
+ qrtunnel.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ qrtunnel = qr:main
@@ -0,0 +1,2 @@
1
+ qrcode[pil]
2
+ pyngrok
@@ -0,0 +1 @@
1
+ qr
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+