|
@@ -1,10 +1,11 @@
|
|
|
package restful_api
|
|
package restful_api
|
|
|
|
|
|
|
|
import (
|
|
import (
|
|
|
- "encoding/json"
|
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
"github.com/go-chi/chi/v5"
|
|
|
"github.com/go-chi/chi/v5/middleware"
|
|
"github.com/go-chi/chi/v5/middleware"
|
|
|
|
|
+ "github.com/go-chi/render"
|
|
|
"github.com/go-playground/validator/v10"
|
|
"github.com/go-playground/validator/v10"
|
|
|
|
|
+ core "github.com/v2fly/v2ray-core/v4"
|
|
|
"github.com/v2fly/v2ray-core/v4/common/net"
|
|
"github.com/v2fly/v2ray-core/v4/common/net"
|
|
|
"github.com/v2fly/v2ray-core/v4/transport/internet"
|
|
"github.com/v2fly/v2ray-core/v4/transport/internet"
|
|
|
|
|
|
|
@@ -12,105 +13,69 @@ import (
|
|
|
"strings"
|
|
"strings"
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
-func JSONResponse(w http.ResponseWriter, data interface{}, code int) {
|
|
|
|
|
- w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
|
|
|
- w.Header().Set("X-Content-Type-Options", "nosniff")
|
|
|
|
|
- w.WriteHeader(code)
|
|
|
|
|
- _ = json.NewEncoder(w).Encode(data)
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
var validate *validator.Validate
|
|
var validate *validator.Validate
|
|
|
|
|
|
|
|
-type StatsUser struct {
|
|
|
|
|
- uuid string `validate:"required_without=email,uuid4"`
|
|
|
|
|
- email string `validate:"required_without=uuid,email"`
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-type StatsUserResponse struct {
|
|
|
|
|
|
|
+type StatsBound struct { // Better name?
|
|
|
Uplink int64 `json:"uplink"`
|
|
Uplink int64 `json:"uplink"`
|
|
|
Downlink int64 `json:"downlink"`
|
|
Downlink int64 `json:"downlink"`
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-func (rs *restfulService) statsUser(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
- query := r.URL.Query()
|
|
|
|
|
- statsUser := &StatsUser{
|
|
|
|
|
- uuid: query.Get("uuid"),
|
|
|
|
|
- email: query.Get("email"),
|
|
|
|
|
- }
|
|
|
|
|
|
|
+func (rs *restfulService) tagStats(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
+ boundType := chi.URLParam(r, "bound_type")
|
|
|
|
|
+ tag := chi.URLParam(r, "tag")
|
|
|
|
|
|
|
|
- if err := validate.Struct(statsUser); err != nil {
|
|
|
|
|
- JSONResponse(w, http.StatusText(422), 422)
|
|
|
|
|
|
|
+ if validate.Var(boundType, "required,oneof=inbounds outbounds") != nil ||
|
|
|
|
|
+ validate.Var(tag, "required,min=1,max=255") != nil {
|
|
|
|
|
+ render.Status(r, http.StatusUnprocessableEntity)
|
|
|
|
|
+ render.JSON(w, r, render.M{})
|
|
|
|
|
+ return
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- response := &StatsUserResponse{
|
|
|
|
|
- Uplink: 0,
|
|
|
|
|
- Downlink: 0,
|
|
|
|
|
|
|
+ bound := boundType[:len(boundType)-1]
|
|
|
|
|
+ upCounter := rs.stats.GetCounter(bound + ">>>" + tag + ">>>traffic>>>uplink")
|
|
|
|
|
+ downCounter := rs.stats.GetCounter(bound + ">>>" + tag + ">>>traffic>>>downlink")
|
|
|
|
|
+ if upCounter == nil || downCounter == nil {
|
|
|
|
|
+ render.Status(r, http.StatusNotFound)
|
|
|
|
|
+ render.JSON(w, r, render.M{})
|
|
|
|
|
+ return
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- JSONResponse(w, response, 200)
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-type Stats struct {
|
|
|
|
|
- tag string `validate:"required,alpha,min=1,max=255"`
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-type StatsBound struct { // Better name?
|
|
|
|
|
- Uplink int64 `json:"uplink"`
|
|
|
|
|
- Downlink int64 `json:"downlink"`
|
|
|
|
|
|
|
+ render.JSON(w, r, &StatsBound{
|
|
|
|
|
+ Uplink: upCounter.Value(),
|
|
|
|
|
+ Downlink: downCounter.Value(),
|
|
|
|
|
+ })
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-type StatsResponse struct {
|
|
|
|
|
- Inbound StatsBound `json:"inbound"`
|
|
|
|
|
- Outbound StatsBound `json:"outbound"`
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-func (rs *restfulService) statsRequest(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
- stats := &Stats{
|
|
|
|
|
- tag: r.URL.Query().Get("tag"),
|
|
|
|
|
- }
|
|
|
|
|
- if err := validate.Struct(stats); err != nil {
|
|
|
|
|
- JSONResponse(w, http.StatusText(422), 422)
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- response := StatsResponse{
|
|
|
|
|
- Inbound: StatsBound{
|
|
|
|
|
- Uplink: 1,
|
|
|
|
|
- Downlink: 1,
|
|
|
|
|
- },
|
|
|
|
|
- Outbound: StatsBound{
|
|
|
|
|
- Uplink: 1,
|
|
|
|
|
- Downlink: 1,
|
|
|
|
|
- }}
|
|
|
|
|
-
|
|
|
|
|
- JSONResponse(w, response, 200)
|
|
|
|
|
|
|
+func (rs *restfulService) version(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
+ render.JSON(w, r, render.M{"version": core.Version()})
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
func (rs *restfulService) TokenAuthMiddleware(next http.Handler) http.Handler {
|
|
func (rs *restfulService) TokenAuthMiddleware(next http.Handler) http.Handler {
|
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
- auth := r.Header.Get("Authorization")
|
|
|
|
|
- const prefix = "Bearer "
|
|
|
|
|
- if !strings.HasPrefix(auth, prefix) {
|
|
|
|
|
- JSONResponse(w, http.StatusText(403), 403)
|
|
|
|
|
- return
|
|
|
|
|
- }
|
|
|
|
|
- auth = strings.TrimPrefix(auth, prefix)
|
|
|
|
|
- if auth != rs.config.AuthToken {
|
|
|
|
|
- JSONResponse(w, http.StatusText(403), 403)
|
|
|
|
|
|
|
+ header := r.Header.Get("Authorization")
|
|
|
|
|
+ text := strings.SplitN(header, " ", 2)
|
|
|
|
|
+
|
|
|
|
|
+ hasInvalidHeader := text[0] != "Bearer"
|
|
|
|
|
+ hasInvalidSecret := len(text) != 2 || text[1] != rs.config.AuthToken
|
|
|
|
|
+ if hasInvalidHeader || hasInvalidSecret {
|
|
|
|
|
+ render.Status(r, http.StatusUnauthorized)
|
|
|
|
|
+ render.JSON(w, r, render.M{})
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
next.ServeHTTP(w, r)
|
|
next.ServeHTTP(w, r)
|
|
|
})
|
|
})
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
func (rs *restfulService) start() error {
|
|
func (rs *restfulService) start() error {
|
|
|
r := chi.NewRouter()
|
|
r := chi.NewRouter()
|
|
|
- r.Use(rs.TokenAuthMiddleware)
|
|
|
|
|
r.Use(middleware.Heartbeat("/ping"))
|
|
r.Use(middleware.Heartbeat("/ping"))
|
|
|
|
|
|
|
|
|
|
+ validate = validator.New()
|
|
|
r.Route("/v1", func(r chi.Router) {
|
|
r.Route("/v1", func(r chi.Router) {
|
|
|
- r.Get("/stats/user", rs.statsUser)
|
|
|
|
|
- r.Get("/stats", rs.statsRequest)
|
|
|
|
|
|
|
+ r.Get("/{bound_type}/{tag}/stats", rs.tagStats)
|
|
|
})
|
|
})
|
|
|
|
|
+ r.Get("/version", rs.version)
|
|
|
|
|
|
|
|
var listener net.Listener
|
|
var listener net.Listener
|
|
|
var err error
|
|
var err error
|
|
@@ -134,6 +99,5 @@ func (rs *restfulService) start() error {
|
|
|
newError("unable to serve restful api").WriteToLog()
|
|
newError("unable to serve restful api").WriteToLog()
|
|
|
}
|
|
}
|
|
|
}()
|
|
}()
|
|
|
-
|
|
|
|
|
return nil
|
|
return nil
|
|
|
}
|
|
}
|