Compare commits
69 commits
d0eeb4ea32
...
c1c9bc8971
| Author | SHA1 | Date | |
|---|---|---|---|
| c1c9bc8971 | |||
| 4c1306d66f | |||
| de6eb2b3b4 | |||
| 8e2e38757f | |||
| 6dfe3f9d42 | |||
| 397e19b745 | |||
| cae8658f61 | |||
| f92cab2abf | |||
| 30506e9731 | |||
| 15f014cf5a | |||
| b2e85cb9f2 | |||
| 2679d0476e | |||
| fa47e9b4b1 | |||
| 17c9e88903 | |||
| 47d7893b91 | |||
| e09044388b | |||
| 3e3b23a0c4 | |||
| e30d1a5f50 | |||
| 4f3a1d7f7b | |||
| 7081f3d571 | |||
| 899f3e0989 | |||
| c7c674f2ae | |||
| 4efff8b0a0 | |||
| e9db206e78 | |||
| af517907d6 | |||
| c945ba30ed | |||
| e7b1f4780e | |||
| 987c1bb72e | |||
| 25f37af35d | |||
| e1eb4dec90 | |||
| d298992b63 | |||
| aba39001d8 | |||
| d600bbcbd9 | |||
| e103d2262a | |||
| ca9dc450c3 | |||
| 021d8bd8f9 | |||
| 8adc08b4c0 | |||
| 3f2c923754 | |||
| 1523b549d2 | |||
| e95ecd6671 | |||
| ea0324c3a7 | |||
| d06b73c3dd | |||
| 8c9a38166b | |||
| e89a483725 | |||
| 074e5e864e | |||
| e20f1dce9d | |||
| 40b890a8e3 | |||
| 08c4749607 | |||
| 8e82d22c77 | |||
| 277e45e807 | |||
| 08a365b8e8 | |||
| e571a629b7 | |||
| 5761850c7f | |||
| 85b422ed39 | |||
| b60dd41455 | |||
| 7d89e120a9 | |||
| a47d0d4196 | |||
| 94f1884ba5 | |||
| 6068e81ff6 | |||
| e183aa6ea9 | |||
| c020aeece7 | |||
| 38caea104f | |||
| ae88b5d892 | |||
| 17d6ebd607 | |||
| 1b4cffec2d | |||
| 11aea3d303 | |||
| bff4e02273 | |||
| 5e0eaa5d11 | |||
| 4f9a308a2d |
339 changed files with 6810 additions and 3252 deletions
|
|
@ -24,9 +24,9 @@ steps:
|
|||
- tar --transform="s@.@./happydomain-${DRONE_COMMIT}@" --exclude-vcs --exclude=./web/node_modules/.cache -czf /dev/shm/happydomain-src.tar.gz .
|
||||
- mkdir deploy
|
||||
- mv /dev/shm/happydomain-src.tar.gz deploy
|
||||
- yarn --cwd web --offline generate:api && sed -i "s/hey-api\.ts';/hey-api';/" web/src/lib/api-base/client.gen.ts
|
||||
- yarn --cwd web --offline run svelte-kit sync && yarn --cwd web --offline generate:api && sed -i "s/hey-api\.ts';/hey-api';/" web/src/lib/api-base/client.gen.ts
|
||||
- yarn --cwd web --offline build
|
||||
- yarn --cwd web-admin --offline generate:api && sed -i "s/hey-api\.ts';/hey-api';/" web/src/lib/api-admin/client.gen.ts
|
||||
- yarn --cwd web-admin --offline run svelte-kit sync && yarn --cwd web-admin --offline generate:api && sed -i "s/hey-api\.ts';/hey-api';/" web/src/lib/api-admin/client.gen.ts
|
||||
- yarn --cwd web-admin --offline build
|
||||
|
||||
- name: backend-commit
|
||||
|
|
@ -218,11 +218,11 @@ steps:
|
|||
commands:
|
||||
- cd web
|
||||
- npm install --network-timeout=100000
|
||||
- npm run generate:api
|
||||
- npx svelte-kit sync && npm run generate:api
|
||||
- npm test
|
||||
- npm run build
|
||||
- cd ../web-admin
|
||||
- npm run generate:api
|
||||
- npx svelte-kit sync && npm run generate:api
|
||||
- npm test
|
||||
- npm run build
|
||||
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ package main
|
|||
//go:generate go run tools/gen_icon.go providers providers
|
||||
//go:generate go run tools/gen_icon.go services svcs
|
||||
//go:generate go run tools/gen_rr_typescript.go web/src/lib/dns_rr.ts
|
||||
//go:generate go run tools/gen_service_specs.go -o web/src/lib/services_specs.ts
|
||||
//go:generate go run tools/gen_dns_type_mapping.go -o internal/usecase/service_specs_dns_types.go
|
||||
//go:generate swag init --exclude internal/api-admin/ --generalInfo internal/api/route/route.go
|
||||
//go:generate swag init --output docs-admin --exclude internal/api/ --generalInfo internal/api-admin/route/route.go
|
||||
|
|
|
|||
13
go.mod
13
go.mod
|
|
@ -9,14 +9,16 @@ require (
|
|||
github.com/altcha-org/altcha-lib-go v1.0.0
|
||||
github.com/coreos/go-oidc/v3 v3.17.0
|
||||
github.com/earthboundkid/versioninfo/v2 v2.24.1
|
||||
github.com/fatih/color v1.18.0
|
||||
github.com/gin-contrib/sessions v1.0.4
|
||||
github.com/fatih/color v1.19.0
|
||||
github.com/gin-contrib/sessions v1.1.0
|
||||
github.com/gin-gonic/gin v1.12.0
|
||||
github.com/go-mail/mail v2.3.1+incompatible
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1
|
||||
github.com/gorilla/securecookie v1.1.2
|
||||
github.com/gorilla/sessions v1.4.0
|
||||
github.com/lib/pq v1.11.2
|
||||
github.com/lib/pq v1.12.3
|
||||
github.com/libdns/ionos v1.2.0
|
||||
github.com/libdns/libdns v1.1.1
|
||||
github.com/miekg/dns v1.1.72
|
||||
github.com/mileusna/useragent v1.3.5
|
||||
github.com/oracle/nosql-go-sdk v1.4.7
|
||||
|
|
@ -26,7 +28,7 @@ require (
|
|||
github.com/swaggo/gin-swagger v1.6.1
|
||||
github.com/swaggo/swag v1.16.6
|
||||
github.com/syndtr/goleveldb v1.0.0
|
||||
github.com/yuin/goldmark v1.7.16
|
||||
github.com/yuin/goldmark v1.8.2
|
||||
golang.org/x/crypto v0.49.0
|
||||
golang.org/x/oauth2 v0.36.0
|
||||
)
|
||||
|
|
@ -74,7 +76,6 @@ require (
|
|||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.8 // indirect
|
||||
github.com/aws/smithy-go v1.24.2 // indirect
|
||||
github.com/aymerick/douceur v0.2.0 // indirect
|
||||
github.com/babolivier/go-doh-client v0.0.0-20201028162107-a76cff4cb8b6 // indirect
|
||||
github.com/benbjohnson/clock v1.3.5 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/billputer/go-namecheap v0.0.0-20210108011502-994a912fb7f9 // indirect
|
||||
|
|
@ -90,7 +91,6 @@ require (
|
|||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/deepmap/oapi-codegen v1.16.3 // indirect
|
||||
github.com/digitalocean/godo v1.176.0 // indirect
|
||||
github.com/dnsimple/dnsimple-go v1.7.0 // indirect
|
||||
github.com/dnsimple/dnsimple-go/v8 v8.1.0 // indirect
|
||||
github.com/exoscale/egoscale v0.102.4 // indirect
|
||||
github.com/failsafe-go/failsafe-go v0.9.6 // indirect
|
||||
|
|
@ -216,7 +216,6 @@ require (
|
|||
go.opentelemetry.io/otel v1.40.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.40.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.40.0 // indirect
|
||||
go.uber.org/mock v0.6.0 // indirect
|
||||
go.uber.org/ratelimit v0.3.1 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
|
|
|
|||
86
go.sum
86
go.sum
|
|
@ -5,8 +5,6 @@ cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIi
|
|||
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
|
||||
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
|
||||
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
|
||||
codeberg.org/miekg/dns v0.6.62 h1:3Uua303EC8Og75QqT+pGRrcvKNTOouehHOQS36KbSqc=
|
||||
codeberg.org/miekg/dns v0.6.62/go.mod h1:fIxAzBMDPnXWSw0fp8+pfZMRiAqYY4+HHYLzUo/S6Dg=
|
||||
codeberg.org/miekg/dns v0.6.67 h1:vsVNsqAOE9uYscJHIHNtoCxiEySQn/B9BEvAUYI5Zmc=
|
||||
codeberg.org/miekg/dns v0.6.67/go.mod h1:58Y3ZTg6Z5ZEm/ZAAwHehbZfrD4u5mE4RByHoPEMyKk=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
|
|
@ -75,74 +73,40 @@ github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP
|
|||
github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg=
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.2 h1:LuT2rzqNQsauaGkPK/7813XxcZ3o3yePY0Iy891T2ls=
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.2/go.mod h1:IvvlAZQXvTXznUPfRVfryiG1fbzE2NGK6m9u39YQ+S4=
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.3 h1:4kQ/fa22KjDt13QCy1+bYADvdgcxpfH18f0zP542kZA=
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.3/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.10 h1:9DMthfO6XWZYLfzZglAgW5Fyou2nRI5CuV44sTedKBI=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.10/go.mod h1:2rUIOnA2JaiqYmSKYmRJlcMWy6qTj1vuRFscppSBMcw=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.11 h1:ftxI5sgz8jZkckuUHXfC/wMUc8u3fG1vQS0plr2F2Zs=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.11/go.mod h1:twF11+6ps9aNRKEDimksp923o44w/Thk9+8YIlzWMmo=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.10 h1:EEhmEUFCE1Yhl7vDhNOI5OCL/iKMdkkYFTRpZXNw7m8=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.10/go.mod h1:RnnlFCAlxQCkN2Q379B67USkBMu1PipEEiibzYN5UTE=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.11 h1:NdV8cwCcAXrCWyxArt58BrvZJ9pZ9Fhf9w6Uh5W3Uyc=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.11/go.mod h1:30yY2zqkMPdrvxBqzI9xQCM+WrlrZKSOpSJEsylVU+8=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.18 h1:Ii4s+Sq3yDfaMLpjrJsqD6SmG/Wq/P5L/hw2qa78UAY=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.18/go.mod h1:6x81qnY++ovptLE6nWQeWrpXxbnlIex+4H4eYYGcqfc=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.19 h1:INUvJxmhdEbVulJYHI061k4TVuS3jzzthNvjqvVvTKM=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.19/go.mod h1:FpZN2QISLdEBWkayloda+sZjVJL+e9Gl0k1SyTgcswU=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18 h1:F43zk1vemYIqPAwhjTjYIz0irU2EY7sOb/F5eJ3HuyM=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18/go.mod h1:w1jdlZXrGKaJcNoL+Nnrj+k5wlpGXqnNrKoP22HvAug=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.19 h1:/sECfyq2JTifMI2JPyZ4bdRN77zJmr6SrS1eL3augIA=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.19/go.mod h1:dMf8A5oAqr9/oxOfLkC/c2LU/uMcALP0Rgn2BD5LWn0=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18 h1:xCeWVjj0ki0l3nruoyP2slHsGArMxeiiaoPN5QZH6YQ=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18/go.mod h1:r/eLGuGCBw6l36ZRWiw6PaZwPXb6YOj+i/7MizNl5/k=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.19 h1:AWeJMk33GTBf6J20XJe6qZoRSJo0WfUhsMdUKhoODXE=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.19/go.mod h1:+GWrYoaAsV7/4pNHpwh1kiNLXkKaSoppxQq9lbH8Ejw=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.5 h1:clHU5fm//kWS1C2HgtgWxfQbFbx4b6rx+5jzhgX9HrI=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.5/go.mod h1:O3h0IK87yXci+kg6flUKzJnWeziQUKciKrLjcatSNcY=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.5 h1:CeY9LUdur+Dxoeldqoun6y4WtJ3RQtzk0JMP2gfUay0=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.5/go.mod h1:AZLZf2fMaahW5s/wMRciu1sYbdsikT/UHwbUjOdEVTc=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.6 h1:XAq62tBTJP/85lFD5oqOOe7YYgWxY9LvWq8plyDvDVg=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.6/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.18 h1:LTRCYFlnnKFlKsyIQxKhJuDuA3ZkrDQMRYm6rXiHlLY=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.18/go.mod h1:XhwkgGG6bHSd00nO/mexWTcTjgd6PjuvWQMqSn2UaEk=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.19 h1:X1Tow7suZk9UCJHE1Iw9GMZJJl0dAnKXXP1NaSDHwmw=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.19/go.mod h1:/rARO8psX+4sfjUQXp5LLifjUt8DuATZ31WptNJTyQA=
|
||||
github.com/aws/aws-sdk-go-v2/service/route53 v1.62.2 h1:zoD/SoiVQi8l8tuQn//VexrXS2yorg/+717JNA4Ble8=
|
||||
github.com/aws/aws-sdk-go-v2/service/route53 v1.62.2/go.mod h1:Ll1DCasPTBFtHK5t/U5WIwGIyRuY3xY+x8/LmqIlqpM=
|
||||
github.com/aws/aws-sdk-go-v2/service/route53 v1.62.3 h1:JRPXnIr0WwFsSHBmuCvT/uh0Vgys+crvwkOghbJEqi8=
|
||||
github.com/aws/aws-sdk-go-v2/service/route53 v1.62.3/go.mod h1:DHddp7OO4bY467WVCqWBzk5+aEWn7vqYkap7UigJzGk=
|
||||
github.com/aws/aws-sdk-go-v2/service/route53domains v1.34.16 h1:k+TqYbG/WtL43wSCALuuPjLPEt//Ck/ZDKpCWrzhjUU=
|
||||
github.com/aws/aws-sdk-go-v2/service/route53domains v1.34.16/go.mod h1:yEr1gPPNbetOFxQV0J9ZLL5cR4U4ujEBgwk6p6oKYc8=
|
||||
github.com/aws/aws-sdk-go-v2/service/route53domains v1.34.17 h1:Fw2SIR63jhfLpFZr6955zU3g9V8ouHC/pRpmmiHmIFM=
|
||||
github.com/aws/aws-sdk-go-v2/service/route53domains v1.34.17/go.mod h1:x9PRRtbCQ/gv1ziQPXFB7nQwQgVLQ+FSvPIkVAhRcYY=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.6 h1:MzORe+J94I+hYu2a6XmV5yC9huoTv8NRcCrUNedDypQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.6/go.mod h1:hXzcHLARD7GeWnifd8j9RWqtfIgxj4/cAtIVIK7hg8g=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.7 h1:Y2cAXlClHsXkkOvWZFXATr34b0hxxloeQu/pAZz2row=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.7/go.mod h1:idzZ7gmDeqeNrSPkdbtMp9qWMgcBwykA7P7Rzh5DXVU=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.11 h1:7oGD8KPfBOJGXiCoRKrrrQkbvCp8N++u36hrLMPey6o=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.11/go.mod h1:0DO9B5EUJQlIDif+XJRWCljZRKsAFKh3gpFz7UnDtOo=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.12 h1:iSsvB9EtQ09YrsmIc44Heqlx5ByGErqhPK1ZQLppias=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.12/go.mod h1:fEWYKTRGoZNl8tZ77i61/ccwOMJdGxwOhWCkp6TXAr0=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.15 h1:edCcNp9eGIUDUCrzoCu1jWAXLGFIizeqkdkKgRlJwWc=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.15/go.mod h1:lyRQKED9xWfgkYC/wmmYfv7iVIM68Z5OQ88ZdcV1QbU=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.16 h1:EnUdUqRP1CNzt2DkV67tJx6XDN4xlfBFm+bzeNOQVb0=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.16/go.mod h1:Jic/xv0Rq/pFNCh3WwpH4BEqdbSAl+IyHro8LbibHD8=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.7 h1:NITQpgo9A5NrDZ57uOWj+abvXSb83BbyggcUBVksN7c=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.7/go.mod h1:sks5UWBhEuWYDPdwlnRFn1w7xWdH29Jcpe+/PJQefEs=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.8 h1:XQTQTF75vnug2TXS8m7CVJfC2nniYPZnO1D4Np761Oo=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.8/go.mod h1:Xgx+PR1NUOjNmQY+tRMnouRp83JRM8pRMw/vCaVhPkI=
|
||||
github.com/aws/smithy-go v1.24.1 h1:VbyeNfmYkWoxMVpGUAbQumkODcYmfMRfZ8yQiH30SK0=
|
||||
github.com/aws/smithy-go v1.24.1/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
|
||||
github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng=
|
||||
github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
|
||||
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
|
||||
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
|
||||
github.com/babolivier/go-doh-client v0.0.0-20201028162107-a76cff4cb8b6 h1:4NNbNM2Iq/k57qEu7WfL67UrbPq1uFWxW4qODCohi+0=
|
||||
github.com/babolivier/go-doh-client v0.0.0-20201028162107-a76cff4cb8b6/go.mod h1:J29hk+f9lJrblVIfiJOtTFk+OblBawmib4uz/VdKzlg=
|
||||
github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o=
|
||||
github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
|
|
@ -181,12 +145,8 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1
|
|||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/deepmap/oapi-codegen v1.16.3 h1:GT9G86SbQtT1r8ZB+4Cybi9VGdu1P5ieNvNdEoCSbrA=
|
||||
github.com/deepmap/oapi-codegen v1.16.3/go.mod h1:JD6ErqeX0nYnhdciLc61Konj3NBASREMlkHOgHn8WAM=
|
||||
github.com/digitalocean/godo v1.175.0 h1:tpfwJFkBzpePxvvFazOn69TXctdxuFlOs7DMVXsI7oU=
|
||||
github.com/digitalocean/godo v1.175.0/go.mod h1:xQsWpVCCbkDrWisHA72hPzPlnC+4W5w/McZY5ij9uvU=
|
||||
github.com/digitalocean/godo v1.176.0 h1:P379vPO5TUre+bUHPEsdSAbl5vIrRRhP91tMIEPoWYU=
|
||||
github.com/digitalocean/godo v1.176.0/go.mod h1:xQsWpVCCbkDrWisHA72hPzPlnC+4W5w/McZY5ij9uvU=
|
||||
github.com/dnsimple/dnsimple-go v1.7.0 h1:JKu9xJtZ3SqOC+BuYgAWeab7+EEx0sz422vu8j611ZY=
|
||||
github.com/dnsimple/dnsimple-go v1.7.0/go.mod h1:EKpuihlWizqYafSnQHGCd/gyvy3HkEQJ7ODB4KdV8T8=
|
||||
github.com/dnsimple/dnsimple-go/v8 v8.1.0 h1:U4ENaNCe5aUFHLiF7lj2NNpLPzFY3YIriu/UzrdfUbg=
|
||||
github.com/dnsimple/dnsimple-go/v8 v8.1.0/go.mod h1:61MdYHRL+p2TBBUVEkxo1n4iRF6s3R9fZcvQvyt5du8=
|
||||
github.com/earthboundkid/versioninfo/v2 v2.24.1 h1:SJTMHaoUx3GzjjnUO1QzP3ZXK6Ee/nbWyCm58eY3oUg=
|
||||
|
|
@ -200,8 +160,8 @@ github.com/failsafe-go/failsafe-go v0.9.6 h1:vPSH2cry0Ee5cnR9wc9qshCDO6jdrMA9elB
|
|||
github.com/failsafe-go/failsafe-go v0.9.6/go.mod h1:IeRpglkcwzKagjDMh90ZhN2l4Ovt3+jemQBUbThag54=
|
||||
github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
|
||||
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
|
||||
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||
github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w=
|
||||
github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE=
|
||||
github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
|
||||
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
|
|
@ -214,12 +174,10 @@ github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9
|
|||
github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||
github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4=
|
||||
github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk=
|
||||
github.com/gin-contrib/sessions v1.0.4 h1:ha6CNdpYiTOK/hTp05miJLbpTSNfOnFg5Jm2kbcqy8U=
|
||||
github.com/gin-contrib/sessions v1.0.4/go.mod h1:ccmkrb2z6iU2osiAHZG3x3J4suJK+OU27oqzlWOqQgs=
|
||||
github.com/gin-contrib/sessions v1.1.0 h1:00mhHfNEGF5sP2fwxa98aRqj1FOJdL6IkR86n2hOiBo=
|
||||
github.com/gin-contrib/sessions v1.1.0/go.mod h1:TyYZDIs6qCQg2SOoYPgMT9pAkmZceVNEJMcv5qbIy60=
|
||||
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
||||
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
||||
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
|
||||
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
|
||||
github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8=
|
||||
github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc=
|
||||
github.com/go-gandi/go-gandi v0.7.0 h1:gsP33dUspsN1M+ZW9HEgHchK9HiaSkYnltO73RHhSZA=
|
||||
|
|
@ -351,8 +309,6 @@ github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/
|
|||
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
|
||||
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/happyDomain/dnscontrol/v4 v4.35.101 h1:9GL4OZ05AXOBUNjRdU72UUJGFAQz6OTCRVQw3WTQuo8=
|
||||
github.com/happyDomain/dnscontrol/v4 v4.35.101/go.mod h1:R6j+Fv+etKriXI3runhnv42nPZPLcn81NNRt9gl1hTs=
|
||||
github.com/happyDomain/dnscontrol/v4 v4.36.100 h1:wrNaUV3Ihcqd9t9+AEIyBiyF1QNAeuFbCj+j8w0a/sM=
|
||||
github.com/happyDomain/dnscontrol/v4 v4.36.100/go.mod h1:7fgVrun0ecnT8fJhcFHQQXBg6yVIfEWRRQOj27hxm+s=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
|
||||
|
|
@ -430,8 +386,16 @@ github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1
|
|||
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/lib/pq v1.11.2 h1:x6gxUeu39V0BHZiugWe8LXZYZ+Utk7hSJGThs8sdzfs=
|
||||
github.com/lib/pq v1.11.2/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=
|
||||
github.com/lib/pq v1.12.0 h1:mC1zeiNamwKBecjHarAr26c/+d8V5w/u4J0I/yASbJo=
|
||||
github.com/lib/pq v1.12.0/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=
|
||||
github.com/lib/pq v1.12.2 h1:ajJNv84limnK3aPbDIhLtcjrUbqAw/5XNdkuI6KNe/Q=
|
||||
github.com/lib/pq v1.12.2/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=
|
||||
github.com/lib/pq v1.12.3 h1:tTWxr2YLKwIvK90ZXEw8GP7UFHtcbTtty8zsI+YjrfQ=
|
||||
github.com/lib/pq v1.12.3/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=
|
||||
github.com/libdns/ionos v1.2.0 h1:FQ2xQTBfsjc7aMArRBBCs9l48Squt76GHXbxDsqOKgw=
|
||||
github.com/libdns/ionos v1.2.0/go.mod h1:g/JYno/+VXdujTGPBDMDeCfeLF0PJyJynsCrFu+2EFQ=
|
||||
github.com/libdns/libdns v1.1.1 h1:wPrHrXILoSHKWJKGd0EiAVmiJbFShguILTg9leS/P/U=
|
||||
github.com/libdns/libdns v1.1.1/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ=
|
||||
github.com/luadns/luadns-go v0.3.0 h1:mN2yhFv/LnGvPw/HmvYUhXe+lc95oXUqjlYVeJeOJng=
|
||||
github.com/luadns/luadns-go v0.3.0/go.mod h1:DmPXbrGMpynq1YNDpvgww3NP5Zf4wXM5raAbGrp5L+8=
|
||||
github.com/mailgun/raymond/v2 v2.0.48 h1:5dmlB680ZkFG2RN/0lvTAghrSxIESeu9/2aeDqACtjw=
|
||||
|
|
@ -488,8 +452,6 @@ github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b h1:Ff
|
|||
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b/go.mod h1:AC62GU6hc0BrNm+9RK9VSiwa/EUe1bkIeFORAMcHvJU=
|
||||
github.com/oracle/nosql-go-sdk v1.4.7 h1:dqVBSMulObDj0JHm1mAncTHrQg8wIiQJNC0JRNKPACg=
|
||||
github.com/oracle/nosql-go-sdk v1.4.7/go.mod h1:xgJE9wxADDbk7vR4FGA4NOt4RNAaIsQOj4sCATmCVXM=
|
||||
github.com/oracle/oci-go-sdk/v65 v65.108.3 h1:n2G4QwGoRNhtLE8r24/+Ny+WpEMdc9ggGpnPvVYM2Yk=
|
||||
github.com/oracle/oci-go-sdk/v65 v65.108.3/go.mod h1:8ZzvzuEG/cFLFZhxg/Mg1w19KqyXBKO3c17QIc5PkGs=
|
||||
github.com/oracle/oci-go-sdk/v65 v65.109.0 h1:EsbFVvcV+uid9SoQnFQbTKS6FgqsM9NtoKmUIovKsbk=
|
||||
github.com/oracle/oci-go-sdk/v65 v65.109.0/go.mod h1:8ZzvzuEG/cFLFZhxg/Mg1w19KqyXBKO3c17QIc5PkGs=
|
||||
github.com/ovh/go-ovh v1.9.0 h1:6K8VoL3BYjVV3In9tPJUdT7qMx9h0GExN9EXx1r2kKE=
|
||||
|
|
@ -516,8 +478,6 @@ github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNw
|
|||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||
github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
|
||||
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
|
||||
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
|
||||
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
|
||||
github.com/prometheus/procfs v0.20.0 h1:AA7aCvjxwAquZAlonN7888f2u4IN8WVeFgBi4k82M4Q=
|
||||
github.com/prometheus/procfs v0.20.0/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
|
||||
github.com/qdm12/reprint v0.0.0-20200326205758-722754a53494 h1:wSmWgpuccqS2IOfmYrbRiUgv+g37W5suLLLxwwniTSc=
|
||||
|
|
@ -638,8 +598,8 @@ github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3Ifn
|
|||
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM=
|
||||
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yuin/goldmark v1.7.16 h1:n+CJdUxaFMiDUNnWC3dMWCIQJSkxH4uz3ZwQBkAlVNE=
|
||||
github.com/yuin/goldmark v1.7.16/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
||||
github.com/yuin/goldmark v1.8.2 h1:kEGpgqJXdgbkhcOgBxkC0X0PmoPG1ZyoZ117rDVp4zE=
|
||||
github.com/yuin/goldmark v1.8.2/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
||||
go.mongodb.org/mongo-driver v1.13.1/go.mod h1:wcDf1JBCXy2mOW0bWHwO/IOYqdca1MPCwDtFu/Z9+eo=
|
||||
go.mongodb.org/mongo-driver v1.17.9 h1:IexDdCuuNJ3BHrELgBlyaH9p60JXAvdzWR128q+U5tU=
|
||||
go.mongodb.org/mongo-driver v1.17.9/go.mod h1:LlOhpH5NUEfhxcAwG0UEkMqwYcc4JU18gtCdGudk/tQ=
|
||||
|
|
@ -684,8 +644,6 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY
|
|||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
|
||||
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
|
||||
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
|
|
@ -732,13 +690,9 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
|||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
|
||||
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
|
||||
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
|
||||
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ=
|
||||
golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
|
||||
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
|
|
@ -751,8 +705,6 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
|||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
|
|
@ -782,8 +734,6 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
|
|
@ -807,8 +757,6 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
|||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
||||
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
||||
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
||||
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||
|
|
@ -841,8 +789,6 @@ gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E
|
|||
gonum.org/v1/netlib v0.0.0-20181029234149-ec6d1f5cefe6/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw=
|
||||
gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw=
|
||||
gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc=
|
||||
google.golang.org/api v0.268.0 h1:hgA3aS4lt9rpF5RCCkX0Q2l7DvHgvlb53y4T4u6iKkA=
|
||||
google.golang.org/api v0.268.0/go.mod h1:HXMyMH496wz+dAJwD/GkAPLd3ZL33Kh0zEG32eNvy9w=
|
||||
google.golang.org/api v0.269.0 h1:qDrTOxKUQ/P0MveH6a7vZ+DNHxJQjtGm/uvdbdGXCQg=
|
||||
google.golang.org/api v0.269.0/go.mod h1:N8Wpcu23Tlccl0zSHEkcAZQKDLdquxK+l9r2LkwAauE=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
|
|
|
|||
|
|
@ -27,7 +27,10 @@ import (
|
|||
|
||||
dnscontrol "github.com/StackExchange/dnscontrol/v4/models"
|
||||
"github.com/StackExchange/dnscontrol/v4/pkg/diff2"
|
||||
"github.com/StackExchange/dnscontrol/v4/pkg/domaintags"
|
||||
"github.com/StackExchange/dnscontrol/v4/pkg/dnsrr"
|
||||
_ "github.com/StackExchange/dnscontrol/v4/pkg/rtype"
|
||||
"github.com/StackExchange/dnscontrol/v4/pkg/rtypecontrol"
|
||||
"github.com/miekg/dns"
|
||||
|
||||
"git.happydns.org/happyDomain/model"
|
||||
|
|
@ -118,6 +121,7 @@ func DNSControlDiffByRecord(oldrrs []happydns.Record, newrrs []happydns.Record,
|
|||
// before converting to DNSControl format.
|
||||
// The origin parameter specifies the zone name (with or without trailing dot).
|
||||
func DNSControlRRtoRC(rrs []happydns.Record, origin string) (dnscontrol.Records, error) {
|
||||
originNoTrailingDot := strings.TrimSuffix(origin, ".")
|
||||
records := make([]*dnscontrol.RecordConfig, len(rrs))
|
||||
|
||||
for i, rr := range rrs {
|
||||
|
|
@ -126,10 +130,25 @@ func DNSControlRRtoRC(rrs []happydns.Record, origin string) (dnscontrol.Records,
|
|||
rr = record.ToRR()
|
||||
}
|
||||
|
||||
rc, err := dnsrr.RRtoRC(rr.(dns.RR), strings.TrimSuffix(origin, "."))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
typeName := dns.TypeToString[rr.Header().Rrtype]
|
||||
|
||||
var rc dnscontrol.RecordConfig
|
||||
var err error
|
||||
|
||||
if _, ok := rtypecontrol.Func[typeName]; ok {
|
||||
dcn := domaintags.MakeDomainNameVarieties(originNoTrailingDot)
|
||||
rcPtr, e := rtypecontrol.NewRecordConfigFromStruct(rr.Header().Name, rr.Header().Ttl, typeName, rr, dcn)
|
||||
if e != nil {
|
||||
return nil, e
|
||||
}
|
||||
rc = *rcPtr
|
||||
} else {
|
||||
rc, err = dnsrr.RRtoRC(rr.(dns.RR), originNoTrailingDot)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
records[i] = &rc
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ import (
|
|||
// and sets the help link to the DNSControl documentation for that provider.
|
||||
func RegisterDNSControlProviderAdapter(creator happydns.ProviderCreatorFunc, infos happydns.ProviderInfos, registerFunc happydns.RegisterProviderFunc) {
|
||||
prvInstance := creator().(DNSControlConfigAdapter)
|
||||
infos.Capabilities = GetDNSControlProviderCapabilities(prvInstance)
|
||||
infos.Capabilities = append(infos.Capabilities, GetDNSControlProviderCapabilities(prvInstance)...)
|
||||
infos.HelpLink = "https://docs.dnscontrol.org/service-providers/providers/" + strings.ToLower(prvInstance.DNSControlName())
|
||||
|
||||
registerFunc(creator, infos)
|
||||
|
|
@ -194,7 +194,13 @@ func (p *DNSControlAdapterNSProvider) GetZoneRecords(domain string) (ret []happy
|
|||
}
|
||||
|
||||
for _, rec := range records {
|
||||
ret = append(ret, rec.ToRR())
|
||||
// rec.ToRR() for modern types (DS, RP, …) returns the rtype wrapper
|
||||
// (e.g. *rtype.DS) rather than the canonical *dns.DS. When these are
|
||||
// later passed back through dnsrr.RRtoRC → DS.FromStruct, the type
|
||||
// assertion fields.(*dns.DS) fails, causing a nil-dereference panic.
|
||||
// dns.Copy invokes the promoted copy() method from the embedded *dns.DS,
|
||||
// which returns a canonical *dns.DS and eliminates the mismatch.
|
||||
ret = append(ret, dns.Copy(rec.ToRR()))
|
||||
}
|
||||
|
||||
return
|
||||
|
|
|
|||
317
internal/adapters/libdns-providers.go
Normal file
317
internal/adapters/libdns-providers.go
Normal file
|
|
@ -0,0 +1,317 @@
|
|||
// This file is part of the happyDomain (R) project.
|
||||
// Copyright (c) 2020-2025 happyDomain
|
||||
// Authors: Pierre-Olivier Mercier, et al.
|
||||
//
|
||||
// This program is offered under a commercial and under the AGPL license.
|
||||
// For commercial licensing, contact us at <contact@happydomain.org>.
|
||||
//
|
||||
// For AGPL licensing:
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/libdns/libdns"
|
||||
"github.com/miekg/dns"
|
||||
|
||||
"git.happydns.org/happyDomain/model"
|
||||
)
|
||||
|
||||
// LibdnsConfigAdapter is an interface that provider configurations must implement
|
||||
// to work with libdns. It allows retrieving the underlying libdns provider instance.
|
||||
type LibdnsConfigAdapter interface {
|
||||
// LibdnsProvider returns the underlying libdns provider instance.
|
||||
// The returned value must implement at least libdns.RecordGetter.
|
||||
LibdnsProvider() any
|
||||
}
|
||||
|
||||
// RegisterLibdnsProviderAdapter registers a DNS provider that uses libdns as its backend.
|
||||
// It automatically populates the provider's capabilities by checking which libdns
|
||||
// interfaces the provider implements.
|
||||
func RegisterLibdnsProviderAdapter(creator happydns.ProviderCreatorFunc, infos happydns.ProviderInfos, registerFunc happydns.RegisterProviderFunc) {
|
||||
prvInstance := creator().(LibdnsConfigAdapter)
|
||||
infos.Capabilities = append(infos.Capabilities, GetLibdnsProviderCapabilities(prvInstance)...)
|
||||
|
||||
registerFunc(creator, infos)
|
||||
}
|
||||
|
||||
// GetLibdnsProviderCapabilities checks which libdns interfaces the provider implements
|
||||
// and returns the corresponding capability strings. Since libdns providers are type-agnostic,
|
||||
// common record types are declared for all providers.
|
||||
func GetLibdnsProviderCapabilities(prvd LibdnsConfigAdapter) (caps []string) {
|
||||
p := prvd.LibdnsProvider()
|
||||
|
||||
if _, ok := p.(libdns.ZoneLister); ok {
|
||||
caps = append(caps, "ListDomains")
|
||||
}
|
||||
|
||||
// libdns providers are type-agnostic, so declare support for common RR types.
|
||||
for _, v := range []uint16{
|
||||
dns.TypeA, dns.TypeAAAA, dns.TypeCNAME, dns.TypeMX,
|
||||
dns.TypeNS, dns.TypeTXT, dns.TypeSRV, dns.TypeCAA,
|
||||
dns.TypePTR,
|
||||
} {
|
||||
caps = append(caps, fmt.Sprintf("rr-%d-%s", v, dns.TypeToString[v]))
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// NewLibdnsProviderAdapter creates a new provider actuator instance from a libdns configuration.
|
||||
// It discovers the provider's capabilities by checking which libdns interfaces it implements.
|
||||
// The provider must implement at least libdns.RecordGetter.
|
||||
func NewLibdnsProviderAdapter(configAdapter LibdnsConfigAdapter) (happydns.ProviderActuator, error) {
|
||||
p := configAdapter.LibdnsProvider()
|
||||
|
||||
adapter := &LibdnsAdapterNSProvider{
|
||||
provider: p,
|
||||
}
|
||||
|
||||
if g, ok := p.(libdns.RecordGetter); ok {
|
||||
adapter.getter = g
|
||||
} else {
|
||||
return nil, fmt.Errorf("libdns provider must implement RecordGetter")
|
||||
}
|
||||
|
||||
if s, ok := p.(libdns.RecordSetter); ok {
|
||||
adapter.setter = s
|
||||
}
|
||||
if a, ok := p.(libdns.RecordAppender); ok {
|
||||
adapter.appender = a
|
||||
}
|
||||
if d, ok := p.(libdns.RecordDeleter); ok {
|
||||
adapter.deleter = d
|
||||
}
|
||||
if z, ok := p.(libdns.ZoneLister); ok {
|
||||
adapter.zoneLister = z
|
||||
}
|
||||
|
||||
return adapter, nil
|
||||
}
|
||||
|
||||
// LibdnsAdapterNSProvider wraps a libdns provider to implement the happyDomain ProviderActuator interface.
|
||||
type LibdnsAdapterNSProvider struct {
|
||||
provider any
|
||||
getter libdns.RecordGetter
|
||||
setter libdns.RecordSetter
|
||||
appender libdns.RecordAppender
|
||||
deleter libdns.RecordDeleter
|
||||
zoneLister libdns.ZoneLister
|
||||
}
|
||||
|
||||
// normalizeZone ensures the zone name has a trailing dot (FQDN format expected by libdns).
|
||||
func normalizeZone(domain string) string {
|
||||
zone := strings.TrimSuffix(domain, ".")
|
||||
return zone + "."
|
||||
}
|
||||
|
||||
// CanListZones checks if the provider supports listing zones.
|
||||
func (p *LibdnsAdapterNSProvider) CanListZones() bool {
|
||||
return p.zoneLister != nil
|
||||
}
|
||||
|
||||
// CanCreateDomain returns false since libdns has no zone creation interface.
|
||||
func (p *LibdnsAdapterNSProvider) CanCreateDomain() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// CreateDomain is not supported by libdns providers.
|
||||
func (p *LibdnsAdapterNSProvider) CreateDomain(fqdn string) error {
|
||||
return fmt.Errorf("libdns provider does not support domain creation")
|
||||
}
|
||||
|
||||
// ListZones retrieves the list of all zones managed by this provider.
|
||||
func (p *LibdnsAdapterNSProvider) ListZones() ([]string, error) {
|
||||
if p.zoneLister == nil {
|
||||
return nil, fmt.Errorf("libdns provider does not support zone listing")
|
||||
}
|
||||
|
||||
zones, err := p.zoneLister.ListZones(context.TODO())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make([]string, len(zones))
|
||||
for i, z := range zones {
|
||||
result[i] = z.Name
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GetZoneRecords retrieves all DNS records for the specified domain from the provider.
|
||||
func (p *LibdnsAdapterNSProvider) GetZoneRecords(domain string) ([]happydns.Record, error) {
|
||||
zone := normalizeZone(domain)
|
||||
|
||||
recs, err := p.getter.GetRecords(context.TODO(), zone)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return libdnsRecordsToHappyDNS(recs, zone)
|
||||
}
|
||||
|
||||
// GetZoneCorrections compares desired records against the current zone state and returns
|
||||
// the changes needed to synchronize them. It uses the DNSControl diff engine to compute
|
||||
// the diff, then creates correction functions that call the libdns provider's API.
|
||||
func (p *LibdnsAdapterNSProvider) GetZoneCorrections(domain string, wantedRecords []happydns.Record) ([]*happydns.Correction, int, error) {
|
||||
zone := normalizeZone(domain)
|
||||
|
||||
// Step 1: Fetch current records from the provider.
|
||||
currentLibdnsRecs, err := p.getter.GetRecords(context.TODO(), zone)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("unable to get current zone records: %w", err)
|
||||
}
|
||||
|
||||
currentRecords, err := libdnsRecordsToHappyDNS(currentLibdnsRecs, zone)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("unable to convert current zone records: %w", err)
|
||||
}
|
||||
|
||||
// Step 2: Compute diff using existing DNSControl diff engine.
|
||||
diffs, nbDiffs, err := DNSControlDiffByRecord(currentRecords, wantedRecords, domain)
|
||||
if err != nil {
|
||||
return nil, nbDiffs, fmt.Errorf("unable to compute zone diff: %w", err)
|
||||
}
|
||||
|
||||
// Build a lookup from happydns Record string → original libdns records (with ProviderData).
|
||||
// This ensures delete operations use the provider's record IDs.
|
||||
libdnsRecordsByKey := make(map[string][]libdns.Record)
|
||||
for _, rec := range currentLibdnsRecs {
|
||||
rr := rec.RR()
|
||||
key := fmt.Sprintf("%s\t%s\t%s", rr.Name, rr.Type, rr.Data)
|
||||
libdnsRecordsByKey[key] = append(libdnsRecordsByKey[key], rec)
|
||||
}
|
||||
|
||||
// Step 3: Create corrections with executable F closures.
|
||||
corrections := make([]*happydns.Correction, len(diffs))
|
||||
for i, diff := range diffs {
|
||||
corrections[i] = &happydns.Correction{
|
||||
Id: diff.Id,
|
||||
Msg: diff.Msg,
|
||||
Kind: diff.Kind,
|
||||
OldRecords: diff.OldRecords,
|
||||
NewRecords: diff.NewRecords,
|
||||
}
|
||||
|
||||
corrections[i].F = p.makeCorrectionFunc(zone, diff, libdnsRecordsByKey)
|
||||
}
|
||||
|
||||
return corrections, nbDiffs, nil
|
||||
}
|
||||
|
||||
// makeCorrectionFunc creates an executable function for a single correction.
|
||||
func (p *LibdnsAdapterNSProvider) makeCorrectionFunc(
|
||||
zone string,
|
||||
diff *happydns.Correction,
|
||||
libdnsRecordsByKey map[string][]libdns.Record,
|
||||
) func() error {
|
||||
kind := diff.Kind
|
||||
|
||||
// Resolve old records to their original libdns Records (with ProviderData).
|
||||
oldRecs := p.resolveOriginalRecords(diff.OldRecords, zone, libdnsRecordsByKey)
|
||||
newRecs := happyDNSRecordsToLibdnsRecords(diff.NewRecords, zone)
|
||||
|
||||
// If we have both appender and deleter, use granular operations.
|
||||
if p.appender != nil && p.deleter != nil {
|
||||
return func() error {
|
||||
ctx := context.TODO()
|
||||
switch kind {
|
||||
case happydns.CorrectionKindAddition:
|
||||
_, err := p.appender.AppendRecords(ctx, zone, newRecs)
|
||||
return err
|
||||
case happydns.CorrectionKindDeletion:
|
||||
_, err := p.deleter.DeleteRecords(ctx, zone, oldRecs)
|
||||
return err
|
||||
case happydns.CorrectionKindUpdate:
|
||||
if _, err := p.deleter.DeleteRecords(ctx, zone, oldRecs); err != nil {
|
||||
return fmt.Errorf("delete phase of update: %w", err)
|
||||
}
|
||||
_, err := p.appender.AppendRecords(ctx, zone, newRecs)
|
||||
if err != nil {
|
||||
return fmt.Errorf("append phase of update: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: use SetRecords if available.
|
||||
if p.setter != nil {
|
||||
return func() error {
|
||||
ctx := context.TODO()
|
||||
switch kind {
|
||||
case happydns.CorrectionKindAddition:
|
||||
// SetRecords with the new records will add them to the zone
|
||||
// for their (name, type) pair.
|
||||
_, err := p.setter.SetRecords(ctx, zone, newRecs)
|
||||
return err
|
||||
case happydns.CorrectionKindDeletion:
|
||||
// To delete, we need to set the (name, type) pair to empty.
|
||||
// DeleteRecords would be better, but we only have SetRecords.
|
||||
// Use DeleteRecords-style wildcard via setter: set with empty set
|
||||
// is not directly possible with SetRecords semantics.
|
||||
// Fall through to delete if we have deleter, otherwise error.
|
||||
return fmt.Errorf("cannot delete records: provider only supports SetRecords, not DeleteRecords")
|
||||
case happydns.CorrectionKindUpdate:
|
||||
// SetRecords replaces all records for the (name, type) pair.
|
||||
_, err := p.setter.SetRecords(ctx, zone, newRecs)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return func() error {
|
||||
return fmt.Errorf("libdns provider does not support record modification")
|
||||
}
|
||||
}
|
||||
|
||||
// resolveOriginalRecords tries to find the original libdns Records (with ProviderData)
|
||||
// for the given happydns Records. This ensures that delete operations use the provider's
|
||||
// record identifiers.
|
||||
func (p *LibdnsAdapterNSProvider) resolveOriginalRecords(
|
||||
records []happydns.Record,
|
||||
zone string,
|
||||
libdnsRecordsByKey map[string][]libdns.Record,
|
||||
) []libdns.Record {
|
||||
result := make([]libdns.Record, 0, len(records))
|
||||
for _, rec := range records {
|
||||
rr := happyDNSRecordToLibdnsRR(rec, zone)
|
||||
key := fmt.Sprintf("%s\t%s\t%s", rr.Name, rr.Type, rr.Data)
|
||||
|
||||
if originals, ok := libdnsRecordsByKey[key]; ok && len(originals) > 0 {
|
||||
// Use the original record and consume it from the map.
|
||||
result = append(result, originals[0])
|
||||
libdnsRecordsByKey[key] = originals[1:]
|
||||
} else {
|
||||
// Fallback: use the converted RR (without ProviderData).
|
||||
result = append(result, rr)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// happyDNSRecordsToLibdnsRecords converts happydns Records to libdns Records (the interface).
|
||||
func happyDNSRecordsToLibdnsRecords(rrs []happydns.Record, zone string) []libdns.Record {
|
||||
result := make([]libdns.Record, len(rrs))
|
||||
for i, rr := range rrs {
|
||||
result[i] = happyDNSRecordToLibdnsRR(rr, zone)
|
||||
}
|
||||
return result
|
||||
}
|
||||
331
internal/adapters/libdns-providers_test.go
Normal file
331
internal/adapters/libdns-providers_test.go
Normal file
|
|
@ -0,0 +1,331 @@
|
|||
// This file is part of the happyDomain (R) project.
|
||||
// Copyright (c) 2020-2025 happyDomain
|
||||
// Authors: Pierre-Olivier Mercier, et al.
|
||||
//
|
||||
// This program is offered under a commercial and under the AGPL license.
|
||||
// For commercial licensing, contact us at <contact@happydomain.org>.
|
||||
//
|
||||
// For AGPL licensing:
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/netip"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/libdns/libdns"
|
||||
"github.com/miekg/dns"
|
||||
|
||||
"git.happydns.org/happyDomain/model"
|
||||
)
|
||||
|
||||
// mockLibdnsProvider implements RecordGetter, RecordAppender, RecordDeleter for testing.
|
||||
type mockLibdnsProvider struct {
|
||||
records []libdns.Record
|
||||
appended []libdns.Record
|
||||
deleted []libdns.Record
|
||||
zones []libdns.Zone
|
||||
appendErr error
|
||||
deleteErr error
|
||||
getErr error
|
||||
listZoneErr error
|
||||
}
|
||||
|
||||
func (m *mockLibdnsProvider) GetRecords(_ context.Context, _ string) ([]libdns.Record, error) {
|
||||
if m.getErr != nil {
|
||||
return nil, m.getErr
|
||||
}
|
||||
return m.records, nil
|
||||
}
|
||||
|
||||
func (m *mockLibdnsProvider) AppendRecords(_ context.Context, _ string, recs []libdns.Record) ([]libdns.Record, error) {
|
||||
if m.appendErr != nil {
|
||||
return nil, m.appendErr
|
||||
}
|
||||
m.appended = append(m.appended, recs...)
|
||||
return recs, nil
|
||||
}
|
||||
|
||||
func (m *mockLibdnsProvider) DeleteRecords(_ context.Context, _ string, recs []libdns.Record) ([]libdns.Record, error) {
|
||||
if m.deleteErr != nil {
|
||||
return nil, m.deleteErr
|
||||
}
|
||||
m.deleted = append(m.deleted, recs...)
|
||||
return recs, nil
|
||||
}
|
||||
|
||||
func (m *mockLibdnsProvider) ListZones(_ context.Context) ([]libdns.Zone, error) {
|
||||
if m.listZoneErr != nil {
|
||||
return nil, m.listZoneErr
|
||||
}
|
||||
return m.zones, nil
|
||||
}
|
||||
|
||||
// mockLibdnsConfig implements LibdnsConfigAdapter.
|
||||
type mockLibdnsConfig struct {
|
||||
provider any
|
||||
}
|
||||
|
||||
func (m *mockLibdnsConfig) LibdnsProvider() any {
|
||||
return m.provider
|
||||
}
|
||||
|
||||
func (m *mockLibdnsConfig) InstantiateProvider() (happydns.ProviderActuator, error) {
|
||||
return NewLibdnsProviderAdapter(m)
|
||||
}
|
||||
|
||||
func TestNewLibdnsProviderAdapter(t *testing.T) {
|
||||
mock := &mockLibdnsProvider{}
|
||||
config := &mockLibdnsConfig{provider: mock}
|
||||
|
||||
adapter, err := NewLibdnsProviderAdapter(config)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if !adapter.CanListZones() {
|
||||
t.Error("expected CanListZones to be true")
|
||||
}
|
||||
if adapter.CanCreateDomain() {
|
||||
t.Error("expected CanCreateDomain to be false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLibdnsAdapter_GetZoneRecords(t *testing.T) {
|
||||
mock := &mockLibdnsProvider{
|
||||
records: []libdns.Record{
|
||||
libdns.Address{
|
||||
Name: "www",
|
||||
TTL: 300 * time.Second,
|
||||
IP: netip.MustParseAddr("192.0.2.1"),
|
||||
},
|
||||
libdns.TXT{
|
||||
Name: "@",
|
||||
TTL: 300 * time.Second,
|
||||
Text: "v=spf1 ~all",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
config := &mockLibdnsConfig{provider: mock}
|
||||
adapter, err := NewLibdnsProviderAdapter(config)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
records, err := adapter.GetZoneRecords("example.com.")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if len(records) != 2 {
|
||||
t.Fatalf("expected 2 records, got %d", len(records))
|
||||
}
|
||||
|
||||
// Check A record
|
||||
if records[0].Header().Rrtype != dns.TypeA {
|
||||
t.Errorf("expected first record to be A, got %s", dns.TypeToString[records[0].Header().Rrtype])
|
||||
}
|
||||
|
||||
// Check TXT record
|
||||
txt, ok := records[1].(*happydns.TXT)
|
||||
if !ok {
|
||||
t.Fatalf("expected second record to be *happydns.TXT, got %T", records[1])
|
||||
}
|
||||
if txt.Txt != "v=spf1 ~all" {
|
||||
t.Errorf("expected TXT 'v=spf1 ~all', got %q", txt.Txt)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLibdnsAdapter_ListZones(t *testing.T) {
|
||||
mock := &mockLibdnsProvider{
|
||||
zones: []libdns.Zone{
|
||||
{Name: "example.com."},
|
||||
{Name: "example.org."},
|
||||
},
|
||||
}
|
||||
|
||||
config := &mockLibdnsConfig{provider: mock}
|
||||
adapter, err := NewLibdnsProviderAdapter(config)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
zones, err := adapter.ListZones()
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if len(zones) != 2 {
|
||||
t.Fatalf("expected 2 zones, got %d", len(zones))
|
||||
}
|
||||
if zones[0] != "example.com." {
|
||||
t.Errorf("expected first zone 'example.com.', got %q", zones[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestLibdnsAdapter_GetZoneCorrections_NoChanges(t *testing.T) {
|
||||
records := []libdns.Record{
|
||||
libdns.Address{
|
||||
Name: "www",
|
||||
TTL: 300 * time.Second,
|
||||
IP: netip.MustParseAddr("192.0.2.1"),
|
||||
},
|
||||
}
|
||||
|
||||
mock := &mockLibdnsProvider{records: records}
|
||||
config := &mockLibdnsConfig{provider: mock}
|
||||
adapter, err := NewLibdnsProviderAdapter(config)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
// Pass the same records as wanted
|
||||
aRR, _ := dns.NewRR("www.example.com. 300 IN A 192.0.2.1")
|
||||
corrections, _, err := adapter.GetZoneCorrections("example.com.", []happydns.Record{aRR})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if len(corrections) != 0 {
|
||||
t.Errorf("expected 0 corrections, got %d", len(corrections))
|
||||
}
|
||||
}
|
||||
|
||||
func TestLibdnsAdapter_GetZoneCorrections_Addition(t *testing.T) {
|
||||
// Provider has one A record, we want to add a CNAME.
|
||||
mock := &mockLibdnsProvider{
|
||||
records: []libdns.Record{
|
||||
libdns.Address{
|
||||
Name: "www",
|
||||
TTL: 300 * time.Second,
|
||||
IP: netip.MustParseAddr("192.0.2.1"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
config := &mockLibdnsConfig{provider: mock}
|
||||
adapter, err := NewLibdnsProviderAdapter(config)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
aRR, _ := dns.NewRR("www.example.com. 300 IN A 192.0.2.1")
|
||||
cnameRR, _ := dns.NewRR("blog.example.com. 300 IN CNAME www.example.com.")
|
||||
corrections, _, err := adapter.GetZoneCorrections("example.com.", []happydns.Record{aRR, cnameRR})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if len(corrections) == 0 {
|
||||
t.Fatal("expected at least 1 correction")
|
||||
}
|
||||
|
||||
// Execute the correction
|
||||
for _, c := range corrections {
|
||||
if c.Kind == happydns.CorrectionKindAddition {
|
||||
if err := c.F(); err != nil {
|
||||
t.Fatalf("unexpected error executing correction: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(mock.appended) == 0 {
|
||||
t.Error("expected records to be appended")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLibdnsAdapter_GetZoneCorrections_Deletion(t *testing.T) {
|
||||
// Provider has two records, we want only one.
|
||||
mock := &mockLibdnsProvider{
|
||||
records: []libdns.Record{
|
||||
libdns.Address{
|
||||
Name: "www",
|
||||
TTL: 300 * time.Second,
|
||||
IP: netip.MustParseAddr("192.0.2.1"),
|
||||
},
|
||||
libdns.Address{
|
||||
Name: "old",
|
||||
TTL: 300 * time.Second,
|
||||
IP: netip.MustParseAddr("192.0.2.2"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
config := &mockLibdnsConfig{provider: mock}
|
||||
adapter, err := NewLibdnsProviderAdapter(config)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
aRR, _ := dns.NewRR("www.example.com. 300 IN A 192.0.2.1")
|
||||
corrections, _, err := adapter.GetZoneCorrections("example.com.", []happydns.Record{aRR})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if len(corrections) == 0 {
|
||||
t.Fatal("expected at least 1 correction")
|
||||
}
|
||||
|
||||
// Execute the deletion correction
|
||||
for _, c := range corrections {
|
||||
if c.Kind == happydns.CorrectionKindDeletion {
|
||||
if err := c.F(); err != nil {
|
||||
t.Fatalf("unexpected error executing correction: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(mock.deleted) == 0 {
|
||||
t.Error("expected records to be deleted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetLibdnsProviderCapabilities(t *testing.T) {
|
||||
mock := &mockLibdnsProvider{}
|
||||
config := &mockLibdnsConfig{provider: mock}
|
||||
|
||||
caps := GetLibdnsProviderCapabilities(config)
|
||||
|
||||
// Should include ListDomains since mock implements ZoneLister
|
||||
found := false
|
||||
for _, c := range caps {
|
||||
if c == "ListDomains" {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Error("expected ListDomains capability")
|
||||
}
|
||||
|
||||
// Should include common RR types
|
||||
expectedTypes := []string{"rr-1-A", "rr-28-AAAA", "rr-5-CNAME", "rr-15-MX", "rr-16-TXT"}
|
||||
for _, expected := range expectedTypes {
|
||||
found = false
|
||||
for _, c := range caps {
|
||||
if c == expected {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("expected capability %s", expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
188
internal/adapters/libdns-records.go
Normal file
188
internal/adapters/libdns-records.go
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
// This file is part of the happyDomain (R) project.
|
||||
// Copyright (c) 2020-2025 happyDomain
|
||||
// Authors: Pierre-Olivier Mercier, et al.
|
||||
//
|
||||
// This program is offered under a commercial and under the AGPL license.
|
||||
// For commercial licensing, contact us at <contact@happydomain.org>.
|
||||
//
|
||||
// For AGPL licensing:
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/StackExchange/dnscontrol/v4/pkg/txtutil"
|
||||
"github.com/libdns/libdns"
|
||||
"github.com/miekg/dns"
|
||||
|
||||
"git.happydns.org/happyDomain/internal/helpers"
|
||||
"git.happydns.org/happyDomain/model"
|
||||
)
|
||||
|
||||
// libdnsToHappyDNSRecord converts a libdns Record to a happydns Record.
|
||||
// The zone parameter should be the FQDN with trailing dot (e.g. "example.com.").
|
||||
// For TXT records, it produces happydns.TXT directly (single concatenated string).
|
||||
func libdnsToHappyDNSRecord(rec libdns.Record, zone string) (happydns.Record, error) {
|
||||
rr := rec.RR()
|
||||
|
||||
fqdn := libdns.AbsoluteName(rr.Name, zone)
|
||||
if !strings.HasSuffix(fqdn, ".") {
|
||||
fqdn += "."
|
||||
}
|
||||
|
||||
ttlSec := uint32(rr.TTL.Seconds())
|
||||
|
||||
// For TXT records, the libdns Data field may be either raw text or
|
||||
// RFC1035 presentation-format with quotes and escaping (depends on provider).
|
||||
// Use txtutil.ParseQuoted to decode presentation-format data.
|
||||
if rr.Type == "TXT" {
|
||||
return &happydns.TXT{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: fqdn,
|
||||
Rrtype: dns.TypeTXT,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: ttlSec,
|
||||
},
|
||||
Txt: decodeTXTData(rr.Data),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// For SPF records (if any provider returns them)
|
||||
if rr.Type == "SPF" {
|
||||
return &happydns.SPF{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: fqdn,
|
||||
Rrtype: dns.TypeSPF,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: ttlSec,
|
||||
},
|
||||
Txt: decodeTXTData(rr.Data),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// For all other record types, build a zone-file line and parse it.
|
||||
line := fmt.Sprintf("%s %d IN %s %s", fqdn, ttlSec, rr.Type, rr.Data)
|
||||
return helpers.ParseRecord(line, zone)
|
||||
}
|
||||
|
||||
// happyDNSRecordToLibdnsRR converts a happydns Record to a libdns RR.
|
||||
// The zone parameter should be the FQDN with trailing dot (e.g. "example.com.").
|
||||
func happyDNSRecordToLibdnsRR(record happydns.Record, zone string) libdns.RR {
|
||||
hdr := record.Header()
|
||||
|
||||
name := libdns.RelativeName(hdr.Name, zone)
|
||||
typStr := dns.TypeToString[hdr.Rrtype]
|
||||
ttl := time.Duration(hdr.Ttl) * time.Second
|
||||
|
||||
// For happydns.TXT / happydns.SPF, extract the raw text directly.
|
||||
if txt, ok := record.(*happydns.TXT); ok {
|
||||
return libdns.RR{
|
||||
Name: name,
|
||||
TTL: ttl,
|
||||
Type: typStr,
|
||||
Data: txt.Txt,
|
||||
}
|
||||
}
|
||||
if spf, ok := record.(*happydns.SPF); ok {
|
||||
return libdns.RR{
|
||||
Name: name,
|
||||
TTL: ttl,
|
||||
Type: typStr,
|
||||
Data: spf.Txt,
|
||||
}
|
||||
}
|
||||
|
||||
// For ConvertibleRecord types, convert to dns.RR first.
|
||||
var dnsRR dns.RR
|
||||
if cr, ok := record.(happydns.ConvertibleRecord); ok {
|
||||
dnsRR = cr.ToRR()
|
||||
} else if rr, ok := record.(dns.RR); ok {
|
||||
dnsRR = rr
|
||||
} else {
|
||||
// Fallback: try to extract rdata from string representation.
|
||||
return libdns.RR{
|
||||
Name: name,
|
||||
TTL: ttl,
|
||||
Type: typStr,
|
||||
Data: extractRdata(record.String(), typStr),
|
||||
}
|
||||
}
|
||||
|
||||
return libdns.RR{
|
||||
Name: name,
|
||||
TTL: ttl,
|
||||
Type: typStr,
|
||||
Data: extractRdata(dnsRR.String(), typStr),
|
||||
}
|
||||
}
|
||||
|
||||
// decodeTXTData decodes TXT record data that may be in RFC1035 presentation
|
||||
// format (quoted, with escaping) or raw text. Some libdns providers (e.g.
|
||||
// PowerDNS) return quoted data like `"value"`, while others (e.g. libdns.TXT)
|
||||
// return raw unquoted text. ParseQuoted handles quoted data correctly but
|
||||
// treats unquoted spaces as separators, so we only use it when quotes are present.
|
||||
func decodeTXTData(s string) string {
|
||||
if strings.ContainsRune(s, '"') {
|
||||
if decoded, err := txtutil.ParseQuoted(s); err == nil {
|
||||
return decoded
|
||||
}
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// extractRdata extracts the rdata portion from a miekg/dns RR string.
|
||||
// The format is: "name.\t<TTL>\tIN\t<TYPE>\t<rdata...>"
|
||||
func extractRdata(rrString string, rrType string) string {
|
||||
// miekg/dns uses tab-separated fields
|
||||
marker := "\tIN\t" + rrType + "\t"
|
||||
idx := strings.Index(rrString, marker)
|
||||
if idx != -1 {
|
||||
return rrString[idx+len(marker):]
|
||||
}
|
||||
|
||||
// Fallback: try space-separated (shouldn't happen with miekg/dns)
|
||||
marker = " IN " + rrType + " "
|
||||
idx = strings.Index(rrString, marker)
|
||||
if idx != -1 {
|
||||
return rrString[idx+len(marker):]
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// libdnsRecordsToHappyDNS converts a slice of libdns Records to happydns Records.
|
||||
func libdnsRecordsToHappyDNS(recs []libdns.Record, zone string) ([]happydns.Record, error) {
|
||||
result := make([]happydns.Record, 0, len(recs))
|
||||
for _, rec := range recs {
|
||||
hdr, err := libdnsToHappyDNSRecord(rec, zone)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting libdns record %v: %w", rec.RR(), err)
|
||||
}
|
||||
result = append(result, hdr)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// happyDNSRecordsToLibdns converts a slice of happydns Records to libdns RR values.
|
||||
func happyDNSRecordsToLibdns(rrs []happydns.Record, zone string) []libdns.RR {
|
||||
result := make([]libdns.RR, len(rrs))
|
||||
for i, rr := range rrs {
|
||||
result[i] = happyDNSRecordToLibdnsRR(rr, zone)
|
||||
}
|
||||
return result
|
||||
}
|
||||
343
internal/adapters/libdns-records_test.go
Normal file
343
internal/adapters/libdns-records_test.go
Normal file
|
|
@ -0,0 +1,343 @@
|
|||
// This file is part of the happyDomain (R) project.
|
||||
// Copyright (c) 2020-2025 happyDomain
|
||||
// Authors: Pierre-Olivier Mercier, et al.
|
||||
//
|
||||
// This program is offered under a commercial and under the AGPL license.
|
||||
// For commercial licensing, contact us at <contact@happydomain.org>.
|
||||
//
|
||||
// For AGPL licensing:
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/libdns/libdns"
|
||||
"github.com/miekg/dns"
|
||||
|
||||
"git.happydns.org/happyDomain/model"
|
||||
)
|
||||
|
||||
func TestLibdnsToHappyDNS_A(t *testing.T) {
|
||||
rec := libdns.Address{}
|
||||
rec.Name = "www"
|
||||
rec.TTL = 300 * time.Second
|
||||
rec.IP = mustParseAddr("192.0.2.1")
|
||||
|
||||
result, err := libdnsToHappyDNSRecord(rec, "example.com.")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if result.Header().Name != "www.example.com." {
|
||||
t.Errorf("expected name www.example.com., got %s", result.Header().Name)
|
||||
}
|
||||
if result.Header().Rrtype != dns.TypeA {
|
||||
t.Errorf("expected type A, got %s", dns.TypeToString[result.Header().Rrtype])
|
||||
}
|
||||
if result.Header().Ttl != 300 {
|
||||
t.Errorf("expected TTL 300, got %d", result.Header().Ttl)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLibdnsToHappyDNS_AAAA(t *testing.T) {
|
||||
rec := libdns.Address{}
|
||||
rec.Name = "@"
|
||||
rec.TTL = 600 * time.Second
|
||||
rec.IP = mustParseAddr("2001:db8::1")
|
||||
|
||||
result, err := libdnsToHappyDNSRecord(rec, "example.com.")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if result.Header().Name != "example.com." {
|
||||
t.Errorf("expected name example.com., got %s", result.Header().Name)
|
||||
}
|
||||
if result.Header().Rrtype != dns.TypeAAAA {
|
||||
t.Errorf("expected type AAAA, got %s", dns.TypeToString[result.Header().Rrtype])
|
||||
}
|
||||
}
|
||||
|
||||
func TestLibdnsToHappyDNS_TXT(t *testing.T) {
|
||||
rec := libdns.TXT{
|
||||
Name: "@",
|
||||
TTL: 300 * time.Second,
|
||||
Text: "v=spf1 include:_spf.google.com ~all",
|
||||
}
|
||||
|
||||
result, err := libdnsToHappyDNSRecord(rec, "example.com.")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
txt, ok := result.(*happydns.TXT)
|
||||
if !ok {
|
||||
t.Fatalf("expected *happydns.TXT, got %T", result)
|
||||
}
|
||||
|
||||
if txt.Txt != "v=spf1 include:_spf.google.com ~all" {
|
||||
t.Errorf("expected TXT value 'v=spf1 include:_spf.google.com ~all', got %q", txt.Txt)
|
||||
}
|
||||
if txt.Hdr.Name != "example.com." {
|
||||
t.Errorf("expected name example.com., got %s", txt.Hdr.Name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLibdnsToHappyDNS_CNAME(t *testing.T) {
|
||||
rec := libdns.CNAME{
|
||||
Name: "www",
|
||||
TTL: 3600 * time.Second,
|
||||
Target: "example.com.",
|
||||
}
|
||||
|
||||
result, err := libdnsToHappyDNSRecord(rec, "example.com.")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if result.Header().Rrtype != dns.TypeCNAME {
|
||||
t.Errorf("expected type CNAME, got %s", dns.TypeToString[result.Header().Rrtype])
|
||||
}
|
||||
}
|
||||
|
||||
func TestLibdnsToHappyDNS_MX(t *testing.T) {
|
||||
rec := libdns.MX{
|
||||
Name: "@",
|
||||
TTL: 3600 * time.Second,
|
||||
Preference: 10,
|
||||
Target: "mail.example.com.",
|
||||
}
|
||||
|
||||
result, err := libdnsToHappyDNSRecord(rec, "example.com.")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if result.Header().Rrtype != dns.TypeMX {
|
||||
t.Errorf("expected type MX, got %s", dns.TypeToString[result.Header().Rrtype])
|
||||
}
|
||||
}
|
||||
|
||||
func TestHappyDNSToLibdns_A(t *testing.T) {
|
||||
rr, _ := dns.NewRR("www.example.com. 300 IN A 192.0.2.1")
|
||||
|
||||
result := happyDNSRecordToLibdnsRR(rr, "example.com.")
|
||||
|
||||
if result.Name != "www" {
|
||||
t.Errorf("expected name 'www', got %q", result.Name)
|
||||
}
|
||||
if result.Type != "A" {
|
||||
t.Errorf("expected type A, got %s", result.Type)
|
||||
}
|
||||
if result.TTL != 300*time.Second {
|
||||
t.Errorf("expected TTL 300s, got %v", result.TTL)
|
||||
}
|
||||
if result.Data != "192.0.2.1" {
|
||||
t.Errorf("expected data '192.0.2.1', got %q", result.Data)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHappyDNSToLibdns_TXT(t *testing.T) {
|
||||
txt := &happydns.TXT{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: "example.com.",
|
||||
Rrtype: dns.TypeTXT,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: 300,
|
||||
},
|
||||
Txt: "v=spf1 include:_spf.google.com ~all",
|
||||
}
|
||||
|
||||
result := happyDNSRecordToLibdnsRR(txt, "example.com.")
|
||||
|
||||
if result.Name != "@" {
|
||||
t.Errorf("expected name '@', got %q", result.Name)
|
||||
}
|
||||
if result.Type != "TXT" {
|
||||
t.Errorf("expected type TXT, got %s", result.Type)
|
||||
}
|
||||
if result.Data != "v=spf1 include:_spf.google.com ~all" {
|
||||
t.Errorf("expected data 'v=spf1 include:_spf.google.com ~all', got %q", result.Data)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHappyDNSToLibdns_Apex(t *testing.T) {
|
||||
rr, _ := dns.NewRR("example.com. 300 IN A 192.0.2.1")
|
||||
|
||||
result := happyDNSRecordToLibdnsRR(rr, "example.com.")
|
||||
|
||||
if result.Name != "@" {
|
||||
t.Errorf("expected name '@', got %q", result.Name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRoundTrip_A(t *testing.T) {
|
||||
original := libdns.Address{}
|
||||
original.Name = "www"
|
||||
original.TTL = 300 * time.Second
|
||||
original.IP = mustParseAddr("192.0.2.1")
|
||||
|
||||
zone := "example.com."
|
||||
|
||||
hdRecord, err := libdnsToHappyDNSRecord(original, zone)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error converting to happydns: %v", err)
|
||||
}
|
||||
|
||||
roundtripped := happyDNSRecordToLibdnsRR(hdRecord, zone)
|
||||
|
||||
origRR := original.RR()
|
||||
if roundtripped.Name != origRR.Name {
|
||||
t.Errorf("name mismatch: got %q, want %q", roundtripped.Name, origRR.Name)
|
||||
}
|
||||
if roundtripped.Type != origRR.Type {
|
||||
t.Errorf("type mismatch: got %q, want %q", roundtripped.Type, origRR.Type)
|
||||
}
|
||||
if roundtripped.TTL != origRR.TTL {
|
||||
t.Errorf("TTL mismatch: got %v, want %v", roundtripped.TTL, origRR.TTL)
|
||||
}
|
||||
if roundtripped.Data != origRR.Data {
|
||||
t.Errorf("data mismatch: got %q, want %q", roundtripped.Data, origRR.Data)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRoundTrip_TXT(t *testing.T) {
|
||||
original := libdns.TXT{
|
||||
Name: "test",
|
||||
TTL: 600 * time.Second,
|
||||
Text: "hello world with spaces and special chars: @#$%",
|
||||
}
|
||||
|
||||
zone := "example.com."
|
||||
|
||||
hdRecord, err := libdnsToHappyDNSRecord(original, zone)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error converting to happydns: %v", err)
|
||||
}
|
||||
|
||||
txt, ok := hdRecord.(*happydns.TXT)
|
||||
if !ok {
|
||||
t.Fatalf("expected *happydns.TXT, got %T", hdRecord)
|
||||
}
|
||||
if txt.Txt != original.Text {
|
||||
t.Errorf("TXT text mismatch after first conversion: got %q, want %q", txt.Txt, original.Text)
|
||||
}
|
||||
|
||||
roundtripped := happyDNSRecordToLibdnsRR(hdRecord, zone)
|
||||
|
||||
origRR := original.RR()
|
||||
if roundtripped.Name != origRR.Name {
|
||||
t.Errorf("name mismatch: got %q, want %q", roundtripped.Name, origRR.Name)
|
||||
}
|
||||
if roundtripped.Type != origRR.Type {
|
||||
t.Errorf("type mismatch: got %q, want %q", roundtripped.Type, origRR.Type)
|
||||
}
|
||||
if roundtripped.Data != origRR.Data {
|
||||
t.Errorf("data mismatch: got %q, want %q", roundtripped.Data, origRR.Data)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLibdnsToHappyDNS_TXT_QuotedData(t *testing.T) {
|
||||
// Some libdns providers (e.g. PowerDNS) return TXT data in RFC1035 presentation format.
|
||||
tests := []struct {
|
||||
name string
|
||||
data string
|
||||
expected string
|
||||
}{
|
||||
{"simple quoted", `"some-acme-challenge-value"`, "some-acme-challenge-value"},
|
||||
{"escaped quote", `"foo\"bar"`, `foo"bar`},
|
||||
{"escaped backslash", `"foo\\bar"`, `foo\bar`},
|
||||
{"multi-chunk", `"chunk1" "chunk2"`, "chunk1chunk2"},
|
||||
{"unquoted passthrough", "v=spf1 ~all", "v=spf1 ~all"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
rec := libdns.RR{
|
||||
Name: "_acme-challenge",
|
||||
TTL: 3600 * time.Second,
|
||||
Type: "TXT",
|
||||
Data: tt.data,
|
||||
}
|
||||
|
||||
result, err := libdnsToHappyDNSRecord(rec, "example.com.")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
txt, ok := result.(*happydns.TXT)
|
||||
if !ok {
|
||||
t.Fatalf("expected *happydns.TXT, got %T", result)
|
||||
}
|
||||
|
||||
if txt.Txt != tt.expected {
|
||||
t.Errorf("expected %q, got %q", tt.expected, txt.Txt)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLibdnsToHappyDNS_TXT_UnquotedData(t *testing.T) {
|
||||
// libdns.TXT returns raw unquoted text — should pass through unchanged.
|
||||
rec := libdns.TXT{
|
||||
Name: "@",
|
||||
TTL: 300 * time.Second,
|
||||
Text: "v=spf1 ~all",
|
||||
}
|
||||
|
||||
result, err := libdnsToHappyDNSRecord(rec, "example.com.")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
txt, ok := result.(*happydns.TXT)
|
||||
if !ok {
|
||||
t.Fatalf("expected *happydns.TXT, got %T", result)
|
||||
}
|
||||
|
||||
if txt.Txt != "v=spf1 ~all" {
|
||||
t.Errorf("expected unquoted TXT value, got %q", txt.Txt)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractRdata(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
rrType string
|
||||
want string
|
||||
}{
|
||||
{"www.example.com.\t300\tIN\tA\t192.0.2.1", "A", "192.0.2.1"},
|
||||
{"example.com.\t3600\tIN\tMX\t10 mail.example.com.", "MX", "10 mail.example.com."},
|
||||
{"example.com.\t300\tIN\tAAAA\t2001:db8::1", "AAAA", "2001:db8::1"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
got := extractRdata(tt.input, tt.rrType)
|
||||
if got != tt.want {
|
||||
t.Errorf("extractRdata(%q, %q) = %q, want %q", tt.input, tt.rrType, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func mustParseAddr(s string) netip.Addr {
|
||||
addr, err := netip.ParseAddr(s)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return addr
|
||||
}
|
||||
|
|
@ -184,7 +184,7 @@ func (pc *ProviderController) UpdateProvider(c *gin.Context) {
|
|||
func (pc *ProviderController) ClearProviders(c *gin.Context) {
|
||||
user := middleware.MyUser(c)
|
||||
if user != nil {
|
||||
providers, err := pc.providerService.ListUserProviders(user)
|
||||
providers, err := pc.providerService.ListUserProviders(c.Request.Context(), user)
|
||||
if err != nil {
|
||||
middleware.ErrorResponse(c, http.StatusInternalServerError, err)
|
||||
return
|
||||
|
|
|
|||
|
|
@ -87,6 +87,23 @@ func (ac *AuthUserController) ChangePassword(c *gin.Context) {
|
|||
ac.lc.Logout(c)
|
||||
}
|
||||
|
||||
// IsAuthUser checks if the currently authenticated session matches the given user identifier.
|
||||
//
|
||||
// @Summary Check if current user
|
||||
// @Schemes
|
||||
// @Description Check if the currently authenticated session matches the given user identifier.
|
||||
// @Tags users
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security securitydefinitions.basic
|
||||
// @Param userId path string true "User identifier"
|
||||
// @Success 204 {null} null
|
||||
// @Failure 401 {object} happydns.ErrorResponse "Authentication failure"
|
||||
// @Router /users/{userId}/is_auth_user [get]
|
||||
func (ac *AuthUserController) IsAuthUser(c *gin.Context) {
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// DeleteAuthUser delete the account related to the given user.
|
||||
//
|
||||
// @Summary Drop account
|
||||
|
|
|
|||
|
|
@ -106,7 +106,7 @@ func (dc *DomainController) AddDomain(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
err = dc.domainService.CreateDomain(user, &uz)
|
||||
err = dc.domainService.CreateDomain(c.Request.Context(), user, &uz)
|
||||
if err != nil {
|
||||
middleware.ErrorResponse(c, http.StatusInternalServerError, err)
|
||||
return
|
||||
|
|
@ -150,7 +150,7 @@ func (dc *DomainController) GetDomain(c *gin.Context) {
|
|||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param domainId path string true "Domain identifier"
|
||||
// @Param body body happydns.Domain true "The new object overriding the current domain"
|
||||
// @Param body body happydns.DomainUpdateInput true "The fields to update"
|
||||
// @Security securitydefinitions.basic
|
||||
// @Success 200 {object} happydns.Domain
|
||||
// @Failure 400 {object} happydns.ErrorResponse "Invalid input"
|
||||
|
|
@ -167,7 +167,7 @@ func (dc *DomainController) UpdateDomain(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
var domain happydns.Domain
|
||||
var domain happydns.DomainUpdateInput
|
||||
err := c.ShouldBindJSON(&domain)
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ func NewDomainLogController(domainLogService happydns.DomainLogUsecase) *DomainL
|
|||
// @Produce json
|
||||
// @Param domainId path string true "Domain identifier"
|
||||
// @Security securitydefinitions.basic
|
||||
// @Success 200 {object} []happydns.DomainLog
|
||||
// @Success 200 {array} happydns.DomainLog
|
||||
// @Failure 401 {object} happydns.ErrorResponse "Authentication failure"
|
||||
// @Failure 404 {object} happydns.ErrorResponse "Domain not found"
|
||||
// @Router /domains/{domainId}/logs [get]
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ func (pc *ProviderController) ListProviders(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
providers, err := pc.providerService.ListUserProviders(user)
|
||||
providers, err := pc.providerService.ListUserProviders(c.Request.Context(), user)
|
||||
if err != nil {
|
||||
middleware.ErrorResponse(c, http.StatusInternalServerError, err)
|
||||
return
|
||||
|
|
@ -119,7 +119,7 @@ func (pc *ProviderController) AddProvider(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
provider, err := pc.providerService.CreateProvider(user, &usrc)
|
||||
provider, err := pc.providerService.CreateProvider(c.Request.Context(), user, &usrc)
|
||||
if err != nil {
|
||||
middleware.ErrorResponse(c, http.StatusInternalServerError, err)
|
||||
return
|
||||
|
|
@ -137,7 +137,7 @@ func (pc *ProviderController) AddProvider(c *gin.Context) {
|
|||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param providerId path string true "Provider identifier"
|
||||
// @Param body body happydns.Provider true "The new object overriding the current provider"
|
||||
// @Param body body happydns.ProviderMinimal true "The new object overriding the current provider"
|
||||
// @Security securitydefinitions.basic
|
||||
// @Success 200 {object} happydns.Provider
|
||||
// @Failure 400 {object} happydns.ErrorResponse "Invalid input"
|
||||
|
|
@ -157,7 +157,7 @@ func (pc *ProviderController) UpdateProvider(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
err = pc.providerService.UpdateProviderFromMessage(old.Id, user, &provider)
|
||||
err = pc.providerService.UpdateProviderFromMessage(c.Request.Context(), old.Id, user, &provider)
|
||||
if err != nil {
|
||||
middleware.ErrorResponse(c, http.StatusInternalServerError, err)
|
||||
return
|
||||
|
|
@ -191,13 +191,13 @@ func (pc *ProviderController) DeleteProvider(c *gin.Context) {
|
|||
|
||||
providermeta := c.MustGet("providermeta").(*happydns.ProviderMeta)
|
||||
|
||||
err := pc.providerService.DeleteProvider(user, providermeta.Id)
|
||||
err := pc.providerService.DeleteProvider(c.Request.Context(), user, providermeta.Id)
|
||||
if err != nil {
|
||||
middleware.ErrorResponse(c, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusNoContent, nil)
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// GetDomainsHostedByProvider lists domains available to management from the given Provider.
|
||||
|
|
@ -210,7 +210,7 @@ func (pc *ProviderController) DeleteProvider(c *gin.Context) {
|
|||
// @Produce json
|
||||
// @Param providerId path string true "Provider identifier"
|
||||
// @Security securitydefinitions.basic
|
||||
// @Success 200 {object} happydns.Provider
|
||||
// @Success 200 {array} string
|
||||
// @Failure 400 {object} happydns.ErrorResponse "Unable to instantiate the provider"
|
||||
// @Failure 400 {object} happydns.ErrorResponse "The provider doesn't support domain listing"
|
||||
// @Failure 400 {object} happydns.ErrorResponse "Provider error"
|
||||
|
|
@ -220,7 +220,7 @@ func (pc *ProviderController) DeleteProvider(c *gin.Context) {
|
|||
func (pc *ProviderController) GetDomainsHostedByProvider(c *gin.Context) {
|
||||
provider := c.MustGet("provider").(*happydns.Provider)
|
||||
|
||||
domains, err := pc.providerService.ListHostedDomains(provider)
|
||||
domains, err := pc.providerService.ListHostedDomains(c.Request.Context(), provider)
|
||||
if err != nil {
|
||||
middleware.ErrorResponse(c, http.StatusBadRequest, err)
|
||||
return
|
||||
|
|
@ -238,8 +238,9 @@ func (pc *ProviderController) GetDomainsHostedByProvider(c *gin.Context) {
|
|||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param providerId path string true "Provider identifier"
|
||||
// @Param fqdn path string true "Fully qualified domain name"
|
||||
// @Security securitydefinitions.basic
|
||||
// @Success 200 {object} happydns.Provider
|
||||
// @Success 204 "Domain created successfully"
|
||||
// @Failure 400 {object} happydns.ErrorResponse "Unable to instantiate the provider"
|
||||
// @Failure 400 {object} happydns.ErrorResponse "The provider doesn't support domain listing"
|
||||
// @Failure 400 {object} happydns.ErrorResponse "Provider error"
|
||||
|
|
@ -249,11 +250,11 @@ func (pc *ProviderController) GetDomainsHostedByProvider(c *gin.Context) {
|
|||
func (pc *ProviderController) CreateDomainOnProvider(c *gin.Context) {
|
||||
provider := c.MustGet("provider").(*happydns.Provider)
|
||||
|
||||
err := pc.providerService.CreateDomainOnProvider(provider, c.Param("fqdn"))
|
||||
err := pc.providerService.CreateDomainOnProvider(c.Request.Context(), provider, c.Param("fqdn"))
|
||||
if err != nil {
|
||||
middleware.ErrorResponse(c, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, true)
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ func NewProviderSettingsController(pSettingsServices happydns.ProviderSettingsUs
|
|||
// @Param body body happydns.ProviderSettingsState true "The current state of the Provider's settings, possibly empty (but not null)"
|
||||
// @Security securitydefinitions.basic
|
||||
// @Success 200 {object} happydns.Provider "The Provider has been created with the given settings"
|
||||
// @Success 202 {object} happydns.ProviderSettingsResponse "The settings need more rafinement"
|
||||
// @Success 202 {object} happydns.ProviderSettingsResponse "The settings need more refinement"
|
||||
// @Failure 400 {object} happydns.ErrorResponse "Invalid input"
|
||||
// @Failure 401 {object} happydns.ErrorResponse "Authentication failure"
|
||||
// @Failure 404 {object} happydns.ErrorResponse "Provider not found"
|
||||
|
|
@ -78,7 +78,7 @@ func (psc *ProviderSettingsController) NextProviderSettingsState(c *gin.Context)
|
|||
return
|
||||
}
|
||||
|
||||
provider, form, err := psc.pSettingsServices.NextProviderSettingsState(&uss, pType, user)
|
||||
provider, form, err := psc.pSettingsServices.NextProviderSettingsState(c.Request.Context(), &uss, pType, user)
|
||||
if err != nil {
|
||||
middleware.ErrorResponse(c, http.StatusInternalServerError, err)
|
||||
return
|
||||
|
|
|
|||
|
|
@ -42,32 +42,6 @@ func NewResolverController(resolverService happydns.ResolverUsecase) *ResolverCo
|
|||
}
|
||||
}
|
||||
|
||||
// DNSMsg is the documentation struct corresponding to dns.Msg
|
||||
type DNSMsg struct {
|
||||
// Question is the Question section of the DNS response.
|
||||
Question []DNSQuestion
|
||||
|
||||
// Answer is the list of Answer records in the DNS response.
|
||||
Answer []any `swaggertype:"object"`
|
||||
|
||||
// Ns is the list of Authoritative records in the DNS response.
|
||||
Ns []any `swaggertype:"object"`
|
||||
|
||||
// Extra is the list of extra records in the DNS response.
|
||||
Extra []any `swaggertype:"object"`
|
||||
}
|
||||
|
||||
type DNSQuestion struct {
|
||||
// Name is the domain name researched.
|
||||
Name string
|
||||
|
||||
// Qtype is the type of record researched.
|
||||
Qtype uint16
|
||||
|
||||
// Qclass is the class of record researched.
|
||||
Qclass uint16
|
||||
}
|
||||
|
||||
// RunResolver performs a NS resolution for a given domain, with options.
|
||||
//
|
||||
// @Summary Perform a DNS resolution.
|
||||
|
|
@ -77,8 +51,7 @@ type DNSQuestion struct {
|
|||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param body body happydns.ResolverRequest true "Options to the resolution"
|
||||
// @Success 200 {object} DNSMsg
|
||||
// @Success 204 {object} happydns.ErrorResponse "No content"
|
||||
// @Success 200 {object} happydns.ResolverResponse
|
||||
// @Failure 400 {object} happydns.ErrorResponse "Invalid input"
|
||||
// @Failure 401 {object} happydns.ErrorResponse "Authentication failure"
|
||||
// @Failure 403 {object} happydns.ErrorResponse "The resolver refused to treat our request"
|
||||
|
|
@ -100,5 +73,5 @@ func (rc *ResolverController) RunResolver(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, r)
|
||||
c.JSON(http.StatusOK, happydns.NewResolverResponseFromMsg(r))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,9 +56,10 @@ func NewServiceController(duService happydns.ZoneServiceUsecase, suService happy
|
|||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security securitydefinitions.basic
|
||||
// @Param domainId path string true "Domain identifier"
|
||||
// @Param zoneId path string true "Zone identifier"
|
||||
// @Param subdomain path string true "Part of the subdomain considered for the service (@ for the root of the zone ; subdomain is relative to the root, do not include it)"
|
||||
// @Param domainId path string true "Domain identifier"
|
||||
// @Param zoneId path string true "Zone identifier"
|
||||
// @Param subdomain path string true "Part of the subdomain considered for the service (@ for the root of the zone ; subdomain is relative to the root, do not include it)"
|
||||
// @Param body body happydns.Service true "Service to add"
|
||||
// @Success 200 {object} happydns.Zone
|
||||
// @Failure 400 {object} happydns.ErrorResponse "Invalid input"
|
||||
// @Failure 401 {object} happydns.ErrorResponse "Authentication failure"
|
||||
|
|
|
|||
|
|
@ -145,6 +145,7 @@ func (ssc *ServiceSpecsController) InitializeServiceSpec(c *gin.Context) {
|
|||
// @Param serviceType path string true "The service's type"
|
||||
// @Param domain query string true "The domain to use to generate the records"
|
||||
// @Param ttl query int false "The TTL used by the generated records"
|
||||
// @Param body body happydns.Service true "The service configuration to generate records for"
|
||||
// @Success 200 {array} happydns.Record
|
||||
// @Failure 400 {object} happydns.ErrorResponse "Invalid request body"
|
||||
// @Failure 404 {object} happydns.ErrorResponse "Service type does not exist"
|
||||
|
|
|
|||
|
|
@ -68,9 +68,9 @@ func (sc *SessionController) SessionHandler(c *gin.Context) {
|
|||
// @Description Get the content of the current user's session.
|
||||
// @Tags users
|
||||
// @Accept json
|
||||
// @Prodsce json
|
||||
// @Produce json
|
||||
// @Security securitydefinitions.basic
|
||||
// @Ssccess 200 {object} happydns.Session
|
||||
// @Success 200 {object} happydns.Session
|
||||
// @Failure 401 {object} happydns.ErrorResponse "Authentication failure"
|
||||
// @Router /session [get]
|
||||
func (sc *SessionController) GetSession(c *gin.Context) {
|
||||
|
|
@ -97,9 +97,9 @@ func (sc *SessionController) GetSession(c *gin.Context) {
|
|||
// @Description Remove the content of the current user's session.
|
||||
// @Tags users
|
||||
// @Accept json
|
||||
// @Prodsce json
|
||||
// @Produce json
|
||||
// @Security securitydefinitions.basic
|
||||
// @Ssccess 204
|
||||
// @Success 204
|
||||
// @Failure 401 {object} happydns.ErrorResponse "Authentication failure"
|
||||
// @Router /session [delete]
|
||||
func (sc *SessionController) ClearSession(c *gin.Context) {
|
||||
|
|
@ -117,9 +117,9 @@ func (sc *SessionController) ClearSession(c *gin.Context) {
|
|||
// @Description Closes all sessions for a given user.
|
||||
// @Tags users
|
||||
// @Accept json
|
||||
// @Prodsce json
|
||||
// @Produce json
|
||||
// @Security securitydefinitions.basic
|
||||
// @Ssccess 204 {null} null
|
||||
// @Success 204
|
||||
// @Failure 401 {object} happydns.ErrorResponse "Authentication failure"
|
||||
// @Router /sessions [delete]
|
||||
func (sc *SessionController) ClearUserSessions(c *gin.Context) {
|
||||
|
|
@ -145,9 +145,9 @@ func (sc *SessionController) ClearUserSessions(c *gin.Context) {
|
|||
// @Description List the sessions open for the current user
|
||||
// @Tags users
|
||||
// @Accept json
|
||||
// @Prodsce json
|
||||
// @Produce json
|
||||
// @Security securitydefinitions.basic
|
||||
// @Ssccess 200 {object} happydns.Session
|
||||
// @Success 200 {array} happydns.Session
|
||||
// @Failure 401 {object} happydns.ErrorResponse "Authentication failure"
|
||||
// @Router /sessions [get]
|
||||
func (sc *SessionController) GetSessions(c *gin.Context) {
|
||||
|
|
@ -173,13 +173,14 @@ func (sc *SessionController) GetSessions(c *gin.Context) {
|
|||
// @Description Create a new session for the current user.
|
||||
// @Tags users
|
||||
// @Accept json
|
||||
// @Prodsce json
|
||||
// @Produce json
|
||||
// @Security securitydefinitions.basic
|
||||
// @Ssccess 200 {object} happydns.Session
|
||||
// @Failure 401 {object} happydns.ErrorResponse "Authentication failure"
|
||||
// @Param body body happydns.SessionInput true "Session to create"
|
||||
// @Success 200 {object} happydns.Session
|
||||
// @Failure 401 {object} happydns.ErrorResponse "Authentication failure"
|
||||
// @Router /sessions [post]
|
||||
func (sc *SessionController) CreateSession(c *gin.Context) {
|
||||
var us happydns.Session
|
||||
var us happydns.SessionInput
|
||||
err := c.ShouldBindJSON(&us)
|
||||
if err != nil {
|
||||
log.Printf("%s sends invalid Session JSON: %s", c.ClientIP(), err.Error())
|
||||
|
|
@ -199,7 +200,7 @@ func (sc *SessionController) CreateSession(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"id": sess.Id})
|
||||
c.JSON(http.StatusOK, sess)
|
||||
}
|
||||
|
||||
// UpdateSession update a session owned by the current user
|
||||
|
|
@ -209,14 +210,15 @@ func (sc *SessionController) CreateSession(c *gin.Context) {
|
|||
// @Description Update a session owned by the current user.
|
||||
// @Tags users
|
||||
// @Accept json
|
||||
// @Param sessionId path string true "Session identifier"
|
||||
// @Prodsce json
|
||||
// @Produce json
|
||||
// @Security securitydefinitions.basic
|
||||
// @Ssccess 200 {object} happydns.Session
|
||||
// @Failure 401 {object} happydns.ErrorResponse "Authentication failure"
|
||||
// @Param sessionId path string true "Session identifier"
|
||||
// @Param body body happydns.SessionInput true "Session fields to update"
|
||||
// @Success 200 {object} happydns.Session
|
||||
// @Failure 401 {object} happydns.ErrorResponse "Authentication failure"
|
||||
// @Router /sessions/{sessionId} [put]
|
||||
func (sc *SessionController) UpdateSession(c *gin.Context) {
|
||||
var us happydns.Session
|
||||
var us happydns.SessionInput
|
||||
err := c.ShouldBindJSON(&us)
|
||||
if err != nil {
|
||||
log.Printf("%s sends invalid Session JSON: %s", c.ClientIP(), err.Error())
|
||||
|
|
|
|||
|
|
@ -53,7 +53,8 @@ func NewUserController(userService happydns.UserUsecase, lc *LoginController) *U
|
|||
// @Tags users
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {object} happydns.User "The created user"
|
||||
// @Param userId path string true "User identifier"
|
||||
// @Success 200 {object} happydns.User "The user"
|
||||
// @Failure 500 {object} happydns.ErrorResponse
|
||||
// @Router /users/{userId} [get]
|
||||
func (uc *UserController) GetUser(c *gin.Context) {
|
||||
|
|
|
|||
|
|
@ -145,7 +145,7 @@ func (zc *ZoneController) DiffZonesHandler(c *gin.Context) {
|
|||
// @Param domainId path string true "Domain identifier"
|
||||
// @Param zoneId path string true "Zone identifier to use as the new one."
|
||||
// @Param oldZoneId path string true "Zone identifier to use as the old one. Currently only @ are expected, to use the currently deployed zone."
|
||||
// @Success 200 {object} []happydns.Correction "Differences, reported as text, one diff per item"
|
||||
// @Success 200 {array} happydns.Correction "Differences, reported as text, one diff per item"
|
||||
// @Failure 400 {object} happydns.ErrorResponse "Invalid input"
|
||||
// @Failure 401 {object} happydns.ErrorResponse "Authentication failure"
|
||||
// @Failure 404 {object} happydns.ErrorResponse "Domain not found"
|
||||
|
|
@ -194,7 +194,7 @@ func (zc *ZoneController) DiffZonesSummary(c *gin.Context) {
|
|||
// @Security securitydefinitions.basic
|
||||
// @Param domainId path string true "Domain identifier"
|
||||
// @Param zoneId path string true "Zone identifier"
|
||||
// @Param body body []string true "Differences (from /diff_zones) to apply"
|
||||
// @Param body body happydns.ApplyZoneForm true "Differences to apply with commit message"
|
||||
// @Success 200 {object} happydns.ZoneMeta "The new Zone metadata containing the current zone"
|
||||
// @Failure 400 {object} happydns.ErrorResponse "Invalid input"
|
||||
// @Failure 401 {object} happydns.ErrorResponse "Authentication failure"
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ func ProviderMetaHandler(providerService happydns.ProviderUsecase) gin.HandlerFu
|
|||
}
|
||||
|
||||
// Retrieve provider meta
|
||||
providermeta, err := providerService.GetUserProviderMeta(user, pid)
|
||||
providermeta, err := providerService.GetUserProviderMeta(c.Request.Context(), user, pid)
|
||||
if err != nil {
|
||||
ErrorResponse(c, http.StatusNotFound, fmt.Errorf("provider not found"))
|
||||
return
|
||||
|
|
@ -77,7 +77,7 @@ func ProviderHandler(providerService happydns.ProviderUsecase) gin.HandlerFunc {
|
|||
}
|
||||
|
||||
// Retrieve provider
|
||||
provider, err := providerService.GetUserProvider(user, pid)
|
||||
provider, err := providerService.GetUserProvider(c.Request.Context(), user, pid)
|
||||
if err != nil {
|
||||
ErrorResponse(c, http.StatusNotFound, fmt.Errorf("provider not found"))
|
||||
return
|
||||
|
|
|
|||
|
|
@ -27,13 +27,13 @@ import (
|
|||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"git.happydns.org/happyDomain/providers"
|
||||
providerReg "git.happydns.org/happyDomain/internal/provider"
|
||||
)
|
||||
|
||||
func ProviderSpecsHandler(c *gin.Context) {
|
||||
psid := string(c.Param("psid"))
|
||||
|
||||
pbody, err := providers.FindProvider(psid)
|
||||
pbody, err := providerReg.FindProvider(psid)
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": fmt.Sprintf("Unable to find provider: %s", err.Error())})
|
||||
return
|
||||
|
|
|
|||
|
|
@ -28,13 +28,13 @@ import (
|
|||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"git.happydns.org/happyDomain/services"
|
||||
intsvc "git.happydns.org/happyDomain/internal/service"
|
||||
)
|
||||
|
||||
func ServiceSpecsHandler(c *gin.Context) {
|
||||
ssid := string(c.Param("ssid"))
|
||||
|
||||
svc, err := svcs.FindSubService(ssid)
|
||||
svc, err := intsvc.FindSubService(ssid)
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": fmt.Sprintf("Unable to find specs: %s", err.Error())})
|
||||
return
|
||||
|
|
|
|||
|
|
@ -22,8 +22,6 @@
|
|||
package route
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"git.happydns.org/happyDomain/internal/api/controller"
|
||||
|
|
@ -36,9 +34,7 @@ func DeclareAuthUserRoutes(router *gin.RouterGroup, authUserUC happydns.AuthUser
|
|||
|
||||
apiUserAuthRoutes := router.Group("/users/:uid")
|
||||
apiUserAuthRoutes.Use(middleware.AuthUserHandler(authUserUC))
|
||||
apiUserAuthRoutes.GET("/is_auth_user", func(c *gin.Context) {
|
||||
c.Status(http.StatusNoContent)
|
||||
})
|
||||
apiUserAuthRoutes.GET("/is_auth_user", ac.IsAuthUser)
|
||||
apiUserAuthRoutes.POST("/delete", ac.DeleteAuthUser)
|
||||
apiUserAuthRoutes.POST("/new_password", ac.ChangePassword)
|
||||
|
||||
|
|
|
|||
|
|
@ -93,6 +93,9 @@ func NewApp(cfg *happydns.Options) *App {
|
|||
app.initStorageEngine()
|
||||
app.initNewsletter()
|
||||
app.initInsights()
|
||||
if err := app.initPlugins(); err != nil {
|
||||
log.Fatalf("Plugin initialization error: %s", err)
|
||||
}
|
||||
app.initUsecases()
|
||||
app.initCaptcha()
|
||||
app.setupRouter()
|
||||
|
|
@ -108,6 +111,9 @@ func NewAppWithStorage(cfg *happydns.Options, store storage.Storage) *App {
|
|||
|
||||
app.initMailer()
|
||||
app.initNewsletter()
|
||||
if err := app.initPlugins(); err != nil {
|
||||
log.Fatalf("Plugin initialization error: %s", err)
|
||||
}
|
||||
app.initUsecases()
|
||||
app.initCaptcha()
|
||||
app.setupRouter()
|
||||
|
|
|
|||
140
internal/app/plugins.go
Normal file
140
internal/app/plugins.go
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
// This file is part of the happyDomain (R) project.
|
||||
// Copyright (c) 2020-2026 happyDomain
|
||||
// Authors: Pierre-Olivier Mercier, et al.
|
||||
//
|
||||
// This program is offered under a commercial and under the AGPL license.
|
||||
// For commercial licensing, contact us at <contact@happydomain.org>.
|
||||
//
|
||||
// For AGPL licensing:
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"plugin"
|
||||
|
||||
sdk "git.happydns.org/checker-sdk-go/checker"
|
||||
"git.happydns.org/happyDomain/internal/checker"
|
||||
)
|
||||
|
||||
// pluginLoader attempts to find and register one specific kind of plugin
|
||||
// symbol from an already-opened .so file.
|
||||
//
|
||||
// It returns (true, nil) when the symbol was found and registration
|
||||
// succeeded, (true, err) when the symbol was found but something went wrong,
|
||||
// and (false, nil) when the symbol simply isn't present in that file (which
|
||||
// is not considered an error — a single .so may implement only a subset of
|
||||
// the known plugin types).
|
||||
type pluginLoader func(p *plugin.Plugin, fname string) (found bool, err error)
|
||||
|
||||
// pluginLoaders is the authoritative list of plugin types that happyDomain
|
||||
// knows about. To support a new plugin type, add a single entry here.
|
||||
var pluginLoaders = []pluginLoader{
|
||||
loadCheckerPlugin,
|
||||
}
|
||||
|
||||
// loadCheckerPlugin handles the NewCheckerPlugin symbol exported by checkers
|
||||
// built against checker-sdk-go (see ../../checker-dummy/README.md).
|
||||
func loadCheckerPlugin(p *plugin.Plugin, fname string) (bool, error) {
|
||||
sym, err := p.Lookup("NewCheckerPlugin")
|
||||
if err != nil {
|
||||
// Symbol not present in this .so — not an error.
|
||||
return false, nil
|
||||
}
|
||||
|
||||
factory, ok := sym.(func() (*sdk.CheckerDefinition, sdk.ObservationProvider, error))
|
||||
if !ok {
|
||||
return true, fmt.Errorf("symbol NewCheckerPlugin has unexpected type %T", sym)
|
||||
}
|
||||
|
||||
def, provider, err := factory()
|
||||
if err != nil {
|
||||
return true, err
|
||||
}
|
||||
if def == nil {
|
||||
return true, fmt.Errorf("NewCheckerPlugin returned a nil CheckerDefinition")
|
||||
}
|
||||
if provider == nil {
|
||||
return true, fmt.Errorf("NewCheckerPlugin returned a nil ObservationProvider")
|
||||
}
|
||||
|
||||
checker.RegisterObservationProvider(provider)
|
||||
checker.RegisterExternalizableChecker(def)
|
||||
log.Printf("Plugin %s (%s) loaded", def.ID, fname)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// initPlugins scans each directory listed in cfg.PluginsDirectories and loads
|
||||
// every .so file found as a Go plugin. A directory that cannot be read is a
|
||||
// fatal configuration error; individual plugin failures are logged and
|
||||
// skipped so that one bad .so does not prevent the others from loading.
|
||||
func (a *App) initPlugins() error {
|
||||
for _, directory := range a.cfg.PluginsDirectories {
|
||||
files, err := os.ReadDir(directory)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to read plugins directory %q: %s", directory, err)
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
if file.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
// Only attempt to load shared-object files.
|
||||
if filepath.Ext(file.Name()) != ".so" {
|
||||
continue
|
||||
}
|
||||
|
||||
fname := filepath.Join(directory, file.Name())
|
||||
|
||||
if err := loadPlugin(fname); err != nil {
|
||||
log.Printf("Unable to load plugin %q: %s", fname, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// loadPlugin opens the .so file at fname and runs every registered
|
||||
// pluginLoader against it. A loader that does not find its symbol is silently
|
||||
// skipped. If no loader recognises any symbol in the file a warning is
|
||||
// logged, because the file might be a valid plugin for a future version of
|
||||
// happyDomain. The first loader error encountered is returned immediately.
|
||||
func loadPlugin(fname string) error {
|
||||
p, err := plugin.Open(fname)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
anyFound := false
|
||||
for _, loader := range pluginLoaders {
|
||||
found, err := loader(p, fname)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if found {
|
||||
anyFound = true
|
||||
}
|
||||
}
|
||||
|
||||
if !anyFound {
|
||||
log.Printf("Warning: plugin %q exports no recognised symbols", fname)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
48
internal/checker/aggregator.go
Normal file
48
internal/checker/aggregator.go
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
// This file is part of the happyDomain (R) project.
|
||||
// Copyright (c) 2020-2025 happyDomain
|
||||
// Authors: Pierre-Olivier Mercier, et al.
|
||||
//
|
||||
// This program is offered under a commercial and under the AGPL license.
|
||||
// For commercial licensing, contact us at <contact@happydomain.org>.
|
||||
//
|
||||
// For AGPL licensing:
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package checker
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"git.happydns.org/happyDomain/model"
|
||||
)
|
||||
|
||||
// WorstStatusAggregator aggregates check states by taking the worst status.
|
||||
type WorstStatusAggregator struct{}
|
||||
|
||||
func (a WorstStatusAggregator) Aggregate(states []happydns.CheckState) happydns.CheckState {
|
||||
worst := happydns.StatusOK
|
||||
var messages []string
|
||||
for _, s := range states {
|
||||
if s.Status > worst {
|
||||
worst = s.Status
|
||||
}
|
||||
if s.Message != "" {
|
||||
messages = append(messages, s.Message)
|
||||
}
|
||||
}
|
||||
return happydns.CheckState{
|
||||
Status: worst,
|
||||
Message: strings.Join(messages, "; "),
|
||||
}
|
||||
}
|
||||
243
internal/checker/observation.go
Normal file
243
internal/checker/observation.go
Normal file
|
|
@ -0,0 +1,243 @@
|
|||
// This file is part of the happyDomain (R) project.
|
||||
// Copyright (c) 2020-2025 happyDomain
|
||||
// Authors: Pierre-Olivier Mercier, et al.
|
||||
//
|
||||
// This program is offered under a commercial and under the AGPL license.
|
||||
// For commercial licensing, contact us at <contact@happydomain.org>.
|
||||
//
|
||||
// For AGPL licensing:
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package checker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
sdk "git.happydns.org/checker-sdk-go/checker"
|
||||
"git.happydns.org/happyDomain/model"
|
||||
)
|
||||
|
||||
// The observation provider registry lives in the Apache-2.0 licensed
|
||||
// checker-sdk-go module, so external plugins can register themselves
|
||||
// without depending on AGPL code. These wrappers preserve the existing
|
||||
// happyDomain call sites.
|
||||
|
||||
// RegisterObservationProvider registers an observation provider globally.
|
||||
func RegisterObservationProvider(p happydns.ObservationProvider) {
|
||||
sdk.RegisterObservationProvider(p)
|
||||
}
|
||||
|
||||
// GetObservationProvider returns the provider for the given key, or nil.
|
||||
func GetObservationProvider(key happydns.ObservationKey) happydns.ObservationProvider {
|
||||
return sdk.FindObservationProvider(key)
|
||||
}
|
||||
|
||||
// GetObservationProviders returns all registered observation providers.
|
||||
func GetObservationProviders() map[happydns.ObservationKey]happydns.ObservationProvider {
|
||||
return sdk.GetObservationProviders()
|
||||
}
|
||||
|
||||
// ObservationCacheLookup resolves a cached observation for a target+key.
|
||||
// Returns the raw data and collection time, or an error if not cached.
|
||||
type ObservationCacheLookup func(target happydns.CheckTarget, key happydns.ObservationKey) (json.RawMessage, time.Time, error)
|
||||
|
||||
// ObservationContext provides lazy-loading, cached, thread-safe access to observation data.
|
||||
// Collected data is serialized to json.RawMessage immediately after collection.
|
||||
type ObservationContext struct {
|
||||
target happydns.CheckTarget
|
||||
opts happydns.CheckerOptions
|
||||
cache map[happydns.ObservationKey]json.RawMessage
|
||||
errors map[happydns.ObservationKey]error
|
||||
mu sync.RWMutex
|
||||
cacheLookup ObservationCacheLookup // nil = no DB cache
|
||||
freshness time.Duration // 0 = always collect
|
||||
providerOverride map[happydns.ObservationKey]happydns.ObservationProvider
|
||||
}
|
||||
|
||||
// NewObservationContext creates a new ObservationContext for the given target and options.
|
||||
// cacheLookup and freshness enable cross-checker observation reuse from stored snapshots.
|
||||
// Pass nil and 0 to disable DB-based caching.
|
||||
func NewObservationContext(target happydns.CheckTarget, opts happydns.CheckerOptions, cacheLookup ObservationCacheLookup, freshness time.Duration) *ObservationContext {
|
||||
return &ObservationContext{
|
||||
target: target,
|
||||
opts: opts,
|
||||
cache: make(map[happydns.ObservationKey]json.RawMessage),
|
||||
errors: make(map[happydns.ObservationKey]error),
|
||||
cacheLookup: cacheLookup,
|
||||
freshness: freshness,
|
||||
}
|
||||
}
|
||||
|
||||
// SetProviderOverride registers a per-context provider that takes precedence
|
||||
// over the global registry for the given observation key. This is used to
|
||||
// substitute local providers with HTTP-backed ones when an endpoint is configured.
|
||||
func (oc *ObservationContext) SetProviderOverride(key happydns.ObservationKey, p happydns.ObservationProvider) {
|
||||
if oc.providerOverride == nil {
|
||||
oc.providerOverride = make(map[happydns.ObservationKey]happydns.ObservationProvider)
|
||||
}
|
||||
oc.providerOverride[key] = p
|
||||
}
|
||||
|
||||
// getProvider returns the observation provider for the given key, checking
|
||||
// per-context overrides first, then falling back to the global registry.
|
||||
func (oc *ObservationContext) getProvider(key happydns.ObservationKey) happydns.ObservationProvider {
|
||||
if oc.providerOverride != nil {
|
||||
if p, ok := oc.providerOverride[key]; ok {
|
||||
return p
|
||||
}
|
||||
}
|
||||
return GetObservationProvider(key)
|
||||
}
|
||||
|
||||
// Get collects observation data for the given key (lazily) and unmarshals it into dest.
|
||||
// Thread-safe: concurrent calls for the same key will only trigger one collection.
|
||||
func (oc *ObservationContext) Get(ctx context.Context, key happydns.ObservationKey, dest any) error {
|
||||
// Fast path: check cache under read lock.
|
||||
oc.mu.RLock()
|
||||
if raw, ok := oc.cache[key]; ok {
|
||||
oc.mu.RUnlock()
|
||||
return json.Unmarshal(raw, dest)
|
||||
}
|
||||
if err, ok := oc.errors[key]; ok {
|
||||
oc.mu.RUnlock()
|
||||
return err
|
||||
}
|
||||
oc.mu.RUnlock()
|
||||
|
||||
// Slow path: acquire write lock and collect.
|
||||
oc.mu.Lock()
|
||||
defer oc.mu.Unlock()
|
||||
|
||||
// Double-check after acquiring write lock.
|
||||
if raw, ok := oc.cache[key]; ok {
|
||||
return json.Unmarshal(raw, dest)
|
||||
}
|
||||
if err, ok := oc.errors[key]; ok {
|
||||
return err
|
||||
}
|
||||
|
||||
// Try DB cache before collecting fresh data.
|
||||
if oc.cacheLookup != nil && oc.freshness > 0 {
|
||||
if raw, collectedAt, err := oc.cacheLookup(oc.target, key); err == nil {
|
||||
if time.Since(collectedAt) < oc.freshness {
|
||||
oc.cache[key] = raw
|
||||
return json.Unmarshal(raw, dest)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
provider := oc.getProvider(key)
|
||||
if provider == nil {
|
||||
err := fmt.Errorf("no observation provider registered for key %q", key)
|
||||
oc.errors[key] = err
|
||||
return err
|
||||
}
|
||||
|
||||
val, err := provider.Collect(ctx, oc.opts)
|
||||
if err != nil {
|
||||
oc.errors[key] = err
|
||||
return err
|
||||
}
|
||||
|
||||
raw, err := json.Marshal(val)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("observation %q: marshal failed: %w", key, err)
|
||||
oc.errors[key] = err
|
||||
return err
|
||||
}
|
||||
|
||||
oc.cache[key] = json.RawMessage(raw)
|
||||
return json.Unmarshal(raw, dest)
|
||||
}
|
||||
|
||||
// Data returns all cached observation data as pre-serialized JSON.
|
||||
func (oc *ObservationContext) Data() map[happydns.ObservationKey]json.RawMessage {
|
||||
oc.mu.RLock()
|
||||
defer oc.mu.RUnlock()
|
||||
|
||||
data := make(map[happydns.ObservationKey]json.RawMessage, len(oc.cache))
|
||||
for k, v := range oc.cache {
|
||||
data[k] = v
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
// HasHTMLReporter returns true if any registered observation provider implements CheckerHTMLReporter.
|
||||
func HasHTMLReporter() bool {
|
||||
for _, p := range sdk.GetObservationProviders() {
|
||||
if _, ok := p.(happydns.CheckerHTMLReporter); ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// GetHTMLReport renders an HTML report for the given observation key and raw JSON data.
|
||||
// Returns (html, true, nil) if the provider supports HTML reports, or ("", false, nil) if not.
|
||||
func GetHTMLReport(key happydns.ObservationKey, raw json.RawMessage) (string, bool, error) {
|
||||
provider := GetObservationProvider(key)
|
||||
if provider == nil {
|
||||
return "", false, fmt.Errorf("no observation provider registered for key %q", key)
|
||||
}
|
||||
|
||||
hr, ok := provider.(happydns.CheckerHTMLReporter)
|
||||
if !ok {
|
||||
return "", false, nil
|
||||
}
|
||||
html, err := hr.GetHTMLReport(raw)
|
||||
return html, true, err
|
||||
}
|
||||
|
||||
// HasMetricsReporter returns true if any registered observation provider implements CheckerMetricsReporter.
|
||||
func HasMetricsReporter() bool {
|
||||
for _, p := range sdk.GetObservationProviders() {
|
||||
if _, ok := p.(happydns.CheckerMetricsReporter); ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// GetMetrics extracts metrics for the given observation key and raw JSON data.
|
||||
// Returns (metrics, true, nil) if the provider supports metrics, or (nil, false, nil) if not.
|
||||
func GetMetrics(key happydns.ObservationKey, raw json.RawMessage, collectedAt time.Time) ([]happydns.CheckMetric, bool, error) {
|
||||
provider := GetObservationProvider(key)
|
||||
if provider == nil {
|
||||
return nil, false, fmt.Errorf("no observation provider registered for key %q", key)
|
||||
}
|
||||
|
||||
mr, ok := provider.(happydns.CheckerMetricsReporter)
|
||||
if !ok {
|
||||
return nil, false, nil
|
||||
}
|
||||
metrics, err := mr.ExtractMetrics(raw, collectedAt)
|
||||
return metrics, true, err
|
||||
}
|
||||
|
||||
// GetAllMetrics extracts metrics from all observation keys in a snapshot.
|
||||
func GetAllMetrics(snap *happydns.ObservationSnapshot) ([]happydns.CheckMetric, error) {
|
||||
var allMetrics []happydns.CheckMetric
|
||||
for key, raw := range snap.Data {
|
||||
metrics, supported, err := GetMetrics(key, raw, snap.CollectedAt)
|
||||
if err != nil || !supported {
|
||||
continue
|
||||
}
|
||||
allMetrics = append(allMetrics, metrics...)
|
||||
}
|
||||
return allMetrics, nil
|
||||
}
|
||||
55
internal/checker/registry.go
Normal file
55
internal/checker/registry.go
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
// This file is part of the happyDomain (R) project.
|
||||
// Copyright (c) 2020-2026 happyDomain
|
||||
// Authors: Pierre-Olivier Mercier, et al.
|
||||
//
|
||||
// This program is offered under a commercial and under the AGPL license.
|
||||
// For commercial licensing, contact us at <contact@happydomain.org>.
|
||||
//
|
||||
// For AGPL licensing:
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package checker
|
||||
|
||||
import (
|
||||
sdk "git.happydns.org/checker-sdk-go/checker"
|
||||
"git.happydns.org/happyDomain/model"
|
||||
)
|
||||
|
||||
// The checker definition registry lives in the Apache-2.0 licensed
|
||||
// checker-sdk-go module, so external plugins can register themselves
|
||||
// without depending on AGPL code. These wrappers preserve the existing
|
||||
// happyDomain call sites.
|
||||
|
||||
// RegisterChecker registers a checker definition globally.
|
||||
func RegisterChecker(c *happydns.CheckerDefinition) {
|
||||
sdk.RegisterChecker(c)
|
||||
}
|
||||
|
||||
// RegisterExternalizableChecker registers a checker that supports being
|
||||
// delegated to a remote HTTP endpoint. It appends an "endpoint" AdminOpt
|
||||
// so the administrator can optionally configure a remote URL.
|
||||
// When the endpoint is left empty, the checker runs locally as usual.
|
||||
func RegisterExternalizableChecker(c *happydns.CheckerDefinition) {
|
||||
sdk.RegisterExternalizableChecker(c)
|
||||
}
|
||||
|
||||
// GetCheckers returns all registered checker definitions.
|
||||
func GetCheckers() map[string]*happydns.CheckerDefinition {
|
||||
return sdk.GetCheckers()
|
||||
}
|
||||
|
||||
// FindChecker returns the checker definition with the given ID, or nil.
|
||||
func FindChecker(id string) *happydns.CheckerDefinition {
|
||||
return sdk.FindChecker(id)
|
||||
}
|
||||
|
|
@ -24,6 +24,7 @@ package config // import "git.happydns.org/happyDomain/config"
|
|||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"runtime"
|
||||
|
||||
"git.happydns.org/happyDomain/internal/storage"
|
||||
"git.happydns.org/happyDomain/model"
|
||||
|
|
@ -45,6 +46,7 @@ func declareFlags(o *happydns.Options) {
|
|||
flag.Var(&JWTSecretKey{&o.JWTSecretKey}, "jwt-secret-key", "Secret key used to verify JWT authentication tokens (a random secret is used if undefined)")
|
||||
flag.Var(&URL{&o.ExternalAuth}, "external-auth", "Base URL to use for login and registration (use embedded forms if left empty)")
|
||||
flag.BoolVar(&o.OptOutInsights, "opt-out-insights", false, "Disable the anonymous usage statistics report. If you care about this project and don't participate in discussions, don't opt-out.")
|
||||
flag.IntVar(&o.CheckerMaxConcurrency, "checker-max-concurrency", runtime.NumCPU(), "Maximum number of checker jobs that can run simultaneously")
|
||||
|
||||
flag.Var(&URL{&o.ListmonkURL}, "newsletter-server-url", "Base URL of the listmonk newsletter server")
|
||||
flag.IntVar(&o.ListmonkID, "newsletter-id", 1, "Listmonk identifier of the list receiving the new user")
|
||||
|
|
@ -60,6 +62,8 @@ func declareFlags(o *happydns.Options) {
|
|||
flag.StringVar(&o.CaptchaProvider, "captcha-provider", o.CaptchaProvider, "Captcha provider to use for bot protection (altcha, hcaptcha, recaptchav2, turnstile, or empty to disable)")
|
||||
flag.IntVar(&o.CaptchaLoginThreshold, "captcha-login-threshold", 3, "Number of failed login attempts before captcha is required (0 = always require when provider configured)")
|
||||
|
||||
flag.Var(&stringSlice{&o.PluginsDirectories}, "plugins-directory", "Path to a directory containing checker plugins (.so files); may be repeated")
|
||||
|
||||
// Others flags are declared in some other files likes sources, storages, ... when they need specials configurations
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -25,8 +25,27 @@ import (
|
|||
"encoding/base64"
|
||||
"net/mail"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// stringSlice is a flag.Value that accumulates string values across repeated
|
||||
// invocations of the same flag (e.g. -plugins-directory a -plugins-directory b).
|
||||
type stringSlice struct {
|
||||
Values *[]string
|
||||
}
|
||||
|
||||
func (s *stringSlice) String() string {
|
||||
if s.Values == nil {
|
||||
return ""
|
||||
}
|
||||
return strings.Join(*s.Values, ",")
|
||||
}
|
||||
|
||||
func (s *stringSlice) Set(value string) error {
|
||||
*s.Values = append(*s.Values, value)
|
||||
return nil
|
||||
}
|
||||
|
||||
type JWTSecretKey struct {
|
||||
Secret *[]byte
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,7 +22,9 @@
|
|||
package forms // import "git.happydns.org/happyDomain/forms"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"git.happydns.org/happyDomain/model"
|
||||
|
|
@ -78,6 +80,53 @@ func GenField(field reflect.StructField) (f *happydns.Field) {
|
|||
return
|
||||
}
|
||||
|
||||
// ValidateStructValues validates the field values of a struct against the
|
||||
// constraints declared in its happydomain struct tags (choices, required).
|
||||
// Since the struct is already typed, basic type checking is handled by the
|
||||
// JSON decoder; this function validates higher-level constraints.
|
||||
func ValidateStructValues(data any) error {
|
||||
if data == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
v := reflect.Indirect(reflect.ValueOf(data))
|
||||
t := v.Type()
|
||||
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
sf := t.Field(i)
|
||||
if sf.Anonymous {
|
||||
if err := ValidateStructValues(v.Field(i).Interface()); err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
field := GenField(sf)
|
||||
fv := v.Field(i)
|
||||
|
||||
if field.Required && fv.IsZero() {
|
||||
label := field.Label
|
||||
if label == "" {
|
||||
label = field.Id
|
||||
}
|
||||
return fmt.Errorf("field %q is required", label)
|
||||
}
|
||||
|
||||
if len(field.Choices) > 0 && fv.Kind() == reflect.String {
|
||||
s := fv.String()
|
||||
if s != "" && !slices.Contains(field.Choices, s) {
|
||||
label := field.Label
|
||||
if label == "" {
|
||||
label = field.Id
|
||||
}
|
||||
return fmt.Errorf("field %q: value %q is not a valid choice (valid: %v)", label, s, field.Choices)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GenStructFields generates corresponding SourceFields of the given Source.
|
||||
func GenStructFields(data any) (fields []*happydns.Field) {
|
||||
if data != nil {
|
||||
|
|
|
|||
|
|
@ -33,6 +33,9 @@ func DoSettingState(fu happydns.FormUsecase, state *happydns.FormState, data any
|
|||
}
|
||||
|
||||
if state.State == 1 {
|
||||
if verr := ValidateStructValues(data); verr != nil {
|
||||
return nil, nil, verr
|
||||
}
|
||||
err = happydns.DoneForm
|
||||
} else {
|
||||
form = defaultForm(data)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
// This file is part of the happyDomain (R) project.
|
||||
// Copyright (c) 2020-2024 happyDomain
|
||||
// Copyright (c) 2020-2026 happyDomain
|
||||
// Authors: Pierre-Olivier Mercier, et al.
|
||||
//
|
||||
// This program is offered under a commercial and under the AGPL license.
|
||||
|
|
@ -19,40 +19,50 @@
|
|||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package providers // import "git.happydns.org/happyDomain/providers"
|
||||
package provider
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"reflect"
|
||||
"slices"
|
||||
|
||||
"git.happydns.org/happyDomain/model"
|
||||
)
|
||||
|
||||
// providers stores all existing Provider in happyDNS.
|
||||
var providersList map[string]happydns.ProviderCreator = map[string]happydns.ProviderCreator{}
|
||||
// providerRegistry stores all existing Provider in happyDNS.
|
||||
var providerRegistry = map[string]happydns.ProviderCreator{}
|
||||
|
||||
// RegisterProvider declares the existence of the given Provider.
|
||||
// RegisterProvider registers a provider definition globally.
|
||||
func RegisterProvider(creator happydns.ProviderCreatorFunc, infos happydns.ProviderInfos) {
|
||||
provider := creator()
|
||||
baseType := reflect.Indirect(reflect.ValueOf(provider)).Type()
|
||||
name := baseType.Name()
|
||||
log.Println("Registering new provider:", name)
|
||||
|
||||
providersList[name] = happydns.ProviderCreator{
|
||||
providerRegistry[name] = happydns.ProviderCreator{
|
||||
Creator: creator,
|
||||
Infos: infos,
|
||||
}
|
||||
}
|
||||
|
||||
// GetProviders retrieves the list of all existing Providers.
|
||||
func GetProviders() *map[string]happydns.ProviderCreator {
|
||||
return &providersList
|
||||
// GetProviders returns all registered provider definitions.
|
||||
func GetProviders() map[string]happydns.ProviderCreator {
|
||||
return providerRegistry
|
||||
}
|
||||
|
||||
// ProviderHasCapability checks if the registered provider type has the given capability.
|
||||
func ProviderHasCapability(provider *happydns.Provider, capability string) bool {
|
||||
creator, ok := providerRegistry[provider.Type]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return slices.Contains(creator.Infos.Capabilities, capability)
|
||||
}
|
||||
|
||||
// FindProvider returns the Provider corresponding to the given name, or an error if it doesn't exist.
|
||||
func FindProvider(name string) (happydns.ProviderBody, error) {
|
||||
src, ok := providersList[name]
|
||||
src, ok := providerRegistry[name]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("Unable to find corresponding provider for `%s`.", name)
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
// This file is part of the happyDomain (R) project.
|
||||
// Copyright (c) 2020-2024 happyDomain
|
||||
// Copyright (c) 2020-2025 happyDomain
|
||||
// Authors: Pierre-Olivier Mercier, et al.
|
||||
//
|
||||
// This program is offered under a commercial and under the AGPL license.
|
||||
|
|
@ -19,7 +19,7 @@
|
|||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package svcs
|
||||
package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
|
@ -240,6 +240,10 @@ func getMostUsedTTL(zone []happydns.Record) uint32 {
|
|||
return max
|
||||
}
|
||||
|
||||
// OrphanCreator is a function that creates an Orphan service wrapping a DNS record.
|
||||
// It is set by the services package during init() to avoid a circular import.
|
||||
var OrphanCreator func(record happydns.Record) happydns.ServiceBody
|
||||
|
||||
// AnalyzeZone converts raw DNS records into higher-level services by running
|
||||
// each registered ServiceAnalyzer in priority order. Records not claimed by
|
||||
// any analyzer are wrapped as Orphan services.
|
||||
|
|
@ -286,21 +290,23 @@ func AnalyzeZone(origin string, records []happydns.Record) (svcs map[happydns.Su
|
|||
}
|
||||
|
||||
// Consider unclaimed records as Orphan
|
||||
for i, record := range a.zone {
|
||||
if a.claimed[i] {
|
||||
continue
|
||||
}
|
||||
// Skip DNSSEC records
|
||||
if helpers.IsDNSSECType(record.Header().Rrtype) {
|
||||
continue
|
||||
}
|
||||
if record.Header().Name == "__dnssec."+origin && record.Header().Rrtype == dns.TypeTXT {
|
||||
continue
|
||||
}
|
||||
if OrphanCreator != nil {
|
||||
for i, record := range a.zone {
|
||||
if a.claimed[i] {
|
||||
continue
|
||||
}
|
||||
// Skip DNSSEC records
|
||||
if helpers.IsDNSSECType(record.Header().Rrtype) {
|
||||
continue
|
||||
}
|
||||
if record.Header().Name == "__dnssec."+origin && record.Header().Rrtype == dns.TypeTXT {
|
||||
continue
|
||||
}
|
||||
|
||||
domain := record.Header().Name
|
||||
domain := record.Header().Name
|
||||
|
||||
a.addService(record, domain, &Orphan{helpers.RRRelativeSubdomain(record, a.GetOrigin(), domain)})
|
||||
a.addService(record, domain, OrphanCreator(helpers.RRRelativeSubdomain(record, a.GetOrigin(), domain)))
|
||||
}
|
||||
}
|
||||
|
||||
svcs = a.services
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
// This file is part of the happyDomain (R) project.
|
||||
// Copyright (c) 2020-2024 happyDomain
|
||||
// Copyright (c) 2020-2025 happyDomain
|
||||
// Authors: Pierre-Olivier Mercier, et al.
|
||||
//
|
||||
// This program is offered under a commercial and under the AGPL license.
|
||||
|
|
@ -19,7 +19,7 @@
|
|||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package svcs
|
||||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
|
@ -61,9 +61,6 @@ func RegisterService(creator happydns.ServiceCreator, analyzer ServiceAnalyzer,
|
|||
|
||||
// Override given parameters by true one
|
||||
infos.Type = name
|
||||
if _, ok := Icons[name]; ok {
|
||||
infos.Icon = "/api/service_specs/" + name + "/icon.png"
|
||||
}
|
||||
|
||||
svc := &Svc{
|
||||
creator,
|
||||
|
|
@ -69,6 +69,11 @@ func (it *KVIterator) Valid() bool {
|
|||
return it.index >= 0 && it.index < len(it.keys)
|
||||
}
|
||||
|
||||
// Err returns nil as in-memory iterators never error.
|
||||
func (it *KVIterator) Err() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Release releases the iterator resources.
|
||||
func (it *KVIterator) Release() {
|
||||
// No resources to release for in-memory iterator
|
||||
|
|
|
|||
|
|
@ -64,6 +64,7 @@ type Iterator interface {
|
|||
Valid() bool
|
||||
Key() string
|
||||
Value() any
|
||||
Err() error
|
||||
}
|
||||
|
||||
type KVStorage interface {
|
||||
|
|
|
|||
|
|
@ -60,6 +60,10 @@ func (s *KVStorage) GetAuthUserByEmail(email string) (*happydns.UserAuth, error)
|
|||
}
|
||||
}
|
||||
|
||||
if err := users.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("Unable to find user with email address '%s'.", email)
|
||||
}
|
||||
|
||||
|
|
@ -77,6 +81,10 @@ func (s *KVStorage) AuthUserExists(email string) (bool, error) {
|
|||
}
|
||||
}
|
||||
|
||||
if err := users.Err(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
|
|
@ -112,5 +120,5 @@ func (s *KVStorage) ClearAuthUsers() error {
|
|||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
return iter.Err()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -79,6 +79,10 @@ func (s *KVStorage) ListDomainLogs(domain *happydns.Domain) (logs []*happydns.Do
|
|||
logs = append(logs, &z)
|
||||
}
|
||||
|
||||
if err = iter.Err(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -51,6 +51,10 @@ func (s *KVStorage) ListDomains(u *happydns.User) (domains []*happydns.Domain, e
|
|||
}
|
||||
}
|
||||
|
||||
if err = iter.Err(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -124,5 +128,5 @@ func (s *KVStorage) ClearDomains() error {
|
|||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
return iter.Err()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -111,7 +111,10 @@ func (it *KVIterator[T]) Key() string {
|
|||
|
||||
// Err returns the first error encountered during iteration, if any.
|
||||
func (it *KVIterator[T]) Err() error {
|
||||
return it.err
|
||||
if it.err != nil {
|
||||
return it.err
|
||||
}
|
||||
return it.iter.Err()
|
||||
}
|
||||
|
||||
// Close releases resources held by the underlying LevelDB iterator.
|
||||
|
|
|
|||
|
|
@ -59,6 +59,10 @@ func (s *KVStorage) ListProviders(u *happydns.User) (srcs happydns.ProviderMessa
|
|||
srcs = append(srcs, srcMsg)
|
||||
}
|
||||
|
||||
if err = iter.Err(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -108,5 +112,5 @@ func (s *KVStorage) ClearProviders() error {
|
|||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
return iter.Err()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -71,6 +71,10 @@ func (s *KVStorage) ListAuthUserSessions(user *happydns.UserAuth) (sessions []*h
|
|||
}
|
||||
}
|
||||
|
||||
if err = iter.Err(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -90,6 +94,10 @@ func (s *KVStorage) ListUserSessions(userid happydns.Identifier) (sessions []*ha
|
|||
}
|
||||
}
|
||||
|
||||
if err = iter.Err(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -112,5 +120,5 @@ func (s *KVStorage) ClearSessions() error {
|
|||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
return iter.Err()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -122,6 +122,10 @@ func migrateFrom0_sourcesProvider(s *KVStorage) (err error) {
|
|||
}
|
||||
}
|
||||
|
||||
if err = iter.Err(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -147,5 +151,9 @@ func migrateFrom0_reparentDomains(s *KVStorage) (err error) {
|
|||
}
|
||||
}
|
||||
|
||||
if err = iter.Err(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -132,6 +132,10 @@ func migrateFrom1_users_tree(s *KVStorage) (err error) {
|
|||
}
|
||||
}
|
||||
|
||||
if err = iter.Err(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -162,6 +166,10 @@ func migrateFrom1_domains(s *KVStorage, oldUserId int64, newUserId string) (err
|
|||
}
|
||||
}
|
||||
|
||||
if err = iter.Err(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -192,6 +200,10 @@ func migrateFrom1_provider(s *KVStorage, oldUserId int64, newUserId string) (err
|
|||
}
|
||||
}
|
||||
|
||||
if err = iter.Err(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -222,5 +234,9 @@ func migrateFrom1_zone(s *KVStorage, oldUserId int64, newUserId string) (err err
|
|||
}
|
||||
}
|
||||
|
||||
if err = iter.Err(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -102,6 +102,10 @@ func migrateFrom2_users_tree(s *KVStorage) (err error) {
|
|||
}
|
||||
}
|
||||
|
||||
if err = iter.Err(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -201,6 +205,10 @@ func migrateFrom2_session(s *KVStorage, oldUserId happydns.HexaString, newUserId
|
|||
}
|
||||
}
|
||||
|
||||
if err = kvIter.Err(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -267,6 +275,10 @@ func migrateFrom2_provider(s *KVStorage, oldUserId happydns.HexaString, newUserI
|
|||
}
|
||||
}
|
||||
|
||||
if err = kvIter.Err(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -354,6 +366,10 @@ func migrateFrom2_domains(s *KVStorage, oldUserId happydns.HexaString, newUserId
|
|||
}
|
||||
}
|
||||
|
||||
if err = kvIter.Err(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -63,5 +63,9 @@ func migrateFrom3_records(s *KVStorage) error {
|
|||
}
|
||||
}
|
||||
|
||||
if err := iter.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1046,6 +1046,10 @@ func migrateFrom7(s *KVStorage) error {
|
|||
}
|
||||
}
|
||||
|
||||
if err := zones.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
zones, err = s.ListAllZones()
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -1082,5 +1086,9 @@ func migrateFrom7(s *KVStorage) error {
|
|||
}
|
||||
}
|
||||
|
||||
if err := zones.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -92,5 +92,9 @@ func migrateFrom8(s *KVStorage) error {
|
|||
}
|
||||
}
|
||||
|
||||
if err := zones.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,5 +44,9 @@ func migrateFrom9(s *KVStorage) (err error) {
|
|||
}
|
||||
}
|
||||
|
||||
if err := sessions.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -60,6 +60,10 @@ func (s *KVStorage) GetUserByEmail(email string) (*happydns.User, error) {
|
|||
}
|
||||
}
|
||||
|
||||
if err := users.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return nil, happydns.ErrUserNotFound
|
||||
}
|
||||
|
||||
|
|
@ -76,6 +80,7 @@ func (s *KVStorage) UserExists(email string) bool {
|
|||
}
|
||||
}
|
||||
|
||||
// Note: iterator errors are swallowed here since the function returns bool only
|
||||
return false
|
||||
}
|
||||
|
||||
|
|
@ -114,5 +119,5 @@ func (s *KVStorage) ClearUsers() error {
|
|||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
return iter.Err()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -92,5 +92,5 @@ func (s *KVStorage) ClearZones() error {
|
|||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
return iter.Err()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,6 +51,10 @@ func (it *LevelDBIterator) Valid() bool {
|
|||
return it.iter.Valid()
|
||||
}
|
||||
|
||||
func (it *LevelDBIterator) Err() error {
|
||||
return it.iter.Error()
|
||||
}
|
||||
|
||||
func (it *LevelDBIterator) Release() {
|
||||
it.iter.Release()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,8 @@ package database
|
|||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/oracle/nosql-go-sdk/nosqldb"
|
||||
"github.com/oracle/nosql-go-sdk/nosqldb/auth/iam"
|
||||
|
|
@ -36,9 +38,11 @@ import (
|
|||
)
|
||||
|
||||
type NoSQLStorage struct {
|
||||
client *nosqldb.Client
|
||||
config *nosqldb.Config
|
||||
table string
|
||||
client *nosqldb.Client
|
||||
config *nosqldb.Config
|
||||
table string
|
||||
prepareSearchOnce sync.Once
|
||||
searchStmt nosqldb.PreparedStatement
|
||||
}
|
||||
|
||||
// NewOCINoSQLStorage establishes the connection to the database
|
||||
|
|
@ -59,6 +63,7 @@ func NewOCINoSQLStorage(cfg *OCINoSQLConfig) (s *NoSQLStorage, err error) {
|
|||
Mode: "cloud",
|
||||
Region: common.Region(cfg.Region),
|
||||
AuthorizationProvider: authProvider,
|
||||
RateLimitingEnabled: true,
|
||||
}
|
||||
|
||||
// Create client
|
||||
|
|
@ -187,10 +192,50 @@ func (n *NoSQLStorage) Delete(key string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (n *NoSQLStorage) Search(prefix string) storage.Iterator {
|
||||
query := fmt.Sprintf("SELECT * FROM %s WHERE regex_like(key, '%s.*')", n.table, prefix)
|
||||
|
||||
return NewIteratorFromRequest(n, &nosqldb.QueryRequest{
|
||||
Statement: query,
|
||||
})
|
||||
// escapeRegexLiteral escapes regex-special characters with a single backslash,
|
||||
// suitable for bind variables where the value is passed directly to the regex engine.
|
||||
func escapeRegexLiteral(s string) string {
|
||||
for _, ch := range []string{`\`, `|`, `.`, `^`, `$`, `*`, `+`, `?`, `(`, `)`, `[`, `]`, `{`, `}`} {
|
||||
s = strings.ReplaceAll(s, ch, `\`+ch)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// escapeRegexForSQL escapes regex-special characters with double backslashes,
|
||||
// suitable for embedding in SQL string literals (the SQL parser consumes one level).
|
||||
func escapeRegexForSQL(s string) string {
|
||||
for _, ch := range []string{`\`, `|`, `.`, `^`, `$`, `*`, `+`, `?`, `(`, `)`, `[`, `]`, `{`, `}`} {
|
||||
s = strings.ReplaceAll(s, ch, `\\`+ch)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (n *NoSQLStorage) prepareSearch() error {
|
||||
var err error
|
||||
n.prepareSearchOnce.Do(func() {
|
||||
stmt := fmt.Sprintf(
|
||||
"DECLARE $pattern STRING; SELECT * FROM %s WHERE regex_like(key, $pattern)",
|
||||
n.table,
|
||||
)
|
||||
res, e := n.client.Prepare(&nosqldb.PrepareRequest{Statement: stmt})
|
||||
if e != nil {
|
||||
err = e
|
||||
return
|
||||
}
|
||||
n.searchStmt = res.PreparedStatement
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (n *NoSQLStorage) Search(prefix string) storage.Iterator {
|
||||
if err := n.prepareSearch(); err != nil {
|
||||
// Fall back to unprepared query (SQL string literal needs double-escaped regex)
|
||||
query := fmt.Sprintf("SELECT * FROM %s WHERE regex_like(key, '%s.*')", n.table, strings.ReplaceAll(escapeRegexForSQL(prefix), "'", "''"))
|
||||
return NewIteratorFromRequest(n, &nosqldb.QueryRequest{Statement: query})
|
||||
}
|
||||
|
||||
// Struct copy — each Search gets its own bindVariables map
|
||||
stmt := n.searchStmt
|
||||
stmt.SetVariable("$pattern", escapeRegexLiteral(prefix)+".*")
|
||||
return NewIteratorFromRequest(n, &nosqldb.QueryRequest{PreparedStatement: &stmt})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,19 +23,20 @@ package database
|
|||
|
||||
import (
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/oracle/nosql-go-sdk/nosqldb"
|
||||
"github.com/oracle/nosql-go-sdk/nosqldb/nosqlerr"
|
||||
"github.com/oracle/nosql-go-sdk/nosqldb/types"
|
||||
)
|
||||
|
||||
type Iterator struct {
|
||||
firstPassed bool
|
||||
n *NoSQLStorage
|
||||
req *nosqldb.QueryRequest
|
||||
res *nosqldb.QueryResult
|
||||
results []*types.MapValue
|
||||
cur_result int
|
||||
err error
|
||||
n *NoSQLStorage
|
||||
req *nosqldb.QueryRequest
|
||||
results []*types.MapValue
|
||||
cur_result int
|
||||
started bool
|
||||
err error
|
||||
}
|
||||
|
||||
func NewIteratorFromRequest(n *NoSQLStorage, req *nosqldb.QueryRequest) *Iterator {
|
||||
|
|
@ -45,41 +46,57 @@ func NewIteratorFromRequest(n *NoSQLStorage, req *nosqldb.QueryRequest) *Iterato
|
|||
}
|
||||
}
|
||||
|
||||
func (i *Iterator) Release() {}
|
||||
func (i *Iterator) Release() {
|
||||
i.req.Close()
|
||||
}
|
||||
|
||||
func (i *Iterator) Next() bool {
|
||||
i.err = nil
|
||||
|
||||
if i.res == nil {
|
||||
if i.firstPassed && i.req.IsDone() {
|
||||
// Advance within current batch.
|
||||
if i.results != nil {
|
||||
i.cur_result++
|
||||
if i.cur_result < len(i.results) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch new batches until we get results or the query is done.
|
||||
// The SDK may return empty batches (e.g. during auto-preparation
|
||||
// or when the read limit is hit), so we must loop.
|
||||
// Note: IsDone() checks continuationKey == nil, which is also true
|
||||
// for a fresh QueryRequest that has never been executed. We skip the
|
||||
// check only before the first Query() call.
|
||||
for {
|
||||
if i.started && i.req.IsDone() {
|
||||
return false
|
||||
}
|
||||
i.firstPassed = true
|
||||
|
||||
i.res, i.err = i.n.client.Query(i.req)
|
||||
res, err := i.n.client.Query(i.req)
|
||||
if err != nil {
|
||||
// Retry with backoff on rate-limit errors
|
||||
if nosqlerr.Is(err, nosqlerr.ReadLimitExceeded, nosqlerr.WriteLimitExceeded, nosqlerr.RequestTimeout) {
|
||||
log.Println("rate limited in iterator, backing off:", err.Error())
|
||||
time.Sleep(2 * time.Second)
|
||||
continue
|
||||
}
|
||||
i.err = err
|
||||
log.Println("error in iterator:", err.Error())
|
||||
return false
|
||||
}
|
||||
i.started = true
|
||||
|
||||
i.results, i.err = res.GetResults()
|
||||
if i.err != nil {
|
||||
log.Println("error in iterator:", i.err.Error())
|
||||
return false
|
||||
}
|
||||
i.results = nil
|
||||
}
|
||||
|
||||
if i.results == nil {
|
||||
i.results, i.err = i.res.GetResults()
|
||||
if i.err != nil {
|
||||
log.Println("error in iterator:", i.err.Error())
|
||||
return false
|
||||
if len(i.results) > 0 {
|
||||
i.cur_result = 0
|
||||
return true
|
||||
}
|
||||
i.cur_result = 0
|
||||
} else {
|
||||
i.cur_result += 1
|
||||
}
|
||||
|
||||
if i.cur_result+1 >= len(i.results) {
|
||||
i.res = nil
|
||||
}
|
||||
|
||||
return i.cur_result < len(i.results)
|
||||
}
|
||||
|
||||
func (i *Iterator) Valid() bool {
|
||||
|
|
@ -98,3 +115,7 @@ func (i *Iterator) Value() any {
|
|||
value, _ := i.results[i.cur_result].Get("value")
|
||||
return value
|
||||
}
|
||||
|
||||
func (i *Iterator) Err() error {
|
||||
return i.err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -89,3 +89,8 @@ func (it *PostgreSQLIterator) Key() string {
|
|||
func (it *PostgreSQLIterator) Value() any {
|
||||
return it.value
|
||||
}
|
||||
|
||||
// Err returns the last error encountered during iteration
|
||||
func (it *PostgreSQLIterator) Err() error {
|
||||
return it.err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@
|
|||
package domain
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
|
|
@ -32,12 +33,12 @@ import (
|
|||
|
||||
// ProviderGetter is an interface for getting providers.
|
||||
type ProviderGetter interface {
|
||||
GetUserProvider(user *happydns.User, providerID happydns.Identifier) (*happydns.Provider, error)
|
||||
GetUserProvider(ctx context.Context, user *happydns.User, providerID happydns.Identifier) (*happydns.Provider, error)
|
||||
}
|
||||
|
||||
// DomainExistenceTester is an interface for testing domain existence.
|
||||
type DomainExistenceTester interface {
|
||||
TestDomainExistence(provider *happydns.Provider, name string) error
|
||||
TestDomainExistence(ctx context.Context, provider *happydns.Provider, name string) error
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
|
|
@ -65,18 +66,18 @@ func NewService(
|
|||
}
|
||||
|
||||
// CreateDomain creates a new domain for the given user.
|
||||
func (s *Service) CreateDomain(user *happydns.User, uz *happydns.Domain) error {
|
||||
func (s *Service) CreateDomain(ctx context.Context, user *happydns.User, uz *happydns.Domain) error {
|
||||
uz, err := happydns.NewDomain(user, uz.DomainName, uz.ProviderId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
provider, err := s.providerService.GetUserProvider(user, uz.ProviderId)
|
||||
provider, err := s.providerService.GetUserProvider(ctx, user, uz.ProviderId)
|
||||
if err != nil {
|
||||
return happydns.ValidationError{Msg: fmt.Sprintf("unable to find the provider.")}
|
||||
}
|
||||
|
||||
if err = s.domainExistence.TestDomainExistence(provider, uz.DomainName); err != nil {
|
||||
if err = s.domainExistence.TestDomainExistence(ctx, provider, uz.DomainName); err != nil {
|
||||
return happydns.NotFoundError{Msg: err.Error()}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -22,9 +22,11 @@
|
|||
package domain_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
providerReg "git.happydns.org/happyDomain/internal/provider"
|
||||
"git.happydns.org/happyDomain/internal/storage"
|
||||
"git.happydns.org/happyDomain/internal/storage/inmemory"
|
||||
kv "git.happydns.org/happyDomain/internal/storage/kvtpl"
|
||||
|
|
@ -32,14 +34,15 @@ import (
|
|||
providerUC "git.happydns.org/happyDomain/internal/usecase/provider"
|
||||
zoneUC "git.happydns.org/happyDomain/internal/usecase/zone"
|
||||
"git.happydns.org/happyDomain/model"
|
||||
"git.happydns.org/happyDomain/providers"
|
||||
)
|
||||
|
||||
var ctx = context.Background()
|
||||
|
||||
// Mock implementations for testing
|
||||
|
||||
func init() {
|
||||
// Register the mock provider
|
||||
providers.RegisterProvider(func() happydns.ProviderBody {
|
||||
providerReg.RegisterProvider(func() happydns.ProviderBody {
|
||||
return &mockProviderBody{}
|
||||
}, happydns.ProviderInfos{
|
||||
Name: "Mock Provider",
|
||||
|
|
@ -159,7 +162,7 @@ func Test_CreateDomain(t *testing.T) {
|
|||
ProviderId: providerId,
|
||||
}
|
||||
|
||||
err := service.CreateDomain(user, domainToCreate)
|
||||
err := service.CreateDomain(ctx, user, domainToCreate)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
|
@ -205,7 +208,7 @@ func Test_CreateDomain_InvalidProvider(t *testing.T) {
|
|||
ProviderId: invalidProviderId,
|
||||
}
|
||||
|
||||
err := service.CreateDomain(user, domainToCreate)
|
||||
err := service.CreateDomain(ctx, user, domainToCreate)
|
||||
if err == nil {
|
||||
t.Error("expected error when creating domain with invalid provider")
|
||||
}
|
||||
|
|
@ -224,7 +227,7 @@ func Test_GetUserDomain(t *testing.T) {
|
|||
DomainName: "example.com",
|
||||
ProviderId: providerId,
|
||||
}
|
||||
err := service.CreateDomain(user, domainToCreate)
|
||||
err := service.CreateDomain(ctx, user, domainToCreate)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create domain: %v", err)
|
||||
}
|
||||
|
|
@ -265,7 +268,7 @@ func Test_GetUserDomain_WrongUser(t *testing.T) {
|
|||
DomainName: "user1-domain.com",
|
||||
ProviderId: providerId,
|
||||
}
|
||||
err := service.CreateDomain(user1, domainToCreate)
|
||||
err := service.CreateDomain(ctx, user1, domainToCreate)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create domain: %v", err)
|
||||
}
|
||||
|
|
@ -317,7 +320,7 @@ func Test_GetUserDomainByFQDN(t *testing.T) {
|
|||
DomainName: "example.com.",
|
||||
ProviderId: providerId,
|
||||
}
|
||||
err := service.CreateDomain(user, domainToCreate)
|
||||
err := service.CreateDomain(ctx, user, domainToCreate)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create domain: %v", err)
|
||||
}
|
||||
|
|
@ -352,7 +355,7 @@ func Test_ListUserDomains(t *testing.T) {
|
|||
DomainName: name,
|
||||
ProviderId: providerId,
|
||||
}
|
||||
err := service.CreateDomain(user, domainToCreate)
|
||||
err := service.CreateDomain(ctx, user, domainToCreate)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create domain %s: %v", name, err)
|
||||
}
|
||||
|
|
@ -385,7 +388,7 @@ func Test_ListUserDomains_MultipleUsers(t *testing.T) {
|
|||
DomainName: fmt.Sprintf("user1-domain%d.com", i),
|
||||
ProviderId: providerId1,
|
||||
}
|
||||
err := service.CreateDomain(user1, domainToCreate)
|
||||
err := service.CreateDomain(ctx, user1, domainToCreate)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create domain: %v", err)
|
||||
}
|
||||
|
|
@ -396,7 +399,7 @@ func Test_ListUserDomains_MultipleUsers(t *testing.T) {
|
|||
DomainName: "user2-domain.com",
|
||||
ProviderId: providerId2,
|
||||
}
|
||||
err := service.CreateDomain(user2, domainToCreate)
|
||||
err := service.CreateDomain(ctx, user2, domainToCreate)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create domain: %v", err)
|
||||
}
|
||||
|
|
@ -451,7 +454,7 @@ func Test_UpdateDomain(t *testing.T) {
|
|||
DomainName: "example.com",
|
||||
ProviderId: providerId,
|
||||
}
|
||||
err := service.CreateDomain(user, domainToCreate)
|
||||
err := service.CreateDomain(ctx, user, domainToCreate)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create domain: %v", err)
|
||||
}
|
||||
|
|
@ -503,7 +506,7 @@ func Test_UpdateDomain_PreventIdChange(t *testing.T) {
|
|||
DomainName: "example.com",
|
||||
ProviderId: providerId,
|
||||
}
|
||||
err := service.CreateDomain(user, domainToCreate)
|
||||
err := service.CreateDomain(ctx, user, domainToCreate)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create domain: %v", err)
|
||||
}
|
||||
|
|
@ -546,7 +549,7 @@ func Test_UpdateDomain_WrongUser(t *testing.T) {
|
|||
DomainName: "user1-domain.com",
|
||||
ProviderId: providerId,
|
||||
}
|
||||
err := service.CreateDomain(user1, domainToCreate)
|
||||
err := service.CreateDomain(ctx, user1, domainToCreate)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create domain: %v", err)
|
||||
}
|
||||
|
|
@ -580,7 +583,7 @@ func Test_DeleteDomain(t *testing.T) {
|
|||
DomainName: "example.com",
|
||||
ProviderId: providerId,
|
||||
}
|
||||
err := service.CreateDomain(user, domainToCreate)
|
||||
err := service.CreateDomain(ctx, user, domainToCreate)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create domain: %v", err)
|
||||
}
|
||||
|
|
@ -621,7 +624,7 @@ func Test_UpdateDomain_Alias(t *testing.T) {
|
|||
DomainName: "example.com",
|
||||
ProviderId: providerId,
|
||||
}
|
||||
err := service.CreateDomain(user, domainToCreate)
|
||||
err := service.CreateDomain(ctx, user, domainToCreate)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create domain: %v", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -89,6 +89,9 @@ func Collect(
|
|||
for authusers.Next() {
|
||||
data.Database.NbAuthUsers++
|
||||
}
|
||||
if err := authusers.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
users, err := store.ListAllUsers()
|
||||
|
|
@ -111,8 +114,8 @@ func Collect(
|
|||
if user.Settings.Newsletter {
|
||||
data.UserSettings.Newsletter++
|
||||
}
|
||||
data.UserSettings.FieldHints[user.Settings.FieldHint]++
|
||||
data.UserSettings.ZoneView[user.Settings.ZoneView]++
|
||||
data.UserSettings.FieldHints[int(user.Settings.FieldHint)]++
|
||||
data.UserSettings.ZoneView[int(user.Settings.ZoneView)]++
|
||||
|
||||
if providers, err := store.ListProviders(user); err == nil {
|
||||
for _, provider := range providers {
|
||||
|
|
@ -129,5 +132,9 @@ func Collect(
|
|||
}
|
||||
}
|
||||
|
||||
if err := users.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &data, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ type DomainUpdater interface {
|
|||
|
||||
// ProviderGetter is an interface for getting providers.
|
||||
type ProviderGetter interface {
|
||||
GetUserProvider(user *happydns.User, providerID happydns.Identifier) (*happydns.Provider, error)
|
||||
GetUserProvider(ctx context.Context, user *happydns.User, providerID happydns.Identifier) (*happydns.Provider, error)
|
||||
}
|
||||
|
||||
// ZoneRetriever is an interface for retrieving zones from providers.
|
||||
|
|
@ -87,7 +87,7 @@ func NewOrchestrator(
|
|||
zoneCorrectionLister := NewZoneCorrectionListerUsecase(providerService, listRecords, zoneCorrectorService, zoneRetrieverService)
|
||||
return &Orchestrator{
|
||||
RemoteZoneImporter: NewRemoteZoneImporterUsecase(appendDomainLog, providerService, zoneImporter, zoneRetrieverService),
|
||||
ZoneCorrectionApplier: NewZoneCorrectionApplierUsecase(appendDomainLog, domainUpdater, zoneCorrectionLister, zoneCreator, zoneGetter, zoneUpdater),
|
||||
ZoneCorrectionApplier: NewZoneCorrectionApplierUsecase(appendDomainLog, domainUpdater, zoneCorrectionLister, zoneCreator, zoneGetter, zoneRetrieverService, zoneUpdater),
|
||||
ZoneImporter: zoneImporter,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
130
internal/usecase/orchestrator/propagation.go
Normal file
130
internal/usecase/orchestrator/propagation.go
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
// This file is part of the happyDomain (R) project.
|
||||
// Copyright (c) 2020-2025 happyDomain
|
||||
// Authors: Pierre-Olivier Mercier, et al.
|
||||
//
|
||||
// This program is offered under a commercial and under the AGPL license.
|
||||
// For commercial licensing, contact us at <contact@happydomain.org>.
|
||||
//
|
||||
// For AGPL licensing:
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package orchestrator
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
|
||||
svc "git.happydns.org/happyDomain/internal/service"
|
||||
zoneUC "git.happydns.org/happyDomain/internal/usecase/zone"
|
||||
"git.happydns.org/happyDomain/model"
|
||||
)
|
||||
|
||||
// SetPropagationTimes stamps each service in newServices with a PropagatedAt
|
||||
// time based on whether the service changed compared to the provider state.
|
||||
// It reuses the same matching technique as ReassociateMetadata (subdomain +
|
||||
// type + ServiceRDataHash).
|
||||
//
|
||||
// For changed/updated services: PropagatedAt = publishTime + old service TTL.
|
||||
// For new services (additions): PropagatedAt = publishTime + SOA minimum TTL
|
||||
// (negative cache duration), falling back to defaultTTL.
|
||||
func SetPropagationTimes(
|
||||
newServices map[happydns.Subdomain][]*happydns.Service,
|
||||
providerRecords []happydns.Record,
|
||||
origin string,
|
||||
defaultTTL uint32,
|
||||
publishTime time.Time,
|
||||
) {
|
||||
// Find SOA minimum TTL for negative cache duration (used for additions).
|
||||
negativeCacheTTL := defaultTTL
|
||||
for _, rr := range providerRecords {
|
||||
if rr.Header().Rrtype == dns.TypeSOA {
|
||||
if soa, ok := rr.(*dns.SOA); ok {
|
||||
negativeCacheTTL = soa.Minttl
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Analyze provider records into old services for comparison.
|
||||
oldServices, oldDefaultTTL, err := svc.AnalyzeZone(origin, providerRecords)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for dn, newSvcs := range newServices {
|
||||
oldSvcs := oldServices[dn]
|
||||
|
||||
// Group old services by type.
|
||||
oldByType := map[string][]*happydns.Service{}
|
||||
for _, s := range oldSvcs {
|
||||
oldByType[s.Type] = append(oldByType[s.Type], s)
|
||||
}
|
||||
|
||||
for _, newSvc := range newSvcs {
|
||||
candidates := oldByType[newSvc.Type]
|
||||
|
||||
if len(candidates) == 0 {
|
||||
// New service (addition): use SOA negative cache TTL.
|
||||
propagatedAt := publishTime.Add(time.Duration(negativeCacheTTL) * time.Second)
|
||||
newSvc.PropagatedAt = &propagatedAt
|
||||
continue
|
||||
}
|
||||
|
||||
newHash := zoneUC.ServiceRDataHash(newSvc, origin, defaultTTL)
|
||||
|
||||
if len(candidates) == 1 {
|
||||
oldSvc := candidates[0]
|
||||
oldHash := zoneUC.ServiceRDataHash(oldSvc, origin, oldDefaultTTL)
|
||||
if newHash != oldHash {
|
||||
// Service changed: use old service TTL.
|
||||
oldTTL := oldDefaultTTL
|
||||
if oldSvc.Ttl != 0 {
|
||||
oldTTL = oldSvc.Ttl
|
||||
}
|
||||
propagatedAt := publishTime.Add(time.Duration(oldTTL) * time.Second)
|
||||
newSvc.PropagatedAt = &propagatedAt
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Multiple candidates: try to find exact RDATA match.
|
||||
matched := false
|
||||
for _, oldSvc := range candidates {
|
||||
if zoneUC.ServiceRDataHash(oldSvc, origin, oldDefaultTTL) == newHash {
|
||||
// Exact match: service unchanged, don't touch PropagatedAt.
|
||||
matched = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !matched {
|
||||
// No exact match: service was modified. Use the max TTL
|
||||
// across all candidates of the same type as a conservative
|
||||
// upper bound.
|
||||
var maxOldTTL uint32
|
||||
for _, oldSvc := range candidates {
|
||||
ttl := oldDefaultTTL
|
||||
if oldSvc.Ttl != 0 {
|
||||
ttl = oldSvc.Ttl
|
||||
}
|
||||
if ttl > maxOldTTL {
|
||||
maxOldTTL = ttl
|
||||
}
|
||||
}
|
||||
propagatedAt := publishTime.Add(time.Duration(maxOldTTL) * time.Second)
|
||||
newSvc.PropagatedAt = &propagatedAt
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -60,7 +60,7 @@ func NewRemoteZoneImporterUsecase(
|
|||
// and imports them via ZoneImporterUsecase. A domain log entry is appended on
|
||||
// success. Returns the newly created zone or an error.
|
||||
func (uc *RemoteZoneImporterUsecase) Import(ctx context.Context, user *happydns.User, domain *happydns.Domain) (*happydns.Zone, error) {
|
||||
provider, err := uc.providerService.GetUserProvider(user, domain.ProviderId)
|
||||
provider, err := uc.providerService.GetUserProvider(ctx, user, domain.ProviderId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,10 +28,12 @@ import (
|
|||
"time"
|
||||
|
||||
adapter "git.happydns.org/happyDomain/internal/adapters"
|
||||
providerReg "git.happydns.org/happyDomain/internal/provider"
|
||||
svc "git.happydns.org/happyDomain/internal/service"
|
||||
domainlogUC "git.happydns.org/happyDomain/internal/usecase/domain_log"
|
||||
zoneUC "git.happydns.org/happyDomain/internal/usecase/zone"
|
||||
"git.happydns.org/happyDomain/model"
|
||||
svcs "git.happydns.org/happyDomain/services"
|
||||
"git.happydns.org/happyDomain/services/abstract"
|
||||
)
|
||||
|
||||
// ZoneCorrectionApplierUsecase applies a user-selected subset of zone
|
||||
|
|
@ -43,6 +45,7 @@ type ZoneCorrectionApplierUsecase struct {
|
|||
domainUpdater DomainUpdater
|
||||
zoneCreator *zoneUC.CreateZoneUsecase
|
||||
zoneGetter *zoneUC.GetZoneUsecase
|
||||
zoneRetriever ZoneRetriever
|
||||
zoneUpdater *zoneUC.UpdateZoneUsecase
|
||||
clock func() time.Time
|
||||
}
|
||||
|
|
@ -56,6 +59,7 @@ func NewZoneCorrectionApplierUsecase(
|
|||
lister *ZoneCorrectionListerUsecase,
|
||||
zoneCreator *zoneUC.CreateZoneUsecase,
|
||||
zoneGetter *zoneUC.GetZoneUsecase,
|
||||
zoneRetriever ZoneRetriever,
|
||||
zoneUpdater *zoneUC.UpdateZoneUsecase,
|
||||
) *ZoneCorrectionApplierUsecase {
|
||||
return &ZoneCorrectionApplierUsecase{
|
||||
|
|
@ -64,6 +68,7 @@ func NewZoneCorrectionApplierUsecase(
|
|||
domainUpdater: domainUpdater,
|
||||
zoneCreator: zoneCreator,
|
||||
zoneGetter: zoneGetter,
|
||||
zoneRetriever: zoneRetriever,
|
||||
zoneUpdater: zoneUpdater,
|
||||
clock: time.Now,
|
||||
}
|
||||
|
|
@ -78,28 +83,28 @@ func (uc *ZoneCorrectionApplierUsecase) computeExecutableCorrections(
|
|||
domain *happydns.Domain,
|
||||
zone *happydns.Zone,
|
||||
wantedCorrections []happydns.Identifier,
|
||||
) (execCorrections []*happydns.Correction, targetRecords []happydns.Record, nbDiffs int, err error) {
|
||||
) (execCorrections []*happydns.Correction, targetRecords []happydns.Record, providerRecords []happydns.Record, nbDiffs int, err error) {
|
||||
// Step 1: Compute the diff and get provider/WIP records.
|
||||
corrections, providerRecords, _, nbDiffs, err := uc.listWithRecords(ctx, user, domain, zone)
|
||||
if err != nil {
|
||||
return nil, nil, nbDiffs, err
|
||||
return nil, nil, nil, nbDiffs, err
|
||||
}
|
||||
|
||||
// Step 2: Build target records from selected corrections.
|
||||
targetRecords = adapter.BuildTargetRecords(providerRecords, corrections, wantedCorrections)
|
||||
|
||||
// Step 3: Get executable corrections from the provider for the target state.
|
||||
provider, err := uc.providerService.GetUserProvider(user, domain.ProviderId)
|
||||
provider, err := uc.providerService.GetUserProvider(ctx, user, domain.ProviderId)
|
||||
if err != nil {
|
||||
return nil, nil, nbDiffs, err
|
||||
return nil, nil, nil, nbDiffs, err
|
||||
}
|
||||
|
||||
execCorrections, nbDiffs, err = uc.zoneCorrector.ListZoneCorrections(ctx, provider, domain, targetRecords)
|
||||
if err != nil {
|
||||
return nil, nil, nbDiffs, fmt.Errorf("unable to compute executable corrections: %w", err)
|
||||
return nil, nil, nil, nbDiffs, fmt.Errorf("unable to compute executable corrections: %w", err)
|
||||
}
|
||||
|
||||
return execCorrections, targetRecords, nbDiffs, nil
|
||||
return execCorrections, targetRecords, providerRecords, nbDiffs, nil
|
||||
}
|
||||
|
||||
// Prepare computes the executable corrections for the given selection without
|
||||
|
|
@ -112,7 +117,7 @@ func (uc *ZoneCorrectionApplierUsecase) Prepare(
|
|||
zone *happydns.Zone,
|
||||
form *happydns.PrepareZoneForm,
|
||||
) (*happydns.PrepareZoneResponse, error) {
|
||||
execCorrections, _, nbDiffs, err := uc.computeExecutableCorrections(ctx, user, domain, zone, form.WantedCorrections)
|
||||
execCorrections, _, _, nbDiffs, err := uc.computeExecutableCorrections(ctx, user, domain, zone, form.WantedCorrections)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -142,7 +147,7 @@ func (uc *ZoneCorrectionApplierUsecase) Apply(
|
|||
zone *happydns.Zone,
|
||||
form *happydns.ApplyZoneForm,
|
||||
) (*happydns.Zone, error) {
|
||||
executableCorrections, targetRecords, _, err := uc.computeExecutableCorrections(ctx, user, domain, zone, form.WantedCorrections)
|
||||
executableCorrections, targetRecords, providerRecords, _, err := uc.computeExecutableCorrections(ctx, user, domain, zone, form.WantedCorrections)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -171,8 +176,22 @@ func (uc *ZoneCorrectionApplierUsecase) Apply(
|
|||
log.Printf("unable to append domain log for %s: %s", domain.DomainName, logErr.Error())
|
||||
}
|
||||
|
||||
// Step 5: Create a published snapshot zone from target records.
|
||||
services, defaultTTL, err := svcs.AnalyzeZone(domain.DomainName, targetRecords)
|
||||
// Step 4b: If provider manages SOA serial, re-fetch to get the actual published state.
|
||||
publishedRecords := targetRecords
|
||||
refetched := false
|
||||
provider, provErr := uc.providerService.GetUserProvider(ctx, user, domain.ProviderId)
|
||||
if provErr == nil && providerReg.ProviderHasCapability(provider, "manages-soa-serial") {
|
||||
fetched, fetchErr := uc.zoneRetriever.RetrieveZone(ctx, provider, domain.DomainName)
|
||||
if fetchErr != nil {
|
||||
log.Printf("%s: unable to re-fetch zone after deploy, using target records: %s", domain.DomainName, fetchErr)
|
||||
} else {
|
||||
publishedRecords = fetched
|
||||
refetched = true
|
||||
}
|
||||
}
|
||||
|
||||
// Step 5: Create a published snapshot zone from published records.
|
||||
services, defaultTTL, err := svc.AnalyzeZone(domain.DomainName, publishedRecords)
|
||||
if err != nil {
|
||||
return nil, happydns.InternalError{
|
||||
Err: fmt.Errorf("unable to analyze target zone: %w", err),
|
||||
|
|
@ -196,6 +215,10 @@ func (uc *ZoneCorrectionApplierUsecase) Apply(
|
|||
}
|
||||
|
||||
now := uc.clock()
|
||||
|
||||
// Compute propagation times for changed services on the snapshot.
|
||||
SetPropagationTimes(services, providerRecords, domain.DomainName, defaultTTL, now)
|
||||
|
||||
snapshot := &happydns.Zone{
|
||||
ZoneMeta: happydns.ZoneMeta{
|
||||
IdAuthor: user.Id,
|
||||
|
|
@ -204,7 +227,7 @@ func (uc *ZoneCorrectionApplierUsecase) Apply(
|
|||
CommitMsg: &form.CommitMsg,
|
||||
CommitDate: &now,
|
||||
Published: &now,
|
||||
ParentZone: &zone.ZoneMeta.Id,
|
||||
ParentZone: zone.ParentZone,
|
||||
},
|
||||
Services: services,
|
||||
}
|
||||
|
|
@ -217,6 +240,28 @@ func (uc *ZoneCorrectionApplierUsecase) Apply(
|
|||
}
|
||||
}
|
||||
|
||||
// Update the parent zone of the WIP zone
|
||||
zone.ParentZone = &snapshot.Id
|
||||
|
||||
// Step 5b: If we re-fetched, update the WIP zone's Origin SOA serial to match.
|
||||
if refetched {
|
||||
if newSerial, ok := extractOriginSOASerial(snapshot); ok {
|
||||
if updateErr := uc.zoneUpdater.Update(zone.Id, func(z *happydns.Zone) {
|
||||
if services, exists := z.Services[""]; exists {
|
||||
for _, s := range services {
|
||||
if s.Type == "abstract.Origin" {
|
||||
if origin, ok := s.Service.(*abstract.Origin); ok && origin.SOA != nil {
|
||||
origin.SOA.Serial = newSerial
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}); updateErr != nil {
|
||||
log.Printf("%s: unable to update WIP zone SOA serial: %s", domain.DomainName, updateErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Step 6: Insert snapshot at ZoneHistory[1] (after WIP at position 0).
|
||||
err = uc.domainUpdater.Update(domain.Id, user, func(domain *happydns.Domain) {
|
||||
if len(domain.ZoneHistory) == 0 {
|
||||
|
|
@ -236,5 +281,27 @@ func (uc *ZoneCorrectionApplierUsecase) Apply(
|
|||
}
|
||||
}
|
||||
|
||||
// Update propagation times on the WIP zone as well.
|
||||
if updateErr := uc.zoneUpdater.Update(zone.Id, func(wipZone *happydns.Zone) {
|
||||
SetPropagationTimes(wipZone.Services, providerRecords, domain.DomainName, wipZone.DefaultTTL, now)
|
||||
}); updateErr != nil {
|
||||
log.Printf("%s: unable to update WIP zone propagation times: %s", domain.DomainName, updateErr)
|
||||
}
|
||||
|
||||
return snapshot, nil
|
||||
}
|
||||
|
||||
// extractOriginSOASerial extracts the SOA serial from the Origin service
|
||||
// at the zone apex, if present.
|
||||
func extractOriginSOASerial(zone *happydns.Zone) (uint32, bool) {
|
||||
if services, exists := zone.Services[""]; exists {
|
||||
for _, s := range services {
|
||||
if s.Type == "abstract.Origin" {
|
||||
if origin, ok := s.Service.(*abstract.Origin); ok && origin.SOA != nil {
|
||||
return origin.SOA.Serial, true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
|
|
|||
427
internal/usecase/orchestrator/zone_correction_applier_test.go
Normal file
427
internal/usecase/orchestrator/zone_correction_applier_test.go
Normal file
|
|
@ -0,0 +1,427 @@
|
|||
// This file is part of the happyDomain (R) project.
|
||||
// Copyright (c) 2020-2026 happyDomain
|
||||
// Authors: Pierre-Olivier Mercier, et al.
|
||||
//
|
||||
// This program is offered under a commercial and under the AGPL license.
|
||||
// For commercial licensing, contact us at <contact@happydomain.org>.
|
||||
//
|
||||
// For AGPL licensing:
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package orchestrator_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
|
||||
providerReg "git.happydns.org/happyDomain/internal/provider"
|
||||
domainlogUC "git.happydns.org/happyDomain/internal/usecase/domain_log"
|
||||
"git.happydns.org/happyDomain/internal/usecase/orchestrator"
|
||||
serviceUC "git.happydns.org/happyDomain/internal/usecase/service"
|
||||
zoneUC "git.happydns.org/happyDomain/internal/usecase/zone"
|
||||
"git.happydns.org/happyDomain/model"
|
||||
"git.happydns.org/happyDomain/services/abstract"
|
||||
|
||||
// Import AXFRDDNS provider to register its capabilities.
|
||||
_ "git.happydns.org/happyDomain/providers"
|
||||
)
|
||||
|
||||
// mockDomainUpdater implements DomainUpdater for testing.
|
||||
type mockDomainUpdater struct {
|
||||
domain *happydns.Domain
|
||||
err error
|
||||
}
|
||||
|
||||
func (m *mockDomainUpdater) Update(_ happydns.Identifier, _ *happydns.User, updateFn func(*happydns.Domain)) error {
|
||||
if m.err != nil {
|
||||
return m.err
|
||||
}
|
||||
if m.domain != nil {
|
||||
updateFn(m.domain)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// inMemoryZoneStorage implements ZoneStorage for testing.
|
||||
type inMemoryZoneStorage struct {
|
||||
zones map[string]*happydns.Zone
|
||||
}
|
||||
|
||||
func newInMemoryZoneStorage() *inMemoryZoneStorage {
|
||||
return &inMemoryZoneStorage{zones: map[string]*happydns.Zone{}}
|
||||
}
|
||||
|
||||
func (s *inMemoryZoneStorage) ListAllZones() (happydns.Iterator[happydns.ZoneMessage], error) {
|
||||
return nil, fmt.Errorf("not implemented")
|
||||
}
|
||||
|
||||
func (s *inMemoryZoneStorage) GetZoneMeta(zoneid happydns.Identifier) (*happydns.ZoneMeta, error) {
|
||||
z, ok := s.zones[zoneid.String()]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("zone not found: %s", zoneid)
|
||||
}
|
||||
return &z.ZoneMeta, nil
|
||||
}
|
||||
|
||||
func (s *inMemoryZoneStorage) GetZone(zoneid happydns.Identifier) (*happydns.ZoneMessage, error) {
|
||||
z, ok := s.zones[zoneid.String()]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("zone not found: %s", zoneid)
|
||||
}
|
||||
|
||||
// Convert Zone to ZoneMessage by marshaling services.
|
||||
msg := &happydns.ZoneMessage{
|
||||
ZoneMeta: z.ZoneMeta,
|
||||
Services: map[happydns.Subdomain][]*happydns.ServiceMessage{},
|
||||
}
|
||||
|
||||
for subdn, svcs := range z.Services {
|
||||
for _, svc := range svcs {
|
||||
body, err := json.Marshal(svc.Service)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
msg.Services[subdn] = append(msg.Services[subdn], &happydns.ServiceMessage{
|
||||
ServiceMeta: svc.ServiceMeta,
|
||||
Service: body,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return msg, nil
|
||||
}
|
||||
|
||||
func (s *inMemoryZoneStorage) CreateZone(zone *happydns.Zone) error {
|
||||
if zone.Id == nil {
|
||||
zone.Id = happydns.Identifier([]byte(fmt.Sprintf("zone-%d", len(s.zones))))
|
||||
}
|
||||
s.zones[zone.Id.String()] = zone
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *inMemoryZoneStorage) UpdateZone(zone *happydns.Zone) error {
|
||||
s.zones[zone.Id.String()] = zone
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *inMemoryZoneStorage) DeleteZone(zoneid happydns.Identifier) error {
|
||||
delete(s.zones, zoneid.String())
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *inMemoryZoneStorage) ClearZones() error {
|
||||
s.zones = map[string]*happydns.Zone{}
|
||||
return nil
|
||||
}
|
||||
|
||||
// mockZoneRetrieverFailOnNth returns records until the Nth call, then fails.
|
||||
type mockZoneRetrieverFailOnNth struct {
|
||||
records []happydns.Record
|
||||
failOnNth int
|
||||
failErr error
|
||||
calls int
|
||||
}
|
||||
|
||||
func (m *mockZoneRetrieverFailOnNth) RetrieveZone(_ context.Context, _ *happydns.Provider, _ string) ([]happydns.Record, error) {
|
||||
m.calls++
|
||||
if m.calls >= m.failOnNth {
|
||||
return nil, m.failErr
|
||||
}
|
||||
return m.records, nil
|
||||
}
|
||||
|
||||
// testZoneRetriever is an interface matching orchestrator.ZoneRetriever.
|
||||
type testZoneRetriever interface {
|
||||
RetrieveZone(ctx context.Context, provider *happydns.Provider, name string) ([]happydns.Record, error)
|
||||
}
|
||||
|
||||
// buildTestApplier creates a ZoneCorrectionApplierUsecase with the given overrides.
|
||||
func buildTestApplier(
|
||||
providerGetter *mockProviderGetter,
|
||||
zoneCorrector *mockZoneCorrector,
|
||||
retriever testZoneRetriever,
|
||||
domainUpdater *mockDomainUpdater,
|
||||
storage *inMemoryZoneStorage,
|
||||
) *orchestrator.ZoneCorrectionApplierUsecase {
|
||||
listRecords := zoneUC.NewListRecordsUsecase(serviceUC.NewListRecordsUsecase())
|
||||
lister := orchestrator.NewZoneCorrectionListerUsecase(
|
||||
providerGetter,
|
||||
listRecords,
|
||||
zoneCorrector,
|
||||
retriever,
|
||||
)
|
||||
|
||||
zoneGetter := zoneUC.NewGetZoneUsecase(storage)
|
||||
zoneCreator := zoneUC.NewCreateZoneUsecase(storage)
|
||||
zoneUpdater := zoneUC.NewUpdateZoneUsease(storage, zoneGetter)
|
||||
|
||||
return orchestrator.NewZoneCorrectionApplierUsecase(
|
||||
domainlogUC.NoopDomainLogAppender{},
|
||||
domainUpdater,
|
||||
lister,
|
||||
zoneCreator,
|
||||
zoneGetter,
|
||||
retriever,
|
||||
zoneUpdater,
|
||||
)
|
||||
}
|
||||
|
||||
func TestApply_NoRefetch_WhenProviderLacksCapability(t *testing.T) {
|
||||
// Provider without manages-soa-serial capability.
|
||||
provider := &happydns.Provider{
|
||||
ProviderMeta: happydns.ProviderMeta{
|
||||
Type: "NoSuchProvider",
|
||||
},
|
||||
}
|
||||
|
||||
storage := newInMemoryZoneStorage()
|
||||
|
||||
wipZoneID := happydns.Identifier([]byte("wip-zone"))
|
||||
wipZone := &happydns.Zone{
|
||||
ZoneMeta: happydns.ZoneMeta{
|
||||
Id: wipZoneID,
|
||||
DefaultTTL: 3600,
|
||||
},
|
||||
Services: map[happydns.Subdomain][]*happydns.Service{},
|
||||
}
|
||||
storage.zones[wipZoneID.String()] = wipZone
|
||||
|
||||
domain := &happydns.Domain{
|
||||
Id: happydns.Identifier([]byte("test-domain")),
|
||||
ProviderId: happydns.Identifier([]byte("test-provider")),
|
||||
DomainName: "example.com.",
|
||||
ZoneHistory: []happydns.Identifier{wipZoneID},
|
||||
}
|
||||
|
||||
retriever := &mockZoneRetriever{records: nil}
|
||||
|
||||
uc := buildTestApplier(
|
||||
&mockProviderGetter{provider: provider},
|
||||
&mockZoneCorrector{corrections: nil, nbDiff: 0},
|
||||
retriever,
|
||||
&mockDomainUpdater{domain: domain},
|
||||
storage,
|
||||
)
|
||||
|
||||
snapshot, err := uc.Apply(
|
||||
context.Background(),
|
||||
&happydns.User{Id: happydns.Identifier([]byte("test-user"))},
|
||||
domain,
|
||||
wipZone,
|
||||
&happydns.ApplyZoneForm{
|
||||
WantedCorrections: nil,
|
||||
CommitMsg: "test deploy",
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if snapshot == nil {
|
||||
t.Fatal("expected snapshot, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestApply_Refetch_WhenProviderManagesSOASerial(t *testing.T) {
|
||||
// Use the DDNSServer type which has manages-soa-serial capability.
|
||||
// First verify it's registered.
|
||||
creators := providerReg.GetProviders()
|
||||
_, hasDDNS := creators["DDNSServer"]
|
||||
if !hasDDNS {
|
||||
t.Skip("DDNSServer provider not registered")
|
||||
}
|
||||
|
||||
provider := &happydns.Provider{
|
||||
ProviderMeta: happydns.ProviderMeta{
|
||||
Type: "DDNSServer",
|
||||
},
|
||||
}
|
||||
|
||||
storage := newInMemoryZoneStorage()
|
||||
|
||||
// Create WIP zone with an Origin service containing old SOA serial.
|
||||
wipZoneID := happydns.Identifier([]byte("wip-zone"))
|
||||
oldSerial := uint32(2024010100)
|
||||
wipZone := &happydns.Zone{
|
||||
ZoneMeta: happydns.ZoneMeta{
|
||||
Id: wipZoneID,
|
||||
DefaultTTL: 3600,
|
||||
},
|
||||
Services: map[happydns.Subdomain][]*happydns.Service{
|
||||
"": {
|
||||
{
|
||||
ServiceMeta: happydns.ServiceMeta{
|
||||
Id: happydns.Identifier([]byte("origin-svc")),
|
||||
Type: "abstract.Origin",
|
||||
},
|
||||
Service: &abstract.Origin{
|
||||
SOA: &dns.SOA{
|
||||
Hdr: dns.RR_Header{Name: "example.com.", Rrtype: dns.TypeSOA, Class: dns.ClassINET, Ttl: 3600},
|
||||
Ns: "ns1.example.com.",
|
||||
Mbox: "admin.example.com.",
|
||||
Serial: oldSerial,
|
||||
},
|
||||
NameServers: []*dns.NS{
|
||||
{Hdr: dns.RR_Header{Name: "example.com.", Rrtype: dns.TypeNS, Class: dns.ClassINET, Ttl: 3600}, Ns: "ns1.example.com."},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
storage.zones[wipZoneID.String()] = wipZone
|
||||
|
||||
domain := &happydns.Domain{
|
||||
Id: happydns.Identifier([]byte("test-domain")),
|
||||
ProviderId: happydns.Identifier([]byte("test-provider")),
|
||||
DomainName: "example.com.",
|
||||
ZoneHistory: []happydns.Identifier{wipZoneID},
|
||||
}
|
||||
|
||||
// The re-fetched records contain a new SOA serial.
|
||||
newSerial := uint32(2024010101)
|
||||
refetchedRecords := []happydns.Record{
|
||||
&dns.SOA{
|
||||
Hdr: dns.RR_Header{Name: "example.com.", Rrtype: dns.TypeSOA, Class: dns.ClassINET, Ttl: 3600},
|
||||
Ns: "ns1.example.com.",
|
||||
Mbox: "admin.example.com.",
|
||||
Serial: newSerial,
|
||||
},
|
||||
&dns.NS{
|
||||
Hdr: dns.RR_Header{Name: "example.com.", Rrtype: dns.TypeNS, Class: dns.ClassINET, Ttl: 3600},
|
||||
Ns: "ns1.example.com.",
|
||||
},
|
||||
}
|
||||
|
||||
retriever := &mockZoneRetriever{records: refetchedRecords}
|
||||
|
||||
uc := buildTestApplier(
|
||||
&mockProviderGetter{provider: provider},
|
||||
&mockZoneCorrector{corrections: nil, nbDiff: 0},
|
||||
retriever,
|
||||
&mockDomainUpdater{domain: domain},
|
||||
storage,
|
||||
)
|
||||
|
||||
snapshot, err := uc.Apply(
|
||||
context.Background(),
|
||||
&happydns.User{Id: happydns.Identifier([]byte("test-user"))},
|
||||
domain,
|
||||
wipZone,
|
||||
&happydns.ApplyZoneForm{
|
||||
WantedCorrections: nil,
|
||||
CommitMsg: "test deploy with SOA",
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
// Verify snapshot has the new serial.
|
||||
snapshotSerial := getOriginSOASerial(t, snapshot)
|
||||
if snapshotSerial != newSerial {
|
||||
t.Errorf("snapshot SOA serial: got %d, want %d", snapshotSerial, newSerial)
|
||||
}
|
||||
|
||||
// Verify WIP zone was patched with new serial.
|
||||
updatedWIP := storage.zones[wipZoneID.String()]
|
||||
wipSerial := getOriginSOASerial(t, updatedWIP)
|
||||
if wipSerial != newSerial {
|
||||
t.Errorf("WIP zone SOA serial: got %d, want %d", wipSerial, newSerial)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApply_RefetchFails_FallsBackToTargetRecords(t *testing.T) {
|
||||
creators := providerReg.GetProviders()
|
||||
_, hasDDNS := creators["DDNSServer"]
|
||||
if !hasDDNS {
|
||||
t.Skip("DDNSServer provider not registered")
|
||||
}
|
||||
|
||||
provider := &happydns.Provider{
|
||||
ProviderMeta: happydns.ProviderMeta{
|
||||
Type: "DDNSServer",
|
||||
},
|
||||
}
|
||||
|
||||
storage := newInMemoryZoneStorage()
|
||||
|
||||
wipZoneID := happydns.Identifier([]byte("wip-zone"))
|
||||
wipZone := &happydns.Zone{
|
||||
ZoneMeta: happydns.ZoneMeta{
|
||||
Id: wipZoneID,
|
||||
DefaultTTL: 3600,
|
||||
},
|
||||
Services: map[happydns.Subdomain][]*happydns.Service{},
|
||||
}
|
||||
storage.zones[wipZoneID.String()] = wipZone
|
||||
|
||||
domain := &happydns.Domain{
|
||||
Id: happydns.Identifier([]byte("test-domain")),
|
||||
ProviderId: happydns.Identifier([]byte("test-provider")),
|
||||
DomainName: "example.com.",
|
||||
ZoneHistory: []happydns.Identifier{wipZoneID},
|
||||
}
|
||||
|
||||
// Retriever succeeds on first call (lister diff), fails on second (re-fetch).
|
||||
retriever := &mockZoneRetrieverFailOnNth{
|
||||
records: nil,
|
||||
failOnNth: 2,
|
||||
failErr: fmt.Errorf("connection refused"),
|
||||
}
|
||||
|
||||
uc := buildTestApplier(
|
||||
&mockProviderGetter{provider: provider},
|
||||
&mockZoneCorrector{corrections: nil, nbDiff: 0},
|
||||
retriever,
|
||||
&mockDomainUpdater{domain: domain},
|
||||
storage,
|
||||
)
|
||||
|
||||
snapshot, err := uc.Apply(
|
||||
context.Background(),
|
||||
&happydns.User{Id: happydns.Identifier([]byte("test-user"))},
|
||||
domain,
|
||||
wipZone,
|
||||
&happydns.ApplyZoneForm{
|
||||
WantedCorrections: nil,
|
||||
CommitMsg: "test deploy fallback",
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if snapshot == nil {
|
||||
t.Fatal("expected snapshot, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
// getOriginSOASerial extracts the SOA serial from the Origin service in a zone.
|
||||
func getOriginSOASerial(t *testing.T, zone *happydns.Zone) uint32 {
|
||||
t.Helper()
|
||||
if services, ok := zone.Services[""]; ok {
|
||||
for _, svc := range services {
|
||||
if svc.Type == "abstract.Origin" {
|
||||
if origin, ok := svc.Service.(*abstract.Origin); ok && origin.SOA != nil {
|
||||
return origin.SOA.Serial
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
t.Fatal("no Origin service with SOA found in zone")
|
||||
return 0
|
||||
}
|
||||
|
|
@ -64,7 +64,7 @@ func (uc *ZoneCorrectionListerUsecase) listWithRecords(
|
|||
domain *happydns.Domain,
|
||||
zone *happydns.Zone,
|
||||
) ([]*happydns.Correction, []happydns.Record, []happydns.Record, int, error) {
|
||||
provider, err := uc.providerService.GetUserProvider(user, domain.ProviderId)
|
||||
provider, err := uc.providerService.GetUserProvider(ctx, user, domain.ProviderId)
|
||||
if err != nil {
|
||||
return nil, nil, nil, 0, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ type mockProviderGetter struct {
|
|||
err error
|
||||
}
|
||||
|
||||
func (m *mockProviderGetter) GetUserProvider(_ *happydns.User, _ happydns.Identifier) (*happydns.Provider, error) {
|
||||
func (m *mockProviderGetter) GetUserProvider(_ context.Context, _ *happydns.User, _ happydns.Identifier) (*happydns.Provider, error) {
|
||||
return m.provider, m.err
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -26,9 +26,9 @@ import (
|
|||
"log"
|
||||
"time"
|
||||
|
||||
svc "git.happydns.org/happyDomain/internal/service"
|
||||
zoneUC "git.happydns.org/happyDomain/internal/usecase/zone"
|
||||
"git.happydns.org/happyDomain/model"
|
||||
"git.happydns.org/happyDomain/services"
|
||||
)
|
||||
|
||||
// ZoneImporterUsecase converts a flat slice of DNS records into a structured
|
||||
|
|
@ -54,7 +54,7 @@ func NewZoneImporterUsecase(domainUpdater DomainUpdater, zoneCreator *zoneUC.Cre
|
|||
// domain's most recent zone, persists the new zone, and prepends its ID to the
|
||||
// domain's history. Returns the created zone or an error.
|
||||
func (uc *ZoneImporterUsecase) Import(user *happydns.User, domain *happydns.Domain, rrs []happydns.Record) (*happydns.Zone, error) {
|
||||
services, defaultTTL, err := svcs.AnalyzeZone(domain.DomainName, rrs)
|
||||
services, defaultTTL, err := svc.AnalyzeZone(domain.DomainName, rrs)
|
||||
if err != nil {
|
||||
return nil, happydns.ValidationError{Msg: fmt.Sprintf("unable to perform the analysis of your zone: %s", err.Error())}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,13 +22,14 @@
|
|||
package provider
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"git.happydns.org/happyDomain/model"
|
||||
)
|
||||
|
||||
// CreateDomainOnProvider creates a domain on the given provider.
|
||||
func (s *Service) CreateDomainOnProvider(provider *happydns.Provider, fqdn string) error {
|
||||
func (s *Service) CreateDomainOnProvider(_ context.Context, provider *happydns.Provider, fqdn string) error {
|
||||
p, err := instantiate(provider)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -42,7 +43,7 @@ func (s *Service) CreateDomainOnProvider(provider *happydns.Provider, fqdn strin
|
|||
}
|
||||
|
||||
// ListHostedDomains lists all domains hosted on the given provider.
|
||||
func (s *Service) ListHostedDomains(provider *happydns.Provider) ([]string, error) {
|
||||
func (s *Service) ListHostedDomains(_ context.Context, provider *happydns.Provider) ([]string, error) {
|
||||
p, err := instantiate(provider)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -56,7 +57,7 @@ func (s *Service) ListHostedDomains(provider *happydns.Provider) ([]string, erro
|
|||
}
|
||||
|
||||
// TestDomainExistence tests whether a domain exists on the given provider.
|
||||
func (s *Service) TestDomainExistence(provider *happydns.Provider, name string) error {
|
||||
func (s *Service) TestDomainExistence(_ context.Context, provider *happydns.Provider, name string) error {
|
||||
instance, err := instantiate(provider)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
|
|||
|
|
@ -26,8 +26,8 @@ import (
|
|||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
providerReg "git.happydns.org/happyDomain/internal/provider"
|
||||
"git.happydns.org/happyDomain/model"
|
||||
"git.happydns.org/happyDomain/providers"
|
||||
)
|
||||
|
||||
// Service handles CRUD operations on DNS providers, with ownership enforcement.
|
||||
|
|
@ -53,7 +53,7 @@ func ParseProvider(msg *happydns.ProviderMessage) (p *happydns.Provider, err err
|
|||
p = &happydns.Provider{}
|
||||
|
||||
p.ProviderMeta = msg.ProviderMeta
|
||||
p.Provider, err = providers.FindProvider(msg.Type)
|
||||
p.Provider, err = providerReg.FindProvider(msg.Type)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
|
@ -72,7 +72,7 @@ func instantiate(p *happydns.Provider) (happydns.ProviderActuator, error) {
|
|||
}
|
||||
|
||||
// CreateProvider creates a new provider for the given user.
|
||||
func (s *Service) CreateProvider(user *happydns.User, msg *happydns.ProviderMessage) (*happydns.Provider, error) {
|
||||
func (s *Service) CreateProvider(_ context.Context, user *happydns.User, msg *happydns.ProviderMessage) (*happydns.Provider, error) {
|
||||
provider, err := ParseProvider(msg)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to parse provider: %w", err)
|
||||
|
|
@ -109,7 +109,7 @@ func (s *Service) getUserProvider(user *happydns.User, providerID happydns.Ident
|
|||
}
|
||||
|
||||
// GetUserProvider retrieves a provider for the given user.
|
||||
func (s *Service) GetUserProvider(user *happydns.User, providerID happydns.Identifier) (*happydns.Provider, error) {
|
||||
func (s *Service) GetUserProvider(_ context.Context, user *happydns.User, providerID happydns.Identifier) (*happydns.Provider, error) {
|
||||
p, err := s.getUserProvider(user, providerID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -119,7 +119,7 @@ func (s *Service) GetUserProvider(user *happydns.User, providerID happydns.Ident
|
|||
}
|
||||
|
||||
// GetUserProviderMeta retrieves provider metadata for the given user.
|
||||
func (s *Service) GetUserProviderMeta(user *happydns.User, providerID happydns.Identifier) (*happydns.ProviderMeta, error) {
|
||||
func (s *Service) GetUserProviderMeta(_ context.Context, user *happydns.User, providerID happydns.Identifier) (*happydns.ProviderMeta, error) {
|
||||
p, err := s.getUserProvider(user, providerID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -129,7 +129,7 @@ func (s *Service) GetUserProviderMeta(user *happydns.User, providerID happydns.I
|
|||
}
|
||||
|
||||
// ListUserProviders retrieves all providers for the given user.
|
||||
func (s *Service) ListUserProviders(user *happydns.User) ([]*happydns.ProviderMeta, error) {
|
||||
func (s *Service) ListUserProviders(_ context.Context, user *happydns.User) ([]*happydns.ProviderMeta, error) {
|
||||
items, err := s.store.ListProviders(user)
|
||||
if err != nil {
|
||||
return nil, happydns.InternalError{
|
||||
|
|
@ -147,8 +147,8 @@ func (s *Service) ListUserProviders(user *happydns.User) ([]*happydns.ProviderMe
|
|||
}
|
||||
|
||||
// UpdateProvider updates a provider using the provided update function.
|
||||
func (s *Service) UpdateProvider(providerID happydns.Identifier, user *happydns.User, updateFn func(*happydns.Provider)) error {
|
||||
provider, err := s.GetUserProvider(user, providerID)
|
||||
func (s *Service) UpdateProvider(ctx context.Context, providerID happydns.Identifier, user *happydns.User, updateFn func(*happydns.Provider)) error {
|
||||
provider, err := s.GetUserProvider(ctx, user, providerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -176,19 +176,21 @@ func (s *Service) UpdateProvider(providerID happydns.Identifier, user *happydns.
|
|||
}
|
||||
|
||||
// UpdateProviderFromMessage updates a provider from a ProviderMessage.
|
||||
func (s *Service) UpdateProviderFromMessage(providerID happydns.Identifier, user *happydns.User, p *happydns.ProviderMessage) error {
|
||||
func (s *Service) UpdateProviderFromMessage(ctx context.Context, providerID happydns.Identifier, user *happydns.User, p *happydns.ProviderMessage) error {
|
||||
newprovider, err := ParseProvider(p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.UpdateProvider(providerID, user, func(provider *happydns.Provider) {
|
||||
*provider = *newprovider
|
||||
return s.UpdateProvider(ctx, providerID, user, func(provider *happydns.Provider) {
|
||||
provider.Type = newprovider.Type
|
||||
provider.Comment = newprovider.Comment
|
||||
provider.Provider = newprovider.Provider
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteProvider deletes a provider for the given user.
|
||||
func (s *Service) DeleteProvider(user *happydns.User, providerID happydns.Identifier) error {
|
||||
func (s *Service) DeleteProvider(_ context.Context, user *happydns.User, providerID happydns.Identifier) error {
|
||||
// Verify ownership before deleting
|
||||
if _, err := s.getUserProvider(user, providerID); err != nil {
|
||||
return err
|
||||
|
|
@ -219,65 +221,65 @@ func NewRestrictedService(cfg *happydns.Options, store ProviderStorage) *Restric
|
|||
}
|
||||
|
||||
// CreateProvider refuses the operation when DisableProviders is set, otherwise delegates to Service.
|
||||
func (s *RestrictedService) CreateProvider(user *happydns.User, msg *happydns.ProviderMessage) (*happydns.Provider, error) {
|
||||
func (s *RestrictedService) CreateProvider(ctx context.Context, user *happydns.User, msg *happydns.ProviderMessage) (*happydns.Provider, error) {
|
||||
if s.config.DisableProviders {
|
||||
return nil, happydns.ForbiddenError{Msg: "cannot add provider as DisableProviders parameter is set."}
|
||||
}
|
||||
|
||||
return s.inner.CreateProvider(user, msg)
|
||||
return s.inner.CreateProvider(ctx, user, msg)
|
||||
}
|
||||
|
||||
// DeleteProvider refuses the operation when DisableProviders is set, otherwise delegates to Service.
|
||||
func (s *RestrictedService) DeleteProvider(user *happydns.User, providerID happydns.Identifier) error {
|
||||
func (s *RestrictedService) DeleteProvider(ctx context.Context, user *happydns.User, providerID happydns.Identifier) error {
|
||||
if s.config.DisableProviders {
|
||||
return happydns.ForbiddenError{Msg: "cannot delete provider as DisableProviders parameter is set."}
|
||||
}
|
||||
|
||||
return s.inner.DeleteProvider(user, providerID)
|
||||
return s.inner.DeleteProvider(ctx, user, providerID)
|
||||
}
|
||||
|
||||
// UpdateProvider refuses the operation when DisableProviders is set, otherwise delegates to Service.
|
||||
func (s *RestrictedService) UpdateProvider(providerID happydns.Identifier, user *happydns.User, updateFn func(*happydns.Provider)) error {
|
||||
func (s *RestrictedService) UpdateProvider(ctx context.Context, providerID happydns.Identifier, user *happydns.User, updateFn func(*happydns.Provider)) error {
|
||||
if s.config.DisableProviders {
|
||||
return happydns.ForbiddenError{Msg: "cannot update provider as DisableProviders parameter is set."}
|
||||
}
|
||||
|
||||
return s.inner.UpdateProvider(providerID, user, updateFn)
|
||||
return s.inner.UpdateProvider(ctx, providerID, user, updateFn)
|
||||
}
|
||||
|
||||
// UpdateProviderFromMessage refuses the operation when DisableProviders is set, otherwise delegates to Service.
|
||||
func (s *RestrictedService) UpdateProviderFromMessage(providerID happydns.Identifier, user *happydns.User, p *happydns.ProviderMessage) error {
|
||||
func (s *RestrictedService) UpdateProviderFromMessage(ctx context.Context, providerID happydns.Identifier, user *happydns.User, p *happydns.ProviderMessage) error {
|
||||
if s.config.DisableProviders {
|
||||
return happydns.ForbiddenError{Msg: "cannot update provider as DisableProviders parameter is set."}
|
||||
}
|
||||
|
||||
return s.inner.UpdateProviderFromMessage(providerID, user, p)
|
||||
return s.inner.UpdateProviderFromMessage(ctx, providerID, user, p)
|
||||
}
|
||||
|
||||
func (s *RestrictedService) CreateDomainOnProvider(provider *happydns.Provider, fqdn string) error {
|
||||
func (s *RestrictedService) CreateDomainOnProvider(ctx context.Context, provider *happydns.Provider, fqdn string) error {
|
||||
if s.config.DisableProviders {
|
||||
return happydns.ForbiddenError{Msg: "cannot create domain on provider as DisableProviders parameter is set."}
|
||||
}
|
||||
|
||||
return s.inner.CreateDomainOnProvider(provider, fqdn)
|
||||
return s.inner.CreateDomainOnProvider(ctx, provider, fqdn)
|
||||
}
|
||||
|
||||
// Read-only operations delegate directly.
|
||||
|
||||
func (s *RestrictedService) GetUserProvider(user *happydns.User, providerID happydns.Identifier) (*happydns.Provider, error) {
|
||||
return s.inner.GetUserProvider(user, providerID)
|
||||
func (s *RestrictedService) GetUserProvider(ctx context.Context, user *happydns.User, providerID happydns.Identifier) (*happydns.Provider, error) {
|
||||
return s.inner.GetUserProvider(ctx, user, providerID)
|
||||
}
|
||||
|
||||
func (s *RestrictedService) GetUserProviderMeta(user *happydns.User, providerID happydns.Identifier) (*happydns.ProviderMeta, error) {
|
||||
return s.inner.GetUserProviderMeta(user, providerID)
|
||||
func (s *RestrictedService) GetUserProviderMeta(ctx context.Context, user *happydns.User, providerID happydns.Identifier) (*happydns.ProviderMeta, error) {
|
||||
return s.inner.GetUserProviderMeta(ctx, user, providerID)
|
||||
}
|
||||
|
||||
func (s *RestrictedService) ListUserProviders(user *happydns.User) ([]*happydns.ProviderMeta, error) {
|
||||
return s.inner.ListUserProviders(user)
|
||||
func (s *RestrictedService) ListUserProviders(ctx context.Context, user *happydns.User) ([]*happydns.ProviderMeta, error) {
|
||||
return s.inner.ListUserProviders(ctx, user)
|
||||
}
|
||||
|
||||
func (s *RestrictedService) ListHostedDomains(provider *happydns.Provider) ([]string, error) {
|
||||
return s.inner.ListHostedDomains(provider)
|
||||
func (s *RestrictedService) ListHostedDomains(ctx context.Context, provider *happydns.Provider) ([]string, error) {
|
||||
return s.inner.ListHostedDomains(ctx, provider)
|
||||
}
|
||||
|
||||
func (s *RestrictedService) ListZoneCorrections(ctx context.Context, provider *happydns.Provider, domain *happydns.Domain, records []happydns.Record) ([]*happydns.Correction, int, error) {
|
||||
|
|
@ -288,6 +290,6 @@ func (s *RestrictedService) RetrieveZone(ctx context.Context, provider *happydns
|
|||
return s.inner.RetrieveZone(ctx, provider, name)
|
||||
}
|
||||
|
||||
func (s *RestrictedService) TestDomainExistence(provider *happydns.Provider, name string) error {
|
||||
return s.inner.TestDomainExistence(provider, name)
|
||||
func (s *RestrictedService) TestDomainExistence(ctx context.Context, provider *happydns.Provider, name string) error {
|
||||
return s.inner.TestDomainExistence(ctx, provider, name)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@
|
|||
package provider_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
|
|
@ -33,6 +34,8 @@ import (
|
|||
"git.happydns.org/happyDomain/providers"
|
||||
)
|
||||
|
||||
var ctx = context.Background()
|
||||
|
||||
func createTestUser(t *testing.T, store storage.Storage, email string) *happydns.User {
|
||||
user := &happydns.User{
|
||||
Id: happydns.Identifier([]byte("user-" + email)),
|
||||
|
|
@ -86,7 +89,7 @@ func Test_CreateProvider(t *testing.T) {
|
|||
user := createTestUser(t, db, "test@example.com")
|
||||
msg := createTestProviderMessage(t, "DDNSServer", "Test DDNS Provider")
|
||||
|
||||
p, err := providerService.CreateProvider(user, msg)
|
||||
p, err := providerService.CreateProvider(ctx, user, msg)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
|
@ -118,13 +121,13 @@ func Test_GetUserProvider(t *testing.T) {
|
|||
|
||||
// Create a provider
|
||||
msg := createTestProviderMessage(t, "DDNSServer", "Test Provider")
|
||||
createdProvider, err := providerService.CreateProvider(user, msg)
|
||||
createdProvider, err := providerService.CreateProvider(ctx, user, msg)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error creating provider: %v", err)
|
||||
}
|
||||
|
||||
// Retrieve the provider
|
||||
retrievedProvider, err := providerService.GetUserProvider(user, createdProvider.Id)
|
||||
retrievedProvider, err := providerService.GetUserProvider(ctx, user, createdProvider.Id)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
|
@ -145,13 +148,13 @@ func Test_GetUserProvider_WrongUser(t *testing.T) {
|
|||
|
||||
// Create a provider for user1
|
||||
msg := createTestProviderMessage(t, "DDNSServer", "User1 Provider")
|
||||
createdProvider, err := providerService.CreateProvider(user1, msg)
|
||||
createdProvider, err := providerService.CreateProvider(ctx, user1, msg)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error creating provider: %v", err)
|
||||
}
|
||||
|
||||
// Try to retrieve the provider as user2
|
||||
_, err = providerService.GetUserProvider(user2, createdProvider.Id)
|
||||
_, err = providerService.GetUserProvider(ctx, user2, createdProvider.Id)
|
||||
if err == nil {
|
||||
t.Error("expected error when retrieving another user's provider")
|
||||
}
|
||||
|
|
@ -166,7 +169,7 @@ func Test_GetUserProvider_NotFound(t *testing.T) {
|
|||
user := createTestUser(t, db, "test@example.com")
|
||||
|
||||
nonexistentID := happydns.Identifier([]byte("nonexistent-id"))
|
||||
_, err := providerService.GetUserProvider(user, nonexistentID)
|
||||
_, err := providerService.GetUserProvider(ctx, user, nonexistentID)
|
||||
if err == nil {
|
||||
t.Error("expected error when retrieving nonexistent provider")
|
||||
}
|
||||
|
|
@ -182,13 +185,13 @@ func Test_GetUserProviderMeta(t *testing.T) {
|
|||
|
||||
// Create a provider
|
||||
msg := createTestProviderMessage(t, "DDNSServer", "Test Provider Meta")
|
||||
createdProvider, err := providerService.CreateProvider(user, msg)
|
||||
createdProvider, err := providerService.CreateProvider(ctx, user, msg)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error creating provider: %v", err)
|
||||
}
|
||||
|
||||
// Retrieve the provider metadata
|
||||
meta, err := providerService.GetUserProviderMeta(user, createdProvider.Id)
|
||||
meta, err := providerService.GetUserProviderMeta(ctx, user, createdProvider.Id)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
|
@ -207,21 +210,21 @@ func Test_ListUserProviders(t *testing.T) {
|
|||
user := createTestUser(t, db, "test@example.com")
|
||||
|
||||
// Create multiple providers
|
||||
_, err := providerService.CreateProvider(user, createTestProviderMessage(t, "DDNSServer", "Provider 1"))
|
||||
_, err := providerService.CreateProvider(ctx, user, createTestProviderMessage(t, "DDNSServer", "Provider 1"))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error creating provider 1: %v", err)
|
||||
}
|
||||
_, err = providerService.CreateProvider(user, createTestProviderMessage(t, "DDNSServer", "Provider 2"))
|
||||
_, err = providerService.CreateProvider(ctx, user, createTestProviderMessage(t, "DDNSServer", "Provider 2"))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error creating provider 2: %v", err)
|
||||
}
|
||||
_, err = providerService.CreateProvider(user, createTestProviderMessage(t, "DDNSServer", "Provider 3"))
|
||||
_, err = providerService.CreateProvider(ctx, user, createTestProviderMessage(t, "DDNSServer", "Provider 3"))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error creating provider 3: %v", err)
|
||||
}
|
||||
|
||||
// List providers
|
||||
providers, err := providerService.ListUserProviders(user)
|
||||
providers, err := providerService.ListUserProviders(ctx, user)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
|
@ -238,23 +241,23 @@ func Test_ListUserProviders_MultipleUsers(t *testing.T) {
|
|||
user2 := createTestUser(t, db, "user2@example.com")
|
||||
|
||||
// Create providers for user1
|
||||
_, err := providerService.CreateProvider(user1, createTestProviderMessage(t, "DDNSServer", "User1 Provider 1"))
|
||||
_, err := providerService.CreateProvider(ctx, user1, createTestProviderMessage(t, "DDNSServer", "User1 Provider 1"))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
_, err = providerService.CreateProvider(user1, createTestProviderMessage(t, "DDNSServer", "User1 Provider 2"))
|
||||
_, err = providerService.CreateProvider(ctx, user1, createTestProviderMessage(t, "DDNSServer", "User1 Provider 2"))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
// Create provider for user2
|
||||
_, err = providerService.CreateProvider(user2, createTestProviderMessage(t, "DDNSServer", "User2 Provider 1"))
|
||||
_, err = providerService.CreateProvider(ctx, user2, createTestProviderMessage(t, "DDNSServer", "User2 Provider 1"))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
// List providers for user1
|
||||
user1Providers, err := providerService.ListUserProviders(user1)
|
||||
user1Providers, err := providerService.ListUserProviders(ctx, user1)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
|
@ -263,7 +266,7 @@ func Test_ListUserProviders_MultipleUsers(t *testing.T) {
|
|||
}
|
||||
|
||||
// List providers for user2
|
||||
user2Providers, err := providerService.ListUserProviders(user2)
|
||||
user2Providers, err := providerService.ListUserProviders(ctx, user2)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
|
@ -279,13 +282,13 @@ func Test_UpdateProvider(t *testing.T) {
|
|||
|
||||
// Create a provider
|
||||
msg := createTestProviderMessage(t, "DDNSServer", "Original comment")
|
||||
createdProvider, err := providerService.CreateProvider(user, msg)
|
||||
createdProvider, err := providerService.CreateProvider(ctx, user, msg)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error creating provider: %v", err)
|
||||
}
|
||||
|
||||
// Update the provider
|
||||
err = providerService.UpdateProvider(createdProvider.Id, user, func(p *happydns.Provider) {
|
||||
err = providerService.UpdateProvider(ctx, createdProvider.Id, user, func(p *happydns.Provider) {
|
||||
p.Comment = "Updated comment"
|
||||
})
|
||||
if err != nil {
|
||||
|
|
@ -293,7 +296,7 @@ func Test_UpdateProvider(t *testing.T) {
|
|||
}
|
||||
|
||||
// Verify the provider was updated
|
||||
updated, err := providerService.GetUserProvider(user, createdProvider.Id)
|
||||
updated, err := providerService.GetUserProvider(ctx, user, createdProvider.Id)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error retrieving updated provider: %v", err)
|
||||
}
|
||||
|
|
@ -309,14 +312,14 @@ func Test_UpdateProvider_PreventIdChange(t *testing.T) {
|
|||
|
||||
// Create a provider
|
||||
msg := createTestProviderMessage(t, "DDNSServer", "Test Provider")
|
||||
createdProvider, err := providerService.CreateProvider(user, msg)
|
||||
createdProvider, err := providerService.CreateProvider(ctx, user, msg)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error creating provider: %v", err)
|
||||
}
|
||||
|
||||
// Try to change the provider ID
|
||||
newID := happydns.Identifier([]byte("new-provider-id"))
|
||||
err = providerService.UpdateProvider(createdProvider.Id, user, func(p *happydns.Provider) {
|
||||
err = providerService.UpdateProvider(ctx, createdProvider.Id, user, func(p *happydns.Provider) {
|
||||
p.Id = newID
|
||||
})
|
||||
if err == nil {
|
||||
|
|
@ -335,13 +338,13 @@ func Test_UpdateProvider_WrongUser(t *testing.T) {
|
|||
|
||||
// Create a provider for user1
|
||||
msg := createTestProviderMessage(t, "DDNSServer", "User1 Provider")
|
||||
createdProvider, err := providerService.CreateProvider(user1, msg)
|
||||
createdProvider, err := providerService.CreateProvider(ctx, user1, msg)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error creating provider: %v", err)
|
||||
}
|
||||
|
||||
// Try to update the provider as user2
|
||||
err = providerService.UpdateProvider(createdProvider.Id, user2, func(p *happydns.Provider) {
|
||||
err = providerService.UpdateProvider(ctx, createdProvider.Id, user2, func(p *happydns.Provider) {
|
||||
p.Comment = "Hijacked"
|
||||
})
|
||||
if err == nil {
|
||||
|
|
@ -356,19 +359,19 @@ func Test_DeleteProvider(t *testing.T) {
|
|||
|
||||
// Create a provider
|
||||
msg := createTestProviderMessage(t, "DDNSServer", "Test Provider")
|
||||
createdProvider, err := providerService.CreateProvider(user, msg)
|
||||
createdProvider, err := providerService.CreateProvider(ctx, user, msg)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error creating provider: %v", err)
|
||||
}
|
||||
|
||||
// Delete the provider
|
||||
err = providerService.DeleteProvider(user, createdProvider.Id)
|
||||
err = providerService.DeleteProvider(ctx, user, createdProvider.Id)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
// Verify the provider was deleted
|
||||
_, err = providerService.GetUserProvider(user, createdProvider.Id)
|
||||
_, err = providerService.GetUserProvider(ctx, user, createdProvider.Id)
|
||||
if err == nil {
|
||||
t.Error("expected error when retrieving deleted provider")
|
||||
}
|
||||
|
|
@ -385,13 +388,13 @@ func Test_DeleteProvider_WrongUser(t *testing.T) {
|
|||
|
||||
// Create a provider for user1
|
||||
msg := createTestProviderMessage(t, "DDNSServer", "User1 Provider")
|
||||
createdProvider, err := providerService.CreateProvider(user1, msg)
|
||||
createdProvider, err := providerService.CreateProvider(ctx, user1, msg)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error creating provider: %v", err)
|
||||
}
|
||||
|
||||
// Try to delete the provider as user2
|
||||
err = providerService.DeleteProvider(user2, createdProvider.Id)
|
||||
err = providerService.DeleteProvider(ctx, user2, createdProvider.Id)
|
||||
if err == nil {
|
||||
t.Error("expected error when deleting another user's provider")
|
||||
}
|
||||
|
|
@ -400,7 +403,7 @@ func Test_DeleteProvider_WrongUser(t *testing.T) {
|
|||
}
|
||||
|
||||
// Verify the provider still exists for user1
|
||||
_, err = providerService.GetUserProvider(user1, createdProvider.Id)
|
||||
_, err = providerService.GetUserProvider(ctx, user1, createdProvider.Id)
|
||||
if err != nil {
|
||||
t.Errorf("provider should still exist for user1, got error: %v", err)
|
||||
}
|
||||
|
|
@ -450,7 +453,7 @@ func Test_RestrictedService_CreateProvider_Disabled(t *testing.T) {
|
|||
user := createTestUser(t, db, "test@example.com")
|
||||
msg := createTestProviderMessage(t, "DDNSServer", "Test Provider")
|
||||
|
||||
_, err := providerService.CreateProvider(user, msg)
|
||||
_, err := providerService.CreateProvider(ctx, user, msg)
|
||||
if err == nil {
|
||||
t.Error("expected error when creating provider with DisableProviders=true")
|
||||
}
|
||||
|
|
@ -467,7 +470,7 @@ func Test_RestrictedService_UpdateProvider_Disabled(t *testing.T) {
|
|||
unrestricted := provider.NewService(db, &mockValidator{})
|
||||
user := createTestUser(t, db, "test@example.com")
|
||||
msg := createTestProviderMessage(t, "DDNSServer", "Test Provider")
|
||||
createdProvider, err := unrestricted.CreateProvider(user, msg)
|
||||
createdProvider, err := unrestricted.CreateProvider(ctx, user, msg)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error creating provider: %v", err)
|
||||
}
|
||||
|
|
@ -478,7 +481,7 @@ func Test_RestrictedService_UpdateProvider_Disabled(t *testing.T) {
|
|||
}
|
||||
restrictedService := provider.NewRestrictedService(config, db)
|
||||
|
||||
err = restrictedService.UpdateProvider(createdProvider.Id, user, func(p *happydns.Provider) {
|
||||
err = restrictedService.UpdateProvider(ctx, createdProvider.Id, user, func(p *happydns.Provider) {
|
||||
p.Comment = "Updated"
|
||||
})
|
||||
if err == nil {
|
||||
|
|
@ -497,7 +500,7 @@ func Test_RestrictedService_DeleteProvider_Disabled(t *testing.T) {
|
|||
unrestricted := provider.NewService(db, &mockValidator{})
|
||||
user := createTestUser(t, db, "test@example.com")
|
||||
msg := createTestProviderMessage(t, "DDNSServer", "Test Provider")
|
||||
createdProvider, err := unrestricted.CreateProvider(user, msg)
|
||||
createdProvider, err := unrestricted.CreateProvider(ctx, user, msg)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error creating provider: %v", err)
|
||||
}
|
||||
|
|
@ -508,7 +511,7 @@ func Test_RestrictedService_DeleteProvider_Disabled(t *testing.T) {
|
|||
}
|
||||
restrictedService := provider.NewRestrictedService(config, db)
|
||||
|
||||
err = restrictedService.DeleteProvider(user, createdProvider.Id)
|
||||
err = restrictedService.DeleteProvider(ctx, user, createdProvider.Id)
|
||||
if err == nil {
|
||||
t.Error("expected error when deleting provider with DisableProviders=true")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@
|
|||
package usecase
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
|
|
@ -41,7 +42,7 @@ func NewProviderSettingsUsecase(cfg *happydns.Options, ps happydns.ProviderUseca
|
|||
}
|
||||
}
|
||||
|
||||
func (psu *providerSettingsUsecase) NextProviderSettingsState(state *happydns.ProviderSettingsState, pType string, user *happydns.User) (*happydns.Provider, *happydns.ProviderSettingsResponse, error) {
|
||||
func (psu *providerSettingsUsecase) NextProviderSettingsState(ctx context.Context, state *happydns.ProviderSettingsState, pType string, user *happydns.User) (*happydns.Provider, *happydns.ProviderSettingsResponse, error) {
|
||||
fu := NewFormUsecase(psu.config)
|
||||
|
||||
form, p, err := forms.DoSettingState(fu, &state.FormState, state.ProviderBody, forms.GenDefaultSettingsForm)
|
||||
|
|
@ -71,7 +72,7 @@ func (psu *providerSettingsUsecase) NextProviderSettingsState(state *happydns.Pr
|
|||
|
||||
if state.Id == nil {
|
||||
// Create a new Provider via the service layer
|
||||
provider, err := psu.providerService.CreateProvider(user, msg)
|
||||
provider, err := psu.providerService.CreateProvider(ctx, user, msg)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
|
@ -79,12 +80,12 @@ func (psu *providerSettingsUsecase) NextProviderSettingsState(state *happydns.Pr
|
|||
return provider, nil, nil
|
||||
} else {
|
||||
// Update an existing Provider via the service layer
|
||||
err := psu.providerService.UpdateProviderFromMessage(*state.Id, user, msg)
|
||||
err := psu.providerService.UpdateProviderFromMessage(ctx, *state.Id, user, msg)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
provider, err := psu.providerService.GetUserProvider(user, *state.Id)
|
||||
provider, err := psu.providerService.GetUserProvider(ctx, user, *state.Id)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import (
|
|||
"strings"
|
||||
|
||||
"git.happydns.org/happyDomain/internal/forms"
|
||||
providerReg "git.happydns.org/happyDomain/internal/provider"
|
||||
"git.happydns.org/happyDomain/model"
|
||||
"git.happydns.org/happyDomain/providers"
|
||||
)
|
||||
|
|
@ -37,10 +38,10 @@ func NewProviderSpecsUsecase() happydns.ProviderSpecsUsecase {
|
|||
}
|
||||
|
||||
func (psu *providerSpecsUsecase) ListProviders() map[string]happydns.ProviderInfos {
|
||||
srcs := providers.GetProviders()
|
||||
srcs := providerReg.GetProviders()
|
||||
|
||||
ret := map[string]happydns.ProviderInfos{}
|
||||
for k, src := range *srcs {
|
||||
for k, src := range srcs {
|
||||
ret[k] = src.Infos
|
||||
}
|
||||
|
||||
|
|
@ -57,7 +58,7 @@ func (psu *providerSpecsUsecase) GetProviderIcon(psid string) ([]byte, error) {
|
|||
}
|
||||
|
||||
func (psu *providerSpecsUsecase) GetProviderSpecs(psid string) (*happydns.ProviderSpecs, error) {
|
||||
pcreator, ok := (*providers.GetProviders())[psid]
|
||||
pcreator, ok := providerReg.GetProviders()[psid]
|
||||
if !ok {
|
||||
return nil, happydns.NotFoundError{Msg: happydns.ErrProviderNotFound.Error()}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,9 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"git.happydns.org/happyDomain/internal/helpers"
|
||||
"git.happydns.org/happyDomain/model"
|
||||
)
|
||||
|
|
@ -45,7 +48,7 @@ func (uc *ListRecordsUsecase) List(svc *happydns.Service, origin string, default
|
|||
|
||||
records, err := svc.Service.GetRecords(svc.Domain, defaultTTL, origin)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("during %s generation: %w", reflect.TypeOf(svc.Service).Elem().Name(), err)
|
||||
}
|
||||
|
||||
for i, record := range records {
|
||||
|
|
|
|||
|
|
@ -6,9 +6,10 @@ import (
|
|||
|
||||
"github.com/miekg/dns"
|
||||
|
||||
intsvc "git.happydns.org/happyDomain/internal/service"
|
||||
"git.happydns.org/happyDomain/internal/usecase/service"
|
||||
"git.happydns.org/happyDomain/model"
|
||||
"git.happydns.org/happyDomain/services"
|
||||
_ "git.happydns.org/happyDomain/services"
|
||||
)
|
||||
|
||||
func TestListOneRecord(t *testing.T) {
|
||||
|
|
@ -25,7 +26,7 @@ func TestListOneRecord(t *testing.T) {
|
|||
|
||||
txt := happydns.NewTXT(rr.(*dns.TXT))
|
||||
|
||||
s, _, err := svcs.AnalyzeZone(origin, []happydns.Record{txt})
|
||||
s, _, err := intsvc.AnalyzeZone(origin, []happydns.Record{txt})
|
||||
if err != nil {
|
||||
t.Fatalf("AnalyzeZone failed: %v", err)
|
||||
}
|
||||
|
|
@ -74,7 +75,7 @@ func TestListRecordDefaultTTL(t *testing.T) {
|
|||
|
||||
txt := happydns.NewTXT(rr.(*dns.TXT))
|
||||
|
||||
s, _, err := svcs.AnalyzeZone(origin, []happydns.Record{txt})
|
||||
s, _, err := intsvc.AnalyzeZone(origin, []happydns.Record{txt})
|
||||
if err != nil {
|
||||
t.Fatalf("AnalyzeZone failed: %v", err)
|
||||
}
|
||||
|
|
@ -102,7 +103,7 @@ func TestListRecordRelative(t *testing.T) {
|
|||
|
||||
txt := happydns.NewTXT(rr.(*dns.TXT))
|
||||
|
||||
s, _, err := svcs.AnalyzeZone("", []happydns.Record{txt})
|
||||
s, _, err := intsvc.AnalyzeZone("", []happydns.Record{txt})
|
||||
if err != nil {
|
||||
t.Fatalf("AnalyzeZone failed: %v", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,8 +24,9 @@ package service
|
|||
import (
|
||||
"encoding/json"
|
||||
|
||||
"git.happydns.org/happyDomain/internal/forms"
|
||||
intsvc "git.happydns.org/happyDomain/internal/service"
|
||||
"git.happydns.org/happyDomain/model"
|
||||
"git.happydns.org/happyDomain/services"
|
||||
)
|
||||
|
||||
// ParseService deserialises a ServiceMessage into a typed Service value.
|
||||
|
|
@ -35,11 +36,16 @@ func ParseService(msg *happydns.ServiceMessage) (svc *happydns.Service, err erro
|
|||
svc = &happydns.Service{}
|
||||
|
||||
svc.ServiceMeta = msg.ServiceMeta
|
||||
svc.Service, err = svcs.FindService(msg.Type)
|
||||
svc.Service, err = intsvc.FindService(msg.Type)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = json.Unmarshal(msg.Service, &svc.Service)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = forms.ValidateStructValues(svc.Service)
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,9 +7,10 @@ import (
|
|||
"github.com/miekg/dns"
|
||||
|
||||
"git.happydns.org/happyDomain/internal/helpers"
|
||||
intsvc "git.happydns.org/happyDomain/internal/service"
|
||||
"git.happydns.org/happyDomain/internal/usecase/service"
|
||||
"git.happydns.org/happyDomain/model"
|
||||
"git.happydns.org/happyDomain/services"
|
||||
_ "git.happydns.org/happyDomain/services"
|
||||
)
|
||||
|
||||
func TestExistsInService(t *testing.T) {
|
||||
|
|
@ -25,7 +26,7 @@ func TestExistsInService(t *testing.T) {
|
|||
|
||||
txt := happydns.NewTXT(rr.(*dns.TXT))
|
||||
|
||||
s, _, err := svcs.AnalyzeZone(origin, []happydns.Record{txt})
|
||||
s, _, err := intsvc.AnalyzeZone(origin, []happydns.Record{txt})
|
||||
if err != nil {
|
||||
t.Fatalf("AnalyzeZone failed: %v", err)
|
||||
}
|
||||
|
|
@ -63,7 +64,7 @@ func TestNotExistsInService(t *testing.T) {
|
|||
|
||||
txt := happydns.NewTXT(rr.(*dns.TXT))
|
||||
|
||||
s, _, err := svcs.AnalyzeZone(origin, []happydns.Record{txt})
|
||||
s, _, err := intsvc.AnalyzeZone(origin, []happydns.Record{txt})
|
||||
if err != nil {
|
||||
t.Fatalf("AnalyzeZone failed: %v", err)
|
||||
}
|
||||
|
|
@ -104,7 +105,7 @@ func TestExistsInRelativeService(t *testing.T) {
|
|||
|
||||
txt := happydns.NewTXT(rr.(*dns.TXT))
|
||||
|
||||
s, _, err := svcs.AnalyzeZone("", []happydns.Record{txt})
|
||||
s, _, err := intsvc.AnalyzeZone("", []happydns.Record{txt})
|
||||
if err != nil {
|
||||
t.Fatalf("AnalyzeZone failed: %v", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,8 +29,9 @@ import (
|
|||
|
||||
"github.com/miekg/dns"
|
||||
|
||||
intsvc "git.happydns.org/happyDomain/internal/service"
|
||||
"git.happydns.org/happyDomain/model"
|
||||
"git.happydns.org/happyDomain/services"
|
||||
svcs "git.happydns.org/happyDomain/services"
|
||||
)
|
||||
|
||||
// serviceSpecsUsecase implements happydns.ServiceSpecsUsecase, providing
|
||||
|
|
@ -47,7 +48,7 @@ func NewServiceSpecsUsecase() happydns.ServiceSpecsUsecase {
|
|||
// ListServices returns metadata (ServiceInfos) for every registered DNS service,
|
||||
// keyed by service type identifier.
|
||||
func (ssu *serviceSpecsUsecase) ListServices() map[string]happydns.ServiceInfos {
|
||||
services := svcs.ListServices()
|
||||
services := intsvc.ListServices()
|
||||
|
||||
ret := map[string]happydns.ServiceInfos{}
|
||||
for k, service := range *services {
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ func (tu *tidyUpUsecase) TidyAuthUsers() error {
|
|||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
return iter.Err()
|
||||
}
|
||||
|
||||
func (tu *tidyUpUsecase) TidyDomains() error {
|
||||
|
|
@ -99,7 +99,7 @@ func (tu *tidyUpUsecase) TidyDomains() error {
|
|||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
return iter.Err()
|
||||
}
|
||||
|
||||
func (tu *tidyUpUsecase) TidyDomainLogs() error {
|
||||
|
|
@ -121,7 +121,7 @@ func (tu *tidyUpUsecase) TidyDomainLogs() error {
|
|||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
return iter.Err()
|
||||
}
|
||||
|
||||
func (tu *tidyUpUsecase) TidyProviders() error {
|
||||
|
|
@ -144,7 +144,7 @@ func (tu *tidyUpUsecase) TidyProviders() error {
|
|||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
return iter.Err()
|
||||
}
|
||||
|
||||
func (tu *tidyUpUsecase) TidySessions() error {
|
||||
|
|
@ -167,7 +167,7 @@ func (tu *tidyUpUsecase) TidySessions() error {
|
|||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
return iter.Err()
|
||||
}
|
||||
|
||||
func (tu *tidyUpUsecase) TidyUsers() error {
|
||||
|
|
@ -190,6 +190,10 @@ func (tu *tidyUpUsecase) TidyZones() error {
|
|||
}
|
||||
}
|
||||
|
||||
if err = iterdn.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
iter, err := tu.store.ListAllZones()
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -216,5 +220,5 @@ func (tu *tidyUpUsecase) TidyZones() error {
|
|||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
return iter.Err()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,9 +23,9 @@ package zone
|
|||
|
||||
import (
|
||||
"git.happydns.org/happyDomain/internal/helpers"
|
||||
intsvc "git.happydns.org/happyDomain/internal/service"
|
||||
"git.happydns.org/happyDomain/internal/usecase/service"
|
||||
"git.happydns.org/happyDomain/model"
|
||||
"git.happydns.org/happyDomain/services"
|
||||
)
|
||||
|
||||
// AddRecordUsecase handles adding a single DNS record to an in-memory Zone,
|
||||
|
|
@ -52,7 +52,7 @@ func (uc *AddRecordUsecase) Add(zone *happydns.Zone, origin string, record happy
|
|||
record.Header().Name = helpers.DomainFQDN(record.Header().Name, origin)
|
||||
|
||||
// Research the service in which the record should be found
|
||||
newsvc, _, err := svcs.AnalyzeZone(origin, []happydns.Record{record})
|
||||
newsvc, _, err := intsvc.AnalyzeZone(origin, []happydns.Record{record})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -74,7 +74,7 @@ func (uc *AddRecordUsecase) Add(zone *happydns.Zone, origin string, record happy
|
|||
svc_rrs = append([]happydns.Record{record}, svc_rrs...)
|
||||
|
||||
// Recreate the service
|
||||
mergedsvc, _, err := svcs.AnalyzeZone(origin, svc_rrs)
|
||||
mergedsvc, _, err := intsvc.AnalyzeZone(origin, svc_rrs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,9 +26,10 @@ import (
|
|||
"reflect"
|
||||
|
||||
"git.happydns.org/happyDomain/internal/helpers"
|
||||
intsvc "git.happydns.org/happyDomain/internal/service"
|
||||
"git.happydns.org/happyDomain/internal/usecase/service"
|
||||
"git.happydns.org/happyDomain/model"
|
||||
"git.happydns.org/happyDomain/services"
|
||||
svcs "git.happydns.org/happyDomain/services"
|
||||
)
|
||||
|
||||
// DeleteRecordUsecase handles removing a single DNS record from an in-memory
|
||||
|
|
@ -75,7 +76,7 @@ func (uc *DeleteRecordUsecase) delete(zone *happydns.Zone, origin string, record
|
|||
|
||||
if len(svc_rrs) > 0 {
|
||||
// Recreate the service
|
||||
newsvc, _, err = svcs.AnalyzeZone(origin, svc_rrs)
|
||||
newsvc, _, err = intsvc.AnalyzeZone(origin, svc_rrs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -150,7 +151,7 @@ func (uc *DeleteRecordUsecase) ReanalyzeOrphan(zone *happydns.Zone, origin strin
|
|||
}
|
||||
|
||||
// Redo analysis
|
||||
newsvcs, _, err := svcs.AnalyzeZone(origin, records)
|
||||
newsvcs, _, err := intsvc.AnalyzeZone(origin, records)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -65,6 +65,7 @@ func transferMetadata(oldSvc, newSvc *happydns.Service, origin string, defaultTT
|
|||
newSvc.UserComment = oldSvc.UserComment
|
||||
newSvc.OwnerId = oldSvc.OwnerId
|
||||
newSvc.Aliases = oldSvc.Aliases
|
||||
newSvc.PropagatedAt = oldSvc.PropagatedAt
|
||||
|
||||
if oldSvc.Ttl != 0 {
|
||||
serviceTtl := oldSvc.Ttl
|
||||
|
|
|
|||
|
|
@ -30,13 +30,13 @@ import (
|
|||
// UserAuth represents an account used for authentication (not used in case of external auth).
|
||||
type UserAuth struct {
|
||||
// Id is the User's identifier.
|
||||
Id Identifier `json:"id" swaggertype:"string"`
|
||||
Id Identifier `json:"id" swaggertype:"string" readonly:"true"`
|
||||
|
||||
// Email is the User's login and mean of contact.
|
||||
Email string `json:"email"`
|
||||
|
||||
// EmailVerification is the time when the User verify its email address.
|
||||
EmailVerification *time.Time `json:"emailVerification,omitempty"`
|
||||
EmailVerification *time.Time `json:"emailVerification,omitempty" format:"date-time"`
|
||||
|
||||
// Password is hashed.
|
||||
Password []byte `json:"password,omitempty"`
|
||||
|
|
@ -45,10 +45,10 @@ type UserAuth struct {
|
|||
PasswordRecoveryKey []byte `json:"passwordRecoveryKey,omitempty"`
|
||||
|
||||
// CreatedAt is the time when the User has register is account.
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
CreatedAt time.Time `json:"createdAt" format:"date-time" readonly:"true"`
|
||||
|
||||
// LastLoggedIn is the time when the User has logged in for the last time.
|
||||
LastLoggedIn *time.Time `json:"lastLoggedIn,omitempty"`
|
||||
LastLoggedIn *time.Time `json:"lastLoggedIn,omitempty" format:"date-time"`
|
||||
|
||||
// AllowCommercials stores the user preference regarding email contacts.
|
||||
AllowCommercials bool `json:"allowCommercials"`
|
||||
|
|
|
|||
298
model/checker.go
Normal file
298
model/checker.go
Normal file
|
|
@ -0,0 +1,298 @@
|
|||
// This file is part of the happyDomain (R) project.
|
||||
// Copyright (c) 2020-2026 happyDomain
|
||||
// Authors: Pierre-Olivier Mercier, et al.
|
||||
//
|
||||
// This program is offered under a commercial and under the AGPL license.
|
||||
// For commercial licensing, contact us at <contact@happydomain.org>.
|
||||
//
|
||||
// For AGPL licensing:
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package happydns
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
sdk "git.happydns.org/checker-sdk-go/checker"
|
||||
)
|
||||
|
||||
// The types and helpers needed by external checker plugins live in the
|
||||
// Apache-2.0 licensed checker-sdk-go module. They are re-exported here as
|
||||
// aliases so the rest of the happyDomain codebase keeps working unchanged.
|
||||
//
|
||||
// Host-only types (Execution, CheckPlan, CheckEvaluation, …) remain
|
||||
// defined in this file because they describe orchestration state that is
|
||||
// internal to the happyDomain server and never crosses the plugin boundary.
|
||||
|
||||
// --- Re-exports from checker-sdk-go ---
|
||||
|
||||
type CheckScopeType = sdk.CheckScopeType
|
||||
|
||||
const (
|
||||
CheckScopeAdmin = sdk.CheckScopeAdmin
|
||||
CheckScopeUser = sdk.CheckScopeUser
|
||||
CheckScopeDomain = sdk.CheckScopeDomain
|
||||
CheckScopeZone = sdk.CheckScopeZone
|
||||
CheckScopeService = sdk.CheckScopeService
|
||||
)
|
||||
|
||||
const (
|
||||
AutoFillDomainName = sdk.AutoFillDomainName
|
||||
AutoFillSubdomain = sdk.AutoFillSubdomain
|
||||
AutoFillZone = sdk.AutoFillZone
|
||||
AutoFillServiceType = sdk.AutoFillServiceType
|
||||
AutoFillService = sdk.AutoFillService
|
||||
)
|
||||
|
||||
type (
|
||||
CheckTarget = sdk.CheckTarget
|
||||
CheckerAvailability = sdk.CheckerAvailability
|
||||
CheckerOptions = sdk.CheckerOptions
|
||||
CheckerOptionDocumentation = sdk.CheckerOptionDocumentation
|
||||
CheckerOptionsDocumentation = sdk.CheckerOptionsDocumentation
|
||||
Status = sdk.Status
|
||||
CheckState = sdk.CheckState
|
||||
CheckMetric = sdk.CheckMetric
|
||||
ObservationKey = sdk.ObservationKey
|
||||
CheckIntervalSpec = sdk.CheckIntervalSpec
|
||||
ObservationProvider = sdk.ObservationProvider
|
||||
CheckRuleInfo = sdk.CheckRuleInfo
|
||||
CheckRule = sdk.CheckRule
|
||||
CheckRuleWithOptions = sdk.CheckRuleWithOptions
|
||||
ObservationGetter = sdk.ObservationGetter
|
||||
CheckAggregator = sdk.CheckAggregator
|
||||
CheckerHTMLReporter = sdk.CheckerHTMLReporter
|
||||
CheckerMetricsReporter = sdk.CheckerMetricsReporter
|
||||
CheckerDefinitionProvider = sdk.CheckerDefinitionProvider
|
||||
CheckerDefinition = sdk.CheckerDefinition
|
||||
OptionsValidator = sdk.OptionsValidator
|
||||
ExternalCollectRequest = sdk.ExternalCollectRequest
|
||||
ExternalCollectResponse = sdk.ExternalCollectResponse
|
||||
ExternalEvaluateRequest = sdk.ExternalEvaluateRequest
|
||||
ExternalEvaluateResponse = sdk.ExternalEvaluateResponse
|
||||
ExternalReportRequest = sdk.ExternalReportRequest
|
||||
)
|
||||
|
||||
const (
|
||||
StatusUnknown = sdk.StatusUnknown
|
||||
StatusOK = sdk.StatusOK
|
||||
StatusInfo = sdk.StatusInfo
|
||||
StatusWarn = sdk.StatusWarn
|
||||
StatusCrit = sdk.StatusCrit
|
||||
StatusError = sdk.StatusError
|
||||
)
|
||||
|
||||
// --- Helpers for converting between target identifier strings and *Identifier ---
|
||||
|
||||
// TargetIdentifier parses a target identifier string into an *Identifier.
|
||||
// Returns nil if the string is empty or cannot be parsed.
|
||||
func TargetIdentifier(s string) *Identifier {
|
||||
if s == "" {
|
||||
return nil
|
||||
}
|
||||
id, err := NewIdentifierFromString(s)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return &id
|
||||
}
|
||||
|
||||
// FormatIdentifier returns the string representation of id, or "" if nil.
|
||||
func FormatIdentifier(id *Identifier) string {
|
||||
if id == nil {
|
||||
return ""
|
||||
}
|
||||
return id.String()
|
||||
}
|
||||
|
||||
// --- Host-only types (orchestration state) ---
|
||||
|
||||
// CheckerRunRequest is the JSON body for manually triggering a checker.
|
||||
type CheckerRunRequest struct {
|
||||
Options CheckerOptions `json:"options,omitempty"`
|
||||
EnabledRules map[string]bool `json:"enabledRules,omitempty"`
|
||||
}
|
||||
|
||||
// CheckerOptionsPositional stores options with their positional key components.
|
||||
type CheckerOptionsPositional struct {
|
||||
CheckName string `json:"checkName"`
|
||||
UserId *Identifier `json:"userId,omitempty"`
|
||||
DomainId *Identifier `json:"domainId,omitempty"`
|
||||
ServiceId *Identifier `json:"serviceId,omitempty"`
|
||||
|
||||
Options CheckerOptions `json:"options"`
|
||||
}
|
||||
|
||||
// CheckPlan is an optional user override for a checker on a specific target.
|
||||
type CheckPlan struct {
|
||||
Id Identifier `json:"id" swaggertype:"string" binding:"required" readonly:"true"`
|
||||
CheckerID string `json:"checkerId" binding:"required" readonly:"true"`
|
||||
Target CheckTarget `json:"target" binding:"required" readonly:"true"`
|
||||
Interval *time.Duration `json:"interval,omitempty" swaggertype:"integer"`
|
||||
Enabled map[string]bool `json:"enabled,omitempty"`
|
||||
}
|
||||
|
||||
// IsFullyDisabled returns true if the enabled map is non-empty and every entry is false.
|
||||
func (p *CheckPlan) IsFullyDisabled() bool {
|
||||
if len(p.Enabled) == 0 {
|
||||
return false
|
||||
}
|
||||
for _, v := range p.Enabled {
|
||||
if v {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// IsRuleEnabled returns whether a specific rule is enabled.
|
||||
// A nil or empty map means all rules are enabled. A missing key means enabled.
|
||||
func (p *CheckPlan) IsRuleEnabled(ruleName string) bool {
|
||||
if len(p.Enabled) == 0 {
|
||||
return true
|
||||
}
|
||||
v, ok := p.Enabled[ruleName]
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// CheckerStatus combines a checker definition with its latest execution and plan for a target.
|
||||
type CheckerStatus struct {
|
||||
*CheckerDefinition
|
||||
LatestExecution *Execution `json:"latestExecution,omitempty"`
|
||||
Plan *CheckPlan `json:"plan,omitempty"`
|
||||
Enabled bool `json:"enabled"`
|
||||
EnabledRules map[string]bool `json:"enabledRules"`
|
||||
}
|
||||
|
||||
// CheckEvaluation is the result of running a checker on observed data.
|
||||
type CheckEvaluation struct {
|
||||
Id Identifier `json:"id" swaggertype:"string" binding:"required" readonly:"true"`
|
||||
PlanID *Identifier `json:"planId,omitempty" swaggertype:"string"`
|
||||
CheckerID string `json:"checkerId" binding:"required"`
|
||||
Target CheckTarget `json:"target" binding:"required"`
|
||||
SnapshotID Identifier `json:"snapshotId" swaggertype:"string" binding:"required" readonly:"true"`
|
||||
EvaluatedAt time.Time `json:"evaluatedAt" binding:"required" readonly:"true" format:"date-time"`
|
||||
States []CheckState `json:"states" binding:"required" readonly:"true"`
|
||||
}
|
||||
|
||||
// ObservationSnapshot holds data collected during an execution.
|
||||
type ObservationSnapshot struct {
|
||||
Id Identifier `json:"id" swaggertype:"string" binding:"required" readonly:"true"`
|
||||
Target CheckTarget `json:"target" binding:"required" readonly:"true"`
|
||||
CollectedAt time.Time `json:"collectedAt" binding:"required" readonly:"true" format:"date-time"`
|
||||
Data map[ObservationKey]json.RawMessage `json:"data" binding:"required" readonly:"true" swaggertype:"object,object"`
|
||||
}
|
||||
|
||||
// ObservationCacheEntry is a lightweight pointer to cached observation data in a snapshot.
|
||||
type ObservationCacheEntry struct {
|
||||
SnapshotID Identifier `json:"snapshotId"`
|
||||
CollectedAt time.Time `json:"collectedAt"`
|
||||
}
|
||||
|
||||
// ExecutionStatus represents the lifecycle state of an execution.
|
||||
type ExecutionStatus int
|
||||
|
||||
const (
|
||||
ExecutionPending ExecutionStatus = iota
|
||||
ExecutionRunning
|
||||
ExecutionDone
|
||||
ExecutionFailed
|
||||
)
|
||||
|
||||
// TriggerType represents what initiated an execution.
|
||||
type TriggerType int
|
||||
|
||||
const (
|
||||
TriggerManual TriggerType = iota
|
||||
TriggerSchedule
|
||||
)
|
||||
|
||||
// TriggerInfo describes the trigger for an execution.
|
||||
type TriggerInfo struct {
|
||||
Type TriggerType `json:"type"`
|
||||
PlanID *Identifier `json:"planId,omitempty" swaggertype:"string"`
|
||||
}
|
||||
|
||||
// Execution represents a single run of a checker pipeline.
|
||||
type Execution struct {
|
||||
Id Identifier `json:"id" swaggertype:"string" binding:"required" readonly:"true"`
|
||||
CheckerID string `json:"checkerId" binding:"required" readonly:"true"`
|
||||
PlanID *Identifier `json:"planId,omitempty" swaggertype:"string" readonly:"true"`
|
||||
Target CheckTarget `json:"target" binding:"required" readonly:"true"`
|
||||
Trigger TriggerInfo `json:"trigger" binding:"required" readonly:"true"`
|
||||
StartedAt time.Time `json:"startedAt" binding:"required" readonly:"true" format:"date-time"`
|
||||
EndedAt *time.Time `json:"endedAt,omitempty" readonly:"true" format:"date-time"`
|
||||
Status ExecutionStatus `json:"status" binding:"required" readonly:"true"`
|
||||
Error string `json:"error,omitempty" readonly:"true"`
|
||||
Result CheckState `json:"result" readonly:"true"`
|
||||
EvaluationID *Identifier `json:"evaluationId,omitempty" swaggertype:"string" readonly:"true"`
|
||||
}
|
||||
|
||||
// CheckerEngine orchestrates the full checker pipeline.
|
||||
type CheckerEngine interface {
|
||||
CreateExecution(checkerID string, target CheckTarget, plan *CheckPlan) (*Execution, error)
|
||||
RunExecution(ctx context.Context, exec *Execution, plan *CheckPlan, runOpts CheckerOptions) (*CheckEvaluation, error)
|
||||
}
|
||||
|
||||
// CheckerOptionsKey builds the positional KV key for checker options.
|
||||
// Format: chckrcfg-{checkerName}/{userId}/{domainId}/{serviceId}
|
||||
func CheckerOptionsKey(checkerName string, userId *Identifier, domainId *Identifier, serviceId *Identifier) string {
|
||||
uid := ""
|
||||
if userId != nil {
|
||||
uid = userId.String()
|
||||
}
|
||||
did := ""
|
||||
if domainId != nil {
|
||||
did = domainId.String()
|
||||
}
|
||||
sid := ""
|
||||
if serviceId != nil {
|
||||
sid = serviceId.String()
|
||||
}
|
||||
return fmt.Sprintf("chckrcfg-%s/%s/%s/%s", checkerName, uid, did, sid)
|
||||
}
|
||||
|
||||
// ParseCheckerOptionsKey extracts the positional components from a KV key.
|
||||
func ParseCheckerOptionsKey(key string) (checkerName string, userId *Identifier, domainId *Identifier, serviceId *Identifier) {
|
||||
trimmed := strings.TrimPrefix(key, "chckrcfg-")
|
||||
parts := strings.SplitN(trimmed, "/", 4)
|
||||
if len(parts) < 4 {
|
||||
return trimmed, nil, nil, nil
|
||||
}
|
||||
|
||||
checkerName = parts[0]
|
||||
if parts[1] != "" {
|
||||
if id, err := NewIdentifierFromString(parts[1]); err == nil {
|
||||
userId = &id
|
||||
}
|
||||
}
|
||||
if parts[2] != "" {
|
||||
if id, err := NewIdentifierFromString(parts[2]); err == nil {
|
||||
domainId = &id
|
||||
}
|
||||
}
|
||||
if parts[3] != "" {
|
||||
if id, err := NewIdentifierFromString(parts[3]); err == nil {
|
||||
serviceId = &id
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
|
@ -93,12 +93,20 @@ type Options struct {
|
|||
|
||||
OIDCClients []OIDCSettings
|
||||
|
||||
// CheckerMaxConcurrency is the maximum number of checker jobs that can
|
||||
// run simultaneously. Defaults to runtime.NumCPU().
|
||||
CheckerMaxConcurrency int
|
||||
|
||||
// CaptchaProvider selects the captcha provider ("hcaptcha", "recaptchav2", "turnstile", or "").
|
||||
CaptchaProvider string
|
||||
|
||||
// CaptchaLoginThreshold is the number of consecutive login failures before captcha is required.
|
||||
// 0 means always require captcha at login (when provider is configured).
|
||||
CaptchaLoginThreshold int
|
||||
|
||||
// PluginsDirectories lists filesystem paths scanned at startup for
|
||||
// checker plugins (.so files).
|
||||
PluginsDirectories []string
|
||||
}
|
||||
|
||||
// GetBaseURL returns the full url to the absolute ExternalURL, including BaseURL.
|
||||
|
|
|
|||
|
|
@ -30,14 +30,14 @@ const (
|
|||
CorrectionKindAddition
|
||||
CorrectionKindUpdate
|
||||
CorrectionKindDeletion
|
||||
CorrectionKindOther = 99
|
||||
CorrectionKindOther CorrectionKind = 99
|
||||
)
|
||||
|
||||
type Correction struct {
|
||||
F func() error `json:"-"`
|
||||
Id Identifier `json:"id,omitempty" swaggertype:"string"`
|
||||
Msg string `json:"msg"`
|
||||
Kind CorrectionKind `json:"kind,omitempty"`
|
||||
Msg string `json:"msg" binding:"required"`
|
||||
Kind CorrectionKind `json:"kind" binding:"required"`
|
||||
OldRecords []Record `json:"-"`
|
||||
NewRecords []Record `json:"-"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@
|
|||
package happydns
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
|
|
@ -41,24 +42,30 @@ type DomainCreationInput struct {
|
|||
// Domain holds information about a domain name own by a User.
|
||||
type Domain struct {
|
||||
// Id is the Domain's identifier in the database.
|
||||
Id Identifier `json:"id" swaggertype:"string"`
|
||||
Id Identifier `json:"id" swaggertype:"string" binding:"required" readonly:"true"`
|
||||
|
||||
// Owner is the identifier of the Domain's Owner.
|
||||
Owner Identifier `json:"id_owner" swaggertype:"string"`
|
||||
Owner Identifier `json:"id_owner" swaggertype:"string" binding:"required"`
|
||||
|
||||
// ProviderId is the identifier of the Provider used to access and edit the
|
||||
// Domain.
|
||||
ProviderId Identifier `json:"id_provider" swaggertype:"string"`
|
||||
ProviderId Identifier `json:"id_provider" swaggertype:"string" binding:"required"`
|
||||
|
||||
// DomainName is the FQDN of the managed Domain.
|
||||
DomainName string `json:"domain"`
|
||||
DomainName string `json:"domain" binding:"required"`
|
||||
|
||||
// Group is a hint string aims to group domains.
|
||||
Group string `json:"group,omitempty"`
|
||||
|
||||
// ZoneHistory are the identifiers to the Zone attached to the current
|
||||
// Domain.
|
||||
ZoneHistory []Identifier `json:"zone_history" swaggertype:"array,string"`
|
||||
ZoneHistory []Identifier `json:"zone_history" swaggertype:"array,string" binding:"required" readonly:"true"`
|
||||
}
|
||||
|
||||
// DomainUpdateInput is used for swagger documentation as Domain update.
|
||||
type DomainUpdateInput struct {
|
||||
// Group is a hint string aims to group domains.
|
||||
Group string `json:"group,omitempty"`
|
||||
}
|
||||
|
||||
func NewDomain(user *User, name string, providerID Identifier) (*Domain, error) {
|
||||
|
|
@ -101,7 +108,7 @@ type Subdomain string
|
|||
type Origin string
|
||||
|
||||
type DomainUsecase interface {
|
||||
CreateDomain(*User, *Domain) error
|
||||
CreateDomain(context.Context, *User, *Domain) error
|
||||
DeleteDomain(Identifier) error
|
||||
ExtendsDomainWithZoneMeta(*Domain) (*DomainWithZoneMetadata, error)
|
||||
GetUserDomain(*User, Identifier) (*Domain, error)
|
||||
|
|
|
|||
|
|
@ -34,8 +34,13 @@ var (
|
|||
ErrSessionNotFound = errors.New("session not found")
|
||||
ErrUserNotFound = errors.New("user not found")
|
||||
ErrUserAlreadyExist = errors.New("user already exists")
|
||||
ErrZoneNotFound = errors.New("zone not found")
|
||||
ErrNotFound = errors.New("not found")
|
||||
ErrZoneNotFound = errors.New("zone not found")
|
||||
ErrCheckerNotFound = errors.New("checker not found")
|
||||
ErrCheckPlanNotFound = errors.New("check plan not found")
|
||||
ErrCheckEvaluationNotFound = errors.New("check evaluation not found")
|
||||
ErrExecutionNotFound = errors.New("execution not found")
|
||||
ErrSnapshotNotFound = errors.New("snapshot not found")
|
||||
ErrNotFound = errors.New("not found")
|
||||
)
|
||||
|
||||
const TryAgainErr = "Sorry, we are currently unable to sent email validation link. Please try again later."
|
||||
|
|
|
|||
|
|
@ -72,10 +72,10 @@ type CustomForm struct {
|
|||
// Field
|
||||
type Field struct {
|
||||
// Id is the field identifier.
|
||||
Id string `json:"id"`
|
||||
Id string `json:"id" binding:"required"`
|
||||
|
||||
// Type is the string representation of the field's type.
|
||||
Type string `json:"type"`
|
||||
Type string `json:"type" binding:"required"`
|
||||
|
||||
// Label is the title given to the field, displayed as <label> tag on the interface.
|
||||
Label string `json:"label,omitempty"`
|
||||
|
|
@ -104,6 +104,10 @@ type Field struct {
|
|||
|
||||
// Description stores an helpfull sentence describing the field.
|
||||
Description string `json:"description,omitempty"`
|
||||
|
||||
// AutoFill indicates that this field is automatically filled by the system
|
||||
// based on execution context (e.g. domain name, zone, service type).
|
||||
AutoFill string `json:"autoFill,omitempty"`
|
||||
}
|
||||
|
||||
type FormState struct {
|
||||
|
|
|
|||
|
|
@ -41,19 +41,19 @@ const (
|
|||
|
||||
type DomainLog struct {
|
||||
// Id is the Log's identifier in the database.
|
||||
Id Identifier `json:"id" swaggertype:"string"`
|
||||
Id Identifier `json:"id" swaggertype:"string" binding:"required" readonly:"true"`
|
||||
|
||||
// IdUser is the identifier of the person responsible for the action.
|
||||
IdUser Identifier `json:"id_user" swaggertype:"string"`
|
||||
IdUser Identifier `json:"id_user" swaggertype:"string" binding:"required" readonly:"true"`
|
||||
|
||||
// Date is the date of the action.
|
||||
Date time.Time `json:"date"`
|
||||
Date time.Time `json:"date" binding:"required" format:"date-time" readonly:"true"`
|
||||
|
||||
// Content is the description of the action logged.
|
||||
Content string `json:"content"`
|
||||
Content string `json:"content" binding:"required" readonly:"true"`
|
||||
|
||||
// Level reports the criticity level of the action logged.
|
||||
Level int8 `json:"level"`
|
||||
Level int8 `json:"level" binding:"required" readonly:"true"`
|
||||
}
|
||||
|
||||
type DomainLogWithDomainId struct {
|
||||
|
|
|
|||
|
|
@ -34,10 +34,10 @@ type ProviderBody interface {
|
|||
// ProviderInfos describes the purpose of a user usable provider.
|
||||
type ProviderInfos struct {
|
||||
// Name is the name displayed.
|
||||
Name string `json:"name"`
|
||||
Name string `json:"name" binding:"required"`
|
||||
|
||||
// Description is a brief description of what the provider is.
|
||||
Description string `json:"description"`
|
||||
Description string `json:"description" binding:"required"`
|
||||
|
||||
// Capabilites is a list of special ability of the provider (automatically filled).
|
||||
Capabilities []string `json:"capabilities,omitempty"`
|
||||
|
|
@ -72,13 +72,13 @@ type ProviderMinimal struct {
|
|||
// ProviderMeta holds the metadata associated to a Provider.
|
||||
type ProviderMeta struct {
|
||||
// Type is the string representation of the Provider's type.
|
||||
Type string `json:"_srctype"`
|
||||
Type string `json:"_srctype" binding:"required"`
|
||||
|
||||
// Id is the Provider's identifier.
|
||||
Id Identifier `json:"_id" swaggertype:"string"`
|
||||
Id Identifier `json:"_id" swaggertype:"string" binding:"required" readonly:"true"`
|
||||
|
||||
// Owner is the User's identifier for the current Provider.
|
||||
Owner Identifier `json:"_ownerid" swaggertype:"string"`
|
||||
Owner Identifier `json:"_ownerid" swaggertype:"string" binding:"required" readonly:"true"`
|
||||
|
||||
// Comment is a string that helps user to distinguish the Provider.
|
||||
Comment string `json:"_comment,omitempty"`
|
||||
|
|
@ -124,18 +124,18 @@ func (p *Provider) Meta() *ProviderMeta {
|
|||
}
|
||||
|
||||
type ProviderUsecase interface {
|
||||
CreateProvider(*User, *ProviderMessage) (*Provider, error)
|
||||
CreateDomainOnProvider(*Provider, string) error
|
||||
DeleteProvider(*User, Identifier) error
|
||||
GetUserProvider(*User, Identifier) (*Provider, error)
|
||||
GetUserProviderMeta(*User, Identifier) (*ProviderMeta, error)
|
||||
ListHostedDomains(*Provider) ([]string, error)
|
||||
ListUserProviders(*User) ([]*ProviderMeta, error)
|
||||
ListZoneCorrections(ctx context.Context, provider *Provider, domain *Domain, records []Record) ([]*Correction, int, error)
|
||||
CreateProvider(context.Context, *User, *ProviderMessage) (*Provider, error)
|
||||
CreateDomainOnProvider(context.Context, *Provider, string) error
|
||||
DeleteProvider(context.Context, *User, Identifier) error
|
||||
GetUserProvider(context.Context, *User, Identifier) (*Provider, error)
|
||||
GetUserProviderMeta(context.Context, *User, Identifier) (*ProviderMeta, error)
|
||||
ListHostedDomains(context.Context, *Provider) ([]string, error)
|
||||
ListUserProviders(context.Context, *User) ([]*ProviderMeta, error)
|
||||
ListZoneCorrections(context.Context, *Provider, *Domain, []Record) ([]*Correction, int, error)
|
||||
RetrieveZone(context.Context, *Provider, string) ([]Record, error)
|
||||
TestDomainExistence(*Provider, string) error
|
||||
UpdateProvider(Identifier, *User, func(*Provider)) error
|
||||
UpdateProviderFromMessage(Identifier, *User, *ProviderMessage) error
|
||||
TestDomainExistence(context.Context, *Provider, string) error
|
||||
UpdateProvider(context.Context, Identifier, *User, func(*Provider)) error
|
||||
UpdateProviderFromMessage(context.Context, Identifier, *User, *ProviderMessage) error
|
||||
}
|
||||
|
||||
type ProviderActuator interface {
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@
|
|||
|
||||
package happydns
|
||||
|
||||
import ()
|
||||
import "context"
|
||||
|
||||
type ProviderSettingsState struct {
|
||||
FormState
|
||||
|
|
@ -35,5 +35,5 @@ type ProviderSettingsResponse struct {
|
|||
}
|
||||
|
||||
type ProviderSettingsUsecase interface {
|
||||
NextProviderSettingsState(*ProviderSettingsState, string, *User) (*Provider, *ProviderSettingsResponse, error)
|
||||
NextProviderSettingsState(context.Context, *ProviderSettingsState, string, *User) (*Provider, *ProviderSettingsResponse, error)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,30 +40,48 @@ type ResolverRequest struct {
|
|||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
// DNSQuestion holds a single DNS question entry.
|
||||
type DNSQuestion struct {
|
||||
// Name is the domain name researched.
|
||||
Name string
|
||||
Name string `json:"name"`
|
||||
|
||||
// Qtype is the type of record researched.
|
||||
Qtype uint16
|
||||
Qtype uint16 `json:"qtype"`
|
||||
|
||||
// Qclass is the class of record researched.
|
||||
Qclass uint16
|
||||
Qclass uint16 `json:"qclass"`
|
||||
}
|
||||
|
||||
// DNSMsg is the documentation struct corresponding to dns.Msg
|
||||
type DNSMsg struct {
|
||||
// ResolverResponse is the API response for a DNS resolution.
|
||||
type ResolverResponse struct {
|
||||
// Question is the Question section of the DNS response.
|
||||
Question []DNSQuestion
|
||||
Question []DNSQuestion `json:"question"`
|
||||
|
||||
// Answer is the list of Answer records in the DNS response.
|
||||
Answer []any `swaggertype:"object"`
|
||||
Answer []dns.RR `json:"answer" swaggertype:"object"`
|
||||
|
||||
// Ns is the list of Authoritative records in the DNS response.
|
||||
Ns []any `swaggertype:"object"`
|
||||
Ns []dns.RR `json:"ns" swaggertype:"object"`
|
||||
|
||||
// Extra is the list of extra records in the DNS response.
|
||||
Extra []any `swaggertype:"object"`
|
||||
Extra []dns.RR `json:"extra" swaggertype:"object"`
|
||||
}
|
||||
|
||||
// NewResolverResponseFromMsg converts a dns.Msg to a ResolverResponse.
|
||||
func NewResolverResponseFromMsg(msg *dns.Msg) *ResolverResponse {
|
||||
resp := &ResolverResponse{
|
||||
Answer: msg.Answer,
|
||||
Ns: msg.Ns,
|
||||
Extra: msg.Extra,
|
||||
}
|
||||
for _, q := range msg.Question {
|
||||
resp.Question = append(resp.Question, DNSQuestion{
|
||||
Name: q.Name,
|
||||
Qtype: q.Qtype,
|
||||
Qclass: q.Qclass,
|
||||
})
|
||||
}
|
||||
return resp
|
||||
}
|
||||
|
||||
type ResolverUsecase interface {
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ package happydns
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
|
|
@ -83,19 +84,19 @@ type SPFContributor interface {
|
|||
// ServiceMeta holds the metadata associated to a Service.
|
||||
type ServiceMeta struct {
|
||||
// Type is the string representation of the Service's type.
|
||||
Type string `json:"_svctype"`
|
||||
Type string `json:"_svctype" binding:"required" readonly:"true"`
|
||||
|
||||
// Id is the Service's identifier.
|
||||
Id Identifier `json:"_id,omitempty" swaggertype:"string"`
|
||||
Id Identifier `json:"_id,omitempty" swaggertype:"string" readonly:"true"`
|
||||
|
||||
// OwnerId is the User's identifier for the current Service.
|
||||
OwnerId Identifier `json:"_ownerid,omitempty" swaggertype:"string"`
|
||||
|
||||
// Domain contains the abstract domain where this Service relates.
|
||||
Domain string `json:"_domain"`
|
||||
Domain string `json:"_domain" binding:"required"`
|
||||
|
||||
// Ttl contains the specific TTL for the underlying Resources.
|
||||
Ttl uint32 `json:"_ttl"`
|
||||
Ttl uint32 `json:"_ttl" binding:"required"`
|
||||
|
||||
// Comment is a string that helps user to distinguish the Service.
|
||||
Comment string `json:"_comment,omitempty"`
|
||||
|
|
@ -108,7 +109,11 @@ type ServiceMeta struct {
|
|||
Aliases []string `json:"_aliases,omitempty"`
|
||||
|
||||
// NbResources holds the number of Resources stored inside this Service.
|
||||
NbResources int `json:"_tmp_hint_nb"`
|
||||
NbResources int `json:"_tmp_hint_nb" binding:"required"`
|
||||
|
||||
// PropagatedAt is the estimated time at which the last published changes
|
||||
// for this service will be fully propagated (old cached records expired).
|
||||
PropagatedAt *time.Time `json:"_propagated_at,omitempty" format:"date-time"`
|
||||
}
|
||||
|
||||
// ServiceCombined combined ServiceMeta + Service
|
||||
|
|
|
|||
|
|
@ -53,13 +53,13 @@ type ServiceRestrictions struct {
|
|||
}
|
||||
|
||||
type ServiceInfos struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"_svctype"`
|
||||
Name string `json:"name" binding:"required"`
|
||||
Type string `json:"_svctype" binding:"required"`
|
||||
Icon string `json:"_svcicon,omitempty"`
|
||||
Description string `json:"description"`
|
||||
Family string `json:"family"`
|
||||
Categories []string `json:"categories"`
|
||||
RecordTypes []uint16 `json:"record_types"`
|
||||
Description string `json:"description" binding:"required"`
|
||||
Family string `json:"family" binding:"required"`
|
||||
Categories []string `json:"categories" binding:"required"`
|
||||
RecordTypes []uint16 `json:"record_types" binding:"required"`
|
||||
Tabs bool `json:"tabs,omitempty"`
|
||||
Restrictions ServiceRestrictions `json:"restrictions,omitempty"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,30 +25,39 @@ import (
|
|||
"time"
|
||||
)
|
||||
|
||||
// Session holds informatin about a User's currently connected.
|
||||
// Session holds information about a User's currently connected.
|
||||
type Session struct {
|
||||
// Id is the Session's identifier.
|
||||
Id string `json:"id"`
|
||||
Id string `json:"id" binding:"required" readonly:"true"`
|
||||
|
||||
// IdUser is the User's identifier of the Session.
|
||||
IdUser Identifier `json:"login" swaggertype:"string"`
|
||||
IdUser Identifier `json:"login" swaggertype:"string" binding:"required" readonly:"true"`
|
||||
|
||||
// Description is a user defined string aims to identify each session.
|
||||
Description string `json:"description"`
|
||||
Description string `json:"description" binding:"required"`
|
||||
|
||||
// IssuedAt holds the creation date of the Session.
|
||||
IssuedAt time.Time `json:"time"`
|
||||
IssuedAt time.Time `json:"time" binding:"required" format:"date-time" readonly:"true"`
|
||||
|
||||
// ExpiresOn holds the expirate date of the Session.
|
||||
ExpiresOn time.Time `json:"exp"`
|
||||
ExpiresOn time.Time `json:"exp" binding:"required" format:"date-time"`
|
||||
|
||||
// ModifiedOn is the last time the session has been updated.
|
||||
ModifiedOn time.Time `json:"upd"`
|
||||
ModifiedOn time.Time `json:"upd" binding:"required" format:"date-time"`
|
||||
|
||||
// Content stores data filled by other modules.
|
||||
Content string `json:"content,omitempty"`
|
||||
}
|
||||
|
||||
// SessionInput is used for creating or updating a session.
|
||||
type SessionInput struct {
|
||||
// Description is a user defined string aims to identify each session.
|
||||
Description string `json:"description"`
|
||||
|
||||
// ExpiresOn holds the expirate date of the Session.
|
||||
ExpiresOn time.Time `json:"exp" format:"date-time"`
|
||||
}
|
||||
|
||||
// ClearSession removes all content from the Session.
|
||||
func (s *Session) ClearSession() {
|
||||
s.Content = ""
|
||||
|
|
|
|||
|
|
@ -29,19 +29,19 @@ import (
|
|||
// User represents an account.
|
||||
type User struct {
|
||||
// Id is the User's identifier.
|
||||
Id Identifier `json:"id" swaggertype:"string"`
|
||||
Id Identifier `json:"id" swaggertype:"string" binding:"required" readonly:"true"`
|
||||
|
||||
// Email is the User's login and means of contact.
|
||||
Email string `json:"email"`
|
||||
Email string `json:"email" binding:"required"`
|
||||
|
||||
// CreatedAt is the time when the User logs in for the first time.
|
||||
CreatedAt time.Time `json:"created_at,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at" format:"date-time" binding:"required" readonly:"true"`
|
||||
|
||||
// LastSeen is the time when the User used happyDNS for the last time (in a 12h frame).
|
||||
LastSeen time.Time `json:"last_seen,omitempty"`
|
||||
LastSeen time.Time `json:"last_seen" format:"date-time" binding:"required" readonly:"true"`
|
||||
|
||||
// Settings holds the settings for an account.
|
||||
Settings UserSettings `json:"settings,omitempty"`
|
||||
Settings UserSettings `json:"settings" binding:"required"`
|
||||
}
|
||||
|
||||
func (u *User) GetUserId() Identifier {
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ import (
|
|||
type UserProfile struct {
|
||||
UserId []byte `json:"userid"`
|
||||
Name string `json:"username"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
CreatedAt time.Time `json:"created_at" format:"date-time"`
|
||||
}
|
||||
|
||||
func (u *UserProfile) GetUserId() Identifier {
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue