1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
package atprotorelaymain
import (
"context"
"errors"
"fmt"
"log/slog"
"net"
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
"sync"
"time"
"github.com/johnandersen777/socialweb-computer-kcp/internal/notify"
"github.com/johnandersen777/socialweb-computer-kcp/internal/relay/local"
)
const (
DefaultListen = "127.0.0.1:2584"
StateFileName = "relay-state.json"
SeedHostsFileName = "seed-hosts.json"
ShutdownGrace = 5 * time.Second
ClientTimeout = 15 * time.Second
readHeaderTimeout = 10 * time.Second
)
type Config struct {
Listen string
StateDir string
PublicURL string
PLCURL string
AllowedHosts []string
SeedHostsFile string
RecrawlWindow time.Duration
IdleAfter time.Duration
MaxBacklog int
StartSeq int64
InsecureHTTP bool
Version string
Log *slog.Logger
}
func SplitList(raw string) []string {
var out []string
for part := range strings.SplitSeq(raw, ",") {
if part = strings.TrimSpace(part); part != "" {
out = append(out, part)
}
}
return out
}
func Hostname(publicURL string) (string, error) {
raw := strings.TrimSpace(publicURL)
if raw == "" {
return "", errors.New("atproto relay: --atproto-relay-public-url is required")
}
parsed, err := url.Parse(raw)
if err != nil || parsed.Scheme != "https" && parsed.Scheme != "http" || parsed.Hostname() == "" || parsed.Path != "" && parsed.Path != "/" {
return "", fmt.Errorf("atproto relay: --atproto-relay-public-url %q is not an http(s) origin", raw)
}
return strings.ToLower(parsed.Host), nil
}
func Run(ctx context.Context, cfg Config) error {
log := cfg.Log
if log == nil {
log = slog.Default()
}
hostname, err := Hostname(cfg.PublicURL)
if err != nil {
return err
}
stateDir := strings.TrimSpace(cfg.StateDir)
if stateDir == "" {
return errors.New("atproto relay: --state-dir is required")
}
if err := os.MkdirAll(stateDir, 0o700); err != nil {
return fmt.Errorf("atproto relay: state dir %s: %w", stateDir, err)
}
seed := strings.TrimSpace(cfg.SeedHostsFile)
if seed == "" {
seed = filepath.Join(stateDir, SeedHostsFileName)
}
runCtx, cancel := context.WithCancel(context.WithoutCancel(ctx))
defer cancel()
relay, err := local.New(runCtx, local.Config{
Hostname: hostname,
PLCURL: cfg.PLCURL,
InsecureHTTP: cfg.InsecureHTTP,
AllowedHosts: cfg.AllowedHosts,
StateFile: filepath.Join(stateDir, StateFileName),
SeedHostsFile: seed,
Recrawl: true,
RecrawlWindow: cfg.RecrawlWindow,
IdleAfter: cfg.IdleAfter,
MaxBacklog: cfg.MaxBacklog,
StartSeq: cfg.StartSeq,
}, local.Deps{
Log: log,
HTTPClient: &http.Client{Timeout: ClientTimeout},
})
if err != nil {
return fmt.Errorf("atproto relay: opening the state in %s: %w", stateDir, err)
}
address := strings.TrimSpace(cfg.Listen)
if address == "" {
address = DefaultListen
}
listener, err := net.Listen("tcp", address)
if err != nil {
relay.Core().Close()
return fmt.Errorf("atproto relay: listening on %s: %w", address, err)
}
httpServer := &http.Server{Handler: relay.Handler(), ReadHeaderTimeout: readHeaderTimeout}
var wg sync.WaitGroup
ran := make(chan struct{})
wg.Go(func() {
defer close(ran)
_ = relay.Run(runCtx)
})
served := make(chan error, 1)
wg.Go(func() { served <- httpServer.Serve(listener) })
log.Info("serve listening", "address", listener.Addr().String(), "hostname", hostname, "stateDir", stateDir,
"allowedHosts", cfg.AllowedHosts, "head", relay.Core().Sequencer().Head(), "recrawlWindow", cfg.RecrawlWindow.String(),
"idleAfter", cfg.IdleAfter.String(), "insecureHTTP", cfg.InsecureHTTP, "version", cfg.Version)
if err := notify.Ready(); err != nil {
log.Warn("sd_notify ready failed", "err", err)
}
var runErr error
select {
case <-ctx.Done():
log.Info("shutting down", "reason", context.Cause(ctx))
case err := <-served:
if !errors.Is(err, http.ErrServerClosed) {
runErr = fmt.Errorf("atproto relay: serving %s: %w", address, err)
}
case <-ran:
runErr = errors.New("atproto relay: the relay stopped")
}
shutdownCtx, stop := context.WithTimeout(context.WithoutCancel(ctx), ShutdownGrace)
defer stop()
_ = httpServer.Shutdown(shutdownCtx)
cancel()
<-ran
_ = httpServer.Close()
wg.Wait()
log.Info("stopped", "head", relay.Core().Sequencer().Head())
return runErr
}