go-dns/hsd.go
Virgil 50b2394fdd feat(hsd): parse single-value name resource records
Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-03 23:57:02 +00:00

324 lines
7.2 KiB
Go

package dns
import (
"bytes"
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"strings"
)
const defaultHSDJSONRPCVersion = "1.0"
// HSDClientOptions configures JSON-RPC access to HSD.
//
// client := dns.NewHSDClient(dns.HSDClientOptions{
// URL: "http://127.0.0.1:14037",
// Username: "user",
// Password: "pass",
// })
type HSDClientOptions struct {
URL string
Username string
Password string
HTTPClient *http.Client
}
type HSDClient struct {
baseURL string
username string
password string
httpClient *http.Client
}
type HSDNameResourceResult struct {
Address NameRecords
}
type HSDBlockchainInfo struct {
TreeRoot string
}
type HSDRPCRequest struct {
JSONRPC string `json:"jsonrpc"`
Method string `json:"method"`
Params []any `json:"params"`
ID int `json:"id"`
}
type HSDRPCResponse struct {
Result json.RawMessage `json:"result"`
Error *HSDRPCError `json:"error"`
}
type HSDRPCError struct {
Code int `json:"code"`
Message string `json:"message"`
}
func (err *HSDRPCError) Error() string {
if err == nil {
return ""
}
return fmt.Sprintf("hsd rpc error (%d): %s", err.Code, err.Message)
}
// NewHSDClient builds a client for HSD JSON-RPC.
//
// client := dns.NewHSDClient(dns.HSDClientOptions{
// URL: "http://127.0.0.1:14037",
// Username: "user",
// Password: "pass",
// })
func NewHSDClient(options HSDClientOptions) *HSDClient {
client := options.HTTPClient
if client == nil {
client = &http.Client{}
}
baseURL := strings.TrimSpace(options.URL)
if baseURL == "" {
baseURL = "http://127.0.0.1:14037"
}
return &HSDClient{
baseURL: baseURL,
username: options.Username,
password: options.Password,
httpClient: client,
}
}
func (client *HSDClient) GetNameResource(ctx context.Context, name string) (NameRecords, error) {
normalized := strings.TrimSpace(name)
if normalized == "" {
return NameRecords{}, errors.New("name is required for getnameresource")
}
request := HSDRPCRequest{
JSONRPC: defaultHSDJSONRPCVersion,
Method: "getnameresource",
Params: []any{normalized},
ID: 1,
}
var result NameRecords
raw, err := client.call(ctx, request)
if err != nil {
return result, err
}
result, err = parseHSDNameResource(raw)
if err != nil {
return NameRecords{}, err
}
return result, nil
}
func (client *HSDClient) GetBlockchainInfo(ctx context.Context) (HSDBlockchainInfo, error) {
var result HSDBlockchainInfo
request := HSDRPCRequest{
JSONRPC: defaultHSDJSONRPCVersion,
Method: "getblockchaininfo",
Params: []any{},
ID: 1,
}
raw, err := client.call(ctx, request)
if err != nil {
return result, err
}
result, err = parseHSDBlockchainInfo(raw)
if err != nil {
return HSDBlockchainInfo{}, err
}
return result, nil
}
func (client *HSDClient) call(ctx context.Context, request HSDRPCRequest) (json.RawMessage, error) {
body, err := json.Marshal(request)
if err != nil {
return nil, err
}
httpRequest, err := http.NewRequestWithContext(ctx, http.MethodPost, client.baseURL, bytes.NewReader(body))
if err != nil {
return nil, err
}
httpRequest.Header.Set("Content-Type", "application/json")
if client.username != "" || client.password != "" {
httpRequest.Header.Set("Authorization", "Basic "+basicAuthToken(client.username, client.password))
}
response, err := client.httpClient.Do(httpRequest)
if err != nil {
return nil, err
}
defer func() { _ = response.Body.Close() }()
responseBody, err := io.ReadAll(response.Body)
if err != nil {
return nil, err
}
if response.StatusCode < 200 || response.StatusCode >= 300 {
return nil, fmt.Errorf("hsd rpc request failed with status %d: %s", response.StatusCode, strings.TrimSpace(string(responseBody)))
}
var decoded HSDRPCResponse
if err := json.Unmarshal(responseBody, &decoded); err != nil {
return nil, err
}
if decoded.Error != nil {
return nil, decoded.Error
}
return decoded.Result, nil
}
func parseHSDNameResource(raw json.RawMessage) (NameRecords, error) {
var wrapper map[string]json.RawMessage
if err := json.Unmarshal(raw, &wrapper); err != nil {
return NameRecords{}, err
}
if recordsRaw, ok := wrapper["records"]; ok {
result, err := parseHSDNameResourceRecords(recordsRaw)
if err != nil {
return NameRecords{}, err
}
return result, nil
}
if hasHSDNameResourceField(wrapper) {
result, err := parseHSDNameResourceRecords(raw)
if err != nil {
return NameRecords{}, err
}
return result, nil
}
return NameRecords{}, errors.New("unable to parse getnameresource result")
}
func parseHSDNameResourceRecords(raw json.RawMessage) (NameRecords, error) {
var fields map[string]json.RawMessage
if err := json.Unmarshal(raw, &fields); err != nil {
return NameRecords{}, err
}
a, err := parseHSDRecordValue(fields["a"])
if err != nil {
return NameRecords{}, err
}
aaaa, err := parseHSDRecordValue(fields["aaaa"])
if err != nil {
return NameRecords{}, err
}
txt, err := parseHSDRecordValue(fields["txt"])
if err != nil {
return NameRecords{}, err
}
ns, err := parseHSDRecordValue(fields["ns"])
if err != nil {
return NameRecords{}, err
}
ds, err := parseHSDRecordValue(fields["ds"])
if err != nil {
return NameRecords{}, err
}
dnsKey, err := parseHSDRecordValue(fields["dnskey"])
if err != nil {
return NameRecords{}, err
}
rrSig, err := parseHSDRecordValue(fields["rrsig"])
if err != nil {
return NameRecords{}, err
}
return NameRecords{
A: a,
AAAA: aaaa,
TXT: txt,
NS: ns,
DS: ds,
DNSKEY: dnsKey,
RRSIG: rrSig,
}, nil
}
func hasHSDNameResourceField(fields map[string]json.RawMessage) bool {
if len(fields) == 0 {
return false
}
_, hasA := fields["a"]
_, hasAAAA := fields["aaaa"]
_, hasTXT := fields["txt"]
_, hasNS := fields["ns"]
_, hasDS := fields["ds"]
_, hasDNSKEY := fields["dnskey"]
_, hasRRSIG := fields["rrsig"]
return hasA || hasAAAA || hasTXT || hasNS || hasDS || hasDNSKEY || hasRRSIG
}
func parseHSDRecordValue(raw json.RawMessage) ([]string, error) {
if len(bytes.TrimSpace(raw)) == 0 {
return nil, nil
}
if trimmed := bytes.TrimSpace(raw); bytes.Equal(trimmed, []byte("null")) {
return nil, nil
}
var values []string
if err := json.Unmarshal(raw, &values); err == nil {
return values, nil
}
var value string
if err := json.Unmarshal(raw, &value); err == nil {
if value == "" {
return nil, nil
}
return []string{value}, nil
}
return nil, fmt.Errorf("unable to parse DNS record field")
}
func parseHSDBlockchainInfo(raw json.RawMessage) (HSDBlockchainInfo, error) {
var info HSDBlockchainInfo
var wrapper map[string]json.RawMessage
if err := json.Unmarshal(raw, &wrapper); err != nil {
return info, err
}
if rawTreeRoot, ok := wrapper["tree_root"]; ok {
if err := json.Unmarshal(rawTreeRoot, &info.TreeRoot); err != nil {
return HSDBlockchainInfo{}, err
}
return info, nil
}
if rawTreeRoot, ok := wrapper["treeRoot"]; ok {
if err := json.Unmarshal(rawTreeRoot, &info.TreeRoot); err != nil {
return HSDBlockchainInfo{}, err
}
return info, nil
}
return HSDBlockchainInfo{}, errors.New("unable to parse getblockchaininfo result")
}
func basicAuthToken(username, password string) string {
return base64.StdEncoding.EncodeToString([]byte(username + ":" + password))
}