2026-04-04 10:29:02 +00:00
|
|
|
package proxy
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"crypto/rand"
|
|
|
|
|
"crypto/sha256"
|
2026-04-04 21:39:58 +00:00
|
|
|
"encoding/binary"
|
2026-04-04 10:29:02 +00:00
|
|
|
"encoding/hex"
|
|
|
|
|
"encoding/json"
|
|
|
|
|
"errors"
|
|
|
|
|
"io"
|
|
|
|
|
"math"
|
|
|
|
|
"net"
|
|
|
|
|
"os"
|
|
|
|
|
"strconv"
|
|
|
|
|
"strings"
|
|
|
|
|
"sync"
|
|
|
|
|
"time"
|
|
|
|
|
)
|
|
|
|
|
|
2026-04-05 01:30:08 +00:00
|
|
|
// Result is the success/error carrier used by constructors and loaders.
|
|
|
|
|
//
|
|
|
|
|
// cfg, result := proxy.LoadConfig("config.json")
|
|
|
|
|
// if !result.OK {
|
|
|
|
|
// return result.Error
|
|
|
|
|
// }
|
2026-04-04 10:29:02 +00:00
|
|
|
type Result struct {
|
|
|
|
|
OK bool
|
|
|
|
|
Error error
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-05 00:52:34 +00:00
|
|
|
func newSuccessResult() Result {
|
2026-04-04 10:29:02 +00:00
|
|
|
return Result{OK: true}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-05 00:52:34 +00:00
|
|
|
func newErrorResult(err error) Result {
|
2026-04-04 10:29:02 +00:00
|
|
|
return Result{OK: false, Error: err}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-05 00:36:29 +00:00
|
|
|
var splitterFactoriesMu sync.RWMutex
|
2026-04-05 00:52:34 +00:00
|
|
|
var splitterFactoriesByMode = map[string]func(*Config, *EventBus) Splitter{}
|
2026-04-04 10:29:02 +00:00
|
|
|
|
2026-04-05 01:30:08 +00:00
|
|
|
// RegisterSplitterFactory installs the constructor used for one proxy mode.
|
2026-04-05 00:28:03 +00:00
|
|
|
//
|
|
|
|
|
// proxy.RegisterSplitterFactory("simple", func(cfg *proxy.Config, bus *proxy.EventBus) proxy.Splitter {
|
|
|
|
|
// return simple.NewSimpleSplitter(cfg, bus, nil)
|
|
|
|
|
// })
|
2026-04-04 10:29:02 +00:00
|
|
|
func RegisterSplitterFactory(mode string, factory func(*Config, *EventBus) Splitter) {
|
2026-04-05 00:36:29 +00:00
|
|
|
splitterFactoriesMu.Lock()
|
|
|
|
|
defer splitterFactoriesMu.Unlock()
|
2026-04-05 00:52:34 +00:00
|
|
|
splitterFactoriesByMode[strings.ToLower(mode)] = factory
|
2026-04-04 10:29:02 +00:00
|
|
|
}
|
|
|
|
|
|
2026-04-05 00:52:34 +00:00
|
|
|
func splitterFactoryForMode(mode string) (func(*Config, *EventBus) Splitter, bool) {
|
2026-04-05 00:36:29 +00:00
|
|
|
splitterFactoriesMu.RLock()
|
|
|
|
|
defer splitterFactoriesMu.RUnlock()
|
2026-04-05 00:52:34 +00:00
|
|
|
factory, ok := splitterFactoriesByMode[strings.ToLower(mode)]
|
2026-04-04 10:29:02 +00:00
|
|
|
return factory, ok
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-05 00:28:03 +00:00
|
|
|
// cfg, result := proxy.LoadConfig("/etc/proxy.json")
|
|
|
|
|
// if !result.OK { return result.Error }
|
2026-04-04 10:29:02 +00:00
|
|
|
func LoadConfig(path string) (*Config, Result) {
|
|
|
|
|
data, err := os.ReadFile(path)
|
|
|
|
|
if err != nil {
|
2026-04-05 00:52:34 +00:00
|
|
|
return nil, newErrorResult(err)
|
2026-04-04 10:29:02 +00:00
|
|
|
}
|
|
|
|
|
|
2026-04-04 23:10:35 +00:00
|
|
|
config := &Config{}
|
|
|
|
|
if err := json.Unmarshal(data, config); err != nil {
|
2026-04-05 00:52:34 +00:00
|
|
|
return nil, newErrorResult(err)
|
2026-04-04 10:29:02 +00:00
|
|
|
}
|
2026-04-04 23:10:35 +00:00
|
|
|
config.configPath = path
|
|
|
|
|
return config, config.Validate()
|
2026-04-04 10:29:02 +00:00
|
|
|
}
|
|
|
|
|
|
2026-04-05 00:28:03 +00:00
|
|
|
// cfg := &proxy.Config{Mode: "nicehash", Bind: []proxy.BindAddr{{Host: "0.0.0.0", Port: 3333}}, Pools: []proxy.PoolConfig{{URL: "pool.example:3333", Enabled: true}}, Workers: proxy.WorkersByRigID}
|
|
|
|
|
// if result := cfg.Validate(); !result.OK { return result }
|
2026-04-04 10:29:02 +00:00
|
|
|
func (c *Config) Validate() Result {
|
|
|
|
|
if c == nil {
|
2026-04-05 00:52:34 +00:00
|
|
|
return newErrorResult(errors.New("config is nil"))
|
2026-04-04 10:29:02 +00:00
|
|
|
}
|
2026-04-04 18:19:09 +00:00
|
|
|
if !isValidMode(c.Mode) {
|
2026-04-05 00:52:34 +00:00
|
|
|
return newErrorResult(errors.New("mode must be \"nicehash\" or \"simple\""))
|
2026-04-04 18:19:09 +00:00
|
|
|
}
|
|
|
|
|
if !isValidWorkersMode(c.Workers) {
|
2026-04-05 00:52:34 +00:00
|
|
|
return newErrorResult(errors.New("workers must be one of \"rig-id\", \"user\", \"password\", \"agent\", \"ip\", or \"false\""))
|
2026-04-04 18:19:09 +00:00
|
|
|
}
|
2026-04-04 10:29:02 +00:00
|
|
|
if len(c.Bind) == 0 {
|
2026-04-05 00:52:34 +00:00
|
|
|
return newErrorResult(errors.New("bind list is empty"))
|
2026-04-04 10:29:02 +00:00
|
|
|
}
|
|
|
|
|
if len(c.Pools) == 0 {
|
2026-04-05 00:52:34 +00:00
|
|
|
return newErrorResult(errors.New("pool list is empty"))
|
2026-04-04 10:29:02 +00:00
|
|
|
}
|
|
|
|
|
for _, pool := range c.Pools {
|
|
|
|
|
if pool.Enabled && strings.TrimSpace(pool.URL) == "" {
|
2026-04-05 00:52:34 +00:00
|
|
|
return newErrorResult(errors.New("enabled pool url is empty"))
|
2026-04-04 10:29:02 +00:00
|
|
|
}
|
|
|
|
|
}
|
2026-04-05 00:52:34 +00:00
|
|
|
return newSuccessResult()
|
2026-04-04 10:29:02 +00:00
|
|
|
}
|
|
|
|
|
|
2026-04-04 18:19:09 +00:00
|
|
|
func isValidMode(mode string) bool {
|
|
|
|
|
switch strings.ToLower(strings.TrimSpace(mode)) {
|
|
|
|
|
case "nicehash", "simple":
|
|
|
|
|
return true
|
|
|
|
|
default:
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func isValidWorkersMode(mode WorkersMode) bool {
|
|
|
|
|
switch mode {
|
|
|
|
|
case WorkersByRigID, WorkersByUser, WorkersByPass, WorkersByAgent, WorkersByIP, WorkersDisabled:
|
|
|
|
|
return true
|
|
|
|
|
default:
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-05 00:07:27 +00:00
|
|
|
// bus := proxy.NewEventBus()
|
|
|
|
|
// bus.Subscribe(proxy.EventLogin, func(e proxy.Event) { _ = e.Miner })
|
2026-04-04 10:29:02 +00:00
|
|
|
func NewEventBus() *EventBus {
|
|
|
|
|
return &EventBus{listeners: make(map[EventType][]EventHandler)}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Subscribe registers a handler for the given event type.
|
|
|
|
|
func (b *EventBus) Subscribe(t EventType, h EventHandler) {
|
|
|
|
|
if b == nil || h == nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
b.mu.Lock()
|
|
|
|
|
defer b.mu.Unlock()
|
|
|
|
|
if b.listeners == nil {
|
|
|
|
|
b.listeners = make(map[EventType][]EventHandler)
|
|
|
|
|
}
|
|
|
|
|
b.listeners[t] = append(b.listeners[t], h)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Dispatch calls all registered handlers for the event's type.
|
|
|
|
|
func (b *EventBus) Dispatch(e Event) {
|
|
|
|
|
if b == nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
b.mu.RLock()
|
|
|
|
|
handlers := append([]EventHandler(nil), b.listeners[e.Type]...)
|
|
|
|
|
b.mu.RUnlock()
|
|
|
|
|
for _, handler := range handlers {
|
2026-04-04 23:16:18 +00:00
|
|
|
func() {
|
|
|
|
|
defer func() {
|
|
|
|
|
_ = recover()
|
|
|
|
|
}()
|
|
|
|
|
handler(e)
|
|
|
|
|
}()
|
2026-04-04 10:29:02 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-05 01:08:23 +00:00
|
|
|
type shareSinkGroup struct {
|
|
|
|
|
sinks []ShareSink
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func newShareSinkGroup(sinks ...ShareSink) *shareSinkGroup {
|
|
|
|
|
group := &shareSinkGroup{sinks: make([]ShareSink, 0, len(sinks))}
|
|
|
|
|
for _, sink := range sinks {
|
|
|
|
|
if sink != nil {
|
|
|
|
|
group.sinks = append(group.sinks, sink)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return group
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (g *shareSinkGroup) OnAccept(e Event) {
|
|
|
|
|
if g == nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
for _, sink := range g.sinks {
|
|
|
|
|
func() {
|
|
|
|
|
defer func() {
|
|
|
|
|
_ = recover()
|
|
|
|
|
}()
|
|
|
|
|
sink.OnAccept(e)
|
|
|
|
|
}()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (g *shareSinkGroup) OnReject(e Event) {
|
|
|
|
|
if g == nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
for _, sink := range g.sinks {
|
|
|
|
|
func() {
|
|
|
|
|
defer func() {
|
|
|
|
|
_ = recover()
|
|
|
|
|
}()
|
|
|
|
|
sink.OnReject(e)
|
|
|
|
|
}()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-04 10:29:02 +00:00
|
|
|
// IsValid returns true when the job contains a blob and job id.
|
2026-04-05 00:36:29 +00:00
|
|
|
//
|
|
|
|
|
// if !job.IsValid() {
|
|
|
|
|
// return
|
|
|
|
|
// }
|
2026-04-04 10:29:02 +00:00
|
|
|
func (j Job) IsValid() bool {
|
|
|
|
|
return j.Blob != "" && j.JobID != ""
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// BlobWithFixedByte replaces the blob byte at position 39 with fixedByte.
|
2026-04-05 00:36:29 +00:00
|
|
|
//
|
|
|
|
|
// partitioned := job.BlobWithFixedByte(0x2A)
|
2026-04-04 10:29:02 +00:00
|
|
|
func (j Job) BlobWithFixedByte(fixedByte uint8) string {
|
|
|
|
|
if len(j.Blob) < 80 {
|
|
|
|
|
return j.Blob
|
|
|
|
|
}
|
|
|
|
|
blob := []byte(j.Blob)
|
|
|
|
|
encoded := make([]byte, 2)
|
|
|
|
|
hex.Encode(encoded, []byte{fixedByte})
|
|
|
|
|
blob[78] = encoded[0]
|
|
|
|
|
blob[79] = encoded[1]
|
|
|
|
|
return string(blob)
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-05 01:05:00 +00:00
|
|
|
// DifficultyFromTarget converts the 8-char little-endian target into a difficulty.
|
2026-04-05 00:36:29 +00:00
|
|
|
//
|
|
|
|
|
// diff := job.DifficultyFromTarget()
|
2026-04-04 10:29:02 +00:00
|
|
|
func (j Job) DifficultyFromTarget() uint64 {
|
|
|
|
|
if len(j.Target) != 8 {
|
|
|
|
|
return 0
|
|
|
|
|
}
|
|
|
|
|
raw, err := hex.DecodeString(j.Target)
|
|
|
|
|
if err != nil || len(raw) != 4 {
|
|
|
|
|
return 0
|
|
|
|
|
}
|
|
|
|
|
target := uint32(raw[0]) | uint32(raw[1])<<8 | uint32(raw[2])<<16 | uint32(raw[3])<<24
|
|
|
|
|
if target == 0 {
|
|
|
|
|
return 0
|
|
|
|
|
}
|
2026-04-05 01:05:00 +00:00
|
|
|
return uint64(math.MaxUint32) / uint64(target)
|
2026-04-04 10:29:02 +00:00
|
|
|
}
|
|
|
|
|
|
2026-04-04 21:08:28 +00:00
|
|
|
func targetFromDifficulty(diff uint64) string {
|
|
|
|
|
if diff <= 1 {
|
|
|
|
|
return "ffffffff"
|
|
|
|
|
}
|
|
|
|
|
maxTarget := uint64(math.MaxUint32)
|
2026-04-05 01:05:00 +00:00
|
|
|
target := (maxTarget + diff - 1) / diff
|
2026-04-04 21:08:28 +00:00
|
|
|
if target == 0 {
|
|
|
|
|
target = 1
|
|
|
|
|
}
|
|
|
|
|
if target > maxTarget {
|
|
|
|
|
target = maxTarget
|
|
|
|
|
}
|
|
|
|
|
var raw [4]byte
|
|
|
|
|
binary.LittleEndian.PutUint32(raw[:], uint32(target))
|
|
|
|
|
return hex.EncodeToString(raw[:])
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-05 00:32:43 +00:00
|
|
|
func EffectiveShareDifficulty(job Job, miner *Miner) uint64 {
|
|
|
|
|
diff := job.DifficultyFromTarget()
|
|
|
|
|
if miner == nil || miner.customDiff == 0 || diff == 0 || diff <= miner.customDiff {
|
|
|
|
|
return diff
|
|
|
|
|
}
|
|
|
|
|
return miner.customDiff
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-04 10:29:02 +00:00
|
|
|
// NewCustomDiff creates a login-time custom difficulty resolver.
|
2026-04-04 21:58:37 +00:00
|
|
|
//
|
|
|
|
|
// resolver := proxy.NewCustomDiff(50000)
|
|
|
|
|
// resolver.OnLogin(proxy.Event{Miner: miner})
|
2026-04-04 10:29:02 +00:00
|
|
|
func NewCustomDiff(globalDiff uint64) *CustomDiff {
|
2026-04-05 00:45:39 +00:00
|
|
|
cd := &CustomDiff{}
|
|
|
|
|
cd.globalDiff.Store(globalDiff)
|
|
|
|
|
return cd
|
2026-04-04 10:29:02 +00:00
|
|
|
}
|
|
|
|
|
|
2026-04-04 22:13:10 +00:00
|
|
|
// OnLogin normalises the login user once during handshake.
|
|
|
|
|
//
|
|
|
|
|
// cd.OnLogin(proxy.Event{Miner: &proxy.Miner{user: "WALLET+50000"}})
|
2026-04-04 10:29:02 +00:00
|
|
|
func (cd *CustomDiff) OnLogin(e Event) {
|
|
|
|
|
if cd == nil || e.Miner == nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-04-04 21:39:58 +00:00
|
|
|
if e.Miner.customDiffResolved {
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-04-05 00:45:39 +00:00
|
|
|
e.Miner.user, e.Miner.customDiff = parseLoginUser(e.Miner.user, cd.globalDiff.Load())
|
2026-04-04 21:39:58 +00:00
|
|
|
e.Miner.customDiffResolved = true
|
2026-04-04 10:29:02 +00:00
|
|
|
}
|
|
|
|
|
|
2026-04-05 00:28:03 +00:00
|
|
|
// limiter := proxy.NewRateLimiter(proxy.RateLimit{MaxConnectionsPerMinute: 30, BanDurationSeconds: 300})
|
|
|
|
|
// if limiter.Allow("203.0.113.42:3333") { /* accept the socket */ }
|
2026-04-04 22:09:58 +00:00
|
|
|
func NewRateLimiter(config RateLimit) *RateLimiter {
|
2026-04-04 10:29:02 +00:00
|
|
|
return &RateLimiter{
|
2026-04-04 22:09:58 +00:00
|
|
|
config: config,
|
2026-04-04 10:29:02 +00:00
|
|
|
buckets: make(map[string]*tokenBucket),
|
|
|
|
|
banned: make(map[string]time.Time),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-05 00:28:03 +00:00
|
|
|
// if limiter.Allow("203.0.113.42:3333") { /* accept the socket */ }
|
2026-04-04 10:29:02 +00:00
|
|
|
func (rl *RateLimiter) Allow(ip string) bool {
|
2026-04-04 22:06:18 +00:00
|
|
|
if rl == nil || rl.config.MaxConnectionsPerMinute <= 0 {
|
2026-04-04 10:29:02 +00:00
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
host := hostOnly(ip)
|
|
|
|
|
now := time.Now()
|
|
|
|
|
|
|
|
|
|
rl.mu.Lock()
|
|
|
|
|
defer rl.mu.Unlock()
|
|
|
|
|
|
|
|
|
|
if until, banned := rl.banned[host]; banned {
|
|
|
|
|
if now.Before(until) {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
delete(rl.banned, host)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bucket, ok := rl.buckets[host]
|
|
|
|
|
if !ok {
|
2026-04-04 22:06:18 +00:00
|
|
|
bucket = &tokenBucket{tokens: rl.config.MaxConnectionsPerMinute, lastRefill: now}
|
2026-04-04 10:29:02 +00:00
|
|
|
rl.buckets[host] = bucket
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-04 22:06:18 +00:00
|
|
|
refillBucket(bucket, rl.config.MaxConnectionsPerMinute, now)
|
2026-04-04 10:29:02 +00:00
|
|
|
if bucket.tokens <= 0 {
|
2026-04-04 22:06:18 +00:00
|
|
|
if rl.config.BanDurationSeconds > 0 {
|
|
|
|
|
rl.banned[host] = now.Add(time.Duration(rl.config.BanDurationSeconds) * time.Second)
|
2026-04-04 10:29:02 +00:00
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bucket.tokens--
|
|
|
|
|
bucket.lastRefill = now
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Tick removes expired ban entries and refills token buckets.
|
2026-04-04 23:13:43 +00:00
|
|
|
//
|
|
|
|
|
// limiter.Tick()
|
2026-04-04 10:29:02 +00:00
|
|
|
func (rl *RateLimiter) Tick() {
|
2026-04-04 22:06:18 +00:00
|
|
|
if rl == nil || rl.config.MaxConnectionsPerMinute <= 0 {
|
2026-04-04 10:29:02 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
now := time.Now()
|
|
|
|
|
|
|
|
|
|
rl.mu.Lock()
|
|
|
|
|
defer rl.mu.Unlock()
|
|
|
|
|
|
|
|
|
|
for host, until := range rl.banned {
|
|
|
|
|
if !now.Before(until) {
|
|
|
|
|
delete(rl.banned, host)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
for _, bucket := range rl.buckets {
|
2026-04-04 22:06:18 +00:00
|
|
|
refillBucket(bucket, rl.config.MaxConnectionsPerMinute, now)
|
2026-04-04 10:29:02 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-05 00:28:03 +00:00
|
|
|
// watcher := proxy.NewConfigWatcher("config.json", func(cfg *proxy.Config) { p.Reload(cfg) })
|
2026-04-05 00:07:27 +00:00
|
|
|
// watcher.Start()
|
2026-04-04 22:09:58 +00:00
|
|
|
func NewConfigWatcher(configPath string, onChange func(*Config)) *ConfigWatcher {
|
2026-04-04 20:45:37 +00:00
|
|
|
watcher := &ConfigWatcher{
|
2026-04-04 22:09:58 +00:00
|
|
|
path: configPath,
|
2026-04-04 10:29:02 +00:00
|
|
|
onChange: onChange,
|
|
|
|
|
done: make(chan struct{}),
|
|
|
|
|
}
|
2026-04-04 22:09:58 +00:00
|
|
|
if info, err := os.Stat(configPath); err == nil {
|
2026-04-04 20:45:37 +00:00
|
|
|
watcher.lastMod = info.ModTime()
|
|
|
|
|
}
|
|
|
|
|
return watcher
|
2026-04-04 10:29:02 +00:00
|
|
|
}
|
|
|
|
|
|
2026-04-05 00:28:03 +00:00
|
|
|
// watcher.Start()
|
2026-04-04 10:29:02 +00:00
|
|
|
func (w *ConfigWatcher) Start() {
|
|
|
|
|
if w == nil || w.path == "" || w.onChange == nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
go func() {
|
|
|
|
|
ticker := time.NewTicker(time.Second)
|
|
|
|
|
defer ticker.Stop()
|
|
|
|
|
for {
|
|
|
|
|
select {
|
|
|
|
|
case <-ticker.C:
|
|
|
|
|
info, err := os.Stat(w.path)
|
|
|
|
|
if err != nil {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
mod := info.ModTime()
|
|
|
|
|
if mod.After(w.lastMod) {
|
|
|
|
|
w.lastMod = mod
|
2026-04-04 23:10:35 +00:00
|
|
|
config, result := LoadConfig(w.path)
|
|
|
|
|
if result.OK && config != nil {
|
|
|
|
|
w.onChange(config)
|
2026-04-04 10:29:02 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
case <-w.done:
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}()
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-05 00:28:03 +00:00
|
|
|
// watcher.Stop()
|
2026-04-04 10:29:02 +00:00
|
|
|
func (w *ConfigWatcher) Stop() {
|
|
|
|
|
if w == nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
select {
|
|
|
|
|
case <-w.done:
|
|
|
|
|
default:
|
|
|
|
|
close(w.done)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func hostOnly(ip string) string {
|
|
|
|
|
host, _, err := net.SplitHostPort(ip)
|
|
|
|
|
if err == nil {
|
|
|
|
|
return host
|
|
|
|
|
}
|
|
|
|
|
return ip
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func refillBucket(bucket *tokenBucket, limit int, now time.Time) {
|
|
|
|
|
if bucket == nil || limit <= 0 {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if bucket.lastRefill.IsZero() {
|
|
|
|
|
bucket.lastRefill = now
|
|
|
|
|
if bucket.tokens <= 0 {
|
|
|
|
|
bucket.tokens = limit
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-04-04 18:49:03 +00:00
|
|
|
interval := time.Duration(time.Minute) / time.Duration(limit)
|
2026-04-04 10:29:02 +00:00
|
|
|
if interval <= 0 {
|
2026-04-04 18:49:03 +00:00
|
|
|
interval = time.Nanosecond
|
2026-04-04 10:29:02 +00:00
|
|
|
}
|
|
|
|
|
elapsed := now.Sub(bucket.lastRefill)
|
|
|
|
|
if elapsed < interval {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
add := int(elapsed / interval)
|
|
|
|
|
bucket.tokens += add
|
|
|
|
|
if bucket.tokens > limit {
|
|
|
|
|
bucket.tokens = limit
|
|
|
|
|
}
|
|
|
|
|
bucket.lastRefill = bucket.lastRefill.Add(time.Duration(add) * interval)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func generateUUID() string {
|
|
|
|
|
var b [16]byte
|
|
|
|
|
if _, err := io.ReadFull(rand.Reader, b[:]); err != nil {
|
|
|
|
|
return strconv.FormatInt(time.Now().UnixNano(), 16)
|
|
|
|
|
}
|
|
|
|
|
b[6] = (b[6] & 0x0f) | 0x40
|
|
|
|
|
b[8] = (b[8] & 0x3f) | 0x80
|
|
|
|
|
var out [36]byte
|
|
|
|
|
hex.Encode(out[0:8], b[0:4])
|
|
|
|
|
out[8] = '-'
|
|
|
|
|
hex.Encode(out[9:13], b[4:6])
|
|
|
|
|
out[13] = '-'
|
|
|
|
|
hex.Encode(out[14:18], b[6:8])
|
|
|
|
|
out[18] = '-'
|
|
|
|
|
hex.Encode(out[19:23], b[8:10])
|
|
|
|
|
out[23] = '-'
|
|
|
|
|
hex.Encode(out[24:36], b[10:16])
|
|
|
|
|
return string(out[:])
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func sha256Hex(data []byte) string {
|
|
|
|
|
sum := sha256.Sum256(data)
|
|
|
|
|
return hex.EncodeToString(sum[:])
|
|
|
|
|
}
|