nailed it 2.0

This commit is contained in:
cottongin
2025-11-02 16:49:12 -05:00
parent 9262ae79dd
commit 673c8025ee
5 changed files with 693 additions and 0 deletions

View File

@@ -0,0 +1,16 @@
module github.com/erikfredericks/get-kosmi-token
go 1.21
require github.com/chromedp/chromedp v0.9.2
require (
github.com/chromedp/cdproto v0.0.0-20230802225258-3cf4e6d46a89 // indirect
github.com/chromedp/sysutil v1.0.0 // indirect
github.com/gobwas/httphead v0.1.0 // indirect
github.com/gobwas/pool v0.2.1 // indirect
github.com/gobwas/ws v1.2.1 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
golang.org/x/sys v0.11.0 // indirect
)

View File

@@ -0,0 +1,23 @@
github.com/chromedp/cdproto v0.0.0-20230802225258-3cf4e6d46a89 h1:aPflPkRFkVwbW6dmcVqfgwp1i+UWGFH6VgR1Jim5Ygc=
github.com/chromedp/cdproto v0.0.0-20230802225258-3cf4e6d46a89/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs=
github.com/chromedp/chromedp v0.9.2 h1:dKtNz4kApb06KuSXoTQIyUC2TrA0fhGDwNZf3bcgfKw=
github.com/chromedp/chromedp v0.9.2/go.mod h1:LkSXJKONWTCHAfQasKFUZI+mxqS4tZqhmtGzzhLsnLs=
github.com/chromedp/sysutil v1.0.0 h1:+ZxhTpfpZlmchB58ih/LBHX52ky7w2VhQVKQMucy3Ic=
github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww=
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.2.1 h1:F2aeBZrm2NDsc7vbovKrWSogd4wvfAxg0FQ89/iqOTk=
github.com/gobwas/ws v1.2.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kULo2bwGEkFvCePZ3qHDDTC3/J9Swo=
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw=
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=

276
cmd/get-kosmi-token/main.go Normal file
View File

@@ -0,0 +1,276 @@
package main
import (
"context"
"flag"
"fmt"
"log"
"os"
"time"
"github.com/chromedp/chromedp"
)
// Standalone script to extract JWT token from Kosmi using browser automation
// Usage: go run get-token.go --email "email@email.com" --password "password"
var (
emailFlag = flag.String("email", "", "Email for Kosmi login")
passwordFlag = flag.String("password", "", "Password for Kosmi login")
headlessFlag = flag.Bool("headless", true, "Run browser in headless mode")
verboseFlag = flag.Bool("verbose", false, "Enable verbose logging")
)
func main() {
flag.Parse()
if *emailFlag == "" || *passwordFlag == "" {
log.Fatal("Error: --email and --password are required")
}
// Set up logging
if !*verboseFlag {
log.SetOutput(os.Stderr)
}
log.Println("Starting browser automation to extract JWT token...")
log.Printf("Email: %s", *emailFlag)
log.Printf("Headless mode: %v", *headlessFlag)
token, err := extractToken(*emailFlag, *passwordFlag, *headlessFlag)
if err != nil {
log.Fatalf("Failed to extract token: %v", err)
}
// Output token to stdout (so it can be captured)
fmt.Println(token)
if *verboseFlag {
log.Printf("✓ Successfully extracted token (length: %d)", len(token))
}
}
func extractToken(email, password string, headless bool) (string, error) {
// Create context with timeout
ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second)
defer cancel()
// Set up chromedp options
opts := []chromedp.ExecAllocatorOption{
chromedp.NoFirstRun,
chromedp.NoDefaultBrowserCheck,
chromedp.DisableGPU,
chromedp.NoSandbox,
}
if headless {
opts = append(opts, chromedp.Headless)
}
// Create allocator context
allocCtx, cancel := chromedp.NewExecAllocator(ctx, opts...)
defer cancel()
// Create browser context - suppress cookie errors unless verbose
logFunc := func(string, ...interface{}) {}
if *verboseFlag {
logFunc = log.Printf
}
ctx, cancel = chromedp.NewContext(allocCtx, chromedp.WithLogf(logFunc))
defer cancel()
var token string
// Run the automation tasks
err := chromedp.Run(ctx,
// Step 1: Navigate to Kosmi
chromedp.ActionFunc(func(ctx context.Context) error {
log.Println("→ Navigating to https://app.kosmi.io")
return chromedp.Navigate("https://app.kosmi.io").Do(ctx)
}),
// Step 2: Wait for page to load
chromedp.WaitReady("body"),
chromedp.Sleep(3*time.Second),
// Step 3: Find and click Login button
chromedp.ActionFunc(func(ctx context.Context) error {
log.Println("→ Looking for Login button...")
// Log all buttons we can see
if *verboseFlag {
var buttonTexts []string
chromedp.Evaluate(`
Array.from(document.querySelectorAll('button')).map(el => el.textContent.trim())
`, &buttonTexts).Do(ctx)
log.Printf(" Found buttons: %v", buttonTexts)
}
var found bool
if err := chromedp.Evaluate(`
(() => {
const buttons = Array.from(document.querySelectorAll('button'));
const btn = buttons.find(el => {
const text = el.textContent.trim();
return text === 'Login' || text === 'Log in';
});
if (btn) {
btn.click();
return true;
}
return false;
})()
`, &found).Do(ctx); err != nil {
return err
}
if !found {
return fmt.Errorf("Login button not found")
}
log.Println("✓ Clicked Login button")
return nil
}),
// Step 4: Wait for login modal
chromedp.Sleep(3*time.Second),
// Step 5: Click "Login with Email" button
chromedp.ActionFunc(func(ctx context.Context) error {
log.Println("→ Looking for 'Login with Email' button...")
if *verboseFlag {
var buttonTexts []string
chromedp.Evaluate(`
Array.from(document.querySelectorAll('button')).map(el => el.textContent.trim())
`, &buttonTexts).Do(ctx)
log.Printf(" Found buttons: %v", buttonTexts)
}
var found bool
if err := chromedp.Evaluate(`
(() => {
const btn = Array.from(document.querySelectorAll('button')).find(el =>
el.textContent.includes('Email')
);
if (btn) {
btn.click();
return true;
}
return false;
})()
`, &found).Do(ctx); err != nil {
return err
}
if !found {
return fmt.Errorf("'Login with Email' button not found")
}
log.Println("✓ Clicked 'Login with Email' button")
return nil
}),
// Step 6: Wait for email form
chromedp.Sleep(3*time.Second),
chromedp.WaitVisible(`input[type="password"]`, chromedp.ByQuery),
// Step 7: Fill in email
chromedp.ActionFunc(func(ctx context.Context) error {
log.Println("→ Filling email field...")
return nil
}),
chromedp.Click(`input[placeholder*="Email"], input[placeholder*="Username"]`, chromedp.ByQuery),
chromedp.Sleep(200*time.Millisecond),
chromedp.SendKeys(`input[placeholder*="Email"], input[placeholder*="Username"]`, email, chromedp.ByQuery),
chromedp.ActionFunc(func(ctx context.Context) error {
log.Println("✓ Email entered")
return nil
}),
chromedp.Sleep(500*time.Millisecond),
// Step 8: Fill in password
chromedp.ActionFunc(func(ctx context.Context) error {
log.Println("→ Filling password field...")
return nil
}),
chromedp.Click(`input[type="password"]`, chromedp.ByQuery),
chromedp.Sleep(200*time.Millisecond),
chromedp.SendKeys(`input[type="password"]`, password+"\n", chromedp.ByQuery),
chromedp.ActionFunc(func(ctx context.Context) error {
log.Println("✓ Password entered and form submitted")
return nil
}),
chromedp.Sleep(500*time.Millisecond),
// Step 10: Wait for login to complete - check for modal to close
chromedp.ActionFunc(func(ctx context.Context) error {
log.Println("→ Waiting for login to complete...")
// Wait for the login modal to disappear (indicates successful login)
maxAttempts := 30 // 15 seconds total
for i := 0; i < maxAttempts; i++ {
time.Sleep(500 * time.Millisecond)
// Check if login modal is gone (successful login)
var modalGone bool
chromedp.Evaluate(`
(() => {
// Check if the email/password form is still visible
const emailInput = document.querySelector('input[placeholder*="Email"], input[placeholder*="Username"]');
const passwordInput = document.querySelector('input[type="password"]');
return !emailInput && !passwordInput;
})()
`, &modalGone).Do(ctx)
if modalGone {
log.Println("✓ Login modal closed - login successful")
// Modal is gone, wait a bit more for token to be set
time.Sleep(2 * time.Second)
chromedp.Evaluate(`localStorage.getItem('token')`, &token).Do(ctx)
if token != "" {
log.Println("✓ Authenticated token received")
return nil
}
}
// Check for error messages
var errorText string
chromedp.Evaluate(`
(() => {
const errorEl = document.querySelector('[role="alert"], .error, .error-message');
return errorEl ? errorEl.textContent.trim() : '';
})()
`, &errorText).Do(ctx)
if errorText != "" && errorText != "null" {
return fmt.Errorf("login failed: %s", errorText)
}
}
// Timeout - get whatever token is there
log.Println("⚠ Login timeout - extracting current token")
chromedp.Evaluate(`localStorage.getItem('token')`, &token).Do(ctx)
if token == "" {
return fmt.Errorf("login timeout: no token found")
}
log.Println("✓ Token extracted (may be anonymous)")
return nil
}),
)
if err != nil {
return "", fmt.Errorf("browser automation failed: %w", err)
}
if token == "" {
return "", fmt.Errorf("failed to extract token from localStorage")
}
return token, nil
}