From 9a53aa77a907552c68edd1c8e9c501780430ad56 Mon Sep 17 00:00:00 2001 From: admin Date: Mon, 23 Feb 2026 00:30:42 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=20quick=20=E5=91=BD?= =?UTF-8?q?=E4=BB=A4=20=E2=80=94=20=E5=85=8D=E5=9F=9F=E5=90=8D=E4=B8=80?= =?UTF-8?q?=E9=94=AE=E7=A9=BF=E9=80=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit cftunnel quick <端口> 无需账户/Token/域名, 自动生成 *.trycloudflare.com 随机公网地址。 --- cmd/quick.go | 20 ++++++++++ internal/daemon/quick.go | 81 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 101 insertions(+) create mode 100644 cmd/quick.go create mode 100644 internal/daemon/quick.go diff --git a/cmd/quick.go b/cmd/quick.go new file mode 100644 index 0000000..cae03f7 --- /dev/null +++ b/cmd/quick.go @@ -0,0 +1,20 @@ +package cmd + +import ( + "github.com/qingchencloud/cftunnel/internal/daemon" + "github.com/spf13/cobra" +) + +func init() { + rootCmd.AddCommand(quickCmd) +} + +var quickCmd = &cobra.Command{ + Use: "quick <端口>", + Short: "快速启动免域名隧道(生成 *.trycloudflare.com 随机域名)", + Long: "无需 Cloudflare 账户、API Token 或域名,一条命令生成临时公网地址。\n适合临时分享、快速调试,Ctrl+C 退出后域名自动失效。", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + return daemon.StartQuick(args[0]) + }, +} diff --git a/internal/daemon/quick.go b/internal/daemon/quick.go new file mode 100644 index 0000000..d7e2d2c --- /dev/null +++ b/internal/daemon/quick.go @@ -0,0 +1,81 @@ +package daemon + +import ( + "bufio" + "fmt" + "io" + "os" + "os/exec" + "os/signal" + "strings" + "syscall" +) + +// StartQuick 启动免域名模式(前台运行,Ctrl+C 退出) +func StartQuick(port string) error { + binPath, err := EnsureCloudflared() + if err != nil { + return err + } + if Running() { + return fmt.Errorf("cloudflared 已在运行,请先执行 cftunnel down") + } + + cmd := exec.Command(binPath, "tunnel", "--url", "http://localhost:"+port) + + // 捕获 stderr 提取随机域名 + stderr, err := cmd.StderrPipe() + if err != nil { + return err + } + cmd.Stdout = os.Stdout + + if err := cmd.Start(); err != nil { + return fmt.Errorf("启动 cloudflared 失败: %w", err) + } + + // 后台读取 stderr,提取域名并转发输出 + go scanForURL(stderr) + + // 捕获 Ctrl+C 优雅退出 + sig := make(chan os.Signal, 1) + signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM) + + done := make(chan error, 1) + go func() { done <- cmd.Wait() }() + + select { + case <-sig: + cmd.Process.Signal(syscall.SIGINT) + <-done + case err := <-done: + if err != nil { + return fmt.Errorf("cloudflared 异常退出: %w", err) + } + } + return nil +} + +func scanForURL(r io.Reader) { + scanner := bufio.NewScanner(r) + for scanner.Scan() { + line := scanner.Text() + // cloudflared 输出格式: ... https://xxx.trycloudflare.com ... + if strings.Contains(line, "trycloudflare.com") { + url := extractURL(line) + if url != "" { + fmt.Printf("\n✔ 隧道已启动: %s\n\n", url) + } + } + fmt.Fprintln(os.Stderr, line) + } +} + +func extractURL(line string) string { + for _, part := range strings.Fields(line) { + if strings.Contains(part, "trycloudflare.com") && strings.HasPrefix(part, "http") { + return part + } + } + return "" +}