Something went wrong. Try again.
small gleam coding and persistent agent daemon with a detachable cli and a canvas ui forked from okami.mom/albedo
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218package webui
import ( "bytes" "context" "encoding/json" "fmt" "net" "net/url" "os/exec" "slices" "strings" "time")
// TailscaleNetwork is the range Tailscale hands its devices addresses from.// Its traffic is authenticated and encrypted device to device, which is why// `albedo web` shares to it without being asked.var TailscaleNetwork = &net.IPNet{IP: net.IPv4(100, 64, 0, 0).To4(), Mask: net.CIDRMask(10, 32)}
// Tailnet is this machine on a Tailscale network: the names other devices// reach it by (MagicDNS, whose suffix is the tailnet's own), and the// certificate domains the control server reports.type Tailnet struct { Names []string certDomains []string}
// ServeHTTPS is the claim on `tailscale serve` for the listener on port. It// returns an error naming the fix when the machine is not on a tailnet, or the// control server offers no certificate for its MagicDNS name (Headscale// without node certificates, or HTTPS turned off), before anything is changed.func (t Tailnet) ServeHTTPS(port int) (Serve, error) { if len(t.Names) == 0 { return Serve{}, fmt.Errorf("--tailnet-https needs this machine signed in to Tailscale") } if !slices.ContainsFunc(t.certDomains, func(domain string) bool { return certificateCovers(domain, t.Names[0]) }) { return Serve{}, fmt.Errorf("this tailnet issues no certificate for %s: turn on HTTPS certificates in the Tailscale admin console, or dns.certificates on Headscale; a proxy that terminates TLS uses --public-url", t.Names[0]) } return NewServe(port), nil}
// certificateCovers reports whether a certificate domain covers name. A// wildcard covers exactly one label.func certificateCovers(domain, name string) bool { domain = strings.TrimSuffix(strings.ToLower(domain), ".") name = strings.ToLower(name) if domain == name { return true } suffix, wildcard := strings.CutPrefix(domain, "*.") if !wildcard { return false } label, rest, found := strings.Cut(name, ".") return found && label != "" && rest == suffix}
// CurrentTailnet asks the tailscale CLI who this machine is, and reports// false when it is not installed, not running or not signed in.func CurrentTailnet(ctx context.Context) (Tailnet, bool) { out, err := runTailscale(ctx, "status", "--json", "--peers=false") if err != nil { return Tailnet{}, false } return parseTailnet([]byte(out))}
func parseTailnet(status []byte) (Tailnet, bool) { var parsed struct { BackendState string Self struct{ DNSName string } CertDomains []string } if err := json.Unmarshal(status, &parsed); err != nil || parsed.BackendState != "Running" { return Tailnet{}, false } full := strings.ToLower(strings.TrimSuffix(parsed.Self.DNSName, ".")) if full == "" { return Tailnet{}, false } short, _, _ := strings.Cut(full, ".") return Tailnet{Names: []string{full, short}, certDomains: parsed.CertDomains}, true}
// Serve is a listener's claim on the node's https:443 root, which `tailscale// serve` publishes with a certificate for the machine's MagicDNS name.type Serve struct{ target string }
// NewServe is the claim for the listener on port.func NewServe(port int) Serve { return Serve{target: Loopback(port).String()}}
// Claim points the https:443 root at this listener, or reuses one that already// does, and returns the https origin it serves. It refuses a root that// publishes anything else, or that Funnel opens to the internet. A handler it// created but cannot confirm is removed before it returns.func (s Serve) Claim(ctx context.Context) (*url.URL, error) { root, err := s.root(ctx) if err != nil { return nil, fmt.Errorf("cannot read the tailscale serve config: %w", err) } if root.ours == "" && len(root.busy) > 0 { return nil, fmt.Errorf("tailscale serve already publishes %s; run `tailscale serve status` to see it, or pass --public-url with the address it serves", strings.Join(root.busy, ", ")) } if root.ours == "" { // tailscale serve exits 0 after printing how to enable HTTPS when // the tailnet has not, so the config read back is the answer. output, err := runTailscale(ctx, "serve", "--bg", "--https=443", s.target) if err == nil { root, err = s.root(ctx) } if err == nil && root.ours == "" { detail := strings.TrimSpace(output) if detail == "" { detail = "check `tailscale serve status`" } err = fmt.Errorf("tailscale serve did not keep the https handler: %s", detail) } if err != nil { if released := s.Release(context.WithoutCancel(ctx)); released != nil { err = fmt.Errorf("%w (and could not remove the handler it may have created: %v)", err, released) } return nil, err } } return ParseOrigin("https://" + root.ours)}
// Release removes the https:443 root while it still proxies this listener; one// someone replaced since is theirs and stays.func (s Serve) Release(ctx context.Context) error { root, err := s.root(ctx) if err != nil || root.ours == "" { return err } _, err = runTailscale(ctx, "serve", "--https=443", "--set-path", "/", "off") return err}
func (s Serve) root(ctx context.Context) (serveRoot, error) { status, err := runTailscale(ctx, "serve", "status", "--json") if err != nil { return serveRoot{}, err } return parseServeRoot([]byte(status), s.target)}
// serveRoot is what `tailscale serve status --json` says about the https:443// root: ours is the host:port whose root proxies the target, and busy names// every published entry ("Web host:443", "TCP 22", an unknown key) otherwise.type serveRoot struct { ours string busy []string}
// parseServeRoot finds the target's root. A key it does not know keeps the// root busy even when the target's root is there, since a newer tailscale may// publish through it, and so does Funnel on that root.func parseServeRoot(status []byte, target string) (serveRoot, error) { var fields map[string]json.RawMessage var config struct { Web map[string]struct { Handlers map[string]struct{ Proxy string } } AllowFunnel map[string]bool } if err := json.Unmarshal(status, &fields); err != nil { return serveRoot{}, fmt.Errorf("tailscale serve status: %w", err) } if err := json.Unmarshal(status, &config); err != nil { return serveRoot{}, fmt.Errorf("tailscale serve status: %w", err) } var busy []string known := true for key, value := range fields { var entries map[string]json.RawMessage if !slices.Contains([]string{"TCP", "Web", "Services", "AllowFunnel", "Foreground"}, key) || json.Unmarshal(value, &entries) != nil { busy, known = append(busy, key), false continue } for name := range entries { busy = append(busy, key+" "+name) } } for hostport, web := range config.Web { _, port, _ := net.SplitHostPort(hostport) if known && port == "443" && web.Handlers["/"].Proxy == target && !config.AllowFunnel[hostport] { return serveRoot{ours: hostport}, nil } } slices.Sort(busy) return serveRoot{busy: busy}, nil}
// runTailscale runs the tailscale CLI, keeping its own message on failure,// which names the fix (not signed in, https off, no permission).func runTailscale(ctx context.Context, args ...string) (string, error) { ctx, cancel := context.WithTimeout(ctx, tailscaleTimeout) defer cancel() command := exec.CommandContext(ctx, "tailscale", args...) var stdout, stderr bytes.Buffer command.Stdout, command.Stderr = &stdout, &stderr if err := command.Run(); err != nil { if message := strings.TrimSpace(stderr.String() + stdout.String()); message != "" { return "", fmt.Errorf("%w: %s", err, message) } return "", err } return stdout.String(), nil}
// tailscaleTimeout bounds one tailscale command. They only talk to the local// tailscaled, and a claim runs at most five, inside the background start's// budget.const tailscaleTimeout = 5 * time.Second