rod / main.go
aripbae's picture
Update main.go
43ec642 verified
Raw
History Blame Contribute Delete
15.3 kB
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"path/filepath"
"runtime"
"strconv"
"strings"
"sync"
"time"
"github.com/go-rod/rod"
"github.com/go-rod/rod/lib/launcher"
"github.com/go-rod/rod/lib/proto"
"github.com/go-rod/stealth"
)
var (
globalBrowser *rod.Browser
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 globalBrowser.MustClose()
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() {
path, _ := launcher.LookPath()
u := launcher.New().
Bin(path).
NoSandbox(true).
Headless(true).
Devtools(false).
Set("disable-gpu", "true").
Set("disable-dev-shm-usage", "true").
Set("disable-blink-features", "AutomationControlled").
Set("exclude-switches", "enable-automation").
Set("disable-gl-drawing-for-tests", "true").
Set("disable-software-rasterizer", "true").
Set("disable-canvas-aa", "true").
Set("disable-2d-canvas-clip-antialiasing", "true").
Set("disable-gpu-rasterization", "true").
Set("no-zygote", "true").
Set("single-process", "true").
Set("disable-infobars", "true").
Set("disable-notifications", "true").
Set("disable-popup-blocking", "true").
Set("disable-background-networking", "true").
Set("disable-renderer-backgrounding", "true").
Set("no-first-run", "true").
Set("no-default-browser-check", "true").
Set("window-size", "1366,768").
Set("lang", "en-US,en").
Set("platform", "Win32").
Set("blink-settings", "primaryHoverType=2,availableHoverTypes=2,primaryPointerType=4,availablePointerTypes=4").
MustLaunch()
globalBrowser = rod.New().ControlURL(u).MustConnect()
}
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
}
var logs []string
results := make(map[string]interface{})
var screenshots []string
var evals []interface{}
var networkMu sync.Mutex
var finalNetworkLogs []SniffedNetworkActivity
pageCtx, cancelPage := context.WithTimeout(context.Background(), time.Duration(tabTimeout)*time.Second)
defer cancelPage()
scopedBrowser := globalBrowser.Context(pageCtx)
p := stealth.MustPage(scopedBrowser)
defer p.MustClose()
_ = p.MustSetExtraHeaders("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8")
_ = p.MustSetExtraHeaders("Accept-Language", "en-US,en;q=0.9")
_ = p.MustSetExtraHeaders("Sec-Ch-Ua", `"Chromium";v="124", "Not A(Brand";v="24", "Google Chrome";v="124"`)
_ = p.MustSetExtraHeaders("Sec-Ch-Ua-Mobile", "?0")
_ = p.MustSetExtraHeaders("Sec-Ch-Ua-Platform", `"Windows"`)
width := 1366
height := 768
if req.Width > 0 && req.Height > 0 {
width = req.Width
height = req.Height
}
_ = p.SetViewport(&proto.EmulationSetDeviceMetricsOverride{
Width: width,
Height: height,
})
uaStr := "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 != "" {
uaStr = req.UserAgent
}
_ = p.SetUserAgent(&proto.NetworkSetUserAgentOverride{
UserAgent: uaStr,
})
if req.SniffNetwork {
maxSize := 1024 * 50
if req.MaxSniffSize > 0 {
maxSize = req.MaxSniffSize
}
router := p.HijackRequests()
router.MustAdd("*", func(ctx *rod.Hijack) {
ctx.MustLoadResponse()
res := ctx.Response
reqInfo := ctx.Request
contentType := strings.ToLower(res.Headers().Get("Content-Type"))
isData := strings.Contains(contentType, "json") ||
strings.Contains(contentType, "text/html") ||
strings.Contains(contentType, "javascript")
activity := SniffedNetworkActivity{
URL: reqInfo.URL().String(),
Method: reqInfo.Method(),
ResponseStatus: 200,
}
reqHeadersMentah, _ := json.Marshal(reqInfo.Headers())
resHeadersMentah, _ := json.Marshal(res.Headers())
reqHeadersMap := make(map[string]string)
resHeadersMap := make(map[string]string)
_ = json.Unmarshal(reqHeadersMentah, &reqHeadersMap)
_ = json.Unmarshal(resHeadersMentah, &resHeadersMap)
activity.RequestHeaders = reqHeadersMap
activity.ResponseHeaders = resHeadersMap
if isData {
body := ctx.Response.Body()
if len(body) > maxSize {
activity.ResponseBody = body[:maxSize] + " ... [TRUNCATED]"
} else {
activity.ResponseBody = body
}
} else {
activity.ResponseBody = "[SKIPPED: NON-DATA OR LARGE ASSET]"
}
networkMu.Lock()
finalNetworkLogs = append(finalNetworkLogs, activity)
networkMu.Unlock()
ctx.ContinueRequest(&proto.FetchContinueRequest{})
})
go router.Run()
}
_, _ = p.Eval(`
(() => {
const tamperWebdriver = () => {
try {
Object.defineProperty(navigator, 'webdriver', {
get: () => false,
configurable: false,
enumerable: true
});
} catch (e) {}
};
tamperWebdriver();
if (!window.chrome) {
Object.defineProperty(window, 'chrome', {
configurable: true,
enumerable: true,
value: { runtime: {} }
});
}
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; }
});
})();
`)
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 = p.Navigate(act.Value)
if err == nil {
_ = p.WaitDOMStable(500*time.Millisecond, 0.1)
if req.BlockImages {
injectStyleScript := `const style = document.createElement('style'); style.innerText = 'img, svg, picture, [style*="background-image"] { display: none !important; }'; document.head.appendChild(style);`
_, _ = p.Eval(injectStyleScript)
}
}
case "wait":
_, err = p.Element(act.Selector)
case "click":
var el *rod.Element
el, err = p.Element(act.Selector)
if err == nil {
err = el.Click(proto.InputMouseButtonLeft, 1)
}
case "type":
var el *rod.Element
el, err = p.Element(act.Selector)
if err == nil {
err = el.Input(act.Value)
}
case "clear":
var el *rod.Element
el, err = p.Element(act.Selector)
if err == nil {
err = el.SelectAllText()
if err == nil {
err = el.Input("")
}
}
case "hover":
var el *rod.Element
el, err = p.Element(act.Selector)
if err == nil {
err = el.Hover()
}
case "select":
var el *rod.Element
el, err = p.Element(act.Selector)
if err == nil {
err = el.Select([]string{act.Value}, true, rod.SelectorTypeRegex)
}
case "key":
jsInputScript := fmt.Sprintf(`(() => { const activeEl = document.activeElement; if (activeEl) { activeEl.value += %q; activeEl.dispatchEvent(new Event('input', { bubbles: true })); activeEl.dispatchEvent(new Event('change', { bubbles: true })); } })()`, act.Value)
_, err = p.Eval(jsInputScript)
case "scroll":
if act.Selector != "" {
var el *rod.Element
el, err = p.Element(act.Selector)
if err == nil {
err = el.ScrollIntoView()
}
} else {
valInt, _ := strconv.Atoi(act.Value)
_, err = p.Eval(fmt.Sprintf("window.scrollTo(0, %d);", valInt))
}
case "setLocation":
coords := strings.Split(act.Value, ",")
lat, _ := strconv.ParseFloat(coords[0], 64)
long, _ := strconv.ParseFloat(coords[1], 64)
accuracy := float64(100)
_ = p.SetViewport(&proto.EmulationSetDeviceMetricsOverride{
Width: width,
Height: height,
})
err = proto.EmulationSetGeolocationOverride{
Latitude: &lat,
Longitude: &long,
Accuracy: &accuracy,
}.Call(p)
case "evaluate":
var jsResult *proto.RuntimeRemoteObject
jsResult, err = p.Eval(act.Value)
if err == nil && jsResult != nil {
evals = append(evals, jsResult.Value.Val())
}
case "extractTurnstile":
var iframe *rod.Element
for retry := 0; retry < 15; retry++ {
iframe, _ = p.Element(`iframe[src*="challenges.cloudflare.com"], iframe[title*="verification"], .cf-turnstile`)
if iframe != nil {
break
}
time.Sleep(500 * time.Millisecond)
}
if iframe != nil {
_ = iframe.ScrollIntoView()
err = iframe.Click(proto.InputMouseButtonLeft, 1)
}
var tokenVal *proto.RuntimeRemoteObject
harvestScript := `
(() => {
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');
return input ? input.value : "";
})()
`
for retry := 0; retry < 30; retry++ {
tokenVal, err = p.Eval(harvestScript)
if err == nil && tokenVal != nil && tokenVal.Value.String() != "" {
break
}
time.Sleep(700 * time.Millisecond)
}
if tokenVal != nil && tokenVal.Value.String() != "" {
results["turnstileToken"] = tokenVal.Value.String()
} else {
results["turnstileToken"] = "FAILED_OR_TIMEOUT"
}
case "screenshot":
var buf []byte
if act.Selector != "" {
var el *rod.Element
el, err = p.Element(act.Selector)
if err == nil {
buf, err = el.Screenshot(proto.PageCaptureScreenshotFormatPng, 100)
}
} else {
buf = p.MustScreenshot("")
}
if err == nil && len(buf) > 0 {
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":
buf := p.MustScreenshotFullPage()
if len(buf) > 0 {
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 {
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)
pagesCount := 0
if globalBrowser != nil {
pages, err := globalBrowser.Pages()
if err == nil {
pagesCount = len(pages)
}
}
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(),
})
}