diff options
Diffstat (limited to 'web/controllers/acme.go')
-rw-r--r-- | web/controllers/acme.go | 149 |
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 @@ | |||
1 | package controllers | ||
2 | |||
3 | import ( | ||
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 | |||
17 | type AcmeChallenge struct { | ||
18 | Zone string `json:"zone" binding:"required"` | ||
19 | Challenge string `json:"challenge" binding:"required"` | ||
20 | } | ||
21 | |||
22 | type AcmeChallengeID struct { | ||
23 | Zone string | ||
24 | Prefix string | ||
25 | Challenge string | ||
26 | } | ||
27 | |||
28 | func 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 | |||
42 | func (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 | |||
51 | func 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 | |||
61 | func 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 | |||
74 | func 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 | |||
116 | func 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 | } | ||