From 53201b65b04520e5148bd5b1d1d6eb7534e092be Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Fri, 16 Nov 2018 16:40:04 +0100 Subject: [PATCH] fix: ACME spec and Cloudflare. --- Gopkg.lock | 41 +- docs/configuration/acme.md | 9 +- integration/resources/compose/pebble.yml | 2 +- vendor/github.com/fatih/structs/LICENSE | 21 + vendor/github.com/fatih/structs/field.go | 141 +++++ vendor/github.com/fatih/structs/structs.go | 584 ++++++++++++++++++ vendor/github.com/fatih/structs/tags.go | 32 + vendor/github.com/kolo/xmlrpc/LICENSE | 19 + vendor/github.com/kolo/xmlrpc/client.go | 169 +++++ vendor/github.com/kolo/xmlrpc/decoder.go | 463 ++++++++++++++ vendor/github.com/kolo/xmlrpc/encoder.go | 164 +++++ vendor/github.com/kolo/xmlrpc/request.go | 57 ++ vendor/github.com/kolo/xmlrpc/response.go | 52 ++ vendor/github.com/kolo/xmlrpc/xmlrpc.go | 19 + .../mitchellh/mapstructure/decode_hooks.go | 65 ++ .../mitchellh/mapstructure/mapstructure.go | 414 ++++++++++--- vendor/github.com/smueller18/goinwx/LICENSE | 21 + .../github.com/smueller18/goinwx/account.go | 54 ++ .../github.com/smueller18/goinwx/contact.go | 150 +++++ vendor/github.com/smueller18/goinwx/domain.go | 303 +++++++++ vendor/github.com/smueller18/goinwx/goinwx.go | 133 ++++ .../smueller18/goinwx/nameserver.go | 275 +++++++++ vendor/github.com/transip/gotransip/LICENSE | 21 + vendor/github.com/transip/gotransip/api.go | 12 + vendor/github.com/transip/gotransip/client.go | 119 ++++ .../transip/gotransip/domain/api.go | 314 ++++++++++ .../transip/gotransip/domain/domain.go | 405 ++++++++++++ vendor/github.com/transip/gotransip/sign.go | 49 ++ vendor/github.com/transip/gotransip/soap.go | 419 +++++++++++++ .../github.com/transip/gotransip/util/util.go | 37 ++ vendor/github.com/xenolf/lego/acme/client.go | 19 +- vendor/github.com/xenolf/lego/acme/http.go | 52 +- .../xenolf/lego/acme/tls_alpn_challenge.go | 6 +- .../providers/dns/cloudflare/cloudflare.go | 2 +- .../lego/providers/dns/conoha/client.go | 205 ++++++ .../lego/providers/dns/conoha/conoha.go | 148 +++++ .../lego/providers/dns/dns_providers.go | 21 + .../lego/providers/dns/httpreq/httpreq.go | 193 ++++++ .../xenolf/lego/providers/dns/inwx/inwx.go | 166 +++++ .../lego/providers/dns/mydnsjp/mydnsjp.go | 140 +++++ .../lego/providers/dns/selectel/client.go | 211 +++++++ .../lego/providers/dns/selectel/selectel.go | 153 +++++ .../lego/providers/dns/transip/transip.go | 150 +++++ .../lego/providers/dns/vscale/client.go | 211 +++++++ .../lego/providers/dns/vscale/vscale.go | 153 +++++ 45 files changed, 6269 insertions(+), 125 deletions(-) create mode 100644 vendor/github.com/fatih/structs/LICENSE create mode 100644 vendor/github.com/fatih/structs/field.go create mode 100644 vendor/github.com/fatih/structs/structs.go create mode 100644 vendor/github.com/fatih/structs/tags.go create mode 100644 vendor/github.com/kolo/xmlrpc/LICENSE create mode 100644 vendor/github.com/kolo/xmlrpc/client.go create mode 100644 vendor/github.com/kolo/xmlrpc/decoder.go create mode 100644 vendor/github.com/kolo/xmlrpc/encoder.go create mode 100644 vendor/github.com/kolo/xmlrpc/request.go create mode 100644 vendor/github.com/kolo/xmlrpc/response.go create mode 100644 vendor/github.com/kolo/xmlrpc/xmlrpc.go create mode 100644 vendor/github.com/smueller18/goinwx/LICENSE create mode 100644 vendor/github.com/smueller18/goinwx/account.go create mode 100644 vendor/github.com/smueller18/goinwx/contact.go create mode 100644 vendor/github.com/smueller18/goinwx/domain.go create mode 100644 vendor/github.com/smueller18/goinwx/goinwx.go create mode 100644 vendor/github.com/smueller18/goinwx/nameserver.go create mode 100644 vendor/github.com/transip/gotransip/LICENSE create mode 100644 vendor/github.com/transip/gotransip/api.go create mode 100644 vendor/github.com/transip/gotransip/client.go create mode 100644 vendor/github.com/transip/gotransip/domain/api.go create mode 100644 vendor/github.com/transip/gotransip/domain/domain.go create mode 100644 vendor/github.com/transip/gotransip/sign.go create mode 100644 vendor/github.com/transip/gotransip/soap.go create mode 100644 vendor/github.com/transip/gotransip/util/util.go create mode 100644 vendor/github.com/xenolf/lego/providers/dns/conoha/client.go create mode 100644 vendor/github.com/xenolf/lego/providers/dns/conoha/conoha.go create mode 100644 vendor/github.com/xenolf/lego/providers/dns/httpreq/httpreq.go create mode 100644 vendor/github.com/xenolf/lego/providers/dns/inwx/inwx.go create mode 100644 vendor/github.com/xenolf/lego/providers/dns/mydnsjp/mydnsjp.go create mode 100644 vendor/github.com/xenolf/lego/providers/dns/selectel/client.go create mode 100644 vendor/github.com/xenolf/lego/providers/dns/selectel/selectel.go create mode 100644 vendor/github.com/xenolf/lego/providers/dns/transip/transip.go create mode 100644 vendor/github.com/xenolf/lego/providers/dns/vscale/client.go create mode 100644 vendor/github.com/xenolf/lego/providers/dns/vscale/vscale.go diff --git a/Gopkg.lock b/Gopkg.lock index cc31d6fa5..dbea7b1e4 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -565,6 +565,12 @@ packages = ["."] revision = "62e9147c64a1ed519147b62a56a14e83e2be02c1" +[[projects]] + name = "github.com/fatih/structs" + packages = ["."] + revision = "4966fc68f5b7593aafa6cbbba2d65ec6e1416047" + version = "v1.1.0" + [[projects]] branch = "master" name = "github.com/flynn/go-shlex" @@ -845,6 +851,12 @@ revision = "59fac5042749a5afb9af70e813da1dd5474f0167" version = "1.0.1" +[[projects]] + branch = "master" + name = "github.com/kolo/xmlrpc" + packages = ["."] + revision = "16bdd962781df9696f40cc2bab924f1a855a7f89" + [[projects]] branch = "master" name = "github.com/konsorten/go-windows-terminal-sequences" @@ -989,10 +1001,10 @@ revision = "2bca23e0e452137f789efbc8610126fd8b94f73b" [[projects]] - branch = "master" name = "github.com/mitchellh/mapstructure" packages = ["."] - revision = "b4575eea38cca1123ec2dc90c26529b5c5acfcff" + revision = "3536a929edddb9a5b34bd6861dc4a9647cb459fe" + version = "v1.1.2" [[projects]] branch = "master" @@ -1200,6 +1212,12 @@ revision = "a67f783a3814b8729bd2dac5780b5f78f8dbd64d" version = "v1.1.0" +[[projects]] + branch = "master" + name = "github.com/smueller18/goinwx" + packages = ["."] + revision = "5d138389109eca96463f44f692408f0d1c731278" + [[projects]] name = "github.com/spf13/pflag" packages = ["."] @@ -1247,6 +1265,16 @@ revision = "b2b6a672cf1e5b90748f79b8b81fc8c5cf0571a1" version = "1.0.2" +[[projects]] + name = "github.com/transip/gotransip" + packages = [ + ".", + "domain", + "util" + ] + revision = "1dc93a7db3567a5ccf865106afac88278ba940cf" + version = "v5.8.1" + [[projects]] branch = "master" name = "github.com/tuvistavie/securerandom" @@ -1362,6 +1390,7 @@ "providers/dns/bluecat", "providers/dns/cloudflare", "providers/dns/cloudxns", + "providers/dns/conoha", "providers/dns/digitalocean", "providers/dns/dnsimple", "providers/dns/dnsmadeeasy", @@ -1378,10 +1407,13 @@ "providers/dns/glesys", "providers/dns/godaddy", "providers/dns/hostingde", + "providers/dns/httpreq", "providers/dns/iij", + "providers/dns/inwx", "providers/dns/lightsail", "providers/dns/linode", "providers/dns/linodev4", + "providers/dns/mydnsjp", "providers/dns/namecheap", "providers/dns/namedotcom", "providers/dns/netcup", @@ -1394,11 +1426,14 @@ "providers/dns/rfc2136", "providers/dns/route53", "providers/dns/sakuracloud", + "providers/dns/selectel", "providers/dns/stackpath", + "providers/dns/transip", "providers/dns/vegadns", + "providers/dns/vscale", "providers/dns/vultr" ] - revision = "1151b4e3befc51b7b215179c87791753721dc6d5" + revision = "a5f0a3ff8026e05cbdd11c391c0e25122497c736" [[projects]] branch = "master" diff --git a/docs/configuration/acme.md b/docs/configuration/acme.md index e25ef3882..12bb6abd0 100644 --- a/docs/configuration/acme.md +++ b/docs/configuration/acme.md @@ -282,6 +282,7 @@ Here is a list of supported `provider`s, that can automate the DNS verification, | [Blue Cat](https://www.bluecatnetworks.com/) | `bluecat` | `BLUECAT_SERVER_URL`, `BLUECAT_USER_NAME`, `BLUECAT_PASSWORD`, `BLUECAT_CONFIG_NAME`, `BLUECAT_DNS_VIEW` | Not tested yet | | [Cloudflare](https://www.cloudflare.com) | `cloudflare` | `CF_API_EMAIL`, `CF_API_KEY` - The `Global API Key` needs to be used, not the `Origin CA Key` | YES | | [CloudXNS](https://www.cloudxns.net) | `cloudxns` | `CLOUDXNS_API_KEY`, `CLOUDXNS_SECRET_KEY` | Not tested yet | +| [ConoHa](https://www.conoha.jp) | `conoha` | `CONOHA_TENANT_ID`, `CONOHA_API_USERNAME`, `CONOHA_API_PASSWORD` | YES | | [DigitalOcean](https://www.digitalocean.com) | `digitalocean` | `DO_AUTH_TOKEN` | YES | | [DNSimple](https://dnsimple.com) | `dnsimple` | `DNSIMPLE_OAUTH_TOKEN`, `DNSIMPLE_BASE_URL` | Not tested yet | | [DNS Made Easy](https://dnsmadeeasy.com) | `dnsmadeeasy` | `DNSMADEEASY_API_KEY`, `DNSMADEEASY_API_SECRET`, `DNSMADEEASY_SANDBOX` | Not tested yet | @@ -289,7 +290,7 @@ Here is a list of supported `provider`s, that can automate the DNS verification, | [DreamHost](https://www.dreamhost.com/) | `dreamhost` | `DREAMHOST_API_KEY` | YES | | [Duck DNS](https://www.duckdns.org/) | `duckdns` | `DUCKDNS_TOKEN` | No | | [Dyn](https://dyn.com) | `dyn` | `DYN_CUSTOMER_NAME`, `DYN_USER_NAME`, `DYN_PASSWORD` | Not tested yet | -| External Program | `exec` | `EXEC_PATH` | Not tested yet | +| External Program | `exec` | `EXEC_PATH` | YES | | [Exoscale](https://www.exoscale.com) | `exoscale` | `EXOSCALE_API_KEY`, `EXOSCALE_API_SECRET`, `EXOSCALE_ENDPOINT` | YES | | [Fast DNS](https://www.akamai.com/) | `fastdns` | `AKAMAI_CLIENT_TOKEN`, `AKAMAI_CLIENT_SECRET`, `AKAMAI_ACCESS_TOKEN` | Not tested yet | | [Gandi](https://www.gandi.net) | `gandi` | `GANDI_API_KEY` | Not tested yet | @@ -298,11 +299,14 @@ Here is a list of supported `provider`s, that can automate the DNS verification, | [GoDaddy](https://godaddy.com/domains) | `godaddy` | `GODADDY_API_KEY`, `GODADDY_API_SECRET` | Not tested yet | | [Google Cloud DNS](https://cloud.google.com/dns/docs/) | `gcloud` | `GCE_PROJECT`, `GCE_SERVICE_ACCOUNT_FILE` | YES | | [hosting.de](https://www.hosting.de) | `hostingde` | `HOSTINGDE_API_KEY`, `HOSTINGDE_ZONE_NAME` | Not tested yet | +| HTTP request | `httpreq` | `HTTPREQ_ENDPOINT`, `HTTPREQ_MODE`, `HTTPREQ_USERNAME`, `HTTPREQ_PASSWORD` | YES | | [IIJ](https://www.iij.ad.jp/) | `iij` | `IIJ_API_ACCESS_KEY`, `IIJ_API_SECRET_KEY`, `IIJ_DO_SERVICE_CODE` | Not tested yet | +| [INWX](https://www.inwx.de/en) | `inwx` | `INWX_USERNAME`, `INWX_PASSWORD` | YES | | [Lightsail](https://aws.amazon.com/lightsail/) | `lightsail` | `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `DNS_ZONE` | Not tested yet | | [Linode](https://www.linode.com) | `linode` | `LINODE_API_KEY` | Not tested yet | | [Linode v4](https://www.linode.com) | `linodev4` | `LINODE_TOKEN` | Not tested yet | | manual | - | none, but you need to run Traefik interactively, turn on `acmeLogging` to see instructions and press Enter. | YES | +| [MyDNS.jp](https://www.mydns.jp/) | `mydnsjp` | `MYDNSJP_MASTER_ID`, `MYDNSJP_PASSWORD` | YES | | [Namecheap](https://www.namecheap.com) | `namecheap` | `NAMECHEAP_API_USER`, `NAMECHEAP_API_KEY` | YES | | [name.com](https://www.name.com/) | `namedotcom` | `NAMECOM_USERNAME`, `NAMECOM_API_TOKEN`, `NAMECOM_SERVER` | Not tested yet | | [Netcup](https://www.netcup.eu/) | `netcup` | `NETCUP_CUSTOMER_NUMBER`, `NETCUP_API_KEY`, `NETCUP_API_PASSWORD` | Not tested yet | @@ -315,8 +319,11 @@ Here is a list of supported `provider`s, that can automate the DNS verification, | [RFC2136](https://tools.ietf.org/html/rfc2136) | `rfc2136` | `RFC2136_TSIG_KEY`, `RFC2136_TSIG_SECRET`, `RFC2136_TSIG_ALGORITHM`, `RFC2136_NAMESERVER` | Not tested yet | | [Route 53](https://aws.amazon.com/route53/) | `route53` | `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `[AWS_REGION]`, `[AWS_HOSTED_ZONE_ID]` or a configured user/instance IAM profile. | YES | | [Sakura Cloud](https://cloud.sakura.ad.jp/) | `sakuracloud` | `SAKURACLOUD_ACCESS_TOKEN`, `SAKURACLOUD_ACCESS_TOKEN_SECRET` | Not tested yet | +| [Selectel](https://selectel.ru/en/) | `selectel` | `SELECTEL_API_TOKEN` | YES | | [Stackpath](https://www.stackpath.com/) | `stackpath` | `STACKPATH_CLIENT_ID`, `STACKPATH_CLIENT_SECRET`, `STACKPATH_STACK_ID` | Not tested yet | +| [TransIP](https://www.transip.nl/) | `transip` | `TRANSIP_ACCOUNT_NAME`, `TRANSIP_PRIVATE_KEY_PATH` | YES | | [VegaDNS](https://github.com/shupp/VegaDNS-API) | `vegadns` | `SECRET_VEGADNS_KEY`, `SECRET_VEGADNS_SECRET`, `VEGADNS_URL` | Not tested yet | +| [Vscale](https://vscale.io/) | `vscale` | `VSCALE_API_TOKEN` | YES | | [VULTR](https://www.vultr.com) | `vultr` | `VULTR_API_KEY` | Not tested yet | #### `resolvers` diff --git a/integration/resources/compose/pebble.yml b/integration/resources/compose/pebble.yml index 2828c66ef..66c358f47 100644 --- a/integration/resources/compose/pebble.yml +++ b/integration/resources/compose/pebble.yml @@ -1,5 +1,5 @@ pebble: - image: letsencrypt/pebble:2018-07-27 + image: letsencrypt/pebble:2018-11-02 command: pebble --dnsserver ${DOCKER_HOST_IP}:5053 ports: - 14000:14000 diff --git a/vendor/github.com/fatih/structs/LICENSE b/vendor/github.com/fatih/structs/LICENSE new file mode 100644 index 000000000..34504e4b3 --- /dev/null +++ b/vendor/github.com/fatih/structs/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014 Fatih Arslan + +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. \ No newline at end of file diff --git a/vendor/github.com/fatih/structs/field.go b/vendor/github.com/fatih/structs/field.go new file mode 100644 index 000000000..e69783230 --- /dev/null +++ b/vendor/github.com/fatih/structs/field.go @@ -0,0 +1,141 @@ +package structs + +import ( + "errors" + "fmt" + "reflect" +) + +var ( + errNotExported = errors.New("field is not exported") + errNotSettable = errors.New("field is not settable") +) + +// Field represents a single struct field that encapsulates high level +// functions around the field. +type Field struct { + value reflect.Value + field reflect.StructField + defaultTag string +} + +// Tag returns the value associated with key in the tag string. If there is no +// such key in the tag, Tag returns the empty string. +func (f *Field) Tag(key string) string { + return f.field.Tag.Get(key) +} + +// Value returns the underlying value of the field. It panics if the field +// is not exported. +func (f *Field) Value() interface{} { + return f.value.Interface() +} + +// IsEmbedded returns true if the given field is an anonymous field (embedded) +func (f *Field) IsEmbedded() bool { + return f.field.Anonymous +} + +// IsExported returns true if the given field is exported. +func (f *Field) IsExported() bool { + return f.field.PkgPath == "" +} + +// IsZero returns true if the given field is not initialized (has a zero value). +// It panics if the field is not exported. +func (f *Field) IsZero() bool { + zero := reflect.Zero(f.value.Type()).Interface() + current := f.Value() + + return reflect.DeepEqual(current, zero) +} + +// Name returns the name of the given field +func (f *Field) Name() string { + return f.field.Name +} + +// Kind returns the fields kind, such as "string", "map", "bool", etc .. +func (f *Field) Kind() reflect.Kind { + return f.value.Kind() +} + +// Set sets the field to given value v. It returns an error if the field is not +// settable (not addressable or not exported) or if the given value's type +// doesn't match the fields type. +func (f *Field) Set(val interface{}) error { + // we can't set unexported fields, so be sure this field is exported + if !f.IsExported() { + return errNotExported + } + + // do we get here? not sure... + if !f.value.CanSet() { + return errNotSettable + } + + given := reflect.ValueOf(val) + + if f.value.Kind() != given.Kind() { + return fmt.Errorf("wrong kind. got: %s want: %s", given.Kind(), f.value.Kind()) + } + + f.value.Set(given) + return nil +} + +// Zero sets the field to its zero value. It returns an error if the field is not +// settable (not addressable or not exported). +func (f *Field) Zero() error { + zero := reflect.Zero(f.value.Type()).Interface() + return f.Set(zero) +} + +// Fields returns a slice of Fields. This is particular handy to get the fields +// of a nested struct . A struct tag with the content of "-" ignores the +// checking of that particular field. Example: +// +// // Field is ignored by this package. +// Field *http.Request `structs:"-"` +// +// It panics if field is not exported or if field's kind is not struct +func (f *Field) Fields() []*Field { + return getFields(f.value, f.defaultTag) +} + +// Field returns the field from a nested struct. It panics if the nested struct +// is not exported or if the field was not found. +func (f *Field) Field(name string) *Field { + field, ok := f.FieldOk(name) + if !ok { + panic("field not found") + } + + return field +} + +// FieldOk returns the field from a nested struct. The boolean returns whether +// the field was found (true) or not (false). +func (f *Field) FieldOk(name string) (*Field, bool) { + value := &f.value + // value must be settable so we need to make sure it holds the address of the + // variable and not a copy, so we can pass the pointer to strctVal instead of a + // copy (which is not assigned to any variable, hence not settable). + // see "https://blog.golang.org/laws-of-reflection#TOC_8." + if f.value.Kind() != reflect.Ptr { + a := f.value.Addr() + value = &a + } + v := strctVal(value.Interface()) + t := v.Type() + + field, ok := t.FieldByName(name) + if !ok { + return nil, false + } + + return &Field{ + field: field, + value: v.FieldByName(name), + }, true +} diff --git a/vendor/github.com/fatih/structs/structs.go b/vendor/github.com/fatih/structs/structs.go new file mode 100644 index 000000000..3a8770652 --- /dev/null +++ b/vendor/github.com/fatih/structs/structs.go @@ -0,0 +1,584 @@ +// Package structs contains various utilities functions to work with structs. +package structs + +import ( + "fmt" + + "reflect" +) + +var ( + // DefaultTagName is the default tag name for struct fields which provides + // a more granular to tweak certain structs. Lookup the necessary functions + // for more info. + DefaultTagName = "structs" // struct's field default tag name +) + +// Struct encapsulates a struct type to provide several high level functions +// around the struct. +type Struct struct { + raw interface{} + value reflect.Value + TagName string +} + +// New returns a new *Struct with the struct s. It panics if the s's kind is +// not struct. +func New(s interface{}) *Struct { + return &Struct{ + raw: s, + value: strctVal(s), + TagName: DefaultTagName, + } +} + +// Map converts the given struct to a map[string]interface{}, where the keys +// of the map are the field names and the values of the map the associated +// values of the fields. The default key string is the struct field name but +// can be changed in the struct field's tag value. The "structs" key in the +// struct's field tag value is the key name. Example: +// +// // Field appears in map as key "myName". +// Name string `structs:"myName"` +// +// A tag value with the content of "-" ignores that particular field. Example: +// +// // Field is ignored by this package. +// Field bool `structs:"-"` +// +// A tag value with the content of "string" uses the stringer to get the value. Example: +// +// // The value will be output of Animal's String() func. +// // Map will panic if Animal does not implement String(). +// Field *Animal `structs:"field,string"` +// +// A tag value with the option of "flatten" used in a struct field is to flatten its fields +// in the output map. Example: +// +// // The FieldStruct's fields will be flattened into the output map. +// FieldStruct time.Time `structs:",flatten"` +// +// A tag value with the option of "omitnested" stops iterating further if the type +// is a struct. Example: +// +// // Field is not processed further by this package. +// Field time.Time `structs:"myName,omitnested"` +// Field *http.Request `structs:",omitnested"` +// +// A tag value with the option of "omitempty" ignores that particular field if +// the field value is empty. Example: +// +// // Field appears in map as key "myName", but the field is +// // skipped if empty. +// Field string `structs:"myName,omitempty"` +// +// // Field appears in map as key "Field" (the default), but +// // the field is skipped if empty. +// Field string `structs:",omitempty"` +// +// Note that only exported fields of a struct can be accessed, non exported +// fields will be neglected. +func (s *Struct) Map() map[string]interface{} { + out := make(map[string]interface{}) + s.FillMap(out) + return out +} + +// FillMap is the same as Map. Instead of returning the output, it fills the +// given map. +func (s *Struct) FillMap(out map[string]interface{}) { + if out == nil { + return + } + + fields := s.structFields() + + for _, field := range fields { + name := field.Name + val := s.value.FieldByName(name) + isSubStruct := false + var finalVal interface{} + + tagName, tagOpts := parseTag(field.Tag.Get(s.TagName)) + if tagName != "" { + name = tagName + } + + // if the value is a zero value and the field is marked as omitempty do + // not include + if tagOpts.Has("omitempty") { + zero := reflect.Zero(val.Type()).Interface() + current := val.Interface() + + if reflect.DeepEqual(current, zero) { + continue + } + } + + if !tagOpts.Has("omitnested") { + finalVal = s.nested(val) + + v := reflect.ValueOf(val.Interface()) + if v.Kind() == reflect.Ptr { + v = v.Elem() + } + + switch v.Kind() { + case reflect.Map, reflect.Struct: + isSubStruct = true + } + } else { + finalVal = val.Interface() + } + + if tagOpts.Has("string") { + s, ok := val.Interface().(fmt.Stringer) + if ok { + out[name] = s.String() + } + continue + } + + if isSubStruct && (tagOpts.Has("flatten")) { + for k := range finalVal.(map[string]interface{}) { + out[k] = finalVal.(map[string]interface{})[k] + } + } else { + out[name] = finalVal + } + } +} + +// Values converts the given s struct's field values to a []interface{}. A +// struct tag with the content of "-" ignores the that particular field. +// Example: +// +// // Field is ignored by this package. +// Field int `structs:"-"` +// +// A value with the option of "omitnested" stops iterating further if the type +// is a struct. Example: +// +// // Fields is not processed further by this package. +// Field time.Time `structs:",omitnested"` +// Field *http.Request `structs:",omitnested"` +// +// A tag value with the option of "omitempty" ignores that particular field and +// is not added to the values if the field value is empty. Example: +// +// // Field is skipped if empty +// Field string `structs:",omitempty"` +// +// Note that only exported fields of a struct can be accessed, non exported +// fields will be neglected. +func (s *Struct) Values() []interface{} { + fields := s.structFields() + + var t []interface{} + + for _, field := range fields { + val := s.value.FieldByName(field.Name) + + _, tagOpts := parseTag(field.Tag.Get(s.TagName)) + + // if the value is a zero value and the field is marked as omitempty do + // not include + if tagOpts.Has("omitempty") { + zero := reflect.Zero(val.Type()).Interface() + current := val.Interface() + + if reflect.DeepEqual(current, zero) { + continue + } + } + + if tagOpts.Has("string") { + s, ok := val.Interface().(fmt.Stringer) + if ok { + t = append(t, s.String()) + } + continue + } + + if IsStruct(val.Interface()) && !tagOpts.Has("omitnested") { + // look out for embedded structs, and convert them to a + // []interface{} to be added to the final values slice + t = append(t, Values(val.Interface())...) + } else { + t = append(t, val.Interface()) + } + } + + return t +} + +// Fields returns a slice of Fields. A struct tag with the content of "-" +// ignores the checking of that particular field. Example: +// +// // Field is ignored by this package. +// Field bool `structs:"-"` +// +// It panics if s's kind is not struct. +func (s *Struct) Fields() []*Field { + return getFields(s.value, s.TagName) +} + +// Names returns a slice of field names. A struct tag with the content of "-" +// ignores the checking of that particular field. Example: +// +// // Field is ignored by this package. +// Field bool `structs:"-"` +// +// It panics if s's kind is not struct. +func (s *Struct) Names() []string { + fields := getFields(s.value, s.TagName) + + names := make([]string, len(fields)) + + for i, field := range fields { + names[i] = field.Name() + } + + return names +} + +func getFields(v reflect.Value, tagName string) []*Field { + if v.Kind() == reflect.Ptr { + v = v.Elem() + } + + t := v.Type() + + var fields []*Field + + for i := 0; i < t.NumField(); i++ { + field := t.Field(i) + + if tag := field.Tag.Get(tagName); tag == "-" { + continue + } + + f := &Field{ + field: field, + value: v.FieldByName(field.Name), + } + + fields = append(fields, f) + + } + + return fields +} + +// Field returns a new Field struct that provides several high level functions +// around a single struct field entity. It panics if the field is not found. +func (s *Struct) Field(name string) *Field { + f, ok := s.FieldOk(name) + if !ok { + panic("field not found") + } + + return f +} + +// FieldOk returns a new Field struct that provides several high level functions +// around a single struct field entity. The boolean returns true if the field +// was found. +func (s *Struct) FieldOk(name string) (*Field, bool) { + t := s.value.Type() + + field, ok := t.FieldByName(name) + if !ok { + return nil, false + } + + return &Field{ + field: field, + value: s.value.FieldByName(name), + defaultTag: s.TagName, + }, true +} + +// IsZero returns true if all fields in a struct is a zero value (not +// initialized) A struct tag with the content of "-" ignores the checking of +// that particular field. Example: +// +// // Field is ignored by this package. +// Field bool `structs:"-"` +// +// A value with the option of "omitnested" stops iterating further if the type +// is a struct. Example: +// +// // Field is not processed further by this package. +// Field time.Time `structs:"myName,omitnested"` +// Field *http.Request `structs:",omitnested"` +// +// Note that only exported fields of a struct can be accessed, non exported +// fields will be neglected. It panics if s's kind is not struct. +func (s *Struct) IsZero() bool { + fields := s.structFields() + + for _, field := range fields { + val := s.value.FieldByName(field.Name) + + _, tagOpts := parseTag(field.Tag.Get(s.TagName)) + + if IsStruct(val.Interface()) && !tagOpts.Has("omitnested") { + ok := IsZero(val.Interface()) + if !ok { + return false + } + + continue + } + + // zero value of the given field, such as "" for string, 0 for int + zero := reflect.Zero(val.Type()).Interface() + + // current value of the given field + current := val.Interface() + + if !reflect.DeepEqual(current, zero) { + return false + } + } + + return true +} + +// HasZero returns true if a field in a struct is not initialized (zero value). +// A struct tag with the content of "-" ignores the checking of that particular +// field. Example: +// +// // Field is ignored by this package. +// Field bool `structs:"-"` +// +// A value with the option of "omitnested" stops iterating further if the type +// is a struct. Example: +// +// // Field is not processed further by this package. +// Field time.Time `structs:"myName,omitnested"` +// Field *http.Request `structs:",omitnested"` +// +// Note that only exported fields of a struct can be accessed, non exported +// fields will be neglected. It panics if s's kind is not struct. +func (s *Struct) HasZero() bool { + fields := s.structFields() + + for _, field := range fields { + val := s.value.FieldByName(field.Name) + + _, tagOpts := parseTag(field.Tag.Get(s.TagName)) + + if IsStruct(val.Interface()) && !tagOpts.Has("omitnested") { + ok := HasZero(val.Interface()) + if ok { + return true + } + + continue + } + + // zero value of the given field, such as "" for string, 0 for int + zero := reflect.Zero(val.Type()).Interface() + + // current value of the given field + current := val.Interface() + + if reflect.DeepEqual(current, zero) { + return true + } + } + + return false +} + +// Name returns the structs's type name within its package. For more info refer +// to Name() function. +func (s *Struct) Name() string { + return s.value.Type().Name() +} + +// structFields returns the exported struct fields for a given s struct. This +// is a convenient helper method to avoid duplicate code in some of the +// functions. +func (s *Struct) structFields() []reflect.StructField { + t := s.value.Type() + + var f []reflect.StructField + + for i := 0; i < t.NumField(); i++ { + field := t.Field(i) + // we can't access the value of unexported fields + if field.PkgPath != "" { + continue + } + + // don't check if it's omitted + if tag := field.Tag.Get(s.TagName); tag == "-" { + continue + } + + f = append(f, field) + } + + return f +} + +func strctVal(s interface{}) reflect.Value { + v := reflect.ValueOf(s) + + // if pointer get the underlying element≤ + for v.Kind() == reflect.Ptr { + v = v.Elem() + } + + if v.Kind() != reflect.Struct { + panic("not struct") + } + + return v +} + +// Map converts the given struct to a map[string]interface{}. For more info +// refer to Struct types Map() method. It panics if s's kind is not struct. +func Map(s interface{}) map[string]interface{} { + return New(s).Map() +} + +// FillMap is the same as Map. Instead of returning the output, it fills the +// given map. +func FillMap(s interface{}, out map[string]interface{}) { + New(s).FillMap(out) +} + +// Values converts the given struct to a []interface{}. For more info refer to +// Struct types Values() method. It panics if s's kind is not struct. +func Values(s interface{}) []interface{} { + return New(s).Values() +} + +// Fields returns a slice of *Field. For more info refer to Struct types +// Fields() method. It panics if s's kind is not struct. +func Fields(s interface{}) []*Field { + return New(s).Fields() +} + +// Names returns a slice of field names. For more info refer to Struct types +// Names() method. It panics if s's kind is not struct. +func Names(s interface{}) []string { + return New(s).Names() +} + +// IsZero returns true if all fields is equal to a zero value. For more info +// refer to Struct types IsZero() method. It panics if s's kind is not struct. +func IsZero(s interface{}) bool { + return New(s).IsZero() +} + +// HasZero returns true if any field is equal to a zero value. For more info +// refer to Struct types HasZero() method. It panics if s's kind is not struct. +func HasZero(s interface{}) bool { + return New(s).HasZero() +} + +// IsStruct returns true if the given variable is a struct or a pointer to +// struct. +func IsStruct(s interface{}) bool { + v := reflect.ValueOf(s) + if v.Kind() == reflect.Ptr { + v = v.Elem() + } + + // uninitialized zero value of a struct + if v.Kind() == reflect.Invalid { + return false + } + + return v.Kind() == reflect.Struct +} + +// Name returns the structs's type name within its package. It returns an +// empty string for unnamed types. It panics if s's kind is not struct. +func Name(s interface{}) string { + return New(s).Name() +} + +// nested retrieves recursively all types for the given value and returns the +// nested value. +func (s *Struct) nested(val reflect.Value) interface{} { + var finalVal interface{} + + v := reflect.ValueOf(val.Interface()) + if v.Kind() == reflect.Ptr { + v = v.Elem() + } + + switch v.Kind() { + case reflect.Struct: + n := New(val.Interface()) + n.TagName = s.TagName + m := n.Map() + + // do not add the converted value if there are no exported fields, ie: + // time.Time + if len(m) == 0 { + finalVal = val.Interface() + } else { + finalVal = m + } + case reflect.Map: + // get the element type of the map + mapElem := val.Type() + switch val.Type().Kind() { + case reflect.Ptr, reflect.Array, reflect.Map, + reflect.Slice, reflect.Chan: + mapElem = val.Type().Elem() + if mapElem.Kind() == reflect.Ptr { + mapElem = mapElem.Elem() + } + } + + // only iterate over struct types, ie: map[string]StructType, + // map[string][]StructType, + if mapElem.Kind() == reflect.Struct || + (mapElem.Kind() == reflect.Slice && + mapElem.Elem().Kind() == reflect.Struct) { + m := make(map[string]interface{}, val.Len()) + for _, k := range val.MapKeys() { + m[k.String()] = s.nested(val.MapIndex(k)) + } + finalVal = m + break + } + + // TODO(arslan): should this be optional? + finalVal = val.Interface() + case reflect.Slice, reflect.Array: + if val.Type().Kind() == reflect.Interface { + finalVal = val.Interface() + break + } + + // TODO(arslan): should this be optional? + // do not iterate of non struct types, just pass the value. Ie: []int, + // []string, co... We only iterate further if it's a struct. + // i.e []foo or []*foo + if val.Type().Elem().Kind() != reflect.Struct && + !(val.Type().Elem().Kind() == reflect.Ptr && + val.Type().Elem().Elem().Kind() == reflect.Struct) { + finalVal = val.Interface() + break + } + + slices := make([]interface{}, val.Len()) + for x := 0; x < val.Len(); x++ { + slices[x] = s.nested(val.Index(x)) + } + finalVal = slices + default: + finalVal = val.Interface() + } + + return finalVal +} diff --git a/vendor/github.com/fatih/structs/tags.go b/vendor/github.com/fatih/structs/tags.go new file mode 100644 index 000000000..136a31eba --- /dev/null +++ b/vendor/github.com/fatih/structs/tags.go @@ -0,0 +1,32 @@ +package structs + +import "strings" + +// tagOptions contains a slice of tag options +type tagOptions []string + +// Has returns true if the given option is available in tagOptions +func (t tagOptions) Has(opt string) bool { + for _, tagOpt := range t { + if tagOpt == opt { + return true + } + } + + return false +} + +// parseTag splits a struct field's tag into its name and a list of options +// which comes after a name. A tag is in the form of: "name,option1,option2". +// The name can be neglectected. +func parseTag(tag string) (string, tagOptions) { + // tag is one of followings: + // "" + // "name" + // "name,opt" + // "name,opt,opt2" + // ",opt" + + res := strings.Split(tag, ",") + return res[0], res[1:] +} diff --git a/vendor/github.com/kolo/xmlrpc/LICENSE b/vendor/github.com/kolo/xmlrpc/LICENSE new file mode 100644 index 000000000..8103dd139 --- /dev/null +++ b/vendor/github.com/kolo/xmlrpc/LICENSE @@ -0,0 +1,19 @@ +Copyright (C) 2012 Dmitry Maksimov + +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. diff --git a/vendor/github.com/kolo/xmlrpc/client.go b/vendor/github.com/kolo/xmlrpc/client.go new file mode 100644 index 000000000..db2eb4f64 --- /dev/null +++ b/vendor/github.com/kolo/xmlrpc/client.go @@ -0,0 +1,169 @@ +package xmlrpc + +import ( + "errors" + "fmt" + "io/ioutil" + "net/http" + "net/http/cookiejar" + "net/rpc" + "net/url" + "sync" +) + +type Client struct { + *rpc.Client +} + +// clientCodec is rpc.ClientCodec interface implementation. +type clientCodec struct { + // url presents url of xmlrpc service + url *url.URL + + // httpClient works with HTTP protocol + httpClient *http.Client + + // cookies stores cookies received on last request + cookies http.CookieJar + + // responses presents map of active requests. It is required to return request id, that + // rpc.Client can mark them as done. + responses map[uint64]*http.Response + mutex sync.Mutex + + response *Response + + // ready presents channel, that is used to link request and it`s response. + ready chan uint64 + + // close notifies codec is closed. + close chan uint64 +} + +func (codec *clientCodec) WriteRequest(request *rpc.Request, args interface{}) (err error) { + httpRequest, err := NewRequest(codec.url.String(), request.ServiceMethod, args) + + if codec.cookies != nil { + for _, cookie := range codec.cookies.Cookies(codec.url) { + httpRequest.AddCookie(cookie) + } + } + + if err != nil { + return err + } + + var httpResponse *http.Response + httpResponse, err = codec.httpClient.Do(httpRequest) + + if err != nil { + return err + } + + if codec.cookies != nil { + codec.cookies.SetCookies(codec.url, httpResponse.Cookies()) + } + + codec.mutex.Lock() + codec.responses[request.Seq] = httpResponse + codec.mutex.Unlock() + + codec.ready <- request.Seq + + return nil +} + +func (codec *clientCodec) ReadResponseHeader(response *rpc.Response) (err error) { + var seq uint64 + + select { + case seq = <-codec.ready: + case <-codec.close: + return errors.New("codec is closed") + } + + codec.mutex.Lock() + httpResponse := codec.responses[seq] + codec.mutex.Unlock() + + if httpResponse.StatusCode < 200 || httpResponse.StatusCode >= 300 { + return fmt.Errorf("request error: bad status code - %d", httpResponse.StatusCode) + } + + respData, err := ioutil.ReadAll(httpResponse.Body) + + if err != nil { + return err + } + + httpResponse.Body.Close() + + resp := NewResponse(respData) + + if resp.Failed() { + response.Error = fmt.Sprintf("%v", resp.Err()) + } + + codec.response = resp + + response.Seq = seq + + codec.mutex.Lock() + delete(codec.responses, seq) + codec.mutex.Unlock() + + return nil +} + +func (codec *clientCodec) ReadResponseBody(v interface{}) (err error) { + if v == nil { + return nil + } + + if err = codec.response.Unmarshal(v); err != nil { + return err + } + + return nil +} + +func (codec *clientCodec) Close() error { + transport := codec.httpClient.Transport.(*http.Transport) + transport.CloseIdleConnections() + + close(codec.close) + + return nil +} + +// NewClient returns instance of rpc.Client object, that is used to send request to xmlrpc service. +func NewClient(requrl string, transport http.RoundTripper) (*Client, error) { + if transport == nil { + transport = http.DefaultTransport + } + + httpClient := &http.Client{Transport: transport} + + jar, err := cookiejar.New(nil) + + if err != nil { + return nil, err + } + + u, err := url.Parse(requrl) + + if err != nil { + return nil, err + } + + codec := clientCodec{ + url: u, + httpClient: httpClient, + close: make(chan uint64), + ready: make(chan uint64), + responses: make(map[uint64]*http.Response), + cookies: jar, + } + + return &Client{rpc.NewClientWithCodec(&codec)}, nil +} diff --git a/vendor/github.com/kolo/xmlrpc/decoder.go b/vendor/github.com/kolo/xmlrpc/decoder.go new file mode 100644 index 000000000..b81fff4c0 --- /dev/null +++ b/vendor/github.com/kolo/xmlrpc/decoder.go @@ -0,0 +1,463 @@ +package xmlrpc + +import ( + "bytes" + "encoding/xml" + "errors" + "fmt" + "io" + "reflect" + "strconv" + "strings" + "time" +) + +const ( + iso8601 = "20060102T15:04:05" + iso8601Z = "20060102T15:04:05Z07:00" + iso8601Hyphen = "2006-01-02T15:04:05" + iso8601HyphenZ = "2006-01-02T15:04:05Z07:00" +) + +var ( + // CharsetReader is a function to generate reader which converts a non UTF-8 + // charset into UTF-8. + CharsetReader func(string, io.Reader) (io.Reader, error) + + timeLayouts = []string{iso8601, iso8601Z, iso8601Hyphen, iso8601HyphenZ} + invalidXmlError = errors.New("invalid xml") +) + +type TypeMismatchError string + +func (e TypeMismatchError) Error() string { return string(e) } + +type decoder struct { + *xml.Decoder +} + +func unmarshal(data []byte, v interface{}) (err error) { + dec := &decoder{xml.NewDecoder(bytes.NewBuffer(data))} + + if CharsetReader != nil { + dec.CharsetReader = CharsetReader + } + + var tok xml.Token + for { + if tok, err = dec.Token(); err != nil { + return err + } + + if t, ok := tok.(xml.StartElement); ok { + if t.Name.Local == "value" { + val := reflect.ValueOf(v) + if val.Kind() != reflect.Ptr { + return errors.New("non-pointer value passed to unmarshal") + } + if err = dec.decodeValue(val.Elem()); err != nil { + return err + } + + break + } + } + } + + // read until end of document + err = dec.Skip() + if err != nil && err != io.EOF { + return err + } + + return nil +} + +func (dec *decoder) decodeValue(val reflect.Value) error { + var tok xml.Token + var err error + + if val.Kind() == reflect.Ptr { + if val.IsNil() { + val.Set(reflect.New(val.Type().Elem())) + } + val = val.Elem() + } + + var typeName string + for { + if tok, err = dec.Token(); err != nil { + return err + } + + if t, ok := tok.(xml.EndElement); ok { + if t.Name.Local == "value" { + return nil + } else { + return invalidXmlError + } + } + + if t, ok := tok.(xml.StartElement); ok { + typeName = t.Name.Local + break + } + + // Treat value data without type identifier as string + if t, ok := tok.(xml.CharData); ok { + if value := strings.TrimSpace(string(t)); value != "" { + if err = checkType(val, reflect.String); err != nil { + return err + } + + val.SetString(value) + return nil + } + } + } + + switch typeName { + case "struct": + ismap := false + pmap := val + valType := val.Type() + + if err = checkType(val, reflect.Struct); err != nil { + if checkType(val, reflect.Map) == nil { + if valType.Key().Kind() != reflect.String { + return fmt.Errorf("only maps with string key type can be unmarshalled") + } + ismap = true + } else if checkType(val, reflect.Interface) == nil && val.IsNil() { + var dummy map[string]interface{} + pmap = reflect.New(reflect.TypeOf(dummy)).Elem() + valType = pmap.Type() + ismap = true + } else { + return err + } + } + + var fields map[string]reflect.Value + + if !ismap { + fields = make(map[string]reflect.Value) + + for i := 0; i < valType.NumField(); i++ { + field := valType.Field(i) + fieldVal := val.FieldByName(field.Name) + + if fieldVal.CanSet() { + if fn := field.Tag.Get("xmlrpc"); fn != "" { + fields[fn] = fieldVal + } else { + fields[field.Name] = fieldVal + } + } + } + } else { + // Create initial empty map + pmap.Set(reflect.MakeMap(valType)) + } + + // Process struct members. + StructLoop: + for { + if tok, err = dec.Token(); err != nil { + return err + } + switch t := tok.(type) { + case xml.StartElement: + if t.Name.Local != "member" { + return invalidXmlError + } + + tagName, fieldName, err := dec.readTag() + if err != nil { + return err + } + if tagName != "name" { + return invalidXmlError + } + + var fv reflect.Value + ok := true + + if !ismap { + fv, ok = fields[string(fieldName)] + } else { + fv = reflect.New(valType.Elem()) + } + + if ok { + for { + if tok, err = dec.Token(); err != nil { + return err + } + if t, ok := tok.(xml.StartElement); ok && t.Name.Local == "value" { + if err = dec.decodeValue(fv); err != nil { + return err + } + + // + if err = dec.Skip(); err != nil { + return err + } + + break + } + } + } + + // + if err = dec.Skip(); err != nil { + return err + } + + if ismap { + pmap.SetMapIndex(reflect.ValueOf(string(fieldName)), reflect.Indirect(fv)) + val.Set(pmap) + } + case xml.EndElement: + break StructLoop + } + } + case "array": + pslice := val + if checkType(val, reflect.Interface) == nil && val.IsNil() { + var dummy []interface{} + pslice = reflect.New(reflect.TypeOf(dummy)).Elem() + } else if err = checkType(val, reflect.Slice); err != nil { + return err + } + + ArrayLoop: + for { + if tok, err = dec.Token(); err != nil { + return err + } + + switch t := tok.(type) { + case xml.StartElement: + if t.Name.Local != "data" { + return invalidXmlError + } + + slice := reflect.MakeSlice(pslice.Type(), 0, 0) + + DataLoop: + for { + if tok, err = dec.Token(); err != nil { + return err + } + + switch tt := tok.(type) { + case xml.StartElement: + if tt.Name.Local != "value" { + return invalidXmlError + } + + v := reflect.New(pslice.Type().Elem()) + if err = dec.decodeValue(v); err != nil { + return err + } + + slice = reflect.Append(slice, v.Elem()) + + // + if err = dec.Skip(); err != nil { + return err + } + case xml.EndElement: + pslice.Set(slice) + val.Set(pslice) + break DataLoop + } + } + case xml.EndElement: + break ArrayLoop + } + } + default: + if tok, err = dec.Token(); err != nil { + return err + } + + var data []byte + + switch t := tok.(type) { + case xml.EndElement: + return nil + case xml.CharData: + data = []byte(t.Copy()) + default: + return invalidXmlError + } + + switch typeName { + case "int", "i4", "i8": + if checkType(val, reflect.Interface) == nil && val.IsNil() { + i, err := strconv.ParseInt(string(data), 10, 64) + if err != nil { + return err + } + + pi := reflect.New(reflect.TypeOf(i)).Elem() + pi.SetInt(i) + val.Set(pi) + } else if err = checkType(val, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64); err != nil { + return err + } else { + i, err := strconv.ParseInt(string(data), 10, val.Type().Bits()) + if err != nil { + return err + } + + val.SetInt(i) + } + case "string", "base64": + str := string(data) + if checkType(val, reflect.Interface) == nil && val.IsNil() { + pstr := reflect.New(reflect.TypeOf(str)).Elem() + pstr.SetString(str) + val.Set(pstr) + } else if err = checkType(val, reflect.String); err != nil { + return err + } else { + val.SetString(str) + } + case "dateTime.iso8601": + var t time.Time + var err error + + for _, layout := range timeLayouts { + t, err = time.Parse(layout, string(data)) + if err == nil { + break + } + } + if err != nil { + return err + } + + if checkType(val, reflect.Interface) == nil && val.IsNil() { + ptime := reflect.New(reflect.TypeOf(t)).Elem() + ptime.Set(reflect.ValueOf(t)) + val.Set(ptime) + } else if _, ok := val.Interface().(time.Time); !ok { + return TypeMismatchError(fmt.Sprintf("error: type mismatch error - can't decode %v to time", val.Kind())) + } else { + val.Set(reflect.ValueOf(t)) + } + case "boolean": + v, err := strconv.ParseBool(string(data)) + if err != nil { + return err + } + + if checkType(val, reflect.Interface) == nil && val.IsNil() { + pv := reflect.New(reflect.TypeOf(v)).Elem() + pv.SetBool(v) + val.Set(pv) + } else if err = checkType(val, reflect.Bool); err != nil { + return err + } else { + val.SetBool(v) + } + case "double": + if checkType(val, reflect.Interface) == nil && val.IsNil() { + i, err := strconv.ParseFloat(string(data), 64) + if err != nil { + return err + } + + pdouble := reflect.New(reflect.TypeOf(i)).Elem() + pdouble.SetFloat(i) + val.Set(pdouble) + } else if err = checkType(val, reflect.Float32, reflect.Float64); err != nil { + return err + } else { + i, err := strconv.ParseFloat(string(data), val.Type().Bits()) + if err != nil { + return err + } + + val.SetFloat(i) + } + default: + return errors.New("unsupported type") + } + + // + if err = dec.Skip(); err != nil { + return err + } + } + + return nil +} + +func (dec *decoder) readTag() (string, []byte, error) { + var tok xml.Token + var err error + + var name string + for { + if tok, err = dec.Token(); err != nil { + return "", nil, err + } + + if t, ok := tok.(xml.StartElement); ok { + name = t.Name.Local + break + } + } + + value, err := dec.readCharData() + if err != nil { + return "", nil, err + } + + return name, value, dec.Skip() +} + +func (dec *decoder) readCharData() ([]byte, error) { + var tok xml.Token + var err error + + if tok, err = dec.Token(); err != nil { + return nil, err + } + + if t, ok := tok.(xml.CharData); ok { + return []byte(t.Copy()), nil + } else { + return nil, invalidXmlError + } +} + +func checkType(val reflect.Value, kinds ...reflect.Kind) error { + if len(kinds) == 0 { + return nil + } + + if val.Kind() == reflect.Ptr { + val = val.Elem() + } + + match := false + + for _, kind := range kinds { + if val.Kind() == kind { + match = true + break + } + } + + if !match { + return TypeMismatchError(fmt.Sprintf("error: type mismatch - can't unmarshal %v to %v", + val.Kind(), kinds[0])) + } + + return nil +} diff --git a/vendor/github.com/kolo/xmlrpc/encoder.go b/vendor/github.com/kolo/xmlrpc/encoder.go new file mode 100644 index 000000000..bb1285ff7 --- /dev/null +++ b/vendor/github.com/kolo/xmlrpc/encoder.go @@ -0,0 +1,164 @@ +package xmlrpc + +import ( + "bytes" + "encoding/xml" + "fmt" + "reflect" + "strconv" + "time" +) + +type encodeFunc func(reflect.Value) ([]byte, error) + +func marshal(v interface{}) ([]byte, error) { + if v == nil { + return []byte{}, nil + } + + val := reflect.ValueOf(v) + return encodeValue(val) +} + +func encodeValue(val reflect.Value) ([]byte, error) { + var b []byte + var err error + + if val.Kind() == reflect.Ptr || val.Kind() == reflect.Interface { + if val.IsNil() { + return []byte(""), nil + } + + val = val.Elem() + } + + switch val.Kind() { + case reflect.Struct: + switch val.Interface().(type) { + case time.Time: + t := val.Interface().(time.Time) + b = []byte(fmt.Sprintf("%s", t.Format(iso8601))) + default: + b, err = encodeStruct(val) + } + case reflect.Map: + b, err = encodeMap(val) + case reflect.Slice: + b, err = encodeSlice(val) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + b = []byte(fmt.Sprintf("%s", strconv.FormatInt(val.Int(), 10))) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + b = []byte(fmt.Sprintf("%s", strconv.FormatUint(val.Uint(), 10))) + case reflect.Float32, reflect.Float64: + b = []byte(fmt.Sprintf("%s", + strconv.FormatFloat(val.Float(), 'g', -1, val.Type().Bits()))) + case reflect.Bool: + if val.Bool() { + b = []byte("1") + } else { + b = []byte("0") + } + case reflect.String: + var buf bytes.Buffer + + xml.Escape(&buf, []byte(val.String())) + + if _, ok := val.Interface().(Base64); ok { + b = []byte(fmt.Sprintf("%s", buf.String())) + } else { + b = []byte(fmt.Sprintf("%s", buf.String())) + } + default: + return nil, fmt.Errorf("xmlrpc encode error: unsupported type") + } + + if err != nil { + return nil, err + } + + return []byte(fmt.Sprintf("%s", string(b))), nil +} + +func encodeStruct(val reflect.Value) ([]byte, error) { + var b bytes.Buffer + + b.WriteString("") + + t := val.Type() + for i := 0; i < t.NumField(); i++ { + b.WriteString("") + f := t.Field(i) + + name := f.Tag.Get("xmlrpc") + if name == "" { + name = f.Name + } + b.WriteString(fmt.Sprintf("%s", name)) + + p, err := encodeValue(val.FieldByName(f.Name)) + if err != nil { + return nil, err + } + b.Write(p) + + b.WriteString("") + } + + b.WriteString("") + + return b.Bytes(), nil +} + +func encodeMap(val reflect.Value) ([]byte, error) { + var t = val.Type() + + if t.Key().Kind() != reflect.String { + return nil, fmt.Errorf("xmlrpc encode error: only maps with string keys are supported") + } + + var b bytes.Buffer + + b.WriteString("") + + keys := val.MapKeys() + + for i := 0; i < val.Len(); i++ { + key := keys[i] + kval := val.MapIndex(key) + + b.WriteString("") + b.WriteString(fmt.Sprintf("%s", key.String())) + + p, err := encodeValue(kval) + + if err != nil { + return nil, err + } + + b.Write(p) + b.WriteString("") + } + + b.WriteString("") + + return b.Bytes(), nil +} + +func encodeSlice(val reflect.Value) ([]byte, error) { + var b bytes.Buffer + + b.WriteString("") + + for i := 0; i < val.Len(); i++ { + p, err := encodeValue(val.Index(i)) + if err != nil { + return nil, err + } + + b.Write(p) + } + + b.WriteString("") + + return b.Bytes(), nil +} diff --git a/vendor/github.com/kolo/xmlrpc/request.go b/vendor/github.com/kolo/xmlrpc/request.go new file mode 100644 index 000000000..acb8251b2 --- /dev/null +++ b/vendor/github.com/kolo/xmlrpc/request.go @@ -0,0 +1,57 @@ +package xmlrpc + +import ( + "bytes" + "fmt" + "net/http" +) + +func NewRequest(url string, method string, args interface{}) (*http.Request, error) { + var t []interface{} + var ok bool + if t, ok = args.([]interface{}); !ok { + if args != nil { + t = []interface{}{args} + } + } + + body, err := EncodeMethodCall(method, t...) + if err != nil { + return nil, err + } + + request, err := http.NewRequest("POST", url, bytes.NewReader(body)) + if err != nil { + return nil, err + } + + request.Header.Set("Content-Type", "text/xml") + request.Header.Set("Content-Length", fmt.Sprintf("%d", len(body))) + + return request, nil +} + +func EncodeMethodCall(method string, args ...interface{}) ([]byte, error) { + var b bytes.Buffer + b.WriteString(``) + b.WriteString(fmt.Sprintf("%s", method)) + + if args != nil { + b.WriteString("") + + for _, arg := range args { + p, err := marshal(arg) + if err != nil { + return nil, err + } + + b.WriteString(fmt.Sprintf("%s", string(p))) + } + + b.WriteString("") + } + + b.WriteString("") + + return b.Bytes(), nil +} diff --git a/vendor/github.com/kolo/xmlrpc/response.go b/vendor/github.com/kolo/xmlrpc/response.go new file mode 100644 index 000000000..6742a1c74 --- /dev/null +++ b/vendor/github.com/kolo/xmlrpc/response.go @@ -0,0 +1,52 @@ +package xmlrpc + +import ( + "regexp" +) + +var ( + faultRx = regexp.MustCompile(`(\s|\S)+`) +) + +type failedResponse struct { + Code int `xmlrpc:"faultCode"` + Error string `xmlrpc:"faultString"` +} + +func (r *failedResponse) err() error { + return &xmlrpcError{ + code: r.Code, + err: r.Error, + } +} + +type Response struct { + data []byte +} + +func NewResponse(data []byte) *Response { + return &Response{ + data: data, + } +} + +func (r *Response) Failed() bool { + return faultRx.Match(r.data) +} + +func (r *Response) Err() error { + failedResp := new(failedResponse) + if err := unmarshal(r.data, failedResp); err != nil { + return err + } + + return failedResp.err() +} + +func (r *Response) Unmarshal(v interface{}) error { + if err := unmarshal(r.data, v); err != nil { + return err + } + + return nil +} diff --git a/vendor/github.com/kolo/xmlrpc/xmlrpc.go b/vendor/github.com/kolo/xmlrpc/xmlrpc.go new file mode 100644 index 000000000..8766403af --- /dev/null +++ b/vendor/github.com/kolo/xmlrpc/xmlrpc.go @@ -0,0 +1,19 @@ +package xmlrpc + +import ( + "fmt" +) + +// xmlrpcError represents errors returned on xmlrpc request. +type xmlrpcError struct { + code int + err string +} + +// Error() method implements Error interface +func (e *xmlrpcError) Error() string { + return fmt.Sprintf("error: \"%s\" code: %d", e.err, e.code) +} + +// Base64 represents value in base64 encoding +type Base64 string diff --git a/vendor/github.com/mitchellh/mapstructure/decode_hooks.go b/vendor/github.com/mitchellh/mapstructure/decode_hooks.go index afcfd5eed..1f0abc65a 100644 --- a/vendor/github.com/mitchellh/mapstructure/decode_hooks.go +++ b/vendor/github.com/mitchellh/mapstructure/decode_hooks.go @@ -2,6 +2,8 @@ package mapstructure import ( "errors" + "fmt" + "net" "reflect" "strconv" "strings" @@ -115,6 +117,69 @@ func StringToTimeDurationHookFunc() DecodeHookFunc { } } +// StringToIPHookFunc returns a DecodeHookFunc that converts +// strings to net.IP +func StringToIPHookFunc() DecodeHookFunc { + return func( + f reflect.Type, + t reflect.Type, + data interface{}) (interface{}, error) { + if f.Kind() != reflect.String { + return data, nil + } + if t != reflect.TypeOf(net.IP{}) { + return data, nil + } + + // Convert it by parsing + ip := net.ParseIP(data.(string)) + if ip == nil { + return net.IP{}, fmt.Errorf("failed parsing ip %v", data) + } + + return ip, nil + } +} + +// StringToIPNetHookFunc returns a DecodeHookFunc that converts +// strings to net.IPNet +func StringToIPNetHookFunc() DecodeHookFunc { + return func( + f reflect.Type, + t reflect.Type, + data interface{}) (interface{}, error) { + if f.Kind() != reflect.String { + return data, nil + } + if t != reflect.TypeOf(net.IPNet{}) { + return data, nil + } + + // Convert it by parsing + _, net, err := net.ParseCIDR(data.(string)) + return net, err + } +} + +// StringToTimeHookFunc returns a DecodeHookFunc that converts +// strings to time.Time. +func StringToTimeHookFunc(layout string) DecodeHookFunc { + return func( + f reflect.Type, + t reflect.Type, + data interface{}) (interface{}, error) { + if f.Kind() != reflect.String { + return data, nil + } + if t != reflect.TypeOf(time.Time{}) { + return data, nil + } + + // Convert it by parsing + return time.Parse(layout, data.(string)) + } +} + // WeaklyTypedHook is a DecodeHookFunc which adds support for weak typing to // the decoder. // diff --git a/vendor/github.com/mitchellh/mapstructure/mapstructure.go b/vendor/github.com/mitchellh/mapstructure/mapstructure.go index 39ec1e943..256ee63fb 100644 --- a/vendor/github.com/mitchellh/mapstructure/mapstructure.go +++ b/vendor/github.com/mitchellh/mapstructure/mapstructure.go @@ -114,12 +114,12 @@ type Metadata struct { Unused []string } -// Decode takes a map and uses reflection to convert it into the -// given Go native structure. val must be a pointer to a struct. -func Decode(m interface{}, rawVal interface{}) error { +// Decode takes an input structure and uses reflection to translate it to +// the output structure. output must be a pointer to a map or struct. +func Decode(input interface{}, output interface{}) error { config := &DecoderConfig{ Metadata: nil, - Result: rawVal, + Result: output, } decoder, err := NewDecoder(config) @@ -127,7 +127,7 @@ func Decode(m interface{}, rawVal interface{}) error { return err } - return decoder.Decode(m) + return decoder.Decode(input) } // WeakDecode is the same as Decode but is shorthand to enable @@ -147,6 +147,40 @@ func WeakDecode(input, output interface{}) error { return decoder.Decode(input) } +// DecodeMetadata is the same as Decode, but is shorthand to +// enable metadata collection. See DecoderConfig for more info. +func DecodeMetadata(input interface{}, output interface{}, metadata *Metadata) error { + config := &DecoderConfig{ + Metadata: metadata, + Result: output, + } + + decoder, err := NewDecoder(config) + if err != nil { + return err + } + + return decoder.Decode(input) +} + +// WeakDecodeMetadata is the same as Decode, but is shorthand to +// enable both WeaklyTypedInput and metadata collection. See +// DecoderConfig for more info. +func WeakDecodeMetadata(input interface{}, output interface{}, metadata *Metadata) error { + config := &DecoderConfig{ + Metadata: metadata, + Result: output, + WeaklyTypedInput: true, + } + + decoder, err := NewDecoder(config) + if err != nil { + return err + } + + return decoder.Decode(input) +} + // NewDecoder returns a new decoder for the given configuration. Once // a decoder has been returned, the same configuration must not be used // again. @@ -184,70 +218,91 @@ func NewDecoder(config *DecoderConfig) (*Decoder, error) { // Decode decodes the given raw interface to the target pointer specified // by the configuration. -func (d *Decoder) Decode(raw interface{}) error { - return d.decode("", raw, reflect.ValueOf(d.config.Result).Elem()) +func (d *Decoder) Decode(input interface{}) error { + return d.decode("", input, reflect.ValueOf(d.config.Result).Elem()) } // Decodes an unknown data type into a specific reflection value. -func (d *Decoder) decode(name string, data interface{}, val reflect.Value) error { - if data == nil { - // If the data is nil, then we don't set anything. +func (d *Decoder) decode(name string, input interface{}, outVal reflect.Value) error { + var inputVal reflect.Value + if input != nil { + inputVal = reflect.ValueOf(input) + + // We need to check here if input is a typed nil. Typed nils won't + // match the "input == nil" below so we check that here. + if inputVal.Kind() == reflect.Ptr && inputVal.IsNil() { + input = nil + } + } + + if input == nil { + // If the data is nil, then we don't set anything, unless ZeroFields is set + // to true. + if d.config.ZeroFields { + outVal.Set(reflect.Zero(outVal.Type())) + + if d.config.Metadata != nil && name != "" { + d.config.Metadata.Keys = append(d.config.Metadata.Keys, name) + } + } return nil } - dataVal := reflect.ValueOf(data) - if !dataVal.IsValid() { - // If the data value is invalid, then we just set the value + if !inputVal.IsValid() { + // If the input value is invalid, then we just set the value // to be the zero value. - val.Set(reflect.Zero(val.Type())) + outVal.Set(reflect.Zero(outVal.Type())) + if d.config.Metadata != nil && name != "" { + d.config.Metadata.Keys = append(d.config.Metadata.Keys, name) + } return nil } if d.config.DecodeHook != nil { - // We have a DecodeHook, so let's pre-process the data. + // We have a DecodeHook, so let's pre-process the input. var err error - data, err = DecodeHookExec( + input, err = DecodeHookExec( d.config.DecodeHook, - dataVal.Type(), val.Type(), data) + inputVal.Type(), outVal.Type(), input) if err != nil { return fmt.Errorf("error decoding '%s': %s", name, err) } } var err error - dataKind := getKind(val) - switch dataKind { + outputKind := getKind(outVal) + switch outputKind { case reflect.Bool: - err = d.decodeBool(name, data, val) + err = d.decodeBool(name, input, outVal) case reflect.Interface: - err = d.decodeBasic(name, data, val) + err = d.decodeBasic(name, input, outVal) case reflect.String: - err = d.decodeString(name, data, val) + err = d.decodeString(name, input, outVal) case reflect.Int: - err = d.decodeInt(name, data, val) + err = d.decodeInt(name, input, outVal) case reflect.Uint: - err = d.decodeUint(name, data, val) + err = d.decodeUint(name, input, outVal) case reflect.Float32: - err = d.decodeFloat(name, data, val) + err = d.decodeFloat(name, input, outVal) case reflect.Struct: - err = d.decodeStruct(name, data, val) + err = d.decodeStruct(name, input, outVal) case reflect.Map: - err = d.decodeMap(name, data, val) + err = d.decodeMap(name, input, outVal) case reflect.Ptr: - err = d.decodePtr(name, data, val) + err = d.decodePtr(name, input, outVal) case reflect.Slice: - err = d.decodeSlice(name, data, val) + err = d.decodeSlice(name, input, outVal) case reflect.Array: - err = d.decodeArray(name, data, val) + err = d.decodeArray(name, input, outVal) case reflect.Func: - err = d.decodeFunc(name, data, val) + err = d.decodeFunc(name, input, outVal) default: // If we reached this point then we weren't able to decode it - return fmt.Errorf("%s: unsupported type: %s", name, dataKind) + return fmt.Errorf("%s: unsupported type: %s", name, outputKind) } // If we reached here, then we successfully decoded SOMETHING, so - // mark the key as used if we're tracking metadata. + // mark the key as used if we're tracking metainput. if d.config.Metadata != nil && name != "" { d.config.Metadata.Keys = append(d.config.Metadata.Keys, name) } @@ -258,7 +313,19 @@ func (d *Decoder) decode(name string, data interface{}, val reflect.Value) error // This decodes a basic type (bool, int, string, etc.) and sets the // value to "data" of that type. func (d *Decoder) decodeBasic(name string, data interface{}, val reflect.Value) error { + if val.IsValid() && val.Elem().IsValid() { + return d.decode(name, data, val.Elem()) + } + dataVal := reflect.ValueOf(data) + + // If the input data is a pointer, and the assigned type is the dereference + // of that exact pointer, then indirect it so that we can assign it. + // Example: *string to string + if dataVal.Kind() == reflect.Ptr && dataVal.Type().Elem() == val.Type() { + dataVal = reflect.Indirect(dataVal) + } + if !dataVal.IsValid() { dataVal = reflect.Zero(val.Type()) } @@ -275,7 +342,7 @@ func (d *Decoder) decodeBasic(name string, data interface{}, val reflect.Value) } func (d *Decoder) decodeString(name string, data interface{}, val reflect.Value) error { - dataVal := reflect.ValueOf(data) + dataVal := reflect.Indirect(reflect.ValueOf(data)) dataKind := getKind(dataVal) converted := true @@ -327,7 +394,7 @@ func (d *Decoder) decodeString(name string, data interface{}, val reflect.Value) } func (d *Decoder) decodeInt(name string, data interface{}, val reflect.Value) error { - dataVal := reflect.ValueOf(data) + dataVal := reflect.Indirect(reflect.ValueOf(data)) dataKind := getKind(dataVal) dataType := dataVal.Type() @@ -369,7 +436,7 @@ func (d *Decoder) decodeInt(name string, data interface{}, val reflect.Value) er } func (d *Decoder) decodeUint(name string, data interface{}, val reflect.Value) error { - dataVal := reflect.ValueOf(data) + dataVal := reflect.Indirect(reflect.ValueOf(data)) dataKind := getKind(dataVal) switch { @@ -412,7 +479,7 @@ func (d *Decoder) decodeUint(name string, data interface{}, val reflect.Value) e } func (d *Decoder) decodeBool(name string, data interface{}, val reflect.Value) error { - dataVal := reflect.ValueOf(data) + dataVal := reflect.Indirect(reflect.ValueOf(data)) dataKind := getKind(dataVal) switch { @@ -443,7 +510,7 @@ func (d *Decoder) decodeBool(name string, data interface{}, val reflect.Value) e } func (d *Decoder) decodeFloat(name string, data interface{}, val reflect.Value) error { - dataVal := reflect.ValueOf(data) + dataVal := reflect.Indirect(reflect.ValueOf(data)) dataKind := getKind(dataVal) dataType := dataVal.Type() @@ -499,38 +566,68 @@ func (d *Decoder) decodeMap(name string, data interface{}, val reflect.Value) er valMap = reflect.MakeMap(mapType) } - // Check input type + // Check input type and based on the input type jump to the proper func dataVal := reflect.Indirect(reflect.ValueOf(data)) - if dataVal.Kind() != reflect.Map { - // In weak mode, we accept a slice of maps as an input... + switch dataVal.Kind() { + case reflect.Map: + return d.decodeMapFromMap(name, dataVal, val, valMap) + + case reflect.Struct: + return d.decodeMapFromStruct(name, dataVal, val, valMap) + + case reflect.Array, reflect.Slice: if d.config.WeaklyTypedInput { - switch dataVal.Kind() { - case reflect.Array, reflect.Slice: - // Special case for BC reasons (covered by tests) - if dataVal.Len() == 0 { - val.Set(valMap) - return nil - } - - for i := 0; i < dataVal.Len(); i++ { - err := d.decode( - fmt.Sprintf("%s[%d]", name, i), - dataVal.Index(i).Interface(), val) - if err != nil { - return err - } - } - - return nil - } + return d.decodeMapFromSlice(name, dataVal, val, valMap) } + fallthrough + + default: return fmt.Errorf("'%s' expected a map, got '%s'", name, dataVal.Kind()) } +} + +func (d *Decoder) decodeMapFromSlice(name string, dataVal reflect.Value, val reflect.Value, valMap reflect.Value) error { + // Special case for BC reasons (covered by tests) + if dataVal.Len() == 0 { + val.Set(valMap) + return nil + } + + for i := 0; i < dataVal.Len(); i++ { + err := d.decode( + fmt.Sprintf("%s[%d]", name, i), + dataVal.Index(i).Interface(), val) + if err != nil { + return err + } + } + + return nil +} + +func (d *Decoder) decodeMapFromMap(name string, dataVal reflect.Value, val reflect.Value, valMap reflect.Value) error { + valType := val.Type() + valKeyType := valType.Key() + valElemType := valType.Elem() // Accumulate errors errors := make([]string, 0) + // If the input data is empty, then we just match what the input data is. + if dataVal.Len() == 0 { + if dataVal.IsNil() { + if !val.IsNil() { + val.Set(dataVal) + } + } else { + // Set to empty allocated value + val.Set(valMap) + } + + return nil + } + for _, k := range dataVal.MapKeys() { fieldName := fmt.Sprintf("%s[%s]", name, k) @@ -563,22 +660,128 @@ func (d *Decoder) decodeMap(name string, data interface{}, val reflect.Value) er return nil } +func (d *Decoder) decodeMapFromStruct(name string, dataVal reflect.Value, val reflect.Value, valMap reflect.Value) error { + typ := dataVal.Type() + for i := 0; i < typ.NumField(); i++ { + // Get the StructField first since this is a cheap operation. If the + // field is unexported, then ignore it. + f := typ.Field(i) + if f.PkgPath != "" { + continue + } + + // Next get the actual value of this field and verify it is assignable + // to the map value. + v := dataVal.Field(i) + if !v.Type().AssignableTo(valMap.Type().Elem()) { + return fmt.Errorf("cannot assign type '%s' to map value field of type '%s'", v.Type(), valMap.Type().Elem()) + } + + tagValue := f.Tag.Get(d.config.TagName) + tagParts := strings.Split(tagValue, ",") + + // Determine the name of the key in the map + keyName := f.Name + if tagParts[0] != "" { + if tagParts[0] == "-" { + continue + } + keyName = tagParts[0] + } + + // If "squash" is specified in the tag, we squash the field down. + squash := false + for _, tag := range tagParts[1:] { + if tag == "squash" { + squash = true + break + } + } + if squash && v.Kind() != reflect.Struct { + return fmt.Errorf("cannot squash non-struct type '%s'", v.Type()) + } + + switch v.Kind() { + // this is an embedded struct, so handle it differently + case reflect.Struct: + x := reflect.New(v.Type()) + x.Elem().Set(v) + + vType := valMap.Type() + vKeyType := vType.Key() + vElemType := vType.Elem() + mType := reflect.MapOf(vKeyType, vElemType) + vMap := reflect.MakeMap(mType) + + err := d.decode(keyName, x.Interface(), vMap) + if err != nil { + return err + } + + if squash { + for _, k := range vMap.MapKeys() { + valMap.SetMapIndex(k, vMap.MapIndex(k)) + } + } else { + valMap.SetMapIndex(reflect.ValueOf(keyName), vMap) + } + + default: + valMap.SetMapIndex(reflect.ValueOf(keyName), v) + } + } + + if val.CanAddr() { + val.Set(valMap) + } + + return nil +} + func (d *Decoder) decodePtr(name string, data interface{}, val reflect.Value) error { + // If the input data is nil, then we want to just set the output + // pointer to be nil as well. + isNil := data == nil + if !isNil { + switch v := reflect.Indirect(reflect.ValueOf(data)); v.Kind() { + case reflect.Chan, + reflect.Func, + reflect.Interface, + reflect.Map, + reflect.Ptr, + reflect.Slice: + isNil = v.IsNil() + } + } + if isNil { + if !val.IsNil() && val.CanSet() { + nilValue := reflect.New(val.Type()).Elem() + val.Set(nilValue) + } + + return nil + } + // Create an element of the concrete (non pointer) type and decode // into that. Then set the value of the pointer to this type. valType := val.Type() valElemType := valType.Elem() + if val.CanSet() { + realVal := val + if realVal.IsNil() || d.config.ZeroFields { + realVal = reflect.New(valElemType) + } - realVal := val - if realVal.IsNil() || d.config.ZeroFields { - realVal = reflect.New(valElemType) + if err := d.decode(name, data, reflect.Indirect(realVal)); err != nil { + return err + } + + val.Set(realVal) + } else { + if err := d.decode(name, data, reflect.Indirect(val)); err != nil { + return err + } } - - if err := d.decode(name, data, reflect.Indirect(realVal)); err != nil { - return err - } - - val.Set(realVal) return nil } @@ -604,30 +807,44 @@ func (d *Decoder) decodeSlice(name string, data interface{}, val reflect.Value) valSlice := val if valSlice.IsNil() || d.config.ZeroFields { + if d.config.WeaklyTypedInput { + switch { + // Slice and array we use the normal logic + case dataValKind == reflect.Slice, dataValKind == reflect.Array: + break + + // Empty maps turn into empty slices + case dataValKind == reflect.Map: + if dataVal.Len() == 0 { + val.Set(reflect.MakeSlice(sliceType, 0, 0)) + return nil + } + // Create slice of maps of other sizes + return d.decodeSlice(name, []interface{}{data}, val) + + case dataValKind == reflect.String && valElemType.Kind() == reflect.Uint8: + return d.decodeSlice(name, []byte(dataVal.String()), val) + + // All other types we try to convert to the slice type + // and "lift" it into it. i.e. a string becomes a string slice. + default: + // Just re-try this function with data as a slice. + return d.decodeSlice(name, []interface{}{data}, val) + } + } + // Check input type if dataValKind != reflect.Array && dataValKind != reflect.Slice { - if d.config.WeaklyTypedInput { - switch { - // Empty maps turn into empty slices - case dataValKind == reflect.Map: - if dataVal.Len() == 0 { - val.Set(reflect.MakeSlice(sliceType, 0, 0)) - return nil - } - - // All other types we try to convert to the slice type - // and "lift" it into it. i.e. a string becomes a string slice. - default: - // Just re-try this function with data as a slice. - return d.decodeSlice(name, []interface{}{data}, val) - } - } - return fmt.Errorf( "'%s': source data must be an array or slice, got %s", name, dataValKind) } + // If the input value is empty, then don't allocate since non-nil != nil + if dataVal.Len() == 0 { + return nil + } + // Make a new slice to hold our result, same size as the original data. valSlice = reflect.MakeSlice(sliceType, dataVal.Len(), dataVal.Len()) } @@ -737,10 +954,29 @@ func (d *Decoder) decodeStruct(name string, data interface{}, val reflect.Value) } dataValKind := dataVal.Kind() - if dataValKind != reflect.Map { - return fmt.Errorf("'%s' expected a map, got '%s'", name, dataValKind) - } + switch dataValKind { + case reflect.Map: + return d.decodeStructFromMap(name, dataVal, val) + case reflect.Struct: + // Not the most efficient way to do this but we can optimize later if + // we want to. To convert from struct to struct we go to map first + // as an intermediary. + m := make(map[string]interface{}) + mval := reflect.Indirect(reflect.ValueOf(&m)) + if err := d.decodeMapFromStruct(name, dataVal, mval, mval); err != nil { + return err + } + + result := d.decodeStructFromMap(name, mval, val) + return result + + default: + return fmt.Errorf("'%s' expected a map, got '%s'", name, dataVal.Kind()) + } +} + +func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) error { dataValType := dataVal.Type() if kind := dataValType.Key().Kind(); kind != reflect.String && kind != reflect.Interface { return fmt.Errorf( diff --git a/vendor/github.com/smueller18/goinwx/LICENSE b/vendor/github.com/smueller18/goinwx/LICENSE new file mode 100644 index 000000000..9df6656cd --- /dev/null +++ b/vendor/github.com/smueller18/goinwx/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 Andrew + +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. diff --git a/vendor/github.com/smueller18/goinwx/account.go b/vendor/github.com/smueller18/goinwx/account.go new file mode 100644 index 000000000..14b2073ae --- /dev/null +++ b/vendor/github.com/smueller18/goinwx/account.go @@ -0,0 +1,54 @@ +package goinwx + +const ( + methodAccountLogin = "account.login" + methodAccountLogout = "account.logout" + methodAccountLock = "account.lock" + methodAccountUnlock = "account.unlock" +) + +type AccountService interface { + Login() error + Logout() error + Lock() error + Unlock(tan string) error +} + +type AccountServiceOp struct { + client *Client +} + +var _ AccountService = &AccountServiceOp{} + +func (s *AccountServiceOp) Login() error { + req := s.client.NewRequest(methodAccountLogin, map[string]interface{}{ + "user": s.client.Username, + "pass": s.client.Password, + }) + + _, err := s.client.Do(*req) + return err +} + +func (s *AccountServiceOp) Logout() error { + req := s.client.NewRequest(methodAccountLogout, nil) + + _, err := s.client.Do(*req) + return err +} + +func (s *AccountServiceOp) Lock() error { + req := s.client.NewRequest(methodAccountLock, nil) + + _, err := s.client.Do(*req) + return err +} + +func (s *AccountServiceOp) Unlock(tan string) error { + req := s.client.NewRequest(methodAccountUnlock, map[string]interface{}{ + "tan": tan, + }) + + _, err := s.client.Do(*req) + return err +} diff --git a/vendor/github.com/smueller18/goinwx/contact.go b/vendor/github.com/smueller18/goinwx/contact.go new file mode 100644 index 000000000..0ae36f5c2 --- /dev/null +++ b/vendor/github.com/smueller18/goinwx/contact.go @@ -0,0 +1,150 @@ +package goinwx + +import ( + "github.com/fatih/structs" + "github.com/mitchellh/mapstructure" +) + +const ( + methodContactInfo = "contact.info" + methodContactList = "contact.list" + methodContactCreate = "contact.create" + methodContactDelete = "contact.delete" + methodContactUpdate = "contact.update" +) + +type ContactService interface { + Create(*ContactCreateRequest) (int, error) + Update(*ContactUpdateRequest) error + Delete(int) error + Info(int) (*ContactInfoResponse, error) + List(string) (*ContactListResponse, error) +} + +type ContactServiceOp struct { + client *Client +} + +var _ ContactService = &ContactServiceOp{} + +type ContactCreateRequest struct { + Type string `structs:"type"` + Name string `structs:"name"` + Org string `structs:"org,omitempty"` + Street string `structs:"street"` + City string `structs:"city"` + PostalCode string `structs:"pc"` + StateProvince string `structs:"sp,omitempty"` + CountryCode string `structs:"cc"` + Voice string `structs:"voice"` + Fax string `structs:"fax,omitempty"` + Email string `structs:"email"` + Remarks string `structs:"remarks,omitempty"` + Protection bool `structs:"protection,omitempty"` + Testing bool `structs:"testing,omitempty"` +} + +type ContactUpdateRequest struct { + Id int `structs:"id"` + Name string `structs:"name,omitempty"` + Org string `structs:"org,omitempty"` + Street string `structs:"street,omitempty"` + City string `structs:"city,omitempty"` + PostalCode string `structs:"pc,omitempty"` + StateProvince string `structs:"sp,omitempty"` + CountryCode string `structs:"cc,omitempty"` + Voice string `structs:"voice,omitempty"` + Fax string `structs:"fax,omitempty"` + Email string `structs:"email,omitempty"` + Remarks string `structs:"remarks,omitempty"` + Protection bool `structs:"protection,omitempty"` + Testing bool `structs:"testing,omitempty"` +} + +type ContactInfoResponse struct { + Contact Contact `mapstructure:"contact"` +} + +type ContactListResponse struct { + Count int + Contacts []Contact `mapstructure:"contact"` +} + +func (s *ContactServiceOp) Create(request *ContactCreateRequest) (int, error) { + req := s.client.NewRequest(methodContactCreate, structs.Map(request)) + + resp, err := s.client.Do(*req) + if err != nil { + return 0, err + } + + var result map[string]int + err = mapstructure.Decode(*resp, &result) + if err != nil { + return 0, err + } + + return result["id"], nil +} + +func (s *ContactServiceOp) Delete(roId int) error { + req := s.client.NewRequest(methodContactDelete, map[string]interface{}{ + "id": roId, + }) + + _, err := s.client.Do(*req) + return err +} + +func (s *ContactServiceOp) Update(request *ContactUpdateRequest) error { + req := s.client.NewRequest(methodContactUpdate, structs.Map(request)) + + _, err := s.client.Do(*req) + return err +} + +func (s *ContactServiceOp) Info(contactId int) (*ContactInfoResponse, error) { + var requestMap = make(map[string]interface{}) + requestMap["wide"] = 1 + + if contactId != 0 { + requestMap["id"] = contactId + } + + req := s.client.NewRequest(methodContactInfo, requestMap) + + resp, err := s.client.Do(*req) + if err != nil { + return nil, err + } + + var result ContactInfoResponse + err = mapstructure.Decode(*resp, &result) + if err != nil { + return nil, err + } + + return &result, nil +} + +func (s *ContactServiceOp) List(search string) (*ContactListResponse, error) { + var requestMap = make(map[string]interface{}) + + if search != "" { + requestMap["search"] = search + } + req := s.client.NewRequest(methodContactList, requestMap) + + resp, err := s.client.Do(*req) + if err != nil { + return nil, err + } + + var result ContactListResponse + err = mapstructure.Decode(*resp, &result) + if err != nil { + return nil, err + } + + return &result, nil +} diff --git a/vendor/github.com/smueller18/goinwx/domain.go b/vendor/github.com/smueller18/goinwx/domain.go new file mode 100644 index 000000000..ddbc86a20 --- /dev/null +++ b/vendor/github.com/smueller18/goinwx/domain.go @@ -0,0 +1,303 @@ +package goinwx + +import ( + "errors" + "fmt" + "time" + + "github.com/fatih/structs" + "github.com/mitchellh/mapstructure" +) + +const ( + methodDomainCheck = "domain.check" + methodDomainCreate = "domain.create" + methodDomainDelete = "domain.delete" + methodDomainGetPrices = "domain.getPrices" + methodDomainGetRules = "domain.getRules" + methodDomainInfo = "domain.info" + methodDomainList = "domain.list" + methodDomainLog = "domain.log" + methodDomainPush = "domain.push" + methodDomainRenew = "domain.renew" + methodDomainRestore = "domain.restore" + methodDomainStats = "domain.stats" + methodDomainTrade = "domain.trade" + methodDomainTransfer = "domain.transfer" + methodDomainTransferOut = "domain.transferOut" + methodDomainUpdate = "domain.update" + methodDomainWhois = "domain.whois" +) + +type DomainService interface { + Check(domains []string) ([]DomainCheckResponse, error) + Register(request *DomainRegisterRequest) (*DomainRegisterResponse, error) + Delete(domain string, scheduledDate time.Time) error + Info(domain string, roId int) (*DomainInfoResponse, error) + GetPrices(tlds []string) ([]DomainPriceResponse, error) + List(*DomainListRequest) (*DomainList, error) + Whois(domain string) (string, error) +} + +type DomainServiceOp struct { + client *Client +} + +var _ DomainService = &DomainServiceOp{} + +type domainCheckResponseRoot struct { + Domains []DomainCheckResponse `mapstructure:"domain"` +} +type DomainCheckResponse struct { + Available int `mapstructure:"avail"` + Status string `mapstructure:"status"` + Name string `mapstructure:"name"` + Domain string `mapstructure:"domain"` + TLD string `mapstructure:"tld"` + CheckMethod string `mapstructure:"checkmethod"` + Price float32 `mapstructure:"price"` + CheckTime float32 `mapstructure:"checktime"` +} + +type domainPriceResponseRoot struct { + Prices []DomainPriceResponse `mapstructure:"price"` +} +type DomainPriceResponse struct { + Tld string `mapstructure:"tld"` + Currency string `mapstructure:"currency"` + CreatePrice float32 `mapstructure:"createPrice"` + MonthlyCreatePrice float32 `mapstructure:"monthlyCreatePrice"` + TransferPrice float32 `mapstructure:"transferPrice"` + RenewalPrice float32 `mapstructure:"renewalPrice"` + MonthlyRenewalPrice float32 `mapstructure:"monthlyRenewalPrice"` + UpdatePrice float32 `mapstructure:"updatePrice"` + TradePrice float32 `mapstructure:"tradePrice"` + TrusteePrice float32 `mapstructure:"trusteePrice"` + MonthlyTrusteePrice float32 `mapstructure:"monthlyTrusteePrice"` + CreatePeriod int `mapstructure:"createPeriod"` + TransferPeriod int `mapstructure:"transferPeriod"` + RenewalPeriod int `mapstructure:"renewalPeriod"` + TradePeriod int `mapstructure:"tradePeriod"` +} + +type DomainRegisterRequest struct { + Domain string `structs:"domain"` + Period string `structs:"period,omitempty"` + Registrant int `structs:"registrant"` + Admin int `structs:"admin"` + Tech int `structs:"tech"` + Billing int `structs:"billing"` + Nameservers []string `structs:"ns,omitempty"` + TransferLock string `structs:"transferLock,omitempty"` + RenewalMode string `structs:"renewalMode,omitempty"` + WhoisProvider string `structs:"whoisProvider,omitempty"` + WhoisUrl string `structs:"whoisUrl,omitempty"` + ScDate string `structs:"scDate,omitempty"` + ExtDate string `structs:"extDate,omitempty"` + Asynchron string `structs:"asynchron,omitempty"` + Voucher string `structs:"voucher,omitempty"` + Testing string `structs:"testing,omitempty"` +} + +type DomainRegisterResponse struct { + RoId int + Price float32 + Currency string +} + +type DomainInfoResponse struct { + RoId int `mapstructure:"roId"` + Domain string `mapstructure:"domain"` + DomainAce string `mapstructure:"domainAce"` + Period string `mapstructure:"period"` + CrDate time.Time `mapstructure:"crDate"` + ExDate time.Time `mapstructure:"exDate"` + UpDate time.Time `mapstructure:"upDate"` + ReDate time.Time `mapstructure:"reDate"` + ScDate time.Time `mapstructure:"scDate"` + TransferLock int `mapstructure:"transferLock"` + Status string `mapstructure:"status"` + AuthCode string `mapstructure:"authCode"` + RenewalMode string `mapstructure:"renewalMode"` + TransferMode string `mapstructure:"transferMode"` + Registrant int `mapstructure:"registrant"` + Admin int `mapstructure:"admin"` + Tech int `mapstructure:"tech"` + Billing int `mapstructure:"billing"` + Nameservers []string `mapstructure:"ns"` + NoDelegation string `mapstructure:"noDelegation"` + Contacts map[string]Contact `mapstructure:"contact"` +} + +type Contact struct { + RoId int + Id string + Type string + Name string + Org string + Street string + City string + PostalCode string `mapstructure:"pc"` + StateProvince string `mapstructure:"sp"` + Country string `mapstructure:"cc"` + Phone string `mapstructure:"voice"` + Fax string + Email string + Remarks string + Protection string +} + +type DomainListRequest struct { + Domain string `structs:"domain,omitempty"` + RoId int `structs:"roId,omitempty"` + Status int `structs:"status,omitempty"` + Registrant int `structs:"registrant,omitempty"` + Admin int `structs:"admin,omitempty"` + Tech int `structs:"tech,omitempty"` + Billing int `structs:"billing,omitempty"` + RenewalMode int `structs:"renewalMode,omitempty"` + TransferLock int `structs:"transferLock,omitempty"` + NoDelegation int `structs:"noDelegation,omitempty"` + Tag int `structs:"tag,omitempty"` + Order int `structs:"order,omitempty"` + Page int `structs:"page,omitempty"` + Pagelimit int `structs:"pagelimit,omitempty"` +} + +type DomainList struct { + Count int + Domains []DomainInfoResponse `mapstructure:"domain"` +} + +func (s *DomainServiceOp) Check(domains []string) ([]DomainCheckResponse, error) { + req := s.client.NewRequest(methodDomainCheck, map[string]interface{}{ + "domain": domains, + "wide": "2", + }) + + resp, err := s.client.Do(*req) + if err != nil { + return nil, err + } + + root := new(domainCheckResponseRoot) + err = mapstructure.Decode(*resp, &root) + if err != nil { + return nil, err + } + + return root.Domains, nil +} + +func (s *DomainServiceOp) GetPrices(tlds []string) ([]DomainPriceResponse, error) { + req := s.client.NewRequest(methodDomainGetPrices, map[string]interface{}{ + "tld": tlds, + "vat": false, + }) + + resp, err := s.client.Do(*req) + if err != nil { + return nil, err + } + + root := new(domainPriceResponseRoot) + err = mapstructure.Decode(*resp, &root) + if err != nil { + return nil, err + } + + return root.Prices, nil +} + +func (s *DomainServiceOp) Register(request *DomainRegisterRequest) (*DomainRegisterResponse, error) { + req := s.client.NewRequest(methodDomainCreate, structs.Map(request)) + + resp, err := s.client.Do(*req) + if err != nil { + return nil, err + } + + var result DomainRegisterResponse + err = mapstructure.Decode(*resp, &result) + if err != nil { + return nil, err + } + + return &result, nil +} + +func (s *DomainServiceOp) Delete(domain string, scheduledDate time.Time) error { + req := s.client.NewRequest(methodDomainDelete, map[string]interface{}{ + "domain": domain, + "scDate": scheduledDate.Format(time.RFC3339), + }) + + _, err := s.client.Do(*req) + return err +} + +func (s *DomainServiceOp) Info(domain string, roId int) (*DomainInfoResponse, error) { + req := s.client.NewRequest(methodDomainInfo, map[string]interface{}{ + "domain": domain, + "wide": "2", + }) + if roId != 0 { + req.Args["roId"] = roId + } + + resp, err := s.client.Do(*req) + if err != nil { + return nil, err + } + + var result DomainInfoResponse + err = mapstructure.Decode(*resp, &result) + if err != nil { + return nil, err + } + fmt.Println("Response", result) + + return &result, nil +} + +func (s *DomainServiceOp) List(request *DomainListRequest) (*DomainList, error) { + if request == nil { + return nil, errors.New("Request can't be nil") + } + requestMap := structs.Map(request) + requestMap["wide"] = "2" + + req := s.client.NewRequest(methodDomainList, requestMap) + + resp, err := s.client.Do(*req) + if err != nil { + return nil, err + } + + var result DomainList + err = mapstructure.Decode(*resp, &result) + if err != nil { + return nil, err + } + + return &result, nil +} + +func (s *DomainServiceOp) Whois(domain string) (string, error) { + req := s.client.NewRequest(methodDomainWhois, map[string]interface{}{ + "domain": domain, + }) + + resp, err := s.client.Do(*req) + if err != nil { + return "", err + } + + var result map[string]string + err = mapstructure.Decode(*resp, &result) + if err != nil { + return "", err + } + + return result["whois"], nil +} diff --git a/vendor/github.com/smueller18/goinwx/goinwx.go b/vendor/github.com/smueller18/goinwx/goinwx.go new file mode 100644 index 000000000..b4d67d621 --- /dev/null +++ b/vendor/github.com/smueller18/goinwx/goinwx.go @@ -0,0 +1,133 @@ +package goinwx + +import ( + "fmt" + "net/url" + + "github.com/kolo/xmlrpc" +) + +const ( + libraryVersion = "0.4.0" + APIBaseUrl = "https://api.domrobot.com/xmlrpc/" + APISandboxBaseUrl = "https://api.ote.domrobot.com/xmlrpc/" + APILanguage = "eng" +) + +// Client manages communication with INWX API. +type Client struct { + // HTTP client used to communicate with the INWX API. + RPCClient *xmlrpc.Client + + // Base URL for API requests. + BaseURL *url.URL + + // API username + Username string + + // API password + Password string + + // User agent for client + APILanguage string + + // Services used for communicating with the API + Account AccountService + Domains DomainService + Nameservers NameserverService + Contacts ContactService +} + +type ClientOptions struct { + Sandbox bool +} + +type Request struct { + ServiceMethod string + Args map[string]interface{} +} + +// Response is a INWX API response. This wraps the standard http.Response returned from INWX. +type Response struct { + Code int `xmlrpc:"code"` + Message string `xmlrpc:"msg"` + ReasonCode string `xmlrpc:"reasonCode"` + Reason string `xmlrpc:"reason"` + ResponseData map[string]interface{} `xmlrpc:"resData"` +} + +// An ErrorResponse reports the error caused by an API request +type ErrorResponse struct { + Code int `xmlrpc:"code"` + Message string `xmlrpc:"msg"` + ReasonCode string `xmlrpc:"reasonCode"` + Reason string `xmlrpc:"reason"` +} + +// NewClient returns a new INWX API client. +func NewClient(username, password string, opts *ClientOptions) *Client { + var useSandbox bool + if opts != nil { + useSandbox = opts.Sandbox + } + + var baseURL *url.URL + + if useSandbox { + baseURL, _ = url.Parse(APISandboxBaseUrl) + } else { + baseURL, _ = url.Parse(APIBaseUrl) + } + + rpcClient, _ := xmlrpc.NewClient(baseURL.String(), nil) + + client := &Client{RPCClient: rpcClient, + BaseURL: baseURL, + Username: username, + Password: password, + } + + client.Account = &AccountServiceOp{client: client} + client.Domains = &DomainServiceOp{client: client} + client.Nameservers = &NameserverServiceOp{client: client} + client.Contacts = &ContactServiceOp{client: client} + + return client +} + +// NewRequest creates an API request. +func (c *Client) NewRequest(serviceMethod string, args map[string]interface{}) *Request { + if args != nil { + args["lang"] = APILanguage + } + + return &Request{ServiceMethod: serviceMethod, Args: args} +} + +// Do sends an API request and returns the API response. +func (c *Client) Do(req Request) (*map[string]interface{}, error) { + var resp Response + err := c.RPCClient.Call(req.ServiceMethod, req.Args, &resp) + if err != nil { + return nil, err + } + + return &resp.ResponseData, CheckResponse(&resp) +} + +func (r *ErrorResponse) Error() string { + if r.Reason != "" { + return fmt.Sprintf("(%d) %s. Reason: (%s) %s", + r.Code, r.Message, r.ReasonCode, r.Reason) + } + return fmt.Sprintf("(%d) %s", r.Code, r.Message) +} + +// CheckResponse checks the API response for errors, and returns them if present. +func CheckResponse(r *Response) error { + if c := r.Code; c >= 1000 && c <= 1500 { + return nil + } + + return &ErrorResponse{Code: r.Code, Message: r.Message, Reason: r.Reason, ReasonCode: r.ReasonCode} +} diff --git a/vendor/github.com/smueller18/goinwx/nameserver.go b/vendor/github.com/smueller18/goinwx/nameserver.go new file mode 100644 index 000000000..38269b461 --- /dev/null +++ b/vendor/github.com/smueller18/goinwx/nameserver.go @@ -0,0 +1,275 @@ +package goinwx + +import ( + "errors" + "fmt" + "time" + + "github.com/fatih/structs" + "github.com/mitchellh/mapstructure" +) + +const ( + methodNameserverCheck = "nameserver.check" + methodNameserverCreate = "nameserver.create" + methodNameserverCreateRecord = "nameserver.createRecord" + methodNameserverDelete = "nameserver.delete" + methodNameserverDeleteRecord = "nameserver.deleteRecord" + methodNameserverInfo = "nameserver.info" + methodNameserverList = "nameserver.list" + methodNameserverUpdate = "nameserver.update" + methodNameserverUpdateRecord = "nameserver.updateRecord" +) + +type NameserverService interface { + Check(domain string, nameservers []string) (*NameserverCheckResponse, error) + Create(*NameserverCreateRequest) (int, error) + Info(*NameserverInfoRequest) (*NamserverInfoResponse, error) + List(domain string) (*NamserverListResponse, error) + CreateRecord(*NameserverRecordRequest) (int, error) + UpdateRecord(recId int, request *NameserverRecordRequest) error + DeleteRecord(recId int) error + FindRecordById(recId int) (*NameserverRecord, *NameserverDomain, error) +} + +type NameserverServiceOp struct { + client *Client +} + +var _ NameserverService = &NameserverServiceOp{} + +type NameserverCheckResponse struct { + Details []string + Status string +} + +type NameserverRecordRequest struct { + RoId int `structs:"roId,omitempty"` + Domain string `structs:"domain,omitempty"` + Type string `structs:"type"` + Content string `structs:"content"` + Name string `structs:"name,omitempty"` + Ttl int `structs:"ttl,omitempty"` + Priority int `structs:"prio,omitempty"` + UrlRedirectType string `structs:"urlRedirectType,omitempty"` + UrlRedirectTitle string `structs:"urlRedirectTitle,omitempty"` + UrlRedirectDescription string `structs:"urlRedirectDescription,omitempty"` + UrlRedirectFavIcon string `structs:"urlRedirectFavIcon,omitempty"` + UrlRedirectKeywords string `structs:"urlRedirectKeywords,omitempty"` +} + +type NameserverCreateRequest struct { + Domain string `structs:"domain"` + Type string `structs:"type"` + Nameservers []string `structs:"ns,omitempty"` + MasterIp string `structs:"masterIp,omitempty"` + Web string `structs:"web,omitempty"` + Mail string `structs:"mail,omitempty"` + SoaEmail string `structs:"soaEmail,omitempty"` + UrlRedirectType string `structs:"urlRedirectType,omitempty"` + UrlRedirectTitle string `structs:"urlRedirectTitle,omitempty"` + UrlRedirectDescription string `structs:"urlRedirectDescription,omitempty"` + UrlRedirectFavIcon string `structs:"urlRedirectFavIcon,omitempty"` + UrlRedirectKeywords string `structs:"urlRedirectKeywords,omitempty"` + Testing bool `structs:"testing,omitempty"` +} + +type NameserverInfoRequest struct { + Domain string `structs:"domain,omitempty"` + RoId int `structs:"roId,omitempty"` + RecordId int `structs:"recordId,omitempty"` + Type string `structs:"type,omitempty"` + Name string `structs:"name,omitempty"` + Content string `structs:"content,omitempty"` + Ttl int `structs:"ttl,omitempty"` + Prio int `structs:"prio,omitempty"` +} + +type NamserverInfoResponse struct { + RoId int + Domain string + Type string + MasterIp string + LastZoneCheck time.Time + SlaveDns interface{} + SOAserial string + Count int + Records []NameserverRecord `mapstructure:"record"` +} + +type NameserverRecord struct { + Id int + Name string + Type string + Content string + Ttl int + Prio int + UrlRedirectType string + UrlRedirectTitle string + UrlRedirectDescription string + UrlRedirectKeywords string + UrlRedirectFavIcon string +} + +type NamserverListResponse struct { + Count int + Domains []NameserverDomain `mapstructure:"domains"` +} + +type NameserverDomain struct { + RoId int `mapstructure:"roId"` + Domain string `mapstructure:"domain"` + Type string `mapstructure:"type"` + MasterIp string `mapstructure:"masterIp"` + Mail string `mapstructure:"mail"` + Web string `mapstructure:"web"` + Url string `mapstructure:"url"` + Ipv4 string `mapstructure:"ipv4"` + Ipv6 string `mapstructure:"ipv6"` +} + +func (s *NameserverServiceOp) Check(domain string, nameservers []string) (*NameserverCheckResponse, error) { + req := s.client.NewRequest(methodNameserverCheck, map[string]interface{}{ + "domain": domain, + "ns": nameservers, + }) + + resp, err := s.client.Do(*req) + if err != nil { + return nil, err + } + + var result NameserverCheckResponse + err = mapstructure.Decode(*resp, &result) + if err != nil { + return nil, err + } + + return &result, nil +} + +func (s *NameserverServiceOp) Info(request *NameserverInfoRequest) (*NamserverInfoResponse, error) { + req := s.client.NewRequest(methodNameserverInfo, structs.Map(request)) + + resp, err := s.client.Do(*req) + if err != nil { + return nil, err + } + var result NamserverInfoResponse + err = mapstructure.Decode(*resp, &result) + if err != nil { + return nil, err + } + + return &result, nil +} + +func (s *NameserverServiceOp) List(domain string) (*NamserverListResponse, error) { + requestMap := map[string]interface{}{ + "domain": "*", + "wide": 2, + } + if domain != "" { + requestMap["domain"] = domain + } + req := s.client.NewRequest(methodNameserverList, requestMap) + + resp, err := s.client.Do(*req) + if err != nil { + return nil, err + } + var result NamserverListResponse + err = mapstructure.Decode(*resp, &result) + if err != nil { + return nil, err + } + + return &result, nil +} + +func (s *NameserverServiceOp) Create(request *NameserverCreateRequest) (int, error) { + req := s.client.NewRequest(methodNameserverCreate, structs.Map(request)) + + resp, err := s.client.Do(*req) + if err != nil { + return 0, err + } + + var result map[string]int + err = mapstructure.Decode(*resp, &result) + if err != nil { + return 0, err + } + + return result["roId"], nil +} + +func (s *NameserverServiceOp) CreateRecord(request *NameserverRecordRequest) (int, error) { + req := s.client.NewRequest(methodNameserverCreateRecord, structs.Map(request)) + + resp, err := s.client.Do(*req) + if err != nil { + return 0, err + } + + var result map[string]int + err = mapstructure.Decode(*resp, &result) + if err != nil { + return 0, err + } + + return result["id"], nil +} + +func (s *NameserverServiceOp) UpdateRecord(recId int, request *NameserverRecordRequest) error { + if request == nil { + return errors.New("Request can't be nil") + } + requestMap := structs.Map(request) + requestMap["id"] = recId + + req := s.client.NewRequest(methodNameserverUpdateRecord, requestMap) + + _, err := s.client.Do(*req) + if err != nil { + return err + } + + return nil +} + +func (s *NameserverServiceOp) DeleteRecord(recId int) error { + req := s.client.NewRequest(methodNameserverDeleteRecord, map[string]interface{}{ + "id": recId, + }) + + _, err := s.client.Do(*req) + if err != nil { + return err + } + + return nil +} + +func (s *NameserverServiceOp) FindRecordById(recId int) (*NameserverRecord, *NameserverDomain, error) { + listResp, err := s.client.Nameservers.List("") + if err != nil { + return nil, nil, err + } + + for _, domainItem := range listResp.Domains { + resp, err := s.client.Nameservers.Info(&NameserverInfoRequest{RoId: domainItem.RoId}) + if err != nil { + return nil, nil, err + } + + for _, record := range resp.Records { + if record.Id == recId { + return &record, &domainItem, nil + } + } + } + + return nil, nil, fmt.Errorf("couldn't find INWX Record for id %d", recId) + +} diff --git a/vendor/github.com/transip/gotransip/LICENSE b/vendor/github.com/transip/gotransip/LICENSE new file mode 100644 index 000000000..352d193e3 --- /dev/null +++ b/vendor/github.com/transip/gotransip/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 TransIP B.V. + +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. diff --git a/vendor/github.com/transip/gotransip/api.go b/vendor/github.com/transip/gotransip/api.go new file mode 100644 index 000000000..46c684480 --- /dev/null +++ b/vendor/github.com/transip/gotransip/api.go @@ -0,0 +1,12 @@ +package gotransip + +// CancellationTime represents the possible ways of canceling a contract +type CancellationTime string + +var ( + // CancellationTimeEnd specifies to cancel the contract when the contract was + // due to end anyway + CancellationTimeEnd CancellationTime = "end" + // CancellationTimeImmediately specifies to cancel the contract immediately + CancellationTimeImmediately CancellationTime = "immediately" +) diff --git a/vendor/github.com/transip/gotransip/client.go b/vendor/github.com/transip/gotransip/client.go new file mode 100644 index 000000000..0be9c400a --- /dev/null +++ b/vendor/github.com/transip/gotransip/client.go @@ -0,0 +1,119 @@ +package gotransip + +import ( + "errors" + "fmt" + "io/ioutil" + "os" +) + +const ( + transipAPIHost = "api.transip.nl" + transipAPINamespace = "http://www.transip.nl/soap" +) + +// APIMode specifies in which mode the API is used. Currently this is only +// supports either readonly or readwrite +type APIMode string + +var ( + // APIModeReadOnly specifies that no changes can be made from API calls + APIModeReadOnly APIMode = "readonly" + // APIModeReadWrite specifies that changes can be made from API calls + APIModeReadWrite APIMode = "readwrite" +) + +// ClientConfig is a tool to easily create a new Client object +type ClientConfig struct { + AccountName string + PrivateKeyPath string + PrivateKeyBody []byte + Mode APIMode +} + +// Client is the interface which all clients should implement +type Client interface { + Call(SoapRequest, interface{}) error // execute request on client +} + +// SOAPClient represents a TransIP API SOAP client, implementing the Client +// interface +type SOAPClient struct { + soapClient soapClient +} + +// Call performs given SOAP request and fills the response into result +func (c SOAPClient) Call(req SoapRequest, result interface{}) error { + return c.soapClient.call(req, result) +} + +// NewSOAPClient returns a new SOAPClient object for given config +// ClientConfig's PrivateKeyPath will override potentially given PrivateKeyBody +func NewSOAPClient(c ClientConfig) (SOAPClient, error) { + // check account name + if len(c.AccountName) == 0 { + return SOAPClient{}, errors.New("AccountName is required") + } + + // check if private key was given in any form + if len(c.PrivateKeyPath) == 0 && len(c.PrivateKeyBody) == 0 { + return SOAPClient{}, errors.New("PrivateKeyPath or PrivateKeyBody is required") + } + + // if PrivateKeyPath was set, this will override any given PrivateKeyBody + if len(c.PrivateKeyPath) > 0 { + // try to open private key and read contents + if _, err := os.Stat(c.PrivateKeyPath); err != nil { + return SOAPClient{}, fmt.Errorf("could not open private key: %s", err.Error()) + } + + // read private key so we can pass the body to the soapClient + var err error + c.PrivateKeyBody, err = ioutil.ReadFile(c.PrivateKeyPath) + if err != nil { + return SOAPClient{}, err + } + } + + // default to APIMode read/write + if len(c.Mode) == 0 { + c.Mode = APIModeReadWrite + } + + // create soapClient and pass it to a new Client pointer + sc := soapClient{ + Login: c.AccountName, + Mode: c.Mode, + PrivateKey: c.PrivateKeyBody, + } + + return SOAPClient{ + soapClient: sc, + }, nil +} + +// FakeSOAPClient is a client doing nothing except implementing the gotransip.Client +// interface +// you can however set a fixture XML body which Call will try to Unmarshal into +// result +// useful for testing +type FakeSOAPClient struct { + fixture []byte // preset this fixture so Call can use it to Unmarshal +} + +// FixtureFromFile reads file and sets content as FakeSOAPClient's fixture +func (f *FakeSOAPClient) FixtureFromFile(file string) (err error) { + // read fixture file + f.fixture, err = ioutil.ReadFile(file) + if err != nil { + err = fmt.Errorf("could not read fixture from file %s: %s", file, err.Error()) + } + + return +} + +// Call doesn't do anything except fill the XML unmarshalled result +func (f FakeSOAPClient) Call(req SoapRequest, result interface{}) error { + // this fake client just parses given fixture as if it was a SOAP response + return parseSoapResponse(f.fixture, req.Padding, 200, result) +} diff --git a/vendor/github.com/transip/gotransip/domain/api.go b/vendor/github.com/transip/gotransip/domain/api.go new file mode 100644 index 000000000..f5f90f8e2 --- /dev/null +++ b/vendor/github.com/transip/gotransip/domain/api.go @@ -0,0 +1,314 @@ +package domain + +import ( + "github.com/transip/gotransip" +) + +// This file holds all DomainService methods directly ported from TransIP API + +// BatchCheckAvailability checks the availability of multiple domains +func BatchCheckAvailability(c gotransip.Client, domainNames []string) ([]CheckResult, error) { + sr := gotransip.SoapRequest{ + Service: serviceName, + Method: "batchCheckAvailability", + } + sr.AddArgument("domainNames", domainNames) + + var v struct { + V []CheckResult `xml:"item"` + } + + err := c.Call(sr, &v) + return v.V, err +} + +// CheckAvailability returns the availability status of a domain. +func CheckAvailability(c gotransip.Client, domainName string) (Status, error) { + sr := gotransip.SoapRequest{ + Service: serviceName, + Method: "checkAvailability", + } + sr.AddArgument("domainName", domainName) + + var v Status + err := c.Call(sr, &v) + return v, err +} + +// GetWhois returns the whois of a domain name +func GetWhois(c gotransip.Client, domainName string) (string, error) { + sr := gotransip.SoapRequest{ + Service: serviceName, + Method: "getWhois", + } + sr.AddArgument("domainName", domainName) + + var v string + err := c.Call(sr, &v) + return v, err +} + +// GetDomainNames returns list with domain names or error when this failed +func GetDomainNames(c gotransip.Client) ([]string, error) { + var d = struct { + D []string `xml:"item"` + }{} + err := c.Call(gotransip.SoapRequest{ + Service: serviceName, + Method: "getDomainNames", + }, &d) + + return d.D, err +} + +// GetInfo returns Domain for given name or error when this failed +func GetInfo(c gotransip.Client, domainName string) (Domain, error) { + sr := gotransip.SoapRequest{ + Service: serviceName, + Method: "getInfo", + } + sr.AddArgument("domainName", domainName) + + var d Domain + err := c.Call(sr, &d) + + return d, err +} + +// BatchGetInfo returns array of Domain for given name or error when this failed +func BatchGetInfo(c gotransip.Client, domainNames []string) ([]Domain, error) { + sr := gotransip.SoapRequest{ + Service: serviceName, + Method: "batchGetInfo", + } + sr.AddArgument("domainNames", domainNames) + + var d = struct { + D []Domain `xml:"item"` + }{} + err := c.Call(sr, &d) + + return d.D, err +} + +// GetAuthCode returns the Auth code for a domainName +func GetAuthCode(c gotransip.Client, domainName string) (string, error) { + sr := gotransip.SoapRequest{ + Service: serviceName, + Method: "getAuthCode", + } + sr.AddArgument("domainName", domainName) + + var v string + err := c.Call(sr, &v) + return v, err +} + +// GetIsLocked returns the lock status for a domainName +func GetIsLocked(c gotransip.Client, domainName string) (bool, error) { + sr := gotransip.SoapRequest{ + Service: serviceName, + Method: "getIsLocked", + } + sr.AddArgument("domainName", domainName) + + var v bool + err := c.Call(sr, &v) + return v, err +} + +// Register registers a domain name and will automatically create and sign a proposition for it +func Register(c gotransip.Client, domain string) error { + sr := gotransip.SoapRequest{ + Service: serviceName, + Method: "register", + } + sr.AddArgument("domain", domain) + + return c.Call(sr, nil) +} + +// Cancel cancels a domain name, will automatically create and sign a cancellation document +func Cancel(c gotransip.Client, domainName string, endTime gotransip.CancellationTime) error { + sr := gotransip.SoapRequest{ + Service: serviceName, + Method: "cancel", + } + sr.AddArgument("domainName", domainName) + sr.AddArgument("endTime", string(endTime)) + + return c.Call(sr, nil) +} + +// TransferWithOwnerChange transfers a domain with changing the owner +func TransferWithOwnerChange(c gotransip.Client, domain, authCode string) error { + sr := gotransip.SoapRequest{ + Service: serviceName, + Method: "transferWithOwnerChange", + } + sr.AddArgument("domain", domain) + sr.AddArgument("authCode", authCode) + + return c.Call(sr, nil) +} + +// TransferWithoutOwnerChange transfers a domain without changing the owner +func TransferWithoutOwnerChange(c gotransip.Client, domain, authCode string) error { + sr := gotransip.SoapRequest{ + Service: serviceName, + Method: "transferWithoutOwnerChange", + } + sr.AddArgument("domain", domain) + sr.AddArgument("authCode", authCode) + + return c.Call(sr, nil) +} + +// SetNameservers starts a nameserver change for this domain, will replace all +// existing nameservers with the new nameservers +func SetNameservers(c gotransip.Client, domainName string, nameservers Nameservers) error { + sr := gotransip.SoapRequest{ + Service: serviceName, + Method: "setNameservers", + } + sr.AddArgument("domainName", domainName) + sr.AddArgument("nameservers", nameservers) + + return c.Call(sr, nil) +} + +// SetLock locks this domain +func SetLock(c gotransip.Client, domainName string) error { + sr := gotransip.SoapRequest{ + Service: serviceName, + Method: "setLock", + } + sr.AddArgument("domainName", domainName) + + return c.Call(sr, nil) +} + +// UnsetLock unlocks this domain +func UnsetLock(c gotransip.Client, domainName string) error { + sr := gotransip.SoapRequest{ + Service: serviceName, + Method: "unsetLock", + } + sr.AddArgument("domainName", domainName) + + return c.Call(sr, nil) +} + +// SetDNSEntries sets the DnsEntries for this Domain, will replace all existing +// dns entries with the new entries +func SetDNSEntries(c gotransip.Client, domainName string, dnsEntries DNSEntries) error { + sr := gotransip.SoapRequest{ + Service: serviceName, + Method: "setDnsEntries", + } + sr.AddArgument("domainName", domainName) + sr.AddArgument("dnsEntries", dnsEntries) + + return c.Call(sr, nil) +} + +// SetOwner starts an owner change of a domain +func SetOwner(c gotransip.Client, domainName, registrantWhoisContact WhoisContact) error { + sr := gotransip.SoapRequest{ + Service: serviceName, + Method: "setOwner", + } + sr.AddArgument("domainName", domainName) + // make sure contact is of type registrant + registrantWhoisContact.Type = "registrant" + sr.AddArgument("registrantWhoisContact", registrantWhoisContact) + + return c.Call(sr, nil) +} + +// SetContacts starts a contact change of a domain, this will replace all existing contacts +func SetContacts(c gotransip.Client, domainName, contacts WhoisContacts) error { + sr := gotransip.SoapRequest{ + Service: serviceName, + Method: "setContacts", + } + sr.AddArgument("domainName", domainName) + sr.AddArgument("contacts", contacts) + + return c.Call(sr, nil) +} + +// GetAllTLDInfos returns slice with TLD objects or error when this failed +func GetAllTLDInfos(c gotransip.Client) ([]TLD, error) { + var d = struct { + TLD []TLD `xml:"item"` + }{} + err := c.Call(gotransip.SoapRequest{ + Service: serviceName, + Method: "getAllTldInfos", + }, &d) + + return d.TLD, err +} + +// GetTldInfo returns info about a specific TLD +func GetTldInfo(c gotransip.Client, tldName string) (TLD, error) { + sr := gotransip.SoapRequest{ + Service: serviceName, + Method: "getTldInfo", + } + sr.AddArgument("tldName", tldName) + + var v TLD + err := c.Call(sr, &v) + return v, err +} + +// GetCurrentDomainAction returns info about the action this domain is currently running +func GetCurrentDomainAction(c gotransip.Client, domainName string) (ActionResult, error) { + sr := gotransip.SoapRequest{ + Service: serviceName, + Method: "getCurrentDomainAction", + } + sr.AddArgument("domainName", domainName) + + var v ActionResult + err := c.Call(sr, &v) + return v, err +} + +// RetryCurrentDomainActionWithNewData retries a failed domain action with new +// domain data. The Domain.Name field must contain the name of the Domain. The +// Nameservers, Contacts, DNSEntries fields contain the new data for this domain. +func RetryCurrentDomainActionWithNewData(c gotransip.Client, domain Domain) error { + sr := gotransip.SoapRequest{ + Service: serviceName, + Method: "retryCurrentDomainActionWithNewData", + } + sr.AddArgument("domain", domain) + + return c.Call(sr, nil) +} + +// RetryTransferWithDifferentAuthCode retries a transfer action with a new authcode +func RetryTransferWithDifferentAuthCode(c gotransip.Client, domain Domain, newAuthCode string) error { + sr := gotransip.SoapRequest{ + Service: serviceName, + Method: "retryTransferWithDifferentAuthCode", + } + sr.AddArgument("domain", domain) + sr.AddArgument("newAuthCode", newAuthCode) + + return c.Call(sr, nil) +} + +// CancelDomainAction cancels a failed domain action +func CancelDomainAction(c gotransip.Client, domain string) error { + sr := gotransip.SoapRequest{ + Service: serviceName, + Method: "cancelDomainAction", + } + sr.AddArgument("domain", domain) + + return c.Call(sr, nil) +} diff --git a/vendor/github.com/transip/gotransip/domain/domain.go b/vendor/github.com/transip/gotransip/domain/domain.go new file mode 100644 index 000000000..56d7b719a --- /dev/null +++ b/vendor/github.com/transip/gotransip/domain/domain.go @@ -0,0 +1,405 @@ +package domain + +import ( + "fmt" + "net" + + "github.com/transip/gotransip" + "github.com/transip/gotransip/util" +) + +const ( + serviceName string = "DomainService" +) + +// Domain represents a Transip_Domain object +// as described at https://api.transip.nl/docs/transip.nl/class-Transip_Domain.html +type Domain struct { + Name string `xml:"name"` + Nameservers []Nameserver `xml:"nameservers>item"` + Contacts []WhoisContact `xml:"contacts>item"` + DNSEntries []DNSEntry `xml:"dnsEntries>item"` + Branding Branding `xml:"branding"` + AuthorizationCode string `xml:"authCode"` + IsLocked bool `xml:"isLocked"` + RegistrationDate util.XMLTime `xml:"registrationDate"` + RenewalDate util.XMLTime `xml:"renewalDate"` +} + +// EncodeParams returns Domain parameters ready to be used for constructing a signature +func (d Domain) EncodeParams(prm gotransip.ParamsContainer) { + idx := prm.Len() + prm.Add(fmt.Sprintf("%d[name]", idx), d.Name) + prm.Add(fmt.Sprintf("%d[authCode]", idx), d.AuthorizationCode) + prm.Add(fmt.Sprintf("%d[isLocked]", idx), fmt.Sprintf("%t", d.IsLocked)) + prm.Add(fmt.Sprintf("%d[registrationDate]", idx), d.RegistrationDate.Format("2006-01-02")) + prm.Add(fmt.Sprintf("%d[renewalDate]", idx), d.RenewalDate.Format("2006-01-02")) + // nameservers + for i, e := range d.Nameservers { + var ipv4, ipv6 string + if e.IPv4Address != nil { + ipv4 = e.IPv4Address.String() + } + if e.IPv6Address != nil { + ipv6 = e.IPv6Address.String() + } + prm.Add(fmt.Sprintf("%d[nameservers][%d][hostname]", idx, i), e.Hostname) + prm.Add(fmt.Sprintf("%d[nameservers][%d][ipv4]", idx, i), ipv4) + prm.Add(fmt.Sprintf("%d[nameservers][%d][ipv6]", idx, i), ipv6) + } + // contacts + for i, e := range d.Contacts { + prm.Add(fmt.Sprintf("%d[contacts][%d][type]", idx, i), e.Type) + prm.Add(fmt.Sprintf("%d[contacts][%d][firstName]", idx, i), e.FirstName) + prm.Add(fmt.Sprintf("%d[contacts][%d][middleName]", idx, i), e.MiddleName) + prm.Add(fmt.Sprintf("%d[contacts][%d][lastName]", idx, i), e.LastName) + prm.Add(fmt.Sprintf("%d[contacts][%d][companyName]", idx, i), e.CompanyName) + prm.Add(fmt.Sprintf("%d[contacts][%d][companyKvk]", idx, i), e.CompanyKvk) + prm.Add(fmt.Sprintf("%d[contacts][%d][companyType]", idx, i), e.CompanyType) + prm.Add(fmt.Sprintf("%d[contacts][%d][street]", idx, i), e.Street) + prm.Add(fmt.Sprintf("%d[contacts][%d][number]", idx, i), e.Number) + prm.Add(fmt.Sprintf("%d[contacts][%d][postalCode]", idx, i), e.PostalCode) + prm.Add(fmt.Sprintf("%d[contacts][%d][city]", idx, i), e.City) + prm.Add(fmt.Sprintf("%d[contacts][%d][phoneNumber]", idx, i), e.PhoneNumber) + prm.Add(fmt.Sprintf("%d[contacts][%d][faxNumber]", idx, i), e.FaxNumber) + prm.Add(fmt.Sprintf("%d[contacts][%d][email]", idx, i), e.Email) + prm.Add(fmt.Sprintf("%d[contacts][%d][country]", idx, i), e.Country) + } + // dnsEntries + for i, e := range d.DNSEntries { + prm.Add(fmt.Sprintf("%d[dnsEntries][%d][name]", idx, i), e.Name) + prm.Add(fmt.Sprintf("%d[dnsEntries][%d][expire]", idx, i), fmt.Sprintf("%d", e.TTL)) + prm.Add(fmt.Sprintf("%d[dnsEntries][%d][type]", idx, i), string(e.Type)) + prm.Add(fmt.Sprintf("%d[dnsEntries][%d][content]", idx, i), e.Content) + } + // branding + prm.Add(fmt.Sprintf("%d[branding][companyName]", idx), d.Branding.CompanyName) + prm.Add(fmt.Sprintf("%d[branding][supportEmail]", idx), d.Branding.SupportEmail) + prm.Add(fmt.Sprintf("%d[branding][companyUrl]", idx), d.Branding.CompanyURL) + prm.Add(fmt.Sprintf("%d[branding][termsOfUsageUrl]", idx), d.Branding.TermsOfUsageURL) + prm.Add(fmt.Sprintf("%d[branding][bannerLine1]", idx), d.Branding.BannerLine1) + prm.Add(fmt.Sprintf("%d[branding][bannerLine2]", idx), d.Branding.BannerLine2) + prm.Add(fmt.Sprintf("%d[branding][bannerLine3]", idx), d.Branding.BannerLine3) +} + +// EncodeArgs returns Domain XML body ready to be passed in the SOAP call +func (d Domain) EncodeArgs(key string) string { + output := fmt.Sprintf(`<%s xsi:type="ns1:Domain"> + %s + %s + %t + %s + %s`, + key, d.Name, d.AuthorizationCode, d.IsLocked, + d.RegistrationDate.Format("2006-01-02"), d.RenewalDate.Format("2006-01-02"), + ) + "\n" + + output += Nameservers(d.Nameservers).EncodeArgs("nameservers") + "\n" + output += WhoisContacts(d.Contacts).EncodeArgs("contacts") + "\n" + output += DNSEntries(d.DNSEntries).EncodeArgs("dnsEntries") + "\n" + output += d.Branding.EncodeArgs("branding") + "\n" + + return fmt.Sprintf("%s", output, key) +} + +// Capability represents the possible capabilities a TLD can have +type Capability string + +var ( + // CapabilityRequiresAuthCode defines this TLD requires an auth code + // to be transferred + CapabilityRequiresAuthCode Capability = "requiresAuthCode" + // CapabilityCanRegister defines this TLD can be registered + CapabilityCanRegister Capability = "canRegister" + // CapabilityCanTransferWithOwnerChange defines this TLD can be transferred + // with change of ownership + CapabilityCanTransferWithOwnerChange Capability = "canTransferWithOwnerChange" + // CapabilityCanTransferWithoutOwnerChange defines this TLD can be + // transferred without change of ownership + CapabilityCanTransferWithoutOwnerChange Capability = "canTransferWithoutOwnerChange" + // CapabilityCanSetLock defines this TLD allows to be locked + CapabilityCanSetLock Capability = "canSetLock" + // CapabilityCanSetOwner defines this TLD supports setting an owner + CapabilityCanSetOwner Capability = "canSetOwner" + // CapabilityCanSetContacts defines this TLD supports setting contacts + CapabilityCanSetContacts Capability = "canSetContacts" + // CapabilityCanSetNameservers defines this TLD supports setting nameservers + CapabilityCanSetNameservers Capability = "canSetNameservers" +) + +// TLD represents a Transip_Tld object as described at +// https://api.transip.nl/docs/transip.nl/class-Transip_Tld.html +type TLD struct { + Name string `xml:"name"` + Price float64 `xml:"price"` + RenewalPrice float64 `xml:"renewalPrice"` + Capabilities []Capability `xml:"capabilities>item"` + RegistrationPeriodLength int64 `xml:"registrationPeriodLength"` + CancelTimeFrame int64 `xml:"cancelTimeFrame"` +} + +// Nameserver represents a Transip_Nameserver object as described at +// https://api.transip.nl/docs/transip.nl/class-Transip_Nameserver.html +type Nameserver struct { + Hostname string `xml:"hostname"` + IPv4Address net.IP `xml:"ipv4"` + IPv6Address net.IP `xml:"ipv6"` +} + +// Nameservers is just an array of Nameserver +// basically only here so it can implement paramsEncoder +type Nameservers []Nameserver + +// EncodeParams returns Nameservers parameters ready to be used for constructing a signature +func (n Nameservers) EncodeParams(prm gotransip.ParamsContainer) { + idx := prm.Len() + for i, e := range n { + var ipv4, ipv6 string + if e.IPv4Address != nil { + ipv4 = e.IPv4Address.String() + } + if e.IPv6Address != nil { + ipv6 = e.IPv6Address.String() + } + prm.Add(fmt.Sprintf("%d[%d][hostname]", idx, i), e.Hostname) + prm.Add(fmt.Sprintf("%d[%d][ipv4]", idx, i), ipv4) + prm.Add(fmt.Sprintf("%d[%d][ipv6]", idx, i), ipv6) + } +} + +// EncodeArgs returns Nameservers XML body ready to be passed in the SOAP call +func (n Nameservers) EncodeArgs(key string) string { + output := fmt.Sprintf(`<%s SOAP-ENC:arrayType="ns1:Nameserver[%d]" xsi:type="ns1:ArrayOfNameserver">`, key, len(n)) + "\n" + for _, e := range n { + var ipv4, ipv6 string + if e.IPv4Address != nil { + ipv4 = e.IPv4Address.String() + } + if e.IPv6Address != nil { + ipv6 = e.IPv6Address.String() + } + output += fmt.Sprintf(` + %s + %s + %s + `, e.Hostname, ipv4, ipv6) + "\n" + } + + return fmt.Sprintf("%s", output, key) +} + +// DNSEntryType represents the possible types of DNS entries +type DNSEntryType string + +var ( + // DNSEntryTypeA represents an A-record + DNSEntryTypeA DNSEntryType = "A" + // DNSEntryTypeAAAA represents an AAAA-record + DNSEntryTypeAAAA DNSEntryType = "AAAA" + // DNSEntryTypeCNAME represents a CNAME-record + DNSEntryTypeCNAME DNSEntryType = "CNAME" + // DNSEntryTypeMX represents an MX-record + DNSEntryTypeMX DNSEntryType = "MX" + // DNSEntryTypeNS represents an NS-record + DNSEntryTypeNS DNSEntryType = "NS" + // DNSEntryTypeTXT represents a TXT-record + DNSEntryTypeTXT DNSEntryType = "TXT" + // DNSEntryTypeSRV represents an SRV-record + DNSEntryTypeSRV DNSEntryType = "SRV" +) + +// DNSEntry represents a Transip_DnsEntry object as described at +// https://api.transip.nl/docs/transip.nl/class-Transip_DnsEntry.html +type DNSEntry struct { + Name string `xml:"name"` + TTL int64 `xml:"expire"` + Type DNSEntryType `xml:"type"` + Content string `xml:"content"` +} + +// DNSEntries is just an array of DNSEntry +// basically only here so it can implement paramsEncoder +type DNSEntries []DNSEntry + +// EncodeParams returns DNSEntries parameters ready to be used for constructing a signature +func (d DNSEntries) EncodeParams(prm gotransip.ParamsContainer) { + idx := prm.Len() + for i, e := range d { + prm.Add(fmt.Sprintf("%d[%d][name]", idx, i), e.Name) + prm.Add(fmt.Sprintf("%d[%d][expire]", idx, i), fmt.Sprintf("%d", e.TTL)) + prm.Add(fmt.Sprintf("%d[%d][type]", idx, i), string(e.Type)) + prm.Add(fmt.Sprintf("%d[%d][content]", idx, i), e.Content) + } +} + +// EncodeArgs returns DNSEntries XML body ready to be passed in the SOAP call +func (d DNSEntries) EncodeArgs(key string) string { + output := fmt.Sprintf(`<%s SOAP-ENC:arrayType="ns1:DnsEntry[%d]" xsi:type="ns1:ArrayOfDnsEntry">`, key, len(d)) + "\n" + for _, e := range d { + output += fmt.Sprintf(` + %s + %d + %s + %s + `, e.Name, e.TTL, e.Type, e.Content) + "\n" + } + + return fmt.Sprintf("%s", output, key) +} + +// Status reflects the current status of a domain in a check result +type Status string + +var ( + // StatusInYourAccount means he domain name is already in your account + StatusInYourAccount Status = "inyouraccount" + // StatusUnavailable means the domain name is currently unavailable and can not be registered due to unknown reasons. + StatusUnavailable Status = "unavailable" + // StatusNotFree means the domain name has already been registered + StatusNotFree Status = "notfree" + // StatusFree means the domain name is currently free, is available and can be registered + StatusFree Status = "free" + // StatusInternalPull means the domain name is currently registered at TransIP and is available to be pulled from another account to yours. + StatusInternalPull Status = "internalpull" + // StatusInternalPush means the domain name is currently registered at TransIP in your accounta and is available to be pushed to another account. + StatusInternalPush Status = "internalpush" +) + +// CheckResult represents a Transip_DomainCheckResult object as described at +// https://api.transip.nl/docs/transip.nl/class-Transip_DomainCheckResult.html +type CheckResult struct { + DomainName string `xml:"domainName"` + Status Status `xml:"status"` + Actions []Action `xml:"actions>item"` +} + +// Branding represents a Transip_DomainBranding object as described at +// https://api.transip.nl/docs/transip.nl/class-Transip_DomainBranding.html +type Branding struct { + CompanyName string `xml:"companyName"` + SupportEmail string `xml:"supportEmail"` + CompanyURL string `xml:"companyUrl"` + TermsOfUsageURL string `xml:"termsOfUsageUrl"` + BannerLine1 string `xml:"bannerLine1"` + BannerLine2 string `xml:"bannerLine2"` + BannerLine3 string `xml:"bannerLine3"` +} + +// EncodeParams returns WhoisContacts parameters ready to be used for constructing a signature +func (b Branding) EncodeParams(prm gotransip.ParamsContainer) { + idx := prm.Len() + prm.Add(fmt.Sprintf("%d[companyName]", idx), b.CompanyName) + prm.Add(fmt.Sprintf("%d[supportEmail]", idx), b.SupportEmail) + prm.Add(fmt.Sprintf("%d[companyUrl]", idx), b.CompanyURL) + prm.Add(fmt.Sprintf("%d[termsOfUsageUrl]", idx), b.TermsOfUsageURL) + prm.Add(fmt.Sprintf("%d[bannerLine1]", idx), b.BannerLine1) + prm.Add(fmt.Sprintf("%d[bannerLine2]", idx), b.BannerLine2) + prm.Add(fmt.Sprintf("%d[bannerLine3]", idx), b.BannerLine3) +} + +// EncodeArgs returns Branding XML body ready to be passed in the SOAP call +func (b Branding) EncodeArgs(key string) string { + return fmt.Sprintf(` + %s + %s + %s + %s + %s + %s + %s + `, b.CompanyName, b.SupportEmail, b.CompanyURL, b.TermsOfUsageURL, b.BannerLine1, b.BannerLine2, b.BannerLine3) +} + +// Action reflects the available actions to perform on a domain +type Action string + +var ( + // ActionRegister registers a domain + ActionRegister Action = "register" + // ActionTransfer transfers a domain to another provider + ActionTransfer Action = "transfer" + // ActionInternalPull transfers a domain to another account at TransIP + ActionInternalPull Action = "internalpull" +) + +// ActionResult represents a Transip_DomainAction object as described at +// https://api.transip.nl/docs/transip.nl/class-Transip_DomainAction.html +type ActionResult struct { + Name string `xml:"name"` + HasFailed bool `xml:"hasFailed"` + Message string `xml:"message"` +} + +// WhoisContact represents a TransIP_WhoisContact object +// as described at https://api.transip.nl/docs/transip.nl/class-Transip_WhoisContact.html +type WhoisContact struct { + Type string `xml:"type"` + FirstName string `xml:"firstName"` + MiddleName string `xml:"middleName"` + LastName string `xml:"lastName"` + CompanyName string `xml:"companyName"` + CompanyKvk string `xml:"companyKvk"` + CompanyType string `xml:"companyType"` + Street string `xml:"street"` + Number string `xml:"number"` + PostalCode string `xml:"postalCode"` + City string `xml:"city"` + PhoneNumber string `xml:"phoneNumber"` + FaxNumber string `xml:"faxNumber"` + Email string `xml:"email"` + Country string `xml:"country"` +} + +// WhoisContacts is just an array of WhoisContact +// basically only here so it can implement paramsEncoder +type WhoisContacts []WhoisContact + +// EncodeParams returns WhoisContacts parameters ready to be used for constructing a signature +func (w WhoisContacts) EncodeParams(prm gotransip.ParamsContainer) { + idx := prm.Len() + for i, e := range w { + prm.Add(fmt.Sprintf("%d[%d][type]", idx, i), e.Type) + prm.Add(fmt.Sprintf("%d[%d][firstName]", idx, i), e.FirstName) + prm.Add(fmt.Sprintf("%d[%d][middleName]", idx, i), e.MiddleName) + prm.Add(fmt.Sprintf("%d[%d][lastName]", idx, i), e.LastName) + prm.Add(fmt.Sprintf("%d[%d][companyName]", idx, i), e.CompanyName) + prm.Add(fmt.Sprintf("%d[%d][companyKvk]", idx, i), e.CompanyKvk) + prm.Add(fmt.Sprintf("%d[%d][companyType]", idx, i), e.CompanyType) + prm.Add(fmt.Sprintf("%d[%d][street]", idx, i), e.Street) + prm.Add(fmt.Sprintf("%d[%d][number]", idx, i), e.Number) + prm.Add(fmt.Sprintf("%d[%d][postalCode]", idx, i), e.PostalCode) + prm.Add(fmt.Sprintf("%d[%d][city]", idx, i), e.City) + prm.Add(fmt.Sprintf("%d[%d][phoneNumber]", idx, i), e.PhoneNumber) + prm.Add(fmt.Sprintf("%d[%d][faxNumber]", idx, i), e.FaxNumber) + prm.Add(fmt.Sprintf("%d[%d][email]", idx, i), e.Email) + prm.Add(fmt.Sprintf("%d[%d][country]", idx, i), e.Country) + } +} + +// EncodeArgs returns WhoisContacts XML body ready to be passed in the SOAP call +func (w WhoisContacts) EncodeArgs(key string) string { + output := fmt.Sprintf(`<%s SOAP-ENC:arrayType="ns1:WhoisContact[%d]" xsi:type="ns1:ArrayOfWhoisContact">`, key, len(w)) + "\n" + for _, e := range w { + output += fmt.Sprintf(` + %s + %s + %s + %s + %s + %s + %s + %s + %s + %s + %s + %s + %s + %s + %s + `, e.Type, e.FirstName, e.MiddleName, e.LastName, e.CompanyName, + e.CompanyKvk, e.CompanyType, e.Street, e.Number, e.PostalCode, e.City, + e.PhoneNumber, e.FaxNumber, e.Email, e.Country) + "\n" + } + + return output + fmt.Sprintf("", key) +} diff --git a/vendor/github.com/transip/gotransip/sign.go b/vendor/github.com/transip/gotransip/sign.go new file mode 100644 index 000000000..8b7d360c1 --- /dev/null +++ b/vendor/github.com/transip/gotransip/sign.go @@ -0,0 +1,49 @@ +package gotransip + +import ( + "crypto" + "crypto/rand" + "crypto/rsa" + "crypto/sha512" + "crypto/x509" + "encoding/base64" + "encoding/pem" + "errors" + "fmt" + "net/url" +) + +var ( + asn1Header = []byte{ + 0x30, 0x51, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, + 0x04, 0x02, 0x03, 0x05, 0x00, 0x04, 0x40, + } +) + +func signWithKey(params *soapParams, key []byte) (string, error) { + // create SHA512 hash of given parameters + h := sha512.New() + h.Write([]byte(params.Encode())) + + // prefix ASN1 header to SHA512 hash + digest := append(asn1Header, h.Sum(nil)...) + + // prepare key struct + block, _ := pem.Decode(key) + if block == nil { + return "", errors.New("could not decode private key") + } + parsed, err := x509.ParsePKCS8PrivateKey(block.Bytes) + if err != nil { + return "", fmt.Errorf("could not parse private key: %s", err.Error()) + } + + pkey := parsed.(*rsa.PrivateKey) + + enc, err := rsa.SignPKCS1v15(rand.Reader, pkey, crypto.Hash(0), digest) + if err != nil { + return "", fmt.Errorf("could not sign data: %s", err.Error()) + } + + return url.QueryEscape(base64.StdEncoding.EncodeToString(enc)), nil +} diff --git a/vendor/github.com/transip/gotransip/soap.go b/vendor/github.com/transip/gotransip/soap.go new file mode 100644 index 000000000..b893b7f90 --- /dev/null +++ b/vendor/github.com/transip/gotransip/soap.go @@ -0,0 +1,419 @@ +package gotransip + +import ( + "bytes" + "crypto/tls" + "encoding/xml" + "errors" + "fmt" + "io/ioutil" + "math/rand" + "net/http" + "net/url" + "regexp" + "strings" + "time" +) + +const ( + // format for SOAP envelopes + soapEnvelopeFixture string = ` + + %s +` +) + +// getSOAPArgs returns XML representing given name and argument as SOAP body +func getSOAPArgs(name string, input ...string) []byte { + var buf bytes.Buffer + + buf.WriteString(fmt.Sprintf("", name)) + for _, x := range input { + buf.WriteString(x) + } + buf.WriteString(fmt.Sprintf("", name)) + + return buf.Bytes() +} + +// getSOAPArg returns XML representing given input argument as SOAP parameters +// in combination with getSOAPArgs you can build SOAP body +func getSOAPArg(name string, input interface{}) (output string) { + switch input.(type) { + case []string: + i := input.([]string) + output = fmt.Sprintf(`<%s SOAP-ENC:arrayType="xsd:string[%d]" xsi:type="ns1:ArrayOfString">`, name, len(i)) + for _, x := range i { + output = output + fmt.Sprintf(`%s`, x) + } + output = output + fmt.Sprintf("", name) + case string: + output = fmt.Sprintf(`<%s xsi:type="xsd:string">%s`, name, input, name) + case int, int32, int64: + output = fmt.Sprintf(`<%s xsi:type="xsd:integer">%d`, name, input, name) + } + + return +} + +type soapFault struct { + Code string `xml:"faultcode,omitempty"` + Description string `xml:"faultstring,omitempty"` +} + +func (s soapFault) String() string { + return fmt.Sprintf("SOAP Fault %s: %s", s.Code, s.Description) +} + +// paramsEncoder allows SoapParams to hook into encoding theirselves, useful when +// fields consist of complex structs +type paramsEncoder interface { + EncodeParams(ParamsContainer) + EncodeArgs(string) string +} + +// ParamsContainer is the interface a type should implement to be able to hold +// SOAP parameters +type ParamsContainer interface { + Len() int + Add(string, interface{}) +} + +// soapParams is a utility to make sure parameter data is encoded into a query +// in the same order as we set them. The TransIP API requires this order for +// verifying the signature +type soapParams struct { + keys []string + values []interface{} +} + +// Add adds parameter data to the end of this SoapParams +func (s *soapParams) Add(k string, v interface{}) { + if s.keys == nil { + s.keys = make([]string, 0) + } + + if s.values == nil { + s.values = make([]interface{}, 0) + } + + s.keys = append(s.keys, k) + s.values = append(s.values, v) +} + +// Len returns amount of parameters set in this SoapParams +func (s soapParams) Len() int { + return len(s.keys) +} + +// Encode returns a URL-like query string that can be used to generate a request's +// signature. It's similar to url.Values.Encode() but without sorting of the keys +// and based on the value's type it tries to encode accordingly. +func (s soapParams) Encode() string { + var buf bytes.Buffer + var key string + + for i, v := range s.values { + // if this is not the first parameter, prefix with & + if i > 0 { + buf.WriteString("&") + } + + // for empty data fields, don't encode anything + if v == nil { + continue + } + + key = s.keys[i] + + switch v.(type) { + case []string: + c := v.([]string) + for j, cc := range c { + if j > 0 { + buf.WriteString("&") + } + buf.WriteString(fmt.Sprintf("%s[%d]=", key, j)) + buf.WriteString(strings.Replace(url.QueryEscape(cc), "+", "%20", -1)) + } + case string: + c := v.(string) + buf.WriteString(fmt.Sprintf("%s=", key)) + buf.WriteString(strings.Replace(url.QueryEscape(c), "+", "%20", -1)) + case int, int8, int16, int32, int64: + buf.WriteString(fmt.Sprintf("%s=", key)) + buf.WriteString(fmt.Sprintf("%d", v)) + default: + continue + } + } + + return buf.String() +} + +type soapHeader struct { + XMLName struct{} `xml:"Header"` + Contents []byte `xml:",innerxml"` +} + +type soapBody struct { + XMLName struct{} `xml:"Body"` + Contents []byte `xml:",innerxml"` +} + +type soapResponse struct { + Response struct { + InnerXML []byte `xml:",innerxml"` + } `xml:"return"` +} + +type soapEnvelope struct { + XMLName struct{} `xml:"Envelope"` + Header soapHeader + Body soapBody +} + +// SoapRequest holds all information for perfoming a SOAP request +// Arguments to the request can be specified with AddArgument +// If padding is defined, the SOAP response will be parsed after it being padded +// with items in Padding in reverse order +type SoapRequest struct { + Service string + Method string + params *soapParams // params used for creating signature + args []string // XML body arguments + Padding []string +} + +// AddArgument adds an argument to the SoapRequest; the arguments ared used to +// fill the XML request body as well as to create a valid signature for the +// request +func (sr *SoapRequest) AddArgument(key string, value interface{}) { + if sr.params == nil { + sr.params = &soapParams{} + } + + // check if value implements paramsEncoder + if pe, ok := value.(paramsEncoder); ok { + sr.args = append(sr.args, pe.EncodeArgs(key)) + pe.EncodeParams(sr.params) + return + } + + switch value.(type) { + case []string: + sr.params.Add(fmt.Sprintf("%d", sr.params.Len()), value) + sr.args = append(sr.args, getSOAPArg(key, value)) + case string: + sr.params.Add(fmt.Sprintf("%d", sr.params.Len()), value) + sr.args = append(sr.args, getSOAPArg(key, value.(string))) + case int, int8, int16, int32, int64: + sr.params.Add(fmt.Sprintf("%d", sr.params.Len()), value) + sr.args = append(sr.args, getSOAPArg(key, fmt.Sprintf("%d", value))) + default: + // check if value implements the String interface + if str, ok := value.(fmt.Stringer); ok { + sr.params.Add(fmt.Sprintf("%d", sr.params.Len()), str.String()) + sr.args = append(sr.args, getSOAPArg(key, str.String())) + } + } +} + +func (sr SoapRequest) getEnvelope() string { + return fmt.Sprintf(soapEnvelopeFixture, transipAPIHost, getSOAPArgs(sr.Method, sr.args...)) +} + +type soapClient struct { + Login string + Mode APIMode + PrivateKey []byte +} + +// httpReqForSoapRequest creates the HTTP request for a specific SoapRequest +// this includes setting the URL, POST body and cookies +func (s soapClient) httpReqForSoapRequest(req SoapRequest) (*http.Request, error) { + // format URL + url := fmt.Sprintf("https://%s/soap/?service=%s", transipAPIHost, req.Service) + + // create HTTP request + // TransIP API SOAP requests are always POST requests + httpReq, err := http.NewRequest("POST", url, strings.NewReader(req.getEnvelope())) + if err != nil { + return nil, err + } + + // generate a number-used-once, a.k.a. nonce + // seeding the RNG is important if we want to do prevent using the same nonce + // in 2 sequential requests + rand.Seed(time.Now().UnixNano()) + nonce := fmt.Sprintf("%d", rand.Int()) + // set time of request, used later for signature as well + timestamp := fmt.Sprintf("%d", time.Now().Unix()) + + // set cookies required for the request + // most of these cookies are used for the signature as well so they should + // obviously match + httpReq.AddCookie(&http.Cookie{ + Name: "login", + Value: s.Login, + }) + httpReq.AddCookie(&http.Cookie{ + Name: "mode", + Value: string(s.Mode), + }) + httpReq.AddCookie(&http.Cookie{ + Name: "timestamp", + Value: timestamp, + }) + httpReq.AddCookie(&http.Cookie{ + Name: "nonce", + Value: nonce, + }) + + // add params required for signature to the request parameters + if req.params == nil { + req.params = &soapParams{} + } + // TransIP API is quite picky on the order of the parameters + // so don't change anything in the order below + req.params.Add("__method", req.Method) + req.params.Add("__service", req.Service) + req.params.Add("__hostname", transipAPIHost) + req.params.Add("__timestamp", timestamp) + req.params.Add("__nonce", nonce) + + signature, err := signWithKey(req.params, s.PrivateKey) + if err != nil { + return nil, err + } + + // add signature of the request to the cookies as well + httpReq.AddCookie(&http.Cookie{ + Name: "signature", + Value: signature, + }) + + return httpReq, nil +} + +func parseSoapResponse(data []byte, padding []string, statusCode int, result interface{}) error { + // try to decode the resulting XML + var env soapEnvelope + if err := xml.Unmarshal(data, &env); err != nil { + return err + } + + // try to decode the body to a soapFault + var fault soapFault + if err := xml.Unmarshal(env.Body.Contents, &fault); err != nil { + return err + } + + // by checking fault's Code, we can determine if the response body in fact + // was a SOAP fault and if it was: return it as an error + if len(fault.Code) > 0 { + return errors.New(fault.String()) + } + + // try to decode into soapResponse + sr := soapResponse{} + if err := xml.Unmarshal(env.Body.Contents, &sr); err != nil { + return err + } + + // if the response was empty and HTTP status was 200, consider it a success + if len(sr.Response.InnerXML) == 0 && statusCode == 200 { + return nil + } + + // it seems like xml.Unmarshal won't work well on the most outer element + // so even when no Padding is defined, always pad with "transip" element + p := append([]string{"transip"}, padding...) + innerXML := padXMLData(sr.Response.InnerXML, p) + + // try to decode to given result interface + return xml.Unmarshal([]byte(innerXML), &result) +} + +func (s *soapClient) call(req SoapRequest, result interface{}) error { + // get http request for soap request + httpReq, err := s.httpReqForSoapRequest(req) + if err != nil { + return err + } + + // create HTTP client and do the actual request + client := &http.Client{Timeout: time.Second * 10} + // make sure to verify the validity of remote certificate + // this is the default, but adding this flag here makes it easier to toggle + // it for testing/debugging + client.Transport = &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: false, + }, + } + resp, err := client.Do(httpReq) + if err != nil { + return fmt.Errorf("request error:\n%s", err.Error()) + } + defer resp.Body.Close() + + // read entire response body + b, err := ioutil.ReadAll(resp.Body) + if err != nil { + return err + } + + // parse SOAP response into given result interface + return parseSoapResponse(b, req.Padding, resp.StatusCode, result) +} + +// apply given padding around the XML data fed into this function +// padding is applied in reverse order, so last element of padding is the +// innermost element in the resulting XML +func padXMLData(data []byte, padding []string) []byte { + // get right information from padding elements by matching to regex + re, _ := regexp.Compile("^]+)>?$") + + var prefix, suffix []byte + var tag, attr string + // go over each padding element + for i := len(padding); i > 0; i-- { + res := re.FindStringSubmatch(padding[i-1]) + // no attribute was given + if len(res[1]) == 0 { + tag = res[2] + attr = "" + } else { + tag = res[1] + attr = " " + res[2] + } + + prefix = []byte(fmt.Sprintf("<%s%s>", tag, attr)) + suffix = []byte(fmt.Sprintf("", tag)) + data = append(append(prefix, data...), suffix...) + } + + return data +} + +// TestParamsContainer is only useful for unit testing the ParamsContainer +// implementation of other type +type TestParamsContainer struct { + Prm string +} + +// Add just makes sure we use Len(), key and value in the result so it can be +// tested +func (t *TestParamsContainer) Add(key string, value interface{}) { + var prefix string + if t.Len() > 0 { + prefix = "&" + } + t.Prm = t.Prm + prefix + fmt.Sprintf("%d%s=%s", t.Len(), key, value) +} + +// Len returns current length of test data in TestParamsContainer +func (t TestParamsContainer) Len() int { + return len(t.Prm) +} diff --git a/vendor/github.com/transip/gotransip/util/util.go b/vendor/github.com/transip/gotransip/util/util.go new file mode 100644 index 000000000..7f1beecf4 --- /dev/null +++ b/vendor/github.com/transip/gotransip/util/util.go @@ -0,0 +1,37 @@ +package util + +import ( + "encoding/xml" + "time" +) + +// KeyValueXML resembles the complex struct for getting key/value pairs from XML +type KeyValueXML struct { + Cont []struct { + Item []struct { + Key string `xml:"key"` + Value string `xml:"value"` + } `xml:"item"` + } `xml:"item"` +} + +// XMLTime is a custom type to decode XML values to time.Time directly +type XMLTime struct { + time.Time +} + +// UnmarshalXML is implemented to be able act as custom XML type +// it tries to parse time from given elements value +func (x *XMLTime) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { + var v string + if err := d.DecodeElement(&v, &start); err != nil { + return err + } + + if p, _ := time.Parse("2006-01-02 15:04:05", v); !p.IsZero() { + *x = XMLTime{p} + } else if p, _ := time.Parse("2006-01-02", v); !p.IsZero() { + *x = XMLTime{p} + } + return nil +} diff --git a/vendor/github.com/xenolf/lego/acme/client.go b/vendor/github.com/xenolf/lego/acme/client.go index 4908d592c..f9857ebd0 100644 --- a/vendor/github.com/xenolf/lego/acme/client.go +++ b/vendor/github.com/xenolf/lego/acme/client.go @@ -665,7 +665,7 @@ func (c *Client) getAuthzForOrder(order orderResource) ([]authorization, error) go func(authzURL string) { var authz authorization - _, err := getJSON(authzURL, &authz) + _, err := postAsGet(c.jws, authzURL, &authz) if err != nil { errc <- domainError{Domain: authz.Identifier.Value, Error: err} return @@ -789,7 +789,7 @@ func (c *Client) requestCertificateForCsr(order orderResource, bundle bool, csr case <-stopTimer.C: return nil, errors.New("certificate polling timed out") case <-retryTick.C: - _, err := getJSON(order.URL, &retOrder) + _, err := postAsGet(c.jws, order.URL, &retOrder) if err != nil { return nil, err } @@ -813,7 +813,7 @@ func (c *Client) requestCertificateForCsr(order orderResource, bundle bool, csr func (c *Client) checkCertResponse(order orderMessage, certRes *CertificateResource, bundle bool) (bool, error) { switch order.Status { case statusValid: - resp, err := httpGet(order.Certificate) + resp, err := postAsGet(c.jws, order.Certificate, nil) if err != nil { return false, err } @@ -871,7 +871,7 @@ func (c *Client) checkCertResponse(order orderMessage, certRes *CertificateResou // getIssuerCertificate requests the issuer certificate func (c *Client) getIssuerCertificate(url string) ([]byte, error) { log.Infof("acme: Requesting issuer cert from %s", url) - resp, err := httpGet(url) + resp, err := postAsGet(c.jws, url, nil) if err != nil { return nil, err } @@ -914,7 +914,10 @@ func parseLinks(links []string) map[string]string { func validate(j *jws, domain, uri string, c challenge) error { var chlng challenge - hdr, err := postJSON(j, uri, c, &chlng) + // Challenge initiation is done by sending a JWS payload containing the + // trivial JSON object `{}`. We use an empty struct instance as the postJSON + // payload here to achieve this result. + hdr, err := postJSON(j, uri, struct{}{}, &chlng) if err != nil { return err } @@ -940,11 +943,15 @@ func validate(j *jws, domain, uri string, c challenge) error { // If it doesn't, we'll just poll hard. ra = 5 } + time.Sleep(time.Duration(ra) * time.Second) - hdr, err = getJSON(uri, &chlng) + resp, err := postAsGet(j, uri, &chlng) if err != nil { return err } + if resp != nil { + hdr = resp.Header + } } } diff --git a/vendor/github.com/xenolf/lego/acme/http.go b/vendor/github.com/xenolf/lego/acme/http.go index 2ddbc0f1b..8343b3690 100644 --- a/vendor/github.com/xenolf/lego/acme/http.go +++ b/vendor/github.com/xenolf/lego/acme/http.go @@ -42,12 +42,14 @@ var ( ) const ( - // defaultGoUserAgent is the Go HTTP package user agent string. Too - // bad it isn't exported. If it changes, we should update it here, too. - defaultGoUserAgent = "Go-http-client/1.1" - // ourUserAgent is the User-Agent of this underlying library package. - ourUserAgent = "xenolf-acme" + // NOTE: Update this with each tagged release. + ourUserAgent = "xenolf-acme/1.2.1" + + // ourUserAgentComment is part of the UA comment linked to the version status of this underlying library package. + // values: detach|release + // NOTE: Update this with each tagged release. + ourUserAgentComment = "detach" // caCertificatesEnvVar is the environment variable name that can be used to // specify the path to PEM encoded CA Certificates that can be used to @@ -151,52 +153,60 @@ func postJSON(j *jws, uri string, reqBody, respBody interface{}) (http.Header, e return nil, errors.New("failed to marshal network message") } - resp, err := j.post(uri, jsonBytes) - if err != nil { - return nil, fmt.Errorf("failed to post JWS message. -> %v", err) + resp, err := post(j, uri, jsonBytes, respBody) + if resp == nil { + return nil, err } defer resp.Body.Close() + return resp.Header, err +} + +func postAsGet(j *jws, uri string, respBody interface{}) (*http.Response, error) { + return post(j, uri, []byte{}, respBody) +} + +func post(j *jws, uri string, reqBody []byte, respBody interface{}) (*http.Response, error) { + resp, err := j.post(uri, reqBody) + if err != nil { + return nil, fmt.Errorf("failed to post JWS message. -> %v", err) + } + if resp.StatusCode >= http.StatusBadRequest { err = handleHTTPError(resp) switch err.(type) { case NonceError: // Retry once if the nonce was invalidated - retryResp, errP := j.post(uri, jsonBytes) + retryResp, errP := j.post(uri, reqBody) if errP != nil { return nil, fmt.Errorf("failed to post JWS message. -> %v", errP) } - defer retryResp.Body.Close() - if retryResp.StatusCode >= http.StatusBadRequest { - return retryResp.Header, handleHTTPError(retryResp) + return retryResp, handleHTTPError(retryResp) } if respBody == nil { - return retryResp.Header, nil + return retryResp, nil } - return retryResp.Header, json.NewDecoder(retryResp.Body).Decode(respBody) - + return retryResp, json.NewDecoder(retryResp.Body).Decode(respBody) default: - return resp.Header, err - + return resp, err } - } if respBody == nil { - return resp.Header, nil + return resp, nil } - return resp.Header, json.NewDecoder(resp.Body).Decode(respBody) + return resp, json.NewDecoder(resp.Body).Decode(respBody) } // userAgent builds and returns the User-Agent string to use in requests. func userAgent() string { - ua := fmt.Sprintf("%s %s (%s; %s) %s", UserAgent, ourUserAgent, runtime.GOOS, runtime.GOARCH, defaultGoUserAgent) + ua := fmt.Sprintf("%s %s (%s; %s; %s)", UserAgent, ourUserAgent, ourUserAgentComment, runtime.GOOS, runtime.GOARCH) return strings.TrimSpace(ua) } diff --git a/vendor/github.com/xenolf/lego/acme/tls_alpn_challenge.go b/vendor/github.com/xenolf/lego/acme/tls_alpn_challenge.go index d80351999..cc70c3502 100644 --- a/vendor/github.com/xenolf/lego/acme/tls_alpn_challenge.go +++ b/vendor/github.com/xenolf/lego/acme/tls_alpn_challenge.go @@ -12,8 +12,8 @@ import ( ) // idPeAcmeIdentifierV1 is the SMI Security for PKIX Certification Extension OID referencing the ACME extension. -// Reference: https://tools.ietf.org/html/draft-ietf-acme-tls-alpn-01#section-5.1 -var idPeAcmeIdentifierV1 = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 30, 1} +// Reference: https://tools.ietf.org/html/draft-ietf-acme-tls-alpn-05#section-5.1 +var idPeAcmeIdentifierV1 = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 31} type tlsALPNChallenge struct { jws *jws @@ -58,7 +58,7 @@ func TLSALPNChallengeBlocks(domain, keyAuth string) ([]byte, []byte, error) { // Add the keyAuth digest as the acmeValidation-v1 extension // (marked as critical such that it won't be used by non-ACME software). - // Reference: https://tools.ietf.org/html/draft-ietf-acme-tls-alpn-01#section-3 + // Reference: https://tools.ietf.org/html/draft-ietf-acme-tls-alpn-05#section-3 extensions := []pkix.Extension{ { Id: idPeAcmeIdentifierV1, diff --git a/vendor/github.com/xenolf/lego/providers/dns/cloudflare/cloudflare.go b/vendor/github.com/xenolf/lego/providers/dns/cloudflare/cloudflare.go index 33c2065e0..92c4b69fa 100644 --- a/vendor/github.com/xenolf/lego/providers/dns/cloudflare/cloudflare.go +++ b/vendor/github.com/xenolf/lego/providers/dns/cloudflare/cloudflare.go @@ -126,7 +126,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { TTL: d.config.TTL, } - response, _ := d.client.CreateDNSRecord(zoneID, dnsRecord) + response, err := d.client.CreateDNSRecord(zoneID, dnsRecord) if err != nil { return fmt.Errorf("cloudflare: failed to create TXT record: %v", err) } diff --git a/vendor/github.com/xenolf/lego/providers/dns/conoha/client.go b/vendor/github.com/xenolf/lego/providers/dns/conoha/client.go new file mode 100644 index 000000000..3fa8b5bbb --- /dev/null +++ b/vendor/github.com/xenolf/lego/providers/dns/conoha/client.go @@ -0,0 +1,205 @@ +package conoha + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "net/http" +) + +const ( + identityBaseURL = "https://identity.%s.conoha.io" + dnsServiceBaseURL = "https://dns-service.%s.conoha.io" +) + +// IdentityRequest is an authentication request body. +type IdentityRequest struct { + Auth Auth `json:"auth"` +} + +// Auth is an authentication information. +type Auth struct { + TenantID string `json:"tenantId"` + PasswordCredentials PasswordCredentials `json:"passwordCredentials"` +} + +// PasswordCredentials is API-user's credentials. +type PasswordCredentials struct { + Username string `json:"username"` + Password string `json:"password"` +} + +// IdentityResponse is an authentication response body. +type IdentityResponse struct { + Access Access `json:"access"` +} + +// Access is an identity information. +type Access struct { + Token Token `json:"token"` +} + +// Token is an api access token. +type Token struct { + ID string `json:"id"` +} + +// DomainListResponse is a response of a domain listing request. +type DomainListResponse struct { + Domains []Domain `json:"domains"` +} + +// Domain is a hosted domain entry. +type Domain struct { + ID string `json:"id"` + Name string `json:"name"` +} + +// RecordListResponse is a response of record listing request. +type RecordListResponse struct { + Records []Record `json:"records"` +} + +// Record is a record entry. +type Record struct { + ID string `json:"id,omitempty"` + Name string `json:"name"` + Type string `json:"type"` + Data string `json:"data"` + TTL int `json:"ttl"` +} + +// Client is a ConoHa API client. +type Client struct { + token string + endpoint string + httpClient *http.Client +} + +// NewClient returns a client instance logged into the ConoHa service. +func NewClient(region string, auth Auth, httpClient *http.Client) (*Client, error) { + if httpClient == nil { + httpClient = &http.Client{} + } + + c := &Client{httpClient: httpClient} + + c.endpoint = fmt.Sprintf(identityBaseURL, region) + + identity, err := c.getIdentity(auth) + if err != nil { + return nil, fmt.Errorf("failed to login: %v", err) + } + + c.token = identity.Access.Token.ID + c.endpoint = fmt.Sprintf(dnsServiceBaseURL, region) + + return c, nil +} + +func (c *Client) getIdentity(auth Auth) (*IdentityResponse, error) { + req := &IdentityRequest{Auth: auth} + + identity := &IdentityResponse{} + + err := c.do(http.MethodPost, "/v2.0/tokens", req, identity) + if err != nil { + return nil, err + } + + return identity, nil +} + +// GetDomainID returns an ID of specified domain. +func (c *Client) GetDomainID(domainName string) (string, error) { + domainList := &DomainListResponse{} + + err := c.do(http.MethodGet, "/v1/domains", nil, domainList) + if err != nil { + return "", err + } + + for _, domain := range domainList.Domains { + if domain.Name == domainName { + return domain.ID, nil + } + } + return "", fmt.Errorf("no such domain: %s", domainName) +} + +// GetRecordID returns an ID of specified record. +func (c *Client) GetRecordID(domainID, recordName, recordType, data string) (string, error) { + recordList := &RecordListResponse{} + + err := c.do(http.MethodGet, fmt.Sprintf("/v1/domains/%s/records", domainID), nil, recordList) + if err != nil { + return "", err + } + + for _, record := range recordList.Records { + if record.Name == recordName && record.Type == recordType && record.Data == data { + return record.ID, nil + } + } + return "", errors.New("no such record") +} + +// CreateRecord adds new record. +func (c *Client) CreateRecord(domainID string, record Record) error { + return c.do(http.MethodPost, fmt.Sprintf("/v1/domains/%s/records", domainID), record, nil) +} + +// DeleteRecord removes specified record. +func (c *Client) DeleteRecord(domainID, recordID string) error { + return c.do(http.MethodDelete, fmt.Sprintf("/v1/domains/%s/records/%s", domainID, recordID), nil, nil) +} + +func (c *Client) do(method, path string, payload, result interface{}) error { + body := bytes.NewReader(nil) + + if payload != nil { + bodyBytes, err := json.Marshal(payload) + if err != nil { + return err + } + body = bytes.NewReader(bodyBytes) + } + + req, err := http.NewRequest(method, c.endpoint+path, body) + if err != nil { + return err + } + + req.Header.Set("Accept", "application/json") + req.Header.Set("Content-Type", "application/json") + req.Header.Set("X-Auth-Token", c.token) + + resp, err := c.httpClient.Do(req) + if err != nil { + return err + } + + if resp.StatusCode != http.StatusOK { + respBody, err := ioutil.ReadAll(resp.Body) + if err != nil { + return err + } + defer resp.Body.Close() + + return fmt.Errorf("HTTP request failed with status code %d: %s", resp.StatusCode, string(respBody)) + } + + if result != nil { + respBody, err := ioutil.ReadAll(resp.Body) + if err != nil { + return err + } + defer resp.Body.Close() + + return json.Unmarshal(respBody, result) + } + + return nil +} diff --git a/vendor/github.com/xenolf/lego/providers/dns/conoha/conoha.go b/vendor/github.com/xenolf/lego/providers/dns/conoha/conoha.go new file mode 100644 index 000000000..a872789f3 --- /dev/null +++ b/vendor/github.com/xenolf/lego/providers/dns/conoha/conoha.go @@ -0,0 +1,148 @@ +// Package conoha implements a DNS provider for solving the DNS-01 challenge +// using ConoHa DNS. +package conoha + +import ( + "errors" + "fmt" + "net/http" + "time" + + "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 { + Region string + TenantID string + Username string + Password string + TTL int + PropagationTimeout time.Duration + PollingInterval time.Duration + HTTPClient *http.Client +} + +// NewDefaultConfig returns a default configuration for the DNSProvider +func NewDefaultConfig() *Config { + return &Config{ + Region: env.GetOrDefaultString("CONOHA_REGION", "tyo1"), + TTL: env.GetOrDefaultInt("CONOHA_TTL", 60), + PropagationTimeout: env.GetOrDefaultSecond("CONOHA_PROPAGATION_TIMEOUT", acme.DefaultPropagationTimeout), + PollingInterval: env.GetOrDefaultSecond("CONOHA_POLLING_INTERVAL", acme.DefaultPollingInterval), + HTTPClient: &http.Client{ + Timeout: env.GetOrDefaultSecond("CONOHA_HTTP_TIMEOUT", 30*time.Second), + }, + } +} + +// DNSProvider is an implementation of the acme.ChallengeProvider interface +type DNSProvider struct { + config *Config + client *Client +} + +// NewDNSProvider returns a DNSProvider instance configured for ConoHa DNS. +// Credentials must be passed in the environment variables: CONOHA_TENANT_ID, CONOHA_API_USERNAME, CONOHA_API_PASSWORD +func NewDNSProvider() (*DNSProvider, error) { + values, err := env.Get("CONOHA_TENANT_ID", "CONOHA_API_USERNAME", "CONOHA_API_PASSWORD") + if err != nil { + return nil, fmt.Errorf("conoha: %v", err) + } + + config := NewDefaultConfig() + config.TenantID = values["CONOHA_TENANT_ID"] + config.Username = values["CONOHA_API_USERNAME"] + config.Password = values["CONOHA_API_PASSWORD"] + + return NewDNSProviderConfig(config) +} + +// NewDNSProviderConfig return a DNSProvider instance configured for ConoHa DNS. +func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { + if config == nil { + return nil, errors.New("conoha: the configuration of the DNS provider is nil") + } + + if config.TenantID == "" || config.Username == "" || config.Password == "" { + return nil, errors.New("conoha: some credentials information are missing") + } + + auth := Auth{ + TenantID: config.TenantID, + PasswordCredentials: PasswordCredentials{ + Username: config.Username, + Password: config.Password, + }, + } + + client, err := NewClient(config.Region, auth, config.HTTPClient) + if err != nil { + return nil, fmt.Errorf("conoha: failed to create client: %v", err) + } + + return &DNSProvider{config: config, client: client}, nil +} + +// Present creates a TXT record to fulfill the dns-01 challenge. +func (d *DNSProvider) Present(domain, token, keyAuth string) error { + fqdn, value, _ := acme.DNS01Record(domain, keyAuth) + + authZone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers) + if err != nil { + return err + } + + id, err := d.client.GetDomainID(authZone) + if err != nil { + return fmt.Errorf("conoha: failed to get domain ID: %v", err) + } + + record := Record{ + Name: fqdn, + Type: "TXT", + Data: value, + TTL: d.config.TTL, + } + + err = d.client.CreateRecord(id, record) + if err != nil { + return fmt.Errorf("conoha: failed to create record: %v", err) + } + + return nil +} + +// CleanUp clears ConoHa DNS TXT record +func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { + fqdn, value, _ := acme.DNS01Record(domain, keyAuth) + + authZone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers) + if err != nil { + return err + } + + domID, err := d.client.GetDomainID(authZone) + if err != nil { + return fmt.Errorf("conoha: failed to get domain ID: %v", err) + } + + recID, err := d.client.GetRecordID(domID, fqdn, "TXT", value) + if err != nil { + return fmt.Errorf("conoha: failed to get record ID: %v", err) + } + + err = d.client.DeleteRecord(domID, recID) + if err != nil { + return fmt.Errorf("conoha: failed to delete record: %v", err) + } + + return nil +} + +// Timeout returns the timeout and interval to use when checking for DNS propagation. +// Adjusting here to cope with spikes in propagation times. +func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { + return d.config.PropagationTimeout, d.config.PollingInterval +} diff --git a/vendor/github.com/xenolf/lego/providers/dns/dns_providers.go b/vendor/github.com/xenolf/lego/providers/dns/dns_providers.go index ff7a7d833..604137d56 100644 --- a/vendor/github.com/xenolf/lego/providers/dns/dns_providers.go +++ b/vendor/github.com/xenolf/lego/providers/dns/dns_providers.go @@ -11,6 +11,7 @@ import ( "github.com/xenolf/lego/providers/dns/bluecat" "github.com/xenolf/lego/providers/dns/cloudflare" "github.com/xenolf/lego/providers/dns/cloudxns" + "github.com/xenolf/lego/providers/dns/conoha" "github.com/xenolf/lego/providers/dns/digitalocean" "github.com/xenolf/lego/providers/dns/dnsimple" "github.com/xenolf/lego/providers/dns/dnsmadeeasy" @@ -27,10 +28,13 @@ import ( "github.com/xenolf/lego/providers/dns/glesys" "github.com/xenolf/lego/providers/dns/godaddy" "github.com/xenolf/lego/providers/dns/hostingde" + "github.com/xenolf/lego/providers/dns/httpreq" "github.com/xenolf/lego/providers/dns/iij" + "github.com/xenolf/lego/providers/dns/inwx" "github.com/xenolf/lego/providers/dns/lightsail" "github.com/xenolf/lego/providers/dns/linode" "github.com/xenolf/lego/providers/dns/linodev4" + "github.com/xenolf/lego/providers/dns/mydnsjp" "github.com/xenolf/lego/providers/dns/namecheap" "github.com/xenolf/lego/providers/dns/namedotcom" "github.com/xenolf/lego/providers/dns/netcup" @@ -43,8 +47,11 @@ import ( "github.com/xenolf/lego/providers/dns/rfc2136" "github.com/xenolf/lego/providers/dns/route53" "github.com/xenolf/lego/providers/dns/sakuracloud" + "github.com/xenolf/lego/providers/dns/selectel" "github.com/xenolf/lego/providers/dns/stackpath" + "github.com/xenolf/lego/providers/dns/transip" "github.com/xenolf/lego/providers/dns/vegadns" + "github.com/xenolf/lego/providers/dns/vscale" "github.com/xenolf/lego/providers/dns/vultr" ) @@ -65,6 +72,8 @@ func NewDNSChallengeProviderByName(name string) (acme.ChallengeProvider, error) return cloudflare.NewDNSProvider() case "cloudxns": return cloudxns.NewDNSProvider() + case "conoha": + return conoha.NewDNSProvider() case "digitalocean": return digitalocean.NewDNSProvider() case "dnsimple": @@ -97,8 +106,12 @@ func NewDNSChallengeProviderByName(name string) (acme.ChallengeProvider, error) return godaddy.NewDNSProvider() case "hostingde": return hostingde.NewDNSProvider() + case "httpreq": + return httpreq.NewDNSProvider() case "iij": return iij.NewDNSProvider() + case "inwx": + return inwx.NewDNSProvider() case "lightsail": return lightsail.NewDNSProvider() case "linode": @@ -107,6 +120,8 @@ func NewDNSChallengeProviderByName(name string) (acme.ChallengeProvider, error) return linodev4.NewDNSProvider() case "manual": return acme.NewDNSProviderManual() + case "mydnsjp": + return mydnsjp.NewDNSProvider() case "namecheap": return namecheap.NewDNSProvider() case "namedotcom": @@ -133,10 +148,16 @@ func NewDNSChallengeProviderByName(name string) (acme.ChallengeProvider, error) return sakuracloud.NewDNSProvider() case "stackpath": return stackpath.NewDNSProvider() + case "selectel": + return selectel.NewDNSProvider() + case "transip": + return transip.NewDNSProvider() case "vegadns": return vegadns.NewDNSProvider() case "vultr": return vultr.NewDNSProvider() + case "vscale": + return vscale.NewDNSProvider() default: return nil, fmt.Errorf("unrecognised DNS provider: %s", name) } diff --git a/vendor/github.com/xenolf/lego/providers/dns/httpreq/httpreq.go b/vendor/github.com/xenolf/lego/providers/dns/httpreq/httpreq.go new file mode 100644 index 000000000..cf957d41b --- /dev/null +++ b/vendor/github.com/xenolf/lego/providers/dns/httpreq/httpreq.go @@ -0,0 +1,193 @@ +// Package httpreq implements a DNS provider for solving the DNS-01 challenge through a HTTP server. +package httpreq + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "net/http" + "net/url" + "os" + "time" + + "github.com/xenolf/lego/acme" + "github.com/xenolf/lego/platform/config/env" +) + +type message struct { + FQDN string `json:"fqdn"` + Value string `json:"value"` +} + +type messageRaw struct { + Domain string `json:"domain"` + Token string `json:"token"` + KeyAuth string `json:"keyAuth"` +} + +// Config is used to configure the creation of the DNSProvider +type Config struct { + Endpoint *url.URL + Mode string + Username string + Password string + PropagationTimeout time.Duration + PollingInterval time.Duration + HTTPClient *http.Client +} + +// NewDefaultConfig returns a default configuration for the DNSProvider +func NewDefaultConfig() *Config { + return &Config{ + PropagationTimeout: env.GetOrDefaultSecond("HTTPREQ_PROPAGATION_TIMEOUT", acme.DefaultPropagationTimeout), + PollingInterval: env.GetOrDefaultSecond("HTTPREQ_POLLING_INTERVAL", acme.DefaultPollingInterval), + HTTPClient: &http.Client{ + Timeout: env.GetOrDefaultSecond("HTTPREQ_HTTP_TIMEOUT", 30*time.Second), + }, + } +} + +// DNSProvider describes a provider for acme-proxy +type DNSProvider struct { + config *Config +} + +// NewDNSProvider returns a DNSProvider instance. +func NewDNSProvider() (*DNSProvider, error) { + values, err := env.Get("HTTPREQ_ENDPOINT") + if err != nil { + return nil, fmt.Errorf("httpreq: %v", err) + } + + endpoint, err := url.Parse(values["HTTPREQ_ENDPOINT"]) + if err != nil { + return nil, fmt.Errorf("httpreq: %v", err) + } + + config := NewDefaultConfig() + config.Mode = os.Getenv("HTTPREQ_MODE") + config.Username = os.Getenv("HTTPREQ_USERNAME") + config.Password = os.Getenv("HTTPREQ_PASSWORD") + config.Endpoint = endpoint + return NewDNSProviderConfig(config) +} + +// NewDNSProviderConfig return a DNSProvider . +func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { + if config == nil { + return nil, errors.New("httpreq: the configuration of the DNS provider is nil") + } + + if config.Endpoint == nil { + return nil, errors.New("httpreq: the endpoint is missing") + } + + return &DNSProvider{config: config}, nil +} + +// Timeout returns the timeout and interval to use when checking for DNS propagation. +// Adjusting here to cope with spikes in propagation times. +func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { + return d.config.PropagationTimeout, d.config.PollingInterval +} + +// Present creates a TXT record to fulfill the dns-01 challenge +func (d *DNSProvider) Present(domain, token, keyAuth string) error { + if d.config.Mode == "RAW" { + msg := &messageRaw{ + Domain: domain, + Token: token, + KeyAuth: keyAuth, + } + + err := d.doPost("/present", msg) + if err != nil { + return fmt.Errorf("httpreq: %v", err) + } + return nil + } + + fqdn, value, _ := acme.DNS01Record(domain, keyAuth) + msg := &message{ + FQDN: fqdn, + Value: value, + } + + err := d.doPost("/present", msg) + if err != nil { + return fmt.Errorf("httpreq: %v", err) + } + return nil +} + +// CleanUp removes the TXT record matching the specified parameters +func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { + if d.config.Mode == "RAW" { + msg := &messageRaw{ + Domain: domain, + Token: token, + KeyAuth: keyAuth, + } + + err := d.doPost("/cleanup", msg) + if err != nil { + return fmt.Errorf("httpreq: %v", err) + } + return nil + } + + fqdn, value, _ := acme.DNS01Record(domain, keyAuth) + msg := &message{ + FQDN: fqdn, + Value: value, + } + + err := d.doPost("/cleanup", msg) + if err != nil { + return fmt.Errorf("httpreq: %v", err) + } + return nil +} + +func (d *DNSProvider) doPost(uri string, msg interface{}) error { + reqBody := &bytes.Buffer{} + err := json.NewEncoder(reqBody).Encode(msg) + if err != nil { + return err + } + + endpoint, err := d.config.Endpoint.Parse(uri) + if err != nil { + return err + } + + req, err := http.NewRequest(http.MethodPost, endpoint.String(), reqBody) + if err != nil { + return err + } + + req.Header.Set("Content-Type", "application/json") + + if len(d.config.Username) > 0 && len(d.config.Password) > 0 { + req.SetBasicAuth(d.config.Username, d.config.Password) + } + + resp, err := d.config.HTTPClient.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode >= http.StatusBadRequest { + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("%d: failed to read response body: %v", resp.StatusCode, err) + } + + return fmt.Errorf("%d: request failed: %v", resp.StatusCode, string(body)) + } + + return nil +} diff --git a/vendor/github.com/xenolf/lego/providers/dns/inwx/inwx.go b/vendor/github.com/xenolf/lego/providers/dns/inwx/inwx.go new file mode 100644 index 000000000..ba6c8cbec --- /dev/null +++ b/vendor/github.com/xenolf/lego/providers/dns/inwx/inwx.go @@ -0,0 +1,166 @@ +// Package inwx implements a DNS provider for solving the DNS-01 challenge using inwx dom robot +package inwx + +import ( + "errors" + "fmt" + "time" + + "github.com/smueller18/goinwx" + "github.com/xenolf/lego/acme" + "github.com/xenolf/lego/log" + "github.com/xenolf/lego/platform/config/env" +) + +// Config is used to configure the creation of the DNSProvider +type Config struct { + Username string + Password string + Sandbox bool + PropagationTimeout time.Duration + PollingInterval time.Duration + TTL int +} + +// NewDefaultConfig returns a default configuration for the DNSProvider +func NewDefaultConfig() *Config { + return &Config{ + PropagationTimeout: env.GetOrDefaultSecond("INWX_PROPAGATION_TIMEOUT", acme.DefaultPropagationTimeout), + PollingInterval: env.GetOrDefaultSecond("INWX_POLLING_INTERVAL", acme.DefaultPollingInterval), + TTL: env.GetOrDefaultInt("INWX_TTL", 300), + Sandbox: env.GetOrDefaultBool("INWX_SANDBOX", false), + } +} + +// DNSProvider is an implementation of the acme.ChallengeProvider interface +type DNSProvider struct { + config *Config + client *goinwx.Client +} + +// NewDNSProvider returns a DNSProvider instance configured for Dyn DNS. +// Credentials must be passed in the environment variables: +// INWX_USERNAME and INWX_PASSWORD. +func NewDNSProvider() (*DNSProvider, error) { + values, err := env.Get("INWX_USERNAME", "INWX_PASSWORD") + if err != nil { + return nil, fmt.Errorf("inwx: %v", err) + } + + config := NewDefaultConfig() + config.Username = values["INWX_USERNAME"] + config.Password = values["INWX_PASSWORD"] + + return NewDNSProviderConfig(config) +} + +// NewDNSProviderConfig return a DNSProvider instance configured for Dyn DNS +func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { + if config == nil { + return nil, errors.New("inwx: the configuration of the DNS provider is nil") + } + + if config.Username == "" || config.Password == "" { + return nil, fmt.Errorf("inwx: credentials missing") + } + + if config.Sandbox { + log.Infof("inwx: sandbox mode is enabled") + } + + client := goinwx.NewClient(config.Username, config.Password, &goinwx.ClientOptions{Sandbox: config.Sandbox}) + + return &DNSProvider{config: config, client: client}, nil +} + +// Present creates a TXT record using the specified parameters +func (d *DNSProvider) Present(domain, token, keyAuth string) error { + fqdn, value, _ := acme.DNS01Record(domain, keyAuth) + + authZone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers) + if err != nil { + return fmt.Errorf("inwx: %v", err) + } + + err = d.client.Account.Login() + if err != nil { + return fmt.Errorf("inwx: %v", err) + } + + defer func() { + errL := d.client.Account.Logout() + if errL != nil { + log.Infof("inwx: failed to logout: %v", errL) + } + }() + + var request = &goinwx.NameserverRecordRequest{ + Domain: acme.UnFqdn(authZone), + Name: acme.UnFqdn(fqdn), + Type: "TXT", + Content: value, + Ttl: d.config.TTL, + } + + _, err = d.client.Nameservers.CreateRecord(request) + if err != nil { + switch err.(type) { + case *goinwx.ErrorResponse: + if err.(*goinwx.ErrorResponse).Message == "Object exists" { + return nil + } + return fmt.Errorf("inwx: %v", err) + default: + return fmt.Errorf("inwx: %v", err) + } + } + + return nil +} + +// CleanUp removes the TXT record matching the specified parameters +func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { + fqdn, _, _ := acme.DNS01Record(domain, keyAuth) + + authZone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers) + if err != nil { + return fmt.Errorf("inwx: %v", err) + } + + err = d.client.Account.Login() + if err != nil { + return fmt.Errorf("inwx: %v", err) + } + + defer func() { + errL := d.client.Account.Logout() + if errL != nil { + log.Infof("inwx: failed to logout: %v", errL) + } + }() + + response, err := d.client.Nameservers.Info(&goinwx.NameserverInfoRequest{ + Domain: acme.UnFqdn(authZone), + Name: acme.UnFqdn(fqdn), + Type: "TXT", + }) + if err != nil { + return fmt.Errorf("inwx: %v", err) + } + + var lastErr error + for _, record := range response.Records { + err = d.client.Nameservers.DeleteRecord(record.Id) + if err != nil { + lastErr = fmt.Errorf("inwx: %v", err) + } + } + + return lastErr +} + +// Timeout returns the timeout and interval to use when checking for DNS propagation. +// Adjusting here to cope with spikes in propagation times. +func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { + return d.config.PropagationTimeout, d.config.PollingInterval +} diff --git a/vendor/github.com/xenolf/lego/providers/dns/mydnsjp/mydnsjp.go b/vendor/github.com/xenolf/lego/providers/dns/mydnsjp/mydnsjp.go new file mode 100644 index 000000000..05cad5686 --- /dev/null +++ b/vendor/github.com/xenolf/lego/providers/dns/mydnsjp/mydnsjp.go @@ -0,0 +1,140 @@ +// Package mydnsjp implements a DNS provider for solving the DNS-01 +// challenge using MyDNS.jp. +package mydnsjp + +import ( + "errors" + "fmt" + "io/ioutil" + "net/http" + "net/url" + "strings" + "time" + + "github.com/xenolf/lego/acme" + "github.com/xenolf/lego/platform/config/env" +) + +const defaultBaseURL = "https://www.mydns.jp/directedit.html" + +// Config is used to configure the creation of the DNSProvider +type Config struct { + MasterID string + Password string + PropagationTimeout time.Duration + PollingInterval time.Duration + HTTPClient *http.Client +} + +// NewDefaultConfig returns a default configuration for the DNSProvider +func NewDefaultConfig() *Config { + return &Config{ + PropagationTimeout: env.GetOrDefaultSecond("MYDNSJP_PROPAGATION_TIMEOUT", 2*time.Minute), + PollingInterval: env.GetOrDefaultSecond("MYDNSJP_POLLING_INTERVAL", 2*time.Second), + HTTPClient: &http.Client{ + Timeout: env.GetOrDefaultSecond("MYDNSJP_HTTP_TIMEOUT", 30*time.Second), + }, + } +} + +// DNSProvider is an implementation of the acme.ChallengeProvider interface +type DNSProvider struct { + config *Config +} + +// NewDNSProvider returns a DNSProvider instance configured for MyDNS.jp. +// Credentials must be passed in the environment variables: MYDNSJP_MASTER_ID and MYDNSJP_PASSWORD. +func NewDNSProvider() (*DNSProvider, error) { + values, err := env.Get("MYDNSJP_MASTER_ID", "MYDNSJP_PASSWORD") + if err != nil { + return nil, fmt.Errorf("mydnsjp: %v", err) + } + + config := NewDefaultConfig() + config.MasterID = values["MYDNSJP_MASTER_ID"] + config.Password = values["MYDNSJP_PASSWORD"] + + return NewDNSProviderConfig(config) +} + +// NewDNSProviderConfig return a DNSProvider instance configured for MyDNS.jp. +func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { + if config == nil { + return nil, errors.New("mydnsjp: the configuration of the DNS provider is nil") + } + + if config.MasterID == "" || config.Password == "" { + return nil, errors.New("mydnsjp: some credentials information are missing") + } + + return &DNSProvider{config: config}, nil +} + +// Timeout returns the timeout and interval to use when checking for DNS propagation. +// Adjusting here to cope with spikes in propagation times. +func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { + return d.config.PropagationTimeout, d.config.PollingInterval +} + +// Present creates a TXT record to fulfill the dns-01 challenge +func (d *DNSProvider) Present(domain, token, keyAuth string) error { + _, value, _ := acme.DNS01Record(domain, keyAuth) + err := d.doRequest(domain, value, "REGIST") + if err != nil { + return fmt.Errorf("mydnsjp: %v", err) + } + return nil +} + +// CleanUp removes the TXT record matching the specified parameters +func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { + _, value, _ := acme.DNS01Record(domain, keyAuth) + err := d.doRequest(domain, value, "DELETE") + if err != nil { + return fmt.Errorf("mydnsjp: %v", err) + } + return nil +} + +func (d *DNSProvider) doRequest(domain, value string, cmd string) error { + req, err := d.buildRequest(domain, value, cmd) + if err != nil { + return err + } + + resp, err := d.config.HTTPClient.Do(req) + if err != nil { + return fmt.Errorf("error querying API: %v", err) + } + + defer resp.Body.Close() + + if resp.StatusCode >= 400 { + var content []byte + content, err = ioutil.ReadAll(resp.Body) + if err != nil { + return err + } + + return fmt.Errorf("request %s failed [status code %d]: %s", req.URL, resp.StatusCode, string(content)) + } + + return nil +} + +func (d *DNSProvider) buildRequest(domain, value string, cmd string) (*http.Request, error) { + params := url.Values{} + params.Set("CERTBOT_DOMAIN", domain) + params.Set("CERTBOT_VALIDATION", value) + params.Set("EDIT_CMD", cmd) + + req, err := http.NewRequest(http.MethodPost, defaultBaseURL, strings.NewReader(params.Encode())) + if err != nil { + return nil, fmt.Errorf("invalid request: %v", err) + } + + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req.SetBasicAuth(d.config.MasterID, d.config.Password) + + return req, nil +} diff --git a/vendor/github.com/xenolf/lego/providers/dns/selectel/client.go b/vendor/github.com/xenolf/lego/providers/dns/selectel/client.go new file mode 100644 index 000000000..cb02230a0 --- /dev/null +++ b/vendor/github.com/xenolf/lego/providers/dns/selectel/client.go @@ -0,0 +1,211 @@ +package selectel + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" +) + +// Domain represents domain name. +type Domain struct { + ID int `json:"id,omitempty"` + Name string `json:"name,omitempty"` +} + +// Record represents DNS record. +type Record struct { + ID int `json:"id,omitempty"` + Name string `json:"name,omitempty"` + Type string `json:"type,omitempty"` // Record type (SOA, NS, A/AAAA, CNAME, SRV, MX, TXT, SPF) + TTL int `json:"ttl,omitempty"` + Email string `json:"email,omitempty"` // Email of domain's admin (only for SOA records) + Content string `json:"content,omitempty"` // Record content (not for SRV) +} + +// APIError API error message +type APIError struct { + Description string `json:"error"` + Code int `json:"code"` + Field string `json:"field"` +} + +func (a *APIError) Error() string { + return fmt.Sprintf("API error: %d - %s - %s", a.Code, a.Description, a.Field) +} + +// ClientOpts represents options to init client. +type ClientOpts struct { + BaseURL string + Token string + UserAgent string + HTTPClient *http.Client +} + +// Client represents DNS client. +type Client struct { + baseURL string + token string + userAgent string + httpClient *http.Client +} + +// NewClient returns a client instance. +func NewClient(opts ClientOpts) *Client { + if opts.HTTPClient == nil { + opts.HTTPClient = &http.Client{} + } + + return &Client{ + token: opts.Token, + baseURL: opts.BaseURL, + httpClient: opts.HTTPClient, + userAgent: opts.UserAgent, + } +} + +// GetDomainByName gets Domain object by its name. +func (c *Client) GetDomainByName(domainName string) (*Domain, error) { + uri := fmt.Sprintf("/%s", domainName) + req, err := c.newRequest(http.MethodGet, uri, nil) + if err != nil { + return nil, err + } + + domain := &Domain{} + _, err = c.do(req, domain) + if err != nil { + return nil, err + } + + return domain, nil +} + +// AddRecord adds Record for given domain. +func (c *Client) AddRecord(domainID int, body Record) (*Record, error) { + uri := fmt.Sprintf("/%d/records/", domainID) + req, err := c.newRequest(http.MethodPost, uri, body) + if err != nil { + return nil, err + } + + record := &Record{} + _, err = c.do(req, record) + if err != nil { + return nil, err + } + + return record, nil +} + +// ListRecords returns list records for specific domain. +func (c *Client) ListRecords(domainID int) ([]*Record, error) { + uri := fmt.Sprintf("/%d/records/", domainID) + req, err := c.newRequest(http.MethodGet, uri, nil) + if err != nil { + return nil, err + } + + var records []*Record + _, err = c.do(req, &records) + if err != nil { + return nil, err + } + return records, nil +} + +// DeleteRecord deletes specific record. +func (c *Client) DeleteRecord(domainID, recordID int) error { + uri := fmt.Sprintf("/%d/records/%d", domainID, recordID) + req, err := c.newRequest(http.MethodDelete, uri, nil) + if err != nil { + return err + } + + _, err = c.do(req, nil) + return err +} + +func (c *Client) newRequest(method, uri string, body interface{}) (*http.Request, error) { + buf := new(bytes.Buffer) + + if body != nil { + err := json.NewEncoder(buf).Encode(body) + if err != nil { + return nil, fmt.Errorf("failed to encode request body with error: %v", err) + } + } + + req, err := http.NewRequest(method, c.baseURL+uri, buf) + if err != nil { + return nil, fmt.Errorf("failed to create new http request with error: %v", err) + } + + req.Header.Add("X-Token", c.token) + req.Header.Add("Content-Type", "application/json") + req.Header.Add("Accept", "application/json") + + return req, nil +} + +func (c *Client) do(req *http.Request, to interface{}) (*http.Response, error) { + resp, err := c.httpClient.Do(req) + if err != nil { + return nil, fmt.Errorf("request failed with error: %v", err) + } + + err = checkResponse(resp) + if err != nil { + return resp, err + } + + if to != nil { + if err = unmarshalBody(resp, to); err != nil { + return resp, err + } + } + + return resp, nil +} + +func checkResponse(resp *http.Response) error { + if resp.StatusCode >= http.StatusBadRequest && + resp.StatusCode <= http.StatusNetworkAuthenticationRequired { + + if resp.Body == nil { + return fmt.Errorf("request failed with status code %d and empty body", resp.StatusCode) + } + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return err + } + defer resp.Body.Close() + + apiError := APIError{} + err = json.Unmarshal(body, &apiError) + if err != nil { + return fmt.Errorf("request failed with status code %d, response body: %s", resp.StatusCode, string(body)) + } + + return fmt.Errorf("request failed with status code %d: %v", resp.StatusCode, apiError) + } + + return nil +} + +func unmarshalBody(resp *http.Response, to interface{}) error { + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return err + } + defer resp.Body.Close() + + err = json.Unmarshal(body, to) + if err != nil { + return fmt.Errorf("unmarshaling error: %v: %s", err, string(body)) + } + + return nil +} diff --git a/vendor/github.com/xenolf/lego/providers/dns/selectel/selectel.go b/vendor/github.com/xenolf/lego/providers/dns/selectel/selectel.go new file mode 100644 index 000000000..9e48b85dc --- /dev/null +++ b/vendor/github.com/xenolf/lego/providers/dns/selectel/selectel.go @@ -0,0 +1,153 @@ +// Package selectel implements a DNS provider for solving the DNS-01 challenge using Selectel Domains API. +// Selectel Domain API reference: https://kb.selectel.com/23136054.html +// Token: https://my.selectel.ru/profile/apikeys +package selectel + +import ( + "errors" + "fmt" + "net/http" + "time" + + "github.com/xenolf/lego/acme" + "github.com/xenolf/lego/platform/config/env" +) + +const ( + defaultBaseURL = "https://api.selectel.ru/domains/v1" + minTTL = 60 +) + +const ( + envNamespace = "SELECTEL_" + baseURLEnvVar = envNamespace + "BASE_URL" + apiTokenEnvVar = envNamespace + "API_TOKEN" + ttlEnvVar = envNamespace + "TTL" + propagationTimeoutEnvVar = envNamespace + "PROPAGATION_TIMEOUT" + pollingIntervalEnvVar = envNamespace + "POLLING_INTERVAL" + httpTimeoutEnvVar = envNamespace + "HTTP_TIMEOUT" +) + +// Config is used to configure the creation of the DNSProvider. +type Config struct { + BaseURL string + Token string + PropagationTimeout time.Duration + PollingInterval time.Duration + TTL int + HTTPClient *http.Client +} + +// NewDefaultConfig returns a default configuration for the DNSProvider. +func NewDefaultConfig() *Config { + return &Config{ + BaseURL: env.GetOrDefaultString(baseURLEnvVar, defaultBaseURL), + TTL: env.GetOrDefaultInt(ttlEnvVar, minTTL), + PropagationTimeout: env.GetOrDefaultSecond(propagationTimeoutEnvVar, 120*time.Second), + PollingInterval: env.GetOrDefaultSecond(pollingIntervalEnvVar, 2*time.Second), + HTTPClient: &http.Client{ + Timeout: env.GetOrDefaultSecond(httpTimeoutEnvVar, 30*time.Second), + }, + } +} + +// DNSProvider is an implementation of the acme.ChallengeProvider interface. +type DNSProvider struct { + config *Config + client *Client +} + +// NewDNSProvider returns a DNSProvider instance configured for Selectel Domains API. +// API token must be passed in the environment variable SELECTEL_API_TOKEN. +func NewDNSProvider() (*DNSProvider, error) { + values, err := env.Get(apiTokenEnvVar) + if err != nil { + return nil, fmt.Errorf("selectel: %v", err) + } + + config := NewDefaultConfig() + config.Token = values[apiTokenEnvVar] + + return NewDNSProviderConfig(config) +} + +// NewDNSProviderConfig return a DNSProvider instance configured for selectel. +func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { + if config == nil { + return nil, errors.New("selectel: the configuration of the DNS provider is nil") + } + + if config.Token == "" { + return nil, errors.New("selectel: credentials missing") + } + + if config.TTL < minTTL { + return nil, fmt.Errorf("selectel: invalid TTL, TTL (%d) must be greater than %d", config.TTL, minTTL) + } + + client := NewClient(ClientOpts{ + BaseURL: config.BaseURL, + Token: config.Token, + UserAgent: acme.UserAgent, + HTTPClient: config.HTTPClient, + }) + + return &DNSProvider{config: config, client: client}, nil +} + +// Timeout returns the Timeout and interval to use when checking for DNS propagation. +// Adjusting here to cope with spikes in propagation times. +func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { + return d.config.PropagationTimeout, d.config.PollingInterval +} + +// Present creates a TXT record to fulfill DNS-01 challenge. +func (d *DNSProvider) Present(domain, token, keyAuth string) error { + fqdn, value, _ := acme.DNS01Record(domain, keyAuth) + + domainObj, err := d.client.GetDomainByName(domain) + if err != nil { + return fmt.Errorf("selectel: %v", err) + } + + txtRecord := Record{ + Type: "TXT", + TTL: d.config.TTL, + Name: fqdn, + Content: value, + } + _, err = d.client.AddRecord(domainObj.ID, txtRecord) + if err != nil { + return fmt.Errorf("selectel: %v", err) + } + + return nil +} + +// CleanUp removes a TXT record used for DNS-01 challenge. +func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { + fqdn, _, _ := acme.DNS01Record(domain, keyAuth) + + domainObj, err := d.client.GetDomainByName(domain) + if err != nil { + return fmt.Errorf("selectel: %v", err) + } + + records, err := d.client.ListRecords(domainObj.ID) + if err != nil { + return fmt.Errorf("selectel: %v", err) + } + + // Delete records with specific FQDN + var lastErr error + for _, record := range records { + if record.Name == fqdn { + err = d.client.DeleteRecord(domainObj.ID, record.ID) + if err != nil { + lastErr = fmt.Errorf("selectel: %v", err) + } + } + } + + return lastErr +} diff --git a/vendor/github.com/xenolf/lego/providers/dns/transip/transip.go b/vendor/github.com/xenolf/lego/providers/dns/transip/transip.go new file mode 100644 index 000000000..aeb155f8f --- /dev/null +++ b/vendor/github.com/xenolf/lego/providers/dns/transip/transip.go @@ -0,0 +1,150 @@ +// Package transip implements a DNS provider for solving the DNS-01 challenge using TransIP. +package transip + +import ( + "errors" + "fmt" + "strings" + "time" + + "github.com/transip/gotransip" + transipdomain "github.com/transip/gotransip/domain" + "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 { + AccountName string + PrivateKeyPath string + PropagationTimeout time.Duration + PollingInterval time.Duration + TTL int64 +} + +// NewDefaultConfig returns a default configuration for the DNSProvider +func NewDefaultConfig() *Config { + return &Config{ + TTL: int64(env.GetOrDefaultInt("TRANSIP_TTL", 10)), + PropagationTimeout: env.GetOrDefaultSecond("TRANSIP_PROPAGATION_TIMEOUT", 10*time.Minute), + PollingInterval: env.GetOrDefaultSecond("TRANSIP_POLLING_INTERVAL", 10*time.Second), + } +} + +// DNSProvider describes a provider for TransIP +type DNSProvider struct { + config *Config + client gotransip.SOAPClient +} + +// NewDNSProvider returns a DNSProvider instance configured for TransIP. +// Credentials must be passed in the environment variables: +// TRANSIP_ACCOUNTNAME, TRANSIP_PRIVATEKEYPATH. +func NewDNSProvider() (*DNSProvider, error) { + values, err := env.Get("TRANSIP_ACCOUNT_NAME", "TRANSIP_PRIVATE_KEY_PATH") + if err != nil { + return nil, fmt.Errorf("transip: %v", err) + } + + config := NewDefaultConfig() + config.AccountName = values["TRANSIP_ACCOUNT_NAME"] + config.PrivateKeyPath = values["TRANSIP_PRIVATE_KEY_PATH"] + + return NewDNSProviderConfig(config) +} + +// NewDNSProviderConfig return a DNSProvider instance configured for TransIP. +func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { + if config == nil { + return nil, errors.New("transip: the configuration of the DNS provider is nil") + } + + client, err := gotransip.NewSOAPClient(gotransip.ClientConfig{ + AccountName: config.AccountName, + PrivateKeyPath: config.PrivateKeyPath, + }) + if err != nil { + return nil, fmt.Errorf("transip: %v", err) + } + + return &DNSProvider{client: client, config: config}, nil +} + +// Timeout returns the timeout and interval to use when checking for DNS propagation. +// Adjusting here to cope with spikes in propagation times. +func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { + return d.config.PropagationTimeout, d.config.PollingInterval +} + +// Present creates a TXT record to fulfill the dns-01 challenge +func (d *DNSProvider) Present(domain, token, keyAuth string) error { + fqdn, value, _ := acme.DNS01Record(domain, keyAuth) + + authZone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers) + if err != nil { + return err + } + + domainName := acme.UnFqdn(authZone) + + // get the subDomain + subDomain := strings.TrimSuffix(acme.UnFqdn(fqdn), "."+domainName) + + // get all DNS entries + info, err := transipdomain.GetInfo(d.client, domainName) + if err != nil { + return fmt.Errorf("transip: error for %s in Present: %v", domain, err) + } + + // include the new DNS entry + dnsEntries := append(info.DNSEntries, transipdomain.DNSEntry{ + Name: subDomain, + TTL: d.config.TTL, + Type: transipdomain.DNSEntryTypeTXT, + Content: value, + }) + + // set the updated DNS entries + err = transipdomain.SetDNSEntries(d.client, domainName, dnsEntries) + if err != nil { + return fmt.Errorf("transip: %v", err) + } + return nil +} + +// CleanUp removes the TXT record matching the specified parameters +func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { + fqdn, _, _ := acme.DNS01Record(domain, keyAuth) + + authZone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers) + if err != nil { + return err + } + + domainName := acme.UnFqdn(authZone) + + // get the subDomain + subDomain := strings.TrimSuffix(acme.UnFqdn(fqdn), "."+domainName) + + // get all DNS entries + info, err := transipdomain.GetInfo(d.client, domainName) + if err != nil { + return fmt.Errorf("transip: error for %s in CleanUp: %v", fqdn, err) + } + + // loop through the existing entries and remove the specific record + updatedEntries := info.DNSEntries[:0] + for _, e := range info.DNSEntries { + if e.Name != subDomain { + updatedEntries = append(updatedEntries, e) + } + } + + // set the updated DNS entries + err = transipdomain.SetDNSEntries(d.client, domainName, updatedEntries) + if err != nil { + return fmt.Errorf("transip: couldn't get Record ID in CleanUp: %sv", err) + } + + return nil +} diff --git a/vendor/github.com/xenolf/lego/providers/dns/vscale/client.go b/vendor/github.com/xenolf/lego/providers/dns/vscale/client.go new file mode 100644 index 000000000..1f4826f29 --- /dev/null +++ b/vendor/github.com/xenolf/lego/providers/dns/vscale/client.go @@ -0,0 +1,211 @@ +package vscale + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" +) + +// Domain represents domain name. +type Domain struct { + ID int `json:"id,omitempty"` + Name string `json:"name,omitempty"` +} + +// Record represents DNS record. +type Record struct { + ID int `json:"id,omitempty"` + Name string `json:"name,omitempty"` + Type string `json:"type,omitempty"` // Record type (SOA, NS, A/AAAA, CNAME, SRV, MX, TXT, SPF) + TTL int `json:"ttl,omitempty"` + Email string `json:"email,omitempty"` // Email of domain's admin (only for SOA records) + Content string `json:"content,omitempty"` // Record content (not for SRV) +} + +// APIError API error message +type APIError struct { + Description string `json:"error"` + Code int `json:"code"` + Field string `json:"field"` +} + +func (a *APIError) Error() string { + return fmt.Sprintf("API error: %d - %s - %s", a.Code, a.Description, a.Field) +} + +// ClientOpts represents options to init client. +type ClientOpts struct { + BaseURL string + Token string + UserAgent string + HTTPClient *http.Client +} + +// Client represents DNS client. +type Client struct { + baseURL string + token string + userAgent string + httpClient *http.Client +} + +// NewClient returns a client instance. +func NewClient(opts ClientOpts) *Client { + if opts.HTTPClient == nil { + opts.HTTPClient = &http.Client{} + } + + return &Client{ + token: opts.Token, + baseURL: opts.BaseURL, + httpClient: opts.HTTPClient, + userAgent: opts.UserAgent, + } +} + +// GetDomainByName gets Domain object by its name. +func (c *Client) GetDomainByName(domainName string) (*Domain, error) { + uri := fmt.Sprintf("/%s", domainName) + req, err := c.newRequest(http.MethodGet, uri, nil) + if err != nil { + return nil, err + } + + domain := &Domain{} + _, err = c.do(req, domain) + if err != nil { + return nil, err + } + + return domain, nil +} + +// AddRecord adds Record for given domain. +func (c *Client) AddRecord(domainID int, body Record) (*Record, error) { + uri := fmt.Sprintf("/%d/records/", domainID) + req, err := c.newRequest(http.MethodPost, uri, body) + if err != nil { + return nil, err + } + + record := &Record{} + _, err = c.do(req, record) + if err != nil { + return nil, err + } + + return record, nil +} + +// ListRecords returns list records for specific domain. +func (c *Client) ListRecords(domainID int) ([]*Record, error) { + uri := fmt.Sprintf("/%d/records/", domainID) + req, err := c.newRequest(http.MethodGet, uri, nil) + if err != nil { + return nil, err + } + + var records []*Record + _, err = c.do(req, &records) + if err != nil { + return nil, err + } + return records, nil +} + +// DeleteRecord deletes specific record. +func (c *Client) DeleteRecord(domainID, recordID int) error { + uri := fmt.Sprintf("/%d/records/%d", domainID, recordID) + req, err := c.newRequest(http.MethodDelete, uri, nil) + if err != nil { + return err + } + + _, err = c.do(req, nil) + return err +} + +func (c *Client) newRequest(method, uri string, body interface{}) (*http.Request, error) { + buf := new(bytes.Buffer) + + if body != nil { + err := json.NewEncoder(buf).Encode(body) + if err != nil { + return nil, fmt.Errorf("failed to encode request body with error: %v", err) + } + } + + req, err := http.NewRequest(method, c.baseURL+uri, buf) + if err != nil { + return nil, fmt.Errorf("failed to create new http request with error: %v", err) + } + + req.Header.Add("X-Token", c.token) + req.Header.Add("Content-Type", "application/json") + req.Header.Add("Accept", "application/json") + + return req, nil +} + +func (c *Client) do(req *http.Request, to interface{}) (*http.Response, error) { + resp, err := c.httpClient.Do(req) + if err != nil { + return nil, fmt.Errorf("request failed with error: %v", err) + } + + err = checkResponse(resp) + if err != nil { + return resp, err + } + + if to != nil { + if err = unmarshalBody(resp, to); err != nil { + return resp, err + } + } + + return resp, nil +} + +func checkResponse(resp *http.Response) error { + if resp.StatusCode >= http.StatusBadRequest && + resp.StatusCode <= http.StatusNetworkAuthenticationRequired { + + if resp.Body == nil { + return fmt.Errorf("request failed with status code %d and empty body", resp.StatusCode) + } + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return err + } + defer resp.Body.Close() + + apiError := APIError{} + err = json.Unmarshal(body, &apiError) + if err != nil { + return fmt.Errorf("request failed with status code %d, response body: %s", resp.StatusCode, string(body)) + } + + return fmt.Errorf("request failed with status code %d: %v", resp.StatusCode, apiError) + } + + return nil +} + +func unmarshalBody(resp *http.Response, to interface{}) error { + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return err + } + defer resp.Body.Close() + + err = json.Unmarshal(body, to) + if err != nil { + return fmt.Errorf("unmarshaling error: %v: %s", err, string(body)) + } + + return nil +} diff --git a/vendor/github.com/xenolf/lego/providers/dns/vscale/vscale.go b/vendor/github.com/xenolf/lego/providers/dns/vscale/vscale.go new file mode 100644 index 000000000..b44b159d9 --- /dev/null +++ b/vendor/github.com/xenolf/lego/providers/dns/vscale/vscale.go @@ -0,0 +1,153 @@ +// Package selectel implements a DNS provider for solving the DNS-01 challenge using Vscale Domains API. +// Vscale Domain API reference: https://developers.vscale.io/documentation/api/v1/#api-Domains +// Token: https://vscale.io/panel/settings/tokens/ +package vscale + +import ( + "errors" + "fmt" + "net/http" + "time" + + "github.com/xenolf/lego/acme" + "github.com/xenolf/lego/platform/config/env" +) + +const ( + defaultBaseURL = "https://api.vscale.io/v1/domains" + minTTL = 60 +) + +const ( + envNamespace = "VSCALE_" + baseURLEnvVar = envNamespace + "BASE_URL" + apiTokenEnvVar = envNamespace + "API_TOKEN" + ttlEnvVar = envNamespace + "TTL" + propagationTimeoutEnvVar = envNamespace + "PROPAGATION_TIMEOUT" + pollingIntervalEnvVar = envNamespace + "POLLING_INTERVAL" + httpTimeoutEnvVar = envNamespace + "HTTP_TIMEOUT" +) + +// Config is used to configure the creation of the DNSProvider. +type Config struct { + BaseURL string + Token string + PropagationTimeout time.Duration + PollingInterval time.Duration + TTL int + HTTPClient *http.Client +} + +// NewDefaultConfig returns a default configuration for the DNSProvider. +func NewDefaultConfig() *Config { + return &Config{ + BaseURL: env.GetOrDefaultString(baseURLEnvVar, defaultBaseURL), + TTL: env.GetOrDefaultInt(ttlEnvVar, minTTL), + PropagationTimeout: env.GetOrDefaultSecond(propagationTimeoutEnvVar, 120*time.Second), + PollingInterval: env.GetOrDefaultSecond(pollingIntervalEnvVar, 2*time.Second), + HTTPClient: &http.Client{ + Timeout: env.GetOrDefaultSecond(httpTimeoutEnvVar, 30*time.Second), + }, + } +} + +// DNSProvider is an implementation of the acme.ChallengeProvider interface. +type DNSProvider struct { + config *Config + client *Client +} + +// NewDNSProvider returns a DNSProvider instance configured for Vscale Domains API. +// API token must be passed in the environment variable VSCALE_API_TOKEN. +func NewDNSProvider() (*DNSProvider, error) { + values, err := env.Get(apiTokenEnvVar) + if err != nil { + return nil, fmt.Errorf("vscale: %v", err) + } + + config := NewDefaultConfig() + config.Token = values[apiTokenEnvVar] + + return NewDNSProviderConfig(config) +} + +// NewDNSProviderConfig return a DNSProvider instance configured for Vscale. +func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { + if config == nil { + return nil, errors.New("vscale: the configuration of the DNS provider is nil") + } + + if config.Token == "" { + return nil, errors.New("vscale: credentials missing") + } + + if config.TTL < minTTL { + return nil, fmt.Errorf("vscale: invalid TTL, TTL (%d) must be greater than %d", config.TTL, minTTL) + } + + client := NewClient(ClientOpts{ + BaseURL: config.BaseURL, + Token: config.Token, + UserAgent: acme.UserAgent, + HTTPClient: config.HTTPClient, + }) + + return &DNSProvider{config: config, client: client}, nil +} + +// Timeout returns the Timeout and interval to use when checking for DNS propagation. +// Adjusting here to cope with spikes in propagation times. +func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { + return d.config.PropagationTimeout, d.config.PollingInterval +} + +// Present creates a TXT record to fulfill DNS-01 challenge. +func (d *DNSProvider) Present(domain, token, keyAuth string) error { + fqdn, value, _ := acme.DNS01Record(domain, keyAuth) + + domainObj, err := d.client.GetDomainByName(domain) + if err != nil { + return fmt.Errorf("vscale: %v", err) + } + + txtRecord := Record{ + Type: "TXT", + TTL: d.config.TTL, + Name: fqdn, + Content: value, + } + _, err = d.client.AddRecord(domainObj.ID, txtRecord) + if err != nil { + return fmt.Errorf("vscale: %v", err) + } + + return nil +} + +// CleanUp removes a TXT record used for DNS-01 challenge. +func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { + fqdn, _, _ := acme.DNS01Record(domain, keyAuth) + + domainObj, err := d.client.GetDomainByName(domain) + if err != nil { + return fmt.Errorf("vscale: %v", err) + } + + records, err := d.client.ListRecords(domainObj.ID) + if err != nil { + return fmt.Errorf("vscale: %v", err) + } + + // Delete records with specific FQDN + var lastErr error + for _, record := range records { + if record.Name == fqdn { + err = d.client.DeleteRecord(domainObj.ID, record.ID) + if err != nil { + lastErr = fmt.Errorf("vscale: %v", err) + } + } + } + + return lastErr +}