Skip to content

Commit

Permalink
Add ip address cache
Browse files Browse the repository at this point in the history
Fix #1. This commit introduces an optional ip address cache which stores
the optained IPv4 and IPv6 addresses between two runs of the program.
This reduces netword usage as the DNS records from netcup only need to be
fetched when the current ip addresses differ between those from the last
run.
  • Loading branch information
Hentra committed Jun 22, 2020
1 parent ef574a7 commit ee4ac25
Show file tree
Hide file tree
Showing 5 changed files with 316 additions and 5 deletions.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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.

Expand Down
180 changes: 180 additions & 0 deletions cache.go
Original file line number Diff line number Diff line change
@@ -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}
}
2 changes: 2 additions & 0 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
}

Expand Down
64 changes: 62 additions & 2 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import(
"strconv"
"log"
"flag"
"time"
)

var (
Expand All @@ -14,6 +15,7 @@ var (
ipv6 string
verbose bool
client *netcup.Client
cache *Cache
)

const (
Expand All @@ -29,6 +31,13 @@ func main() {
loadIPv6()

configureDomains()

if cache != nil {
err := cache.Store()
if err != nil {
log.Fatal(err)
}
}
}

func init() {
Expand All @@ -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() {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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...)
}
69 changes: 69 additions & 0 deletions tags
Original file line number Diff line number Diff line change
@@ -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 /[email protected]/
!_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

0 comments on commit ee4ac25

Please sign in to comment.