| 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) |
| |
|
|
| 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(), |
| }) |
| } |