package ssmapi import ( C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing/common" "net/http" "github.com/go-chi/chi/v5" "github.com/go-chi/render" ) func (s *Server) setupRoutes(r chi.Router) { r.Group(func(r chi.Router) { r.Get("/", s.getServerInfo) r.Get("/nodes", s.getNodes) r.Post("/nodes", s.addNode) r.Get("/nodes/{id}", s.getNode) r.Put("/nodes/{id}", s.updateNode) r.Delete("/nodes/{id}", s.deleteNode) r.Get("/users", s.listUser) r.Post("/users", s.addUser) r.Get("/users/{username}", s.getUser) r.Put("/users/{username}", s.updateUser) r.Delete("/users/{username}", s.deleteUser) r.Get("/stats", s.getStats) }) } func (s *Server) getServerInfo(writer http.ResponseWriter, request *http.Request) { render.JSON(writer, request, render.M{ "server": "sing-box", "apiVersion": "v1", "_sing_box_version": C.Version, }) } func (s *Server) getNodes(writer http.ResponseWriter, request *http.Request) { var response struct { Protocols []string `json:"protocols"` Shadowsocks []ShadowsocksNodeObject `json:"shadowsocks,omitempty"` } for _, node := range s.nodes { if !common.Contains(response.Protocols, node.Protocol()) { response.Protocols = append(response.Protocols, node.Protocol()) } switch node.Protocol() { case C.TypeShadowsocks: response.Shadowsocks = append(response.Shadowsocks, node.Shadowsocks()) } } render.JSON(writer, request, &response) } func (s *Server) addNode(writer http.ResponseWriter, request *http.Request) { writer.WriteHeader(http.StatusNotImplemented) } func (s *Server) getNode(writer http.ResponseWriter, request *http.Request) { nodeID := chi.URLParam(request, "id") if nodeID == "" { writer.WriteHeader(http.StatusBadRequest) return } for _, node := range s.nodes { if nodeID == node.ID() { render.JSON(writer, request, render.M{ node.Protocol(): node.Object(), }) return } } } func (s *Server) updateNode(writer http.ResponseWriter, request *http.Request) { writer.WriteHeader(http.StatusNotImplemented) } func (s *Server) deleteNode(writer http.ResponseWriter, request *http.Request) { writer.WriteHeader(http.StatusNotImplemented) } type SSMUserObject struct { UserName string `json:"username"` Password string `json:"uPSK,omitempty"` DownlinkBytes int64 `json:"downlinkBytes"` UplinkBytes int64 `json:"uplinkBytes"` DownlinkPackets int64 `json:"downlinkPackets"` UplinkPackets int64 `json:"uplinkPackets"` TCPSessions int64 `json:"tcpSessions"` UDPSessions int64 `json:"udpSessions"` } func (s *Server) listUser(writer http.ResponseWriter, request *http.Request) { render.JSON(writer, request, render.M{ "users": s.userManager.List(), }) } func (s *Server) addUser(writer http.ResponseWriter, request *http.Request) { var addRequest struct { UserName string `json:"username"` Password string `json:"uPSK"` } err := render.DecodeJSON(request.Body, &addRequest) if err != nil { render.Status(request, http.StatusBadRequest) render.PlainText(writer, request, err.Error()) return } err = s.userManager.Add(addRequest.UserName, addRequest.Password) if err != nil { render.Status(request, http.StatusBadRequest) render.PlainText(writer, request, err.Error()) return } writer.WriteHeader(http.StatusCreated) } func (s *Server) getUser(writer http.ResponseWriter, request *http.Request) { userName := chi.URLParam(request, "username") if userName == "" { writer.WriteHeader(http.StatusBadRequest) return } uPSK, loaded := s.userManager.Get(userName) if !loaded { writer.WriteHeader(http.StatusNotFound) return } user := SSMUserObject{ UserName: userName, Password: uPSK, } s.trafficManager.ReadUser(&user) render.JSON(writer, request, user) } func (s *Server) updateUser(writer http.ResponseWriter, request *http.Request) { userName := chi.URLParam(request, "username") if userName == "" { writer.WriteHeader(http.StatusBadRequest) return } var updateRequest struct { Password string `json:"uPSK"` } err := render.DecodeJSON(request.Body, &updateRequest) if err != nil { render.Status(request, http.StatusBadRequest) render.PlainText(writer, request, err.Error()) return } _, loaded := s.userManager.Get(userName) if !loaded { writer.WriteHeader(http.StatusNotFound) return } err = s.userManager.Update(userName, updateRequest.Password) if err != nil { render.Status(request, http.StatusBadRequest) render.PlainText(writer, request, err.Error()) return } writer.WriteHeader(http.StatusNoContent) } func (s *Server) deleteUser(writer http.ResponseWriter, request *http.Request) { userName := chi.URLParam(request, "username") if userName == "" { writer.WriteHeader(http.StatusBadRequest) return } _, loaded := s.userManager.Get(userName) if !loaded { writer.WriteHeader(http.StatusNotFound) return } err := s.userManager.Delete(userName) if err != nil { render.Status(request, http.StatusBadRequest) render.PlainText(writer, request, err.Error()) return } writer.WriteHeader(http.StatusNoContent) } func (s *Server) getStats(writer http.ResponseWriter, request *http.Request) { requireClear := chi.URLParam(request, "clear") == "true" users := s.userManager.List() s.trafficManager.ReadUsers(users) for i := range users { users[i].Password = "" } uplinkBytes, downlinkBytes, uplinkPackets, downlinkPackets, tcpSessions, udpSessions := s.trafficManager.ReadGlobal() if requireClear { s.trafficManager.Clear() } render.JSON(writer, request, render.M{ "uplinkBytes": uplinkBytes, "downlinkBytes": downlinkBytes, "uplinkPackets": uplinkPackets, "downlinkPackets": downlinkPackets, "tcpSessions": tcpSessions, "udpSessions": udpSessions, "users": users, }) }