1
0
mirror of https://github.com/StackExchange/dnscontrol.git synced 2024-05-11 05:55:12 +00:00

New DNS provider PowerDNS (#748)

* Added PowerDNS as dns provider

* Remove unnecessary comments

* Some tests

* Implemented feedback
This commit is contained in:
Jan-Philipp Benecke
2020-05-30 15:54:07 +02:00
committed by GitHub
parent 5269540827
commit ffd4e46dda
43 changed files with 1504 additions and 0 deletions

View File

@ -0,0 +1,24 @@
package pdnshttp
import (
"net/http"
)
type ClientAuthenticator interface {
OnRequest(*http.Request) error
OnConnect(*http.Client) error
}
// NoopAuthenticator provides an "empty" implementation of the
// ClientAuthenticator interface.
type NoopAuthenticator struct{}
// OnRequest is applied each time a HTTP request is built.
func (NoopAuthenticator) OnRequest(*http.Request) error {
return nil
}
// OnConnect is applied on the entire connection as soon as it is set up.
func (NoopAuthenticator) OnConnect(*http.Client) error {
return nil
}

View File

@ -0,0 +1,16 @@
package pdnshttp
import "net/http"
type APIKeyAuthenticator struct {
APIKey string
}
func (a *APIKeyAuthenticator) OnRequest(r *http.Request) error {
r.Header.Set("X-API-Key", a.APIKey)
return nil
}
func (a *APIKeyAuthenticator) OnConnect(*http.Client) error {
return nil
}

View File

@ -0,0 +1,55 @@
package pdnshttp
import (
"crypto"
"crypto/tls"
"crypto/x509"
"fmt"
"net/http"
)
type TLSClientCertificateAuthenticator struct {
CACerts []*x509.Certificate
ClientCert tls.Certificate
ClientKey crypto.PrivateKey
}
func (a *TLSClientCertificateAuthenticator) OnRequest(r *http.Request) error {
return nil
}
func (a *TLSClientCertificateAuthenticator) OnConnect(c *http.Client) error {
if c.Transport == nil {
c.Transport = http.DefaultTransport
}
t, ok := c.Transport.(*http.Transport)
if !ok {
return fmt.Errorf("client.Transport is no *http.Transport, instead %t", c.Transport)
}
if t.TLSClientConfig == nil {
t.TLSClientConfig = &tls.Config{}
}
if t.TLSClientConfig.Certificates == nil {
t.TLSClientConfig.Certificates = make([]tls.Certificate, 0, 1)
}
t.TLSClientConfig.Certificates = append(t.TLSClientConfig.Certificates, a.ClientCert)
if t.TLSClientConfig.RootCAs == nil {
systemPool, err := x509.SystemCertPool()
if err != nil {
return err
}
t.TLSClientConfig.RootCAs = systemPool
}
for i := range a.CACerts {
t.TLSClientConfig.RootCAs.AddCert(a.CACerts[i])
}
return nil
}

View File

@ -0,0 +1,119 @@
package pdnshttp
import (
"context"
"encoding/json"
"io"
"net/http"
"net/http/httputil"
"strings"
)
type Client struct {
baseURL string
httpClient *http.Client
authenticator ClientAuthenticator
debugOutput io.Writer
}
// NewClient returns a new PowerDNS HTTP client
func NewClient(baseURL string, hc *http.Client, auth ClientAuthenticator, debugOutput io.Writer) *Client {
c := Client{
baseURL: baseURL,
httpClient: hc,
authenticator: auth,
debugOutput: debugOutput,
}
return &c
}
// NewRequest builds a new request. Usually, this method should not be used;
// prefer using the "Get", "Post", ... methods if possible.
func (c *Client) NewRequest(method string, path string, body io.Reader) (*http.Request, error) {
path = strings.TrimPrefix(path, "/")
req, err := http.NewRequest(method, c.baseURL+"/"+path, body)
if err != nil {
return nil, err
}
if c.authenticator != nil {
if err := c.authenticator.OnRequest(req); err != nil {
return nil, err
}
}
return req, err
}
// Get executes a GET request
func (c *Client) Get(ctx context.Context, path string, out interface{}, opts ...RequestOption) error {
return c.doRequest(ctx, http.MethodGet, path, out, opts...)
}
// Post executes a POST request
func (c *Client) Post(ctx context.Context, path string, out interface{}, opts ...RequestOption) error {
return c.doRequest(ctx, http.MethodPost, path, out, opts...)
}
// Put executes a PUT request
func (c *Client) Put(ctx context.Context, path string, out interface{}, opts ...RequestOption) error {
return c.doRequest(ctx, http.MethodPut, path, out, opts...)
}
// Patch executes a PATCH request
func (c *Client) Patch(ctx context.Context, path string, out interface{}, opts ...RequestOption) error {
return c.doRequest(ctx, http.MethodPatch, path, out, opts...)
}
// Delete executes a DELETE request
func (c *Client) Delete(ctx context.Context, path string, out interface{}, opts ...RequestOption) error {
return c.doRequest(ctx, http.MethodDelete, path, out, opts...)
}
func (c *Client) doRequest(ctx context.Context, method string, path string, out interface{}, opts ...RequestOption) error {
req, err := c.NewRequest(method, path, nil)
if err != nil {
return err
}
for i := range opts {
if err := opts[i](req); err != nil {
return err
}
}
req = req.WithContext(ctx)
reqDump, _ := httputil.DumpRequestOut(req, true)
c.debugOutput.Write(reqDump)
res, err := c.httpClient.Do(req)
if err != nil {
return err
}
resDump, _ := httputil.DumpResponse(res, true)
c.debugOutput.Write(resDump)
if res.StatusCode == http.StatusNotFound {
return ErrNotFound{URL: req.URL.String()}
} else if res.StatusCode >= 400 {
return ErrUnexpectedStatus{URL: req.URL.String(), StatusCode: res.StatusCode}
}
if out != nil {
if w, ok := out.(io.Writer); ok {
_, err := io.Copy(w, res.Body)
return err
}
dec := json.NewDecoder(res.Body)
err = dec.Decode(out)
if err != nil {
return err
}
}
return nil
}

View File

@ -0,0 +1,31 @@
package pdnshttp
import "fmt"
type ErrNotFound struct {
URL string
}
func (e ErrNotFound) Error() string {
return fmt.Sprintf("not found: %s", e.URL)
}
type ErrUnexpectedStatus struct {
URL string
StatusCode int
}
func (e ErrUnexpectedStatus) Error() string {
return fmt.Sprintf("unexpected status code %d: %s", e.StatusCode, e.URL)
}
func IsNotFound(err error) bool {
switch err.(type) {
case ErrNotFound:
return true
case *ErrNotFound:
return true
}
return false
}

View File

@ -0,0 +1,58 @@
package pdnshttp
import (
"bytes"
"encoding/json"
"io"
"io/ioutil"
"net/http"
)
// RequestOption is a special type of function that can be passed to most HTTP
// request functions in this package; it is used to modify an HTTP request and
// to implement special request logic.
type RequestOption func(*http.Request) error
// WithJSONRequestBody adds a JSON body to a request. The input type can be
// anything, as long as it can be marshaled by "json.Marshal". This method will
// also automatically set the correct content type and content-length.
func WithJSONRequestBody(in interface{}) RequestOption {
return func(req *http.Request) error {
if in == nil {
return nil
}
buf := bytes.Buffer{}
enc := json.NewEncoder(&buf)
err := enc.Encode(in)
if err != nil {
return err
}
rc := ioutil.NopCloser(&buf)
copyBuf := buf.Bytes()
req.Body = rc
req.Header.Set("Content-Type", "application/json")
req.ContentLength = int64(buf.Len())
req.GetBody = func() (io.ReadCloser, error) {
r := bytes.NewReader(copyBuf)
return ioutil.NopCloser(r), nil
}
return nil
}
}
// WithQueryValue adds a query parameter to a request's URL.
func WithQueryValue(key, value string) RequestOption {
return func(req *http.Request) error {
q := req.URL.Query()
q.Set(key, value)
req.URL.RawQuery = q.Encode()
return nil
}
}