Native Xray
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// EnsureXrayConfigSchema creates the DB table used as the canonical store for
|
||||
// Xray JSON configs. The config_file path is used as a stable key so local and
|
||||
// remote nodes can keep independent configs in the same database if needed.
|
||||
func (s *Store) EnsureXrayConfigSchema(ctx context.Context) error {
|
||||
stmts := []string{
|
||||
`CREATE TABLE IF NOT EXISTS xray_configs (
|
||||
config_key TEXT PRIMARY KEY,
|
||||
config_json JSONB NOT NULL,
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
)`,
|
||||
`ALTER TABLE xray_configs ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()`,
|
||||
}
|
||||
for _, stmt := range stmts {
|
||||
if _, err := s.db.ExecContext(ctx, stmt); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Store) GetXrayConfig(ctx context.Context, configKey string) ([]byte, bool, error) {
|
||||
if configKey == "" {
|
||||
configKey = "default"
|
||||
}
|
||||
var raw string
|
||||
err := s.db.QueryRowContext(ctx, `SELECT config_json::text FROM xray_configs WHERE config_key = $1`, configKey).Scan(&raw)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, false, nil
|
||||
}
|
||||
return nil, false, err
|
||||
}
|
||||
if !json.Valid([]byte(raw)) {
|
||||
return nil, false, fmt.Errorf("stored Xray config %q is not valid JSON", configKey)
|
||||
}
|
||||
return []byte(raw), true, nil
|
||||
}
|
||||
|
||||
func (s *Store) UpsertXrayConfig(ctx context.Context, configKey string, data []byte) error {
|
||||
if configKey == "" {
|
||||
configKey = "default"
|
||||
}
|
||||
if !json.Valid(data) {
|
||||
return fmt.Errorf("invalid Xray JSON config")
|
||||
}
|
||||
_, err := s.db.ExecContext(ctx, `
|
||||
INSERT INTO xray_configs (config_key, config_json, updated_at)
|
||||
VALUES ($1, $2::jsonb, NOW())
|
||||
ON CONFLICT (config_key) DO UPDATE SET
|
||||
config_json = EXCLUDED.config_json,
|
||||
updated_at = NOW()`, configKey, string(data))
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *Store) ListXrayClientsByInbound(ctx context.Context, inboundTag string) ([]*XrayClientMeta, error) {
|
||||
rows, err := s.db.QueryContext(ctx, `
|
||||
SELECT uuid, name, email, inbound_tag, COALESCE(owner_username, ''), expires_at, max_conns, created_at,
|
||||
COALESCE(total_uplink_bytes, 0), COALESCE(total_downlink_bytes, 0), last_active, COALESCE(active_connections, 0)
|
||||
FROM xray_clients WHERE inbound_tag = $1 ORDER BY created_at DESC`, inboundTag)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
return scanXrayClientMetaRows(rows)
|
||||
}
|
||||
|
||||
// ImportXrayClientsFromConfig mirrors client UUIDs found in an existing Xray
|
||||
// JSON config into xray_clients. This lets native mode inherit users from the
|
||||
// external Xray config and keeps the DB as the hot-reloadable client index.
|
||||
// It intentionally preserves owner/expiry/quota fields for existing rows.
|
||||
func (s *Store) ImportXrayClientsFromConfig(ctx context.Context, data []byte) (int, error) {
|
||||
if s == nil || len(data) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
var cfg struct {
|
||||
Inbounds []struct {
|
||||
Tag string `json:"tag"`
|
||||
Protocol string `json:"protocol"`
|
||||
Settings struct {
|
||||
Clients []struct {
|
||||
ID string `json:"id"`
|
||||
Password string `json:"password"`
|
||||
Email string `json:"email"`
|
||||
} `json:"clients"`
|
||||
Users []struct {
|
||||
ID string `json:"id"`
|
||||
Password string `json:"password"`
|
||||
Email string `json:"email"`
|
||||
} `json:"users"`
|
||||
} `json:"settings"`
|
||||
} `json:"inbounds"`
|
||||
}
|
||||
if err := json.Unmarshal(data, &cfg); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
imported := 0
|
||||
for _, ib := range cfg.Inbounds {
|
||||
proto := strings.ToLower(strings.TrimSpace(ib.Protocol))
|
||||
if proto != "vless" && proto != "vmess" && proto != "trojan" {
|
||||
continue
|
||||
}
|
||||
inboundTag := strings.TrimSpace(ib.Tag)
|
||||
configClients := ib.Settings.Clients
|
||||
if len(ib.Settings.Users) > 0 {
|
||||
configClients = append(configClients, ib.Settings.Users...)
|
||||
}
|
||||
for _, c := range configClients {
|
||||
uuid := strings.TrimSpace(c.ID)
|
||||
if uuid == "" {
|
||||
uuid = strings.TrimSpace(c.Password)
|
||||
}
|
||||
if uuid == "" {
|
||||
continue
|
||||
}
|
||||
email := strings.TrimSpace(c.Email)
|
||||
if email == "" {
|
||||
email = uuid
|
||||
}
|
||||
_, err := s.db.ExecContext(ctx, `
|
||||
INSERT INTO xray_clients (uuid, name, email, inbound_tag)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
ON CONFLICT (uuid) DO UPDATE SET
|
||||
email = CASE WHEN xray_clients.email = '' THEN EXCLUDED.email ELSE xray_clients.email END,
|
||||
name = CASE WHEN xray_clients.name = '' THEN EXCLUDED.name ELSE xray_clients.name END,
|
||||
inbound_tag = CASE WHEN EXCLUDED.inbound_tag <> '' THEN EXCLUDED.inbound_tag ELSE xray_clients.inbound_tag END`,
|
||||
uuid, email, email, inboundTag)
|
||||
if err != nil {
|
||||
return imported, err
|
||||
}
|
||||
imported++
|
||||
}
|
||||
}
|
||||
return imported, nil
|
||||
}
|
||||
Reference in New Issue
Block a user