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

NEW REGISTRAR: OpenSRS (#275)

* Initial commit for OpenSRS registrar support #272
* sort existing name servers before comparing.
* vendor philhug/opensrs-go
* Update docs for OpenSRS #272
* Cache OpenSRS client to prevent http connection leak
* run go fmt
This commit is contained in:
Philipp Hug
2018-03-05 05:07:22 +01:00
committed by Tom Limoncelli
parent 20f0c984e4
commit dfd015e5cd
10 changed files with 795 additions and 0 deletions

21
vendor/github.com/philhug/opensrs-go/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2017 Philipp Hug
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,44 @@
package opensrs
import (
"crypto/md5"
"encoding/hex"
)
const (
httpHeaderUserName = "X-UserName"
httpHeaderSignature = "X-Signature"
)
// Provides credentials that can be used for authenticating with OpenSRS.
//
type Credentials interface {
// Returns the HTTP headers that should be set
// to authenticate the HTTP Request.
Headers(xml []byte) map[string]string
}
// API key MD5 authentication
type apiKeyMD5Credentials struct {
userName string
apiKey string
}
// NewApiKeyMD5Credentials construct Credentials using the OpenSRS MD5 Api Key method.
func NewApiKeyMD5Credentials(userName string, apiKey string) Credentials {
return &apiKeyMD5Credentials{userName: userName, apiKey: apiKey}
}
func (c *apiKeyMD5Credentials) Headers(xml []byte) map[string]string {
h := md5.New()
h.Write(xml)
h.Write([]byte(c.apiKey))
m := hex.EncodeToString(h.Sum(nil))
h = md5.New()
h.Write([]byte(m))
h.Write([]byte(c.apiKey))
m = hex.EncodeToString(h.Sum(nil))
return map[string]string{httpHeaderUserName: c.userName, httpHeaderSignature: m}
}

View File

@@ -0,0 +1,56 @@
package opensrs
import (
"strconv"
)
// DomainsService handles communication with the domain related
// methods of the OpenSRS API.
//
type DomainsService struct {
client *Client
}
// GetDomain fetches a domain.
//
func (s *DomainsService) GetDomain(domainIdentifier string, domainType string, limit int) (*OpsResponse, error) {
opsResponse := OpsResponse{}
opsRequestAttributes := OpsRequestAttributes{Domain: domainIdentifier, Limit: strconv.Itoa(limit), Type: domainType}
resp, err := s.client.post("GET", "DOMAIN", opsRequestAttributes, &opsResponse)
if err != nil {
return nil, err
}
_ = resp
return &opsResponse, nil
}
// UpdateDomainNameservers changes domain servers on a domain.
//
func (s *DomainsService) UpdateDomainNameservers(domainIdentifier string, newDs []string) (*OpsResponse, error) {
opsResponse := OpsResponse{}
opsRequestAttributes := OpsRequestAttributes{Domain: domainIdentifier, AssignNs: newDs, OpType: "assign"}
resp, err := s.client.post("ADVANCED_UPDATE_NAMESERVERS", "DOMAIN", opsRequestAttributes, &opsResponse)
if err != nil {
return nil, err
}
_ = resp
return &opsResponse, nil
}
// GetDNSZone fetches zone info for a domain.
//
func (s *DomainsService) GetDNSZone(domainIdentifier string) (*OpsResponse, error) {
opsResponse := OpsResponse{}
opsRequestAttributes := OpsRequestAttributes{Domain: domainIdentifier}
resp, err := s.client.post("GET_DNS_ZONE", "DOMAIN", opsRequestAttributes, &opsResponse)
if err != nil {
return nil, err
}
_ = resp
return &opsResponse, nil
}

231
vendor/github.com/philhug/opensrs-go/opensrs/opensrs.go generated vendored Normal file
View File

@@ -0,0 +1,231 @@
// Package opensrs provides a client for the OpenSRS API.
// In order to use this package you will need a OpenSRS account.
package opensrs
import (
"bytes"
"crypto/tls"
"fmt"
"io/ioutil"
"log"
"net/http"
"net/url"
)
const (
// Version identifies the current library version.
// This is a pro-forma convention given that Go dependencies
// tends to be fetched directly from the repo.
// It is also used in the user-agent identify the client.
Version = "0.0.1"
// defaultBaseURL to the OpenSRS production API.
//defaultBaseURL = "https://rr-n1-tor.opensrs.net:55443"
defaultBaseURL = "https://horizon.opensrs.net:55443"
// userAgent represents the default user agent used
// when no other user agent is set.
defaultUserAgent = "opensrs-go/" + Version
)
// Client represents a client to the OpenSRS API.
type Client struct {
// HttpClient is the underlying HTTP client
// used to communicate with the API.
HttpClient *http.Client
// Credentials used for accessing the OpenSRS API
Credentials Credentials
// BaseURL for API requests.
// Defaults to the public OpenSRS API, but can be set to a different endpoint (e.g. the sandbox).
BaseURL string
// UserAgent used when communicating with the OpenSRS API.
UserAgent string
// Services used for talking to different parts of the OpenSRS API.
Domains *DomainsService
// Set to true to output debugging logs during API calls
Debug bool
}
// NewClient returns a new OpenSRS API client using the given credentials.
func NewClient(credentials Credentials) *Client {
proxyUrl, _ := url.Parse("http://127.0.0.1:8080")
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify : true},
Proxy: http.ProxyURL(proxyUrl),
}
c := &Client{Credentials: credentials, HttpClient: &http.Client{Transport: tr}, BaseURL: defaultBaseURL}
c.Domains = &DomainsService{client: c}
return c
}
// NewRequest creates an API request.
// The path is expected to be a relative path and will be resolved
// according to the BaseURL of the Client. Paths should always be specified without a preceding slash.
func (c *Client) NewRequest(method, path string, payload interface{}) (*http.Request, error) {
url := c.BaseURL + path
body := new(bytes.Buffer)
if payload != nil {
xml, err := ToXml(payload)
if err != nil {
return nil, err
}
body = bytes.NewBuffer(xml)
}
req, err := http.NewRequest(method, url, body)
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "text/xml")
req.Header.Add("Accept", "text/xml")
req.Header.Add("User-Agent", formatUserAgent(c.UserAgent))
for key, value := range c.Credentials.Headers(body.Bytes()) {
req.Header.Add(key, value)
}
return req, nil
}
// formatUserAgent builds the final user agent to use for HTTP requests.
//
// If no custom user agent is provided, the default user agent is used.
//
// opensrs-go/1.0
//
// If a custom user agent is provided, the final user agent is the combination of the custom user agent
// prepended by the default user agent.
//
// opensrs-go/1.0 customAgentFlag
//
func formatUserAgent(customUserAgent string) string {
if customUserAgent == "" {
return defaultUserAgent
}
return fmt.Sprintf("%s %s", defaultUserAgent, customUserAgent)
}
func (c *Client) post(action string, object string, attributes OpsRequestAttributes, obj *OpsResponse) (*http.Response, error) {
payload := OpsRequest{Action: action, Object: object, Protocol: "XCP", Attributes: attributes}
req, err := c.NewRequest("POST", "", payload)
if err != nil {
return nil, err
}
return c.Do(req, obj)
}
// Do sends an API request and returns the API response.
//
// The API response is JSON decoded and stored in the value pointed by obj,
// or returned as an error if an API error has occurred.
// If obj implements the io.Writer interface, the raw response body will be written to obj,
// without attempting to decode it.
func (c *Client) Do(req *http.Request, obj *OpsResponse) (*http.Response, error) {
if c.Debug {
log.Printf("Executing request (%v): %#v", req.URL, req)
}
resp, err := c.HttpClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if c.Debug {
log.Printf("Response received: %#v", resp)
}
err = CheckResponse(resp)
if err != nil {
return resp, err
}
// If obj implements the io.Writer,
// the response body is decoded into v.
if obj != nil {
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
err = FromXml(b, obj)
if err != nil {
return resp, err
}
err = CheckOpsResponse(resp, obj)
if err != nil {
return resp, err
}
}
return resp, err
}
// An ErrorResponse represents an API response that generated an error.
type ErrorResponse struct {
HttpResponse *http.Response
OpsResponse *OpsResponse
// human-readable message
Message string `json:"message"`
}
// Error implements the error interface.
func (r *ErrorResponse) Error() string {
s := fmt.Sprintf("%v %v: ",
r.HttpResponse.Request.Method, r.HttpResponse.Request.URL)
if r.OpsResponse != nil {
s = s + fmt.Sprintf("%v %v",
r.OpsResponse.ResponseCode,
r.OpsResponse.ResponseText)
} else {
s = s + fmt.Sprintf("%v %v",
r.HttpResponse.StatusCode, r.Message)
}
return s
}
// CheckResponse checks the API response for errors, and returns them if present.
// A response is considered an error if the status code is different than 2xx. Specific requests
// may have additional requirements, but this is sufficient in most of the cases.
func CheckResponse(resp *http.Response) error {
if code := resp.StatusCode; 200 <= code && code <= 299 {
return nil
}
errorResponse := &ErrorResponse{}
errorResponse.HttpResponse = resp
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
}
err = FromXml(b, errorResponse)
if err != nil {
return err
}
return errorResponse
}
func CheckOpsResponse(resp *http.Response, or *OpsResponse) error {
if or.IsSuccess == "1" {
return nil
}
errorResponse := &ErrorResponse{}
errorResponse.HttpResponse = resp
errorResponse.OpsResponse = or
return errorResponse
}

View File

@@ -0,0 +1,92 @@
package opensrs
type NameserverList []struct {
Name string `json:"name"`
IpAddress string `json:"ipaddress,omitempty"`
Ipv6 string `json:"ipv6,omitempty"`
SortOrder string `json:"sortorder,omitempty"`
}
type ARecord struct {
IpAddress string `json:"ipaddress,omitempty"`
SubDomain string `json:"subdomain,omitempty"`
}
type AAAARecord struct {
Ipv6Address string `json:"ipv6_address,omitempty"`
SubDomain string `json:"subdomain,omitempty"`
}
type CNAMERecord struct {
HostName string `json:"hostname,omitempty"`
SubDomain string `json:"subdomain,omitempty"`
}
type MXRecord struct {
Priority string `json:"priority,omitempty"`
SubDomain string `json:"subdomain,omitempty"`
HostName string `json:"hostname,omitempty"`
}
type SRVRecord struct {
Priority string `json:"priority,omitempty"`
Weight string `json:"weight,omitempty"`
SubDomain string `json:"subdomain,omitempty"`
HostName string `json:"hostname,omitempty"`
Port string `json:"port,omitempty"`
}
type TXTRecord struct {
SubDomain string `json:"subdomain,omitempty"`
Text string `json:"text,omitempty"`
}
type DnsRecords struct {
A []ARecord `json:"A,omitempty"`
AAAA []AAAARecord `json:"AAAA,omitempty"`
CNAME []CNAMERecord `json:"CNAME,omitempty"`
MX []MXRecord `json:"MX,omitempty"`
SRV []SRVRecord `json:"SRV,omitempty"`
TXT []TXTRecord `json:"TXT,omitempty"`
}
func (n NameserverList) ToString() []string {
domains := make([]string, len(n))
for i, ns := range n {
domains[i] = ns.Name
}
return domains
}
type OpsRequestAttributes struct {
Domain string `json:"domain"`
Limit string `json:"limit,omitempty"`
Type string `json:"type,omitempty"`
Data string `json:"data,omitempty"`
AffectDomains string `json:"affect_domains,omitempty"`
NameserverList NameserverList `json:"nameserver_list,omitempty"`
OpType string `json:"op_type,omitempty"`
AssignNs []string `json:"assign_ns,omitempty"`
}
type OpsResponse struct {
Action string `json:"action"`
Object string `json:"object"`
Protocol string `json:"protocol"`
IsSuccess string `json:"is_success"`
ResponseCode string `json:"response_code"`
ResponseText string `json:"response_text"`
Attributes struct {
NameserverList NameserverList `json:"nameserver_list,omitempty"`
Type string `json:"type,omitempty"`
LockState string `json:"lock_state,omitempty"`
Records DnsRecords `json:"records,omitempty"`
} `json:"attributes"`
}
type OpsRequest struct {
Action string `json:"action"`
Object string `json:"object"`
Protocol string `json:"protocol"`
Attributes OpsRequestAttributes `json:"attributes"`
}

176
vendor/github.com/philhug/opensrs-go/opensrs/xml.go generated vendored Normal file
View File

@@ -0,0 +1,176 @@
package opensrs
import (
"encoding/json"
"encoding/xml"
"errors"
"log"
"reflect"
"strconv"
"strings"
)
type Header struct {
XMLName xml.Name `xml:"header"`
Version string `xml:"version"`
}
type Item struct {
XMLName xml.Name `xml:"item"`
Key string `xml:"key,attr"`
DtArray *DtArray `xml:"dt_array,omitempty"`
DtAssoc *DtAssoc `xml:"dt_assoc,omitempty"`
Value string `xml:",chardata"`
}
func (i *Item) decode() interface{} {
if i.DtAssoc != nil {
return i.DtAssoc.decode()
}
if i.DtArray != nil {
return i.DtArray.decode()
}
return i.Value
}
type DtArray struct {
XMLName xml.Name `xml:"dt_array"`
ItemList []Item `xml:"item,omitempty"`
}
func (d *DtArray) decode() []interface{} {
m := make([]interface{}, 0)
for _, element := range d.ItemList {
m = append(m, element.decode())
}
return m
}
type DtAssoc struct {
XMLName xml.Name `xml:"dt_assoc"`
ItemList []Item `xml:"item,omitempty"`
}
func (d *DtAssoc) decode() Map {
m := make(Map)
for _, element := range d.ItemList {
m[element.Key] = element.decode()
}
return m
}
type DataBlock struct {
XMLName xml.Name `xml:"data_block"`
DtAssoc *DtAssoc `xml:"dt_assoc,omitempty"`
//DtArray DtArray `xml:"dt_array,omitempty"`
}
type Map map[string]interface{}
func (d *DataBlock) decode() Map {
m := make(Map)
if d.DtAssoc != nil {
return d.DtAssoc.decode()
}
return m
}
func encodeItem(key string, value reflect.Value) Item {
item := Item{}
item.Key = key
v := internalEncode(value)
s, ok := v.(string)
if ok {
item.Value = s
}
dtass, ok := v.(DtAssoc)
if ok {
item.DtAssoc = &dtass
}
dtarr, ok := v.(DtArray)
if ok {
item.DtArray = &dtarr
}
return item
}
func internalEncode(v reflect.Value) (p interface{}) {
t := v.Type()
switch t.Kind() {
case reflect.Interface:
return internalEncode(v.Elem())
case reflect.String:
return v.Interface().(string)
case reflect.Struct:
dt := DtAssoc{}
for i := 0; i < t.NumField(); i++ {
key := strings.ToLower(t.Field(i).Name)
value := v.Field(i)
item := encodeItem(key, value)
dt.ItemList = append(dt.ItemList, item)
}
return dt
case reflect.Map: // DtAssoc
dt := DtAssoc{}
for _, k := range v.MapKeys() {
v := v.MapIndex(k)
key := k.String()
item := encodeItem(key, v)
dt.ItemList = append(dt.ItemList, item)
}
return dt
case reflect.Slice: //DtArray
dt := DtArray{}
for i := 0; i < v.Len(); i++ {
key := strconv.Itoa(i)
value := v.Index(i)
item := encodeItem(key, value)
dt.ItemList = append(dt.ItemList, item)
}
return dt
default:
log.Println("FAIL, unknown type", t.Kind())
}
return nil
}
type Body struct {
XMLName xml.Name `xml:"body"`
DataBlock DataBlock `xml:"data_block"`
}
type OPSEnvelope struct {
XMLName xml.Name `xml:"OPS_envelope"`
Header Header `xml:"header"`
Body Body `xml:"body"`
}
func FromXml(b []byte, v interface{}) error {
var q OPSEnvelope
err := xml.Unmarshal(b, &q)
if err != nil {
return err
}
m := q.Body.DataBlock.decode()
jsonString, _ := json.Marshal(m)
log.Println(string(jsonString))
return json.Unmarshal(jsonString, &v)
}
func ToXml(v interface{}) (b []byte, err error) {
jsonString, _ := json.Marshal(v)
var m interface{}
json.Unmarshal(jsonString, &m)
q := OPSEnvelope{Header: Header{Version: "0.9"}, Body: Body{}}
dtass, ok := internalEncode(reflect.ValueOf(m)).(DtAssoc)
if ok {
q.Body.DataBlock.DtAssoc = &dtass
} else {
return nil, errors.New("Encoding failed")
}
return xml.MarshalIndent(q, "", " ")
}