165 lines
4.7 KiB
Go
165 lines
4.7 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"log"
|
|
"time"
|
|
)
|
|
|
|
// XrayClientMeta holds metadata stored in PostgreSQL for an Xray client.
|
|
// Xray's own config only stores uuid/email/level; expiry and display name live here.
|
|
type XrayClientMeta struct {
|
|
UUID string
|
|
Name string
|
|
Email string
|
|
InboundTag string
|
|
ExpiresAt *time.Time
|
|
MaxConns int
|
|
CreatedAt time.Time
|
|
}
|
|
|
|
func (s *Store) EnsureXrayClientsSchema(ctx context.Context) error {
|
|
_, err := s.db.ExecContext(ctx, `
|
|
CREATE TABLE IF NOT EXISTS xray_clients (
|
|
uuid TEXT PRIMARY KEY,
|
|
name TEXT NOT NULL DEFAULT '',
|
|
email TEXT NOT NULL DEFAULT '',
|
|
inbound_tag TEXT NOT NULL DEFAULT '',
|
|
expires_at TIMESTAMPTZ,
|
|
max_conns INT NOT NULL DEFAULT 0,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
)`)
|
|
return err
|
|
}
|
|
|
|
func (s *Store) UpsertXrayClientMeta(ctx context.Context, m XrayClientMeta) error {
|
|
var expiresAt interface{}
|
|
if m.ExpiresAt != nil {
|
|
expiresAt = *m.ExpiresAt
|
|
}
|
|
_, err := s.db.ExecContext(ctx, `
|
|
INSERT INTO xray_clients (uuid, name, email, inbound_tag, expires_at, max_conns)
|
|
VALUES ($1, $2, $3, $4, $5, $6)
|
|
ON CONFLICT (uuid) DO UPDATE SET
|
|
name = EXCLUDED.name,
|
|
email = EXCLUDED.email,
|
|
inbound_tag = EXCLUDED.inbound_tag,
|
|
expires_at = EXCLUDED.expires_at,
|
|
max_conns = EXCLUDED.max_conns`,
|
|
m.UUID, m.Name, m.Email, m.InboundTag, expiresAt, m.MaxConns)
|
|
return err
|
|
}
|
|
|
|
func (s *Store) GetXrayClientMeta(ctx context.Context, uuid string) (*XrayClientMeta, error) {
|
|
m := &XrayClientMeta{}
|
|
var expiresAt sql.NullTime
|
|
err := s.db.QueryRowContext(ctx, `
|
|
SELECT uuid, name, email, inbound_tag, expires_at, max_conns, created_at
|
|
FROM xray_clients WHERE uuid = $1`, uuid).
|
|
Scan(&m.UUID, &m.Name, &m.Email, &m.InboundTag, &expiresAt, &m.MaxConns, &m.CreatedAt)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if expiresAt.Valid {
|
|
m.ExpiresAt = &expiresAt.Time
|
|
}
|
|
return m, nil
|
|
}
|
|
|
|
func (s *Store) DeleteXrayClientMeta(ctx context.Context, uuid string) error {
|
|
_, err := s.db.ExecContext(ctx, `DELETE FROM xray_clients WHERE uuid = $1`, uuid)
|
|
return err
|
|
}
|
|
|
|
func (s *Store) ListAllXrayClients(ctx context.Context) ([]*XrayClientMeta, error) {
|
|
rows, err := s.db.QueryContext(ctx, `
|
|
SELECT uuid, name, email, inbound_tag, expires_at, max_conns, created_at
|
|
FROM xray_clients ORDER BY created_at DESC`)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var out []*XrayClientMeta
|
|
for rows.Next() {
|
|
m := &XrayClientMeta{}
|
|
var expiresAt sql.NullTime
|
|
if err := rows.Scan(&m.UUID, &m.Name, &m.Email, &m.InboundTag, &expiresAt, &m.MaxConns, &m.CreatedAt); err != nil {
|
|
return nil, err
|
|
}
|
|
if expiresAt.Valid {
|
|
m.ExpiresAt = &expiresAt.Time
|
|
}
|
|
out = append(out, m)
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
func (s *Store) ListExpiredXrayClients(ctx context.Context) ([]*XrayClientMeta, error) {
|
|
rows, err := s.db.QueryContext(ctx, `
|
|
SELECT uuid, name, email, inbound_tag, expires_at, max_conns, created_at
|
|
FROM xray_clients WHERE expires_at IS NOT NULL AND expires_at <= NOW()`)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var out []*XrayClientMeta
|
|
for rows.Next() {
|
|
m := &XrayClientMeta{}
|
|
var expiresAt sql.NullTime
|
|
if err := rows.Scan(&m.UUID, &m.Name, &m.Email, &m.InboundTag, &expiresAt, &m.MaxConns, &m.CreatedAt); err != nil {
|
|
return nil, err
|
|
}
|
|
if expiresAt.Valid {
|
|
m.ExpiresAt = &expiresAt.Time
|
|
}
|
|
out = append(out, m)
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
// startXrayClientExpiryChecker runs a background goroutine that removes expired
|
|
// Xray clients from both the config file and the database every 5 minutes.
|
|
func startXrayClientExpiryChecker(store *Store) {
|
|
if store == nil {
|
|
return
|
|
}
|
|
go func() {
|
|
ticker := time.NewTicker(5 * time.Minute)
|
|
defer ticker.Stop()
|
|
for range ticker.C {
|
|
ctx := context.Background()
|
|
expired, err := store.ListExpiredXrayClients(ctx)
|
|
if err != nil {
|
|
log.Printf("xray expiry checker: list error: %v", err)
|
|
continue
|
|
}
|
|
if len(expired) == 0 {
|
|
continue
|
|
}
|
|
needRestart := false
|
|
for _, m := range expired {
|
|
tag := m.InboundTag
|
|
if tag == "" {
|
|
_ = store.DeleteXrayClientMeta(ctx, m.UUID)
|
|
continue
|
|
}
|
|
if err := xrayMgr.RemoveXrayClient(tag, m.UUID); err != nil {
|
|
log.Printf("xray expiry: remove %s from %s: %v", m.UUID, tag, err)
|
|
} else {
|
|
needRestart = true
|
|
}
|
|
if err := store.DeleteXrayClientMeta(ctx, m.UUID); err != nil {
|
|
log.Printf("xray expiry: delete meta %s: %v", m.UUID, err)
|
|
}
|
|
log.Printf("xray expiry: removed expired client %q (%s) from inbound %s", m.Name, m.UUID, tag)
|
|
}
|
|
if needRestart {
|
|
if err := xrayMgr.Restart(); err != nil {
|
|
log.Printf("xray expiry: restart error: %v", err)
|
|
}
|
|
}
|
|
}
|
|
}()
|
|
}
|