refactored providers setup, added porkbun
This commit is contained in:
parent
7fd7334ee7
commit
ab0de2b77f
26
main.go
26
main.go
|
@ -5,31 +5,25 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
"ncddns/mod"
|
||||||
|
"ncddns/providers/gandi"
|
||||||
|
"ncddns/providers/namecheap"
|
||||||
|
"ncddns/providers/porkbun"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
baseURL = "https://dynamicdns.park-your-domain.com/update?"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
byName = map[string]IPSetter{
|
byName = map[string]IPSetter{
|
||||||
"namecheap": NameCheap{},
|
"namecheap": namecheap.Client{},
|
||||||
"gandi": Gandi{},
|
"gandi": gandi.Client{},
|
||||||
|
"porkbun": porkbun.Client{},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
type IPSetRequest struct {
|
|
||||||
IP string
|
|
||||||
Domain string
|
|
||||||
Password string
|
|
||||||
Host string
|
|
||||||
}
|
|
||||||
|
|
||||||
type IPSetter interface {
|
type IPSetter interface {
|
||||||
IPSet([]IPSetRequest) error
|
IPSet([]mod.IPSetRequest) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type DomainTuple struct {
|
type DomainTuple struct {
|
||||||
|
@ -83,7 +77,7 @@ func main() {
|
||||||
fatal("config", err)
|
fatal("config", err)
|
||||||
for _, cfgEntry := range cfg {
|
for _, cfgEntry := range cfg {
|
||||||
fmt.Println(cfgEntry.Domain)
|
fmt.Println(cfgEntry.Domain)
|
||||||
err = cfgEntry.Setter.IPSet([]IPSetRequest{
|
err = cfgEntry.Setter.IPSet([]mod.IPSetRequest{
|
||||||
{
|
{
|
||||||
IP: ip,
|
IP: ip,
|
||||||
Domain: cfgEntry.Domain,
|
Domain: cfgEntry.Domain,
|
||||||
|
@ -113,5 +107,5 @@ func IP() (string, error) {
|
||||||
return "", fmt.Errorf("read body: %w", err)
|
return "", fmt.Errorf("read body: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return string(body), nil
|
return strings.TrimSpace(string(body)), nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -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...)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,7 +1,8 @@
|
||||||
package main
|
package gandi
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"ncddns/mod"
|
||||||
|
|
||||||
"github.com/go-gandi/go-gandi"
|
"github.com/go-gandi/go-gandi"
|
||||||
"github.com/go-gandi/go-gandi/config"
|
"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 {
|
if len(reqs) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -53,7 +54,7 @@ func (g Gandi) IPSet(reqs []IPSetRequest) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (Gandi) EnsureAllDomainsAreSame(reqs []IPSetRequest) error {
|
func (Client) EnsureAllDomainsAreSame(reqs []mod.IPSetRequest) error {
|
||||||
first := reqs[0].Domain
|
first := reqs[0].Domain
|
||||||
for _, dom := range reqs {
|
for _, dom := range reqs {
|
||||||
if first != dom.Domain {
|
if first != dom.Domain {
|
|
@ -1,16 +1,21 @@
|
||||||
package main
|
package namecheap
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"ncddns/mod"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"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 {
|
for _, req := range reqs {
|
||||||
if err := nc.SetSingle(req); err != nil {
|
if err := nc.SetSingle(req); err != nil {
|
||||||
return fmt.Errorf("%s: %w", req.Domain, err)
|
return fmt.Errorf("%s: %w", req.Domain, err)
|
||||||
|
@ -19,7 +24,7 @@ func (nc NameCheap) IPSet(reqs []IPSetRequest) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (NameCheap) SetSingle(req IPSetRequest) error {
|
func (Client) SetSingle(req mod.IPSetRequest) error {
|
||||||
params := url.Values{}
|
params := url.Values{}
|
||||||
params.Add("host", req.Host)
|
params.Add("host", req.Host)
|
||||||
params.Add("domain", req.Domain)
|
params.Add("domain", req.Domain)
|
|
@ -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"`
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
Loading…
Reference in New Issue