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())