github.com/laof/lite-speed-test@v0.0.0-20230930011949-1f39b7037845/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/laof/lite-speed-test/config" 22 "github.com/laof/lite-speed-test/utils" 23 "github.com/laof/lite-speed-test/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 }