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
5.3 kB 140 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 (optional)</label> 59 <input type="password" id="password" name="password" autocomplete="off"> 60 <div class="hint">Know it? Enter it to connect &amp; seed immediately. Leave it blank to submit for the graft admin to review and approve instead.</div> 61 62 <button type="submit">Submit</button> 63 </form> 64</div> 65<p><a class="nav-link" href="/add-peer">Add a Forgejo peer instead &rarr;</a></p> 66`)) 67 68func (h *Handler) addRadiclePeer(w http.ResponseWriter, r *http.Request) { 69 switch r.Method { 70 case http.MethodGet: 71 h.renderAddRadiclePeer(w, map[string]any{"Series": seriesNames(h.cfg.Series())}) 72 case http.MethodPost: 73 h.submitAddRadiclePeer(w, r) 74 default: 75 http.Error(w, "method not allowed", http.StatusMethodNotAllowed) 76 } 77} 78 79func (h *Handler) renderAddRadiclePeer(w http.ResponseWriter, data map[string]any) { 80 render(w, "add a radicle peer", "SELF-SERVICE ONBOARDING", renderFragment(addRadiclePeerTmpl, data)) 81} 82 83func (h *Handler) submitAddRadiclePeer(w http.ResponseWriter, r *http.Request) { 84 ip := clientIP(r) 85 if !h.submitLimiter.allowed(ip) { 86 h.renderAddRadiclePeer(w, map[string]any{"Series": seriesNames(h.cfg.Series()), "Error": "too many submissions from your address — try again later"}) 87 return 88 } 89 if err := r.ParseForm(); err != nil { 90 h.renderAddRadiclePeer(w, map[string]any{"Series": seriesNames(h.cfg.Series()), "Error": "couldn't read the form"}) 91 return 92 } 93 form := formData(r, "series", "node_id", "address", "password") 94 retry := func(msg string) { 95 h.renderAddRadiclePeer(w, map[string]any{ 96 "Series": seriesNames(h.cfg.Series()), "Error": msg, "SelectedSeries": form["series"], 97 "NodeID": form["node_id"], "Address": form["address"], 98 }) 99 } 100 h.submitLimiter.recordFailure(ip) 101 102 series := findSeries(h.cfg.Series(), form["series"]) 103 if series == nil { 104 retry("pick a federation to join") 105 return 106 } 107 if !validNodeID.MatchString(form["node_id"]) { 108 retry("that doesn't look like a valid node id") 109 return 110 } 111 if !validNodeAddress.MatchString(form["address"]) { 112 retry("address must be host:port") 113 return 114 } 115 116 if form["password"] != "" { 117 if !checkPassword(h.cfg, form["password"]) { 118 h.passwordLimiter.recordFailure(ip) 119 retry("wrong admin password") 120 return 121 } 122 if _, err := runRad(h.cfg.DefaultRadHome, "node", "connect", form["node_id"]+"@"+form["address"]); err != nil { 123 retry("could not connect to that node: " + err.Error()) 124 return 125 } 126 if _, err := runRad(h.cfg.DefaultRadHome, "seed", series.RadicleRID); err != nil { 127 retry("connected, but could not seed the repository: " + err.Error()) 128 return 129 } 130 render(w, "radicle peer added", "SELF-SERVICE ONBOARDING", renderFragment(onboardOKTmpl, map[string]any{"Title": "Radicle peer added", "Name": form["node_id"], "Approved": true})) 131 return 132 } 133 134 if _, err := h.cfg.Store.CreatePendingRadiclePeer(series.Name, form["node_id"], form["address"]); err != nil { 135 retry("could not save: " + err.Error()) 136 return 137 } 138 139 render(w, "radicle peer submitted", "SELF-SERVICE ONBOARDING", renderFragment(onboardOKTmpl, map[string]any{"Title": "Radicle peer submitted", "Name": form["node_id"], "Approved": false})) 140}