-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fit folders into a parent folder for organization.
- Loading branch information
Showing
25 changed files
with
4,078 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,162 @@ | ||
package chromedpundetected | ||
|
||
import ( | ||
"context" | ||
"encoding/json" | ||
"fmt" | ||
"io" | ||
"os" | ||
"time" | ||
|
||
"github.com/Davincible/chromedp-undetected/util/easyjson" | ||
"github.com/chromedp/cdproto/cdp" | ||
"github.com/chromedp/cdproto/emulation" | ||
"github.com/chromedp/cdproto/network" | ||
"github.com/chromedp/chromedp" | ||
) | ||
|
||
// Cookie is used to set browser cookies. | ||
type Cookie struct { | ||
Name string `json:"name" yaml:"name"` | ||
Value string `json:"value" yaml:"value"` | ||
Domain string `json:"domain" yaml:"domain"` | ||
Path string `json:"path" yaml:"path"` | ||
Expires float64 `json:"expires" yaml:"expires"` | ||
HTTPOnly bool `json:"httpOnly" yaml:"httpOnly"` | ||
Secure bool `json:"secure" yaml:"secure"` | ||
} | ||
|
||
// UserAgentOverride overwrites the Chrome user agent. | ||
// | ||
// It's better to use this method than emulation.UserAgentOverride. | ||
func UserAgentOverride(userAgent string) chromedp.ActionFunc { | ||
return func(ctx context.Context) error { | ||
return cdp.Execute(ctx, "Network.setUserAgentOverride", | ||
emulation.SetUserAgentOverride(userAgent), nil) | ||
} | ||
} | ||
|
||
// LoadCookiesFromFile takes a file path to a json file containing cookies, and | ||
// loads in the cookies into the browser. | ||
func LoadCookiesFromFile(path string) chromedp.ActionFunc { | ||
return chromedp.ActionFunc(func(ctx context.Context) error { | ||
f, err := os.Open(path) //nolint:gosec | ||
if err != nil { | ||
return fmt.Errorf("failed to open file '%s': %w", path, err) | ||
} | ||
|
||
data, err := io.ReadAll(f) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
if err := f.Close(); err != nil { | ||
return err | ||
} | ||
|
||
var cookies []Cookie | ||
if err := json.Unmarshal(data, &cookies); err != nil { | ||
return fmt.Errorf("unmarshal cookies from json: %w", err) | ||
} | ||
|
||
return LoadCookies(cookies)(ctx) | ||
}) | ||
} | ||
|
||
// LoadCookies will load a set of cookies into the browser. | ||
func LoadCookies(cookies []Cookie) chromedp.ActionFunc { | ||
return chromedp.ActionFunc(func(ctx context.Context) error { | ||
for _, cookie := range cookies { | ||
expiry := cdp.TimeSinceEpoch(time.Unix(int64(cookie.Expires), 0)) | ||
if err := network.SetCookie(cookie.Name, cookie.Value). | ||
WithHTTPOnly(cookie.HTTPOnly). | ||
WithSecure(cookie.Secure). | ||
WithDomain(cookie.Domain). | ||
WithPath(cookie.Path). | ||
WithExpires(&expiry). | ||
Do(ctx); err != nil { | ||
return err | ||
} | ||
} | ||
return nil | ||
}) | ||
} | ||
|
||
// SaveCookies extracts the cookies from the current URL and appends them to | ||
// provided array. | ||
func SaveCookies(cookies *[]Cookie) chromedp.ActionFunc { | ||
return chromedp.ActionFunc(func(ctx context.Context) error { | ||
c, err := network.GetCookies().Do(ctx) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
for _, cookie := range c { | ||
*cookies = append(*cookies, Cookie{ | ||
Name: cookie.Name, | ||
Value: cookie.Value, | ||
Domain: cookie.Domain, | ||
Path: cookie.Path, | ||
Expires: cookie.Expires, | ||
HTTPOnly: cookie.HTTPOnly, | ||
Secure: cookie.HTTPOnly, | ||
}) | ||
} | ||
|
||
return nil | ||
}) | ||
} | ||
|
||
// SaveCookiesTo extracts the cookies from the current page and saves them | ||
// as JSON to the provided path. | ||
func SaveCookiesTo(path string) chromedp.ActionFunc { | ||
return chromedp.ActionFunc(func(ctx context.Context) error { | ||
var c []Cookie | ||
|
||
if err := SaveCookies(&c).Do(ctx); err != nil { | ||
return err | ||
} | ||
|
||
b, err := json.Marshal(c) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
if err := os.WriteFile(path, b, 0644); err != nil { //nolint:gosec | ||
return err | ||
} | ||
|
||
return nil | ||
}) | ||
} | ||
|
||
// RunCommandWithRes runs any Chrome Dev Tools command, with any params and | ||
// sets the result to the res parameter. Make sure it is a pointer. | ||
// | ||
// In contrast to the native method of chromedp, with this method you can directly | ||
// pass in a map with the data passed to the command. | ||
func RunCommandWithRes(method string, params, res any) chromedp.ActionFunc { | ||
return chromedp.ActionFunc(func(ctx context.Context) error { | ||
i := easyjson.New(params) | ||
o := easyjson.New(res) | ||
|
||
return cdp.Execute(ctx, method, i, o) | ||
}) | ||
} | ||
|
||
// RunCommand runs any Chrome Dev Tools command, with any params. | ||
// | ||
// In contrast to the native method of chromedp, with this method you can directly | ||
// pass in a map with the data passed to the command. | ||
func RunCommand(method string, params any) chromedp.ActionFunc { | ||
return chromedp.ActionFunc(func(ctx context.Context) error { | ||
i := easyjson.New(params) | ||
|
||
return cdp.Execute(ctx, method, i, nil) | ||
}) | ||
} | ||
|
||
// BlockURLs blocks a set of URLs in Chrome. | ||
func BlockURLs(url ...string) chromedp.ActionFunc { | ||
return RunCommand("Network.setBlockedURLs", map[string][]string{"urls": url}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,177 @@ | ||
//go:build unix | ||
|
||
// Package chromedpundetected provides a chromedp context with an undetected | ||
// Chrome browser. | ||
package chromedpundetected | ||
|
||
import ( | ||
"context" | ||
"net" | ||
"os" | ||
"os/exec" | ||
"path" | ||
"strconv" | ||
"strings" | ||
"syscall" | ||
|
||
"github.com/Xuanwo/go-locale" | ||
"github.com/chromedp/chromedp" | ||
"github.com/google/uuid" | ||
"golang.org/x/exp/slog" | ||
) | ||
|
||
var ( | ||
// DefaultUserDirPrefix Defaults. | ||
DefaultUserDirPrefix = "chromedp-undetected-" | ||
) | ||
|
||
// New creates a context with an undetected Chrome executor. | ||
func New(config Config) (context.Context, context.CancelFunc, error) { | ||
var opts []chromedp.ExecAllocatorOption | ||
|
||
userDataDir := path.Join(os.TempDir(), DefaultUserDirPrefix+uuid.NewString()) | ||
if len(config.ChromePath) > 0 { | ||
userDataDir = config.ChromePath | ||
} | ||
|
||
headlessOpts, closeFrameBuffer, err := headlessFlag(config) | ||
if err != nil { | ||
return nil, func() {}, err | ||
} | ||
|
||
opts = append(opts, localeFlag()) | ||
opts = append(opts, supressWelcomeFlag()...) | ||
opts = append(opts, logLevelFlag(config)) | ||
opts = append(opts, debuggerAddrFlag(config)...) | ||
opts = append(opts, noSandboxFlag(config)...) | ||
opts = append(opts, chromedp.UserDataDir(userDataDir)) | ||
opts = append(opts, headlessOpts...) | ||
opts = append(opts, config.ChromeFlags...) | ||
|
||
ctx := context.Background() | ||
if config.Ctx != nil { | ||
ctx = config.Ctx | ||
} | ||
|
||
cancelT := func() {} | ||
if config.Timeout > 0 { | ||
ctx, cancelT = context.WithTimeout(ctx, config.Timeout) | ||
} | ||
|
||
ctx, cancelA := chromedp.NewExecAllocator(ctx, opts...) | ||
ctx, cancelC := chromedp.NewContext(ctx, config.ContextOptions...) | ||
|
||
cancel := func() { | ||
cancelT() | ||
cancelA() | ||
cancelC() | ||
|
||
if err := closeFrameBuffer(); err != nil { | ||
slog.Error("failed to close Xvfb", err) | ||
} | ||
|
||
if len(config.ChromePath) == 0 { | ||
_ = os.RemoveAll(userDataDir) //nolint:errcheck | ||
} | ||
} | ||
|
||
return ctx, cancel, nil | ||
} | ||
|
||
func supressWelcomeFlag() []chromedp.ExecAllocatorOption { | ||
return []chromedp.ExecAllocatorOption{ | ||
chromedp.Flag("no-first-run", true), | ||
chromedp.Flag("no-default-browser-check", true), | ||
} | ||
} | ||
|
||
func debuggerAddrFlag(config Config) []chromedp.ExecAllocatorOption { | ||
port := strconv.Itoa(config.Port) | ||
if config.Port == 0 { | ||
port = getRandomPort() | ||
} | ||
|
||
return []chromedp.ExecAllocatorOption{ | ||
chromedp.Flag("remote-debugging-host", "127.0.0.1"), | ||
chromedp.Flag("remote-debugging-port", port), | ||
} | ||
} | ||
|
||
func localeFlag() chromedp.ExecAllocatorOption { | ||
lang := "en-US" | ||
if tag, err := locale.Detect(); err != nil && len(tag.String()) > 0 { | ||
lang = tag.String() | ||
} | ||
|
||
return chromedp.Flag("lang", lang) | ||
} | ||
|
||
func noSandboxFlag(config Config) []chromedp.ExecAllocatorOption { | ||
var opts []chromedp.ExecAllocatorOption | ||
|
||
if config.NoSandbox { | ||
opts = append(opts, | ||
chromedp.Flag("no-sandbox", true), | ||
chromedp.Flag("test-type", true)) | ||
} | ||
|
||
return opts | ||
} | ||
|
||
func logLevelFlag(config Config) chromedp.ExecAllocatorOption { | ||
return chromedp.Flag("log-level", strconv.Itoa(config.LogLevel)) | ||
} | ||
|
||
func headlessFlag(config Config) ([]chromedp.ExecAllocatorOption, func() error, error) { | ||
var opts []chromedp.ExecAllocatorOption | ||
|
||
cleanup := func() error { return nil } | ||
|
||
if config.Headless { | ||
// Create virtual display | ||
frameBuffer, err := newFrameBuffer("1920x1080x24") | ||
if err != nil { | ||
return nil, nil, err | ||
} | ||
|
||
cleanup = frameBuffer.Stop | ||
|
||
opts = append(opts, | ||
// chromedp.Flag("headless", true), | ||
chromedp.Flag("window-size", "1920,1080"), | ||
chromedp.Flag("start-maximized", true), | ||
chromedp.Flag("no-sandbox", true), | ||
chromedp.ModifyCmdFunc(func(cmd *exec.Cmd) { | ||
cmd.Env = append(cmd.Env, "DISPLAY=:"+frameBuffer.Display) | ||
cmd.Env = append(cmd.Env, "XAUTHORITY="+frameBuffer.AuthPath) | ||
|
||
// Default modify command per chromedp | ||
if _, ok := os.LookupEnv("LAMBDA_TASK_ROOT"); ok { | ||
// do nothing on AWS Lambda | ||
return | ||
} | ||
|
||
if cmd.SysProcAttr == nil { | ||
cmd.SysProcAttr = new(syscall.SysProcAttr) | ||
} | ||
|
||
// When the parent process dies (Go), kill the child as well. | ||
cmd.SysProcAttr.Pdeathsig = syscall.SIGKILL | ||
}), | ||
) | ||
} | ||
|
||
return opts, cleanup, nil | ||
} | ||
|
||
func getRandomPort() string { | ||
l, err := net.Listen("tcp", "127.0.0.1:0") | ||
if err == nil { | ||
addr := l.Addr().String() | ||
_ = l.Close() //nolint:errcheck,gosec | ||
|
||
return strings.Split(addr, ":")[1] | ||
} | ||
|
||
return "42069" | ||
} |
Oops, something went wrong.