Files
晴天 d976ee2c33 feat: relay 模式 + SSH 远程部署 + 官网移动端适配 + 国内镜像源
- 新增 relay 模式: frp 中继引擎,TCP/UDP 全协议穿透
- 新增 relay server setup: SSH 远程安装 frps 服务端
- 新增 sshutil 包: 纯 Go SSH 连接/远程执行
- 官网: 移动端响应式适配 (480/768/900px 三断点)
- 官网: 修复 Star 按钮样式,特性卡片 11→12
- install.sh: 国内镜像源加速 (ghfast/gh-proxy/ghproxy)
- Docker Compose + install-relay.sh 服务端部署方案
2026-02-26 22:29:56 +08:00

269 lines
7.4 KiB
Go

package cmd
import (
"context"
"encoding/hex"
"fmt"
"strings"
"github.com/charmbracelet/huh"
"github.com/qingchencloud/cftunnel/internal/authproxy"
"github.com/qingchencloud/cftunnel/internal/cfapi"
"github.com/qingchencloud/cftunnel/internal/config"
"github.com/qingchencloud/cftunnel/internal/daemon"
"github.com/spf13/cobra"
)
var (
wizardDomain string
wizardPort string
wizardAuth string
wizardName string
)
func init() {
wizardCmd.Flags().StringVar(&wizardDomain, "domain", "", "完整域名 (如 chat.example.com)")
wizardCmd.Flags().StringVar(&wizardPort, "port", "", "本地服务端口")
wizardCmd.Flags().StringVar(&wizardName, "name", "", "路由名称 (默认使用域名前缀)")
wizardCmd.Flags().StringVar(&wizardAuth, "auth", "", "密码保护 (格式: 用户名:密码)")
rootCmd.AddCommand(wizardCmd)
}
var wizardCmd = &cobra.Command{
Use: "wizard",
Short: "交互式引导,一条命令完成全部配置",
Long: `交互式引导,一条命令完成 Tunnel 创建和路由添加。
如果已有配置,会自动跳过已完成的步骤,用户只需提供新增的路由信息。
示例:
cftunnel wizard # 交互模式
cftunnel wizard --domain chat.example.com --port 8080 # 命令行模式`,
RunE: runWizard,
}
func runWizard(cmd *cobra.Command, args []string) error {
fmt.Println("╔════════════════════════════════════════╗")
fmt.Println("║ Cloudflare Tunnel 向导 ║")
fmt.Println("╚════════════════════════════════════════╝")
fmt.Println()
// ============ 第1步: 加载或创建配置 ============
cfg, err := config.Load()
if err != nil {
return err
}
// ============ 第2步: 配置认证信息 ============
if cfg.Auth.APIToken == "" || cfg.Auth.AccountID == "" {
fmt.Println("📋 第1步: 配置 Cloudflare 认证信息")
fmt.Println(" (如果已有配置,可以直接按回车跳过)")
fmt.Println()
var apiToken, accountID string
err := huh.NewForm(
huh.NewGroup(
huh.NewInput().Title("API Token").Value(&apiToken).
Placeholder("从 https://dash.cloudflare.com/profile/api-tokens 创建"),
huh.NewInput().Title("Account ID").Value(&accountID).
Placeholder("32位字符,从 Cloudflare dashboard 获取"),
),
).Run()
if err != nil {
return err
}
if strings.TrimSpace(apiToken) != "" {
cfg.Auth.APIToken = strings.TrimSpace(apiToken)
}
if strings.TrimSpace(accountID) != "" {
cfg.Auth.AccountID = strings.TrimSpace(accountID)
}
if cfg.Auth.APIToken == "" || cfg.Auth.AccountID == "" {
return fmt.Errorf("API Token 和 Account ID 不能同时为空")
}
if err := cfg.Save(); err != nil {
return err
}
fmt.Println("✓ 认证信息已保存")
fmt.Println()
}
// ============ 第3步: 创建 Tunnel (如果不存在) ============
if cfg.Tunnel.ID == "" {
fmt.Println("📋 第2步: 创建 Tunnel")
fmt.Println()
var tunnelName string
err := huh.NewForm(
huh.NewGroup(
huh.NewInput().Title("Tunnel 名称").Value(&tunnelName).
Placeholder("如: my-tunnel"),
),
).Run()
if err != nil {
return err
}
tunnelName = strings.TrimSpace(tunnelName)
if tunnelName == "" {
return fmt.Errorf("Tunnel 名称不能为空")
}
fmt.Printf("正在创建 Tunnel: %s\n", tunnelName)
client := cfapi.New(cfg.Auth.APIToken, cfg.Auth.AccountID)
ctx := context.Background()
tunnel, err := client.CreateTunnel(ctx, tunnelName)
if err != nil {
return fmt.Errorf("创建 Tunnel 失败: %w", err)
}
// 获取 tunnel token
tunnelToken, err := client.GetTunnelToken(ctx, tunnel.ID)
if err != nil {
return fmt.Errorf("获取 Tunnel Token 失败: %w", err)
}
cfg.Tunnel = config.TunnelConfig{
ID: tunnel.ID,
Name: tunnelName,
Token: tunnelToken,
}
if err := cfg.Save(); err != nil {
return err
}
fmt.Printf("✓ Tunnel 创建成功: %s\n", tunnelName)
fmt.Println()
} else {
fmt.Printf("✓ 已有 Tunnel: %s (%s)\n", cfg.Tunnel.Name, cfg.Tunnel.ID)
fmt.Println()
}
// ============ 第4步: 添加路由 ============
client := cfapi.New(cfg.Auth.APIToken, cfg.Auth.AccountID)
ctx := context.Background()
// 启动 tunnel(如果未运行)
if !daemon.Running() {
fmt.Println("📋 第3步: 启动 Tunnel")
go daemon.Start(cfg.Tunnel.Token)
fmt.Println("✓ Tunnel 已启动")
fmt.Println()
}
// 获取域名和端口
domain := strings.TrimSpace(wizardDomain)
port := strings.TrimSpace(wizardPort)
routeName := strings.TrimSpace(wizardName)
if domain == "" || port == "" {
fmt.Println("📋 第4步: 添加路由")
fmt.Println()
err := huh.NewForm(
huh.NewGroup(
huh.NewInput().Title("域名").Value(&domain).
Placeholder("如: chat.example.com"),
huh.NewInput().Title("本地端口").Value(&port).
Placeholder("如: 8080"),
huh.NewInput().Title("路由名称(可选)").Value(&routeName).
Placeholder("默认使用域名前缀"),
),
).Run()
if err != nil {
return err
}
}
domain = strings.TrimSpace(domain)
port = strings.TrimSpace(port)
routeName = strings.TrimSpace(routeName)
if domain == "" || port == "" {
return fmt.Errorf("域名和端口不能为空")
}
// 生成路由名称
if routeName == "" {
// 从域名提取前缀,如 chat.example.com -> chat
parts := strings.Split(domain, ".")
if len(parts) >= 2 {
routeName = parts[0]
} else {
routeName = domain
}
}
// 检查路由是否已存在
if cfg.FindRoute(routeName) != nil {
return fmt.Errorf("路由 %s 已存在", routeName)
}
service := "http://localhost:" + port
fmt.Printf("正在添加路由: %s -> %s\n", domain, service)
// 查找 Zone
zone, err := findZoneForDomain(client, ctx, domain)
if err != nil {
return err
}
// 创建 DNS CNAME 记录
target := cfg.Tunnel.ID + ".cfargotunnel.com"
fmt.Printf("正在创建 DNS 记录: %s -> %s\n", domain, target)
recordID, err := client.CreateCNAME(ctx, zone.ID, domain, target)
if err != nil {
return err
}
// 构建路由配置
route := config.RouteConfig{
Name: routeName,
Hostname: domain,
Service: service,
ZoneID: zone.ID,
DNSRecordID: recordID,
}
// 密码保护
if wizardAuth != "" {
user, pass, err := parseAuth(wizardAuth)
if err != nil {
return err
}
route.Auth = &config.AuthProxy{
Username: user,
Password: pass,
SigningKey: hex.EncodeToString(authproxy.RandomKey()),
}
fmt.Printf("✓ 已启用密码保护: %s\n", wizardAuth)
}
// 保存路由
cfg.Routes = append(cfg.Routes, route)
if err := cfg.Save(); err != nil {
return err
}
// 推送 ingress
fmt.Println("正在同步 ingress 配置...")
if err := pushIngress(client, ctx, cfg); err != nil {
return fmt.Errorf("推送 ingress 失败: %w", err)
}
fmt.Println()
fmt.Println("╔════════════════════════════════════════╗")
fmt.Println("║ ✅ 全部完成! ║")
fmt.Println("╚════════════════════════════════════════╝")
fmt.Printf("路由已添加: %s -> %s\n", domain, service)
fmt.Printf("外网访问: https://%s\n", domain)
fmt.Println()
fmt.Println("提示: 使用 cftunnel status 查看状态")
return nil
}