1
0
mirror of https://github.com/StackExchange/dnscontrol.git synced 2024-05-11 05:55:12 +00:00

Nameserver overhaul (#17)

* go changes to support nameservers_from

* clear nameservers before giving to dsp.

* work

* work

* nameserver updates.

* remove unused

* name.com stinks at NS records.

* whitespace

* removing web(belongs in own repo). First sketch of DSP vs NAMESERVER_FROM

* add DEFAULTS to replace defaultDsps.

* initial gcloud provider. Simple records work.

* namedotcom can do subdomain ns records now.

* fix for mx and txt

* kill dsp acronym
This commit is contained in:
Craig Peterson
2016-12-16 13:10:27 -07:00
committed by GitHub
parent 9cb81da20e
commit 1ea80d5347
50 changed files with 672 additions and 66342 deletions

4
.gitignore vendored
View File

@ -3,3 +3,7 @@ dnscontrol-Darwin
dnscontrol-Linux
dnscontrol.exe
dnscontrol
dnsconfig.js
creds.json
integrationTest
ExternalDNS

View File

@ -0,0 +1 @@
D("foo.com","reg","dsp")

View File

@ -2,19 +2,19 @@
var conf = {
registrars: [],
dns_service_providers: [],
dns_providers: [],
domains: []
};
var defaultDsps = [];
var defaultArgs = [];
function initialize(){
conf = {
registrars: [],
dns_service_providers: [],
dns_providers: [],
domains: []
};
defaultDsps = [];
defaultArgs = [];
}
function NewRegistrar(name,type,meta) {
@ -26,17 +26,17 @@ function NewRegistrar(name,type,meta) {
return name;
}
function NewDSP(name, type, meta) {
function NewDnsProvider(name, type, meta) {
if ((typeof meta === 'object') && ('ip_conversions' in meta)) {
meta.ip_conversions = format_tt(meta.ip_conversions)
}
var dsp = {name: name, type: type, meta: meta};
conf.dns_service_providers.push(dsp);
conf.dns_providers.push(dsp);
return name;
}
function newDomain(name,registrar) {
return {name: name, registrar: registrar, meta:{}, records:[], dsps: [], defaultTTL: 0, nameservers:[]};
return {name: name, registrar: registrar, meta:{}, records:[], dnsProviders: {}, defaultTTL: 0, nameservers:[]};
}
function processDargs(m, domain) {
@ -44,7 +44,6 @@ function processDargs(m, domain) {
// function: call it with domain
// array: process recursively
// object: merge it into metadata
// string: assume it is a dsp
if (_.isFunction(m)) {
m(domain);
} else if (_.isArray(m)) {
@ -53,10 +52,8 @@ function processDargs(m, domain) {
}
} else if (_.isObject(m)) {
_.extend(domain.meta,m);
} else if (_.isString(m)) {
domain.dsps.push(m);
} else {
console.log("WARNING: domain modifier type unsupported: ", typeof m, " Domain: ", domain)
throw "WARNING: domain modifier type unsupported: "+ typeof m + " Domain: "+ domain.name;
}
}
@ -67,11 +64,21 @@ function D(name,registrar) {
var m = arguments[i];
processDargs(m, domain)
}
var toAdd = _(defaultDsps).difference(domain.dsps);
_(toAdd).each(function(x) { domain.dsps.push(x)});
for (var i = 0; i< defaultArgs.length; i++){
processDargs(defaultArgs[i],domain)
}
conf.domains.push(domain)
}
// DEFAULTS provides a set of default arguments to apply to all future domains.
// Each call to DEFAULTS will clear any previous values set.
function DEFAULTS(){
defaultArgs = [];
for (var i = 0; i<arguments.length; i++) {
defaultArgs.push(arguments[i]);
}
}
// TTL(v): Set the TTL for a DNS record.
function TTL(v) {
return function(r) {
@ -86,6 +93,20 @@ function DefaultTTL(v) {
}
}
// DnsProvider("providerName", 0)
// nsCount of 0 means don't use or register any nameservers.
// nsCount not provider means use all.
function DnsProvider(name, nsCount){
if(typeof nsCount === 'undefined'){
nsCount = -1;
}
return function(d) {
d.dnsProviders[name] = nsCount;
}
}
// A(name,ip, recordModifiers...)
function A(name, ip) {
var mods = getModifiers(arguments,2)

View File

@ -10,10 +10,16 @@ import (
"github.com/StackExchange/dnscontrol/models"
)
const testDir = "js/parse_tests"
const (
testDir = "js/parse_tests"
errorDir = "js/error_tests"
)
func init() {
os.Chdir("..") // go up a directory so we helpers.js is in a consistent place.
}
func TestParsedFiles(t *testing.T) {
os.Chdir("..") // go up a directory so we helpers.js is in a consistent place.
files, err := ioutil.ReadDir(testDir)
if err != nil {
t.Fatal(err)
@ -53,6 +59,27 @@ func TestParsedFiles(t *testing.T) {
t.Error("Expected and actual json don't match")
t.Log("Expected:", string(expectedJson))
t.Log("Actual:", string(actualJson))
t.FailNow()
}
}
}
func TestErrors(t *testing.T) {
files, err := ioutil.ReadDir(errorDir)
if err != nil {
t.Fatal(err)
}
for _, f := range files {
if filepath.Ext(f.Name()) != ".js" {
continue
}
t.Log(f.Name(), "------")
content, err := ioutil.ReadFile(filepath.Join(errorDir, f.Name()))
if err != nil {
t.Fatal(err)
}
if _, err = ExecuteJavascript(string(content), true); err == nil {
t.Fatal("Expected error but found none")
}
}
}

View File

@ -1,6 +1,6 @@
var REG = NewRegistrar("Third-Party","NONE");
var CF = NewDSP("Cloudflare", "CLOUDFLAREAPI")
D("foo.com",REG,CF,
var CF = NewDnsProvider("Cloudflare", "CLOUDFLAREAPI")
D("foo.com",REG,DnsProvider(CF),
A("@","1.2.3.4")
);

View File

@ -5,7 +5,7 @@
"type": "NONE"
}
],
"dns_service_providers": [
"dns_providers": [
{
"name": "Cloudflare",
"type": "CLOUDFLAREAPI"
@ -15,9 +15,9 @@
{
"name": "foo.com",
"registrar": "Third-Party",
"dsps": [
"Cloudflare"
],
"dnsProviders": {
"Cloudflare":-1
},
"records": [
{
"type": "A",

View File

@ -1,5 +1,5 @@
var REG = NewRegistrar("Third-Party","NONE");
var CF = NewDSP("Cloudflare", "CLOUDFLAREAPI")
D("foo.com",REG,CF,
var CF = NewDnsProvider("Cloudflare", "CLOUDFLAREAPI")
D("foo.com",REG,DnsProvider(CF),
A("@","1.2.3.4",TTL(42))
);

View File

@ -5,7 +5,7 @@
"type": "NONE"
}
],
"dns_service_providers": [
"dns_providers": [
{
"name": "Cloudflare",
"type": "CLOUDFLAREAPI"
@ -15,9 +15,9 @@
{
"name": "foo.com",
"registrar": "Third-Party",
"dsps": [
"Cloudflare"
],
"dnsProviders": {
"Cloudflare":-1
},
"records": [
{
"type": "A",

View File

@ -5,12 +5,12 @@
"type": "CLOUDFLAREAPI"
}
],
"dns_service_providers": [],
"dns_providers": [],
"domains": [
{
"name": "foo.com",
"registrar": "Cloudflare",
"dsps": [],
"dnsProviders": {},
"records": [
{
"type": "A",

View File

@ -1,9 +1,9 @@
var REG = NewRegistrar("Third-Party","NONE");
var CF = NewDSP("Cloudflare", "CLOUDFLAREAPI")
var CF = NewDnsProvider("Cloudflare", "CLOUDFLAREAPI")
var BASE = IP("1.2.3.4")
D("foo.com",REG,CF,
D("foo.com",REG,DnsProvider(CF,0),
A("@",BASE),
A("p1",BASE+1),
A("p255", BASE+255)

View File

@ -5,7 +5,7 @@
"type": "NONE"
}
],
"dns_service_providers": [
"dns_providers": [
{
"name": "Cloudflare",
"type": "CLOUDFLAREAPI"
@ -15,9 +15,9 @@
{
"name": "foo.com",
"registrar": "Third-Party",
"dsps": [
"Cloudflare"
],
"dnsProviders": {
"Cloudflare":0
},
"records": [
{ "type": "A","name": "@","target": "1.2.3.4"},
{ "type": "A","name": "p1","target": "1.2.3.5"},

View File

@ -1,7 +1,7 @@
var REG = NewRegistrar("Third-Party","NONE");
var CF = NewDSP("Cloudflare", "CLOUDFLAREAPI")
D("foo.com",REG,CF,
var CF = NewDnsProvider("Cloudflare", "CLOUDFLAREAPI")
D("foo.com",REG,DnsProvider(CF,2),
A("@","1.2.3.4")
);
D("foo.com",REG);

View File

@ -4,7 +4,7 @@
"type": "NONE"
}
],
"dns_service_providers": [
"dns_providers": [
{
"name": "Cloudflare",
"type": "CLOUDFLAREAPI"
@ -14,9 +14,9 @@
{
"name": "foo.com",
"registrar": "Third-Party",
"dsps": [
"Cloudflare"
],
"dnsProviders": {
"Cloudflare":2
},
"records": [
{
"type": "A",
@ -28,7 +28,7 @@
{
"name": "foo.com",
"registrar": "Third-Party",
"dsps": [],
"dnsProviders": {},
"records": []
}
]

View File

@ -1,5 +1,5 @@
var REG = NewRegistrar("Third-Party","NONE");
var CF = NewDSP("Cloudflare", "CLOUDFLAREAPI")
var CF = NewDnsProvider("Cloudflare", "CLOUDFLAREAPI")
var TRANSFORM_INT = [
{low: "0.0.0.0", high: "1.1.1.1", newBase: "2.2.2.2" },
@ -7,6 +7,6 @@ var TRANSFORM_INT = [
{low: "1.1.1.1", high: IP("2.2.2.2"), newIP: ["3.3.3.3","4.4.4.4",IP("5.5.5.5")]}
]
D("foo.com",REG,CF,
D("foo.com",REG,DnsProvider(CF),
A("@","1.2.3.4",{transform: TRANSFORM_INT})
);

View File

@ -5,7 +5,7 @@
"type": "NONE"
}
],
"dns_service_providers": [
"dns_providers": [
{
"name": "Cloudflare",
"type": "CLOUDFLAREAPI"
@ -15,9 +15,9 @@
{
"name": "foo.com",
"registrar": "Third-Party",
"dsps": [
"Cloudflare"
],
"dnsProviders": {
"Cloudflare":-1
},
"records": [
{
"type": "A",

View File

@ -1,11 +1,11 @@
{
"registrars": [],
"dns_service_providers": [],
"dns_providers": [],
"domains": [
{
"name": "foo.com",
"registrar": "reg",
"dsps": [],
"dnsProviders": {},
"records": [
{
"type": "IMPORT_TRANSFORM",

View File

@ -190,44 +190,46 @@ var _escData = map[string]*_escFile{
"/helpers.js": {
local: "js/helpers.js",
size: 6729,
size: 7195,
modtime: 0,
compressed: `
H4sIAAAJbogA/7RZfW/bvBH/35/iJmC1tKhykrbZINfDvCftg2K1WyTuFsAwDEaibbZ6A0k7zQLnsw9H
UhIl2U8SYO0faUTey+9eeDxenK2gICRnkXSGvd6OcIjybAUjeOgBAHC6ZkJywkUI84Wv1uJMLAXlOxbR
ZcHzHYtpYztPCcvUQm9vZMZ0RbaJvBSFgBHMF8Neb7XNIsnyDFjGJCMJ+y91Pa20geAYimcgaaPB7/1Q
g+wA2luQpvTuqlTpZiSlvrwvqJ9SSTwDi63AxUWvgolfMBqBMxlPv40/O1rRXv1EH3C6RqNQXAhKqGIJ
1U8fUHiofhqI6IWgtjwotmLjcrr2hiYycsszJagD/vL6q1tr0LIt4OAq6PlKbcBoNIJ+fvudRrLvwatX
4PZZsYzybEe5YHkm+sAyLcOzgoILQZMQRrDKeUrkUkr3wL7Xckksipe75GDQtXdiUTzlnYzeXaqU0A6q
/OtVCa8YG5gqorD+1aB72ON2lPNYhPOFjxbpBCwzbDb7HMKpryQhakzQ+WLfBFXwPKJCXBK+Fm7qm6S1
nT0YoGeBkmgDaR6zFaPcx1gyCUwACYKgQWskhxCRJEGiOyY3Rq5NSDgn92EJAE3ZcsF2NLm3qXRyYCj4
miqVmcyVA2IiiU2JpSRbh0CE2Ka0RIduqajw5CwDJj4ajG7aSKsyu1zjhGG1sweaCFrxjxH6AWb0k4vZ
9V2lbVd209vz74vK4Q3C/THFX5Q3DmheBvSnpFlsoAfoID89bsG1ctYBQYYfk0kn9gEhNkeUZyJPaJDk
a9f5z/hq+mn6e2ikVOmiC9Q2E9uiyLmkcQiOPm9YCHxwQB8Mtdx2yB7zdTCAy/axCeE3TomkQOByem1E
BPBNUJAbCgXhJKWScgFElCcFSBYjLBHUR6Aj2BioyoQ2ZHT88GrvVJFnMILzIbD3hK+3Kc2kCBKareVm
COzkxPY2Uqcwgopwzha1q4+cy1YVk/k4jmEES9e6VbwgZqsV5TSLqGvF00BduorLC/BEu6UX3J8ePHSj
/9PbazZd//SNZiqeQaSjM5t9dndeCNdUKu/PZp+VU3RstPctn2vyZuGroHDbTTyQMoER7MpLzWRDVeMa
ao0bKvVqTRtlBdzmPYIhtjHEQV1Su1DGOiVYUdbjiUl7EQSBV6s1dMAKO8MwGWEEayorNrdKCf/cexod
ieMrpdeNfWfs+CUalOw1kY7HzwZbkf5ivOPxH0L+bTqefDCNEOFrKp/AbdGDZviF4JUyg96g61owu5m9
AH9F/evRz25mT2Gf3GgwBWc5Z/L+eTaUXFCxvdiYN88wRpVxVYpKPdZVZVsKzuTG8cF2qw9dY6fXL4hT
SfzrwzS9fipKmIXXH67+/eHKNsAG2yJogX6i9ln9o3Z3s2tWokLz/95CVqmvG3PJSSbwcynJbWJeMHhG
UP98nuR3IZz5sGHrTQjnPt66/ySChvBm4YPefltuv1Pbn76GcLFYaDGqN3TO4BHO4RHewOMQ3sIjvINH
gEe4cHo6QAnLqH579eybe3Q6BAbvoQXy0P2t6PEB0aKtrnAkUOhgBKwI1K/D6vWmPr0Hqy+12kq96TXb
slLWMkhJoUn8Kl7MeygfHdv0PM6ly7y9F3zPWeY6vmO1Uti+HRZccmrtVsvX6jhMRCqz8KNhGC78gWlq
u2uckVmZh9//NwONcMtEheK4kTy/w/Qw+5XOIkjyO8/vLmNC1usGfc9ysPpdTwFU8pkXdX5nbIBHcDw0
AzEYUzWh2R+CU3ZanyZfv1zNlrOr8fT645eriT5UCUFP6Sysu8XqCD6fyZcyeVZh0IOFCDvYRq1tq3J8
cP5R9fV+5Vb976HfOkL9sF0vbJTefuE1nnOIthlwTiPTMkqZdGOsnfj129XvH1zLQXrBGBgH/6K0+Jb9
yPI7bP5XJBG0LLZflh3mau0Iv+Rb2qiI7btB+EISfugWmS8OPDAU8VC9MY4+L+rbEanmbGG/HUxgkaY5
DrBDqSYhnZvHqMBquzJFHx/a2Ta9pdyvXt8FiuJUiAD0FEYCk0FVKLAmTBWLa+4iG7sRWx9ZQ9Oda2H6
PdiDm+NXk4/5ENqtfN2gqDmJmaqYgc/hsUdMIxZTuCWCxpBnemZU0r+Gj63hh9DDD3yF6G4Cn6L4VfYD
NeuXg4MOpG0MOxSt9lwInz7C5KaWbM09SsMqh9ux6+QTXnzvdcYcySawHqxIN2eLxt7zJiuQupxGVuGF
F4w4QJtfZlNVNgTI3Ax/RJdB2R5UxPDqFVgTnHqjfSdViC3exojRYu0y7jtL1YAGy1NnOvN8qpa3zBlK
1fC0HgPfOAe8hzLLvMAwHhTc9cLhCc/k6GinOdlxr3+womDZ+k+e0zbl4P0bB2ZUU06do+ZcldNoqEsx
K6Ae8FaXlIAVz1PYSFmEg4GQJPqR7yhfJfldEOXpgAz+dnb67q9vTwdn52cXF6dY03eMlAzfyY6IiLNC
BuQ230rFk7BbTvj94DZhhcm/YCNT63r96sa59HrW4AhGEOcyEEXCpNsP+k0rXPXvJJ6fLry/nL+78E7w
42zhWV/nja83C681Ti7bmW1aKmYr/FJT9W0W0xXLaOzZf8tQup3G3wdaE0GU1mXJtmmr9Ma6Ov/5/N3F
gQvqDXbSf1d15fVrfT5qmQoiTIjcBKskzznqHKCddXpY0uEE+kEfTiAedi+wGF3yvwAAAP//w3EYSkka
AAA=
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
`,
},

55
main.go
View File

@ -14,23 +14,23 @@ import (
"github.com/StackExchange/dnscontrol/js"
"github.com/StackExchange/dnscontrol/models"
"github.com/StackExchange/dnscontrol/nameservers"
"github.com/StackExchange/dnscontrol/normalize"
"github.com/StackExchange/dnscontrol/providers"
"github.com/StackExchange/dnscontrol/providers/config"
"github.com/StackExchange/dnscontrol/web"
//Define all known providers here. They should each register themselves with the providers package via init function.
_ "github.com/StackExchange/dnscontrol/providers/activedir"
_ "github.com/StackExchange/dnscontrol/providers/bind"
_ "github.com/StackExchange/dnscontrol/providers/cloudflare"
_ "github.com/StackExchange/dnscontrol/providers/gandi"
_ "github.com/StackExchange/dnscontrol/providers/google"
_ "github.com/StackExchange/dnscontrol/providers/namecheap"
_ "github.com/StackExchange/dnscontrol/providers/namedotcom"
_ "github.com/StackExchange/dnscontrol/providers/route53"
)
//go:generate esc -modtime 0 -o js/static.go -pkg js -include helpers\.js -ignore go -prefix js js
//go:generate esc -modtime 0 -o web/static.go -pkg web -include=bundle\.js -ignore node_modules -prefix web web
// One of these config options must be set.
var jsFile = flag.String("js", "dnsconfig.js", "Javascript file containing dns config")
@ -56,10 +56,6 @@ func main() {
fmt.Println(versionString())
return
}
if command == "web" {
runWebServer()
return
}
var dnsConfig *models.DNSConfig
if *stdin {
@ -144,21 +140,25 @@ func main() {
continue
}
fmt.Printf("******************** Domain: %s\n", domain.Name)
for pi, prov := range domain.Dsps {
nsList, err := nameservers.DetermineNameservers(domain, 0, dsps)
if err != nil {
log.Fatal(err)
}
domain.Nameservers = nsList
nameservers.AddNSRecords(domain)
for prov := range domain.DNSProviders {
dc, err := domain.Copy()
if err != nil {
log.Fatal(err)
}
shouldrun := shouldRunProvider(prov)
if shouldrun {
fmt.Printf("----- DNS Provider: %s\n", prov)
} else {
if pi == 0 {
fmt.Printf("----- DNS Provider: %s (read-only)\n", prov)
} else {
fmt.Printf("----- DNS Provider: %s (skipping)\n", prov)
}
shouldrun := shouldRunProvider(prov, dc)
statusLbl := ""
if !shouldrun {
statusLbl = "(skipping)"
}
fmt.Printf("----- DNS Provider: %s%s\n", prov, statusLbl)
if !shouldrun {
continue
}
dsp, ok := dsps[prov]
if !ok {
@ -170,14 +170,10 @@ func main() {
fmt.Printf("Error getting corrections: %s\n", err)
continue DomainLoop
}
storeNameservers(dc, domain)
if !shouldrun {
continue
}
totalCorrections += len(corrections)
anyErrors = printOrRunCorrections(corrections, command) || anyErrors
}
if !shouldRunProvider(domain.Registrar) {
if run := shouldRunProvider(domain.Registrar, domain); !run {
continue
}
fmt.Printf("----- Registrar: %s\n", domain.Registrar)
@ -252,12 +248,12 @@ func printOrRunCorrections(corrections []*models.Correction, command string) (an
return anyErrors
}
func shouldRunProvider(p string) bool {
func shouldRunProvider(p string, dc *models.DomainConfig) bool {
if *flagProviders == "all" {
return true
}
if *flagProviders == "" {
return p != "bind"
return (p != "bind")
// NOTE(tlim): Hardcoding bind is a hacky way to make it off by default.
// As a result, bind only runs if you list it in -providers or use
// -providers=all.
@ -290,17 +286,6 @@ func shouldRunDomain(d string) bool {
return false
}
func storeNameservers(from, to *models.DomainConfig) {
if len(to.Nameservers) == 0 && len(from.Nameservers) > 0 {
to.Nameservers = from.Nameservers
}
}
func runWebServer() {
fmt.Printf("Running Webserver on :8080 (js = %s , creds = %s)", *jsFile, *configFile)
web.Serve(*jsFile, *configFile, *devMode)
}
// Version management. 2 Goals:
// 1. Someone who just does "go get" has at least some information.
// 2. If built with build.sh, more specific build information gets put in.

View File

@ -10,15 +10,15 @@ import (
"reflect"
"strconv"
"github.com/miekg/dns"
"github.com/StackExchange/dnscontrol/transform"
"github.com/miekg/dns"
)
const DefaultTTL = uint32(300)
type DNSConfig struct {
Registrars []*RegistrarConfig `json:"registrars"`
DNSProviders []*DNSProviderConfig `json:"dns_service_providers"`
DNSProviders []*DNSProviderConfig `json:"dns_providers"`
Domains []*DomainConfig `json:"domains"`
}
@ -138,14 +138,22 @@ type Nameserver struct {
Target string `json:"target"`
}
func StringsToNameservers(nss []string) []*Nameserver {
nservers := []*Nameserver{}
for _, ns := range nss {
nservers = append(nservers, &Nameserver{Name: ns})
}
return nservers
}
type DomainConfig struct {
Name string `json:"name"` // NO trailing "."
Registrar string `json:"registrar"`
Dsps []string `json:"dsps"`
Metadata map[string]string `json:"meta,omitempty"`
Records []*RecordConfig `json:"records"`
Nameservers []*Nameserver `json:"nameservers,omitempty"`
KeepUnknown bool `json:"keepunknown"`
Name string `json:"name"` // NO trailing "."
Registrar string `json:"registrar"`
DNSProviders map[string]int `json:"dnsProviders"`
Metadata map[string]string `json:"meta,omitempty"`
Records []*RecordConfig `json:"records"`
Nameservers []*Nameserver `json:"nameservers,omitempty"`
KeepUnknown bool `json:"keepunknown"`
}
func (dc *DomainConfig) Copy() (*DomainConfig, error) {

View File

@ -0,0 +1,69 @@
//Package nameservers provides logic for dynamically finding nameservers for a domain, and configuring NS records for them.
package nameservers
import (
"fmt"
"strings"
"github.com/StackExchange/dnscontrol/models"
"github.com/StackExchange/dnscontrol/providers"
"github.com/miekg/dns/dnsutil"
"strconv"
)
//DetermineNameservers will find all nameservers we should use for a domain. It follows the following rules:
//1. All explicitly defined NAMESERVER records will be used.
//2. Each DSP declares how many nameservers to use. Default is all. 0 indicates to use none.
func DetermineNameservers(dc *models.DomainConfig, maxNS int, dsps map[string]providers.DNSServiceProvider) ([]*models.Nameserver, error) {
//always take explicit
ns := dc.Nameservers
for dsp, n := range dc.DNSProviders {
if n == 0 {
continue
}
fmt.Printf("----- Getting nameservers from: %s\n", dsp)
p, ok := dsps[dsp]
if !ok {
return nil, fmt.Errorf("DNS provider %s not declared", dsp)
}
nss, err := p.GetNameservers(dc.Name)
if err != nil {
return nil, err
}
take := len(nss)
if n > 0 && n < take {
take = n
}
for i := 0; i < take; i++ {
ns = append(ns, nss[i])
}
}
return ns, nil
}
//AddNSRecords creates NS records on a domain corresponding to the nameservers specified.
func AddNSRecords(dc *models.DomainConfig) {
ttl := uint32(300)
if ttls, ok := dc.Metadata["ns_ttl"]; ok {
t, err := strconv.ParseUint(ttls, 10, 32)
if err != nil {
fmt.Printf("WARNING: ns_ttl fpr %s (%s) is not a valid int", dc.Name, ttls)
} else {
ttl = uint32(t)
}
}
for _, ns := range dc.Nameservers {
rc := &models.RecordConfig{
Type: "NS",
Name: "@",
Target: ns.Name,
Metadata: map[string]string{},
TTL: ttl,
}
if !strings.HasSuffix(rc.Target, ".") {
rc.Target += "."
}
rc.NameFQDN = dnsutil.AddOrigin(rc.Name, dc.Name)
dc.Records = append(dc.Records, rc)
}
}

View File

@ -50,16 +50,16 @@ func assert_valid_ipv6(label string) error {
}
// assert_valid_cname_target returns 1 if target is not valid for cnames.
func assert_valid_target(label string) error {
if label == "@" {
func assert_valid_target(target string) error {
if target == "@" {
return nil
}
if len(label) < 1 {
return fmt.Errorf("WARNING: null label.")
if len(target) < 1 {
return fmt.Errorf("WARNING: null target.")
}
// If it containts a ".", it must end in a ".".
if strings.ContainsRune(label, '.') && label[len(label)-1] != '.' {
return fmt.Errorf("WARNING: label (%v) includes a (.), must end with a (.)", label)
if strings.ContainsRune(target, '.') && target[len(target)-1] != '.' {
return fmt.Errorf("WARNING: target (%v) includes a (.), must end with a (.)", target)
}
return nil
}
@ -112,6 +112,9 @@ func validateTargets(rec *models.RecordConfig, domain_name string) (errs []error
check(assert_no_enddot(label))
check(assert_no_underscores(label))
check(assert_valid_target(target))
if label == "@" {
check(fmt.Errorf("cannot create NS record for bare domain. Use NAMESERVER instead"))
}
case "TXT", "IMPORT_TRANSFORM":
default:
errs = append(errs, fmt.Errorf("Unimplemented record type (%v) domain=%v name=%v",
@ -197,7 +200,6 @@ func NormalizeAndValidateConfig(config *models.DNSConfig) (errs []error) {
// Normalize Records.
for _, rec := range domain.Records {
// Validate the unmodified inputs:
if err := validateRecordTypes(rec, domain.Name); err != nil {
errs = append(errs, err)

View File

@ -1,8 +1,9 @@
package normalize
import (
"github.com/StackExchange/dnscontrol/models"
"testing"
"github.com/StackExchange/dnscontrol/models"
)
func Test_assert_no_enddot(t *testing.T) {
@ -105,6 +106,20 @@ func Test_transform_cname(t *testing.T) {
}
}
func TestNSAtRoot(t *testing.T) {
//do not allow ns records for @
rec := &models.RecordConfig{Name: "test", Type: "NS", Target: "ns1.name.com."}
errs := validateTargets(rec, "foo.com")
if len(errs) > 0 {
t.Error("Expect no error with ns record on subdomain")
}
rec.Name = "@"
errs = validateTargets(rec, "foo.com")
if len(errs) != 1 {
t.Error("Expect error with ns record on @")
}
}
func TestTransforms(t *testing.T) {
var tests = []struct {
givenIP string

View File

@ -24,6 +24,11 @@ type RecordConfigJson struct {
TTL uint32 `json:"timetolive"`
}
func (c *adProvider) GetNameservers(string) ([]*models.Nameserver, error) {
//TODO: If using AD for publicly hosted zones, probably pull these from config.
return nil, nil
}
// GetDomainCorrections gets existing records, diffs them against existing, and returns corrections.
func (c *adProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {

View File

@ -49,8 +49,9 @@ func (s SoaInfo) String() string {
}
type Bind struct {
Default_ns []string `json:"default_ns"`
Default_Soa SoaInfo `json:"default_soa"`
Default_NS []string `json:"default_ns"`
Default_Soa SoaInfo `json:"default_soa"`
nameservers []*models.Nameserver `json:"-"`
}
var bindBaseDir = flag.String("bindtree", "zones", "BIND: Directory that stores BIND zonefiles.")
@ -138,18 +139,8 @@ func makeDefaultSOA(info SoaInfo, origin string) *models.RecordConfig {
return &soa_rec
}
func makeDefaultNS(origin string, names []string) []*models.RecordConfig {
var result []*models.RecordConfig
for _, n := range names {
rc := &models.RecordConfig{
Type: "NS",
Name: "@",
Target: n,
}
rc.NameFQDN = dnsutil.AddOrigin(rc.Name, origin)
result = append(result, rc)
}
return result
func (c *Bind) GetNameservers(string) ([]*models.Nameserver, error) {
return c.nameservers, nil
}
func (c *Bind) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
@ -207,11 +198,6 @@ func (c *Bind) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correcti
}
}
// Add NS records:
if len(c.Default_ns) != 0 && !dc.HasRecordTypeName("NS", "@") {
expectedRecords = append(expectedRecords, makeDefaultNS(dc.Name, c.Default_ns)...)
dc.Records = append(dc.Records, makeDefaultNS(dc.Name, c.Default_ns)...)
}
// Add SOA record:
if !dc.HasRecordTypeName("SOA", "@") {
expectedRecords = append(expectedRecords, soa_rec)
@ -286,7 +272,7 @@ func (c *Bind) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correcti
}
func initBind(config map[string]string, providermeta json.RawMessage) (providers.DNSServiceProvider, error) {
// m -- the json blob from creds.json
// config -- the key/values from creds.json
// meta -- the json blob from NewReq('name', 'TYPE', meta)
api := &Bind{}
@ -296,6 +282,7 @@ func initBind(config map[string]string, providermeta json.RawMessage) (providers
return nil, err
}
}
api.nameservers = models.StringsToNameservers(api.Default_NS)
return api, nil
}

View File

@ -115,21 +115,20 @@ func TestWriteZoneFileOrder(t *testing.T) {
r, _ := dns.NewRR(fmt.Sprintf("%s 300 IN A 1.2.3.%d", name, i))
records = append(records, r)
}
records[0].Header().Name = "stackoverflow.com."
records[1].Header().Name = "@"
buf := &bytes.Buffer{}
WriteZoneFile(buf, records, "stackoverflow.com.", 300)
// Compare
if buf.String() != testdataOrder {
t.Log("Found:")
t.Log(buf.String())
t.Log("Expected:")
t.Log(testdataOrder)
t.Fatalf("Zone file does not match.")
}
parseAndRegen(t, buf, testdataOrder)
// Now shuffle the list many times and make sure it still works:
for iteration := 5; iteration > 0; iteration-- {
// Randomize the list:
perm := rand.Perm(len(records))

View File

@ -8,11 +8,11 @@ import (
"strings"
"time"
"github.com/miekg/dns/dnsutil"
"github.com/StackExchange/dnscontrol/models"
"github.com/StackExchange/dnscontrol/providers"
"github.com/StackExchange/dnscontrol/providers/diff"
"github.com/StackExchange/dnscontrol/transform"
"github.com/miekg/dns/dnsutil"
)
/*
@ -38,7 +38,7 @@ type CloudflareApi struct {
ApiKey string `json:"apikey"`
ApiUser string `json:"apiuser"`
domainIndex map[string]string
nameservers map[string][]*models.Nameserver
nameservers map[string][]string
ipConversions []transform.IpConversion
secretIPs []net.IP
ignoredLabels []string
@ -53,6 +53,18 @@ func labelMatches(label string, matches []string) bool {
}
return false
}
func (c *CloudflareApi) GetNameservers(domain string) ([]*models.Nameserver, error) {
if c.domainIndex == nil {
if err := c.fetchDomainList(); err != nil {
return nil, err
}
}
ns, ok := c.nameservers[domain]
if !ok {
return nil, fmt.Errorf("Nameservers for %s not found in cloudflare account", domain)
}
return models.StringsToNameservers(ns), nil
}
func (c *CloudflareApi) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
if c.domainIndex == nil {
@ -64,8 +76,6 @@ func (c *CloudflareApi) GetDomainCorrections(dc *models.DomainConfig) ([]*models
if !ok {
return nil, fmt.Errorf("%s not listed in zones for cloudflare account", dc.Name)
}
dc.Nameservers = c.nameservers[dc.Name]
if err := c.preprocessConfig(dc); err != nil {
return nil, err
}
@ -94,6 +104,12 @@ func (c *CloudflareApi) GetDomainCorrections(dc *models.DomainConfig) ([]*models
log.Fatalf("FATAL: dnsconfig contains label that matches ignored_labels: %#v is in %v)\n", rec.Name, c.ignoredLabels)
// Since we log.Fatalf, we don't need to be clean here.
}
if rec.Type == "NS" && rec.NameFQDN == dc.Name {
if !strings.HasSuffix(rec.Target, ".ns.cloudflare.com.") {
log.Printf("Warning: cloudflare does not support modifying NS records on base domain. %s will not be added.", rec.Target)
}
continue
}
expectedRecords = append(expectedRecords, recordWrapper{rec})
}
_, create, del, mod := diff.IncrementalDiff(records, expectedRecords)

View File

@ -20,7 +20,7 @@ const (
// get list of domains for account. Cache so the ids can be looked up from domain name
func (c *CloudflareApi) fetchDomainList() error {
c.domainIndex = map[string]string{}
c.nameservers = map[string][]*models.Nameserver{}
c.nameservers = map[string][]string{}
page := 1
for {
zr := &zoneResponse{}
@ -34,7 +34,7 @@ func (c *CloudflareApi) fetchDomainList() error {
for _, zone := range zr.Result {
c.domainIndex[zone.Name] = zone.ID
for _, ns := range zone.Nameservers {
c.nameservers[zone.Name] = append(c.nameservers[zone.Name], &models.Nameserver{Name: ns})
c.nameservers[zone.Name] = append(c.nameservers[zone.Name], ns)
}
}
ri := zr.ResultInfo

View File

@ -10,6 +10,7 @@ import (
"github.com/StackExchange/dnscontrol/providers"
"github.com/StackExchange/dnscontrol/providers/diff"
gandidomain "github.com/prasmussen/gandi-api/domain"
gandirecord "github.com/prasmussen/gandi-api/domain/zone/record"
)
@ -72,31 +73,37 @@ func (c *cfRecord) GetComparisionData() string {
return fmt.Sprintf("%d", c.Ttl)
}
func (c *GandiApi) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
if c.domainIndex == nil {
if err := c.fetchDomainList(); err != nil {
return nil, err
}
func (c *GandiApi) getDomainInfo(domain string) (*gandidomain.DomainInfo, error) {
if err := c.fetchDomainList(); err != nil {
return nil, err
}
_, ok := c.domainIndex[dc.Name]
_, ok := c.domainIndex[domain]
if !ok {
return nil, fmt.Errorf("%s not listed in zones for gandi account", dc.Name)
return nil, fmt.Errorf("%s not listed in zones for gandi account", domain)
}
return c.fetchDomainInfo(domain)
}
domaininfo, err := c.fetchDomainInfo(dc.Name)
func (c *GandiApi) GetNameservers(domain string) ([]*models.Nameserver, error) {
domaininfo, err := c.getDomainInfo(domain)
if err != nil {
return nil, err
}
ns := []*models.Nameserver{}
for _, nsname := range domaininfo.Nameservers {
dc.Nameservers = append(dc.Nameservers, &models.Nameserver{Name: nsname})
ns = append(ns, &models.Nameserver{Name: nsname})
}
return ns, nil
}
func (c *GandiApi) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
domaininfo, err := c.getDomainInfo(dc.Name)
if err != nil {
return nil, err
}
foundRecords, err := c.getZoneRecords(domaininfo.ZoneId)
if err != nil {
return nil, err
}
// Convert to []diff.Records and compare:
foundDiffRecords := make([]diff.Record, len(foundRecords))
for i, rec := range foundRecords {

View File

@ -16,6 +16,9 @@ import (
// fetchDomainList gets list of domains for account. Cache ids for easy lookup.
func (c *GandiApi) fetchDomainList() error {
if c.domainIndex != nil {
return nil
}
c.domainIndex = map[string]int64{}
gc := gandiclient.New(c.ApiKey, gandiclient.Production)
domain := gandidomain.New(gc)

217
providers/google/google.go Normal file
View File

@ -0,0 +1,217 @@
package google
import (
"context"
"encoding/json"
"fmt"
"github.com/StackExchange/dnscontrol/models"
"github.com/StackExchange/dnscontrol/providers"
"github.com/StackExchange/dnscontrol/providers/diff"
"golang.org/x/oauth2"
gauth "golang.org/x/oauth2/google"
"google.golang.org/api/dns/v1"
"strings"
)
func init() {
providers.RegisterDomainServiceProviderType("GCLOUD", New)
}
type gcloud struct {
client *dns.Service
project string
zones map[string]*dns.ManagedZone
}
func New(cfg map[string]string, _ json.RawMessage) (providers.DNSServiceProvider, error) {
for _, key := range []string{"clientId", "clientSecret", "refreshToken", "project"} {
if cfg[key] == "" {
return nil, fmt.Errorf("%s required for google cloud provider", key)
}
}
ocfg := &oauth2.Config{
Endpoint: gauth.Endpoint,
ClientID: cfg["clientId"],
ClientSecret: cfg["clientSecret"],
}
tok := &oauth2.Token{
RefreshToken: cfg["refreshToken"],
}
client := ocfg.Client(context.Background(), tok)
dcli, err := dns.New(client)
if err != nil {
return nil, err
}
return &gcloud{
client: dcli,
project: cfg["project"],
}, nil
}
func (g *gcloud) getZone(domain string) (*dns.ManagedZone, error) {
if g.zones == nil {
g.zones = map[string]*dns.ManagedZone{}
pageToken := ""
for {
resp, err := g.client.ManagedZones.List(g.project).PageToken(pageToken).Do()
if err != nil {
return nil, err
}
for _, z := range resp.ManagedZones {
g.zones[z.DnsName] = z
}
if pageToken = resp.NextPageToken; pageToken == "" {
break
}
}
}
if g.zones[domain+"."] == nil {
return nil, fmt.Errorf("Domain %s not found in gcloud account", domain)
}
return g.zones[domain+"."], nil
}
func (g *gcloud) GetNameservers(domain string) ([]*models.Nameserver, error) {
zone, err := g.getZone(domain)
if err != nil {
return nil, err
}
return models.StringsToNameservers(zone.NameServers), nil
}
type key struct {
Type string
Name string
}
func keyFor(r *dns.ResourceRecordSet) key {
return key{Type: r.Type, Name: r.Name}
}
func keyForRec(r *models.RecordConfig) key {
return key{Type: r.Type, Name: r.NameFQDN + "."}
}
func (g *gcloud) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
rrs, zoneName, err := g.getRecords(dc.Name)
if err != nil {
return nil, err
}
//convert to dnscontrol RecordConfig format
existingRecords := []diff.Record{}
oldRRs := map[key]*dns.ResourceRecordSet{}
for _, set := range rrs {
nameWithoutDot := set.Name
if strings.HasSuffix(nameWithoutDot, ".") {
nameWithoutDot = nameWithoutDot[:len(nameWithoutDot)-1]
}
oldRRs[keyFor(set)] = set
for _, rec := range set.Rrdatas {
r := &models.RecordConfig{
NameFQDN: nameWithoutDot,
Type: set.Type,
Target: rec,
TTL: uint32(set.Ttl),
}
existingRecords = append(existingRecords, r)
}
}
w := []diff.Record{}
for _, want := range dc.Records {
if want.TTL == 0 {
want.TTL = 300
}
if want.Type == "MX" {
want.Target = fmt.Sprintf("%d %s", want.Priority, want.Target)
want.Priority = 0
} else if want.Type == "TXT" {
//add quotes to txts
want.Target = fmt.Sprintf(`"%s"`, want.Target)
}
w = append(w, want)
}
// first collect keys that have changed
_, create, delete, modify := diff.IncrementalDiff(existingRecords, w)
changedKeys := map[key]bool{}
desc := ""
for _, c := range create {
desc += fmt.Sprintln(c)
changedKeys[keyForRec(c.Desired.(*models.RecordConfig))] = true
}
for _, d := range delete {
desc += fmt.Sprintln(d)
changedKeys[keyForRec(d.Existing.(*models.RecordConfig))] = true
}
for _, m := range modify {
desc += fmt.Sprintln(m)
changedKeys[keyForRec(m.Existing.(*models.RecordConfig))] = true
}
if len(changedKeys) == 0 {
return nil, nil
}
chg := &dns.Change{Kind: "dns#change"}
for ck := range changedKeys {
// remove old version (if present)
if old, ok := oldRRs[ck]; ok {
chg.Deletions = append(chg.Deletions, old)
}
//collect records to replace with
newRRs := &dns.ResourceRecordSet{
Name: ck.Name,
Type: ck.Type,
Kind: "dns#resourceRecordSet",
}
for _, r := range dc.Records {
if keyForRec(r) == ck {
newRRs.Rrdatas = append(newRRs.Rrdatas, r.Target)
newRRs.Ttl = int64(r.TTL)
}
}
if len(newRRs.Rrdatas) > 0 {
chg.Additions = append(chg.Additions, newRRs)
}
}
runChange := func() error {
_, err := g.client.Changes.Create(g.project, zoneName, chg).Do()
return err
}
return []*models.Correction{
{
Msg: desc,
F: runChange,
},
}, nil
}
func (g *gcloud) getRecords(domain string) ([]*dns.ResourceRecordSet, string, error) {
zone, err := g.getZone(domain)
if err != nil {
return nil, "", err
}
pageToken := ""
sets := []*dns.ResourceRecordSet{}
for {
call := g.client.ResourceRecordSets.List(g.project, zone.Name)
if pageToken != "" {
call = call.PageToken(pageToken)
}
resp, err := call.Do()
if err != nil {
return nil, "", err
}
for _, rrs := range resp.Rrsets {
if rrs.Type == "SOA" {
continue
}
sets = append(sets, rrs)
}
if pageToken = resp.NextPageToken; pageToken == "" {
break
}
}
return sets, zone.Name, nil
}

View File

@ -9,14 +9,44 @@ import (
"github.com/StackExchange/dnscontrol/models"
)
func (n *nameDotCom) GetRegistrarCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
foundNameservers, err := n.getNameservers(dc.Name)
var nsRegex = regexp.MustCompile(`ns([1-4])[a-z]{3}\.name\.com`)
func (n *nameDotCom) GetNameservers(domain string) ([]*models.Nameserver, error) {
//This is an interesting edge case. Name.com expects you to SET the nameservers to ns[1-4].name.com,
//but it will internally set it to ns1xyz.name.com, where xyz is a uniqueish 3 letters.
//In order to avoid endless loops, we will use the unique nameservers if present, or else the generic ones if not.
nss, err := n.getNameserversRaw(domain)
if err != nil {
return nil, err
}
if defaultNsRegexp.MatchString(foundNameservers) {
foundNameservers = "ns1.name.com,ns2.name.com,ns3.name.com,ns4.name.com"
toUse := []string{"ns1.name.com", "ns2.name.com", "ns3.name.com", "ns4.name.com"}
for _, ns := range nss {
if matches := nsRegex.FindStringSubmatch(ns); len(matches) == 2 && len(matches[1]) == 1 {
idx := matches[1][0] - '1' //regex ensures proper range
toUse[idx] = matches[0]
}
}
return models.StringsToNameservers(toUse), nil
}
func (n *nameDotCom) getNameserversRaw(domain string) ([]string, error) {
result := &getDomainResult{}
if err := n.get(apiGetDomain(domain), result); err != nil {
return nil, err
}
if err := result.getErr(); err != nil {
return nil, err
}
sort.Strings(result.Nameservers)
return result.Nameservers, nil
}
func (n *nameDotCom) GetRegistrarCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
nss, err := n.getNameserversRaw(dc.Name)
if err != nil {
return nil, err
}
foundNameservers := strings.Join(nss, ",")
expected := []string{}
for _, ns := range dc.Nameservers {
name := strings.TrimRight(ns.Name, ".")
@ -52,19 +82,6 @@ type getDomainResult struct {
Nameservers []string `json:"nameservers"`
}
// returns comma joined list of nameservers (in alphabetical order)
func (n *nameDotCom) getNameservers(domain string) (string, error) {
result := &getDomainResult{}
if err := n.get(apiGetDomain(domain), result); err != nil {
return "", err
}
if err := result.getErr(); err != nil {
return "", err
}
sort.Strings(result.Nameservers)
return strings.Join(result.Nameservers, ","), nil
}
func (n *nameDotCom) updateNameservers(ns []string, domain string) func() error {
return func() error {
dat := struct {

View File

@ -4,6 +4,7 @@ import (
"fmt"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/StackExchange/dnscontrol/models"
@ -30,7 +31,7 @@ func teardown() {
server.Close()
}
func TestGetNameservers(t *testing.T) {
func TestGetNameserversRaw(t *testing.T) {
for i, test := range []struct {
givenNs, expected string
}{
@ -54,7 +55,7 @@ func TestGetNameservers(t *testing.T) {
w.Write(domainResponse(test.givenNs))
})
found, err := client.getNameservers("example.tld")
found, err := client.getNameserversRaw("example.tld")
if err != nil {
if test.expected == "ERR" {
continue
@ -66,7 +67,7 @@ func TestGetNameservers(t *testing.T) {
t.Errorf("Expected error on test %d, but was none", i)
continue
}
if found != test.expected {
if strings.Join(found, ",") != test.expected {
t.Errorf("Test %d: Expected '%s', but found '%s'", i, test.expected, found)
}
}
@ -148,3 +149,34 @@ func domainResponse(ns string) []byte {
}
var nameComError = []byte(`{"result":{"code":251,"message":"Authentication Error - Invalid Username Or Api Token"}}`)
func TestGetNameservers(t *testing.T) {
const d = "ns1.name.com,ns2.name.com,ns3.name.com,ns4.name.com"
for i, test := range []struct {
givenNs, expected string
}{
//empty or external dsp, use ns1-4.name.com
{"", d},
{`"foo.ns.tld","bar.ns.tld"`, d},
//if already on name.com, use the existing nameservers
{`"ns1aaa.name.com","ns2bbb.name.com","ns3ccc.name.com","ns4ddd.name.com"`, "ns1aaa.name.com,ns2bbb.name.com,ns3ccc.name.com,ns4ddd.name.com"},
//also handle half and half
{`"ns1aaa.name.com","ns2bbb.name.com","ns3ccc.aws.net","ns4ddd.awsdns.org"`, "ns1aaa.name.com,ns2bbb.name.com,ns3.name.com,ns4.name.com"},
{`"nsa.azuredns.com","ns2b.gandhi.net","ns3ccc.name.com","ns4ddd.name.com"`, "ns1.name.com,ns2.name.com,ns3ccc.name.com,ns4ddd.name.com"},
} {
setup()
defer teardown()
mux.HandleFunc("/domain/get/example.tld", func(w http.ResponseWriter, r *http.Request) {
w.Write(domainResponse(test.givenNs))
})
found, err := client.GetNameservers("example.tld")
if err != nil {
t.Errorf("Test %d: %s", i, err)
continue
}
if strings.Join(found, ",") != test.expected {
t.Errorf("Test %d: Expected '%s', but found '%s'", i, test.expected, found)
}
}
}

View File

@ -2,6 +2,8 @@ package namedotcom
import (
"fmt"
"log"
"strings"
"github.com/miekg/dns/dnsutil"
@ -17,7 +19,6 @@ var defaultNameservers = []*models.Nameserver{
}
func (n *nameDotCom) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
dc.Nameservers = defaultNameservers
records, err := n.getRecords(dc.Name)
if err != nil {
return nil, err
@ -27,12 +28,19 @@ func (n *nameDotCom) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Co
actual[i] = records[i]
}
desired := make([]diff.Record, len(dc.Records))
for i, rec := range dc.Records {
desired := make([]diff.Record, 0, len(dc.Records))
for _, rec := range dc.Records {
if rec.TTL == 0 {
rec.TTL = 300
}
desired[i] = rec
if rec.Type == "NS" && rec.NameFQDN == dc.Name {
// name.com does change base domain NS records. dnscontrol will print warnings if you try to set them to anything besides the name.com defaults.
if !strings.HasSuffix(rec.Target, ".name.com.") {
log.Printf("Warning: name.com does not allow NS records on base domain to be modified. %s will not be added.", rec.Target)
}
continue
}
desired = append(desired, rec)
}
_, create, del, mod := diff.IncrementalDiff(actual, desired)
@ -117,10 +125,8 @@ func (n *nameDotCom) getRecords(domain string) ([]*nameComRecord, error) {
for _, rc := range result.Records {
if rc.Type == "CNAME" || rc.Type == "MX" || rc.Type == "NS" {
rc.Content = rc.Content + "."
}
}
return result.Records, nil
}

View File

@ -15,6 +15,7 @@ type Registrar interface {
//DNSServiceProvider is able to generate a set of corrections that need to be made to correct records for a domain
type DNSServiceProvider interface {
GetNameservers(domain string) ([]*models.Nameserver, error)
GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error)
}
@ -84,12 +85,10 @@ func CreateDsps(d *models.DNSConfig, providerConfigs map[string]map[string]strin
//log.Printf("dsp.Name=%#v\n", dsp.Name)
rawMsg, ok := providerConfigs[dsp.Name]
if !ok {
return nil, fmt.Errorf("DNSServiceProvider %s not listed in -providers file.", dsp.Name)
return nil, fmt.Errorf("DNSServiceProvider %s not listed in -providers file", dsp.Name)
}
provider, err := createDNSProvider(dsp.Type, rawMsg, dsp.Metadata)
if err != nil {
log.Printf("createDNSProvider provider=%#v\n", provider)
log.Printf("createDNSProvider err=%#v\n", err)
return nil, err
}
dsps[dsp.Name] = provider
@ -104,6 +103,10 @@ func (n None) GetRegistrarCorrections(dc *models.DomainConfig) ([]*models.Correc
return nil, nil
}
func (n None) GetNameservers(string) ([]*models.Nameserver, error) {
return nil, nil
}
func (n None) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
return nil, nil
}
@ -112,8 +115,4 @@ func init() {
RegisterRegistrarType("NONE", func(map[string]string) (Registrar, error) {
return None{}, nil
})
RegisterDomainServiceProviderType("NONE", func(map[string]string, json.RawMessage) (DNSServiceProvider, error) {
return None{}, nil
})
}

View File

@ -3,16 +3,17 @@ package route53
import (
"encoding/json"
"fmt"
"log"
"strings"
"time"
"github.com/StackExchange/dnscontrol/models"
"github.com/StackExchange/dnscontrol/providers"
"github.com/StackExchange/dnscontrol/providers/diff"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/session"
r53 "github.com/aws/aws-sdk-go/service/route53"
"github.com/StackExchange/dnscontrol/models"
"github.com/StackExchange/dnscontrol/providers"
"github.com/StackExchange/dnscontrol/providers/diff"
)
type route53Provider struct {
@ -40,12 +41,17 @@ func init() {
func sPtr(s string) *string {
return &s
}
func (r *route53Provider) getZones() error {
if r.zones != nil {
return nil
}
var nextMarker *string
r.zones = make(map[string]*r53.HostedZone)
for {
inp := &r53.ListHostedZonesInput{MaxItems: sPtr("1"), Marker: nextMarker}
if nextMarker != nil {
fmt.Println(*nextMarker)
}
inp := &r53.ListHostedZonesInput{Marker: nextMarker}
out, err := r.client.ListHostedZones(inp)
if err != nil {
return err
@ -72,12 +78,31 @@ func getKey(r diff.Record) key {
return key{r.GetName(), r.GetType()}
}
func (r *route53Provider) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
if r.zones == nil {
if err := r.getZones(); err != nil {
return nil, err
}
func (r *route53Provider) GetNameservers(domain string) ([]*models.Nameserver, error) {
if err := r.getZones(); err != nil {
return nil, err
}
zone, ok := r.zones[domain]
if !ok {
log.Printf("WARNING: Domain %s is not on your route 53 account. Dnscontrol will add it, but you will need to run a second time to configure nameservers properly.", domain)
return nil, nil
}
z, err := r.client.GetHostedZone(&r53.GetHostedZoneInput{Id: zone.Id})
if err != nil {
return nil, err
}
ns := []*models.Nameserver{}
for _, nsPtr := range z.DelegationSet.NameServers {
ns = append(ns, &models.Nameserver{Name: *nsPtr})
}
return ns, nil
}
func (r *route53Provider) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
if err := r.getZones(); err != nil {
return nil, err
}
var corrections = []*models.Correction{}
zone, ok := r.zones[dc.Name]
// add zone if it doesn't exist
@ -108,15 +133,11 @@ func (r *route53Provider) GetDomainCorrections(dc *models.DomainConfig) ([]*mode
}
//convert to dnscontrol RecordConfig format
dc.Nameservers = nil
var existingRecords = []*models.RecordConfig{}
var existingRecords = []diff.Record{}
for _, set := range records {
for _, rec := range set.ResourceRecords {
if *set.Type == "SOA" {
continue
} else if *set.Type == "NS" && strings.TrimSuffix(*set.Name, ".") == dc.Name {
dc.Nameservers = append(dc.Nameservers, &models.Nameserver{Name: strings.TrimSuffix(*rec.Value, ".")})
continue
}
r := &models.RecordConfig{
NameFQDN: unescape(set.Name),
@ -127,10 +148,7 @@ func (r *route53Provider) GetDomainCorrections(dc *models.DomainConfig) ([]*mode
existingRecords = append(existingRecords, r)
}
}
e, w := []diff.Record{}, []diff.Record{}
for _, ex := range existingRecords {
e = append(e, ex)
}
w := []diff.Record{}
for _, want := range dc.Records {
if want.TTL == 0 {
want.TTL = 300
@ -146,7 +164,7 @@ func (r *route53Provider) GetDomainCorrections(dc *models.DomainConfig) ([]*mode
//diff
changeDesc := ""
_, create, delete, modify := diff.IncrementalDiff(e, w)
_, create, delete, modify := diff.IncrementalDiff(existingRecords, w)
namesToUpdate := map[key]bool{}
for _, c := range create {

View File

@ -1,3 +0,0 @@
{
"presets": ["es2015"]
}

View File

@ -1,115 +0,0 @@
<template>
<div class="container">
<div class='clearfix'>
<h3 class='pull-left'> DNSControl </h3>
<button class='btn btn-success pull-right' @click="save" style='margin-top:15px'>Save</button>
</div>
<multiselect name="domains" :options="domains" placeholder="jump to domain" :selected="null" :multiple="false" :searchable="true" :on-change="domainSelect" :show-labels="false" :reset-after="true"></multiselect>
<br/>
<multiselect name="vars" :options="vars" placeholder="jump to var" :selected="null" :multiple="false" :searchable="true" :on-change="varSelect" :show-labels="false" :reset-after="true"></multiselect>
<br/>
<editor :text.once="content" v-ref:editor :on-change="textChanged"></editor>
<div v-show="error" class="alert alert-danger" role="alert">{{error}}</div>
<dns-config v-if="config" :data="config" :selected="lastSelected" ></dns-config>
</div>
</template>
<script>
import Multiselect from 'vue-multiselect'
import Editor from './Editor'
import DnsConfig from './DNSConfig'
import 'whatwg-fetch';
import _ from 'lodash';
window._ = _;
export default {
data:function(){
return {
domains: [],
vars: [],
content: window.initialScript,
config: null,
error: null,
lastSelected: ""
};
},
created: function(){
this.parse();
},
methods:{
parse: function(){
var domainRE = this.buildDomainRegex();
var match;
var domains = [];
while(match = domainRE.exec(this.content)){
domains.push(match[2]);
}
domains.sort(function (a, b) {return a.toLowerCase().localeCompare(b.toLowerCase());});
var varRE = this.buildVarRegex();
var vars = [];
while(match = varRE.exec(this.content)){
vars.push(match[1]);
}
vars.sort(function (a, b) {return a.toLowerCase().localeCompare(b.toLowerCase());});
this.domains = domains;
this.vars = vars;
this.run();
},
textChanged: function(e){
this.content = e;
this.parse();
},
domainSelect: function(val){
var searchRegex = this.buildDomainRegex(val);
this.$refs.editor.jump(searchRegex);
this.lastSelected = val;
},
varSelect: function(val){
var searchRegex = this.buildVarRegex(val);
this.$refs.editor.jump(searchRegex);
},
buildDomainRegex: function(d){
// / ^\s*D\s*\(\s*(['"])((?:\\\1|.)*?)\1/gm; //holy crap
/*
^ //start of line
\s*D\s*\(\s* // D( with any whitespace
(['"]) // single or double quote (matching group 1)
((?:\\\1|.)*?) //voodoo. Everything that is not the same char as the start quote
\1 //same quote again
Turned into string and double escaped slashes below
*/
if (!d){
d = "((?:\\\\\\1|.)*?)"
}
return new RegExp("^\\s*D\\s*\\(\\s*(['\"])"+d+"\\1","gm");
},
buildVarRegex: function(v){
if (!v){
v = "([^\\s=]+)"
}
return new RegExp("^\s*var "+v,"gm");
},
run: function(){
this.error = null;
this.config = null;
try{
initialize();
eval(this.content);
this.config= conf;
this.stale = false;
}catch(e){
this.error = e.toString();
}
},
save: function(){
fetch('/api/save', {
method: 'POST',
body: this.content
}) //TODO: error handling
}
},
components:{Multiselect,Editor,DnsConfig}
}
</script>

View File

@ -1,57 +0,0 @@
<template>
<div>
<div class='clearfix'>
<div class='pull-left'>{{correction.Msg}}
<span v-show="!hasRun" class="label label-primary noselect" @click="run">Run!</span>
<span v-show="ok" class='glyphicon glyphicon-ok' style='color:rgb(93, 197, 150);'></span>
</div>
<spinner :loading="running" class='pull-left' height="15px" ></spinner>
</div>
<alert v-if="error" type="danger" >
{{error}}
</alert>
</div>
</template>
<script>
import spinner from 'vue-spinner/src/ScaleLoader.vue'
import { alert } from 'vue-strap'
export default{
data: function(){return{
hasRun: false,
running: false,
error: "",
ok: false
};},
methods: {
run: function(){
if (this.hasRun){return;}
this.hasRun = true;
this.running = true;
this.error = "";
this.ok = false;
var self = this;
fetch("/api/run?id="+this.correction.ID,{method: "POST"})
.then(function(response) {
if (response.status != 200){
response.text().then(function(txt){
self.error = txt;
self.running = false;
self.hasRun = false;
})
return
}
self.ok= true
self.running=false
});
}
},
props:{
correction: Object
},
components:{spinner,alert}
}
</script>
<style>
</style>

View File

@ -1,41 +0,0 @@
<template>
<div>
<run-results :show.sync="showModal" :config="data" :query="query"></run-results>
<domain v-for="domain in sortedDomains" :d="domain" :on-preview="preview"></domain>
</div>
</template>
<script>
import Domain from './Domain'
import RunResults from './RunResults'
export default{
data: function(){return{
showModal: false,
query: {},
};},
props: {
data: Object,
selected: String,
},
computed: {
// hack to bring selected domain to the top.
// computed property with domains in the correct sort order.
sortedDomains: function(){
var selected = this.selected;
return _.sortBy(this.data.domains,function(d){
return d.name != selected;
},function(d,i){return i;})
}
},
methods:{
preview: function(data){
this.query = data;
this.showModal = true;
}
},
components: {Domain,RunResults}
}
</script>
<style>
</style>

View File

@ -1,88 +0,0 @@
<template>
<div class='domain'>
<div class='row'>
<div class='col-md-12' style='padding-left:20px;padding-right:25px;'>
<div class='pull-left'><h2>{{d.name}}</h2></div>
<div class='meta pull-left'><metadata :meta="d.meta"></metadata></div>
<tooltip trigger="hover" placement="bottom" content="Compare data with provider state and view corrections">
<div class='pull-right'><button class='btn btn-default btn-domain' @click="preview">Preview</button></div>
</tooltip>
</div>
</div>
<tooltip trigger="hover" placement="bottom" :content="regTooltip()">
<span :class="['label','noselect',registrarEnabled?'label-success':'label-default']" @click="registrarEnabled = !registrarEnabled">{{d.registrar}}</span>
</tooltip>
<tooltip v-for="(i,dsp) in d.dsps" trigger="hover" placement="bottom" :content="dspTooltip(i)">
<span :class="['label','noselect',dsps[i]?'label-success':'label-default']" @click="dsps.$set(i,!this.dsps[i])">{{dsp}}</span>
</tooltip>
<record v-for="record in d.records" :record="record"></record>
</div>
</template>
<script>
import Metadata from './Metadata'
import Record from './Record'
import { tooltip } from 'vue-strap'
export default{
name:"domain",
props: {
d:Object,
onPreview: {type:Function, required:true} //accepts a data object and returns a fetch promise
},
data:function(){return{
registrarEnabled: true,
dsps: []
}},
created: function(){
for(var i = 0; i<this.d.dsps.length; i++){
this.dsps[i] = true;
}
},
methods:{
preview: function(){
var data = {
Domain: this.d.name,
Registrar: this.registrarEnabled,
Dsps: this.dsps
}
this.onPreview(data);
},
regTooltip: function(){
return (this.registrarEnabled?"Disable":"Enable")+" checking of registrar entries with "+this.d.registrar;
},
dspTooltip: function(i){
return (this.dsps[i]?"Disable":"Enable")+" checking of domain records with "+this.d.dsps[i];
}
},
components: {Metadata,Record,tooltip}
}
</script>
<style>
.domain{
padding-bottom: 15px;
}
.noselect {
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
cursor: pointer;
}
.domain:nth-of-type(odd) {
background: #e0e0e0;
}
.label {
margin-right: 4px;
}
.btn-domain {
margin-top: 20px;
}
.meta{
margin-top: 28px;
margin-left: 10px;
}
</style>

View File

@ -1,64 +0,0 @@
<template>
<pre class="editor" v-el:editor>{{text}}</pre>
</template>
<script>
import ace from 'brace'
import 'brace/mode/javascript';
import 'brace/ext/searchbox';
import 'brace/ext/language_tools';
export default {
props: {
text: {type: String, required: true},
onChange: {type: Function}
},
data: function(){ return{
editor: null,
debounced: null,
}
},
methods:{
change: function(e){
if (!this.debounced){
var self = this;
this.debounced = _.debounce(
function(){
if (self.onChange){
self.onChange(self.editor.getValue());
}
},500
)
}
this.debounced();
},
jump: function(r){
this.editor.find(r,{
backwards: false,
wrap: true,
caseSensitive: false,
wholeWord: false,
regExp: true,
});
}
},
ready: function(){
var el = this.$els.editor;
this.editor = ace.edit(el);
this.editor.getSession().setMode('ace/mode/javascript');
this.editor.getSession().on('change', this.change);
this.editor.setOptions({
enableBasicAutocompletion: true
});
}
}
</script>
<style>
.editor{
width: 100%;
height: 500px;
}
</style>

View File

@ -1,41 +0,0 @@
<template>
<div style="position: relative">
<popover v-if="hasMeta()" effect="fade" placement="right" title="Metadata" trigger="hover">
<span class="label label-info noselect">Metadata</span>
<div slot="content">
<table class='table '>
<tbody>
<tr v-for="(k,v) in meta">
<td><b>{{k}}</b></td>
<td>{{v}}</td>
</tr>
</tbody>
</table>
</div>
</popover>
</div>
</template>
<script>
import { popover } from 'vue-strap'
export default{
name: "metadata",
data: function(){return{
};},
methods:{
hasMeta: function(){
return Object.keys(this.meta).length > 0;
}
},
props:{
meta:{type:Object, required: true}
},
components:{popover}
}
</script>
<style>
</style>

View File

@ -1,19 +0,0 @@
<template>
<div class='clearfix'><b class='pull-left'>{{record.type}} {{record.name}} {{record.target | truncate 50}}</b>
<div style='margin-left:8px' class='pull-left'><metadata :meta="record.meta"></metadata></div></div>
</template>
<script>
import Metadata from './Metadata'
export default{
data: function(){return{
};},
props: {
record:{type: Object, required:true}
},
components:{Metadata}
}
</script>
<style>
</style>

View File

@ -1,81 +0,0 @@
<template>
<modal :show.sync="show" :title="query.Domain" width="75%">
<div slot='modal-body'><div style='margin:15px;'>
<spinner :loading="loading"></spinner>
<alert v-if="error" type="danger" >
{{error}}
</alert>
<div v-for="(provider,list) in corrections">
<h3>{{provider}} <span v-show="list.length==0" class='glyphicon glyphicon-ok' style='color:rgb(93, 197, 150);'></span></h3>
<div v-for="correction in list">
<correction :correction="correction"></correction>
</div>
</div>
</div></div>
<div slot="modal-footer" class="modal-footer">
<button type="button" class="btn btn-default" @click="show = false">Close</button>
</div>
</modal>
</template>
<script>
import { modal,alert } from 'vue-strap'
import spinner from 'vue-spinner/src/ScaleLoader.vue'
import Correction from './Correction'
export default{
props:{
show:{
required: true,
type: Boolean,
twoWay: true
},
query: {type: Object, default:{}},
config: Object
},
data: function(){return{
loading: false,
error: "",
corrections: []
};},
methods:{
preview: function(){
var self = this;
fetch('/api/preview', {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({Config: this.config, Query: this.query})
}).then(function(response) {
if (response.status != 200){
response.text().then(function(txt){
self.error = txt;
self.loading = false;
})
return
}
response.json().then(function(j){
self.corrections= j
self.loading=false
})
});
}
},
created: function(){
this.$watch('show', function(newVal){
if (newVal && !this.loading){
this.loading = true;
this.error = "";
this.corrections = [];
this.preview();
}
})
},
components:{modal,alert,spinner,Correction}
}
</script>
<style>
</style>

View File

@ -1,15 +0,0 @@
import Vue from 'vue'
import App from './components/App'
import truncate from 'vue-truncate'
Vue.use(truncate);
Vue.config.devtools = true;
// mount a root Vue instance
new Vue({
el: 'body',
components: {
app: App
}
})

File diff suppressed because one or more lines are too long

View File

@ -1,35 +0,0 @@
{
"name": "dnscontrol",
"version": "1.0.0",
"description": "web ide for dnscontrol",
"main": "bundle.js",
"scripts": {
"watch": "webpack -w"
},
"author": "",
"license": "MIT",
"dependencies": {
"brace": "^0.8.0",
"lodash": "^4.14.1",
"vue": "^1.0.26",
"vue-multiselect": "^0.3.1",
"vue-spinner": "^1.0.2",
"vue-strap": "^1.0.11",
"vue-truncate": "^1.0.0",
"whatwg-fetch": "^1.0.0"
},
"devDependencies": {
"babel-core": "^6.11.4",
"babel-loader": "^6.2.4",
"babel-plugin-transform-runtime": "^6.12.0",
"babel-preset-es2015": "^6.9.0",
"babel-runtime": "^6.11.6",
"css-loader": "^0.23.1",
"strip-sourcemap-loader": "0.0.1",
"vue-hot-reload-api": "^2.0.5",
"vue-html-loader": "^1.2.3",
"vue-loader": "^8.5.3",
"vue-style-loader": "^1.0.0",
"webpack": "^1.13.1"
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,280 +0,0 @@
package web
import (
"bytes"
"crypto/rand"
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
"html/template"
"io/ioutil"
"log"
"net/http"
"os"
"os/exec"
"time"
"github.com/NYTimes/gziphandler"
"github.com/StackExchange/dnscontrol/js"
"github.com/StackExchange/dnscontrol/models"
"github.com/StackExchange/dnscontrol/normalize"
"github.com/StackExchange/dnscontrol/providers"
"github.com/StackExchange/dnscontrol/providers/config"
)
var (
jsFilename, credsFilename string
)
var apiMux = http.NewServeMux()
func Serve(jsFile, creds string, devMode bool) {
if devMode {
runWebpack()
}
jsFilename, credsFilename = jsFile, creds
http.HandleFunc("/helpers.js", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/javascript")
helpers := js.GetHelpers(devMode)
w.Write([]byte(helpers))
})
http.Handle("/bundle.js", gziphandler.GzipHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/javascript")
w.Write(FSMustByte(devMode, "/bundle.js"))
})))
http.Handle("/api/", apiMux)
api("/api/save", save, "POST")
api("/api/preview", preview, "POST")
api("/api/run", runCorrection, "POST")
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
err := func() error {
script, err := ioutil.ReadFile(jsFile)
if err != nil {
return err
}
b64 := base64.StdEncoding.EncodeToString(script)
buf := &bytes.Buffer{}
err = index.Execute(buf, b64)
if err != nil {
return err
}
w.Write(buf.Bytes())
return nil
}()
if err != nil {
http.Error(w, err.Error(), 500)
}
})
log.Fatal(http.ListenAndServe(":8080", nil))
}
func save(r *http.Request) (interface{}, error) {
body, err := ioutil.ReadAll(r.Body)
if err != nil {
return nil, err
}
defer r.Body.Close()
err = ioutil.WriteFile(jsFilename, body, os.FileMode(0660))
if err != nil {
return nil, err
}
return nil, nil
}
type correction struct {
*models.Correction
ID string
created time.Time
}
var cache = map[string]*correction{}
func newCorrection(c *models.Correction) *correction {
corr := &correction{
Correction: c,
created: time.Now(),
ID: randomString(),
}
cache[corr.ID] = corr
return corr
}
func randomString() string {
buf := make([]byte, 6)
rand.Read(buf)
return hex.EncodeToString(buf)
}
func runCorrection(r *http.Request) (interface{}, error) {
id := r.FormValue("id")
c, ok := cache[id]
if !ok {
return nil, fmt.Errorf("Cannot find correction %s.", id)
}
return nil, c.Correction.F()
}
func preview(r *http.Request) (interface{}, error) {
decoder := json.NewDecoder(r.Body)
defer r.Body.Close()
data := &struct {
Config *models.DNSConfig
Query struct {
Domain string
Registrar bool
Dsps []bool
}
}{}
if err := decoder.Decode(data); err != nil {
return nil, err
}
//limit to only the domain we care about
data.Config.Domains = []*models.DomainConfig{data.Config.FindDomain(data.Query.Domain)}
errs := normalize.NormalizeAndValidateConfig(data.Config)
if len(errs) > 0 {
buf := &bytes.Buffer{}
for _, err := range errs {
fmt.Fprintln(buf, err)
}
return nil, fmt.Errorf(buf.String())
}
configs, err := config.LoadProviderConfigs(credsFilename)
if err != nil {
return nil, err
}
registrars, err := providers.CreateRegistrars(data.Config, configs)
if err != nil {
return nil, err
}
dsps, err := providers.CreateDsps(data.Config, configs)
if err != nil {
return nil, err
}
domain := data.Config.FindDomain(data.Query.Domain)
if domain == nil {
return nil, fmt.Errorf("Didn't find domain %s in config.", data.Query.Domain)
}
corrections := map[string][]*correction{}
for i, dspName := range domain.Dsps {
readOnly := false
if i >= len(data.Query.Dsps) || !data.Query.Dsps[i] {
if len(domain.Nameservers) == 0 {
readOnly = true
} else {
continue
}
}
dsp, ok := dsps[dspName]
if !ok {
return nil, fmt.Errorf("DSP %s not found", dspName)
}
dom, err := domain.Copy()
if err != nil {
return nil, err
}
cs := []*correction{}
domCorrections, err := dsp.GetDomainCorrections(dom)
if err != nil {
return nil, err
}
if len(domain.Nameservers) == 0 && len(dom.Nameservers) > 0 {
domain.Nameservers = dom.Nameservers
}
for _, c := range domCorrections {
cs = append(cs, newCorrection(c))
}
if !readOnly {
corrections["DSP: "+dspName] = cs
}
}
if data.Query.Registrar {
reg, ok := registrars[domain.Registrar]
if !ok {
return nil, fmt.Errorf("Registrar %s not found", domain.Registrar)
}
dom, err := domain.Copy()
if err != nil {
return nil, err
}
cs := []*correction{}
regCorrections, err := reg.GetRegistrarCorrections(dom)
if err != nil {
return nil, err
}
for _, c := range regCorrections {
cs = append(cs, newCorrection(c))
}
corrections["Registrar: "+domain.Registrar] = cs
}
return corrections, nil
}
var index = template.Must(template.New("index").Parse(`
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>DNSControl IDE</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" integrity="sha384-1q8mTJOASx8j1Au+a5WDVnPi2lkFfwwEAa8hDDdjZlpLegxhjVME1fgjWPGmkzs7" crossorigin="anonymous">
</head>
<body>
<app></app>
<script>var initialScript = decodeURIComponent(escape(window.atob("{{.}}")))</script>
<script src="./helpers.js" type="text/javascript"></script>
<script src="./bundle.js" type="text/javascript"></script>
</body>
</html>
`))
type apiHandler func(r *http.Request) (interface{}, error)
func api(route string, f apiHandler, methods ...string) {
apiMux.Handle(route, gziphandler.GzipHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if len(methods) > 0 {
ok := false
for _, meth := range methods {
if meth == r.Method {
ok = true
break
}
}
if !ok {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
}
ret, err := f(r)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
if ret == nil {
return
}
m := json.NewEncoder(w)
if err = m.Encode(ret); err != nil {
http.Error(w, err.Error(), 500)
}
})))
}
func runWebpack() {
cmd := exec.Command("npm", "run", "watch")
cmd.Dir = "web"
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
go func() { log.Fatal(cmd.Run()) }()
}

View File

@ -1,37 +0,0 @@
var path = require('path')
var webpack = require('webpack');
module.exports = {
// entry point of our application
entry: './app/main.js',
// where to place the compiled bundle
output: {
path: __dirname,
filename: 'bundle.js'
},
resolve: {
extensions: ['', '.js', '.vue'],
fallback: [path.join(__dirname, './node_modules')],
},
resolveLoader: {
fallback: [path.join(__dirname, '../node_modules')]
},
module: {
loaders: [
{
test: /\.js$/,
exclude: /(node_modules|bower_components)/,
loader: 'babel'
},
{
test: /\.js$/,
include: /node_modules/,
loader: 'strip-sourcemap-loader'
},
{
test: /\.vue$/, // a regex for matching all files that end in `.vue`
loader: 'vue' // loader to use for matched files
}
]
}
}