Commit 6baea83b authored by Your Name's avatar Your Name

Add TOR hidden service support (v0.5.0)

- Add full TOR hidden service support for exposing AISBF over TOR network
- Create aisbf/tor.py module with TorHiddenService class
- Add TorConfig model to config.py for TOR configuration
- Update config/aisbf.json with TOR settings
- Integrate TOR service into main.py with startup/shutdown handlers
- Add TOR configuration UI to dashboard settings with real-time status
- Add get_tor_status MCP tool for monitoring TOR service
- Update README.md and DOCUMENTATION.md with TOR guides
- Add stem library dependency to requirements.txt
- Bump version to 0.5.0 in pyproject.toml
- Support both ephemeral and persistent hidden services
- All AISBF endpoints accessible over TOR network
parent 96379f03
...@@ -621,6 +621,24 @@ This AI.PROMPT file is automatically updated when significant changes are made t ...@@ -621,6 +621,24 @@ This AI.PROMPT file is automatically updated when significant changes are made t
### Recent Updates ### Recent Updates
**2026-03-23 - TOR Hidden Service Support**
- Added full TOR hidden service support for exposing AISBF over TOR network
- Created aisbf/tor.py module with TorHiddenService class for managing TOR connections
- Added TorConfig model to config.py for TOR configuration management
- Updated config/aisbf.json with TOR configuration section (enabled, control_port, control_host, control_password, hidden_service_dir, hidden_service_port, socks_port, socks_host)
- Integrated TOR service into main.py with startup/shutdown event handlers
- Added TOR configuration UI to dashboard settings page with real-time status display
- Added get_tor_status MCP tool for monitoring TOR hidden service status (fullconfig access required)
- Updated README.md with comprehensive TOR configuration guide
- Updated DOCUMENTATION.md with detailed TOR setup, troubleshooting, and security considerations
- Added stem library dependency to requirements.txt
- Bumped version to 0.5.0 in pyproject.toml
- Supports both ephemeral (temporary) and persistent (fixed onion address) hidden services
- Dashboard displays TOR status, onion address, and provides configuration interface
- All AISBF endpoints (API, dashboard, MCP) accessible over TOR network
- Automatic TOR service initialization on startup when enabled
- Proper cleanup on shutdown to remove ephemeral services
**2026-03-23 - Configurable Error Cooldown Implementation** **2026-03-23 - Configurable Error Cooldown Implementation**
- Added `error_cooldown` field to Model class in models.py for model-specific cooldown configuration - Added `error_cooldown` field to Model class in models.py for model-specific cooldown configuration
- Added `default_error_cooldown` field to ProviderConfig in config.py for provider-level defaults - Added `default_error_cooldown` field to ProviderConfig in config.py for provider-level defaults
......
...@@ -96,6 +96,197 @@ Installs to: ...@@ -96,6 +96,197 @@ Installs to:
3. Copies default configs from installed location to `~/.aisbf/` 3. Copies default configs from installed location to `~/.aisbf/`
4. Loads configuration from `~/.aisbf/` on subsequent runs 4. Loads configuration from `~/.aisbf/` on subsequent runs
## TOR Hidden Service Support
AISBF includes full support for exposing the API and dashboard over the TOR network as a hidden service. This provides anonymous access to your AI proxy server without revealing your server's IP address.
### Prerequisites
**TOR Installation:**
```bash
# Ubuntu/Debian
sudo apt-get install tor
# CentOS/RHEL
sudo yum install tor
# macOS
brew install tor
```
**Python stem Library:**
```bash
pip install stem
```
**Enable TOR Control Port:**
Edit `/etc/tor/torrc` (or `~/.torrc` on macOS):
```
ControlPort 9051
CookieAuthentication 1
```
Restart TOR:
```bash
sudo systemctl restart tor # Linux
brew services restart tor # macOS
```
### Configuration
TOR hidden service can be configured via the dashboard or configuration file.
**Configuration File (`~/.aisbf/aisbf.json`):**
```json
{
"tor": {
"enabled": true,
"control_port": 9051,
"control_host": "127.0.0.1",
"control_password": null,
"hidden_service_dir": null,
"hidden_service_port": 80,
"socks_port": 9050,
"socks_host": "127.0.0.1"
}
}
```
**Configuration Options:**
- **`enabled`**: Enable/disable TOR hidden service (default: false)
- **`control_port`**: TOR control port (default: 9051)
- **`control_host`**: TOR control host (default: 127.0.0.1)
- **`control_password`**: Optional password for TOR control authentication
- **`hidden_service_dir`**: Directory for persistent hidden service keys (null for ephemeral)
- **`hidden_service_port`**: Port exposed on the hidden service (default: 80)
- **`socks_port`**: TOR SOCKS proxy port (default: 9050)
- **`socks_host`**: TOR SOCKS proxy host (default: 127.0.0.1)
### Ephemeral vs Persistent Hidden Services
**Ephemeral Hidden Service (Default):**
- Temporary service created on startup
- New onion address generated each time
- No files stored on disk
- Ideal for testing or temporary deployments
- Set `hidden_service_dir` to `null`
**Persistent Hidden Service:**
- Permanent service with fixed onion address
- Address persists across restarts
- Keys stored in specified directory
- Ideal for production use
- Set `hidden_service_dir` to a path (e.g., `~/.aisbf/tor_hidden_service`)
Example persistent configuration:
```json
{
"tor": {
"enabled": true,
"hidden_service_dir": "~/.aisbf/tor_hidden_service",
"hidden_service_port": 80
}
}
```
### Dashboard Configuration
1. Navigate to **Dashboard → Settings**
2. Scroll to **TOR Hidden Service** section
3. Enable **Enable TOR Hidden Service** checkbox
4. Configure options as needed
5. Save settings and restart server
The dashboard displays:
- Current TOR status (Active/Disabled/Error)
- Onion address (when active)
- Real-time status updates
### Accessing the Hidden Service
Once enabled, the onion address is displayed:
- In server logs on startup
- In Dashboard → Settings → TOR Hidden Service status
- Via MCP `get_tor_status` tool (fullconfig access required)
Access via TOR Browser or TOR-enabled client:
```
http://your-onion-address.onion/
```
All AISBF endpoints are available over TOR:
- API endpoints: `http://your-onion-address.onion/api/v1/chat/completions`
- Dashboard: `http://your-onion-address.onion/dashboard`
- MCP server: `http://your-onion-address.onion/mcp`
### Security Considerations
**Authentication:**
- TOR provides anonymity, not authentication
- Enable API authentication in AISBF settings
- Use strong dashboard passwords
- Consider IP-based access controls for clearnet access
**Best Practices:**
- Use persistent hidden services for production
- Regularly update TOR and AISBF
- Monitor access logs for suspicious activity
- Enable rate limiting
- Use HTTPS for clearnet access (TOR handles encryption)
**Network Isolation:**
- TOR traffic is automatically encrypted
- Hidden services don't reveal server IP
- Consider running AISBF in isolated environment
- Use firewall rules to restrict clearnet access
### Troubleshooting
**Connection Failed:**
- Verify TOR is running: `systemctl status tor` or `brew services list`
- Check TOR control port is enabled in torrc
- Verify control port is accessible: `telnet 127.0.0.1 9051`
- Check AISBF logs for detailed error messages
**Authentication Failed:**
- Verify CookieAuthentication is enabled in torrc
- Check control_password matches torrc configuration
- Ensure AISBF has permission to read TOR cookie file
**Onion Address Not Generated:**
- Check TOR logs: `journalctl -u tor` or `tail -f /var/log/tor/log`
- Verify hidden_service_dir permissions (if using persistent)
- Ensure stem library is installed: `pip list | grep stem`
**Service Not Accessible:**
- Verify AISBF is running and listening on configured port
- Check hidden_service_port matches AISBF local port
- Test local access first: `curl http://localhost:17765`
- Verify TOR Browser is configured correctly
### MCP Integration
The MCP server includes a `get_tor_status` tool for monitoring TOR hidden service status:
```json
{
"method": "tools/call",
"params": {
"name": "get_tor_status",
"arguments": {}
}
}
```
Response includes:
- `enabled`: Whether TOR is enabled
- `connected`: Connection status to TOR control port
- `onion_address`: Current onion address (if active)
- `service_id`: Service ID for ephemeral services
- `control_host` and `control_port`: TOR control connection details
## API Endpoints ## API Endpoints
### General Endpoints ### General Endpoints
......
...@@ -18,6 +18,7 @@ A modular proxy server for managing multiple AI provider integrations with unifi ...@@ -18,6 +18,7 @@ A modular proxy server for managing multiple AI provider integrations with unifi
- **Effective Context Tracking**: Reports total tokens used (effective_context) for every request - **Effective Context Tracking**: Reports total tokens used (effective_context) for every request
- **SSL/TLS Support**: Built-in HTTPS support with Let's Encrypt integration and automatic certificate renewal - **SSL/TLS Support**: Built-in HTTPS support with Let's Encrypt integration and automatic certificate renewal
- **Self-Signed Certificates**: Automatic generation of self-signed certificates for development/testing - **Self-Signed Certificates**: Automatic generation of self-signed certificates for development/testing
- **TOR Hidden Service**: Full support for exposing AISBF over TOR network as a hidden service
- **MCP Server**: Model Context Protocol server for remote agent configuration and model access (SSE and HTTP streaming) - **MCP Server**: Model Context Protocol server for remote agent configuration and model access (SSE and HTTP streaming)
## Author ## Author
...@@ -123,6 +124,109 @@ The system will automatically: ...@@ -123,6 +124,109 @@ The system will automatically:
- Check certificate expiry on startup - Check certificate expiry on startup
- Renew certificates when they expire within 30 days - Renew certificates when they expire within 30 days
### TOR Hidden Service Configuration
AISBF can be exposed over the TOR network as a hidden service, providing anonymous access to your AI proxy server.
#### Prerequisites
- TOR must be installed and running on your system
- TOR control port must be enabled (default: 9051)
**Installation:**
```bash
# Ubuntu/Debian
sudo apt-get install tor
# CentOS/RHEL
sudo yum install tor
# macOS
brew install tor
```
**Enable TOR Control Port:**
Edit `/etc/tor/torrc` (or `~/.torrc` on macOS) and add:
```
ControlPort 9051
CookieAuthentication 1
```
Then restart TOR:
```bash
sudo systemctl restart tor # Linux
brew services restart tor # macOS
```
#### Configuration
**Via Dashboard:**
1. Navigate to Dashboard → Settings
2. Scroll to "TOR Hidden Service" section
3. Enable "Enable TOR Hidden Service"
4. Configure settings:
- **Control Host**: TOR control port host (default: 127.0.0.1)
- **Control Port**: TOR control port (default: 9051)
- **Control Password**: Optional password for TOR control authentication
- **Hidden Service Directory**: Leave blank for ephemeral service, or specify path for persistent service
- **Hidden Service Port**: Port exposed on the hidden service (default: 80)
- **SOCKS Proxy Host**: TOR SOCKS proxy host (default: 127.0.0.1)
- **SOCKS Proxy Port**: TOR SOCKS proxy port (default: 9050)
5. Save settings and restart server
**Via Configuration File:**
Edit `~/.aisbf/aisbf.json`:
```json
{
"tor": {
"enabled": true,
"control_port": 9051,
"control_host": "127.0.0.1",
"control_password": null,
"hidden_service_dir": null,
"hidden_service_port": 80,
"socks_port": 9050,
"socks_host": "127.0.0.1"
}
}
```
#### Ephemeral vs Persistent Hidden Services
**Ephemeral (Default):**
- Temporary hidden service created on startup
- New onion address generated each time
- No files stored on disk
- Ideal for temporary or testing purposes
- Set `hidden_service_dir` to `null` or leave blank
**Persistent:**
- Permanent hidden service with fixed onion address
- Address persists across restarts
- Keys stored in specified directory
- Ideal for production use
- Set `hidden_service_dir` to a path (e.g., `~/.aisbf/tor_hidden_service`)
#### Accessing Your Hidden Service
Once enabled, the onion address will be displayed:
- In the server logs on startup
- In the Dashboard → Settings → TOR Hidden Service status section
- Via MCP `get_tor_status` tool (requires fullconfig access)
Access your service via TOR Browser or any TOR-enabled client:
```
http://your-onion-address.onion/
```
#### Security Considerations
- TOR hidden services provide anonymity but not authentication
- Enable API authentication in AISBF settings for additional security
- Use strong dashboard passwords
- Consider using persistent hidden services for production
- Monitor access logs for suspicious activity
- Keep TOR and AISBF updated
### Provider-Level Defaults ### Provider-Level Defaults
Providers can now define default settings that cascade to all models: Providers can now define default settings that cascade to all models:
......
...@@ -93,12 +93,24 @@ class AutoselectConfig(BaseModel): ...@@ -93,12 +93,24 @@ class AutoselectConfig(BaseModel):
fallback: str fallback: str
available_models: List[AutoselectModelInfo] available_models: List[AutoselectModelInfo]
class TorConfig(BaseModel):
"""Configuration for TOR hidden service"""
enabled: bool = False
control_port: int = 9051
control_host: str = "127.0.0.1"
control_password: Optional[str] = None
hidden_service_dir: Optional[str] = None
hidden_service_port: int = 80
socks_port: int = 9050
socks_host: str = "127.0.0.1"
class AppConfig(BaseModel): class AppConfig(BaseModel):
providers: Dict[str, ProviderConfig] providers: Dict[str, ProviderConfig]
rotations: Dict[str, RotationConfig] rotations: Dict[str, RotationConfig]
autoselect: Dict[str, AutoselectConfig] autoselect: Dict[str, AutoselectConfig]
condensation: Optional[CondensationConfig] = None condensation: Optional[CondensationConfig] = None
error_tracking: Dict[str, Dict] error_tracking: Dict[str, Dict]
tor: Optional[TorConfig] = None
class Config: class Config:
def __init__(self): def __init__(self):
...@@ -116,6 +128,7 @@ class Config: ...@@ -116,6 +128,7 @@ class Config:
self._load_rotations() self._load_rotations()
self._load_autoselect() self._load_autoselect()
self._load_condensation() self._load_condensation()
self._load_tor()
self._initialize_error_tracking() self._initialize_error_tracking()
self._log_configuration_summary() self._log_configuration_summary()
...@@ -319,6 +332,36 @@ class Config: ...@@ -319,6 +332,36 @@ class Config:
self._loaded_files['condensation'] = str(providers_path.absolute()) self._loaded_files['condensation'] = str(providers_path.absolute())
logger.info(f"Loaded condensation config: provider_id={self.condensation.provider_id}, model={self.condensation.model}, enabled={self.condensation.enabled}") logger.info(f"Loaded condensation config: provider_id={self.condensation.provider_id}, model={self.condensation.model}, enabled={self.condensation.enabled}")
logger.info(f"=== Config._load_condensation END ===") logger.info(f"=== Config._load_condensation END ===")
def _load_tor(self):
"""Load TOR configuration from aisbf.json"""
import logging
logger = logging.getLogger(__name__)
logger.info(f"=== Config._load_tor START ===")
aisbf_path = Path.home() / '.aisbf' / 'aisbf.json'
logger.info(f"Looking for TOR config in: {aisbf_path}")
if not aisbf_path.exists():
logger.info(f"User config not found, falling back to source config")
# Fallback to source config if user config doesn't exist
try:
source_dir = self._get_config_source_dir()
aisbf_path = source_dir / 'aisbf.json'
logger.info(f"Using source config at: {aisbf_path}")
except FileNotFoundError:
logger.warning("Could not find aisbf.json for TOR config")
self.tor = TorConfig()
return
logger.info(f"Loading TOR config from: {aisbf_path}")
with open(aisbf_path) as f:
data = json.load(f)
tor_data = data.get('tor', {})
self.tor = TorConfig(**tor_data)
self._loaded_files['tor'] = str(aisbf_path.absolute())
logger.info(f"Loaded TOR config: enabled={self.tor.enabled}, control_port={self.tor.control_port}, hidden_service_port={self.tor.hidden_service_port}")
logger.info(f"=== Config._load_tor END ===")
def _initialize_error_tracking(self): def _initialize_error_tracking(self):
self.error_tracking = {} self.error_tracking = {}
...@@ -374,5 +417,8 @@ class Config: ...@@ -374,5 +417,8 @@ class Config:
def get_condensation(self) -> CondensationConfig: def get_condensation(self) -> CondensationConfig:
return self.condensation return self.condensation
def get_tor(self) -> TorConfig:
return self.tor
config = Config() config = Config()
...@@ -371,6 +371,15 @@ class MCPServer: ...@@ -371,6 +371,15 @@ class MCPServer:
"required": ["server_data"] "required": ["server_data"]
} }
}, },
{
"name": "get_tor_status",
"description": "Get TOR hidden service status (requires fullconfig access)",
"inputSchema": {
"type": "object",
"properties": {},
"required": []
}
},
{ {
"name": "delete_autoselect_config", "name": "delete_autoselect_config",
"description": "Delete an autoselect configuration (requires fullconfig access)", "description": "Delete an autoselect configuration (requires fullconfig access)",
...@@ -456,6 +465,7 @@ class MCPServer: ...@@ -456,6 +465,7 @@ class MCPServer:
'set_provider_config': self._set_provider_config, 'set_provider_config': self._set_provider_config,
'get_server_config': self._get_server_config, 'get_server_config': self._get_server_config,
'set_server_config': self._set_server_config, 'set_server_config': self._set_server_config,
'get_tor_status': self._get_tor_status,
'delete_autoselect_config': self._delete_autoselect_config, 'delete_autoselect_config': self._delete_autoselect_config,
'delete_rotation_config': self._delete_rotation_config, 'delete_rotation_config': self._delete_rotation_config,
'delete_provider_config': self._delete_provider_config, 'delete_provider_config': self._delete_provider_config,
...@@ -855,6 +865,38 @@ class MCPServer: ...@@ -855,6 +865,38 @@ class MCPServer:
return {"status": "success", "message": "Server config saved. Restart server for changes to take effect."} return {"status": "success", "message": "Server config saved. Restart server for changes to take effect."}
async def _get_tor_status(self, args: Dict) -> Dict:
"""Get TOR hidden service status"""
# Import tor_service from main module
try:
import main
tor_service = getattr(main, 'tor_service', None)
if tor_service:
status = tor_service.get_status()
return {"tor_status": status}
else:
return {
"tor_status": {
"enabled": False,
"connected": False,
"onion_address": None,
"service_id": None,
"control_host": None,
"control_port": None,
"hidden_service_port": None
}
}
except Exception as e:
logger.error(f"Error getting TOR status: {e}")
return {
"tor_status": {
"enabled": False,
"connected": False,
"error": str(e)
}
}
async def _delete_autoselect_config(self, args: Dict) -> Dict: async def _delete_autoselect_config(self, args: Dict) -> Dict:
"""Delete autoselect configuration""" """Delete autoselect configuration"""
autoselect_id = args.get('autoselect_id') autoselect_id = args.get('autoselect_id')
......
"""
Copyright (C) 2026 Stefy Lanza <stefy@nexlab.net>
AISBF - AI Service Broker Framework || AI Should Be Free
TOR hidden service management for AISBF.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Why did the programmer quit his job? Because he didn't get arrays!
"""
import logging
from pathlib import Path
from typing import Optional, Dict, Any
import os
logger = logging.getLogger(__name__)
class TorHiddenService:
"""
Manages TOR hidden service for AISBF.
This class handles:
- Connection to TOR control port
- Creation of hidden services (ephemeral or persistent)
- Retrieval of onion addresses
- Status monitoring
"""
def __init__(self, tor_config):
"""
Initialize TOR hidden service manager.
Args:
tor_config: TorConfig object with TOR settings
"""
self.config = tor_config
self.controller = None
self.onion_address = None
self.service_id = None
self._is_connected = False
def is_enabled(self) -> bool:
"""Check if TOR is enabled in configuration"""
return self.config.enabled if self.config else False
def is_connected(self) -> bool:
"""Check if connected to TOR control port"""
return self._is_connected
def connect(self) -> bool:
"""
Connect to TOR control port.
Returns:
bool: True if connection successful, False otherwise
"""
if not self.is_enabled():
logger.info("TOR is not enabled in configuration")
return False
try:
from stem.control import Controller
from stem import SocketError
logger.info(f"Connecting to TOR control port at {self.config.control_host}:{self.config.control_port}")
self.controller = Controller.from_port(
address=self.config.control_host,
port=self.config.control_port
)
# Authenticate
if self.config.control_password:
logger.info("Authenticating with TOR control port using password")
self.controller.authenticate(password=self.config.control_password)
else:
logger.info("Authenticating with TOR control port (no password)")
self.controller.authenticate()
self._is_connected = True
logger.info("Successfully connected to TOR control port")
return True
except ImportError:
logger.error("stem library not installed. Install with: pip install stem")
return False
except SocketError as e:
logger.error(f"Failed to connect to TOR control port: {e}")
logger.error("Make sure TOR is running and ControlPort is configured")
return False
except Exception as e:
logger.error(f"Error connecting to TOR: {e}")
return False
def create_hidden_service(self, local_port: int) -> Optional[str]:
"""
Create a TOR hidden service.
Args:
local_port: Local port where AISBF is running
Returns:
str: Onion address if successful, None otherwise
"""
if not self._is_connected:
logger.error("Not connected to TOR control port")
return None
try:
from stem.control import Controller
# Determine if we should use persistent or ephemeral hidden service
if self.config.hidden_service_dir:
# Persistent hidden service
hidden_service_dir = Path(self.config.hidden_service_dir).expanduser()
hidden_service_dir.mkdir(parents=True, exist_ok=True)
logger.info(f"Creating persistent hidden service in {hidden_service_dir}")
# Set hidden service configuration
self.controller.set_options([
('HiddenServiceDir', str(hidden_service_dir)),
('HiddenServicePort', f'{self.config.hidden_service_port} 127.0.0.1:{local_port}')
])
# Read the hostname file to get onion address
hostname_file = hidden_service_dir / 'hostname'
if hostname_file.exists():
self.onion_address = hostname_file.read_text().strip()
logger.info(f"Persistent hidden service created: {self.onion_address}")
else:
logger.error("Failed to read onion address from hostname file")
return None
else:
# Ephemeral hidden service
logger.info("Creating ephemeral hidden service")
response = self.controller.create_ephemeral_hidden_service(
ports={self.config.hidden_service_port: local_port},
await_publication=True
)
self.onion_address = f"{response.service_id}.onion"
self.service_id = response.service_id
logger.info(f"Ephemeral hidden service created: {self.onion_address}")
return self.onion_address
except Exception as e:
logger.error(f"Error creating hidden service: {e}")
return None
def get_onion_address(self) -> Optional[str]:
"""
Get the onion address of the hidden service.
Returns:
str: Onion address if available, None otherwise
"""
return self.onion_address
def get_status(self) -> Dict[str, Any]:
"""
Get status information about the TOR hidden service.
Returns:
dict: Status information
"""
return {
'enabled': self.is_enabled(),
'connected': self._is_connected,
'onion_address': self.onion_address,
'service_id': self.service_id,
'control_host': self.config.control_host if self.config else None,
'control_port': self.config.control_port if self.config else None,
'hidden_service_port': self.config.hidden_service_port if self.config else None
}
def disconnect(self):
"""Disconnect from TOR control port and cleanup"""
if self.controller:
try:
# Remove ephemeral hidden service if it exists
if self.service_id:
logger.info(f"Removing ephemeral hidden service: {self.service_id}")
self.controller.remove_ephemeral_hidden_service(self.service_id)
self.controller.close()
logger.info("Disconnected from TOR control port")
except Exception as e:
logger.error(f"Error disconnecting from TOR: {e}")
finally:
self.controller = None
self._is_connected = False
self.onion_address = None
self.service_id = None
def __enter__(self):
"""Context manager entry"""
self.connect()
return self
def __exit__(self, exc_type, exc_val, exc_tb):
"""Context manager exit"""
self.disconnect()
def setup_tor_hidden_service(tor_config, local_port: int) -> Optional[TorHiddenService]:
"""
Setup TOR hidden service for AISBF.
Args:
tor_config: TorConfig object
local_port: Local port where AISBF is running
Returns:
TorHiddenService: Configured hidden service or None if disabled/failed
"""
if not tor_config or not tor_config.enabled:
logger.info("TOR hidden service is disabled")
return None
logger.info("Setting up TOR hidden service...")
tor_service = TorHiddenService(tor_config)
if not tor_service.connect():
logger.error("Failed to connect to TOR control port")
return None
onion_address = tor_service.create_hidden_service(local_port)
if onion_address:
logger.info("=" * 80)
logger.info("=== TOR HIDDEN SERVICE ACTIVE ===")
logger.info("=" * 80)
logger.info(f"Onion Address: {onion_address}")
logger.info(f"Hidden Service Port: {tor_config.hidden_service_port}")
logger.info(f"Local Port: {local_port}")
logger.info("=" * 80)
return tor_service
else:
logger.error("Failed to create TOR hidden service")
tor_service.disconnect()
return None
...@@ -32,5 +32,15 @@ ...@@ -32,5 +32,15 @@
"internal_model": { "internal_model": {
"condensation_model_id": "huihui-ai/Qwen2.5-0.5B-Instruct-abliterated-v3", "condensation_model_id": "huihui-ai/Qwen2.5-0.5B-Instruct-abliterated-v3",
"autoselect_model_id": "huihui-ai/Qwen2.5-0.5B-Instruct-abliterated-v3" "autoselect_model_id": "huihui-ai/Qwen2.5-0.5B-Instruct-abliterated-v3"
},
"tor": {
"enabled": false,
"control_port": 9051,
"control_host": "127.0.0.1",
"control_password": null,
"hidden_service_dir": null,
"hidden_service_port": 80,
"socks_port": 9050,
"socks_host": "127.0.0.1"
} }
} }
...@@ -31,6 +31,7 @@ from aisbf.models import ChatCompletionRequest, ChatCompletionResponse ...@@ -31,6 +31,7 @@ from aisbf.models import ChatCompletionRequest, ChatCompletionResponse
from aisbf.handlers import RequestHandler, RotationHandler, AutoselectHandler from aisbf.handlers import RequestHandler, RotationHandler, AutoselectHandler
from aisbf.mcp import mcp_server, MCPAuthLevel, load_mcp_config from aisbf.mcp import mcp_server, MCPAuthLevel, load_mcp_config
from aisbf.database import initialize_database from aisbf.database import initialize_database
from aisbf.tor import setup_tor_hidden_service, TorHiddenService
from starlette.middleware.sessions import SessionMiddleware from starlette.middleware.sessions import SessionMiddleware
from starlette.middleware.base import BaseHTTPMiddleware from starlette.middleware.base import BaseHTTPMiddleware
from starlette.datastructures import Headers from starlette.datastructures import Headers
...@@ -480,6 +481,7 @@ autoselect_handler = None ...@@ -480,6 +481,7 @@ autoselect_handler = None
server_config = None server_config = None
config = None config = None
_initialized = False _initialized = False
tor_service = None
# Model cache for dynamically fetched provider models # Model cache for dynamically fetched provider models
_model_cache = {} _model_cache = {}
...@@ -664,7 +666,7 @@ async def get_provider_models(provider_id: str, provider_config) -> list: ...@@ -664,7 +666,7 @@ async def get_provider_models(provider_id: str, provider_config) -> list:
@app.on_event("startup") @app.on_event("startup")
async def startup_event(): async def startup_event():
"""Initialize app on startup (for uvicorn import case).""" """Initialize app on startup (for uvicorn import case)."""
global config, server_config, _cache_refresh_task global config, server_config, _cache_refresh_task, tor_service
if not _initialized: if not _initialized:
# Use environment variable for config dir if set # Use environment variable for config dir if set
custom_config_dir = get_config_dir() custom_config_dir = get_config_dir()
...@@ -689,9 +691,23 @@ async def startup_event(): ...@@ -689,9 +691,23 @@ async def startup_event():
if 'condensation' in config._loaded_files: if 'condensation' in config._loaded_files:
logger.info(f"Condensation: {config._loaded_files['condensation']}") logger.info(f"Condensation: {config._loaded_files['condensation']}")
if 'tor' in config._loaded_files:
logger.info(f"TOR: {config._loaded_files['tor']}")
logger.info("=" * 80) logger.info("=" * 80)
logger.info("") logger.info("")
# Setup TOR hidden service if enabled
if config and hasattr(config, 'tor') and config.tor:
tor_config = config.tor
if tor_config.enabled:
local_port = server_config.get('port', 17765) if server_config else 17765
tor_service = setup_tor_hidden_service(tor_config, local_port)
if tor_service:
logger.info("TOR hidden service successfully initialized")
else:
logger.warning("TOR hidden service initialization failed")
# Start background task for model cache refresh # Start background task for model cache refresh
if _cache_refresh_task is None: if _cache_refresh_task is None:
_cache_refresh_task = asyncio.create_task(refresh_model_cache()) _cache_refresh_task = asyncio.create_task(refresh_model_cache())
...@@ -744,6 +760,17 @@ async def startup_event(): ...@@ -744,6 +760,17 @@ async def startup_event():
logger.info(f"Available rotations: {list(config.rotations.keys()) if config else []}") logger.info(f"Available rotations: {list(config.rotations.keys()) if config else []}")
logger.info(f"Available autoselect: {list(config.autoselect.keys()) if config else []}") logger.info(f"Available autoselect: {list(config.autoselect.keys()) if config else []}")
@app.on_event("shutdown")
async def shutdown_event():
"""Cleanup on shutdown"""
global tor_service
# Cleanup TOR hidden service
if tor_service:
logger.info("Shutting down TOR hidden service...")
tor_service.disconnect()
logger.info("TOR hidden service shutdown complete")
# Authentication middleware # Authentication middleware
@app.middleware("http") @app.middleware("http")
async def auth_middleware(request: Request, call_next): async def auth_middleware(request: Request, call_next):
...@@ -1292,7 +1319,15 @@ async def dashboard_settings_save( ...@@ -1292,7 +1319,15 @@ async def dashboard_settings_save(
autoselect_model_id: str = Form(...), autoselect_model_id: str = Form(...),
mcp_enabled: bool = Form(False), mcp_enabled: bool = Form(False),
autoselect_tokens: str = Form(""), autoselect_tokens: str = Form(""),
fullconfig_tokens: str = Form("") fullconfig_tokens: str = Form(""),
tor_enabled: bool = Form(False),
tor_control_port: int = Form(9051),
tor_control_host: str = Form("127.0.0.1"),
tor_control_password: str = Form(""),
tor_hidden_service_dir: str = Form(""),
tor_hidden_service_port: int = Form(80),
tor_socks_port: int = Form(9050),
tor_socks_host: str = Form("127.0.0.1")
): ):
"""Save server settings""" """Save server settings"""
auth_check = require_dashboard_auth(request) auth_check = require_dashboard_auth(request)
...@@ -1327,6 +1362,18 @@ async def dashboard_settings_save( ...@@ -1327,6 +1362,18 @@ async def dashboard_settings_save(
aisbf_config['mcp']['autoselect_tokens'] = [t.strip() for t in autoselect_tokens.split('\n') if t.strip()] aisbf_config['mcp']['autoselect_tokens'] = [t.strip() for t in autoselect_tokens.split('\n') if t.strip()]
aisbf_config['mcp']['fullconfig_tokens'] = [t.strip() for t in fullconfig_tokens.split('\n') if t.strip()] aisbf_config['mcp']['fullconfig_tokens'] = [t.strip() for t in fullconfig_tokens.split('\n') if t.strip()]
# Update TOR config
if 'tor' not in aisbf_config:
aisbf_config['tor'] = {}
aisbf_config['tor']['enabled'] = tor_enabled
aisbf_config['tor']['control_port'] = tor_control_port
aisbf_config['tor']['control_host'] = tor_control_host
aisbf_config['tor']['control_password'] = tor_control_password if tor_control_password else None
aisbf_config['tor']['hidden_service_dir'] = tor_hidden_service_dir if tor_hidden_service_dir else None
aisbf_config['tor']['hidden_service_port'] = tor_hidden_service_port
aisbf_config['tor']['socks_port'] = tor_socks_port
aisbf_config['tor']['socks_host'] = tor_socks_host
# Save config # Save config
config_path = Path.home() / '.aisbf' / 'aisbf.json' config_path = Path.home() / '.aisbf' / 'aisbf.json'
config_path.parent.mkdir(parents=True, exist_ok=True) config_path.parent.mkdir(parents=True, exist_ok=True)
...@@ -1364,6 +1411,30 @@ async def dashboard_restart(request: Request): ...@@ -1364,6 +1411,30 @@ async def dashboard_restart(request: Request):
return JSONResponse({"message": "Server is restarting..."}) return JSONResponse({"message": "Server is restarting..."})
@app.get("/dashboard/tor/status")
async def dashboard_tor_status(request: Request):
"""Get TOR hidden service status"""
auth_check = require_dashboard_auth(request)
if auth_check:
return auth_check
global tor_service
if tor_service:
status = tor_service.get_status()
else:
status = {
'enabled': False,
'connected': False,
'onion_address': None,
'service_id': None,
'control_host': None,
'control_port': None,
'hidden_service_port': None
}
return JSONResponse(status)
@app.get("/dashboard/docs", response_class=HTMLResponse) @app.get("/dashboard/docs", response_class=HTMLResponse)
async def dashboard_docs(request: Request): async def dashboard_docs(request: Request):
"""Display documentation""" """Display documentation"""
......
...@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" ...@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "aisbf" name = "aisbf"
version = "0.4.0" version = "0.5.0"
description = "AISBF - AI Service Broker Framework || AI Should Be Free - A modular proxy server for managing multiple AI provider integrations" description = "AISBF - AI Service Broker Framework || AI Should Be Free - A modular proxy server for managing multiple AI provider integrations"
readme = "README.md" readme = "README.md"
license = "GPL-3.0-or-later" license = "GPL-3.0-or-later"
......
...@@ -17,4 +17,5 @@ jinja2 ...@@ -17,4 +17,5 @@ jinja2
itsdangerous itsdangerous
bs4 bs4
protobuf>=3.20,<4 protobuf>=3.20,<4
markdown markdown
\ No newline at end of file stem
\ No newline at end of file
...@@ -132,9 +132,74 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. ...@@ -132,9 +132,74 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<small style="color: #666; display: block; margin-top: 5px;">Used when autoselect selection_model is set to "internal"</small> <small style="color: #666; display: block; margin-top: 5px;">Used when autoselect selection_model is set to "internal"</small>
</div> </div>
<h3 style="margin: 30px 0 20px;">TOR Hidden Service</h3>
<div id="tor-status" style="margin-bottom: 20px; padding: 15px; background: #0f3460; border-radius: 6px; border-left: 4px solid #16213e;">
<div style="display: flex; align-items: center; gap: 10px; margin-bottom: 10px;">
<strong>Status:</strong>
<span id="tor-status-text" style="color: #666;">Loading...</span>
</div>
<div id="tor-onion-address" style="display: none; margin-top: 10px;">
<strong>Onion Address:</strong>
<code id="tor-onion-value" style="background: #16213e; padding: 5px 10px; border-radius: 4px; display: inline-block; margin-left: 10px;"></code>
</div>
</div>
<div class="form-group">
<label>
<input type="checkbox" name="tor_enabled" id="tor_enabled" {% if config.tor and config.tor.enabled %}checked{% endif %} onchange="toggleTorFields()">
Enable TOR Hidden Service
</label>
<small style="color: #666; display: block; margin-top: 5px;">Expose AISBF over TOR network as a hidden service</small>
</div>
<div id="tor-fields" style="display: {% if config.tor and config.tor.enabled %}block{% else %}none{% endif %};">
<div class="form-group">
<label for="tor_control_host">TOR Control Host</label>
<input type="text" id="tor_control_host" name="tor_control_host" value="{{ config.tor.control_host if config.tor else '127.0.0.1' }}">
<small style="color: #666; display: block; margin-top: 5px;">TOR control port host (default: 127.0.0.1)</small>
</div>
<div class="form-group">
<label for="tor_control_port">TOR Control Port</label>
<input type="number" id="tor_control_port" name="tor_control_port" value="{{ config.tor.control_port if config.tor else 9051 }}">
<small style="color: #666; display: block; margin-top: 5px;">TOR control port (default: 9051)</small>
</div>
<div class="form-group">
<label for="tor_control_password">TOR Control Password</label>
<input type="password" id="tor_control_password" name="tor_control_password" value="{{ config.tor.control_password if config.tor and config.tor.control_password else '' }}" placeholder="Leave blank if no password">
<small style="color: #666; display: block; margin-top: 5px;">Password for TOR control port authentication (optional)</small>
</div>
<div class="form-group">
<label for="tor_hidden_service_dir">Hidden Service Directory</label>
<input type="text" id="tor_hidden_service_dir" name="tor_hidden_service_dir" value="{{ config.tor.hidden_service_dir if config.tor and config.tor.hidden_service_dir else '' }}" placeholder="Leave blank for ephemeral service">
<small style="color: #666; display: block; margin-top: 5px;">Directory for persistent hidden service. Leave blank for ephemeral (temporary) service.</small>
</div>
<div class="form-group">
<label for="tor_hidden_service_port">Hidden Service Port</label>
<input type="number" id="tor_hidden_service_port" name="tor_hidden_service_port" value="{{ config.tor.hidden_service_port if config.tor else 80 }}">
<small style="color: #666; display: block; margin-top: 5px;">Port exposed on the hidden service (default: 80)</small>
</div>
<div class="form-group">
<label for="tor_socks_host">SOCKS Proxy Host</label>
<input type="text" id="tor_socks_host" name="tor_socks_host" value="{{ config.tor.socks_host if config.tor else '127.0.0.1' }}">
<small style="color: #666; display: block; margin-top: 5px;">TOR SOCKS proxy host (default: 127.0.0.1)</small>
</div>
<div class="form-group">
<label for="tor_socks_port">SOCKS Proxy Port</label>
<input type="number" id="tor_socks_port" name="tor_socks_port" value="{{ config.tor.socks_port if config.tor else 9050 }}">
<small style="color: #666; display: block; margin-top: 5px;">TOR SOCKS proxy port (default: 9050)</small>
</div>
</div>
<div style="display: flex; gap: 10px; margin-top: 30px;"> <div style="display: flex; gap: 10px; margin-top: 30px;">
<button type="submit" class="btn">Save Settings</button> <button type="submit" class="btn">Save Settings</button>
<a href="/dashboard" class="btn btn-secondary">Cancel</a> <a href="{{ url_for(request, '/dashboard') }}" class="btn btn-secondary">Cancel</a>
</div> </div>
</form> </form>
...@@ -149,5 +214,53 @@ function toggleSSLFields() { ...@@ -149,5 +214,53 @@ function toggleSSLFields() {
sslFields.style.display = 'none'; sslFields.style.display = 'none';
} }
} }
function toggleTorFields() {
const torEnabled = document.getElementById('tor_enabled').checked;
const torFields = document.getElementById('tor-fields');
if (torEnabled) {
torFields.style.display = 'block';
} else {
torFields.style.display = 'none';
}
}
async function checkTorStatus() {
try {
const response = await fetch('{{ url_for(request, "/dashboard/tor/status") }}');
const status = await response.json();
const statusText = document.getElementById('tor-status-text');
const onionAddressDiv = document.getElementById('tor-onion-address');
const onionValue = document.getElementById('tor-onion-value');
if (status.enabled && status.connected && status.onion_address) {
statusText.textContent = 'Active';
statusText.style.color = '#4caf50';
onionAddressDiv.style.display = 'block';
onionValue.textContent = status.onion_address;
} else if (status.enabled && !status.connected) {
statusText.textContent = 'Enabled but not connected';
statusText.style.color = '#ff9800';
onionAddressDiv.style.display = 'none';
} else {
statusText.textContent = 'Disabled';
statusText.style.color = '#666';
onionAddressDiv.style.display = 'none';
}
} catch (error) {
console.error('Error checking TOR status:', error);
document.getElementById('tor-status-text').textContent = 'Error checking status';
document.getElementById('tor-status-text').style.color = '#f44336';
}
}
// Check TOR status on page load
document.addEventListener('DOMContentLoaded', function() {
checkTorStatus();
// Refresh status every 30 seconds
setInterval(checkTorStatus, 30000);
});
</script> </script>
{% endblock %} {% endblock %}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment