Update README
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -168,4 +168,5 @@ Thumbs.db
|
|||||||
|
|
||||||
# Project specific
|
# Project specific
|
||||||
OctoPrint_Tailscale_Funnel.egg-info/
|
OctoPrint_Tailscale_Funnel.egg-info/
|
||||||
**/OctoPrint_Tailscale_Funnel.egg-info/
|
**/OctoPrint_Tailscale_Funnel.egg-info/
|
||||||
|
dist/
|
52
README.md
52
README.md
@@ -1,52 +0,0 @@
|
|||||||
# OctoPrint Tailscale Funnel Plugin
|
|
||||||
|
|
||||||
This plugin makes your OctoPrint instance accessible from anywhere via Tailscale Funnel, without needing to configure port forwarding, dynamic DNS, or complex firewall settings.
|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
* Enable/disable Tailscale Funnel access directly from OctoPrint's settings
|
|
||||||
* Monitor the current Funnel connection status
|
|
||||||
* Display the public URL for accessing OctoPrint remotely
|
|
||||||
* Configure the port to expose via Funnel
|
|
||||||
|
|
||||||
## Requirements
|
|
||||||
|
|
||||||
* OctoPrint 1.3.0 or higher
|
|
||||||
* Tailscale installed and configured on the system
|
|
||||||
* Python 3.7 or higher
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
1. Install Tailscale on your system and ensure it's running (see https://tailscale.com/download/linux or run `curl -fsSL https://tailscale.com/install.sh | sh`)
|
|
||||||
2. Start Tailscale on your system (run `sudo tailscale up`)
|
|
||||||
3. Authenticate using the Tailscale URL (e.g. https://login.tailscale.com/a/<some random characters>)
|
|
||||||
4. Install the plugin through OctoPrint's plugin manager (go to Settings -> Plugins -> Install and search for "Tailscale Funnel")
|
|
||||||
5. Configure the plugin settings in OctoPrint's settings panel
|
|
||||||
6. Enable Funnel through the plugin interface
|
|
||||||
|
|
||||||
## Building from Source
|
|
||||||
|
|
||||||
If you want to build the plugin from source, please refer to the [BUILDING.md](octoprint_tailscale_funnel/BUILDING.md) file for detailed instructions.
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
The plugin adds a new section to OctoPrint's settings panel with the following options:
|
|
||||||
|
|
||||||
* **Port**: The port to expose via Funnel (default: 80)
|
|
||||||
* **Confirm Enable**: Require confirmation before enabling Funnel (default: True)
|
|
||||||
|
|
||||||
## Security Considerations
|
|
||||||
|
|
||||||
Enabling Funnel makes your OctoPrint instance accessible from the public internet. Only enable it when needed and disable it when finished. The plugin will show a confirmation dialog before enabling Funnel if the "Confirm Enable" option is checked.
|
|
||||||
|
|
||||||
## API Endpoints
|
|
||||||
|
|
||||||
The plugin exposes the following API endpoints:
|
|
||||||
|
|
||||||
* `GET /api/plugin/tailscale_funnel/status` - Get current Funnel status
|
|
||||||
* `POST /api/plugin/tailscale_funnel/enable` - Enable Tailscale Funnel
|
|
||||||
* `POST /api/plugin/tailscale_funnel/disable` - Disable Tailscale Funnel
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
AGPLv3
|
|
@@ -55,7 +55,7 @@ This will create distribution files in the `dist/` directory:
|
|||||||
|
|
||||||
First, install the zip utility if not already installed:
|
First, install the zip utility if not already installed:
|
||||||
```bash
|
```bash
|
||||||
sudo apt install zip # On Ubuntu/Debian
|
sudo apt install zip # On Ubuntu/Debian/Raspbian
|
||||||
```
|
```
|
||||||
|
|
||||||
Then create a proper zip file:
|
Then create a proper zip file:
|
||||||
|
@@ -1,230 +0,0 @@
|
|||||||
# coding=utf-8
|
|
||||||
from __future__ import absolute_import
|
|
||||||
|
|
||||||
import octoprint.plugin
|
|
||||||
from .tailscale_interface import TailscaleInterface, TailscaleError, TailscaleNotInstalledError, TailscaleNotRunningError
|
|
||||||
from .status_monitor import StatusMonitor
|
|
||||||
|
|
||||||
|
|
||||||
class TailscaleFunnelPlugin(octoprint.plugin.StartupPlugin,
|
|
||||||
octoprint.plugin.SettingsPlugin,
|
|
||||||
octoprint.plugin.AssetPlugin,
|
|
||||||
octoprint.plugin.TemplatePlugin,
|
|
||||||
octoprint.plugin.BlueprintPlugin):
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self._logger = None
|
|
||||||
self.tailscale_interface = None
|
|
||||||
self.status_monitor = None
|
|
||||||
|
|
||||||
##~~ StartupPlugin mixin
|
|
||||||
|
|
||||||
def on_after_startup(self):
|
|
||||||
self._logger = self._plugin_manager.get_logger("octoprint_tailscale_funnel")
|
|
||||||
self._logger.info("Tailscale Funnel Plugin started")
|
|
||||||
self.tailscale_interface = TailscaleInterface(self._logger)
|
|
||||||
self.status_monitor = StatusMonitor(self)
|
|
||||||
self.status_monitor.start()
|
|
||||||
|
|
||||||
def on_shutdown(self):
|
|
||||||
if self.status_monitor:
|
|
||||||
self.status_monitor.stop()
|
|
||||||
|
|
||||||
##~~ SettingsPlugin mixin
|
|
||||||
|
|
||||||
def get_settings_defaults(self):
|
|
||||||
return dict(
|
|
||||||
enabled=False,
|
|
||||||
port=80,
|
|
||||||
confirm_enable=True
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_settings_version(self):
|
|
||||||
return 1
|
|
||||||
|
|
||||||
def on_settings_migrate(self, target, current):
|
|
||||||
if current is None:
|
|
||||||
# Migrate from settings version 0 to 1
|
|
||||||
if self._settings.has(["funnel_enabled"]):
|
|
||||||
self._settings.set_boolean(["enabled"], self._settings.get_boolean(["funnel_enabled"]))
|
|
||||||
self._settings.remove(["funnel_enabled"])
|
|
||||||
|
|
||||||
def on_settings_save(self, data):
|
|
||||||
# Save the settings
|
|
||||||
octoprint.plugin.SettingsPlugin.on_settings_save(self, data)
|
|
||||||
|
|
||||||
# If enabled setting changed, update funnel status
|
|
||||||
if "enabled" in data:
|
|
||||||
enabled = self._settings.get_boolean(["enabled"])
|
|
||||||
port = self._settings.get_int(["port"])
|
|
||||||
|
|
||||||
try:
|
|
||||||
if enabled:
|
|
||||||
# Enable funnel
|
|
||||||
if self.tailscale_interface:
|
|
||||||
success = self.tailscale_interface.enable_funnel(port)
|
|
||||||
if success:
|
|
||||||
self._logger.info("Tailscale Funnel enabled on port {}".format(port))
|
|
||||||
else:
|
|
||||||
self._logger.error("Failed to enable Tailscale Funnel on port {}".format(port))
|
|
||||||
else:
|
|
||||||
# Disable funnel
|
|
||||||
if self.tailscale_interface:
|
|
||||||
success = self.tailscale_interface.disable_funnel(port)
|
|
||||||
if success:
|
|
||||||
self._logger.info("Tailscale Funnel disabled on port {}".format(port))
|
|
||||||
else:
|
|
||||||
self._logger.error("Failed to disable Tailscale Funnel on port {}".format(port))
|
|
||||||
except TailscaleError as e:
|
|
||||||
self._logger.error("Tailscale error: {}".format(str(e)))
|
|
||||||
except Exception as e:
|
|
||||||
self._logger.error("Unexpected error: {}".format(str(e)))
|
|
||||||
|
|
||||||
##~~ AssetPlugin mixin
|
|
||||||
|
|
||||||
def get_assets(self):
|
|
||||||
return dict(
|
|
||||||
js=["js/tailscale_funnel.js"],
|
|
||||||
css=["css/tailscale_funnel.css"],
|
|
||||||
less=["less/tailscale_funnel.less"]
|
|
||||||
)
|
|
||||||
|
|
||||||
##~~ TemplatePlugin mixin
|
|
||||||
|
|
||||||
def get_template_configs(self):
|
|
||||||
return [
|
|
||||||
dict(type="settings", custom_bindings=False)
|
|
||||||
]
|
|
||||||
|
|
||||||
##~~ BlueprintPlugin mixin
|
|
||||||
|
|
||||||
@octoprint.plugin.BlueprintPlugin.route("/status", methods=["GET"])
|
|
||||||
def get_status(self):
|
|
||||||
if not self.tailscale_interface:
|
|
||||||
return {"status": "error", "message": "Tailscale interface not initialized"}, 500
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Check if tailscale is installed
|
|
||||||
if not self.tailscale_interface.is_tailscale_installed():
|
|
||||||
return {"status": "error", "message": "Tailscale is not installed"}, 500
|
|
||||||
|
|
||||||
# Check if tailscale is running
|
|
||||||
if not self.tailscale_interface.is_tailscale_running():
|
|
||||||
return {"status": "error", "message": "Tailscale is not running"}, 500
|
|
||||||
|
|
||||||
# Get funnel status
|
|
||||||
funnel_enabled = self.tailscale_interface.is_funnel_enabled()
|
|
||||||
public_url = self.tailscale_interface.get_public_url() if funnel_enabled else None
|
|
||||||
|
|
||||||
return {
|
|
||||||
"status": "success",
|
|
||||||
"data": {
|
|
||||||
"funnel_enabled": funnel_enabled,
|
|
||||||
"public_url": public_url,
|
|
||||||
"tailscale_installed": True,
|
|
||||||
"tailscale_running": True
|
|
||||||
}
|
|
||||||
}
|
|
||||||
except TailscaleNotInstalledError as e:
|
|
||||||
return {"status": "error", "message": "Tailscale is not installed: {}".format(str(e))}, 500
|
|
||||||
except TailscaleNotRunningError as e:
|
|
||||||
return {"status": "error", "message": "Tailscale is not running: {}".format(str(e))}, 500
|
|
||||||
except TailscaleError as e:
|
|
||||||
return {"status": "error", "message": "Tailscale error: {}".format(str(e))}, 500
|
|
||||||
except Exception as e:
|
|
||||||
self._logger.error("Unexpected error in get_status: {}".format(str(e)))
|
|
||||||
return {"status": "error", "message": "Unexpected error: {}".format(str(e))}, 500
|
|
||||||
|
|
||||||
@octoprint.plugin.BlueprintPlugin.route("/enable", methods=["POST"])
|
|
||||||
def enable_funnel(self):
|
|
||||||
if not self.tailscale_interface:
|
|
||||||
return {"status": "error", "message": "Tailscale interface not initialized"}, 500
|
|
||||||
|
|
||||||
try:
|
|
||||||
port = self._settings.get_int(["port"])
|
|
||||||
success = self.tailscale_interface.enable_funnel(port)
|
|
||||||
|
|
||||||
if success:
|
|
||||||
# Update settings
|
|
||||||
self._settings.set_boolean(["enabled"], True)
|
|
||||||
self._settings.save()
|
|
||||||
|
|
||||||
public_url = self.tailscale_interface.get_public_url()
|
|
||||||
return {
|
|
||||||
"status": "success",
|
|
||||||
"message": "Tailscale Funnel enabled successfully",
|
|
||||||
"data": {
|
|
||||||
"public_url": public_url
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else:
|
|
||||||
return {"status": "error", "message": "Failed to enable Tailscale Funnel"}, 500
|
|
||||||
except TailscaleNotInstalledError as e:
|
|
||||||
return {"status": "error", "message": "Tailscale is not installed: {}".format(str(e))}, 500
|
|
||||||
except TailscaleError as e:
|
|
||||||
return {"status": "error", "message": "Tailscale error: {}".format(str(e))}, 500
|
|
||||||
except Exception as e:
|
|
||||||
self._logger.error("Unexpected error in enable_funnel: {}".format(str(e)))
|
|
||||||
return {"status": "error", "message": "Unexpected error: {}".format(str(e))}, 500
|
|
||||||
|
|
||||||
@octoprint.plugin.BlueprintPlugin.route("/disable", methods=["POST"])
|
|
||||||
def disable_funnel(self):
|
|
||||||
if not self.tailscale_interface:
|
|
||||||
return {"status": "error", "message": "Tailscale interface not initialized"}, 500
|
|
||||||
|
|
||||||
try:
|
|
||||||
port = self._settings.get_int(["port"])
|
|
||||||
success = self.tailscale_interface.disable_funnel(port)
|
|
||||||
|
|
||||||
if success:
|
|
||||||
# Update settings
|
|
||||||
self._settings.set_boolean(["enabled"], False)
|
|
||||||
self._settings.save()
|
|
||||||
|
|
||||||
return {
|
|
||||||
"status": "success",
|
|
||||||
"message": "Tailscale Funnel disabled successfully"
|
|
||||||
}
|
|
||||||
else:
|
|
||||||
return {"status": "error", "message": "Failed to disable Tailscale Funnel"}, 500
|
|
||||||
except TailscaleNotInstalledError as e:
|
|
||||||
return {"status": "error", "message": "Tailscale is not installed: {}".format(str(e))}, 500
|
|
||||||
except TailscaleError as e:
|
|
||||||
return {"status": "error", "message": "Tailscale error: {}".format(str(e))}, 500
|
|
||||||
except Exception as e:
|
|
||||||
self._logger.error("Unexpected error in disable_funnel: {}".format(str(e)))
|
|
||||||
return {"status": "error", "message": "Unexpected error: {}".format(str(e))}, 500
|
|
||||||
|
|
||||||
##~~ Softwareupdate hook
|
|
||||||
|
|
||||||
def get_update_information(self):
|
|
||||||
return dict(
|
|
||||||
tailscale_funnel=dict(
|
|
||||||
displayName="Tailscale Funnel Plugin",
|
|
||||||
displayVersion=self._plugin_version,
|
|
||||||
|
|
||||||
# version check: github repository
|
|
||||||
type="github_release",
|
|
||||||
user="markus",
|
|
||||||
repo="OctoPrint-Tailscale-Funnel",
|
|
||||||
current=self._plugin_version,
|
|
||||||
|
|
||||||
# update method: pip
|
|
||||||
pip="https://github.com/markus/OctoPrint-Tailscale-Funnel/archive/{target_version}.zip"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# If you want your plugin to be registered within OctoPrint under its own name,
|
|
||||||
# define it here.
|
|
||||||
__plugin_name__ = "Tailscale Funnel Plugin"
|
|
||||||
__plugin_pythoncompat__ = ">=3.7,<4" # python 3.7+
|
|
||||||
|
|
||||||
def __plugin_load__():
|
|
||||||
global __plugin_implementation__
|
|
||||||
__plugin_implementation__ = TailscaleFunnelPlugin()
|
|
||||||
|
|
||||||
global __plugin_hooks__
|
|
||||||
__plugin_hooks__ = {
|
|
||||||
"octoprint.plugin.softwareupdate.check_config": __plugin_implementation__.get_update_information
|
|
||||||
}
|
|
@@ -1,74 +0,0 @@
|
|||||||
# coding=utf-8
|
|
||||||
from __future__ import absolute_import
|
|
||||||
|
|
||||||
import threading
|
|
||||||
import time
|
|
||||||
|
|
||||||
|
|
||||||
class StatusMonitor:
|
|
||||||
def __init__(self, plugin, interval=30):
|
|
||||||
self.plugin = plugin
|
|
||||||
self.interval = interval
|
|
||||||
self._logger = plugin._logger
|
|
||||||
self._stop_event = threading.Event()
|
|
||||||
self._thread = None
|
|
||||||
|
|
||||||
def start(self):
|
|
||||||
"""
|
|
||||||
Start the status monitoring thread
|
|
||||||
"""
|
|
||||||
if self._thread is not None and self._thread.is_alive():
|
|
||||||
self._logger.warning("Status monitor is already running")
|
|
||||||
return
|
|
||||||
|
|
||||||
self._stop_event.clear()
|
|
||||||
self._thread = threading.Thread(target=self._monitor_loop)
|
|
||||||
self._thread.daemon = True
|
|
||||||
self._thread.start()
|
|
||||||
self._logger.info("Status monitor started")
|
|
||||||
|
|
||||||
def stop(self):
|
|
||||||
"""
|
|
||||||
Stop the status monitoring thread
|
|
||||||
"""
|
|
||||||
if self._thread is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
self._stop_event.set()
|
|
||||||
self._thread.join()
|
|
||||||
self._thread = None
|
|
||||||
self._logger.info("Status monitor stopped")
|
|
||||||
|
|
||||||
def _monitor_loop(self):
|
|
||||||
"""
|
|
||||||
Main monitoring loop
|
|
||||||
"""
|
|
||||||
while not self._stop_event.is_set():
|
|
||||||
try:
|
|
||||||
# Check status if tailscale interface is available
|
|
||||||
if self.plugin.tailscale_interface:
|
|
||||||
# Get current funnel status
|
|
||||||
funnel_enabled = self.plugin.tailscale_interface.is_funnel_enabled()
|
|
||||||
|
|
||||||
# Update settings if needed
|
|
||||||
current_setting = self.plugin._settings.get_boolean(["enabled"])
|
|
||||||
if funnel_enabled != current_setting:
|
|
||||||
self.plugin._settings.set_boolean(["enabled"], funnel_enabled)
|
|
||||||
self.plugin._settings.save()
|
|
||||||
self._logger.info("Funnel status updated in settings: {}".format(funnel_enabled))
|
|
||||||
|
|
||||||
# Send a notification to the frontend
|
|
||||||
self.plugin._plugin_manager.send_plugin_message(
|
|
||||||
self.plugin._identifier,
|
|
||||||
{
|
|
||||||
"type": "funnel_status_change",
|
|
||||||
"enabled": funnel_enabled
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self._logger.error("Error in status monitoring loop: {}".format(str(e)))
|
|
||||||
|
|
||||||
# Wait for the next interval or until stopped
|
|
||||||
if self._stop_event.wait(self.interval):
|
|
||||||
break
|
|
@@ -1,161 +0,0 @@
|
|||||||
# coding=utf-8
|
|
||||||
from __future__ import absolute_import
|
|
||||||
|
|
||||||
import subprocess
|
|
||||||
import json
|
|
||||||
import re
|
|
||||||
|
|
||||||
|
|
||||||
class TailscaleError(Exception):
|
|
||||||
"""Base exception for Tailscale interface errors"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class TailscaleNotInstalledError(TailscaleError):
|
|
||||||
"""Exception raised when Tailscale is not installed"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class TailscaleNotRunningError(TailscaleError):
|
|
||||||
"""Exception raised when Tailscale is not running"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class FunnelNotEnabledError(TailscaleError):
|
|
||||||
"""Exception raised when Funnel is not enabled"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class TailscaleInterface:
|
|
||||||
def __init__(self, logger=None):
|
|
||||||
self._logger = logger
|
|
||||||
|
|
||||||
def _run_command(self, command):
|
|
||||||
"""
|
|
||||||
Run a command and return the result
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
result = subprocess.run(command, shell=True, capture_output=True, text=True, timeout=30)
|
|
||||||
if result.returncode == 0:
|
|
||||||
return {"success": True, "output": result.stdout.strip(), "error": None}
|
|
||||||
else:
|
|
||||||
return {"success": False, "output": None, "error": result.stderr.strip()}
|
|
||||||
except subprocess.TimeoutExpired:
|
|
||||||
if self._logger:
|
|
||||||
self._logger.error("Command timed out: '{}'".format(command))
|
|
||||||
return {"success": False, "output": None, "error": "Command timed out"}
|
|
||||||
except Exception as e:
|
|
||||||
if self._logger:
|
|
||||||
self._logger.error("Error running command '{}': {}".format(command, str(e)))
|
|
||||||
return {"success": False, "output": None, "error": str(e)}
|
|
||||||
|
|
||||||
def is_tailscale_installed(self):
|
|
||||||
"""
|
|
||||||
Check if tailscale is installed
|
|
||||||
"""
|
|
||||||
result = self._run_command("which tailscale")
|
|
||||||
return result["success"] and result["output"] != ""
|
|
||||||
|
|
||||||
def is_tailscale_running(self):
|
|
||||||
"""
|
|
||||||
Check if tailscale is running
|
|
||||||
"""
|
|
||||||
result = self._run_command("tailscale status --json")
|
|
||||||
return result["success"]
|
|
||||||
|
|
||||||
def get_tailscale_status(self):
|
|
||||||
"""
|
|
||||||
Get tailscale status as JSON
|
|
||||||
"""
|
|
||||||
if not self.is_tailscale_installed():
|
|
||||||
raise TailscaleNotInstalledError("Tailscale is not installed")
|
|
||||||
|
|
||||||
result = self._run_command("tailscale status --json")
|
|
||||||
if result["success"]:
|
|
||||||
try:
|
|
||||||
return json.loads(result["output"])
|
|
||||||
except json.JSONDecodeError as e:
|
|
||||||
if self._logger:
|
|
||||||
self._logger.error("Error parsing tailscale status JSON: {}".format(str(e)))
|
|
||||||
raise TailscaleError("Error parsing tailscale status: {}".format(str(e)))
|
|
||||||
else:
|
|
||||||
raise TailscaleNotRunningError("Tailscale is not running: {}".format(result["error"]))
|
|
||||||
|
|
||||||
def is_funnel_enabled(self):
|
|
||||||
"""
|
|
||||||
Check if tailscale funnel is enabled
|
|
||||||
"""
|
|
||||||
if not self.is_tailscale_installed():
|
|
||||||
raise TailscaleNotInstalledError("Tailscale is not installed")
|
|
||||||
|
|
||||||
result = self._run_command("tailscale funnel status --json")
|
|
||||||
if result["success"]:
|
|
||||||
try:
|
|
||||||
status = json.loads(result["output"])
|
|
||||||
# Check if any funnel is active
|
|
||||||
for _, ports in status.get("AllowFunnel", {}).items():
|
|
||||||
if any(ports.values()):
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
except json.JSONDecodeError as e:
|
|
||||||
if self._logger:
|
|
||||||
self._logger.error("Error parsing funnel status JSON: {}".format(str(e)))
|
|
||||||
raise TailscaleError("Error parsing funnel status: {}".format(str(e)))
|
|
||||||
else:
|
|
||||||
# If the command fails, it might be because Funnel is not enabled
|
|
||||||
# Check if it's a specific error we can handle
|
|
||||||
if "funnel" in result["error"].lower() and "not enabled" in result["error"].lower():
|
|
||||||
return False
|
|
||||||
raise TailscaleError("Error checking funnel status: {}".format(result["error"]))
|
|
||||||
|
|
||||||
def enable_funnel(self, port):
|
|
||||||
"""
|
|
||||||
Enable tailscale funnel for the specified port
|
|
||||||
"""
|
|
||||||
if not self.is_tailscale_installed():
|
|
||||||
raise TailscaleNotInstalledError("Tailscale is not installed")
|
|
||||||
|
|
||||||
command = "tailscale funnel {} on".format(port)
|
|
||||||
result = self._run_command(command)
|
|
||||||
return result["success"]
|
|
||||||
|
|
||||||
def disable_funnel(self, port):
|
|
||||||
"""
|
|
||||||
Disable tailscale funnel for the specified port
|
|
||||||
"""
|
|
||||||
if not self.is_tailscale_installed():
|
|
||||||
raise TailscaleNotInstalledError("Tailscale is not installed")
|
|
||||||
|
|
||||||
command = "tailscale funnel {} off".format(port)
|
|
||||||
result = self._run_command(command)
|
|
||||||
return result["success"]
|
|
||||||
|
|
||||||
def get_public_url(self):
|
|
||||||
"""
|
|
||||||
Get the public URL for accessing the service via funnel
|
|
||||||
"""
|
|
||||||
if not self.is_tailscale_installed():
|
|
||||||
raise TailscaleNotInstalledError("Tailscale is not installed")
|
|
||||||
|
|
||||||
result = self._run_command("tailscale funnel status --json")
|
|
||||||
if result["success"]:
|
|
||||||
try:
|
|
||||||
status = json.loads(result["output"])
|
|
||||||
# Extract the public URL
|
|
||||||
# This is a simplified approach - in practice, you might need to parse more details
|
|
||||||
funnel_status = status.get("AllowFunnel", {})
|
|
||||||
if funnel_status:
|
|
||||||
# Get the first available funnel URL
|
|
||||||
for key, ports in funnel_status.items():
|
|
||||||
if any(ports.values()):
|
|
||||||
# Extract the domain from the key
|
|
||||||
match = re.search(r'https://([a-zA-Z0-9\-\.]+)', key)
|
|
||||||
if match:
|
|
||||||
return "https://{}".format(match.group(1))
|
|
||||||
return None
|
|
||||||
except (json.JSONDecodeError, KeyError) as e:
|
|
||||||
if self._logger:
|
|
||||||
self._logger.error("Error parsing funnel status for URL: {}".format(str(e)))
|
|
||||||
raise TailscaleError("Error parsing funnel status for URL: {}".format(str(e)))
|
|
||||||
else:
|
|
||||||
raise TailscaleError("Error getting funnel status: {}".format(result["error"]))
|
|
Reference in New Issue
Block a user