From 725b6b816afb378ed8285b0a5b174677de393eb5 Mon Sep 17 00:00:00 2001 From: keisuk-t Date: Sat, 8 Sep 2018 19:52:36 +0900 Subject: [PATCH] Add DNS Provider for IIJ (#606) --- Gopkg.lock | 13 + cli.go | 1 + providers/dns/dns_providers.go | 3 + providers/dns/iij/iij.go | 211 ++++++++++++++ providers/dns/iij/iij_test.go | 132 +++++++++ vendor/github.com/iij/doapi/api.go | 164 +++++++++++ vendor/github.com/iij/doapi/parse.go | 267 ++++++++++++++++++ .../github.com/iij/doapi/protocol/Commit.go | 44 +++ .../iij/doapi/protocol/RecordAdd.go | 51 ++++ .../iij/doapi/protocol/RecordDelete.go | 47 +++ .../iij/doapi/protocol/RecordGet.go | 48 ++++ .../iij/doapi/protocol/RecordListGet.go | 47 +++ vendor/github.com/iij/doapi/protocol/Reset.go | 45 +++ .../iij/doapi/protocol/ZoneListGet.go | 45 +++ .../github.com/iij/doapi/protocol/common.go | 53 ++++ 15 files changed, 1171 insertions(+) create mode 100644 providers/dns/iij/iij.go create mode 100644 providers/dns/iij/iij_test.go create mode 100644 vendor/github.com/iij/doapi/api.go create mode 100644 vendor/github.com/iij/doapi/parse.go create mode 100644 vendor/github.com/iij/doapi/protocol/Commit.go create mode 100644 vendor/github.com/iij/doapi/protocol/RecordAdd.go create mode 100644 vendor/github.com/iij/doapi/protocol/RecordDelete.go create mode 100644 vendor/github.com/iij/doapi/protocol/RecordGet.go create mode 100644 vendor/github.com/iij/doapi/protocol/RecordListGet.go create mode 100644 vendor/github.com/iij/doapi/protocol/Reset.go create mode 100644 vendor/github.com/iij/doapi/protocol/ZoneListGet.go create mode 100644 vendor/github.com/iij/doapi/protocol/common.go diff --git a/Gopkg.lock b/Gopkg.lock index fc4cb679..576545fd 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -202,6 +202,17 @@ revision = "d460ce9f8df2e77fb1ba55ca87fafed96c607494" version = "v1.0.0" +[[projects]] + branch = "master" + digest = "1:fed64c975620bacb8b3e531e31974149eef07fa0c4bc99185d35c4beaa6bd98a" + name = "github.com/iij/doapi" + packages = [ + ".", + "protocol", + ] + pruneopts = "NUT" + revision = "afe9fa11fb27e5e8ab2dfc1675e91387aa63eaa2" + [[projects]] digest = "1:ac6d01547ec4f7f673311b4663909269bfb8249952de3279799289467837c3cc" name = "github.com/jmespath/go-jmespath" @@ -506,6 +517,8 @@ "github.com/edeckers/auroradnsclient/records", "github.com/edeckers/auroradnsclient/zones", "github.com/exoscale/egoscale", + "github.com/iij/doapi", + "github.com/iij/doapi/protocol", "github.com/miekg/dns", "github.com/namedotcom/go/namecom", "github.com/ovh/go-ovh/ovh", diff --git a/cli.go b/cli.go index ab6ad55f..e4d0fd0e 100644 --- a/cli.go +++ b/cli.go @@ -213,6 +213,7 @@ Here is an example bash command using the CloudFlare DNS provider: fmt.Fprintln(w, "\tgandiv5:\tGANDIV5_API_KEY") fmt.Fprintln(w, "\tgcloud:\tGCE_PROJECT, GCE_SERVICE_ACCOUNT_FILE") fmt.Fprintln(w, "\tglesys:\tGLESYS_API_USER, GLESYS_API_KEY") + fmt.Fprintln(w, "\tiij:\tIIJ_API_ACCESS_KEY, IIJ_API_SECRET_KEY, IIJ_DO_SERVICE_CODE") fmt.Fprintln(w, "\tlinode:\tLINODE_API_KEY") fmt.Fprintln(w, "\tlightsail:\tAWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, DNS_ZONE") fmt.Fprintln(w, "\tmanual:\tnone") diff --git a/providers/dns/dns_providers.go b/providers/dns/dns_providers.go index ec8c3187..973b8053 100644 --- a/providers/dns/dns_providers.go +++ b/providers/dns/dns_providers.go @@ -24,6 +24,7 @@ import ( "github.com/xenolf/lego/providers/dns/gcloud" "github.com/xenolf/lego/providers/dns/glesys" "github.com/xenolf/lego/providers/dns/godaddy" + "github.com/xenolf/lego/providers/dns/iij" "github.com/xenolf/lego/providers/dns/lightsail" "github.com/xenolf/lego/providers/dns/linode" "github.com/xenolf/lego/providers/dns/namecheap" @@ -82,6 +83,8 @@ func NewDNSChallengeProviderByName(name string) (acme.ChallengeProvider, error) return gcloud.NewDNSProvider() case "godaddy": return godaddy.NewDNSProvider() + case "iij": + return iij.NewDNSProvider() case "lightsail": return lightsail.NewDNSProvider() case "linode": diff --git a/providers/dns/iij/iij.go b/providers/dns/iij/iij.go new file mode 100644 index 00000000..ea9e5778 --- /dev/null +++ b/providers/dns/iij/iij.go @@ -0,0 +1,211 @@ +// Package iij implements a DNS provider for solving the DNS-01 challenge using IIJ DNS. +package iij + +import ( + "fmt" + "strings" + "time" + + "github.com/iij/doapi" + "github.com/iij/doapi/protocol" + "github.com/xenolf/lego/acme" + "github.com/xenolf/lego/platform/config/env" +) + +// Config is used to configure the creation of the DNSProvider +type Config struct { + AccessKey string + SecretKey string + DoServiceCode string +} + +// DNSProvider implements the acme.ChallengeProvider interface +type DNSProvider struct { + api *doapi.API + config *Config +} + +// NewDNSProvider returns a DNSProvider instance configured for IIJ DO +func NewDNSProvider() (*DNSProvider, error) { + values, err := env.Get("IIJ_API_ACCESS_KEY", "IIJ_API_SECRET_KEY", "IIJ_DO_SERVICE_CODE") + if err != nil { + return nil, fmt.Errorf("IIJ: %v", err) + } + + return NewDNSProviderConfig(&Config{ + AccessKey: values["IIJ_API_ACCESS_KEY"], + SecretKey: values["IIJ_API_SECRET_KEY"], + DoServiceCode: values["IIJ_DO_SERVICE_CODE"], + }) +} + +// NewDNSProviderConfig takes a given config ans returns a custom configured +// DNSProvider instance +func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { + return &DNSProvider{ + api: doapi.NewAPI(config.AccessKey, config.SecretKey), + config: config, + }, nil +} + +// Timeout returns the timeout and interval to use when checking for DNS propagation. +func (p *DNSProvider) Timeout() (timeout, interval time.Duration) { + return time.Minute * 2, time.Second * 4 +} + +// Present creates a TXT record using the specified parameters +func (p *DNSProvider) Present(domain, token, keyAuth string) error { + _, value, _ := acme.DNS01Record(domain, keyAuth) + return p.addTxtRecord(domain, value) +} + +// CleanUp removes the TXT record matching the specified parameters +func (p *DNSProvider) CleanUp(domain, token, keyAuth string) error { + _, value, _ := acme.DNS01Record(domain, keyAuth) + return p.deleteTxtRecord(domain, value) +} + +func (p *DNSProvider) addTxtRecord(domain, value string) error { + zones, err := p.listZones() + if err != nil { + return err + } + + owner, zone, err := splitDomain(domain, zones) + if err != nil { + return err + } + + request := protocol.RecordAdd{ + DoServiceCode: p.config.DoServiceCode, + ZoneName: zone, + Owner: owner, + TTL: "300", + RecordType: "TXT", + RData: value, + } + + response := &protocol.RecordAddResponse{} + + if err := doapi.Call(*p.api, request, response); err != nil { + return err + } + + return p.commit() +} + +func (p *DNSProvider) deleteTxtRecord(domain, value string) error { + zones, err := p.listZones() + if err != nil { + return err + } + + owner, zone, err := splitDomain(domain, zones) + if err != nil { + return err + } + + id, err := p.findTxtRecord(owner, zone, value) + if err != nil { + return err + } + + request := protocol.RecordDelete{ + DoServiceCode: p.config.DoServiceCode, + ZoneName: zone, + RecordID: id, + } + + response := &protocol.RecordDeleteResponse{} + + if err := doapi.Call(*p.api, request, response); err != nil { + return err + } + + return p.commit() +} + +func (p *DNSProvider) commit() error { + request := protocol.Commit{ + DoServiceCode: p.config.DoServiceCode, + } + + response := &protocol.CommitResponse{} + + return doapi.Call(*p.api, request, response) +} + +func (p *DNSProvider) findTxtRecord(owner, zone, value string) (string, error) { + request := protocol.RecordListGet{ + DoServiceCode: p.config.DoServiceCode, + ZoneName: zone, + } + + response := &protocol.RecordListGetResponse{} + + if err := doapi.Call(*p.api, request, response); err != nil { + return "", err + } + + var id string + + for _, record := range response.RecordList { + if record.Owner == owner && record.RecordType == "TXT" && record.RData == "\""+value+"\"" { + id = record.Id + } + } + + if id == "" { + return "", fmt.Errorf("%s record in %s not found", owner, zone) + } + + return id, nil +} + +func (p *DNSProvider) listZones() ([]string, error) { + request := protocol.ZoneListGet{ + DoServiceCode: p.config.DoServiceCode, + } + + response := &protocol.ZoneListGetResponse{} + + if err := doapi.Call(*p.api, request, response); err != nil { + return nil, err + } + + return response.ZoneList, nil +} + +func splitDomain(domain string, zones []string) (string, string, error) { + parts := strings.Split(strings.Trim(domain, "."), ".") + + var owner string + var zone string + + for i := 0; i < len(parts)-1; i++ { + zone = strings.Join(parts[i:], ".") + if zoneContains(zone, zones) { + baseOwner := strings.Join(parts[0:i], ".") + if len(baseOwner) > 0 { + baseOwner = "." + baseOwner + } + owner = "_acme-challenge" + baseOwner + break + } + } + + if len(owner) == 0 { + return "", "", fmt.Errorf("%s not found", domain) + } + + return owner, zone, nil +} + +func zoneContains(zone string, zones []string) bool { + for _, z := range zones { + if zone == z { + return true + } + } + return false +} diff --git a/providers/dns/iij/iij_test.go b/providers/dns/iij/iij_test.go new file mode 100644 index 00000000..dcc99cc3 --- /dev/null +++ b/providers/dns/iij/iij_test.go @@ -0,0 +1,132 @@ +package iij + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var ( + apiAccessKeyEnv string + apiSecretKeyEnv string + doServiceCodeEnv string + testDomain string + liveTest bool +) + +func init() { + apiAccessKeyEnv = os.Getenv("IIJ_API_ACCESS_KEY") + apiSecretKeyEnv = os.Getenv("IIJ_API_SECRET_KEY") + doServiceCodeEnv = os.Getenv("IIJ_DO_SERVICE_CODE") + + testDomain = os.Getenv("IIJ_API_TESTDOMAIN") + + if len(apiAccessKeyEnv) > 0 && len(apiSecretKeyEnv) > 0 && len(doServiceCodeEnv) > 0 && len(testDomain) > 0 { + liveTest = true + } +} + +func restoreEnv() { + os.Setenv("IIJ_API_ACCESS_KEY", apiAccessKeyEnv) + os.Setenv("IIJ_API_SECRET_KEY", apiSecretKeyEnv) + os.Setenv("IIJ_DO_SERVICE_CODE", doServiceCodeEnv) + os.Setenv("IIJ_API_TESTDOMAIN", testDomain) +} + +func TestSplitDomain(t *testing.T) { + testCases := []struct { + desc string + domain string + zones []string + expectedOwner string + expectedZone string + }{ + { + desc: "domain equals zone", + domain: "domain.com", + zones: []string{"domain.com"}, + expectedOwner: "_acme-challenge", + expectedZone: "domain.com", + }, + { + desc: "with a sub domain", + domain: "my.domain.com", + zones: []string{"domain.com"}, + expectedOwner: "_acme-challenge.my", + expectedZone: "domain.com", + }, + { + desc: "with a sub domain in a zone", + domain: "my.sub.domain.com", + zones: []string{"sub.domain.com", "domain.com"}, + expectedOwner: "_acme-challenge.my", + expectedZone: "sub.domain.com", + }, + { + desc: "with a sub sub domain", + domain: "my.sub.domain.com", + zones: []string{"domain1.com", "domain.com"}, + expectedOwner: "_acme-challenge.my.sub", + expectedZone: "domain.com", + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + owner, zone, err := splitDomain(test.domain, test.zones) + require.NoError(t, err) + + assert.Equal(t, test.expectedOwner, owner) + assert.Equal(t, test.expectedZone, zone) + }) + } + +} + +func TestNewDNSProviderMissingCredErr(t *testing.T) { + defer restoreEnv() + os.Setenv("IIJ_API_ACCESS_KEY", "") + os.Setenv("IIJ_API_SECRET_KEY", "") + os.Setenv("IIJ_DO_SERVICE_CODE", "") + + _, err := NewDNSProvider() + assert.EqualError(t, err, "IIJ: some credentials information are missing: IIJ_API_ACCESS_KEY,IIJ_API_SECRET_KEY,IIJ_DO_SERVICE_CODE") +} + +func TestNewDNSProvider(t *testing.T) { + if !liveTest { + t.Skip("skipping live test") + } + + _, err := NewDNSProvider() + assert.NoError(t, err) +} + +func TestDNSProvider_Present(t *testing.T) { + if !liveTest { + t.Skip("skipping live test") + } + + provider, err := NewDNSProvider() + assert.NoError(t, err) + + err = provider.Present(testDomain, "", "123d==") + assert.NoError(t, err) +} + +func TestDNSProvider_CleanUp(t *testing.T) { + if !liveTest { + t.Skip("skipping live test") + } + + provider, err := NewDNSProvider() + assert.NoError(t, err) + + err = provider.CleanUp(testDomain, "", "123d==") + assert.NoError(t, err) +} diff --git a/vendor/github.com/iij/doapi/api.go b/vendor/github.com/iij/doapi/api.go new file mode 100644 index 00000000..28e29a80 --- /dev/null +++ b/vendor/github.com/iij/doapi/api.go @@ -0,0 +1,164 @@ +// Package doapi : DO APIクライアントモジュール +package doapi + +import ( + "bytes" + "crypto/hmac" + "crypto/sha1" + "crypto/sha256" + "crypto/tls" + "encoding/base64" + "encoding/json" + "fmt" + "hash" + "io" + "net/http" + "net/url" + "sort" + "strings" + "time" + + log "github.com/sirupsen/logrus" +) + +const ( + HmacSHA1 = "HmacSHA1" + HmacSHA256 = "HmacSHA256" + SignatureVersion2 = "2" + APIVersion = "20140601" + EndpointJSON = "https://do.api.iij.jp/" + // EndpointJSON = "http://localhost:9999/" + TimeLayout = "2006-01-02T15:04:05Z" + PostContentType = "application/json" +) + +// API の呼び出し先に関連する構造 +type API struct { + AccessKey string + SecretKey string + Endpoint string + SignMethod string + Expires time.Duration + Insecure bool +} + +// NewAPI API構造体のコンストラクタ +func NewAPI(accesskey, secretkey string) *API { + dur, _ := time.ParseDuration("1h") + return &API{AccessKey: accesskey, + SecretKey: secretkey, + Endpoint: EndpointJSON, + SignMethod: HmacSHA256, + Expires: dur, + } +} + +func convert1(r byte) string { + passchar := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_~.-" + if strings.ContainsRune(passchar, rune(r)) { + return string(r) + } + return fmt.Sprintf("%%%02X", r) +} + +// CustomEscape escape string +func CustomEscape(v string) string { + res := "" + for _, c := range []byte(v) { + res += convert1(c) + } + return res +} + +// String2Sign get string to calculate signature +func String2Sign(method string, header http.Header, param url.URL) string { + var keys []string + ctflag := false + for k := range header { + hdr := strings.ToLower(k) + if strings.HasPrefix(hdr, "x-iijapi-") { + keys = append(keys, hdr) + } else if hdr == "content-type" || hdr == "content-md5" { + keys = append(keys, hdr) + ctflag = true + } + } + sort.Strings(keys) + var target []string + target = append(target, method) + target = append(target, "") + if !ctflag { + target = append(target, "") + } + for _, k := range keys { + if k == "content-type" || k == "content-md5" { + target = append(target, header.Get(k)) + } else { + target = append(target, k+":"+header.Get(k)) + } + } + target = append(target, param.Path) + return strings.Join(target, "\n") +} + +// Sign get signature string +func (a API) Sign(method string, header http.Header, param url.URL, signmethod string) http.Header { + header.Set("x-iijapi-Expire", time.Now().Add(a.Expires).UTC().Format(TimeLayout)) + header.Set("x-iijapi-SignatureMethod", signmethod) + header.Set("x-iijapi-SignatureVersion", SignatureVersion2) + tgtstr := String2Sign(method, header, param) + var hfn func() hash.Hash + switch signmethod { + case HmacSHA1: + hfn = sha1.New + case HmacSHA256: + hfn = sha256.New + } + mac := hmac.New(hfn, []byte(a.SecretKey)) + io.WriteString(mac, tgtstr) + macstr := mac.Sum(nil) + header.Set("Authorization", "IIJAPI "+a.AccessKey+":"+base64.StdEncoding.EncodeToString(macstr)) + return header +} + +// Get : low-level Get +func (a API) Get(param url.URL) (resp *http.Response, err error) { + return a.PostSome("GET", param, nil) +} + +// PostSome : low-level Call +func (a API) PostSome(method string, param url.URL, body interface{}) (resp *http.Response, err error) { + cl := http.Client{} + if a.Insecure { + tr := &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + } + cl.Transport = tr + } + log.Debug("param", param) + var buf *bytes.Buffer + if body != nil { + var bufb []byte + bufb, err = json.Marshal(body) + if len(bufb) > 2 { + log.Debug("call with body", method, string(bufb)) + buf = bytes.NewBuffer(bufb) + } else { + // string(bufb)=="{}" + log.Debug("call without body(empty)", method) + buf = bytes.NewBufferString("") + body = nil + } + } else { + log.Debug("call without body(nil)", method) + buf = bytes.NewBufferString("") + } + req, err := http.NewRequest(method, param.String(), buf) + if body != nil { + req.Header.Add("content-type", PostContentType) + } + req.Header = a.Sign(method, req.Header, param, HmacSHA256) + log.Debug("sign", req.Header) + resp, err = cl.Do(req) + return +} diff --git a/vendor/github.com/iij/doapi/parse.go b/vendor/github.com/iij/doapi/parse.go new file mode 100644 index 00000000..61401e33 --- /dev/null +++ b/vendor/github.com/iij/doapi/parse.go @@ -0,0 +1,267 @@ +package doapi + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "net/http" + "net/url" + "reflect" + "strings" + "text/template" + + log "github.com/sirupsen/logrus" + + "github.com/iij/doapi/protocol" +) + +func execTemplate(name string, tmplstr string, arg interface{}) string { + tmpl, err := template.New(name).Parse(tmplstr) + if err != nil { + panic(err) + } + buf := new(bytes.Buffer) + if err = tmpl.Execute(buf, arg); err != nil { + panic(err) + } + return buf.String() +} + +// GetPath APIのURIのパス部分を求める +func GetPath(arg protocol.CommonArg) string { + return "/r/" + APIVersion + execTemplate(arg.APIName(), arg.URI(), arg) +} + +// GetParam APIのクエリストリング部分を求める +func GetParam(api API, arg protocol.CommonArg) *url.URL { + param, err := url.Parse(api.Endpoint) + if err != nil { + panic(err) + } + param.Path = GetPath(arg) + q := param.Query() + _, toQuery, _ := ArgumentListType(arg) + typs := reflect.TypeOf(arg) + vals := reflect.ValueOf(arg) + for _, key := range toQuery { + if _, flag := typs.FieldByName(key); !flag { + log.Info("no field", key) + continue + } + if val := vals.FieldByName(key).String(); len(val) != 0 { + q.Set(key, val) + } + } + param.RawQuery = q.Encode() + return param +} + +// GetBody API呼び出しのリクエストボディ(JSON文字列)を求める +func GetBody(arg protocol.CommonArg) string { + b, err := json.Marshal(arg) + if err != nil { + panic(err) + } + return string(b) +} + +// Call API呼び出しを実行し、レスポンスを得る +func Call(api API, arg protocol.CommonArg, resp interface{}) (err error) { + if err = Validate(arg); err != nil { + return + } + var res *http.Response + param := GetParam(api, arg) + log.Debug("method", arg.Method(), "param", param, "arg", arg) + if res, err = api.PostSome(arg.Method(), *param, arg); err != nil { + log.Error("PostSome", err) + return + } + log.Debug("res", res, "err", err) + var b []byte + if b, err = ioutil.ReadAll(res.Body); err != nil { + log.Error("ioutil.ReadAll", err) + return + } + log.Debug("data", string(b)) + if err = json.Unmarshal(b, &resp); err != nil { + log.Error("json.Unmarshal", err) + return + } + var cresp = protocol.CommonResponse{} + err = json.Unmarshal(b, &cresp) + if err == nil && cresp.ErrorResponse.ErrorType != "" { + return fmt.Errorf("%s: %s", cresp.ErrorResponse.ErrorType, cresp.ErrorResponse.ErrorMessage) + } + return +} + +// Validate APIの必須引数が入っているかどうかをチェック +func Validate(arg protocol.CommonArg) error { + var res []string + typs := reflect.TypeOf(arg) + vals := reflect.ValueOf(arg) + for i := 0; i < typs.NumField(); i++ { + fld := typs.Field(i) + tagstrJSON := fld.Tag.Get("json") + tagstrP2 := fld.Tag.Get("p2pub") + if strings.Contains(tagstrJSON, "omitempty") { + // optional argument + continue + } + if strings.Contains(tagstrP2, "query") { + // optional argument + continue + } + if val := vals.Field(i).String(); len(val) == 0 { + res = append(res, fld.Name) + } + } + if len(res) != 0 { + return fmt.Errorf("missing arguments: %+v", res) + } + return nil +} + +// ArgumentList API引数のリストを求める。必須とオプションに分類 +func ArgumentList(arg protocol.CommonArg) (required, optional []string) { + typs := reflect.TypeOf(arg) + for i := 0; i < typs.NumField(); i++ { + fld := typs.Field(i) + tagstrJSON := fld.Tag.Get("json") + tagstrP2 := fld.Tag.Get("p2pub") + if strings.Contains(tagstrJSON, "omitempty") || strings.Contains(tagstrP2, "query") { + optional = append(optional, fld.Name) + } else { + required = append(required, fld.Name) + } + } + return +} + +// ArgumentListType API引数のリストを求める。URI埋め込み、クエリストリング、JSONに分類 +func ArgumentListType(arg protocol.CommonArg) (toURI, toQuery, toJSON []string) { + typs := reflect.TypeOf(arg) + for i := 0; i < typs.NumField(); i++ { + fld := typs.Field(i) + tagstrJSON := fld.Tag.Get("json") + tagstrP2 := fld.Tag.Get("p2pub") + if strings.Contains(tagstrP2, "query") { + toQuery = append(toQuery, fld.Name) + } else if strings.HasPrefix(tagstrJSON, "-") { + toURI = append(toURI, fld.Name) + } else { + toJSON = append(toJSON, fld.Name) + } + } + return +} + +func argumentAltKeyList(arg protocol.CommonArg) (toAltQuery, toAltJSON map[string]string) { + toAltQuery = make(map[string]string) + toAltJSON = make(map[string]string) + typs := reflect.TypeOf(arg) + for i := 0; i < typs.NumField(); i++ { + fld := typs.Field(i) + tagstrJSON := fld.Tag.Get("json") + tagstrP2 := fld.Tag.Get("p2pub") + altKey := strings.Split(tagstrJSON, ",")[0] + if altKey == "" || altKey == "-" { + continue + } + if strings.Contains(tagstrP2, "query") { + toAltQuery[fld.Name] = altKey + } else { + toAltJSON[fld.Name] = altKey + } + } + return +} + +// ValidateMap APIの必須引数が入っているかどうかをチェック +func ValidateMap(name string, data map[string]string) error { + var res []string + typs := protocol.TypeMap[name] + for i := 0; i < typs.NumField(); i++ { + fld := typs.Field(i) + tagstrJSON := fld.Tag.Get("json") + tagstrP2 := fld.Tag.Get("p2pub") + if strings.Contains(tagstrJSON, "omitempty") { + // optional argument + continue + } + if strings.Contains(tagstrP2, "query") { + // optional argument + continue + } + if data[fld.Name] == "" { + res = append(res, fld.Name) + } + } + if len(res) != 0 { + return fmt.Errorf("missing arguments: %+v", res) + } + return nil +} + +// CallWithMap API呼び出しを実行する。引数と戻り値が構造体ではなくmap +func CallWithMap(api API, name string, data map[string]string, resp map[string]interface{}) error { + if err := ValidateMap(name, data); err != nil { + return err + } + argt := protocol.TypeMap[name] + arg := reflect.Zero(argt).Interface().(protocol.CommonArg) + var res *http.Response + param, err := url.Parse(api.Endpoint) + if err != nil { + panic(err) + } + param.Path = "/r/" + APIVersion + execTemplate(name, arg.URI(), data) + q := param.Query() + _, toQuery, toJSON := ArgumentListType(arg) + log.Debug("query", toQuery, "json", toJSON, "path", param.Path) + toAltQuery, toAltJSON := argumentAltKeyList(arg) + log.Debug("query altkey - ", toAltQuery, " json altkey - ", toAltJSON) + var jsonmap = map[string]interface{}{} + for _, v := range toJSON { + if len(data[v]) != 0 { + if altkey, ok := toAltJSON[v]; ok { + jsonmap[altkey] = data[v] + } else { + jsonmap[v] = data[v] + } + } + } + for _, v := range toQuery { + if len(data[v]) != 0 { + if altkey, ok := toAltQuery[v]; ok { + q.Set(altkey, data[v]) + } else { + q.Set(v, data[v]) + } + } + } + param.RawQuery = q.Encode() + if res, err = api.PostSome(arg.Method(), *param, jsonmap); err != nil { + log.Error("PostSome", err) + return err + } + log.Debug("res", res) + var b []byte + if b, err = ioutil.ReadAll(res.Body); err != nil { + log.Error("ioutil.ReadAll", err) + return err + } + log.Debug("data", string(b)) + if err = json.Unmarshal(b, &resp); err != nil { + log.Error("json.Unmarshal", err) + return err + } + if val, ok := resp["ErrorResponse"]; ok { + errstr := execTemplate("ErrorResponse", "{{.ErrorType}}: {{.ErrorMessage}}", val) + return errors.New(errstr) + } + return nil +} diff --git a/vendor/github.com/iij/doapi/protocol/Commit.go b/vendor/github.com/iij/doapi/protocol/Commit.go new file mode 100644 index 00000000..d7c7fef2 --- /dev/null +++ b/vendor/github.com/iij/doapi/protocol/Commit.go @@ -0,0 +1,44 @@ +package protocol + +import ( + "reflect" +) + +// Commit +type Commit struct { + DoServiceCode string `json:"-"` // DO契約のサービスコード(do########) +} + +// URI /{{.DoServiceCode}}/commit.json +func (t Commit) URI() string { + return "/{{.DoServiceCode}}/commit.json" +} + +// APIName Commit +func (t Commit) APIName() string { + return "Commit" +} + +// Method PUT +func (t Commit) Method() string { + return "PUT" +} + +// http://manual.iij.jp/dns/doapi/754632.html +func (t Commit) Document() string { + return "http://manual.iij.jp/dns/doapi/754632.html" +} + +// JPName PUT Commit +func (t Commit) JPName() string { + return "PUT commit" +} +func init() { + APIlist = append(APIlist, Commit{}) + TypeMap["Commit"] = reflect.TypeOf(Commit{}) +} + +// CommitResponse PUT Commitのレスポンス +type CommitResponse struct { + *CommonResponse +} diff --git a/vendor/github.com/iij/doapi/protocol/RecordAdd.go b/vendor/github.com/iij/doapi/protocol/RecordAdd.go new file mode 100644 index 00000000..f35cd303 --- /dev/null +++ b/vendor/github.com/iij/doapi/protocol/RecordAdd.go @@ -0,0 +1,51 @@ +package protocol + +import ( + "reflect" +) + +// RecordAdd POST record (同期) +// http://manual.iij.jp/dns/doapi/754517.html +type RecordAdd struct { + DoServiceCode string `json:"-"` // DO契約のサービスコード(do########) + ZoneName string `json:"-"` // Zone Name + Owner string // owner of record + TTL string // TTL of record + RecordType string // type of record + RData string // data of record +} + +// URI /:GisServiceCode/fw-lbs/:IflServiceCode/filters/:IpVersion/:Direction.json +func (t RecordAdd) URI() string { + return "/{{.DoServiceCode}}/{{.ZoneName}}/record.json" +} + +// APIName RecordAdd +func (t RecordAdd) APIName() string { + return "RecordAdd" +} + +// Method POST +func (t RecordAdd) Method() string { + return "POST" +} + +// http://manual.iij.jp/dns/doapi/754517.html +func (t RecordAdd) Document() string { + return "http://manual.iij.jp/dns/doapi/754517.html" +} + +// JPName POST record +func (t RecordAdd) JPName() string { + return "POST record" +} +func init() { + APIlist = append(APIlist, RecordAdd{}) + TypeMap["RecordAdd"] = reflect.TypeOf(RecordAdd{}) +} + +// RecordAddResponse POST recordのレスポンス +type RecordAddResponse struct { + *CommonResponse + Record ResourceRecord +} diff --git a/vendor/github.com/iij/doapi/protocol/RecordDelete.go b/vendor/github.com/iij/doapi/protocol/RecordDelete.go new file mode 100644 index 00000000..cf3aa520 --- /dev/null +++ b/vendor/github.com/iij/doapi/protocol/RecordDelete.go @@ -0,0 +1,47 @@ +package protocol + +import ( + "reflect" +) + +// RecordDelete DELETE record +// http://manual.iij.jp/dns/doapi/754525.html +type RecordDelete struct { + DoServiceCode string `json:"-"` // DO契約のサービスコード(do########) + ZoneName string `json:"-"` // Zone Name + RecordID string `json:"-"` // Record ID +} + +// URI /{{.DoServiceCode}}/{{.ZoneName}}/record/{{.RecordID}}.json +func (t RecordDelete) URI() string { + return "/{{.DoServiceCode}}/{{.ZoneName}}/record/{{.RecordID}}.json" +} + +// APIName RecordDelete +func (t RecordDelete) APIName() string { + return "RecordDelete" +} + +// Method DELETE +func (t RecordDelete) Method() string { + return "DELETE" +} + +// http://manual.iij.jp/dns/doapi/754525.html +func (t RecordDelete) Document() string { + return "http://manual.iij.jp/dns/doapi/754525.html" +} + +// JPName DELETE record +func (t RecordDelete) JPName() string { + return "DELETE record" +} +func init() { + APIlist = append(APIlist, RecordDelete{}) + TypeMap["RecordDelete"] = reflect.TypeOf(RecordDelete{}) +} + +// RecordDeleteResponse DELETE recordのレスポンス +type RecordDeleteResponse struct { + *CommonResponse +} diff --git a/vendor/github.com/iij/doapi/protocol/RecordGet.go b/vendor/github.com/iij/doapi/protocol/RecordGet.go new file mode 100644 index 00000000..1da14920 --- /dev/null +++ b/vendor/github.com/iij/doapi/protocol/RecordGet.go @@ -0,0 +1,48 @@ +package protocol + +import ( + "reflect" +) + +// GET records +// http://manual.iij.jp/dns/doapi/754619.html +type RecordGet struct { + DoServiceCode string `json:"-"` // DO契約のサービスコード(do########) + ZoneName string `json:"-"` // ゾーン名 + RecordID string `json:"-"` // +} + +// URI /{{.DoServiceCode}}/{{.ZoneName}}/record/{{.RecordID}}.json +func (t RecordGet) URI() string { + return "/{{.DoServiceCode}}/{{.ZoneName}}/record/{{.RecordID}}.json" +} + +// APIName RecordGet +func (t RecordGet) APIName() string { + return "RecordGet" +} + +// Method GET +func (t RecordGet) Method() string { + return "GET" +} + +// http://manual.iij.jp/dns/doapi/754503.html +func (t RecordGet) Document() string { + return "http://manual.iij.jp/dns/doapi/754503.html" +} + +// JPName GET record +func (t RecordGet) JPName() string { + return "GET record" +} +func init() { + APIlist = append(APIlist, RecordGet{}) + TypeMap["RecordGet"] = reflect.TypeOf(RecordGet{}) +} + +// RecordGetResponse フィルタリングルール情報取得のレスポンス +type RecordGetResponse struct { + *CommonResponse + Record ResourceRecord +} diff --git a/vendor/github.com/iij/doapi/protocol/RecordListGet.go b/vendor/github.com/iij/doapi/protocol/RecordListGet.go new file mode 100644 index 00000000..c60b819b --- /dev/null +++ b/vendor/github.com/iij/doapi/protocol/RecordListGet.go @@ -0,0 +1,47 @@ +package protocol + +import ( + "reflect" +) + +// GET records +type RecordListGet struct { + DoServiceCode string `json:"-"` // DO契約のサービスコード(do########) + ZoneName string `json:"-"` // ゾーン名 +} + +// URI /{{.DoServiceCode}}/{{.ZoneName}}/records/DETAIL.json +func (t RecordListGet) URI() string { + return "/{{.DoServiceCode}}/{{.ZoneName}}/records/DETAIL.json" +} + +// APIName RecordListGet +func (t RecordListGet) APIName() string { + return "RecordListGet" +} + +// Method GET +func (t RecordListGet) Method() string { + return "GET" +} + +// http://manual.iij.jp/dns/doapi/754619.html +func (t RecordListGet) Document() string { + return "http://manual.iij.jp/dns/doapi/754619.html" +} + +// JPName GET records +func (t RecordListGet) JPName() string { + return "GET records" +} +func init() { + APIlist = append(APIlist, RecordListGet{}) + TypeMap["RecordListGet"] = reflect.TypeOf(RecordListGet{}) +} + +// RecordListGetResponse GET recordsのレスポンス +type RecordListGetResponse struct { + *CommonResponse + RecordList []ResourceRecord + StaticRecordList []ResourceRecord +} diff --git a/vendor/github.com/iij/doapi/protocol/Reset.go b/vendor/github.com/iij/doapi/protocol/Reset.go new file mode 100644 index 00000000..5a95158b --- /dev/null +++ b/vendor/github.com/iij/doapi/protocol/Reset.go @@ -0,0 +1,45 @@ +package protocol + +import ( + "reflect" +) + +// Reset PUT reset (同期) +type Reset struct { + DoServiceCode string `json:"-"` // DO契約のサービスコード(do########) + ZoneName string `json:"-"` // Zone name +} + +// URI /{{.DoServiceCode}}/{{.ZoneName}}/reset.json +func (t Reset) URI() string { + return "/{{.DoServiceCode}}/{{.ZoneName}}/reset.json" +} + +// APIName Reset +func (t Reset) APIName() string { + return "Reset" +} + +// Method PUT +func (t Reset) Method() string { + return "PUT" +} + +// http://manual.iij.jp/dns/doapi/754610.html +func (t Reset) Document() string { + return "http://manual.iij.jp/dns/doapi/754610.html" +} + +// JPName PUT reset +func (t Reset) JPName() string { + return "PUT Reset" +} +func init() { + APIlist = append(APIlist, Reset{}) + TypeMap["Reset"] = reflect.TypeOf(Reset{}) +} + +// ResetResponse PUT resetのレスポンス +type ResetResponse struct { + *CommonResponse +} diff --git a/vendor/github.com/iij/doapi/protocol/ZoneListGet.go b/vendor/github.com/iij/doapi/protocol/ZoneListGet.go new file mode 100644 index 00000000..1ddf1bad --- /dev/null +++ b/vendor/github.com/iij/doapi/protocol/ZoneListGet.go @@ -0,0 +1,45 @@ +package protocol + +import ( + "reflect" +) + +// GET zones +type ZoneListGet struct { + DoServiceCode string `json:"-"` // DO契約のサービスコード(do########) +} + +// URI /{{.DoServiceCode}}/zones.json +func (t ZoneListGet) URI() string { + return "/{{.DoServiceCode}}/zones.json" +} + +// APIName ZoneListGet +func (t ZoneListGet) APIName() string { + return "ZoneListGet" +} + +// Method GET +func (t ZoneListGet) Method() string { + return "GET" +} + +// http://manual.iij.jp/dns/doapi/754466.html +func (t ZoneListGet) Document() string { + return "http://manual.iij.jp/dns/doapi/754466.html" +} + +// JPName GET zones +func (t ZoneListGet) JPName() string { + return "GET zones" +} +func init() { + APIlist = append(APIlist, ZoneListGet{}) + TypeMap["ZoneListGet"] = reflect.TypeOf(ZoneListGet{}) +} + +// ZoneListGetResponse GET zonesのレスポンス +type ZoneListGetResponse struct { + *CommonResponse + ZoneList []string +} diff --git a/vendor/github.com/iij/doapi/protocol/common.go b/vendor/github.com/iij/doapi/protocol/common.go new file mode 100644 index 00000000..77f7a2e5 --- /dev/null +++ b/vendor/github.com/iij/doapi/protocol/common.go @@ -0,0 +1,53 @@ +package protocol + +//go:generate python doc2struct.py + +import ( + "fmt" + "reflect" +) + +type CommonArg interface { + APIName() string + Method() string + URI() string + Document() string + JPName() string +} + +type CommonResponse struct { + RequestId string + ErrorResponse struct { + RequestId string + ErrorType string + ErrorMessage string + } +} + +var APIlist []CommonArg + +var TypeMap = map[string]reflect.Type{} + +type ResourceRecord struct { + Id string `json:",omitempty"` + Status string + Owner string + TTL string + RecordType string + RData string +} + +func (r *ResourceRecord) String() string { + return fmt.Sprintf("%s %s IN %s %s", r.Owner, r.TTL, r.RecordType, r.RData) +} + +func (r *ResourceRecord) FQDN(zone string) string { + return fmt.Sprintf("%s.%s %s IN %s %s", r.Owner, zone, r.TTL, r.RecordType, r.RData) +} + +const ( + UNCAHNGED = "UNCHANGED" + ADDING = "ADDING" + DELETING = "DELETING" + DELETED = "DELETED" +)