mirror of
https://github.com/yusing/godoxy.git
synced 2026-01-18 09:27:17 +01:00
Compare commits
18 Commits
v0.11.2
...
v0.11.6-bu
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c49016f22c | ||
|
|
8da63daf02 | ||
|
|
c5fd21552e | ||
|
|
27409abc24 | ||
|
|
21c9e46274 | ||
|
|
22a12d3116 | ||
|
|
89d93dd878 | ||
|
|
66853dfc52 | ||
|
|
c72f66d64b | ||
|
|
59bc342a40 | ||
|
|
e11579df10 | ||
|
|
6a8f6fb4b5 | ||
|
|
8f20bd3840 | ||
|
|
f1abb745fe | ||
|
|
cb2990f6e8 | ||
|
|
fb2f850311 | ||
|
|
2b9c0f09ee | ||
|
|
efe3eb4ce7 |
11
.env.example
11
.env.example
@@ -1,3 +1,6 @@
|
||||
# docker image tag (latest, nightly)
|
||||
TAG=latest
|
||||
|
||||
# set timezone to get correct log timestamp
|
||||
TZ=ETC/UTC
|
||||
|
||||
@@ -47,8 +50,12 @@ GODOXY_API_ADDR=127.0.0.1:8888
|
||||
# Frontend listening port
|
||||
GODOXY_FRONTEND_PORT=3000
|
||||
|
||||
# Prometheus Metrics
|
||||
GODOXY_PROMETHEUS_ENABLED=true
|
||||
# Frontend aliases (subdomains / FQDNs, e.g. godoxy, godoxy.domain.com)
|
||||
GODOXY_FRONTEND_ALIASES=godoxy
|
||||
|
||||
# Docker socket
|
||||
# /var/run/podman/podman.sock for podman
|
||||
DOCKER_SOCKET=/var/run/docker.sock
|
||||
|
||||
# Debug mode
|
||||
GODOXY_DEBUG=false
|
||||
13
agent/go.mod
13
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
|
||||
|
||||
26
agent/go.sum
26
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=
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
services:
|
||||
frontend:
|
||||
image: ghcr.io/yusing/godoxy-frontend:latest
|
||||
image: ghcr.io/yusing/godoxy-frontend:${TAG:-latest}
|
||||
container_name: godoxy-frontend
|
||||
restart: unless-stopped
|
||||
network_mode: host # do not change this
|
||||
@@ -13,7 +13,7 @@ services:
|
||||
|
||||
# modify below to fit your needs
|
||||
labels:
|
||||
proxy.aliases: godoxy
|
||||
proxy.aliases: ${GODOXY_FRONTEND_ALIASES:-godoxy}
|
||||
proxy.godoxy.port: ${GODOXY_FRONTEND_PORT:-3000}
|
||||
# proxy.godoxy.middlewares.cidr_whitelist: |
|
||||
# status: 403
|
||||
@@ -24,13 +24,13 @@ services:
|
||||
# - 192.168.0.0/16
|
||||
# - 172.16.0.0/12
|
||||
app:
|
||||
image: ghcr.io/yusing/godoxy:latest
|
||||
image: ghcr.io/yusing/godoxy:${TAG:-latest}
|
||||
container_name: godoxy
|
||||
restart: always
|
||||
network_mode: host # do not change this
|
||||
env_file: .env
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- ${DOCKER_SOCKET:-/var/run/docker.sock}:/var/run/docker.sock
|
||||
- ./config:/app/config
|
||||
- ./logs:/app/logs
|
||||
- ./error_pages:/app/error_pages
|
||||
|
||||
14
go.mod
14
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
|
||||
|
||||
19
go.sum
19
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=
|
||||
|
||||
@@ -3,11 +3,9 @@ package acl
|
||||
import (
|
||||
"github.com/puzpuzpuz/xsync/v3"
|
||||
acl "github.com/yusing/go-proxy/internal/acl/types"
|
||||
"go.uber.org/atomic"
|
||||
)
|
||||
|
||||
var cityCache = xsync.NewMapOf[string, *acl.City]()
|
||||
var numCachedLookup atomic.Uint64
|
||||
|
||||
func (cfg *MaxMindConfig) lookupCity(ip *acl.IPInfo) (*acl.City, bool) {
|
||||
if ip.City != nil {
|
||||
@@ -20,7 +18,7 @@ func (cfg *MaxMindConfig) lookupCity(ip *acl.IPInfo) (*acl.City, bool) {
|
||||
|
||||
city, ok := cityCache.Load(ip.Str)
|
||||
if ok {
|
||||
numCachedLookup.Inc()
|
||||
ip.City = city
|
||||
return city, true
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"github.com/puzpuzpuz/xsync/v3"
|
||||
"github.com/rs/zerolog"
|
||||
acl "github.com/yusing/go-proxy/internal/acl/types"
|
||||
"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/logging/accesslog"
|
||||
@@ -63,7 +64,7 @@ type checkCache struct {
|
||||
const cacheTTL = 1 * time.Minute
|
||||
|
||||
func (c *checkCache) Expired() bool {
|
||||
return c.created.Add(cacheTTL).After(utils.TimeNow())
|
||||
return c.created.Add(cacheTTL).Before(utils.TimeNow())
|
||||
}
|
||||
|
||||
//TODO: add stats
|
||||
@@ -153,7 +154,10 @@ func (c *Config) Start(parent *task.Task) gperr.Error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *config) cacheRecord(info *acl.IPInfo, allow bool) {
|
||||
func (c *Config) cacheRecord(info *acl.IPInfo, allow bool) {
|
||||
if common.ForceResolveCountry && info.City == nil {
|
||||
c.MaxMind.lookupCity(info)
|
||||
}
|
||||
c.ipCache.Store(info.Str, &checkCache{
|
||||
IPInfo: info,
|
||||
allow: allow,
|
||||
@@ -175,7 +179,7 @@ func (c *Config) IPAllowed(ip net.IP) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// always allow private and loopback
|
||||
// always allow loopback
|
||||
// loopback is not logged
|
||||
if ip.IsLoopback() {
|
||||
return true
|
||||
|
||||
@@ -54,7 +54,7 @@ func (cfg *MaxMindConfig) LoadMaxMindDB(parent task.Parent) gperr.Error {
|
||||
|
||||
path := dbPath(cfg.Database)
|
||||
reader, err := maxmindDBOpen(path)
|
||||
exists := true
|
||||
valid := true
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, os.ErrNotExist):
|
||||
@@ -65,20 +65,19 @@ func (cfg *MaxMindConfig) LoadMaxMindDB(parent task.Parent) gperr.Error {
|
||||
return gperr.Wrap(err)
|
||||
}
|
||||
}
|
||||
exists = false
|
||||
valid = false
|
||||
}
|
||||
|
||||
if !exists {
|
||||
if !valid {
|
||||
cfg.logger.Info().Msg("MaxMind DB not found/invalid, downloading...")
|
||||
reader, err = cfg.download()
|
||||
if err != nil {
|
||||
if err = cfg.download(); err != nil {
|
||||
return ErrDownloadFailure.With(err)
|
||||
}
|
||||
} else {
|
||||
cfg.logger.Info().Msg("MaxMind DB loaded")
|
||||
cfg.db.Reader = reader
|
||||
go cfg.scheduleUpdate(parent)
|
||||
}
|
||||
cfg.logger.Info().Msg("MaxMind DB loaded")
|
||||
|
||||
cfg.db.Reader = reader
|
||||
go cfg.scheduleUpdate(parent)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -137,17 +136,10 @@ func (cfg *MaxMindConfig) update() {
|
||||
Time("latest", remoteLastModified.Local()).
|
||||
Time("current", cfg.lastUpdate).
|
||||
Msg("MaxMind DB update available")
|
||||
reader, err := cfg.download()
|
||||
if err != nil {
|
||||
if err = cfg.download(); err != nil {
|
||||
cfg.logger.Err(err).Msg("failed to update MaxMind DB")
|
||||
return
|
||||
}
|
||||
cfg.db.Lock()
|
||||
cfg.db.Close()
|
||||
cfg.db.Reader = reader
|
||||
cfg.setLastUpdate(*remoteLastModified)
|
||||
cfg.db.Unlock()
|
||||
|
||||
cfg.logger.Info().Msg("MaxMind DB updated")
|
||||
}
|
||||
|
||||
@@ -190,57 +182,87 @@ func (cfg *MaxMindConfig) checkLastest() (lastModifiedT *time.Time, err error) {
|
||||
return &lastModifiedTime, nil
|
||||
}
|
||||
|
||||
func (cfg *MaxMindConfig) download() (*maxminddb.Reader, error) {
|
||||
func (cfg *MaxMindConfig) download() error {
|
||||
resp, err := newReq(cfg, http.MethodGet)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("%w: %d", ErrResponseNotOK, resp.StatusCode)
|
||||
return fmt.Errorf("%w: %d", ErrResponseNotOK, resp.StatusCode)
|
||||
}
|
||||
|
||||
path := dbPath(cfg.Database)
|
||||
tmpPath := path + "-tmp.tar.gz"
|
||||
file, err := os.OpenFile(tmpPath, os.O_CREATE|os.O_WRONLY, 0o644)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dbFile := dbPath(cfg.Database)
|
||||
tmpGZPath := dbFile + "-tmp.tar.gz"
|
||||
tmpDBPath := dbFile + "-tmp"
|
||||
|
||||
cfg.logger.Info().Msg("MaxMind DB downloading...")
|
||||
|
||||
_, err = io.Copy(file, resp.Body)
|
||||
if err != nil {
|
||||
file.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
file.Close()
|
||||
|
||||
// extract .tar.gz and move only the dbFilename to path
|
||||
err = extractFileFromTarGz(tmpPath, dbFilename(cfg.Database), path)
|
||||
if err != nil {
|
||||
return nil, gperr.New("failed to extract database from archive").With(err)
|
||||
}
|
||||
// cleanup the tar.gz file
|
||||
_ = os.Remove(tmpPath)
|
||||
|
||||
db, err := maxmindDBOpen(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return db, nil
|
||||
}
|
||||
|
||||
func extractFileFromTarGz(tarGzPath, targetFilename, destPath string) error {
|
||||
f, err := os.Open(tarGzPath)
|
||||
tmpGZFile, err := os.OpenFile(tmpGZPath, os.O_CREATE|os.O_RDWR, 0o644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
gzr, err := gzip.NewReader(f)
|
||||
// cleanup the tar.gz file
|
||||
defer func() {
|
||||
_ = tmpGZFile.Close()
|
||||
_ = os.Remove(tmpGZPath)
|
||||
}()
|
||||
|
||||
cfg.logger.Info().Msg("MaxMind DB downloading...")
|
||||
|
||||
_, err = io.Copy(tmpGZFile, resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := tmpGZFile.Seek(0, io.SeekStart); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// extract .tar.gz and to database
|
||||
err = extractFileFromTarGz(tmpGZFile, dbFilename(cfg.Database), tmpDBPath)
|
||||
|
||||
if err != nil {
|
||||
return gperr.New("failed to extract database from archive").With(err)
|
||||
}
|
||||
|
||||
// test if the downloaded database is valid
|
||||
db, err := maxmindDBOpen(tmpDBPath)
|
||||
if err != nil {
|
||||
_ = os.Remove(tmpDBPath)
|
||||
return err
|
||||
}
|
||||
|
||||
db.Close()
|
||||
err = os.Rename(tmpDBPath, dbFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cfg.db.Lock()
|
||||
defer cfg.db.Unlock()
|
||||
if cfg.db.Reader != nil {
|
||||
cfg.db.Reader.Close()
|
||||
}
|
||||
cfg.db.Reader, err = maxmindDBOpen(dbFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
lastModifiedStr := resp.Header.Get("Last-Modified")
|
||||
lastModifiedTime, err := time.Parse(http.TimeFormat, lastModifiedStr)
|
||||
if err == nil {
|
||||
cfg.setLastUpdate(lastModifiedTime)
|
||||
}
|
||||
|
||||
cfg.logger.Info().Msg("MaxMind DB downloaded")
|
||||
return nil
|
||||
}
|
||||
|
||||
func extractFileFromTarGz(tarGzFile *os.File, targetFilename, destPath string) error {
|
||||
defer tarGzFile.Close()
|
||||
|
||||
gzr, err := gzip.NewReader(tarGzFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package acl
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"compress/gzip"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
@@ -144,9 +146,17 @@ func Test_MaxMindConfig_download(t *testing.T) {
|
||||
logger: zerolog.Nop(),
|
||||
}
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
io.Copy(w, strings.NewReader("FAKEMMDB"))
|
||||
gz := gzip.NewWriter(w)
|
||||
t := tar.NewWriter(gz)
|
||||
t.WriteHeader(&tar.Header{
|
||||
Name: dbFilename(MaxMindGeoLite),
|
||||
})
|
||||
t.Write([]byte("1234"))
|
||||
t.Close()
|
||||
gz.Close()
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
oldURL := dbURL
|
||||
dbURL = func(MaxMindDatabaseType) string { return server.URL }
|
||||
defer func() { dbURL = oldURL }()
|
||||
@@ -163,26 +173,26 @@ func Test_MaxMindConfig_download(t *testing.T) {
|
||||
}
|
||||
defer func() { maxmindDBOpen = origOpen }()
|
||||
|
||||
rw := &fakeReadCloser{}
|
||||
req, err := http.NewRequest(http.MethodGet, server.URL, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("newReq() error = %v", err)
|
||||
}
|
||||
|
||||
rw := httptest.NewRecorder()
|
||||
oldNewReq := newReq
|
||||
newReq = func(cfg *MaxMindConfig, method string) (*http.Response, error) {
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: rw,
|
||||
}, nil
|
||||
server.Config.Handler.ServeHTTP(rw, req)
|
||||
return rw.Result(), nil
|
||||
}
|
||||
defer func() { newReq = oldNewReq }()
|
||||
|
||||
db, err := cfg.download()
|
||||
err = cfg.download()
|
||||
if err != nil {
|
||||
t.Fatalf("download() error = %v", err)
|
||||
}
|
||||
if db == nil {
|
||||
if cfg.db.Reader == nil {
|
||||
t.Error("expected db instance")
|
||||
}
|
||||
if !rw.closed {
|
||||
t.Error("expected rw to be closed")
|
||||
}
|
||||
}
|
||||
|
||||
func Test_MaxMindConfig_loadMaxMindDB(t *testing.T) {
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package acl
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
type TCPListener struct {
|
||||
@@ -9,6 +11,17 @@ type TCPListener struct {
|
||||
lis net.Listener
|
||||
}
|
||||
|
||||
type noConn struct{}
|
||||
|
||||
func (noConn) Read(b []byte) (int, error) { return 0, io.EOF }
|
||||
func (noConn) Write(b []byte) (int, error) { return 0, io.EOF }
|
||||
func (noConn) Close() error { return nil }
|
||||
func (noConn) LocalAddr() net.Addr { return nil }
|
||||
func (noConn) RemoteAddr() net.Addr { return nil }
|
||||
func (noConn) SetDeadline(t time.Time) error { return nil }
|
||||
func (noConn) SetReadDeadline(t time.Time) error { return nil }
|
||||
func (noConn) SetWriteDeadline(t time.Time) error { return nil }
|
||||
|
||||
func (cfg *Config) WrapTCP(lis net.Listener) net.Listener {
|
||||
if cfg == nil {
|
||||
return lis
|
||||
@@ -32,11 +45,11 @@ func (s *TCPListener) Accept() (net.Conn, error) {
|
||||
if !ok {
|
||||
// Not a TCPAddr, drop
|
||||
c.Close()
|
||||
return nil, nil
|
||||
return noConn{}, nil
|
||||
}
|
||||
if !s.acl.IPAllowed(addr.IP) {
|
||||
c.Close()
|
||||
return nil, nil
|
||||
return noConn{}, nil
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/yusing/go-proxy/internal/common"
|
||||
@@ -38,22 +39,40 @@ 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)
|
||||
}
|
||||
return next
|
||||
}
|
||||
|
||||
func AuthCheckHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if err := defaultAuth.CheckToken(r); err != nil {
|
||||
http.Redirect(w, r, "/v1/auth/login", http.StatusFound)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
func AuthCheckHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if err := defaultAuth.CheckToken(r); err != nil {
|
||||
defaultAuth.LoginHandler(w, r)
|
||||
} else {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -82,10 +82,6 @@ func (cfg *Config) Validate() gperr.Error {
|
||||
}
|
||||
|
||||
func (cfg *Config) GetLegoConfig() (*User, *lego.Config, gperr.Error) {
|
||||
if cfg == nil {
|
||||
cfg = new(Config)
|
||||
}
|
||||
|
||||
if err := cfg.Validate(); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@ func (p *Provider) ObtainCert() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
if p.cfg.Provider == ProviderLocal {
|
||||
if p.cfg.Provider == ProviderPseudo {
|
||||
t := time.NewTicker(1000 * time.Millisecond)
|
||||
defer t.Stop()
|
||||
logging.Info().Msg("init client for pseudo provider")
|
||||
|
||||
@@ -16,7 +16,6 @@ const (
|
||||
ConfigPath = ConfigBasePath + "/" + ConfigFileName
|
||||
|
||||
IconListCachePath = ConfigBasePath + "/.icon_list_cache.json"
|
||||
IconCachePath = ConfigBasePath + "/.icon_cache.json"
|
||||
|
||||
NamespaceHomepageOverrides = ".homepage"
|
||||
NamespaceIconCache = ".icon_cache"
|
||||
|
||||
@@ -58,6 +58,8 @@ var (
|
||||
MetricsDisableDisk = GetEnvBool("METRICS_DISABLE_DISK", false)
|
||||
MetricsDisableNetwork = GetEnvBool("METRICS_DISABLE_NETWORK", false)
|
||||
MetricsDisableSensors = GetEnvBool("METRICS_DISABLE_SENSORS", false)
|
||||
|
||||
ForceResolveCountry = GetEnvBool("FORCE_RESOLVE_COUNTRY", false)
|
||||
)
|
||||
|
||||
func GetEnv[T any](key string, defaultValue T, parser func(string) (T, error)) T {
|
||||
|
||||
@@ -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
|
||||
@@ -265,6 +267,10 @@ func (cfg *Config) initAutoCert(autocertCfg *autocert.Config) gperr.Error {
|
||||
return nil
|
||||
}
|
||||
|
||||
if autocertCfg == nil {
|
||||
autocertCfg = new(autocert.Config)
|
||||
}
|
||||
|
||||
user, legoCfg, err := autocertCfg.GetLegoConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -274,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())
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"bufio"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -53,6 +54,7 @@ type (
|
||||
)
|
||||
|
||||
const (
|
||||
StdoutbufSize = 64
|
||||
MinBufferSize = 4 * kilobyte
|
||||
MaxBufferSize = 1 * megabyte
|
||||
)
|
||||
@@ -79,6 +81,22 @@ func NewMockAccessLogger(parent task.Parent, cfg *RequestLoggerConfig) *AccessLo
|
||||
return NewAccessLoggerWithIO(parent, NewMockFile(), cfg)
|
||||
}
|
||||
|
||||
func unwrap[Writer any](w io.Writer) []Writer {
|
||||
var result []Writer
|
||||
if unwrapped, ok := w.(MultiWriterInterface); ok {
|
||||
for _, w := range unwrapped.Unwrap() {
|
||||
if unwrapped, ok := w.(Writer); ok {
|
||||
result = append(result, unwrapped)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
if unwrapped, ok := w.(Writer); ok {
|
||||
return []Writer{unwrapped}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewAccessLoggerWithIO(parent task.Parent, writer WriterWithName, anyCfg AnyConfig) *AccessLogger {
|
||||
cfg := anyCfg.ToConfig()
|
||||
if cfg.BufferSize == 0 {
|
||||
@@ -90,6 +108,10 @@ func NewAccessLoggerWithIO(parent task.Parent, writer WriterWithName, anyCfg Any
|
||||
if cfg.BufferSize > MaxBufferSize {
|
||||
cfg.BufferSize = MaxBufferSize
|
||||
}
|
||||
if _, ok := writer.(*os.File); ok {
|
||||
cfg.BufferSize = StdoutbufSize
|
||||
}
|
||||
|
||||
l := &AccessLogger{
|
||||
task: parent.Subtask("accesslog."+writer.Name(), true),
|
||||
cfg: cfg,
|
||||
@@ -99,23 +121,8 @@ func NewAccessLoggerWithIO(parent task.Parent, writer WriterWithName, anyCfg Any
|
||||
logger: logging.With().Str("file", writer.Name()).Logger(),
|
||||
}
|
||||
|
||||
if unwrapped, ok := writer.(MultiWriterInterface); ok {
|
||||
for _, w := range unwrapped.Unwrap() {
|
||||
if sr, ok := w.(supportRotate); ok {
|
||||
l.supportRotate = append(l.supportRotate, sr)
|
||||
}
|
||||
if closer, ok := w.(io.Closer); ok {
|
||||
l.closer = append(l.closer, closer)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if sr, ok := writer.(supportRotate); ok {
|
||||
l.supportRotate = append(l.supportRotate, sr)
|
||||
}
|
||||
if closer, ok := writer.(io.Closer); ok {
|
||||
l.closer = append(l.closer, closer)
|
||||
}
|
||||
}
|
||||
l.supportRotate = unwrap[supportRotate](writer)
|
||||
l.closer = unwrap[io.Closer](writer)
|
||||
|
||||
if cfg.req != nil {
|
||||
fmt := CommonFormatter{cfg: &cfg.req.Fields}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -17,8 +17,7 @@ import (
|
||||
)
|
||||
|
||||
type cloudflareRealIP struct {
|
||||
realIP realIP
|
||||
Recursive bool
|
||||
realIP realIP
|
||||
}
|
||||
|
||||
const (
|
||||
@@ -47,7 +46,7 @@ var CloudflareRealIP = NewMiddleware[cloudflareRealIP]()
|
||||
func (cri *cloudflareRealIP) setup() {
|
||||
cri.realIP.RealIPOpts = RealIPOpts{
|
||||
Header: "CF-Connecting-IP",
|
||||
Recursive: cri.Recursive,
|
||||
Recursive: true,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -35,8 +35,8 @@ var allMiddlewares = map[string]*Middleware{
|
||||
}
|
||||
|
||||
var (
|
||||
ErrUnknownMiddleware = gperr.New("unknown middleware")
|
||||
ErrDuplicatedMiddleware = gperr.New("duplicated middleware")
|
||||
ErrUnknownMiddleware = gperr.New("unknown middleware")
|
||||
ErrMiddlewareAlreadyExists = gperr.New("middleware with the same name already exists")
|
||||
)
|
||||
|
||||
func Get(name string) (*Middleware, Error) {
|
||||
@@ -69,7 +69,7 @@ func LoadComposeFiles() {
|
||||
for name, m := range mws {
|
||||
name = strutils.ToLowerNoSnake(name)
|
||||
if _, ok := allMiddlewares[name]; ok {
|
||||
errs.Add(ErrDuplicatedMiddleware.Subject(name))
|
||||
errs.Add(ErrMiddlewareAlreadyExists.Subject(name))
|
||||
continue
|
||||
}
|
||||
allMiddlewares[name] = m
|
||||
|
||||
@@ -83,9 +83,6 @@ func (disp *Dispatcher) start() {
|
||||
}
|
||||
|
||||
func (disp *Dispatcher) dispatch(msg *LogMessage) {
|
||||
if true {
|
||||
return
|
||||
}
|
||||
task := disp.task.Subtask("dispatcher")
|
||||
defer task.Finish("notif dispatched")
|
||||
|
||||
|
||||
@@ -80,8 +80,11 @@ func (p *DockerProvider) loadRoutesImpl() (route.Routes, gperr.Error) {
|
||||
errs.Add(err.Subject(container.ContainerName))
|
||||
}
|
||||
for k, v := range newEntries {
|
||||
if routes.Contains(k) {
|
||||
errs.Addf("duplicated alias %s", k)
|
||||
if conflict, ok := routes[k]; ok {
|
||||
errs.Add(gperr.Multiline().
|
||||
Addf("route with alias %s already exists", k).
|
||||
Addf("container %s", container.ContainerName).
|
||||
Addf("conflicting container %s", conflict.Container.ContainerName))
|
||||
} else {
|
||||
routes[k] = v
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -60,7 +60,7 @@ func Ver(major, minor, patch int) Version {
|
||||
}
|
||||
|
||||
func (v Version) String() string {
|
||||
return fmt.Sprintf("%d.%d.%d", v.Generation, v.Major, v.Minor)
|
||||
return fmt.Sprintf("v%d.%d.%d", v.Generation, v.Major, v.Minor)
|
||||
}
|
||||
|
||||
func (v Version) MarshalText() ([]byte, error) {
|
||||
@@ -101,17 +101,17 @@ func ParseVersion(v string) (ver Version) {
|
||||
if len(parts) != 3 {
|
||||
return
|
||||
}
|
||||
major, err := strconv.Atoi(parts[0])
|
||||
gen, err := strconv.Atoi(parts[0])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
minor, err := strconv.Atoi(parts[1])
|
||||
major, err := strconv.Atoi(parts[1])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
patch, err := strconv.Atoi(parts[2])
|
||||
minor, err := strconv.Atoi(parts[2])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return Ver(major, minor, patch)
|
||||
return Ver(gen, major, minor)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user