317 lines
8.4 KiB
Go
317 lines
8.4 KiB
Go
package dns
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"slices"
|
|
"strings"
|
|
)
|
|
|
|
// MainchainClientOptions configures JSON-RPC access to the main chain alias source.
|
|
//
|
|
// client := dns.NewMainchainAliasClient(dns.MainchainClientOptions{
|
|
// URL: "http://127.0.0.1:14037",
|
|
// Username: "user",
|
|
// Password: "pass",
|
|
// })
|
|
type MainchainClientOptions struct {
|
|
URL string
|
|
Username string
|
|
Password string
|
|
HTTPClient *http.Client
|
|
}
|
|
|
|
// MainchainAliasClientConfiguration is the explicit long-form alias for MainchainClientOptions.
|
|
//
|
|
// client := dns.NewMainchainAliasClientFromConfiguration(dns.MainchainAliasClientConfiguration{
|
|
// URL: "http://127.0.0.1:14037",
|
|
// Username: "user",
|
|
// Password: "pass",
|
|
// })
|
|
type MainchainAliasClientConfiguration = MainchainClientOptions
|
|
|
|
// MainchainAliasClientConfig is kept for compatibility with older call sites.
|
|
//
|
|
// Deprecated: use MainchainAliasClientConfiguration instead.
|
|
type MainchainAliasClientConfig = MainchainClientOptions
|
|
|
|
type MainchainAliasClient struct {
|
|
baseURL string
|
|
username string
|
|
password string
|
|
httpClient *http.Client
|
|
}
|
|
|
|
type MainchainRPCRequest struct {
|
|
JSONRPC string `json:"jsonrpc"`
|
|
Method string `json:"method"`
|
|
Params []any `json:"params"`
|
|
ID int `json:"id"`
|
|
}
|
|
|
|
type MainchainRPCResponse struct {
|
|
Result json.RawMessage `json:"result"`
|
|
Error *HSDRPCError `json:"error"`
|
|
}
|
|
|
|
// NewMainchainAliasClient builds a client for alias discovery on the main chain.
|
|
//
|
|
// client := dns.NewMainchainAliasClient(dns.MainchainClientOptions{
|
|
// URL: "http://127.0.0.1:14037",
|
|
// Username: "user",
|
|
// Password: "pass",
|
|
// })
|
|
func NewMainchainAliasClient(options MainchainClientOptions) *MainchainAliasClient {
|
|
client := options.HTTPClient
|
|
if client == nil {
|
|
client = &http.Client{}
|
|
}
|
|
|
|
baseURL := strings.TrimSpace(options.URL)
|
|
if baseURL == "" {
|
|
baseURL = "http://127.0.0.1:14037"
|
|
}
|
|
|
|
return &MainchainAliasClient{
|
|
baseURL: baseURL,
|
|
username: options.Username,
|
|
password: options.Password,
|
|
httpClient: client,
|
|
}
|
|
}
|
|
|
|
// NewMainchainAliasClientFromOptions is the explicit long-form constructor name.
|
|
//
|
|
// client := dns.NewMainchainAliasClientFromOptions(dns.MainchainClientOptions{
|
|
// URL: "http://127.0.0.1:14037",
|
|
// Username: "user",
|
|
// Password: "pass",
|
|
// })
|
|
func NewMainchainAliasClientFromOptions(options MainchainClientOptions) *MainchainAliasClient {
|
|
return NewMainchainAliasClient(options)
|
|
}
|
|
|
|
// NewMainchainAliasClientFromConfiguration is the explicit constructor name for the main-chain configuration alias.
|
|
//
|
|
// client := dns.NewMainchainAliasClientFromConfiguration(dns.MainchainAliasClientConfiguration{
|
|
// URL: "http://127.0.0.1:14037",
|
|
// Username: "user",
|
|
// Password: "pass",
|
|
// })
|
|
func NewMainchainAliasClientFromConfiguration(options MainchainAliasClientConfiguration) *MainchainAliasClient {
|
|
return NewMainchainAliasClient(options)
|
|
}
|
|
|
|
// GetAllAliasDetails returns alias names mapped to HNS DNS names.
|
|
//
|
|
// client := dns.NewMainchainAliasClient(dns.MainchainClientOptions{
|
|
// URL: "http://127.0.0.1:14037",
|
|
// Username: "user",
|
|
// Password: "pass",
|
|
// })
|
|
// aliases, err := client.GetAllAliasDetails(context.Background())
|
|
func (client *MainchainAliasClient) GetAllAliasDetails(ctx context.Context) ([]string, error) {
|
|
request := MainchainRPCRequest{
|
|
JSONRPC: defaultHSDJSONRPCVersion,
|
|
Method: "get_all_alias_details",
|
|
Params: []any{},
|
|
ID: 1,
|
|
}
|
|
|
|
raw, err := client.call(ctx, request)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
aliases, err := parseMainchainAliases(raw)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return aliases, nil
|
|
}
|
|
|
|
func (client *MainchainAliasClient) call(ctx context.Context, request MainchainRPCRequest) (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("mainchain rpc request failed with status %d: %s", response.StatusCode, strings.TrimSpace(string(responseBody)))
|
|
}
|
|
|
|
var decoded MainchainRPCResponse
|
|
if err := json.Unmarshal(responseBody, &decoded); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if decoded.Error != nil {
|
|
return nil, decoded.Error
|
|
}
|
|
|
|
return decoded.Result, nil
|
|
}
|
|
|
|
func parseMainchainAliases(raw json.RawMessage) ([]string, error) {
|
|
var result []string
|
|
if err := json.Unmarshal(raw, &result); err == nil {
|
|
return normalizeAliasList(result), nil
|
|
}
|
|
|
|
var wrappedResult map[string]json.RawMessage
|
|
if err := json.Unmarshal(raw, &wrappedResult); err == nil {
|
|
if aliasesRaw, ok := wrappedResult["aliases"]; ok {
|
|
return parseMainchainAliases(aliasesRaw)
|
|
}
|
|
if resultRaw, ok := wrappedResult["result"]; ok {
|
|
return parseMainchainAliases(resultRaw)
|
|
}
|
|
}
|
|
|
|
var rawRecords []json.RawMessage
|
|
if err := json.Unmarshal(raw, &rawRecords); err != nil {
|
|
return nil, errors.New("unable to parse get_all_alias_details result")
|
|
}
|
|
|
|
parsed := make([]string, 0, len(rawRecords))
|
|
for _, rawRecord := range rawRecords {
|
|
next, err := parseMainchainAliasRecord(rawRecord)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if next != "" {
|
|
parsed = append(parsed, next)
|
|
}
|
|
}
|
|
return normalizeAliasList(parsed), nil
|
|
}
|
|
|
|
func parseMainchainAliasRecord(raw json.RawMessage) (string, error) {
|
|
var name string
|
|
if err := json.Unmarshal(raw, &name); err == nil {
|
|
return normalizeName(name), nil
|
|
}
|
|
|
|
var record map[string]json.RawMessage
|
|
if err := json.Unmarshal(raw, &record); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
var candidate string
|
|
if value, ok := record["hns"]; ok {
|
|
if hns, ok := decodeString(value); ok {
|
|
candidate = normalizeName(hns)
|
|
}
|
|
}
|
|
if candidate == "" && record["comment"] != nil {
|
|
if comment, ok := decodeString(record["comment"]); ok {
|
|
candidate = extractAliasFromComment(comment)
|
|
}
|
|
}
|
|
if candidate == "" && record["alias"] != nil {
|
|
if alias, ok := decodeString(record["alias"]); ok {
|
|
candidate = normalizeName(alias)
|
|
}
|
|
}
|
|
if candidate == "" && record["name"] != nil {
|
|
if alias, ok := decodeString(record["name"]); ok {
|
|
candidate = normalizeName(alias)
|
|
}
|
|
}
|
|
return candidate, nil
|
|
}
|
|
|
|
func decodeString(raw json.RawMessage) (string, bool) {
|
|
var value string
|
|
if len(raw) == 0 {
|
|
return "", false
|
|
}
|
|
if err := json.Unmarshal(raw, &value); err != nil {
|
|
return "", false
|
|
}
|
|
value = strings.TrimSpace(value)
|
|
return value, value != ""
|
|
}
|
|
|
|
func extractAliasFromComment(comment string) string {
|
|
fields := strings.Fields(comment)
|
|
for index, token := range fields {
|
|
normalizedToken := strings.ToLower(strings.TrimSpace(token))
|
|
switch {
|
|
case strings.HasPrefix(normalizedToken, "hns="):
|
|
alias := strings.TrimSuffix(strings.TrimPrefix(normalizedToken, "hns="), ";")
|
|
if alias != "" {
|
|
return normalizeName(alias)
|
|
}
|
|
case normalizedToken == "hns" && index+2 < len(fields):
|
|
nextToken := strings.TrimSpace(strings.ToLower(fields[index+1]))
|
|
if nextToken != "=" && nextToken != ":" && nextToken != ":=" {
|
|
continue
|
|
}
|
|
alias := strings.TrimSpace(fields[index+2])
|
|
alias = strings.TrimSuffix(alias, ";")
|
|
alias = strings.TrimSuffix(alias, ",")
|
|
if alias != "" {
|
|
return normalizeName(alias)
|
|
}
|
|
}
|
|
}
|
|
|
|
commentLower := strings.ToLower(comment)
|
|
if marker := strings.Index(commentLower, "hns"); marker >= 0 {
|
|
alias := comment[marker+3:]
|
|
alias = strings.TrimLeft(alias, " \t:=;")
|
|
if trim := strings.IndexAny(alias, " ;,\t\r\n"); trim >= 0 {
|
|
alias = alias[:trim]
|
|
}
|
|
alias = strings.TrimSpace(alias)
|
|
alias = strings.TrimSuffix(alias, ";")
|
|
alias = strings.TrimSuffix(alias, ",")
|
|
return normalizeName(alias)
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
func normalizeAliasList(raw []string) []string {
|
|
seen := map[string]bool{}
|
|
normalized := make([]string, 0, len(raw))
|
|
for _, name := range raw {
|
|
next := normalizeName(name)
|
|
if next == "" {
|
|
continue
|
|
}
|
|
if seen[next] {
|
|
continue
|
|
}
|
|
seen[next] = true
|
|
normalized = append(normalized, next)
|
|
}
|
|
slices.Sort(normalized)
|
|
return normalized
|
|
}
|