suxitunnel 0.1.1__tar.gz → 0.1.3__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.
@@ -48,3 +48,7 @@ build/
48
48
 
49
49
  # Local cloudflared downloads
50
50
  .cloudflare-tunnel/
51
+ cloudflared
52
+ cloudflared.exe
53
+
54
+ .claude/settings.local.json
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: suxitunnel
3
- Version: 0.1.1
3
+ Version: 0.1.3
4
4
  Summary: A simple Python library to expose local servers using Cloudflare Tunnel
5
5
  Project-URL: Homepage, https://github.com/sadwx/suxitunnel
6
6
  Project-URL: Repository, https://github.com/sadwx/suxitunnel
@@ -35,6 +35,21 @@ pip install suxitunnel
35
35
 
36
36
  ## Quick Start
37
37
 
38
+ ### Command Line
39
+
40
+ ```bash
41
+ # Expose local port 8000 (random URL)
42
+ suxitunnel --port 8000
43
+
44
+ # Or use short form
45
+ suxitunnel -p 8000
46
+
47
+ # Use a named tunnel with dedicated URL (requires Cloudflare account)
48
+ suxitunnel --port 8000 --tunnel-token YOUR_TOKEN
49
+ ```
50
+
51
+ ### Python API
52
+
38
53
  ```python
39
54
  from suxitunnel import Tunnel
40
55
 
@@ -57,6 +72,8 @@ That's it! No configuration, no authentication, no manual setup.
57
72
  - 🌍 **Global CDN** - Fast access from anywhere in the world
58
73
  - 📦 **Auto-install** - Automatically downloads cloudflared if needed
59
74
  - 🐍 **Simple API** - Clean, Pythonic interface
75
+ - 💻 **CLI Support** - Use from command line or as a library
76
+ - 🏷️ **Named Tunnels** - Support for dedicated URLs with tunnel tokens
60
77
 
61
78
  ## Usage
62
79
 
@@ -162,7 +179,31 @@ tunnel = Tunnel(8000, auto_download=False)
162
179
 
163
180
  ## API Reference
164
181
 
165
- ### `Tunnel(port, host="localhost", cloudflared_path=None, auto_download=True)`
182
+ ### Command Line Interface
183
+
184
+ ```bash
185
+ suxitunnel [-h] -p PORT [--tunnel-token TUNNEL_TOKEN] [--local-https]
186
+ ```
187
+
188
+ **Arguments:**
189
+ - `-p, --port PORT`: Local port to expose (required)
190
+ - `--tunnel-token TOKEN`: Cloudflare tunnel token for named tunnels (optional)
191
+ - `--local-https`: Connect to local server via HTTPS (for self-signed certs)
192
+ - `-h, --help`: Show help message
193
+
194
+ **Examples:**
195
+ ```bash
196
+ # Quick tunnel with random URL
197
+ suxitunnel --port 8000
198
+
199
+ # Named tunnel with dedicated URL
200
+ suxitunnel --port 8000 --tunnel-token eyJhIjoiNz...
201
+
202
+ # Local HTTPS server (e.g., .NET Core with HTTPS)
203
+ suxitunnel --port 5001 --local-https
204
+ ```
205
+
206
+ ### `Tunnel(port, host="localhost", cloudflared_path=None, auto_download=True, tunnel_token=None, local_https=False)`
166
207
 
167
208
  Create a tunnel to expose a local port.
168
209
 
@@ -171,19 +212,28 @@ Create a tunnel to expose a local port.
171
212
  - `host` (str): Local hostname (default: "localhost")
172
213
  - `cloudflared_path` (str): Path to cloudflared binary (auto-detected if None)
173
214
  - `auto_download` (bool): Auto-download cloudflared if not found (default: True)
215
+ - `tunnel_token` (str): Cloudflare tunnel token for named tunnels (optional)
216
+ - `local_https` (bool): Set to True if local server uses HTTPS (default: False)
174
217
 
175
218
  **Attributes:**
176
219
  - `url` (str): Public HTTPS URL for the tunnel
177
220
  - `port` (int): Local port being exposed
178
221
  - `host` (str): Local hostname
222
+ - `tunnel_token` (str): Tunnel token if using named tunnel
179
223
  - `is_alive()` (bool): Check if tunnel is running
180
224
  - `close()`: Close the tunnel
181
225
 
182
226
  **Example:**
183
227
  ```python
228
+ # Quick tunnel (random URL)
184
229
  tunnel = Tunnel(8000)
185
230
  print(tunnel.url) # https://abc123.trycloudflare.com
186
231
  tunnel.close()
232
+
233
+ # Named tunnel (dedicated URL)
234
+ tunnel = Tunnel(8000, tunnel_token="your-token")
235
+ print(tunnel.url) # https://your-configured-domain.com
236
+ tunnel.close()
187
237
  ```
188
238
 
189
239
  ### `open_tunnel(port, host="localhost")`
@@ -216,7 +266,7 @@ Your App (localhost:8000) → cloudflared → Cloudflare Edge → Internet
216
266
 
217
267
  ### Cloudflared Binary
218
268
 
219
- The library automatically downloads the `cloudflared` binary on first use if it's not already installed. The binary is saved to `~/.cloudflare-tunnel/`.
269
+ The library automatically downloads the `cloudflared` binary on first use if it's not already installed. The binary is saved to the library's installation directory.
220
270
 
221
271
  Supported platforms:
222
272
  - Linux (x64, ARM64)
@@ -237,12 +287,18 @@ Supported platforms:
237
287
 
238
288
  ## Limitations
239
289
 
240
- 1. **Random URLs**: Each tunnel gets a random `*.trycloudflare.com` URL (no custom domains)
290
+ **Quick Tunnels (default):**
291
+ 1. **Random URLs**: Each tunnel gets a random `*.trycloudflare.com` URL
241
292
  2. **Temporary**: Tunnels are meant for development/testing, not production
242
293
  3. **No persistence**: URL changes each time you create a new tunnel
243
294
  4. **Rate limits**: Subject to Cloudflare's fair use policy
244
295
 
245
- For production use with custom domains, see the full [cloudflared documentation](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/).
296
+ **Named Tunnels (with `--tunnel-token`):**
297
+ - Requires a free Cloudflare account
298
+ - Provides persistent, dedicated URLs
299
+ - Better suited for staging/production environments
300
+
301
+ For setting up named tunnels, see the [Cloudflare Tunnel documentation](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/).
246
302
 
247
303
  ## Security Considerations
248
304
 
@@ -12,6 +12,21 @@ pip install suxitunnel
12
12
 
13
13
  ## Quick Start
14
14
 
15
+ ### Command Line
16
+
17
+ ```bash
18
+ # Expose local port 8000 (random URL)
19
+ suxitunnel --port 8000
20
+
21
+ # Or use short form
22
+ suxitunnel -p 8000
23
+
24
+ # Use a named tunnel with dedicated URL (requires Cloudflare account)
25
+ suxitunnel --port 8000 --tunnel-token YOUR_TOKEN
26
+ ```
27
+
28
+ ### Python API
29
+
15
30
  ```python
16
31
  from suxitunnel import Tunnel
17
32
 
@@ -34,6 +49,8 @@ That's it! No configuration, no authentication, no manual setup.
34
49
  - 🌍 **Global CDN** - Fast access from anywhere in the world
35
50
  - 📦 **Auto-install** - Automatically downloads cloudflared if needed
36
51
  - 🐍 **Simple API** - Clean, Pythonic interface
52
+ - 💻 **CLI Support** - Use from command line or as a library
53
+ - 🏷️ **Named Tunnels** - Support for dedicated URLs with tunnel tokens
37
54
 
38
55
  ## Usage
39
56
 
@@ -139,7 +156,31 @@ tunnel = Tunnel(8000, auto_download=False)
139
156
 
140
157
  ## API Reference
141
158
 
142
- ### `Tunnel(port, host="localhost", cloudflared_path=None, auto_download=True)`
159
+ ### Command Line Interface
160
+
161
+ ```bash
162
+ suxitunnel [-h] -p PORT [--tunnel-token TUNNEL_TOKEN] [--local-https]
163
+ ```
164
+
165
+ **Arguments:**
166
+ - `-p, --port PORT`: Local port to expose (required)
167
+ - `--tunnel-token TOKEN`: Cloudflare tunnel token for named tunnels (optional)
168
+ - `--local-https`: Connect to local server via HTTPS (for self-signed certs)
169
+ - `-h, --help`: Show help message
170
+
171
+ **Examples:**
172
+ ```bash
173
+ # Quick tunnel with random URL
174
+ suxitunnel --port 8000
175
+
176
+ # Named tunnel with dedicated URL
177
+ suxitunnel --port 8000 --tunnel-token eyJhIjoiNz...
178
+
179
+ # Local HTTPS server (e.g., .NET Core with HTTPS)
180
+ suxitunnel --port 5001 --local-https
181
+ ```
182
+
183
+ ### `Tunnel(port, host="localhost", cloudflared_path=None, auto_download=True, tunnel_token=None, local_https=False)`
143
184
 
144
185
  Create a tunnel to expose a local port.
145
186
 
@@ -148,19 +189,28 @@ Create a tunnel to expose a local port.
148
189
  - `host` (str): Local hostname (default: "localhost")
149
190
  - `cloudflared_path` (str): Path to cloudflared binary (auto-detected if None)
150
191
  - `auto_download` (bool): Auto-download cloudflared if not found (default: True)
192
+ - `tunnel_token` (str): Cloudflare tunnel token for named tunnels (optional)
193
+ - `local_https` (bool): Set to True if local server uses HTTPS (default: False)
151
194
 
152
195
  **Attributes:**
153
196
  - `url` (str): Public HTTPS URL for the tunnel
154
197
  - `port` (int): Local port being exposed
155
198
  - `host` (str): Local hostname
199
+ - `tunnel_token` (str): Tunnel token if using named tunnel
156
200
  - `is_alive()` (bool): Check if tunnel is running
157
201
  - `close()`: Close the tunnel
158
202
 
159
203
  **Example:**
160
204
  ```python
205
+ # Quick tunnel (random URL)
161
206
  tunnel = Tunnel(8000)
162
207
  print(tunnel.url) # https://abc123.trycloudflare.com
163
208
  tunnel.close()
209
+
210
+ # Named tunnel (dedicated URL)
211
+ tunnel = Tunnel(8000, tunnel_token="your-token")
212
+ print(tunnel.url) # https://your-configured-domain.com
213
+ tunnel.close()
164
214
  ```
165
215
 
166
216
  ### `open_tunnel(port, host="localhost")`
@@ -193,7 +243,7 @@ Your App (localhost:8000) → cloudflared → Cloudflare Edge → Internet
193
243
 
194
244
  ### Cloudflared Binary
195
245
 
196
- The library automatically downloads the `cloudflared` binary on first use if it's not already installed. The binary is saved to `~/.cloudflare-tunnel/`.
246
+ The library automatically downloads the `cloudflared` binary on first use if it's not already installed. The binary is saved to the library's installation directory.
197
247
 
198
248
  Supported platforms:
199
249
  - Linux (x64, ARM64)
@@ -214,12 +264,18 @@ Supported platforms:
214
264
 
215
265
  ## Limitations
216
266
 
217
- 1. **Random URLs**: Each tunnel gets a random `*.trycloudflare.com` URL (no custom domains)
267
+ **Quick Tunnels (default):**
268
+ 1. **Random URLs**: Each tunnel gets a random `*.trycloudflare.com` URL
218
269
  2. **Temporary**: Tunnels are meant for development/testing, not production
219
270
  3. **No persistence**: URL changes each time you create a new tunnel
220
271
  4. **Rate limits**: Subject to Cloudflare's fair use policy
221
272
 
222
- For production use with custom domains, see the full [cloudflared documentation](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/).
273
+ **Named Tunnels (with `--tunnel-token`):**
274
+ - Requires a free Cloudflare account
275
+ - Provides persistent, dedicated URLs
276
+ - Better suited for staging/production environments
277
+
278
+ For setting up named tunnels, see the [Cloudflare Tunnel documentation](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/).
223
279
 
224
280
  ## Security Considerations
225
281
 
@@ -3,7 +3,7 @@ suxitunnel
3
3
  A simple Python library to expose local servers using Cloudflare Tunnel
4
4
  """
5
5
 
6
- from .suxitunnel import Tunnel, TunnelError, open_tunnel
6
+ from suxitunnel import Tunnel, TunnelError, open_tunnel
7
7
 
8
8
  __version__ = "0.1.0"
9
9
  __all__ = ["Tunnel", "TunnelError", "open_tunnel"]
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "suxitunnel"
3
- version = "0.1.1"
3
+ version = "0.1.3"
4
4
  description = "A simple Python library to expose local servers using Cloudflare Tunnel"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -22,6 +22,9 @@ dependencies = [
22
22
  "requests>=2.32.0",
23
23
  ]
24
24
 
25
+ [project.scripts]
26
+ suxitunnel = "suxitunnel:main"
27
+
25
28
  [project.urls]
26
29
  Homepage = "https://github.com/sadwx/suxitunnel"
27
30
  Repository = "https://github.com/sadwx/suxitunnel"
@@ -47,5 +50,9 @@ include = [
47
50
  [dependency-groups]
48
51
  dev = [
49
52
  "build>=1.4.0",
53
+ "fastapi>=0.128.0",
54
+ "flask>=3.1.2",
55
+ "pytest>=9.0.2",
50
56
  "twine>=6.2.0",
57
+ "uvicorn>=0.40.0",
51
58
  ]
@@ -51,19 +51,25 @@ class Tunnel:
51
51
  port: int,
52
52
  host: str = "localhost",
53
53
  cloudflared_path: Optional[str] = None,
54
- auto_download: bool = True
54
+ auto_download: bool = True,
55
+ tunnel_token: Optional[str] = None,
56
+ local_https: bool = False
55
57
  ):
56
58
  """
57
59
  Create a tunnel to expose a local port.
58
-
60
+
59
61
  Args:
60
62
  port: Local port to expose (e.g., 8000)
61
63
  host: Local host (default: "localhost")
62
64
  cloudflared_path: Path to cloudflared binary (auto-detected if None)
63
65
  auto_download: Automatically download cloudflared if not found
66
+ tunnel_token: Cloudflare tunnel token for named tunnels (optional)
67
+ local_https: Set to True if local server uses HTTPS (default: False)
64
68
  """
65
69
  self.port = port
66
70
  self.host = host
71
+ self.tunnel_token = tunnel_token
72
+ self.local_https = local_https
67
73
  self.url: Optional[str] = None
68
74
  self._process: Optional[subprocess.Popen] = None
69
75
  self._ready = threading.Event()
@@ -86,8 +92,20 @@ class Tunnel:
86
92
  # Start the tunnel
87
93
  self._start()
88
94
 
95
+ def _get_library_dir(self) -> Path:
96
+ """Get the directory where the library is located"""
97
+ return Path(__file__).parent
98
+
89
99
  def _find_cloudflared(self) -> Optional[str]:
90
100
  """Try to find cloudflared in common locations"""
101
+ system = platform.system().lower()
102
+ binary_name = "cloudflared.exe" if system == "windows" else "cloudflared"
103
+
104
+ # First check the library directory (where downloaded binaries are stored)
105
+ lib_dir_path = self._get_library_dir() / binary_name
106
+ if lib_dir_path.exists():
107
+ return str(lib_dir_path)
108
+
91
109
  # Check if in PATH
92
110
  try:
93
111
  result = subprocess.run(
@@ -99,29 +117,31 @@ class Tunnel:
99
117
  return "cloudflared"
100
118
  except (FileNotFoundError, subprocess.TimeoutExpired):
101
119
  pass
102
-
120
+
103
121
  # Check common install locations
104
122
  possible_paths = [
105
123
  "/usr/local/bin/cloudflared",
106
124
  "/usr/bin/cloudflared",
107
125
  str(Path.home() / "bin" / "cloudflared"),
108
126
  str(Path.home() / ".local" / "bin" / "cloudflared"),
127
+ # Legacy download location
128
+ str(Path.home() / ".cloudflare-tunnel" / binary_name),
109
129
  ]
110
-
130
+
111
131
  for path in possible_paths:
112
132
  if Path(path).exists():
113
133
  return path
114
-
134
+
115
135
  return None
116
136
 
117
137
  def _download_cloudflared(self) -> str:
118
138
  """Download cloudflared binary for the current platform"""
119
139
  system = platform.system().lower()
120
140
  machine = platform.machine().lower()
121
-
141
+
122
142
  # Determine download URL
123
143
  base_url = "https://github.com/cloudflare/cloudflared/releases/latest/download"
124
-
144
+
125
145
  if system == "darwin": # macOS
126
146
  if "arm" in machine or "aarch64" in machine:
127
147
  filename = "cloudflared-darwin-arm64.tgz"
@@ -136,13 +156,12 @@ class Tunnel:
136
156
  filename = "cloudflared-windows-amd64.exe"
137
157
  else:
138
158
  raise TunnelError(f"Unsupported platform: {system} {machine}")
139
-
159
+
140
160
  download_url = f"{base_url}/{filename}"
141
-
142
- # Download location
143
- download_dir = Path.home() / ".cloudflare-tunnel"
144
- download_dir.mkdir(parents=True, exist_ok=True)
145
-
161
+
162
+ # Download to the library directory
163
+ download_dir = self._get_library_dir()
164
+
146
165
  if filename.endswith(".tgz"):
147
166
  archive_path = download_dir / filename
148
167
  binary_path = download_dir / "cloudflared"
@@ -150,7 +169,7 @@ class Tunnel:
150
169
  binary_path = download_dir / "cloudflared.exe"
151
170
  else:
152
171
  binary_path = download_dir / "cloudflared"
153
-
172
+
154
173
  # Check if already downloaded
155
174
  if binary_path.exists():
156
175
  return str(binary_path)
@@ -190,14 +209,29 @@ class Tunnel:
190
209
 
191
210
  def _start(self):
192
211
  """Start the cloudflared tunnel"""
193
- local_url = f"http://{self.host}:{self.port}"
194
-
195
- # Use Cloudflare Quick Tunnels (no authentication required!)
196
- cmd = [
197
- self.cloudflared_path,
198
- "tunnel",
199
- "--url", local_url
200
- ]
212
+ protocol = "https" if self.local_https else "http"
213
+ local_url = f"{protocol}://{self.host}:{self.port}"
214
+
215
+ if self.tunnel_token:
216
+ # Named tunnel with token
217
+ cmd = [
218
+ self.cloudflared_path,
219
+ "tunnel",
220
+ "--url", local_url,
221
+ "run",
222
+ "--token", self.tunnel_token
223
+ ]
224
+ else:
225
+ # Quick tunnel (no authentication required)
226
+ cmd = [
227
+ self.cloudflared_path,
228
+ "tunnel",
229
+ "--url", local_url
230
+ ]
231
+
232
+ # Add --no-tls-verify for local HTTPS servers with self-signed certs
233
+ if self.local_https:
234
+ cmd.append("--no-tls-verify")
201
235
 
202
236
  try:
203
237
  self._process = subprocess.Popen(
@@ -278,19 +312,77 @@ class Tunnel:
278
312
  return f"<Tunnel port={self.port} url={self.url} status={status}>"
279
313
 
280
314
 
315
+ def parse_args(args: list[str] | None = None):
316
+ """Parse command line arguments for the CLI."""
317
+ import argparse
318
+
319
+ parser = argparse.ArgumentParser(
320
+ prog="suxitunnel",
321
+ description="Expose local servers via Cloudflare Tunnel"
322
+ )
323
+ parser.add_argument(
324
+ "-p", "--port",
325
+ type=int,
326
+ required=True,
327
+ help="Local port to expose"
328
+ )
329
+ parser.add_argument(
330
+ "--tunnel-token",
331
+ type=str,
332
+ default=None,
333
+ help="Cloudflare tunnel token for named tunnels (optional)"
334
+ )
335
+ parser.add_argument(
336
+ "--local-https",
337
+ action="store_true",
338
+ default=False,
339
+ help="Use HTTPS to connect to local server (for servers with self-signed certs)"
340
+ )
341
+
342
+ return parser.parse_args(args)
343
+
344
+
281
345
  def open_tunnel(port: int, host: str = "localhost") -> Tunnel:
282
346
  """
283
347
  Convenience function to create a tunnel.
284
-
348
+
285
349
  Args:
286
350
  port: Local port to expose
287
351
  host: Local host (default: "localhost")
288
-
352
+
289
353
  Returns:
290
354
  Tunnel instance
291
-
355
+
292
356
  Example:
293
357
  tunnel = open_tunnel(8000)
294
358
  print(f"Public URL: {tunnel.url}")
295
359
  """
296
360
  return Tunnel(port=port, host=host)
361
+
362
+
363
+ def main(args: list[str] | None = None) -> None:
364
+ """CLI entry point for suxitunnel."""
365
+ parsed = parse_args(args)
366
+
367
+ tunnel = Tunnel(
368
+ port=parsed.port,
369
+ tunnel_token=parsed.tunnel_token,
370
+ local_https=parsed.local_https
371
+ )
372
+
373
+ print(f"Tunnel URL: {tunnel.url}")
374
+ print("Press Ctrl+C to stop the tunnel")
375
+
376
+ try:
377
+ while tunnel.is_alive():
378
+ time.sleep(1)
379
+ except KeyboardInterrupt:
380
+ pass
381
+ finally:
382
+ tunnel.close()
383
+
384
+ raise SystemExit(0)
385
+
386
+
387
+ if __name__ == "__main__":
388
+ main()
File without changes