Handle zone diff and update from abstract page

This commit is contained in:
nemunaire 2020-06-25 23:47:50 +02:00
parent 7ff145f26f
commit 464ec8601c
15 changed files with 629 additions and 38 deletions

View File

@ -124,7 +124,7 @@ func addDomain(_ *config.Options, u *happydns.User, p httprouter.Params, body io
}
}
func delDomain(_ *config.Options, domain *happydns.Domain, body io.Reader) Response {
func delDomain(_ *config.Options, domain *happydns.Domain, _ httprouter.Params, body io.Reader) Response {
if err := storage.MainStore.DeleteDomain(domain); err != nil {
return APIErrorResponse{
err: err,
@ -136,7 +136,7 @@ func delDomain(_ *config.Options, domain *happydns.Domain, body io.Reader) Respo
}
}
func domainHandler(f func(*config.Options, *happydns.Domain, io.Reader) Response) func(*config.Options, *happydns.User, httprouter.Params, io.Reader) Response {
func domainHandler(f func(*config.Options, *happydns.Domain, httprouter.Params, io.Reader) Response) func(*config.Options, *happydns.User, httprouter.Params, io.Reader) Response {
return func(opts *config.Options, u *happydns.User, ps httprouter.Params, body io.Reader) Response {
if domain, err := storage.MainStore.GetDomainByDN(u, ps.ByName("domain")); err != nil {
return APIErrorResponse{
@ -144,7 +144,7 @@ func domainHandler(f func(*config.Options, *happydns.Domain, io.Reader) Response
err: errors.New("Domain not found"),
}
} else {
return f(opts, domain, body)
return f(opts, domain, ps, body)
}
}
}
@ -157,7 +157,7 @@ type apiDomain struct {
ZoneHistory []happydns.ZoneMeta `json:"zone_history"`
}
func getDomain(_ *config.Options, domain *happydns.Domain, body io.Reader) Response {
func getDomain(_ *config.Options, domain *happydns.Domain, _ httprouter.Params, body io.Reader) Response {
ret := &apiDomain{
Id: domain.Id,
IdUser: domain.IdUser,
@ -183,7 +183,7 @@ func getDomain(_ *config.Options, domain *happydns.Domain, body io.Reader) Respo
}
}
func axfrDomain(opts *config.Options, domain *happydns.Domain, body io.Reader) Response {
func axfrDomain(opts *config.Options, domain *happydns.Domain, _ httprouter.Params, body io.Reader) Response {
source, err := storage.MainStore.GetSource(&happydns.User{Id: domain.IdUser}, domain.IdSource)
if err != nil {
return APIErrorResponse{
@ -215,7 +215,7 @@ type uploadedRR struct {
RR string `json:"string"`
}
func addRR(opts *config.Options, domain *happydns.Domain, body io.Reader) Response {
func addRR(opts *config.Options, domain *happydns.Domain, _ httprouter.Params, body io.Reader) Response {
var urr uploadedRR
err := json.NewDecoder(body).Decode(&urr)
if err != nil {
@ -253,7 +253,7 @@ func addRR(opts *config.Options, domain *happydns.Domain, body io.Reader) Respon
}
}
func delRR(opts *config.Options, domain *happydns.Domain, body io.Reader) Response {
func delRR(opts *config.Options, domain *happydns.Domain, _ httprouter.Params, body io.Reader) Response {
var urr uploadedRR
err := json.NewDecoder(body).Decode(&urr)
if err != nil {

View File

@ -61,7 +61,7 @@ func listServices(_ *config.Options, _ httprouter.Params, _ io.Reader) Response
}
}
func analyzeDomain(opts *config.Options, domain *happydns.Domain, body io.Reader) Response {
func analyzeDomain(opts *config.Options, domain *happydns.Domain, _ httprouter.Params, body io.Reader) Response {
source, err := storage.MainStore.GetSource(&happydns.User{Id: domain.IdUser}, domain.IdSource)
if err != nil {
return APIErrorResponse{

View File

@ -44,22 +44,28 @@ import (
"time"
"github.com/julienschmidt/httprouter"
"github.com/miekg/dns"
"git.happydns.org/happydns/config"
"git.happydns.org/happydns/model"
"git.happydns.org/happydns/services"
"git.happydns.org/happydns/sources"
"git.happydns.org/happydns/storage"
)
func init() {
router.GET("/api/domains/:domain/zone/:zoneid", apiAuthHandler(zoneHandler(getZone)))
router.PATCH("/api/domains/:domain/zone/:zoneid", apiAuthHandler(zoneHandler(updateZoneService)))
router.GET("/api/domains/:domain/zone/:zoneid/:subdomain", apiAuthHandler(zoneHandler(getZoneSubdomain)))
router.POST("/api/domains/:domain/zone/:zoneid/:subdomain", apiAuthHandler(zoneHandler(addZoneService)))
router.GET("/api/domains/:domain/zone/:zoneid/:subdomain/*serviceid", apiAuthHandler(zoneHandler(getZoneService)))
router.DELETE("/api/domains/:domain/zone/:zoneid/:subdomain/*serviceid", apiAuthHandler(zoneHandler(deleteZoneService)))
router.POST("/api/domains/:domain/import_zone", apiAuthHandler(domainHandler(importZone)))
router.POST("/api/domains/:domain/view_zone/:zoneid", apiAuthHandler(zoneHandler(viewZone)))
router.POST("/api/domains/:domain/apply_zone/:zoneid", apiAuthHandler(zoneHandler(applyZone)))
router.POST("/api/domains/:domain/diff_zones/:zoneid1/:zoneid2", apiAuthHandler(domainHandler(diffZones)))
}
func zoneHandler(f func(*config.Options, *happydns.Domain, *happydns.Zone, httprouter.Params, io.Reader) Response) func(*config.Options, *happydns.User, httprouter.Params, io.Reader) Response {
@ -71,16 +77,9 @@ func zoneHandler(f func(*config.Options, *happydns.Domain, *happydns.Zone, httpr
}
}
return domainHandler(func(opts *config.Options, domain *happydns.Domain, body io.Reader) Response {
return domainHandler(func(opts *config.Options, domain *happydns.Domain, ps httprouter.Params, body io.Reader) Response {
// Check that the zoneid exists in the domain history
found := false
for _, v := range domain.ZoneHistory {
if v == zoneid {
found = true
break
}
}
if !found {
if !domain.HasZone(zoneid) {
return APIErrorResponse{
status: http.StatusNotFound,
err: errors.New("Zone not found"),
@ -173,7 +172,7 @@ func getZoneService(opts *config.Options, domain *happydns.Domain, zone *happydn
}
}
func importZone(opts *config.Options, domain *happydns.Domain, body io.Reader) Response {
func importZone(opts *config.Options, domain *happydns.Domain, _ httprouter.Params, body io.Reader) Response {
source, err := storage.MainStore.GetSource(&happydns.User{Id: domain.IdUser}, domain.IdSource)
if err != nil {
return APIErrorResponse{
@ -226,6 +225,180 @@ func importZone(opts *config.Options, domain *happydns.Domain, body io.Reader) R
}
}
func diffZones(opts *config.Options, domain *happydns.Domain, ps httprouter.Params, body io.Reader) Response {
zoneid1, err := strconv.ParseInt(ps.ByName("zoneid1"), 10, 64)
if err != nil && ps.ByName("zoneid1") != "@" {
return APIErrorResponse{
err: err,
}
}
zoneid2, err := strconv.ParseInt(ps.ByName("zoneid2"), 10, 64)
if err != nil && ps.ByName("zoneid2") != "@" {
return APIErrorResponse{
err: err,
}
}
if zoneid1 == 0 && zoneid2 == 0 {
return APIErrorResponse{
err: fmt.Errorf("Both zoneId can't reference the live version"),
}
}
var zone1 []dns.RR
var zone2 []dns.RR
if zoneid1 == 0 || zoneid2 == 0 {
source, err := storage.MainStore.GetSource(&happydns.User{Id: domain.IdUser}, domain.IdSource)
if err != nil {
return APIErrorResponse{
err: err,
}
}
if zoneid1 == 0 {
zone1, err = source.ImportZone(domain)
}
if zoneid2 == 0 {
zone2, err = source.ImportZone(domain)
}
if err != nil {
return APIErrorResponse{
err: err,
}
}
}
if zoneid1 != 0 {
if !domain.HasZone(zoneid1) {
return APIErrorResponse{
status: http.StatusNotFound,
err: errors.New("Zone A not found"),
}
} else if z1, err := storage.MainStore.GetZone(zoneid1); err != nil {
return APIErrorResponse{
status: http.StatusNotFound,
err: errors.New("Zone A not found"),
}
} else {
zone1 = z1.GenerateRRs(domain.DomainName)
}
}
if zoneid2 != 0 {
if !domain.HasZone(zoneid2) {
return APIErrorResponse{
status: http.StatusNotFound,
err: errors.New("Zone B not found"),
}
} else if z2, err := storage.MainStore.GetZone(zoneid2); err != nil {
return APIErrorResponse{
status: http.StatusNotFound,
err: errors.New("Zone B not found"),
}
} else {
zone2 = z2.GenerateRRs(domain.DomainName)
}
}
toAdd, toDel := sources.DiffZones(zone1, zone2)
var rrAdd []string
for _, rr := range toAdd {
rrAdd = append(rrAdd, rr.String())
}
var rrDel []string
for _, rr := range toDel {
rrDel = append(rrDel, rr.String())
}
return APIResponse{
response: map[string]interface{}{
"toAdd": rrAdd,
"toDel": rrDel,
},
}
}
func applyZone(opts *config.Options, domain *happydns.Domain, zone *happydns.Zone, _ httprouter.Params, body io.Reader) Response {
source, err := storage.MainStore.GetSource(&happydns.User{Id: domain.IdUser}, domain.IdSource)
if err != nil {
return APIErrorResponse{
err: err,
}
}
newSOA, err := sources.ApplyZone(source, domain, zone.GenerateRRs(domain.DomainName))
if err != nil {
return APIErrorResponse{
err: err,
}
}
// Update serial
if newSOA != nil {
for _, svc := range zone.Services[""] {
if origin, ok := svc.Service.(*svcs.Origin); ok {
origin.Serial = newSOA.Serial
break
}
}
}
// 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 {
return APIErrorResponse{
err: err,
}
}
domain.ZoneHistory = append(
[]int64{newZone.Id}, domain.ZoneHistory...)
err = storage.MainStore.UpdateDomain(domain)
if err != nil {
return APIErrorResponse{
err: err,
}
}
// 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 {
return APIErrorResponse{
err: err,
}
}
return APIResponse{
response: newZone.ZoneMeta,
}
}
func viewZone(opts *config.Options, domain *happydns.Domain, zone *happydns.Zone, _ httprouter.Params, body io.Reader) Response {
var ret string
for _, rr := range zone.GenerateRRs(domain.DomainName) {
ret += rr.String() + "\n"
}
return APIResponse{
response: ret,
}
}
func updateZoneService(opts *config.Options, domain *happydns.Domain, zone *happydns.Zone, _ httprouter.Params, body io.Reader) Response {
usc := &happydns.ServiceCombined{}
err := json.NewDecoder(body).Decode(&usc)

View File

@ -35,7 +35,7 @@
<div v-if="!isLoading" class="pt-3">
<h-subdomain-item v-for="(dn, index) in sortedDomains" :key="index" :dn="dn" :origin="domain.domain" :services="services" :zone-services="myServices.services[dn]===undefined?[]:myServices.services[dn]" :aliases="aliases[dn]===undefined?[]:aliases[dn]" :zone-meta="zoneMeta" @updateMyServices="updateMyServices($event)" @addSubdomain="addSubdomain()" @addNewAlias="addNewAlias($event)" @addNewService="addNewService($event)" />
<b-modal id="modal-addSvc" :size="modal && modal.step === 2 ? 'lg' : ''" @ok="handleModalSvcOk">
<b-modal id="modal-addSvc" :size="modal && modal.step === 2 ? 'lg' : ''" scrollable @ok="handleModalSvcOk">
<template v-slot:modal-title>
Add a new service to <span class="text-monospace">{{ modal.dn | fqdn(domain.domain) }}</span>
</template>
@ -46,8 +46,12 @@
<b-input v-model="modal.dn" autofocus />
</b-input-group>
</p>
<p v-else-if="modal.step === 1">Select a new service to add to <span class="text-monospace">{{ modal.dn | fqdn(domain.domain) }}</span>:</p>
<p v-else-if="modal.step === 2">Fill the information for the {{ services[modal.svcSelected].name }} at <span class="text-monospace">{{ modal.dn | fqdn(domain.domain) }}</span>:</p>
<p v-else-if="modal.step === 1">
Select a new service to add to <span class="text-monospace">{{ modal.dn | fqdn(domain.domain) }}</span>:
</p>
<p v-else-if="modal.step === 2">
Fill the information for the {{ services[modal.svcSelected].name }} at <span class="text-monospace">{{ modal.dn | fqdn(domain.domain) }}</span>:
</p>
<b-list-group v-if="modal.step === 1">
<b-list-group-item v-for="(svc, idx) in services" :key="idx" :active="modal.svcSelected === idx" button @click="modal.svcSelected = idx">
{{ svc.name }}

View File

@ -37,6 +37,8 @@ import {
BadgePlugin,
BIcon,
BIconArrowRight,
BIconCloudDownload,
BIconCloudUpload,
BIconCheck,
BIconChevronDown,
BIconChevronLeft,
@ -45,6 +47,7 @@ import {
BIconLink,
BIconLink45deg,
BIconListTask,
BIconListUl,
BIconPencil,
BIconPerson,
BIconPersonCheck,
@ -105,6 +108,8 @@ Vue.use(ToastPlugin)
Vue.component('BIcon', BIcon)
Vue.component('BIconArrowRight', BIconArrowRight)
Vue.component('BIconCloudDownload', BIconCloudDownload)
Vue.component('BIconCloudUpload', BIconCloudUpload)
Vue.component('BIconCheck', BIconCheck)
Vue.component('BIconChevronDown', BIconChevronDown)
Vue.component('BIconChevronLeft', BIconChevronLeft)
@ -113,6 +118,7 @@ Vue.component('BIconChevronUp', BIconChevronUp)
Vue.component('BIconLink', BIconLink)
Vue.component('BIconLink45deg', BIconLink45deg)
Vue.component('BIconListTask', BIconListTask)
Vue.component('BIconListUl', BIconListUl)
Vue.component('BIconPencil', BIconPencil)
Vue.component('BIconPerson', BIconPerson)
Vue.component('BIconPersonCheck', BIconPersonCheck)

View File

@ -36,6 +36,18 @@ export default {
return Api().get('/api/domains/' + encodeURIComponent(domain) + '/zone/' + encodeURIComponent(id))
},
applyZone (domain, id) {
return Api().post('/api/domains/' + encodeURIComponent(domain) + '/apply_zone/' + encodeURIComponent(id))
},
diffZone (domain, id1, id2) {
return Api().post('/api/domains/' + encodeURIComponent(domain) + '/diff_zones/' + encodeURIComponent(id1) + '/' + encodeURIComponent(id2))
},
viewZone (domain, id) {
return Api().post('/api/domains/' + encodeURIComponent(domain) + '/view_zone/' + encodeURIComponent(id))
},
addZoneService (domain, id, subdomain, service) {
if (subdomain === '') {
subdomain = '@'

View File

@ -37,12 +37,36 @@
<b-spinner label="Spinning" />
<p>Please wait while we are importing your domain&nbsp;&hellip;</p>
</div>
<h-subdomain-list v-if="!importInProgress && selectedHistory" :domain="domain" :zone-meta="selectedHistory" />
<div v-else-if="selectedHistory">
<div class="mt-2 text-right">
<b-button size="sm" class="mx-1" @click="importZone()"><b-icon icon="cloud-download" aria-hidden="true" /> Re-import</b-button>
<b-button size="sm" class="mx-1" @click="viewZone()"><b-icon icon="list-ul" aria-hidden="true" /> View</b-button>
<b-button size="sm" variant="success" class="mx-1" @click="showDiff()"><b-icon icon="cloud-upload" aria-hidden="true" /> Apply</b-button>
</div>
<h-subdomain-list :domain="domain" :zone-meta="selectedHistory" />
</div>
<b-modal id="modal-viewZone" title="View zone" size="lg" scrollable ok-only>
<pre style="overflow: initial">{{ zoneContent }}</pre>
</b-modal>
<b-modal id="modal-applyZone" size="lg" scrollable @ok="applyDiff()">
<template v-slot:modal-title>
Review the modifications that will be applied to <span class="text-monospace">{{ domain.domain }}</span>
</template>
<div v-for="(line, n) in zoneDiffAdd" :key="'a' + n" class="text-monospace text-success" style="white-space: nowrap">
+{{ line }}
</div>
<div v-for="(line, n) in zoneDiffDel" :key="'d' + n" class="text-monospace text-danger" style="white-space: nowrap">
-{{ line }}
</div>
</b-modal>
</div>
</template>
<script>
import axios from 'axios'
import ZoneApi from '@/services/ZoneApi'
export default {
components: {
@ -59,7 +83,10 @@ export default {
data: function () {
return {
importInProgress: false,
selectedHistory: null
selectedHistory: null,
zoneContent: null,
zoneDiffAdd: null,
zoneDiffDel: null
}
},
@ -78,19 +105,91 @@ export default {
methods: {
pullDomain () {
if (this.domain.zone_history === null || this.domain.zone_history.length === 0) {
this.importInProgress = true
axios
.post('/api/domains/' + encodeURIComponent(this.domain.domain) + '/import_zone')
.then(
(response) => {
this.importInProgress = false
this.selectedHistory = response.data
this.$parent.$emit('updateDomainInfo')
}
)
this.importZone()
} else {
this.selectedHistory = this.domain.zone_history[0]
}
},
importZone () {
this.importInProgress = true
axios
.post('/api/domains/' + encodeURIComponent(this.domain.domain) + '/import_zone')
.then(
(response) => {
this.importInProgress = false
this.selectedHistory = response.data
this.$parent.$emit('updateDomainInfo')
}
)
},
showDiff () {
ZoneApi.diffZone(this.domain.domain, '@', this.selectedHistory.id)
.then(
(response) => {
if (response.data.toAdd == null && response.data.toDel == null) {
this.$bvModal.msgBoxOk('There is no changes to apply! Current zone is in sync with the server.')
} else {
this.zoneDiffAdd = response.data.toAdd
this.zoneDiffDel = response.data.toDel
this.$bvModal.show('modal-applyZone')
}
},
(error) => {
this.$bvToast.toast(
error.response.data.errmsg, {
title: 'An error occurs when applying the zone!',
autoHideDelay: 5000,
variant: 'danger',
toaster: 'b-toaster-content-right'
}
)
})
},
applyDiff () {
ZoneApi.applyZone(this.domain.domain, this.selectedHistory.id)
.then(
(response) => {
this.$bvToast.toast(
'!', {
title: 'Zone applied successfully!',
autoHideDelay: 5000,
variant: 'success',
toaster: 'b-toaster-content-right'
}
)
},
(error) => {
this.$bvToast.toast(
error.response.data.errmsg, {
title: 'An error occurs when applying the zone!',
autoHideDelay: 5000,
variant: 'danger',
toaster: 'b-toaster-content-right'
}
)
})
},
viewZone () {
ZoneApi.viewZone(this.domain.domain, this.selectedHistory.id)
.then(
(response) => {
this.zoneContent = response.data
this.$bvModal.show('modal-viewZone')
},
(error) => {
this.$bvToast.toast(
error.response.data.errmsg, {
title: 'An error occurs when applying the zone!',
autoHideDelay: 5000,
variant: 'danger',
toaster: 'b-toaster-content-right'
}
)
})
}
}
}

View File

@ -53,6 +53,15 @@ func (d *Domain) NormalizedNSServer() string {
}
}
func (d *Domain) HasZone(zoneId int64) (found bool) {
for _, v := range d.ZoneHistory {
if v == zoneId {
return true
}
}
return
}
func NewDomain(u *User, st *SourceMeta, dn string) (d *Domain) {
d = &Domain{
IdUser: u.Id,

View File

@ -41,6 +41,7 @@ type Source interface {
ImportZone(*Domain) ([]dns.RR, error)
AddRR(*Domain, dns.RR) error
DeleteRR(*Domain, dns.RR) error
UpdateSOA(*Domain, *dns.SOA, bool) error
}
type SourceMeta struct {

View File

@ -35,6 +35,8 @@ import (
"bytes"
"errors"
"time"
"github.com/miekg/dns"
)
type ZoneMeta struct {
@ -52,6 +54,21 @@ type Zone struct {
Services map[string][]*ServiceCombined `json:"services"`
}
func (z *Zone) DerivateNew() *Zone {
newZone := new(Zone)
newZone.ZoneMeta.IdAuthor = z.ZoneMeta.IdAuthor
newZone.ZoneMeta.DefaultTTL = z.ZoneMeta.DefaultTTL
newZone.ZoneMeta.LastModified = time.Now()
newZone.Services = map[string][]*ServiceCombined{}
for subdomain, svcs := range z.Services {
newZone.Services[subdomain] = svcs
}
return newZone
}
func (z *Zone) FindService(id []byte) (string, *ServiceCombined) {
for subdomain := range z.Services {
if svc := z.FindSubdomainService(subdomain, id); svc != nil {
@ -98,3 +115,24 @@ func (z *Zone) EraseService(subdomain string, origin string, id []byte, s *Servi
return errors.New("Service not found")
}
func (z *Zone) GenerateRRs(origin string) (rrs []dns.RR) {
for subdomain, svcs := range z.Services {
if subdomain == "" {
subdomain = origin
} else {
subdomain += "." + origin
}
for _, svc := range svcs {
var ttl uint32
if svc.Ttl == 0 {
ttl = z.DefaultTTL
} else {
ttl = svc.Ttl
}
rrs = append(rrs, svc.GenRRs(subdomain, ttl)...)
}
}
return
}

View File

@ -39,6 +39,7 @@ import (
"github.com/miekg/dns"
"git.happydns.org/happydns/model"
"git.happydns.org/happydns/utils"
)
type MX struct {
@ -196,7 +197,7 @@ func (s *EMail) GenRRs(domain string, ttl uint32) (rrs []dns.RR) {
Class: dns.ClassINET,
Ttl: ttl,
},
Txt: []string{"\"v=spf1\" " + s.SPF.String()},
Txt: utils.SplitN("v=spf1 "+s.SPF.String(), 255),
})
}
@ -208,7 +209,7 @@ func (s *EMail) GenRRs(domain string, ttl uint32) (rrs []dns.RR) {
Class: dns.ClassINET,
Ttl: ttl,
},
Txt: []string{d.String()},
Txt: utils.SplitN(d.String(), 255),
})
}
@ -220,7 +221,7 @@ func (s *EMail) GenRRs(domain string, ttl uint32) (rrs []dns.RR) {
Class: dns.ClassINET,
Ttl: ttl,
},
Txt: []string{s.DMARC.String()},
Txt: utils.SplitN(s.DMARC.String(), 255),
})
}
@ -232,7 +233,7 @@ func (s *EMail) GenRRs(domain string, ttl uint32) (rrs []dns.RR) {
Class: dns.ClassINET,
Ttl: ttl,
},
Txt: []string{s.MTA_STS.String()},
Txt: utils.SplitN(s.MTA_STS.String(), 255),
})
}
@ -244,7 +245,7 @@ func (s *EMail) GenRRs(domain string, ttl uint32) (rrs []dns.RR) {
Class: dns.ClassINET,
Ttl: ttl,
},
Txt: []string{s.TLS_RPT.String()},
Txt: utils.SplitN(s.TLS_RPT.String(), 255),
})
}
return

View File

@ -162,6 +162,20 @@ func (s *DDNSServer) DeleteRR(domain *happydns.Domain, rr dns.RR) error {
return err
}
func (s *DDNSServer) UpdateSOA(domain *happydns.Domain, newSOA *dns.SOA, refreshSerial bool) (err error) {
if refreshSerial {
now := time.Now()
todaySerial := uint32(now.Year()*1000000 + int(now.Month())*10000 + now.Day()*100)
if newSOA.Serial >= todaySerial {
newSOA.Serial += 1
} else {
newSOA.Serial = todaySerial
}
}
return s.AddRR(domain, newSOA)
}
func init() {
sources.RegisterSource("git.happydns.org/happydns/sources/ddns/DDNSServer", func() happydns.Source {
return &DDNSServer{}

111
sources/diff.go Normal file
View File

@ -0,0 +1,111 @@
// Copyright or © or Copr. happyDNS (2020)
//
// contact@happydns.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 sources // import "happydns.org/sources"
import (
"github.com/miekg/dns"
"git.happydns.org/happydns/model"
)
func DiffZones(a []dns.RR, b []dns.RR) (toAdd []dns.RR, toDel []dns.RR) {
loopDel:
for _, rrA := range a {
for _, rrB := range b {
if rrA.String() == rrB.String() {
continue loopDel
}
}
toDel = append(toDel, rrA)
}
loopAdd:
for _, rrB := range b {
for _, rrA := range a {
if rrB.String() == rrA.String() {
continue loopAdd
}
}
toAdd = append(toAdd, rrB)
}
return
}
func DiffZone(s happydns.Source, domain *happydns.Domain, rrs []dns.RR) (toAdd []dns.RR, toDel []dns.RR, err error) {
// Get the actuals RR-set
var current []dns.RR
current, err = s.ImportZone(domain)
if err != nil {
return
}
toAdd, toDel = DiffZones(current, rrs)
return
}
func ApplyZone(s happydns.Source, domain *happydns.Domain, rrs []dns.RR) (*dns.SOA, error) {
toAdd, toDel, err := DiffZone(s, domain, rrs)
if err != nil {
return nil, err
}
var newSOA *dns.SOA
// Apply diff
for _, rr := range toDel {
if rr.Header().Rrtype == dns.TypeSOA {
continue
}
if err := s.DeleteRR(domain, rr); err != nil {
return nil, err
}
}
for _, rr := range toAdd {
if rr.Header().Rrtype == dns.TypeSOA {
newSOA = rr.(*dns.SOA)
continue
}
if err := s.AddRR(domain, rr); err != nil {
return nil, err
}
}
// Update SOA record
if newSOA != nil {
err = s.UpdateSOA(domain, newSOA, false)
}
return newSOA, err
}

View File

@ -232,6 +232,72 @@ func (s *OVHAPI) DeleteRR(dn *happydns.Domain, rr dns.RR) (err error) {
return
}
type OVH_SOA struct {
Server string `json:"server"`
Email string `json:"email"`
Serial uint32 `json:"serial"`
Refresh uint32 `json:"refresh"`
Expire uint32 `json:"expire"`
NxTtl uint32 `json:"nxDomainTtl"`
Ttl uint32 `json:"ttl"`
}
func (s *OVHAPI) UpdateSOA(dn *happydns.Domain, newSOA *dns.SOA, refreshSerial bool) (err error) {
var client *ovh.Client
client, err = s.newClient()
if err != nil {
return
}
// Get current SOA
var curSOA OVH_SOA
err = client.Get(
fmt.Sprintf("/domain/zone/%s/soa", strings.TrimSuffix(dn.DomainName, ".")),
&curSOA)
if err != nil {
return
}
// Is there any change?
changes := false
if curSOA.Server != newSOA.Ns {
curSOA.Server = newSOA.Ns
changes = true
}
if curSOA.Email != newSOA.Mbox {
curSOA.Email = newSOA.Mbox
changes = true
}
if curSOA.Refresh != newSOA.Refresh {
curSOA.Refresh = newSOA.Refresh
changes = true
}
if curSOA.Expire != newSOA.Expire {
curSOA.Expire = newSOA.Expire
changes = true
}
if curSOA.NxTtl != newSOA.Minttl {
curSOA.NxTtl = newSOA.Minttl
changes = true
}
// OVH handles automatically serial update, so only force non-refresh
if !refreshSerial && curSOA.Serial != newSOA.Serial {
curSOA.Serial = newSOA.Serial
changes = true
}
newSOA.Serial = curSOA.Serial
if changes {
err = client.Post(fmt.Sprintf("/domain/zone/%s/refresh", strings.TrimSuffix(dn.DomainName, ".")), nil, nil)
if err != nil {
return
}
}
return
}
func init() {
sources.RegisterSource("git.happydns.org/happydns/sources/ovh/OVHAPI", func() happydns.Source {
return &OVHAPI{}

57
utils/dns.go Normal file
View File

@ -0,0 +1,57 @@
// Copyright or © or Copr. happyDNS (2020)
//
// contact@happydns.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 utils
import ()
// SplitN splits a string into N sized string chunks.
// This function is a copy of https://github.com/miekg/dns/blob/master/types.go#L1509
// awaiting its exportation
func SplitN(s string, n int) []string {
if len(s) < n {
return []string{s}
}
sx := []string{}
p, i := 0, n
for {
if i <= len(s) {
sx = append(sx, s[p:i])
} else {
sx = append(sx, s[p:])
break
}
p, i = p+n, i+n
}
return sx
}