package controllers import ( "encoding/base64" "encoding/json" "log" "net/http" "strings" "github.com/gin-gonic/gin" "code.crute.me/mcrute/frame" "code.crute.me/mcrute/go_ddns_manager/dns" "code.crute.me/mcrute/go_ddns_manager/web/middleware" ) type AcmeChallenge struct { Zone string `json:"zone" binding:"required"` Challenge string `json:"challenge" binding:"required"` } type AcmeChallengeID struct { Zone string Prefix string Challenge string } func DecodeAcmeChallengeID(v string) *AcmeChallengeID { rid, err := base64.URLEncoding.DecodeString(v) if err != nil { return nil } var id AcmeChallengeID if err = json.Unmarshal(rid, &id); err != nil { return nil } return &id } func (i AcmeChallengeID) Encode() string { id, err := json.Marshal(i) if err != nil { return "" } return base64.URLEncoding.EncodeToString(id) } func joinDomainParts(parts ...string) string { p := []string{} for _, i := range parts { if strings.TrimSpace(i) != "" { p = append(p, i) } } return strings.Join(p, ".") } func jsonError(c *gin.Context, status int, msg interface{}) { var o string switch msg := msg.(type) { case string: o = msg case error: o = msg.Error() } c.JSON(status, gin.H{"error": o}) } func CreateAcmeChallenge(c *gin.Context) { cfg := middleware.GetServerConfig(c) var ch AcmeChallenge if err := c.ShouldBindJSON(&ch); err != nil { jsonError(c, http.StatusBadRequest, err) return } zone, prefix := cfg.BindConfig.FindClosestZone(ch.Zone, cfg.AcmeView) if zone == nil { jsonError(c, http.StatusNotFound, "Zone not found") return } if !cfg.IsAcmeClientAllowed(middleware.GetAcmeAuthContext(c), zone.Name) { jsonError(c, http.StatusForbidden, "Zone update not allowed") return } txn := cfg.DNSClient.StartUpdate(zone).Upsert(&dns.TXT{ Name: joinDomainParts("_acme-challenge", prefix), Ttl: 5, Txt: []string{ch.Challenge}, }) if err := cfg.DNSClient.SendUpdate(txn); err != nil { log.Printf("error Insert: %s", err) jsonError(c, http.StatusInternalServerError, err) return } url := frame.MakeURL(c.Request, "/acme/%s", AcmeChallengeID{ Zone: zone.Name, Prefix: prefix, Challenge: ch.Challenge, }.Encode()).String() c.Writer.Header().Set("Location", url) c.JSON(http.StatusCreated, gin.H{"created": url}) } func DeleteAcmeChallenge(c *gin.Context) { cfg := middleware.GetServerConfig(c) id := DecodeAcmeChallengeID(c.Param("id")) if id == nil { jsonError(c, http.StatusBadRequest, "unable to decode ID") return } zone := cfg.BindConfig.Zone(cfg.AcmeView, id.Zone) if zone == nil { jsonError(c, http.StatusNotFound, "Zone not found") return } if !cfg.IsAcmeClientAllowed(middleware.GetAcmeAuthContext(c), zone.Name) { jsonError(c, http.StatusForbidden, "Zone update not allowed") return } txn := cfg.DNSClient.StartUpdate(zone).Remove(&dns.TXT{ Name: joinDomainParts("_acme-challenge", id.Prefix), Ttl: 5, Txt: []string{id.Challenge}, }) if err := cfg.DNSClient.SendUpdate(txn); err != nil { log.Printf("error Remove: %s", err) jsonError(c, http.StatusInternalServerError, err) return } c.String(http.StatusNoContent, "") }