Handle zone diff and update from abstract page
This commit is contained in:
parent
7ff145f26f
commit
464ec8601c
|
@ -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 {
|
||||
|
|
|
@ -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{
|
||||
|
|
193
api/zones.go
193
api/zones.go
|
@ -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)
|
||||
|
|
|
@ -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 }}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 = '@'
|
||||
|
|
|
@ -37,12 +37,36 @@
|
|||
<b-spinner label="Spinning" />
|
||||
<p>Please wait while we are importing your domain …</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'
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
111
sources/diff.go
Normal 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
|
||||
}
|
|
@ -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
57
utils/dns.go
Normal 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
|
||||
}
|
Loading…
Reference in New Issue
Block a user