feat(dns): add resource json round-trip
This commit is contained in:
parent
dab6e84850
commit
b71552d08b
2 changed files with 447 additions and 0 deletions
|
|
@ -6,6 +6,8 @@ import (
|
|||
"bytes"
|
||||
"encoding/base32"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
|
|
@ -28,6 +30,11 @@ type Resource struct {
|
|||
Records []ResourceRecord
|
||||
}
|
||||
|
||||
// ResourceJSON represents the JSON form of a DNS resource.
|
||||
type ResourceJSON struct {
|
||||
Records []json.RawMessage `json:"records"`
|
||||
}
|
||||
|
||||
// DSRecord mirrors the JS DS payload entry.
|
||||
type DSRecord struct {
|
||||
KeyTag uint16
|
||||
|
|
@ -36,38 +43,85 @@ type DSRecord struct {
|
|||
Digest []byte
|
||||
}
|
||||
|
||||
// DSRecordJSON represents the JSON form of a DS record.
|
||||
type DSRecordJSON struct {
|
||||
Type string `json:"type"`
|
||||
KeyTag uint16 `json:"keyTag"`
|
||||
Algorithm uint8 `json:"algorithm"`
|
||||
DigestType uint8 `json:"digestType"`
|
||||
Digest string `json:"digest"`
|
||||
}
|
||||
|
||||
// NSRecord mirrors the JS NS payload entry.
|
||||
type NSRecord struct {
|
||||
NS string
|
||||
}
|
||||
|
||||
// NSRecordJSON represents the JSON form of an NS record.
|
||||
type NSRecordJSON struct {
|
||||
Type string `json:"type"`
|
||||
NS string `json:"ns"`
|
||||
}
|
||||
|
||||
// GLUE4Record mirrors the JS IPv4 glue payload entry.
|
||||
type GLUE4Record struct {
|
||||
NS string
|
||||
Address netip.Addr
|
||||
}
|
||||
|
||||
// GLUE4RecordJSON represents the JSON form of a GLUE4 record.
|
||||
type GLUE4RecordJSON struct {
|
||||
Type string `json:"type"`
|
||||
NS string `json:"ns"`
|
||||
Address string `json:"address"`
|
||||
}
|
||||
|
||||
// GLUE6Record mirrors the JS IPv6 glue payload entry.
|
||||
type GLUE6Record struct {
|
||||
NS string
|
||||
Address netip.Addr
|
||||
}
|
||||
|
||||
// GLUE6RecordJSON represents the JSON form of a GLUE6 record.
|
||||
type GLUE6RecordJSON struct {
|
||||
Type string `json:"type"`
|
||||
NS string `json:"ns"`
|
||||
Address string `json:"address"`
|
||||
}
|
||||
|
||||
// SYNTH4Record mirrors the JS synthesized IPv4 payload entry.
|
||||
type SYNTH4Record struct {
|
||||
Address netip.Addr
|
||||
}
|
||||
|
||||
// SYNTH4RecordJSON represents the JSON form of a SYNTH4 record.
|
||||
type SYNTH4RecordJSON struct {
|
||||
Type string `json:"type"`
|
||||
Address string `json:"address"`
|
||||
}
|
||||
|
||||
// SYNTH6Record mirrors the JS synthesized IPv6 payload entry.
|
||||
type SYNTH6Record struct {
|
||||
Address netip.Addr
|
||||
}
|
||||
|
||||
// SYNTH6RecordJSON represents the JSON form of a SYNTH6 record.
|
||||
type SYNTH6RecordJSON struct {
|
||||
Type string `json:"type"`
|
||||
Address string `json:"address"`
|
||||
}
|
||||
|
||||
// TXTRecord mirrors the JS TXT payload entry.
|
||||
type TXTRecord struct {
|
||||
Entries []string
|
||||
}
|
||||
|
||||
// TXTRecordJSON represents the JSON form of a TXT record.
|
||||
type TXTRecordJSON struct {
|
||||
Type string `json:"type"`
|
||||
Entries []string `json:"txt"`
|
||||
}
|
||||
|
||||
// NewResource constructs a resource with the reference default TTL.
|
||||
func NewResource() *Resource {
|
||||
return &Resource{TTL: DEFAULT_TTL}
|
||||
|
|
@ -257,6 +311,53 @@ func (r *Resource) GetDecode(raw []byte) error {
|
|||
return r.Decode(raw)
|
||||
}
|
||||
|
||||
// GetJSON converts the resource into its JSON representation.
|
||||
func (r *Resource) GetJSON() ResourceJSON {
|
||||
if r == nil {
|
||||
return ResourceJSON{}
|
||||
}
|
||||
|
||||
jsonView := ResourceJSON{
|
||||
Records: make([]json.RawMessage, 0, len(r.Records)),
|
||||
}
|
||||
|
||||
for _, record := range r.Records {
|
||||
if record == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
raw, err := json.Marshal(resourceRecordJSON(record))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
jsonView.Records = append(jsonView.Records, raw)
|
||||
}
|
||||
|
||||
return jsonView
|
||||
}
|
||||
|
||||
// FromJSON populates the resource from its JSON representation.
|
||||
func (r *Resource) FromJSON(jsonView ResourceJSON) error {
|
||||
if r == nil {
|
||||
return core.E("dns.Resource.FromJSON", "resource is required", nil)
|
||||
}
|
||||
|
||||
r.TTL = DEFAULT_TTL
|
||||
r.Records = r.Records[:0]
|
||||
|
||||
for _, raw := range jsonView.Records {
|
||||
record, err := resourceRecordFromJSON(raw)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
r.Records = append(r.Records, record)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DecodeResource decodes a raw DNS resource payload.
|
||||
func DecodeResource(raw []byte) (*Resource, error) {
|
||||
resource := NewResource()
|
||||
|
|
@ -272,6 +373,288 @@ func GetDecodeResource(raw []byte) (*Resource, error) {
|
|||
return DecodeResource(raw)
|
||||
}
|
||||
|
||||
func resourceRecordJSON(record ResourceRecord) any {
|
||||
switch rr := record.(type) {
|
||||
case DSRecord:
|
||||
return rr.GetJSON()
|
||||
case *DSRecord:
|
||||
return rr.GetJSON()
|
||||
case NSRecord:
|
||||
return rr.GetJSON()
|
||||
case *NSRecord:
|
||||
return rr.GetJSON()
|
||||
case GLUE4Record:
|
||||
return rr.GetJSON()
|
||||
case *GLUE4Record:
|
||||
return rr.GetJSON()
|
||||
case GLUE6Record:
|
||||
return rr.GetJSON()
|
||||
case *GLUE6Record:
|
||||
return rr.GetJSON()
|
||||
case SYNTH4Record:
|
||||
return rr.GetJSON()
|
||||
case *SYNTH4Record:
|
||||
return rr.GetJSON()
|
||||
case SYNTH6Record:
|
||||
return rr.GetJSON()
|
||||
case *SYNTH6Record:
|
||||
return rr.GetJSON()
|
||||
case TXTRecord:
|
||||
return rr.GetJSON()
|
||||
case *TXTRecord:
|
||||
return rr.GetJSON()
|
||||
default:
|
||||
return map[string]any{"type": "UNKNOWN"}
|
||||
}
|
||||
}
|
||||
|
||||
type resourceRecordProbe struct {
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
func resourceRecordFromJSON(raw json.RawMessage) (ResourceRecord, error) {
|
||||
var probe resourceRecordProbe
|
||||
if err := json.Unmarshal(raw, &probe); err != nil {
|
||||
return nil, core.E("dns.Resource.FromJSON", "invalid record encoding", err)
|
||||
}
|
||||
|
||||
switch probe.Type {
|
||||
case "DS":
|
||||
var jsonRecord DSRecordJSON
|
||||
if err := json.Unmarshal(raw, &jsonRecord); err != nil {
|
||||
return nil, core.E("dns.Resource.FromJSON", "invalid DS record", err)
|
||||
}
|
||||
var record DSRecord
|
||||
if err := record.FromJSON(jsonRecord); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return record, nil
|
||||
case "NS":
|
||||
var jsonRecord NSRecordJSON
|
||||
if err := json.Unmarshal(raw, &jsonRecord); err != nil {
|
||||
return nil, core.E("dns.Resource.FromJSON", "invalid NS record", err)
|
||||
}
|
||||
var record NSRecord
|
||||
if err := record.FromJSON(jsonRecord); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return record, nil
|
||||
case "GLUE4":
|
||||
var jsonRecord GLUE4RecordJSON
|
||||
if err := json.Unmarshal(raw, &jsonRecord); err != nil {
|
||||
return nil, core.E("dns.Resource.FromJSON", "invalid GLUE4 record", err)
|
||||
}
|
||||
var record GLUE4Record
|
||||
if err := record.FromJSON(jsonRecord); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return record, nil
|
||||
case "GLUE6":
|
||||
var jsonRecord GLUE6RecordJSON
|
||||
if err := json.Unmarshal(raw, &jsonRecord); err != nil {
|
||||
return nil, core.E("dns.Resource.FromJSON", "invalid GLUE6 record", err)
|
||||
}
|
||||
var record GLUE6Record
|
||||
if err := record.FromJSON(jsonRecord); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return record, nil
|
||||
case "SYNTH4":
|
||||
var jsonRecord SYNTH4RecordJSON
|
||||
if err := json.Unmarshal(raw, &jsonRecord); err != nil {
|
||||
return nil, core.E("dns.Resource.FromJSON", "invalid SYNTH4 record", err)
|
||||
}
|
||||
var record SYNTH4Record
|
||||
if err := record.FromJSON(jsonRecord); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return record, nil
|
||||
case "SYNTH6":
|
||||
var jsonRecord SYNTH6RecordJSON
|
||||
if err := json.Unmarshal(raw, &jsonRecord); err != nil {
|
||||
return nil, core.E("dns.Resource.FromJSON", "invalid SYNTH6 record", err)
|
||||
}
|
||||
var record SYNTH6Record
|
||||
if err := record.FromJSON(jsonRecord); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return record, nil
|
||||
case "TXT":
|
||||
var jsonRecord TXTRecordJSON
|
||||
if err := json.Unmarshal(raw, &jsonRecord); err != nil {
|
||||
return nil, core.E("dns.Resource.FromJSON", "invalid TXT record", err)
|
||||
}
|
||||
var record TXTRecord
|
||||
if err := record.FromJSON(jsonRecord); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return record, nil
|
||||
default:
|
||||
return nil, core.E("dns.Resource.FromJSON", "unknown record type", nil)
|
||||
}
|
||||
}
|
||||
|
||||
// GetJSON converts the DS record into its JSON representation.
|
||||
func (r DSRecord) GetJSON() DSRecordJSON {
|
||||
return DSRecordJSON{
|
||||
Type: "DS",
|
||||
KeyTag: r.KeyTag,
|
||||
Algorithm: r.Algorithm,
|
||||
DigestType: r.DigestType,
|
||||
Digest: fmt.Sprintf("%x", r.Digest),
|
||||
}
|
||||
}
|
||||
|
||||
// FromJSON populates the DS record from its JSON representation.
|
||||
func (r *DSRecord) FromJSON(jsonView DSRecordJSON) error {
|
||||
if jsonView.Type != "DS" {
|
||||
return core.E("dns.DSRecord.FromJSON", "invalid DS record type", nil)
|
||||
}
|
||||
|
||||
raw, err := hex.DecodeString(jsonView.Digest)
|
||||
if err != nil {
|
||||
return core.E("dns.DSRecord.FromJSON", "invalid digest encoding", err)
|
||||
}
|
||||
|
||||
r.KeyTag = jsonView.KeyTag
|
||||
r.Algorithm = jsonView.Algorithm
|
||||
r.DigestType = jsonView.DigestType
|
||||
r.Digest = append(r.Digest[:0], raw...)
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetJSON converts the NS record into its JSON representation.
|
||||
func (r NSRecord) GetJSON() NSRecordJSON {
|
||||
return NSRecordJSON{
|
||||
Type: "NS",
|
||||
NS: r.NS,
|
||||
}
|
||||
}
|
||||
|
||||
// FromJSON populates the NS record from its JSON representation.
|
||||
func (r *NSRecord) FromJSON(jsonView NSRecordJSON) error {
|
||||
if jsonView.Type != "NS" {
|
||||
return core.E("dns.NSRecord.FromJSON", "invalid NS record type", nil)
|
||||
}
|
||||
|
||||
r.NS = jsonView.NS
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetJSON converts the GLUE4 record into its JSON representation.
|
||||
func (r GLUE4Record) GetJSON() GLUE4RecordJSON {
|
||||
return GLUE4RecordJSON{
|
||||
Type: "GLUE4",
|
||||
NS: r.NS,
|
||||
Address: r.Address.String(),
|
||||
}
|
||||
}
|
||||
|
||||
// FromJSON populates the GLUE4 record from its JSON representation.
|
||||
func (r *GLUE4Record) FromJSON(jsonView GLUE4RecordJSON) error {
|
||||
if jsonView.Type != "GLUE4" {
|
||||
return core.E("dns.GLUE4Record.FromJSON", "invalid GLUE4 record type", nil)
|
||||
}
|
||||
|
||||
addr, err := netip.ParseAddr(jsonView.Address)
|
||||
if err != nil || !addr.Is4() {
|
||||
return core.E("dns.GLUE4Record.FromJSON", "invalid IPv4 address", err)
|
||||
}
|
||||
|
||||
r.NS = jsonView.NS
|
||||
r.Address = addr
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetJSON converts the GLUE6 record into its JSON representation.
|
||||
func (r GLUE6Record) GetJSON() GLUE6RecordJSON {
|
||||
return GLUE6RecordJSON{
|
||||
Type: "GLUE6",
|
||||
NS: r.NS,
|
||||
Address: r.Address.String(),
|
||||
}
|
||||
}
|
||||
|
||||
// FromJSON populates the GLUE6 record from its JSON representation.
|
||||
func (r *GLUE6Record) FromJSON(jsonView GLUE6RecordJSON) error {
|
||||
if jsonView.Type != "GLUE6" {
|
||||
return core.E("dns.GLUE6Record.FromJSON", "invalid GLUE6 record type", nil)
|
||||
}
|
||||
|
||||
addr, err := netip.ParseAddr(jsonView.Address)
|
||||
if err != nil || !addr.Is6() {
|
||||
return core.E("dns.GLUE6Record.FromJSON", "invalid IPv6 address", err)
|
||||
}
|
||||
|
||||
r.NS = jsonView.NS
|
||||
r.Address = addr
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetJSON converts the SYNTH4 record into its JSON representation.
|
||||
func (r SYNTH4Record) GetJSON() SYNTH4RecordJSON {
|
||||
return SYNTH4RecordJSON{
|
||||
Type: "SYNTH4",
|
||||
Address: r.Address.String(),
|
||||
}
|
||||
}
|
||||
|
||||
// FromJSON populates the SYNTH4 record from its JSON representation.
|
||||
func (r *SYNTH4Record) FromJSON(jsonView SYNTH4RecordJSON) error {
|
||||
if jsonView.Type != "SYNTH4" {
|
||||
return core.E("dns.SYNTH4Record.FromJSON", "invalid SYNTH4 record type", nil)
|
||||
}
|
||||
|
||||
addr, err := netip.ParseAddr(jsonView.Address)
|
||||
if err != nil || !addr.Is4() {
|
||||
return core.E("dns.SYNTH4Record.FromJSON", "invalid IPv4 address", err)
|
||||
}
|
||||
|
||||
r.Address = addr
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetJSON converts the SYNTH6 record into its JSON representation.
|
||||
func (r SYNTH6Record) GetJSON() SYNTH6RecordJSON {
|
||||
return SYNTH6RecordJSON{
|
||||
Type: "SYNTH6",
|
||||
Address: r.Address.String(),
|
||||
}
|
||||
}
|
||||
|
||||
// FromJSON populates the SYNTH6 record from its JSON representation.
|
||||
func (r *SYNTH6Record) FromJSON(jsonView SYNTH6RecordJSON) error {
|
||||
if jsonView.Type != "SYNTH6" {
|
||||
return core.E("dns.SYNTH6Record.FromJSON", "invalid SYNTH6 record type", nil)
|
||||
}
|
||||
|
||||
addr, err := netip.ParseAddr(jsonView.Address)
|
||||
if err != nil || !addr.Is6() {
|
||||
return core.E("dns.SYNTH6Record.FromJSON", "invalid IPv6 address", err)
|
||||
}
|
||||
|
||||
r.Address = addr
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetJSON converts the TXT record into its JSON representation.
|
||||
func (r TXTRecord) GetJSON() TXTRecordJSON {
|
||||
return TXTRecordJSON{
|
||||
Type: "TXT",
|
||||
Entries: append([]string(nil), r.Entries...),
|
||||
}
|
||||
}
|
||||
|
||||
// FromJSON populates the TXT record from its JSON representation.
|
||||
func (r *TXTRecord) FromJSON(jsonView TXTRecordJSON) error {
|
||||
if jsonView.Type != "TXT" {
|
||||
return core.E("dns.TXTRecord.FromJSON", "invalid TXT record type", nil)
|
||||
}
|
||||
|
||||
r.Entries = append(r.Entries[:0], jsonView.Entries...)
|
||||
return nil
|
||||
}
|
||||
|
||||
func encodeRecord(buf *bytes.Buffer, record ResourceRecord) error {
|
||||
switch rr := record.(type) {
|
||||
case DSRecord:
|
||||
|
|
|
|||
|
|
@ -98,6 +98,70 @@ func TestResourceEncodeDecodeRoundTrip(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestResourceJSONRoundTrip(t *testing.T) {
|
||||
resource := NewResource()
|
||||
resource.Records = []ResourceRecord{
|
||||
DSRecord{
|
||||
KeyTag: 1,
|
||||
Algorithm: 2,
|
||||
DigestType: 3,
|
||||
Digest: []byte{0xaa, 0xbb, 0xcc},
|
||||
},
|
||||
NSRecord{NS: "ns1.example."},
|
||||
GLUE4Record{NS: "ns1.example.", Address: netip.MustParseAddr("192.0.2.1")},
|
||||
GLUE6Record{NS: "ns2.example.", Address: netip.MustParseAddr("2001:db8::1")},
|
||||
SYNTH4Record{Address: netip.MustParseAddr("198.51.100.9")},
|
||||
SYNTH6Record{Address: netip.MustParseAddr("2001:db8::9")},
|
||||
TXTRecord{Entries: []string{"hello", "world"}},
|
||||
}
|
||||
|
||||
jsonView := resource.GetJSON()
|
||||
if len(jsonView.Records) != len(resource.Records) {
|
||||
t.Fatalf("GetJSON returned %d records, want %d", len(jsonView.Records), len(resource.Records))
|
||||
}
|
||||
|
||||
var decoded Resource
|
||||
if err := decoded.FromJSON(jsonView); err != nil {
|
||||
t.Fatalf("FromJSON returned error: %v", err)
|
||||
}
|
||||
|
||||
if decoded.TTL != DEFAULT_TTL {
|
||||
t.Fatalf("decoded TTL = %d, want %d", decoded.TTL, DEFAULT_TTL)
|
||||
}
|
||||
|
||||
if len(decoded.Records) != len(resource.Records) {
|
||||
t.Fatalf("decoded %d records, want %d", len(decoded.Records), len(resource.Records))
|
||||
}
|
||||
|
||||
if ds, ok := decoded.Records[0].(DSRecord); !ok || ds.KeyTag != 1 || ds.Algorithm != 2 || ds.DigestType != 3 || !bytes.Equal(ds.Digest, []byte{0xaa, 0xbb, 0xcc}) {
|
||||
t.Fatalf("decoded DS record = %#v, want keyTag=1 algorithm=2 digestType=3 digest=aabbcc", decoded.Records[0])
|
||||
}
|
||||
|
||||
if ns, ok := decoded.Records[1].(NSRecord); !ok || ns.NS != "ns1.example." {
|
||||
t.Fatalf("decoded NS record = %#v, want ns1.example.", decoded.Records[1])
|
||||
}
|
||||
|
||||
if glue4, ok := decoded.Records[2].(GLUE4Record); !ok || glue4.NS != "ns1.example." || glue4.Address.String() != "192.0.2.1" {
|
||||
t.Fatalf("decoded GLUE4 record = %#v, want ns1.example./192.0.2.1", decoded.Records[2])
|
||||
}
|
||||
|
||||
if glue6, ok := decoded.Records[3].(GLUE6Record); !ok || glue6.NS != "ns2.example." || glue6.Address.String() != "2001:db8::1" {
|
||||
t.Fatalf("decoded GLUE6 record = %#v, want ns2.example./2001:db8::1", decoded.Records[3])
|
||||
}
|
||||
|
||||
if synth4, ok := decoded.Records[4].(SYNTH4Record); !ok || synth4.Address.String() != "198.51.100.9" {
|
||||
t.Fatalf("decoded SYNTH4 record = %#v, want 198.51.100.9", decoded.Records[4])
|
||||
}
|
||||
|
||||
if synth6, ok := decoded.Records[5].(SYNTH6Record); !ok || synth6.Address.String() != "2001:db8::9" {
|
||||
t.Fatalf("decoded SYNTH6 record = %#v, want 2001:db8::9", decoded.Records[5])
|
||||
}
|
||||
|
||||
if txt, ok := decoded.Records[6].(TXTRecord); !ok || len(txt.Entries) != 2 || txt.Entries[0] != "hello" || txt.Entries[1] != "world" {
|
||||
t.Fatalf("decoded TXT record = %#v, want [hello world]", decoded.Records[6])
|
||||
}
|
||||
}
|
||||
|
||||
func TestResourceTypeHelpers(t *testing.T) {
|
||||
resource := NewResource()
|
||||
resource.Records = []ResourceRecord{
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue