tcp协议 ssh远程 丐版但美味

cpolar’s charge for free is not good ,really.there I show u cpolar helper.

不买官子了,就是一个脚本长柱运行,使用python中命令行库运行cpolar tcp 22读取cpolar的输出给脚本,脚本状态机 最后发邮件,当然需要一个理清状态机的过程,有网络恢复对应处理逻辑,也就是说他可以在网络恢复后通过发邮件给自己(我用的qq邮箱),你不就拿到新的url和port了嘛,

cpolar-monitor.ps1:

#Requires -Version 5.1
<#
.SYNOPSIS
    cpolar monitor 
    Network-aware tunnel monitoring with URL comparison
#>

# ==================== Config ====================
$Script:SmtpServer = "smtp.qq.com"
$Script:SmtpPort = 587
$Script:SenderEmail = "###@qq.com"
$Script:AuthCode = "邮箱认证密钥"
$Script:ReceiverEmail = "###@qq.com"
$Script:LogFile = Join-Path $PSScriptRoot "cpolar-monitor.log"
$Script:CpolarCaptureScript = Join-Path $PSScriptRoot "cpolar_capture.py"
$Script:PythonProcess = $null

# Network detection config
$Script:PingTargets = @("8.8.8.8", "1.1.1.1", "223.5.5.5")  # Google, Cloudflare, AliDNS
$Script:MaxFailures = 3
$Script:PingTimeout = 2000  # ms

# SSH config for email links
$Script:SshUsername = "jake"

# ==================== Logging ====================
function Write-Log {
    param([string]$Message, [string]$Level = 'INFO')
    $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
    $logEntry = "[$timestamp] [$Level] $Message"
    switch ($Level) {
        'INFO'  { Write-Host $logEntry -ForegroundColor Cyan }
        'WARN'  { Write-Host $logEntry -ForegroundColor Yellow }
        'ERROR' { Write-Host $logEntry -ForegroundColor Red }
    }
    Add-Content -Path $Script:LogFile -Value $logEntry -Encoding UTF8 -ErrorAction SilentlyContinue
}

# ==================== Network Detection ====================
function Test-Network {
    <# 
    Ping multiple targets to check network connectivity
    Returns $true if ANY target responds, $false if ALL fail
    #>
    $ping = New-Object System.Net.NetworkInformation.Ping
    
    foreach ($target in $Script:PingTargets) {
        try {
            $reply = $ping.Send($target, $Script:PingTimeout)
            if ($reply.Status -eq 'Success') {
                return $true
            }
        } catch {
            # Continue to next target
        }
    }
    return $false
}

# ==================== Email ====================
function Send-EmailNotification {
    param(
        [string]$TunnelUrl,
        [switch]$IsReconnect,
        [switch]$UrlChanged
    )
    
    Start-Sleep -Seconds 2
    
    try {
        $dns = [System.Net.Dns]::GetHostAddresses($Script:SmtpServer)
        Write-Log "DNS resolved: $($Script:SmtpServer) -> $($dns[0].IPAddressToString)"
    } catch {
        Write-Log "DNS resolution failed, waiting 5s..." -Level 'WARN'
        Start-Sleep -Seconds 5
    }
    
    $maxRetries = 3
    $retryDelay = 10
    
    # Determine email content based on notification type
    if ($IsReconnect -and $UrlChanged) {
        $emailSubject = "[cpolar] Tunnel Reconnected (URL Changed) - $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')"
        $emailTitle = "cpolar Tunnel Reconnected"
        $emailDesc = "Tunnel recovered from network disconnection.<br><strong style='color: #FF5722;'>URL:PORT CHANGED!</strong>"
    }
    elseif ($IsReconnect) {
        $emailSubject = "[cpolar] Tunnel Reconnected - $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')"
        $emailTitle = "cpolar Tunnel Reconnected"
        $emailDesc = "Tunnel recovered from network disconnection."
    }
    else {
        $emailSubject = "[cpolar] Tunnel Online - $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')"
        $emailTitle = "cpolar Tunnel Online"
        $emailDesc = "TCP tunnel established."
    }
    
    # Parse URL for SSH link
    $sshLink = ""
    $sshCommand = ""
    $tunnelHost = ""
    $tunnelPort = ""
    if ($TunnelUrl -match 'tcp://([^:]+):(\d+)') {
        $tunnelHost = $matches[1]
        $tunnelPort = $matches[2]
        $sshLink = "ssh://$($Script:SshUsername)@$tunnelHost`:$tunnelPort"
        $sshCommand = "ssh -p $tunnelPort $($Script:SshUsername)@$tunnelHost"
    }
    
    for ($attempt = 1; $attempt -le $maxRetries; $attempt++) {
        try {
            Write-Log "Email attempt $attempt/$maxRetries..."
            
            $mail = New-Object System.Net.Mail.MailMessage
            $mail.From = $Script:SenderEmail
            $mail.To.Add($Script:ReceiverEmail)
            $mail.Subject = $emailSubject
            $mail.SubjectEncoding = [System.Text.Encoding]::UTF8
            $mail.Body = @"
<!DOCTYPE html>
<html>
<head><meta charset="UTF-8"></head>
<body style="font-family: Arial, sans-serif; padding: 20px;">
<h2 style="color: #4CAF50;">$emailTitle</h2>
<p>$emailDesc</p>
<h3 style="color: #2196F3;">Tunnel URL:</h3>
<p style="font-size: 18px; background-color: #f5f5f5; padding: 15px; border-radius: 5px;">
<code>$TunnelUrl</code>
</p>
<h4>Quick Connect:</h4>
<p><a href="$sshLink" style="color:#0066cc; text-decoration:underline; font-size:16px;">Click to connect with Termius</a>
<br><span style="color:#888; font-size:12px;">(Requires ssh:// protocol handler)</span></p>
<h4>SSH Command:</h4>
<p style="background-color: #f0f0f0; padding: 10px; border-radius: 5px; font-family: monospace;">
$sshCommand
</p>
<strong>Host:</strong><code>$tunnelHost</code><br>
<strong>Port:</strong><code>$tunnelPort</code><br>
<p><small>Time: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')</small></p>
</body>
</html>
"@
            $mail.BodyEncoding = [System.Text.Encoding]::UTF8
            $mail.IsBodyHtml = $true
            
            $smtp = New-Object System.Net.Mail.SmtpClient($Script:SmtpServer, $Script:SmtpPort)
            $smtp.EnableSsl = $true
            $smtp.Timeout = 30000
            $smtp.Credentials = New-Object System.Net.NetworkCredential($Script:SenderEmail, $Script:AuthCode)
            $smtp.Send($mail)
            Write-Log "Email sent: $TunnelUrl"
            return $true
        } catch {
            Write-Log "Email attempt $attempt failed: $($_.Exception.Message)" -Level 'ERROR'
            if ($attempt -lt $maxRetries) {
                Write-Log "Retrying in $retryDelay seconds..."
                Start-Sleep -Seconds $retryDelay
            }
        }
    }
    return $false
}

# ==================== Process Management ====================
function Start-PythonMonitor {
    if ($Script:PythonProcess -and !$Script:PythonProcess.HasExited) {
        return
    }
    
    Write-Log "Starting Python monitor..."
    
    $psi = New-Object System.Diagnostics.ProcessStartInfo
    $psi.FileName = "python"
    $psi.Arguments = "`"$($Script:CpolarCaptureScript)`""
    $psi.UseShellExecute = $false
    $psi.RedirectStandardOutput = $true
    $psi.RedirectStandardError = $true
    $psi.CreateNoWindow = $true
    
    $Script:PythonProcess = New-Object System.Diagnostics.Process
    $Script:PythonProcess.StartInfo = $psi
    $Script:PythonProcess.Start() | Out-Null
    
    Write-Log "Python monitor started, PID: $($Script:PythonProcess.Id)"
}

function Stop-PythonMonitor {
    if ($Script:PythonProcess -and !$Script:PythonProcess.HasExited) {
        Write-Log "Stopping Python monitor..."
        $Script:PythonProcess.Kill()
        $Script:PythonProcess = $null
    }
    
    # Kill cpolar processes
    $cpolar = Get-Process -Name cpolar -ErrorAction SilentlyContinue
    if ($cpolar) {
        $cpolar | Stop-Process -Force
        Write-Log "cpolar stopped ($($cpolar.Count) processes)"
    }
}

function Cleanup-AllProcesses {
    $cpolar = Get-Process -Name cpolar -ErrorAction SilentlyContinue
    if ($cpolar) {
        $cpolar | Stop-Process -Force
        Write-Log "Cleaned up $($cpolar.Count) cpolar processes"
    }
    
    $python = Get-Process -Name python -ErrorAction SilentlyContinue
    if ($python) {
        $python | Stop-Process -Force
        Write-Log "Cleaned up $($python.Count) python processes"
    }
}

function Read-PythonOutput {
    <# Read available output from Python process #>
    $result = $null
    
    if ($Script:PythonProcess -and !$Script:PythonProcess.HasExited) {
        while ($Script:PythonProcess.StandardOutput.Peek() -gt 0) {
            $line = $Script:PythonProcess.StandardOutput.ReadLine()
            if ($line) {
                Write-Host "  [Python] $line" -ForegroundColor DarkGray
                
                # URL: captured from cpolar output
                if ($line -match '^URL:\s*(tcp://.+)') {
                    $result = @{Type='url';Url=$matches[1].Trim()}
                }
            }
        }
    }
    
    return $result
}

# ==================== Main ====================
function Main {
    Cleanup-AllProcesses
    
    Write-Log "========================================"
    Write-Log "cpolar monitor v7.1 started"
    Write-Log "Network detection: ping $($Script:PingTargets -join ', ')"
    Write-Log "SSH username: $($Script:SshUsername)"
    Write-Log "========================================"
    
    # State variables
    $lastUrl = $null
    $networkDown = $false
    $consecutiveFailures = 0
    $waitingForReconnectUrl = $false  # True when network just recovered
    
    try {
        # Initial network check
        if (Test-Network) {
            Write-Log "Network OK, starting monitor..."
            Start-PythonMonitor
        } else {
            Write-Log "Network unavailable, waiting..." -Level 'WARN'
            $networkDown = $true
        }
        
        while ($true) {
            # Network detection
            if (Test-Network) {
                $consecutiveFailures = 0
                
                if ($networkDown) {
                    # Network just recovered
                    Write-Log "Network recovered!"
                    $networkDown = $false
                    $waitingForReconnectUrl = $true  # Mark that we need reconnect notification
                    Start-PythonMonitor
                }
            } elseif (!$networkDown) {
                # Only count failures when not already in network down state
                $consecutiveFailures++
                Write-Log "Network check failed ($consecutiveFailures/$($Script:MaxFailures))" -Level 'WARN'
                
                if ($consecutiveFailures -ge $Script:MaxFailures) {
                    Write-Log "Network disconnected! Stopping processes..." -Level 'ERROR'
                    $networkDown = $true
                    Stop-PythonMonitor
                }
            }
            # else: already in network down state, skip counting
            
            # Read output from Python (only when network is up)
            if (!$networkDown) {
                # Check if Python process is still running
                if ($Script:PythonProcess -and $Script:PythonProcess.HasExited) {
                    Write-Log "Python process exited, restarting..." -Level 'WARN'
                    Start-PythonMonitor
                }
                
                # Read output from Python
                $result = Read-PythonOutput
                
                if ($result) {
                    $currentUrl = $result.Url
                    
                    # URL comparison logic
                    if ($null -eq $lastUrl) {
                        # First URL - initial connection
                        $lastUrl = $currentUrl
                        Write-Log "Got tunnel URL: $currentUrl"
                        $null = Send-EmailNotification -TunnelUrl $currentUrl
                    }
                    elseif ($waitingForReconnectUrl) {
                        # Network just recovered, check URL
                        $waitingForReconnectUrl = $false
                        
                        if ($currentUrl -eq $lastUrl) {
                            # Same URL after network recovery - Reconnect
                            Write-Log "Reconnected with same URL: $currentUrl"
                            $null = Send-EmailNotification -TunnelUrl $currentUrl -IsReconnect
                        } else {
                            # Different URL after network recovery - URL Changed
                            $lastUrl = $currentUrl
                            Write-Log "Reconnected with new URL: $currentUrl"
                            $null = Send-EmailNotification -TunnelUrl $currentUrl -IsReconnect -UrlChanged
                        }
                    }
                    elseif ($currentUrl -ne $lastUrl) {
                        # URL changed (not from network recovery)
                        $lastUrl = $currentUrl
                        Write-Log "URL changed: $currentUrl"
                        $null = Send-EmailNotification -TunnelUrl $currentUrl -UrlChanged
                    }
                    # else: same URL and not from network recovery - ignore duplicate
                }
            }
            
            Start-Sleep -Milliseconds 500
        }
    } finally {
        Stop-PythonMonitor
        Write-Log "Script stopped"
    }
}

# ==================== Entry ====================
try { Main } catch {
    Write-Log "Exception: $($_.Exception.Message)" -Level 'ERROR'
    Write-Log $_.ScriptStackTrace -Level 'ERROR'
}

cpolar_capture.py:

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
cpolar capture - minimal URL capture for cpolar tunnel
Outputs URL whenever captured from cpolar output
"""

import sys
import time
import re
import subprocess

# URL pattern for cpolar tunnel addresses
URL_PATTERN = re.compile(r'tcp://[0-9]+\.tcp\.(?:vip\.)?cpolar\.(?:top|cn):[0-9]+')

def parse_url(output):
    """Extract tunnel URL from cpolar output"""
    match = URL_PATTERN.search(output)
    return match.group() if match else None

def ensure_dependency():
    """Ensure pywinpty is installed"""
    try:
        from winpty import PtyProcess
        return True
    except ImportError:
        print("INSTALLING: pywinpty...")
        try:
            subprocess.check_call([
                sys.executable, "-m", "pip", "install", "pywinpty", "-q"
            ])
            print("INSTALLED: pywinpty")
            return True
        except:
            return False

def kill_all_cpolar():
    """Kill all existing cpolar processes"""
    try:
        subprocess.run(['taskkill', '/F', '/IM', 'cpolar.exe'], 
                      capture_output=True, timeout=5)
        time.sleep(1)
    except:
        pass

def capture_cpolar():
    """Capture cpolar output, output URL whenever found"""
    if not ensure_dependency():
        print("ERROR: Cannot install pywinpty")
        return
    
    from winpty import PtyProcess
    
    # Kill any existing cpolar processes first
    kill_all_cpolar()
    
    output = ""
    proc = None
    last_outputted_url = None  # Avoid duplicate output for same URL
    
    print("STARTING: cpolar capture v7.0")
    print("-" * 50)
    
    while True:
        # Start cpolar if not running
        if proc is None:
            kill_all_cpolar()
            
            try:
                proc = PtyProcess.spawn('cpolar tcp 22')
                output = ""
                print(f"STARTED: cpolar PID={proc.pid}")
            except Exception as e:
                print(f"ERROR: Failed to start cpolar: {e}")
                time.sleep(10)
                continue
        
        # Read output
        try:
            chunk = proc.read(4096)
            if chunk:
                output += chunk
                # Keep only last 10KB to avoid memory growth
                if len(output) > 10000:
                    output = output[-10000:]
                
                # Check for tunnel URL
                url = parse_url(output)
                if url:
                    # Output URL if it's new or different
                    if url != last_outputted_url:
                        print(f"URL: {url}")
                        last_outputted_url = url
                        sys.stdout.flush()
                    
                    # Clear URL from output to prevent re-detection
                    output = output.replace(url, '')
                        
        except IOError:
            time.sleep(0.1)
        except Exception as e:
            if "closed" in str(e).lower() or proc is None:
                proc = None
                last_outputted_url = None  # Reset for new session
                print("STATUS: cpolar terminated, restarting...")
                time.sleep(2)
            else:
                time.sleep(0.5)

def main():
    try:
        capture_cpolar()
    except KeyboardInterrupt:
        print("\nEXIT: interrupted by user")
        kill_all_cpolar()
        return 130
    except Exception as e:
        print(f"ERROR: {e}")
        kill_all_cpolar()
        return 1
    return 0

if __name__ == "__main__":
    sys.exit(main())
1 个赞