summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMike Crute <mike@crute.us>2022-01-30 12:24:26 -0800
committerMike Crute <mike@crute.us>2022-01-30 12:24:26 -0800
commit1c24090da6b95ea304677d36f7cb6458034c08b9 (patch)
treeb375186f692a1e850f2f35ad5a3295e90ab7906c
parent74c33667f4c317bed29a014306130b9dd8990538 (diff)
downloadgo_ddns_manager-1c24090da6b95ea304677d36f7cb6458034c08b9.tar.bz2
go_ddns_manager-1c24090da6b95ea304677d36f7cb6458034c08b9.tar.xz
go_ddns_manager-1c24090da6b95ea304677d36f7cb6458034c08b9.zip
Add ACMEv2 endpoints
The ACMEv2 endpoints are easier to use for clients running the Golang acme.autocert.Manager. They require no tracking of state and also handle DNS propagation checking so the client can remain simple. Once the legacy REST client for the v1 endpoints is gone those old endpoints can be removed.
-rw-r--r--dns/client.go70
-rw-r--r--main.go17
-rw-r--r--web/controllers/acmev2.go90
3 files changed, 175 insertions, 2 deletions
diff --git a/dns/client.go b/dns/client.go
index 525444a..f39bb4b 100644
--- a/dns/client.go
+++ b/dns/client.go
@@ -1,7 +1,9 @@
1package dns 1package dns
2 2
3import ( 3import (
4 "context"
4 "fmt" 5 "fmt"
6 "time"
5 7
6 "github.com/miekg/dns" 8 "github.com/miekg/dns"
7 9
@@ -9,7 +11,9 @@ import (
9) 11)
10 12
11type DNSClient struct { 13type DNSClient struct {
12 Server string 14 Server string
15 RecursiveResolvers []string
16 PollTimeout time.Duration
13} 17}
14 18
15type DNSTransaction struct { 19type DNSTransaction struct {
@@ -142,3 +146,67 @@ func (c *DNSClient) SendQuery(t *DNSTransaction) ([]dns.RR, error) {
142 146
143 return in.Answer, nil 147 return in.Answer, nil
144} 148}
149
150// TODO: Copied from the letsencrypt service, merge this into existing functions
151func (c *DNSClient) sendReadQuery(ctx context.Context, fqdn string, rtype uint16, nameserver string) (*dns.Msg, error) {
152 udp := &dns.Client{Net: "udp"}
153 tcp := &dns.Client{Net: "tcp"}
154
155 m := &dns.Msg{}
156 m.SetQuestion(fqdn, rtype)
157 m.SetEdns0(4096, false)
158 m.RecursionDesired = true
159
160 in, _, err := udp.ExchangeContext(ctx, m, nameserver)
161 if in != nil && in.Truncated {
162 // If the TCP request succeeds, the err will reset to nil
163 in, _, err = tcp.ExchangeContext(ctx, m, nameserver)
164 }
165
166 if err != nil {
167 return nil, err
168 }
169
170 return in, err
171}
172
173func (c *DNSClient) WaitForDNSPropagation(ctx context.Context, fqdn, value string) error {
174 if c.RecursiveResolvers == nil {
175 return fmt.Errorf("DNSClient.WaitForDNSPropagation: RecursiveResolvers not set")
176 }
177
178 pt := c.PollTimeout
179 if pt == 0 {
180 pt = 3 * time.Second
181 }
182
183 timer := time.NewTicker(pt)
184 defer timer.Stop()
185
186 for {
187 // Give the server the initial timout to satisfy the request
188 select {
189 case <-ctx.Done():
190 return fmt.Errorf("DNSClient.WaitForDNSPropagation: context has expired, polling terminated")
191 case <-timer.C:
192 }
193
194 ok_count := 0
195 for _, rs := range c.RecursiveResolvers {
196 r, err := c.sendReadQuery(ctx, fqdn, dns.TypeTXT, rs)
197 if err != nil {
198 return err
199 }
200
201 if len(r.Answer) > 0 {
202 if r.Answer[0].(*dns.TXT).Txt[0] == value {
203 ok_count++
204 }
205 }
206 }
207
208 if ok_count == len(c.RecursiveResolvers) {
209 return nil
210 }
211 }
212}
diff --git a/main.go b/main.go
index 351633b..8b6ba10 100644
--- a/main.go
+++ b/main.go
@@ -3,6 +3,7 @@ package main
3import ( 3import (
4 "encoding/json" 4 "encoding/json"
5 "io/ioutil" 5 "io/ioutil"
6 "time"
6 7
7 "github.com/gin-gonic/gin" 8 "github.com/gin-gonic/gin"
8 9
@@ -21,7 +22,14 @@ func loadConfig(app *frame.WebApp, args []string) (interface{}, error) {
21 view := app.GetStringArgument("view_name") 22 view := app.GetStringArgument("view_name")
22 23
23 scfg := &web.ServerConfig{ 24 scfg := &web.ServerConfig{
24 DNSClient: &dns.DNSClient{Server: server}, 25 DNSClient: &dns.DNSClient{
26 Server: server,
27 RecursiveResolvers: []string{
28 "google-public-dns-a.google.com:53",
29 "google-public-dns-b.google.com:53",
30 },
31 PollTimeout: 3 * time.Second,
32 },
25 AcmeView: view, 33 AcmeView: view,
26 DynamicDnsView: view, 34 DynamicDnsView: view,
27 } 35 }
@@ -64,6 +72,13 @@ func prepareServer(c interface{}, router *gin.Engine) error {
64 acme.DELETE("/:id", controllers.DeleteAcmeChallenge) 72 acme.DELETE("/:id", controllers.DeleteAcmeChallenge)
65 } 73 }
66 74
75 acme2 := router.Group("/acmev2")
76 acme2.Use(middleware.AcmeAuthMiddleware)
77 {
78 acme2.POST("/:domain/:challenge", controllers.CreateAcmeChallengeV2)
79 acme2.DELETE("/:domain/:challenge", controllers.DeleteAcmeChallengeV2)
80 }
81
67 manage := router.Group("/manage") 82 manage := router.Group("/manage")
68 manage.Use(middleware.ApiAuthMiddleware) 83 manage.Use(middleware.ApiAuthMiddleware)
69 { 84 {
diff --git a/web/controllers/acmev2.go b/web/controllers/acmev2.go
new file mode 100644
index 0000000..a2fadf5
--- /dev/null
+++ b/web/controllers/acmev2.go
@@ -0,0 +1,90 @@
1package controllers
2
3import (
4 "fmt"
5 "log"
6 "net/http"
7
8 "code.crute.me/mcrute/go_ddns_manager/dns"
9 "code.crute.me/mcrute/go_ddns_manager/web/middleware"
10 "github.com/gin-gonic/gin"
11)
12
13func CreateAcmeChallengeV2(c *gin.Context) {
14 cfg := middleware.GetServerConfig(c)
15
16 var ch AcmeChallenge
17 if err := c.ShouldBindJSON(&ch); err != nil {
18 jsonError(c, http.StatusBadRequest, err)
19 return
20 }
21
22 zone, prefix := cfg.BindConfig.FindClosestZone(ch.Zone, cfg.AcmeView)
23 if zone == nil {
24 jsonError(c, http.StatusNotFound, "Zone not found")
25 return
26 }
27
28 if !cfg.IsAcmeClientAllowed(middleware.GetAcmeAuthContext(c), zone.Name) {
29 jsonError(c, http.StatusForbidden, "Zone update not allowed")
30 return
31 }
32
33 txn := cfg.DNSClient.StartUpdate(zone).Upsert(&dns.TXT{
34 Name: joinDomainParts("_acme-challenge", prefix),
35 Ttl: 5,
36 Txt: []string{ch.Challenge},
37 })
38
39 if err := cfg.DNSClient.SendUpdate(txn); err != nil {
40 log.Printf("error Insert: %s", err)
41 jsonError(c, http.StatusInternalServerError, err)
42 return
43 }
44
45 if err := cfg.DNSClient.WaitForDNSPropagation(
46 c.Request.Context(),
47 fmt.Sprintf("_acme-challenge.%s.", prefix),
48 ch.Challenge,
49 ); err != nil {
50 jsonError(c, http.StatusInternalServerError, fmt.Errorf("Error polling for DNS propagation: %w", err))
51 return
52 }
53
54 c.JSON(http.StatusCreated, "")
55}
56
57func DeleteAcmeChallengeV2(c *gin.Context) {
58 cfg := middleware.GetServerConfig(c)
59
60 var ch AcmeChallenge
61 if err := c.ShouldBindJSON(&ch); err != nil {
62 jsonError(c, http.StatusBadRequest, err)
63 return
64 }
65
66 zone, prefix := cfg.BindConfig.FindClosestZone(ch.Zone, cfg.AcmeView)
67 if zone == nil {
68 jsonError(c, http.StatusNotFound, "Zone not found")
69 return
70 }
71
72 if !cfg.IsAcmeClientAllowed(middleware.GetAcmeAuthContext(c), zone.Name) {
73 jsonError(c, http.StatusForbidden, "Zone update not allowed")
74 return
75 }
76
77 txn := cfg.DNSClient.StartUpdate(zone).Remove(&dns.TXT{
78 Name: joinDomainParts("_acme-challenge", prefix),
79 Ttl: 5,
80 Txt: []string{ch.Challenge},
81 })
82
83 if err := cfg.DNSClient.SendUpdate(txn); err != nil {
84 log.Printf("error Remove: %s", err)
85 jsonError(c, http.StatusInternalServerError, err)
86 return
87 }
88
89 c.String(http.StatusNoContent, "")
90}