Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions actions.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,11 @@ import (
"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"

"github.com/Davincible/chromedp-undetected/util/easyjson"
)

// Cookie is used to set browser cookies.
Expand Down
177 changes: 177 additions & 0 deletions chrome_unix.go
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"
}
120 changes: 120 additions & 0 deletions chrome_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
// Package chromedpundetected provides a chromedp context with an undetected
// Chrome browser.
package chromedpundetected

import (
"context"
"net"
"os"
"path"
"strconv"
"strings"

"github.com/Xuanwo/go-locale"
"github.com/chromedp/chromedp"
"github.com/google/uuid"
)

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
}

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, 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 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 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"
}
73 changes: 73 additions & 0 deletions chrome_windows_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package chromedpundetected

import (
"context"
"fmt"
"testing"
"time"

"github.com/chromedp/chromedp"
"github.com/hashicorp/go-multierror"
)

var n = 3

func TestChromedpundetected(t *testing.T) {
testRun(t,
n,
NewConfig(
WithTimeout(20*time.Second),
WithHeadless(),
),
func(ctx context.Context) error {
return chromedp.Run(ctx,
chromedp.Navigate("https://nowsecure.nl"),
chromedp.WaitVisible(`//div[@class="hystericalbg"]`),
)
},
)

t.Logf("Undetected!")
}

func testRun(t *testing.T, n int, cfg Config, run func(ctx context.Context) error) {
var gerr error
var success bool

// Attempt to run the tests multiple times, as in CI they are unstable.
for i := 0; i < n; i++ {
t.Logf("Attempt %d/%d", i+1, n)

ctx, cancel, err := New(cfg)
if err != nil {
gerr = multierror.Append(gerr, fmt.Errorf("create context: %w", err))
continue
}

if err = run(ctx); err != nil {
gerr = multierror.Append(gerr, fmt.Errorf("chromedp run: %w", err))

// Close Chrome instance.
ctxN, cancelN := context.WithTimeout(ctx, time.Second*10)
if err := chromedp.Cancel(ctxN); err != nil {
t.Logf("failed to cancel: %v", err)
}
cancelN()
cancel()

continue
}

success = true
if gerr != nil {
t.Logf("attempt %d, errors: %s", i+1, gerr.Error())
}

cancel()
break
}

if !success {
t.Fatal(gerr)
}
}
2 changes: 1 addition & 1 deletion config.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ type Option func(*Config)

// Config is a undetected Chrome config.
type Config struct {
// Ctx is the base context to use. By default a background context will be used.
// Ctx is the base context to use. By default, a background context will be used.
Ctx context.Context `json:"-" yaml:"-"`

// ContextOptions are chromedp context option.
Expand Down
Loading