Federated via graft
0

Configure Feed

Select the types of activity you want to include in your feed.

graft-source / internal / admin / radicle_peer.go
4.6 kB 129 lines
1package admin 2 3import ( 4 "bytes" 5 "context" 6 "fmt" 7 "html/template" 8 "net/http" 9 "os" 10 "os/exec" 11 "regexp" 12 "time" 13) 14 15// radExecTimeout bounds "rad node connect" / "rad seed" — a network 16// handshake with a node that may never answer shouldn't hang the request. 17const radExecTimeout = 60 * time.Second 18 19var validNodeID = regexp.MustCompile(`^[a-zA-Z0-9]{1,80}$`) 20var validNodeAddress = regexp.MustCompile(`^[a-zA-Z0-9.\-]+:[0-9]{1,5}$`) 21 22// runRad runs the rad CLI against graft's own local Radicle identity — 23// the same one every pair (static or dynamic) already pushes as. Never 24// scoped to a single repo (unlike internal/radicle.Client), since 25// "connect" operates on the node itself, not a repository. 26func runRad(radHome string, args ...string) (string, error) { 27 ctx, cancel := context.WithTimeout(context.Background(), radExecTimeout) 28 defer cancel() 29 cmd := exec.CommandContext(ctx, "rad", args...) 30 cmd.Env = append(os.Environ(), "RAD_HOME="+radHome) 31 var out bytes.Buffer 32 cmd.Stdout = &out 33 cmd.Stderr = &out 34 if err := cmd.Run(); err != nil { 35 return out.String(), fmt.Errorf("rad %v: %w: %s", args, err, out.String()) 36 } 37 return out.String(), nil 38} 39 40var addRadiclePeerTmpl = template.Must(template.New("add-radicle-peer").Parse(` 41<h1>Add a Radicle peer</h1> 42<p class="lead">Connects a new Radicle node to the mesh and has it seed an existing federation.</p> 43{{if .Error}}<div class="notice bad">{{.Error}}</div>{{end}} 44<div class="card"> 45 <form method="POST" action="/add-radicle-peer"> 46 <label for="series">Federation (series)</label> 47 <select id="series" name="series" required> 48 <option value="">— choose —</option> 49 {{range .Series}}<option value="{{.}}" {{if eq . $.SelectedSeries}}selected{{end}}>{{.}}</option>{{end}} 50 </select> 51 52 <label for="node_id">Node ID</label> 53 <input type="text" id="node_id" name="node_id" required value="{{.NodeID}}" placeholder="z6Mk..."> 54 55 <label for="address">Address</label> 56 <input type="text" id="address" name="address" required value="{{.Address}}" placeholder="host:8776"> 57 58 <label for="password">Admin password</label> 59 <input type="password" id="password" name="password" required autocomplete="off"> 60 61 <button type="submit">Connect &amp; seed</button> 62 </form> 63</div> 64<p><a class="nav-link" href="/add-peer">Add a Forgejo peer instead &rarr;</a></p> 65`)) 66 67func (h *Handler) addRadiclePeer(w http.ResponseWriter, r *http.Request) { 68 switch r.Method { 69 case http.MethodGet: 70 h.renderAddRadiclePeer(w, map[string]any{"Series": seriesNames(h.cfg.Series())}) 71 case http.MethodPost: 72 h.submitAddRadiclePeer(w, r) 73 default: 74 http.Error(w, "method not allowed", http.StatusMethodNotAllowed) 75 } 76} 77 78func (h *Handler) renderAddRadiclePeer(w http.ResponseWriter, data map[string]any) { 79 render(w, "add a radicle peer", "SELF-SERVICE ONBOARDING", renderFragment(addRadiclePeerTmpl, data)) 80} 81 82func (h *Handler) submitAddRadiclePeer(w http.ResponseWriter, r *http.Request) { 83 ip := clientIP(r) 84 if !h.passwordLimiter.allowed(ip) { 85 h.renderAddRadiclePeer(w, map[string]any{"Series": seriesNames(h.cfg.Series()), "Error": "too many attempts from your address — try again later"}) 86 return 87 } 88 if err := r.ParseForm(); err != nil { 89 h.renderAddRadiclePeer(w, map[string]any{"Series": seriesNames(h.cfg.Series()), "Error": "couldn't read the form"}) 90 return 91 } 92 form := formData(r, "series", "node_id", "address", "password") 93 retry := func(msg string) { 94 h.renderAddRadiclePeer(w, map[string]any{ 95 "Series": seriesNames(h.cfg.Series()), "Error": msg, "SelectedSeries": form["series"], 96 "NodeID": form["node_id"], "Address": form["address"], 97 }) 98 } 99 100 if !checkPassword(h.cfg, form["password"]) { 101 h.passwordLimiter.recordFailure(ip) 102 retry("wrong admin password") 103 return 104 } 105 series := findSeries(h.cfg.Series(), form["series"]) 106 if series == nil { 107 retry("pick a federation to join") 108 return 109 } 110 if !validNodeID.MatchString(form["node_id"]) { 111 retry("that doesn't look like a valid node id") 112 return 113 } 114 if !validNodeAddress.MatchString(form["address"]) { 115 retry("address must be host:port") 116 return 117 } 118 119 if _, err := runRad(h.cfg.DefaultRadHome, "node", "connect", form["node_id"]+"@"+form["address"]); err != nil { 120 retry("could not connect to that node: " + err.Error()) 121 return 122 } 123 if _, err := runRad(h.cfg.DefaultRadHome, "seed", series.RadicleRID); err != nil { 124 retry("connected, but could not seed the repository: " + err.Error()) 125 return 126 } 127 128 render(w, "radicle peer added", "SELF-SERVICE ONBOARDING", renderFragment(onboardOKTmpl, map[string]string{"Title": "Radicle peer added", "Name": form["node_id"]})) 129}