2025-07-12 12:12:23 +03:00
|
|
|
package autopeering
|
2025-07-08 04:28:42 +03:00
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"io/fs"
|
|
|
|
|
"math/rand"
|
|
|
|
|
"net"
|
|
|
|
|
"net/url"
|
|
|
|
|
"os"
|
|
|
|
|
"path/filepath"
|
|
|
|
|
"regexp"
|
|
|
|
|
"sort"
|
|
|
|
|
"strings"
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
git "github.com/go-git/go-git/v5"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type Peer struct {
|
|
|
|
|
URL url.URL
|
|
|
|
|
Online bool
|
|
|
|
|
Latency time.Duration
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var PublicPeers string
|
|
|
|
|
|
|
|
|
|
const repoURL = "https://github.com/yggdrasil-network/public-peers"
|
|
|
|
|
|
|
|
|
|
func readPeersFile(path string) []url.URL {
|
|
|
|
|
data, err := os.ReadFile(path)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
var peers []url.URL
|
|
|
|
|
for line := range strings.SplitSeq(string(data), "\n") {
|
|
|
|
|
line = strings.TrimSpace(line)
|
|
|
|
|
|
|
|
|
|
if line != "" {
|
|
|
|
|
url, err := url.Parse(line)
|
|
|
|
|
if err != nil {
|
|
|
|
|
panic(err)
|
|
|
|
|
}
|
|
|
|
|
peers = append(peers, *url)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return peers
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GetPublicPeers fetches the public-peers repository and returns a list of peer
|
|
|
|
|
// connection strings. If fetching fails, it falls back to reading peers from
|
|
|
|
|
// peers.txt in the current working directory. It returns an empty slice if no
|
|
|
|
|
// peers can be retrieved.
|
|
|
|
|
func GetPublicPeers() []url.URL {
|
|
|
|
|
tempDir, err := os.MkdirTemp("", "public-peers-*")
|
|
|
|
|
if err != nil {
|
|
|
|
|
return readPeersFile("peers.txt")
|
|
|
|
|
}
|
|
|
|
|
defer os.RemoveAll(tempDir)
|
|
|
|
|
|
|
|
|
|
_, err = git.PlainCloneContext(context.Background(), tempDir, false, &git.CloneOptions{URL: repoURL, Depth: 1})
|
|
|
|
|
if err != nil {
|
|
|
|
|
return readPeersFile("peers.txt")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var peers []url.URL
|
|
|
|
|
re := regexp.MustCompile(`(?m)^\s*(tcp|tls)://[^\s]+`)
|
|
|
|
|
filepath.WalkDir(tempDir, func(path string, d fs.DirEntry, err error) error {
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
if d.IsDir() || !strings.HasSuffix(d.Name(), ".md") || d.Name() == "README.md" {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
data, err := os.ReadFile(path)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
for _, m := range re.FindAllStringSubmatch(string(data), -1) {
|
|
|
|
|
url, err := url.Parse(strings.TrimSpace(m[1]))
|
|
|
|
|
if err != nil {
|
|
|
|
|
panic(err)
|
|
|
|
|
}
|
|
|
|
|
peers = append(peers, *url)
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
if len(peers) == 0 {
|
|
|
|
|
return readPeersFile("peers.txt")
|
|
|
|
|
}
|
|
|
|
|
return peers
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get n online peers with best latency from a peer list
|
|
|
|
|
func GetClosestPeers(peerList []url.URL, n int) []url.URL {
|
|
|
|
|
var result []url.URL
|
|
|
|
|
onlinePeers := testPeers(peerList)
|
|
|
|
|
|
|
|
|
|
// Filter online peers
|
|
|
|
|
x := 0
|
|
|
|
|
for _, p := range onlinePeers {
|
|
|
|
|
if p.Online {
|
|
|
|
|
onlinePeers[x] = p
|
|
|
|
|
x++
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
onlinePeers = onlinePeers[:x]
|
|
|
|
|
|
|
|
|
|
sort.Slice(onlinePeers, func(i, j int) bool {
|
|
|
|
|
return onlinePeers[i].Latency < onlinePeers[j].Latency
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
for i := 0; i < len(onlinePeers); i++ {
|
|
|
|
|
if len(result) == n {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
result = append(result, onlinePeers[i].URL)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return result
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Pick n random peers from a list
|
|
|
|
|
func RandomPick(peerList []url.URL, n int) []url.URL {
|
|
|
|
|
if len(peerList) <= n {
|
|
|
|
|
return peerList
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var res []url.URL
|
|
|
|
|
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
|
|
|
|
for _, i := range r.Perm(n) {
|
|
|
|
|
res = append(res, peerList[i])
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return res
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const defaultTimeout time.Duration = time.Duration(3) * time.Second
|
|
|
|
|
|
|
|
|
|
func testPeers(peers []url.URL) []Peer {
|
|
|
|
|
var res []Peer
|
|
|
|
|
results := make(chan Peer)
|
|
|
|
|
|
|
|
|
|
for _, p := range peers {
|
|
|
|
|
go testPeer(p, results)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for range peers {
|
|
|
|
|
res = append(res, <-results)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return res
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func testPeer(peer url.URL, results chan Peer) {
|
|
|
|
|
p := Peer{peer, false, 0.0}
|
|
|
|
|
p.Online = false
|
|
|
|
|
t0 := time.Now()
|
|
|
|
|
|
|
|
|
|
var network string
|
|
|
|
|
if peer.Scheme == "tcp" || peer.Scheme == "tls" {
|
|
|
|
|
network = "tcp"
|
|
|
|
|
} else { // skip, not supported yet
|
|
|
|
|
results <- p
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
conn, err := net.DialTimeout(network, peer.Host, defaultTimeout)
|
|
|
|
|
if err == nil {
|
|
|
|
|
t1 := time.Now()
|
|
|
|
|
conn.Close()
|
|
|
|
|
p.Online = true
|
|
|
|
|
p.Latency = t1.Sub(t0)
|
|
|
|
|
}
|
|
|
|
|
results <- p
|
|
|
|
|
}
|