github.com/xxf098/lite-proxy@v0.15.1-0.20230422081941-12c69f323218/web/server.go (about)

     1  package web
     2  
     3  import (
     4  	"context"
     5  	"crypto/md5"
     6  	"encoding/base64"
     7  	"encoding/json"
     8  	"errors"
     9  	"fmt"
    10  	"io"
    11  	"log"
    12  	"net"
    13  	"net/http"
    14  	"net/url"
    15  	"os"
    16  	"strconv"
    17  	"strings"
    18  	"time"
    19  
    20  	"github.com/gorilla/websocket"
    21  	"github.com/xxf098/lite-proxy/config"
    22  	"github.com/xxf098/lite-proxy/utils"
    23  	"github.com/xxf098/lite-proxy/web/render"
    24  )
    25  
    26  var upgrader = websocket.Upgrader{}
    27  
    28  func ServeFile(port int) error {
    29  	// TODO: Mobile UI
    30  	http.HandleFunc("/", serverFile)
    31  	http.HandleFunc("/test", updateTest)
    32  	http.HandleFunc("/getSubscriptionLink", getSubscriptionLink)
    33  	http.HandleFunc("/getSubscription", getSubscription)
    34  	http.HandleFunc("/generateResult", generateResult)
    35  	log.Printf("Start server at http://127.0.0.1:%d\n", port)
    36  	if ipAddr, err := localIP(); err == nil {
    37  		log.Printf("Start server at http://%s", net.JoinHostPort(ipAddr.String(), strconv.Itoa(port)))
    38  	}
    39  	err := http.ListenAndServe(fmt.Sprintf(":%d", port), nil)
    40  	return err
    41  }
    42  
    43  // func ServeWasm(port int) error {
    44  // 	http.Handle("/", http.FileServer(http.FS(wasmStatic)))
    45  // 	log.Printf("Start server at http://127.0.0.1:%d", port)
    46  // 	err := http.ListenAndServe(fmt.Sprintf(":%d", port), nil)
    47  // 	return err
    48  // }
    49  
    50  func serverFile(w http.ResponseWriter, r *http.Request) {
    51  	h := http.FileServer(http.FS(guiStatic))
    52  	r.URL.Path = "gui/dist" + r.URL.Path
    53  	h.ServeHTTP(w, r)
    54  }
    55  
    56  func updateTest(w http.ResponseWriter, r *http.Request) {
    57  	c, err := upgrader.Upgrade(w, r, nil)
    58  	if err != nil {
    59  		log.Print("upgrade:", err)
    60  		return
    61  	}
    62  	defer c.Close()
    63  	ctx, cancel := context.WithCancel(context.Background())
    64  	defer cancel()
    65  	for {
    66  		mt, message, err := c.ReadMessage()
    67  		if err != nil {
    68  			log.Println("read:", err)
    69  			break
    70  		}
    71  		// log.Printf("recv: %s", message)
    72  		links, options, err := parseMessage(message)
    73  		if err != nil {
    74  			msg := `{"info": "error", "reason": "invalidsub"}`
    75  			c.WriteMessage(mt, []byte(msg))
    76  			continue
    77  		}
    78  		if options.Unique {
    79  			uniqueLinks := []string{}
    80  			uniqueMap := map[string]struct{}{}
    81  			for _, link := range links {
    82  				cfg, err := config.Link2Config(link)
    83  				if err != nil {
    84  					continue
    85  				}
    86  				key := fmt.Sprintf("%s%d%s%s%s", cfg.Server, cfg.Port, cfg.Password, cfg.Protocol, cfg.SNI)
    87  				if _, ok := uniqueMap[key]; !ok {
    88  					uniqueLinks = append(uniqueLinks, link)
    89  					uniqueMap[key] = struct{}{}
    90  				}
    91  			}
    92  			links = uniqueLinks
    93  		}
    94  		p := ProfileTest{
    95  			Writer:      c,
    96  			MessageType: mt,
    97  			Links:       links,
    98  			Options:     options,
    99  		}
   100  		go p.testAll(ctx)
   101  		// err = c.WriteMessage(mt, getMsgByte(0, "gotspeed"))
   102  		// if err != nil {
   103  		// 	log.Println("write:", err)
   104  		// 	break
   105  		// }
   106  	}
   107  }
   108  
   109  func readConfig(configPath string) (*ProfileTestOptions, error) {
   110  	data, err := os.ReadFile(configPath)
   111  	if err != nil {
   112  		return nil, err
   113  	}
   114  	options := &ProfileTestOptions{}
   115  	if err = json.Unmarshal(data, options); err != nil {
   116  		return nil, err
   117  	}
   118  	if options.Concurrency < 1 {
   119  		options.Concurrency = 1
   120  	}
   121  	if options.Language == "" {
   122  		options.Language = "en"
   123  	}
   124  	if options.Theme == "" {
   125  		options.Theme = "rainbow"
   126  	}
   127  	if options.Timeout < 8 {
   128  		options.Timeout = 8
   129  	}
   130  	options.Timeout = options.Timeout * time.Second
   131  	return options, nil
   132  }
   133  
   134  func TestFromCMD(subscription string, configPath *string) error {
   135  	ctx, cancel := context.WithCancel(context.Background())
   136  	defer cancel()
   137  	options := ProfileTestOptions{
   138  		GroupName:       "Default",
   139  		SpeedTestMode:   "all",
   140  		PingMethod:      "googleping",
   141  		SortMethod:      "rspeed",
   142  		Concurrency:     2,
   143  		TestMode:        2,
   144  		Subscription:    subscription,
   145  		Language:        "en",
   146  		FontSize:        24,
   147  		Theme:           "rainbow",
   148  		Timeout:         15 * time.Second,
   149  		GeneratePicMode: PIC_PATH,
   150  		OutputMode:      PIC_PATH,
   151  	}
   152  	if configPath != nil {
   153  		if opt, err := readConfig(*configPath); err == nil {
   154  			options = *opt
   155  			if options.GeneratePicMode != 0 {
   156  				options.OutputMode = options.GeneratePicMode
   157  			}
   158  			// options.GeneratePic = true
   159  		}
   160  	}
   161  	// check url
   162  	if len(subscription) > 0 && subscription != options.Subscription {
   163  		if _, err := url.Parse(subscription); err == nil {
   164  			options.Subscription = subscription
   165  		} else if _, err := os.Stat(subscription); err == nil {
   166  			options.Subscription = subscription
   167  		}
   168  	}
   169  	if jsonOpt, err := json.Marshal(options); err == nil {
   170  		log.Printf("json options: %s\n", string(jsonOpt))
   171  	}
   172  	_, err := TestContext(ctx, options, &OutputMessageWriter{})
   173  	return err
   174  }
   175  
   176  // use as golang api
   177  func TestContext(ctx context.Context, options ProfileTestOptions, w MessageWriter) (render.Nodes, error) {
   178  	links, err := ParseLinks(options.Subscription)
   179  	if err != nil {
   180  		return nil, err
   181  	}
   182  	// outputMessageWriter := OutputMessageWriter{}
   183  	p := ProfileTest{
   184  		Writer:      w,
   185  		MessageType: 1,
   186  		Links:       links,
   187  		Options:     &options,
   188  	}
   189  	return p.testAll(ctx)
   190  }
   191  
   192  // use as golang api
   193  func TestAsyncContext(ctx context.Context, options ProfileTestOptions) (chan render.Node, []string, error) {
   194  	links, err := ParseLinks(options.Subscription)
   195  	if err != nil {
   196  		return nil, nil, err
   197  	}
   198  	// outputMessageWriter := OutputMessageWriter{}
   199  	p := ProfileTest{
   200  		Writer:      nil,
   201  		MessageType: ALLTEST,
   202  		Links:       links,
   203  		Options:     &options,
   204  	}
   205  	nodeChan, err := p.TestAll(ctx, nil)
   206  	return nodeChan, links, err
   207  }
   208  
   209  type TestResult struct {
   210  	TotalTraffic string `json:"totalTraffic"`
   211  	TotalTime    string `json:"totalTime"`
   212  	Language     string `json:"language"`
   213  	FontSize     int    `json:"fontSize"`
   214  	Theme        string `json:"theme"`
   215  	// SortMethod   string       `json:"sortMethod"`
   216  	Nodes render.Nodes `json:"nodes"`
   217  }
   218  
   219  func generateResult(w http.ResponseWriter, r *http.Request) {
   220  	result := TestResult{}
   221  	if r.Body == nil {
   222  		http.Error(w, "Please send a request body", 400)
   223  		return
   224  	}
   225  	data, err := io.ReadAll(r.Body)
   226  	if err != nil {
   227  		http.Error(w, "Please send a request body", 400)
   228  		return
   229  	}
   230  	if err = json.Unmarshal(data, &result); err != nil {
   231  		http.Error(w, err.Error(), 400)
   232  		return
   233  	}
   234  	fontPath := "WenQuanYiMicroHei-01.ttf"
   235  	options := render.NewTableOptions(40, 30, 0.5, 0.5, result.FontSize, 0.5, fontPath, result.Language, result.Theme, "Asia/Shanghai", FontBytes)
   236  	table, err := render.NewTableWithOption(result.Nodes, &options)
   237  	if err != nil {
   238  		http.Error(w, err.Error(), 400)
   239  		return
   240  	}
   241  	linksCount := 0
   242  	successCount := 0
   243  	for _, v := range result.Nodes {
   244  		linksCount += 1
   245  		if v.IsOk {
   246  			successCount += 1
   247  		}
   248  	}
   249  	msg := table.FormatTraffic(result.TotalTraffic, result.TotalTime, fmt.Sprintf("%d/%d", successCount, linksCount))
   250  	if picdata, err := table.EncodeB64(msg); err == nil {
   251  		fmt.Fprint(w, picdata)
   252  	}
   253  
   254  }
   255  
   256  func isPrivateIP(ip net.IP) bool {
   257  	var privateIPBlocks []*net.IPNet
   258  	for _, cidr := range []string{
   259  		// don't check loopback ips
   260  		//"127.0.0.0/8",    // IPv4 loopback
   261  		//"::1/128",        // IPv6 loopback
   262  		//"fe80::/10",      // IPv6 link-local
   263  		"10.0.0.0/8",     // RFC1918
   264  		"172.16.0.0/12",  // RFC1918
   265  		"192.168.0.0/16", // RFC1918
   266  	} {
   267  		_, block, _ := net.ParseCIDR(cidr)
   268  		privateIPBlocks = append(privateIPBlocks, block)
   269  	}
   270  
   271  	for _, block := range privateIPBlocks {
   272  		if block.Contains(ip) {
   273  			return true
   274  		}
   275  	}
   276  
   277  	return false
   278  }
   279  
   280  func localIP() (net.IP, error) {
   281  	ifaces, err := net.Interfaces()
   282  	if err != nil {
   283  		return nil, err
   284  	}
   285  	for _, i := range ifaces {
   286  		addrs, err := i.Addrs()
   287  		if err != nil {
   288  			return nil, err
   289  		}
   290  
   291  		for _, addr := range addrs {
   292  			var ip net.IP
   293  			switch v := addr.(type) {
   294  			case *net.IPNet:
   295  				ip = v.IP
   296  			case *net.IPAddr:
   297  				ip = v.IP
   298  			}
   299  
   300  			if isPrivateIP(ip) {
   301  				return ip, nil
   302  			}
   303  		}
   304  	}
   305  
   306  	return nil, errors.New("no IP")
   307  }
   308  
   309  type GetSubscriptionLink struct {
   310  	FilePath string `json:"filePath"`
   311  	Group    string `json:"group"`
   312  }
   313  
   314  var subscriptionLinkMap map[string]string = make(map[string]string)
   315  
   316  func getSubscriptionLink(w http.ResponseWriter, r *http.Request) {
   317  	body := GetSubscriptionLink{}
   318  	if r.Body == nil {
   319  		http.Error(w, "Invalid Parameter", 400)
   320  		return
   321  	}
   322  	data, err := io.ReadAll(r.Body)
   323  	if err != nil {
   324  		http.Error(w, "Invalid Parameter", 400)
   325  		return
   326  	}
   327  	if err = json.Unmarshal(data, &body); err != nil {
   328  		http.Error(w, err.Error(), 400)
   329  		return
   330  	}
   331  	if len(body.FilePath) == 0 || len(body.Group) == 0 {
   332  		http.Error(w, "Invalid Parameter", 400)
   333  		return
   334  	}
   335  	ipAddr, err := localIP()
   336  	if err != nil {
   337  		http.Error(w, err.Error(), 400)
   338  		return
   339  	}
   340  	md5Hash := fmt.Sprintf("%x", md5.Sum([]byte(body.FilePath)))
   341  	subscriptionLinkMap[md5Hash] = body.FilePath
   342  	subscriptionLink := fmt.Sprintf("http://%s:10888/getSubscription?key=%s&group=%s", ipAddr.String(), md5Hash, body.Group)
   343  	fmt.Fprint(w, subscriptionLink)
   344  }
   345  
   346  // POST
   347  func getSubscription(w http.ResponseWriter, r *http.Request) {
   348  	queries := r.URL.Query()
   349  	key := queries.Get("key")
   350  	if len(key) < 1 {
   351  		http.Error(w, "Key not found", 400)
   352  		return
   353  	}
   354  	// sub format
   355  	sub := queries.Get("sub")
   356  	filePath, ok := subscriptionLinkMap[key]
   357  	if !ok {
   358  		http.Error(w, "Wrong key", 400)
   359  		return
   360  	}
   361  	// convert yaml link
   362  	if isYamlFile(filePath) && utils.IsUrl(filePath) {
   363  		links, err := getSubscriptionLinks(filePath)
   364  		if err != nil {
   365  			http.Error(w, err.Error(), 400)
   366  			return
   367  		}
   368  		b64Data := base64.StdEncoding.EncodeToString([]byte(strings.Join(links, "\n")))
   369  		w.Write([]byte(b64Data))
   370  		return
   371  	}
   372  	// FIXME
   373  	if isYamlFile(filePath) {
   374  		data, err := writeClash(filePath)
   375  		if err != nil {
   376  			http.Error(w, err.Error(), 400)
   377  			return
   378  		}
   379  		w.Write(data)
   380  		return
   381  	}
   382  	data, err := os.ReadFile(filePath)
   383  	if err != nil {
   384  		http.Error(w, err.Error(), 400)
   385  		return
   386  	}
   387  
   388  	if len(data) > 128 && strings.Contains(string(data[:128]), "proxies:") {
   389  		if dataClash, err := writeClash(filePath); err == nil && len(dataClash) > 0 {
   390  			data = dataClash
   391  		}
   392  	}
   393  	// convert shadowrocket to v2ray
   394  	if sub == "v2ray" {
   395  		if dataShadowrocket, err := writeShadowrocket(data); err == nil && len(dataShadowrocket) > 0 {
   396  			data = dataShadowrocket
   397  		}
   398  	}
   399  
   400  	w.Write(data)
   401  }
   402  
   403  func writeClash(filePath string) ([]byte, error) {
   404  	links, err := parseClashFileByLine(filePath)
   405  	if err != nil {
   406  		//
   407  		return nil, err
   408  	}
   409  	subscription := []byte(strings.Join(links, "\n"))
   410  	data := make([]byte, base64.StdEncoding.EncodedLen(len(subscription)))
   411  	base64.StdEncoding.Encode(data, subscription)
   412  	return data, nil
   413  }
   414  
   415  func writeShadowrocket(data []byte) ([]byte, error) {
   416  	links, err := ParseLinks(string(data))
   417  	if err != nil {
   418  		return nil, err
   419  	}
   420  	newLinks := make([]string, 0, len(links))
   421  	for _, link := range links {
   422  		if strings.HasPrefix(link, "vmess://") && strings.Contains(link, "&") {
   423  			if newLink, err := config.ShadowrocketLinkToVmessLink(link); err == nil {
   424  				newLinks = append(newLinks, newLink)
   425  			}
   426  		} else {
   427  			newLinks = append(newLinks, link)
   428  		}
   429  	}
   430  	subscription := []byte(strings.Join(newLinks, "\n"))
   431  	data = make([]byte, base64.StdEncoding.EncodedLen(len(subscription)))
   432  	base64.StdEncoding.Encode(data, subscription)
   433  	return data, nil
   434  }