Browse Source

Add command to fetch a URL

世界 2 years ago
parent
commit
99b2ab5526
3 changed files with 135 additions and 10 deletions
  1. 23 1
      cmd/sing-box/cmd_tools.go
  2. 17 9
      cmd/sing-box/cmd_tools_connect.go
  3. 95 0
      cmd/sing-box/cmd_tools_fetch.go

+ 23 - 1
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
+	}
+}

+ 17 - 9
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)

+ 95 - 0
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
+}