ChromeDP / main.go
aripbae's picture
improve
abdbb24 verified
Raw
History Blame Contribute Delete
20.4 kB
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"path/filepath"
"runtime"
"strconv"
"strings"
"sync"
"time"
"github.com/chromedp/cdproto/emulation"
"github.com/chromedp/cdproto/network"
"github.com/chromedp/cdproto/security"
"github.com/chromedp/cdproto/target"
"github.com/chromedp/chromedp"
)
var (
globalAllocatorCancel context.CancelFunc
globalBrowserCtx context.Context
statsMu sync.Mutex
totalRequests int64
totalErrors int64
activeTabs int64
startTime time.Time
tmpDir string
)
type AdvancedAction struct {
Type string `json:"type"`
Selector string `json:"selector,omitempty"`
Value string `json:"value,omitempty"`
FileName string `json:"fileName,omitempty"`
}
type Payload struct {
Timeout int `json:"timeout,omitempty"`
Width int `json:"width,omitempty"`
Height int `json:"height,omitempty"`
UserAgent string `json:"userAgent,omitempty"`
IgnoreHTTPSErrors bool `json:"ignoreHTTPSErrors,omitempty"`
DisableJS bool `json:"disableJS,omitempty"`
BlockImages bool `json:"blockImages,omitempty"`
SniffNetwork bool `json:"sniffNetwork,omitempty"`
MaxSniffSize int `json:"maxSniffSize,omitempty"`
Headers map[string]interface{} `json:"headers,omitempty"`
Actions []AdvancedAction `json:"actions"`
}
type StandardResponse struct {
Logs []string `json:"logs"`
Result map[string]interface{} `json:"result"`
Error string `json:"error,omitempty"`
}
type StatsResponse struct {
Uptime string `json:"uptime"`
TotalRequests int64 `json:"totalRequests"`
TotalErrors int64 `json:"totalErrors"`
ActiveTabs int64 `json:"activeTabs"`
OpenPagesCount int `json:"openPagesCount"`
GoVersion string `json:"goVersion"`
NumGoroutine int `json:"numGoroutine"`
AllocMemoryMB uint64 `json:"allocMemoryMB"`
TotalAllocMB uint64 `json:"totalAllocMB"`
SysMemoryMB uint64 `json:"sysMemoryMB"`
NumGC uint32 `json:"numGC"`
}
type SniffedNetworkActivity struct {
URL string `json:"url"`
Method string `json:"method"`
RequestHeaders map[string]string `json:"requestHeaders,omitempty"`
ResponseStatus int `json:"responseStatus,omitempty"`
ResponseHeaders map[string]string `json:"responseHeaders,omitempty"`
ResponseBody string `json:"responseBody,omitempty"`
}
func main() {
port := "7860"
startTime = time.Now()
tmpDir = os.TempDir()
initGlobalBrowser()
defer globalAllocatorCancel()
http.HandleFunc("/", statsHandler)
http.HandleFunc("/execute", executeHandler)
// http.HandleFunc("/stats", statsHandler)
fs := http.FileServer(http.Dir(tmpDir))
http.Handle("/tmp/", http.StripPrefix("/tmp/", fs))
fmt.Printf("Running on port %s...\n", port)
if err := http.ListenAndServe(":"+port, nil); err != nil {
log.Fatal(err)
}
}
func initGlobalBrowser() {
opts := append(chromedp.DefaultExecAllocatorOptions[:],
chromedp.NoSandbox,
chromedp.Flag("disable-sandbox", true),
chromedp.Headless,
chromedp.DisableGPU,
chromedp.Flag("disable-dev-shm-usage", true),
chromedp.Flag("disable-blink-features", "AutomationControlled"),
chromedp.Flag("exclude-switches", "enable-automation"),
chromedp.Flag("use-fake-codec-for-media-stream", true),
chromedp.Flag("disable-gl-drawing-for-tests", true),
chromedp.Flag("disable-software-rasterizer", true),
chromedp.Flag("disable-canvas-aa", true),
chromedp.Flag("disable-2d-canvas-clip-antialiasing", true),
chromedp.Flag("disable-2d-canvas-image-chromium", true),
chromedp.Flag("disable-gpu-rasterization", true),
chromedp.Flag("no-zygote", true),
chromedp.Flag("single-process", true),
chromedp.Flag("disable-infobars", true),
chromedp.Flag("disable-notifications", true),
chromedp.Flag("disable-popup-blocking", true),
chromedp.Flag("disable-background-networking", true),
chromedp.Flag("disable-renderer-backgrounding", true),
chromedp.Flag("no-first-run", true),
chromedp.Flag("no-default-browser-check", true),
chromedp.Flag("window-size", "1366,768"),
chromedp.Flag("lang", "en-US,en"),
chromedp.Flag("platform", "Win32"),
chromedp.Flag("blink-settings", "primaryHoverType=2,availableHoverTypes=2,primaryPointerType=4,availablePointerTypes=4"),
)
allocCtx, cancelAlloc := chromedp.NewExecAllocator(context.Background(), opts...)
globalAllocatorCancel = cancelAlloc
ctx, _ := chromedp.NewContext(allocCtx)
globalBrowserCtx = ctx
if err := chromedp.Run(globalBrowserCtx); err != nil {
log.Fatalf("Failed to initialize Chromium: %v", err)
}
}
func executeHandler(w http.ResponseWriter, r *http.Request) {
statsMu.Lock()
totalRequests++
activeTabs++
statsMu.Unlock()
defer func() {
statsMu.Lock()
activeTabs--
statsMu.Unlock()
}()
if r.Method != http.MethodPost {
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
return
}
var req Payload
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
tabTimeout := 30
if req.Timeout > 0 {
tabTimeout = req.Timeout
}
tabCtx, cancelTab := chromedp.NewContext(globalBrowserCtx)
defer cancelTab()
timeoutCtx, cancelTimeout := context.WithTimeout(tabCtx, time.Duration(tabTimeout)*time.Second)
defer cancelTimeout()
var logs []string
results := make(map[string]interface{})
var screenshots []string
var evals []interface{}
var networkMu sync.Mutex
networkRegistry := make(map[network.RequestID]*SniffedNetworkActivity)
var closedRequestIDs []network.RequestID
if req.SniffNetwork {
_ = chromedp.Run(timeoutCtx, network.Enable())
chromedp.ListenTarget(timeoutCtx, func(ev interface{}) {
switch e := ev.(type) {
case *network.EventRequestWillBeSent:
networkMu.Lock()
reqHeaders := make(map[string]string)
for k, v := range e.Request.Headers {
if strVal, ok := v.(string); ok {
reqHeaders[k] = strVal
}
}
networkRegistry[e.RequestID] = &SniffedNetworkActivity{
URL: e.Request.URL,
Method: e.Request.Method,
RequestHeaders: reqHeaders,
}
networkMu.Unlock()
case *network.EventResponseReceived:
networkMu.Lock()
if activity, exists := networkRegistry[e.RequestID]; exists {
activity.ResponseStatus = int(e.Response.Status)
resHeaders := make(map[string]string)
for k, v := range e.Response.Headers {
if strVal, ok := v.(string); ok {
resHeaders[k] = strVal
}
}
activity.ResponseHeaders = resHeaders
}
networkMu.Unlock()
case *network.EventLoadingFinished:
networkMu.Lock()
closedRequestIDs = append(closedRequestIDs, e.RequestID)
networkMu.Unlock()
}
})
}
_ = chromedp.Run(timeoutCtx, chromedp.ActionFunc(func(ctx context.Context) error {
_ = target.SetAutoAttach(true, false).WithFlatten(true).Do(ctx)
hookScript := `
window.__turnstileToken = '';
const hookTurnstile = () => {
if (!window.turnstile || window.turnstile.__hooked) return;
const origRender = window.turnstile.render.bind(window.turnstile);
window.turnstile.render = function(container, params) {
if (params && typeof params.callback === 'function') {
const origCb = params.callback;
params.callback = function(token) {
window.__turnstileToken = token;
return origCb(token);
};
}
return origRender(container, params);
};
window.turnstile.__hooked = true;
};
Object.defineProperty(window, 'turnstile', {
configurable: true,
set(v) { this.__ts = v; setTimeout(hookTurnstile, 0); },
get() { return this.__ts; }
});
`
var tmp interface{}
return chromedp.Evaluate(hookScript, &tmp).Do(ctx)
}))
if req.Width > 0 && req.Height > 0 {
logs = append(logs, fmt.Sprintf("Setup Viewport: %dx%d", req.Width, req.Height))
_ = chromedp.Run(timeoutCtx, chromedp.EmulateViewport(int64(req.Width), int64(req.Height)))
} else {
_ = chromedp.Run(timeoutCtx, chromedp.EmulateViewport(1366, 768))
}
defaultUA := "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36"
if req.UserAgent != "" {
defaultUA = req.UserAgent
}
_ = chromedp.Run(timeoutCtx, chromedp.ActionFunc(func(ctx context.Context) error {
return emulation.SetUserAgentOverride(defaultUA).Do(ctx)
}))
baseHeaders := network.Headers{
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8",
"Accept-Language": "en-US,en;q=0.9",
"Cache-Control": "no-cache",
"Pragma": "no-cache",
"Sec-Ch-Ua": `"Chromium";v="124", "Not A(Brand";v="24", "Google Chrome";v="124"`,
"Sec-Ch-Ua-Mobile": "?0",
"Sec-Ch-Ua-Platform": `"Windows"`,
"Sec-Fetch-Dest": "document",
"Sec-Fetch-Mode": "navigate",
"Sec-Fetch-Site": "none",
"Sec-Fetch-User": "?1",
"Upgrade-Insecure-Requests": "1",
}
for k, v := range req.Headers {
baseHeaders[k] = v
}
_ = chromedp.Run(timeoutCtx, network.SetExtraHTTPHeaders(baseHeaders))
if req.IgnoreHTTPSErrors {
logs = append(logs, "Setup Ignore HTTPS Errors")
_ = chromedp.Run(timeoutCtx, security.SetIgnoreCertificateErrors(true))
}
if req.DisableJS {
logs = append(logs, "Setup Disable JavaScript")
_ = chromedp.Run(timeoutCtx, emulation.SetScriptExecutionDisabled(true))
}
for i, act := range req.Actions {
logMsg := fmt.Sprintf("Step %d: [%s]", i+1, act.Type)
if act.Selector != "" {
logMsg += fmt.Sprintf(" -> Target: %s", act.Selector)
}
logs = append(logs, logMsg)
var err error
switch act.Type {
case "navigate":
err = chromedp.Run(timeoutCtx, chromedp.Navigate(act.Value))
if err == nil && req.BlockImages {
logs = append(logs, "Injecting CSS to block images visual rendering")
var tmp interface{}
injectStyleScript := `const style = document.createElement('style'); style.innerText = 'img, svg, picture, [style*="background-image"] { display: none !important; }'; document.head.appendChild(style);`
_ = chromedp.Run(timeoutCtx, chromedp.Evaluate(injectStyleScript, &tmp))
}
case "wait":
err = chromedp.Run(timeoutCtx, chromedp.WaitReady(act.Selector, chromedp.ByQuery))
case "click":
err = chromedp.Run(timeoutCtx, chromedp.Click(act.Selector, chromedp.ByQuery))
case "type":
err = chromedp.Run(timeoutCtx, chromedp.SendKeys(act.Selector, act.Value, chromedp.ByQuery))
case "clear":
err = chromedp.Run(timeoutCtx, chromedp.Clear(act.Selector, chromedp.ByQuery))
case "hover":
var tmp interface{}
hoverScript := fmt.Sprintf(`const e = document.querySelector("%s"); if(e) { e.dispatchEvent(new MouseEvent('mouseover', {bubbles: true})); }`, act.Selector)
err = chromedp.Run(timeoutCtx, chromedp.Evaluate(hoverScript, &tmp))
case "select":
var tmp interface{}
selectScript := fmt.Sprintf(`const e = document.querySelector("%s"); if(e) { e.value = "%s"; e.dispatchEvent(new Event('change', {bubbles: true})); }`, act.Selector, act.Value)
err = chromedp.Run(timeoutCtx, chromedp.Evaluate(selectScript, &tmp))
case "key":
var tmp interface{}
keyScript := fmt.Sprintf(`const e = document.activeElement; if(e) { e.dispatchEvent(new KeyboardEvent('keydown', {'key': '%s', bubbles: true})); }`, act.Value)
err = chromedp.Run(timeoutCtx, chromedp.Evaluate(keyScript, &tmp))
case "scroll":
if act.Selector != "" {
err = chromedp.Run(timeoutCtx, chromedp.ScrollIntoView(act.Selector, chromedp.ByQuery))
} else {
scrollScript := fmt.Sprintf("window.scrollTo(0, %s);", act.Value)
var tmp interface{}
err = chromedp.Run(timeoutCtx, chromedp.Evaluate(scrollScript, &tmp))
}
case "setLocation":
coords := strings.Split(act.Value, ",")
lat, _ := strconv.ParseFloat(coords[0], 64)
long, _ := strconv.ParseFloat(coords[1], 64)
timezone := coords[2]
err = chromedp.Run(timeoutCtx,
emulation.SetGeolocationOverride().WithLatitude(lat).WithLongitude(long).WithAccuracy(100),
emulation.SetTimezoneOverride(timezone),
)
case "evaluate":
var jsResult interface{}
err = chromedp.Run(timeoutCtx, chromedp.Evaluate(act.Value, &jsResult))
if err == nil {
evals = append(evals, jsResult)
}
case "extractTurnstile":
logs = append(logs, "Attempting to bypass and extract Cloudflare Turnstile...")
var coordinates map[string]interface{}
findIframeScript := `
(() => {
const iframe = document.querySelector('iframe[src*="challenges.cloudflare.com"]') ||
document.querySelector('iframe[title*="verification"]') ||
document.querySelector('.cf-turnstile');
if (!iframe) return null;
const rect = iframe.getBoundingClientRect();
return { x: rect.left + (rect.width / 2), y: rect.top + (rect.height / 2), found: true };
})();
`
for retry := 0; retry < 15; retry++ {
_ = chromedp.Run(timeoutCtx, chromedp.Evaluate(findIframeScript, &coordinates))
if coordinates != nil && coordinates["found"] == true {
break
}
time.Sleep(500 * time.Millisecond)
}
if coordinates != nil && coordinates["found"] == true {
posX := coordinates["x"].(float64)
posY := coordinates["y"].(float64)
logs = append(logs, fmt.Sprintf("Interactive Turnstile detected. Sending click to coordinate: X=%.2f, Y=%.2f", posX, posY))
err = chromedp.Run(timeoutCtx, chromedp.MouseClickXY(posX, posY))
}
var token interface{}
harvestScript := `
(async () => {
for (let i = 0; i < 40; i++) {
if (window.__turnstileToken && window.__turnstileToken.length > 10) {
return window.__turnstileToken;
}
let input = document.querySelector('input[name="cf-turnstile-response"]') ||
document.querySelector('input[name="g-recaptcha-response"]') ||
document.getElementById('cf-turnstile-response');
if (input && input.value && input.value.length > 10) {
return input.value;
}
await new Promise(r => setTimeout(r, 700));
}
return "";
})();
`
err = chromedp.Run(timeoutCtx, chromedp.Evaluate(harvestScript, &token))
if err == nil && token != "" {
logs = append(logs, "Cloudflare Turnstile bypassed successfully via Object Hook/DOM!")
results["turnstileToken"] = token
} else {
networkMu.Lock()
foundToken := ""
for _, activity := range networkRegistry {
if strings.Contains(activity.URL, "/v0/g/") && activity.Method == "POST" {
lines := strings.Split(activity.URL, "/")
if len(lines) > 0 {
foundToken = lines[len(lines)-1]
}
}
}
networkMu.Unlock()
if foundToken != "" {
logs = append(logs, "Cloudflare Turnstile extracted via active Network Sniffing trace!")
results["turnstileToken"] = foundToken
} else {
logs = append(logs, "Turnstile verification failed or timed out.")
results["turnstileToken"] = "FAILED_OR_TIMEOUT"
}
}
case "screenshot":
var buf []byte
if act.Selector != "" {
err = chromedp.Run(timeoutCtx, chromedp.Screenshot(act.Selector, &buf, chromedp.ByQuery))
} else {
err = chromedp.Run(timeoutCtx, chromedp.CaptureScreenshot(&buf))
}
if err == nil {
name := act.FileName
if name == "" {
name = fmt.Sprintf("ss-%d.png", time.Now().UnixNano())
}
name = strings.ReplaceAll(name, " ", "_")
filePath := filepath.Join(tmpDir, name)
err = os.WriteFile(filePath, buf, 0644)
if err == nil {
screenshots = append(screenshots, "/tmp/"+name)
}
}
case "screenshotFullPage":
var buf []byte
var width, height int64
sizeScript := `(() => { return { width: Math.max(document.body.scrollWidth, document.documentElement.scrollWidth), height: Math.max(document.body.scrollHeight, document.documentElement.scrollHeight) }; })()`
var dimensions map[string]interface{}
err = chromedp.Run(timeoutCtx, chromedp.Evaluate(sizeScript, &dimensions))
if err == nil && dimensions != nil {
width = int64(dimensions["width"].(float64))
height = int64(dimensions["height"].(float64))
_ = chromedp.Run(timeoutCtx, chromedp.EmulateViewport(width, height))
err = chromedp.Run(timeoutCtx, chromedp.CaptureScreenshot(&buf))
}
if err == nil {
name := act.FileName
if name == "" {
name = fmt.Sprintf("fullss-%d.png", time.Now().UnixNano())
}
name = strings.ReplaceAll(name, " ", "_")
filePath := filepath.Join(tmpDir, name)
err = os.WriteFile(filePath, buf, 0644)
if err == nil {
screenshots = append(screenshots, "/tmp/"+name)
}
}
}
if err != nil {
statsMu.Lock()
totalErrors++
statsMu.Unlock()
returnError(w, logs, fmt.Errorf("Failed in step %d (%s): %w", i+1, act.Type, err))
return
}
}
results["screenshots"] = screenshots
results["evals"] = evals
if req.SniffNetwork {
maxSize := 1024 * 50
if req.MaxSniffSize > 0 {
maxSize = req.MaxSniffSize
}
networkMu.Lock()
copiedIDs := make([]network.RequestID, len(closedRequestIDs))
copy(copiedIDs, closedRequestIDs)
copiedRegistry := make(map[network.RequestID]SniffedNetworkActivity)
for _, id := range copiedIDs {
if actPtr, exists := networkRegistry[id]; exists && actPtr != nil {
copiedRegistry[id] = *actPtr
}
}
networkMu.Unlock()
var finalNetworkLogs []SniffedNetworkActivity
for _, reqID := range copiedIDs {
activity, exists := copiedRegistry[reqID]
if !exists {
continue
}
contentType := strings.ToLower(activity.ResponseHeaders["content-type"])
isData := strings.Contains(contentType, "json") ||
strings.Contains(contentType, "text/html") ||
strings.Contains(contentType, "javascript")
if isData && activity.ResponseStatus == 200 {
var bodyBuf []byte
sniffCtx, sniffCancel := context.WithTimeout(timeoutCtx, 1*time.Second)
_ = chromedp.Run(sniffCtx, chromedp.ActionFunc(func(ctx context.Context) error {
var err error
bodyBuf, err = network.GetResponseBody(reqID).Do(ctx)
return err
}))
sniffCancel()
if len(bodyBuf) > 0 {
if len(bodyBuf) > maxSize {
activity.ResponseBody = string(bodyBuf[:maxSize]) + " ... [TRUNCATED]"
} else {
activity.ResponseBody = string(bodyBuf)
}
} else {
activity.ResponseBody = "[SKIPPED: BODY ACQUISITION TIMEOUT OR EMPTY]"
}
} else {
activity.ResponseBody = "[SKIPPED: NON-DATA OR LARGE ASSET]"
}
finalNetworkLogs = append(finalNetworkLogs, activity)
}
results["networkLogs"] = finalNetworkLogs
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(StandardResponse{
Logs: logs,
Result: results,
})
}
func statsHandler(w http.ResponseWriter, r *http.Request) {
var m runtime.MemStats
runtime.ReadMemStats(&m)
var targets []*target.Info
ctx, cancel := context.WithTimeout(globalBrowserCtx, 2*time.Second)
infos, err := chromedp.Targets(ctx)
cancel()
if err == nil {
targets = infos
}
pagesCount := 0
for _, t := range targets {
if t.Type == "page" {
pagesCount++
}
}
statsMu.Lock()
res := StatsResponse{
Uptime: time.Since(startTime).String(),
TotalRequests: totalRequests,
TotalErrors: totalErrors,
ActiveTabs: activeTabs,
OpenPagesCount: pagesCount,
GoVersion: runtime.Version(),
NumGoroutine: runtime.NumGoroutine(),
AllocMemoryMB: m.Alloc / 1024 / 1024,
TotalAllocMB: m.TotalAlloc / 1024 / 1024,
SysMemoryMB: m.Sys / 1024 / 1024,
NumGC: m.NumGC,
}
statsMu.Unlock()
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(res)
}
func returnError(w http.ResponseWriter, logs []string, err error) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(StandardResponse{
Logs: logs,
Error: err.Error(),
})
}