package iroh
import (
"bytes"
"context"
"fmt"
"io"
"iter"
"net/http"
"net/url"
"strings"
"time"
"github.com/tmc/go-iroh/dns"
"github.com/tmc/go-iroh/key"
"github.com/tmc/go-iroh/watch"
)
const PkarrProvenance = "pkarr"
const (
N0DNSPkarrRelayProd = "https://dns.iroh.link/pkarr"
N0DNSPkarrRelayStaging = "https://staging-dns.iroh.link/pkarr"
DefaultPkarrTTL uint32 = 30
DefaultRepublishInterval = 5 * time .Minute
)
type PkarrPublisher struct {
endpointID key .EndpointID
addrFilter AddrFilter
value *watch .Value [*dns .EndpointInfo ]
cancel context .CancelFunc
done chan struct {}
}
type PkarrPublisherConfig struct {
TTL uint32
RepublishInterval time .Duration
AddrFilter AddrFilter
HTTPClient *http .Client
}
func NewPkarrPublisher (secretKey key .SecretKey , relayURL string , cfg *PkarrPublisherConfig ) (*PkarrPublisher , error ) {
ttl := DefaultPkarrTTL
republishInterval := DefaultRepublishInterval
filter := RelayOnlyFilter
var httpClient *http .Client
if cfg != nil {
if cfg .TTL != 0 {
ttl = cfg .TTL
}
if cfg .RepublishInterval != 0 {
republishInterval = cfg .RepublishInterval
}
if cfg .AddrFilter != nil {
filter = cfg .AddrFilter
}
httpClient = cfg .HTTPClient
}
client , err := newPkarrRelayClient (relayURL , httpClient )
if err != nil {
return nil , fmt .Errorf ("pkarr publisher: %w" , err )
}
ctx , cancel := context .WithCancel (context .Background ())
p := &PkarrPublisher {
endpointID : secretKey .Public ().EndpointID (),
addrFilter : filter ,
value : watch .NewValue [*dns .EndpointInfo ](nil ),
cancel : cancel ,
done : make (chan struct {}),
}
svc := &publisherService {
secretKey : secretKey ,
client : client ,
watcher : p .value .Watch (),
ttl : ttl ,
republishInterval : republishInterval ,
}
go func () {
defer close (p .done )
svc .run (ctx )
}()
return p , nil
}
func N0PkarrPublisher (secretKey key .SecretKey , cfg *PkarrPublisherConfig ) (*PkarrPublisher , error ) {
return NewPkarrPublisher (secretKey , N0DNSPkarrRelayProd , cfg )
}
func (p *PkarrPublisher ) Publish (data dns .EndpointData ) {
filtered := applyFilter (data , p .addrFilter )
info := dns .EndpointInfo {ID : p .endpointID , Data : filtered }
p .value .Set (&info )
}
func (p *PkarrPublisher ) Close () error {
p .cancel ()
<-p .done
return nil
}
type publisherService struct {
secretKey key .SecretKey
client *pkarrRelayClient
watcher watch .Observer [*dns .EndpointInfo ]
ttl uint32
republishInterval time .Duration
}
func (s *publisherService ) run (ctx context .Context ) {
changed := make (chan struct {}, 1 )
go func () {
for {
if _ , err := s .watcher .Updated (ctx ); err != nil {
return
}
select {
case changed <- struct {}{}:
default :
}
}
}()
var failedAttempts int
republish := time .NewTimer (time .Duration (1 << 62 ))
defer republish .Stop ()
for {
if info := s .watcher .Current (); info != nil {
if err := s .publishCurrent (ctx , *info ); err != nil {
if ctx .Err () != nil {
return
}
failedAttempts ++
resetTimer (republish , time .Duration (failedAttempts )*time .Second )
} else {
failedAttempts = 0
resetTimer (republish , s .republishInterval )
}
}
select {
case <- ctx .Done ():
return
case <- republish .C :
case <- changed :
}
}
}
func (s *publisherService ) publishCurrent (ctx context .Context , info dns .EndpointInfo ) error {
packet , err := info .ToSignedPacket (s .secretKey , s .ttl )
if err != nil {
return fmt .Errorf ("encode signed packet: %w" , err )
}
return s .client .publish (ctx , packet )
}
func resetTimer(t *time .Timer , d time .Duration ) {
if !t .Stop () {
select {
case <- t .C :
default :
}
}
t .Reset (d )
}
type PkarrResolver struct {
client *pkarrRelayClient
}
type PkarrResolverConfig struct {
HTTPClient *http .Client
}
func NewPkarrResolver (relayURL string , cfg *PkarrResolverConfig ) (*PkarrResolver , error ) {
var httpClient *http .Client
if cfg != nil {
httpClient = cfg .HTTPClient
}
client , err := newPkarrRelayClient (relayURL , httpClient )
if err != nil {
return nil , fmt .Errorf ("pkarr resolver: %w" , err )
}
return &PkarrResolver {client : client }, nil
}
func N0PkarrResolver (cfg *PkarrResolverConfig ) (*PkarrResolver , error ) {
return NewPkarrResolver (N0DNSPkarrRelayProd , cfg )
}
func (r *PkarrResolver ) Resolve (ctx context .Context , id key .EndpointID ) iter .Seq2 [Item , error ] {
return func (yield func (Item , error ) bool ) {
packet , err := r .client .resolve (ctx , id )
if err != nil {
if ctx .Err () == nil {
yield (Item {}, lookupErr (PkarrProvenance , err ))
}
return
}
info , err := dns .EndpointInfoFromSignedPacket (packet )
if err != nil {
if ctx .Err () == nil {
yield (Item {}, lookupErr (PkarrProvenance , err ))
}
return
}
if ctx .Err () == nil {
yield (NewItem (info , PkarrProvenance , nil ), nil )
}
}
}
type pkarrRelayClient struct {
httpClient *http .Client
relayURL *url .URL
}
func newPkarrRelayClient(relayURL string , client *http .Client ) (*pkarrRelayClient , error ) {
u , err := url .Parse (relayURL )
if err != nil {
return nil , fmt .Errorf ("parse relay url: %w" , err )
}
if client == nil {
client = &http .Client {Timeout : 30 * time .Second }
}
return &pkarrRelayClient {httpClient : client , relayURL : u }, nil
}
func (c *pkarrRelayClient ) keyURL (z32 string ) string {
u := *c .relayURL
u .Path = strings .TrimRight (u .Path , "/" ) + "/" + z32
return u .String ()
}
func (c *pkarrRelayClient ) publish (ctx context .Context , packet *dns .SignedPacket ) error {
body := packet .RelayPayload ()
target := c .keyURL (packet .PublicKey ().EndpointID ().Z32 ())
req , err := http .NewRequestWithContext (ctx , http .MethodPut , target , bytes .NewReader (body ))
if err != nil {
return fmt .Errorf ("build request: %w" , err )
}
resp , err := c .httpClient .Do (req )
if err != nil {
return fmt .Errorf ("http put: %w" , err )
}
defer resp .Body .Close ()
io .Copy (io .Discard , resp .Body )
if resp .StatusCode < 200 || resp .StatusCode >= 300 {
return fmt .Errorf ("pkarr relay returned status %d" , resp .StatusCode )
}
return nil
}
func (c *pkarrRelayClient ) resolve (ctx context .Context , id key .EndpointID ) (*dns .SignedPacket , error ) {
target := c .keyURL (id .Z32 ())
req , err := http .NewRequestWithContext (ctx , http .MethodGet , target , nil )
if err != nil {
return nil , fmt .Errorf ("build request: %w" , err )
}
resp , err := c .httpClient .Do (req )
if err != nil {
return nil , fmt .Errorf ("http get: %w" , err )
}
defer resp .Body .Close ()
if resp .StatusCode < 200 || resp .StatusCode >= 300 {
io .Copy (io .Discard , resp .Body )
return nil , fmt .Errorf ("pkarr relay returned status %d" , resp .StatusCode )
}
payload , err := io .ReadAll (resp .Body )
if err != nil {
return nil , fmt .Errorf ("read payload: %w" , err )
}
pubBytes := id .PublicKey ().Bytes ()
wire := make ([]byte , 0 , len (pubBytes )+len (payload ))
wire = append (wire , pubBytes [:]...)
wire = append (wire , payload ...)
packet , err := dns .SignedPacketFromBytes (wire )
if err != nil {
return nil , fmt .Errorf ("decode signed packet: %w" , err )
}
return packet , nil
}
The pages are generated with Golds v0.8.4 . (GOOS=linux GOARCH=amd64)
Golds is a Go 101 project developed by Tapir Liu .
PR and bug reports are welcome and can be submitted to the issue list .
Please follow @zigo_101 (reachable from the left QR code) to get the latest news of Golds .