add multi credentials support
This commit is contained in:
parent
c5a780dfb0
commit
c88eb9c7d8
|
@ -22,12 +22,13 @@ var (
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
logger = logrus.StandardLogger()
|
logger = logrus.StandardLogger()
|
||||||
config ddnsclient.ClientConfig
|
config utils.ClientConfig
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
viper.BindEnv("CONFIG_PATH")
|
viper.BindEnv("CONFIG_PATH")
|
||||||
viper.SetConfigType("yaml")
|
viper.SetConfigType("yaml")
|
||||||
|
|
||||||
if conf := viper.GetString("CONFIG_PATH"); conf == "" {
|
if conf := viper.GetString("CONFIG_PATH"); conf == "" {
|
||||||
viper.SetConfigFile("ddnsclient.yaml")
|
viper.SetConfigFile("ddnsclient.yaml")
|
||||||
} else {
|
} else {
|
||||||
|
|
31
config.go
31
config.go
|
@ -1,31 +0,0 @@
|
||||||
package ddnsclient
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/datahearth/ddnsclient/pkg/providers/google"
|
|
||||||
"github.com/datahearth/ddnsclient/pkg/providers/ovh"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ClientConfig struct {
|
|
||||||
Logger Logger `mapstructure:"logger"`
|
|
||||||
Providers Providers `mapstructure:"providers"`
|
|
||||||
Watchers WatcherConfig `mapstructure:"watchers"`
|
|
||||||
UpdateTime int `mapstructure:"update-time,omitempty"`
|
|
||||||
PendingDnsPropagation int `mapstructure:"pending-dns-propagation,omitempty"`
|
|
||||||
WebIP string `mapstructure:"web-ip,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Logger struct {
|
|
||||||
Level string `mapstructure:"level"`
|
|
||||||
DisableTimestamp bool `mapstructure:"disable-timestamp,omitempty"`
|
|
||||||
DisableColor bool `mapstructure:"disable-color,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Providers struct {
|
|
||||||
Ovh ovh.OvhConfig `mapstructure:"ovh,omitempty"`
|
|
||||||
Google google.GoogleConfig `mapstructure:"google,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type WatcherConfig struct {
|
|
||||||
Ovh []string `yaml:"ovh,omitempty"`
|
|
||||||
Google []string `yaml:"google,omitempty"`
|
|
||||||
}
|
|
69
main.go
69
main.go
|
@ -4,14 +4,10 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"reflect"
|
|
||||||
"strings"
|
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/datahearth/ddnsclient/pkg/providers"
|
"github.com/datahearth/ddnsclient/pkg/utils"
|
||||||
"github.com/datahearth/ddnsclient/pkg/providers/google"
|
|
||||||
"github.com/datahearth/ddnsclient/pkg/providers/ovh"
|
|
||||||
"github.com/datahearth/ddnsclient/pkg/watcher"
|
"github.com/datahearth/ddnsclient/pkg/watcher"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
@ -24,25 +20,15 @@ var (
|
||||||
)
|
)
|
||||||
|
|
||||||
// Start create a new instance of ddns-client
|
// Start create a new instance of ddns-client
|
||||||
func Start(logger logrus.FieldLogger, config ClientConfig) error {
|
func Start(logger logrus.FieldLogger, config utils.ClientConfig) error {
|
||||||
log := logger.WithFields(logrus.Fields{
|
log := logger.WithFields(logrus.Fields{
|
||||||
"pkg": "ddnsclient",
|
"pkg": "ddnsclient",
|
||||||
"component": "root",
|
"component": "root",
|
||||||
})
|
})
|
||||||
|
|
||||||
fields := reflect.ValueOf(config.Watchers)
|
ws := make([]watcher.Watcher, 0, len(config.Watchers))
|
||||||
ws := []watcher.Watcher{}
|
for _, cw := range config.Watchers {
|
||||||
|
w, err := watcher.NewWatcher(logger, &cw, config.WebIP)
|
||||||
// * check providers and watchers config
|
|
||||||
// todo: invalid condition but while still exit in the next step. To be corrected
|
|
||||||
if fields.NumField() == 0 || reflect.ValueOf(config.Providers).NumField() == 0 {
|
|
||||||
return ErrWatchersConfigLen
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := 0; i < fields.NumField(); i++ {
|
|
||||||
providerName := strings.ToLower(fields.Type().Field(i).Name)
|
|
||||||
|
|
||||||
w, err := CreateWatcher(providerName, config.WebIP, logger, config.Watchers, config.Providers, config.PendingDnsPropagation)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Warnf("Provider error: %v. Skipping...\n", err.Error())
|
logger.Warnf("Provider error: %v. Skipping...\n", err.Error())
|
||||||
continue
|
continue
|
||||||
|
@ -51,24 +37,20 @@ func Start(logger logrus.FieldLogger, config ClientConfig) error {
|
||||||
ws = append(ws, w)
|
ws = append(ws, w)
|
||||||
}
|
}
|
||||||
|
|
||||||
// * check for valid created watchers
|
|
||||||
if len(ws) == 0 {
|
if len(ws) == 0 {
|
||||||
return ErrWatcherCreationLen
|
return ErrWatcherCreationLen
|
||||||
}
|
}
|
||||||
|
|
||||||
// * create signal watcher
|
|
||||||
sigc := make(chan os.Signal, 1)
|
sigc := make(chan os.Signal, 1)
|
||||||
signal.Notify(sigc, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
|
signal.Notify(sigc, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
|
||||||
defer close(sigc)
|
defer close(sigc)
|
||||||
|
|
||||||
// * create close and error channel
|
|
||||||
chClose := make(chan struct{})
|
chClose := make(chan struct{})
|
||||||
chErr := make(chan error)
|
chErr := make(chan error)
|
||||||
defer close(chClose)
|
defer close(chClose)
|
||||||
defer close(chErr)
|
defer close(chErr)
|
||||||
|
|
||||||
logger.Infoln("Start watching periodically for changes!")
|
logger.Infoln("Start watching periodically for changes!")
|
||||||
// * run every created watchers in goroutines
|
|
||||||
for _, w := range ws {
|
for _, w := range ws {
|
||||||
tickTime := config.UpdateTime
|
tickTime := config.UpdateTime
|
||||||
if tickTime == 0 {
|
if tickTime == 0 {
|
||||||
|
@ -79,7 +61,6 @@ func Start(logger logrus.FieldLogger, config ClientConfig) error {
|
||||||
go w.Run(t, chClose, chErr)
|
go w.Run(t, chClose, chErr)
|
||||||
}
|
}
|
||||||
|
|
||||||
// * listening for errors and exit signal
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case err := <-chErr:
|
case err := <-chErr:
|
||||||
|
@ -92,43 +73,3 @@ func Start(logger logrus.FieldLogger, config ClientConfig) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func CreateWatcher(provider, webIP string, logger logrus.FieldLogger, wc WatcherConfig, ps Providers, pendingDnsPropagation int) (watcher.Watcher, error) {
|
|
||||||
var sbs []string
|
|
||||||
var p providers.Provider
|
|
||||||
var err error
|
|
||||||
|
|
||||||
// * check for implemented providers
|
|
||||||
switch provider {
|
|
||||||
case "ovh":
|
|
||||||
logger.Debugln("create OVH provider")
|
|
||||||
p, err = ovh.NewOVH(logger, &ps.Ovh)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
sbs = wc.Ovh
|
|
||||||
|
|
||||||
case "google":
|
|
||||||
logger.Debugln("create GOOGLE provider")
|
|
||||||
p, err = google.NewGoogle(logger, &ps.Google)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
sbs = wc.Google
|
|
||||||
|
|
||||||
default:
|
|
||||||
return nil, ErrInvalidProvider
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(sbs) == 0 {
|
|
||||||
return nil, ErrSbsLen
|
|
||||||
}
|
|
||||||
|
|
||||||
// * create provider's watcher
|
|
||||||
w, err := watcher.NewWatcher(logger, p, sbs, webIP, provider, pendingDnsPropagation)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return w, nil
|
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,119 @@
|
||||||
|
package provider
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/datahearth/ddnsclient/pkg/utils"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Provider is the default interface for all providers
|
||||||
|
type Provider interface {
|
||||||
|
UpdateSubdomains(ip string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type provider struct {
|
||||||
|
logger logrus.FieldLogger
|
||||||
|
config utils.Config
|
||||||
|
name string
|
||||||
|
url string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewProvider(logger logrus.FieldLogger, config utils.Config, url, name string) (Provider, error) {
|
||||||
|
if logger == nil {
|
||||||
|
return nil, utils.ErrNilLogger
|
||||||
|
}
|
||||||
|
if name == "" {
|
||||||
|
return nil, utils.ErrInvalidName
|
||||||
|
}
|
||||||
|
if url == "" {
|
||||||
|
if utils.DefaultURLs[name] == "" {
|
||||||
|
return nil, utils.ErrInvalidURL
|
||||||
|
}
|
||||||
|
url = utils.DefaultURLs[name]
|
||||||
|
}
|
||||||
|
logger = logger.WithField("pkg", "providers")
|
||||||
|
|
||||||
|
return &provider{
|
||||||
|
config: config,
|
||||||
|
logger: logger,
|
||||||
|
name: name,
|
||||||
|
url: url,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *provider) UpdateSubdomains(srvIP string) error {
|
||||||
|
for _, sb := range p.config.Subdomains {
|
||||||
|
ip, err := utils.RetrieveSubdomainIP(sb)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if ip == srvIP {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
p.logger.WithFields(logrus.Fields{
|
||||||
|
"component": "UpdateSubdomains",
|
||||||
|
"server-ip": srvIP,
|
||||||
|
"subdomain-address": ip,
|
||||||
|
"subdomain": sb,
|
||||||
|
}).Infoln("IP addresses doesn't match. Updating subdomain's ip...")
|
||||||
|
if err := p.updateSubdomain(sb, srvIP); err != nil {
|
||||||
|
if err != utils.ErrReadBody && err != utils.ErrWrongStatusCode {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
p.logger.WithError(err).WithFields(logrus.Fields{
|
||||||
|
"component": "UpdateSubdomains",
|
||||||
|
"subdomain": sb,
|
||||||
|
"new-ip": srvIP,
|
||||||
|
}).Warnln("failed to update subdomain ip")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *provider) updateSubdomain(subdomain, ip string) error {
|
||||||
|
newURL := strings.ReplaceAll(p.url, "SUBDOMAIN", subdomain)
|
||||||
|
newURL = strings.ReplaceAll(newURL, "NEWIP", ip)
|
||||||
|
logger := p.logger.WithFields(logrus.Fields{
|
||||||
|
"component": "UpdateIP",
|
||||||
|
"updated-url": newURL,
|
||||||
|
"subdomain": subdomain,
|
||||||
|
"new-ip": ip,
|
||||||
|
})
|
||||||
|
|
||||||
|
req, err := http.NewRequest("GET", newURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return utils.ErrCreateNewRequest
|
||||||
|
}
|
||||||
|
req.SetBasicAuth(p.config.Username, p.config.Password)
|
||||||
|
|
||||||
|
logger.Debugln("calling DDNS provider for subdomain update")
|
||||||
|
c := new(http.Client)
|
||||||
|
resp, err := c.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return utils.ErrUpdateRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.ContentLength != 0 {
|
||||||
|
b, err := ioutil.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return utils.ErrReadBody
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.Contains(string(b), "good "+ip) && !strings.Contains(string(b), "nochg "+ip) {
|
||||||
|
return errors.New("failed to update subdomain ip. Error: " + string(b))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode != 200 {
|
||||||
|
return utils.ErrWrongStatusCode
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -1,99 +0,0 @@
|
||||||
package google
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/datahearth/ddnsclient/pkg/providers"
|
|
||||||
"github.com/datahearth/ddnsclient/pkg/utils"
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
// ErrNilGoogleConfig is thrown when GOOGLE configuration is empty
|
|
||||||
ErrNilGoogleConfig = errors.New("GOOGLE config is mandatory")
|
|
||||||
// ErrInvalidConfig is thrown when no username and password are provided and URL doesn't contains them
|
|
||||||
ErrInvalidConfig = errors.New("username and password are required if url doesn't contains them")
|
|
||||||
// ErrReadBody is thrown when reader failed to read response body
|
|
||||||
ErrReadBody = errors.New("failed to read response body")
|
|
||||||
)
|
|
||||||
|
|
||||||
// GoogleConfig is the struct for the yaml configuration file
|
|
||||||
type GoogleConfig struct {
|
|
||||||
URL string `mapstructure:"url"`
|
|
||||||
Username string `mapstructure:"username,omitempty"`
|
|
||||||
Password string `mapstructure:"password,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type google struct {
|
|
||||||
config *GoogleConfig
|
|
||||||
logger logrus.FieldLogger
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewGoogle returns a new instance of the GOOGLE provider
|
|
||||||
func NewGoogle(logger logrus.FieldLogger, googleConfig *GoogleConfig) (providers.Provider, error) {
|
|
||||||
if googleConfig == nil {
|
|
||||||
return nil, ErrNilGoogleConfig
|
|
||||||
}
|
|
||||||
if logger == nil {
|
|
||||||
return nil, utils.ErrNilLogger
|
|
||||||
}
|
|
||||||
if (googleConfig.Username == "" && googleConfig.Password == "") && !strings.Contains(googleConfig.URL, "@") {
|
|
||||||
return nil, ErrInvalidConfig
|
|
||||||
}
|
|
||||||
|
|
||||||
logger = logger.WithField("pkg", "provider-google")
|
|
||||||
|
|
||||||
return &google{
|
|
||||||
config: googleConfig,
|
|
||||||
logger: logger,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateIP updates the subdomain A record
|
|
||||||
func (g *google) UpdateIP(subdomain, ip string) error {
|
|
||||||
newURL := strings.ReplaceAll(g.config.URL, "SUBDOMAIN", subdomain)
|
|
||||||
newURL = strings.ReplaceAll(newURL, "NEWIP", ip)
|
|
||||||
logger := g.logger.WithFields(logrus.Fields{
|
|
||||||
"component": "update-ip",
|
|
||||||
"ovh-update-url": newURL,
|
|
||||||
"subdomain": subdomain,
|
|
||||||
"new-ip": ip,
|
|
||||||
})
|
|
||||||
|
|
||||||
// * create POST request
|
|
||||||
req, err := http.NewRequest("POST", newURL, nil)
|
|
||||||
if err != nil {
|
|
||||||
return utils.ErrCreateNewRequest
|
|
||||||
}
|
|
||||||
// * use basic auth if config is set
|
|
||||||
if g.config.Username != "" && g.config.Password != "" {
|
|
||||||
logger.Debugln("username and password passed in config. Use basic auth")
|
|
||||||
req.SetBasicAuth(g.config.Username, g.config.Password)
|
|
||||||
}
|
|
||||||
|
|
||||||
// * perform POST request
|
|
||||||
logger.Debugln("calling Google DynDNS to update subdomain IP")
|
|
||||||
c := new(http.Client)
|
|
||||||
resp, err := c.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return utils.ErrUpdateRequest
|
|
||||||
}
|
|
||||||
|
|
||||||
// * read response body
|
|
||||||
b, err := ioutil.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return ErrReadBody
|
|
||||||
}
|
|
||||||
|
|
||||||
// * check for error response
|
|
||||||
// doc: https://support.google.com/domains/answer/6147083?hl=en#zippy=%2Cusing-the-api-to-update-your-dynamic-dns-record
|
|
||||||
// todo: check why the hell do I need to use () for conditions here !!!!
|
|
||||||
if (strings.Contains(string(b), "good "+ip) != true) || (strings.Contains(string(b), "nochg "+ip) != false) {
|
|
||||||
return errors.New("failed to update subdomain ip. Google error: " + string(b))
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -1,6 +0,0 @@
|
||||||
package providers
|
|
||||||
|
|
||||||
// Provider is the default interface for all providers
|
|
||||||
type Provider interface {
|
|
||||||
UpdateIP(subdomain, ip string) error
|
|
||||||
}
|
|
|
@ -1,76 +0,0 @@
|
||||||
package ovh
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/datahearth/ddnsclient/pkg/providers"
|
|
||||||
"github.com/datahearth/ddnsclient/pkg/utils"
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ErrNilOvhConfig is thrown when OVH configuration is empty
|
|
||||||
var ErrNilOvhConfig = errors.New("OVH config is mandatory")
|
|
||||||
|
|
||||||
type OvhConfig struct {
|
|
||||||
URL string `mapstructure:"url,omitempty"`
|
|
||||||
Username string `mapstructure:"username"`
|
|
||||||
Password string `mapstructure:"password"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ovh struct {
|
|
||||||
config *OvhConfig
|
|
||||||
logger logrus.FieldLogger
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewOVH returns a new instance of the OVH provider
|
|
||||||
func NewOVH(logger logrus.FieldLogger, ovhConfig *OvhConfig) (providers.Provider, error) {
|
|
||||||
if ovhConfig == nil {
|
|
||||||
return nil, ErrNilOvhConfig
|
|
||||||
}
|
|
||||||
if logger == nil {
|
|
||||||
return nil, utils.ErrNilLogger
|
|
||||||
}
|
|
||||||
if ovhConfig.URL == "" {
|
|
||||||
ovhConfig.URL = "http://www.ovh.com/nic/update?system=dyndns&hostname=SUBDOMAIN&myip=NEWIP"
|
|
||||||
}
|
|
||||||
|
|
||||||
logger = logger.WithField("pkg", "provider-ovh")
|
|
||||||
|
|
||||||
return &ovh{
|
|
||||||
config: ovhConfig,
|
|
||||||
logger: logger,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ovh *ovh) UpdateIP(subdomain, ip string) error {
|
|
||||||
newURL := strings.ReplaceAll(ovh.config.URL, "SUBDOMAIN", subdomain)
|
|
||||||
newURL = strings.ReplaceAll(newURL, "NEWIP", ip)
|
|
||||||
logger := ovh.logger.WithFields(logrus.Fields{
|
|
||||||
"component": "update-ip",
|
|
||||||
"ovh-update-url": newURL,
|
|
||||||
"subdomain": subdomain,
|
|
||||||
"new-ip": ip,
|
|
||||||
})
|
|
||||||
|
|
||||||
// * create GET request
|
|
||||||
req, err := http.NewRequest("GET", newURL, nil)
|
|
||||||
if err != nil {
|
|
||||||
return utils.ErrCreateNewRequest
|
|
||||||
}
|
|
||||||
req.SetBasicAuth(ovh.config.Username, ovh.config.Password)
|
|
||||||
|
|
||||||
// * perform GET request
|
|
||||||
logger.Debugln("calling OVH DynHost to update subdomain IP")
|
|
||||||
c := new(http.Client)
|
|
||||||
resp, err := c.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return utils.ErrUpdateRequest
|
|
||||||
}
|
|
||||||
if resp.StatusCode != 200 {
|
|
||||||
return utils.ErrWrongStatusCode
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -1,92 +0,0 @@
|
||||||
package subdomain
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"net"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/datahearth/ddnsclient/pkg/utils"
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ErrIpLength is thrown when subdomain no or multiples remote IP address
|
|
||||||
var ErrIpLenght = errors.New("zero or more than 1 ips have been found")
|
|
||||||
|
|
||||||
type (
|
|
||||||
PendingSubdomains map[time.Time]Subdomain
|
|
||||||
subdomain struct {
|
|
||||||
logger logrus.FieldLogger
|
|
||||||
subdomainAddr string
|
|
||||||
ip string
|
|
||||||
}
|
|
||||||
Subdomain interface {
|
|
||||||
CheckIPAddr(srvIP string) (bool, error)
|
|
||||||
GetSubdomainIP() string
|
|
||||||
retrieveSubdomainIP() error
|
|
||||||
GetSubdomainAddr() string
|
|
||||||
SubIsPending(sbs PendingSubdomains) bool
|
|
||||||
FindSubdomain(sbs PendingSubdomains) Subdomain
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// NewSubdomain instanciate a new http implementation
|
|
||||||
func NewSubdomain(logger logrus.FieldLogger, subdomainAddr string) (Subdomain, error) {
|
|
||||||
if logger == nil {
|
|
||||||
return nil, utils.ErrNilLogger
|
|
||||||
}
|
|
||||||
|
|
||||||
logger = logger.WithField("pkg", "subdomain")
|
|
||||||
|
|
||||||
return &subdomain{
|
|
||||||
logger: logger,
|
|
||||||
subdomainAddr: subdomainAddr,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// RetrieveSubdomainIP will retrieve the subdomain IP with a HEAD request
|
|
||||||
func (sd *subdomain) retrieveSubdomainIP() error {
|
|
||||||
ips, err := net.LookupIP(sd.subdomainAddr)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(ips) != 1 {
|
|
||||||
return ErrIpLenght
|
|
||||||
}
|
|
||||||
|
|
||||||
ip := ips[0].String()
|
|
||||||
if strings.Contains(ip, ":") {
|
|
||||||
ip, _, err = net.SplitHostPort(ip)
|
|
||||||
if err != nil {
|
|
||||||
return utils.ErrSplitAddr
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sd.ip = ip
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// CheckIPAddr will compare the server IP and the subdomain IP
|
|
||||||
func (sd *subdomain) CheckIPAddr(srvIP string) (bool, error) {
|
|
||||||
if err := sd.retrieveSubdomainIP(); err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if srvIP != sd.ip {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetSubdomainIP returns the subdomain IP
|
|
||||||
func (sd *subdomain) GetSubdomainIP() string {
|
|
||||||
return sd.ip
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetSubdomainAddr returns the subdomain address
|
|
||||||
func (sd *subdomain) GetSubdomainAddr() string {
|
|
||||||
return sd.subdomainAddr
|
|
||||||
}
|
|
|
@ -1,52 +0,0 @@
|
||||||
package subdomain
|
|
||||||
|
|
||||||
import "time"
|
|
||||||
|
|
||||||
// SubIsPending check if the current subdomain is waiting the DNS propagation.
|
|
||||||
func (sb *subdomain) SubIsPending(sbs PendingSubdomains) bool {
|
|
||||||
for _, sub := range sbs {
|
|
||||||
if sb == sub {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// FindSubdomain returns a subdomain found in the pending map of subdomain.
|
|
||||||
// If not found, it returns nil.
|
|
||||||
func (sb *subdomain) FindSubdomain(sbs PendingSubdomains) Subdomain {
|
|
||||||
for _, sub := range sbs {
|
|
||||||
if sub == sb {
|
|
||||||
return sb
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// CheckPendingSubdomains check if any pending subdomains are waiting to be restored.
|
|
||||||
// If so, it/they will be returned as a slice.
|
|
||||||
// If not, it returns nil.
|
|
||||||
func CheckPendingSubdomains(sbs PendingSubdomains, now time.Time) PendingSubdomains {
|
|
||||||
delSbs := make(PendingSubdomains)
|
|
||||||
for t, sb := range sbs {
|
|
||||||
if t.Add(5 * time.Minute).Before(now) {
|
|
||||||
delSbs[t] = sb
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(delSbs) < 1 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return delSbs
|
|
||||||
}
|
|
||||||
|
|
||||||
func DeletePendingSubdomains(delSbs PendingSubdomains, pending PendingSubdomains) PendingSubdomains {
|
|
||||||
for t := range delSbs {
|
|
||||||
delete(pending, t)
|
|
||||||
}
|
|
||||||
|
|
||||||
return pending
|
|
||||||
}
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
package utils
|
||||||
|
|
||||||
|
type ClientConfig struct {
|
||||||
|
Logger Logger `mapstructure:"logger"`
|
||||||
|
Watchers []Watcher `mapstructure:"watchers"`
|
||||||
|
UpdateTime int `mapstructure:"update-time,omitempty"`
|
||||||
|
PendingDnsPropagation int `mapstructure:"pending-dns-propagation,omitempty"`
|
||||||
|
WebIP string `mapstructure:"web-ip,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Logger struct {
|
||||||
|
Level string `mapstructure:"level"`
|
||||||
|
DisableTimestamp bool `mapstructure:"disable-timestamp,omitempty"`
|
||||||
|
DisableColor bool `mapstructure:"disable-color,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Watcher struct {
|
||||||
|
Name string `yaml:"name"`
|
||||||
|
URL string `yaml:"url,omitempty"`
|
||||||
|
Config []Config `yaml:"config"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Username string `yaml:"username"`
|
||||||
|
Password string `yaml:"password"`
|
||||||
|
Subdomains []string `yaml:"subdomains"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var DefaultURLs = map[string]string{
|
||||||
|
"ovh": "http://www.ovh.com/nic/update?system=dyndns&hostname=SUBDOMAIN&myip=NEWIP",
|
||||||
|
"google": "https://domains.google.com/nic/update?hostname=SUBDOMAIN&myip=NEWIP",
|
||||||
|
"webIP": "http://dynamicdns.park-your-domain.com/getip",
|
||||||
|
}
|
|
@ -8,18 +8,28 @@ import (
|
||||||
var (
|
var (
|
||||||
// ErrNilLogger is thrown when the parameter logger is nil
|
// ErrNilLogger is thrown when the parameter logger is nil
|
||||||
ErrNilLogger = errors.New("logger is mandatory")
|
ErrNilLogger = errors.New("logger is mandatory")
|
||||||
// ErrNilProvider ...
|
|
||||||
ErrNilProvider = errors.New("provider is mandatory")
|
|
||||||
// ErrWrongStatusCode is thrown when the response status code isn't a 200
|
// ErrWrongStatusCode is thrown when the response status code isn't a 200
|
||||||
ErrWrongStatusCode = errors.New("web-ip returns a non 200 status code")
|
ErrWrongStatusCode = errors.New("web-ip returns a non 200 status code")
|
||||||
// ErrGetServerIP is thrown when HTTP can't contact the web-ip service
|
// ErrGetServerIP is thrown when HTTP can't contact the web-ip service
|
||||||
ErrGetServerIP = errors.New("HTTP error")
|
ErrGetServerIP = errors.New("HTTP error")
|
||||||
// ErrParseHTTPBody is thrown when the HTTP service can't parse the body response
|
// ErrParseHTTPBody is thrown when the HTTP service can't parse the body response
|
||||||
ErrParseHTTPBody = errors.New("can't parse response body")
|
ErrParseHTTPBody = errors.New("can't parse response body")
|
||||||
// ErrSplitAddr ...
|
// ErrSplitAddr is thrown when the remote address can't be splitted
|
||||||
ErrSplitAddr = errors.New("can't split subdomain remote IP address")
|
ErrSplitAddr = errors.New("can't split subdomain remote IP address")
|
||||||
// ErrCreateNewRequest ...
|
// ErrCreateNewRequest is thrown when http request creation failed
|
||||||
ErrCreateNewRequest = errors.New("can't create http request")
|
ErrCreateNewRequest = errors.New("can't create http request")
|
||||||
// ErrUpdateRequest ...
|
// ErrUpdateRequest is thrown when the update request failed
|
||||||
ErrUpdateRequest = errors.New("failed to set new IP address")
|
ErrUpdateRequest = errors.New("failed to set new IP address")
|
||||||
|
// ErrInvalidURL is thrown when user does not provide a URL and it does not exist in default urls
|
||||||
|
ErrInvalidURL = errors.New("no url was provided")
|
||||||
|
// ErrInvalidName is thrown when provider name was not provided
|
||||||
|
ErrInvalidName = errors.New("no provider name was provided")
|
||||||
|
// ErrReadBody is thrown when body response can't be parsed
|
||||||
|
ErrReadBody = errors.New("failed to read response body")
|
||||||
|
// ErrNilWatcher is thrown when no watcher config was provided
|
||||||
|
ErrNilWatcher = errors.New("watcher is mandatory")
|
||||||
|
// ErrIpLength is thrown when subdomain no or multiples remote IP address
|
||||||
|
ErrIpLenght = errors.New("zero or more than 1 ips have been found")
|
||||||
|
// ErrNilConfig is thrown when an empty config is provided
|
||||||
|
ErrNilConfig = errors.New("config is mandatory")
|
||||||
)
|
)
|
||||||
|
|
|
@ -2,7 +2,9 @@ package utils
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
|
@ -54,18 +56,8 @@ func SetupLogger(logger *logrus.Logger) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func AggregateSubdomains(subdomains []string, domain string) []string {
|
// RetrieveServerIP will use the defined web-ip service to get the server public address
|
||||||
agdSub := make([]string, len(subdomains))
|
|
||||||
for i, sd := range subdomains {
|
|
||||||
agdSub[i] = sd + "." + domain
|
|
||||||
}
|
|
||||||
|
|
||||||
return agdSub
|
|
||||||
}
|
|
||||||
|
|
||||||
// RetrieveServerIP will use the defined web-ip service to get the server public address and save it to the struct
|
|
||||||
func RetrieveServerIP(webIP string) (string, error) {
|
func RetrieveServerIP(webIP string) (string, error) {
|
||||||
// * retrieve client's server IP
|
|
||||||
resp, err := http.Get(webIP)
|
resp, err := http.Get(webIP)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", ErrGetServerIP
|
return "", ErrGetServerIP
|
||||||
|
@ -74,7 +66,6 @@ func RetrieveServerIP(webIP string) (string, error) {
|
||||||
return "", ErrWrongStatusCode
|
return "", ErrWrongStatusCode
|
||||||
}
|
}
|
||||||
|
|
||||||
// * get ip from body
|
|
||||||
d, err := ioutil.ReadAll(resp.Body)
|
d, err := ioutil.ReadAll(resp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", ErrParseHTTPBody
|
return "", ErrParseHTTPBody
|
||||||
|
@ -82,3 +73,25 @@ func RetrieveServerIP(webIP string) (string, error) {
|
||||||
|
|
||||||
return string(d), nil
|
return string(d), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RetrieveSubdomainIP will retrieve the subdomain IP
|
||||||
|
func RetrieveSubdomainIP(addr string) (string, error) {
|
||||||
|
ips, err := net.LookupIP(addr)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(ips) != 1 {
|
||||||
|
return "", ErrIpLenght
|
||||||
|
}
|
||||||
|
|
||||||
|
ip := ips[0].String()
|
||||||
|
if strings.Contains(ip, ":") {
|
||||||
|
ip, _, err = net.SplitHostPort(ip)
|
||||||
|
if err != nil {
|
||||||
|
return "", ErrSplitAddr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ip, nil
|
||||||
|
}
|
||||||
|
|
|
@ -3,8 +3,7 @@ package watcher
|
||||||
import (
|
import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/datahearth/ddnsclient/pkg/providers"
|
"github.com/datahearth/ddnsclient/pkg/provider"
|
||||||
"github.com/datahearth/ddnsclient/pkg/subdomain"
|
|
||||||
"github.com/datahearth/ddnsclient/pkg/utils"
|
"github.com/datahearth/ddnsclient/pkg/utils"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
@ -14,58 +13,47 @@ type Watcher interface {
|
||||||
}
|
}
|
||||||
|
|
||||||
type watcher struct {
|
type watcher struct {
|
||||||
logger logrus.FieldLogger
|
logger logrus.FieldLogger
|
||||||
provider providers.Provider
|
providers []provider.Provider
|
||||||
subdomains []subdomain.Subdomain
|
firstRun bool
|
||||||
pendingSubdomains subdomain.PendingSubdomains
|
webIP string
|
||||||
firstRun bool
|
providerName string
|
||||||
pendingDnsPropagation int
|
|
||||||
webIP string
|
|
||||||
providerName string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewWatcher creates a watcher a given provider and its subdomains
|
// NewWatcher creates a watcher a given provider config
|
||||||
func NewWatcher(logger logrus.FieldLogger, provider providers.Provider, sbs []string, webIP, providerName string, pendingDnsPropagation int) (Watcher, error) {
|
func NewWatcher(logger logrus.FieldLogger, w *utils.Watcher, webIP string) (Watcher, error) {
|
||||||
if logger == nil {
|
if logger == nil {
|
||||||
return nil, utils.ErrNilLogger
|
return nil, utils.ErrNilLogger
|
||||||
}
|
}
|
||||||
if provider == nil {
|
if w == nil {
|
||||||
return nil, utils.ErrNilProvider
|
return nil, utils.ErrNilWatcher
|
||||||
}
|
}
|
||||||
if webIP == "" {
|
if webIP == "" {
|
||||||
webIP = "http://dynamicdns.park-your-domain.com/getip"
|
webIP = utils.DefaultURLs["webIP"]
|
||||||
}
|
}
|
||||||
if pendingDnsPropagation == 0 {
|
|
||||||
pendingDnsPropagation = 180
|
|
||||||
}
|
|
||||||
logger = logger.WithField("pkg", "watcher")
|
|
||||||
|
|
||||||
subdomains := make([]subdomain.Subdomain, len(sbs))
|
providers := []provider.Provider{}
|
||||||
for i, sb := range sbs {
|
for _, c := range w.Config {
|
||||||
sub, err := subdomain.NewSubdomain(logger, sb)
|
p, err := provider.NewProvider(logger, c, w.URL, w.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
providers = append(providers, p)
|
||||||
subdomains[i] = sub
|
|
||||||
}
|
}
|
||||||
|
logger = logger.WithField("pkg", "watcher")
|
||||||
|
|
||||||
return &watcher{
|
return &watcher{
|
||||||
logger: logger,
|
logger: logger,
|
||||||
provider: provider,
|
providers: providers,
|
||||||
subdomains: subdomains,
|
webIP: webIP,
|
||||||
webIP: webIP,
|
firstRun: true,
|
||||||
firstRun: true,
|
providerName: w.Name,
|
||||||
pendingSubdomains: make(map[time.Time]subdomain.Subdomain),
|
|
||||||
pendingDnsPropagation: pendingDnsPropagation,
|
|
||||||
providerName: providerName,
|
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *watcher) Run(t *time.Ticker, chClose chan struct{}, chErr chan error) {
|
func (w *watcher) Run(t *time.Ticker, chClose chan struct{}, chErr chan error) {
|
||||||
logger := w.logger.WithField("component", "Run")
|
logger := w.logger.WithField("component", "Run")
|
||||||
|
|
||||||
go w.checkPendingSubdomains(chClose)
|
|
||||||
if w.firstRun {
|
if w.firstRun {
|
||||||
if err := w.runDDNSCheck(); err != nil {
|
if err := w.runDDNSCheck(); err != nil {
|
||||||
chErr <- err
|
chErr <- err
|
||||||
|
@ -77,7 +65,7 @@ func (w *watcher) Run(t *time.Ticker, chClose chan struct{}, chErr chan error) {
|
||||||
select {
|
select {
|
||||||
case <-chClose:
|
case <-chClose:
|
||||||
t.Stop()
|
t.Stop()
|
||||||
logger.Infoln("Close watcher channel triggered. Ticker stopped")
|
logger.WithField("provider", w.providerName).Infoln("Close watcher channel triggered. Ticker stopped")
|
||||||
return
|
return
|
||||||
case <-t.C:
|
case <-t.C:
|
||||||
if err := w.runDDNSCheck(); err != nil {
|
if err := w.runDDNSCheck(); err != nil {
|
||||||
|
@ -92,64 +80,18 @@ func (w *watcher) runDDNSCheck() error {
|
||||||
|
|
||||||
logger.Infof("Starting [%s] DDNS check...\n", w.providerName)
|
logger.Infof("Starting [%s] DDNS check...\n", w.providerName)
|
||||||
|
|
||||||
|
logger.Debugln("Checking server IP...")
|
||||||
srvIP, err := utils.RetrieveServerIP(w.webIP)
|
srvIP, err := utils.RetrieveServerIP(w.webIP)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
logger.Debugln("Checking server IP...")
|
|
||||||
|
|
||||||
for _, sb := range w.subdomains {
|
for _, p := range w.providers {
|
||||||
if sb.SubIsPending(w.pendingSubdomains) {
|
if err := p.UpdateSubdomains(srvIP); err != nil {
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.Debugf("Checking subdomain %s...\n", sb.GetSubdomainAddr())
|
|
||||||
ok, err := sb.CheckIPAddr(srvIP)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
subAddr := sb.GetSubdomainAddr()
|
|
||||||
if !ok {
|
|
||||||
logger.WithFields(logrus.Fields{
|
|
||||||
"server-ip": srvIP,
|
|
||||||
"subdomain-address": subAddr,
|
|
||||||
}).Infoln("IP addresses doesn't match. Updating subdomain's ip...")
|
|
||||||
if err := w.provider.UpdateIP(subAddr, srvIP); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
logger.WithFields(logrus.Fields{
|
|
||||||
"server-ip": srvIP,
|
|
||||||
"subdomain-address": subAddr,
|
|
||||||
}).Infoln("Subdomain's ip updated! Removing from checks for 5 mins")
|
|
||||||
|
|
||||||
w.pendingSubdomains[time.Now()] = sb
|
|
||||||
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.Debugf("%s is up to date. \n", subAddr)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Infof("[%s] DDNS check finished\n", w.providerName)
|
logger.Infof("[%s] DDNS check finished\n", w.providerName)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *watcher) checkPendingSubdomains(chClose chan struct{}) {
|
|
||||||
logger := w.logger.WithField("component", "checkPendingSubdomains")
|
|
||||||
t := time.NewTicker(time.Second * time.Duration(w.pendingDnsPropagation))
|
|
||||||
|
|
||||||
logger.Debugln("Start checking for pending subdomains...")
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-chClose:
|
|
||||||
logger.Debugln("Close pending subdomains")
|
|
||||||
return
|
|
||||||
case <-t.C:
|
|
||||||
logger.Debugln("Checking pending subdomains...")
|
|
||||||
if delSbs := subdomain.CheckPendingSubdomains(w.pendingSubdomains, time.Now()); delSbs != nil {
|
|
||||||
w.pendingSubdomains = subdomain.DeletePendingSubdomains(delSbs, w.pendingSubdomains)
|
|
||||||
logger.Debugln("Pendings subdomains found. Cleaned.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
Loading…
Reference in New Issue