You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

176 lines
4.5 KiB

  1. package p2p
  2. import (
  3. "context"
  4. "errors"
  5. "fmt"
  6. "net"
  7. "net/url"
  8. "regexp"
  9. "strconv"
  10. "strings"
  11. "github.com/tendermint/tendermint/types"
  12. )
  13. var (
  14. // stringHasScheme tries to detect URLs with schemes. It looks for a : before a / (if any).
  15. stringHasScheme = func(str string) bool {
  16. return strings.Contains(str, "://")
  17. }
  18. // reSchemeIsHost tries to detect URLs where the scheme part is instead a
  19. // hostname, i.e. of the form "host:80/path" where host: is a hostname.
  20. reSchemeIsHost = regexp.MustCompile(`^[^/:]+:\d+(/|$)`)
  21. )
  22. // NodeAddress is a node address URL. It differs from a transport Endpoint in
  23. // that it contains the node's ID, and that the address hostname may be resolved
  24. // into multiple IP addresses (and thus multiple endpoints).
  25. //
  26. // If the URL is opaque, i.e. of the form "scheme:opaque", then the opaque part
  27. // is expected to contain a node ID.
  28. type NodeAddress struct {
  29. NodeID types.NodeID
  30. Protocol Protocol
  31. Hostname string
  32. Port uint16
  33. Path string
  34. }
  35. // ParseNodeAddress parses a node address URL into a NodeAddress, normalizing
  36. // and validating it.
  37. func ParseNodeAddress(urlString string) (NodeAddress, error) {
  38. // url.Parse requires a scheme, so if it fails to parse a scheme-less URL
  39. // we try to apply a default scheme.
  40. url, err := url.Parse(urlString)
  41. if (err != nil || url.Scheme == "") &&
  42. (!stringHasScheme(urlString) || reSchemeIsHost.MatchString(urlString)) {
  43. url, err = url.Parse(string(defaultProtocol) + "://" + urlString)
  44. }
  45. if err != nil {
  46. return NodeAddress{}, fmt.Errorf("invalid node address %q: %w", urlString, err)
  47. }
  48. address := NodeAddress{
  49. Protocol: Protocol(strings.ToLower(url.Scheme)),
  50. }
  51. // Opaque URLs are expected to contain only a node ID.
  52. if url.Opaque != "" {
  53. address.NodeID = types.NodeID(url.Opaque)
  54. return address, address.Validate()
  55. }
  56. // Otherwise, just parse a normal networked URL.
  57. if url.User != nil {
  58. address.NodeID = types.NodeID(strings.ToLower(url.User.Username()))
  59. }
  60. address.Hostname = strings.ToLower(url.Hostname())
  61. if portString := url.Port(); portString != "" {
  62. port64, err := strconv.ParseUint(portString, 10, 16)
  63. if err != nil {
  64. return NodeAddress{}, fmt.Errorf("invalid port %q: %w", portString, err)
  65. }
  66. address.Port = uint16(port64)
  67. }
  68. address.Path = url.Path
  69. if url.RawQuery != "" {
  70. address.Path += "?" + url.RawQuery
  71. }
  72. if url.Fragment != "" {
  73. address.Path += "#" + url.Fragment
  74. }
  75. if address.Path != "" {
  76. switch address.Path[0] {
  77. case '/', '#', '?':
  78. default:
  79. address.Path = "/" + address.Path
  80. }
  81. }
  82. return address, address.Validate()
  83. }
  84. // Resolve resolves a NodeAddress into a set of Endpoints, by expanding
  85. // out a DNS hostname to IP addresses.
  86. func (a NodeAddress) Resolve(ctx context.Context) ([]Endpoint, error) {
  87. if a.Protocol == "" {
  88. return nil, errors.New("address has no protocol")
  89. }
  90. // If there is no hostname, this is an opaque URL in the form
  91. // "scheme:opaque", and the opaque part is assumed to be node ID used as
  92. // Path.
  93. if a.Hostname == "" {
  94. if a.NodeID == "" {
  95. return nil, errors.New("local address has no node ID")
  96. }
  97. return []Endpoint{{
  98. Protocol: a.Protocol,
  99. Path: string(a.NodeID),
  100. }}, nil
  101. }
  102. ips, err := net.DefaultResolver.LookupIP(ctx, "ip", a.Hostname)
  103. if err != nil {
  104. return nil, err
  105. }
  106. endpoints := make([]Endpoint, len(ips))
  107. for i, ip := range ips {
  108. endpoints[i] = Endpoint{
  109. Protocol: a.Protocol,
  110. IP: ip,
  111. Port: a.Port,
  112. Path: a.Path,
  113. }
  114. }
  115. return endpoints, nil
  116. }
  117. // String formats the address as a URL string.
  118. func (a NodeAddress) String() string {
  119. u := url.URL{Scheme: string(a.Protocol)}
  120. if a.NodeID != "" {
  121. u.User = url.User(string(a.NodeID))
  122. }
  123. switch {
  124. case a.Hostname != "":
  125. if a.Port > 0 {
  126. u.Host = net.JoinHostPort(a.Hostname, strconv.Itoa(int(a.Port)))
  127. } else {
  128. u.Host = a.Hostname
  129. }
  130. u.Path = a.Path
  131. case a.Protocol != "" && (a.Path == "" || a.Path == string(a.NodeID)):
  132. u.User = nil
  133. u.Opaque = string(a.NodeID) // e.g. memory:id
  134. case a.Path != "" && a.Path[0] != '/':
  135. u.Path = "/" + a.Path // e.g. some/path
  136. default:
  137. u.Path = a.Path // e.g. /some/path
  138. }
  139. return strings.TrimPrefix(u.String(), "//")
  140. }
  141. // Validate validates a NodeAddress.
  142. func (a NodeAddress) Validate() error {
  143. if a.Protocol == "" {
  144. return errors.New("no protocol")
  145. }
  146. if a.NodeID == "" {
  147. return errors.New("no peer ID")
  148. } else if err := a.NodeID.Validate(); err != nil {
  149. return fmt.Errorf("invalid peer ID: %w", err)
  150. }
  151. if a.Port > 0 && a.Hostname == "" {
  152. return errors.New("cannot specify port without hostname")
  153. }
  154. return nil
  155. }