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.
- qrtunnel-0.1.0/PKG-INFO +93 -0
- qrtunnel-0.1.0/README.md +73 -0
- qrtunnel-0.1.0/pyproject.toml +33 -0
- qrtunnel-0.1.0/qr.py +809 -0
- qrtunnel-0.1.0/qrtunnel.egg-info/PKG-INFO +93 -0
- qrtunnel-0.1.0/qrtunnel.egg-info/SOURCES.txt +9 -0
- qrtunnel-0.1.0/qrtunnel.egg-info/dependency_links.txt +1 -0
- qrtunnel-0.1.0/qrtunnel.egg-info/entry_points.txt +2 -0
- qrtunnel-0.1.0/qrtunnel.egg-info/requires.txt +2 -0
- qrtunnel-0.1.0/qrtunnel.egg-info/top_level.txt +1 -0
- qrtunnel-0.1.0/setup.cfg +4 -0
qrtunnel-0.1.0/PKG-INFO
ADDED
|
@@ -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`.
|
qrtunnel-0.1.0/README.md
ADDED
|
@@ -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 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
qr
|
qrtunnel-0.1.0/setup.cfg
ADDED