diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml new file mode 100644 index 0000000..8704165 --- /dev/null +++ b/.github/workflows/go.yml @@ -0,0 +1,32 @@ +name: Go + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + build: + name: Go Build + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + steps: + - name: Set up Go 1.x + uses: actions/setup-go@v2 + with: + go-version: '>= 1.20.4' + id: go + + - name: Check out code into the Go module directory + uses: actions/checkout@v3 + + - name: Linters + run: | + go vet ./... + + - name: Test + run: | + go test ./... diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 622d6b5..0000000 --- a/.travis.yml +++ /dev/null @@ -1,8 +0,0 @@ -language: go - -go: - - 1.7 - - master - -script: - - go test -v ./... diff --git a/README.md b/README.md index 288aa1f..b8d1889 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ https://www.openrdap.org/demo - live demo ## Features * Command line RDAP client +* Output formats: text, JSON, WHOIS style * Query types supported: * ip * domain @@ -24,11 +25,10 @@ https://www.openrdap.org/demo - live demo * nameserver-search-by-ip * entity-search * entity-search-by-handle -* Query bootstrapping (automatic RDAP server URL detection for ip/domain/autnum/(experimental) entity queries) +* Automatic server detection for ip/domain/autnum/entities +* Object tags support * Bootstrap cache (optional, uses ~/.openrdap by default) * X.509 client authentication -* Output formats: text, JSON, WHOIS style -* Experimental [object tagging](https://datatracker.ietf.org/doc/draft-ietf-regext-rdap-object-tag/) support ## Installation @@ -36,7 +36,7 @@ This program uses Go. The Go compiler is available from https://golang.org/. To install: - go get -u github.com/openrdap/rdap/cmd/rdap + go install github.com/openrdap/rdap/cmd/rdap@master This will install the "rdap" binary in your $GOPATH/go/bin directory. Try running: @@ -45,10 +45,16 @@ This will install the "rdap" binary in your $GOPATH/go/bin directory. Try runnin ## Usage | Query type | Usage | -| --- | --- | +| ------------------------- | ------------------------------------------------------------------------ | | Domain (.com) | rdap -v example.com | -| Network | rdap -v 2001:db8:: | -| Autnum | rdap -v AS15169 | +| IPv4 Address | rdap -v 192.0.2.0 | +| IPv6 Address | rdap -v 2001:db8:: | +| Autonomous System (ASN) | rdap -v AS15169 | +| Entity (with object tag) | rdap -v OPS4-RIPE | + +## Advanced usage (server must be specified using -s; not all servers support all query types) +| Query type | Usage | +| ------------------------- | ------------------------------------------------------------------------ | | Nameserver | rdap -v -t nameserver -s https://rdap.verisign.com/com/v1 ns1.google.com | | Help | rdap -v -t help -s https://rdap.verisign.com/com/v1 | | Domain Search | rdap -v -t domain-search -s $SERVER_URL example*.gtld | @@ -61,24 +67,679 @@ This will install the "rdap" binary in your $GOPATH/go/bin directory. Try runnin See https://www.openrdap.org/docs. +## Example output + +Click the examples to see the output: + +
+rdap example.com + +```Domain: + Domain Name: EXAMPLE.COM + Handle: 2336799_DOMAIN_COM-VRSN + Status: client delete prohibited + Status: client transfer prohibited + Status: client update prohibited + Conformance: rdap_level_0 + Conformance: icann_rdap_technical_implementation_guide_0 + Conformance: icann_rdap_response_profile_0 + Notice: + Title: Terms of Use + Description: Service subject to Terms of Use. + Link: https://www.verisign.com/domain-names/registration-data-access-protocol/terms-service/index.xhtml + Notice: + Title: Status Codes + Description: For more information on domain status codes, please visit https://icann.org/epp + Link: https://icann.org/epp + Notice: + Title: RDDS Inaccuracy Complaint Form + Description: URL of the ICANN RDDS Inaccuracy Complaint Form: https://icann.org/wicf + Link: https://icann.org/wicf + Link: https://rdap.verisign.com/com/v1/domain/EXAMPLE.COM + Event: + Action: registration + Date: 1995-08-14T04:00:00Z + Event: + Action: expiration + Date: 2023-08-13T04:00:00Z + Event: + Action: last changed + Date: 2023-05-12T15:13:35Z + Event: + Action: last update of RDAP database + Date: 2023-05-16T20:36:06Z + Secure DNS: + Delegation Signed: true + DSData: + Key Tag: 370 + Algorithm: 13 + Digest: BE74359954660069D5C63D200C39F5603827D7DD02B56F120EE9F3A86764247C + DigestType: 2 + Entity: + Handle: 376 + Public ID: + Type: IANA Registrar ID + Identifier: 376 + Role: registrar + vCard version: 4.0 + vCard fn: RESERVED-Internet Assigned Numbers Authority + Entity: + Role: abuse + vCard version: 4.0 + Nameserver: + Nameserver: A.IANA-SERVERS.NET + Nameserver: + Nameserver: B.IANA-SERVERS.NET +``` + +
+ +
+rdap 8.8.8.8 + +```IP Network: + Handle: NET-8-8-8-0-1 + Start Address: 8.8.8.0 + End Address: 8.8.8.255 + IP Version: v4 + Name: LVLT-GOGL-8-8-8 + Type: ALLOCATION + ParentHandle: NET-8-0-0-0-1 + Status: active + Port43: whois.arin.net + Notice: + Title: Terms of Service + Description: By using the ARIN RDAP/Whois service, you are agreeing to the RDAP/Whois Terms of Use + Link: https://www.arin.net/resources/registry/whois/tou/ + Notice: + Title: Whois Inaccuracy Reporting + Description: If you see inaccuracies in the results, please visit: + Link: https://www.arin.net/resources/registry/whois/inaccuracy_reporting/ + Notice: + Title: Copyright Notice + Description: Copyright 1997-2023, American Registry for Internet Numbers, Ltd. + Entity: + Handle: GOGL + Port43: whois.arin.net + Remark: + Title: Registration Comments + Description: Please note that the recommended way to file abuse complaints are located in the following links. + Description: To report abuse and illegal activity: https://www.google.com/contact/ + Description: For legal requests: http://support.google.com/legal + Description: Regards, + Description: The Google Team + Link: https://rdap.arin.net/registry/entity/GOGL + Link: https://whois.arin.net/rest/org/GOGL + Event: + Action: last changed + Date: 2019-10-31T15:45:45-04:00 + Event: + Action: registration + Date: 2000-03-30T00:00:00-05:00 + Role: registrant + vCard version: 4.0 + vCard fn: Google LLC + vCard kind: org + Entity: + Handle: ABUSE5250-ARIN + Status: validated + Port43: whois.arin.net + Remark: + Title: Registration Comments + Description: Please note that the recommended way to file abuse complaints are located in the following links. + Description: To report abuse and illegal activity: https://www.google.com/contact/ + Description: For legal requests: http://support.google.com/legal + Description: Regards, + Description: The Google Team + Link: https://rdap.arin.net/registry/entity/ABUSE5250-ARIN + Link: https://whois.arin.net/rest/poc/ABUSE5250-ARIN + Event: + Action: last changed + Date: 2022-10-24T08:43:11-04:00 + Event: + Action: registration + Date: 2015-11-06T15:36:35-05:00 + Role: abuse + vCard version: 4.0 + vCard fn: Abuse + vCard org: Abuse + vCard kind: group + vCard email: network-abuse@google.com + vCard tel: +1-650-253-0000 + Entity: + Handle: ZG39-ARIN + Status: validated + Port43: whois.arin.net + Link: https://rdap.arin.net/registry/entity/ZG39-ARIN + Link: https://whois.arin.net/rest/poc/ZG39-ARIN + Event: + Action: last changed + Date: 2022-11-10T07:12:44-05:00 + Event: + Action: registration + Date: 2000-11-30T13:54:08-05:00 + Role: technical + Role: administrative + vCard version: 4.0 + vCard fn: Google LLC + vCard org: Google LLC + vCard kind: group + vCard email: arin-contact@google.com + vCard tel: +1-650-253-0000 + Link: https://rdap.arin.net/registry/ip/8.8.8.0 + Link: https://whois.arin.net/rest/net/NET-8-8-8-0-1 + Link: https://rdap.arin.net/registry/ip/8.0.0.0/9 + Event: + Action: last changed + Date: 2014-03-14T16:52:05-04:00 + Event: + Action: registration + Date: 2014-03-14T16:52:05-04:00 + cidr0_cidrs: + v4prefix: 8.8.8.0 + length: 24 +``` + +
+ +
+rdap --json AS15169 + +``` +{ + "rdapConformance": [ + "nro_rdap_profile_0", + "rdap_level_0", + "nro_rdap_profile_asn_flat_0" + ], + "notices": [ + { + "title": "Terms of Service", + "description": [ + "By using the ARIN RDAP/Whois service, you are agreeing to the RDAP/Whois Terms of Use" + ], + "links": [ + { + "value": "https://rdap.arin.net/registry/autnum/15169", + "rel": "terms-of-service", + "type": "text/html", + "href": "https://www.arin.net/resources/registry/whois/tou/" + } + ] + }, + { + "title": "Whois Inaccuracy Reporting", + "description": [ + "If you see inaccuracies in the results, please visit: " + ], + "links": [ + { + "value": "https://rdap.arin.net/registry/autnum/15169", + "rel": "inaccuracy-report", + "type": "text/html", + "href": "https://www.arin.net/resources/registry/whois/inaccuracy_reporting/" + } + ] + }, + { + "title": "Copyright Notice", + "description": [ + "Copyright 1997-2023, American Registry for Internet Numbers, Ltd." + ] + } + ], + "handle": "AS15169", + "startAutnum": 15169, + "endAutnum": 15169, + "name": "GOOGLE", + "events": [ + { + "eventAction": "last changed", + "eventDate": "2012-02-24T09:44:34-05:00" + }, + { + "eventAction": "registration", + "eventDate": "2000-03-30T00:00:00-05:00" + } + ], + "links": [ + { + "value": "https://rdap.arin.net/registry/autnum/15169", + "rel": "self", + "type": "application/rdap+json", + "href": "https://rdap.arin.net/registry/autnum/15169" + }, + { + "value": "https://rdap.arin.net/registry/autnum/15169", + "rel": "alternate", + "type": "application/xml", + "href": "https://whois.arin.net/rest/asn/AS15169" + } + ], + "entities": [ + { + "handle": "GOGL", + "vcardArray": [ + "vcard", + [ + [ + "version", + {}, + "text", + "4.0" + ], + [ + "fn", + {}, + "text", + "Google LLC" + ], + [ + "adr", + { + "label": "1600 Amphitheatre Parkway\nMountain View\nCA\n94043\nUnited States" + }, + "text", + [ + "", + "", + "", + "", + "", + "", + "" + ] + ], + [ + "kind", + {}, + "text", + "org" + ] + ] + ], + "roles": [ + "registrant" + ], + "remarks": [ + { + "title": "Registration Comments", + "description": [ + "Please note that the recommended way to file abuse complaints are located in the following links. ", + "", + "To report abuse and illegal activity: https://www.google.com/contact/", + "", + "For legal requests: http://support.google.com/legal ", + "", + "Regards, ", + "The Google Team" + ] + } + ], + "links": [ + { + "value": "https://rdap.arin.net/registry/autnum/15169", + "rel": "self", + "type": "application/rdap+json", + "href": "https://rdap.arin.net/registry/entity/GOGL" + }, + { + "value": "https://rdap.arin.net/registry/autnum/15169", + "rel": "alternate", + "type": "application/xml", + "href": "https://whois.arin.net/rest/org/GOGL" + } + ], + "events": [ + { + "eventAction": "last changed", + "eventDate": "2019-10-31T15:45:45-04:00" + }, + { + "eventAction": "registration", + "eventDate": "2000-03-30T00:00:00-05:00" + } + ], + "entities": [ + { + "handle": "ABUSE5250-ARIN", + "vcardArray": [ + "vcard", + [ + [ + "version", + {}, + "text", + "4.0" + ], + [ + "adr", + { + "label": "1600 Amphitheatre Parkway\nMountain View\nCA\n94043\nUnited States" + }, + "text", + [ + "", + "", + "", + "", + "", + "", + "" + ] + ], + [ + "fn", + {}, + "text", + "Abuse" + ], + [ + "org", + {}, + "text", + "Abuse" + ], + [ + "kind", + {}, + "text", + "group" + ], + [ + "email", + {}, + "text", + "network-abuse@google.com" + ], + [ + "tel", + { + "type": [ + "work", + "voice" + ] + }, + "text", + "+1-650-253-0000" + ] + ] + ], + "roles": [ + "abuse" + ], + "remarks": [ + { + "title": "Registration Comments", + "description": [ + "Please note that the recommended way to file abuse complaints are located in the following links.", + "", + "To report abuse and illegal activity: https://www.google.com/contact/", + "", + "For legal requests: http://support.google.com/legal ", + "", + "Regards,", + "The Google Team" + ] + } + ], + "links": [ + { + "value": "https://rdap.arin.net/registry/autnum/15169", + "rel": "self", + "type": "application/rdap+json", + "href": "https://rdap.arin.net/registry/entity/ABUSE5250-ARIN" + }, + { + "value": "https://rdap.arin.net/registry/autnum/15169", + "rel": "alternate", + "type": "application/xml", + "href": "https://whois.arin.net/rest/poc/ABUSE5250-ARIN" + } + ], + "events": [ + { + "eventAction": "last changed", + "eventDate": "2022-10-24T08:43:11-04:00" + }, + { + "eventAction": "registration", + "eventDate": "2015-11-06T15:36:35-05:00" + } + ], + "status": [ + "validated" + ], + "port43": "whois.arin.net", + "objectClassName": "entity" + }, + { + "handle": "ZG39-ARIN", + "vcardArray": [ + "vcard", + [ + [ + "version", + {}, + "text", + "4.0" + ], + [ + "adr", + { + "label": "1600 Amphitheatre Parkway\nMountain View\nCA\n94043\nUnited States" + }, + "text", + [ + "", + "", + "", + "", + "", + "", + "" + ] + ], + [ + "fn", + {}, + "text", + "Google LLC" + ], + [ + "org", + {}, + "text", + "Google LLC" + ], + [ + "kind", + {}, + "text", + "group" + ], + [ + "email", + {}, + "text", + "arin-contact@google.com" + ], + [ + "tel", + { + "type": [ + "work", + "voice" + ] + }, + "text", + "+1-650-253-0000" + ] + ] + ], + "roles": [ + "technical", + "administrative" + ], + "links": [ + { + "value": "https://rdap.arin.net/registry/autnum/15169", + "rel": "self", + "type": "application/rdap+json", + "href": "https://rdap.arin.net/registry/entity/ZG39-ARIN" + }, + { + "value": "https://rdap.arin.net/registry/autnum/15169", + "rel": "alternate", + "type": "application/xml", + "href": "https://whois.arin.net/rest/poc/ZG39-ARIN" + } + ], + "events": [ + { + "eventAction": "last changed", + "eventDate": "2022-11-10T07:12:44-05:00" + }, + { + "eventAction": "registration", + "eventDate": "2000-11-30T13:54:08-05:00" + } + ], + "status": [ + "validated" + ], + "port43": "whois.arin.net", + "objectClassName": "entity" + } + ], + "port43": "whois.arin.net", + "objectClassName": "entity" + }, + { + "handle": "ZG39-ARIN", + "vcardArray": [ + "vcard", + [ + [ + "version", + {}, + "text", + "4.0" + ], + [ + "adr", + { + "label": "1600 Amphitheatre Parkway\nMountain View\nCA\n94043\nUnited States" + }, + "text", + [ + "", + "", + "", + "", + "", + "", + "" + ] + ], + [ + "fn", + {}, + "text", + "Google LLC" + ], + [ + "org", + {}, + "text", + "Google LLC" + ], + [ + "kind", + {}, + "text", + "group" + ], + [ + "email", + {}, + "text", + "arin-contact@google.com" + ], + [ + "tel", + { + "type": [ + "work", + "voice" + ] + }, + "text", + "+1-650-253-0000" + ] + ] + ], + "roles": [ + "technical" + ], + "links": [ + { + "value": "https://rdap.arin.net/registry/autnum/15169", + "rel": "self", + "type": "application/rdap+json", + "href": "https://rdap.arin.net/registry/entity/ZG39-ARIN" + }, + { + "value": "https://rdap.arin.net/registry/autnum/15169", + "rel": "alternate", + "type": "application/xml", + "href": "https://whois.arin.net/rest/poc/ZG39-ARIN" + } + ], + "events": [ + { + "eventAction": "last changed", + "eventDate": "2022-11-10T07:12:44-05:00" + }, + { + "eventAction": "registration", + "eventDate": "2000-11-30T13:54:08-05:00" + } + ], + "status": [ + "validated" + ], + "port43": "whois.arin.net", + "objectClassName": "entity" + } + ], + "port43": "whois.arin.net", + "status": [ + "active" + ], + "objectClassName": "autnum" +} +``` + +
+ ## Go docs [![godoc](https://godoc.org/github.com/openrdap/rdap?status.png)](https://godoc.org/github.com/openrdap/rdap) -## Requires -Go 1.7+ +## Uses +Go 1.20+ ## Links - Wikipedia - [Registration Data Access Protocol](https://en.wikipedia.org/wiki/Registration_Data_Access_Protocol) -- [ICANN RDAP pilot](https://www.icann.org/rdap) +- ICANN - [RDAP](https://www.icann.org/rdap) - [OpenRDAP](https://www.openrdap.org) - https://data.iana.org/rdap/ - Official IANA bootstrap information -- https://test.rdap.net/rdap/ - Test alternate bootstrap service with more experimental RDAP servers - [RFC 7480 HTTP Usage in the Registration Data Access Protocol (RDAP)](https://tools.ietf.org/html/rfc7480) - [RFC 7481 Security Services for the Registration Data Access Protocol (RDAP)](https://tools.ietf.org/html/rfc7481) - [RFC 7482 Registration Data Access Protocol (RDAP) Query Format](https://tools.ietf.org/html/rfc7482) - [RFC 7483 JSON Responses for the Registration Data Access Protocol (RDAP)](https://tools.ietf.org/html/rfc7483) - [RFC 7484 Finding the Authoritative Registration Data (RDAP) Service](https://tools.ietf.org/html/rfc7484) - +- [RFC 8521 Registration Data Access Protocol (RDAP) Object Tagging](https://datatracker.ietf.org/doc/rfc8521/) diff --git a/bootstrap/client.go b/bootstrap/client.go index a1955a0..4daadbe 100644 --- a/bootstrap/client.go +++ b/bootstrap/client.go @@ -430,7 +430,7 @@ func (c *Client) filenameFor(r RegistryType) string { return filename } -// Filename returns the JSON document filename: One of {asn,dns,ipv4,ipv6,service_provider}.json. +// Filename returns the JSON document filename: One of {asn,dns,ipv4,ipv6,object-tags}.json. func (r RegistryType) Filename() string { switch r { case ASN: @@ -442,8 +442,7 @@ func (r RegistryType) Filename() string { case IPv6: return "ipv6.json" case ServiceProvider: - // This is a guess and will need fixing to match whatever IANA chooses. - return "serviceprovider-draft-03.json" + return "object-tags.json" default: panic("Unknown RegistryType") } diff --git a/bootstrap/client_test.go b/bootstrap/client_test.go index e4983e9..b767ce6 100644 --- a/bootstrap/client_test.go +++ b/bootstrap/client_test.go @@ -5,7 +5,6 @@ package bootstrap import ( - "net/url" "testing" "github.com/openrdap/rdap/test" @@ -66,15 +65,9 @@ func TestLookups(t *testing.T) { }, { ServiceProvider, - "12345~VRSN", + "12345-FRNIC", true, - []string{"https://rdap.verisignlabs.com/rdap/v1"}, - }, - { - ServiceProvider, - "12345-VRSN", - true, - []string{"https://rdap.verisignlabs.com/rdap/v1"}, + []string{"https://rdap.nic.fr/"}, }, } @@ -87,10 +80,6 @@ func TestLookups(t *testing.T) { for _, test := range tests { var r *Answer - if test.Registry == ServiceProvider { - c.BaseURL, _ = url.Parse("https://test.rdap.net/rdap/") - } - question := &Question{ RegistryType: test.Registry, Query: test.Input, diff --git a/bootstrap/file.go b/bootstrap/file.go index d6e05c4..5b20a1d 100644 --- a/bootstrap/file.go +++ b/bootstrap/file.go @@ -52,13 +52,22 @@ func NewFile(jsonDocument []byte) (*File, error) { f.Entries = make(map[string][]*url.URL) for _, s := range doc.Services { - if len(s) != 2 { + var entries []string + var rawURLs []string + + switch len(s) { + case 2: + // {asn,dns,ipv4,ipv6}.json + entries = s[0] + rawURLs = s[1] + case 3: + // object-tags.json + entries = s[1] + rawURLs = s[2] + default: return nil, errors.New("Malformed bootstrap (bad services array)") } - entries := s[0] - rawURLs := s[1] - var urls []*url.URL for _, rawURL := range rawURLs { diff --git a/bootstrap/service_provider_registry.go b/bootstrap/service_provider_registry.go index 89f3b01..74077dc 100644 --- a/bootstrap/service_provider_registry.go +++ b/bootstrap/service_provider_registry.go @@ -22,7 +22,7 @@ type ServiceProviderRegistry struct { // Provider JSON document. // // The document format is specified in -// https://datatracker.ietf.org/doc/draft-hollenbeck-regext-rdap-object-tag/. +// https://datatracker.ietf.org/doc/rfc8521/. func NewServiceProviderRegistry(json []byte) (*ServiceProviderRegistry, error) { var r *File r, err := NewFile(json) @@ -50,11 +50,7 @@ func (s *ServiceProviderRegistry) Lookup(question *Question) (*Answer, error) { input := question.Query // Valid input looks like 12345-VRSN. - offset := strings.LastIndexByte(input, '~') - - if offset == -1 { - offset = strings.LastIndexByte(input, '-') - } + offset := strings.LastIndexByte(input, '-') if offset == -1 || offset == len(input)-1 { return &Answer{ diff --git a/bootstrap/service_provider_registry_test.go b/bootstrap/service_provider_registry_test.go index ca32261..a3ddf9a 100644 --- a/bootstrap/service_provider_registry_test.go +++ b/bootstrap/service_provider_registry_test.go @@ -11,10 +11,10 @@ import ( ) func TestServiceProviderRegistryLookups(t *testing.T) { - test.Start(test.BootstrapExperimental) + test.Start(test.Bootstrap) defer test.Finish() - var bytes []byte = test.Get("https://test.rdap.net/rdap/serviceprovider-draft-03.json") + var bytes []byte = test.Get("https://data.iana.org/rdap/object-tags.json") var s *ServiceProviderRegistry s, err := NewServiceProviderRegistry(bytes) @@ -31,58 +31,28 @@ func TestServiceProviderRegistryLookups(t *testing.T) { []string{}, }, { - "~", + "12345-FRNIC", false, - "", - []string{}, - }, - { - "X~VRSN~", - false, - "", - []string{}, - }, - { - "12345~VRSN", - false, - "VRSN", - []string{"https://rdap.verisignlabs.com/rdap/v1"}, - }, - { - "*~VRSN", - false, - "VRSN", - []string{"https://rdap.verisignlabs.com/rdap/v1"}, - }, - { - "~VRSN", - false, - "VRSN", - []string{"https://rdap.verisignlabs.com/rdap/v1"}, - }, - { - "12345-VRSN", - false, - "VRSN", - []string{"https://rdap.verisignlabs.com/rdap/v1"}, + "FRNIC", + []string{"https://rdap.nic.fr/"}, }, { - "*-VRSN", + "*-FRNIC", false, - "VRSN", - []string{"https://rdap.verisignlabs.com/rdap/v1"}, + "FRNIC", + []string{"https://rdap.nic.fr/"}, }, { - "-VRSN", + "-FRNIC", false, - "VRSN", - []string{"https://rdap.verisignlabs.com/rdap/v1"}, + "FRNIC", + []string{"https://rdap.nic.fr/"}, }, { - "A-B-VRSN", + "A-B-FRNIC", false, - "VRSN", - []string{"https://rdap.verisignlabs.com/rdap/v1"}, + "FRNIC", + []string{"https://rdap.nic.fr/"}, }, } diff --git a/cli.go b/cli.go index 3038fe2..b874b97 100644 --- a/cli.go +++ b/cli.go @@ -12,7 +12,6 @@ import ( "net" "net/http" "net/url" - "os" "strconv" "strings" "time" @@ -23,42 +22,33 @@ import ( "golang.org/x/crypto/pkcs12" - kingpin "gopkg.in/alecthomas/kingpin.v2" + kingpin "github.com/alecthomas/kingpin/v2" ) var ( - version = "OpenRDAP v0.0.1" + version = "OpenRDAP v0.9.1" usageText = version + ` (www.openrdap.org) Usage: rdap [OPTIONS] DOMAIN|IP|ASN|ENTITY|NAMESERVER|RDAP-URL - e.g. rdap example.cz + e.g. rdap example.com rdap 192.0.2.0 rdap 2001:db8:: rdap AS2856 + rdap OPS4-RIPE rdap https://rdap.nic.cz/domain/example.cz - rdap -f registrant -f administrative -f billing amazon.com.br rdap --json https://rdap.nic.cz/domain/example.cz rdap -s https://rdap.nic.cz -t help Options: -h, --help Show help message. + -V, --version Print version and quit. -v, --verbose Print verbose messages on STDERR. -T, --timeout=SECS Timeout after SECS seconds (default: 30). -k, --insecure Disable SSL certificate verification. - -e, --experimental Enable some experimental options: - - Use the bootstrap service https://test.rdap.net/rdap - - Enable object tag support - -Authentication options: - -P, --p12=cert.p12[:password] Use client certificate & private key (PKCS#12 format) -or: - -C, --cert=cert.pem Use client certificate (PEM format) - -K, --key=cert.key Use client private key (PEM format) - Output Options: --text Output RDAP, plain text "tree" format (default). -w, --whois Output WHOIS style (domain queries only). @@ -93,10 +83,12 @@ Advanced options (bootstrapping): --bs-url=URL Bootstrap service URL (default: https://data.iana.org/rdap) --bs-ttl=SECS Bootstrap cache time in seconds (default: 3600) -Advanced options (experiments): - --exp=test_rdap_net Use the bootstrap service https://test.rdap.net/rdap - --exp=object_tag Enable object tag support - (draft-hollenbeck-regext-rdap-object-tag) +Advanced options (authentication): + -P, --p12=cert.p12[:password] Use client certificate & private key (PKCS#12 format) +or: + -C, --cert=cert.pem Use client certificate (PEM format) + -K, --key=cert.key Use client private key (PEM format) + ` ) @@ -142,6 +134,7 @@ func RunCLI(args []string, stdout io.Writer, stderr io.Writer, options CLIOption // Command line options. verboseFlag := app.Flag("verbose", "").Short('v').Bool() + versionFlag := app.Flag("version", "").Short('V').Bool() timeoutFlag := app.Flag("timeout", "").Short('T').Default("30").Uint16() insecureFlag := app.Flag("insecure", "").Short('k').Bool() @@ -179,6 +172,12 @@ func RunCLI(args []string, stdout io.Writer, stderr io.Writer, options CLIOption return 1 } + // Print version string? + if *versionFlag { + fmt.Fprintln(stdout, version) + return 0 + } + var verbose func(text string) if *verboseFlag { verbose = func(text string) { @@ -214,9 +213,8 @@ func RunCLI(args []string, stdout io.Writer, stderr io.Writer, options CLIOption // Enable the -e selection of experiments? if *experimentalFlag { - verbose("rdap: Enabled -e/--experiments: test_rdap_net, object_tag") + verbose("rdap: Enabled -e/--experiments: test_rdap_net") experiments["test_rdap_net"] = true - experiments["object_tag"] = true } // Forced sandbox mode? @@ -461,6 +459,7 @@ func RunCLI(args []string, stdout io.Writer, stderr io.Writer, options CLIOption // Custom HTTP client. Used to disable TLS certificate verification. transport := &http.Transport{ + Proxy: http.ProxyFromEnvironment, TLSClientConfig: tlsConfig, } @@ -476,9 +475,8 @@ func RunCLI(args []string, stdout io.Writer, stderr io.Writer, options CLIOption HTTP: httpClient, Bootstrap: bs, - Verbose: verbose, - UserAgent: version, - ServiceProviderExperiment: experiments["object_tag"], + Verbose: verbose, + UserAgent: version, } if *insecureFlag { @@ -526,14 +524,14 @@ func RunCLI(args []string, stdout io.Writer, stderr io.Writer, options CLIOption // Print the raw response out? if *outputFormatRaw { - fmt.Printf("%s", resp.HTTP[0].Body) + fmt.Fprintf(stdout, "%s", resp.HTTP[0].Body) } // Print the response, JSON pretty-printed? if *outputFormatJSON { var out bytes.Buffer json.Indent(&out, resp.HTTP[0].Body, "", " ") - out.WriteTo(os.Stdout) + out.WriteTo(stdout) } // Print WHOIS style response out? diff --git a/client.go b/client.go index c3319c6..5b45cf3 100644 --- a/client.go +++ b/client.go @@ -20,54 +20,58 @@ import ( // This client executes RDAP requests, and returns the responses as Go values. // // Quick usage: -// client := &rdap.Client{} -// domain, err := client.QueryDomain("example.cz") // -// if err == nil { -// fmt.Printf("Handle=%s Domain=%s\n", domain.Handle, domain.LDHName) -// } +// client := &rdap.Client{} +// domain, err := client.QueryDomain("example.cz") +// +// if err == nil { +// fmt.Printf("Handle=%s Domain=%s\n", domain.Handle, domain.LDHName) +// } +// // The QueryDomain(), QueryAutnum(), and QueryIP() methods all provide full contact information, and timeout after 30s. // // Normal usage: -// // Query example.cz. -// req := &rdap.Request{ -// Type: rdap.DomainRequest, -// Query: "example.cz", -// } // -// client := &rdap.Client{} -// resp, err := client.Do(req) +// // Query example.cz. +// req := &rdap.Request{ +// Type: rdap.DomainRequest, +// Query: "example.cz", +// } +// +// client := &rdap.Client{} +// resp, err := client.Do(req) // -// if domain, ok := resp.Object.(*rdap.Domain); ok { -// fmt.Printf("Handle=%s Domain=%s\n", domain.Handle, domain.LDHName) -// } +// if domain, ok := resp.Object.(*rdap.Domain); ok { +// fmt.Printf("Handle=%s Domain=%s\n", domain.Handle, domain.LDHName) +// } // // Advanced usage: // // This demonstrates custom FetchRoles, a custom Context, a custom HTTP client, // a custom Bootstrapper, and a custom timeout. -// // Nameserver query on rdap.nic.cz. -// server, _ := url.Parse("https://rdap.nic.cz") -// req := &rdap.Request{ -// Type: rdap.NameserverRequest, -// Query: "a.ns.nic.cz", -// FetchRoles: []string{"all"}, -// Timeout: time.Second * 45, // Custom timeout. // -// Server: server, -// } +// // Nameserver query on rdap.nic.cz. +// server, _ := url.Parse("https://rdap.nic.cz") +// req := &rdap.Request{ +// Type: rdap.NameserverRequest, +// Query: "a.ns.nic.cz", +// FetchRoles: []string{"all"}, +// Timeout: time.Second * 45, // Custom timeout. // -// req = req.WithContext(ctx) // Custom context (see https://blog.golang.org/context). +// Server: server, +// } // -// client := &rdap.Client{} -// client.HTTP = &http.Client{} // Custom HTTP client. -// client.Bootstrap = &bootstrap.Client{} // Custom bootstapper. +// req = req.WithContext(ctx) // Custom context (see https://blog.golang.org/context). // -// resp, err := client.Do(req) +// client := &rdap.Client{} +// client.HTTP = &http.Client{} // Custom HTTP client. +// client.Bootstrap = &bootstrap.Client{} // Custom bootstapper. // -// if ns, ok := resp.Object.(*rdap.Nameserver); ok { -// fmt.Printf("Handle=%s Domain=%s\n", ns.Handle, ns.LDHName) -// } +// resp, err := client.Do(req) +// +// if ns, ok := resp.Object.(*rdap.Nameserver); ok { +// fmt.Printf("Handle=%s Domain=%s\n", ns.Handle, ns.LDHName) +// } type Client struct { HTTP *http.Client Bootstrap *bootstrap.Client @@ -75,8 +79,11 @@ type Client struct { // Optional callback function for verbose messages. Verbose func(text string) + UserAgent string + + // Service Provider support is now always enabled. + // This field is ignored. ServiceProviderExperiment bool - UserAgent string } func (c *Client) Do(req *Request) (*Response, error) { @@ -123,7 +130,7 @@ func (c *Client) Do(req *Request) (*Response, error) { var bootstrapType *bootstrap.RegistryType = bootstrapTypeFor(req) - if bootstrapType == nil || (*bootstrapType == bootstrap.ServiceProvider && !c.ServiceProviderExperiment) { + if bootstrapType == nil { return nil, &ClientError{ Type: BootstrapNotSupported, Text: fmt.Sprintf("Cannot run query type '%s' without a server URL, "+ diff --git a/decode_data.go b/decode_data.go index 2cb1554..9f05a60 100644 --- a/decode_data.go +++ b/decode_data.go @@ -60,7 +60,6 @@ func (r DecodeData) Notes(name string) []string { // // |name| is the RDAP field name (not the Go field name), so "port43", not // "Port43". For a full list of decoded field names, use Fields(). -// func (r DecodeData) Value(name string) interface{} { if v, ok := r.values[name]; ok { return v diff --git a/decoder.go b/decoder.go index 6dba633..1d57d34 100644 --- a/decoder.go +++ b/decoder.go @@ -20,36 +20,37 @@ import ( // // To decode an RDAP response: // -// jsonBlob := []byte(` -// { -// "objectClassName": "domain", -// "rdapConformance": ["rdap_level_0"], -// "handle": "EXAMPLECOM", -// "ldhName": "example.com", -// "entities": [] -// } -// `) -// -// d := rdap.NewDecoder(jsonBlob) -// result, err := d.Decode() -// -// if err != nil { -// if domain, ok := result.(*rdap.Domain); ok { -// fmt.Printf("Domain name = %s\n", domain.LDHName) -// } -// } +// jsonBlob := []byte(` +// { +// "objectClassName": "domain", +// "rdapConformance": ["rdap_level_0"], +// "handle": "EXAMPLECOM", +// "ldhName": "example.com", +// "entities": [] +// } +// `) +// +// d := rdap.NewDecoder(jsonBlob) +// result, err := d.Decode() +// +// if err != nil { +// if domain, ok := result.(*rdap.Domain); ok { +// fmt.Printf("Domain name = %s\n", domain.LDHName) +// } +// } // // RDAP responses are decoded into the following types: -// &rdap.Error{} - Responses with an errorCode value. -// &rdap.Autnum{} - Responses with objectClassName="autnum". -// &rdap.Domain{} - Responses with objectClassName="domain". -// &rdap.Entity{} - Responses with objectClassName="entity". -// &rdap.IPNetwork{} - Responses with objectClassName="ip network". -// &rdap.Nameserver{} - Responses with objectClassName="nameserver". -// &rdap.DomainSearchResults{} - Responses with a domainSearchResults array. -// &rdap.EntitySearchResults{} - Responses with a entitySearchResults array. -// &rdap.NameserverSearchResults{} - Responses with a nameserverSearchResults array. -// &rdap.Help{} - All other valid JSON responses. +// +// &rdap.Error{} - Responses with an errorCode value. +// &rdap.Autnum{} - Responses with objectClassName="autnum". +// &rdap.Domain{} - Responses with objectClassName="domain". +// &rdap.Entity{} - Responses with objectClassName="entity". +// &rdap.IPNetwork{} - Responses with objectClassName="ip network". +// &rdap.Nameserver{} - Responses with objectClassName="nameserver". +// &rdap.DomainSearchResults{} - Responses with a domainSearchResults array. +// &rdap.EntitySearchResults{} - Responses with a entitySearchResults array. +// &rdap.NameserverSearchResults{} - Responses with a nameserverSearchResults array. +// &rdap.Help{} - All other valid JSON responses. // // Note that an RDAP server may return a different response type than expected. // @@ -95,16 +96,17 @@ func NewDecoder(jsonBlob []byte, opts ...DecoderOption) *Decoder { // returned. // // The possible results are: -// &rdap.Error{} - Responses with an errorCode value. -// &rdap.Autnum{} - Responses with objectClassName="autnum". -// &rdap.Domain{} - Responses with objectClassName="domain". -// &rdap.Entity{} - Responses with objectClassName="entity". -// &rdap.IPNetwork{} - Responses with objectClassName="ip network". -// &rdap.Nameserver{} - Responses with objectClassName="nameserver". -// &rdap.DomainSearchResults{} - Responses with a domainSearchResults array. -// &rdap.EntitySearchResults{} - Responses with a entitySearchResults array. -// &rdap.NameserverSearchResults{} - Responses with a nameserverSearchResults array. -// &rdap.Help{} - All other valid JSON responses. +// +// &rdap.Error{} - Responses with an errorCode value. +// &rdap.Autnum{} - Responses with objectClassName="autnum". +// &rdap.Domain{} - Responses with objectClassName="domain". +// &rdap.Entity{} - Responses with objectClassName="entity". +// &rdap.IPNetwork{} - Responses with objectClassName="ip network". +// &rdap.Nameserver{} - Responses with objectClassName="nameserver". +// &rdap.DomainSearchResults{} - Responses with a domainSearchResults array. +// &rdap.EntitySearchResults{} - Responses with a entitySearchResults array. +// &rdap.NameserverSearchResults{} - Responses with a nameserverSearchResults array. +// &rdap.Help{} - All other valid JSON responses. // // On serious errors (e.g. JSON syntax error) an error is returned. Otherwise, // decoding is performed on a best-effort basis, and "minor errors" (such as @@ -737,7 +739,7 @@ func (d *Decoder) decodePtr(keyName string, src interface{}, dst reflect.Value, var err error if dst.Type().Elem().Name() == "VCard" { - vcard, vcardError := newVCardImpl(src) + vcard, vcardError := newVCardImpl(src, VCardOptions{}) if vcardError == nil { dst.Set(reflect.ValueOf(vcard)) diff --git a/doc.go b/doc.go index bfb07c4..1814c81 100644 --- a/doc.go +++ b/doc.go @@ -9,27 +9,30 @@ // This client executes RDAP queries and returns the responses as Go values. // // Quick usage: -// client := &rdap.Client{} -// domain, err := client.QueryDomain("example.cz") // -// if err == nil { -// fmt.Printf("Handle=%s Domain=%s\n", domain.Handle, domain.LDHName) -// } +// client := &rdap.Client{} +// domain, err := client.QueryDomain("example.cz") +// +// if err == nil { +// fmt.Printf("Handle=%s Domain=%s\n", domain.Handle, domain.LDHName) +// } +// // The QueryDomain(), QueryAutnum(), and QueryIP() methods all provide full contact information, and timeout after 30s. // // Normal usage: -// // Query example.cz. -// req := &rdap.Request{ -// Type: rdap.DomainRequest, -// Query: "example.cz", -// } -// -// client := &rdap.Client{} -// resp, err := client.Do(req) -// -// if domain, ok := resp.Object.(*rdap.Domain); ok { -// fmt.Printf("Handle=%s Domain=%s\n", domain.Handle, domain.LDHName) -// } +// +// // Query example.cz. +// req := &rdap.Request{ +// Type: rdap.DomainRequest, +// Query: "example.cz", +// } +// +// client := &rdap.Client{} +// resp, err := client.Do(req) +// +// if domain, ok := resp.Object.(*rdap.Domain); ok { +// fmt.Printf("Handle=%s Domain=%s\n", domain.Handle, domain.LDHName) +// } // // As of June 2017, all five number registries (AFRINIC, ARIN, APNIC, LANIC, // RIPE) run RDAP servers. A small number of TLDs (top level domains) support diff --git a/go.mod b/go.mod index 5e7c596..ed0e5fe 100644 --- a/go.mod +++ b/go.mod @@ -1,14 +1,16 @@ module github.com/openrdap/rdap -go 1.12 +go 1.19 require ( - github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect - github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4 // indirect + github.com/alecthomas/kingpin/v2 v2.3.2 github.com/davecgh/go-spew v1.1.1 - github.com/jarcoal/httpmock v1.0.4 + github.com/jarcoal/httpmock v1.3.0 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 - gopkg.in/alecthomas/kingpin.v2 v2.2.6 + golang.org/x/crypto v0.17.0 +) + +require ( + github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect + github.com/xhit/go-str2duration/v2 v2.1.0 // indirect ) diff --git a/go.sum b/go.sum index 6ea77af..941e832 100644 --- a/go.sum +++ b/go.sum @@ -1,25 +1,24 @@ -github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM= -github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4 h1:Hs82Z41s6SdL1CELW+XaDYmOH4hkBN4/N9og/AsOv7E= -github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/kingpin/v2 v2.3.2 h1:H0aULhgmSzN8xQ3nX1uxtdlTHYoPLu5AhHxWrKI6ocU= +github.com/alecthomas/kingpin/v2 v2.3.2/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE= +github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 h1:s6gZFSlWYmbqAuRjVTiNNhvNRfY2Wxp9nhfyel4rklc= +github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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/jarcoal/httpmock v1.3.0 h1:2RJ8GP0IIaWwcC9Fp2BmVi8Kog3v2Hn7VXM3fTd+nuc= +github.com/jarcoal/httpmock v1.3.0/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg= +github.com/maxatome/go-testdeep v1.12.0 h1:Ql7Go8Tg0C1D/uMMX59LAoYK7LffeJQ6X2T04nTH68g= 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= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 h1:HuIa8hRrWRSrqYzx1qI49NNxhdi2PrY7gxVSq1JjLDc= -golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc= -gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= +github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc= +github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU= +golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= +golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/request.go b/request.go index 20cd2c7..275467e 100644 --- a/request.go +++ b/request.go @@ -77,31 +77,31 @@ func (r RequestType) String() string { // A Request represents an RDAP request. // -// req := &rdap.Request{ -// Type: rdap.DomainRequest, -// Query: "example.cz", -// } +// req := &rdap.Request{ +// Type: rdap.DomainRequest, +// Query: "example.cz", +// } // // RDAP supports many request types. These are: // -// RequestType | Bootstrapped? | HTTP request path | Example Query -// -------------------------------------------+---------------+-------------------------+---------------- -// rdap.AutnumRequest | Yes | autnum/QUERY | AS2846 -// rdap.DomainRequest | Yes | domain/QUERY | example.cz -// rdap.EntityRequest | Experimental | entity/QUERY | 86860670-VRSN -// rdap.HelpRequest | No | help | N/A -// rdap.IPRequest | Yes | ip/QUERY | 2001:db8::1 -// rdap.NameserverRequest | No | nameserver/QUERY | ns1.skip.org -// | | | -// rdap.DomainSearchRequest | No | domains?name=QUERY | exampl*.com -// rdap.DomainSearchByNameserverRequest | No | domains?nsLdhName=QUERY | ns1.exampl*.com -// rdap.DomainSearchByNameserverIPRequest | No | domains?nsIp=QUERY | 192.0.2.0 -// rdap.NameserverSearchRequest | No | nameservers?name=QUERY | ns1.exampl*.com -// rdap.NameserverSearchByNameserverIPRequest | No | nameservers?ip=QUERY | 192.0.2.0 -// rdap.EntitySearchRequest | No | entities?fn=QUERY | ABC*-VRSN -// rdap.EntitySearchByHandleRequest | No | entities?handle=QUERY | ABC*-VRSN -// | | | -// rdap.RawRequest | N/A | N/A | N/A +// RequestType | Bootstrapped? | HTTP request path | Example Query +// -------------------------------------------+---------------+-------------------------+---------------- +// rdap.AutnumRequest | Yes | autnum/QUERY | AS2846 +// rdap.DomainRequest | Yes | domain/QUERY | example.cz +// rdap.EntityRequest | Experimental | entity/QUERY | 86860670-VRSN +// rdap.HelpRequest | No | help | N/A +// rdap.IPRequest | Yes | ip/QUERY | 2001:db8::1 +// rdap.NameserverRequest | No | nameserver/QUERY | ns1.skip.org +// | | | +// rdap.DomainSearchRequest | No | domains?name=QUERY | exampl*.com +// rdap.DomainSearchByNameserverRequest | No | domains?nsLdhName=QUERY | ns1.exampl*.com +// rdap.DomainSearchByNameserverIPRequest | No | domains?nsIp=QUERY | 192.0.2.0 +// rdap.NameserverSearchRequest | No | nameservers?name=QUERY | ns1.exampl*.com +// rdap.NameserverSearchByNameserverIPRequest | No | nameservers?ip=QUERY | 192.0.2.0 +// rdap.EntitySearchRequest | No | entities?fn=QUERY | ABC*-VRSN +// rdap.EntitySearchByHandleRequest | No | entities?handle=QUERY | ABC*-VRSN +// | | | +// rdap.RawRequest | N/A | N/A | N/A // // See https://tools.ietf.org/html/rfc7482 for more information on RDAP request // types. @@ -112,21 +112,22 @@ func (r RequestType) String() string { // // For other Request types, you must specify the RDAP server: // -// // Nameserver query on rdap.nic.cz. -// server, _ := url.Parse("https://rdap.nic.cz") -// req := &rdap.Request{ -// Type: rdap.NameserverRequest, -// Query: "a.ns.nic.cz", +// // Nameserver query on rdap.nic.cz. +// server, _ := url.Parse("https://rdap.nic.cz") +// req := &rdap.Request{ +// Type: rdap.NameserverRequest, +// Query: "a.ns.nic.cz", // -// Server: server, -// } +// Server: server, +// } // // RawRequest is a special case for existing RDAP request URLs: -// rdapURL, _ := url.Parse("https://rdap.example/mystery/query?ip=192.0.2.0") -// req := &rdap.Request{ -// Type: rdap.RawRequest, -// Server: rdapURL, -// } +// +// rdapURL, _ := url.Parse("https://rdap.example/mystery/query?ip=192.0.2.0") +// req := &rdap.Request{ +// Type: rdap.RawRequest, +// Server: rdapURL, +// } type Request struct { // Request type. Type RequestType @@ -227,15 +228,16 @@ func (r *Request) pathAndValues() (string, url.Values) { // URL constructs and returns the RDAP Request URL. // // As an example: -// server, _ := url.Parse("https://rdap.nic.cz") -// req := &rdap.Request{ -// Type: rdap.NameserverRequest, -// Query: "a.ns.nic.cz", // -// Server: server, -// } +// server, _ := url.Parse("https://rdap.nic.cz") +// req := &rdap.Request{ +// Type: rdap.NameserverRequest, +// Query: "a.ns.nic.cz", +// +// Server: server, +// } // -// fmt.Println(req.URL()) // Prints https://rdap.nic.cz/nameserver/a.ns.nic.cz. +// fmt.Println(req.URL()) // Prints https://rdap.nic.cz/nameserver/a.ns.nic.cz. // // Returns nil if the Server field is nil. // @@ -431,11 +433,11 @@ func NewRequest(requestType RequestType, query string) *Request { // NewAutoRequest creates a Request by guessing the type required for |queryText|. // // The following types are suppported: -// - RawRequest - e.g. https://example.com/domain/example2.com -// - DomainRequest - e.g. example.com, https://example.com, http://example.com/ -// - IPRequest - e.g. 192.0.2.0, 2001:db8::, 192.0.2.0/24, 2001:db8::/128 -// - AutnumRequest - e.g. AS2856, 5400 -// - EntityRequest - all other queries. +// - RawRequest - e.g. https://example.com/domain/example2.com +// - DomainRequest - e.g. example.com, https://example.com, http://example.com/ +// - IPRequest - e.g. 192.0.2.0, 2001:db8::, 192.0.2.0/24, 2001:db8::/128 +// - AutnumRequest - e.g. AS2856, 5400 +// - EntityRequest - all other queries. // // Returns a Request. Use r.Type to find the RequestType chosen. func NewAutoRequest(queryText string) *Request { diff --git a/test/http.go b/test/http.go index dd9a91f..c8ca93b 100644 --- a/test/http.go +++ b/test/http.go @@ -82,10 +82,7 @@ func loadTestDatasets() { load(Bootstrap, 200, "https://data.iana.org/rdap/dns.json", "bootstrap/dns.json") load(Bootstrap, 200, "https://data.iana.org/rdap/ipv4.json", "bootstrap/ipv4.json") load(Bootstrap, 200, "https://data.iana.org/rdap/ipv6.json", "bootstrap/ipv6.json") - - // Experimental bootstrap file for service providers. - // https://datatracker.ietf.org/doc/draft-hollenbeck-regext-rdap-object-tag/ . - load(BootstrapExperimental, 200, "https://test.rdap.net/rdap/serviceprovider-draft-03.json", "bootstrap_experimental/service_provider.json") + load(Bootstrap, 200, "https://data.iana.org/rdap/object-tags.json", "bootstrap/object-tags.json") // Malformed bootstrap files. load(BootstrapMalformed, 200, "https://www.example.org/dns_bad_services.json", "bootstrap_malformed/dns_bad_services.json") diff --git a/test/testdata/bootstrap/object-tags.json b/test/testdata/bootstrap/object-tags.json new file mode 100644 index 0000000..8024d76 --- /dev/null +++ b/test/testdata/bootstrap/object-tags.json @@ -0,0 +1,63 @@ +{ + "description": "RDAP bootstrap file for service provider object tags", + "publication": "2022-12-29T04:00:02Z", + "services": [ + [ + [ + "info@arin.net" + ], + [ + "ARIN" + ], + [ + "https://rdap.arin.net/registry/", + "http://rdap.arin.net/registry/" + ] + ], + [ + [ + "carlos@lacnic.net" + ], + [ + "LACNIC" + ], + [ + "https://rdap.lacnic.net/rdap/" + ] + ], + [ + [ + "bje@apnic.net" + ], + [ + "APNIC" + ], + [ + "https://rdap.apnic.net/" + ] + ], + [ + [ + "kranjbar@ripe.net" + ], + [ + "RIPE" + ], + [ + "https://rdap.db.ripe.net/" + ] + ], + [ + [ + "tld-tech@nic.fr" + ], + [ + "FRNIC" + ], + [ + "https://rdap.nic.fr/" + ] + ] + ], + "version": "1.0" +} \ No newline at end of file diff --git a/test/testdata/bootstrap_experimental/service_provider.json b/test/testdata/bootstrap_experimental/service_provider.json deleted file mode 100644 index c16177b..0000000 --- a/test/testdata/bootstrap_experimental/service_provider.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "version": "1.0", - "publication": "2017-04-26T00:00:00Z", - "description": "RDAP service provider bootstrap values (experimental, hosted on openrdap.org)", - "services": [ - [ - ["VRSN"], - [ - "https://rdap.verisignlabs.com/rdap/v1" - ] - ] - ] -} - diff --git a/test/testdata/jcard/error_invalid_properties.json b/test/testdata/jcard/error_invalid_properties.json new file mode 100644 index 0000000..aa418f8 --- /dev/null +++ b/test/testdata/jcard/error_invalid_properties.json @@ -0,0 +1,10 @@ +["vcard", + [ + ["lang", { "pref": "1" }, "language-tag", "fr"], + ["lang", { "pref": "2" }, "language-tag", "pt"], + ["lang", { "pref": "3" }, "language-tag", "de"], + ["lang", { "pref": "4" }, "language-tag", "es"], + + ["lang", {"type": "language-tag"}, "en"] + ] +] diff --git a/vcard.go b/vcard.go index 78a80e2..a1f3695 100644 --- a/vcard.go +++ b/vcard.go @@ -24,19 +24,20 @@ import ( // telephone numbers. RFC6350 documents a set of standard properties. // // RFC7095 describes the JSON document format, which looks like: -// ["vcard", [ -// [ -// ["version", {}, "text", "4.0"], -// ["fn", {}, "text", "Joe Appleseed"], -// ["tel", { -// "type":["work", "voice"], -// }, -// "uri", -// "tel:+1-555-555-1234;ext=555" -// ], -// ... -// ] -// ] +// +// ["vcard", [ +// [ +// ["version", {}, "text", "4.0"], +// ["fn", {}, "text", "Joe Appleseed"], +// ["tel", { +// "type":["work", "voice"], +// }, +// "uri", +// "tel:+1-555-555-1234;ext=555" +// ], +// ... +// ] +// ] type VCard struct { Properties []*VCardProperty } @@ -44,9 +45,10 @@ type VCard struct { // VCardProperty represents a single vCard property. // // Each vCard property has four fields, these are: -// Name Parameters Type Value -// ----- -------------------------- ----- ----------------------------- -// ["tel", {"type":["work", "voice"]}, "uri", "tel:+1-555-555-1234;ext=555"] +// +// Name Parameters Type Value +// ----- -------------------------- ----- ----------------------------- +// ["tel", {"type":["work", "voice"]}, "uri", "tel:+1-555-555-1234;ext=555"] type VCardProperty struct { Name string @@ -71,6 +73,14 @@ type VCardProperty struct { Value interface{} } +// VCardOptions specifies options for the VCard decoder routine. +type VCardOptions struct { + // By default, any invalid VCard property causes the entire VCard decode to fail. + // + // Set IgnoreInvalidProperties to true to silently skip any invalid properties. + IgnoreInvalidProperties bool +} + // Values returns a simplified representation of the VCardProperty value. // // This is convenient for accessing simple unstructured data (e.g. "fn", "tel"). @@ -107,10 +117,10 @@ func (p *VCardProperty) appendValueStrings(v interface{}, strings *[]string) { // String returns the vCard as a multiline human readable string. For example: // -// vCard[ -// version (type=text, parameters=map[]): [4.0] -// mixed (type=text, parameters=map[]): [abc true 42 [def false 43]] -// ] +// vCard[ +// version (type=text, parameters=map[]): [4.0] +// mixed (type=text, parameters=map[]): [abc true 42 [def false 43]] +// ] // // This is intended for debugging only, and is not machine parsable. func (v *VCard) String() string { @@ -125,7 +135,7 @@ func (v *VCard) String() string { // String returns the VCardProperty as a human readable string. For example: // -// mixed (type=text, parameters=map[]): [abc true 42 [def false 43]] +// mixed (type=text, parameters=map[]): [abc true 42 [def false 43]] // // This is intended for debugging only, and is not machine parsable. func (p *VCardProperty) String() string { @@ -133,7 +143,20 @@ func (p *VCardProperty) String() string { } // NewVCard creates a VCard from jsonBlob. +// +// Default options are used for the VCard decoder (see NewVCardWithOptions). func NewVCard(jsonBlob []byte) (*VCard, error) { + vcard, err := NewVCardWithOptions(jsonBlob, VCardOptions{}) + return vcard, err +} + +// NewVCardWithOptions creates a VCard from jsonBlob. options specifies options for +// the VCard decoder. +// +// Example usage: +// +// vcard, err := NewVCardWithOptions(jsonBlob, VCardOptions{IgnoreInvalidProperties: true}) +func NewVCardWithOptions(jsonBlob []byte, options VCardOptions) (*VCard, error) { var top []interface{} err := json.Unmarshal(jsonBlob, &top) @@ -142,12 +165,12 @@ func NewVCard(jsonBlob []byte) (*VCard, error) { } var vcard *VCard - vcard, err = newVCardImpl(top) + vcard, err = newVCardImpl(top, options) return vcard, err } -func newVCardImpl(src interface{}) (*VCard, error) { +func newVCardImpl(src interface{}, options VCardOptions) (*VCard, error) { top, ok := src.([]interface{}) if !ok || len(top) != 2 { @@ -169,58 +192,72 @@ func newVCardImpl(src interface{}) (*VCard, error) { var p interface{} for _, p = range top[1].([]interface{}) { - var a []interface{} - var ok bool - a, ok = p.([]interface{}) - - if !ok { - return nil, vCardError("jCard property was not an array") - } else if len(a) < 4 { - return nil, vCardError("jCard property too short (>=4 array elements required)") + property, err := decodeVCardProperty(p) + + if err != nil { + if options.IgnoreInvalidProperties { + continue + } else { + return nil, err + } } - name, ok := a[0].(string) + v.Properties = append(v.Properties, property) + } - if !ok { - return nil, vCardError("jCard property name invalid") - } + return v, nil +} - var parameters map[string][]string - var err error - parameters, err = readParameters(a[1]) +func decodeVCardProperty(p interface{}) (*VCardProperty, error) { + var a []interface{} + var ok bool + a, ok = p.([]interface{}) - if err != nil { - return nil, err - } + if !ok { + return nil, vCardError("jCard property was not an array") + } else if len(a) < 4 { + return nil, vCardError("jCard property too short (>=4 array elements required)") + } - propertyType, ok := a[2].(string) + name, ok := a[0].(string) - if !ok { - return nil, vCardError("jCard property type invalid") - } + if !ok { + return nil, vCardError("jCard property name invalid") + } - var value interface{} - if len(a) == 4 { - value, err = readValue(a[3], 0) - } else { - value, err = readValue(a[3:], 0) - } + var parameters map[string][]string + var err error + parameters, err = readParameters(a[1]) - if err != nil { - return nil, err - } + if err != nil { + return nil, err + } - property := &VCardProperty{ - Name: name, - Type: propertyType, - Parameters: parameters, - Value: value, - } + propertyType, ok := a[2].(string) - v.Properties = append(v.Properties, property) + if !ok { + return nil, vCardError("jCard property type invalid") } - return v, nil + var value interface{} + if len(a) == 4 { + value, err = readValue(a[3], 0) + } else { + value, err = readValue(a[3:], 0) + } + + if err != nil { + return nil, err + } + + property := &VCardProperty{ + Name: name, + Type: propertyType, + Parameters: parameters, + Value: value, + } + + return property, nil } // Get returns a list of the vCard Properties with VCardProperty name |name|. @@ -431,6 +468,13 @@ func (v *VCard) Email() string { return v.getFirstPropertySingleString("email") } +// Org returns the VCard's org. +// +// Returns empty string if the VCard contains no organization. +func (v *VCard) Org() string { + return v.getFirstPropertySingleString("org") +} + 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..d395524 100644 --- a/vcard_test.go +++ b/vcard_test.go @@ -34,6 +34,20 @@ func TestVCardErrors(t *testing.T) { } } +func TestVCardIgnoreInvalidProperties(t *testing.T) { + json := test.LoadFile("jcard/error_invalid_properties.json") + + j1, err1 := NewVCardWithOptions(json, VCardOptions{IgnoreInvalidProperties: true}) + if j1 == nil || len(j1.Properties) != 4 || err1 != nil { + t.Errorf("jCard with ignored errors not parsed correctly\n") + } + + j2, err2 := NewVCardWithOptions(json, VCardOptions{IgnoreInvalidProperties: false}) + if j2 != nil || err2 == nil { + t.Errorf("jCard with errors unexpectedly parsed\n") + } +} + func TestVCardExample(t *testing.T) { j, err := NewVCard(test.LoadFile("jcard/example.json")) if j == nil || err != nil { @@ -143,6 +157,7 @@ func TestVCardQuickAccessors(t *testing.T) { j.Tel(), j.Fax(), j.Email(), + j.Org(), } expected := []string{ @@ -157,6 +172,7 @@ func TestVCardQuickAccessors(t *testing.T) { "tel:+1-418-656-9254;ext=102", "", "simon.perreault@viagenie.ca", + "Viagenie", } if !reflect.DeepEqual(got, expected) {