summaryrefslogtreecommitdiff
path: root/web/controllers/acme.go
diff options
context:
space:
mode:
Diffstat (limited to 'web/controllers/acme.go')
-rw-r--r--web/controllers/acme.go149
1 files changed, 149 insertions, 0 deletions
diff --git a/web/controllers/acme.go b/web/controllers/acme.go
new file mode 100644
index 0000000..f40b2ec
--- /dev/null
+++ b/web/controllers/acme.go
@@ -0,0 +1,149 @@
1package controllers
2
3import (
4 "encoding/base64"
5 "encoding/json"
6 "log"
7 "net/http"
8 "strings"
9
10 "github.com/gin-gonic/gin"
11
12 "code.crute.me/mcrute/go_ddns_manager/dns"
13 "code.crute.me/mcrute/go_ddns_manager/web"
14 "code.crute.me/mcrute/go_ddns_manager/web/middleware"
15)
16
17type AcmeChallenge struct {
18 Zone string `json:"zone" binding:"required"`
19 Challenge string `json:"challenge" binding:"required"`
20}
21
22type AcmeChallengeID struct {
23 Zone string
24 Prefix string
25 Challenge string
26}
27
28func DecodeAcmeChallengeID(v string) *AcmeChallengeID {
29 rid, err := base64.URLEncoding.DecodeString(v)
30 if err != nil {
31 return nil
32 }
33
34 var id AcmeChallengeID
35 if err = json.Unmarshal(rid, &id); err != nil {
36 return nil
37 }
38
39 return &id
40}
41
42func (i AcmeChallengeID) Encode() string {
43 id, err := json.Marshal(i)
44 if err != nil {
45 return ""
46 }
47
48 return base64.URLEncoding.EncodeToString(id)
49}
50
51func joinDomainParts(parts ...string) string {
52 p := []string{}
53 for _, i := range parts {
54 if strings.TrimSpace(i) != "" {
55 p = append(p, i)
56 }
57 }
58 return strings.Join(p, ".")
59}
60
61func jsonError(c *gin.Context, status int, msg interface{}) {
62 var o string
63
64 switch msg := msg.(type) {
65 case string:
66 o = msg
67 case error:
68 o = msg.Error()
69 }
70
71 c.JSON(status, gin.H{"error": o})
72}
73
74func CreateAcmeChallenge(c *gin.Context) {
75 cfg := middleware.GetServerConfig(c)
76
77 var ch AcmeChallenge
78 if err := c.ShouldBindJSON(&ch); err != nil {
79 jsonError(c, http.StatusBadRequest, err)
80 return
81 }
82
83 zone, prefix := cfg.BindConfig.FindClosestZone(ch.Zone, cfg.AcmeView)
84 if zone == nil {
85 jsonError(c, http.StatusNotFound, "Zone not found")
86 return
87 }
88
89 if !cfg.IsAcmeClientAllowed(middleware.GetAcmeAuthContext(c), zone.Name) {
90 jsonError(c, http.StatusForbidden, "Zone update not allowed")
91 return
92 }
93
94 txn := cfg.DNSClient.StartUpdate(zone).Upsert(&dns.TXT{
95 Name: joinDomainParts("_acme-challenge", prefix),
96 Ttl: 5,
97 Txt: []string{ch.Challenge},
98 })
99
100 if err := cfg.DNSClient.SendUpdate(txn); err != nil {
101 log.Printf("error Insert: %s", err)
102 jsonError(c, http.StatusInternalServerError, err)
103 return
104 }
105
106 url := web.MakeURL(c.Request, "/acme/%s", AcmeChallengeID{
107 Zone: zone.Name,
108 Prefix: prefix,
109 Challenge: ch.Challenge,
110 }.Encode()).String()
111
112 c.Writer.Header().Set("Location", url)
113 c.JSON(http.StatusCreated, gin.H{"created": url})
114}
115
116func DeleteAcmeChallenge(c *gin.Context) {
117 cfg := middleware.GetServerConfig(c)
118
119 id := DecodeAcmeChallengeID(c.Param("id"))
120 if id == nil {
121 jsonError(c, http.StatusBadRequest, "unable to decode ID")
122 return
123 }
124
125 zone := cfg.BindConfig.Zone(cfg.AcmeView, id.Zone)
126 if zone == nil {
127 jsonError(c, http.StatusNotFound, "Zone not found")
128 return
129 }
130
131 if !cfg.IsAcmeClientAllowed(middleware.GetAcmeAuthContext(c), zone.Name) {
132 jsonError(c, http.StatusForbidden, "Zone update not allowed")
133 return
134 }
135
136 txn := cfg.DNSClient.StartUpdate(zone).Remove(&dns.TXT{
137 Name: joinDomainParts("_acme-challenge", id.Prefix),
138 Ttl: 5,
139 Txt: []string{id.Challenge},
140 })
141
142 if err := cfg.DNSClient.SendUpdate(txn); err != nil {
143 log.Printf("error Remove: %s", err)
144 jsonError(c, http.StatusInternalServerError, err)
145 return
146 }
147
148 c.String(http.StatusNoContent, "")
149}