Repository: xenolf/lego
Pull request #606 (2018-07-25 04:16:04)

Add DNS Provider for IIJ
Add support for Internet Initiative Japan DNS outsource service
IIJ is the first commercial ISP in Japan



Patch:
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/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"
+)


Test patch:
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)
+}