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  }