diff options
-rw-r--r-- | dns/client.go | 70 | ||||
-rw-r--r-- | main.go | 17 | ||||
-rw-r--r-- | web/controllers/acmev2.go | 90 |
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 @@ | |||
1 | package dns | 1 | package dns |
2 | 2 | ||
3 | import ( | 3 | import ( |
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 | ||
11 | type DNSClient struct { | 13 | type DNSClient struct { |
12 | Server string | 14 | Server string |
15 | RecursiveResolvers []string | ||
16 | PollTimeout time.Duration | ||
13 | } | 17 | } |
14 | 18 | ||
15 | type DNSTransaction struct { | 19 | type 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 | ||
151 | func (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 | |||
173 | func (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 | } | ||
@@ -3,6 +3,7 @@ package main | |||
3 | import ( | 3 | import ( |
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 @@ | |||
1 | package controllers | ||
2 | |||
3 | import ( | ||
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 | |||
13 | func 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 | |||
57 | func 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 | } | ||