| package orchestrator |
|
|
| import ( |
| "fmt" |
| "net/http" |
| "net/url" |
| "strings" |
|
|
| "github.com/pinchtab/pinchtab/internal/bridge" |
| "github.com/pinchtab/pinchtab/internal/handlers" |
| iproxy "github.com/pinchtab/pinchtab/internal/proxy" |
| "github.com/pinchtab/pinchtab/internal/web" |
| ) |
|
|
| |
| |
| |
| |
| |
| func (o *Orchestrator) proxyTabRequest(w http.ResponseWriter, r *http.Request) { |
| tabID := r.PathValue("id") |
| if tabID == "" { |
| web.Error(w, 400, fmt.Errorf("tab id required")) |
| return |
| } |
|
|
| proxyToInstance := func(inst *bridge.Instance) { |
| targetURL, buildErr := o.instancePathURLFromBridge(inst, r.URL.Path, r.URL.RawQuery) |
| if buildErr != nil { |
| web.Error(w, 502, buildErr) |
| return |
| } |
| o.proxyToURL(w, r, targetURL) |
| } |
|
|
| |
| if o.instanceMgr != nil { |
| if inst, err := o.instanceMgr.FindInstanceByTabID(tabID); err == nil { |
| proxyToInstance(inst) |
| return |
| } |
| } |
|
|
| |
| inst, err := o.findRunningInstanceByTabID(tabID) |
| if err == nil { |
| |
| if o.instanceMgr != nil { |
| o.instanceMgr.Locator.Register(tabID, inst.ID) |
| } |
| proxyToInstance(&inst.Instance) |
| return |
| } |
|
|
| |
| |
| |
| |
| if only := o.singleRunningInstance(); only != nil { |
| proxyToInstance(&only.Instance) |
| return |
| } |
|
|
| web.Error(w, 404, err) |
| } |
|
|
| |
| func (o *Orchestrator) proxyToInstance(w http.ResponseWriter, r *http.Request) { |
| id := r.PathValue("id") |
|
|
| o.mu.RLock() |
| inst, ok := o.instances[id] |
| o.mu.RUnlock() |
|
|
| if !ok { |
| web.Error(w, 404, fmt.Errorf("instance %q not found", id)) |
| return |
| } |
|
|
| if inst.Status != "running" { |
| web.Error(w, 503, fmt.Errorf("instance %q is not running (status: %s)", id, inst.Status)) |
| return |
| } |
|
|
| targetPath := r.URL.Path |
| if len(targetPath) > len("/instances/"+id) { |
| targetPath = targetPath[len("/instances/"+id):] |
| } else { |
| targetPath = "" |
| } |
|
|
| targetURL, err := o.instancePathURL(inst, targetPath, r.URL.RawQuery) |
| if err != nil { |
| web.Error(w, 502, err) |
| return |
| } |
| o.proxyToURL(w, r, targetURL) |
| } |
|
|
| |
| func (o *Orchestrator) proxyToURL(w http.ResponseWriter, r *http.Request, targetURL *url.URL) { |
| iproxy.Forward(w, r, targetURL, iproxy.Options{ |
| Client: o.client, |
| AllowedURL: func(u *url.URL) bool { |
| return o.proxyTargetInstance(u) != nil |
| }, |
| RewriteRequest: func(req *http.Request) { |
| if inst := o.proxyTargetInstance(targetURL); inst != nil { |
| o.applyInstanceAuth(req, inst) |
| } |
| }, |
| }) |
| } |
|
|
| |
| func (o *Orchestrator) findRunningInstanceByTabID(tabID string) (*InstanceInternal, error) { |
| o.mu.RLock() |
| instances := make([]*InstanceInternal, 0, len(o.instances)) |
| for _, inst := range o.instances { |
| if inst.Status == "running" && instanceIsActive(inst) { |
| instances = append(instances, inst) |
| } |
| } |
| o.mu.RUnlock() |
|
|
| for _, inst := range instances { |
| tabs, err := o.fetchTabs(inst) |
| if err != nil { |
| continue |
| } |
| for _, tab := range tabs { |
| if tab.ID == tabID || o.idMgr.TabIDFromCDPTarget(tab.ID) == tabID { |
| return inst, nil |
| } |
| } |
| } |
| return nil, fmt.Errorf("tab %q not found", tabID) |
| } |
|
|
| func (o *Orchestrator) handleProxyScreencast(w http.ResponseWriter, r *http.Request) { |
| id := r.PathValue("id") |
|
|
| o.mu.RLock() |
| inst, ok := o.instances[id] |
| o.mu.RUnlock() |
| if !ok || inst.Status != "running" { |
| web.Error(w, 404, fmt.Errorf("instance not found or not running")) |
| return |
| } |
|
|
| targetURL, err := o.instancePathURL(inst, "/screencast", r.URL.RawQuery) |
| if err != nil { |
| web.Error(w, 502, err) |
| return |
| } |
|
|
| req := r.Clone(r.Context()) |
| req.Header = r.Header.Clone() |
| o.applyInstanceAuth(req, inst) |
|
|
| |
| handlers.ProxyWebSocket(w, req, targetURL.String()) |
| } |
|
|
| func (o *Orchestrator) instancePathURL(inst *InstanceInternal, path, rawQuery string) (*url.URL, error) { |
| if inst == nil { |
| return nil, fmt.Errorf("instance not found") |
| } |
| baseURL, err := o.parseHTTPInstanceURL(inst.URL, inst.Port) |
| if err != nil { |
| return nil, err |
| } |
| target := &url.URL{ |
| Scheme: baseURL.Scheme, |
| Host: baseURL.Host, |
| Path: path, |
| RawQuery: rawQuery, |
| } |
| return target, nil |
| } |
|
|
| func (o *Orchestrator) instancePathURLFromBridge(inst *bridge.Instance, path, rawQuery string) (*url.URL, error) { |
| if inst == nil { |
| return nil, fmt.Errorf("instance not found") |
| } |
| baseURL, err := o.parseHTTPInstanceURL(inst.URL, inst.Port) |
| if err != nil { |
| return nil, err |
| } |
| target := &url.URL{ |
| Scheme: baseURL.Scheme, |
| Host: baseURL.Host, |
| Path: path, |
| RawQuery: rawQuery, |
| } |
| return target, nil |
| } |
|
|
| func (o *Orchestrator) parseHTTPInstanceURL(rawURL, port string) (*url.URL, error) { |
| if rawURL == "" && port != "" { |
| rawURL = "http://localhost:" + port |
| } |
| parsed, err := url.Parse(rawURL) |
| if err != nil { |
| return nil, fmt.Errorf("invalid instance URL %q: %w", rawURL, err) |
| } |
| if parsed.Scheme != "http" && parsed.Scheme != "https" { |
| return nil, fmt.Errorf("instance %q is not an HTTP bridge", rawURL) |
| } |
| if parsed.Host == "" { |
| return nil, fmt.Errorf("invalid instance URL %q", rawURL) |
| } |
| if parsed.Path != "" && parsed.Path != "/" { |
| return nil, fmt.Errorf("instance URL %q must not include a path", rawURL) |
| } |
| return parsed, nil |
| } |
|
|
| func (o *Orchestrator) proxyTargetInstance(targetURL *url.URL) *InstanceInternal { |
| if targetURL == nil { |
| return nil |
| } |
| o.mu.RLock() |
| defer o.mu.RUnlock() |
| for _, inst := range o.instances { |
| baseURL, err := o.parseHTTPInstanceURL(inst.URL, inst.Port) |
| if err != nil { |
| continue |
| } |
| if sameOrigin(baseURL, targetURL) { |
| return inst |
| } |
| } |
| return nil |
| } |
|
|
| func sameOrigin(a, b *url.URL) bool { |
| if a == nil || b == nil { |
| return false |
| } |
| return strings.EqualFold(a.Scheme, b.Scheme) && strings.EqualFold(a.Host, b.Host) |
| } |
|
|
| func (o *Orchestrator) applyInstanceAuth(req *http.Request, inst *InstanceInternal) { |
| if req == nil || inst == nil { |
| return |
| } |
| token := inst.authToken |
| if token == "" { |
| token = o.childAuthToken |
| } |
| if token != "" { |
| req.Header.Set("Authorization", "Bearer "+token) |
| } |
| } |
|
|
| |
| func classifyLaunchError(err error) int { |
| msg := err.Error() |
| if strings.Contains(msg, "cannot contain") || strings.Contains(msg, "cannot be empty") { |
| return 400 |
| } |
| if strings.Contains(msg, "already") || strings.Contains(msg, "in use") { |
| return 409 |
| } |
| return 500 |
| } |
|
|
| func (o *Orchestrator) singleRunningInstance() *InstanceInternal { |
| o.mu.RLock() |
| defer o.mu.RUnlock() |
|
|
| var only *InstanceInternal |
| for _, inst := range o.instances { |
| if inst.Status != "running" || !instanceIsActive(inst) { |
| continue |
| } |
| if only != nil { |
| return nil |
| } |
| only = inst |
| } |
| return only |
| } |
|
|