306 lines
8.0 KiB
Go

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
}