package route import ( "bytes" "encoding/json" "net" "net/http" "strings" "time" C "github.com/Dreamacro/clash/constant" _ "github.com/Dreamacro/clash/constant/mime" "github.com/Dreamacro/clash/log" "github.com/Dreamacro/clash/tunnel/statistic" "github.com/go-chi/chi/v5" "github.com/go-chi/cors" "github.com/go-chi/render" "github.com/gorilla/websocket" ) var ( serverSecret = "" serverAddr = "" uiPath = "" upgrader = websocket.Upgrader{ CheckOrigin: func(r *http.Request) bool { return true }, } ) type Traffic struct { Up int64 `json:"up"` Down int64 `json:"down"` } func SetUIPath(path string) { uiPath = C.Path.Resolve(path) } func Start(addr string, secret string) { if serverAddr != "" { return } serverAddr = addr serverSecret = secret r := chi.NewRouter() corsM := cors.New(cors.Options{ AllowedOrigins: []string{"*"}, AllowedMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE"}, AllowedHeaders: []string{"Content-Type", "Authorization"}, MaxAge: 300, }) r.Use(corsM.Handler) r.Group(func(r chi.Router) { r.Use(authentication) r.Get("/", hello) r.Get("/logs", getLogs) r.Get("/traffic", traffic) r.Get("/version", version) r.Mount("/configs", configRouter()) r.Mount("/proxies", proxyRouter()) r.Mount("/rules", ruleRouter()) r.Mount("/connections", connectionRouter()) r.Mount("/providers/proxies", proxyProviderRouter()) r.Mount("/providers/rules", ruleProviderRouter()) r.Mount("/script", scriptRouter()) r.Mount("/cache", cacheRouter()) }) if uiPath != "" { r.Group(func(r chi.Router) { fs := http.StripPrefix("/ui", http.FileServer(http.Dir(uiPath))) r.Get("/ui", http.RedirectHandler("/ui/", http.StatusTemporaryRedirect).ServeHTTP) r.Get("/ui/*", func(w http.ResponseWriter, r *http.Request) { fs.ServeHTTP(w, r) }) }) } l, err := net.Listen("tcp", addr) if err != nil { log.Errorln("External controller listen error: %s", err) return } serverAddr = l.Addr().String() log.Infoln("RESTful API listening at: %s", serverAddr) if err = http.Serve(l, r); err != nil { log.Errorln("External controller serve error: %s", err) } } func authentication(next http.Handler) http.Handler { fn := func(w http.ResponseWriter, r *http.Request) { if serverSecret == "" { next.ServeHTTP(w, r) return } // Browser websocket not support custom header if websocket.IsWebSocketUpgrade(r) && r.URL.Query().Get("token") != "" { token := r.URL.Query().Get("token") if token != serverSecret { render.Status(r, http.StatusUnauthorized) render.JSON(w, r, ErrUnauthorized) return } next.ServeHTTP(w, r) return } header := r.Header.Get("Authorization") bearer, token, found := strings.Cut(header, " ") hasInvalidHeader := bearer != "Bearer" hasInvalidSecret := !found || token != serverSecret if hasInvalidHeader || hasInvalidSecret { render.Status(r, http.StatusUnauthorized) render.JSON(w, r, ErrUnauthorized) return } next.ServeHTTP(w, r) } return http.HandlerFunc(fn) } func hello(w http.ResponseWriter, r *http.Request) { render.JSON(w, r, render.M{"hello": "clash.meta"}) } func traffic(w http.ResponseWriter, r *http.Request) { var wsConn *websocket.Conn if websocket.IsWebSocketUpgrade(r) { var err error wsConn, err = upgrader.Upgrade(w, r, nil) if err != nil { return } } if wsConn == nil { w.Header().Set("Content-Type", "application/json") render.Status(r, http.StatusOK) } tick := time.NewTicker(time.Second) defer tick.Stop() t := statistic.DefaultManager buf := &bytes.Buffer{} var err error for range tick.C { buf.Reset() up, down := t.Now() if err := json.NewEncoder(buf).Encode(Traffic{ Up: up, Down: down, }); err != nil { break } if wsConn == nil { _, err = w.Write(buf.Bytes()) w.(http.Flusher).Flush() } else { err = wsConn.WriteMessage(websocket.TextMessage, buf.Bytes()) } if err != nil { break } } } type Log struct { Type string `json:"type"` Payload string `json:"payload"` } func getLogs(w http.ResponseWriter, r *http.Request) { levelText := r.URL.Query().Get("level") if levelText == "" { levelText = "info" } level, ok := log.LogLevelMapping[levelText] if !ok { render.Status(r, http.StatusBadRequest) render.JSON(w, r, ErrBadRequest) return } var wsConn *websocket.Conn if websocket.IsWebSocketUpgrade(r) { var err error wsConn, err = upgrader.Upgrade(w, r, nil) if err != nil { return } } if wsConn == nil { w.Header().Set("Content-Type", "application/json") render.Status(r, http.StatusOK) } sub := log.Subscribe() defer log.UnSubscribe(sub) buf := &bytes.Buffer{} var err error for elm := range sub { buf.Reset() logM := elm if logM.LogLevel < level { continue } if err := json.NewEncoder(buf).Encode(Log{ Type: logM.Type(), Payload: logM.Payload, }); err != nil { break } if wsConn == nil { _, err = w.Write(buf.Bytes()) w.(http.Flusher).Flush() } else { err = wsConn.WriteMessage(websocket.TextMessage, buf.Bytes()) } if err != nil { break } } } func version(w http.ResponseWriter, r *http.Request) { render.JSON(w, r, render.M{"meta": C.Meta, "version": C.Version}) }