diff --git a/cmd/sing-box/cmd_tools.go b/cmd/sing-box/cmd_tools.go index bc36b861..ee3cd4b2 100644 --- a/cmd/sing-box/cmd_tools.go +++ b/cmd/sing-box/cmd_tools.go @@ -4,14 +4,16 @@ import ( "context" box "github.com/sagernet/sing-box" + "github.com/sagernet/sing-box/option" E "github.com/sagernet/sing/common/exceptions" + N "github.com/sagernet/sing/common/network" "github.com/spf13/cobra" ) var commandTools = &cobra.Command{ Use: "tools", - Short: "experimental tools", + Short: "Experimental tools", } func init() { @@ -23,6 +25,10 @@ func createPreStartedClient() (*box.Box, error) { if err != nil { return nil, err } + if options.Log == nil { + options.Log = &option.LogOptions{} + } + options.Log.Disabled = true instance, err := box.New(context.Background(), options, nil) if err != nil { return nil, E.Cause(err, "create service") @@ -33,3 +39,19 @@ func createPreStartedClient() (*box.Box, error) { } return instance, nil } + +func createDialer(instance *box.Box, network string, outboundTag string) (N.Dialer, error) { + if outboundTag == "" { + outbound := instance.Router().DefaultOutbound(network) + if outbound == nil { + return nil, E.New("missing default outbound") + } + return outbound, nil + } else { + outbound, loaded := instance.Router().Outbound(outboundTag) + if !loaded { + return nil, E.New("outbound not found: ", outboundTag) + } + return outbound, nil + } +} diff --git a/cmd/sing-box/cmd_tools_connect.go b/cmd/sing-box/cmd_tools_connect.go index ebf0fb92..ab832786 100644 --- a/cmd/sing-box/cmd_tools_connect.go +++ b/cmd/sing-box/cmd_tools_connect.go @@ -15,11 +15,14 @@ import ( "github.com/spf13/cobra" ) -var commandFlagNetwork string +var ( + commandConnectFlagNetwork string + commandConnectFlagOutbound string +) var commandConnect = &cobra.Command{ Use: "connect [address]", - Short: "connect to a address through default outbound", + Short: "Connect to an address", Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { err := connect(args[0]) @@ -30,25 +33,27 @@ var commandConnect = &cobra.Command{ } func init() { - commandConnect.Flags().StringVar(&commandFlagNetwork, "network", "tcp", "network type") + commandConnect.Flags().StringVar(&commandConnectFlagNetwork, "network", "tcp", "network type") + commandConnect.Flags().StringVar(&commandConnectFlagOutbound, "outbound", "", "outbound tag") commandTools.AddCommand(commandConnect) } func connect(address string) error { - switch N.NetworkName(commandFlagNetwork) { + switch N.NetworkName(commandConnectFlagNetwork) { case N.NetworkTCP, N.NetworkUDP: default: - return E.Cause(N.ErrUnknownNetwork, commandFlagNetwork) + return E.Cause(N.ErrUnknownNetwork, commandConnectFlagNetwork) } instance, err := createPreStartedClient() if err != nil { return err } - outbound := instance.Router().DefaultOutbound(commandFlagNetwork) - if outbound == nil { - return E.New("missing default outbound") + defer instance.Close() + dialer, err := createDialer(instance, commandConnectFlagNetwork, commandConnectFlagOutbound) + if err != nil { + return err } - conn, err := outbound.DialContext(context.Background(), commandFlagNetwork, M.ParseSocksaddr(address)) + conn, err := dialer.DialContext(context.Background(), commandConnectFlagNetwork, M.ParseSocksaddr(address)) if err != nil { return E.Cause(err, "connect to server") } @@ -59,6 +64,9 @@ func connect(address string) error { group.Append("download", func(ctx context.Context) error { return common.Error(bufio.Copy(os.Stdout, conn)) }) + group.Cleanup(func() { + conn.Close() + }) err = group.Run(context.Background()) if E.IsClosed(err) { log.Info(err) diff --git a/cmd/sing-box/cmd_tools_fetch.go b/cmd/sing-box/cmd_tools_fetch.go new file mode 100644 index 00000000..4669d5d4 --- /dev/null +++ b/cmd/sing-box/cmd_tools_fetch.go @@ -0,0 +1,95 @@ +package main + +import ( + "context" + "errors" + "io" + "net" + "net/http" + "net/url" + "os" + + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing/common/bufio" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + + "github.com/spf13/cobra" +) + +var commandFetchFlagOutbound string + +var commandFetch = &cobra.Command{ + Use: "fetch", + Short: "Fetch an URL", + Args: cobra.MinimumNArgs(1), + Run: func(cmd *cobra.Command, args []string) { + err := fetch(args) + if err != nil { + log.Fatal(err) + } + }, +} + +func init() { + commandFetch.Flags().StringVar(&commandFetchFlagOutbound, "outbound", "", "outbound tag") + commandTools.AddCommand(commandFetch) +} + +var httpClient *http.Client + +func fetch(args []string) error { + instance, err := createPreStartedClient() + if err != nil { + return err + } + defer instance.Close() + httpClient = &http.Client{ + Transport: &http.Transport{ + DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { + dialer, err := createDialer(instance, N.NetworkTCP, commandFetchFlagOutbound) + if err != nil { + return nil, err + } + return dialer.DialContext(ctx, network, M.ParseSocksaddr(addr)) + }, + ForceAttemptHTTP2: true, + }, + } + defer httpClient.CloseIdleConnections() + for _, urlString := range args { + parsedURL, err := url.Parse(urlString) + if err != nil { + return err + } + switch parsedURL.Scheme { + case "": + parsedURL.Scheme = "http" + fallthrough + case "http", "https": + err = fetchHTTP(parsedURL) + if err != nil { + return err + } + } + } + return nil +} + +func fetchHTTP(parsedURL *url.URL) error { + request, err := http.NewRequest("GET", parsedURL.String(), nil) + if err != nil { + return err + } + request.Header.Add("User-Agent", "curl/7.88.0") + response, err := httpClient.Do(request) + if err != nil { + return err + } + defer response.Body.Close() + _, err = bufio.Copy(os.Stdout, response.Body) + if errors.Is(err, io.EOF) { + return nil + } + return err +}