From ce87321e723181b5742ce93aec30ed8503112ca7 Mon Sep 17 00:00:00 2001 From: Kai Schwarz Date: Thu, 14 Jun 2018 15:44:14 +0200 Subject: [PATCH] new provider module HEXONET --- providers/hexonet/domains.go | 24 ++ providers/hexonet/error.go | 11 + providers/hexonet/hexonetProvider.go | 63 +++ providers/hexonet/nameservers.go | 102 +++++ providers/hexonet/records.go | 265 +++++++++++++ providers/hexonet/records_test.go | 51 +++ .../github.com/hexonet/go-sdk/CONTRIBUTING.md | 85 ++++ vendor/github.com/hexonet/go-sdk/HISTORY.md | 20 + vendor/github.com/hexonet/go-sdk/LICENSE | 21 + vendor/github.com/hexonet/go-sdk/README.md | 122 ++++++ .../github.com/hexonet/go-sdk/apiconnector.go | 9 + .../hexonet/go-sdk/client/client.go | 203 ++++++++++ .../go-sdk/client/socketcfg/socketcfg.go | 99 +++++ .../response/hashresponse/hashresponse.go | 366 ++++++++++++++++++ .../go-sdk/response/hashresponse/templates.go | 74 ++++ .../response/listresponse/listresponse.go | 98 +++++ .../hexonet/go-sdk/scripts/changelog.sh | 1 + .../hexonet/go-sdk/scripts/test-go.sh | 11 + .../hexonet/go-sdk/scripts/validate-go.sh | 52 +++ vendor/vendor.json | 9 + 20 files changed, 1686 insertions(+) create mode 100644 providers/hexonet/domains.go create mode 100644 providers/hexonet/error.go create mode 100644 providers/hexonet/hexonetProvider.go create mode 100644 providers/hexonet/nameservers.go create mode 100644 providers/hexonet/records.go create mode 100644 providers/hexonet/records_test.go create mode 100644 vendor/github.com/hexonet/go-sdk/CONTRIBUTING.md create mode 100644 vendor/github.com/hexonet/go-sdk/HISTORY.md create mode 100644 vendor/github.com/hexonet/go-sdk/LICENSE create mode 100644 vendor/github.com/hexonet/go-sdk/README.md create mode 100644 vendor/github.com/hexonet/go-sdk/apiconnector.go create mode 100644 vendor/github.com/hexonet/go-sdk/client/client.go create mode 100644 vendor/github.com/hexonet/go-sdk/client/socketcfg/socketcfg.go create mode 100644 vendor/github.com/hexonet/go-sdk/response/hashresponse/hashresponse.go create mode 100644 vendor/github.com/hexonet/go-sdk/response/hashresponse/templates.go create mode 100644 vendor/github.com/hexonet/go-sdk/response/listresponse/listresponse.go create mode 100755 vendor/github.com/hexonet/go-sdk/scripts/changelog.sh create mode 100755 vendor/github.com/hexonet/go-sdk/scripts/test-go.sh create mode 100755 vendor/github.com/hexonet/go-sdk/scripts/validate-go.sh diff --git a/providers/hexonet/domains.go b/providers/hexonet/domains.go new file mode 100644 index 0000000000..a033c8965c --- /dev/null +++ b/providers/hexonet/domains.go @@ -0,0 +1,24 @@ +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) + } + return nil +} diff --git a/providers/hexonet/error.go b/providers/hexonet/error.go new file mode 100644 index 0000000000..373bebf30e --- /dev/null +++ b/providers/hexonet/error.go @@ -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()) +} diff --git a/providers/hexonet/hexonetProvider.go b/providers/hexonet/hexonetProvider.go new file mode 100644 index 0000000000..9330aa7823 --- /dev/null +++ b/providers/hexonet/hexonetProvider.go @@ -0,0 +1,63 @@ +// 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" + + "github.com/StackExchange/dnscontrol/providers" + hxcl "github.com/hexonet/go-sdk/client" + "github.com/pkg/errors" +) + +// 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.Can(), + providers.CanUseCAA: providers.Cannot(), + providers.CanUsePTR: providers.Can(), + providers.CanUseRoute53Alias: providers.Cannot(), + providers.CanUseSRV: providers.Can(), + providers.CanUseTLSA: providers.Cannot(), + providers.CanUseTXTMulti: providers.Can(), + providers.CantUseNOPURGE: providers.Cannot(), + providers.DocCreateDomains: providers.Can(), + providers.DocDualHost: providers.Cannot("Apex NS records not editable"), + providers.DocOfficiallySupported: providers.Can(), +} + +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 api.APIEntity != "1234" && api.APIEntity != "54cd" { + return nil, errors.Errorf("wrong api system entity used. use \"1234\" for OT&E system or \"54cd\" for Live system") + } + if api.APIEntity == "1234" { + api.client.UseOTESystem() + } + if api.APILogin == "" || api.APIPassword == "" { + return nil, errors.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) +} diff --git a/providers/hexonet/nameservers.go b/providers/hexonet/nameservers.go new file mode 100644 index 0000000000..57115ad962 --- /dev/null +++ b/providers/hexonet/nameservers.go @@ -0,0 +1,102 @@ +package hexonet + +import ( + "errors" + "fmt" + "regexp" + "sort" + "strconv" + "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["NAMESERVER"+strconv.Itoa(idx)] = ns + } + response := n.client.Request(cmd) + code := response.Code() + if code != 200 { + return errors.New(strconv.Itoa(code) + " " + response.Description()) + } + return nil + } +} diff --git a/providers/hexonet/records.go b/providers/hexonet/records.go new file mode 100644 index 0000000000..afe317d6f0 --- /dev/null +++ b/providers/hexonet/records.go @@ -0,0 +1,265 @@ +package hexonet + +import ( + "bytes" + "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.//TODO -> hexonet allows a minimum TTL of 300, or 5 minutes. + 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" { + rec.Type = "CNAME" + } + } + + checkNSModifications(dc) //TODO can we drop that out or is ok to reuse? + + // 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["ADDRR"+strconv.Itoa(addrridx)] = n.createRecordString(rec, dc.Name) + addrridx++ + } + for _, d := range del { + changes = true + fmt.Fprintln(buf, d) + rec := d.Existing.Original.(*HXRecord) + params["DELRR"+strconv.Itoa(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["DELRR"+strconv.Itoa(addrridx)] = n.createRecordString(new, dc.Name) + params["DELRR"+strconv.Itoa(delrridx)] = n.deleteRecordString(old, 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 checkNSModifications(dc *models.DomainConfig) { + newList := make([]*models.RecordConfig, 0, len(dc.Records)) + for _, rec := range dc.Records { + if rec.Type == "NS" && rec.GetLabel() == "@" { + continue //TODO Apex NS records are automatically created for the domain's nameservers and cannot be managed otherwise via the hexonet API. + } + newList = append(newList, rec) + } + dc.Records = newList +} + +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) updateZoneBy(params map[string]string, zone string) error { + cmd := map[string]string{ + "COMMAND": "UpdateDNSZone", + "DNSZONE": zone, + "INCSERIAL": "1", + } + for key, val := range params { + cmd[key] = val + } + 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 + r := n.client.RequestAll(map[string]string{ + "COMMAND": "QueryDNSZoneRRList", + "DNSZONE": domain + ".", + "SHORT": "1", + "EXTENDED": "0", + }) + + if !r.IsSuccess() { + return nil, n.GetHXApiError("Failed loading resource records for zone", domain, r) + } + rrs := r.GetColumn("RR") + for _, rr := range rrs { + spl := strings.Split(rr, " ") + record := &HXRecord{ + Raw: rr, + DomainName: domain, + Host: spl[0], //todo: check srv rec + 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:len(spl)-1], " ") //todo: check srv rec + } else { + record.Answer = strings.Join(spl[4:len(spl)-1], " ") + } + } + 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": + // nothing + case "TXT": + record.Answer = encodeTxt(rc.TxtStrings) + case "SRV": + record.Answer = fmt.Sprintf("%d %d %v", rc.SrvWeight, rc.SrvPort, rc.GetTargetField()) + 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} +} diff --git a/providers/hexonet/records_test.go b/providers/hexonet/records_test.go new file mode 100644 index 0000000000..3f9db2b427 --- /dev/null +++ b/providers/hexonet/records_test.go @@ -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, "`, `")) + } + } + } + } +} diff --git a/vendor/github.com/hexonet/go-sdk/CONTRIBUTING.md b/vendor/github.com/hexonet/go-sdk/CONTRIBUTING.md new file mode 100644 index 0000000000..da468d072b --- /dev/null +++ b/vendor/github.com/hexonet/go-sdk/CONTRIBUTING.md @@ -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/ diff --git a/vendor/github.com/hexonet/go-sdk/HISTORY.md b/vendor/github.com/hexonet/go-sdk/HISTORY.md new file mode 100644 index 0000000000..e2ab1f4d63 --- /dev/null +++ b/vendor/github.com/hexonet/go-sdk/HISTORY.md @@ -0,0 +1,20 @@ +### Changelog + +All notable changes to this project will be documented in this file. Dates are displayed in UTC. + +Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). + +#### Unreleased + +- updated versioning info in readme [`607de6f`](https://github.com/hexonet/go-sdk/commit/607de6f787df016f42216e38dcee5a22f4042093) +- 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) diff --git a/vendor/github.com/hexonet/go-sdk/LICENSE b/vendor/github.com/hexonet/go-sdk/LICENSE new file mode 100644 index 0000000000..c65316d630 --- /dev/null +++ b/vendor/github.com/hexonet/go-sdk/LICENSE @@ -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. diff --git a/vendor/github.com/hexonet/go-sdk/README.md b/vendor/github.com/hexonet/go-sdk/README.md new file mode 100644 index 0000000000..b51b6a7f22 --- /dev/null +++ b/vendor/github.com/hexonet/go-sdk/README.md @@ -0,0 +1,122 @@ +# 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 github.com/hexonet/go-sdk@` 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@`. 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() + 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/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. diff --git a/vendor/github.com/hexonet/go-sdk/apiconnector.go b/vendor/github.com/hexonet/go-sdk/apiconnector.go new file mode 100644 index 0000000000..ffebb812ef --- /dev/null +++ b/vendor/github.com/hexonet/go-sdk/apiconnector.go @@ -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() { +} diff --git a/vendor/github.com/hexonet/go-sdk/client/client.go b/vendor/github.com/hexonet/go-sdk/client/client.go new file mode 100644 index 0000000000..4fd5251cc9 --- /dev/null +++ b/vendor/github.com/hexonet/go-sdk/client/client.go @@ -0,0 +1,203 @@ +// 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 ( + "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 { + 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{ + 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) +} + +// 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") +} + +// 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) +} + +// 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) + return listresponse.NewListResponse(tpl) + } + 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) + return listresponse.NewListResponse(tpl) + } + 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) + return listresponse.NewListResponse(tpl) + } + return listresponse.NewListResponse(string(response)) + } + tpl := hashresponse.NewTemplates().Get("commonerror") + tpl = strings.Replace(tpl, "####ERRMSG####", string(resp.StatusCode)+resp.Status, 1) + return listresponse.NewListResponse(tpl) +} + +// 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) +} diff --git a/vendor/github.com/hexonet/go-sdk/client/socketcfg/socketcfg.go b/vendor/github.com/hexonet/go-sdk/client/socketcfg/socketcfg.go new file mode 100644 index 0000000000..36ff44f37a --- /dev/null +++ b/vendor/github.com/hexonet/go-sdk/client/socketcfg/socketcfg.go @@ -0,0 +1,99 @@ +// 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 +} + +// 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() +} diff --git a/vendor/github.com/hexonet/go-sdk/response/hashresponse/hashresponse.go b/vendor/github.com/hexonet/go-sdk/response/hashresponse/hashresponse.go new file mode 100644 index 0000000000..14d23838ae --- /dev/null +++ b/vendor/github.com/hexonet/go-sdk/response/hashresponse/hashresponse.go @@ -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 +} diff --git a/vendor/github.com/hexonet/go-sdk/response/hashresponse/templates.go b/vendor/github.com/hexonet/go-sdk/response/hashresponse/templates.go new file mode 100644 index 0000000000..6c9c60faac --- /dev/null +++ b/vendor/github.com/hexonet/go-sdk/response/hashresponse/templates.go @@ -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) +} diff --git a/vendor/github.com/hexonet/go-sdk/response/listresponse/listresponse.go b/vendor/github.com/hexonet/go-sdk/response/listresponse/listresponse.go new file mode 100644 index 0000000000..c047e87dfd --- /dev/null +++ b/vendor/github.com/hexonet/go-sdk/response/listresponse/listresponse.go @@ -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 +} diff --git a/vendor/github.com/hexonet/go-sdk/scripts/changelog.sh b/vendor/github.com/hexonet/go-sdk/scripts/changelog.sh new file mode 100755 index 0000000000..ffac2896fe --- /dev/null +++ b/vendor/github.com/hexonet/go-sdk/scripts/changelog.sh @@ -0,0 +1 @@ +auto-changelog --commit-limit false --output HISTORY.md diff --git a/vendor/github.com/hexonet/go-sdk/scripts/test-go.sh b/vendor/github.com/hexonet/go-sdk/scripts/test-go.sh new file mode 100755 index 0000000000..1189586e16 --- /dev/null +++ b/vendor/github.com/hexonet/go-sdk/scripts/test-go.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +set -euo pipefail + +echo +echo "==> Running automated tests <==" +cd test +go test +cd .. + +exit diff --git a/vendor/github.com/hexonet/go-sdk/scripts/validate-go.sh b/vendor/github.com/hexonet/go-sdk/scripts/validate-go.sh new file mode 100755 index 0000000000..2ecf5dfb3f --- /dev/null +++ b/vendor/github.com/hexonet/go-sdk/scripts/validate-go.sh @@ -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 diff --git a/vendor/vendor.json b/vendor/vendor.json index a088a37e67..2d35f57179 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -253,6 +253,15 @@ "revision": "dec09d789f3dba190787f8b4454c7d3c936fed9e", "revisionTime": "2017-11-29T19:10:14Z" }, + { + "checksumSHA1": "iobZL1afEPciEGjtbmXExntMikk=", + "path": "github.com/hexonet/go-sdk", + "revision": "e007900b3e5c1907ba0682343c9f2253f7742b03", + "revisionTime": "2018-07-04T12:34:45Z", + "tree": true, + "version": "v1.1.0", + "versionExact": "v1.1.0" + }, { "checksumSHA1": "blwbl9vPvRLtL5QlZgfpLvsFiZ4=", "path": "github.com/jmespath/go-jmespath",