Skip to content

Latest commit

 

History

History
677 lines (535 loc) · 16.8 KB

File metadata and controls

677 lines (535 loc) · 16.8 KB

WebService Documentation

The WebService class provides static file serving capabilities for the SignalWire SDK. It follows the same architectural pattern as SearchService, allowing it to run as a standalone service or alongside your AI agents.

Table of Contents

Overview

WebService is designed to serve static files with configurable security features. It's perfect for:

  • Serving agent documentation and API specs
  • Hosting static assets (images, CSS, JavaScript)
  • Serving generated reports and exports
  • Providing configuration files and templates
  • Hosting agent UI components

Key Features

  • Multiple directory mounting - Serve different directories at different URL paths
  • Security-first design - Authentication, CORS, security headers, file filtering
  • HTTPS support - Full SSL/TLS support with PEM files
  • Directory browsing - Optional HTML directory listings
  • MIME type handling - Automatic content-type detection
  • Path traversal protection - Prevents access outside designated directories
  • File filtering - Allow/block specific file extensions

Installation

WebService is included in the core SignalWire SDK:

pip install signalwire-sdk

Quick Start

from signalwire import WebService

# Create a service to serve files
service = WebService(
    port=8002,
    directories={
        "/docs": "./documentation",
        "/assets": "./static/assets"
    }
)

# Start the service
service.start()
# Service available at http://localhost:8002
# Basic Auth: dev:w00t (auto-generated)

Configuration

WebService can be configured through multiple methods (in order of priority):

1. Constructor Parameters

service = WebService(
    port=8002,                          # Port to bind to
    directories={                       # URL path to directory mappings
        "/docs": "./documentation",
        "/assets": "./static"
    },
    basic_auth=("admin", "secret"),    # Custom authentication
    enable_directory_browsing=True,     # Allow directory listings
    allowed_extensions=['.html', '.css', '.js'],  # Whitelist extensions
    blocked_extensions=['.env', '.key'],          # Blacklist extensions
    max_file_size=100 * 1024 * 1024,   # Max file size (100MB)
    enable_cors=True                    # Enable CORS headers
)

2. Environment Variables

# Basic authentication
export SWML_BASIC_AUTH_USER="admin"
export SWML_BASIC_AUTH_PASS="secretpassword"

# SSL/HTTPS configuration
export SWML_SSL_ENABLED=true
export SWML_SSL_CERT="/path/to/cert.pem"
export SWML_SSL_KEY="/path/to/key.pem"

# Security settings
export SWML_ALLOWED_HOSTS="example.com,*.example.com"
export SWML_CORS_ORIGINS="https://app.example.com"

3. Configuration File

Create a web.json or swml_web.json file:

{
    "service": {
        "port": 8002,
        "directories": {
            "/docs": "./documentation",
            "/api": "./api-specs",
            "/reports": "./generated/reports"
        },
        "enable_directory_browsing": true,
        "max_file_size": 52428800,
        "allowed_extensions": [".html", ".css", ".js", ".json", ".pdf"],
        "blocked_extensions": [".env", ".key", ".pem"]
    },
    "security": {
        "basic_auth": {
            "username": "admin",
            "password": "secure123"
        },
        "ssl_enabled": true,
        "ssl_cert": "/etc/ssl/certs/server.crt",
        "ssl_key": "/etc/ssl/private/server.key",
        "allowed_hosts": ["*"],
        "cors_origins": ["*"]
    }
}

Security Features

Basic Authentication

WebService implements HTTP Basic Authentication. Credentials can be set via:

  1. Constructor: basic_auth=("username", "password")
  2. Environment: SWML_BASIC_AUTH_USER and SWML_BASIC_AUTH_PASS
  3. Config file: security.basic_auth section
  4. Auto-generated: If not specified, generates random credentials

File Security

Default Blocked Extensions/Files

  • .env, .git, .gitignore
  • .key, .pem, .crt
  • .pyc, __pycache__
  • .DS_Store, .swp

Path Traversal Protection

WebService prevents access outside designated directories:

# These attempts will be blocked:
# GET /docs/../../../etc/passwd
# GET /docs/./././../config.json

File Size Limits

Default maximum file size is 100MB. Configure with:

service = WebService(max_file_size=50 * 1024 * 1024)  # 50MB

Security Headers

Automatically adds security headers to all responses:

  • X-Content-Type-Options: nosniff
  • X-Frame-Options: DENY
  • X-XSS-Protection: 1; mode=block
  • Strict-Transport-Security (when HTTPS is enabled)

HTTPS/SSL Support

WebService provides multiple ways to enable HTTPS:

Method 1: Environment Variables

# Using file paths
export SWML_SSL_CERT="/path/to/cert.pem"
export SWML_SSL_KEY="/path/to/key.pem"

# Or using inline PEM content
export SWML_SSL_CERT_INLINE="-----BEGIN CERTIFICATE-----
MIIDXTCCAkWgAwIBAgIJAKLdQVPy...
-----END CERTIFICATE-----"
export SWML_SSL_KEY_INLINE="-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQE...
-----END PRIVATE KEY-----"

Method 2: Direct Parameters

service = WebService(directories={"/docs": "./docs"})
service.start(
    ssl_cert="/path/to/cert.pem",
    ssl_key="/path/to/key.pem"
)
# Service available at https://localhost:8002

Method 3: Configuration File

{
    "security": {
        "ssl_enabled": true,
        "ssl_cert": "/etc/ssl/certs/server.crt",
        "ssl_key": "/etc/ssl/private/server.key"
    }
}

Generating Self-Signed Certificates

For development/testing:

# Generate a self-signed certificate
openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem \
    -days 365 -nodes -subj "/CN=localhost"

# Use with WebService
export SWML_SSL_CERT="cert.pem"
export SWML_SSL_KEY="key.pem"

API Endpoints

GET /health

Health check endpoint (no authentication required)

Response:

{
    "status": "healthy",
    "directories": ["/docs", "/assets"],
    "ssl_enabled": false,
    "auth_required": true,
    "directory_browsing": true
}

GET /

Root endpoint showing available directories

Response: HTML page listing all mounted directories

GET /{route}/{file_path}

Serve files from mounted directories

Parameters:

  • route: The mounted directory route (e.g., /docs)
  • file_path: Path to file within the directory

Response:

  • File content with appropriate MIME type
  • 404 if file not found
  • 403 if file type blocked or directory browsing disabled

Usage Examples

Basic File Serving

from signalwire import WebService

# Serve documentation
service = WebService(
    directories={
        "/docs": "./documentation",
        "/api": "./api-specs"
    }
)
service.start()

# Files accessible at:
# http://localhost:8002/docs/index.html
# http://localhost:8002/api/swagger.json

With Directory Browsing

service = WebService(
    directories={"/files": "./public"},
    enable_directory_browsing=True  # Allow browsing directories
)
service.start()

# Browse files at: http://localhost:8002/files/

Restricted File Types

# Only serve web assets
service = WebService(
    directories={"/web": "./www"},
    allowed_extensions=['.html', '.css', '.js', '.png', '.jpg', '.woff2'],
    enable_directory_browsing=False
)

Dynamic Directory Management

service = WebService()

# Add directories after initialization
service.add_directory("/docs", "./documentation")
service.add_directory("/reports", "./generated/reports")

# Remove a directory
service.remove_directory("/reports")

service.start()

With Custom Authentication

service = WebService(
    directories={"/private": "./sensitive-docs"},
    basic_auth=("admin", "super-secret-password")
)
service.start()

HTTPS with Let's Encrypt

# Assuming you have Let's Encrypt certificates
service = WebService(
    directories={"/secure": "./secure-files"}
)
service.start(
    ssl_cert="/etc/letsencrypt/live/example.com/fullchain.pem",
    ssl_key="/etc/letsencrypt/live/example.com/privkey.pem"
)
# Service available at https://example.com:8002

Multi-Environment Configuration

import os

# Development vs Production
if os.getenv("ENVIRONMENT") == "production":
    service = WebService(
        port=443,
        directories={"/": "./dist"},
        enable_directory_browsing=False
    )
    service.start(
        host="0.0.0.0",
        ssl_cert="/etc/ssl/certs/production.crt",
        ssl_key="/etc/ssl/private/production.key"
    )
else:
    service = WebService(
        port=8002,
        directories={"/": "./src"},
        enable_directory_browsing=True
    )
    service.start()

Deployment Patterns

Standalone Service

Run WebService as a dedicated static file server:

# web_server.py
from signalwire import WebService

if __name__ == "__main__":
    service = WebService(
        port=8002,
        directories={
            "/docs": "/var/www/docs",
            "/assets": "/var/www/assets",
            "/downloads": "/var/www/downloads"
        }
    )
    service.start()

Alongside AI Agents

Run WebService alongside your AI agents on different ports:

# main.py
from signalwire import AgentBase, WebService
import threading

# Start WebService in background
def run_web_service():
    web = WebService(
        port=8002,
        directories={"/docs": "./agent-docs"}
    )
    web.start()

# Start web service thread
web_thread = threading.Thread(target=run_web_service, daemon=True)
web_thread.start()

# Run your agent
class MyAgent(AgentBase):
    def __init__(self):
        super().__init__(name="My Agent")

agent = MyAgent()
agent.serve(port=3000)  # Agent on port 3000, WebService on 8002

Docker Deployment

FROM python:3.9-slim

WORKDIR /app

# Install SDK
RUN pip install signalwire-sdk

# Copy static files
COPY ./static /app/static
COPY ./web_config.json /app/web_config.json

# Expose port
EXPOSE 8002

# Run WebService
CMD ["python", "-c", "from signalwire import WebService; WebService(config_file='web_config.json').start()"]

Systemd Service

Create /etc/systemd/system/signalwire-web.service:

[Unit]
Description=SignalWire Web Service
After=network.target

[Service]
Type=simple
User=www-data
WorkingDirectory=/opt/signalwire
Environment="SWML_SSL_CERT=/etc/ssl/certs/server.crt"
Environment="SWML_SSL_KEY=/etc/ssl/private/server.key"
ExecStart=/usr/bin/python3 -c "from signalwire import WebService; WebService(directories={'/': '/var/www/html'}).start()"
Restart=always

[Install]
WantedBy=multi-user.target

Nginx Reverse Proxy

For production, use Nginx as a reverse proxy:

server {
    listen 80;
    server_name static.example.com;
    
    # Redirect to HTTPS
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    server_name static.example.com;
    
    ssl_certificate /etc/ssl/certs/example.com.crt;
    ssl_certificate_key /etc/ssl/private/example.com.key;
    
    location / {
        proxy_pass http://localhost:8002;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        
        # Cache static assets
        location ~* \.(jpg|jpeg|png|gif|ico|css|js)$ {
            proxy_pass http://localhost:8002;
            expires 1h;
            add_header Cache-Control "public, immutable";
        }
    }
}

Best Practices

Security

  1. Always use HTTPS in production - Protect data in transit
  2. Change default credentials - Never use auto-generated auth in production
  3. Restrict file types - Use allowed_extensions to whitelist safe files
  4. Disable directory browsing - Turn off in production environments
  5. Use reverse proxy - Put Nginx/Apache in front for additional security

Performance

  1. Set appropriate cache headers - WebService adds 1-hour cache by default
  2. Limit file sizes - Adjust max_file_size based on your needs
  3. Use CDN for static assets - Offload traffic for better performance
  4. Compress large files - Use gzip/brotli at reverse proxy level

Organization

  1. Separate content types - Use different routes for different file types
  2. Version your assets - Include version in path (e.g., /assets/v1/)
  3. Use index.html - Provide default files for directories
  4. Document your structure - Maintain clear directory organization

Troubleshooting

Common Issues

Issue: "FastAPI not available"

# Install FastAPI and uvicorn
pip install fastapi uvicorn

Issue: SSL certificate errors

# Check certificate paths
import os
print(os.path.exists("/path/to/cert.pem"))  # Should be True
print(os.path.exists("/path/to/key.pem"))   # Should be True

Issue: Permission denied

# Ensure read permissions on directories
chmod -R 755 /path/to/static/files

Issue: Directory not found

# Use absolute paths
import os
service = WebService(
    directories={
        "/docs": os.path.abspath("./documentation")
    }
)

Debug Logging

Enable debug logging to troubleshoot issues:

import logging
logging.basicConfig(level=logging.DEBUG)

service = WebService(directories={"/test": "./test"})
service.start()

API Reference

WebService Class

class WebService:
    def __init__(self,
                 port: int = 8002,
                 directories: Dict[str, str] = None,
                 basic_auth: Optional[Tuple[str, str]] = None,
                 config_file: Optional[str] = None,
                 enable_directory_browsing: bool = False,
                 allowed_extensions: Optional[list] = None,
                 blocked_extensions: Optional[list] = None,
                 max_file_size: int = 100 * 1024 * 1024,
                 enable_cors: bool = True)

Parameters

  • port: Port to bind to (default: 8002)
  • directories: Dictionary mapping URL paths to local directories
  • basic_auth: Tuple of (username, password) for authentication
  • config_file: Path to JSON configuration file
  • enable_directory_browsing: Allow directory listing (default: False)
  • allowed_extensions: List of allowed file extensions
  • blocked_extensions: List of blocked file extensions
  • max_file_size: Maximum file size in bytes (default: 100MB)
  • enable_cors: Enable CORS headers (default: True)

Methods

start()
def start(self,
          host: str = "0.0.0.0",
          port: Optional[int] = None,
          ssl_cert: Optional[str] = None,
          ssl_key: Optional[str] = None)

Start the web service.

add_directory()
def add_directory(self, route: str, directory: str) -> None

Add a new directory to serve.

remove_directory()
def remove_directory(self, route: str) -> None

Remove a directory from being served.

Integration with SignalWire Agents

WebService complements AI agents by providing static file serving:

from signalwire import AgentBase, WebService

class DocumentationAgent(AgentBase):
    def __init__(self):
        super().__init__(name="Documentation Assistant")
        
        # Reference documentation served by WebService
        self.prompt_add_section(
            "Documentation",
            "User documentation is available at https://example.com:8002/docs/"
        )
    
        @self.tool(
            "get_doc_link",
            description="Get link to a documentation page",
            parameters={
                "doc_name": {"type": "string", "description": "Name of the documentation page"}
            }
        )
        def get_doc_link(self, args, raw_data):
            doc_name = args.get('doc_name')
            return FunctionResult(
                f"Documentation available at: https://example.com:8002/docs/{doc_name}.html"
            )

# Run both services
if __name__ == "__main__":
    # Start WebService for documentation
    web = WebService(
        port=8002,
        directories={"/docs": "./documentation"}
    )
    
    # Start agent
    agent = DocumentationAgent()
    
    # Run in threads or separate processes
    import threading
    web_thread = threading.Thread(target=web.start, daemon=True)
    web_thread.start()
    
    agent.serve(port=3000)

Summary

WebService provides a secure, configurable static file server that integrates with the SignalWire SDK. It follows the same architectural patterns as other SDK services, making it familiar and easy to use while providing configurable security features and flexible deployment options.