package api import ( "context" "errors" "fmt" "log/slog" "net" "slices" "strings" "github.com/maximotejeda/ddns/internal/adapter/checker" prov "github.com/maximotejeda/ddns/internal/adapter/provider" "github.com/maximotejeda/ddns/internal/application/core/domain" "github.com/maximotejeda/ddns/internal/application/core/domain/cf" "github.com/maximotejeda/ddns/internal/application/core/helpers" "github.com/maximotejeda/ddns/internal/port" ) type Application struct { ctx context.Context log *slog.Logger client port.Provider publicIP *net.IP zoneRecords []cf.Result Domains map[string]domain.DomainRecords } // NewApplication // Returns an instance to a new app with a provider client // Needs some info from the account to work on // Params: // // provider: provider client to interact with // toke: token to authorize actions on provider // id: id of user or zoner where to work with func NewApplication(ctx context.Context, log *slog.Logger, provider, token, id string) (app *Application, err error) { log.Debug("creating new appication") var pClient port.Provider switch provider { case "cf": pClient = prov.NewCloudflareClient(ctx, log, id, token) } logApp := log.With("location", "[Application]") app = &Application{ ctx: ctx, log: logApp, client: pClient, } return app, nil } // Show // Print in stdout the dns records for zone func (app *Application) Show(name string) { app.log.Debug("calling show") fmt.Printf("%22s %5s \t%14s \t%s\n\n", "name", "type", "ip", "ID") for _, it := range app.zoneRecords { subDomainName := strings.ReplaceAll(strings.ReplaceAll(it.Name, name, ""), ".", "") if subDomainName == "" { subDomainName = "*root*" } fmt.Printf("%+22s %5s \t%14s \t%s\n", subDomainName, it.Type, it.Content, it.ID) } checkClient := checker.NewClient(app.log, "cf") ip, err := checkClient.Do() if err != nil { app.log.Error("[checker] ip", "error", err) } fmt.Printf("\nrequester IP => %s\n", ip) } // Details ... // Print details on a given record func (app *Application) Details(id string) { app.log.Debug("calling details") resp, err := app.client.Details(id) if err != nil { app.log.Error("[datails]", "error", err) } fmt.Printf(` Zone Name: %s - ID: %s - Domain Name: %s Type: %s - IP: %s - Proxied: %t Comment: %s Created On: %s Modified On: %s Meta: %#v Tags: %#v TTL: %d `, resp.Result.ZoneName, resp.Result.ID, resp.Result.Name, resp.Result.Type, resp.Result.Content, resp.Result.Proxied, resp.Result.Comment, resp.Result.CreatedOn, resp.Result.ModifiedOn, resp.Result.Meta, resp.Result.Tags, resp.Result.TTL) } // List func (app *Application) List() { resp, err := app.client.List() if err != nil { app.log.Error("[Show]", "error", err) panic(err) } app.zoneRecords = resp.Result } // Update func (app *Application) Update(re *cf.Result, rBody cf.RequestBody) { rBody.Comment = "updating from app cli tool" res, err := app.client.Update(re.ID, &rBody) if err != nil { app.log.Error("[Update]", "error", err) } app.log.Debug("updating", "record", re.Name, "IP", rBody.Content, "Type", re.Type, "ID", "re.ID") app.log.Info("response", "success", res.Success, "errors", res.Errors) } // Create func (app *Application) Create(rBody cf.RequestBody) (res *cf.DetailsResult, err error) { rBody.Comment = "Creating from app cli tool" res, err = app.client.Create(&rBody) if err != nil { app.log.Error("[Create]", "error", err) } return res, err } // Overwrite func (app *Application) Overwrite(re *cf.Result, rBody *cf.RequestBody) { rBody.Comment = rBody.Comment + " Overwrite from app cli tool" res, err := app.client.Overwrite(*rBody, re.ID) if err != nil { app.log.Error("[Overwrite]", "error", err) } fmt.Println("Overwrite:") fmt.Printf("%v", res) } // Delete func (app Application) Delete(re *cf.Result) { res, err := app.client.Delete(re.ID) if err != nil { app.log.Error("[Delete]", "error", err) } fmt.Println("Delete: ") fmt.Printf("%v", res) } // Operation // expose required func (app *Application) Operation(op string, name, tipo, ipSTR,rID, comment string, proxied bool) { var ( res *checker.Response err error rBody *cf.RequestBody ) // check current ip func() { checker := checker.NewClient(app.log, "cf") res, err = checker.Do() if err != nil { app.log.Error("checking public ip", "error", err) panic(err) } }() if ipSTR == "" { app.publicIP = res.IP tipo = res.Type } else { ip1 := net.ParseIP(ipSTR) app.publicIP = &ip1 if ip1.To4() != nil { tipo = "A" } else if ip1.To16() != nil { tipo = "AAA" } } // populate struct with dns records // and populate subdomains inside each domain app.List() app.Domains, _ = helpers.SubdomainIdentify(name, app.zoneRecords) dn := helpers.SelectDomain(name, app.Domains) // will always be available if subdomain is valid rs := app.SelectRecord(dn, tipo) if rs == nil { app.log.Debug("record not selected", "ip", ipSTR, "name", name, "type", tipo) } //fmt.Println(dn, rs, app.zoneRecords) app.Domains, _ = helpers.SubdomainIdentify(name, app.zoneRecords) if res == nil { fmt.Printf("%#v", app.Domains) panic("record does not exist") } switch op { case "update": if name == "" { panic("name cant be empty for op update") } if rs == nil { if dn == "*" { for _, rec := range app.zoneRecords { if rec.Content != app.publicIP.String() { rBody = app.GenerateReqBody(rec.Name, res.Type, app.publicIP.String(), comment, proxied) app.Update(&rec, *rBody) } else { app.log.Info("same ip for", "record", rec.Name, "DstIP", rec.Content, "NewIP", app.publicIP) } } } else { app.log.Error(fmt.Sprintf("record is not created or active create first: %s -> %s", name, dn)) } } else { if rs.Content != app.publicIP.String() { rBody = app.GenerateReqBody(name, res.Type, app.publicIP.String(),comment, proxied) app.Update(rs, *rBody) } else { app.log.Error("same ip on dns record") } } case "create": // as we can have more than one subdomain/with the same name and diff ip if name == "" { panic("name cant be empty for op create") } fmt.Printf("domain name: %s", dn) rBody = app.GenerateReqBody(dn, tipo, app.publicIP.String(),comment, proxied) app.Create(*rBody) case "delete": if name == "" { panic("name cant be empty for op delete") } if rs == nil { app.log.Error(fmt.Sprintf("could not find record: %s -> %s", name, dn), "operation", "delete", "rs", rs) } else { app.Delete(rs) } case "overwrite": if name == "" { app.log.Error("name cant be empty for op overwrite") } if rs == nil { app.log.Error(fmt.Sprintf("could not find record: %s -> %s", name, dn), "operation", "overwrite") } else { rBody = app.GenerateReqBody(dn, tipo, app.publicIP.String(), comment, proxied) app.Overwrite(rs, rBody) } case "details": if name == "" { app.log.Error("name cant be empty for op details") } if rs != nil { rBody = app.GenerateReqBody(dn, tipo, app.publicIP.String(), comment, proxied) app.Details(rs.ID) } //rBody := app.GenerateReqBody(name, tipo, app.publicIP.String(), proxied) if rs == nil { app.log.Error("record does not exist") } case "show": app.Show(name) default: app.log.Error("unknown operation", "operation", op) panic("unknown operation") } } // SelectRecord func (app *Application) SelectRecord(name, tipo string) (res *cf.Result) { res, _ = app.SelectNameAndType(name, tipo) return res } // SelectNameAndType func (app *Application) SelectNameAndType(name, tipo string) (rec *cf.Result, err error) { i := slices.IndexFunc(app.zoneRecords, func(r cf.Result) bool { return name == r.Name && strings.ToUpper(tipo) == r.Type }) if i >= 0 { return &app.zoneRecords[i], nil } else { return nil, errors.New("record not found") } } func (app *Application) GenerateReqBody(name, tipo, ipSTR, comment string, proxied bool) (rBody *cf.RequestBody) { rBody = &cf.RequestBody{} rBody.Comment = comment rBody.DomainName = name rBody.Type = tipo rBody.Content = ipSTR rBody.Proxied = proxied rBody.TTL = 3600 return rBody }