diff --git a/README.md b/README.md index b0c5a83..53df51e 100644 --- a/README.md +++ b/README.md @@ -32,10 +32,10 @@ dynamic dns needs. * Multi domain support * Subdomain support * TTL update support -* Creation of a DNS record if it doesn't already exists. +* Creation of a DNS record if it doesn't already exist. * Multi host support (nice when you need to update both `@` and `*`) * IPv6 support -* Verbose option (when you specify `-v` you get alot of information) +* Verbose option (when you specify `-v` you get plenty information) ### Missing @@ -60,7 +60,7 @@ recommended. After that run following commands: go install This will create a binary named `dyndns-netcup-go` and install it to your go -binary home. Make sure your `GOPATH` environment variable is set. +binary home. Make sure your `GOPATH` environment variable is set. Refer to [Usage](#usage) for further information. diff --git a/cache.go b/cache.go new file mode 100644 index 0000000..9359dea --- /dev/null +++ b/cache.go @@ -0,0 +1,180 @@ +package main + +import( + "encoding/csv" + "log" + "io" + "os" + "time" +) + +const ( + defaultDir string = "/dyndns-netcup-go" + defaultIPCache string = "ip.cache" +) + +type Cache struct { + location string + timeout time.Duration + changes bool + entries []CacheEntry +} + +type CacheEntry struct { + host string + ipv4 string + ipv6 string +} + +func NewCache(location string, timeout time.Duration) (*Cache, error) { + if location == "" { + var err error + location, err = os.UserCacheDir() + if err != nil { + return nil, err + } + + location += defaultDir + + if _, err := os.Stat(location); os.IsNotExist(err) { + os.MkdirAll(location, 0700) + } + + location += "/" + defaultIPCache + } + + return &Cache{location, timeout, false, nil}, nil +} + +func (c *Cache) Load() error { + csvfile, err := os.Open(c.location) + defer csvfile.Close() + if err != nil { + if os.IsNotExist(err) { + return nil + } else { + return err + } + } + + fileinfo, err := csvfile.Stat() + if err != nil { + return err + } + + if time.Now().Sub(fileinfo.ModTime()) > c.timeout { + return nil + } + + r := csv.NewReader(csvfile) + + for { + record, err := r.Read() + if err == io.EOF { + break + } + if err != nil { + return err + } + + entry := CacheEntry{ + host : record[0], + ipv4 : record[1], + ipv6 : record[2], + } + + c.entries = append(c.entries, entry) + } + + return nil +} + +func (c *Cache) SetIPv4(domain, host, ipv4 string) { + entry := c.getEntry(domain, host) + if entry == nil { + newEntry := CacheEntry { + host: host+domain, + ipv4: ipv4, + ipv6: "", + } + + c.entries = append(c.entries, newEntry) + } else { + entry.ipv4 = ipv4 + } + + c.changes = true +} + +func (c *Cache) SetIPv6(domain, host, ipv6 string) { + entry := c.getEntry(domain, host) + if entry == nil { + newEntry := CacheEntry { + host: host+domain, + ipv4: "", + ipv6: ipv6, + } + + c.entries = append(c.entries, newEntry) + } else { + entry.ipv6 = ipv6 + } + + c.changes = true +} + +func (c *Cache) GetIPv4(domain, host string) string { + entry := c.getEntry(domain, host) + if entry == nil { + return "" + } + + return entry.ipv4 +} + +func (c *Cache) GetIPv6(domain, host string) string { + entry := c.getEntry(domain, host) + if entry == nil { + return "" + } + + return entry.ipv6 +} + +func (c *Cache) getEntry(domain, host string) *CacheEntry { + for i, entry := range c.entries { + if entry.host == (host+domain) { + return &c.entries[i] + } + } + + return nil +} + +func (c *Cache) Store() error { + if !c.changes { + return nil + } + + csvfile, err := os.Create(c.location) + if err != nil { + return err + } + + writer := csv.NewWriter(csvfile) + defer writer.Flush() + + for _, entry := range c.entries { + err = writer.Write(entry.toArray()) + log.Printf("Written %s, %s, %s to %s", entry.host, entry.ipv4, entry.ipv6, c.location) + if err != nil { + return err + } + } + + return nil +} + +func (e *CacheEntry) toArray() []string { + return []string{e.host, e.ipv4, e.ipv6} +} diff --git a/config.go b/config.go index a6f263b..3cd6390 100644 --- a/config.go +++ b/config.go @@ -9,6 +9,8 @@ type Config struct { CustomerNumber int `yaml:"CUSTOMERNR"` ApiKey string `yaml:"APIKEY"` ApiPassword string `yaml:"APIPASSWORD"` + IPCache string `yaml:"IP-CACHE"` + IPCacheTimeout int `yaml:"IP-CACHE-TIMEOUT"` Domains []Domain `yaml:"DOMAINS"` } diff --git a/main.go b/main.go index f9c1f2d..17d6d38 100644 --- a/main.go +++ b/main.go @@ -5,6 +5,7 @@ import( "strconv" "log" "flag" + "time" ) var ( @@ -14,6 +15,7 @@ var ( ipv6 string verbose bool client *netcup.Client + cache *Cache ) const ( @@ -29,6 +31,13 @@ func main() { loadIPv6() configureDomains() + + if cache != nil { + err := cache.Store() + if err != nil { + log.Fatal(err) + } + } } func init() { @@ -47,6 +56,20 @@ func init() { if err != nil { log.Fatal(err) } + + if config.IPCacheTimeout > 0 { + cache, err = NewCache(config.IPCache, time.Duration(config.IPCacheTimeout) * time.Second) + if err != nil { + logWarning("Cannot aquire cachefile: " + err.Error()) + } else { + err = cache.Load() + if err != nil { + log.Fatal(err) + } + } + + } + } func login() { @@ -80,9 +103,42 @@ func loadIPv6() { func configureDomains() { for _, domain := range config.Domains { - configureZone(domain) - configureRecords(domain) + if needsUpdate(domain) { + configureZone(domain) + configureRecords(domain) + } } + +} + +func needsUpdate(domain Domain) bool { + if cache == nil { + return true + } + + update := false + + for _, host := range domain.Hosts { + hostIPv4 := cache.GetIPv4(domain.Name, host) + if hostIPv4 == "" || hostIPv4 != ipv4 { + cache.SetIPv4(domain.Name, host, ipv4) + update = true + } + + if domain.IPv6 { + hostIPv6 := cache.GetIPv6(domain.Name, host) + if hostIPv6 == "" || hostIPv6 != ipv6 { + cache.SetIPv6(domain.Name, host, ipv6) + update = true + } + } + + if !update { + logInfo("Host %s is in cache and needs no update", host) + } + } + + return update } func configureZone(domain Domain) { @@ -194,3 +250,7 @@ func logInfo(msg string, v ...interface{}) { log.Printf(msg, v...) } } + +func logWarning(msg string, v ...interface{}) { + log.Printf("[Warning]: " + msg, v...) +} diff --git a/tags b/tags new file mode 100644 index 0000000..131db2b --- /dev/null +++ b/tags @@ -0,0 +1,69 @@ +!_TAG_FILE_FORMAT 2 /extended format; --format=1 will not append ;" to lines/ +!_TAG_FILE_SORTED 1 /0=unsorted, 1=sorted, 2=foldcase/ +!_TAG_PROGRAM_AUTHOR Darren Hiebert /dhiebert@users.sourceforge.net/ +!_TAG_PROGRAM_NAME Exuberant Ctags // +!_TAG_PROGRAM_URL http://ctags.sourceforge.net /official site/ +!_TAG_PROGRAM_VERSION 5.9~svn20110310 // +AddParam netcup/request.go /^func (p Params) AddParam(key string, value interface{}) {$/;" f +Client netcup/client.go /^type Client struct {$/;" t +Config config.go /^type Config struct {$/;" t +DNSRecord netcup/response.go /^type DNSRecord struct {$/;" t +DNSRecordSet netcup/response.go /^type DNSRecordSet struct {$/;" t +DNSZone netcup/response.go /^type DNSZone struct {$/;" t +Domain config.go /^type Domain struct {$/;" t +ErrNoApiSessionid netcup/client.go /^ ErrNoApiSessionid = errors.New("netcup: There is no ApiSessionId. Are you logged in?")$/;" v +GetARecordOccurences netcup/response.go /^func (r *DNSRecordSet) GetARecordOccurences(hostname string) int {$/;" f +GetRecord netcup/response.go /^func (r *DNSRecordSet) GetRecord(name, dnstype string) (*DNSRecord, bool) {$/;" f +InfoDnsRecords netcup/client.go /^func (c *Client) InfoDnsRecords(domainname string) (error, *DNSRecordSet) {$/;" f +InfoDnsZone netcup/client.go /^func (c *Client) InfoDnsZone(domainname string) (error, *DNSZone) {$/;" f +LoadConfig config.go /^func LoadConfig(filename string) (*Config, error) {$/;" f +Login netcup/client.go /^func (c *Client) Login() error {$/;" f +LoginResponse netcup/response.go /^type LoginResponse struct {$/;" t +NewClient netcup/client.go /^func NewClient(customernumber int, apikey, apipassword string) *Client {$/;" f +NewDNSRecord netcup/response.go /^func NewDNSRecord(hostname, dnstype, destination string) *DNSRecord {$/;" f +NewDNSRecordSet netcup/response.go /^func NewDNSRecordSet(records []DNSRecord) *DNSRecordSet {$/;" f +NewParams netcup/request.go /^func NewParams() Params {$/;" f +NewRequest netcup/request.go /^func NewRequest(action string, params *Params) *Request {$/;" f +Params netcup/request.go /^type Params map[string]interface{}$/;" t +Request netcup/request.go /^type Request struct {$/;" t +Response netcup/response.go /^type Response struct {$/;" t +SetVerbose netcup/client.go /^func SetVerbose(isVerbose bool) {$/;" f +UpdateDnsRecords netcup/client.go /^func (c *Client) UpdateDnsRecords(domainname string, dnsRecordSet *DNSRecordSet) error {$/;" f +UpdateDnsZone netcup/client.go /^func (c *Client) UpdateDnsZone(domainname string, dnszone *DNSZone) error {$/;" f +basicAuthParams netcup/client.go /^func (c *Client) basicAuthParams(domainname string) *Params {$/;" f +client main.go /^ client *netcup.Client$/;" v +config main.go /^ config *Config$/;" v +configFile main.go /^ configFile string$/;" v +configUsage main.go /^ configUsage = "Specify location of the config file"$/;" c +configureARecord main.go /^func configureARecord(host string, records *netcup.DNSRecordSet) *netcup.DNSRecord {$/;" f +configureDomains main.go /^func configureDomains() {$/;" f +configureRecords main.go /^func configureRecords(domain Domain) {$/;" f +configureZone main.go /^func configureZone(domain Domain) {$/;" f +defaultConfigFile main.go /^ defaultConfigFile = "config.yml"$/;" c +do ip.go /^func do(url string) (string, error) {$/;" f +do netcup/client.go /^func (c *Client) do(req *Request) (*Response, error) {$/;" f +getFormattedError netcup/response.go /^func (r *Response) getFormattedError() string {$/;" f +getFormattedStatus netcup/response.go /^func (r *Response) getFormattedStatus() string {$/;" f +getIPv4 ip.go /^func getIPv4() (string, error) {$/;" f +getIPv6 ip.go /^func getIPv6() (string, error) {$/;" f +init main.go /^func init() {$/;" f +ipv4 main.go /^ ipv4 string$/;" v +ipv6 main.go /^ ipv6 string$/;" v +isError netcup/response.go /^func (r *Response) isError() bool {$/;" f +isSuccess netcup/response.go /^func (r *Response) isSuccess() bool {$/;" f +loadIPv4 main.go /^func loadIPv4() {$/;" f +loadIPv6 main.go /^func loadIPv6() {$/;" f +logInfo main.go /^func logInfo(msg string, v ...interface{}) {$/;" f +logInfo netcup/client.go /^func logInfo(msg string, v ...interface{}) {$/;" f +login main.go /^func login() {$/;" f +main config.go /^package main$/;" p +main ip.go /^package main$/;" p +main main.go /^func main() {$/;" f +main main.go /^package main$/;" p +netcup netcup/client.go /^package netcup$/;" p +netcup netcup/request.go /^package netcup$/;" p +netcup netcup/response.go /^package netcup$/;" p +url netcup/client.go /^ url = "https:\/\/ccp.netcup.net\/run\/webservice\/servers\/endpoint.php?JSON"$/;" c +verbose main.go /^ verbose bool$/;" v +verbose netcup/client.go /^ verbose = false$/;" v +verboseUsage main.go /^ verboseUsage = "Use verbose output"$/;" c