github.com/igoogolx/clash@v1.19.8/hub/route/server.go (about) 1 package route 2 3 import ( 4 "bytes" 5 "crypto/subtle" 6 "encoding/json" 7 "net" 8 "net/http" 9 "strings" 10 "time" 11 "unsafe" 12 13 C "github.com/igoogolx/clash/constant" 14 "github.com/igoogolx/clash/log" 15 "github.com/igoogolx/clash/tunnel/statistic" 16 17 "github.com/Dreamacro/protobytes" 18 "github.com/go-chi/chi/v5" 19 "github.com/go-chi/cors" 20 "github.com/go-chi/render" 21 "github.com/gorilla/websocket" 22 ) 23 24 var ( 25 serverSecret = "" 26 serverAddr = "" 27 28 uiPath = "" 29 30 upgrader = websocket.Upgrader{ 31 CheckOrigin: func(r *http.Request) bool { 32 return true 33 }, 34 } 35 ) 36 37 type Traffic struct { 38 Up int64 `json:"up"` 39 Down int64 `json:"down"` 40 } 41 42 func SetUIPath(path string) { 43 uiPath = C.Path.Resolve(path) 44 } 45 46 func Start(addr string, secret string) { 47 if serverAddr != "" { 48 return 49 } 50 51 serverAddr = addr 52 serverSecret = secret 53 54 r := chi.NewRouter() 55 56 cors := cors.New(cors.Options{ 57 AllowedOrigins: []string{"*"}, 58 AllowedMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE"}, 59 AllowedHeaders: []string{"Content-Type", "Authorization"}, 60 MaxAge: 300, 61 }) 62 63 r.Use(cors.Handler) 64 r.Group(func(r chi.Router) { 65 r.Use(authentication) 66 67 r.Get("/", hello) 68 r.Get("/logs", getLogs) 69 r.Get("/traffic", traffic) 70 r.Get("/version", version) 71 r.Mount("/configs", configRouter()) 72 r.Mount("/inbounds", inboundRouter()) 73 r.Mount("/proxies", proxyRouter()) 74 r.Mount("/rules", ruleRouter()) 75 r.Mount("/connections", connectionRouter()) 76 r.Mount("/providers/proxies", proxyProviderRouter()) 77 r.Mount("/dns", dnsRouter()) 78 }) 79 80 if uiPath != "" { 81 r.Group(func(r chi.Router) { 82 fs := http.StripPrefix("/ui", http.FileServer(http.Dir(uiPath))) 83 r.Get("/ui", http.RedirectHandler("/ui/", http.StatusTemporaryRedirect).ServeHTTP) 84 r.Get("/ui/*", func(w http.ResponseWriter, r *http.Request) { 85 fs.ServeHTTP(w, r) 86 }) 87 }) 88 } 89 90 l, err := net.Listen("tcp", addr) 91 if err != nil { 92 log.Errorln("External controller listen error: %s", err) 93 return 94 } 95 serverAddr = l.Addr().String() 96 log.Infoln("RESTful API listening at: %s", serverAddr) 97 if err = http.Serve(l, r); err != nil { 98 log.Errorln("External controller serve error: %s", err) 99 } 100 } 101 102 func safeEuqal(a, b string) bool { 103 aBuf := unsafe.Slice(unsafe.StringData(a), len(a)) 104 bBuf := unsafe.Slice(unsafe.StringData(b), len(b)) 105 return subtle.ConstantTimeCompare(aBuf, bBuf) == 1 106 } 107 108 func authentication(next http.Handler) http.Handler { 109 fn := func(w http.ResponseWriter, r *http.Request) { 110 if serverSecret == "" { 111 next.ServeHTTP(w, r) 112 return 113 } 114 115 // Browser websocket not support custom header 116 if websocket.IsWebSocketUpgrade(r) && r.URL.Query().Get("token") != "" { 117 token := r.URL.Query().Get("token") 118 if !safeEuqal(token, serverSecret) { 119 render.Status(r, http.StatusUnauthorized) 120 render.JSON(w, r, ErrUnauthorized) 121 return 122 } 123 next.ServeHTTP(w, r) 124 return 125 } 126 127 header := r.Header.Get("Authorization") 128 bearer, token, found := strings.Cut(header, " ") 129 130 hasInvalidHeader := bearer != "Bearer" 131 hasInvalidSecret := !found || !safeEuqal(token, serverSecret) 132 if hasInvalidHeader || hasInvalidSecret { 133 render.Status(r, http.StatusUnauthorized) 134 render.JSON(w, r, ErrUnauthorized) 135 return 136 } 137 next.ServeHTTP(w, r) 138 } 139 return http.HandlerFunc(fn) 140 } 141 142 func hello(w http.ResponseWriter, r *http.Request) { 143 render.JSON(w, r, render.M{"hello": "clash"}) 144 } 145 146 func traffic(w http.ResponseWriter, r *http.Request) { 147 var wsConn *websocket.Conn 148 if websocket.IsWebSocketUpgrade(r) { 149 var err error 150 wsConn, err = upgrader.Upgrade(w, r, nil) 151 if err != nil { 152 return 153 } 154 } 155 156 if wsConn == nil { 157 w.Header().Set("Content-Type", "application/json") 158 render.Status(r, http.StatusOK) 159 } 160 161 tick := time.NewTicker(time.Second) 162 defer tick.Stop() 163 t := statistic.DefaultManager 164 buf := protobytes.BytesWriter{} 165 var err error 166 for range tick.C { 167 buf.Reset() 168 up, down := t.Now() 169 if err := json.NewEncoder(&buf).Encode(Traffic{ 170 Up: up, 171 Down: down, 172 }); err != nil { 173 break 174 } 175 176 if wsConn == nil { 177 _, err = w.Write(buf.Bytes()) 178 w.(http.Flusher).Flush() 179 } else { 180 err = wsConn.WriteMessage(websocket.TextMessage, buf.Bytes()) 181 } 182 183 if err != nil { 184 break 185 } 186 } 187 } 188 189 type Log struct { 190 Type string `json:"type"` 191 Payload string `json:"payload"` 192 } 193 194 func getLogs(w http.ResponseWriter, r *http.Request) { 195 levelText := r.URL.Query().Get("level") 196 if levelText == "" { 197 levelText = "info" 198 } 199 200 level, ok := log.LogLevelMapping[levelText] 201 if !ok { 202 render.Status(r, http.StatusBadRequest) 203 render.JSON(w, r, ErrBadRequest) 204 return 205 } 206 207 var wsConn *websocket.Conn 208 if websocket.IsWebSocketUpgrade(r) { 209 var err error 210 wsConn, err = upgrader.Upgrade(w, r, nil) 211 if err != nil { 212 return 213 } 214 } 215 216 if wsConn == nil { 217 w.Header().Set("Content-Type", "application/json") 218 render.Status(r, http.StatusOK) 219 } 220 221 ch := make(chan log.Event, 1024) 222 sub := log.Subscribe() 223 defer log.UnSubscribe(sub) 224 buf := &bytes.Buffer{} 225 226 go func() { 227 for elm := range sub { 228 log := elm.(log.Event) 229 select { 230 case ch <- log: 231 default: 232 } 233 } 234 close(ch) 235 }() 236 237 for log := range ch { 238 if log.LogLevel < level { 239 continue 240 } 241 buf.Reset() 242 243 if err := json.NewEncoder(buf).Encode(Log{ 244 Type: log.Type(), 245 Payload: log.Payload, 246 }); err != nil { 247 break 248 } 249 250 var err error 251 if wsConn == nil { 252 _, err = w.Write(buf.Bytes()) 253 w.(http.Flusher).Flush() 254 } else { 255 err = wsConn.WriteMessage(websocket.TextMessage, buf.Bytes()) 256 } 257 258 if err != nil { 259 break 260 } 261 } 262 } 263 264 func version(w http.ResponseWriter, r *http.Request) { 265 render.JSON(w, r, render.M{"version": C.Version}) 266 }