happyDomain/api/zones.go

480 lines
15 KiB
Go

// Copyright or © or Copr. happyDNS (2020)
//
// contact@happydomain.org
//
// This software is a computer program whose purpose is to provide a modern
// interface to interact with DNS systems.
//
// This software is governed by the CeCILL license under French law and abiding
// by the rules of distribution of free software. You can use, modify and/or
// redistribute the software under the terms of the CeCILL license as
// circulated by CEA, CNRS and INRIA at the following URL
// "http://www.cecill.info".
//
// As a counterpart to the access to the source code and rights to copy, modify
// and redistribute granted by the license, users are provided only with a
// limited warranty and the software's author, the holder of the economic
// rights, and the successive licensors have only limited liability.
//
// In this respect, the user's attention is drawn to the risks associated with
// loading, using, modifying and/or developing or reproducing the software by
// the user in light of its specific status of free software, that may mean
// that it is complicated to manipulate, and that also therefore means that it
// is reserved for developers and experienced professionals having in-depth
// computer knowledge. Users are therefore encouraged to load and test the
// software's suitability as regards their requirements in conditions enabling
// the security of their systems and/or data to be ensured and, more generally,
// to use and operate it in the same conditions as regards security.
//
// The fact that you are presently reading this means that you have had
// knowledge of the CeCILL license and that you accept its terms.
package api
import (
"fmt"
"log"
"net/http"
"strings"
"time"
"github.com/StackExchange/dnscontrol/v3/models"
"github.com/gin-gonic/gin"
"github.com/miekg/dns"
"git.happydns.org/happydomain/config"
"git.happydns.org/happydomain/model"
"git.happydns.org/happydomain/services"
"git.happydns.org/happydomain/storage"
)
func declareZonesRoutes(cfg *config.Options, router *gin.RouterGroup) {
router.POST("/import_zone", importZone)
router.POST("/diff_zones/:zoneid1/:zoneid2", diffZones)
apiZonesRoutes := router.Group("/zone/:zoneid")
apiZonesRoutes.Use(ZoneHandler)
apiZonesRoutes.POST("/view", viewZone)
apiZonesRoutes.POST("/apply_changes", applyZone)
apiZonesRoutes.GET("", GetZone)
apiZonesRoutes.PATCH("", UpdateZoneService)
apiZonesSubdomainRoutes := apiZonesRoutes.Group("/:subdomain")
apiZonesSubdomainRoutes.Use(subdomainHandler)
apiZonesSubdomainRoutes.GET("", getZoneSubdomain)
apiZonesSubdomainRoutes.POST("/services", addZoneService)
declareServiceSettingsRoutes(cfg, apiZonesSubdomainRoutes)
apiZonesSubdomainServiceIdRoutes := apiZonesSubdomainRoutes.Group("/services/:serviceid")
apiZonesSubdomainServiceIdRoutes.Use(serviceIdHandler)
apiZonesSubdomainServiceIdRoutes.GET("", getZoneService)
apiZonesSubdomainServiceIdRoutes.DELETE("", deleteZoneService)
apiZonesSubdomainServiceIdRoutes.GET("/records", getServiceRecords)
}
func loadZoneFromId(domain *happydns.Domain, id string) (*happydns.Zone, int, error) {
zoneid, err := happydns.NewIdentifierFromString(id)
if err != nil {
return nil, http.StatusBadRequest, fmt.Errorf("Invalid zoneid: %q", id)
}
// Check that the zoneid exists in the domain history
if !domain.HasZone(zoneid) {
return nil, http.StatusNotFound, fmt.Errorf("Zone not found: %q", id)
}
zone, err := storage.MainStore.GetZone(zoneid)
if err != nil {
return nil, http.StatusNotFound, fmt.Errorf("Zone not found: %q", id)
}
return zone, http.StatusOK, nil
}
func ZoneHandler(c *gin.Context) {
domain := c.MustGet("domain").(*happydns.Domain)
zone, statuscode, err := loadZoneFromId(domain, c.Param("zoneid"))
if err != nil {
c.AbortWithStatusJSON(statuscode, gin.H{"errmsg": err.Error()})
return
}
c.Set("zone", zone)
c.Next()
}
func GetZone(c *gin.Context) {
zone := c.MustGet("zone").(*happydns.Zone)
c.JSON(http.StatusOK, zone)
}
func subdomainHandler(c *gin.Context) {
domain := c.MustGet("domain").(*happydns.Domain)
subdomain := strings.TrimSuffix(strings.TrimSuffix(strings.TrimSuffix(c.Param("subdomain"), "."+domain.DomainName), "@"), domain.DomainName)
c.Set("subdomain", subdomain)
c.Next()
}
func getZoneSubdomain(c *gin.Context) {
zone := c.MustGet("zone").(*happydns.Zone)
subdomain := c.MustGet("subdomain").(string)
c.JSON(http.StatusOK, gin.H{"services": zone.Services[subdomain]})
}
func addZoneService(c *gin.Context) {
domain := c.MustGet("domain").(*happydns.Domain)
zone := c.MustGet("zone").(*happydns.Zone)
subdomain := c.MustGet("subdomain").(string)
usc := &happydns.ServiceCombined{}
err := c.ShouldBindJSON(&usc)
if err != nil {
log.Printf("%s sends invalid service JSON: %s", c.ClientIP(), err.Error())
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": fmt.Sprintf("Something is wrong in received data: %s", err.Error())})
return
}
if usc.Service == nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Unable to parse the given service."})
return
}
err = zone.AppendService(subdomain, domain.DomainName, usc)
if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": fmt.Sprintf("Unable to add service: %s", err.Error())})
return
}
err = storage.MainStore.UpdateZone(zone)
if err != nil {
log.Printf("%s: Unable to UpdateZone in updateZoneService: %s", c.ClientIP(), err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Sorry, we are currently unable to update your zone. Please retry later."})
return
}
c.JSON(http.StatusOK, zone)
}
func serviceIdHandler(c *gin.Context) {
serviceid, err := happydns.NewIdentifierFromString(c.Param("serviceid"))
if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": fmt.Sprintf("Bad service identifier: %s", err.Error())})
return
}
c.Set("serviceid", serviceid)
c.Next()
}
func getZoneService(c *gin.Context) {
zone := c.MustGet("zone").(*happydns.Zone)
serviceid := c.MustGet("serviceid").([]byte)
subdomain := c.MustGet("subdomain").(string)
c.JSON(http.StatusOK, zone.FindSubdomainService(subdomain, serviceid))
}
func importZone(c *gin.Context) {
user := c.MustGet("LoggedUser").(*happydns.User)
domain := c.MustGet("domain").(*happydns.Domain)
provider, err := storage.MainStore.GetProvider(user, domain.IdProvider)
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": fmt.Sprintf("Unable to find your provider: %s", err.Error())})
return
}
zone, err := provider.ImportZone(domain)
if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
return
}
services, defaultTTL, err := svcs.AnalyzeZone(domain.DomainName, zone)
if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
return
}
myZone := &happydns.Zone{
ZoneMeta: happydns.ZoneMeta{
IdAuthor: domain.IdUser,
DefaultTTL: defaultTTL,
LastModified: time.Now(),
},
Services: services,
}
// Create history zone
err = storage.MainStore.CreateZone(myZone)
if err != nil {
log.Printf("%s: unable to CreateZone in importZone: %s\n", c.ClientIP(), err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Sorry, we are unable to create your zone."})
return
}
domain.ZoneHistory = append(
[]happydns.Identifier{myZone.Id}, domain.ZoneHistory...)
// Create wip zone
err = storage.MainStore.CreateZone(myZone)
if err != nil {
log.Printf("%s: unable to CreateZone2 in importZone: %s\n", c.ClientIP(), err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Sorry, we are unable to create your zone."})
return
}
domain.ZoneHistory = append(
[]happydns.Identifier{myZone.Id}, domain.ZoneHistory...)
err = storage.MainStore.UpdateDomain(domain)
if err != nil {
log.Printf("%s: unable to UpdateDomain in importZone: %s\n", c.ClientIP(), err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Sorry, we are unable to create your zone."})
return
}
c.JSON(http.StatusOK, &myZone.ZoneMeta)
}
func diffZones(c *gin.Context) {
user := c.MustGet("LoggedUser").(*happydns.User)
domain := c.MustGet("domain").(*happydns.Domain)
if c.Param("zoneid1") != "@" {
c.AbortWithStatusJSON(http.StatusNotImplemented, gin.H{"errmsg": "Diff between two zone is not implemented."})
return
}
provider, err := storage.MainStore.GetProvider(user, domain.IdProvider)
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, fmt.Errorf("Unable to find the given provider: %q for %q", domain.IdProvider, domain.DomainName))
return
}
zone, statuscode, err := loadZoneFromId(domain, c.Param("zoneid2"))
if err != nil {
c.AbortWithStatusJSON(statuscode, gin.H{"errmsg": err.Error()})
return
}
records, err := models.RRstoRCs(zone.GenerateRRs(domain.DomainName), strings.TrimSuffix(domain.DomainName, "."))
if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
return
}
dc := &models.DomainConfig{
Name: strings.TrimSuffix(domain.DomainName, "."),
Records: records,
}
corrections, err := provider.GetDomainCorrections(dc)
if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
return
}
var rrCorected []string
for _, c := range corrections {
rrCorected = append(rrCorected, c.Msg)
}
c.JSON(http.StatusOK, rrCorected)
}
func applyZone(c *gin.Context) {
user := c.MustGet("LoggedUser").(*happydns.User)
domain := c.MustGet("domain").(*happydns.Domain)
zone := c.MustGet("zone").(*happydns.Zone)
provider, err := storage.MainStore.GetProvider(user, domain.IdProvider)
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, fmt.Errorf("Unable to find the given provider: %q for %q", domain.IdProvider, domain.DomainName))
return
}
records, err := models.RRstoRCs(zone.GenerateRRs(domain.DomainName), strings.TrimSuffix(domain.DomainName, "."))
if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
return
}
dc := &models.DomainConfig{
Name: strings.TrimSuffix(domain.DomainName, "."),
Records: records,
}
var wantedCorrections []string
err = c.ShouldBindJSON(&wantedCorrections)
if err != nil {
log.Printf("%s sends invalid string array JSON: %s", c.ClientIP(), err.Error())
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": fmt.Sprintf("Something is wrong in received data: %s", err.Error())})
return
}
corrections, err := provider.GetDomainCorrections(dc)
for _, cr := range corrections {
for ic, wc := range wantedCorrections {
if wc == cr.Msg {
log.Printf("%s: apply correction: %s", domain.DomainName, cr.Msg)
err := cr.F()
if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": fmt.Sprintf("Unable to update the zone: %s", err.Error())})
return
}
wantedCorrections = append(wantedCorrections[:ic], wantedCorrections[ic+1:]...)
break
}
}
}
if len(wantedCorrections) > 0 {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": fmt.Sprintf("Unable to perform the following changes: %s", wantedCorrections)})
return
}
// Create a new zone in history for futher updates
newZone := zone.DerivateNew()
//newZone.IdAuthor = //TODO get current user id
err = storage.MainStore.CreateZone(newZone)
if err != nil {
log.Printf("%s was unable to CreateZone", c.ClientIP(), err)
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Sorry, we are unable to create the zone now."})
return
}
domain.ZoneHistory = append(
[]happydns.Identifier{newZone.Id}, domain.ZoneHistory...)
err = storage.MainStore.UpdateDomain(domain)
if err != nil {
log.Printf("%s was unable to UpdateDomain", c.ClientIP(), err)
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Sorry, we are unable to create the zone now."})
return
}
// Commit changes in previous zone
now := time.Now()
// zone.ZoneMeta.IdAuthor = // TODO get current user id
zone.ZoneMeta.Published = &now
zone.LastModified = time.Now()
err = storage.MainStore.UpdateZone(zone)
if err != nil {
log.Printf("%s was unable to UpdateZone", c.ClientIP(), err)
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Sorry, we are unable to create the zone now."})
return
}
c.JSON(http.StatusOK, newZone.ZoneMeta)
}
func viewZone(c *gin.Context) {
domain := c.MustGet("domain").(*happydns.Domain)
zone := c.MustGet("zone").(*happydns.Zone)
var ret string
for _, rr := range zone.GenerateRRs(domain.DomainName) {
ret += rr.String() + "\n"
}
c.JSON(http.StatusOK, ret)
}
func UpdateZoneService(c *gin.Context) {
domain := c.MustGet("domain").(*happydns.Domain)
zone := c.MustGet("zone").(*happydns.Zone)
usc := &happydns.ServiceCombined{}
err := c.ShouldBindJSON(&usc)
if err != nil {
log.Printf("%s sends invalid domain JSON: %s", c.ClientIP(), err.Error())
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": fmt.Sprintf("Something is wrong in received data: %s", err.Error())})
return
}
err = zone.EraseService(usc.Domain, domain.DomainName, usc.Id, usc)
if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": fmt.Sprintf("Unable to delete service: %s", err.Error())})
return
}
zone.LastModified = time.Now()
err = storage.MainStore.UpdateZone(zone)
if err != nil {
log.Printf("%s: Unable to UpdateZone in updateZoneService: %s", c.ClientIP(), err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Sorry, we are currently unable to update your zone. Please retry later."})
return
}
c.JSON(http.StatusOK, zone)
}
func deleteZoneService(c *gin.Context) {
domain := c.MustGet("domain").(*happydns.Domain)
zone := c.MustGet("zone").(*happydns.Zone)
serviceid := c.MustGet("serviceid").(happydns.Identifier)
subdomain := c.MustGet("subdomain").(string)
err := zone.EraseService(subdomain, domain.DomainName, serviceid, nil)
if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": fmt.Sprintf("Unable to delete service: %s", err.Error())})
return
}
zone.LastModified = time.Now()
err = storage.MainStore.UpdateZone(zone)
if err != nil {
log.Printf("%s: Unable to UpdateZone in deleteZoneService: %s", c.ClientIP(), err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Sorry, we are currently unable to update your zone. Please retry later."})
return
}
c.JSON(http.StatusOK, zone)
}
type serviceRecord struct {
String string `json:"string"`
Fields *dns.RR `json:"fields,omitempty"`
}
func getServiceRecords(c *gin.Context) {
domain := c.MustGet("domain").(*happydns.Domain)
zone := c.MustGet("zone").(*happydns.Zone)
serviceid := c.MustGet("serviceid").(happydns.Identifier)
subdomain := c.MustGet("subdomain").(string)
svc := zone.FindSubdomainService(subdomain, serviceid)
if svc == nil {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Service not found."})
return
}
var ret []serviceRecord
for _, rr := range svc.GenRRs(subdomain, 3600, domain.DomainName) {
ret = append(ret, serviceRecord{
String: rr.String(),
Fields: &rr,
})
}
c.JSON(http.StatusOK, ret)
}