// Package control runs the proxy's connection to T5DE's hub, and lets the client's T5DE settings page see and // change it: requests to http://t5de-proxy.local/ (only the client can make them: they need the proxy's token) // are answered here. // // GET /hub {signedIn, hub, username, signingIn, code, page, error} // POST /hub/signin starts signing in (hub= optional): opens the hub's page for the code in the browser // POST /hub/signout signs this computer out package control import ( "context" "encoding/json" "errors" "log" "net/url" "os" "strings" "sync" "git.t5de.dev/t5de/t5de-proxy/internal/hub" "git.t5de.dev/t5de/t5de-proxy/internal/proxy" ) // Host is the address the client uses for the proxy itself. const Host = "t5de-proxy.local" // Manager keeps the hub connection running and answers the client's settings page. type Manager struct { Path string // hub.json DefaultHub string Log *log.Logger // Attach is called with each new hub client before it runs (to connect it to the rest of the proxy). Attach func(c *hub.Client) // OpenBrowser opens a page in the person's browser. OpenBrowser func(page string) mu sync.Mutex client *hub.Client saved *hub.Saved stop context.CancelFunc username string signin *signin } type signin struct { code, page, err string running bool cancel context.CancelFunc } // Start connects to the hub if the proxy is signed in. func (m *Manager) Start() { saved, err := hub.Load(m.Path) if err != nil { if !errors.Is(err, hub.ErrSignedOut) { m.Log.Printf("hub: %v", err) } return } m.run(saved) } func (m *Manager) run(saved *hub.Saved) { c := hub.New(m.Path, saved, m.Log) if m.Attach != nil { m.Attach(c) } ctx, cancel := context.WithCancel(context.Background()) m.mu.Lock() if m.stop != nil { m.stop() } m.client, m.saved, m.stop, m.username = c, saved, cancel, "" m.mu.Unlock() go c.Run(ctx) go m.lookUpName(ctx, c) } func (m *Manager) lookUpName(ctx context.Context, c *hub.Client) { var me struct { Person struct { Username string `json:"username"` } `json:"person"` } if err := c.Call(ctx, "GET", "/api/v1/me", nil, &me); err != nil { return } m.mu.Lock() if m.client == c { m.username = me.Person.Username } m.mu.Unlock() } type status struct { SignedIn bool `json:"signedIn"` Hub string `json:"hub"` Username string `json:"username"` SigningIn bool `json:"signingIn"` Code string `json:"code"` Page string `json:"page"` Error string `json:"error"` } func (m *Manager) status() status { m.mu.Lock() defer m.mu.Unlock() s := status{Hub: m.DefaultHub} if m.saved != nil { s.SignedIn, s.Hub, s.Username = true, m.saved.URL, m.username } if si := m.signin; si != nil { s.SigningIn, s.Code, s.Page, s.Error = si.running, si.code, si.page, si.err } return s } // SignIn starts signing in to a hub; the code to enter shows in the status, and the browser opens the page. func (m *Manager) SignIn(hubURL string) { if hubURL == "" { hubURL = m.DefaultHub } ctx, cancel := context.WithCancel(context.Background()) si := &signin{running: true, cancel: cancel} m.mu.Lock() if m.signin != nil && m.signin.running { m.signin.cancel() } m.signin = si m.mu.Unlock() go func() { saved, err := hub.SignIn(ctx, hubURL, func(code, page string) { m.mu.Lock() si.code, si.page = code, page m.mu.Unlock() if m.OpenBrowser != nil { m.OpenBrowser(page) } }) if err == nil { err = saved.Save(m.Path) } m.mu.Lock() si.running = false if err != nil { si.err = err.Error() } m.mu.Unlock() if err != nil { m.Log.Printf("hub: signing in: %v", err) return } m.Log.Printf("hub: signed in to %s", saved.URL) m.run(saved) }() } // SignOut ends this computer's sign-in: the session on the hub, the saved sign-in, and the connection. func (m *Manager) SignOut() { m.mu.Lock() c, stop := m.client, m.stop m.client, m.saved, m.stop, m.username, m.signin = nil, nil, nil, "", nil m.mu.Unlock() if c != nil { c.Call(context.Background(), "POST", "/api/auth/sign-out", map[string]any{}, nil) } if stop != nil { stop() } if err := os.Remove(m.Path); err != nil && !errors.Is(err, os.ErrNotExist) { m.Log.Printf("hub: signing out: %v", err) } m.Log.Printf("hub: signed out") } // Intercept implements proxy.Interceptor: it answers requests to Host. func (m *Manager) Intercept(x *proxy.Exchange) error { if !strings.EqualFold(x.URL.Hostname(), Host) { return nil } method := x.Request.Method switch { case x.URL.Path == "/hub" && method == "GET": case x.URL.Path == "/hub/signin" && method == "POST": form, _ := url.ParseQuery(string(x.Body)) hubURL := strings.TrimSuffix(form.Get("hub"), "/") if hubURL != "" { u, err := url.Parse(hubURL) if err != nil || (u.Scheme != "https" && u.Hostname() != "localhost" && u.Hostname() != "127.0.0.1") { x.Response = proxy.LocalResponse(400, "application/json", []byte(`{"error":"the hub needs an https address"}`)) return nil } } m.SignIn(hubURL) case x.URL.Path == "/hub/signout" && method == "POST": m.SignOut() default: x.Response = proxy.LocalResponse(404, "application/json", []byte(`{"error":"not found"}`)) return nil } b, _ := json.Marshal(m.status()) x.Response = proxy.LocalResponse(200, "application/json", b) return nil }