Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/prism/proxy/internal/cache"
|
||||
"github.com/prism/proxy/internal/configbuilder"
|
||||
"github.com/prism/proxy/internal/core"
|
||||
"github.com/prism/proxy/internal/geodata"
|
||||
applog "github.com/prism/proxy/internal/log"
|
||||
"github.com/prism/proxy/internal/mihomoapi"
|
||||
"github.com/prism/proxy/internal/store"
|
||||
"github.com/prism/proxy/internal/subscription"
|
||||
)
|
||||
|
||||
type App struct {
|
||||
Store *store.Store
|
||||
Cache *cache.Cache
|
||||
Engine *core.Engine
|
||||
Builder *configbuilder.Builder
|
||||
Subscription *subscription.Service
|
||||
Logs *applog.Collector
|
||||
DataDir string
|
||||
LastReloadError string
|
||||
|
||||
engineMu sync.RWMutex
|
||||
engineOK bool
|
||||
engineAt time.Time
|
||||
}
|
||||
|
||||
func (a *App) ReloadEngine(ctx context.Context) error {
|
||||
geodata.InitHomeDir(a.DataDir)
|
||||
settings, err := a.Store.GetSettings(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
geodata.ApplyMihomoURLs(geodata.ConfigFromSettings(settings))
|
||||
|
||||
yaml, err := a.Builder.Build(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
apiPort := settings["mihomo_api_port"]
|
||||
if apiPort == "" {
|
||||
apiPort = "9090"
|
||||
}
|
||||
secret := settings["api_secret"]
|
||||
a.Engine.Configure("127.0.0.1:"+apiPort, secret)
|
||||
|
||||
if err := a.applyConfig(ctx, yaml); err != nil {
|
||||
a.Logs.Add("warn", "prism", "full config failed, trying minimal: "+err.Error())
|
||||
minimal, mErr := a.Builder.BuildMinimal(ctx)
|
||||
if mErr != nil {
|
||||
return err
|
||||
}
|
||||
if mErr := a.applyConfig(ctx, minimal); mErr != nil {
|
||||
a.LastReloadError = err.Error()
|
||||
if last := a.Cache.GetLastYAML(); last != nil {
|
||||
if fallbackErr := a.Engine.Reload(last); fallbackErr == nil {
|
||||
a.Logs.Add("warn", "prism", "kept previous engine config after reload failure")
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
a.LastReloadError = "rules simplified: " + err.Error()
|
||||
a.Logs.Add("warn", "prism", a.LastReloadError)
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) applyConfig(ctx context.Context, yaml []byte) error {
|
||||
if err := a.Engine.Reload(yaml); err != nil {
|
||||
return err
|
||||
}
|
||||
a.LastReloadError = ""
|
||||
a.Cache.SetLastYAML(yaml)
|
||||
a.Logs.Add("info", "prism", "engine reloaded successfully")
|
||||
a.engineMu.Lock()
|
||||
a.engineOK = true
|
||||
a.engineAt = time.Now()
|
||||
a.engineMu.Unlock()
|
||||
_ = a.Store.AddAuditLog(ctx, "reload", "engine", "config applied")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) MihomoAPIBase(ctx context.Context) (string, string, error) {
|
||||
settings, err := a.Store.GetSettings(ctx)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
port := settings["mihomo_api_port"]
|
||||
if port == "" {
|
||||
port = "9090"
|
||||
}
|
||||
return fmt.Sprintf("http://127.0.0.1:%s", port), settings["api_secret"], nil
|
||||
}
|
||||
|
||||
func (a *App) EngineReachable(ctx context.Context) bool {
|
||||
if a.Engine.Running() {
|
||||
return true
|
||||
}
|
||||
a.engineMu.RLock()
|
||||
if time.Since(a.engineAt) < 15*time.Second {
|
||||
ok := a.engineOK
|
||||
a.engineMu.RUnlock()
|
||||
return ok
|
||||
}
|
||||
a.engineMu.RUnlock()
|
||||
|
||||
ok := a.probeEngine(ctx)
|
||||
a.engineMu.Lock()
|
||||
a.engineOK = ok
|
||||
a.engineAt = time.Now()
|
||||
a.engineMu.Unlock()
|
||||
return ok
|
||||
}
|
||||
|
||||
// EngineStatusCached returns last-known engine reachability without blocking on Mihomo.
|
||||
func (a *App) EngineStatusCached() bool {
|
||||
if a.Engine.Running() {
|
||||
return true
|
||||
}
|
||||
a.engineMu.RLock()
|
||||
defer a.engineMu.RUnlock()
|
||||
return a.engineOK
|
||||
}
|
||||
|
||||
func (a *App) RefreshEngineStatusAsync() {
|
||||
go func() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
defer cancel()
|
||||
ok := a.probeEngine(ctx)
|
||||
a.engineMu.Lock()
|
||||
a.engineOK = ok
|
||||
a.engineAt = time.Now()
|
||||
a.engineMu.Unlock()
|
||||
}()
|
||||
}
|
||||
|
||||
func (a *App) probeEngine(ctx context.Context) bool {
|
||||
base, secret, err := a.MihomoAPIBase(ctx)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return mihomoapi.New(base, secret).Reachable()
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/prism/proxy/internal/country"
|
||||
)
|
||||
|
||||
func (a *App) ProxyDisplayNames(ctx context.Context) (map[string]string, error) {
|
||||
nodes, err := a.Store.ListProxyNodes(ctx, "", nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
groups, err := a.Store.ListProxyGroups(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m := map[string]string{
|
||||
"DIRECT": "DIRECT",
|
||||
"REJECT": "REJECT",
|
||||
}
|
||||
for _, n := range nodes {
|
||||
m[n.RuntimeName] = n.Name
|
||||
}
|
||||
for _, g := range groups {
|
||||
m[g.RuntimeName] = g.Name
|
||||
}
|
||||
seen := map[string]bool{}
|
||||
for _, n := range nodes {
|
||||
if n.Country == "" || seen[n.Country] {
|
||||
continue
|
||||
}
|
||||
seen[n.Country] = true
|
||||
rn := country.GroupRuntime(n.Country)
|
||||
m[rn] = country.Name(n.Country) + "(国家池)"
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (a *App) PolicyGroupRuntimes(ctx context.Context) (map[string]bool, error) {
|
||||
groups, err := a.Store.ListProxyGroups(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := map[string]bool{
|
||||
"GLOBAL": true,
|
||||
"PROXY": true,
|
||||
}
|
||||
for _, g := range groups {
|
||||
if g.Enabled {
|
||||
out[g.RuntimeName] = true
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func DisplayName(names map[string]string, key string) string {
|
||||
if key == "" {
|
||||
return ""
|
||||
}
|
||||
if name, ok := names[key]; ok && name != "" {
|
||||
return name
|
||||
}
|
||||
if strings.HasPrefix(key, "country_") {
|
||||
code := strings.TrimPrefix(key, "country_")
|
||||
return country.Name(code) + "(国家池)"
|
||||
}
|
||||
return key
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/prism/proxy/internal/country"
|
||||
"github.com/prism/proxy/internal/mihomoapi"
|
||||
"github.com/prism/proxy/internal/store"
|
||||
)
|
||||
|
||||
type OutboundMember struct {
|
||||
RuntimeName string `json:"runtime_name"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type OutboundGroup struct {
|
||||
RuntimeName string `json:"runtime_name"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Source string `json:"source"`
|
||||
Now string `json:"now"`
|
||||
NowName string `json:"now_name"`
|
||||
Selectable bool `json:"selectable"`
|
||||
IsCountry bool `json:"is_country"`
|
||||
Members []OutboundMember `json:"members"`
|
||||
}
|
||||
|
||||
type OutboundOverview struct {
|
||||
DefaultPolicy string `json:"default_policy"`
|
||||
Groups []OutboundGroup `json:"groups"`
|
||||
}
|
||||
|
||||
func (a *App) OutboundOverview(ctx context.Context) (OutboundOverview, error) {
|
||||
settings, err := a.Store.GetSettings(ctx)
|
||||
if err != nil {
|
||||
return OutboundOverview{}, err
|
||||
}
|
||||
defaultPolicy := settings["default_policy"]
|
||||
if defaultPolicy == "" {
|
||||
defaultPolicy = "DIRECT"
|
||||
}
|
||||
|
||||
base, secret, err := a.MihomoAPIBase(ctx)
|
||||
if err != nil {
|
||||
return OutboundOverview{}, err
|
||||
}
|
||||
live, err := mihomoapi.New(base, secret).ListProxiesCached(8 * time.Second)
|
||||
if err != nil {
|
||||
return OutboundOverview{}, fmt.Errorf("mihomo proxies: %w", err)
|
||||
}
|
||||
|
||||
nodes, err := a.Store.ListProxyNodes(ctx, "", nil)
|
||||
if err != nil {
|
||||
return OutboundOverview{}, err
|
||||
}
|
||||
groups, err := a.Store.ListProxyGroups(ctx)
|
||||
if err != nil {
|
||||
return OutboundOverview{}, err
|
||||
}
|
||||
display := buildDisplayMap(nodes, groups)
|
||||
|
||||
var outGroups []OutboundGroup
|
||||
seen := map[string]bool{}
|
||||
for _, g := range groups {
|
||||
if !g.Enabled {
|
||||
continue
|
||||
}
|
||||
state, ok := live[g.RuntimeName]
|
||||
if !ok || !mihomoapi.IsPolicyGroupType(state.Type) {
|
||||
continue
|
||||
}
|
||||
seen[g.RuntimeName] = true
|
||||
outGroups = append(outGroups, buildOutboundGroup(g.RuntimeName, g.Name, g.Source, state, display))
|
||||
}
|
||||
for name, state := range live {
|
||||
if seen[name] || !mihomoapi.IsPolicyGroupType(state.Type) {
|
||||
continue
|
||||
}
|
||||
outGroups = append(outGroups, buildOutboundGroup(name, display[name], "runtime", state, display))
|
||||
}
|
||||
|
||||
return OutboundOverview{
|
||||
DefaultPolicy: defaultPolicy,
|
||||
Groups: outGroups,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (a *App) SelectOutboundGroup(ctx context.Context, groupRuntime, proxyRuntime string) error {
|
||||
base, secret, err := a.MihomoAPIBase(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := mihomoapi.New(base, secret).SelectProxy(groupRuntime, proxyRuntime); err != nil {
|
||||
return err
|
||||
}
|
||||
_ = a.Store.AddAuditLog(ctx, "select", "outbound", groupRuntime+" -> "+proxyRuntime)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) SetDefaultPolicy(ctx context.Context, policy string) error {
|
||||
if err := a.Store.SetSettings(ctx, map[string]string{"default_policy": policy}); err != nil {
|
||||
return err
|
||||
}
|
||||
return a.ReloadEngine(ctx)
|
||||
}
|
||||
|
||||
func buildOutboundGroup(runtimeName, name, source string, state mihomoapi.ProxyState, display map[string]string) OutboundGroup {
|
||||
if name == "" {
|
||||
name = runtimeName
|
||||
}
|
||||
autoOnly := state.Type == "URLTest" || state.Type == "Fallback"
|
||||
members := make([]OutboundMember, 0, len(state.All))
|
||||
for _, m := range state.All {
|
||||
if autoOnly && (m == "DIRECT" || m == "REJECT" || m == "REJECT-DROP") {
|
||||
continue
|
||||
}
|
||||
if m == "DIRECT" || m == "REJECT" || m == "REJECT-DROP" {
|
||||
members = append(members, OutboundMember{RuntimeName: m, Name: m})
|
||||
continue
|
||||
}
|
||||
members = append(members, OutboundMember{
|
||||
RuntimeName: m,
|
||||
Name: displayName(display, m),
|
||||
})
|
||||
}
|
||||
return OutboundGroup{
|
||||
RuntimeName: runtimeName,
|
||||
Name: name,
|
||||
Type: state.Type,
|
||||
Source: source,
|
||||
Now: state.Now,
|
||||
NowName: displayName(display, state.Now),
|
||||
Selectable: state.Type == "Selector",
|
||||
IsCountry: strings.HasPrefix(runtimeName, "country_"),
|
||||
Members: members,
|
||||
}
|
||||
}
|
||||
|
||||
func buildDisplayMap(nodes []store.ProxyNode, groups []store.ProxyGroup) map[string]string {
|
||||
m := map[string]string{
|
||||
"DIRECT": "DIRECT",
|
||||
"REJECT": "REJECT",
|
||||
}
|
||||
seen := map[string]bool{}
|
||||
for _, n := range nodes {
|
||||
m[n.RuntimeName] = n.Name
|
||||
if n.Country != "" && !seen[n.Country] {
|
||||
seen[n.Country] = true
|
||||
m[country.GroupRuntime(n.Country)] = country.Name(n.Country) + "(国家池)"
|
||||
}
|
||||
}
|
||||
for _, g := range groups {
|
||||
m[g.RuntimeName] = g.Name
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func displayName(display map[string]string, runtime string) string {
|
||||
if name, ok := display[runtime]; ok && name != "" {
|
||||
return name
|
||||
}
|
||||
return runtime
|
||||
}
|
||||
Reference in New Issue
Block a user