server.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429
  1. package clashapi
  2. import (
  3. "bytes"
  4. "context"
  5. "errors"
  6. "net"
  7. "net/http"
  8. "os"
  9. "strings"
  10. "time"
  11. "github.com/sagernet/sing-box/adapter"
  12. "github.com/sagernet/sing-box/common/json"
  13. "github.com/sagernet/sing-box/common/urltest"
  14. C "github.com/sagernet/sing-box/constant"
  15. "github.com/sagernet/sing-box/experimental"
  16. "github.com/sagernet/sing-box/experimental/clashapi/cachefile"
  17. "github.com/sagernet/sing-box/experimental/clashapi/trafficontrol"
  18. "github.com/sagernet/sing-box/log"
  19. "github.com/sagernet/sing-box/option"
  20. "github.com/sagernet/sing/common"
  21. E "github.com/sagernet/sing/common/exceptions"
  22. F "github.com/sagernet/sing/common/format"
  23. N "github.com/sagernet/sing/common/network"
  24. "github.com/sagernet/sing/service"
  25. "github.com/sagernet/sing/service/filemanager"
  26. "github.com/sagernet/websocket"
  27. "github.com/go-chi/chi/v5"
  28. "github.com/go-chi/cors"
  29. "github.com/go-chi/render"
  30. )
  31. func init() {
  32. experimental.RegisterClashServerConstructor(NewServer)
  33. }
  34. var _ adapter.ClashServer = (*Server)(nil)
  35. type Server struct {
  36. ctx context.Context
  37. router adapter.Router
  38. logger log.Logger
  39. httpServer *http.Server
  40. trafficManager *trafficontrol.Manager
  41. urlTestHistory *urltest.HistoryStorage
  42. mode string
  43. storeSelected bool
  44. storeFakeIP bool
  45. cacheFilePath string
  46. cacheID string
  47. cacheFile adapter.ClashCacheFile
  48. externalController bool
  49. externalUI string
  50. externalUIDownloadURL string
  51. externalUIDownloadDetour string
  52. }
  53. func NewServer(ctx context.Context, router adapter.Router, logFactory log.ObservableFactory, options option.ClashAPIOptions) (adapter.ClashServer, error) {
  54. trafficManager := trafficontrol.NewManager()
  55. chiRouter := chi.NewRouter()
  56. server := &Server{
  57. ctx: ctx,
  58. router: router,
  59. logger: logFactory.NewLogger("clash-api"),
  60. httpServer: &http.Server{
  61. Addr: options.ExternalController,
  62. Handler: chiRouter,
  63. },
  64. trafficManager: trafficManager,
  65. mode: strings.ToLower(options.DefaultMode),
  66. storeSelected: options.StoreSelected,
  67. externalController: options.ExternalController != "",
  68. storeFakeIP: options.StoreFakeIP,
  69. externalUIDownloadURL: options.ExternalUIDownloadURL,
  70. externalUIDownloadDetour: options.ExternalUIDownloadDetour,
  71. }
  72. server.urlTestHistory = service.PtrFromContext[urltest.HistoryStorage](ctx)
  73. if server.urlTestHistory == nil {
  74. server.urlTestHistory = urltest.NewHistoryStorage()
  75. }
  76. if server.mode == "" {
  77. server.mode = "rule"
  78. }
  79. if options.StoreSelected || options.StoreFakeIP || options.ExternalController == "" {
  80. cachePath := os.ExpandEnv(options.CacheFile)
  81. if cachePath == "" {
  82. cachePath = "cache.db"
  83. }
  84. if foundPath, loaded := C.FindPath(cachePath); loaded {
  85. cachePath = foundPath
  86. } else {
  87. cachePath = filemanager.BasePath(ctx, cachePath)
  88. }
  89. server.cacheFilePath = cachePath
  90. server.cacheID = options.CacheID
  91. }
  92. cors := cors.New(cors.Options{
  93. AllowedOrigins: []string{"*"},
  94. AllowedMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE"},
  95. AllowedHeaders: []string{"Content-Type", "Authorization"},
  96. MaxAge: 300,
  97. })
  98. chiRouter.Use(cors.Handler)
  99. chiRouter.Group(func(r chi.Router) {
  100. r.Use(authentication(options.Secret))
  101. r.Get("/", hello(options.ExternalUI != ""))
  102. r.Get("/logs", getLogs(logFactory))
  103. r.Get("/traffic", traffic(trafficManager))
  104. r.Get("/version", version)
  105. r.Mount("/configs", configRouter(server, logFactory, server.logger))
  106. r.Mount("/proxies", proxyRouter(server, router))
  107. r.Mount("/rules", ruleRouter(router))
  108. r.Mount("/connections", connectionRouter(router, trafficManager))
  109. r.Mount("/providers/proxies", proxyProviderRouter())
  110. r.Mount("/providers/rules", ruleProviderRouter())
  111. r.Mount("/script", scriptRouter())
  112. r.Mount("/profile", profileRouter())
  113. r.Mount("/cache", cacheRouter(router))
  114. r.Mount("/dns", dnsRouter(router))
  115. server.setupMetaAPI(r)
  116. })
  117. if options.ExternalUI != "" {
  118. server.externalUI = filemanager.BasePath(ctx, os.ExpandEnv(options.ExternalUI))
  119. chiRouter.Group(func(r chi.Router) {
  120. fs := http.StripPrefix("/ui", http.FileServer(http.Dir(server.externalUI)))
  121. r.Get("/ui", http.RedirectHandler("/ui/", http.StatusTemporaryRedirect).ServeHTTP)
  122. r.Get("/ui/*", func(w http.ResponseWriter, r *http.Request) {
  123. fs.ServeHTTP(w, r)
  124. })
  125. })
  126. }
  127. return server, nil
  128. }
  129. func (s *Server) PreStart() error {
  130. if s.cacheFilePath != "" {
  131. cacheFile, err := cachefile.Open(s.cacheFilePath, s.cacheID)
  132. if err != nil {
  133. return E.Cause(err, "open cache file")
  134. }
  135. s.cacheFile = cacheFile
  136. }
  137. return nil
  138. }
  139. func (s *Server) Start() error {
  140. if s.externalController {
  141. s.checkAndDownloadExternalUI()
  142. listener, err := net.Listen("tcp", s.httpServer.Addr)
  143. if err != nil {
  144. return E.Cause(err, "external controller listen error")
  145. }
  146. s.logger.Info("restful api listening at ", listener.Addr())
  147. go func() {
  148. err = s.httpServer.Serve(listener)
  149. if err != nil && !errors.Is(err, http.ErrServerClosed) {
  150. s.logger.Error("external controller serve error: ", err)
  151. }
  152. }()
  153. }
  154. return nil
  155. }
  156. func (s *Server) Close() error {
  157. return common.Close(
  158. common.PtrOrNil(s.httpServer),
  159. s.trafficManager,
  160. s.cacheFile,
  161. )
  162. }
  163. func (s *Server) Mode() string {
  164. return s.mode
  165. }
  166. func (s *Server) StoreSelected() bool {
  167. return s.storeSelected
  168. }
  169. func (s *Server) StoreFakeIP() bool {
  170. return s.storeFakeIP
  171. }
  172. func (s *Server) CacheFile() adapter.ClashCacheFile {
  173. return s.cacheFile
  174. }
  175. func (s *Server) HistoryStorage() *urltest.HistoryStorage {
  176. return s.urlTestHistory
  177. }
  178. func (s *Server) TrafficManager() *trafficontrol.Manager {
  179. return s.trafficManager
  180. }
  181. func (s *Server) RoutedConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, matchedRule adapter.Rule) (net.Conn, adapter.Tracker) {
  182. tracker := trafficontrol.NewTCPTracker(conn, s.trafficManager, castMetadata(metadata), s.router, matchedRule)
  183. return tracker, tracker
  184. }
  185. func (s *Server) RoutedPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, matchedRule adapter.Rule) (N.PacketConn, adapter.Tracker) {
  186. tracker := trafficontrol.NewUDPTracker(conn, s.trafficManager, castMetadata(metadata), s.router, matchedRule)
  187. return tracker, tracker
  188. }
  189. func castMetadata(metadata adapter.InboundContext) trafficontrol.Metadata {
  190. var inbound string
  191. if metadata.Inbound != "" {
  192. inbound = metadata.InboundType + "/" + metadata.Inbound
  193. } else {
  194. inbound = metadata.InboundType
  195. }
  196. var domain string
  197. if metadata.Domain != "" {
  198. domain = metadata.Domain
  199. } else {
  200. domain = metadata.Destination.Fqdn
  201. }
  202. var processPath string
  203. if metadata.ProcessInfo != nil {
  204. if metadata.ProcessInfo.ProcessPath != "" {
  205. processPath = metadata.ProcessInfo.ProcessPath
  206. } else if metadata.ProcessInfo.PackageName != "" {
  207. processPath = metadata.ProcessInfo.PackageName
  208. }
  209. if processPath == "" {
  210. if metadata.ProcessInfo.UserId != -1 {
  211. processPath = F.ToString(metadata.ProcessInfo.UserId)
  212. }
  213. } else if metadata.ProcessInfo.User != "" {
  214. processPath = F.ToString(processPath, " (", metadata.ProcessInfo.User, ")")
  215. } else if metadata.ProcessInfo.UserId != -1 {
  216. processPath = F.ToString(processPath, " (", metadata.ProcessInfo.UserId, ")")
  217. }
  218. }
  219. return trafficontrol.Metadata{
  220. NetWork: metadata.Network,
  221. Type: inbound,
  222. SrcIP: metadata.Source.Addr,
  223. DstIP: metadata.Destination.Addr,
  224. SrcPort: F.ToString(metadata.Source.Port),
  225. DstPort: F.ToString(metadata.Destination.Port),
  226. Host: domain,
  227. DNSMode: "normal",
  228. ProcessPath: processPath,
  229. }
  230. }
  231. func authentication(serverSecret string) func(next http.Handler) http.Handler {
  232. return func(next http.Handler) http.Handler {
  233. fn := func(w http.ResponseWriter, r *http.Request) {
  234. if serverSecret == "" {
  235. next.ServeHTTP(w, r)
  236. return
  237. }
  238. // Browser websocket not support custom header
  239. if websocket.IsWebSocketUpgrade(r) && r.URL.Query().Get("token") != "" {
  240. token := r.URL.Query().Get("token")
  241. if token != serverSecret {
  242. render.Status(r, http.StatusUnauthorized)
  243. render.JSON(w, r, ErrUnauthorized)
  244. return
  245. }
  246. next.ServeHTTP(w, r)
  247. return
  248. }
  249. header := r.Header.Get("Authorization")
  250. bearer, token, found := strings.Cut(header, " ")
  251. hasInvalidHeader := bearer != "Bearer"
  252. hasInvalidSecret := !found || token != serverSecret
  253. if hasInvalidHeader || hasInvalidSecret {
  254. render.Status(r, http.StatusUnauthorized)
  255. render.JSON(w, r, ErrUnauthorized)
  256. return
  257. }
  258. next.ServeHTTP(w, r)
  259. }
  260. return http.HandlerFunc(fn)
  261. }
  262. }
  263. func hello(redirect bool) func(w http.ResponseWriter, r *http.Request) {
  264. return func(w http.ResponseWriter, r *http.Request) {
  265. if redirect {
  266. http.Redirect(w, r, "/ui/", http.StatusTemporaryRedirect)
  267. } else {
  268. render.JSON(w, r, render.M{"hello": "clash"})
  269. }
  270. }
  271. }
  272. var upgrader = websocket.Upgrader{
  273. CheckOrigin: func(r *http.Request) bool {
  274. return true
  275. },
  276. }
  277. type Traffic struct {
  278. Up int64 `json:"up"`
  279. Down int64 `json:"down"`
  280. }
  281. func traffic(trafficManager *trafficontrol.Manager) func(w http.ResponseWriter, r *http.Request) {
  282. return func(w http.ResponseWriter, r *http.Request) {
  283. var wsConn *websocket.Conn
  284. if websocket.IsWebSocketUpgrade(r) {
  285. var err error
  286. wsConn, err = upgrader.Upgrade(w, r, nil)
  287. if err != nil {
  288. return
  289. }
  290. }
  291. if wsConn == nil {
  292. w.Header().Set("Content-Type", "application/json")
  293. render.Status(r, http.StatusOK)
  294. }
  295. tick := time.NewTicker(time.Second)
  296. defer tick.Stop()
  297. buf := &bytes.Buffer{}
  298. var err error
  299. for range tick.C {
  300. buf.Reset()
  301. up, down := trafficManager.Now()
  302. if err := json.NewEncoder(buf).Encode(Traffic{
  303. Up: up,
  304. Down: down,
  305. }); err != nil {
  306. break
  307. }
  308. if wsConn == nil {
  309. _, err = w.Write(buf.Bytes())
  310. w.(http.Flusher).Flush()
  311. } else {
  312. err = wsConn.WriteMessage(websocket.TextMessage, buf.Bytes())
  313. }
  314. if err != nil {
  315. break
  316. }
  317. }
  318. }
  319. }
  320. type Log struct {
  321. Type string `json:"type"`
  322. Payload string `json:"payload"`
  323. }
  324. func getLogs(logFactory log.ObservableFactory) func(w http.ResponseWriter, r *http.Request) {
  325. return func(w http.ResponseWriter, r *http.Request) {
  326. levelText := r.URL.Query().Get("level")
  327. if levelText == "" {
  328. levelText = "info"
  329. }
  330. level, ok := log.ParseLevel(levelText)
  331. if ok != nil {
  332. render.Status(r, http.StatusBadRequest)
  333. render.JSON(w, r, ErrBadRequest)
  334. return
  335. }
  336. subscription, done, err := logFactory.Subscribe()
  337. if err != nil {
  338. render.Status(r, http.StatusNoContent)
  339. return
  340. }
  341. defer logFactory.UnSubscribe(subscription)
  342. var wsConn *websocket.Conn
  343. if websocket.IsWebSocketUpgrade(r) {
  344. var err error
  345. wsConn, err = upgrader.Upgrade(w, r, nil)
  346. if err != nil {
  347. return
  348. }
  349. }
  350. if wsConn == nil {
  351. w.Header().Set("Content-Type", "application/json")
  352. render.Status(r, http.StatusOK)
  353. }
  354. buf := &bytes.Buffer{}
  355. var logEntry log.Entry
  356. for {
  357. select {
  358. case <-done:
  359. return
  360. case logEntry = <-subscription:
  361. }
  362. if logEntry.Level > level {
  363. continue
  364. }
  365. buf.Reset()
  366. err = json.NewEncoder(buf).Encode(Log{
  367. Type: log.FormatLevel(logEntry.Level),
  368. Payload: logEntry.Message,
  369. })
  370. if err != nil {
  371. break
  372. }
  373. if wsConn == nil {
  374. _, err = w.Write(buf.Bytes())
  375. w.(http.Flusher).Flush()
  376. } else {
  377. err = wsConn.WriteMessage(websocket.TextMessage, buf.Bytes())
  378. }
  379. if err != nil {
  380. break
  381. }
  382. }
  383. }
  384. }
  385. func version(w http.ResponseWriter, r *http.Request) {
  386. render.JSON(w, r, render.M{"version": "sing-box " + C.Version, "premium": true, "meta": true})
  387. }