diff --git a/.gitignore b/.gitignore index a6f5b61..0589c01 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,5 @@ dist/ go.work config.yml + +.idea/ \ No newline at end of file diff --git a/config.sample.yml b/config.sample.yml index b5ec589..3004081 100644 --- a/config.sample.yml +++ b/config.sample.yml @@ -1,9 +1,12 @@ gandi: baseurl: https://dns.api.gandi.net/api/v5 - ttl: 300 + ttl: 1800 porkbun: baseurl: https://porkbun.com/api/json/v3 - ttl: 600 + ttl: 1800 +cloudflare: + baseurl: https://api.cloudflare.com/client/v4 + ttl: 1800 api: port: 9595 hideApiKeyInLogs: true diff --git a/internal/api/api.go b/internal/api/api.go index 2a431ac..23de9a1 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -2,6 +2,7 @@ package api import ( "fmt" + "github.com/davidramiro/frigabun/pkg/cloudflare" "net/http" "strings" @@ -78,6 +79,18 @@ func HandleUpdateRequest(c echo.Context) error { if err != nil { return c.String(err.Code, err.Message) } + } else if request.Registrar == "cloudflare" { + dns := &cloudflare.CloudflareDns{ + IP: request.IP, + Domain: request.Domain, + Subdomain: subdomains[i], + ApiKey: request.ApiSecretKey, + ZoneId: request.ApiKey, + } + err := dns.SaveRecord() + if err != nil { + return c.String(err.Code, err.Message) + } } else { return c.String(http.StatusBadRequest, "missing or invalid registrar") } diff --git a/internal/config/config.go b/internal/config/config.go index bb5cb3f..92e5231 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -20,6 +20,11 @@ type Config struct { TTL int `yaml:"ttl"` } `yaml:"porkbun"` + Cloudflare struct { + BaseUrl string `yaml:"baseurl"` + TTL int `yaml:"ttl"` + } `yaml:"cloudflare"` + Api struct { Port string `yaml:"port"` ApiKeyHidden bool `yaml:"hideApiKeyInLogs"` diff --git a/pkg/cloudflare/cloudflare.go b/pkg/cloudflare/cloudflare.go new file mode 100644 index 0000000..56b4179 --- /dev/null +++ b/pkg/cloudflare/cloudflare.go @@ -0,0 +1,175 @@ +package cloudflare + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + + "github.com/davidramiro/frigabun/internal/config" + "github.com/davidramiro/frigabun/internal/logger" +) + +type CloudflareDns struct { + IP string + Domain string + Subdomain string + ZoneId string + ApiKey string +} + +type CloudflareApiRequest struct { + Subdomain string `json:"name"` + Type string `json:"type"` + TTL int `json:"ttl"` + IP string `json:"content"` +} + +type CloudflareQueryResponse struct { + Errors []struct { + Message string `json:"message"` + } `json:"errors"` + Result []struct { + Name string `json:"name"` + Id string `json:"id"` + } `json:"result"` +} + +type CloudflareUpdateError struct { + Code int + Message string +} + +func (c *CloudflareDns) SaveRecord() *CloudflareUpdateError { + + endpoint := fmt.Sprintf("%s/zones/%s/dns_records", config.AppConfig.Cloudflare.BaseUrl, + c.ZoneId) + + req, err := http.NewRequest("GET", endpoint, nil) + + var r CloudflareQueryResponse + + req.Header.Set("Content-Type", "application/json; charset=utf-8") + req.Header.Set("Authorization", "Bearer "+c.ApiKey) + + client := &http.Client{} + + resp, err := client.Do(req) + if err != nil { + return &CloudflareUpdateError{Code: 400, Message: "error getting cloudflare request"} + } + + b, _ := io.ReadAll(resp.Body) + err = json.Unmarshal(b, &r) + + if resp.StatusCode != http.StatusOK || len(r.Errors) > 0 || err != nil { + + logger.Log.Error().Msg("could not query record:" + string(b)) + return &CloudflareUpdateError{400, "could not query record: " + string(b)} + } + + var id string + + if len(r.Errors) == 0 && len(r.Result) > 0 { + for _, e := range r.Result { + if e.Name == c.Subdomain+"."+c.Domain { + id = e.Id + } + } + } + + if len(id) == 0 { + return c.NewRecord() + } else { + return c.UpdateRecord(id) + } +} + +func (c *CloudflareDns) NewRecord() *CloudflareUpdateError { + cloudflareRequest := &CloudflareApiRequest{ + Subdomain: fmt.Sprintf("%s.%s", c.Subdomain, c.Domain), + IP: c.IP, + TTL: config.AppConfig.Cloudflare.TTL, + Type: "A", + } + + endpoint := fmt.Sprintf("%s/zones/%s/dns_records", config.AppConfig.Cloudflare.BaseUrl, + c.ZoneId) + logger.Log.Info().Str("subdomain", cloudflareRequest.Subdomain).Str("endpoint", endpoint).Str("IP", cloudflareRequest.IP).Msg("building update request") + + body, err := json.Marshal(cloudflareRequest) + if err != nil { + logger.Log.Error().Err(err).Msg("marshalling failed") + return &CloudflareUpdateError{Code: 400, Message: "could not parse request"} + } + + req, err := http.NewRequest("POST", endpoint, bytes.NewBuffer(body)) + if err != nil { + logger.Log.Error().Err(err).Msg("building request failed failed") + return &CloudflareUpdateError{Code: 400, Message: "could not create request for cloudflare"} + } + + req.Header.Set("Content-Type", "application/json; charset=utf-8") + req.Header.Set("Authorization", "Bearer "+c.ApiKey) + + client := &http.Client{} + + resp, err := client.Do(req) + if err != nil { + logger.Log.Error().Err(err).Msg("executing request failed") + return &CloudflareUpdateError{Code: 500, Message: "could execute request"} + } + + if resp.StatusCode != 200 { + b, _ := io.ReadAll(resp.Body) + logger.Log.Error().Msg("gandi rejected request") + return &CloudflareUpdateError{Code: resp.StatusCode, Message: "cloudflare rejected request: " + string(b)} + } + + return nil +} + +func (c *CloudflareDns) UpdateRecord(id string) *CloudflareUpdateError { + cloudflareRequest := &CloudflareApiRequest{ + Subdomain: fmt.Sprintf("%s.%s", c.Subdomain, c.Domain), + IP: c.IP, + TTL: config.AppConfig.Cloudflare.TTL, + Type: "A", + } + + endpoint := fmt.Sprintf("%s/zones/%s/dns_records/%s", config.AppConfig.Cloudflare.BaseUrl, + c.ZoneId, id) + logger.Log.Info().Str("subdomain", cloudflareRequest.Subdomain).Str("endpoint", endpoint).Str("IP", cloudflareRequest.IP).Msg("building update request") + + body, err := json.Marshal(cloudflareRequest) + if err != nil { + logger.Log.Error().Err(err).Msg("marshalling failed") + return &CloudflareUpdateError{Code: 400, Message: "could not parse request"} + } + + req, err := http.NewRequest("PUT", endpoint, bytes.NewBuffer(body)) + if err != nil { + logger.Log.Error().Err(err).Msg("building request failed failed") + return &CloudflareUpdateError{Code: 400, Message: "could not create request for cloudflare"} + } + + req.Header.Set("Content-Type", "application/json; charset=utf-8") + req.Header.Set("Authorization", "Bearer "+c.ApiKey) + + client := &http.Client{} + + resp, err := client.Do(req) + if err != nil { + logger.Log.Error().Err(err).Msg("executing request failed") + return &CloudflareUpdateError{Code: 500, Message: "could execute request"} + } + + if resp.StatusCode != 200 { + b, _ := io.ReadAll(resp.Body) + logger.Log.Error().Msg("gandi rejected request") + return &CloudflareUpdateError{Code: resp.StatusCode, Message: "cloudflare rejected request: " + string(b)} + } + + return nil +}