diff --git a/bootstrap/client.go b/bootstrap/client.go index a1955a0..8ee3971 100644 --- a/bootstrap/client.go +++ b/bootstrap/client.go @@ -91,10 +91,12 @@ import ( "context" "crypto/sha256" "encoding/hex" + "encoding/json" "fmt" "io/ioutil" "net/http" "net/url" + "os" "time" "github.com/openrdap/rdap/bootstrap/cache" @@ -103,6 +105,13 @@ import ( // A RegistryType represents a bootstrap registry type. type RegistryType int +type jsonDocument struct { + Description string + Publication string + Version string + Services [][][]string +} + const ( DNS RegistryType = iota IPv4 @@ -236,19 +245,48 @@ func (c *Client) download(ctx context.Context, registry RegistryType) ([]byte, R return nil, nil, fmt.Errorf("Server returned non-200 status code: %s", resp.Status) } - json, err := ioutil.ReadAll(resp.Body) + jsonString, err := ioutil.ReadAll(resp.Body) if err != nil { return nil, nil, err } + // existiert ein custom-RegistryType file? + dc := cache.NewDiskCache() + if _, err := os.Stat(dc.Dir + "/custom-" + c.filenameFor(registry)); err == nil { + // path/to/whatever exists + custom, err := ioutil.ReadFile(dc.Dir + "/custom-" + c.filenameFor(registry)) + if err != nil { + return nil, nil, err + } + + var customDecoded jsonDocument + err = json.Unmarshal(custom, &customDecoded) + if err != nil { + return nil, nil, err + } + + var decoded jsonDocument + err = json.Unmarshal(jsonString, &decoded) + if err != nil { + return nil, nil, err + } + + decoded.Services = append(decoded.Services, customDecoded.Services...) + + jsonString, err = json.Marshal(decoded) + if err != nil { + return nil, nil, err + } + } + var s Registry - s, err = newRegistry(registry, json) + s, err = newRegistry(registry, jsonString) if err != nil { - return json, nil, err + return jsonString, nil, err } - return json, s, nil + return jsonString, s, nil } func (c *Client) freshenFromCache(registry RegistryType) { diff --git a/bootstrap/file.go b/bootstrap/file.go index d6e05c4..f9644c6 100644 --- a/bootstrap/file.go +++ b/bootstrap/file.go @@ -81,3 +81,48 @@ func NewFile(jsonDocument []byte) (*File, error) { return f, nil } + +func (f *File) AddEntries(jsonDocument []byte) error { + var doc struct { + Description string + Publication string + Version string + + Services [][][]string + } + + err := json.Unmarshal(jsonDocument, &doc) + if err != nil { + return err + } + + for _, s := range doc.Services { + if len(s) != 2 { + return errors.New("Malformed bootstrap (bad services array)") + } + + entries := s[0] + rawURLs := s[1] + + var urls []*url.URL + + for _, rawURL := range rawURLs { + url, err := url.Parse(rawURL) + + // Ignore unparsable URLs. + if err != nil { + continue + } + + urls = append(urls, url) + } + + if len(urls) > 0 { + for _, entry := range entries { + f.Entries[entry] = urls + } + } + } + + return nil +} diff --git a/cli.go b/cli.go index 3038fe2..1396613 100644 --- a/cli.go +++ b/cli.go @@ -504,6 +504,45 @@ func RunCLI(args []string, stdout io.Writer, stderr io.Writer, options CLIOption return 1 } + // resp.Object + secondLookup := false + var registrarUrl *url.URL + if req.Type.String() == "domain" { + // try to follow + + d, ok := resp.Object.(*Domain) + if ok { + for _, link := range d.Links { + if link.Href != resp.URL.String() { + // zweiter lookup + registrarUrl, err = url.Parse(link.Href) + secondLookup = true + if err != nil { + printError(stderr, fmt.Sprintf("Error: %s", err)) + return 1 + } + } + } + } + } + + var secondReq *Request + var secondResp *Response + if secondLookup { + secondStart := time.Now() + + secondReq = NewRawRequest(registrarUrl) + secondResp, err = client.Do(secondReq) + + verbose("") + verbose(fmt.Sprintf("rdap: Finished second query in %s", time.Since(secondStart))) + + if err != nil { + printError(stderr, fmt.Sprintf("Registrar Error: %s", err)) + secondLookup = false + } + } + // Insert a blank line to seperate verbose messages/proper output. if *verboseFlag { fmt.Fprintln(stderr, "") @@ -516,28 +555,46 @@ func RunCLI(args []string, stdout io.Writer, stderr io.Writer, options CLIOption // Print the response out in text format? if *outputFormatText { + fmt.Fprintln(stdout, "RDAP from Registry:") printer := &Printer{ Writer: stdout, BriefLinks: true, } printer.Print(resp.Object) + if secondLookup { + fmt.Fprintf(stdout, "\n\nRDAP from Registrar:\n") + printer.Print(secondResp.Object) + } } // Print the raw response out? if *outputFormatRaw { + fmt.Fprintln(stdout, "RDAP from Registry:") fmt.Printf("%s", resp.HTTP[0].Body) + if secondLookup { + fmt.Fprintf(stdout, "\n\nRDAP from Registrar:\n") + fmt.Printf("%s", secondResp.HTTP[0].Body) + } } // Print the response, JSON pretty-printed? if *outputFormatJSON { var out bytes.Buffer json.Indent(&out, resp.HTTP[0].Body, "", " ") + fmt.Fprintln(stdout, "RDAP from Registry:") out.WriteTo(os.Stdout) + + if secondLookup { + json.Indent(&out, secondResp.HTTP[0].Body, "", " ") + fmt.Fprintf(stdout, "\n\nRDAP from Registrar:\n") + out.WriteTo(os.Stdout) + } } // Print WHOIS style response out? if *outputFormatWhois { + fmt.Fprintln(stdout, "RDAP from Registry:") w := resp.ToWhoisStyleResponse() for _, key := range w.KeyDisplayOrder { @@ -545,6 +602,17 @@ func RunCLI(args []string, stdout io.Writer, stderr io.Writer, options CLIOption fmt.Fprintf(stdout, "%s: %s\n", key, safePrint(value)) } } + + if secondLookup { + fmt.Fprintf(stdout, "\n\nRDAP from Registrar:\n") + w = secondResp.ToWhoisStyleResponse() + + for _, key := range w.KeyDisplayOrder { + for _, value := range w.Data[key] { + fmt.Fprintf(stdout, "%s: %s\n", key, safePrint(value)) + } + } + } } _ = fetchRolesFlag diff --git a/client.go b/client.go index c3319c6..0d393b2 100644 --- a/client.go +++ b/client.go @@ -174,6 +174,7 @@ func (c *Client) Do(req *Request) (*Response, error) { for _, r := range reqs { c.Verbose(fmt.Sprintf("client: GET %s", r.URL())) + resp.URL = r.URL() httpResponse := c.get(r) resp.HTTP = append(resp.HTTP, httpResponse) diff --git a/contrib/custom-dns.json b/contrib/custom-dns.json new file mode 100644 index 0000000..32db32c --- /dev/null +++ b/contrib/custom-dns.json @@ -0,0 +1,262 @@ +{ + "description": "RDAP bootstrap Oliver Dick", + "publication": "2021-05-11T21:00:02Z", + "services": [ + [ + [ + "bh" + ], + [ + "https://rdap.centralnic.com/bh/" + ] + ], + [ + [ + "cc" + ], + [ + "https://tld-rdap.verisign.com/cc/v1/" + ] + ], + [ + [ + "ch" + ], + [ + "https://rdap.nic.ch/" + ] + ], + [ + [ + "cz" + ], + [ + "https://rdap.nic.cz/" + ] + ], + [ + [ + "de" + ], + [ + "https://rdap.denic.de/" + ] + ], + [ + [ + "dk" + ], + [ + "https://rdap.dk-hostmaster.dk/" + ] + ], + [ + [ + "fm" + ], + [ + "https://rdap.centralnic.com/fm/" + ] + ], + [ + [ + "fo" + ], + [ + "https://rdap.centralnic.com/fo/" + ] + ], + [ + [ + "gd" + ], + [ + "https://rdap.centralnic.com/gd/" + ] + ], + [ + [ + "it" + ], + [ + "https://rdap.pubtest.nic.it/" + ] + ], + [ + [ + "li" + ], + [ + "https://rdap.nic.li/" + ] + ], + [ + [ + "no" + ], + [ + "https://rdap.norid.no/" + ] + ], + [ + [ + "pw" + ], + [ + "https://rdap.centralnic.com/pw/" + ] + ], + [ + [ + "tv" + ], + [ + "https://tld-rdap.verisign.com/tv/v1/" + ] + ], + [ + [ + "vg" + ], + [ + "https://rdap.centralnic.com/vg/" + ] + ], + [ + [ + "ae.org" + ], + [ + "https://rdap.centralnic.com/ae.org/" + ] + ], + [ + [ + "br.com" + ], + [ + "https://rdap.centralnic.com/br.com/" + ] + ], + [ + [ + "cn.com" + ], + [ + "https://rdap.centralnic.com/cn.com/" + ] + ], + [ + [ + "com.de" + ], + [ + "https://rdap.centralnic.com/com.de/" + ] + ], + [ + [ + "de.com" + ], + [ + "https://rdap.centralnic.com/de.com/" + ] + ], + [ + [ + "eu.com" + ], + [ + "https://rdap.centralnic.com/eu.com/" + ] + ], + [ + [ + "gb.net" + ], + [ + "https://rdap.centralnic.com/gb.net/" + ] + ], + [ + [ + "gr.com" + ], + [ + "https://rdap.centralnic.com/gr.com/" + ] + ], + [ + [ + "jpn.com" + ], + [ + "https://rdap.centralnic.com/jpn.com/" + ] + ], + [ + [ + "jp.net" + ], + [ + "https://rdap.centralnic.com/jp.net/" + ] + ], + [ + [ + "ru.com" + ], + [ + "https://rdap.centralnic.com/ru.com/" + ] + ], + [ + [ + "sa.com" + ], + [ + "https://rdap.centralnic.com/sa.com/" + ] + ], + [ + [ + "uk.com" + ], + [ + "https://rdap.centralnic.com/uk.com/" + ] + ], + [ + [ + "uk.net" + ], + [ + "https://rdap.centralnic.com/uk.net/" + ] + ], + [ + [ + "us.com" + ], + [ + "https://rdap.centralnic.com/us.com/" + ] + ], + [ + [ + "us.org" + ], + [ + "https://rdap.centralnic.com/us.org/" + ] + ], + [ + [ + "za.com" + ], + [ + "https://rdap.centralnic.com/za.com/" + ] + ] + ] +} \ No newline at end of file diff --git a/example_test.go b/example_test.go index ec3b324..9e6361d 100644 --- a/example_test.go +++ b/example_test.go @@ -73,7 +73,7 @@ func Example() { // Region : 'ON' // PostalCode: 'M5E 1W5' // Country : 'Canada' - // Tel : 'tel:+1-555-555-5555' + // Tel : '+1-555-555-5555' // Fax : '' // Email : 'hi@example.com' } diff --git a/go.mod b/go.mod index 5e7c596..1ebfa87 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4 // indirect github.com/davecgh/go-spew v1.1.1 github.com/jarcoal/httpmock v1.0.4 + github.com/mailru/easyjson v0.7.7 github.com/mitchellh/go-homedir v1.1.0 github.com/stretchr/testify v1.3.0 // indirect golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 diff --git a/go.sum b/go.sum index 6ea77af..4923030 100644 --- a/go.sum +++ b/go.sum @@ -7,6 +7,9 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/jarcoal/httpmock v1.0.4 h1:jp+dy/+nonJE4g4xbVtl9QdrUNbn6/3hDT5R4nDIZnA= github.com/jarcoal/httpmock v1.0.4/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT8nX0Txik= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= diff --git a/print.go b/print.go index e1efc70..eaeb168 100644 --- a/print.go +++ b/print.go @@ -481,11 +481,22 @@ func (p *Printer) printEntity(e *Entity, indentLevel uint) { } if e.VCard != nil { - for _, property := range e.VCard.Properties { - for _, str := range property.Values() { - p.printValue("vCard "+property.Name, str, indentLevel) - } - } + p.printValue("vCard Version", e.VCard.Version(), indentLevel) + p.printValue("vCard Name", e.VCard.Name(), indentLevel) + p.printValue("vCard Org", e.VCard.Org(), indentLevel) + for _, lang := range e.VCard.Languages() { + p.printValue("vCard Lang", lang, indentLevel) + } + p.printValue("vCard POBox", e.VCard.POBox(), indentLevel) + p.printValue("vCard ExtendedAddress", e.VCard.ExtendedAddress(), indentLevel) + p.printValue("vCard Street", e.VCard.StreetAddress(), indentLevel) + p.printValue("vCard Locality", e.VCard.Locality(), indentLevel) + p.printValue("vCard Region", e.VCard.Region(), indentLevel) + p.printValue("vCard PostalCode", e.VCard.PostalCode(), indentLevel) + p.printValue("vCard CountryCode", e.VCard.CountryCode(), indentLevel) + p.printValue("vCard Tel", e.VCard.Tel(), indentLevel) + p.printValue("vCard Fax", e.VCard.Fax(), indentLevel) + p.printValue("vCard ContactURI", e.VCard.ContactURI(), indentLevel) } if !p.BriefOutput { diff --git a/response.go b/response.go index 4cefbe2..e8d93ac 100644 --- a/response.go +++ b/response.go @@ -6,6 +6,7 @@ package rdap import ( "net/http" + "net/url" "time" "github.com/openrdap/rdap/bootstrap" @@ -15,6 +16,7 @@ type Response struct { Object RDAPObject BootstrapAnswer *bootstrap.Answer HTTP []*HTTPResponse + URL *url.URL } type RDAPObject interface{} diff --git a/vcard.go b/vcard.go index 78a80e2..3144d61 100644 --- a/vcard.go +++ b/vcard.go @@ -80,24 +80,34 @@ type VCardProperty struct { func (p *VCardProperty) Values() []string { strings := make([]string, 0, 1) - p.appendValueStrings(p.Value, &strings) + p.appendValueStrings(p.Value, &strings, 0) return strings } -func (p *VCardProperty) appendValueStrings(v interface{}, strings *[]string) { +func (p *VCardProperty) appendValueStrings(v interface{}, stringValues *[]string, deep int) { + deep = deep + 1 switch v := v.(type) { case nil: - *strings = append(*strings, "") + *stringValues = append(*stringValues, "") case bool: - *strings = append(*strings, strconv.FormatBool(v)) + *stringValues = append(*stringValues, strconv.FormatBool(v)) case float64: - *strings = append(*strings, strconv.FormatFloat(v, 'f', -1, 64)) + *stringValues = append(*stringValues, strconv.FormatFloat(v, 'f', -1, 64)) case string: - *strings = append(*strings, v) + *stringValues = append(*stringValues, v) case []interface{}: - for _, v2 := range v { - p.appendValueStrings(v2, strings) + if deep < 2 { + for _, v2 := range v { + p.appendValueStrings(v2, stringValues, deep) + } + } else { + var all string + for _, v2 := range v { + all += v2.(string) + ", " + } + all = strings.TrimSuffix(all, ", ") + *stringValues = append(*stringValues, all) } default: panic("Unknown type") @@ -318,11 +328,35 @@ func (v *VCard) getFirstPropertySingleString(name string) string { return strings.Join(property.Values(), " ") } +func (v *VCard) getAllPropertiesString(name string) []string { + properties := v.Get(name) + var ret []string + for _, property := range properties { + ret = append(ret, strings.Join(property.Values(), " ")) + } + return ret +} + +// Version returns the VCard's version, e.g. "4.0". +func (v *VCard) Version() string { + return v.getFirstPropertySingleString("version") +} + +// Languages returns the VCard's languages, e.g. "de". +func (v *VCard) Languages() []string { + return v.getAllPropertiesString("lang") +} + // Name returns the VCard's name. e.g. "John Smith". func (v *VCard) Name() string { return v.getFirstPropertySingleString("fn") } +// Org returns the VCard's org. e.g. "hosting.de GmbH". +func (v *VCard) Org() string { + return v.getFirstPropertySingleString("org") +} + // POBox returns the address's PO Box. // // Returns empty string if no address is present. @@ -375,6 +409,24 @@ func (v *VCard) Country() string { return v.getFirstAddressField(6) } +// CountryCode returns the Country Code from Parameter CC. +// +// See https://tools.ietf.org/html/rfc8605 +// +// Returns empty string if no address is present. +func (v *VCard) CountryCode() string { + adr := v.GetFirst("adr") + if adr == nil { + return "" + } + + if _, ok := adr.Parameters["cc"]; ok { + return adr.Parameters["cc"][0] + } + + return "" +} + // Tel returns the VCard's first (voice) telephone number. // // Returns empty string if the VCard contains no suitable telephone number. @@ -396,7 +448,7 @@ func (v *VCard) Tel() string { } if isVoice && len(p.Values()) > 0 { - return (p.Values())[0] + return strings.Replace((p.Values())[0], "tel:", "", -1) } } @@ -414,7 +466,7 @@ func (v *VCard) Fax() string { for _, t := range types { if t == "fax" { if len(p.Values()) > 0 { - return (p.Values())[0] + return strings.Replace((p.Values())[0], "tel:", "", -1) } } } @@ -431,6 +483,15 @@ func (v *VCard) Email() string { return v.getFirstPropertySingleString("email") } +// ContactURI returns the VCard's first contact Uri. +// +// See https://tools.ietf.org/html/rfc8605 +// +// Returns empty string if the VCard contains no contact-uri. +func (v *VCard) ContactURI() string { + return v.getFirstPropertySingleString("contact-uri") +} + func (v *VCard) getFirstAddressField(index int) string { adr := v.GetFirst("adr") if adr == nil { diff --git a/vcard_test.go b/vcard_test.go index 754cfa7..08939cb 100644 --- a/vcard_test.go +++ b/vcard_test.go @@ -154,7 +154,7 @@ func TestVCardQuickAccessors(t *testing.T) { "QC", "G1V 2M2", "Canada", - "tel:+1-418-656-9254;ext=102", + "+1-418-656-9254;ext=102", "", "simon.perreault@viagenie.ca", }