summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-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}