diff --git a/agent/go.mod b/agent/go.mod index 5413616..0ec02c9 100644 --- a/agent/go.mod +++ b/agent/go.mod @@ -9,7 +9,7 @@ require ( github.com/docker/docker v28.1.1+incompatible github.com/rs/zerolog v1.34.0 github.com/stretchr/testify v1.10.0 - github.com/yusing/go-proxy v0.11.1 + github.com/yusing/go-proxy v0.11.5 ) replace github.com/docker/docker => github.com/godoxy-app/docker v0.0.0-20250418000134-7af8fd7b079e @@ -18,12 +18,15 @@ require ( github.com/Microsoft/go-winio v0.6.2 // indirect github.com/PuerkitoBio/goquery v1.10.3 // indirect github.com/andybalholm/cascadia v1.3.3 // indirect + github.com/buger/goterm v1.0.4 // indirect github.com/bytedance/sonic v1.13.2 // indirect github.com/bytedance/sonic/loader v0.2.4 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cloudwego/base64x v0.1.5 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/diskfs/go-diskfs v1.6.0 // indirect github.com/distribution/reference v0.6.0 // indirect + github.com/djherbis/times v1.6.0 // indirect github.com/docker/cli v28.1.1+incompatible // indirect github.com/docker/go-connections v0.5.0 // indirect github.com/docker/go-units v0.5.0 // indirect @@ -40,11 +43,15 @@ require ( github.com/goccy/go-yaml v1.17.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/google/pprof v0.0.0-20250423184734-337e5dd93bb4 // indirect - github.com/gotify/server/v2 v2.6.1 // indirect + github.com/gorilla/websocket v1.5.3 // indirect + github.com/gotify/server/v2 v2.6.3 // indirect + github.com/jinzhu/copier v0.4.0 // indirect github.com/klauspost/cpuid/v2 v2.2.10 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/lithammer/fuzzysearch v1.1.8 // indirect github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 // indirect + github.com/luthermonson/go-proxmox v0.2.2 // indirect + github.com/magefile/mage v1.15.0 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/miekg/dns v1.1.65 // indirect @@ -59,7 +66,7 @@ require ( github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect github.com/quic-go/qpack v0.5.1 // indirect github.com/quic-go/quic-go v0.51.0 // indirect - github.com/samber/lo v1.49.1 // indirect + github.com/samber/lo v1.50.0 // indirect github.com/samber/slog-common v0.18.1 // indirect github.com/samber/slog-zerolog/v2 v2.7.3 // indirect github.com/shirou/gopsutil/v4 v4.25.3 // indirect diff --git a/agent/go.sum b/agent/go.sum index 3bd8e27..71eda65 100644 --- a/agent/go.sum +++ b/agent/go.sum @@ -43,6 +43,8 @@ github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4 github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I= github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= +github.com/elliotwutingfeng/asciiset v0.0.0-20230602022725-51bbb787efab h1:h1UgjJdAAhj+uPL68n7XASS6bU+07ZX1WJvVS2eyoeY= +github.com/elliotwutingfeng/asciiset v0.0.0-20230602022725-51bbb787efab/go.mod h1:GLo/8fDswSAniFG+BFIaiSPcK610jyzgEhWYPQwuQdw= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY= @@ -68,6 +70,8 @@ github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= +github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/goccy/go-yaml v1.17.1 h1:LI34wktB2xEE3ONG/2Ar54+/HJVBriAGJ55PHls4YuY= @@ -88,14 +92,20 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/gotify/server/v2 v2.6.1 h1:Kf7v5fzBxzELzZa/jonWfwJMkqYqh1LBzBpCmt5QIAI= -github.com/gotify/server/v2 v2.6.1/go.mod h1:Dk8HLyTVDqmXM8YEg6tjROBen6mxyHZFRggJFHTwZLc= +github.com/gotify/server/v2 v2.6.3 h1:2sLDRsQ/No1+hcFwFDvjNtwKepfCSIR8L3BkXl/Vz1I= +github.com/gotify/server/v2 v2.6.3/go.mod h1:IyeQ/iL3vetcuqUAzkCMVObIMGGJx4zb13/mVatIwE8= github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 h1:e9Rjr40Z98/clHv5Yg79Is0NtosR5LXRvdr7o/6NwbA= github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1/go.mod h1:tIxuGz/9mpox++sgp9fJjHO0+q1X9/UOWd798aAm22M= +github.com/h2non/gock v1.2.0 h1:K6ol8rfrRkUOefooBC8elXoaNGYkpp7y2qcxGG6BzUE= +github.com/h2non/gock v1.2.0/go.mod h1:tNhoxHYW2W42cYkYb1WqzdbYIieALC99kpYr7rH/BQk= +github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw= +github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI= github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8= github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= @@ -143,8 +153,12 @@ github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJw github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= github.com/oschwald/maxminddb-golang v1.13.1 h1:G3wwjdN9JmIK2o/ermkHM+98oX5fS+k5MbwsmL4MRQE= github.com/oschwald/maxminddb-golang v1.13.1/go.mod h1:K4pgV9N/GcK694KSTmVSDTODk4IsCNThNdTmnaBZ/F8= +github.com/pierrec/lz4/v4 v4.1.17 h1:kV4Ip+/hUBC+8T6+2EgburRtkE9ef4nbY3f4dFhGjMc= +github.com/pierrec/lz4/v4 v4.1.17/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/xattr v0.4.9 h1:5883YPCtkSd8LFbs13nXplj9g9tlrwoJRjgpgMu1/fE= +github.com/pkg/xattr v0.4.9/go.mod h1:di8WF84zAKk8jzR1UBTEWh9AUlIZZ7M/JNt8e9B6ktU= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -161,8 +175,8 @@ github.com/quic-go/quic-go v0.51.0/go.mod h1:MFlGGpcpJqRAfmYi6NC2cptDPSxRWTOGNuP github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= -github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew= -github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o= +github.com/samber/lo v1.50.0 h1:XrG0xOeHs+4FQ8gJR97zDz5uOFMW7OwFWiFVzqopKgY= +github.com/samber/lo v1.50.0/go.mod h1:RjZyNk6WSnUFRKK6EyOhsRJMqft3G+pg7dCWHQCWvsc= github.com/samber/slog-common v0.18.1 h1:c0EipD/nVY9HG5shgm/XAs67mgpWDMF+MmtptdJNCkQ= github.com/samber/slog-common v0.18.1/go.mod h1:QNZiNGKakvrfbJ2YglQXLCZauzkI9xZBjOhWFKS3IKk= github.com/samber/slog-zerolog/v2 v2.7.3 h1:/MkPDl/tJhijN2GvB1MWwBn2FU8RiL3rQ8gpXkQm2EY= @@ -188,6 +202,8 @@ github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfj github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8= +github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/vincent-petithory/dataurl v1.0.0 h1:cXw+kPto8NLuJtlMsI152irrVw9fRDX8AbShPRpg2CI= github.com/vincent-petithory/dataurl v1.0.0/go.mod h1:FHafX5vmDzyP+1CQATJn7WFKc9CvnvxyvZy6I1MrG/U= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -271,8 +287,10 @@ golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210331175145-43e1dd70ce54/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/cmd/main.go b/cmd/main.go index 4c00570..681dbbc 100755 --- a/cmd/main.go +++ b/cmd/main.go @@ -6,13 +6,13 @@ import ( "os" "sync" - "github.com/yusing/go-proxy/internal" "github.com/yusing/go-proxy/internal/api/v1/query" "github.com/yusing/go-proxy/internal/auth" "github.com/yusing/go-proxy/internal/common" "github.com/yusing/go-proxy/internal/config" "github.com/yusing/go-proxy/internal/dnsproviders" "github.com/yusing/go-proxy/internal/gperr" + "github.com/yusing/go-proxy/internal/homepage" "github.com/yusing/go-proxy/internal/logging" "github.com/yusing/go-proxy/internal/logging/memlogger" "github.com/yusing/go-proxy/internal/metrics/systeminfo" @@ -50,7 +50,7 @@ func main() { rawLogger.Println("ok") return case common.CommandListIcons: - icons, err := internal.ListAvailableIcons() + icons, err := homepage.ListAvailableIcons() if err != nil { rawLogger.Fatal(err) } @@ -79,7 +79,7 @@ func main() { logging.Info().Msgf("GoDoxy version %s", pkg.GetVersion()) logging.Trace().Msg("trace enabled") parallel( - internal.InitIconListCache, + homepage.InitIconListCache, systeminfo.Poller.Start, ) diff --git a/go.mod b/go.mod index 4e66a73..0eaaff4 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,7 @@ require ( github.com/go-acme/lego/v4 v4.23.1 // acme client github.com/go-playground/validator/v10 v10.26.0 // validator github.com/gobwas/glob v0.2.3 // glob matcher for route rules - github.com/gotify/server/v2 v2.6.1 // reference the Message struct for json response + github.com/gotify/server/v2 v2.6.3 // reference the Message struct for json response github.com/lithammer/fuzzysearch v1.1.8 // fuzzy search for searching icons and filtering metrics github.com/puzpuzpuz/xsync/v3 v3.5.1 // lock free map for concurrent operations github.com/rs/zerolog v1.34.0 // logging @@ -41,13 +41,13 @@ require ( github.com/samber/slog-zerolog/v2 v2.7.3 github.com/spf13/afero v1.14.0 github.com/stretchr/testify v1.10.0 - github.com/yusing/go-proxy/agent v0.0.0-00010101000000-000000000000 - github.com/yusing/go-proxy/internal/dnsproviders v0.0.0-00010101000000-000000000000 + github.com/yusing/go-proxy/agent v0.0.0-20250428032249-8da63daf0202 + github.com/yusing/go-proxy/internal/dnsproviders v0.0.0-20250428032249-8da63daf0202 go.uber.org/atomic v1.11.0 ) require ( - github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 // indirect go.opentelemetry.io/proto/otlp v1.5.0 // indirect ) @@ -131,7 +131,7 @@ require ( github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-retryablehttp v0.7.7 // indirect github.com/hashicorp/go-uuid v1.0.3 // indirect - github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.146 // indirect + github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.147 // indirect github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df // indirect github.com/infobloxopen/infoblox-go-client/v2 v2.10.0 // indirect github.com/jinzhu/copier v0.4.0 // indirect @@ -191,7 +191,7 @@ require ( github.com/sacloud/iaas-api-go v1.14.0 // indirect github.com/sacloud/packages-go v0.0.11 // indirect github.com/sagikazarmark/locafero v0.9.0 // indirect - github.com/samber/lo v1.49.1 // indirect + github.com/samber/lo v1.50.0 // indirect github.com/samber/slog-common v0.18.1 // indirect github.com/scaleway/scaleway-sdk-go v1.0.0-beta.33 // indirect github.com/selectel/domains-go v1.1.0 // indirect @@ -207,7 +207,7 @@ require ( github.com/spf13/pflag v1.0.6 // indirect github.com/spf13/viper v1.20.1 // indirect github.com/subosito/gotenv v1.6.0 // indirect - github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1151 // indirect + github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1154 // indirect github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1136 // indirect github.com/tjfoc/gmsm v1.4.1 // indirect github.com/tklauser/go-sysconf v0.3.15 // indirect diff --git a/go.sum b/go.sum index ce1298d..fd2698d 100644 --- a/go.sum +++ b/go.sum @@ -1082,16 +1082,19 @@ github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/z github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/gotify/server/v2 v2.6.1 h1:Kf7v5fzBxzELzZa/jonWfwJMkqYqh1LBzBpCmt5QIAI= -github.com/gotify/server/v2 v2.6.1/go.mod h1:Dk8HLyTVDqmXM8YEg6tjROBen6mxyHZFRggJFHTwZLc= +github.com/gotify/server/v2 v2.6.3 h1:2sLDRsQ/No1+hcFwFDvjNtwKepfCSIR8L3BkXl/Vz1I= +github.com/gotify/server/v2 v2.6.3/go.mod h1:IyeQ/iL3vetcuqUAzkCMVObIMGGJx4zb13/mVatIwE8= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0/go.mod h1:hgWBS7lorOAVIJEQMi4ZsPv9hVvWI6+ch50m39Pf2Ks= github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3/go.mod h1:o//XUCC/F+yRGJoPO/VU0GSB0f8Nhgmxx0VIRUvaC0w= github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 h1:e9Rjr40Z98/clHv5Yg79Is0NtosR5LXRvdr7o/6NwbA= github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1/go.mod h1:tIxuGz/9mpox++sgp9fJjHO0+q1X9/UOWd798aAm22M= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI= github.com/h2non/gock v1.2.0 h1:K6ol8rfrRkUOefooBC8elXoaNGYkpp7y2qcxGG6BzUE= github.com/h2non/gock v1.2.0/go.mod h1:tNhoxHYW2W42cYkYb1WqzdbYIieALC99kpYr7rH/BQk= github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw= @@ -1147,8 +1150,8 @@ github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/J github.com/hashicorp/serf v0.9.5/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk= github.com/hashicorp/serf v0.10.1/go.mod h1:yL2t6BqATOLGc5HF7qbFkTfXoPIY0WZdWHfEvMqbG+4= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.146 h1:ld5s5UeA9zgyFsZskVD2Tr6k6VnJWkvaLm5nqvfOEf4= -github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.146/go.mod h1:Y/+YLCFCJtS29i2MbYPTUlNNfwXvkzEsZKR0imY/2aY= +github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.147 h1:ip9+1n9+THhYgChlQpgDLVDVTv4LVJ7AoyPBJBaX2MY= +github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.147/go.mod h1:Y/+YLCFCJtS29i2MbYPTUlNNfwXvkzEsZKR0imY/2aY= github.com/hudl/fargo v1.4.0/go.mod h1:9Ai6uvFy5fQNq6VPKtg+Ceq1+eTY4nKUlR2JElEOcDo= github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= @@ -1517,8 +1520,8 @@ github.com/sacloud/packages-go v0.0.11/go.mod h1:XNF5MCTWcHo9NiqWnYctVbASSSZR3ZO github.com/sagikazarmark/crypt v0.10.0/go.mod h1:gwTNHQVoOS3xp9Xvz5LLR+1AauC5M6880z5NWzdhOyQ= github.com/sagikazarmark/locafero v0.9.0 h1:GbgQGNtTrEmddYDSAH9QLRyfAHY12md+8YFTqyMTC9k= github.com/sagikazarmark/locafero v0.9.0/go.mod h1:UBUyz37V+EdMS3hDF3QWIiVr/2dPrx49OMO0Bn0hJqk= -github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew= -github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o= +github.com/samber/lo v1.50.0 h1:XrG0xOeHs+4FQ8gJR97zDz5uOFMW7OwFWiFVzqopKgY= +github.com/samber/lo v1.50.0/go.mod h1:RjZyNk6WSnUFRKK6EyOhsRJMqft3G+pg7dCWHQCWvsc= github.com/samber/slog-common v0.18.1 h1:c0EipD/nVY9HG5shgm/XAs67mgpWDMF+MmtptdJNCkQ= github.com/samber/slog-common v0.18.1/go.mod h1:QNZiNGKakvrfbJ2YglQXLCZauzkI9xZBjOhWFKS3IKk= github.com/samber/slog-zerolog/v2 v2.7.3 h1:/MkPDl/tJhijN2GvB1MWwBn2FU8RiL3rQ8gpXkQm2EY= @@ -1615,8 +1618,8 @@ github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNG github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1136/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0= -github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1151 h1:SBbEaeCwhqmyAEEF5ubpg/2vv3RO6SdBsOSYhpnJaL4= -github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1151/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1154 h1:tc2GXLGwpjaZdapd7pEpUjoeWU5gl3XUuZzDEyes7fg= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1154/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1136 h1:kMIdSU5IvpOROh27ToVQ3hlm6ym3lCRs9tnGCOBoZqk= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1136/go.mod h1:FpyIz3mymKaExVs6Fz27kxDBS42jqZn7vbACtxdeEH4= github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho= diff --git a/internal/api/v1/list.go b/internal/api/v1/list.go index 918daea..99cb1b1 100644 --- a/internal/api/v1/list.go +++ b/internal/api/v1/list.go @@ -6,9 +6,9 @@ import ( "strconv" "strings" - "github.com/yusing/go-proxy/internal" "github.com/yusing/go-proxy/internal/common" config "github.com/yusing/go-proxy/internal/config/types" + "github.com/yusing/go-proxy/internal/homepage" "github.com/yusing/go-proxy/internal/net/gphttp" "github.com/yusing/go-proxy/internal/net/gphttp/middleware" "github.com/yusing/go-proxy/internal/route/routes" @@ -67,7 +67,7 @@ func List(cfg config.ConfigInstance, w http.ResponseWriter, r *http.Request) { if err != nil { limit = 0 } - icons, err := internal.SearchIcons(r.FormValue("keyword"), limit) + icons, err := homepage.SearchIcons(r.FormValue("keyword"), limit) if err != nil { gphttp.ClientError(w, err) return diff --git a/internal/auth/auth.go b/internal/auth/auth.go index 6e8d231..14c7dbf 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -1,6 +1,7 @@ package auth import ( + "context" "net/http" "github.com/yusing/go-proxy/internal/common" @@ -38,17 +39,35 @@ func IsOIDCEnabled() bool { return common.OIDCIssuerURL != "" } +type nextHandler struct{} + +var nextHandlerContextKey = nextHandler{} + func RequireAuth(next http.HandlerFunc) http.HandlerFunc { - if IsEnabled() { - return func(w http.ResponseWriter, r *http.Request) { - if err := defaultAuth.CheckToken(r); err != nil { - gphttp.ClientError(w, err, http.StatusUnauthorized) - } else { - next(w, r) - } - } + if !IsEnabled() { + return next + } + return func(w http.ResponseWriter, r *http.Request) { + if err := defaultAuth.CheckToken(r); err != nil { + if IsFrontend(r) { + r = r.WithContext(context.WithValue(r.Context(), nextHandlerContextKey, next)) + defaultAuth.LoginHandler(w, r) + } else { + gphttp.ClientError(w, err, http.StatusUnauthorized) + } + return + } + next(w, r) + } +} + +func ProceedNext(w http.ResponseWriter, r *http.Request) { + next, ok := r.Context().Value(nextHandlerContextKey).(http.HandlerFunc) + if ok { + next(w, r) + } else { + w.WriteHeader(http.StatusOK) } - return next } func AuthCheckHandler(w http.ResponseWriter, r *http.Request) { diff --git a/internal/auth/oauth_refresh.go b/internal/auth/oauth_refresh.go index 800680b..017d046 100644 --- a/internal/auth/oauth_refresh.go +++ b/internal/auth/oauth_refresh.go @@ -1,11 +1,13 @@ package auth import ( + "context" "crypto/rand" - "encoding/base64" + "encoding/hex" "errors" "fmt" "net/http" + "sync" "time" "github.com/golang-jwt/jwt/v5" @@ -19,6 +21,10 @@ type oauthRefreshToken struct { Username string `json:"username"` RefreshToken string `json:"refresh_token"` Expiry time.Time `json:"expiry"` + + result *refreshResult + err error + mu sync.Mutex } type Session struct { @@ -27,6 +33,12 @@ type Session struct { Groups []string `json:"groups"` } +type refreshResult struct { + newSession Session + jwt string + jwtExpiry time.Time +} + type sessionClaims struct { Session jwt.RegisteredClaims @@ -34,11 +46,12 @@ type sessionClaims struct { type sessionID string -var oauthRefreshTokens jsonstore.MapStore[oauthRefreshToken] +var oauthRefreshTokens jsonstore.MapStore[*oauthRefreshToken] var ( defaultRefreshTokenExpiry = 30 * 24 * time.Hour // 1 month refreshBefore = 30 * time.Second + sessionInvalidateDelay = 3 * time.Second ) var ( @@ -50,7 +63,7 @@ const sessionTokenIssuer = "GoDoxy" func init() { if IsOIDCEnabled() { - oauthRefreshTokens = jsonstore.Store[oauthRefreshToken]("oauth_refresh_tokens") + oauthRefreshTokens = jsonstore.Store[*oauthRefreshToken]("oauth_refresh_tokens") } } @@ -61,7 +74,7 @@ func (token *oauthRefreshToken) expired() bool { func newSessionID() sessionID { b := make([]byte, 32) _, _ = rand.Read(b) - return sessionID(base64.StdEncoding.EncodeToString(b)) + return sessionID(hex.EncodeToString(b)) } func newSession(username string, groups []string) Session { @@ -72,26 +85,28 @@ func newSession(username string, groups []string) Session { } } -// getOnceOAuthRefreshToken returns the refresh token for the given session. +// getOAuthRefreshToken returns the refresh token for the given session. // // The token is removed from the store after retrieval. -func getOnceOAuthRefreshToken(claims *Session) (*oauthRefreshToken, bool) { +func getOAuthRefreshToken(claims *Session) (*oauthRefreshToken, bool) { token, ok := oauthRefreshTokens.Load(string(claims.SessionID)) if !ok { return nil, false } - invalidateOAuthRefreshToken(claims.SessionID) + if token.expired() { + invalidateOAuthRefreshToken(claims.SessionID) return nil, false } + if claims.Username != token.Username { return nil, false } - return &token, true + return token, true } func storeOAuthRefreshToken(sessionID sessionID, username, token string) { - oauthRefreshTokens.Store(string(sessionID), oauthRefreshToken{ + oauthRefreshTokens.Store(string(sessionID), &oauthRefreshToken{ Username: username, RefreshToken: token, Expiry: time.Now().Add(defaultRefreshTokenExpiry), @@ -135,51 +150,75 @@ func (auth *OIDCProvider) parseSessionJWT(sessionJWT string) (claims *sessionCla return claims, sessionToken.Valid && claims.Issuer == sessionTokenIssuer, nil } -func (auth *OIDCProvider) TryRefreshToken(w http.ResponseWriter, r *http.Request, sessionJWT string) error { +func (auth *OIDCProvider) TryRefreshToken(ctx context.Context, sessionJWT string) (*refreshResult, error) { // verify the session cookie claims, valid, err := auth.parseSessionJWT(sessionJWT) if err != nil { - return fmt.Errorf("%w: %w", ErrInvalidSessionToken, err) + return nil, fmt.Errorf("session: %s - %w: %w", claims.SessionID, ErrInvalidSessionToken, err) } if !valid { - return ErrInvalidSessionToken + return nil, ErrInvalidSessionToken } // check if refresh is possible - refreshToken, ok := getOnceOAuthRefreshToken(&claims.Session) + refreshToken, ok := getOAuthRefreshToken(&claims.Session) if !ok { - return errNoRefreshToken + return nil, errNoRefreshToken } if !auth.checkAllowed(claims.Username, claims.Groups) { - return ErrUserNotAllowed + return nil, ErrUserNotAllowed + } + + return auth.doRefreshToken(ctx, refreshToken, &claims.Session) +} + +func (auth *OIDCProvider) doRefreshToken(ctx context.Context, refreshToken *oauthRefreshToken, claims *Session) (*refreshResult, error) { + refreshToken.mu.Lock() + defer refreshToken.mu.Unlock() + + // already refreshed + // this must be called after refresh but before invalidate + if refreshToken.result != nil || refreshToken.err != nil { + return refreshToken.result, refreshToken.err } // this step refreshes the token // see https://cs.opensource.google/go/x/oauth2/+/refs/tags/v0.29.0:oauth2.go;l=313 - newToken, err := auth.oauthConfig.TokenSource(r.Context(), &oauth2.Token{ + newToken, err := auth.oauthConfig.TokenSource(ctx, &oauth2.Token{ RefreshToken: refreshToken.RefreshToken, }).Token() if err != nil { - return fmt.Errorf("%w: %w", ErrRefreshTokenFailure, err) + refreshToken.err = fmt.Errorf("session: %s - %w: %w", claims.SessionID, ErrRefreshTokenFailure, err) + return nil, refreshToken.err } - idTokenJWT, idToken, err := auth.getIdToken(r.Context(), newToken) + idTokenJWT, idToken, err := auth.getIdToken(ctx, newToken) if err != nil { - return err + refreshToken.err = fmt.Errorf("session: %s - %w: %w", claims.SessionID, ErrRefreshTokenFailure, err) + return nil, refreshToken.err } + // in case there're multiple requests for the same session to refresh + // invalidate the token after a short delay + go func() { + <-time.After(sessionInvalidateDelay) + invalidateOAuthRefreshToken(claims.SessionID) + }() + sessionID := newSessionID() logging.Debug().Str("username", claims.Username).Time("expiry", newToken.Expiry).Msg("refreshed token") storeOAuthRefreshToken(sessionID, claims.Username, newToken.RefreshToken) - // set new idToken and new sessionToken - auth.setIDTokenCookie(w, r, idTokenJWT, time.Until(idToken.Expiry)) - auth.setSessionTokenCookie(w, r, Session{ - SessionID: sessionID, - Username: claims.Username, - Groups: claims.Groups, - }) - return nil + refreshToken.result = &refreshResult{ + newSession: Session{ + SessionID: sessionID, + Username: claims.Username, + Groups: claims.Groups, + }, + jwt: idTokenJWT, + jwtExpiry: idToken.Expiry, + } + return refreshToken.result, nil } diff --git a/internal/auth/oidc.go b/internal/auth/oidc.go index 36e4a1b..c1068a5 100644 --- a/internal/auth/oidc.go +++ b/internal/auth/oidc.go @@ -13,6 +13,7 @@ import ( "github.com/coreos/go-oidc/v3/oidc" "github.com/yusing/go-proxy/internal/common" + "github.com/yusing/go-proxy/internal/gperr" "github.com/yusing/go-proxy/internal/logging" "github.com/yusing/go-proxy/internal/net/gphttp" "github.com/yusing/go-proxy/internal/utils" @@ -47,7 +48,12 @@ const ( OIDCLogoutPath = "/auth/logout" ) -var errMissingIDToken = errors.New("missing id_token field from oauth token") +var ( + errMissingIDToken = errors.New("missing id_token field from oauth token") + + ErrMissingOAuthToken = gperr.New("missing oauth token") + ErrInvalidOAuthToken = gperr.New("invalid oauth token") +) // generateState generates a random string for OIDC state. const oidcStateLength = 32 @@ -148,12 +154,19 @@ func (auth *OIDCProvider) HandleAuth(w http.ResponseWriter, r *http.Request) { func (auth *OIDCProvider) LoginHandler(w http.ResponseWriter, r *http.Request) { // check for session token sessionToken, err := r.Cookie(CookieOauthSessionToken) - if err == nil { - err = auth.TryRefreshToken(w, r, sessionToken.Value) - if err != nil { - logging.Debug().Err(err).Msg("failed to refresh token") - auth.clearCookie(w, r) + if err == nil { // session token exists + result, err := auth.TryRefreshToken(r.Context(), sessionToken.Value) + // redirect back to where they requested + // when token refresh is ok + if err == nil { + auth.setIDTokenCookie(w, r, result.jwt, time.Until(result.jwtExpiry)) + auth.setSessionTokenCookie(w, r, result.newSession) + ProceedNext(w, r) + return } + // clear cookies then redirect to home + logging.Err(err).Msg("failed to refresh token") + auth.clearCookie(w, r) http.Redirect(w, r, "/", http.StatusFound) return } diff --git a/internal/auth/utils.go b/internal/auth/utils.go index ba0297e..f1c20d6 100644 --- a/internal/auth/utils.go +++ b/internal/auth/utils.go @@ -10,22 +10,21 @@ import ( ) var ( - ErrMissingOAuthToken = gperr.New("missing oauth token") ErrMissingSessionToken = gperr.New("missing session token") - ErrInvalidOAuthToken = gperr.New("invalid oauth token") ErrInvalidSessionToken = gperr.New("invalid session token") ErrUserNotAllowed = gperr.New("user not allowed") ) +func IsFrontend(r *http.Request) bool { + return r.Host == common.APIHTTPAddr +} + func requestHost(r *http.Request) string { // check if it's from backend - switch r.Host { - case common.APIHTTPAddr: - // use XFH + if IsFrontend(r) { return r.Header.Get("X-Forwarded-Host") - default: - return r.Host } + return r.Host } // cookieDomain returns the fully qualified domain name of the request host diff --git a/internal/common/constants.go b/internal/common/constants.go index 254b1d6..6ddf633 100644 --- a/internal/common/constants.go +++ b/internal/common/constants.go @@ -16,7 +16,6 @@ const ( ConfigPath = ConfigBasePath + "/" + ConfigFileName IconListCachePath = ConfigBasePath + "/.icon_list_cache.json" - IconCachePath = ConfigBasePath + "/.icon_cache.json" NamespaceHomepageOverrides = ".homepage" NamespaceIconCache = ".icon_cache" diff --git a/internal/config/config.go b/internal/config/config.go index fbef211..ffcf773 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -18,6 +18,7 @@ import ( "github.com/yusing/go-proxy/internal/logging" "github.com/yusing/go-proxy/internal/net/gphttp/server" "github.com/yusing/go-proxy/internal/notif" + "github.com/yusing/go-proxy/internal/proxmox" proxy "github.com/yusing/go-proxy/internal/route/provider" "github.com/yusing/go-proxy/internal/task" "github.com/yusing/go-proxy/internal/utils" @@ -229,6 +230,7 @@ func (cfg *Config) load() gperr.Error { errs.Add(cfg.entrypoint.SetAccessLogger(cfg.task, model.Entrypoint.AccessLog)) cfg.initNotification(model.Providers.Notification) errs.Add(cfg.initAutoCert(model.AutoCert)) + errs.Add(cfg.initProxmox(model.Providers.Proxmox)) errs.Add(cfg.loadRouteProviders(&model.Providers)) cfg.value = model @@ -278,6 +280,17 @@ func (cfg *Config) initAutoCert(autocertCfg *autocert.Config) gperr.Error { return nil } +func (cfg *Config) initProxmox(proxmoxCfg []proxmox.Config) gperr.Error { + proxmox.Clients.Clear() + var errs = gperr.NewBuilder() + for _, cfg := range proxmoxCfg { + if err := cfg.Init(); err != nil { + errs.Add(err.Subject(cfg.URL)) + } + } + return errs.Error() +} + func (cfg *Config) errIfExists(p *proxy.Provider) gperr.Error { if _, ok := cfg.providers.Load(p.String()); ok { return gperr.Errorf("provider %s already exists", p.String()) diff --git a/internal/config/types/config.go b/internal/config/types/config.go index b1d657e..0934d6f 100644 --- a/internal/config/types/config.go +++ b/internal/config/types/config.go @@ -12,6 +12,7 @@ import ( "github.com/yusing/go-proxy/internal/gperr" "github.com/yusing/go-proxy/internal/logging/accesslog" "github.com/yusing/go-proxy/internal/notif" + "github.com/yusing/go-proxy/internal/proxmox" "github.com/yusing/go-proxy/internal/utils" ) @@ -30,6 +31,7 @@ type ( Docker map[string]string `json:"docker" yaml:"docker,omitempty" validate:"non_empty_docker_keys,dive,unix_addr|url"` Agents []*agent.AgentConfig `json:"agents" yaml:"agents,omitempty"` Notification []notif.NotificationConfig `json:"notification" yaml:"notification,omitempty"` + Proxmox []proxmox.Config `json:"proxmox" yaml:"proxmox,omitempty"` } Entrypoint struct { Middlewares []map[string]any `json:"middlewares"` diff --git a/internal/homepage/icon_cache.go b/internal/homepage/icon_cache.go index d7b2bc4..4f0f658 100644 --- a/internal/homepage/icon_cache.go +++ b/internal/homepage/icon_cache.go @@ -7,6 +7,7 @@ import ( "time" "github.com/yusing/go-proxy/internal/common" + "github.com/yusing/go-proxy/internal/jsonstore" "github.com/yusing/go-proxy/internal/logging" "github.com/yusing/go-proxy/internal/task" "github.com/yusing/go-proxy/internal/utils" @@ -15,34 +16,24 @@ import ( type cacheEntry struct { Icon []byte `json:"icon"` - ContentType string `json:"content_type"` + ContentType string `json:"content_type,omitempty"` LastAccess atomic.Value[time.Time] `json:"last_access"` } // cache key can be absolute url or route name. var ( - iconCache = make(map[string]*cacheEntry) - iconCacheMu sync.RWMutex + iconCache = jsonstore.Store[*cacheEntry](common.NamespaceIconCache) + iconMu sync.RWMutex ) const ( iconCacheTTL = 3 * 24 * time.Hour cleanUpInterval = time.Minute - maxCacheSize = 1024 * 1024 // 1MB + maxIconSize = 1024 * 1024 // 1MB maxCacheEntries = 100 ) -func InitIconCache() { - iconCacheMu.Lock() - defer iconCacheMu.Unlock() - - err := utils.LoadJSONIfExist(common.IconCachePath, &iconCache) - if err != nil { - logging.Error().Err(err).Msg("failed to load icon cache") - } else if len(iconCache) > 0 { - logging.Info().Int("count", len(iconCache)).Msg("icon cache loaded") - } - +func init() { go func() { cleanupTicker := time.NewTicker(cleanUpInterval) defer cleanupTicker.Stop() @@ -55,36 +46,21 @@ func InitIconCache() { } } }() - - task.OnProgramExit("save_favicon_cache", func() { - iconCacheMu.Lock() - defer iconCacheMu.Unlock() - - if len(iconCache) == 0 { - return - } - - if err := utils.SaveJSON(common.IconCachePath, &iconCache, 0o644); err != nil { - logging.Error().Err(err).Msg("failed to save icon cache") - } - }) } func pruneExpiredIconCache() { - iconCacheMu.Lock() - defer iconCacheMu.Unlock() - nPruned := 0 - for key, icon := range iconCache { + for key, icon := range iconCache.Range { if icon.IsExpired() { - delete(iconCache, key) + iconCache.Delete(key) nPruned++ } } - if len(iconCache) > maxCacheEntries { + if iconCache.Size() > maxCacheEntries { + iconCache.Clear() newIconCache := make(map[string]*cacheEntry, maxCacheEntries) i := 0 - for key, icon := range iconCache { + for key, icon := range iconCache.Range { if i == maxCacheEntries { break } @@ -93,7 +69,9 @@ func pruneExpiredIconCache() { i++ } } - iconCache = newIconCache + for key, icon := range newIconCache { + iconCache.Store(key, icon) + } } if nPruned > 0 { logging.Info().Int("pruned", nPruned).Msg("pruned expired icon cache") @@ -101,21 +79,18 @@ func pruneExpiredIconCache() { } func PruneRouteIconCache(route route) { - iconCacheMu.Lock() - defer iconCacheMu.Unlock() - delete(iconCache, route.Key()) + iconCache.Delete(route.Key()) } func loadIconCache(key string) *FetchResult { - iconCacheMu.RLock() - defer iconCacheMu.RUnlock() - - icon, ok := iconCache[key] + iconMu.RLock() + defer iconMu.RUnlock() + icon, ok := iconCache.Load(key) if ok && len(icon.Icon) > 0 { logging.Debug(). Str("key", key). Msg("icon found in cache") - icon.LastAccess.Store(time.Now()) + icon.LastAccess.Store(utils.TimeNow()) return &FetchResult{Icon: icon.Icon, contentType: icon.ContentType} } return nil @@ -123,15 +98,17 @@ func loadIconCache(key string) *FetchResult { func storeIconCache(key string, result *FetchResult) { icon := result.Icon - if len(icon) > maxCacheSize { + if len(icon) > maxIconSize { logging.Debug().Int("size", len(icon)).Msg("icon cache size exceeds max cache size") return } - iconCacheMu.Lock() - defer iconCacheMu.Unlock() + + iconMu.Lock() + defer iconMu.Unlock() + entry := &cacheEntry{Icon: icon, ContentType: result.contentType} entry.LastAccess.Store(time.Now()) - iconCache[key] = entry + iconCache.Store(key, entry) logging.Debug().Str("key", key).Int("size", len(icon)).Msg("stored icon cache") } @@ -140,12 +117,20 @@ func (e *cacheEntry) IsExpired() bool { } func (e *cacheEntry) UnmarshalJSON(data []byte) error { + var tmp struct { + Icon []byte `json:"icon"` + ContentType string `json:"content_type,omitempty"` + LastAccess time.Time `json:"last_access"` + } // check if data is json if json.Valid(data) { - err := json.Unmarshal(data, &e) + err := json.Unmarshal(data, &tmp) // return only if unmarshal is successful // otherwise fallback to base64 if err == nil { + e.Icon = tmp.Icon + e.ContentType = tmp.ContentType + e.LastAccess.Store(tmp.LastAccess) return nil } } diff --git a/internal/homepage/list-icons.go b/internal/homepage/list-icons.go index f5ae736..927d694 100644 --- a/internal/homepage/list-icons.go +++ b/internal/homepage/list-icons.go @@ -10,6 +10,7 @@ import ( "github.com/lithammer/fuzzysearch/fuzzy" "github.com/yusing/go-proxy/internal/common" "github.com/yusing/go-proxy/internal/logging" + "github.com/yusing/go-proxy/internal/task" "github.com/yusing/go-proxy/internal/utils" ) @@ -68,6 +69,10 @@ func InitIconListCache() { Int("display_names", len(iconsCache.DisplayNames)). Msg("icon list cache loaded") } + + task.OnProgramExit("save_icon_list_cache", func() { + utils.SaveJSON(common.IconListCachePath, iconsCache, 0o644) + }) } func ListAvailableIcons() (*Cache, error) { diff --git a/internal/idlewatcher/watcher.go b/internal/idlewatcher/watcher.go index c78aa6d..42ce4d8 100644 --- a/internal/idlewatcher/watcher.go +++ b/internal/idlewatcher/watcher.go @@ -131,7 +131,7 @@ func NewWatcher(parent task.Parent, r routes.Route) (*Watcher, error) { case routes.StreamRoute: w.stream = r default: - return nil, gperr.New("unexpected route type") + return nil, gperr.Errorf("unexpected route type: %T", r) } ctx, cancel := context.WithTimeout(parent.Context(), reqTimeout) diff --git a/internal/list-icons.go b/internal/list-icons.go deleted file mode 100644 index 1f3d2c6..0000000 --- a/internal/list-icons.go +++ /dev/null @@ -1,297 +0,0 @@ -package internal - -import ( - "encoding/json" - "io" - "net/http" - "sync" - "time" - - "github.com/lithammer/fuzzysearch/fuzzy" - "github.com/yusing/go-proxy/internal/common" - "github.com/yusing/go-proxy/internal/logging" - "github.com/yusing/go-proxy/internal/utils" -) - -type GitHubContents struct { //! keep this, may reuse in future - Type string `json:"type"` - Path string `json:"path"` - Name string `json:"name"` - Sha string `json:"sha"` - Size int `json:"size"` -} - -type ( - IconsMap map[string]map[string]struct{} - IconList []string - Cache struct { - WalkxCode, Selfhst IconsMap - DisplayNames ReferenceDisplayNameMap - IconList IconList // combined into a single list - } - ReferenceDisplayNameMap map[string]string -) - -func (icons *Cache) needUpdate() bool { - return len(icons.WalkxCode) == 0 || len(icons.Selfhst) == 0 || len(icons.IconList) == 0 || len(icons.DisplayNames) == 0 -} - -const updateInterval = 2 * time.Hour - -var ( - iconsCache *Cache - iconsCahceMu sync.RWMutex - lastUpdate time.Time -) - -const ( - walkxcodeIcons = "https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/tree.json" - selfhstIcons = "https://cdn.selfh.st/directory/icons.json" -) - -func InitIconListCache() { - iconsCahceMu.Lock() - defer iconsCahceMu.Unlock() - - iconsCache = &Cache{ - WalkxCode: make(IconsMap), - Selfhst: make(IconsMap), - DisplayNames: make(ReferenceDisplayNameMap), - IconList: []string{}, - } - err := utils.LoadJSONIfExist(common.IconListCachePath, iconsCache) - if err != nil { - logging.Error().Err(err).Msg("failed to load icon list cache config") - } else if len(iconsCache.IconList) > 0 { - logging.Info(). - Int("icons", len(iconsCache.IconList)). - Int("display_names", len(iconsCache.DisplayNames)). - Msg("icon list cache loaded") - } -} - -func ListAvailableIcons() (*Cache, error) { - iconsCahceMu.RLock() - if time.Since(lastUpdate) < updateInterval { - if !iconsCache.needUpdate() { - iconsCahceMu.RUnlock() - return iconsCache, nil - } - } - iconsCahceMu.RUnlock() - - iconsCahceMu.Lock() - defer iconsCahceMu.Unlock() - - logging.Info().Msg("updating icon data") - icons, err := fetchIconData() - if err != nil { - return nil, err - } - logging.Info(). - Int("icons", len(icons.IconList)). - Int("display_names", len(icons.DisplayNames)). - Msg("icons list updated") - - iconsCache = icons - lastUpdate = time.Now() - - err = utils.SaveJSON(common.IconListCachePath, iconsCache, 0o644) - if err != nil { - logging.Warn().Err(err).Msg("failed to save icon list cache") - } - return icons, nil -} - -func SearchIcons(keyword string, limit int) ([]string, error) { - icons, err := ListAvailableIcons() - if err != nil { - return nil, err - } - if keyword == "" { - return utils.Slice(icons.IconList, limit), nil - } - return utils.Slice(fuzzy.Find(keyword, icons.IconList), limit), nil -} - -func HasWalkxCodeIcon(name string, filetype string) bool { - icons, err := ListAvailableIcons() - if err != nil { - logging.Error().Err(err).Msg("failed to list icons") - return false - } - if _, ok := icons.WalkxCode[filetype]; !ok { - return false - } - _, ok := icons.WalkxCode[filetype][name+"."+filetype] - return ok -} - -func HasSelfhstIcon(name string, filetype string) bool { - icons, err := ListAvailableIcons() - if err != nil { - logging.Error().Err(err).Msg("failed to list icons") - return false - } - if _, ok := icons.Selfhst[filetype]; !ok { - return false - } - _, ok := icons.Selfhst[filetype][name+"."+filetype] - return ok -} - -func GetDisplayName(reference string) (string, bool) { - icons, err := ListAvailableIcons() - if err != nil { - logging.Error().Err(err).Msg("failed to list icons") - return "", false - } - displayName, ok := icons.DisplayNames[reference] - return displayName, ok -} - -func fetchIconData() (*Cache, error) { - walkxCodeIconMap, walkxCodeIconList, err := fetchWalkxCodeIcons() - if err != nil { - return nil, err - } - - n := 0 - for _, items := range walkxCodeIconMap { - n += len(items) - } - - selfhstIconMap, selfhstIconList, referenceToNames, err := fetchSelfhstIcons() - if err != nil { - return nil, err - } - - return &Cache{ - WalkxCode: walkxCodeIconMap, - Selfhst: selfhstIconMap, - DisplayNames: referenceToNames, - IconList: append(walkxCodeIconList, selfhstIconList...), - }, nil -} - -/* -format: - - { - "png": [ - "*.png", - ], - "svg": [ - "*.svg", - ], - "webp": [ - "*.webp", - ] - } -*/ -func fetchWalkxCodeIcons() (IconsMap, IconList, error) { - req, err := http.NewRequest(http.MethodGet, walkxcodeIcons, nil) - if err != nil { - return nil, nil, err - } - resp, err := http.DefaultClient.Do(req) - if err != nil { - return nil, nil, err - } - defer resp.Body.Close() - - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, nil, err - } - - data := make(map[string][]string) - err = json.Unmarshal(body, &data) - if err != nil { - return nil, nil, err - } - icons := make(IconsMap, len(data)) - iconList := make(IconList, 0, 2000) - for fileType, files := range data { - icons[fileType] = make(map[string]struct{}, len(files)) - for _, icon := range files { - icons[fileType][icon] = struct{}{} - iconList = append(iconList, "@walkxcode/"+icon) - } - } - return icons, iconList, nil -} - -/* -format: - - { - "Name": "2FAuth", - "Reference": "2fauth", - "SVG": "Yes", - "PNG": "Yes", - "WebP": "Yes", - "Light": "Yes", - "Category": "Self-Hosted", - "CreatedAt": "2024-08-16 00:27:23+00:00" - } -*/ -func fetchSelfhstIcons() (IconsMap, IconList, ReferenceDisplayNameMap, error) { - type SelfhStIcon struct { - Name string `json:"Name"` - Reference string `json:"Reference"` - SVG string `json:"SVG"` - PNG string `json:"PNG"` - WebP string `json:"WebP"` - // Light string - // Category string - // CreatedAt string - } - - req, err := http.NewRequest(http.MethodGet, selfhstIcons, nil) - if err != nil { - return nil, nil, nil, err - } - resp, err := http.DefaultClient.Do(req) - if err != nil { - return nil, nil, nil, err - } - defer resp.Body.Close() - - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, nil, nil, err - } - - data := make([]SelfhStIcon, 0, 2000) - err = json.Unmarshal(body, &data) - if err != nil { - return nil, nil, nil, err - } - - iconList := make(IconList, 0, len(data)*3) - icons := make(IconsMap) - icons["svg"] = make(map[string]struct{}, len(data)) - icons["png"] = make(map[string]struct{}, len(data)) - icons["webp"] = make(map[string]struct{}, len(data)) - - referenceToNames := make(ReferenceDisplayNameMap, len(data)) - - for _, item := range data { - if item.SVG == "Yes" { - icons["svg"][item.Reference+".svg"] = struct{}{} - iconList = append(iconList, "@selfhst/"+item.Reference+".svg") - } - if item.PNG == "Yes" { - icons["png"][item.Reference+".png"] = struct{}{} - iconList = append(iconList, "@selfhst/"+item.Reference+".png") - } - if item.WebP == "Yes" { - icons["webp"][item.Reference+".webp"] = struct{}{} - iconList = append(iconList, "@selfhst/"+item.Reference+".webp") - } - referenceToNames[item.Reference] = item.Name - } - - return icons, iconList, referenceToNames, nil -} diff --git a/internal/net/gphttp/loadbalancer/loadbalancer.go b/internal/net/gphttp/loadbalancer/loadbalancer.go index ce718dd..972bbec 100644 --- a/internal/net/gphttp/loadbalancer/loadbalancer.go +++ b/internal/net/gphttp/loadbalancer/loadbalancer.go @@ -133,11 +133,6 @@ func (lb *LoadBalancer) AddServer(srv Server) { lb.rebalance() lb.impl.OnAddServer(srv) - - lb.l.Debug(). - Str("action", "add"). - Str("server", srv.Name()). - Msgf("%d servers available", lb.pool.Size()) } func (lb *LoadBalancer) RemoveServer(srv Server) { diff --git a/internal/route/provider/event_handler.go b/internal/route/provider/event_handler.go index 2f2b939..1bf2998 100644 --- a/internal/route/provider/event_handler.go +++ b/internal/route/provider/event_handler.go @@ -12,19 +12,13 @@ import ( type EventHandler struct { provider *Provider - errs *gperr.Builder - added *gperr.Builder - removed *gperr.Builder - updated *gperr.Builder + errs *gperr.Builder } func (p *Provider) newEventHandler() *EventHandler { return &EventHandler{ provider: p, errs: gperr.NewBuilder("event errors"), - added: gperr.NewBuilder("added"), - removed: gperr.NewBuilder("removed"), - updated: gperr.NewBuilder("updated"), } } @@ -88,15 +82,12 @@ func (handler *EventHandler) Add(parent task.Parent, route *route.Route) { err := handler.provider.startRoute(parent, route) if err != nil { handler.errs.Add(err.Subject("add")) - } else { - handler.added.Adds(route.Alias) } } func (handler *EventHandler) Remove(route *route.Route) { route.Finish("route removed") delete(handler.provider.routes, route.Alias) - handler.removed.Adds(route.Alias) } func (handler *EventHandler) Update(parent task.Parent, oldRoute *route.Route, newRoute *route.Route) { @@ -104,18 +95,11 @@ func (handler *EventHandler) Update(parent task.Parent, oldRoute *route.Route, n err := handler.provider.startRoute(parent, newRoute) if err != nil { handler.errs.Add(err.Subject("update")) - } else { - handler.updated.Adds(newRoute.Alias) } } func (handler *EventHandler) Log() { - results := gperr.NewBuilder("event occurred") - results.AddFrom(handler.added, false) - results.AddFrom(handler.removed, false) - results.AddFrom(handler.updated, false) - results.AddFrom(handler.errs, false) - if result := results.String(); result != "" { - handler.provider.Logger().Info().Msg(result) + if err := handler.errs.Error(); err != nil { + handler.provider.Logger().Info().Msg(err.Error()) } } diff --git a/internal/route/reverse_proxy.go b/internal/route/reverse_proxy.go index d35c20e..3240cb3 100755 --- a/internal/route/reverse_proxy.go +++ b/internal/route/reverse_proxy.go @@ -22,19 +22,19 @@ import ( "github.com/yusing/go-proxy/internal/watcher/health/monitor" ) -type ( - ReveseProxyRoute struct { - *Route +type ReveseProxyRoute struct { + *Route - HealthMon health.HealthMonitor `json:"health,omitempty"` + HealthMon health.HealthMonitor `json:"health,omitempty"` - loadBalancer *loadbalancer.LoadBalancer - handler http.Handler - rp *reverseproxy.ReverseProxy + loadBalancer *loadbalancer.LoadBalancer + handler http.Handler + rp *reverseproxy.ReverseProxy - task *task.Task - } -) + task *task.Task +} + +var _ routes.ReverseProxyRoute = (*ReveseProxyRoute)(nil) // var globalMux = http.NewServeMux() // TODO: support regex subdomain matching. @@ -88,6 +88,11 @@ func NewReverseProxyRoute(base *Route) (*ReveseProxyRoute, gperr.Error) { return r, nil } +// ReverseProxy implements routes.ReverseProxyRoute. +func (r *ReveseProxyRoute) ReverseProxy() *reverseproxy.ReverseProxy { + return r.rp +} + // Start implements task.TaskStarter. func (r *ReveseProxyRoute) Start(parent task.Parent) gperr.Error { if existing, ok := routes.HTTP.Get(r.Key()); ok && !r.UseLoadBalance() { diff --git a/internal/route/route.go b/internal/route/route.go index 65ee8b7..4834d21 100644 --- a/internal/route/route.go +++ b/internal/route/route.go @@ -8,7 +8,6 @@ import ( "github.com/docker/docker/api/types/container" "github.com/yusing/go-proxy/agent/pkg/agent" - "github.com/yusing/go-proxy/internal" "github.com/yusing/go-proxy/internal/docker" "github.com/yusing/go-proxy/internal/gperr" "github.com/yusing/go-proxy/internal/homepage" @@ -484,7 +483,7 @@ func (r *Route) FinalizeHomepageConfig() { } else { key = r.Alias } - displayName, ok := internal.GetDisplayName(key) + displayName, ok := homepage.GetDisplayName(key) if ok { hp.Name = displayName } else {