mirror of
https://github.com/shtorm-7/sing-box-extended.git
synced 2026-05-25 21:51:47 +03:00
Add new admin panel, failover, dns fallback, providers, limiters. Update XHTTP
This commit is contained in:
109
service/manager_api/grpc/client/client.go
Normal file
109
service/manager_api/grpc/client/client.go
Normal file
@@ -0,0 +1,109 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"sync"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
boxService "github.com/sagernet/sing-box/adapter/service"
|
||||
"github.com/sagernet/sing-box/common/dialer"
|
||||
"github.com/sagernet/sing-box/common/tls"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
pb "github.com/sagernet/sing-box/service/manager_api/grpc/manager"
|
||||
"github.com/sagernet/sing/common"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/connectivity"
|
||||
"google.golang.org/grpc/credentials"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
"google.golang.org/grpc/metadata"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
boxService.Adapter
|
||||
|
||||
ctx context.Context
|
||||
logger log.ContextLogger
|
||||
dialer N.Dialer
|
||||
creds credentials.TransportCredentials
|
||||
options option.ManagerAPIClientOptions
|
||||
|
||||
conn *grpc.ClientConn
|
||||
mtx sync.Mutex
|
||||
}
|
||||
|
||||
func NewClient(ctx context.Context, logger log.ContextLogger, tag string, options option.ManagerAPIClientOptions) (*Client, error) {
|
||||
outboundDialer, err := dialer.New(ctx, options.DialerOptions, options.ServerIsDomain())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
creds := insecure.NewCredentials()
|
||||
if options.TLS != nil {
|
||||
tlsConfig, err := tls.NewClient(ctx, logger, options.Server, common.PtrValueOrDefault(options.TLS))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
creds = &tlsCreds{config: tlsConfig}
|
||||
}
|
||||
return &Client{
|
||||
Adapter: boxService.NewAdapter(C.TypeManagerAPI, tag),
|
||||
ctx: ctx,
|
||||
logger: logger,
|
||||
dialer: outboundDialer,
|
||||
creds: creds,
|
||||
options: options,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Client) Start(stage adapter.StartStage) error { return nil }
|
||||
func (s *Client) Close() error {
|
||||
s.mtx.Lock()
|
||||
defer s.mtx.Unlock()
|
||||
if s.conn != nil {
|
||||
return s.conn.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Client) client() (pb.ManagerClient, error) {
|
||||
conn, err := s.getConn()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return pb.NewManagerClient(conn), nil
|
||||
}
|
||||
|
||||
func (s *Client) getConn() (*grpc.ClientConn, error) {
|
||||
s.mtx.Lock()
|
||||
defer s.mtx.Unlock()
|
||||
if s.conn != nil {
|
||||
state := s.conn.GetState()
|
||||
if state != connectivity.Shutdown && state != connectivity.TransientFailure {
|
||||
return s.conn, nil
|
||||
}
|
||||
}
|
||||
conn, err := grpc.NewClient(
|
||||
s.options.ServerOptions.Build().String(),
|
||||
grpc.WithContextDialer(func(ctx context.Context, addr string) (net.Conn, error) {
|
||||
return s.dialer.DialContext(ctx, N.NetworkTCP, M.ParseSocksaddr(addr))
|
||||
}),
|
||||
grpc.WithTransportCredentials(s.creds),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s.conn = conn
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
func (s *Client) callContext() context.Context {
|
||||
if s.options.APIKey == "" {
|
||||
return s.ctx
|
||||
}
|
||||
return metadata.AppendToOutgoingContext(s.ctx, "authorization", s.options.APIKey)
|
||||
}
|
||||
146
service/manager_api/grpc/client/converter.go
Normal file
146
service/manager_api/grpc/client/converter.go
Normal file
@@ -0,0 +1,146 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
CM "github.com/sagernet/sing-box/service/manager/constant"
|
||||
pb "github.com/sagernet/sing-box/service/manager_api/grpc/manager"
|
||||
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
func mapError(err error) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
if st, ok := status.FromError(err); ok && st.Code() == codes.NotFound {
|
||||
return CM.ErrNotFound
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func toIntSlice(values []int32) []int {
|
||||
out := make([]int, len(values))
|
||||
for i, v := range values {
|
||||
out[i] = int(v)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func toInt32Slice(values []int) []int32 {
|
||||
out := make([]int32, len(values))
|
||||
for i, v := range values {
|
||||
out[i] = int32(v)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func timeFromNano(ns int64) time.Time { return time.Unix(0, ns) }
|
||||
|
||||
func convertSquad(v *pb.Squad) CM.Squad {
|
||||
return CM.Squad{
|
||||
ID: int(v.GetId()),
|
||||
Name: v.GetName(),
|
||||
CreatedAt: timeFromNano(v.GetCreatedAt()),
|
||||
UpdatedAt: timeFromNano(v.GetUpdatedAt()),
|
||||
}
|
||||
}
|
||||
|
||||
func convertNode(v *pb.Node) CM.Node {
|
||||
return CM.Node{
|
||||
UUID: v.GetUuid(),
|
||||
Name: v.GetName(),
|
||||
SquadIDs: toIntSlice(v.GetSquadIds()),
|
||||
CreatedAt: timeFromNano(v.GetCreatedAt()),
|
||||
UpdatedAt: timeFromNano(v.GetUpdatedAt()),
|
||||
}
|
||||
}
|
||||
|
||||
func convertUser(v *pb.User) CM.User {
|
||||
return CM.User{
|
||||
ID: int(v.GetId()),
|
||||
SquadIDs: toIntSlice(v.GetSquadIds()),
|
||||
Username: v.GetUsername(),
|
||||
Inbound: v.GetInbound(),
|
||||
Type: v.GetType(),
|
||||
UUID: v.GetUuid(),
|
||||
Password: v.GetPassword(),
|
||||
Secret: v.GetSecret(),
|
||||
Flow: v.GetFlow(),
|
||||
AlterID: int(v.GetAlterId()),
|
||||
CreatedAt: timeFromNano(v.GetCreatedAt()),
|
||||
UpdatedAt: timeFromNano(v.GetUpdatedAt()),
|
||||
}
|
||||
}
|
||||
|
||||
func convertBandwidthLimiter(v *pb.BandwidthLimiter) CM.BandwidthLimiter {
|
||||
return CM.BandwidthLimiter{
|
||||
ID: int(v.GetId()),
|
||||
SquadIDs: toIntSlice(v.GetSquadIds()),
|
||||
Username: v.GetUsername(),
|
||||
Outbound: v.GetOutbound(),
|
||||
Strategy: v.GetStrategy(),
|
||||
ConnectionType: v.GetConnectionType(),
|
||||
Mode: v.GetMode(),
|
||||
FlowKeys: v.GetFlowKeys(),
|
||||
Speed: v.GetSpeed(),
|
||||
RawSpeed: v.GetRawSpeed(),
|
||||
CreatedAt: timeFromNano(v.GetCreatedAt()),
|
||||
UpdatedAt: timeFromNano(v.GetUpdatedAt()),
|
||||
}
|
||||
}
|
||||
|
||||
func convertTrafficLimiter(v *pb.TrafficLimiter) CM.TrafficLimiter {
|
||||
return CM.TrafficLimiter{
|
||||
ID: int(v.GetId()),
|
||||
SquadIDs: toIntSlice(v.GetSquadIds()),
|
||||
Username: v.GetUsername(),
|
||||
Outbound: v.GetOutbound(),
|
||||
Strategy: v.GetStrategy(),
|
||||
Mode: v.GetMode(),
|
||||
RawUsed: v.GetRawUsed(),
|
||||
Quota: v.GetQuota(),
|
||||
RawQuota: v.GetRawQuota(),
|
||||
CreatedAt: timeFromNano(v.GetCreatedAt()),
|
||||
UpdatedAt: timeFromNano(v.GetUpdatedAt()),
|
||||
}
|
||||
}
|
||||
|
||||
func convertConnectionLimiter(v *pb.ConnectionLimiter) CM.ConnectionLimiter {
|
||||
return CM.ConnectionLimiter{
|
||||
ID: int(v.GetId()),
|
||||
SquadIDs: toIntSlice(v.GetSquadIds()),
|
||||
Username: v.GetUsername(),
|
||||
Outbound: v.GetOutbound(),
|
||||
Strategy: v.GetStrategy(),
|
||||
ConnectionType: v.GetConnectionType(),
|
||||
LockType: v.GetLockType(),
|
||||
Count: v.GetCount(),
|
||||
CreatedAt: timeFromNano(v.GetCreatedAt()),
|
||||
UpdatedAt: timeFromNano(v.GetUpdatedAt()),
|
||||
}
|
||||
}
|
||||
|
||||
func convertRateLimiter(v *pb.RateLimiter) CM.RateLimiter {
|
||||
return CM.RateLimiter{
|
||||
ID: int(v.GetId()),
|
||||
SquadIDs: toIntSlice(v.GetSquadIds()),
|
||||
Username: v.GetUsername(),
|
||||
Outbound: v.GetOutbound(),
|
||||
Strategy: v.GetStrategy(),
|
||||
ConnectionType: v.GetConnectionType(),
|
||||
Count: v.GetCount(),
|
||||
Interval: v.GetInterval(),
|
||||
CreatedAt: timeFromNano(v.GetCreatedAt()),
|
||||
UpdatedAt: timeFromNano(v.GetUpdatedAt()),
|
||||
}
|
||||
}
|
||||
|
||||
func convertFilters(filters map[string][]string) *pb.Filters {
|
||||
values := make(map[string]*pb.StringList, len(filters))
|
||||
for k, v := range filters {
|
||||
values[k] = &pb.StringList{Values: append([]string(nil), v...)}
|
||||
}
|
||||
return &pb.Filters{Values: values}
|
||||
}
|
||||
658
service/manager_api/grpc/client/manager.go
Normal file
658
service/manager_api/grpc/client/manager.go
Normal file
@@ -0,0 +1,658 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
CM "github.com/sagernet/sing-box/service/manager/constant"
|
||||
pb "github.com/sagernet/sing-box/service/manager_api/grpc/manager"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
)
|
||||
|
||||
var _ CM.Manager = (*Client)(nil)
|
||||
|
||||
func (s *Client) CreateSquad(in CM.SquadCreate) (CM.Squad, error) {
|
||||
c, err := s.client()
|
||||
if err != nil {
|
||||
return CM.Squad{}, err
|
||||
}
|
||||
reply, err := c.CreateSquad(s.callContext(), &pb.SquadCreate{Name: in.Name})
|
||||
if err != nil {
|
||||
return CM.Squad{}, mapError(err)
|
||||
}
|
||||
return convertSquad(reply), nil
|
||||
}
|
||||
|
||||
func (s *Client) GetSquads(filters map[string][]string) ([]CM.Squad, error) {
|
||||
c, err := s.client()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
reply, err := c.GetSquads(s.callContext(), convertFilters(filters))
|
||||
if err != nil {
|
||||
return nil, mapError(err)
|
||||
}
|
||||
out := make([]CM.Squad, len(reply.GetValues()))
|
||||
for i, v := range reply.GetValues() {
|
||||
out[i] = convertSquad(v)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s *Client) GetSquadsCount(filters map[string][]string) (int, error) {
|
||||
c, err := s.client()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
reply, err := c.GetSquadsCount(s.callContext(), convertFilters(filters))
|
||||
if err != nil {
|
||||
return 0, mapError(err)
|
||||
}
|
||||
return int(reply.GetCount()), nil
|
||||
}
|
||||
|
||||
func (s *Client) GetSquad(id int) (CM.Squad, error) {
|
||||
c, err := s.client()
|
||||
if err != nil {
|
||||
return CM.Squad{}, err
|
||||
}
|
||||
reply, err := c.GetSquad(s.callContext(), &pb.IdRequest{Id: int32(id)})
|
||||
if err != nil {
|
||||
return CM.Squad{}, mapError(err)
|
||||
}
|
||||
return convertSquad(reply), nil
|
||||
}
|
||||
|
||||
func (s *Client) UpdateSquad(id int, in CM.SquadUpdate) (CM.Squad, error) {
|
||||
c, err := s.client()
|
||||
if err != nil {
|
||||
return CM.Squad{}, err
|
||||
}
|
||||
reply, err := c.UpdateSquad(s.callContext(), &pb.SquadUpdateRequest{
|
||||
Id: int32(id),
|
||||
Update: &pb.SquadUpdate{Name: in.Name},
|
||||
})
|
||||
if err != nil {
|
||||
return CM.Squad{}, mapError(err)
|
||||
}
|
||||
return convertSquad(reply), nil
|
||||
}
|
||||
|
||||
func (s *Client) DeleteSquad(id int) (CM.Squad, error) {
|
||||
c, err := s.client()
|
||||
if err != nil {
|
||||
return CM.Squad{}, err
|
||||
}
|
||||
reply, err := c.DeleteSquad(s.callContext(), &pb.IdRequest{Id: int32(id)})
|
||||
if err != nil {
|
||||
return CM.Squad{}, mapError(err)
|
||||
}
|
||||
return convertSquad(reply), nil
|
||||
}
|
||||
|
||||
func (s *Client) CreateNode(in CM.NodeCreate) (CM.Node, error) {
|
||||
c, err := s.client()
|
||||
if err != nil {
|
||||
return CM.Node{}, err
|
||||
}
|
||||
reply, err := c.CreateNode(s.callContext(), &pb.NodeCreate{
|
||||
Uuid: in.UUID,
|
||||
Name: in.Name,
|
||||
SquadIds: toInt32Slice(in.SquadIDs),
|
||||
})
|
||||
if err != nil {
|
||||
return CM.Node{}, mapError(err)
|
||||
}
|
||||
return convertNode(reply), nil
|
||||
}
|
||||
|
||||
func (s *Client) GetNodes(filters map[string][]string) ([]CM.Node, error) {
|
||||
c, err := s.client()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
reply, err := c.GetNodes(s.callContext(), convertFilters(filters))
|
||||
if err != nil {
|
||||
return nil, mapError(err)
|
||||
}
|
||||
out := make([]CM.Node, len(reply.GetValues()))
|
||||
for i, v := range reply.GetValues() {
|
||||
out[i] = convertNode(v)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s *Client) GetNodesCount(filters map[string][]string) (int, error) {
|
||||
c, err := s.client()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
reply, err := c.GetNodesCount(s.callContext(), convertFilters(filters))
|
||||
if err != nil {
|
||||
return 0, mapError(err)
|
||||
}
|
||||
return int(reply.GetCount()), nil
|
||||
}
|
||||
|
||||
func (s *Client) GetNode(uuid string) (CM.Node, error) {
|
||||
c, err := s.client()
|
||||
if err != nil {
|
||||
return CM.Node{}, err
|
||||
}
|
||||
reply, err := c.GetNode(s.callContext(), &pb.UuidRequest{Uuid: uuid})
|
||||
if err != nil {
|
||||
return CM.Node{}, mapError(err)
|
||||
}
|
||||
return convertNode(reply), nil
|
||||
}
|
||||
|
||||
func (s *Client) GetNodeStatus(uuid string) (string, error) {
|
||||
c, err := s.client()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
reply, err := c.GetNodeStatus(s.callContext(), &pb.UuidRequest{Uuid: uuid})
|
||||
if err != nil {
|
||||
return "", mapError(err)
|
||||
}
|
||||
return reply.GetStatus(), nil
|
||||
}
|
||||
|
||||
func (s *Client) UpdateNode(uuid string, in CM.NodeUpdate) (CM.Node, error) {
|
||||
c, err := s.client()
|
||||
if err != nil {
|
||||
return CM.Node{}, err
|
||||
}
|
||||
reply, err := c.UpdateNode(s.callContext(), &pb.NodeUpdateRequest{
|
||||
Uuid: uuid,
|
||||
Update: &pb.NodeUpdate{Name: in.Name},
|
||||
})
|
||||
if err != nil {
|
||||
return CM.Node{}, mapError(err)
|
||||
}
|
||||
return convertNode(reply), nil
|
||||
}
|
||||
|
||||
func (s *Client) DeleteNode(uuid string) (CM.Node, error) {
|
||||
c, err := s.client()
|
||||
if err != nil {
|
||||
return CM.Node{}, err
|
||||
}
|
||||
reply, err := c.DeleteNode(s.callContext(), &pb.UuidRequest{Uuid: uuid})
|
||||
if err != nil {
|
||||
return CM.Node{}, mapError(err)
|
||||
}
|
||||
return convertNode(reply), nil
|
||||
}
|
||||
|
||||
func (s *Client) CreateUser(in CM.UserCreate) (CM.User, error) {
|
||||
c, err := s.client()
|
||||
if err != nil {
|
||||
return CM.User{}, err
|
||||
}
|
||||
reply, err := c.CreateUser(s.callContext(), &pb.UserCreate{
|
||||
SquadIds: toInt32Slice(in.SquadIDs),
|
||||
Username: in.Username,
|
||||
Inbound: in.Inbound,
|
||||
Type: in.Type,
|
||||
Uuid: in.UUID,
|
||||
Password: in.Password,
|
||||
Secret: in.Secret,
|
||||
Flow: in.Flow,
|
||||
AlterId: int32(in.AlterID),
|
||||
})
|
||||
if err != nil {
|
||||
return CM.User{}, mapError(err)
|
||||
}
|
||||
return convertUser(reply), nil
|
||||
}
|
||||
|
||||
func (s *Client) GetUsers(filters map[string][]string) ([]CM.User, error) {
|
||||
c, err := s.client()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
reply, err := c.GetUsers(s.callContext(), convertFilters(filters))
|
||||
if err != nil {
|
||||
return nil, mapError(err)
|
||||
}
|
||||
out := make([]CM.User, len(reply.GetValues()))
|
||||
for i, v := range reply.GetValues() {
|
||||
out[i] = convertUser(v)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s *Client) GetUsersCount(filters map[string][]string) (int, error) {
|
||||
c, err := s.client()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
reply, err := c.GetUsersCount(s.callContext(), convertFilters(filters))
|
||||
if err != nil {
|
||||
return 0, mapError(err)
|
||||
}
|
||||
return int(reply.GetCount()), nil
|
||||
}
|
||||
|
||||
func (s *Client) GetUser(id int) (CM.User, error) {
|
||||
c, err := s.client()
|
||||
if err != nil {
|
||||
return CM.User{}, err
|
||||
}
|
||||
reply, err := c.GetUser(s.callContext(), &pb.IdRequest{Id: int32(id)})
|
||||
if err != nil {
|
||||
return CM.User{}, mapError(err)
|
||||
}
|
||||
return convertUser(reply), nil
|
||||
}
|
||||
|
||||
func (s *Client) UpdateUser(id int, in CM.UserUpdate) (CM.User, error) {
|
||||
c, err := s.client()
|
||||
if err != nil {
|
||||
return CM.User{}, err
|
||||
}
|
||||
reply, err := c.UpdateUser(s.callContext(), &pb.UserUpdateRequest{
|
||||
Id: int32(id),
|
||||
Update: &pb.UserUpdate{
|
||||
Uuid: in.UUID,
|
||||
Password: in.Password,
|
||||
Secret: in.Secret,
|
||||
Flow: in.Flow,
|
||||
AlterId: int32(in.AlterID),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return CM.User{}, mapError(err)
|
||||
}
|
||||
return convertUser(reply), nil
|
||||
}
|
||||
|
||||
func (s *Client) DeleteUser(id int) (CM.User, error) {
|
||||
c, err := s.client()
|
||||
if err != nil {
|
||||
return CM.User{}, err
|
||||
}
|
||||
reply, err := c.DeleteUser(s.callContext(), &pb.IdRequest{Id: int32(id)})
|
||||
if err != nil {
|
||||
return CM.User{}, mapError(err)
|
||||
}
|
||||
return convertUser(reply), nil
|
||||
}
|
||||
|
||||
func (s *Client) CreateBandwidthLimiter(in CM.BandwidthLimiterCreate) (CM.BandwidthLimiter, error) {
|
||||
c, err := s.client()
|
||||
if err != nil {
|
||||
return CM.BandwidthLimiter{}, err
|
||||
}
|
||||
reply, err := c.CreateBandwidthLimiter(s.callContext(), &pb.BandwidthLimiterCreate{
|
||||
SquadIds: toInt32Slice(in.SquadIDs),
|
||||
Username: in.Username,
|
||||
Outbound: in.Outbound,
|
||||
Strategy: in.Strategy,
|
||||
ConnectionType: in.ConnectionType,
|
||||
Mode: in.Mode,
|
||||
FlowKeys: in.FlowKeys,
|
||||
Speed: in.Speed,
|
||||
})
|
||||
if err != nil {
|
||||
return CM.BandwidthLimiter{}, mapError(err)
|
||||
}
|
||||
return convertBandwidthLimiter(reply), nil
|
||||
}
|
||||
|
||||
func (s *Client) GetBandwidthLimiters(filters map[string][]string) ([]CM.BandwidthLimiter, error) {
|
||||
c, err := s.client()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
reply, err := c.GetBandwidthLimiters(s.callContext(), convertFilters(filters))
|
||||
if err != nil {
|
||||
return nil, mapError(err)
|
||||
}
|
||||
out := make([]CM.BandwidthLimiter, len(reply.GetValues()))
|
||||
for i, v := range reply.GetValues() {
|
||||
out[i] = convertBandwidthLimiter(v)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s *Client) GetBandwidthLimitersCount(filters map[string][]string) (int, error) {
|
||||
c, err := s.client()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
reply, err := c.GetBandwidthLimitersCount(s.callContext(), convertFilters(filters))
|
||||
if err != nil {
|
||||
return 0, mapError(err)
|
||||
}
|
||||
return int(reply.GetCount()), nil
|
||||
}
|
||||
|
||||
func (s *Client) GetBandwidthLimiter(id int) (CM.BandwidthLimiter, error) {
|
||||
c, err := s.client()
|
||||
if err != nil {
|
||||
return CM.BandwidthLimiter{}, err
|
||||
}
|
||||
reply, err := c.GetBandwidthLimiter(s.callContext(), &pb.IdRequest{Id: int32(id)})
|
||||
if err != nil {
|
||||
return CM.BandwidthLimiter{}, mapError(err)
|
||||
}
|
||||
return convertBandwidthLimiter(reply), nil
|
||||
}
|
||||
|
||||
func (s *Client) UpdateBandwidthLimiter(id int, in CM.BandwidthLimiterUpdate) (CM.BandwidthLimiter, error) {
|
||||
c, err := s.client()
|
||||
if err != nil {
|
||||
return CM.BandwidthLimiter{}, err
|
||||
}
|
||||
reply, err := c.UpdateBandwidthLimiter(s.callContext(), &pb.BandwidthLimiterUpdateRequest{
|
||||
Id: int32(id),
|
||||
Update: &pb.BandwidthLimiterUpdate{
|
||||
Username: in.Username,
|
||||
Outbound: in.Outbound,
|
||||
Strategy: in.Strategy,
|
||||
ConnectionType: in.ConnectionType,
|
||||
Mode: in.Mode,
|
||||
FlowKeys: in.FlowKeys,
|
||||
Speed: in.Speed,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return CM.BandwidthLimiter{}, mapError(err)
|
||||
}
|
||||
return convertBandwidthLimiter(reply), nil
|
||||
}
|
||||
|
||||
func (s *Client) DeleteBandwidthLimiter(id int) (CM.BandwidthLimiter, error) {
|
||||
c, err := s.client()
|
||||
if err != nil {
|
||||
return CM.BandwidthLimiter{}, err
|
||||
}
|
||||
reply, err := c.DeleteBandwidthLimiter(s.callContext(), &pb.IdRequest{Id: int32(id)})
|
||||
if err != nil {
|
||||
return CM.BandwidthLimiter{}, mapError(err)
|
||||
}
|
||||
return convertBandwidthLimiter(reply), nil
|
||||
}
|
||||
|
||||
func (s *Client) CreateTrafficLimiter(in CM.TrafficLimiterCreate) (CM.TrafficLimiter, error) {
|
||||
c, err := s.client()
|
||||
if err != nil {
|
||||
return CM.TrafficLimiter{}, err
|
||||
}
|
||||
reply, err := c.CreateTrafficLimiter(s.callContext(), &pb.TrafficLimiterCreate{
|
||||
SquadIds: toInt32Slice(in.SquadIDs),
|
||||
Username: in.Username,
|
||||
Outbound: in.Outbound,
|
||||
Strategy: in.Strategy,
|
||||
Mode: in.Mode,
|
||||
Quota: in.Quota,
|
||||
})
|
||||
if err != nil {
|
||||
return CM.TrafficLimiter{}, mapError(err)
|
||||
}
|
||||
return convertTrafficLimiter(reply), nil
|
||||
}
|
||||
|
||||
func (s *Client) GetTrafficLimiters(filters map[string][]string) ([]CM.TrafficLimiter, error) {
|
||||
c, err := s.client()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
reply, err := c.GetTrafficLimiters(s.callContext(), convertFilters(filters))
|
||||
if err != nil {
|
||||
return nil, mapError(err)
|
||||
}
|
||||
out := make([]CM.TrafficLimiter, len(reply.GetValues()))
|
||||
for i, v := range reply.GetValues() {
|
||||
out[i] = convertTrafficLimiter(v)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s *Client) GetTrafficLimitersCount(filters map[string][]string) (int, error) {
|
||||
c, err := s.client()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
reply, err := c.GetTrafficLimitersCount(s.callContext(), convertFilters(filters))
|
||||
if err != nil {
|
||||
return 0, mapError(err)
|
||||
}
|
||||
return int(reply.GetCount()), nil
|
||||
}
|
||||
|
||||
func (s *Client) GetTrafficLimiter(id int) (CM.TrafficLimiter, error) {
|
||||
c, err := s.client()
|
||||
if err != nil {
|
||||
return CM.TrafficLimiter{}, err
|
||||
}
|
||||
reply, err := c.GetTrafficLimiter(s.callContext(), &pb.IdRequest{Id: int32(id)})
|
||||
if err != nil {
|
||||
return CM.TrafficLimiter{}, mapError(err)
|
||||
}
|
||||
return convertTrafficLimiter(reply), nil
|
||||
}
|
||||
|
||||
func (s *Client) UpdateTrafficLimiter(id int, in CM.TrafficLimiterUpdate) (CM.TrafficLimiter, error) {
|
||||
c, err := s.client()
|
||||
if err != nil {
|
||||
return CM.TrafficLimiter{}, err
|
||||
}
|
||||
reply, err := c.UpdateTrafficLimiter(s.callContext(), &pb.TrafficLimiterUpdateRequest{
|
||||
Id: int32(id),
|
||||
Update: &pb.TrafficLimiterUpdate{
|
||||
Username: in.Username,
|
||||
Outbound: in.Outbound,
|
||||
Strategy: in.Strategy,
|
||||
Mode: in.Mode,
|
||||
Quota: in.Quota,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return CM.TrafficLimiter{}, mapError(err)
|
||||
}
|
||||
return convertTrafficLimiter(reply), nil
|
||||
}
|
||||
|
||||
func (s *Client) UpdateTrafficLimiterUsed(_ int, _ uint64) (CM.TrafficLimiter, error) {
|
||||
return CM.TrafficLimiter{}, E.New("UpdateTrafficLimiterUsed not implemented over gRPC")
|
||||
}
|
||||
|
||||
func (s *Client) DeleteTrafficLimiter(id int) (CM.TrafficLimiter, error) {
|
||||
c, err := s.client()
|
||||
if err != nil {
|
||||
return CM.TrafficLimiter{}, err
|
||||
}
|
||||
reply, err := c.DeleteTrafficLimiter(s.callContext(), &pb.IdRequest{Id: int32(id)})
|
||||
if err != nil {
|
||||
return CM.TrafficLimiter{}, mapError(err)
|
||||
}
|
||||
return convertTrafficLimiter(reply), nil
|
||||
}
|
||||
|
||||
func (s *Client) CreateConnectionLimiter(in CM.ConnectionLimiterCreate) (CM.ConnectionLimiter, error) {
|
||||
c, err := s.client()
|
||||
if err != nil {
|
||||
return CM.ConnectionLimiter{}, err
|
||||
}
|
||||
reply, err := c.CreateConnectionLimiter(s.callContext(), &pb.ConnectionLimiterCreate{
|
||||
SquadIds: toInt32Slice(in.SquadIDs),
|
||||
Username: in.Username,
|
||||
Outbound: in.Outbound,
|
||||
Strategy: in.Strategy,
|
||||
ConnectionType: in.ConnectionType,
|
||||
LockType: in.LockType,
|
||||
Count: in.Count,
|
||||
})
|
||||
if err != nil {
|
||||
return CM.ConnectionLimiter{}, mapError(err)
|
||||
}
|
||||
return convertConnectionLimiter(reply), nil
|
||||
}
|
||||
|
||||
func (s *Client) GetConnectionLimiters(filters map[string][]string) ([]CM.ConnectionLimiter, error) {
|
||||
c, err := s.client()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
reply, err := c.GetConnectionLimiters(s.callContext(), convertFilters(filters))
|
||||
if err != nil {
|
||||
return nil, mapError(err)
|
||||
}
|
||||
out := make([]CM.ConnectionLimiter, len(reply.GetValues()))
|
||||
for i, v := range reply.GetValues() {
|
||||
out[i] = convertConnectionLimiter(v)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s *Client) GetConnectionLimitersCount(filters map[string][]string) (int, error) {
|
||||
c, err := s.client()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
reply, err := c.GetConnectionLimitersCount(s.callContext(), convertFilters(filters))
|
||||
if err != nil {
|
||||
return 0, mapError(err)
|
||||
}
|
||||
return int(reply.GetCount()), nil
|
||||
}
|
||||
|
||||
func (s *Client) GetConnectionLimiter(id int) (CM.ConnectionLimiter, error) {
|
||||
c, err := s.client()
|
||||
if err != nil {
|
||||
return CM.ConnectionLimiter{}, err
|
||||
}
|
||||
reply, err := c.GetConnectionLimiter(s.callContext(), &pb.IdRequest{Id: int32(id)})
|
||||
if err != nil {
|
||||
return CM.ConnectionLimiter{}, mapError(err)
|
||||
}
|
||||
return convertConnectionLimiter(reply), nil
|
||||
}
|
||||
|
||||
func (s *Client) UpdateConnectionLimiter(id int, in CM.ConnectionLimiterUpdate) (CM.ConnectionLimiter, error) {
|
||||
c, err := s.client()
|
||||
if err != nil {
|
||||
return CM.ConnectionLimiter{}, err
|
||||
}
|
||||
reply, err := c.UpdateConnectionLimiter(s.callContext(), &pb.ConnectionLimiterUpdateRequest{
|
||||
Id: int32(id),
|
||||
Update: &pb.ConnectionLimiterUpdate{
|
||||
Username: in.Username,
|
||||
Outbound: in.Outbound,
|
||||
Strategy: in.Strategy,
|
||||
ConnectionType: in.ConnectionType,
|
||||
LockType: in.LockType,
|
||||
Count: in.Count,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return CM.ConnectionLimiter{}, mapError(err)
|
||||
}
|
||||
return convertConnectionLimiter(reply), nil
|
||||
}
|
||||
|
||||
func (s *Client) DeleteConnectionLimiter(id int) (CM.ConnectionLimiter, error) {
|
||||
c, err := s.client()
|
||||
if err != nil {
|
||||
return CM.ConnectionLimiter{}, err
|
||||
}
|
||||
reply, err := c.DeleteConnectionLimiter(s.callContext(), &pb.IdRequest{Id: int32(id)})
|
||||
if err != nil {
|
||||
return CM.ConnectionLimiter{}, mapError(err)
|
||||
}
|
||||
return convertConnectionLimiter(reply), nil
|
||||
}
|
||||
|
||||
func (s *Client) CreateRateLimiter(in CM.RateLimiterCreate) (CM.RateLimiter, error) {
|
||||
c, err := s.client()
|
||||
if err != nil {
|
||||
return CM.RateLimiter{}, err
|
||||
}
|
||||
reply, err := c.CreateRateLimiter(s.callContext(), &pb.RateLimiterCreate{
|
||||
SquadIds: toInt32Slice(in.SquadIDs),
|
||||
Username: in.Username,
|
||||
Outbound: in.Outbound,
|
||||
Strategy: in.Strategy,
|
||||
ConnectionType: in.ConnectionType,
|
||||
Count: in.Count,
|
||||
Interval: in.Interval,
|
||||
})
|
||||
if err != nil {
|
||||
return CM.RateLimiter{}, mapError(err)
|
||||
}
|
||||
return convertRateLimiter(reply), nil
|
||||
}
|
||||
|
||||
func (s *Client) GetRateLimiters(filters map[string][]string) ([]CM.RateLimiter, error) {
|
||||
c, err := s.client()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
reply, err := c.GetRateLimiters(s.callContext(), convertFilters(filters))
|
||||
if err != nil {
|
||||
return nil, mapError(err)
|
||||
}
|
||||
out := make([]CM.RateLimiter, len(reply.GetValues()))
|
||||
for i, v := range reply.GetValues() {
|
||||
out[i] = convertRateLimiter(v)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s *Client) GetRateLimitersCount(filters map[string][]string) (int, error) {
|
||||
c, err := s.client()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
reply, err := c.GetRateLimitersCount(s.callContext(), convertFilters(filters))
|
||||
if err != nil {
|
||||
return 0, mapError(err)
|
||||
}
|
||||
return int(reply.GetCount()), nil
|
||||
}
|
||||
|
||||
func (s *Client) GetRateLimiter(id int) (CM.RateLimiter, error) {
|
||||
c, err := s.client()
|
||||
if err != nil {
|
||||
return CM.RateLimiter{}, err
|
||||
}
|
||||
reply, err := c.GetRateLimiter(s.callContext(), &pb.IdRequest{Id: int32(id)})
|
||||
if err != nil {
|
||||
return CM.RateLimiter{}, mapError(err)
|
||||
}
|
||||
return convertRateLimiter(reply), nil
|
||||
}
|
||||
|
||||
func (s *Client) UpdateRateLimiter(id int, in CM.RateLimiterUpdate) (CM.RateLimiter, error) {
|
||||
c, err := s.client()
|
||||
if err != nil {
|
||||
return CM.RateLimiter{}, err
|
||||
}
|
||||
reply, err := c.UpdateRateLimiter(s.callContext(), &pb.RateLimiterUpdateRequest{
|
||||
Id: int32(id),
|
||||
Update: &pb.RateLimiterUpdate{
|
||||
Username: in.Username,
|
||||
Outbound: in.Outbound,
|
||||
Strategy: in.Strategy,
|
||||
ConnectionType: in.ConnectionType,
|
||||
Count: in.Count,
|
||||
Interval: in.Interval,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return CM.RateLimiter{}, mapError(err)
|
||||
}
|
||||
return convertRateLimiter(reply), nil
|
||||
}
|
||||
|
||||
func (s *Client) DeleteRateLimiter(id int) (CM.RateLimiter, error) {
|
||||
c, err := s.client()
|
||||
if err != nil {
|
||||
return CM.RateLimiter{}, err
|
||||
}
|
||||
reply, err := c.DeleteRateLimiter(s.callContext(), &pb.IdRequest{Id: int32(id)})
|
||||
if err != nil {
|
||||
return CM.RateLimiter{}, mapError(err)
|
||||
}
|
||||
return convertRateLimiter(reply), nil
|
||||
}
|
||||
43
service/manager_api/grpc/client/tls.go
Normal file
43
service/manager_api/grpc/client/tls.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
|
||||
"github.com/sagernet/sing-box/common/tls"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"google.golang.org/grpc/credentials"
|
||||
)
|
||||
|
||||
type tlsCreds struct {
|
||||
config tls.Config
|
||||
}
|
||||
|
||||
func (c tlsCreds) Info() credentials.ProtocolInfo {
|
||||
return credentials.ProtocolInfo{
|
||||
SecurityProtocol: "tls",
|
||||
SecurityVersion: "1.2",
|
||||
ServerName: c.config.ServerName(),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *tlsCreds) ClientHandshake(ctx context.Context, authority string, rawConn net.Conn) (net.Conn, credentials.AuthInfo, error) {
|
||||
conn, err := tls.ClientHandshake(ctx, rawConn, c.config)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return conn, credentials.TLSInfo{State: conn.ConnectionState()}, err
|
||||
}
|
||||
|
||||
func (c *tlsCreds) ServerHandshake(rawConn net.Conn) (net.Conn, credentials.AuthInfo, error) {
|
||||
return nil, nil, E.New("not implemented")
|
||||
}
|
||||
|
||||
func (c *tlsCreds) Clone() credentials.TransportCredentials {
|
||||
return &tlsCreds{config: c.config.Clone()}
|
||||
}
|
||||
|
||||
func (c *tlsCreds) OverrideServerName(serverNameOverride string) error {
|
||||
c.config.SetServerName(serverNameOverride)
|
||||
return nil
|
||||
}
|
||||
3319
service/manager_api/grpc/manager/manager.pb.go
Normal file
3319
service/manager_api/grpc/manager/manager.pb.go
Normal file
File diff suppressed because it is too large
Load Diff
345
service/manager_api/grpc/manager/manager.proto
Normal file
345
service/manager_api/grpc/manager/manager.proto
Normal file
@@ -0,0 +1,345 @@
|
||||
syntax = "proto3";
|
||||
|
||||
option go_package = "github.com/sagernet/sing-box/service/manager_api/grpc/manager";
|
||||
|
||||
package manager_api.v1;
|
||||
|
||||
service Manager {
|
||||
rpc CreateSquad (SquadCreate) returns (Squad);
|
||||
rpc GetSquads (Filters) returns (SquadList);
|
||||
rpc GetSquadsCount (Filters) returns (CountReply);
|
||||
rpc GetSquad (IdRequest) returns (Squad);
|
||||
rpc UpdateSquad (SquadUpdateRequest) returns (Squad);
|
||||
rpc DeleteSquad (IdRequest) returns (Squad);
|
||||
|
||||
rpc CreateNode (NodeCreate) returns (Node);
|
||||
rpc GetNodes (Filters) returns (NodeList);
|
||||
rpc GetNodesCount (Filters) returns (CountReply);
|
||||
rpc GetNode (UuidRequest) returns (Node);
|
||||
rpc GetNodeStatus (UuidRequest) returns (NodeStatusReply);
|
||||
rpc UpdateNode (NodeUpdateRequest) returns (Node);
|
||||
rpc DeleteNode (UuidRequest) returns (Node);
|
||||
|
||||
rpc CreateUser (UserCreate) returns (User);
|
||||
rpc GetUsers (Filters) returns (UserList);
|
||||
rpc GetUsersCount (Filters) returns (CountReply);
|
||||
rpc GetUser (IdRequest) returns (User);
|
||||
rpc UpdateUser (UserUpdateRequest) returns (User);
|
||||
rpc DeleteUser (IdRequest) returns (User);
|
||||
|
||||
rpc CreateBandwidthLimiter (BandwidthLimiterCreate) returns (BandwidthLimiter);
|
||||
rpc GetBandwidthLimiters (Filters) returns (BandwidthLimiterList);
|
||||
rpc GetBandwidthLimitersCount (Filters) returns (CountReply);
|
||||
rpc GetBandwidthLimiter (IdRequest) returns (BandwidthLimiter);
|
||||
rpc UpdateBandwidthLimiter (BandwidthLimiterUpdateRequest) returns (BandwidthLimiter);
|
||||
rpc DeleteBandwidthLimiter (IdRequest) returns (BandwidthLimiter);
|
||||
|
||||
rpc CreateTrafficLimiter (TrafficLimiterCreate) returns (TrafficLimiter);
|
||||
rpc GetTrafficLimiters (Filters) returns (TrafficLimiterList);
|
||||
rpc GetTrafficLimitersCount (Filters) returns (CountReply);
|
||||
rpc GetTrafficLimiter (IdRequest) returns (TrafficLimiter);
|
||||
rpc UpdateTrafficLimiter (TrafficLimiterUpdateRequest) returns (TrafficLimiter);
|
||||
rpc DeleteTrafficLimiter (IdRequest) returns (TrafficLimiter);
|
||||
|
||||
rpc CreateConnectionLimiter (ConnectionLimiterCreate) returns (ConnectionLimiter);
|
||||
rpc GetConnectionLimiters (Filters) returns (ConnectionLimiterList);
|
||||
rpc GetConnectionLimitersCount (Filters) returns (CountReply);
|
||||
rpc GetConnectionLimiter (IdRequest) returns (ConnectionLimiter);
|
||||
rpc UpdateConnectionLimiter (ConnectionLimiterUpdateRequest) returns (ConnectionLimiter);
|
||||
rpc DeleteConnectionLimiter (IdRequest) returns (ConnectionLimiter);
|
||||
|
||||
rpc CreateRateLimiter (RateLimiterCreate) returns (RateLimiter);
|
||||
rpc GetRateLimiters (Filters) returns (RateLimiterList);
|
||||
rpc GetRateLimitersCount (Filters) returns (CountReply);
|
||||
rpc GetRateLimiter (IdRequest) returns (RateLimiter);
|
||||
rpc UpdateRateLimiter (RateLimiterUpdateRequest) returns (RateLimiter);
|
||||
rpc DeleteRateLimiter (IdRequest) returns (RateLimiter);
|
||||
}
|
||||
|
||||
message Squad {
|
||||
int32 id = 1;
|
||||
string name = 2;
|
||||
int64 created_at = 3;
|
||||
int64 updated_at = 4;
|
||||
}
|
||||
|
||||
message SquadCreate {
|
||||
string name = 1;
|
||||
}
|
||||
|
||||
message SquadUpdate {
|
||||
string name = 1;
|
||||
}
|
||||
|
||||
message SquadList {
|
||||
repeated Squad values = 1;
|
||||
}
|
||||
|
||||
message SquadUpdateRequest {
|
||||
int32 id = 1;
|
||||
SquadUpdate update = 2;
|
||||
}
|
||||
|
||||
message Node {
|
||||
string uuid = 1;
|
||||
string name = 2;
|
||||
repeated int32 squad_ids = 3;
|
||||
int64 created_at = 4;
|
||||
int64 updated_at = 5;
|
||||
}
|
||||
|
||||
message NodeCreate {
|
||||
string uuid = 1;
|
||||
string name = 2;
|
||||
repeated int32 squad_ids = 3;
|
||||
}
|
||||
|
||||
message NodeUpdate {
|
||||
string name = 1;
|
||||
}
|
||||
|
||||
message NodeList {
|
||||
repeated Node values = 1;
|
||||
}
|
||||
|
||||
message NodeUpdateRequest {
|
||||
string uuid = 1;
|
||||
NodeUpdate update = 2;
|
||||
}
|
||||
|
||||
message User {
|
||||
int32 id = 1;
|
||||
repeated int32 squad_ids = 2;
|
||||
string username = 3;
|
||||
string inbound = 4;
|
||||
string type = 5;
|
||||
string uuid = 6;
|
||||
string password = 7;
|
||||
string secret = 8;
|
||||
string flow = 9;
|
||||
int32 alter_id = 10;
|
||||
int64 created_at = 11;
|
||||
int64 updated_at = 12;
|
||||
}
|
||||
|
||||
message UserCreate {
|
||||
repeated int32 squad_ids = 1;
|
||||
string username = 2;
|
||||
string inbound = 3;
|
||||
string type = 4;
|
||||
string uuid = 5;
|
||||
string password = 6;
|
||||
string secret = 7;
|
||||
string flow = 8;
|
||||
int32 alter_id = 9;
|
||||
}
|
||||
|
||||
message UserUpdate {
|
||||
string uuid = 1;
|
||||
string password = 2;
|
||||
string secret = 3;
|
||||
string flow = 4;
|
||||
int32 alter_id = 5;
|
||||
}
|
||||
|
||||
message UserList {
|
||||
repeated User values = 1;
|
||||
}
|
||||
|
||||
message UserUpdateRequest {
|
||||
int32 id = 1;
|
||||
UserUpdate update = 2;
|
||||
}
|
||||
|
||||
message BandwidthLimiter {
|
||||
int32 id = 1;
|
||||
repeated int32 squad_ids = 2;
|
||||
string username = 3;
|
||||
string outbound = 4;
|
||||
string strategy = 5;
|
||||
string connection_type = 6;
|
||||
string mode = 7;
|
||||
repeated string flow_keys = 8;
|
||||
string speed = 9;
|
||||
uint64 raw_speed = 10;
|
||||
int64 created_at = 11;
|
||||
int64 updated_at = 12;
|
||||
}
|
||||
|
||||
message BandwidthLimiterCreate {
|
||||
repeated int32 squad_ids = 1;
|
||||
string username = 2;
|
||||
string outbound = 3;
|
||||
string strategy = 4;
|
||||
string connection_type = 5;
|
||||
string mode = 6;
|
||||
repeated string flow_keys = 7;
|
||||
string speed = 8;
|
||||
}
|
||||
|
||||
message BandwidthLimiterUpdate {
|
||||
string username = 1;
|
||||
string outbound = 2;
|
||||
string strategy = 3;
|
||||
string connection_type = 4;
|
||||
string mode = 5;
|
||||
repeated string flow_keys = 6;
|
||||
string speed = 7;
|
||||
}
|
||||
|
||||
message BandwidthLimiterList {
|
||||
repeated BandwidthLimiter values = 1;
|
||||
}
|
||||
|
||||
message BandwidthLimiterUpdateRequest {
|
||||
int32 id = 1;
|
||||
BandwidthLimiterUpdate update = 2;
|
||||
}
|
||||
|
||||
message TrafficLimiter {
|
||||
int32 id = 1;
|
||||
repeated int32 squad_ids = 2;
|
||||
string username = 3;
|
||||
string outbound = 4;
|
||||
string strategy = 5;
|
||||
string mode = 6;
|
||||
uint64 raw_used = 7;
|
||||
string quota = 8;
|
||||
uint64 raw_quota = 9;
|
||||
int64 created_at = 10;
|
||||
int64 updated_at = 11;
|
||||
}
|
||||
|
||||
message TrafficLimiterCreate {
|
||||
repeated int32 squad_ids = 1;
|
||||
string username = 2;
|
||||
string outbound = 3;
|
||||
string strategy = 4;
|
||||
string mode = 5;
|
||||
string quota = 6;
|
||||
}
|
||||
|
||||
message TrafficLimiterUpdate {
|
||||
string username = 1;
|
||||
string outbound = 2;
|
||||
string strategy = 3;
|
||||
string mode = 4;
|
||||
string quota = 5;
|
||||
}
|
||||
|
||||
message TrafficLimiterList {
|
||||
repeated TrafficLimiter values = 1;
|
||||
}
|
||||
|
||||
message TrafficLimiterUpdateRequest {
|
||||
int32 id = 1;
|
||||
TrafficLimiterUpdate update = 2;
|
||||
}
|
||||
|
||||
message ConnectionLimiter {
|
||||
int32 id = 1;
|
||||
repeated int32 squad_ids = 2;
|
||||
string username = 3;
|
||||
string outbound = 4;
|
||||
string strategy = 5;
|
||||
string connection_type = 6;
|
||||
string lock_type = 7;
|
||||
uint32 count = 8;
|
||||
int64 created_at = 9;
|
||||
int64 updated_at = 10;
|
||||
}
|
||||
|
||||
message ConnectionLimiterCreate {
|
||||
repeated int32 squad_ids = 1;
|
||||
string username = 2;
|
||||
string outbound = 3;
|
||||
string strategy = 4;
|
||||
string connection_type = 5;
|
||||
string lock_type = 6;
|
||||
uint32 count = 7;
|
||||
}
|
||||
|
||||
message ConnectionLimiterUpdate {
|
||||
string username = 1;
|
||||
string outbound = 2;
|
||||
string strategy = 3;
|
||||
string connection_type = 4;
|
||||
string lock_type = 5;
|
||||
uint32 count = 6;
|
||||
}
|
||||
|
||||
message ConnectionLimiterList {
|
||||
repeated ConnectionLimiter values = 1;
|
||||
}
|
||||
|
||||
message ConnectionLimiterUpdateRequest {
|
||||
int32 id = 1;
|
||||
ConnectionLimiterUpdate update = 2;
|
||||
}
|
||||
|
||||
message RateLimiter {
|
||||
int32 id = 1;
|
||||
repeated int32 squad_ids = 2;
|
||||
string username = 3;
|
||||
string outbound = 4;
|
||||
string strategy = 5;
|
||||
string connection_type = 6;
|
||||
uint32 count = 7;
|
||||
string interval = 8;
|
||||
int64 created_at = 9;
|
||||
int64 updated_at = 10;
|
||||
}
|
||||
|
||||
message RateLimiterCreate {
|
||||
repeated int32 squad_ids = 1;
|
||||
string username = 2;
|
||||
string outbound = 3;
|
||||
string strategy = 4;
|
||||
string connection_type = 5;
|
||||
uint32 count = 6;
|
||||
string interval = 7;
|
||||
}
|
||||
|
||||
message RateLimiterUpdate {
|
||||
string username = 1;
|
||||
string outbound = 2;
|
||||
string strategy = 3;
|
||||
string connection_type = 4;
|
||||
uint32 count = 5;
|
||||
string interval = 6;
|
||||
}
|
||||
|
||||
message RateLimiterList {
|
||||
repeated RateLimiter values = 1;
|
||||
}
|
||||
|
||||
message RateLimiterUpdateRequest {
|
||||
int32 id = 1;
|
||||
RateLimiterUpdate update = 2;
|
||||
}
|
||||
|
||||
message IdRequest {
|
||||
int32 id = 1;
|
||||
}
|
||||
|
||||
message UuidRequest {
|
||||
string uuid = 1;
|
||||
}
|
||||
|
||||
message CountReply {
|
||||
int64 count = 1;
|
||||
}
|
||||
|
||||
message NodeStatusReply {
|
||||
string status = 1;
|
||||
}
|
||||
|
||||
message StringList {
|
||||
repeated string values = 1;
|
||||
}
|
||||
|
||||
message Filters {
|
||||
map<string, StringList> values = 1;
|
||||
}
|
||||
|
||||
message Empty {}
|
||||
1717
service/manager_api/grpc/manager/manager_grpc.pb.go
Normal file
1717
service/manager_api/grpc/manager/manager_grpc.pb.go
Normal file
File diff suppressed because it is too large
Load Diff
137
service/manager_api/grpc/server/converter.go
Normal file
137
service/manager_api/grpc/server/converter.go
Normal file
@@ -0,0 +1,137 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
CM "github.com/sagernet/sing-box/service/manager/constant"
|
||||
pb "github.com/sagernet/sing-box/service/manager_api/grpc/manager"
|
||||
)
|
||||
|
||||
func toIntSlice(values []int32) []int {
|
||||
out := make([]int, len(values))
|
||||
for i, v := range values {
|
||||
out[i] = int(v)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func toInt32Slice(values []int) []int32 {
|
||||
out := make([]int32, len(values))
|
||||
for i, v := range values {
|
||||
out[i] = int32(v)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func convertSquad(v CM.Squad) *pb.Squad {
|
||||
return &pb.Squad{
|
||||
Id: int32(v.ID),
|
||||
Name: v.Name,
|
||||
CreatedAt: v.CreatedAt.UnixNano(),
|
||||
UpdatedAt: v.UpdatedAt.UnixNano(),
|
||||
}
|
||||
}
|
||||
|
||||
func convertNode(v CM.Node) *pb.Node {
|
||||
return &pb.Node{
|
||||
Uuid: v.UUID,
|
||||
Name: v.Name,
|
||||
SquadIds: toInt32Slice(v.SquadIDs),
|
||||
CreatedAt: v.CreatedAt.UnixNano(),
|
||||
UpdatedAt: v.UpdatedAt.UnixNano(),
|
||||
}
|
||||
}
|
||||
|
||||
func convertUser(v CM.User) *pb.User {
|
||||
return &pb.User{
|
||||
Id: int32(v.ID),
|
||||
SquadIds: toInt32Slice(v.SquadIDs),
|
||||
Username: v.Username,
|
||||
Inbound: v.Inbound,
|
||||
Type: v.Type,
|
||||
Uuid: v.UUID,
|
||||
Password: v.Password,
|
||||
Secret: v.Secret,
|
||||
Flow: v.Flow,
|
||||
AlterId: int32(v.AlterID),
|
||||
CreatedAt: v.CreatedAt.UnixNano(),
|
||||
UpdatedAt: v.UpdatedAt.UnixNano(),
|
||||
}
|
||||
}
|
||||
|
||||
func convertBandwidthLimiter(v CM.BandwidthLimiter) *pb.BandwidthLimiter {
|
||||
return &pb.BandwidthLimiter{
|
||||
Id: int32(v.ID),
|
||||
SquadIds: toInt32Slice(v.SquadIDs),
|
||||
Username: v.Username,
|
||||
Outbound: v.Outbound,
|
||||
Strategy: v.Strategy,
|
||||
ConnectionType: v.ConnectionType,
|
||||
Mode: v.Mode,
|
||||
FlowKeys: v.FlowKeys,
|
||||
Speed: v.Speed,
|
||||
RawSpeed: v.RawSpeed,
|
||||
CreatedAt: v.CreatedAt.UnixNano(),
|
||||
UpdatedAt: v.UpdatedAt.UnixNano(),
|
||||
}
|
||||
}
|
||||
|
||||
func convertTrafficLimiter(v CM.TrafficLimiter) *pb.TrafficLimiter {
|
||||
return &pb.TrafficLimiter{
|
||||
Id: int32(v.ID),
|
||||
SquadIds: toInt32Slice(v.SquadIDs),
|
||||
Username: v.Username,
|
||||
Outbound: v.Outbound,
|
||||
Strategy: v.Strategy,
|
||||
Mode: v.Mode,
|
||||
RawUsed: v.RawUsed,
|
||||
Quota: v.Quota,
|
||||
RawQuota: v.RawQuota,
|
||||
CreatedAt: v.CreatedAt.UnixNano(),
|
||||
UpdatedAt: v.UpdatedAt.UnixNano(),
|
||||
}
|
||||
}
|
||||
|
||||
func convertConnectionLimiter(v CM.ConnectionLimiter) *pb.ConnectionLimiter {
|
||||
return &pb.ConnectionLimiter{
|
||||
Id: int32(v.ID),
|
||||
SquadIds: toInt32Slice(v.SquadIDs),
|
||||
Username: v.Username,
|
||||
Outbound: v.Outbound,
|
||||
Strategy: v.Strategy,
|
||||
ConnectionType: v.ConnectionType,
|
||||
LockType: v.LockType,
|
||||
Count: v.Count,
|
||||
CreatedAt: v.CreatedAt.UnixNano(),
|
||||
UpdatedAt: v.UpdatedAt.UnixNano(),
|
||||
}
|
||||
}
|
||||
|
||||
func convertRateLimiter(v CM.RateLimiter) *pb.RateLimiter {
|
||||
return &pb.RateLimiter{
|
||||
Id: int32(v.ID),
|
||||
SquadIds: toInt32Slice(v.SquadIDs),
|
||||
Username: v.Username,
|
||||
Outbound: v.Outbound,
|
||||
Strategy: v.Strategy,
|
||||
ConnectionType: v.ConnectionType,
|
||||
Count: v.Count,
|
||||
Interval: v.Interval,
|
||||
CreatedAt: v.CreatedAt.UnixNano(),
|
||||
UpdatedAt: v.UpdatedAt.UnixNano(),
|
||||
}
|
||||
}
|
||||
|
||||
func convertFilters(req *pb.Filters) map[string][]string {
|
||||
filters := map[string][]string{}
|
||||
for k, v := range req.GetValues() {
|
||||
filters[k] = append([]string(nil), v.GetValues()...)
|
||||
}
|
||||
return filters
|
||||
}
|
||||
|
||||
func convertListFilters(req *pb.Filters) map[string][]string {
|
||||
filters := convertFilters(req)
|
||||
if _, ok := filters["limit"]; !ok {
|
||||
filters["limit"] = []string{"100"}
|
||||
}
|
||||
return filters
|
||||
}
|
||||
465
service/manager_api/grpc/server/rpc.go
Normal file
465
service/manager_api/grpc/server/rpc.go
Normal file
@@ -0,0 +1,465 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
CM "github.com/sagernet/sing-box/service/manager/constant"
|
||||
pb "github.com/sagernet/sing-box/service/manager_api/grpc/manager"
|
||||
)
|
||||
|
||||
func (s *Server) CreateSquad(_ context.Context, req *pb.SquadCreate) (*pb.Squad, error) {
|
||||
v, err := s.manager.CreateSquad(CM.SquadCreate{Name: req.GetName()})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return convertSquad(v), nil
|
||||
}
|
||||
|
||||
func (s *Server) GetSquads(_ context.Context, req *pb.Filters) (*pb.SquadList, error) {
|
||||
items, err := s.manager.GetSquads(convertListFilters(req))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := make([]*pb.Squad, len(items))
|
||||
for i, v := range items {
|
||||
out[i] = convertSquad(v)
|
||||
}
|
||||
return &pb.SquadList{Values: out}, nil
|
||||
}
|
||||
|
||||
func (s *Server) GetSquadsCount(_ context.Context, req *pb.Filters) (*pb.CountReply, error) {
|
||||
n, err := s.manager.GetSquadsCount(convertFilters(req))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &pb.CountReply{Count: int64(n)}, nil
|
||||
}
|
||||
|
||||
func (s *Server) GetSquad(_ context.Context, req *pb.IdRequest) (*pb.Squad, error) {
|
||||
v, err := s.manager.GetSquad(int(req.GetId()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return convertSquad(v), nil
|
||||
}
|
||||
|
||||
func (s *Server) UpdateSquad(_ context.Context, req *pb.SquadUpdateRequest) (*pb.Squad, error) {
|
||||
v, err := s.manager.UpdateSquad(int(req.GetId()), CM.SquadUpdate{Name: req.GetUpdate().GetName()})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return convertSquad(v), nil
|
||||
}
|
||||
|
||||
func (s *Server) DeleteSquad(_ context.Context, req *pb.IdRequest) (*pb.Squad, error) {
|
||||
v, err := s.manager.DeleteSquad(int(req.GetId()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return convertSquad(v), nil
|
||||
}
|
||||
|
||||
func (s *Server) CreateNode(_ context.Context, req *pb.NodeCreate) (*pb.Node, error) {
|
||||
v, err := s.manager.CreateNode(CM.NodeCreate{
|
||||
UUID: req.GetUuid(),
|
||||
Name: req.GetName(),
|
||||
SquadIDs: toIntSlice(req.GetSquadIds()),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return convertNode(v), nil
|
||||
}
|
||||
|
||||
func (s *Server) GetNodes(_ context.Context, req *pb.Filters) (*pb.NodeList, error) {
|
||||
items, err := s.manager.GetNodes(convertListFilters(req))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := make([]*pb.Node, len(items))
|
||||
for i, v := range items {
|
||||
out[i] = convertNode(v)
|
||||
}
|
||||
return &pb.NodeList{Values: out}, nil
|
||||
}
|
||||
|
||||
func (s *Server) GetNodesCount(_ context.Context, req *pb.Filters) (*pb.CountReply, error) {
|
||||
n, err := s.manager.GetNodesCount(convertFilters(req))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &pb.CountReply{Count: int64(n)}, nil
|
||||
}
|
||||
|
||||
func (s *Server) GetNode(_ context.Context, req *pb.UuidRequest) (*pb.Node, error) {
|
||||
v, err := s.manager.GetNode(req.GetUuid())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return convertNode(v), nil
|
||||
}
|
||||
|
||||
func (s *Server) GetNodeStatus(_ context.Context, req *pb.UuidRequest) (*pb.NodeStatusReply, error) {
|
||||
status, err := s.manager.GetNodeStatus(req.GetUuid())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &pb.NodeStatusReply{Status: status}, nil
|
||||
}
|
||||
|
||||
func (s *Server) UpdateNode(_ context.Context, req *pb.NodeUpdateRequest) (*pb.Node, error) {
|
||||
v, err := s.manager.UpdateNode(req.GetUuid(), CM.NodeUpdate{Name: req.GetUpdate().GetName()})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return convertNode(v), nil
|
||||
}
|
||||
|
||||
func (s *Server) DeleteNode(_ context.Context, req *pb.UuidRequest) (*pb.Node, error) {
|
||||
v, err := s.manager.DeleteNode(req.GetUuid())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return convertNode(v), nil
|
||||
}
|
||||
|
||||
func (s *Server) CreateUser(_ context.Context, req *pb.UserCreate) (*pb.User, error) {
|
||||
v, err := s.manager.CreateUser(CM.UserCreate{
|
||||
SquadIDs: toIntSlice(req.GetSquadIds()),
|
||||
Username: req.GetUsername(),
|
||||
Inbound: req.GetInbound(),
|
||||
Type: req.GetType(),
|
||||
UUID: req.GetUuid(),
|
||||
Password: req.GetPassword(),
|
||||
Secret: req.GetSecret(),
|
||||
Flow: req.GetFlow(),
|
||||
AlterID: int(req.GetAlterId()),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return convertUser(v), nil
|
||||
}
|
||||
|
||||
func (s *Server) GetUsers(_ context.Context, req *pb.Filters) (*pb.UserList, error) {
|
||||
items, err := s.manager.GetUsers(convertListFilters(req))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := make([]*pb.User, len(items))
|
||||
for i, v := range items {
|
||||
out[i] = convertUser(v)
|
||||
}
|
||||
return &pb.UserList{Values: out}, nil
|
||||
}
|
||||
|
||||
func (s *Server) GetUsersCount(_ context.Context, req *pb.Filters) (*pb.CountReply, error) {
|
||||
n, err := s.manager.GetUsersCount(convertFilters(req))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &pb.CountReply{Count: int64(n)}, nil
|
||||
}
|
||||
|
||||
func (s *Server) GetUser(_ context.Context, req *pb.IdRequest) (*pb.User, error) {
|
||||
v, err := s.manager.GetUser(int(req.GetId()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return convertUser(v), nil
|
||||
}
|
||||
|
||||
func (s *Server) UpdateUser(_ context.Context, req *pb.UserUpdateRequest) (*pb.User, error) {
|
||||
u := req.GetUpdate()
|
||||
v, err := s.manager.UpdateUser(int(req.GetId()), CM.UserUpdate{
|
||||
UUID: u.GetUuid(),
|
||||
Password: u.GetPassword(),
|
||||
Secret: u.GetSecret(),
|
||||
Flow: u.GetFlow(),
|
||||
AlterID: int(u.GetAlterId()),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return convertUser(v), nil
|
||||
}
|
||||
|
||||
func (s *Server) DeleteUser(_ context.Context, req *pb.IdRequest) (*pb.User, error) {
|
||||
v, err := s.manager.DeleteUser(int(req.GetId()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return convertUser(v), nil
|
||||
}
|
||||
|
||||
func (s *Server) CreateBandwidthLimiter(_ context.Context, req *pb.BandwidthLimiterCreate) (*pb.BandwidthLimiter, error) {
|
||||
v, err := s.manager.CreateBandwidthLimiter(CM.BandwidthLimiterCreate{
|
||||
SquadIDs: toIntSlice(req.GetSquadIds()),
|
||||
Username: req.GetUsername(),
|
||||
Outbound: req.GetOutbound(),
|
||||
Strategy: req.GetStrategy(),
|
||||
ConnectionType: req.GetConnectionType(),
|
||||
Mode: req.GetMode(),
|
||||
FlowKeys: req.GetFlowKeys(),
|
||||
Speed: req.GetSpeed(),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return convertBandwidthLimiter(v), nil
|
||||
}
|
||||
|
||||
func (s *Server) GetBandwidthLimiters(_ context.Context, req *pb.Filters) (*pb.BandwidthLimiterList, error) {
|
||||
items, err := s.manager.GetBandwidthLimiters(convertListFilters(req))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := make([]*pb.BandwidthLimiter, len(items))
|
||||
for i, v := range items {
|
||||
out[i] = convertBandwidthLimiter(v)
|
||||
}
|
||||
return &pb.BandwidthLimiterList{Values: out}, nil
|
||||
}
|
||||
|
||||
func (s *Server) GetBandwidthLimitersCount(_ context.Context, req *pb.Filters) (*pb.CountReply, error) {
|
||||
n, err := s.manager.GetBandwidthLimitersCount(convertFilters(req))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &pb.CountReply{Count: int64(n)}, nil
|
||||
}
|
||||
|
||||
func (s *Server) GetBandwidthLimiter(_ context.Context, req *pb.IdRequest) (*pb.BandwidthLimiter, error) {
|
||||
v, err := s.manager.GetBandwidthLimiter(int(req.GetId()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return convertBandwidthLimiter(v), nil
|
||||
}
|
||||
|
||||
func (s *Server) UpdateBandwidthLimiter(_ context.Context, req *pb.BandwidthLimiterUpdateRequest) (*pb.BandwidthLimiter, error) {
|
||||
u := req.GetUpdate()
|
||||
v, err := s.manager.UpdateBandwidthLimiter(int(req.GetId()), CM.BandwidthLimiterUpdate{
|
||||
Username: u.GetUsername(),
|
||||
Outbound: u.GetOutbound(),
|
||||
Strategy: u.GetStrategy(),
|
||||
ConnectionType: u.GetConnectionType(),
|
||||
Mode: u.GetMode(),
|
||||
FlowKeys: u.GetFlowKeys(),
|
||||
Speed: u.GetSpeed(),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return convertBandwidthLimiter(v), nil
|
||||
}
|
||||
|
||||
func (s *Server) DeleteBandwidthLimiter(_ context.Context, req *pb.IdRequest) (*pb.BandwidthLimiter, error) {
|
||||
v, err := s.manager.DeleteBandwidthLimiter(int(req.GetId()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return convertBandwidthLimiter(v), nil
|
||||
}
|
||||
|
||||
func (s *Server) CreateTrafficLimiter(_ context.Context, req *pb.TrafficLimiterCreate) (*pb.TrafficLimiter, error) {
|
||||
v, err := s.manager.CreateTrafficLimiter(CM.TrafficLimiterCreate{
|
||||
SquadIDs: toIntSlice(req.GetSquadIds()),
|
||||
Username: req.GetUsername(),
|
||||
Outbound: req.GetOutbound(),
|
||||
Strategy: req.GetStrategy(),
|
||||
Mode: req.GetMode(),
|
||||
Quota: req.GetQuota(),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return convertTrafficLimiter(v), nil
|
||||
}
|
||||
|
||||
func (s *Server) GetTrafficLimiters(_ context.Context, req *pb.Filters) (*pb.TrafficLimiterList, error) {
|
||||
items, err := s.manager.GetTrafficLimiters(convertListFilters(req))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := make([]*pb.TrafficLimiter, len(items))
|
||||
for i, v := range items {
|
||||
out[i] = convertTrafficLimiter(v)
|
||||
}
|
||||
return &pb.TrafficLimiterList{Values: out}, nil
|
||||
}
|
||||
|
||||
func (s *Server) GetTrafficLimitersCount(_ context.Context, req *pb.Filters) (*pb.CountReply, error) {
|
||||
n, err := s.manager.GetTrafficLimitersCount(convertFilters(req))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &pb.CountReply{Count: int64(n)}, nil
|
||||
}
|
||||
|
||||
func (s *Server) GetTrafficLimiter(_ context.Context, req *pb.IdRequest) (*pb.TrafficLimiter, error) {
|
||||
v, err := s.manager.GetTrafficLimiter(int(req.GetId()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return convertTrafficLimiter(v), nil
|
||||
}
|
||||
|
||||
func (s *Server) UpdateTrafficLimiter(_ context.Context, req *pb.TrafficLimiterUpdateRequest) (*pb.TrafficLimiter, error) {
|
||||
u := req.GetUpdate()
|
||||
v, err := s.manager.UpdateTrafficLimiter(int(req.GetId()), CM.TrafficLimiterUpdate{
|
||||
Username: u.GetUsername(),
|
||||
Outbound: u.GetOutbound(),
|
||||
Strategy: u.GetStrategy(),
|
||||
Mode: u.GetMode(),
|
||||
Quota: u.GetQuota(),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return convertTrafficLimiter(v), nil
|
||||
}
|
||||
|
||||
func (s *Server) DeleteTrafficLimiter(_ context.Context, req *pb.IdRequest) (*pb.TrafficLimiter, error) {
|
||||
v, err := s.manager.DeleteTrafficLimiter(int(req.GetId()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return convertTrafficLimiter(v), nil
|
||||
}
|
||||
|
||||
func (s *Server) CreateConnectionLimiter(_ context.Context, req *pb.ConnectionLimiterCreate) (*pb.ConnectionLimiter, error) {
|
||||
v, err := s.manager.CreateConnectionLimiter(CM.ConnectionLimiterCreate{
|
||||
SquadIDs: toIntSlice(req.GetSquadIds()),
|
||||
Username: req.GetUsername(),
|
||||
Outbound: req.GetOutbound(),
|
||||
Strategy: req.GetStrategy(),
|
||||
ConnectionType: req.GetConnectionType(),
|
||||
LockType: req.GetLockType(),
|
||||
Count: req.GetCount(),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return convertConnectionLimiter(v), nil
|
||||
}
|
||||
|
||||
func (s *Server) GetConnectionLimiters(_ context.Context, req *pb.Filters) (*pb.ConnectionLimiterList, error) {
|
||||
items, err := s.manager.GetConnectionLimiters(convertListFilters(req))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := make([]*pb.ConnectionLimiter, len(items))
|
||||
for i, v := range items {
|
||||
out[i] = convertConnectionLimiter(v)
|
||||
}
|
||||
return &pb.ConnectionLimiterList{Values: out}, nil
|
||||
}
|
||||
|
||||
func (s *Server) GetConnectionLimitersCount(_ context.Context, req *pb.Filters) (*pb.CountReply, error) {
|
||||
n, err := s.manager.GetConnectionLimitersCount(convertFilters(req))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &pb.CountReply{Count: int64(n)}, nil
|
||||
}
|
||||
|
||||
func (s *Server) GetConnectionLimiter(_ context.Context, req *pb.IdRequest) (*pb.ConnectionLimiter, error) {
|
||||
v, err := s.manager.GetConnectionLimiter(int(req.GetId()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return convertConnectionLimiter(v), nil
|
||||
}
|
||||
|
||||
func (s *Server) UpdateConnectionLimiter(_ context.Context, req *pb.ConnectionLimiterUpdateRequest) (*pb.ConnectionLimiter, error) {
|
||||
u := req.GetUpdate()
|
||||
v, err := s.manager.UpdateConnectionLimiter(int(req.GetId()), CM.ConnectionLimiterUpdate{
|
||||
Username: u.GetUsername(),
|
||||
Outbound: u.GetOutbound(),
|
||||
Strategy: u.GetStrategy(),
|
||||
ConnectionType: u.GetConnectionType(),
|
||||
LockType: u.GetLockType(),
|
||||
Count: u.GetCount(),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return convertConnectionLimiter(v), nil
|
||||
}
|
||||
|
||||
func (s *Server) DeleteConnectionLimiter(_ context.Context, req *pb.IdRequest) (*pb.ConnectionLimiter, error) {
|
||||
v, err := s.manager.DeleteConnectionLimiter(int(req.GetId()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return convertConnectionLimiter(v), nil
|
||||
}
|
||||
|
||||
func (s *Server) CreateRateLimiter(_ context.Context, req *pb.RateLimiterCreate) (*pb.RateLimiter, error) {
|
||||
v, err := s.manager.CreateRateLimiter(CM.RateLimiterCreate{
|
||||
SquadIDs: toIntSlice(req.GetSquadIds()),
|
||||
Username: req.GetUsername(),
|
||||
Outbound: req.GetOutbound(),
|
||||
Strategy: req.GetStrategy(),
|
||||
ConnectionType: req.GetConnectionType(),
|
||||
Count: req.GetCount(),
|
||||
Interval: req.GetInterval(),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return convertRateLimiter(v), nil
|
||||
}
|
||||
|
||||
func (s *Server) GetRateLimiters(_ context.Context, req *pb.Filters) (*pb.RateLimiterList, error) {
|
||||
items, err := s.manager.GetRateLimiters(convertListFilters(req))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := make([]*pb.RateLimiter, len(items))
|
||||
for i, v := range items {
|
||||
out[i] = convertRateLimiter(v)
|
||||
}
|
||||
return &pb.RateLimiterList{Values: out}, nil
|
||||
}
|
||||
|
||||
func (s *Server) GetRateLimitersCount(_ context.Context, req *pb.Filters) (*pb.CountReply, error) {
|
||||
n, err := s.manager.GetRateLimitersCount(convertFilters(req))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &pb.CountReply{Count: int64(n)}, nil
|
||||
}
|
||||
|
||||
func (s *Server) GetRateLimiter(_ context.Context, req *pb.IdRequest) (*pb.RateLimiter, error) {
|
||||
v, err := s.manager.GetRateLimiter(int(req.GetId()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return convertRateLimiter(v), nil
|
||||
}
|
||||
|
||||
func (s *Server) UpdateRateLimiter(_ context.Context, req *pb.RateLimiterUpdateRequest) (*pb.RateLimiter, error) {
|
||||
u := req.GetUpdate()
|
||||
v, err := s.manager.UpdateRateLimiter(int(req.GetId()), CM.RateLimiterUpdate{
|
||||
Username: u.GetUsername(),
|
||||
Outbound: u.GetOutbound(),
|
||||
Strategy: u.GetStrategy(),
|
||||
ConnectionType: u.GetConnectionType(),
|
||||
Count: u.GetCount(),
|
||||
Interval: u.GetInterval(),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return convertRateLimiter(v), nil
|
||||
}
|
||||
|
||||
func (s *Server) DeleteRateLimiter(_ context.Context, req *pb.IdRequest) (*pb.RateLimiter, error) {
|
||||
v, err := s.manager.DeleteRateLimiter(int(req.GetId()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return convertRateLimiter(v), nil
|
||||
}
|
||||
157
service/manager_api/grpc/server/server.go
Normal file
157
service/manager_api/grpc/server/server.go
Normal file
@@ -0,0 +1,157 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/subtle"
|
||||
"errors"
|
||||
"sync"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
boxService "github.com/sagernet/sing-box/adapter/service"
|
||||
"github.com/sagernet/sing-box/common/listener"
|
||||
"github.com/sagernet/sing-box/common/tls"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
CM "github.com/sagernet/sing-box/service/manager/constant"
|
||||
pb "github.com/sagernet/sing-box/service/manager_api/grpc/manager"
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
aTLS "github.com/sagernet/sing/common/tls"
|
||||
"github.com/sagernet/sing/service"
|
||||
|
||||
"golang.org/x/net/http2"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/metadata"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
pb.UnimplementedManagerServer
|
||||
boxService.Adapter
|
||||
|
||||
ctx context.Context
|
||||
logger log.ContextLogger
|
||||
listener *listener.Listener
|
||||
tlsConfig tls.ServerConfig
|
||||
grpcServer *grpc.Server
|
||||
manager CM.Manager
|
||||
options option.ManagerAPIServerOptions
|
||||
|
||||
mtx sync.Mutex
|
||||
}
|
||||
|
||||
func NewServer(ctx context.Context, logger log.ContextLogger, tag string, options option.ManagerAPIServerOptions) (*Server, error) {
|
||||
if options.APIKey == "" {
|
||||
return nil, E.New("missing api key")
|
||||
}
|
||||
return &Server{
|
||||
Adapter: boxService.NewAdapter(C.TypeManagerAPI, tag),
|
||||
ctx: ctx,
|
||||
logger: logger,
|
||||
listener: listener.New(listener.Options{
|
||||
Context: ctx,
|
||||
Logger: logger,
|
||||
Network: []string{N.NetworkTCP},
|
||||
Listen: options.ListenOptions,
|
||||
}),
|
||||
options: options,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Server) Start(stage adapter.StartStage) error {
|
||||
if stage != adapter.StartStateStart {
|
||||
return nil
|
||||
}
|
||||
boxManager := service.FromContext[adapter.ServiceManager](s.ctx)
|
||||
managerService, ok := boxManager.Get(s.options.Manager)
|
||||
if !ok {
|
||||
return E.New("manager ", s.options.Manager, " not found")
|
||||
}
|
||||
s.manager, ok = managerService.(CM.Manager)
|
||||
if !ok {
|
||||
return E.New("invalid ", s.options.Manager, " manager")
|
||||
}
|
||||
if s.options.TLS != nil {
|
||||
tlsConfig, err := tls.NewServer(s.ctx, s.logger, common.PtrValueOrDefault(s.options.TLS))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.tlsConfig = tlsConfig
|
||||
}
|
||||
if s.tlsConfig != nil {
|
||||
if err := s.tlsConfig.Start(); err != nil {
|
||||
return E.Cause(err, "create TLS config")
|
||||
}
|
||||
}
|
||||
tcpListener, err := s.listener.ListenTCP()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if s.tlsConfig != nil {
|
||||
if !common.Contains(s.tlsConfig.NextProtos(), http2.NextProtoTLS) {
|
||||
s.tlsConfig.SetNextProtos(append([]string{"h2"}, s.tlsConfig.NextProtos()...))
|
||||
}
|
||||
tcpListener = aTLS.NewListener(tcpListener, s.tlsConfig)
|
||||
}
|
||||
s.grpcServer = grpc.NewServer(
|
||||
grpc.ChainUnaryInterceptor(s.unaryAuthInterceptor, unaryErrorInterceptor),
|
||||
grpc.StreamInterceptor(s.streamAuthInterceptor),
|
||||
)
|
||||
pb.RegisterManagerServer(s.grpcServer, s)
|
||||
go func() {
|
||||
if err := s.grpcServer.Serve(tcpListener); err != nil && !errors.Is(err, grpc.ErrServerStopped) {
|
||||
s.logger.Error("serve error: ", err)
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) Close() error {
|
||||
if s.grpcServer != nil {
|
||||
s.grpcServer.GracefulStop()
|
||||
}
|
||||
return common.Close(
|
||||
common.PtrOrNil(s.listener),
|
||||
s.tlsConfig,
|
||||
)
|
||||
}
|
||||
|
||||
func (s *Server) unaryAuthInterceptor(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) {
|
||||
if err := s.authorize(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return handler(ctx, req)
|
||||
}
|
||||
|
||||
func (s *Server) streamAuthInterceptor(srv any, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
|
||||
if err := s.authorize(ss.Context()); err != nil {
|
||||
return err
|
||||
}
|
||||
return handler(srv, ss)
|
||||
}
|
||||
|
||||
func unaryErrorInterceptor(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) {
|
||||
resp, err := handler(ctx, req)
|
||||
if err == CM.ErrNotFound {
|
||||
return resp, status.Error(codes.NotFound, err.Error())
|
||||
}
|
||||
return resp, err
|
||||
}
|
||||
|
||||
func (s *Server) authorize(ctx context.Context) error {
|
||||
md, ok := metadata.FromIncomingContext(ctx)
|
||||
if !ok {
|
||||
return status.Error(codes.Unauthenticated, "missing api key")
|
||||
}
|
||||
values := md.Get("authorization")
|
||||
if len(values) == 0 {
|
||||
return status.Error(codes.Unauthenticated, "missing api key")
|
||||
}
|
||||
if subtle.ConstantTimeCompare([]byte(values[0]), []byte(s.options.APIKey)) == 0 {
|
||||
return status.Error(codes.Unauthenticated, "invalid api key")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
133
service/manager_api/http/client/client.go
Normal file
133
service/manager_api/http/client/client.go
Normal file
@@ -0,0 +1,133 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
boxService "github.com/sagernet/sing-box/adapter/service"
|
||||
"github.com/sagernet/sing-box/common/dialer"
|
||||
"github.com/sagernet/sing-box/common/tls"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
CM "github.com/sagernet/sing-box/service/manager/constant"
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
boxService.Adapter
|
||||
|
||||
ctx context.Context
|
||||
logger log.ContextLogger
|
||||
options option.ManagerAPIClientOptions
|
||||
httpClient *http.Client
|
||||
baseURL string
|
||||
}
|
||||
|
||||
func NewClient(ctx context.Context, logger log.ContextLogger, tag string, options option.ManagerAPIClientOptions) (*Client, error) {
|
||||
outboundDialer, err := dialer.New(ctx, options.DialerOptions, options.ServerIsDomain())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
transport := &http.Transport{
|
||||
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
return outboundDialer.DialContext(ctx, network, M.ParseSocksaddr(addr))
|
||||
},
|
||||
}
|
||||
scheme := "http"
|
||||
if options.TLS != nil {
|
||||
tlsConfig, err := tls.NewClient(ctx, logger, options.Server, common.PtrValueOrDefault(options.TLS))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !common.Contains(tlsConfig.NextProtos(), "http/1.1") {
|
||||
tlsConfig.SetNextProtos(append([]string{"http/1.1"}, tlsConfig.NextProtos()...))
|
||||
}
|
||||
transport.DialTLSContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
rawConn, err := outboundDialer.DialContext(ctx, network, M.ParseSocksaddr(addr))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tls.ClientHandshake(ctx, rawConn, tlsConfig)
|
||||
}
|
||||
scheme = "https"
|
||||
}
|
||||
return &Client{
|
||||
Adapter: boxService.NewAdapter(C.TypeManagerAPI, tag),
|
||||
ctx: ctx,
|
||||
logger: logger,
|
||||
options: options,
|
||||
httpClient: &http.Client{Transport: transport},
|
||||
baseURL: scheme + "://" + options.ServerOptions.Build().String() + "/manager/v1",
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Client) Start(stage adapter.StartStage) error { return nil }
|
||||
|
||||
func (s *Client) Close() error {
|
||||
s.httpClient.CloseIdleConnections()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Client) doJSON(method, path string, query url.Values, body any, out any) error {
|
||||
var reqBody io.Reader
|
||||
if body != nil {
|
||||
b, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
reqBody = bytes.NewReader(b)
|
||||
}
|
||||
u := s.baseURL + path
|
||||
if len(query) > 0 {
|
||||
u += "?" + query.Encode()
|
||||
}
|
||||
req, err := http.NewRequestWithContext(s.ctx, method, u, reqBody)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if body != nil {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
if s.options.APIKey != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+s.options.APIKey)
|
||||
}
|
||||
resp, err := s.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode == http.StatusNotFound {
|
||||
return CM.ErrNotFound
|
||||
}
|
||||
if resp.StatusCode >= 400 {
|
||||
msg, _ := io.ReadAll(resp.Body)
|
||||
return E.New("manager-api http ", resp.StatusCode, ": ", string(msg))
|
||||
}
|
||||
if out != nil {
|
||||
return json.NewDecoder(resp.Body).Decode(out)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func filtersQuery(filters map[string][]string) url.Values {
|
||||
q := url.Values{}
|
||||
for k, vs := range filters {
|
||||
for _, v := range vs {
|
||||
q.Add(k, v)
|
||||
}
|
||||
}
|
||||
return q
|
||||
}
|
||||
|
||||
func intPath(id int) string { return "/" + strconv.Itoa(id) }
|
||||
func stringPath(id string) string { return "/" + id }
|
||||
233
service/manager_api/http/client/manager.go
Normal file
233
service/manager_api/http/client/manager.go
Normal file
@@ -0,0 +1,233 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
CM "github.com/sagernet/sing-box/service/manager/constant"
|
||||
)
|
||||
|
||||
var _ CM.Manager = (*Client)(nil)
|
||||
|
||||
type countReply struct {
|
||||
Count int `json:"count"`
|
||||
}
|
||||
|
||||
func (s *Client) CreateSquad(in CM.SquadCreate) (CM.Squad, error) {
|
||||
return postItem[CM.Squad](s, "/squads", in)
|
||||
}
|
||||
|
||||
func (s *Client) GetSquads(f map[string][]string) ([]CM.Squad, error) {
|
||||
return getList[CM.Squad](s, "/squads", f)
|
||||
}
|
||||
|
||||
func (s *Client) GetSquadsCount(f map[string][]string) (int, error) {
|
||||
return getCount(s, "/squads", f)
|
||||
}
|
||||
|
||||
func (s *Client) GetSquad(id int) (CM.Squad, error) {
|
||||
return getItem[CM.Squad](s, "/squads"+intPath(id))
|
||||
}
|
||||
|
||||
func (s *Client) UpdateSquad(id int, in CM.SquadUpdate) (CM.Squad, error) {
|
||||
return putItem[CM.Squad](s, "/squads"+intPath(id), in)
|
||||
}
|
||||
|
||||
func (s *Client) DeleteSquad(id int) (CM.Squad, error) {
|
||||
return deleteItem[CM.Squad](s, "/squads"+intPath(id))
|
||||
}
|
||||
|
||||
func (s *Client) CreateNode(in CM.NodeCreate) (CM.Node, error) {
|
||||
return postItem[CM.Node](s, "/nodes", in)
|
||||
}
|
||||
|
||||
func (s *Client) GetNodes(f map[string][]string) ([]CM.Node, error) {
|
||||
return getList[CM.Node](s, "/nodes", f)
|
||||
}
|
||||
|
||||
func (s *Client) GetNodesCount(f map[string][]string) (int, error) {
|
||||
return getCount(s, "/nodes", f)
|
||||
}
|
||||
|
||||
func (s *Client) GetNode(uuid string) (CM.Node, error) {
|
||||
return getItem[CM.Node](s, "/nodes"+stringPath(uuid))
|
||||
}
|
||||
|
||||
func (s *Client) GetNodeStatus(uuid string) (string, error) {
|
||||
var reply struct {
|
||||
Status string `json:"status"`
|
||||
}
|
||||
if err := s.doJSON(http.MethodGet, "/nodes"+stringPath(uuid)+"/status", nil, nil, &reply); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return reply.Status, nil
|
||||
}
|
||||
|
||||
func (s *Client) UpdateNode(uuid string, in CM.NodeUpdate) (CM.Node, error) {
|
||||
return putItem[CM.Node](s, "/nodes"+stringPath(uuid), in)
|
||||
}
|
||||
|
||||
func (s *Client) DeleteNode(uuid string) (CM.Node, error) {
|
||||
return deleteItem[CM.Node](s, "/nodes"+stringPath(uuid))
|
||||
}
|
||||
|
||||
func (s *Client) CreateUser(in CM.UserCreate) (CM.User, error) {
|
||||
return postItem[CM.User](s, "/users", in)
|
||||
}
|
||||
|
||||
func (s *Client) GetUsers(f map[string][]string) ([]CM.User, error) {
|
||||
return getList[CM.User](s, "/users", f)
|
||||
}
|
||||
|
||||
func (s *Client) GetUsersCount(f map[string][]string) (int, error) {
|
||||
return getCount(s, "/users", f)
|
||||
}
|
||||
|
||||
func (s *Client) GetUser(id int) (CM.User, error) {
|
||||
return getItem[CM.User](s, "/users"+intPath(id))
|
||||
}
|
||||
|
||||
func (s *Client) UpdateUser(id int, in CM.UserUpdate) (CM.User, error) {
|
||||
return putItem[CM.User](s, "/users"+intPath(id), in)
|
||||
}
|
||||
|
||||
func (s *Client) DeleteUser(id int) (CM.User, error) {
|
||||
return deleteItem[CM.User](s, "/users"+intPath(id))
|
||||
}
|
||||
|
||||
func (s *Client) CreateBandwidthLimiter(in CM.BandwidthLimiterCreate) (CM.BandwidthLimiter, error) {
|
||||
return postItem[CM.BandwidthLimiter](s, "/bandwidth-limiters", in)
|
||||
}
|
||||
|
||||
func (s *Client) GetBandwidthLimiters(f map[string][]string) ([]CM.BandwidthLimiter, error) {
|
||||
return getList[CM.BandwidthLimiter](s, "/bandwidth-limiters", f)
|
||||
}
|
||||
|
||||
func (s *Client) GetBandwidthLimitersCount(f map[string][]string) (int, error) {
|
||||
return getCount(s, "/bandwidth-limiters", f)
|
||||
}
|
||||
|
||||
func (s *Client) GetBandwidthLimiter(id int) (CM.BandwidthLimiter, error) {
|
||||
return getItem[CM.BandwidthLimiter](s, "/bandwidth-limiters"+intPath(id))
|
||||
}
|
||||
|
||||
func (s *Client) UpdateBandwidthLimiter(id int, in CM.BandwidthLimiterUpdate) (CM.BandwidthLimiter, error) {
|
||||
return putItem[CM.BandwidthLimiter](s, "/bandwidth-limiters"+intPath(id), in)
|
||||
}
|
||||
|
||||
func (s *Client) DeleteBandwidthLimiter(id int) (CM.BandwidthLimiter, error) {
|
||||
return deleteItem[CM.BandwidthLimiter](s, "/bandwidth-limiters"+intPath(id))
|
||||
}
|
||||
|
||||
func (s *Client) CreateTrafficLimiter(in CM.TrafficLimiterCreate) (CM.TrafficLimiter, error) {
|
||||
return postItem[CM.TrafficLimiter](s, "/traffic-limiters", in)
|
||||
}
|
||||
|
||||
func (s *Client) GetTrafficLimiters(f map[string][]string) ([]CM.TrafficLimiter, error) {
|
||||
return getList[CM.TrafficLimiter](s, "/traffic-limiters", f)
|
||||
}
|
||||
|
||||
func (s *Client) GetTrafficLimitersCount(f map[string][]string) (int, error) {
|
||||
return getCount(s, "/traffic-limiters", f)
|
||||
}
|
||||
|
||||
func (s *Client) GetTrafficLimiter(id int) (CM.TrafficLimiter, error) {
|
||||
return getItem[CM.TrafficLimiter](s, "/traffic-limiters"+intPath(id))
|
||||
}
|
||||
|
||||
func (s *Client) UpdateTrafficLimiter(id int, in CM.TrafficLimiterUpdate) (CM.TrafficLimiter, error) {
|
||||
return putItem[CM.TrafficLimiter](s, "/traffic-limiters"+intPath(id), in)
|
||||
}
|
||||
|
||||
func (s *Client) UpdateTrafficLimiterUsed(id int, used uint64) (CM.TrafficLimiter, error) {
|
||||
return putItem[CM.TrafficLimiter](s, "/traffic-limiters"+intPath(id)+"/used", struct {
|
||||
Used uint64 `json:"used"`
|
||||
}{Used: used})
|
||||
}
|
||||
|
||||
func (s *Client) DeleteTrafficLimiter(id int) (CM.TrafficLimiter, error) {
|
||||
return deleteItem[CM.TrafficLimiter](s, "/traffic-limiters"+intPath(id))
|
||||
}
|
||||
|
||||
func (s *Client) CreateConnectionLimiter(in CM.ConnectionLimiterCreate) (CM.ConnectionLimiter, error) {
|
||||
return postItem[CM.ConnectionLimiter](s, "/connection-limiters", in)
|
||||
}
|
||||
|
||||
func (s *Client) GetConnectionLimiters(f map[string][]string) ([]CM.ConnectionLimiter, error) {
|
||||
return getList[CM.ConnectionLimiter](s, "/connection-limiters", f)
|
||||
}
|
||||
|
||||
func (s *Client) GetConnectionLimitersCount(f map[string][]string) (int, error) {
|
||||
return getCount(s, "/connection-limiters", f)
|
||||
}
|
||||
|
||||
func (s *Client) GetConnectionLimiter(id int) (CM.ConnectionLimiter, error) {
|
||||
return getItem[CM.ConnectionLimiter](s, "/connection-limiters"+intPath(id))
|
||||
}
|
||||
|
||||
func (s *Client) UpdateConnectionLimiter(id int, in CM.ConnectionLimiterUpdate) (CM.ConnectionLimiter, error) {
|
||||
return putItem[CM.ConnectionLimiter](s, "/connection-limiters"+intPath(id), in)
|
||||
}
|
||||
|
||||
func (s *Client) DeleteConnectionLimiter(id int) (CM.ConnectionLimiter, error) {
|
||||
return deleteItem[CM.ConnectionLimiter](s, "/connection-limiters"+intPath(id))
|
||||
}
|
||||
|
||||
func (s *Client) CreateRateLimiter(in CM.RateLimiterCreate) (CM.RateLimiter, error) {
|
||||
return postItem[CM.RateLimiter](s, "/rate-limiters", in)
|
||||
}
|
||||
|
||||
func (s *Client) GetRateLimiters(f map[string][]string) ([]CM.RateLimiter, error) {
|
||||
return getList[CM.RateLimiter](s, "/rate-limiters", f)
|
||||
}
|
||||
|
||||
func (s *Client) GetRateLimitersCount(f map[string][]string) (int, error) {
|
||||
return getCount(s, "/rate-limiters", f)
|
||||
}
|
||||
|
||||
func (s *Client) GetRateLimiter(id int) (CM.RateLimiter, error) {
|
||||
return getItem[CM.RateLimiter](s, "/rate-limiters"+intPath(id))
|
||||
}
|
||||
|
||||
func (s *Client) UpdateRateLimiter(id int, in CM.RateLimiterUpdate) (CM.RateLimiter, error) {
|
||||
return putItem[CM.RateLimiter](s, "/rate-limiters"+intPath(id), in)
|
||||
}
|
||||
|
||||
func (s *Client) DeleteRateLimiter(id int) (CM.RateLimiter, error) {
|
||||
return deleteItem[CM.RateLimiter](s, "/rate-limiters"+intPath(id))
|
||||
}
|
||||
|
||||
func getList[T any](c *Client, path string, filters map[string][]string) ([]T, error) {
|
||||
var out []T
|
||||
err := c.doJSON(http.MethodGet, path, filtersQuery(filters), nil, &out)
|
||||
return out, err
|
||||
}
|
||||
|
||||
func getCount(c *Client, path string, filters map[string][]string) (int, error) {
|
||||
var out countReply
|
||||
err := c.doJSON(http.MethodGet, path+"/count", filtersQuery(filters), nil, &out)
|
||||
return out.Count, err
|
||||
}
|
||||
|
||||
func getItem[T any](c *Client, path string) (T, error) {
|
||||
var out T
|
||||
err := c.doJSON(http.MethodGet, path, nil, nil, &out)
|
||||
return out, err
|
||||
}
|
||||
|
||||
func postItem[T any, In any](c *Client, path string, in In) (T, error) {
|
||||
var out T
|
||||
err := c.doJSON(http.MethodPost, path, nil, in, &out)
|
||||
return out, err
|
||||
}
|
||||
|
||||
func putItem[T any, In any](c *Client, path string, in In) (T, error) {
|
||||
var out T
|
||||
err := c.doJSON(http.MethodPut, path, nil, in, &out)
|
||||
return out, err
|
||||
}
|
||||
|
||||
func deleteItem[T any](c *Client, path string) (T, error) {
|
||||
var out T
|
||||
err := c.doJSON(http.MethodDelete, path, nil, nil, &out)
|
||||
return out, err
|
||||
}
|
||||
872
service/manager_api/http/server/openapi.yaml
Normal file
872
service/manager_api/http/server/openapi.yaml
Normal file
@@ -0,0 +1,872 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: Manager API
|
||||
version: 1.0.0
|
||||
servers:
|
||||
- url: /manager/v1
|
||||
security:
|
||||
- BearerAuth: []
|
||||
tags:
|
||||
- name: Squads
|
||||
- name: Nodes
|
||||
- name: Users
|
||||
- name: BandwidthLimiters
|
||||
- name: TrafficLimiters
|
||||
- name: ConnectionLimiters
|
||||
- name: RateLimiters
|
||||
- name: Info
|
||||
paths:
|
||||
/version:
|
||||
get:
|
||||
tags: [Info]
|
||||
summary: Server version
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required: [version]
|
||||
properties:
|
||||
version: {type: string, example: "1.13.11-abc1234"}
|
||||
/squads:
|
||||
get:
|
||||
tags: [Squads]
|
||||
summary: List squads
|
||||
parameters:
|
||||
- {in: query, name: id, schema: {type: integer, format: int32}}
|
||||
- {$ref: "#/components/parameters/FilterIDIn"}
|
||||
- {in: query, name: name, schema: {type: string}}
|
||||
- {$ref: "#/components/parameters/FilterCreatedAtStart"}
|
||||
- {$ref: "#/components/parameters/FilterCreatedAtEnd"}
|
||||
- {$ref: "#/components/parameters/FilterUpdatedAtStart"}
|
||||
- {$ref: "#/components/parameters/FilterUpdatedAtEnd"}
|
||||
- {$ref: "#/components/parameters/FilterSortAsc"}
|
||||
- {$ref: "#/components/parameters/FilterSortDesc"}
|
||||
- {$ref: "#/components/parameters/FilterOffset"}
|
||||
- {$ref: "#/components/parameters/FilterLimit"}
|
||||
responses:
|
||||
"200": {content: {application/json: {schema: {type: array, items: {$ref: "#/components/schemas/Squad"}}}}}
|
||||
"500": {$ref: "#/components/responses/InternalError"}
|
||||
post:
|
||||
tags: [Squads]
|
||||
summary: Create squad
|
||||
requestBody: {required: true, content: {application/json: {schema: {$ref: "#/components/schemas/SquadCreate"}}}}
|
||||
responses:
|
||||
"201": {content: {application/json: {schema: {$ref: "#/components/schemas/Squad"}}}}
|
||||
"400": {$ref: "#/components/responses/BadRequest"}
|
||||
/squads/count:
|
||||
get:
|
||||
tags: [Squads]
|
||||
summary: Count squads
|
||||
parameters:
|
||||
- {in: query, name: id, schema: {type: integer, format: int32}}
|
||||
- {$ref: "#/components/parameters/FilterIDIn"}
|
||||
- {in: query, name: name, schema: {type: string}}
|
||||
- {$ref: "#/components/parameters/FilterCreatedAtStart"}
|
||||
- {$ref: "#/components/parameters/FilterCreatedAtEnd"}
|
||||
- {$ref: "#/components/parameters/FilterUpdatedAtStart"}
|
||||
- {$ref: "#/components/parameters/FilterUpdatedAtEnd"}
|
||||
- {$ref: "#/components/parameters/FilterSortAsc"}
|
||||
- {$ref: "#/components/parameters/FilterSortDesc"}
|
||||
- {$ref: "#/components/parameters/FilterOffset"}
|
||||
- {$ref: "#/components/parameters/FilterLimit"}
|
||||
responses:
|
||||
"200": {$ref: "#/components/responses/Count"}
|
||||
"500": {$ref: "#/components/responses/InternalError"}
|
||||
/squads/{id}:
|
||||
parameters: [{$ref: "#/components/parameters/IntID"}]
|
||||
get:
|
||||
tags: [Squads]
|
||||
summary: Get squad
|
||||
responses:
|
||||
"200": {content: {application/json: {schema: {$ref: "#/components/schemas/Squad"}}}}
|
||||
"400": {$ref: "#/components/responses/BadRequest"}
|
||||
"404": {$ref: "#/components/responses/NotFound"}
|
||||
"500": {$ref: "#/components/responses/InternalError"}
|
||||
put:
|
||||
tags: [Squads]
|
||||
summary: Update squad
|
||||
requestBody: {required: true, content: {application/json: {schema: {$ref: "#/components/schemas/SquadUpdate"}}}}
|
||||
responses:
|
||||
"200": {content: {application/json: {schema: {$ref: "#/components/schemas/Squad"}}}}
|
||||
"400": {$ref: "#/components/responses/BadRequest"}
|
||||
"404": {$ref: "#/components/responses/NotFound"}
|
||||
delete:
|
||||
tags: [Squads]
|
||||
summary: Delete squad
|
||||
responses:
|
||||
"200": {content: {application/json: {schema: {$ref: "#/components/schemas/Squad"}}}}
|
||||
"404": {$ref: "#/components/responses/NotFound"}
|
||||
"500": {$ref: "#/components/responses/InternalError"}
|
||||
|
||||
/nodes:
|
||||
get:
|
||||
tags: [Nodes]
|
||||
summary: List nodes
|
||||
parameters:
|
||||
- {in: query, name: uuid, schema: {type: string, format: uuid}}
|
||||
- {in: query, name: name, schema: {type: string}}
|
||||
- {$ref: "#/components/parameters/FilterSquadIdIn"}
|
||||
- {$ref: "#/components/parameters/FilterCreatedAtStart"}
|
||||
- {$ref: "#/components/parameters/FilterCreatedAtEnd"}
|
||||
- {$ref: "#/components/parameters/FilterUpdatedAtStart"}
|
||||
- {$ref: "#/components/parameters/FilterUpdatedAtEnd"}
|
||||
- {$ref: "#/components/parameters/FilterSortAsc"}
|
||||
- {$ref: "#/components/parameters/FilterSortDesc"}
|
||||
- {$ref: "#/components/parameters/FilterOffset"}
|
||||
- {$ref: "#/components/parameters/FilterLimit"}
|
||||
responses:
|
||||
"200": {content: {application/json: {schema: {type: array, items: {$ref: "#/components/schemas/Node"}}}}}
|
||||
"500": {$ref: "#/components/responses/InternalError"}
|
||||
post:
|
||||
tags: [Nodes]
|
||||
summary: Create node
|
||||
requestBody: {required: true, content: {application/json: {schema: {$ref: "#/components/schemas/NodeCreate"}}}}
|
||||
responses:
|
||||
"201": {content: {application/json: {schema: {$ref: "#/components/schemas/Node"}}}}
|
||||
"400": {$ref: "#/components/responses/BadRequest"}
|
||||
/nodes/count:
|
||||
get:
|
||||
tags: [Nodes]
|
||||
summary: Count nodes
|
||||
parameters:
|
||||
- {in: query, name: uuid, schema: {type: string, format: uuid}}
|
||||
- {in: query, name: name, schema: {type: string}}
|
||||
- {$ref: "#/components/parameters/FilterSquadIdIn"}
|
||||
- {$ref: "#/components/parameters/FilterCreatedAtStart"}
|
||||
- {$ref: "#/components/parameters/FilterCreatedAtEnd"}
|
||||
- {$ref: "#/components/parameters/FilterUpdatedAtStart"}
|
||||
- {$ref: "#/components/parameters/FilterUpdatedAtEnd"}
|
||||
- {$ref: "#/components/parameters/FilterSortAsc"}
|
||||
- {$ref: "#/components/parameters/FilterSortDesc"}
|
||||
- {$ref: "#/components/parameters/FilterOffset"}
|
||||
- {$ref: "#/components/parameters/FilterLimit"}
|
||||
responses:
|
||||
"200": {$ref: "#/components/responses/Count"}
|
||||
"500": {$ref: "#/components/responses/InternalError"}
|
||||
/nodes/{uuid}:
|
||||
parameters: [{$ref: "#/components/parameters/NodeUUID"}]
|
||||
get:
|
||||
tags: [Nodes]
|
||||
summary: Get node
|
||||
responses:
|
||||
"200": {content: {application/json: {schema: {$ref: "#/components/schemas/Node"}}}}
|
||||
"404": {$ref: "#/components/responses/NotFound"}
|
||||
"500": {$ref: "#/components/responses/InternalError"}
|
||||
put:
|
||||
tags: [Nodes]
|
||||
summary: Update node
|
||||
requestBody: {required: true, content: {application/json: {schema: {$ref: "#/components/schemas/NodeUpdate"}}}}
|
||||
responses:
|
||||
"200": {content: {application/json: {schema: {$ref: "#/components/schemas/Node"}}}}
|
||||
"400": {$ref: "#/components/responses/BadRequest"}
|
||||
"404": {$ref: "#/components/responses/NotFound"}
|
||||
delete:
|
||||
tags: [Nodes]
|
||||
summary: Delete node
|
||||
responses:
|
||||
"200": {content: {application/json: {schema: {$ref: "#/components/schemas/Node"}}}}
|
||||
"404": {$ref: "#/components/responses/NotFound"}
|
||||
"500": {$ref: "#/components/responses/InternalError"}
|
||||
/nodes/{uuid}/status:
|
||||
parameters: [{$ref: "#/components/parameters/NodeUUID"}]
|
||||
get:
|
||||
tags: [Nodes]
|
||||
summary: Get node status
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required: [status]
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
enum: [online, offline]
|
||||
|
||||
/users:
|
||||
get:
|
||||
tags: [Users]
|
||||
summary: List users
|
||||
parameters:
|
||||
- {in: query, name: id, schema: {type: integer, format: int32}}
|
||||
- {in: query, name: username, schema: {type: string}}
|
||||
- {in: query, name: inbound, schema: {type: string}}
|
||||
- {in: query, name: type, schema: {type: string, enum: [hysteria, hysteria2, mtproxy, trojan, tuic, vless, vmess]}}
|
||||
- {$ref: "#/components/parameters/FilterSquadIdIn"}
|
||||
- {$ref: "#/components/parameters/FilterCreatedAtStart"}
|
||||
- {$ref: "#/components/parameters/FilterCreatedAtEnd"}
|
||||
- {$ref: "#/components/parameters/FilterUpdatedAtStart"}
|
||||
- {$ref: "#/components/parameters/FilterUpdatedAtEnd"}
|
||||
- {$ref: "#/components/parameters/FilterSortAsc"}
|
||||
- {$ref: "#/components/parameters/FilterSortDesc"}
|
||||
- {$ref: "#/components/parameters/FilterOffset"}
|
||||
- {$ref: "#/components/parameters/FilterLimit"}
|
||||
responses:
|
||||
"200": {content: {application/json: {schema: {type: array, items: {$ref: "#/components/schemas/User"}}}}}
|
||||
"500": {$ref: "#/components/responses/InternalError"}
|
||||
post:
|
||||
tags: [Users]
|
||||
summary: Create user
|
||||
requestBody: {required: true, content: {application/json: {schema: {$ref: "#/components/schemas/UserCreate"}}}}
|
||||
responses:
|
||||
"201": {content: {application/json: {schema: {$ref: "#/components/schemas/User"}}}}
|
||||
"400": {$ref: "#/components/responses/BadRequest"}
|
||||
/users/count:
|
||||
get:
|
||||
tags: [Users]
|
||||
summary: Count users
|
||||
parameters:
|
||||
- {in: query, name: id, schema: {type: integer, format: int32}}
|
||||
- {in: query, name: username, schema: {type: string}}
|
||||
- {in: query, name: inbound, schema: {type: string}}
|
||||
- {in: query, name: type, schema: {type: string, enum: [hysteria, hysteria2, mtproxy, trojan, tuic, vless, vmess]}}
|
||||
- {$ref: "#/components/parameters/FilterSquadIdIn"}
|
||||
- {$ref: "#/components/parameters/FilterCreatedAtStart"}
|
||||
- {$ref: "#/components/parameters/FilterCreatedAtEnd"}
|
||||
- {$ref: "#/components/parameters/FilterUpdatedAtStart"}
|
||||
- {$ref: "#/components/parameters/FilterUpdatedAtEnd"}
|
||||
- {$ref: "#/components/parameters/FilterSortAsc"}
|
||||
- {$ref: "#/components/parameters/FilterSortDesc"}
|
||||
- {$ref: "#/components/parameters/FilterOffset"}
|
||||
- {$ref: "#/components/parameters/FilterLimit"}
|
||||
responses:
|
||||
"200": {$ref: "#/components/responses/Count"}
|
||||
"500": {$ref: "#/components/responses/InternalError"}
|
||||
/users/{id}:
|
||||
parameters: [{$ref: "#/components/parameters/IntID"}]
|
||||
get:
|
||||
tags: [Users]
|
||||
summary: Get user
|
||||
responses:
|
||||
"200": {content: {application/json: {schema: {$ref: "#/components/schemas/User"}}}}
|
||||
"404": {$ref: "#/components/responses/NotFound"}
|
||||
"500": {$ref: "#/components/responses/InternalError"}
|
||||
put:
|
||||
tags: [Users]
|
||||
summary: Update user
|
||||
requestBody: {required: true, content: {application/json: {schema: {$ref: "#/components/schemas/UserUpdate"}}}}
|
||||
responses:
|
||||
"200": {content: {application/json: {schema: {$ref: "#/components/schemas/User"}}}}
|
||||
"400": {$ref: "#/components/responses/BadRequest"}
|
||||
"404": {$ref: "#/components/responses/NotFound"}
|
||||
delete:
|
||||
tags: [Users]
|
||||
summary: Delete user
|
||||
responses:
|
||||
"200": {content: {application/json: {schema: {$ref: "#/components/schemas/User"}}}}
|
||||
"404": {$ref: "#/components/responses/NotFound"}
|
||||
"500": {$ref: "#/components/responses/InternalError"}
|
||||
|
||||
/bandwidth-limiters:
|
||||
get:
|
||||
tags: [BandwidthLimiters]
|
||||
summary: List bandwidth limiters
|
||||
parameters:
|
||||
- {in: query, name: id, schema: {type: integer, format: int32}}
|
||||
- {in: query, name: strategy, schema: {type: string, enum: [global, connection]}}
|
||||
- {in: query, name: mode, schema: {type: string, enum: [upload, download, bidirectional]}}
|
||||
- {in: query, name: type, schema: {type: string}}
|
||||
- {in: query, name: username, schema: {type: string}}
|
||||
- {in: query, name: down_start, schema: {type: string}}
|
||||
- {in: query, name: down_end, schema: {type: string}}
|
||||
- {in: query, name: up_start, schema: {type: string}}
|
||||
- {in: query, name: up_end, schema: {type: string}}
|
||||
- {$ref: "#/components/parameters/FilterSquadIdIn"}
|
||||
- {$ref: "#/components/parameters/FilterCreatedAtStart"}
|
||||
- {$ref: "#/components/parameters/FilterCreatedAtEnd"}
|
||||
- {$ref: "#/components/parameters/FilterUpdatedAtStart"}
|
||||
- {$ref: "#/components/parameters/FilterUpdatedAtEnd"}
|
||||
- {$ref: "#/components/parameters/FilterSortAsc"}
|
||||
- {$ref: "#/components/parameters/FilterSortDesc"}
|
||||
- {$ref: "#/components/parameters/FilterOffset"}
|
||||
- {$ref: "#/components/parameters/FilterLimit"}
|
||||
responses:
|
||||
"200": {content: {application/json: {schema: {type: array, items: {$ref: "#/components/schemas/BandwidthLimiter"}}}}}
|
||||
"500": {$ref: "#/components/responses/InternalError"}
|
||||
post:
|
||||
tags: [BandwidthLimiters]
|
||||
summary: Create bandwidth limiter
|
||||
requestBody: {required: true, content: {application/json: {schema: {$ref: "#/components/schemas/BandwidthLimiterCreate"}}}}
|
||||
responses:
|
||||
"201": {content: {application/json: {schema: {$ref: "#/components/schemas/BandwidthLimiter"}}}}
|
||||
"400": {$ref: "#/components/responses/BadRequest"}
|
||||
/bandwidth-limiters/count:
|
||||
get:
|
||||
tags: [BandwidthLimiters]
|
||||
summary: Count bandwidth limiters
|
||||
parameters:
|
||||
- {in: query, name: id, schema: {type: integer, format: int32}}
|
||||
- {in: query, name: strategy, schema: {type: string, enum: [global, connection]}}
|
||||
- {in: query, name: mode, schema: {type: string, enum: [upload, download, bidirectional]}}
|
||||
- {in: query, name: type, schema: {type: string}}
|
||||
- {in: query, name: username, schema: {type: string}}
|
||||
- {in: query, name: down_start, schema: {type: string}}
|
||||
- {in: query, name: down_end, schema: {type: string}}
|
||||
- {in: query, name: up_start, schema: {type: string}}
|
||||
- {in: query, name: up_end, schema: {type: string}}
|
||||
- {$ref: "#/components/parameters/FilterSquadIdIn"}
|
||||
- {$ref: "#/components/parameters/FilterCreatedAtStart"}
|
||||
- {$ref: "#/components/parameters/FilterCreatedAtEnd"}
|
||||
- {$ref: "#/components/parameters/FilterUpdatedAtStart"}
|
||||
- {$ref: "#/components/parameters/FilterUpdatedAtEnd"}
|
||||
- {$ref: "#/components/parameters/FilterSortAsc"}
|
||||
- {$ref: "#/components/parameters/FilterSortDesc"}
|
||||
- {$ref: "#/components/parameters/FilterOffset"}
|
||||
- {$ref: "#/components/parameters/FilterLimit"}
|
||||
responses:
|
||||
"200": {$ref: "#/components/responses/Count"}
|
||||
"500": {$ref: "#/components/responses/InternalError"}
|
||||
/bandwidth-limiters/{id}:
|
||||
parameters: [{$ref: "#/components/parameters/IntID"}]
|
||||
get:
|
||||
tags: [BandwidthLimiters]
|
||||
summary: Get bandwidth limiter
|
||||
responses:
|
||||
"200": {content: {application/json: {schema: {$ref: "#/components/schemas/BandwidthLimiter"}}}}
|
||||
"404": {$ref: "#/components/responses/NotFound"}
|
||||
"500": {$ref: "#/components/responses/InternalError"}
|
||||
put:
|
||||
tags: [BandwidthLimiters]
|
||||
summary: Update bandwidth limiter
|
||||
requestBody: {required: true, content: {application/json: {schema: {$ref: "#/components/schemas/BandwidthLimiterUpdate"}}}}
|
||||
responses:
|
||||
"200": {content: {application/json: {schema: {$ref: "#/components/schemas/BandwidthLimiter"}}}}
|
||||
"400": {$ref: "#/components/responses/BadRequest"}
|
||||
"404": {$ref: "#/components/responses/NotFound"}
|
||||
delete:
|
||||
tags: [BandwidthLimiters]
|
||||
summary: Delete bandwidth limiter
|
||||
responses:
|
||||
"200": {content: {application/json: {schema: {$ref: "#/components/schemas/BandwidthLimiter"}}}}
|
||||
"404": {$ref: "#/components/responses/NotFound"}
|
||||
|
||||
/traffic-limiters:
|
||||
get:
|
||||
tags: [TrafficLimiters]
|
||||
summary: List traffic limiters
|
||||
parameters:
|
||||
- {in: query, name: id, schema: {type: integer, format: int32}}
|
||||
- {in: query, name: username, schema: {type: string}}
|
||||
- {in: query, name: outbound, schema: {type: string}}
|
||||
- {in: query, name: mode, schema: {type: string, enum: [upload, download, bidirectional]}}
|
||||
- {in: query, name: used_start, schema: {type: string}}
|
||||
- {in: query, name: used_end, schema: {type: string}}
|
||||
- {in: query, name: quota_start, schema: {type: string}}
|
||||
- {in: query, name: quota_end, schema: {type: string}}
|
||||
- {$ref: "#/components/parameters/FilterSquadIdIn"}
|
||||
- {$ref: "#/components/parameters/FilterCreatedAtStart"}
|
||||
- {$ref: "#/components/parameters/FilterCreatedAtEnd"}
|
||||
- {$ref: "#/components/parameters/FilterUpdatedAtStart"}
|
||||
- {$ref: "#/components/parameters/FilterUpdatedAtEnd"}
|
||||
- {$ref: "#/components/parameters/FilterSortAsc"}
|
||||
- {$ref: "#/components/parameters/FilterSortDesc"}
|
||||
- {$ref: "#/components/parameters/FilterOffset"}
|
||||
- {$ref: "#/components/parameters/FilterLimit"}
|
||||
responses:
|
||||
"200": {content: {application/json: {schema: {type: array, items: {$ref: "#/components/schemas/TrafficLimiter"}}}}}
|
||||
"500": {$ref: "#/components/responses/InternalError"}
|
||||
post:
|
||||
tags: [TrafficLimiters]
|
||||
summary: Create traffic limiter
|
||||
requestBody: {required: true, content: {application/json: {schema: {$ref: "#/components/schemas/TrafficLimiterCreate"}}}}
|
||||
responses:
|
||||
"201": {content: {application/json: {schema: {$ref: "#/components/schemas/TrafficLimiter"}}}}
|
||||
"400": {$ref: "#/components/responses/BadRequest"}
|
||||
/traffic-limiters/count:
|
||||
get:
|
||||
tags: [TrafficLimiters]
|
||||
summary: Count traffic limiters
|
||||
parameters:
|
||||
- {in: query, name: id, schema: {type: integer, format: int32}}
|
||||
- {in: query, name: username, schema: {type: string}}
|
||||
- {in: query, name: outbound, schema: {type: string}}
|
||||
- {in: query, name: mode, schema: {type: string, enum: [upload, download, bidirectional]}}
|
||||
- {in: query, name: used_start, schema: {type: string}}
|
||||
- {in: query, name: used_end, schema: {type: string}}
|
||||
- {in: query, name: quota_start, schema: {type: string}}
|
||||
- {in: query, name: quota_end, schema: {type: string}}
|
||||
- {$ref: "#/components/parameters/FilterSquadIdIn"}
|
||||
- {$ref: "#/components/parameters/FilterCreatedAtStart"}
|
||||
- {$ref: "#/components/parameters/FilterCreatedAtEnd"}
|
||||
- {$ref: "#/components/parameters/FilterUpdatedAtStart"}
|
||||
- {$ref: "#/components/parameters/FilterUpdatedAtEnd"}
|
||||
- {$ref: "#/components/parameters/FilterSortAsc"}
|
||||
- {$ref: "#/components/parameters/FilterSortDesc"}
|
||||
- {$ref: "#/components/parameters/FilterOffset"}
|
||||
- {$ref: "#/components/parameters/FilterLimit"}
|
||||
responses:
|
||||
"200": {$ref: "#/components/responses/Count"}
|
||||
"500": {$ref: "#/components/responses/InternalError"}
|
||||
/traffic-limiters/{id}:
|
||||
parameters: [{$ref: "#/components/parameters/IntID"}]
|
||||
get:
|
||||
tags: [TrafficLimiters]
|
||||
summary: Get traffic limiter
|
||||
responses:
|
||||
"200": {content: {application/json: {schema: {$ref: "#/components/schemas/TrafficLimiter"}}}}
|
||||
"404": {$ref: "#/components/responses/NotFound"}
|
||||
"500": {$ref: "#/components/responses/InternalError"}
|
||||
put:
|
||||
tags: [TrafficLimiters]
|
||||
summary: Update traffic limiter
|
||||
requestBody: {required: true, content: {application/json: {schema: {$ref: "#/components/schemas/TrafficLimiterUpdate"}}}}
|
||||
responses:
|
||||
"200": {content: {application/json: {schema: {$ref: "#/components/schemas/TrafficLimiter"}}}}
|
||||
"400": {$ref: "#/components/responses/BadRequest"}
|
||||
"404": {$ref: "#/components/responses/NotFound"}
|
||||
delete:
|
||||
tags: [TrafficLimiters]
|
||||
summary: Delete traffic limiter
|
||||
responses:
|
||||
"200": {content: {application/json: {schema: {$ref: "#/components/schemas/TrafficLimiter"}}}}
|
||||
"404": {$ref: "#/components/responses/NotFound"}
|
||||
|
||||
/connection-limiters:
|
||||
get:
|
||||
tags: [ConnectionLimiters]
|
||||
summary: List connection limiters
|
||||
parameters:
|
||||
- {in: query, name: id, schema: {type: integer, format: int32}}
|
||||
- {in: query, name: strategy, schema: {type: string, enum: [connection]}}
|
||||
- {in: query, name: username, schema: {type: string}}
|
||||
- {in: query, name: outbound, schema: {type: string}}
|
||||
- {in: query, name: connection_type, schema: {type: string, enum: [default, hwid, mux, ip]}}
|
||||
- {in: query, name: lock_type, schema: {type: string, enum: [manager, default]}}
|
||||
- {$ref: "#/components/parameters/FilterSquadIdIn"}
|
||||
- {$ref: "#/components/parameters/FilterCreatedAtStart"}
|
||||
- {$ref: "#/components/parameters/FilterCreatedAtEnd"}
|
||||
- {$ref: "#/components/parameters/FilterUpdatedAtStart"}
|
||||
- {$ref: "#/components/parameters/FilterUpdatedAtEnd"}
|
||||
- {$ref: "#/components/parameters/FilterSortAsc"}
|
||||
- {$ref: "#/components/parameters/FilterSortDesc"}
|
||||
- {$ref: "#/components/parameters/FilterOffset"}
|
||||
- {$ref: "#/components/parameters/FilterLimit"}
|
||||
responses:
|
||||
"200": {content: {application/json: {schema: {type: array, items: {$ref: "#/components/schemas/ConnectionLimiter"}}}}}
|
||||
"500": {$ref: "#/components/responses/InternalError"}
|
||||
post:
|
||||
tags: [ConnectionLimiters]
|
||||
summary: Create connection limiter
|
||||
requestBody: {required: true, content: {application/json: {schema: {$ref: "#/components/schemas/ConnectionLimiterCreate"}}}}
|
||||
responses:
|
||||
"201": {content: {application/json: {schema: {$ref: "#/components/schemas/ConnectionLimiter"}}}}
|
||||
"400": {$ref: "#/components/responses/BadRequest"}
|
||||
/connection-limiters/count:
|
||||
get:
|
||||
tags: [ConnectionLimiters]
|
||||
summary: Count connection limiters
|
||||
parameters:
|
||||
- {in: query, name: id, schema: {type: integer, format: int32}}
|
||||
- {in: query, name: strategy, schema: {type: string, enum: [connection]}}
|
||||
- {in: query, name: username, schema: {type: string}}
|
||||
- {in: query, name: outbound, schema: {type: string}}
|
||||
- {in: query, name: connection_type, schema: {type: string, enum: [default, hwid, mux, ip]}}
|
||||
- {in: query, name: lock_type, schema: {type: string, enum: [manager, default]}}
|
||||
- {$ref: "#/components/parameters/FilterSquadIdIn"}
|
||||
- {$ref: "#/components/parameters/FilterCreatedAtStart"}
|
||||
- {$ref: "#/components/parameters/FilterCreatedAtEnd"}
|
||||
- {$ref: "#/components/parameters/FilterUpdatedAtStart"}
|
||||
- {$ref: "#/components/parameters/FilterUpdatedAtEnd"}
|
||||
- {$ref: "#/components/parameters/FilterSortAsc"}
|
||||
- {$ref: "#/components/parameters/FilterSortDesc"}
|
||||
- {$ref: "#/components/parameters/FilterOffset"}
|
||||
- {$ref: "#/components/parameters/FilterLimit"}
|
||||
responses:
|
||||
"200": {$ref: "#/components/responses/Count"}
|
||||
"500": {$ref: "#/components/responses/InternalError"}
|
||||
/connection-limiters/{id}:
|
||||
parameters: [{$ref: "#/components/parameters/IntID"}]
|
||||
get:
|
||||
tags: [ConnectionLimiters]
|
||||
summary: Get connection limiter
|
||||
responses:
|
||||
"200": {content: {application/json: {schema: {$ref: "#/components/schemas/ConnectionLimiter"}}}}
|
||||
"404": {$ref: "#/components/responses/NotFound"}
|
||||
"500": {$ref: "#/components/responses/InternalError"}
|
||||
put:
|
||||
tags: [ConnectionLimiters]
|
||||
summary: Update connection limiter
|
||||
requestBody: {required: true, content: {application/json: {schema: {$ref: "#/components/schemas/ConnectionLimiterUpdate"}}}}
|
||||
responses:
|
||||
"200": {content: {application/json: {schema: {$ref: "#/components/schemas/ConnectionLimiter"}}}}
|
||||
"400": {$ref: "#/components/responses/BadRequest"}
|
||||
"404": {$ref: "#/components/responses/NotFound"}
|
||||
delete:
|
||||
tags: [ConnectionLimiters]
|
||||
summary: Delete connection limiter
|
||||
responses:
|
||||
"200": {content: {application/json: {schema: {$ref: "#/components/schemas/ConnectionLimiter"}}}}
|
||||
"404": {$ref: "#/components/responses/NotFound"}
|
||||
|
||||
/rate-limiters:
|
||||
get:
|
||||
tags: [RateLimiters]
|
||||
summary: List rate limiters
|
||||
parameters:
|
||||
- {in: query, name: id, schema: {type: integer, format: int32}}
|
||||
- {in: query, name: strategy, schema: {type: string, enum: [fixed_window, sliding_window, token_bucket, leaky_bucket]}}
|
||||
- {in: query, name: username, schema: {type: string}}
|
||||
- {in: query, name: outbound, schema: {type: string}}
|
||||
- {in: query, name: connection_type, schema: {type: string, enum: [hwid, mux, ip, default]}}
|
||||
- {in: query, name: interval, schema: {type: string}}
|
||||
- {in: query, name: count_start, schema: {type: integer, format: int64}}
|
||||
- {in: query, name: count_end, schema: {type: integer, format: int64}}
|
||||
- {$ref: "#/components/parameters/FilterSquadIdIn"}
|
||||
- {$ref: "#/components/parameters/FilterCreatedAtStart"}
|
||||
- {$ref: "#/components/parameters/FilterCreatedAtEnd"}
|
||||
- {$ref: "#/components/parameters/FilterUpdatedAtStart"}
|
||||
- {$ref: "#/components/parameters/FilterUpdatedAtEnd"}
|
||||
- {$ref: "#/components/parameters/FilterSortAsc"}
|
||||
- {$ref: "#/components/parameters/FilterSortDesc"}
|
||||
- {$ref: "#/components/parameters/FilterOffset"}
|
||||
- {$ref: "#/components/parameters/FilterLimit"}
|
||||
responses:
|
||||
"200": {content: {application/json: {schema: {type: array, items: {$ref: "#/components/schemas/RateLimiter"}}}}}
|
||||
"500": {$ref: "#/components/responses/InternalError"}
|
||||
post:
|
||||
tags: [RateLimiters]
|
||||
summary: Create rate limiter
|
||||
requestBody: {required: true, content: {application/json: {schema: {$ref: "#/components/schemas/RateLimiterCreate"}}}}
|
||||
responses:
|
||||
"201": {content: {application/json: {schema: {$ref: "#/components/schemas/RateLimiter"}}}}
|
||||
"400": {$ref: "#/components/responses/BadRequest"}
|
||||
/rate-limiters/count:
|
||||
get:
|
||||
tags: [RateLimiters]
|
||||
summary: Count rate limiters
|
||||
parameters:
|
||||
- {in: query, name: id, schema: {type: integer, format: int32}}
|
||||
- {in: query, name: strategy, schema: {type: string, enum: [fixed_window, sliding_window, token_bucket, leaky_bucket]}}
|
||||
- {in: query, name: username, schema: {type: string}}
|
||||
- {in: query, name: outbound, schema: {type: string}}
|
||||
- {in: query, name: connection_type, schema: {type: string, enum: [hwid, mux, ip, default]}}
|
||||
- {in: query, name: interval, schema: {type: string}}
|
||||
- {in: query, name: count_start, schema: {type: integer, format: int64}}
|
||||
- {in: query, name: count_end, schema: {type: integer, format: int64}}
|
||||
- {$ref: "#/components/parameters/FilterSquadIdIn"}
|
||||
- {$ref: "#/components/parameters/FilterCreatedAtStart"}
|
||||
- {$ref: "#/components/parameters/FilterCreatedAtEnd"}
|
||||
- {$ref: "#/components/parameters/FilterUpdatedAtStart"}
|
||||
- {$ref: "#/components/parameters/FilterUpdatedAtEnd"}
|
||||
- {$ref: "#/components/parameters/FilterSortAsc"}
|
||||
- {$ref: "#/components/parameters/FilterSortDesc"}
|
||||
- {$ref: "#/components/parameters/FilterOffset"}
|
||||
- {$ref: "#/components/parameters/FilterLimit"}
|
||||
responses:
|
||||
"200": {$ref: "#/components/responses/Count"}
|
||||
"500": {$ref: "#/components/responses/InternalError"}
|
||||
/rate-limiters/{id}:
|
||||
parameters: [{$ref: "#/components/parameters/IntID"}]
|
||||
get:
|
||||
tags: [RateLimiters]
|
||||
summary: Get rate limiter
|
||||
responses:
|
||||
"200": {content: {application/json: {schema: {$ref: "#/components/schemas/RateLimiter"}}}}
|
||||
"404": {$ref: "#/components/responses/NotFound"}
|
||||
"500": {$ref: "#/components/responses/InternalError"}
|
||||
put:
|
||||
tags: [RateLimiters]
|
||||
summary: Update rate limiter
|
||||
requestBody: {required: true, content: {application/json: {schema: {$ref: "#/components/schemas/RateLimiterUpdate"}}}}
|
||||
responses:
|
||||
"200": {content: {application/json: {schema: {$ref: "#/components/schemas/RateLimiter"}}}}
|
||||
"400": {$ref: "#/components/responses/BadRequest"}
|
||||
"404": {$ref: "#/components/responses/NotFound"}
|
||||
delete:
|
||||
tags: [RateLimiters]
|
||||
summary: Delete rate limiter
|
||||
responses:
|
||||
"200": {content: {application/json: {schema: {$ref: "#/components/schemas/RateLimiter"}}}}
|
||||
"404": {$ref: "#/components/responses/NotFound"}
|
||||
|
||||
components:
|
||||
securitySchemes:
|
||||
BearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
parameters:
|
||||
FilterCreatedAtStart: {in: query, name: created_at_start, schema: {type: string, format: date-time}}
|
||||
FilterCreatedAtEnd: {in: query, name: created_at_end, schema: {type: string, format: date-time}}
|
||||
FilterUpdatedAtStart: {in: query, name: updated_at_start, schema: {type: string, format: date-time}}
|
||||
FilterUpdatedAtEnd: {in: query, name: updated_at_end, schema: {type: string, format: date-time}}
|
||||
FilterSortAsc: {in: query, name: sort_asc, schema: {type: string}}
|
||||
FilterSortDesc: {in: query, name: sort_desc, schema: {type: string}}
|
||||
FilterOffset: {in: query, name: offset, schema: {type: integer, format: int64, minimum: 0}}
|
||||
FilterLimit: {in: query, name: limit, schema: {type: integer, format: int64, minimum: 1}}
|
||||
FilterSquadIdIn:
|
||||
in: query
|
||||
name: squad_id_in
|
||||
schema: {type: array, items: {type: integer, format: int32}}
|
||||
style: form
|
||||
explode: true
|
||||
FilterIDIn:
|
||||
in: query
|
||||
name: id_in
|
||||
schema: {type: array, items: {type: integer, format: int32}}
|
||||
style: form
|
||||
explode: true
|
||||
IntID:
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
schema: {type: integer, format: int32, example: 1}
|
||||
NodeUUID:
|
||||
in: path
|
||||
name: uuid
|
||||
required: true
|
||||
schema: {type: string, format: uuid, example: "a3b8c9d0-4e2f-4a1b-8c3d-9e7f6a5b4c3d"}
|
||||
responses:
|
||||
BadRequest:
|
||||
content:
|
||||
text/plain:
|
||||
schema: {type: string}
|
||||
NotFound:
|
||||
InternalError:
|
||||
content:
|
||||
text/plain:
|
||||
schema: {type: string}
|
||||
Count:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required: [count]
|
||||
properties:
|
||||
count: {type: integer, format: int64, example: 42}
|
||||
schemas:
|
||||
SquadIDs:
|
||||
type: array
|
||||
minItems: 1
|
||||
items: {type: integer, format: int32}
|
||||
example: [1]
|
||||
|
||||
Squad:
|
||||
type: object
|
||||
required: [id, name, created_at, updated_at]
|
||||
properties:
|
||||
id: {type: integer, format: int32}
|
||||
name: {type: string, example: "default"}
|
||||
created_at: {type: string, format: date-time}
|
||||
updated_at: {type: string, format: date-time}
|
||||
SquadCreate:
|
||||
type: object
|
||||
required: [name]
|
||||
properties:
|
||||
name: {type: string, example: "default"}
|
||||
SquadUpdate:
|
||||
type: object
|
||||
required: [name]
|
||||
properties:
|
||||
name: {type: string, example: "default-renamed"}
|
||||
|
||||
Node:
|
||||
type: object
|
||||
required: [uuid, name, squad_ids, created_at, updated_at]
|
||||
properties:
|
||||
uuid: {type: string, format: uuid}
|
||||
name: {type: string, example: "node-1"}
|
||||
squad_ids: {$ref: "#/components/schemas/SquadIDs"}
|
||||
created_at: {type: string, format: date-time}
|
||||
updated_at: {type: string, format: date-time}
|
||||
NodeCreate:
|
||||
type: object
|
||||
required: [uuid, name, squad_ids]
|
||||
properties:
|
||||
uuid: {type: string, format: uuid}
|
||||
name: {type: string, example: "node-1"}
|
||||
squad_ids: {$ref: "#/components/schemas/SquadIDs"}
|
||||
NodeUpdate:
|
||||
type: object
|
||||
required: [name]
|
||||
properties:
|
||||
name: {type: string, example: "node-1-renamed"}
|
||||
|
||||
User:
|
||||
type: object
|
||||
required: [id, squad_ids, username, inbound, type, uuid, password, secret, flow, alter_id, created_at, updated_at]
|
||||
properties:
|
||||
id: {type: integer, format: int32}
|
||||
squad_ids: {$ref: "#/components/schemas/SquadIDs"}
|
||||
username: {type: string, example: "alice"}
|
||||
inbound: {type: string, example: "vless-in"}
|
||||
type: {type: string, enum: [hysteria, hysteria2, mtproxy, trojan, tuic, vless, vmess]}
|
||||
uuid: {type: string}
|
||||
password: {type: string}
|
||||
secret: {type: string}
|
||||
flow: {type: string}
|
||||
alter_id: {type: integer, format: int32}
|
||||
created_at: {type: string, format: date-time}
|
||||
updated_at: {type: string, format: date-time}
|
||||
UserCreate:
|
||||
type: object
|
||||
required: [squad_ids, username, inbound, type]
|
||||
properties:
|
||||
squad_ids: {$ref: "#/components/schemas/SquadIDs"}
|
||||
username: {type: string, example: "alice"}
|
||||
inbound: {type: string, example: "vless-in"}
|
||||
type:
|
||||
type: string
|
||||
enum: [hysteria, hysteria2, mtproxy, trojan, tuic, vless, vmess]
|
||||
uuid: {type: string, format: uuid}
|
||||
password: {type: string}
|
||||
secret: {type: string}
|
||||
flow: {type: string}
|
||||
alter_id: {type: integer, format: int32}
|
||||
UserUpdate:
|
||||
type: object
|
||||
properties:
|
||||
uuid: {type: string, format: uuid}
|
||||
password: {type: string}
|
||||
secret: {type: string}
|
||||
flow: {type: string}
|
||||
alter_id: {type: integer, format: int32}
|
||||
|
||||
BandwidthLimiter:
|
||||
type: object
|
||||
required: [id, squad_ids, outbound, strategy, mode, speed, raw_speed, created_at, updated_at]
|
||||
properties:
|
||||
id: {type: integer, format: int32}
|
||||
squad_ids: {$ref: "#/components/schemas/SquadIDs"}
|
||||
username: {type: string}
|
||||
outbound: {type: string, example: "direct"}
|
||||
strategy: {type: string, enum: [global, connection]}
|
||||
connection_type: {type: string, enum: [default, hwid, mux, ip]}
|
||||
mode: {type: string, enum: [upload, download, bidirectional]}
|
||||
flow_keys: {type: array, items: {type: string, enum: [user, destination, ip, hwid, mux]}}
|
||||
speed: {type: string, example: "10mbit"}
|
||||
raw_speed: {type: integer, format: int64}
|
||||
created_at: {type: string, format: date-time}
|
||||
updated_at: {type: string, format: date-time}
|
||||
BandwidthLimiterCreate:
|
||||
type: object
|
||||
required: [squad_ids, outbound, strategy, mode, speed]
|
||||
properties:
|
||||
squad_ids: {$ref: "#/components/schemas/SquadIDs"}
|
||||
username: {type: string}
|
||||
outbound: {type: string, example: "direct"}
|
||||
strategy: {type: string, enum: [global, connection]}
|
||||
connection_type: {type: string, enum: [default, hwid, mux, ip]}
|
||||
mode: {type: string, enum: [upload, download, bidirectional]}
|
||||
flow_keys: {type: array, items: {type: string, enum: [user, destination, ip, hwid, mux]}}
|
||||
speed: {type: string, example: "10mbit"}
|
||||
BandwidthLimiterUpdate:
|
||||
type: object
|
||||
required: [outbound, strategy, mode, speed]
|
||||
properties:
|
||||
username: {type: string}
|
||||
outbound: {type: string}
|
||||
strategy: {type: string, enum: [global, connection]}
|
||||
connection_type: {type: string, enum: [default, hwid, mux, ip]}
|
||||
mode: {type: string, enum: [upload, download, bidirectional]}
|
||||
flow_keys: {type: array, items: {type: string, enum: [user, destination, ip, hwid, mux]}}
|
||||
speed: {type: string}
|
||||
|
||||
TrafficLimiter:
|
||||
type: object
|
||||
required: [id, squad_ids, outbound, strategy, mode, raw_used, quota, raw_quota, created_at, updated_at]
|
||||
properties:
|
||||
id: {type: integer, format: int32}
|
||||
squad_ids: {$ref: "#/components/schemas/SquadIDs"}
|
||||
username: {type: string}
|
||||
outbound: {type: string, example: "direct"}
|
||||
strategy: {type: string, enum: [global, bypass]}
|
||||
mode: {type: string, enum: [upload, download, bidirectional]}
|
||||
raw_used: {type: integer, format: int64}
|
||||
quota: {type: string, example: "10gb"}
|
||||
raw_quota: {type: integer, format: int64}
|
||||
created_at: {type: string, format: date-time}
|
||||
updated_at: {type: string, format: date-time}
|
||||
TrafficLimiterCreate:
|
||||
type: object
|
||||
required: [squad_ids, outbound, strategy, mode, quota]
|
||||
properties:
|
||||
squad_ids: {$ref: "#/components/schemas/SquadIDs"}
|
||||
username: {type: string}
|
||||
outbound: {type: string, example: "direct"}
|
||||
strategy: {type: string, enum: [global, bypass]}
|
||||
mode: {type: string, enum: [upload, download, bidirectional]}
|
||||
quota: {type: string, example: "10gb"}
|
||||
TrafficLimiterUpdate:
|
||||
type: object
|
||||
required: [outbound, strategy, mode, quota]
|
||||
properties:
|
||||
username: {type: string}
|
||||
outbound: {type: string}
|
||||
strategy: {type: string, enum: [global, bypass]}
|
||||
mode: {type: string, enum: [upload, download, bidirectional]}
|
||||
quota: {type: string}
|
||||
|
||||
ConnectionLimiter:
|
||||
type: object
|
||||
required: [id, squad_ids, outbound, strategy, lock_type, count, created_at, updated_at]
|
||||
properties:
|
||||
id: {type: integer, format: int32}
|
||||
squad_ids: {$ref: "#/components/schemas/SquadIDs"}
|
||||
username: {type: string}
|
||||
outbound: {type: string, example: "direct"}
|
||||
strategy: {type: string, enum: [connection]}
|
||||
connection_type: {type: string, enum: [default, hwid, mux, ip]}
|
||||
lock_type: {type: string, enum: [manager, default]}
|
||||
count: {type: integer, format: int64}
|
||||
created_at: {type: string, format: date-time}
|
||||
updated_at: {type: string, format: date-time}
|
||||
ConnectionLimiterCreate:
|
||||
type: object
|
||||
required: [squad_ids, outbound, strategy, lock_type, count]
|
||||
properties:
|
||||
squad_ids: {$ref: "#/components/schemas/SquadIDs"}
|
||||
username: {type: string}
|
||||
outbound: {type: string, example: "direct"}
|
||||
strategy: {type: string, enum: [connection]}
|
||||
connection_type: {type: string, enum: [default, hwid, mux, ip]}
|
||||
lock_type: {type: string, enum: [manager, default]}
|
||||
count: {type: integer, format: int64}
|
||||
ConnectionLimiterUpdate:
|
||||
type: object
|
||||
required: [outbound, strategy, lock_type, count]
|
||||
properties:
|
||||
username: {type: string}
|
||||
outbound: {type: string}
|
||||
strategy: {type: string, enum: [connection]}
|
||||
connection_type: {type: string, enum: [default, hwid, mux, ip]}
|
||||
lock_type: {type: string, enum: [manager, default]}
|
||||
count: {type: integer, format: int64}
|
||||
|
||||
RateLimiter:
|
||||
type: object
|
||||
required: [id, squad_ids, outbound, strategy, connection_type, count, interval, created_at, updated_at]
|
||||
properties:
|
||||
id: {type: integer, format: int32}
|
||||
squad_ids: {$ref: "#/components/schemas/SquadIDs"}
|
||||
username: {type: string}
|
||||
outbound: {type: string, example: "direct"}
|
||||
strategy: {type: string, enum: [fixed_window, sliding_window, token_bucket, leaky_bucket]}
|
||||
connection_type: {type: string, enum: [hwid, mux, ip, default]}
|
||||
count: {type: integer, format: int64}
|
||||
interval: {type: string, example: "1s"}
|
||||
created_at: {type: string, format: date-time}
|
||||
updated_at: {type: string, format: date-time}
|
||||
RateLimiterCreate:
|
||||
type: object
|
||||
required: [squad_ids, outbound, strategy, connection_type, count, interval]
|
||||
properties:
|
||||
squad_ids: {$ref: "#/components/schemas/SquadIDs"}
|
||||
username: {type: string}
|
||||
outbound: {type: string, example: "direct"}
|
||||
strategy: {type: string, enum: [fixed_window, sliding_window, token_bucket, leaky_bucket]}
|
||||
connection_type: {type: string, enum: [hwid, mux, ip, default]}
|
||||
count: {type: integer, format: int64}
|
||||
interval: {type: string, example: "1s"}
|
||||
RateLimiterUpdate:
|
||||
type: object
|
||||
required: [outbound, strategy, connection_type, count, interval]
|
||||
properties:
|
||||
username: {type: string}
|
||||
outbound: {type: string}
|
||||
strategy: {type: string, enum: [fixed_window, sliding_window, token_bucket, leaky_bucket]}
|
||||
connection_type: {type: string, enum: [hwid, mux, ip, default]}
|
||||
count: {type: integer, format: int64}
|
||||
interval: {type: string}
|
||||
530
service/manager_api/http/server/server.go
Normal file
530
service/manager_api/http/server/server.go
Normal file
@@ -0,0 +1,530 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/subtle"
|
||||
_ "embed"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
boxService "github.com/sagernet/sing-box/adapter/service"
|
||||
"github.com/sagernet/sing-box/common/listener"
|
||||
"github.com/sagernet/sing-box/common/tls"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing-box/service/manager/constant"
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
aTLS "github.com/sagernet/sing/common/tls"
|
||||
sHTTP "github.com/sagernet/sing/protocol/http"
|
||||
"github.com/sagernet/sing/service"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/render"
|
||||
"golang.org/x/net/http2"
|
||||
)
|
||||
|
||||
type APIServer struct {
|
||||
boxService.Adapter
|
||||
|
||||
ctx context.Context
|
||||
logger log.ContextLogger
|
||||
listener *listener.Listener
|
||||
tlsConfig tls.ServerConfig
|
||||
httpServer *http.Server
|
||||
manager constant.Manager
|
||||
options option.ManagerAPIServerOptions
|
||||
}
|
||||
|
||||
func NewAPIServer(ctx context.Context, logger log.ContextLogger, tag string, options option.ManagerAPIServerOptions) (*APIServer, error) {
|
||||
if options.APIKey == "" {
|
||||
return nil, E.New("missing api key")
|
||||
}
|
||||
return &APIServer{
|
||||
Adapter: boxService.NewAdapter(C.TypeManagerAPI, tag),
|
||||
ctx: ctx,
|
||||
logger: logger,
|
||||
listener: listener.New(listener.Options{
|
||||
Context: ctx,
|
||||
Logger: logger,
|
||||
Network: []string{N.NetworkTCP},
|
||||
Listen: options.ListenOptions,
|
||||
}),
|
||||
options: options,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *APIServer) Start(stage adapter.StartStage) error {
|
||||
if stage != adapter.StartStateStart {
|
||||
return nil
|
||||
}
|
||||
boxManager := service.FromContext[adapter.ServiceManager](s.ctx)
|
||||
managerService, ok := boxManager.Get(s.options.Manager)
|
||||
if !ok {
|
||||
return E.New("manager ", s.options.Manager, " not found")
|
||||
}
|
||||
s.manager, ok = managerService.(constant.Manager)
|
||||
if !ok {
|
||||
return E.New("invalid ", s.options.Manager, " manager")
|
||||
}
|
||||
chiRouter := chi.NewRouter()
|
||||
s.Route(chiRouter)
|
||||
if s.options.TLS != nil {
|
||||
tlsConfig, err := tls.NewServer(s.ctx, s.logger, common.PtrValueOrDefault(s.options.TLS))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.tlsConfig = tlsConfig
|
||||
}
|
||||
if s.tlsConfig != nil {
|
||||
err := s.tlsConfig.Start()
|
||||
if err != nil {
|
||||
return E.Cause(err, "create TLS config")
|
||||
}
|
||||
}
|
||||
tcpListener, err := s.listener.ListenTCP()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if s.tlsConfig != nil {
|
||||
if !common.Contains(s.tlsConfig.NextProtos(), http2.NextProtoTLS) {
|
||||
s.tlsConfig.SetNextProtos(append([]string{"h2"}, s.tlsConfig.NextProtos()...))
|
||||
}
|
||||
tcpListener = aTLS.NewListener(tcpListener, s.tlsConfig)
|
||||
}
|
||||
s.httpServer = &http.Server{
|
||||
Handler: chiRouter,
|
||||
}
|
||||
go func() {
|
||||
err = s.httpServer.Serve(tcpListener)
|
||||
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
s.logger.Error("serve error: ", err)
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *APIServer) Close() error {
|
||||
return common.Close(
|
||||
common.PtrOrNil(s.httpServer),
|
||||
common.PtrOrNil(s.listener),
|
||||
s.tlsConfig,
|
||||
)
|
||||
}
|
||||
|
||||
func (s *APIServer) Route(r chi.Router) {
|
||||
r.Route("/manager/v1", func(r chi.Router) {
|
||||
r.Use(newCORSMiddleware(s.options.CORS))
|
||||
r.Use(func(handler http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
||||
s.logger.Debug(request.Method, " ", request.RequestURI, " ", sHTTP.SourceAddress(request))
|
||||
handler.ServeHTTP(writer, request)
|
||||
})
|
||||
})
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(s.requireAPIKey)
|
||||
r.Get("/version", func(w http.ResponseWriter, req *http.Request) {
|
||||
render.JSON(w, req, render.M{
|
||||
"version": C.Version,
|
||||
})
|
||||
})
|
||||
registerIntCRUD(r, "/squads",
|
||||
s.manager.GetSquads, s.manager.GetSquadsCount,
|
||||
s.manager.GetSquad, s.manager.CreateSquad,
|
||||
s.manager.UpdateSquad, s.manager.DeleteSquad)
|
||||
registerIntCRUD(r, "/users",
|
||||
s.manager.GetUsers, s.manager.GetUsersCount,
|
||||
s.manager.GetUser, s.manager.CreateUser,
|
||||
s.manager.UpdateUser, s.manager.DeleteUser)
|
||||
registerIntCRUD(r, "/bandwidth-limiters",
|
||||
s.manager.GetBandwidthLimiters, s.manager.GetBandwidthLimitersCount,
|
||||
s.manager.GetBandwidthLimiter, s.manager.CreateBandwidthLimiter,
|
||||
s.manager.UpdateBandwidthLimiter, s.manager.DeleteBandwidthLimiter)
|
||||
registerIntCRUD(r, "/traffic-limiters",
|
||||
s.manager.GetTrafficLimiters, s.manager.GetTrafficLimitersCount,
|
||||
s.manager.GetTrafficLimiter, s.manager.CreateTrafficLimiter,
|
||||
s.manager.UpdateTrafficLimiter, s.manager.DeleteTrafficLimiter)
|
||||
r.Put("/traffic-limiters/{id}/used", s.updateTrafficLimiterUsed)
|
||||
registerIntCRUD(r, "/connection-limiters",
|
||||
s.manager.GetConnectionLimiters, s.manager.GetConnectionLimitersCount,
|
||||
s.manager.GetConnectionLimiter, s.manager.CreateConnectionLimiter,
|
||||
s.manager.UpdateConnectionLimiter, s.manager.DeleteConnectionLimiter)
|
||||
registerIntCRUD(r, "/rate-limiters",
|
||||
s.manager.GetRateLimiters, s.manager.GetRateLimitersCount,
|
||||
s.manager.GetRateLimiter, s.manager.CreateRateLimiter,
|
||||
s.manager.UpdateRateLimiter, s.manager.DeleteRateLimiter)
|
||||
r.Route("/nodes", func(r chi.Router) {
|
||||
r.Get("/", listHandler(s.manager.GetNodes))
|
||||
r.Post("/", createHandler(s.manager.CreateNode))
|
||||
r.Get("/count", countHandler(s.manager.GetNodesCount))
|
||||
r.Get("/{uuid}", getByStringIDHandler("uuid", s.manager.GetNode))
|
||||
r.Put("/{uuid}", updateByStringIDHandler("uuid", s.manager.UpdateNode))
|
||||
r.Delete("/{uuid}", deleteByStringIDHandler("uuid", s.manager.DeleteNode))
|
||||
r.Get("/{uuid}/status", func(w http.ResponseWriter, req *http.Request) {
|
||||
status, err := s.manager.GetNodeStatus(chi.URLParam(req, "uuid"))
|
||||
if err != nil {
|
||||
writeError(w, req, err)
|
||||
return
|
||||
}
|
||||
render.JSON(w, req, render.M{"status": status})
|
||||
})
|
||||
})
|
||||
})
|
||||
r.Get("/swagger", func(w http.ResponseWriter, req *http.Request) {
|
||||
http.Redirect(w, req, req.URL.Path+"/", http.StatusMovedPermanently)
|
||||
})
|
||||
r.Get("/swagger/", s.swaggerUI)
|
||||
r.Get("/swagger/openapi.yaml", s.swaggerSpec)
|
||||
})
|
||||
}
|
||||
|
||||
// updateTrafficLimiterUsed overwrites the running raw_used counter
|
||||
// of a traffic limiter. Used by the admin panel "reset traffic" button
|
||||
// (which posts {"used": 0}); also fine for any operator who needs to
|
||||
// nudge the counter to a specific number.
|
||||
func (s *APIServer) updateTrafficLimiterUsed(w http.ResponseWriter, req *http.Request) {
|
||||
id, err := strconv.Atoi(chi.URLParam(req, "id"))
|
||||
if err != nil {
|
||||
writeBadRequest(w, req, err)
|
||||
return
|
||||
}
|
||||
var body struct {
|
||||
Used uint64 `json:"used"`
|
||||
}
|
||||
if err := render.DecodeJSON(req.Body, &body); err != nil {
|
||||
writeBadRequest(w, req, err)
|
||||
return
|
||||
}
|
||||
item, err := s.manager.UpdateTrafficLimiterUsed(id, body.Used)
|
||||
if err != nil {
|
||||
writeUpdateError(w, req, err)
|
||||
return
|
||||
}
|
||||
render.JSON(w, req, item)
|
||||
}
|
||||
|
||||
func (s *APIServer) requireAPIKey(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
||||
header := request.Header.Get("Authorization")
|
||||
if header == "" {
|
||||
writer.Header().Set("WWW-Authenticate", `Bearer realm="manager-api"`)
|
||||
render.Status(request, http.StatusUnauthorized)
|
||||
render.PlainText(writer, request, "missing api key")
|
||||
return
|
||||
}
|
||||
token := strings.TrimPrefix(header, "Bearer ")
|
||||
if token == header {
|
||||
writer.Header().Set("WWW-Authenticate", `Bearer realm="manager-api"`)
|
||||
render.Status(request, http.StatusUnauthorized)
|
||||
render.PlainText(writer, request, "invalid api key format")
|
||||
return
|
||||
}
|
||||
if subtle.ConstantTimeCompare([]byte(token), []byte(s.options.APIKey)) == 0 {
|
||||
render.Status(request, http.StatusUnauthorized)
|
||||
render.PlainText(writer, request, "invalid api key")
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(writer, request)
|
||||
})
|
||||
}
|
||||
|
||||
func newCORSMiddleware(cfg *option.ManagerAPICORSOptions) func(http.Handler) http.Handler {
|
||||
const (
|
||||
allowedMethods = "GET, POST, PUT, DELETE, OPTIONS"
|
||||
fallbackHeaders = "Authorization, Content-Type"
|
||||
)
|
||||
var (
|
||||
originSet map[string]struct{}
|
||||
allowAnyOrigin = true
|
||||
exposedHeaders string
|
||||
maxAge = "600"
|
||||
)
|
||||
if cfg != nil {
|
||||
hasWildcard := false
|
||||
filtered := make([]string, 0, len(cfg.AllowedOrigins))
|
||||
for _, o := range cfg.AllowedOrigins {
|
||||
if o == "*" {
|
||||
hasWildcard = true
|
||||
continue
|
||||
}
|
||||
if o == "" {
|
||||
continue
|
||||
}
|
||||
filtered = append(filtered, o)
|
||||
}
|
||||
if len(filtered) > 0 && !hasWildcard {
|
||||
originSet = make(map[string]struct{}, len(filtered))
|
||||
for _, o := range filtered {
|
||||
originSet[o] = struct{}{}
|
||||
}
|
||||
allowAnyOrigin = false
|
||||
}
|
||||
if len(cfg.ExposedHeaders) > 0 {
|
||||
exposedHeaders = strings.Join(cfg.ExposedHeaders, ", ")
|
||||
}
|
||||
if cfg.MaxAge > 0 {
|
||||
maxAge = strconv.Itoa(cfg.MaxAge)
|
||||
}
|
||||
}
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
origin := r.Header.Get("Origin")
|
||||
h := w.Header()
|
||||
h.Set("Vary", "Origin")
|
||||
emitOrigin := ""
|
||||
if !allowAnyOrigin {
|
||||
if _, ok := originSet[origin]; ok {
|
||||
emitOrigin = origin
|
||||
}
|
||||
} else if origin != "" {
|
||||
emitOrigin = origin
|
||||
} else {
|
||||
emitOrigin = "*"
|
||||
}
|
||||
if emitOrigin != "" {
|
||||
h.Set("Access-Control-Allow-Origin", emitOrigin)
|
||||
}
|
||||
h.Set("Access-Control-Allow-Methods", allowedMethods)
|
||||
if reqHeaders := r.Header.Get("Access-Control-Request-Headers"); reqHeaders != "" {
|
||||
h.Set("Access-Control-Allow-Headers", reqHeaders)
|
||||
} else {
|
||||
h.Set("Access-Control-Allow-Headers", fallbackHeaders)
|
||||
}
|
||||
if exposedHeaders != "" {
|
||||
h.Set("Access-Control-Expose-Headers", exposedHeaders)
|
||||
}
|
||||
h.Set("Access-Control-Max-Age", maxAge)
|
||||
if r.Method == http.MethodOptions {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (s *APIServer) swaggerUI(writer http.ResponseWriter, request *http.Request) {
|
||||
writer.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
_, _ = writer.Write([]byte(swaggerUIHTML))
|
||||
}
|
||||
|
||||
func (s *APIServer) swaggerSpec(writer http.ResponseWriter, _ *http.Request) {
|
||||
writer.Header().Set("Content-Type", "application/yaml")
|
||||
_, _ = writer.Write(openAPISpec)
|
||||
}
|
||||
|
||||
func registerIntCRUD[T any, CR any, UP any](
|
||||
r chi.Router, path string,
|
||||
list func(map[string][]string) ([]T, error),
|
||||
count func(map[string][]string) (int, error),
|
||||
get func(int) (T, error),
|
||||
create func(CR) (T, error),
|
||||
update func(int, UP) (T, error),
|
||||
del func(int) (T, error),
|
||||
) {
|
||||
r.Route(path, func(r chi.Router) {
|
||||
r.Get("/", listHandler(list))
|
||||
r.Post("/", createHandler(create))
|
||||
r.Get("/count", countHandler(count))
|
||||
r.Get("/{id}", getByIntIDHandler("id", get))
|
||||
r.Put("/{id}", updateByIntIDHandler("id", update))
|
||||
r.Delete("/{id}", deleteByIntIDHandler("id", del))
|
||||
})
|
||||
}
|
||||
|
||||
func listHandler[T any](fn func(map[string][]string) ([]T, error)) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, req *http.Request) {
|
||||
filters := parseListFilters(req.URL.Query())
|
||||
applyDefaultLimit(filters)
|
||||
items, err := fn(filters)
|
||||
if err != nil {
|
||||
writeError(w, req, err)
|
||||
return
|
||||
}
|
||||
if items == nil {
|
||||
items = []T{}
|
||||
}
|
||||
render.JSON(w, req, items)
|
||||
}
|
||||
}
|
||||
|
||||
func applyDefaultLimit(filters map[string][]string) {
|
||||
if _, ok := filters["limit"]; !ok {
|
||||
filters["limit"] = []string{"100"}
|
||||
}
|
||||
}
|
||||
|
||||
func countHandler(fn func(map[string][]string) (int, error)) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, req *http.Request) {
|
||||
count, err := fn(parseListFilters(req.URL.Query()))
|
||||
if err != nil {
|
||||
writeError(w, req, err)
|
||||
return
|
||||
}
|
||||
render.JSON(w, req, render.M{"count": count})
|
||||
}
|
||||
}
|
||||
|
||||
func parseListFilters(q url.Values) map[string][]string {
|
||||
out := make(map[string][]string, len(q))
|
||||
for k, vs := range q {
|
||||
if !strings.HasSuffix(k, "_in") {
|
||||
out[k] = vs
|
||||
continue
|
||||
}
|
||||
expanded := make([]string, 0, len(vs))
|
||||
for _, v := range vs {
|
||||
s := strings.TrimSpace(v)
|
||||
s = strings.TrimPrefix(s, "[")
|
||||
s = strings.TrimSuffix(s, "]")
|
||||
for _, p := range strings.Split(s, ",") {
|
||||
p = strings.TrimSpace(p)
|
||||
if p != "" {
|
||||
expanded = append(expanded, p)
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(expanded) == 0 {
|
||||
continue
|
||||
}
|
||||
out[k] = expanded
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func createHandler[T, CR any](fn func(CR) (T, error)) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, req *http.Request) {
|
||||
var body CR
|
||||
if err := render.DecodeJSON(req.Body, &body); err != nil {
|
||||
writeBadRequest(w, req, err)
|
||||
return
|
||||
}
|
||||
item, err := fn(body)
|
||||
if err != nil {
|
||||
writeBadRequest(w, req, err)
|
||||
return
|
||||
}
|
||||
render.Status(req, http.StatusCreated)
|
||||
render.JSON(w, req, item)
|
||||
}
|
||||
}
|
||||
|
||||
func getByIntIDHandler[T any](idKey string, fn func(int) (T, error)) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, req *http.Request) {
|
||||
id, err := strconv.Atoi(chi.URLParam(req, idKey))
|
||||
if err != nil {
|
||||
writeBadRequest(w, req, err)
|
||||
return
|
||||
}
|
||||
item, err := fn(id)
|
||||
if err != nil {
|
||||
writeError(w, req, err)
|
||||
return
|
||||
}
|
||||
render.JSON(w, req, item)
|
||||
}
|
||||
}
|
||||
|
||||
func getByStringIDHandler[T any](idKey string, fn func(string) (T, error)) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, req *http.Request) {
|
||||
item, err := fn(chi.URLParam(req, idKey))
|
||||
if err != nil {
|
||||
writeError(w, req, err)
|
||||
return
|
||||
}
|
||||
render.JSON(w, req, item)
|
||||
}
|
||||
}
|
||||
|
||||
func updateByIntIDHandler[T, UP any](idKey string, fn func(int, UP) (T, error)) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, req *http.Request) {
|
||||
id, err := strconv.Atoi(chi.URLParam(req, idKey))
|
||||
if err != nil {
|
||||
writeBadRequest(w, req, err)
|
||||
return
|
||||
}
|
||||
var body UP
|
||||
if err := render.DecodeJSON(req.Body, &body); err != nil {
|
||||
writeBadRequest(w, req, err)
|
||||
return
|
||||
}
|
||||
item, err := fn(id, body)
|
||||
if err != nil {
|
||||
writeUpdateError(w, req, err)
|
||||
return
|
||||
}
|
||||
render.JSON(w, req, item)
|
||||
}
|
||||
}
|
||||
|
||||
func updateByStringIDHandler[T, UP any](idKey string, fn func(string, UP) (T, error)) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, req *http.Request) {
|
||||
var body UP
|
||||
if err := render.DecodeJSON(req.Body, &body); err != nil {
|
||||
writeBadRequest(w, req, err)
|
||||
return
|
||||
}
|
||||
item, err := fn(chi.URLParam(req, idKey), body)
|
||||
if err != nil {
|
||||
writeUpdateError(w, req, err)
|
||||
return
|
||||
}
|
||||
render.JSON(w, req, item)
|
||||
}
|
||||
}
|
||||
|
||||
func deleteByIntIDHandler[T any](idKey string, fn func(int) (T, error)) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, req *http.Request) {
|
||||
id, err := strconv.Atoi(chi.URLParam(req, idKey))
|
||||
if err != nil {
|
||||
writeBadRequest(w, req, err)
|
||||
return
|
||||
}
|
||||
item, err := fn(id)
|
||||
if err != nil {
|
||||
writeError(w, req, err)
|
||||
return
|
||||
}
|
||||
render.JSON(w, req, item)
|
||||
}
|
||||
}
|
||||
|
||||
func deleteByStringIDHandler[T any](idKey string, fn func(string) (T, error)) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, req *http.Request) {
|
||||
item, err := fn(chi.URLParam(req, idKey))
|
||||
if err != nil {
|
||||
writeError(w, req, err)
|
||||
return
|
||||
}
|
||||
render.JSON(w, req, item)
|
||||
}
|
||||
}
|
||||
|
||||
func writeBadRequest(w http.ResponseWriter, req *http.Request, err error) {
|
||||
render.Status(req, http.StatusBadRequest)
|
||||
render.PlainText(w, req, err.Error())
|
||||
}
|
||||
|
||||
func writeError(w http.ResponseWriter, req *http.Request, err error) {
|
||||
if err == constant.ErrNotFound {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
render.Status(req, http.StatusInternalServerError)
|
||||
render.PlainText(w, req, err.Error())
|
||||
}
|
||||
|
||||
func writeUpdateError(w http.ResponseWriter, req *http.Request, err error) {
|
||||
if err == constant.ErrNotFound {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
render.Status(req, http.StatusBadRequest)
|
||||
render.PlainText(w, req, err.Error())
|
||||
}
|
||||
145
service/manager_api/http/server/server_test.go
Normal file
145
service/manager_api/http/server/server_test.go
Normal file
@@ -0,0 +1,145 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/sagernet/sing-box/option"
|
||||
)
|
||||
|
||||
func TestCORSMiddleware_Preflight(t *testing.T) {
|
||||
called := false
|
||||
h := newCORSMiddleware(nil)(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
|
||||
called = true
|
||||
}))
|
||||
|
||||
req := httptest.NewRequest(http.MethodOptions, "/manager/v1/squads", nil)
|
||||
req.Header.Set("Origin", "http://localhost:8081")
|
||||
req.Header.Set("Access-Control-Request-Method", "GET")
|
||||
req.Header.Set("Access-Control-Request-Headers", "Authorization, Content-Type")
|
||||
rec := httptest.NewRecorder()
|
||||
h.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusNoContent {
|
||||
t.Fatalf("preflight status = %d, want 204", rec.Code)
|
||||
}
|
||||
if called {
|
||||
t.Fatal("next handler should not run for OPTIONS preflight")
|
||||
}
|
||||
if got := rec.Header().Get("Access-Control-Allow-Origin"); got != "http://localhost:8081" {
|
||||
t.Fatalf("Access-Control-Allow-Origin = %q, want echoed origin", got)
|
||||
}
|
||||
if got := rec.Header().Get("Access-Control-Allow-Headers"); got != "Authorization, Content-Type" {
|
||||
t.Fatalf("Access-Control-Allow-Headers = %q, want echoed request headers", got)
|
||||
}
|
||||
if got := rec.Header().Get("Access-Control-Allow-Methods"); got == "" {
|
||||
t.Fatal("Access-Control-Allow-Methods should be set")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCORSMiddleware_PassesThroughGET(t *testing.T) {
|
||||
called := false
|
||||
h := newCORSMiddleware(nil)(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
called = true
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/manager/v1/squads/count", nil)
|
||||
req.Header.Set("Origin", "http://localhost:8081")
|
||||
req.Header.Set("Authorization", "Bearer test")
|
||||
rec := httptest.NewRecorder()
|
||||
h.ServeHTTP(rec, req)
|
||||
|
||||
if !called {
|
||||
t.Fatal("next handler should run for GET")
|
||||
}
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d, want 200", rec.Code)
|
||||
}
|
||||
if got := rec.Header().Get("Access-Control-Allow-Origin"); got != "http://localhost:8081" {
|
||||
t.Fatalf("Access-Control-Allow-Origin = %q, want echoed origin", got)
|
||||
}
|
||||
if got := rec.Header().Get("Vary"); got != "Origin" {
|
||||
t.Fatalf("Vary = %q, want Origin", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCORSMiddleware_NoOriginFallsBackToWildcard(t *testing.T) {
|
||||
h := newCORSMiddleware(nil)(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {}))
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/manager/v1/squads/count", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
h.ServeHTTP(rec, req)
|
||||
|
||||
if got := rec.Header().Get("Access-Control-Allow-Origin"); got != "*" {
|
||||
t.Fatalf("Access-Control-Allow-Origin = %q, want * for missing origin", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCORSMiddleware_AllowedOriginsAllowList(t *testing.T) {
|
||||
cfg := &option.ManagerAPICORSOptions{
|
||||
AllowedOrigins: []string{"https://panel.example.com"},
|
||||
}
|
||||
h := newCORSMiddleware(cfg)(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {}))
|
||||
|
||||
allowed := httptest.NewRequest(http.MethodGet, "/manager/v1/squads/count", nil)
|
||||
allowed.Header.Set("Origin", "https://panel.example.com")
|
||||
rec := httptest.NewRecorder()
|
||||
h.ServeHTTP(rec, allowed)
|
||||
if got := rec.Header().Get("Access-Control-Allow-Origin"); got != "https://panel.example.com" {
|
||||
t.Fatalf("allowed origin = %q, want exact echo", got)
|
||||
}
|
||||
|
||||
denied := httptest.NewRequest(http.MethodGet, "/manager/v1/squads/count", nil)
|
||||
denied.Header.Set("Origin", "https://attacker.example")
|
||||
rec = httptest.NewRecorder()
|
||||
h.ServeHTTP(rec, denied)
|
||||
if got := rec.Header().Get("Access-Control-Allow-Origin"); got != "" {
|
||||
t.Fatalf("denied origin = %q, want empty", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCORSMiddleware_StaticCredentialsHeader(t *testing.T) {
|
||||
h := newCORSMiddleware(nil)(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {}))
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/manager/v1/squads/count", nil)
|
||||
req.Header.Set("Origin", "https://panel.example.com")
|
||||
rec := httptest.NewRecorder()
|
||||
h.ServeHTTP(rec, req)
|
||||
|
||||
if got := rec.Header().Get("Access-Control-Allow-Credentials"); got != "" {
|
||||
t.Fatalf("Access-Control-Allow-Credentials = %q, want empty (credentials are statically disabled)", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCORSMiddleware_FullConfig(t *testing.T) {
|
||||
cfg := &option.ManagerAPICORSOptions{
|
||||
AllowedOrigins: []string{"*"},
|
||||
ExposedHeaders: []string{"X-Total-Count"},
|
||||
MaxAge: 120,
|
||||
}
|
||||
h := newCORSMiddleware(cfg)(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {}))
|
||||
|
||||
req := httptest.NewRequest(http.MethodOptions, "/manager/v1/squads", nil)
|
||||
req.Header.Set("Origin", "https://panel.example.com")
|
||||
req.Header.Set("Access-Control-Request-Headers", "Authorization, Content-Type")
|
||||
rec := httptest.NewRecorder()
|
||||
h.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusNoContent {
|
||||
t.Fatalf("preflight status = %d, want 204", rec.Code)
|
||||
}
|
||||
if got := rec.Header().Get("Access-Control-Allow-Methods"); got != "GET, POST, PUT, DELETE, OPTIONS" {
|
||||
t.Fatalf("Allow-Methods = %q, want static methods list", got)
|
||||
}
|
||||
if got := rec.Header().Get("Access-Control-Allow-Headers"); got != "Authorization, Content-Type" {
|
||||
t.Fatalf("Allow-Headers = %q, want echoed request headers", got)
|
||||
}
|
||||
if got := rec.Header().Get("Access-Control-Expose-Headers"); got != "X-Total-Count" {
|
||||
t.Fatalf("Expose-Headers = %q, want %q", got, "X-Total-Count")
|
||||
}
|
||||
if got := rec.Header().Get("Access-Control-Max-Age"); got != "120" {
|
||||
t.Fatalf("Max-Age = %q, want %q", got, "120")
|
||||
}
|
||||
}
|
||||
28
service/manager_api/http/server/swagger.go
Normal file
28
service/manager_api/http/server/swagger.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package server
|
||||
|
||||
import _ "embed"
|
||||
|
||||
//go:embed openapi.yaml
|
||||
var openAPISpec []byte
|
||||
|
||||
const swaggerUIHTML = `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Manager API - Swagger UI</title>
|
||||
<link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@5/swagger-ui.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="swagger-ui"></div>
|
||||
<script src="https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js"></script>
|
||||
<script>
|
||||
window.onload = () => {
|
||||
window.ui = SwaggerUIBundle({
|
||||
url: "./openapi.yaml",
|
||||
dom_id: "#swagger-ui",
|
||||
deepLinking: true,
|
||||
});
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>`
|
||||
51
service/manager_api/service.go
Normal file
51
service/manager_api/service.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package manager_api
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
boxService "github.com/sagernet/sing-box/adapter/service"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
grpcClient "github.com/sagernet/sing-box/service/manager_api/grpc/client"
|
||||
grpcServer "github.com/sagernet/sing-box/service/manager_api/grpc/server"
|
||||
httpClient "github.com/sagernet/sing-box/service/manager_api/http/client"
|
||||
httpServer "github.com/sagernet/sing-box/service/manager_api/http/server"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
)
|
||||
|
||||
func RegisterService(registry *boxService.Registry) {
|
||||
boxService.Register[option.ManagerAPIOptions](registry, C.TypeManagerAPI, NewService)
|
||||
}
|
||||
|
||||
func NewService(ctx context.Context, logger log.ContextLogger, tag string, options option.ManagerAPIOptions) (adapter.Service, error) {
|
||||
switch options.APIType {
|
||||
case C.ManagerAPIServer:
|
||||
switch options.ProtocolType {
|
||||
case C.ManagerAPIProtocolHTTP:
|
||||
return httpServer.NewAPIServer(ctx, logger, tag, options.ServerOptions)
|
||||
case C.ManagerAPIProtocolGrpc:
|
||||
return grpcServer.NewServer(ctx, logger, tag, options.ServerOptions)
|
||||
case "":
|
||||
return nil, E.New("missing protocol type")
|
||||
default:
|
||||
return nil, E.New("unknown protocol type: ", options.ProtocolType)
|
||||
}
|
||||
case C.ManagerAPIClient:
|
||||
switch options.ProtocolType {
|
||||
case C.ManagerAPIProtocolHTTP:
|
||||
return httpClient.NewClient(ctx, logger, tag, options.ClientOptions)
|
||||
case C.ManagerAPIProtocolGrpc:
|
||||
return grpcClient.NewClient(ctx, logger, tag, options.ClientOptions)
|
||||
case "":
|
||||
return nil, E.New("missing protocol type")
|
||||
default:
|
||||
return nil, E.New("unknown protocol type: ", options.ProtocolType)
|
||||
}
|
||||
case "":
|
||||
return nil, E.New("missing api type")
|
||||
default:
|
||||
return nil, E.New("unknown api type: ", options.APIType)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user