refactored providers setup, added porkbun

This commit is contained in:
emilis 2022-12-29 12:59:48 +00:00
parent 7fd7334ee7
commit ab0de2b77f
6 changed files with 208 additions and 24 deletions

26
main.go
View File

@ -5,31 +5,25 @@ import (
"fmt"
"io"
"io/ioutil"
"ncddns/mod"
"ncddns/providers/gandi"
"ncddns/providers/namecheap"
"ncddns/providers/porkbun"
"net/http"
"os"
"strings"
)
const (
baseURL = "https://dynamicdns.park-your-domain.com/update?"
)
var (
byName = map[string]IPSetter{
"namecheap": NameCheap{},
"gandi": Gandi{},
"namecheap": namecheap.Client{},
"gandi": gandi.Client{},
"porkbun": porkbun.Client{},
}
)
type IPSetRequest struct {
IP string
Domain string
Password string
Host string
}
type IPSetter interface {
IPSet([]IPSetRequest) error
IPSet([]mod.IPSetRequest) error
}
type DomainTuple struct {
@ -83,7 +77,7 @@ func main() {
fatal("config", err)
for _, cfgEntry := range cfg {
fmt.Println(cfgEntry.Domain)
err = cfgEntry.Setter.IPSet([]IPSetRequest{
err = cfgEntry.Setter.IPSet([]mod.IPSetRequest{
{
IP: ip,
Domain: cfgEntry.Domain,
@ -113,5 +107,5 @@ func IP() (string, error) {
return "", fmt.Errorf("read body: %w", err)
}
return string(body), nil
return strings.TrimSpace(string(body)), nil
}

51
mod/mod.go Normal file
View File

@ -0,0 +1,51 @@
package mod
import (
"encoding/json"
"flag"
"fmt"
"io"
)
var (
Verbose bool
)
func init() {
flag.BoolVar(&Verbose, "v", false, "verbose")
}
type IPSetRequest struct {
IP string
Domain string
Password string
Host string
}
func MustJSON(v any) []byte {
bytes, err := json.Marshal(v)
if err != nil {
panic(err)
}
return bytes
}
func TryBody(body io.Reader) string {
content, err := io.ReadAll(body)
if err != nil {
return fmt.Sprintf("failed reading body: %s", err.Error())
}
return string(content)
}
func Verboseln(args ...any) {
if Verbose {
fmt.Println(args...)
}
}
func Verbosefln(format string, args ...any) {
if Verbose {
fmt.Printf(format+"\n", args...)
}
}

View File

@ -1,7 +1,8 @@
package main
package gandi
import (
"fmt"
"ncddns/mod"
"github.com/go-gandi/go-gandi"
"github.com/go-gandi/go-gandi/config"
@ -25,9 +26,9 @@ var (
}
)
type Gandi struct{}
type Client struct{}
func (g Gandi) IPSet(reqs []IPSetRequest) error {
func (g Client) IPSet(reqs []mod.IPSetRequest) error {
if len(reqs) == 0 {
return nil
}
@ -53,7 +54,7 @@ func (g Gandi) IPSet(reqs []IPSetRequest) error {
return err
}
func (Gandi) EnsureAllDomainsAreSame(reqs []IPSetRequest) error {
func (Client) EnsureAllDomainsAreSame(reqs []mod.IPSetRequest) error {
first := reqs[0].Domain
for _, dom := range reqs {
if first != dom.Domain {

View File

@ -1,16 +1,21 @@
package main
package namecheap
import (
"fmt"
"io"
"ncddns/mod"
"net/http"
"net/url"
"os"
)
type NameCheap struct{}
const (
baseURL = "https://dynamicdns.park-your-domain.com/update?"
)
func (nc NameCheap) IPSet(reqs []IPSetRequest) error {
type Client struct{}
func (nc Client) IPSet(reqs []mod.IPSetRequest) error {
for _, req := range reqs {
if err := nc.SetSingle(req); err != nil {
return fmt.Errorf("%s: %w", req.Domain, err)
@ -19,7 +24,7 @@ func (nc NameCheap) IPSet(reqs []IPSetRequest) error {
return nil
}
func (NameCheap) SetSingle(req IPSetRequest) error {
func (Client) SetSingle(req mod.IPSetRequest) error {
params := url.Values{}
params.Add("host", req.Host)
params.Add("domain", req.Domain)

View File

@ -0,0 +1,36 @@
package porkbun
type EditRequest struct {
Request
Name string `json:"name"`
Type string `json:"type"`
IP string `json:"content"`
TTL string `json:"ttl"`
}
func (EditRequest) FromRecord(r Record, keys Request) EditRequest {
return EditRequest{
Request: keys,
Name: r.Name,
Type: r.Type,
IP: r.IP,
TTL: "300",
}
}
type retrieveResponse struct {
Status string `json:"status"`
Records []Record `json:"records"`
}
type Request struct {
APIKey string `json:"apikey"`
Secret string `json:"secretapikey"`
}
type Record struct {
ID string `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
IP string `json:"content"`
}

View File

@ -0,0 +1,97 @@
package porkbun
import (
"bytes"
"encoding/json"
"fmt"
"ncddns/mod"
"net/http"
"strings"
)
const (
baseURL = "https://porkbun.com/api/json/v3"
retrieve = "/dns/retrieve/"
edit = "/dns/edit/"
keySplitter = ":"
)
type Client struct {
}
func (Client) retrieve(domain string, keys Request) ([]Record, error) {
url := baseURL + retrieve + domain
mod.Verboseln("POST", url)
resp, err := http.Post(url, "application/json", bytes.NewBuffer(mod.MustJSON(keys)))
if err != nil {
return nil, fmt.Errorf("POST retrieve: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("non-ok status code [%d]: %s", resp.StatusCode, mod.TryBody(resp.Body))
}
res := retrieveResponse{}
if err := json.NewDecoder(resp.Body).Decode(&res); err != nil {
return nil, fmt.Errorf("json: %w", err)
}
if res.Status != "SUCCESS" {
return nil, fmt.Errorf("non-SUCCESS status: %s", res.Status)
}
return res.Records, nil
}
func (Client) edit(req EditRequest, id, domain string) error {
url := baseURL + edit + domain + "/" + id
mod.Verboseln("POST", url)
resp, err := http.Post(url, "application/json", bytes.NewReader(mod.MustJSON(req)))
if err != nil {
return fmt.Errorf("POST edit: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("non-ok status code [%d]: %s", resp.StatusCode, mod.TryBody(resp.Body))
}
resp.Body.Close()
return nil
}
func (c Client) IPSet(reqs []mod.IPSetRequest) error {
keyParts := strings.Split(reqs[0].Password, keySplitter)
if len(keyParts) != 2 {
panic(fmt.Sprintf("non-2 length split for password [%s]", reqs[0].Password))
}
keys := Request{
APIKey: keyParts[0],
Secret: keyParts[1],
}
var err error
domainRecords := map[string][]Record{}
for _, req := range reqs {
records, ok := domainRecords[req.Domain]
if !ok {
records, err = c.retrieve(req.Domain, keys)
if err != nil {
return fmt.Errorf("[%s]: retrieve: %w", req.Domain, err)
}
domainRecords[req.Domain] = records
}
for _, record := range records {
if record.Type != "A" && record.Type != "AAAA" {
mod.Verbosefln("Skipping [%s/%s/%s] due to type [%s]", record.IP, record.Name, record.IP, record.Type)
continue
}
mod.Verbosefln("Current IP for [%s/%s]: [%s], want [%s]", req.Domain, record.Type, record.IP, req.IP)
if req.IP == record.IP {
mod.Verboseln("Skipping...")
continue
}
edit := EditRequest{}.FromRecord(record, keys)
edit.IP = req.IP
if err := c.edit(edit, record.ID, req.Domain); err != nil {
return fmt.Errorf("[%s]: edit: %w", req.Domain, err)
}
}
}
return nil
}