mirror of
https://github.com/StackExchange/dnscontrol.git
synced 2025-01-11 01:47:53 +08:00
7865e37c8f
* Add RWTH provider * fix Owners order * Reorganize RWTH Provider * Fix staticcheck and code style issues Co-authored-by: Tom Limoncelli <tlimoncelli@stackoverflow.com>
200 lines
5.5 KiB
Go
200 lines
5.5 KiB
Go
package rwth
|
|
|
|
// The documentation is hosted at https://noc-portal.rz.rwth-aachen.de/dns-admin/en/api_tokens and
|
|
// https://blog.rwth-aachen.de/itc/2022/07/13/api-im-dns-admin/
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"github.com/StackExchange/dnscontrol/v3/models"
|
|
"github.com/StackExchange/dnscontrol/v3/pkg/printer"
|
|
"github.com/miekg/dns"
|
|
"io/ioutil"
|
|
"net/http"
|
|
"net/url"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
baseURL = "https://noc-portal.rz.rwth-aachen.de/dns-admin/api/v1"
|
|
)
|
|
|
|
type RecordReply struct {
|
|
ID int `json:"id"`
|
|
ZoneID int `json:"zone_id"`
|
|
Type string `json:"type"`
|
|
Content string `json:"content"`
|
|
Status string `json:"status"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
Editable bool `json:"editable"`
|
|
rec dns.RR // Store miekg/dns
|
|
}
|
|
|
|
type zone struct {
|
|
ID int `json:"id"`
|
|
ZoneName string `json:"zone_name"`
|
|
Status string `json:"status"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
LastDeploy time.Time `json:"last_deploy"`
|
|
Dnssec struct {
|
|
ZoneSigningKey struct {
|
|
CreatedAt time.Time `json:"created_at"`
|
|
} `json:"zone_signing_key"`
|
|
KeySigningKey struct {
|
|
CreatedAt time.Time `json:"created_at"`
|
|
} `json:"key_signing_key"`
|
|
} `json:"dnssec"`
|
|
}
|
|
|
|
func checkIsLockedSystemAPIRecord(record RecordReply) error {
|
|
if record.Type == "soa_record" {
|
|
// The upload of a BIND zone file can change the SOA record.
|
|
// Implementing this edge case this is too complex for now.
|
|
return fmt.Errorf("SOA records are locked in RWTH zones. They are hence not available for updating")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func checkIsLockedSystemRecord(record *models.RecordConfig) error {
|
|
if record.Type == "SOA" {
|
|
// The upload of a BIND zone file can change the SOA record.
|
|
// Implementing this edge case this is too complex for now.
|
|
return fmt.Errorf("SOA records are locked in RWTH zones. They are hence not available for updating")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (api *rwthProvider) createRecord(domain string, record *models.RecordConfig) error {
|
|
if err := checkIsLockedSystemRecord(record); err != nil {
|
|
return err
|
|
}
|
|
|
|
req := url.Values{}
|
|
req.Set("record_content", api.printRecConfig(*record))
|
|
return api.request("/create_record", "POST", req, nil)
|
|
}
|
|
|
|
func (api *rwthProvider) destroyRecord(record RecordReply) error {
|
|
if err := checkIsLockedSystemAPIRecord(record); err != nil {
|
|
return err
|
|
}
|
|
req := url.Values{}
|
|
req.Set("record_id", strconv.Itoa(record.ID))
|
|
return api.request("/destroy_record", "DELETE", req, nil)
|
|
}
|
|
|
|
func (api *rwthProvider) updateRecord(id int, record models.RecordConfig) error {
|
|
if err := checkIsLockedSystemRecord(&record); err != nil {
|
|
return err
|
|
}
|
|
req := url.Values{}
|
|
req.Set("record_id", strconv.Itoa(id))
|
|
req.Set("record_content", api.printRecConfig(record))
|
|
return api.request("/update_record", "POST", req, nil)
|
|
}
|
|
|
|
func (api *rwthProvider) getAllRecords(domain string) ([]models.RecordConfig, error) {
|
|
zone, err := api.getZone(domain)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
records := make([]models.RecordConfig, 0)
|
|
response := []RecordReply{}
|
|
request := url.Values{}
|
|
request.Set("zone_id", strconv.Itoa(zone.ID))
|
|
if err := api.request("/list_records", "GET", request, &response); err != nil {
|
|
return nil, fmt.Errorf("failed fetching zone records for %q: %w", domain, err)
|
|
}
|
|
for _, apiRecord := range response {
|
|
if checkIsLockedSystemAPIRecord(apiRecord) != nil {
|
|
continue
|
|
}
|
|
dnsRec, err := NewRR(apiRecord.Content) // Parse content as DNS record
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
recConfig, err := models.RRtoRC(dnsRec, domain) // and make it a RC
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
recConfig.Original = apiRecord // but keep our ApiRecord as the original
|
|
|
|
records = append(records, recConfig)
|
|
}
|
|
return records, nil
|
|
}
|
|
|
|
func (api *rwthProvider) getAllZones() error {
|
|
if api.zones != nil {
|
|
return nil
|
|
}
|
|
zones := map[string]zone{}
|
|
response := &[]zone{}
|
|
if err := api.request("/list_zones", "GET", url.Values{}, response); err != nil {
|
|
return fmt.Errorf("failed fetching zones: %w", err)
|
|
}
|
|
for _, zone := range *response {
|
|
zones[zone.ZoneName] = zone
|
|
}
|
|
api.zones = zones
|
|
return nil
|
|
}
|
|
|
|
func (api *rwthProvider) getZone(name string) (*zone, error) {
|
|
if err := api.getAllZones(); err != nil {
|
|
return nil, err
|
|
}
|
|
zone, ok := api.zones[name]
|
|
if !ok {
|
|
return nil, fmt.Errorf("%q is not a zone in this RWTH account", name)
|
|
}
|
|
return &zone, nil
|
|
}
|
|
|
|
// Deploy the zone
|
|
func (api *rwthProvider) deployZone(domain string) error {
|
|
zone, err := api.getZone(domain)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
req := url.Values{}
|
|
req.Set("zone_id", strconv.Itoa(zone.ID))
|
|
return api.request("/deploy_zone", "POST", req, nil)
|
|
}
|
|
|
|
// Send a request
|
|
func (api *rwthProvider) request(endpoint string, method string, request url.Values, target interface{}) error {
|
|
requestBody := strings.NewReader(request.Encode())
|
|
req, err := http.NewRequest(method, baseURL+endpoint, requestBody)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
req.Header.Add("PRIVATE-TOKEN", api.apiToken)
|
|
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
cleanupResponseBody := func() {
|
|
err := resp.Body.Close()
|
|
if err != nil {
|
|
printer.Printf("failed closing response body: %q\n", err)
|
|
}
|
|
}
|
|
|
|
defer cleanupResponseBody()
|
|
if resp.StatusCode != http.StatusOK {
|
|
data, _ := ioutil.ReadAll(resp.Body)
|
|
printer.Printf(string(data))
|
|
return fmt.Errorf("bad status code from RWTH: %d not 200", resp.StatusCode)
|
|
}
|
|
if target == nil {
|
|
return nil
|
|
}
|
|
decoder := json.NewDecoder(resp.Body)
|
|
return decoder.Decode(target)
|
|
}
|