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

new provider module HEXONET (#373)

This commit is contained in:
Kai Schwarz
2018-08-30 14:54:42 +02:00
committed by Craig Peterson
parent 402fc449e2
commit 3e5d223675
26 changed files with 1927 additions and 0 deletions

1
OWNERS
View File

@ -5,6 +5,7 @@ providers/digitalocean @Deraen
providers/dnsimple @aeden
providers/gandi @TomOnTime
# providers/gcloud
providers/hexonet @papakai
providers/linode @koesie10
providers/namecheap @captncraig
# providers/namedotcom

View File

@ -20,6 +20,7 @@ Currently supported DNS providers:
- DNSimple
- Gandi
- Google
- HEXONET
- Linode
- Namecheap
- Name.com

View File

@ -14,6 +14,7 @@
<th class="rotate"><div><span>GANDI</span></div></th>
<th class="rotate"><div><span>GANDI-LIVEDNS</span></div></th>
<th class="rotate"><div><span>GCLOUD</span></div></th>
<th class="rotate"><div><span>HEXONET</span></div></th>
<th class="rotate"><div><span>LINODE</span></div></th>
<th class="rotate"><div><span>NAMECHEAP</span></div></th>
<th class="rotate"><div><span>NAMEDOTCOM</span></div></th>
@ -53,6 +54,9 @@
<td class="success">
<i class="fa fa-check text-success" aria-hidden="true"></i>
</td>
<td class="danger" data-toggle="tooltip" data-container="body" data-placement="top" title="Actively maintained provider module.">
<i class="fa has-tooltip fa-times text-danger" aria-hidden="true"></i>
</td>
<td class="danger">
<i class="fa fa-times text-danger" aria-hidden="true"></i>
</td>
@ -125,6 +129,9 @@
<td class="success">
<i class="fa fa-check text-success" aria-hidden="true"></i>
</td>
<td class="success">
<i class="fa fa-check text-success" aria-hidden="true"></i>
</td>
<td class="danger">
<i class="fa fa-times text-danger" aria-hidden="true"></i>
</td>
@ -167,6 +174,9 @@
<td class="danger">
<i class="fa fa-times text-danger" aria-hidden="true"></i>
</td>
<td class="success">
<i class="fa fa-check text-success" aria-hidden="true"></i>
</td>
<td class="danger">
<i class="fa fa-times text-danger" aria-hidden="true"></i>
</td>
@ -214,6 +224,9 @@
<td><i class="fa fa-minus dim"></i></td>
<td><i class="fa fa-minus dim"></i></td>
<td><i class="fa fa-minus dim"></i></td>
<td class="danger" data-toggle="tooltip" data-container="body" data-placement="top" title="Using ALIAS is possible through our extended DNS (X-DNS) service. Feel free to get in touch with us.">
<i class="fa has-tooltip fa-times text-danger" aria-hidden="true"></i>
</td>
<td><i class="fa fa-minus dim"></i></td>
<td class="danger">
<i class="fa fa-times text-danger" aria-hidden="true"></i>
@ -259,6 +272,9 @@
<td class="success">
<i class="fa fa-check text-success" aria-hidden="true"></i>
</td>
<td class="success">
<i class="fa fa-check text-success" aria-hidden="true"></i>
</td>
<td><i class="fa fa-minus dim"></i></td>
<td class="danger">
<i class="fa fa-times text-danger" aria-hidden="true"></i>
@ -300,6 +316,9 @@
<td class="success">
<i class="fa fa-check text-success" aria-hidden="true"></i>
</td>
<td class="success">
<i class="fa fa-check text-success" aria-hidden="true"></i>
</td>
<td><i class="fa fa-minus dim"></i></td>
<td class="danger">
<i class="fa fa-times text-danger" aria-hidden="true"></i>
@ -349,6 +368,9 @@
<td class="success">
<i class="fa fa-check text-success" aria-hidden="true"></i>
</td>
<td class="success">
<i class="fa fa-check text-success" aria-hidden="true"></i>
</td>
<td><i class="fa fa-minus dim"></i></td>
<td class="danger" data-toggle="tooltip" data-container="body" data-placement="top" title="The namecheap web console allows you to make SRV records, but their api does not let you read or set them">
<i class="fa has-tooltip fa-times text-danger" aria-hidden="true"></i>
@ -390,6 +412,9 @@
<td><i class="fa fa-minus dim"></i></td>
<td><i class="fa fa-minus dim"></i></td>
<td><i class="fa fa-minus dim"></i></td>
<td class="success">
<i class="fa fa-check text-success" aria-hidden="true"></i>
</td>
<td><i class="fa fa-minus dim"></i></td>
<td class="danger">
<i class="fa fa-times text-danger" aria-hidden="true"></i>
@ -419,6 +444,9 @@
<td><i class="fa fa-minus dim"></i></td>
<td><i class="fa fa-minus dim"></i></td>
<td><i class="fa fa-minus dim"></i></td>
<td class="success">
<i class="fa fa-check text-success" aria-hidden="true"></i>
</td>
<td><i class="fa fa-minus dim"></i></td>
<td><i class="fa fa-minus dim"></i></td>
<td class="success">
@ -444,6 +472,9 @@
<td><i class="fa fa-minus dim"></i></td>
<td><i class="fa fa-minus dim"></i></td>
<td><i class="fa fa-minus dim"></i></td>
<td class="danger" data-toggle="tooltip" data-container="body" data-placement="top" title="Using ALIAS is possible through our extended DNS (X-DNS) service. Feel free to get in touch with us.">
<i class="fa has-tooltip fa-times text-danger" aria-hidden="true"></i>
</td>
<td><i class="fa fa-minus dim"></i></td>
<td><i class="fa fa-minus dim"></i></td>
<td><i class="fa fa-minus dim"></i></td>
@ -477,6 +508,9 @@
<td class="success">
<i class="fa fa-check text-success" aria-hidden="true"></i>
</td>
<td class="success">
<i class="fa fa-check text-success" aria-hidden="true"></i>
</td>
<td class="danger">
<i class="fa fa-times text-danger" aria-hidden="true"></i>
</td>
@ -528,6 +562,9 @@
<td class="success">
<i class="fa fa-check text-success" aria-hidden="true"></i>
</td>
<td class="success">
<i class="fa fa-check text-success" aria-hidden="true"></i>
</td>
<td class="danger">
<i class="fa fa-times text-danger" aria-hidden="true"></i>
</td>
@ -588,6 +625,9 @@
<td class="success">
<i class="fa fa-check text-success" aria-hidden="true"></i>
</td>
<td class="success">
<i class="fa fa-check text-success" aria-hidden="true"></i>
</td>
<td class="danger">
<i class="fa fa-times text-danger" aria-hidden="true"></i>
</td>

View File

@ -0,0 +1,92 @@
---
name: HEXONET
title: HEXONET Provider
layout: default
jsId: HEXONET
---
# HEXONET Provider
HEXONET is a leading developer and operator of domain names and DNS platforms.
Individual, service provider and registrars around the globe choose HEXONET for
domains and DNS because of our advanced technology, operational performance and
up-time, and most importantly for DNS expertise. DnsControl with HEXONET's DNS
marries DNS automation with an industry-leading DNS platform that supports DNSSEC,
PremiumDNS via Anycast Network, and nearly all of DnsControl's listed provider features.
## Configuration
Please provide your HEXONET login data in your credentials file `creds.json` as follows:
{% highlight json %}
{
"hexonet": {
"apilogin": "your-hexonet-account-id",
"apipassword": "your-hexonet-account-password",
"apientity": "LIVE", // for the LIVE system; use "OTE" for the OT&E system
"ipaddress": "172.31.3.16", // provide here your outgoing ip address
"debugmode": "0", // set it to "1" to get debug output of the communication with our Backend System API
}
}
{% endhighlight %}
Here a working example for our OT&E System:
{% highlight json %}
{
"hexonet": {
"apilogin": "test.user",
"apipassword": "test.passw0rd",
"apientity": "OTE",
"debugmode": "0",
}
}
{% endhighlight %}
## Usage
Here an example DNS Configuration `dnsconfig.js` using our provider module.
Even though it shows how you use us as Domain Registrar AND DNS Provider, we don't force you to do that.
You are free to decide if you want to use both of our provider technology or just one of them.
{% highlight javascript %}
// Providers:
var REG_HX = NewRegistrar('hexonet', 'HEXONET');
var DNS_HX = NewDnsProvider('hexonet', 'HEXONET');
// Set Default TTL for all RR to reflect our Backend API Default
// If you use additional DNS Providers, configure a default TTL
// per domain using the domain modifyer DefaultTTL instead.
// also check this issue for [NAMESERVER TTL](https://github.com/StackExchange/dnscontrol/issues/176).
DEFAULTS(
{"ns_ttl":"3600"},
DefaultTTL(3600)
);
// Domains:
D('abhoster.com', REG_HX, DnsProvider(DNS_HX),
NAMESERVER('ns1.ispapi.net'),
NAMESERVER('ns2.ispapi.net'),
NAMESERVER('ns3.ispapi.net'),
NAMESERVER('ns4.ispapi.net'),
A('elk1', '10.190.234.178'),
A('test', '56.123.54.12')
);
{% endhighlight %}
## Metadata
This provider does not recognize any special metadata fields unique to HEXONET.
## New domains
If a dnszone does not exist in your HEXONET account, DNSControl will *not* automatically add it with the `dnscontrol push` or `dnscontrol preview` command. You'll need to do that via the control panel manually or using the command `dnscontrol create-domains`.
This is because it could lead to unwanted costs on customer-side that we want to avoid.
## Debug Mode
As shown in the configuration examples above, this can be activated on demand and it can be used to check the API commands send to our system.
In general this is thought for our purpose to have an easy way to dive into issues. But if you're interested what's going on, feel free to activate it.
## IP Filter
In case you have ip filter settings made for you HEXONET account, please provide your outgoing ip address as shown in the configuration examples above.

View File

@ -42,6 +42,14 @@
"private_key": "$GCLOUD_PRIVATEKEY",
"project_id": "$GCLOUD_PROJECT"
},
"HEXONET": {
"apilogin": "$HEXONET_UID",
"apipassword": "$HEXONET_PW",
"apientity": "$HEXONET_ENTITY",
"debugmode": "$HEXONET_DEBUGMODE",
"ipaddress": "$HEXONET_IP",
"domain": "dnscontrol.com"
},
"LINODE": {
"COMMENT": "25: Linode's hostname validation does not allow the target domain TLD",
"token": "$LINODE_TOKEN",

View File

@ -10,6 +10,7 @@ import (
_ "github.com/StackExchange/dnscontrol/providers/dnsimple"
_ "github.com/StackExchange/dnscontrol/providers/gandi"
_ "github.com/StackExchange/dnscontrol/providers/gcloud"
_ "github.com/StackExchange/dnscontrol/providers/hexonet"
_ "github.com/StackExchange/dnscontrol/providers/linode"
_ "github.com/StackExchange/dnscontrol/providers/namecheap"
_ "github.com/StackExchange/dnscontrol/providers/namedotcom"

View File

@ -0,0 +1,26 @@
package hexonet
//EnsureDomainExists returns an error
// * if access to dnszone is not allowed (not authorized) or
// * if it doesn't exist and creating it fails
func (n *HXClient) EnsureDomainExists(domain string) error {
r := n.client.Request(map[string]string{
"COMMAND": "StatusDNSZone",
"DNSZONE": domain + ".",
})
code := r.Code()
if code == 545 {
r = n.client.Request(map[string]string{
"COMMAND": "CreateDNSZone",
"DNSZONE": domain + ".",
})
if !r.IsSuccess() {
return n.GetHXApiError("Failed to create not existing zone for domain", domain, r)
}
} else if code == 531 {
return n.GetHXApiError("Not authorized to manage dnszone", domain, r)
} else if r.IsError() || r.IsError() {
return n.GetHXApiError("Error while checking status of dnszone", domain, r)
}
return nil
}

View File

@ -0,0 +1,11 @@
package hexonet
import (
lr "github.com/hexonet/go-sdk/response/listresponse"
"github.com/pkg/errors"
)
// GetHXApiError returns an error including API error code and error description.
func (n *HXClient) GetHXApiError(format string, objectid string, r *lr.ListResponse) error {
return errors.Errorf(format+" %s. [%s %s]", objectid, r.Code(), r.Description())
}

View File

@ -0,0 +1,69 @@
// Package hexonet implements a registrar that uses the hexonet api to set name servers. It will self register it's providers when imported.
package hexonet
import (
"encoding/json"
"fmt"
"github.com/StackExchange/dnscontrol/providers"
hxcl "github.com/hexonet/go-sdk/client"
)
// HXClient describes a connection to the hexonet API.
type HXClient struct {
APILogin string
APIPassword string
APIEntity string
client *hxcl.Client
}
var features = providers.DocumentationNotes{
providers.CanUseAlias: providers.Cannot("Using ALIAS is possible through our extended DNS (X-DNS) service. Feel free to get in touch with us."),
providers.CanUseCAA: providers.Can(),
providers.CanUsePTR: providers.Can(),
providers.CanUseRoute53Alias: providers.Cannot("Using ALIAS is possible through our extended DNS (X-DNS) service. Feel free to get in touch with us."),
providers.CanUseSRV: providers.Can(),
providers.CanUseTLSA: providers.Can(),
providers.CanUseTXTMulti: providers.Can(),
providers.CantUseNOPURGE: providers.Can(),
providers.DocCreateDomains: providers.Can(),
providers.DocDualHost: providers.Can(),
providers.DocOfficiallySupported: providers.Cannot("Actively maintained provider module."),
}
func newProvider(conf map[string]string) (*HXClient, error) {
api := &HXClient{
client: hxcl.NewClient(),
}
api.APILogin, api.APIPassword, api.APIEntity = conf["apilogin"], conf["apipassword"], conf["apientity"]
if conf["debugmode"] == "1" {
api.client.EnableDebugMode()
}
if len(conf["ipaddress"]) > 0 {
api.client.SetIPAddress(conf["ipaddress"])
}
if api.APIEntity != "OTE" && api.APIEntity != "LIVE" {
return nil, fmt.Errorf("wrong api system entity used. use \"OTE\" for OT&E system or \"LIVE\" for Live system")
}
if api.APIEntity == "OTE" {
api.client.UseOTESystem()
}
if api.APILogin == "" || api.APIPassword == "" {
return nil, fmt.Errorf("missing login credentials apilogin or apipassword")
}
api.client.SetCredentials(api.APILogin, api.APIPassword, "")
return api, nil
}
func newReg(conf map[string]string) (providers.Registrar, error) {
return newProvider(conf)
}
func newDsp(conf map[string]string, meta json.RawMessage) (providers.DNSServiceProvider, error) {
return newProvider(conf)
}
func init() {
providers.RegisterRegistrarType("HEXONET", newReg)
providers.RegisterDomainServiceProviderType("HEXONET", newDsp, features)
}

View File

@ -0,0 +1,100 @@
package hexonet
import (
"fmt"
"regexp"
"sort"
"strings"
"github.com/StackExchange/dnscontrol/models"
)
var defaultNameservers = []*models.Nameserver{
{Name: "ns1.ispapi.net"},
{Name: "ns2.ispapi.net"},
{Name: "ns3.ispapi.net"},
}
var nsRegex = regexp.MustCompile(`ns([1-3]{1})[0-9]+\.ispapi\.net`)
// GetNameservers gets the nameservers set on a domain.
func (n *HXClient) GetNameservers(domain string) ([]*models.Nameserver, error) {
// This is an interesting edge case. hexonet expects you to SET the nameservers to ns[1-3].ispapi.net,
// but it will internally set it to (ns1xyz|ns2uvw|ns3asd).ispapi.net, where xyz/uvw/asd is a uniqueish number.
// In order to avoid endless loops, we will use the unique nameservers if present, or else the generic ones if not.
nss, err := n.getNameserversRaw(domain)
if err != nil {
return nil, err
}
toUse := []string{
defaultNameservers[0].Name,
defaultNameservers[1].Name,
defaultNameservers[2].Name,
}
for _, ns := range nss {
if matches := nsRegex.FindStringSubmatch(ns); len(matches) == 2 && len(matches[1]) == 1 {
idx := matches[1][0] - '1' // regex ensures proper range
toUse[idx] = matches[0]
}
}
return models.StringsToNameservers(toUse), nil
}
func (n *HXClient) getNameserversRaw(domain string) ([]string, error) {
r := n.client.Request(map[string]string{
"COMMAND": "StatusDomain",
"DOMAIN": domain,
})
code := r.Code()
if code != 200 {
return nil, n.GetHXApiError("Could not get status for domain", domain, r)
}
ns := r.GetColumn("NAMESERVER")
sort.Strings(ns)
return ns, nil
}
// GetRegistrarCorrections gathers corrections that would being n to match dc.
func (n *HXClient) GetRegistrarCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
nss, err := n.getNameserversRaw(dc.Name)
if err != nil {
return nil, err
}
foundNameservers := strings.Join(nss, ",")
expected := []string{}
for _, ns := range dc.Nameservers {
name := strings.TrimRight(ns.Name, ".")
expected = append(expected, name)
}
sort.Strings(expected)
expectedNameservers := strings.Join(expected, ",")
if foundNameservers != expectedNameservers {
return []*models.Correction{
{
Msg: fmt.Sprintf("Update nameservers %s -> %s", foundNameservers, expectedNameservers),
F: n.updateNameservers(expected, dc.Name),
},
}, nil
}
return nil, nil
}
func (n *HXClient) updateNameservers(ns []string, domain string) func() error {
return func() error {
cmd := map[string]string{
"COMMAND": "ModifyDomain",
"DOMAIN": domain,
}
for idx, ns := range ns {
cmd[fmt.Sprintf("NAMESERVER%d", idx)] = ns
}
response := n.client.Request(cmd)
code := response.Code()
if code != 200 {
return fmt.Errorf(fmt.Sprintf("%d %s", code, response.Description()))
}
return nil
}
}

View File

@ -0,0 +1,277 @@
package hexonet
import (
"bytes"
"encoding/json"
"fmt"
"regexp"
"strconv"
"strings"
"github.com/pkg/errors"
"github.com/StackExchange/dnscontrol/models"
"github.com/StackExchange/dnscontrol/providers/diff"
)
// HXRecord covers an individual DNS resource record.
type HXRecord struct {
// Raw api value of that RR
Raw string
// DomainName is the zone that the record belongs to.
DomainName string
// Host is the hostname relative to the zone: e.g. for a record for blog.example.org, domain would be "example.org" and host would be "blog".
// An apex record would be specified by either an empty host "" or "@".
// A SRV record would be specified by "_{service}._{protocal}.{host}": e.g. "_sip._tcp.phone" for _sip._tcp.phone.example.org.
Host string
// FQDN is the Fully Qualified Domain Name. It is the combination of the host and the domain name. It always ends in a ".". FQDN is ignored in CreateRecord, specify via the Host field instead.
Fqdn string
// Type is one of the following: A, AAAA, ANAME, CNAME, MX, NS, SRV, or TXT.
Type string
// Answer is either the IP address for A or AAAA records; the target for ANAME, CNAME, MX, or NS records; the text for TXT records.
// For SRV records, answer has the following format: "{weight} {port} {target}" e.g. "1 5061 sip.example.org".
Answer string
// TTL is the time this record can be cached for in seconds.
TTL uint32
// Priority is only required for MX and SRV records, it is ignored for all others.
Priority uint32
}
// GetDomainCorrections gathers correctios that would bring n to match dc.
func (n *HXClient) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
dc.Punycode()
records, err := n.getRecords(dc.Name)
if err != nil {
return nil, err
}
actual := make([]*models.RecordConfig, len(records))
for i, r := range records {
actual[i] = toRecord(r, dc.Name)
}
for _, rec := range dc.Records {
if rec.Type == "ALIAS" {
return nil, fmt.Errorf("We support realtime ALIAS RR over our X-DNS service, please get in touch with us")
}
}
//checkNSModifications(dc)
// Normalize
models.PostProcessRecords(actual)
differ := diff.New(dc)
_, create, del, mod := differ.IncrementalDiff(actual)
corrections := []*models.Correction{}
buf := &bytes.Buffer{}
// Print a list of changes. Generate an actual change that is the zone
changes := false
params := map[string]string{}
delrridx := 0
addrridx := 0
for _, cre := range create {
changes = true
fmt.Fprintln(buf, cre)
rec := cre.Desired
params[fmt.Sprintf("ADDRR%d", addrridx)] = n.createRecordString(rec, dc.Name)
addrridx++
}
for _, d := range del {
changes = true
fmt.Fprintln(buf, d)
rec := d.Existing.Original.(*HXRecord)
params[fmt.Sprintf("DELRR%d", delrridx)] = n.deleteRecordString(rec, dc.Name)
delrridx++
}
for _, chng := range mod {
changes = true
fmt.Fprintln(buf, chng)
old := chng.Existing.Original.(*HXRecord)
new := chng.Desired
params[fmt.Sprintf("DELRR%d", delrridx)] = n.deleteRecordString(old, dc.Name)
params[fmt.Sprintf("ADDRR%d", addrridx)] = n.createRecordString(new, dc.Name)
addrridx++
delrridx++
}
msg := fmt.Sprintf("GENERATE_ZONEFILE: %s\n", dc.Name) + buf.String()
if changes {
corrections = append(corrections, &models.Correction{
Msg: msg,
F: func() error {
return n.updateZoneBy(params, dc.Name)
},
})
}
return corrections, nil
}
func toRecord(r *HXRecord, origin string) *models.RecordConfig {
rc := &models.RecordConfig{
Type: r.Type,
TTL: r.TTL,
Original: r,
}
fqdn := r.Fqdn[:len(r.Fqdn)-1]
rc.SetLabelFromFQDN(fqdn, origin)
switch rtype := r.Type; rtype {
case "TXT":
rc.SetTargetTXTs(decodeTxt(r.Answer))
case "MX":
if err := rc.SetTargetMX(uint16(r.Priority), r.Answer); err != nil {
panic(errors.Wrap(err, "unparsable MX record received from hexonet api"))
}
case "SRV":
if err := rc.SetTargetSRVPriorityString(uint16(r.Priority), r.Answer); err != nil {
panic(errors.Wrap(err, "unparsable SRV record received from hexonet api"))
}
default: // "A", "AAAA", "ANAME", "CNAME", "NS"
if err := rc.PopulateFromString(rtype, r.Answer, r.Fqdn); err != nil {
panic(errors.Wrap(err, "unparsable record received from hexonet api"))
}
}
return rc
}
func (n *HXClient) showCommand(cmd map[string]string) {
b, err := json.MarshalIndent(cmd, "", " ")
if err != nil {
fmt.Println("error:", err)
}
fmt.Print(string(b))
}
func (n *HXClient) updateZoneBy(params map[string]string, domain string) error {
zone := domain + "."
cmd := map[string]string{
"COMMAND": "UpdateDNSZone",
"DNSZONE": zone,
"INCSERIAL": "1",
}
for key, val := range params {
cmd[key] = val
}
// n.showCommand(cmd)
r := n.client.Request(cmd)
if !r.IsSuccess() {
return n.GetHXApiError("Error while updating zone", zone, r)
}
return nil
}
func (n *HXClient) getRecords(domain string) ([]*HXRecord, error) {
var records []*HXRecord
zone := domain + "."
cmd := map[string]string{
"COMMAND": "QueryDNSZoneRRList",
"DNSZONE": zone,
"SHORT": "1",
"EXTENDED": "0",
}
r := n.client.Request(cmd)
if !r.IsSuccess() {
if r.Code() == 545 {
return nil, n.GetHXApiError("Use `dnscontrol create-domains` to create not-existing zone", domain, r)
}
return nil, n.GetHXApiError("Failed loading resource records for zone", domain, r)
}
rrs := r.GetColumn("RR")
for _, rr := range rrs {
spl := strings.Split(rr, " ")
if spl[3] != "SOA" {
record := &HXRecord{
Raw: rr,
DomainName: domain,
Host: spl[0],
Fqdn: domain + ".",
Type: spl[3],
}
ttl, _ := strconv.ParseUint(spl[1], 10, 32)
record.TTL = uint32(ttl)
if record.Host != "@" {
record.Fqdn = spl[0] + "." + record.Fqdn
}
if record.Type == "MX" || record.Type == "SRV" {
prio, _ := strconv.ParseUint(spl[4], 10, 32)
record.Priority = uint32(prio)
record.Answer = strings.Join(spl[5:], " ")
} else {
record.Answer = strings.Join(spl[4:], " ")
}
records = append(records, record)
}
}
return records, nil
}
func (n *HXClient) createRecordString(rc *models.RecordConfig, domain string) string {
record := &HXRecord{
DomainName: domain,
Host: rc.GetLabel(),
Type: rc.Type,
Answer: rc.GetTargetField(),
TTL: rc.TTL,
Priority: uint32(rc.MxPreference),
}
switch rc.Type { // #rtype_variations
case "A", "AAAA", "ANAME", "CNAME", "MX", "NS", "PTR":
// nothing
case "TLSA":
record.Answer = fmt.Sprintf(`%v %v %v %s`, rc.TlsaUsage, rc.TlsaSelector, rc.TlsaMatchingType, rc.Target)
case "CAA":
record.Answer = fmt.Sprintf(`%v %s "%s"`, rc.CaaFlag, rc.CaaTag, record.Answer)
case "TXT":
record.Answer = encodeTxt(rc.TxtStrings)
case "SRV":
record.Answer = fmt.Sprintf("%d %d %v", rc.SrvWeight, rc.SrvPort, record.Answer)
record.Priority = uint32(rc.SrvPriority)
default:
panic(fmt.Sprintf("createRecord rtype %v unimplemented", rc.Type))
// We panic so that we quickly find any switch statements
// that have not been updated for a new RR type.
}
str := record.Host + " " + fmt.Sprint(record.TTL) + " IN " + record.Type + " "
if record.Type == "MX" || record.Type == "SRV" {
str += fmt.Sprint(record.Priority) + " "
}
str += record.Answer
return str
}
func (n *HXClient) deleteRecordString(record *HXRecord, domain string) string {
return record.Raw
}
// encodeTxt encodes TxtStrings for sending in the CREATE/MODIFY API:
func encodeTxt(txts []string) string {
ans := txts[0]
if len(txts) > 1 {
ans = ""
for _, t := range txts {
ans += `"` + strings.Replace(t, `"`, `\"`, -1) + `"`
}
}
return ans
}
// finds a string surrounded by quotes that might contain an escaped quote character.
var quotedStringRegexp = regexp.MustCompile(`"((?:[^"\\]|\\.)*)"`)
// decodeTxt decodes the TXT record as received from hexonet api and
// returns the list of strings.
func decodeTxt(s string) []string {
if len(s) >= 2 && s[0] == '"' && s[len(s)-1] == '"' {
txtStrings := []string{}
for _, t := range quotedStringRegexp.FindAllStringSubmatch(s, -1) {
txtString := strings.Replace(t[1], `\"`, `"`, -1)
txtStrings = append(txtStrings, txtString)
}
return txtStrings
}
return []string{s}
}

View File

@ -0,0 +1,51 @@
package hexonet
import (
"strings"
"testing"
)
var txtData = []struct {
decoded []string
encoded string
}{
{[]string{`simple`}, `simple`},
{[]string{`changed`}, `changed`},
{[]string{`with spaces`}, `with spaces`},
{[]string{`with whitespace`}, `with whitespace`},
{[]string{"one", "two"}, `"one""two"`},
{[]string{"eh", "bee", "cee"}, `"eh""bee""cee"`},
{[]string{"o\"ne", "tw\"o"}, `"o\"ne""tw\"o"`},
{[]string{"dimple"}, `dimple`},
{[]string{"fun", "two"}, `"fun""two"`},
{[]string{"eh", "bzz", "cee"}, `"eh""bzz""cee"`},
}
func TestEncodeTxt(t *testing.T) {
// Test encoded the lists of strings into a string:
for i, test := range txtData {
enc := encodeTxt(test.decoded)
if enc != test.encoded {
t.Errorf("%v: txt\n data: []string{%v}\nexpected: %s\n got: %s",
i, "`"+strings.Join(test.decoded, "`, `")+"`", test.encoded, enc)
}
}
}
func TestDecodeTxt(t *testing.T) {
// Test decoded a string into the list of strings:
for i, test := range txtData {
data := test.encoded
got := decodeTxt(data)
wanted := test.decoded
if len(got) != len(wanted) {
t.Errorf("%v: txt\n decode: %v\nexpected: `%v`\n got: `%v`\n", i, data, strings.Join(wanted, "`, `"), strings.Join(got, "`, `"))
} else {
for j := range got {
if got[j] != wanted[j] {
t.Errorf("%v: txt\n decode: %v\nexpected: `%v`\n got: `%v`\n", i, data, strings.Join(wanted, "`, `"), strings.Join(got, "`, `"))
}
}
}
}
}

85
vendor/github.com/hexonet/go-sdk/CONTRIBUTING.md generated vendored Normal file
View File

@ -0,0 +1,85 @@
# Contributing
When contributing to this repository, please first discuss the change you wish to make via issue,
email, or any other method with the owners of this repository before making a change.
Please note we have a code of conduct, please follow it in all your interactions with the project.
## Pull Request Process
Read [here](https://github.com/hexonet/go-sdk/wiki/Development-Guide#pull-request-pr-procedure).
## Code of Conduct
### Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, gender identity and expression, level of experience,
nationality, personal appearance, race, religion, or sexual identity and
orientation.
### Our Standards
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
### Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
### Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
### Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at [INSERT EMAIL ADDRESS]. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
### Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at [http://contributor-covenant.org/version/1/4][version]
[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/

48
vendor/github.com/hexonet/go-sdk/HISTORY.md generated vendored Normal file
View File

@ -0,0 +1,48 @@
### Changelog
All notable changes to this project will be documented in this file. Dates are displayed in UTC.
#### [v1.2.0](https://github.com/hexonet/go-sdk/compare/v1.1.0...v1.2.0) (2 August 2018)
- added debugMode; possibility to enable debug output [`a46d0cf`](https://github.com/hexonet/go-sdk/commit/a46d0cfefaafec10bfaac536be7cbe365d837eaf)
- updated changelog [`3ae8d48`](https://github.com/hexonet/go-sdk/commit/3ae8d48d1dbf989d714ff206f84dac8c7858a98b)
- HM-329 change changelog template [`689b56e`](https://github.com/hexonet/go-sdk/commit/689b56ee2a9cf53604e7c835879fb635999da94c)
- Add missing -tree option to govendor fetch command [`b6b0108`](https://github.com/hexonet/go-sdk/commit/b6b01080ead0752d5f27e8d0e6a7215250cbd13a)
- Update README.md [`203d8a0`](https://github.com/hexonet/go-sdk/commit/203d8a0abdc88ab5a06aeae8a3a70c9f9489f381)
#### [v1.1.0](https://github.com/hexonet/go-sdk/compare/v1.0.1...v1.1.0) (4 July 2018)
- updated tests; added scripts; changelog [`9415d4e`](https://github.com/hexonet/go-sdk/commit/9415d4e8f9e0db44fc252b43dbac5e021d988943)
- Update README.md [`285345c`](https://github.com/hexonet/go-sdk/commit/285345cd882f4a1417995930b200fe6d3dbac8f0)
- Update README.md [`25345f8`](https://github.com/hexonet/go-sdk/commit/25345f80c9d3b71273a28493f6839796cecaa727)
- Update CONTRIBUTING.md [`cc1cbb2`](https://github.com/hexonet/go-sdk/commit/cc1cbb2e16a8740fa0236b63a1474d9161b04cd2)
- Update README.md [`731bf85`](https://github.com/hexonet/go-sdk/commit/731bf8584526c7be4962894e29b1e616f3d830a0)
- Update README.md [`132895a`](https://github.com/hexonet/go-sdk/commit/132895aa28ba90a42ad62a91a8c2855d6d372999)
- Update README.md [`d74cfc5`](https://github.com/hexonet/go-sdk/commit/d74cfc584c746719566a07ae63bab0f7a7da0d68)
- Update README.md [`e007900`](https://github.com/hexonet/go-sdk/commit/e007900b3e5c1907ba0682343c9f2253f7742b03)
- Update README.md [`06806ef`](https://github.com/hexonet/go-sdk/commit/06806ef58699fac27869c7ab70d2a69fa4a79f00)
- Update README.md [`31f2df2`](https://github.com/hexonet/go-sdk/commit/31f2df2cc9071176f3a9c231ed5715a9e1a31823)
- Update CONTRIBUTING.md [`59bdfe9`](https://github.com/hexonet/go-sdk/commit/59bdfe989caa5b6788f2e92d3188a866f6ae0917)
- Update README.md [`792cb7f`](https://github.com/hexonet/go-sdk/commit/792cb7fb6f91583f1fcbfdc2ed8b5295caf5a572)
#### [v1.0.1](https://github.com/hexonet/go-sdk/compare/v1.0.0...v1.0.1) (28 June 2018)
- readme: added some standard go badges [`9d12d6f`](https://github.com/hexonet/go-sdk/commit/9d12d6fda2053258188aa0edefc4b75e458ab159)
- updated versioning info in readme [`607de6f`](https://github.com/hexonet/go-sdk/commit/607de6f787df016f42216e38dcee5a22f4042093)
- fix misspelling [`a8a817c`](https://github.com/hexonet/go-sdk/commit/a8a817c729ad118b9598165bb95cf81a919e8124)
- fix slack badge [`9cc090b`](https://github.com/hexonet/go-sdk/commit/9cc090b845a7c482ebe095f31cee73e1cd6a491e)
- Update README.md [`efe73fb`](https://github.com/hexonet/go-sdk/commit/efe73fb57039d0d101a96c5171b862493b3a3adf)
#### v1.0.0 (20 June 2018)
- updated readme [`1078244`](https://github.com/hexonet/go-sdk/commit/107824423f630da8978afacbf5cac5c1c31331e4)
- updated readme [`2d49a83`](https://github.com/hexonet/go-sdk/commit/2d49a83bf9275b2bbd6a42b2aee1b06b0878b230)
- updated readme [`5215f7a`](https://github.com/hexonet/go-sdk/commit/5215f7a1f6e1e195f692ea9ecffe444f23a47627)
- updated imports [`c9a184c`](https://github.com/hexonet/go-sdk/commit/c9a184cf865de416e747ac637a0b9d99ea60b0e7)
- updated imports [`be9f573`](https://github.com/hexonet/go-sdk/commit/be9f57375d5777075f2268e23d70eb47650f1bd1)
- updated readme [`358ab17`](https://github.com/hexonet/go-sdk/commit/358ab17bdb082a9f3b4c40d37fc07a1c4105ee45)
- updated readme [`0d98aa2`](https://github.com/hexonet/go-sdk/commit/0d98aa26a89b69d9a4a0a29942e25b6de5979f4a)
- update readme [`c5f13d8`](https://github.com/hexonet/go-sdk/commit/c5f13d8db64cbf3af6f753cec5d0b9359b439a69)
- updated readme [`3861e83`](https://github.com/hexonet/go-sdk/commit/3861e83ca2edb12b18e79a601f310c5168b90c08)
- initial release [`205e1b4`](https://github.com/hexonet/go-sdk/commit/205e1b4db0404d69567970bd9793a255e5470136)
- Initial commit [`26ff843`](https://github.com/hexonet/go-sdk/commit/26ff843470f09df434dc4b5d1ee32d2e9115858a)

21
vendor/github.com/hexonet/go-sdk/LICENSE generated vendored Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2018 HEXONET
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.

128
vendor/github.com/hexonet/go-sdk/README.md generated vendored Normal file
View File

@ -0,0 +1,128 @@
# go-sdk
[![GoDoc](https://godoc.org/github.com/hexonet/go-sdk?status.svg)](https://godoc.org/github.com/hexonet/go-sdk)
[![Go Report Card](https://goreportcard.com/badge/github.com/hexonet/go-sdk)](https://goreportcard.com/report/github.com/hexonet/go-sdk)
[![cover.run](https://cover.run/go/github.com/hexonet/go-sdk.svg?style=flat&tag=golang-1.10)](https://cover.run/go?tag=golang-1.10&repo=github.com%2Fhexonet%2Fgo-sdk)
[![Slack Widget](https://camo.githubusercontent.com/984828c0b020357921853f59eaaa65aaee755542/68747470733a2f2f73332e65752d63656e7472616c2d312e616d617a6f6e6177732e636f6d2f6e6774756e612f6a6f696e2d75732d6f6e2d736c61636b2e706e67)](https://hexonet-sdk.slack.com/messages/CBFHLTL2X)
This module is a connector library for the insanely fast HEXONET Backend API. For further informations visit our [homepage](http://hexonet.net) and do not hesitate to [contact us](https://www.hexonet.net/contact).
## Resources
* [Usage Guide](https://github.com/hexonet/go-sdk/blob/master/README.md#how-to-use-this-module-in-your-project)
* [SDK Documenation](https://godoc.org/github.com/hexonet/go-sdk)
* [HEXONET Backend API Documentation](https://github.com/hexonet/hexonet-api-documentation/tree/master/API)
* [Release Notes](https://github.com/hexonet/go-sdk/releases)
* [Development Guide](https://github.com/hexonet/go-sdk/wiki/Development-Guide)
## How to use this module in your project
We have also a demo app available showing how to integrate and use our SDK. See [here](https://github.com/hexonet/go-sdk-demo).
### Requirements
* Installed [GO/GOLANG](https://golang.org/doc/install). Restart your machine after installing GO.
* Installed [govendor](https://github.com/kardianos/govendor).
NOTE: Make sure you add the go binary path to your PATH environment variable. Add the below lines for a standard installation into your profile configuration file (~/.profile).
```bash
export GOPATH=$HOME/go
export PATH=$PATH:$GOPATH/bin
```
Then reload the profile configuration by `source ~/.profile`.
### Using govendor
Use [govendor](https://github.com/kardianos/govendor) for the dependency installation by `govendor fetch -tree github.com/hexonet/go-sdk@<tag id>` where *tag id* corresponds to a [release version tag](https://github.com/hexonet/go-sdk/releases). You can update this dependency later on by `govendor sync github.com/hexonet/go-sdk@<new tag id>`. The dependencies will be installed in your project's subfolder "vendor". Import the module in your project as shown in the examples below.
For more details on govendor, please read the [CheatSheet](https://github.com/kardianos/govendor/wiki/Govendor-CheatSheet) and also the [developer guide](https://github.com/kardianos/govendor/blob/master/doc/dev-guide.md).
### Usage Examples
Please have an eye on our [HEXONET Backend API documentation](https://github.com/hexonet/hexonet-api-documentation/tree/master/API). Here you can find information on available Commands and their response data.
#### Session based API Communication
```go
package main
import (
"github.com/hexonet/go-sdk/client"
"fmt"
)
func main() {
cl := client.NewClient()
cl.SetCredentials("test.user", "test.passw0rd", "")//username, password, otp code (2FA)
cl.UseOTESystem()
// use this to provide your outgoing ip address for api communication
// to be used in case you have ip filter settings active
// cl.SetIPAddress("174.21.132.16");
// cl.EnableDebugMode() // to activate debug outputs of the API communication
r := cl.Login()
if r.IsSuccess() {
fmt.Println("Login succeeded.")
cmd := map[string]string{
"COMMAND": "StatusAccount",
}
r = cl.Request(cmd)
if r.IsSuccess() {
fmt.Println("Command succeeded.")
r = cl.Logout()
if r.IsSuccess() {
fmt.Println("Logout succeeded.")
} else {
fmt.Println("Logout failed.")
}
} else {
fmt.Println("Command failed.")
}
} else {
fmt.Println("Login failed.")
}
}
```
#### Sessionless API Communication
```go
package main
import (
"github.com/hexonet/go-sdk/client"
"fmt"
)
func main() {
cl := client.NewClient()
cl.SetCredentials("test.user", "test.passw0rd", "")
cl.UseOTESystem()
cmd := map[string]string{
"COMMAND": "StatusAccount",
}
r := cl.Request(cmd)
if r.IsSuccess() {
fmt.Println("Command succeeded.")
} else {
fmt.Println("Command failed.")
}
}
```
## Contributing
Please read [our development guide](https://github.com/hexonet/go-sdk/wiki/Development-Guide) for details on our code of conduct, and the process for submitting pull requests to us.
## Authors
* **Kai Schwarz** - *lead development* - [PapaKai](https://github.com/papakai)
See also the list of [contributors](https://github.com/hexonet/go-sdk/graphs/contributors) who participated in this project.
## License
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.

9
vendor/github.com/hexonet/go-sdk/apiconnector.go generated vendored Normal file
View File

@ -0,0 +1,9 @@
// Copyright (c) 2018 Kai Schwarz (1API GmbH). All rights reserved.
//
// Use of this source code is governed by the MIT
// license that can be found in the LICENSE.md file.
package main
func main() {
}

242
vendor/github.com/hexonet/go-sdk/client/client.go generated vendored Normal file
View File

@ -0,0 +1,242 @@
// Copyright (c) 2018 Kai Schwarz (1API GmbH). All rights reserved.
//
// Use of this source code is governed by the MIT
// license that can be found in the LICENSE.md file.
// Package client contains all you need to communicate with the insanely fast 1API backend API.
package client
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"regexp"
"strconv"
"strings"
"github.com/hexonet/go-sdk/client/socketcfg"
"github.com/hexonet/go-sdk/response/hashresponse"
"github.com/hexonet/go-sdk/response/listresponse"
)
// Client is the entry point class for communicating with the insanely fast 1API backend api.
// It allows two ways of communication:
// * session based communication
// * sessionless communication
//
// A session based communication makes sense in case you use it to
// build your own frontend on top. It allows also to use 2FA
// (2 Factor Auth) by providing "otp" in the config parameter of
// the login method.
// A sessionless communication makes sense in case you do not need
// to care about the above and you have just to request some commands.
//
// Possible commands can be found at https://github.com/hexonet/hexonet-api-documentation/tree/master/API
type Client struct {
debugMode bool
socketTimeout int
apiurl string
socketcfg.Socketcfg
}
// NewClient represents the constructor for struct Client.
// The client is by default set to communicate with the LIVE system. Use method UseOTESystem to switch to the OT&E system instance.
func NewClient() *Client {
cl := &Client{
debugMode: false,
socketTimeout: 300000,
apiurl: "https://coreapi.1api.net/api/call.cgi",
Socketcfg: socketcfg.Socketcfg{},
}
cl.UseLiveSystem()
return cl
}
// EncodeData method to use to encode provided data (socket configuration and api command) before sending it to the API server
// It returns the encoded data ready to use within POST request of type "application/x-www-form-urlencoded"
func (c *Client) EncodeData(cfg *socketcfg.Socketcfg, cmd map[string]string) string {
var tmp, data strings.Builder
tmp.WriteString(cfg.EncodeData())
tmp.WriteString(url.QueryEscape("s_command"))
tmp.WriteString("=")
for k, v := range cmd {
re := regexp.MustCompile(`\r?\n`)
v = re.ReplaceAllString(v, "")
if len(v) > 0 {
data.WriteString(k)
data.WriteString("=")
data.WriteString(v)
data.WriteString("\n")
}
}
tmp.WriteString(url.QueryEscape(data.String()))
return tmp.String()
}
// Getapiurl is the getter method for apiurl property
func (c *Client) Getapiurl() string {
return c.apiurl
}
// Setapiurl is the setter method for apiurl
func (c *Client) Setapiurl(url string) {
c.apiurl = url
}
// SetCredentials method to set username and password and otp code to use for api communication
// set otp code to empty string, if you do not use 2FA
func (c *Client) SetCredentials(username string, password string, otpcode string) {
c.Socketcfg.SetCredentials(username, password, otpcode)
}
// SetIPAddress method to set api client to submit this ip address in api communication
func (c *Client) SetIPAddress(ip string) {
c.Socketcfg.SetIPAddress(ip)
}
// SetSubuserView method to activate the use of a subuser account as data view
func (c *Client) SetSubuserView(username string) {
c.Socketcfg.SetUser(username)
}
// ResetSubuserView method to deactivate the use of a subuser account as data view
func (c *Client) ResetSubuserView() {
c.Socketcfg.SetUser("")
}
// UseLiveSystem method to set api client to communicate with the LIVE backend API
func (c *Client) UseLiveSystem() {
c.Socketcfg.SetEntity("54cd")
}
// UseOTESystem method to set api client to communicate with the OT&E backend API
func (c *Client) UseOTESystem() {
c.Socketcfg.SetEntity("1234")
}
// EnableDebugMode method to enable debugMode for debug output
func (c *Client) EnableDebugMode() {
c.debugMode = true
}
// DisableDebugMode method to disable debugMode for debug output
func (c *Client) DisableDebugMode() {
c.debugMode = false
}
// Request method requests the given command to the api server and returns the response as ListResponse.
func (c *Client) Request(cmd map[string]string) *listresponse.ListResponse {
if c.Socketcfg == (socketcfg.Socketcfg{}) {
return listresponse.NewListResponse(hashresponse.NewTemplates().Get("expired"))
}
return c.dorequest(cmd, &c.Socketcfg)
}
// debugRequest method used to trigger debug output in case debugMode is activated
func (c *Client) debugRequest(cmd map[string]string, data string, r *listresponse.ListResponse) {
if c.debugMode {
j, _ := json.Marshal(cmd)
fmt.Printf("%s\n", j)
fmt.Println("POST: " + data)
fmt.Println(strconv.Itoa(r.Code()) + " " + r.Description() + "\n")
}
}
// RequestAll method requests ALL entries matching the request criteria by the given command from api server.
// So useful for client-side lists. Finally it returns the response as ListResponse.
func (c *Client) RequestAll(cmd map[string]string) *listresponse.ListResponse {
if c.Socketcfg == (socketcfg.Socketcfg{}) {
return listresponse.NewListResponse(hashresponse.NewTemplates().Get("expired"))
}
cmd["LIMIT"] = "1"
cmd["FIRST"] = "0"
r := c.dorequest(cmd, &c.Socketcfg)
if r.IsSuccess() {
cmd["LIMIT"] = strconv.Itoa(r.Total())
cmd["FIRST"] = "0"
r = c.dorequest(cmd, &c.Socketcfg)
}
return r
}
// request the given command to the api server by using the provided socket configuration and return the response as ListResponse.
func (c *Client) dorequest(cmd map[string]string, cfg *socketcfg.Socketcfg) *listresponse.ListResponse {
data := c.EncodeData(cfg, cmd)
client := &http.Client{}
req, err := http.NewRequest("POST", c.apiurl, strings.NewReader(data))
if err != nil {
tpl := hashresponse.NewTemplates().Get("commonerror")
tpl = strings.Replace(tpl, "####ERRMSG####", err.Error(), 1)
r := listresponse.NewListResponse(tpl)
c.debugRequest(cmd, data, r)
return r
}
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
req.Header.Add("Expect", "")
resp, err2 := client.Do(req)
if err2 != nil {
tpl := hashresponse.NewTemplates().Get("commonerror")
tpl = strings.Replace(tpl, "####ERRMSG####", err2.Error(), 1)
r := listresponse.NewListResponse(tpl)
c.debugRequest(cmd, data, r)
return r
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusOK {
response, err := ioutil.ReadAll(resp.Body)
if err != nil {
tpl := hashresponse.NewTemplates().Get("commonerror")
tpl = strings.Replace(tpl, "####ERRMSG####", err.Error(), 1)
r := listresponse.NewListResponse(tpl)
c.debugRequest(cmd, data, r)
return r
}
r := listresponse.NewListResponse(string(response))
c.debugRequest(cmd, data, r)
return r
}
tpl := hashresponse.NewTemplates().Get("commonerror")
tpl = strings.Replace(tpl, "####ERRMSG####", string(resp.StatusCode)+resp.Status, 1)
r := listresponse.NewListResponse(tpl)
c.debugRequest(cmd, data, r)
return r
}
// Login method to use as entry point for session based communication.
// Response is returned as ListResponse.
func (c *Client) Login() *listresponse.ListResponse {
return c.dologin(map[string]string{"COMMAND": "StartSession"})
}
// LoginExtended method to use as entry point for session based communication.
// This method allows to provide further command parameters for startsession command.
// Response is returned as ListResponse.
func (c *Client) LoginExtended(cmdparams map[string]string) *listresponse.ListResponse {
cmd := map[string]string{"COMMAND": "StartSession"}
for k, v := range cmdparams {
cmd[k] = v
}
return c.dologin(cmd)
}
// dologin method used internally to perform a login using the given command.
// Response is returned as ListResponse.
func (c *Client) dologin(cmd map[string]string) *listresponse.ListResponse {
r := c.dorequest(cmd, &c.Socketcfg)
if r.Code() == 200 {
sessid, _ := r.GetColumnIndex("SESSION", 0)
c.Socketcfg.SetSession(sessid)
}
return r
}
// Logout method to use for session based communication.
// This method logs you out and destroys the api session.
// Response is returned as ListResponse.
func (c *Client) Logout() *listresponse.ListResponse {
cmd := map[string]string{"COMMAND": "EndSession"}
return c.dorequest(cmd, &c.Socketcfg)
}

View File

@ -0,0 +1,106 @@
// Copyright (c) 2018 Kai Schwarz (1API GmbH). All rights reserved.
//
// Use of this source code is governed by the MIT
// license that can be found in the LICENSE.md file.
// Package socketcfg provides apiconnector client connection settings
package socketcfg
import (
"net/url"
"strings"
)
// Socketcfg is a struct representing connection settings used as POST data for http request against the insanely fast 1API backend API.
type Socketcfg struct {
login string
pw string
remoteaddr string
entity string
session string
user string
otp string
}
// SetIPAddress method to set remote ip address to be submitted to the HEXONET API.
// This ip address is being considered when you have ip filter settings activated.
// To reset this, simply provide an empty string as parameter.
func (s *Socketcfg) SetIPAddress(ip string) {
s.remoteaddr = ip
}
// SetCredentials method to set username and password to use for api communication
func (s *Socketcfg) SetCredentials(username string, password string, otpcode string) {
s.login = username
s.pw = password
s.otp = otpcode
}
// SetEntity method to set the system entity id used to communicate with
// "1234" -> OT&E system, "54cd" -> LIVE system
func (s *Socketcfg) SetEntity(entityid string) {
s.entity = entityid
}
// SetSession method to set a API session id to use for api communication instead of credentials
// which is basically required in case you plan to use session based communication or if you want to use 2FA
func (s *Socketcfg) SetSession(sessionid string) {
s.login = ""
s.pw = ""
s.otp = ""
s.session = sessionid
}
// SetUser method to set an user account (must be subuser account of your login user) to use for API communication
// use this if you want to make changes on that subuser account or if you want to have his data view
func (s *Socketcfg) SetUser(username string) {
s.user = username
}
// EncodeData method to return the struct data ready to submit within POST request of type "application/x-www-form-urlencoded"
func (s *Socketcfg) EncodeData() string {
var tmp strings.Builder
if len(s.login) > 0 {
tmp.WriteString(url.QueryEscape("s_login"))
tmp.WriteString("=")
tmp.WriteString(url.QueryEscape(s.login))
tmp.WriteString("&")
}
if len(s.pw) > 0 {
tmp.WriteString(url.QueryEscape("s_pw"))
tmp.WriteString("=")
tmp.WriteString(url.QueryEscape(s.pw))
tmp.WriteString("&")
}
if len(s.remoteaddr) > 0 {
tmp.WriteString(url.QueryEscape("s_remoteaddr"))
tmp.WriteString("=")
tmp.WriteString(url.QueryEscape(s.remoteaddr))
tmp.WriteString("&")
}
if len(s.entity) > 0 {
tmp.WriteString(url.QueryEscape("s_entity"))
tmp.WriteString("=")
tmp.WriteString(url.QueryEscape(s.entity))
tmp.WriteString("&")
}
if len(s.session) > 0 {
tmp.WriteString(url.QueryEscape("s_session"))
tmp.WriteString("=")
tmp.WriteString(url.QueryEscape(s.session))
tmp.WriteString("&")
}
if len(s.user) > 0 {
tmp.WriteString(url.QueryEscape("s_user"))
tmp.WriteString("=")
tmp.WriteString(url.QueryEscape(s.user))
tmp.WriteString("&")
}
if len(s.otp) > 0 {
tmp.WriteString(url.QueryEscape("s_otp"))
tmp.WriteString("=")
tmp.WriteString(url.QueryEscape(s.otp))
tmp.WriteString("&")
}
return tmp.String()
}

View File

@ -0,0 +1,366 @@
// Copyright (c) 2018 Kai Schwarz (1API GmbH). All rights reserved.
//
// Use of this source code is governed by the MIT
// license that can be found in the LICENSE.md file.
// Package hashresponse covers all functionality to handle an API response in hash format and provides access to a response template manager
// to cover http error cases etc. with API response format.
package hashresponse
import (
"errors"
"fmt"
"math"
"regexp"
"strconv"
"strings"
)
// HashResponse class provides basic functionality to work with API responses.
type HashResponse struct {
// represents the parsed API response data
hash map[string]interface{}
// represents the raw API response data
raw string
// represents the pattern to match columns used for pagination
pagerRegexp regexp.Regexp
// represents the column filter pattern
columnFilterRegexp regexp.Regexp
// represents an flag to turn column filter on/off
columnFilterActive bool
}
// NewHashResponse represents the constructor for struct HashResponse.
// Provide the raw api response string as parameter.
func NewHashResponse(r string) *HashResponse {
res := r
if len(res) == 0 {
res = NewTemplates().Get("empty")
}
hr := &HashResponse{
raw: res,
columnFilterActive: false,
pagerRegexp: *regexp.MustCompile("^(TOTAL|FIRST|LAST|LIMIT|COUNT)$"),
}
hr.hash = hr.Parse(hr.raw)
return hr
}
// GetRaw method to return the api raw (but filtered - in case of useColRegexp) response data
func (hr *HashResponse) GetRaw() string {
return hr.GetRawByFilter(false)
}
// GetRawByFilter method to return the api raw response data.
// Use noColumnFilter parameter to explicitly suppress a current active column filter.
func (hr *HashResponse) GetRawByFilter(noColumnFilter bool) string {
if noColumnFilter || !hr.columnFilterActive {
return hr.raw
}
return hr.Serialize(hr.GetHash())
}
// GetHash method to return the parsed api response
func (hr *HashResponse) GetHash() map[string]interface{} {
if hr.columnFilterActive {
var h = make(map[string]interface{})
for k, v := range hr.hash {
h[k] = v
}
properties := hr.hash["PROPERTY"]
if properties != nil {
d := make(map[string][]string)
for k, v := range properties.(map[string][]string) {
if hr.columnFilterRegexp.MatchString(k) {
d[k] = v
}
}
h["PROPERTY"] = d
}
return h
}
return hr.hash
}
// DisableColumnFilter method to turn of column filter
func (hr *HashResponse) DisableColumnFilter() {
hr.columnFilterActive = false
// hr.columnFilterRegexp = nil
}
// EnableColumnFilter method to set a column filter
func (hr *HashResponse) EnableColumnFilter(pattern string) {
hr.columnFilterActive = true
hr.columnFilterRegexp = *regexp.MustCompile(pattern)
}
// Code method to access the api response code
func (hr *HashResponse) Code() int {
var x int
fmt.Sscanf(hr.hash["CODE"].(string), "%d", &x)
return x
}
// Description method to access the api response description
func (hr *HashResponse) Description() string {
return hr.hash["DESCRIPTION"].(string)
}
// Runtime method to access the api response runtime
func (hr *HashResponse) Runtime() float64 {
s, _ := strconv.ParseFloat(hr.hash["RUNTIME"].(string), 64)
return s
}
// Queuetime method to access the api response queuetime
func (hr *HashResponse) Queuetime() float64 {
s, _ := strconv.ParseFloat(hr.hash["QUEUETIME"].(string), 64)
return s
}
// First method to access the pagination data "first".
// Represents the row index of 1st row of the current response of the whole result set
func (hr *HashResponse) First() int {
val, _ := hr.GetColumnIndex("FIRST", 0)
if len(val) == 0 {
return 0
}
var x int
fmt.Sscanf(val, "%d", &x)
return x
}
// Count method to access the pagination data "count"
// Represents the count of rows returned in the current response
func (hr *HashResponse) Count() int {
val, _ := hr.GetColumnIndex("COUNT", 0)
if len(val) != 0 {
var x int
fmt.Sscanf(val, "%d", &x)
return x
}
c := 0
max := 0
cols := hr.GetColumnKeys()
for _, el := range cols {
col := hr.GetColumn(el)
c = len(col)
if c > max {
max = c
}
}
return c
}
// Last method to access the pagination data "last"
// Represents the row index of last row of the current response of the whole result set
func (hr *HashResponse) Last() int {
val, _ := hr.GetColumnIndex("LAST", 0)
if len(val) == 0 {
return hr.Count() - 1
}
var x int
fmt.Sscanf(val, "%d", &x)
return x
}
// Limit method to access the pagination data "limit"
// represents the limited amount of rows requested to be returned
func (hr *HashResponse) Limit() int {
val, _ := hr.GetColumnIndex("LIMIT", 0)
if len(val) == 0 {
return hr.Count()
}
var x int
fmt.Sscanf(val, "%d", &x)
return x
}
// Total method to access the pagination data "total"
// represents the total amount of rows available in the whole result set
func (hr *HashResponse) Total() int {
val, _ := hr.GetColumnIndex("TOTAL", 0)
if len(val) == 0 {
return hr.Count()
}
var x int
fmt.Sscanf(val, "%d", &x)
return x
}
// Pages method to return the amount of pages of the current result set
func (hr *HashResponse) Pages() int {
t := hr.Total()
if t > 0 {
return int(math.Ceil(float64(t) / float64(hr.Limit())))
}
return 1
}
// Page method to return the number of the current page
func (hr *HashResponse) Page() int {
if hr.Count() > 0 {
// limit cannot be 0 as this.count() will cover this, no worries
d := float64(hr.First()) / float64(hr.Limit())
return int(math.Floor(d)) + 1
}
return 1
}
// Prevpage method to get the previous page number
func (hr *HashResponse) Prevpage() int {
p := hr.Page() - 1
if p > 0 {
return p
}
return 1
}
// Nextpage method to get the next page number
func (hr *HashResponse) Nextpage() int {
p := hr.Page() + 1
pages := hr.Pages()
if p <= pages {
return p
}
return pages
}
// GetPagination method to return all pagination data at once
func (hr *HashResponse) GetPagination() map[string]int {
pagination := make(map[string]int)
pagination["FIRST"] = hr.First()
pagination["LAST"] = hr.Last()
pagination["COUNT"] = hr.Count()
pagination["TOTAL"] = hr.Total()
pagination["LIMIT"] = hr.Limit()
pagination["PAGES"] = hr.Pages()
pagination["PAGE"] = hr.Page()
pagination["PAGENEXT"] = hr.Nextpage()
pagination["PAGEPREV"] = hr.Prevpage()
return pagination
}
// IsSuccess method to check if the api response represents a success case
func (hr *HashResponse) IsSuccess() bool {
code := hr.Code()
return (code >= 200 && code < 300)
}
// IsTmpError method to check if the api response represents a temporary error case
func (hr *HashResponse) IsTmpError() bool {
code := hr.Code()
return (code >= 400 && code < 500)
}
// IsError method to check if the api response represents an error case
func (hr *HashResponse) IsError() bool {
code := hr.Code()
return (code >= 500 && code <= 600)
}
// GetColumnKeys method to get a full list available columns in api response
func (hr *HashResponse) GetColumnKeys() []string {
var columns []string
if hr.hash == nil {
return columns
}
property := hr.hash["PROPERTY"]
if property == nil {
return columns
}
for k := range property.(map[string][]string) {
if !hr.pagerRegexp.MatchString(k) {
columns = append(columns, k)
}
}
return columns
}
// GetColumn method to get the full column data for the given column id
func (hr *HashResponse) GetColumn(columnid string) []string {
if hr.hash == nil || hr.hash["PROPERTY"] == nil {
return nil
}
return hr.hash["PROPERTY"].(map[string][]string)[columnid]
}
// GetColumnIndex method to get a response data field by column id and index
func (hr *HashResponse) GetColumnIndex(columnid string, index int) (string, error) {
if hr.hash == nil || hr.hash["PROPERTY"] == nil {
return "", errors.New("column not found")
}
column := hr.hash["PROPERTY"].(map[string][]string)[columnid]
if column == nil || len(column) <= index {
return "", errors.New("index not found")
}
return column[index], nil
}
// Serialize method to stringify a parsed api response
func (hr *HashResponse) Serialize(hash map[string]interface{}) string {
var plain strings.Builder
plain.WriteString("[RESPONSE]")
for k := range hash {
if strings.Compare(k, "PROPERTY") == 0 {
for k2, v2 := range hash[k].(map[string][]string) {
for i, v3 := range v2 {
plain.WriteString("\r\nPROPERTY[")
plain.WriteString(k2)
plain.WriteString("][")
plain.WriteString(fmt.Sprintf("%d", i))
plain.WriteString("]=")
plain.WriteString(v3)
}
}
} else {
tmp := hash[k].(string)
if len(tmp) > 0 {
plain.WriteString("\r\n")
plain.WriteString(k)
plain.WriteString("=")
plain.WriteString(tmp)
}
}
}
plain.WriteString("\r\nEOF\r\n")
return plain.String()
}
// Parse method to parse the given raw api response
func (hr *HashResponse) Parse(r string) map[string]interface{} {
hash := make(map[string]interface{})
tmp := strings.Split(strings.Replace(r, "\r", "", -1), "\n")
p1 := regexp.MustCompile("^([^\\=]*[^\\t\\= ])[\\t ]*=[\\t ]*(.*)$")
p2 := regexp.MustCompile("(?i)^property\\[([^\\]]*)\\]\\[([0-9]+)\\]")
properties := make(map[string][]string)
for _, row := range tmp {
m := p1.MatchString(row)
if m {
groups := p1.FindStringSubmatch(row)
property := strings.ToUpper(groups[1])
mm := p2.MatchString(property)
if mm {
groups2 := p2.FindStringSubmatch(property)
key := strings.Replace(strings.ToUpper(groups2[1]), "\\s", "", -1)
// idx2 := strconv.Atoi(groups2[2])
list := make([]string, len(properties[key]))
copy(list, properties[key])
pat := regexp.MustCompile("[\\t ]*$")
rep1 := "${1}$2"
list = append(list, pat.ReplaceAllString(groups[2], rep1))
properties[key] = list
} else {
val := groups[2]
if len(val) > 0 {
pat := regexp.MustCompile("[\\t ]*$")
hash[property] = pat.ReplaceAllString(val, "")
}
}
}
}
if len(properties) > 0 {
hash["PROPERTY"] = properties
}
return hash
}

View File

@ -0,0 +1,74 @@
// Copyright (c) 2018 Kai Schwarz (1API GmbH). All rights reserved.
//
// Use of this source code is governed by the MIT
// license that can be found in the LICENSE.md file.
package hashresponse
import (
"strings"
)
// Templates class manages default api response templates to be used for different reasons.
// It also provides functionality to compare a response against a template.
//
// Basically used to provide custom response templates that are used in error cases to have a useful way to responds to the client.
type Templates struct {
// represents the template container
templates map[string]string
}
// NewTemplates represents the constructor for struct Templates.
func NewTemplates() *Templates {
tpls := make(map[string]string)
tpls["empty"] = "[RESPONSE]\r\ncode=423\r\ndescription=Empty API response\r\nEOF\r\n"
tpls["error"] = "[RESPONSE]\r\ncode=421\r\ndescription=Command failed due to server error. Client should try again\r\nEOF\r\n"
tpls["expired"] = "[RESPONSE]\r\ncode=530\r\ndescription=SESSION NOT FOUND\r\nEOF\r\n"
tpls["commonerror"] = "[RESPONSE]\r\nDESCRIPTION=Command failed;####ERRMSG####;\r\nCODE=500\r\nQUEUETIME=0\r\nRUNTIME=0\r\nEOF"
return &Templates{
templates: tpls,
}
}
// GetAll method to get all available response templates
func (dr *Templates) GetAll() map[string]string {
return dr.templates
}
// GetParsed method to get a parsed response template by given template id.
func (dr *Templates) GetParsed(templateid string) map[string]interface{} {
hr := NewHashResponse(dr.Get(templateid))
return hr.GetHash()
}
// Get method to get a raw response template by given template id.
func (dr *Templates) Get(templateid string) string {
return dr.templates[templateid]
}
// Set method to set a response template by given template id and content
func (dr *Templates) Set(templateid string, templatecontent string) {
dr.templates[templateid] = templatecontent
}
// SetParsed method to set a response template by given template id and parsed content
func (dr *Templates) SetParsed(templateid string, templatecontent map[string]interface{}) {
hr := NewHashResponse("")
dr.templates[templateid] = hr.Serialize(templatecontent)
}
// Match method to compare a given raw api response with a response template identfied by id.
// It compares CODE and DESCRIPTION.
func (dr *Templates) Match(r string, templateid string) bool {
tpl := NewHashResponse(dr.Get(templateid))
rr := NewHashResponse(r)
return (tpl.Code() == rr.Code() && strings.Compare(tpl.Description(), rr.Description()) == 0)
}
// MatchParsed method to compare a given parsed api response with a response template identified by id.
// It compares CODE and DESCRIPTION.
func (dr *Templates) MatchParsed(r map[string]interface{}, templateid string) bool {
tpl := dr.GetParsed(templateid)
return (strings.Compare(tpl["CODE"].(string), r["CODE"].(string)) == 0 &&
strings.Compare(tpl["DESCRIPTION"].(string), r["DESCRIPTION"].(string)) == 0)
}

View File

@ -0,0 +1,98 @@
// Copyright (c) 2018 Kai Schwarz (1API GmbH). All rights reserved.
//
// Use of this source code is governed by the MIT
// license that can be found in the LICENSE.md file.
// Package listresponse covers all functionality to handle an API response in list format, but as well provides access to the hash format
package listresponse
import (
"github.com/hexonet/go-sdk/response/hashresponse"
)
// ListResponse class provides extra functionality to work with API responses.
// It provides methods that are useful for data representation in table format.
// In general the apiconnector Client always returns this type of response to be as flexible as possible.
type ListResponse struct {
*hashresponse.HashResponse
currentIndex int
rows [][]string
}
// NewListResponse represents the constructor for struct ListResponse
func NewListResponse(r string) *ListResponse {
lr := &ListResponse{
rows: [][]string{},
currentIndex: 0,
}
lr.HashResponse = hashresponse.NewHashResponse(r)
rows := lr.rows
h := lr.GetHash()
cols := lr.GetColumnKeys()
if lr.IsSuccess() && h["PROPERTY"] != nil {
size := len(cols)
cc := lr.Count()
for i := 0; i < cc; i++ { //loop over amount of rows/indexes
var row []string
for c := 0; c < size; c++ { //loop over all columns
colkey := cols[c]
values := lr.GetColumn(colkey)
if values != nil && len(values) > i {
row = append(row, values[i])
}
}
rows = append(rows, row)
}
}
lr.rows = rows
return lr
}
// GetList method to return the list of available rows
func (lr *ListResponse) GetList() [][]string {
return lr.rows
}
// HasNext method to check if there's a further row after current row
func (lr *ListResponse) HasNext() bool {
len := len(lr.rows)
if len == 0 || lr.currentIndex+1 >= len {
return false
}
return true
}
// Next method to access next row.
// Use HasNext method before.
func (lr *ListResponse) Next() []string {
lr.currentIndex++
return lr.rows[lr.currentIndex]
}
// HasPrevious method to check if there is a row available before current row.
func (lr *ListResponse) HasPrevious() bool {
if lr.currentIndex == 0 {
return false
}
return true
}
// Previous method to access previous row.
// Use HasPrevious method before.
func (lr *ListResponse) Previous() []string {
lr.currentIndex--
return lr.rows[lr.currentIndex]
}
// Current method to return current row
func (lr *ListResponse) Current() []string {
if len(lr.rows) == 0 {
return nil
}
return lr.rows[lr.currentIndex]
}
// Rewind method to reset the iterator index
func (lr *ListResponse) Rewind() {
lr.currentIndex = 0
}

2
vendor/github.com/hexonet/go-sdk/scripts/changelog.sh generated vendored Executable file
View File

@ -0,0 +1,2 @@
#!/bin/bash
auto-changelog --template https://raw.githubusercontent.com/hexonet/auto-changelog-templates/master/compact.hts --commit-limit false --output HISTORY.md

10
vendor/github.com/hexonet/go-sdk/scripts/test-go.sh generated vendored Executable file
View File

@ -0,0 +1,10 @@
#!/usr/bin/env bash
set -euo pipefail
echo
echo "==> Running automated tests <=="
cd test
go test
cd .. || exit
exit

52
vendor/github.com/hexonet/go-sdk/scripts/validate-go.sh generated vendored Executable file
View File

@ -0,0 +1,52 @@
#!/usr/bin/env bash
# Copyright 2016 The Kubernetes Authors All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
set -euo pipefail
exit_code=0
if ! hash gometalinter.v1 2>/dev/null ; then
go get -u gopkg.in/alecthomas/gometalinter.v1
gometalinter.v1 --install
fi
echo
echo "==> Running static validations <=="
# Run linters that should return errors
gometalinter.v1 \
--disable-all \
--enable deadcode \
--severity deadcode:error \
--enable gofmt \
--enable ineffassign \
--enable misspell \
--enable vet \
--tests \
--vendor \
--deadline 60s \
./... || exit_code=1
echo
echo "==> Running linters <=="
# Run linters that should return warnings
gometalinter.v1 \
--disable-all \
--enable golint \
--vendor \
--skip proto \
--deadline 60s \
./... || :
exit $exit_code

9
vendor/vendor.json vendored
View File

@ -253,6 +253,15 @@
"revision": "dec09d789f3dba190787f8b4454c7d3c936fed9e",
"revisionTime": "2017-11-29T19:10:14Z"
},
{
"checksumSHA1": "8NVOwlGdEBlSFIata3vQABKA0Xs=",
"path": "github.com/hexonet/go-sdk",
"revision": "e12cede7032b5fb069cbeb6df5d5e86e95cfc654",
"revisionTime": "2018-08-03T10:21:36Z",
"tree": true,
"version": "v1.2.1",
"versionExact": "v1.2.1"
},
{
"checksumSHA1": "blwbl9vPvRLtL5QlZgfpLvsFiZ4=",
"path": "github.com/jmespath/go-jmespath",