prosoon-pf 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.
- prosoon_pf-0.1.0/PKG-INFO +57 -0
- prosoon_pf-0.1.0/README.md +43 -0
- prosoon_pf-0.1.0/pyproject.toml +30 -0
- prosoon_pf-0.1.0/setup.cfg +4 -0
- prosoon_pf-0.1.0/src/prosoon_pf/__init__.py +3 -0
- prosoon_pf-0.1.0/src/prosoon_pf/cli.py +240 -0
- prosoon_pf-0.1.0/src/prosoon_pf.egg-info/PKG-INFO +57 -0
- prosoon_pf-0.1.0/src/prosoon_pf.egg-info/SOURCES.txt +9 -0
- prosoon_pf-0.1.0/src/prosoon_pf.egg-info/dependency_links.txt +1 -0
- prosoon_pf-0.1.0/src/prosoon_pf.egg-info/entry_points.txt +2 -0
- prosoon_pf-0.1.0/src/prosoon_pf.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: prosoon-pf
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Quick-start a local HTTP server and optionally expose it via a Cloudflare tunnel.
|
|
5
|
+
Author: Prasun Banerjee
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: http,server,cloudflare,tunnel,share,cli
|
|
8
|
+
Classifier: Environment :: Console
|
|
9
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Requires-Python: >=3.8
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
# prosoon-pf
|
|
16
|
+
|
|
17
|
+
Quick-start a local HTTP server, and optionally expose it to the internet with a
|
|
18
|
+
Cloudflare quick tunnel — in one command. Linux only for now.
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pip install prosoon-pf
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Usage
|
|
27
|
+
|
|
28
|
+
Serve the current directory locally on port 8000:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
prosoon-pf
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Pick a port and a directory:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
prosoon-pf 9000 --dir ~/share
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Expose it publicly (prints a `https://….trycloudflare.com` link):
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
prosoon-pf 8000 --public
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
If `cloudflared` isn't installed, `--public` mode will ask before installing it.
|
|
47
|
+
|
|
48
|
+
## Public mode is public
|
|
49
|
+
|
|
50
|
+
When you use `--public`, **every file in the served directory becomes
|
|
51
|
+
downloadable by anyone with the link** for as long as the command runs. The tool
|
|
52
|
+
shows you the directory contents and asks for confirmation first. Don't serve a
|
|
53
|
+
folder that contains passwords, keys, `.ovpn` files, or anything private.
|
|
54
|
+
|
|
55
|
+
## License
|
|
56
|
+
|
|
57
|
+
MIT
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# prosoon-pf
|
|
2
|
+
|
|
3
|
+
Quick-start a local HTTP server, and optionally expose it to the internet with a
|
|
4
|
+
Cloudflare quick tunnel — in one command. Linux only for now.
|
|
5
|
+
|
|
6
|
+
## Install
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
pip install prosoon-pf
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Usage
|
|
13
|
+
|
|
14
|
+
Serve the current directory locally on port 8000:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
prosoon-pf
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Pick a port and a directory:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
prosoon-pf 9000 --dir ~/share
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Expose it publicly (prints a `https://….trycloudflare.com` link):
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
prosoon-pf 8000 --public
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
If `cloudflared` isn't installed, `--public` mode will ask before installing it.
|
|
33
|
+
|
|
34
|
+
## Public mode is public
|
|
35
|
+
|
|
36
|
+
When you use `--public`, **every file in the served directory becomes
|
|
37
|
+
downloadable by anyone with the link** for as long as the command runs. The tool
|
|
38
|
+
shows you the directory contents and asks for confirmation first. Don't serve a
|
|
39
|
+
folder that contains passwords, keys, `.ovpn` files, or anything private.
|
|
40
|
+
|
|
41
|
+
## License
|
|
42
|
+
|
|
43
|
+
MIT
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "prosoon-pf"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Quick-start a local HTTP server and optionally expose it via a Cloudflare tunnel."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "Prasun Banerjee" }]
|
|
13
|
+
keywords = ["http", "server", "cloudflare", "tunnel", "share", "cli"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Environment :: Console",
|
|
16
|
+
"Operating System :: POSIX :: Linux",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
[project.urls]
|
|
22
|
+
# Homepage = "N/A"
|
|
23
|
+
|
|
24
|
+
# This is what makes `prosoon-pf` a runnable command after install.
|
|
25
|
+
# Format: command-name = "import.path:function"
|
|
26
|
+
[project.scripts]
|
|
27
|
+
prosoon-pf = "prosoon_pf.cli:main"
|
|
28
|
+
|
|
29
|
+
[tool.setuptools.packages.find]
|
|
30
|
+
where = ["src"]
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
"""
|
|
2
|
+
prosoon-pf — quick-start an HTTP server and optionally expose it publicly.
|
|
3
|
+
|
|
4
|
+
What it automates (the commands you'd otherwise type by hand):
|
|
5
|
+
1. python3 -m http.server <port>
|
|
6
|
+
2. ensure cloudflared is installed (check -> ask consent -> install)
|
|
7
|
+
3. cloudflared tunnel --url http://localhost:<port>
|
|
8
|
+
|
|
9
|
+
Design notes for future-you:
|
|
10
|
+
- We use subprocess to run the real programs rather than reimplementing
|
|
11
|
+
them. http.server and cloudflared already exist and are battle-tested;
|
|
12
|
+
our job is just orchestration.
|
|
13
|
+
- We never run a privileged install without asking. Auto-sudo from a pip
|
|
14
|
+
package is exactly what makes a package look malicious.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import argparse
|
|
18
|
+
import http.server
|
|
19
|
+
import os
|
|
20
|
+
import re
|
|
21
|
+
import shutil
|
|
22
|
+
import signal
|
|
23
|
+
import socketserver
|
|
24
|
+
import subprocess
|
|
25
|
+
import sys
|
|
26
|
+
import threading
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
|
|
29
|
+
from . import __version__
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# ----------------------------------------------------------------------------
|
|
33
|
+
# Step 0: environment checks
|
|
34
|
+
# ----------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
def check_python_version(minimum=(3, 8)):
|
|
37
|
+
"""Refuse to run on ancient Python. We rely on f-strings and pathlib,
|
|
38
|
+
so anything below 3.8 is not worth supporting."""
|
|
39
|
+
if sys.version_info < minimum:
|
|
40
|
+
need = ".".join(str(n) for n in minimum)
|
|
41
|
+
have = ".".join(str(n) for n in sys.version_info[:3])
|
|
42
|
+
sys.exit(f"[!] Python {need}+ required, but you're on {have}.")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def cloudflared_path():
|
|
46
|
+
"""Return the path to cloudflared if it's on PATH, else None.
|
|
47
|
+
shutil.which is the Python equivalent of the shell `which` command."""
|
|
48
|
+
return shutil.which("cloudflared")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# ----------------------------------------------------------------------------
|
|
52
|
+
# Step 1: the local server
|
|
53
|
+
# ----------------------------------------------------------------------------
|
|
54
|
+
|
|
55
|
+
def serve_directory(port, directory):
|
|
56
|
+
"""Start http.server bound to the given port, serving `directory`.
|
|
57
|
+
|
|
58
|
+
We run this in-process (not via subprocess) so we control the lifecycle
|
|
59
|
+
cleanly and can shut it down on Ctrl+C. ThreadingHTTPServer handles
|
|
60
|
+
multiple requests without blocking, which matters when a browser opens
|
|
61
|
+
several connections at once.
|
|
62
|
+
"""
|
|
63
|
+
directory = str(Path(directory).resolve())
|
|
64
|
+
|
|
65
|
+
# A handler factory that serves from `directory` instead of cwd.
|
|
66
|
+
def handler(*args, **kwargs):
|
|
67
|
+
return http.server.SimpleHTTPRequestHandler(
|
|
68
|
+
*args, directory=directory, **kwargs
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
# allow_reuse_address avoids "Address already in use" on quick restarts.
|
|
72
|
+
socketserver.ThreadingTCPServer.allow_reuse_address = True
|
|
73
|
+
httpd = socketserver.ThreadingTCPServer(("0.0.0.0", port), handler)
|
|
74
|
+
|
|
75
|
+
# Run the blocking serve loop on a background thread so main() can
|
|
76
|
+
# continue (e.g. to start the tunnel) and still catch Ctrl+C.
|
|
77
|
+
thread = threading.Thread(target=httpd.serve_forever, daemon=True)
|
|
78
|
+
thread.start()
|
|
79
|
+
return httpd
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
# ----------------------------------------------------------------------------
|
|
83
|
+
# Step 2: ensure cloudflared exists
|
|
84
|
+
# ----------------------------------------------------------------------------
|
|
85
|
+
|
|
86
|
+
INSTALL_HINT = (
|
|
87
|
+
"Install cloudflared manually, then re-run:\n"
|
|
88
|
+
" wget https://github.com/cloudflare/cloudflared/releases/latest/"
|
|
89
|
+
"download/cloudflared-linux-amd64.deb\n"
|
|
90
|
+
" sudo dpkg -i cloudflared-linux-amd64.deb"
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def ensure_cloudflared(assume_yes=False):
|
|
95
|
+
"""Return a path to cloudflared, installing it (with consent) if missing.
|
|
96
|
+
|
|
97
|
+
Linux-only for now. We ask before doing anything that needs sudo.
|
|
98
|
+
"""
|
|
99
|
+
existing = cloudflared_path()
|
|
100
|
+
if existing:
|
|
101
|
+
return existing
|
|
102
|
+
|
|
103
|
+
print("[!] cloudflared is not installed.")
|
|
104
|
+
if not assume_yes:
|
|
105
|
+
answer = input(" Install it now? (needs sudo) [y/N]: ").strip().lower()
|
|
106
|
+
if answer not in ("y", "yes"):
|
|
107
|
+
sys.exit("[!] Cannot create a public tunnel without cloudflared.\n" + INSTALL_HINT)
|
|
108
|
+
|
|
109
|
+
arch = os.uname().machine
|
|
110
|
+
# Map uname arch -> the .deb Cloudflare publishes.
|
|
111
|
+
deb_arch = {"x86_64": "amd64", "aarch64": "arm64", "armv7l": "arm"}.get(arch)
|
|
112
|
+
if deb_arch is None:
|
|
113
|
+
sys.exit(f"[!] Unsupported architecture: {arch}.\n" + INSTALL_HINT)
|
|
114
|
+
|
|
115
|
+
url = (
|
|
116
|
+
f"https://github.com/cloudflare/cloudflared/releases/latest/"
|
|
117
|
+
f"download/cloudflared-linux-{deb_arch}.deb"
|
|
118
|
+
)
|
|
119
|
+
deb = f"/tmp/cloudflared-{deb_arch}.deb"
|
|
120
|
+
|
|
121
|
+
try:
|
|
122
|
+
print(f"[*] Downloading {url}")
|
|
123
|
+
subprocess.run(["wget", "-qO", deb, url], check=True)
|
|
124
|
+
print("[*] Installing (you may be prompted for your sudo password)...")
|
|
125
|
+
subprocess.run(["sudo", "dpkg", "-i", deb], check=True)
|
|
126
|
+
except subprocess.CalledProcessError as exc:
|
|
127
|
+
sys.exit(f"[!] Install failed ({exc}).\n" + INSTALL_HINT)
|
|
128
|
+
finally:
|
|
129
|
+
# Clean up the downloaded .deb regardless of outcome.
|
|
130
|
+
try:
|
|
131
|
+
os.remove(deb)
|
|
132
|
+
except OSError:
|
|
133
|
+
pass
|
|
134
|
+
|
|
135
|
+
path = cloudflared_path()
|
|
136
|
+
if not path:
|
|
137
|
+
sys.exit("[!] cloudflared still not found after install.\n" + INSTALL_HINT)
|
|
138
|
+
return path
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
# ----------------------------------------------------------------------------
|
|
142
|
+
# Step 3: the tunnel
|
|
143
|
+
# ----------------------------------------------------------------------------
|
|
144
|
+
|
|
145
|
+
TUNNEL_URL_RE = re.compile(r"https://[-\w]+\.trycloudflare\.com")
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def start_tunnel(cloudflared, port):
|
|
149
|
+
"""Launch a cloudflared quick tunnel and stream its output, printing the
|
|
150
|
+
public URL prominently when it appears.
|
|
151
|
+
|
|
152
|
+
cloudflared writes its status (including the URL) to stderr, so we merge
|
|
153
|
+
stderr into stdout and read line by line.
|
|
154
|
+
"""
|
|
155
|
+
proc = subprocess.Popen(
|
|
156
|
+
[cloudflared, "tunnel", "--url", f"http://localhost:{port}"],
|
|
157
|
+
stdout=subprocess.PIPE,
|
|
158
|
+
stderr=subprocess.STDOUT,
|
|
159
|
+
text=True,
|
|
160
|
+
bufsize=1,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
printed = False
|
|
164
|
+
for line in proc.stdout:
|
|
165
|
+
match = TUNNEL_URL_RE.search(line)
|
|
166
|
+
if match and not printed:
|
|
167
|
+
url = match.group(0)
|
|
168
|
+
print("\n" + "=" * 56)
|
|
169
|
+
print(f" Public URL: {url}")
|
|
170
|
+
print("=" * 56 + "\n")
|
|
171
|
+
printed = True
|
|
172
|
+
# Uncomment to see raw cloudflared logs:
|
|
173
|
+
# print(line, end="")
|
|
174
|
+
return proc
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
# ----------------------------------------------------------------------------
|
|
178
|
+
# Wiring it together
|
|
179
|
+
# ----------------------------------------------------------------------------
|
|
180
|
+
|
|
181
|
+
def main(argv=None):
|
|
182
|
+
parser = argparse.ArgumentParser(
|
|
183
|
+
prog="prosoon-pf",
|
|
184
|
+
description="Quick-start a local HTTP server, optionally exposed via Cloudflare.",
|
|
185
|
+
)
|
|
186
|
+
parser.add_argument("port", nargs="?", type=int, default=8000,
|
|
187
|
+
help="Port to serve on (default: 8000).")
|
|
188
|
+
parser.add_argument("-d", "--dir", default=".",
|
|
189
|
+
help="Directory to serve (default: current dir).")
|
|
190
|
+
parser.add_argument("--public", action="store_true",
|
|
191
|
+
help="Expose the server publicly via a Cloudflare tunnel.")
|
|
192
|
+
parser.add_argument("-y", "--yes", action="store_true",
|
|
193
|
+
help="Skip the cloudflared install confirmation.")
|
|
194
|
+
parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
|
|
195
|
+
args = parser.parse_args(argv)
|
|
196
|
+
|
|
197
|
+
check_python_version()
|
|
198
|
+
|
|
199
|
+
serve_dir = Path(args.dir).resolve()
|
|
200
|
+
|
|
201
|
+
# The safety confirmation for public mode. This is the whole reason the
|
|
202
|
+
# tool is trustworthy: the user sees exactly what they're about to expose.
|
|
203
|
+
if args.public:
|
|
204
|
+
print(f"[!] PUBLIC mode: everything in {serve_dir} will be reachable")
|
|
205
|
+
print(" by anyone with the link, for as long as this runs.")
|
|
206
|
+
try:
|
|
207
|
+
entries = sorted(p.name for p in serve_dir.iterdir())
|
|
208
|
+
except OSError:
|
|
209
|
+
entries = []
|
|
210
|
+
if entries:
|
|
211
|
+
preview = ", ".join(entries[:8]) + (" ..." if len(entries) > 8 else "")
|
|
212
|
+
print(f" Contents: {preview}")
|
|
213
|
+
if not args.yes:
|
|
214
|
+
confirm = input(" Expose this directory publicly? [y/N]: ").strip().lower()
|
|
215
|
+
if confirm not in ("y", "yes"):
|
|
216
|
+
sys.exit("[*] Aborted. Nothing was exposed.")
|
|
217
|
+
|
|
218
|
+
httpd = serve_directory(args.port, serve_dir)
|
|
219
|
+
print(f"[*] Serving {serve_dir} at http://localhost:{args.port}")
|
|
220
|
+
|
|
221
|
+
tunnel_proc = None
|
|
222
|
+
try:
|
|
223
|
+
if args.public:
|
|
224
|
+
cf = ensure_cloudflared(assume_yes=args.yes)
|
|
225
|
+
print("[*] Starting Cloudflare tunnel...")
|
|
226
|
+
tunnel_proc = start_tunnel(cf, args.port) # blocks, streaming logs
|
|
227
|
+
else:
|
|
228
|
+
print("[*] Local mode. Press Ctrl+C to stop.")
|
|
229
|
+
signal.pause() # sleep until a signal (Ctrl+C) arrives
|
|
230
|
+
except KeyboardInterrupt:
|
|
231
|
+
print("\n[*] Shutting down...")
|
|
232
|
+
finally:
|
|
233
|
+
if tunnel_proc and tunnel_proc.poll() is None:
|
|
234
|
+
tunnel_proc.terminate()
|
|
235
|
+
httpd.shutdown()
|
|
236
|
+
print("[*] Stopped.")
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
if __name__ == "__main__":
|
|
240
|
+
main()
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: prosoon-pf
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Quick-start a local HTTP server and optionally expose it via a Cloudflare tunnel.
|
|
5
|
+
Author: Prasun Banerjee
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: http,server,cloudflare,tunnel,share,cli
|
|
8
|
+
Classifier: Environment :: Console
|
|
9
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Requires-Python: >=3.8
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
# prosoon-pf
|
|
16
|
+
|
|
17
|
+
Quick-start a local HTTP server, and optionally expose it to the internet with a
|
|
18
|
+
Cloudflare quick tunnel — in one command. Linux only for now.
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pip install prosoon-pf
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Usage
|
|
27
|
+
|
|
28
|
+
Serve the current directory locally on port 8000:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
prosoon-pf
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Pick a port and a directory:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
prosoon-pf 9000 --dir ~/share
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Expose it publicly (prints a `https://….trycloudflare.com` link):
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
prosoon-pf 8000 --public
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
If `cloudflared` isn't installed, `--public` mode will ask before installing it.
|
|
47
|
+
|
|
48
|
+
## Public mode is public
|
|
49
|
+
|
|
50
|
+
When you use `--public`, **every file in the served directory becomes
|
|
51
|
+
downloadable by anyone with the link** for as long as the command runs. The tool
|
|
52
|
+
shows you the directory contents and asks for confirmation first. Don't serve a
|
|
53
|
+
folder that contains passwords, keys, `.ovpn` files, or anything private.
|
|
54
|
+
|
|
55
|
+
## License
|
|
56
|
+
|
|
57
|
+
MIT
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
src/prosoon_pf/__init__.py
|
|
4
|
+
src/prosoon_pf/cli.py
|
|
5
|
+
src/prosoon_pf.egg-info/PKG-INFO
|
|
6
|
+
src/prosoon_pf.egg-info/SOURCES.txt
|
|
7
|
+
src/prosoon_pf.egg-info/dependency_links.txt
|
|
8
|
+
src/prosoon_pf.egg-info/entry_points.txt
|
|
9
|
+
src/prosoon_pf.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
prosoon_pf
|