//go:build linux && rootrunner package host import ( "crypto/ed25519" "encoding/base64" "encoding/json" "fmt" "io" "net" "net/http" "net/url" "os" "path/filepath" "strings" "sync/atomic" "testing" "time" "github.com/carlosframework/platform/internal/console" "github.com/carlosframework/platform/internal/provision" ) func plan14ConsolePayload() { st, err := console.Open(os.Getenv("CONSOLE_DB")) if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } defer st.Close() srv, err := console.NewServer(console.Config{Origin: "http://tenant", Keymail: os.Getenv("CONSOLE_KEYMAIL")}, st) if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } ln, err := net.Listen("tcp", os.Getenv("CONSOLE_ADDR")) if err == nil { err = http.Serve(ln, srv) } fmt.Fprintln(os.Stderr, err) os.Exit(1) } func plan14ConsoleProgram(t *testing.T, host, endpoint string) (claimUnitProgram, func(*http.Client)) { t.Helper() ln, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatal(err) } var contacted atomic.Int64 srv := &http.Server{Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { contacted.Add(1) w.WriteHeader(http.StatusBadRequest) })} go srv.Serve(ln) t.Cleanup(func() { srv.Close() }) keymail := "http://" + ln.Addr().String() p := claimUnitProgram{Schema: 1, Host: host, Args: []string{"plan14-console"}, Env: []string{ "PLAN14_CONSOLE=1", "CONSOLE_ADDR=" + endpoint, "CONSOLE_DB=" + provision.NestedDBPath("/data", host), "CONSOLE_KEYMAIL=" + keymail, }} device := "" return p, func(c *http.Client) { t.Helper() check := func(path, body string) map[string]any { t.Helper() resp, err := c.Post("http://tenant"+path, "application/json", strings.NewReader(body)) if err != nil { t.Fatal(err) } defer resp.Body.Close() var result map[string]any if err := json.NewDecoder(resp.Body).Decode(&result); err != nil || resp.StatusCode != http.StatusOK { t.Fatal(path, resp.Status, result, err) } return result } if device == "" { started := check("/auth/cli/start", `{"label":"reserved console"}`) device, _ = started["device_code"].(string) if device == "" { t.Fatal("console did not write login request", started) } } body, _ := json.Marshal(map[string]string{"device_code": device}) result := check("/auth/cli/poll", string(body)) if result["status"] != "pending" { t.Fatal("console lost persistent login request", result) } client := *c client.CheckRedirect = func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse } resp, err := client.Get("http://tenant/auth/signin") if err != nil { t.Fatal(err) } resp.Body.Close() location, err := url.Parse(resp.Header.Get("Location")) if err != nil || resp.StatusCode != http.StatusSeeOther || !strings.HasPrefix(location.String(), keymail+"/") { t.Fatal("console sign-in did not select the test upstream", err, resp.Status) } req, _ := http.NewRequest(http.MethodGet, "http://tenant/auth/callback?code=runner&state="+url.QueryEscape(location.Query().Get("state")), nil) for _, cookie := range resp.Cookies() { req.AddCookie(cookie) } resp, err = client.Do(req) if err != nil { t.Fatal("console exchange", err) } io.Copy(io.Discard, resp.Body) resp.Body.Close() if resp.StatusCode != http.StatusBadGateway || contacted.Load() != 0 { t.Fatal("console reached a sibling TCP upstream", resp.Status, contacted.Load()) } control := &http.Client{Timeout: time.Second} resp, err = control.Get(keymail) if err != nil { t.Fatal("upstream control", err) } resp.Body.Close() if contacted.Swap(0) != 1 { t.Fatal("control did not reach upstream") } } } func plan14ConsoleBinaryProgram(t *testing.T, p claimUnitProgram) claimUnitProgram { t.Helper() pub, _, err := ed25519.GenerateKey(nil) if err != nil { t.Fatal(err) } p.Args = []string{"serve"} p.Env = p.Env[1:] p.Env = append(p.Env, "CONSOLE_ORIGIN=http://tenant", "CARLOS_RELEASE_PUBKEY="+base64.StdEncoding.EncodeToString(pub), "CARLOS_DEPLOYMENT_DIR="+filepath.Join(provision.InstanceDir("/data", p.Host), "deployment"), "AWS_EC2_METADATA_DISABLED=true") return p }