Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 43 additions & 3 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (
"github.com/metacubex/mihomo/component/sniffer"
"github.com/metacubex/mihomo/component/trie"
C "github.com/metacubex/mihomo/constant"
"github.com/metacubex/mihomo/tunnel/statistic"
P "github.com/metacubex/mihomo/constant/provider"
snifferTypes "github.com/metacubex/mihomo/constant/sniffer"
"github.com/metacubex/mihomo/dns"
Expand Down Expand Up @@ -179,6 +180,11 @@ type DNS struct {
type Profile struct {
StoreSelected bool
StoreFakeIP bool

StoreTrafficStats bool
TrafficStatsRetentionDays int
TrafficStatsMaxHosts int
TrafficStatsPersistInterval int
}

// TLS config
Expand Down Expand Up @@ -363,6 +369,11 @@ type RawExperimental struct {
type RawProfile struct {
StoreSelected bool `yaml:"store-selected" json:"store-selected"`
StoreFakeIP bool `yaml:"store-fake-ip" json:"store-fake-ip"`

StoreTrafficStats bool `yaml:"store-traffic-stats" json:"store-traffic-stats"`
TrafficStatsRetentionDays int `yaml:"traffic-stats-retention-days" json:"traffic-stats-retention-days"`
TrafficStatsMaxHosts int `yaml:"traffic-stats-max-hosts" json:"traffic-stats-max-hosts"`
TrafficStatsPersistInterval int `yaml:"traffic-stats-persist-interval" json:"traffic-stats-persist-interval"`
}

type RawGeoXUrl struct {
Expand Down Expand Up @@ -579,7 +590,10 @@ func DefaultRawConfig() *RawConfig {
QUICGoDisableECN: true,
},
Profile: RawProfile{
StoreSelected: true,
StoreSelected: true,
TrafficStatsRetentionDays: defaultTrafficStatsRetentionDays,
TrafficStatsMaxHosts: defaultTrafficStatsMaxHosts,
TrafficStatsPersistInterval: defaultTrafficStatsPersistInterval,
},
GeoXUrl: RawGeoXUrl{
Mmdb: "https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.metadb",
Expand Down Expand Up @@ -749,6 +763,22 @@ func ParseRawConfig(rawCfg *RawConfig) (*Config, error) {
return nil, err
}

statistic.InitTrafficStats(statistic.TrafficStatsOptions{
Enabled: config.Profile.StoreTrafficStats,
RetentionDays: config.Profile.TrafficStatsRetentionDays,
MaxHosts: config.Profile.TrafficStatsMaxHosts,
PersistInterval: time.Duration(config.Profile.TrafficStatsPersistInterval) * time.Second,
Path: C.Path.Resolve("traffic-stats.json"),
})

statistic.InitTrafficStats(statistic.TrafficStatsOptions{
Enabled: config.Profile.StoreTrafficStats,
RetentionDays: config.Profile.TrafficStatsRetentionDays,
MaxHosts: config.Profile.TrafficStatsMaxHosts,
PersistInterval: time.Duration(config.Profile.TrafficStatsPersistInterval) * time.Second,
Path: C.Path.Resolve("traffic-stats.json"),
})

elapsedTime := time.Since(startTime) / time.Millisecond // duration in ms
log.Infoln("Initial configuration complete, total time: %dms", elapsedTime) //Segment finished in xxm

Expand Down Expand Up @@ -859,10 +889,20 @@ func parseNTP(cfg *RawConfig) (*NTP, error) {
}, nil
}

const (
defaultTrafficStatsRetentionDays = 30
defaultTrafficStatsMaxHosts = 20000
defaultTrafficStatsPersistInterval = 300
)

func parseProfile(cfg *RawConfig) (*Profile, error) {
return &Profile{
StoreSelected: cfg.Profile.StoreSelected,
StoreFakeIP: cfg.Profile.StoreFakeIP,
StoreSelected: cfg.Profile.StoreSelected,
StoreFakeIP: cfg.Profile.StoreFakeIP,
StoreTrafficStats: cfg.Profile.StoreTrafficStats,
TrafficStatsRetentionDays: cfg.Profile.TrafficStatsRetentionDays,
TrafficStatsMaxHosts: cfg.Profile.TrafficStatsMaxHosts,
TrafficStatsPersistInterval: cfg.Profile.TrafficStatsPersistInterval,
}, nil
}

Expand Down
12 changes: 12 additions & 0 deletions hub/route/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,8 @@ func router(isDebug bool, secret string, dohServer string, cors Cors) *chi.Mux {
r.Mount("/group", groupRouter())
r.Mount("/rules", ruleRouter())
r.Mount("/connections", connectionRouter())
r.Mount("/traffic/hosts", trafficHostsRouter())
r.Mount("/traffic/hosts", trafficHostsRouter())
r.Mount("/providers/proxies", proxyProviderRouter())
r.Mount("/providers/rules", ruleProviderRouter())
r.Mount("/cache", cacheRouter())
Expand All @@ -148,6 +150,16 @@ func router(isDebug bool, secret string, dohServer string, cors Cors) *chi.Mux {
fs := http.StripPrefix("/ui", http.FileServer(http.Dir(uiPath)))
r.Get("/ui", http.RedirectHandler("/ui/", http.StatusTemporaryRedirect).ServeHTTP)
r.Get("/ui/*", func(w http.ResponseWriter, r *http.Request) {
rel := strings.TrimPrefix(r.URL.Path, "/ui")
if rel != "" && rel != "/" {
if info, err := os.Stat(filepath.Join(uiPath, filepath.FromSlash(rel))); err != nil || info.IsDir() {
// SPA 回退:前端路由的深层链接交给 200.html
if _, statErr := os.Stat(filepath.Join(uiPath, "200.html")); statErr == nil {
http.ServeFile(w, r, filepath.Join(uiPath, "200.html"))
return
}
}
}
fs.ServeHTTP(w, r)
})
})
Expand Down
107 changes: 107 additions & 0 deletions hub/route/traffic_hosts.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package route

import (
"strconv"
"strings"
"time"

"github.com/metacubex/chi"
"github.com/metacubex/chi/render"
"github.com/metacubex/http"

"github.com/metacubex/mihomo/tunnel/statistic"
)

const (
trafficHostsDefaultTop = 50
trafficHostsMaxTop = 1000
)

func trafficHostsRouter() http.Handler {
r := chi.NewRouter()
r.Get("/", getTrafficHosts)
r.Get("/check", getTrafficHostsCheck)
return r
}

func getTrafficHosts(w http.ResponseWriter, r *http.Request) {
stats := statistic.GetTrafficStats()
if !stats.Enabled() {
render.Status(r, http.StatusNotFound)
render.JSON(w, r, newError("traffic statistics is disabled"))
return
}

query := r.URL.Query()

since, err := strconv.ParseInt(query.Get("since"), 10, 64)
if err != nil {
render.Status(r, http.StatusBadRequest)
render.JSON(w, r, newError("invalid `since`"))
return
}
until := time.Now().Unix()
if raw := query.Get("until"); raw != "" {
until, err = strconv.ParseInt(raw, 10, 64)
if err != nil {
render.Status(r, http.StatusBadRequest)
render.JSON(w, r, newError("invalid `until`"))
return
}
}
if since > until {
render.Status(r, http.StatusBadRequest)
render.JSON(w, r, newError("`since` must not be greater than `until`"))
return
}

match := strings.ToLower(query.Get("match"))
suffixMatch := true
switch match {
case "", "suffix":
case "exact":
suffixMatch = false
default:
render.Status(r, http.StatusBadRequest)
render.JSON(w, r, newError("`match` must be `suffix` or `exact`"))
return
}

group := strings.ToLower(query.Get("group"))
switch group {
case "":
group = "all"
case "proxy", "direct", "all":
default:
render.Status(r, http.StatusBadRequest)
render.JSON(w, r, newError("`group` must be `proxy`, `direct` or `all`"))
return
}

top := trafficHostsDefaultTop
if raw := query.Get("top"); raw != "" {
top, err = strconv.Atoi(raw)
if err != nil || top <= 0 {
render.Status(r, http.StatusBadRequest)
render.JSON(w, r, newError("invalid `top`"))
return
}
if top > trafficHostsMaxTop {
top = trafficHostsMaxTop
}
}

domain := strings.ToLower(strings.TrimSpace(query.Get("domain")))
resp := stats.Query(since, until, domain, suffixMatch, group, top)
render.JSON(w, r, resp)
}

func getTrafficHostsCheck(w http.ResponseWriter, r *http.Request) {
stats := statistic.GetTrafficStats()
if !stats.Enabled() {
render.Status(r, http.StatusNotFound)
render.JSON(w, r, newError("traffic statistics is disabled"))
return
}
render.JSON(w, r, stats.Check())
}
Loading