diff --git a/README.md b/README.md index d82d314..2eb0323 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,7 @@ capture metadata https://example.com --pretty capture animated https://example.com -X duration=5 -o recording.gif capture sessions create --max-ttl-seconds 300 --pretty +capture sessions create --cdp --pretty capture sessions get sess_123 --pretty capture sessions action sess_123 goto --payload-json '{"url":"https://example.com"}' capture sessions action sess_123 screenshot -X fullPage=true --pretty @@ -108,9 +109,21 @@ func main() { "fullPage": true, }) c.CloseSession(sessionID) + + // CDP browser sessions + cdpCreated, _ := c.CreateSession(&capture.CreateSessionOptions{ + CDP: true, + }) + cdpSession := cdpCreated["session"].(map[string]interface{}) + connectURL := cdpSession["connectUrl"].(string) + println(connectURL) } ``` +CDP sessions return a `connectUrl` in the session object for Chrome DevTools +Protocol clients. CDP cannot be combined with `Proxy`/`--proxy` or +`BypassBotDetection`/`--bypass-bot-detection`. + ### Options ```go diff --git a/capture.go b/capture.go index aa8b57c..4a73c8f 100644 --- a/capture.go +++ b/capture.go @@ -29,6 +29,7 @@ type RequestOptions map[string]interface{} type CreateSessionOptions struct { MaxTtlSeconds int `json:"maxTtlSeconds,omitempty"` + CDP bool `json:"cdp,omitempty"` Proxy bool `json:"proxy,omitempty"` BypassBotDetection bool `json:"bypassBotDetection,omitempty"` } diff --git a/capture_test.go b/capture_test.go index 7a25937..30b2ca5 100644 --- a/capture_test.go +++ b/capture_test.go @@ -359,12 +359,53 @@ func TestCreateSession(t *testing.T) { if requestBody["maxTtlSeconds"] != float64(300) || requestBody["proxy"] != true { t.Fatalf("unexpected request body: %#v", requestBody) } + if _, ok := requestBody["cdp"]; ok { + t.Fatalf("cdp should be omitted for non-CDP session requests: %#v", requestBody) + } session, ok := response["session"].(map[string]interface{}) if !ok || response["success"] != true || session["id"] != "sess_123" || session["maxTtlSeconds"] != float64(300) { t.Fatalf("unexpected response: %#v", response) } } +func TestCreateSessionCDP(t *testing.T) { + var requestBody map[string]interface{} + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if err := json.NewDecoder(r.Body).Decode(&requestBody); err != nil { + t.Errorf("failed to decode request body: %v", err) + } + w.WriteHeader(http.StatusCreated) + _ = json.NewEncoder(w).Encode(map[string]interface{}{ + "success": true, + "session": map[string]interface{}{ + "id": "sess_cdp", + "status": "active", + "cdp": true, + "connectUrl": "wss://connect.capture.page/sess_cdp", + }, + }) + })) + defer server.Close() + + c := New("user_123", "secret") + c.EdgeURL = server.URL + response, err := c.CreateSession(&CreateSessionOptions{CDP: true}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if requestBody["cdp"] != true { + t.Fatalf("expected cdp request body, got %#v", requestBody) + } + if _, ok := requestBody["proxy"]; ok { + t.Fatalf("proxy should be omitted for CDP-only session requests: %#v", requestBody) + } + session, ok := response["session"].(map[string]interface{}) + if !ok || session["cdp"] != true || session["connectUrl"] != "wss://connect.capture.page/sess_cdp" { + t.Fatalf("unexpected response: %#v", response) + } +} + func TestGetCloseAndExecuteAction(t *testing.T) { var requests []string var actionBody map[string]interface{} diff --git a/example/main.go b/example/main.go index 82f488c..8fad202 100644 --- a/example/main.go +++ b/example/main.go @@ -107,4 +107,16 @@ func main() { fmt.Println("PDF saved as document.pdf") } } + + // Example 7: Create a CDP session and read its connectUrl + cdpSessionResp, err := c.CreateSession(&capture.CreateSessionOptions{ + CDP: true, + }) + if err != nil { + log.Printf("Error creating CDP session: %v", err) + } else if session, ok := cdpSessionResp["session"].(map[string]interface{}); ok { + if connectURL, ok := session["connectUrl"].(string); ok { + fmt.Printf("CDP Connect URL: %s\n", connectURL) + } + } } diff --git a/internal/cli/sessions.go b/internal/cli/sessions.go index cb0cb8a..41f88bc 100644 --- a/internal/cli/sessions.go +++ b/internal/cli/sessions.go @@ -46,6 +46,7 @@ var sessionsActionCmd = &cobra.Command{ var ( sessionMaxTTLSeconds int + sessionCDP bool sessionProxy bool sessionBypassBotDetection bool sessionsPretty bool @@ -58,6 +59,7 @@ func init() { sessionsCmd.AddCommand(sessionsCreateCmd, sessionsGetCmd, sessionsCloseCmd, sessionsActionCmd) sessionsCreateCmd.Flags().IntVar(&sessionMaxTTLSeconds, "max-ttl-seconds", 0, "Maximum session lifetime in seconds") + sessionsCreateCmd.Flags().BoolVar(&sessionCDP, "cdp", false, "Create a Chrome DevTools Protocol session") sessionsCreateCmd.Flags().BoolVar(&sessionProxy, "proxy", false, "Use the authenticated user's configured browser proxy") sessionsCreateCmd.Flags().BoolVar(&sessionBypassBotDetection, "bypass-bot-detection", false, "Use Capture's bot-detection bypass browser when available") sessionsCreateCmd.Flags().BoolVar(&sessionsPretty, "pretty", false, "Pretty print JSON output") @@ -74,6 +76,7 @@ func runSessionsCreate(cmd *cobra.Command, args []string) error { client := newCaptureClient() options := &capture.CreateSessionOptions{ MaxTtlSeconds: sessionMaxTTLSeconds, + CDP: sessionCDP, Proxy: sessionProxy, BypassBotDetection: sessionBypassBotDetection, } diff --git a/internal/cli/sessions_test.go b/internal/cli/sessions_test.go new file mode 100644 index 0000000..510ac72 --- /dev/null +++ b/internal/cli/sessions_test.go @@ -0,0 +1,13 @@ +package cli + +import "testing" + +func TestSessionsCreateCommandHasCDPFlag(t *testing.T) { + flag := sessionsCreateCmd.Flags().Lookup("cdp") + if flag == nil { + t.Fatal("expected sessions create --cdp flag") + } + if flag.DefValue != "false" { + t.Fatalf("expected --cdp default false, got %q", flag.DefValue) + } +}