Files
unshackle/unshackle/commands/remote_auth.py
Andy 965482a1e4 feat: merge upstream dev branch
- Add Gluetun dynamic VPN-to-HTTP proxy provider
   - Add remote services and authentication system
   - Add country code utilities
   - Add Docker binary detection
   - Update proxy providers
2025-11-25 20:23:06 +00:00

226 lines
7.5 KiB
Python

"""CLI command for authenticating remote services."""
from typing import Optional
import click
from rich.table import Table
from unshackle.core.config import config
from unshackle.core.console import console
from unshackle.core.constants import context_settings
from unshackle.core.remote_auth import RemoteAuthenticator
@click.group(short_help="Manage remote service authentication.", context_settings=context_settings)
def remote_auth() -> None:
"""Authenticate and manage sessions for remote services."""
pass
@remote_auth.command(name="authenticate")
@click.argument("service", type=str)
@click.option(
"-r", "--remote", type=str, help="Remote server name or URL (from config)", required=False
)
@click.option("-p", "--profile", type=str, help="Profile to use for authentication")
def authenticate_command(service: str, remote: Optional[str], profile: Optional[str]) -> None:
"""
Authenticate a service locally and upload session to remote server.
This command:
1. Authenticates the service locally (shows browser, handles 2FA, etc.)
2. Extracts the authenticated session
3. Uploads the session to the remote server
The server will use this pre-authenticated session for all requests.
Examples:
unshackle remote-auth authenticate DSNP
unshackle remote-auth authenticate NF --profile john
unshackle remote-auth auth AMZN --remote my-server
"""
# Get remote server config
remote_config = _get_remote_config(remote)
if not remote_config:
return
remote_url = remote_config["url"]
api_key = remote_config["api_key"]
server_name = remote_config.get("name", remote_url)
console.print(f"\n[bold cyan]Authenticating {service} for remote server:[/bold cyan] {server_name}")
console.print(f"[dim]Server: {remote_url}[/dim]\n")
# Create authenticator
authenticator = RemoteAuthenticator(remote_url, api_key)
# Authenticate and save locally
success = authenticator.authenticate_and_save(service, profile)
if success:
console.print(f"\n[bold green]✓ Success![/bold green] Session saved locally. You can now use remote_{service} service.")
else:
console.print(f"\n[bold red]✗ Failed to authenticate {service}[/bold red]")
raise click.Abort()
@remote_auth.command(name="status")
@click.option(
"-r", "--remote", type=str, help="Remote server name or URL (from config)", required=False
)
def status_command(remote: Optional[str]) -> None:
"""
Show status of all authenticated sessions in local cache.
Examples:
unshackle remote-auth status
unshackle remote-auth status --remote my-server
"""
import datetime
from unshackle.core.local_session_cache import get_local_session_cache
# Get local session cache
cache = get_local_session_cache()
# Get remote server config (optional filter)
remote_url = None
if remote:
remote_config = _get_remote_config(remote)
if remote_config:
remote_url = remote_config["url"]
server_name = remote_config.get("name", remote_url)
else:
server_name = "All Remotes"
# Get sessions (filtered by remote if specified)
sessions = cache.list_sessions(remote_url)
if not sessions:
if remote_url:
console.print(f"\n[yellow]No authenticated sessions for {server_name}[/yellow]")
else:
console.print("\n[yellow]No authenticated sessions in local cache[/yellow]")
console.print("\nUse [cyan]unshackle remote-auth authenticate <SERVICE>[/cyan] to add sessions")
return
# Display sessions in table
table = Table(title=f"Local Authenticated Sessions - {server_name}")
table.add_column("Remote", style="magenta")
table.add_column("Service", style="cyan")
table.add_column("Profile", style="green")
table.add_column("Cached", style="dim")
table.add_column("Age", style="yellow")
table.add_column("Status", style="bold")
for session in sessions:
cached_time = datetime.datetime.fromtimestamp(session["cached_at"]).strftime("%Y-%m-%d %H:%M")
# Format age
age_seconds = session["age_seconds"]
if age_seconds < 3600:
age_str = f"{age_seconds // 60}m"
elif age_seconds < 86400:
age_str = f"{age_seconds // 3600}h"
else:
age_str = f"{age_seconds // 86400}d"
# Status
status = "[red]Expired" if session["expired"] else "[green]Valid"
# Short remote URL for display
remote_display = session["remote_url"].replace("https://", "").replace("http://", "")
if len(remote_display) > 30:
remote_display = remote_display[:27] + "..."
table.add_row(
remote_display,
session["service_tag"],
session["profile"],
cached_time,
age_str,
status
)
console.print()
console.print(table)
console.print("\n[dim]Sessions are stored locally and expire after 24 hours[/dim]")
console.print()
@remote_auth.command(name="delete")
@click.argument("service", type=str)
@click.option(
"-r", "--remote", type=str, help="Remote server name or URL (from config)", required=False
)
@click.option("-p", "--profile", type=str, default="default", help="Profile name")
def delete_command(service: str, remote: Optional[str], profile: str) -> None:
"""
Delete an authenticated session from local cache.
Examples:
unshackle remote-auth delete DSNP
unshackle remote-auth delete NF --profile john
"""
from unshackle.core.local_session_cache import get_local_session_cache
# Get remote server config
remote_config = _get_remote_config(remote)
if not remote_config:
return
remote_url = remote_config["url"]
cache = get_local_session_cache()
console.print(f"\n[yellow]Deleting local session for {service} (profile: {profile})...[/yellow]")
deleted = cache.delete_session(remote_url, service, profile)
if deleted:
console.print("[green]✓ Session deleted from local cache[/green]")
else:
console.print(f"[red]✗ No session found for {service} (profile: {profile})[/red]")
def _get_remote_config(remote: Optional[str]) -> Optional[dict]:
"""
Get remote server configuration.
Args:
remote: Remote server name or URL, or None for first configured remote
Returns:
Remote config dict or None
"""
if not config.remote_services:
console.print("[red]No remote services configured in unshackle.yaml[/red]")
console.print("\nAdd a remote service to your config:")
console.print("[dim]remote_services:")
console.print(" - url: https://your-server.com")
console.print(" api_key: your-api-key")
console.print(" name: my-server[/dim]")
return None
# If no remote specified, use the first one
if not remote:
return config.remote_services[0]
# Check if remote is a name
for remote_config in config.remote_services:
if remote_config.get("name") == remote:
return remote_config
# Check if remote is a URL
for remote_config in config.remote_services:
if remote_config.get("url") == remote:
return remote_config
console.print(f"[red]Remote server '{remote}' not found in config[/red]")
console.print("\nAvailable remotes:")
for remote_config in config.remote_services:
name = remote_config.get("name", remote_config.get("url"))
console.print(f" - {name}")
return None