mirror of
https://github.com/phishingclub/phishingclub.git
synced 2026-07-30 07:28:48 +02:00
fix remote browser improved remote support
Signed-off-by: RonniSkansing <rskansing@gmail.com>
This commit is contained in:
@@ -34,6 +34,18 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
libxext6 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# headful chrome support. xvfb provides the display, one per browser session so
|
||||
# sessions cannot read each other through X, and chrome draws its window with gtk
|
||||
# and uses the screensaver, xtest and input extensions
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
xvfb \
|
||||
libgtk-3-0 \
|
||||
libxss1 \
|
||||
libxtst6 \
|
||||
libxi6 \
|
||||
libxcursor1 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# install deps
|
||||
#RUN go install github.com/cosmtrek/air@latest \
|
||||
#RUN go install github.com/go-delve/delve/cmd/dlv@1.9.1
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-rod/rod"
|
||||
@@ -702,6 +703,15 @@ func (m *RemoteBrowserController) ServeVictim(g *gin.Context) {
|
||||
streamMouseMoveLast := map[string]time.Time{}
|
||||
const streamMouseMoveMinInterval = 16 * time.Millisecond
|
||||
|
||||
// Paste is the only recipient input carrying a payload, and it is the only
|
||||
// one whose cost in the browser scales with its size. In remote mode one
|
||||
// browser serves every session, so an unbounded paste loop from a single
|
||||
// recipient would degrade all of them. Bound both size and rate: an address
|
||||
// or a password never comes close to the cap.
|
||||
var streamPasteLast time.Time
|
||||
const streamPasteMaxLen = 4096
|
||||
const streamPasteMinInterval = 200 * time.Millisecond
|
||||
|
||||
for {
|
||||
_, msg, err := conn.ReadMessage()
|
||||
if err != nil {
|
||||
@@ -733,6 +743,7 @@ func (m *RemoteBrowserController) ServeVictim(g *gin.Context) {
|
||||
KeyCode int64 `json:"keyCode"`
|
||||
Modifiers int64 `json:"modifiers"`
|
||||
CharText string `json:"charText"`
|
||||
Text string `json:"text"`
|
||||
Width float64 `json:"width"`
|
||||
Height float64 `json:"height"`
|
||||
Dpr float64 `json:"dpr"`
|
||||
@@ -765,6 +776,33 @@ func (m *RemoteBrowserController) ServeVictim(g *gin.Context) {
|
||||
}
|
||||
if val, exists := activeNamedStreams.Load(cmd.Name); exists {
|
||||
si := val.(*streamInfo)
|
||||
// Paste carries text, not a position, so it must not go through
|
||||
// the coordinate mapping below, which drops the event whenever the
|
||||
// stream box is not measured yet.
|
||||
if cmd.Action == "paste" {
|
||||
now := time.Now()
|
||||
if cmd.Text == "" || now.Sub(streamPasteLast) < streamPasteMinInterval {
|
||||
continue
|
||||
}
|
||||
streamPasteLast = now
|
||||
text := cmd.Text
|
||||
if len(text) > streamPasteMaxLen {
|
||||
// Cut back to a rune boundary so a multi byte character
|
||||
// split by the cap does not reach Chrome as broken UTF-8.
|
||||
text = text[:streamPasteMaxLen]
|
||||
for len(text) > 0 && !utf8.ValidString(text) {
|
||||
text = text[:len(text)-1]
|
||||
}
|
||||
}
|
||||
if page := sess.getBrowserPage(); page != nil {
|
||||
payload, _ := json.Marshal(map[string]interface{}{
|
||||
"type": "paste",
|
||||
"text": text,
|
||||
})
|
||||
m.dispatchInput(page, payload)
|
||||
}
|
||||
continue
|
||||
}
|
||||
// cmd.X/Y are in cropped-canvas JPEG pixels; map back to CDP CSS coords.
|
||||
cdpX, cdpY, ok := si.getInputCoords(cmd.X, cmd.Y)
|
||||
if ok {
|
||||
@@ -1299,6 +1337,11 @@ func (m *RemoteBrowserController) StreamLiveSession(g *gin.Context) {
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
// Focus emulation is set per target, so a popup starts without it and
|
||||
// falls back to the real window focus. Only one window can hold that,
|
||||
// which would leave a background popup reporting itself unfocused and
|
||||
// stop it rendering.
|
||||
proto.EmulationSetFocusEmulationEnabled{Enabled: true}.Call(newPage) //nolint:errcheck
|
||||
tabsMu.Lock()
|
||||
tabs[info.TargetID] = &tabEntry{page: newPage, url: info.URL}
|
||||
tabsMu.Unlock()
|
||||
|
||||
@@ -191,9 +191,28 @@
|
||||
}, { passive: false });
|
||||
}
|
||||
|
||||
// isPasteCombo reports the paste shortcut. Its default action must run so the
|
||||
// browser fires a paste event, and the key itself must not be forwarded: the
|
||||
// remote browser would paste from its own clipboard, which lives on the
|
||||
// server and holds nothing the person in front of this page copied.
|
||||
// Shift+Insert pastes on Linux and Windows just like Ctrl+V.
|
||||
function isPasteCombo(e) {
|
||||
if ((e.ctrlKey || e.metaKey) && !e.altKey && (e.key === 'v' || e.key === 'V')) return true;
|
||||
return e.shiftKey && !e.ctrlKey && !e.metaKey && e.key === 'Insert';
|
||||
}
|
||||
|
||||
// Releasing the modifier before the letter leaves a keyup whose modifier
|
||||
// flags no longer say paste. Forwarding it would send the remote page a
|
||||
// keyup with no matching keydown, so remember which keys were held back.
|
||||
var suppressedKeys = {};
|
||||
|
||||
// Arrow keys: always preventDefault (prevent page scroll when canvas is focused),
|
||||
// but only forwarded to the remote browser when { arrowKeys: true }.
|
||||
canvas.addEventListener('keydown', function (e) {
|
||||
if (isPasteCombo(e)) {
|
||||
suppressedKeys[e.code || e.key] = true;
|
||||
return;
|
||||
}
|
||||
var isArrow = !!ARROW_KEYS[e.key];
|
||||
e.preventDefault();
|
||||
if (isArrow && !allowArrows) return;
|
||||
@@ -204,6 +223,11 @@
|
||||
});
|
||||
|
||||
canvas.addEventListener('keyup', function (e) {
|
||||
var held = e.code || e.key;
|
||||
if (isPasteCombo(e) || suppressedKeys[held]) {
|
||||
delete suppressedKeys[held];
|
||||
return;
|
||||
}
|
||||
var isArrow = !!ARROW_KEYS[e.key];
|
||||
e.preventDefault();
|
||||
if (isArrow && !allowArrows) return;
|
||||
@@ -212,6 +236,19 @@
|
||||
modifiers: (e.altKey ? 1 : 0) | (e.ctrlKey ? 2 : 0) | (e.metaKey ? 4 : 0) | (e.shiftKey ? 8 : 0) });
|
||||
});
|
||||
|
||||
// Paste carries its text on the event itself, so filling a form from a
|
||||
// password manager or from a copied address works without asking for
|
||||
// clipboard permission, which would put a browser prompt in front of the
|
||||
// page. Only what is pasted into this canvas is read, never the clipboard
|
||||
// on its own. The server turns it into Input.insertText on the remote page.
|
||||
canvas.addEventListener('paste', function (e) {
|
||||
e.preventDefault();
|
||||
var data = e.clipboardData || window.clipboardData;
|
||||
var text = data ? data.getData('text') : '';
|
||||
if (!text) return;
|
||||
snd({ type: 'stream_input', name: name, action: 'paste', text: text });
|
||||
});
|
||||
|
||||
canvas.addEventListener('contextmenu', function (e) { e.preventDefault(); });
|
||||
}
|
||||
};
|
||||
|
||||
+163
-14
@@ -13,7 +13,9 @@ import (
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
@@ -210,6 +212,94 @@ func resolveBrowserRootDir() (string, error) {
|
||||
return filepath.Join(filepath.Dir(execPath), "data", "browser"), nil
|
||||
}
|
||||
|
||||
// xvfbScreen is the geometry of a session's private display. The recipient's real
|
||||
// viewport and screen metrics are applied over CDP after the page opens, so this
|
||||
// only has to be large enough to hold the browser window.
|
||||
const xvfbScreen = "1920x1080x24"
|
||||
|
||||
// x11SocketDir is where X servers place their per display sockets. Xvfb does not
|
||||
// create it, so a bare container image where nothing else has made it would leave
|
||||
// the server unable to open a socket. Creating it here keeps headful working
|
||||
// without adding a step to every Dockerfile.
|
||||
const x11SocketDir = "/tmp/.X11-unix"
|
||||
|
||||
// startPrivateDisplay launches an Xvfb dedicated to a single browser session and
|
||||
// returns its display name together with a stop function.
|
||||
//
|
||||
// Sessions get a display each instead of sharing one for two reasons. A dedicated
|
||||
// display means the browser window is the only window on it, so no two sessions
|
||||
// can compete for the foreground, which is what previously froze every session but
|
||||
// the most recent one. It also stops the accidental crossover a shared display
|
||||
// allows, where any client can read the windows and input of every other client
|
||||
// on it. This is not a boundary against a hostile escaped renderer: every browser
|
||||
// runs as the same user, so such code could reach a sibling display regardless.
|
||||
// That case is the already accepted remote browser RCE risk; guarding it would
|
||||
// need separate users or namespaces, not an X cookie, which the same user can read.
|
||||
//
|
||||
// Xvfb chooses the display number itself and reports it back on the descriptor
|
||||
// passed with -displayfd, so concurrent sessions never race for the same number.
|
||||
func startPrivateDisplay() (string, func(), error) {
|
||||
// Best effort: usually already present. If creating it was actually needed
|
||||
// and failed, the read below surfaces the concrete failure.
|
||||
_ = os.MkdirAll(x11SocketDir, 0o777)
|
||||
|
||||
pr, pw, err := os.Pipe()
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
cmd := exec.Command("Xvfb", "-displayfd", "3", "-screen", "0", xvfbScreen, "-nolisten", "tcp", "-noreset")
|
||||
cmd.ExtraFiles = []*os.File{pw} // the child sees this as descriptor 3
|
||||
if err := cmd.Start(); err != nil {
|
||||
pr.Close() //nolint:errcheck
|
||||
pw.Close() //nolint:errcheck
|
||||
return "", nil, fmt.Errorf("xvfb start failed: %w", err)
|
||||
}
|
||||
// Xvfb owns the write end now. Dropping ours means a read returns instead of
|
||||
// blocking if the server dies before reporting a display.
|
||||
pw.Close() //nolint:errcheck
|
||||
|
||||
exited := make(chan error, 1)
|
||||
go func() { exited <- cmd.Wait() }()
|
||||
|
||||
var stopOnce sync.Once
|
||||
stop := func() {
|
||||
stopOnce.Do(func() {
|
||||
if cmd.Process != nil {
|
||||
// SIGTERM first so Xvfb removes its own lock file and socket;
|
||||
// SIGKILL would strand both and slowly litter /tmp.
|
||||
cmd.Process.Signal(syscall.SIGTERM) //nolint:errcheck
|
||||
select {
|
||||
case <-exited:
|
||||
case <-time.After(3 * time.Second):
|
||||
cmd.Process.Kill() //nolint:errcheck
|
||||
<-exited
|
||||
}
|
||||
}
|
||||
pr.Close() //nolint:errcheck
|
||||
})
|
||||
}
|
||||
|
||||
pr.SetReadDeadline(time.Now().Add(10 * time.Second)) //nolint:errcheck
|
||||
var out []byte
|
||||
chunk := make([]byte, 16)
|
||||
for !bytes.ContainsRune(out, '\n') && len(out) < 32 {
|
||||
n, readErr := pr.Read(chunk)
|
||||
if n > 0 {
|
||||
out = append(out, chunk[:n]...)
|
||||
}
|
||||
if readErr != nil {
|
||||
stop()
|
||||
return "", nil, fmt.Errorf("xvfb did not report a display: %w", readErr)
|
||||
}
|
||||
}
|
||||
num := strings.TrimSpace(string(out))
|
||||
if num == "" {
|
||||
stop()
|
||||
return "", nil, errors.New("xvfb reported an empty display")
|
||||
}
|
||||
return ":" + num, stop, nil
|
||||
}
|
||||
|
||||
// chromeSterrWriter is an io.Writer that fans Chrome stdout/stderr lines out to
|
||||
// the session emitter (when emitter != nil) and/or the app logger (when logger != nil).
|
||||
type chromeSterrWriter struct {
|
||||
@@ -664,6 +754,33 @@ func (r *Runner) Run(ctx context.Context) error {
|
||||
emitter.errorf(fmt.Sprintf("browser connect failed: %v", connectErr))
|
||||
return goja.Undefined()
|
||||
}
|
||||
// Every session runs in its own browser context. All sessions share one
|
||||
// remote browser, so on the default context they would also share one
|
||||
// cookie jar, one localStorage and one cache: a session captured for one
|
||||
// recipient would be readable from the page of any other recipient
|
||||
// running at the same time. A browser context keeps that storage separate
|
||||
// and discards all of it when the context is disposed.
|
||||
//
|
||||
// DisposeOnDetach makes the browser drop the context when this control
|
||||
// connection goes away, which covers the paths where the session ends
|
||||
// without running its own cleanup: victim disconnect, script timeout,
|
||||
// operator cancel, and a server crash.
|
||||
ctxRes, ctxErr := (proto.TargetCreateBrowserContext{DisposeOnDetach: true}).Call(browser)
|
||||
if ctxErr != nil {
|
||||
// Do not close the browser here. It is shared with every other
|
||||
// session and closing it would end all of them.
|
||||
emitter.errorf(fmt.Sprintf("browser context create failed: %v", ctxErr))
|
||||
return goja.Undefined()
|
||||
}
|
||||
// Rebind to the new context. rod carries BrowserContextID into every
|
||||
// target it creates, and its Close disposes the context instead of the
|
||||
// whole browser once the field is set, so from here on the shared remote
|
||||
// browser is never closed by this session.
|
||||
isolated := *browser
|
||||
isolated.BrowserContextID = ctxRes.BrowserContextID
|
||||
browser = &isolated
|
||||
emitter.log(fmt.Sprintf("[session] isolated browser context %s", ctxRes.BrowserContextID))
|
||||
|
||||
var pageErr error
|
||||
page, pageErr = browser.Page(proto.TargetCreateTarget{URL: "about:blank"})
|
||||
if pageErr != nil {
|
||||
@@ -699,6 +816,34 @@ func (r *Runner) Run(ctx context.Context) error {
|
||||
}
|
||||
chromeLogger = cw
|
||||
}
|
||||
chromeVars := []string{
|
||||
"XDG_CONFIG_HOME=" + filepath.Join(rootDir, "config"),
|
||||
"XDG_CACHE_HOME=" + filepath.Join(rootDir, "cache"),
|
||||
}
|
||||
// Headful needs a display, and it has to be one this session alone owns.
|
||||
if !opts.Headless {
|
||||
display, stopDisplay, dispErr := startPrivateDisplay()
|
||||
switch {
|
||||
case dispErr == nil:
|
||||
emitter.log(fmt.Sprintf("[session] private display %s", display))
|
||||
go func() {
|
||||
<-outerCtx.Done()
|
||||
stopDisplay()
|
||||
}()
|
||||
// Appended last so it beats any DISPLAY inherited from the
|
||||
// server process: the last value wins once the environment is
|
||||
// deduplicated on the way to the browser.
|
||||
chromeVars = append(chromeVars, "DISPLAY="+display)
|
||||
case os.Getenv("DISPLAY") != "":
|
||||
// A desktop session already has a display. Use it rather than
|
||||
// failing, and say so, because it is shared with everything
|
||||
// else drawing on that desktop.
|
||||
emitter.log(fmt.Sprintf("[session] warning: no private display (%v), falling back to the shared %s", dispErr, os.Getenv("DISPLAY")))
|
||||
default:
|
||||
emitter.errorf(fmt.Sprintf("headful browser needs a display: %v", dispErr))
|
||||
return goja.Undefined()
|
||||
}
|
||||
}
|
||||
l := launcher.New().
|
||||
Headless(opts.Headless).
|
||||
Logger(chromeLogger).
|
||||
@@ -706,10 +851,7 @@ func (r *Runner) Run(ctx context.Context) error {
|
||||
Set("crash-dumps-dir", crashDir).
|
||||
Set("disable-blink-features", "AutomationControlled").
|
||||
Delete("enable-automation").
|
||||
Env(chromeEnv(
|
||||
"XDG_CONFIG_HOME="+filepath.Join(rootDir, "config"),
|
||||
"XDG_CACHE_HOME="+filepath.Join(rootDir, "cache"),
|
||||
)...)
|
||||
Env(chromeEnv(chromeVars...)...)
|
||||
|
||||
var binPath string
|
||||
if r.ExecPath != "" {
|
||||
@@ -834,6 +976,18 @@ func (r *Runner) Run(ctx context.Context) error {
|
||||
applyRemoteIdentity(page, opts.UserAgent, emitter)
|
||||
}
|
||||
|
||||
// Headful Chrome ties a page's focus and render pipeline to the state of its
|
||||
// operating system window, and one browser or one display can only ever have
|
||||
// a single foreground window. Concurrent sessions would then fight over it:
|
||||
// whichever page was raised last keeps rendering while the rest are treated
|
||||
// as background, stop producing screencast frames, and report
|
||||
// document.hasFocus() false. Focus emulation detaches the page from that
|
||||
// window state, so every session renders and reads as focused at the same
|
||||
// time. It also removes the unfocused window tell in headful mode.
|
||||
if err := (proto.EmulationSetFocusEmulationEnabled{Enabled: true}).Call(page); err != nil {
|
||||
emitter.log(fmt.Sprintf("[session] warning: focus emulation failed: %v", err))
|
||||
}
|
||||
|
||||
// Timezone override keeps the navigator/Intl timezone aligned with the exit
|
||||
// network's geolocation. Left to the operator because only they know the
|
||||
// proxy/exit location; an unset value keeps the host timezone.
|
||||
@@ -887,11 +1041,10 @@ func (r *Runner) Run(ctx context.Context) error {
|
||||
|
||||
session.Set("close", func(call goja.FunctionCall) goja.Value {
|
||||
emitter.log("[session] closing")
|
||||
if opts.Remote != "" {
|
||||
page.Close() //nolint:errcheck
|
||||
} else {
|
||||
browser.Close() //nolint:errcheck
|
||||
}
|
||||
// Both branches call Close on the browser handle, and in remote mode
|
||||
// that handle is bound to this session's browser context, so it
|
||||
// disposes the context and its storage rather than the shared browser.
|
||||
browser.Close() //nolint:errcheck
|
||||
return goja.Undefined()
|
||||
})
|
||||
|
||||
@@ -974,11 +1127,7 @@ func (r *Runner) Run(ctx context.Context) error {
|
||||
return goja.Undefined()
|
||||
case <-idleCh:
|
||||
emitter.log(fmt.Sprintf("[session] idle timeout (%dms), closing", opts.IdleTimeout))
|
||||
if opts.Remote != "" {
|
||||
page.Close() //nolint:errcheck
|
||||
} else {
|
||||
browser.Close() //nolint:errcheck
|
||||
}
|
||||
browser.Close() //nolint:errcheck
|
||||
return goja.Undefined()
|
||||
case msg := <-r.Incoming:
|
||||
resetIdle()
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
# development docker file
|
||||
# real Google Chrome running headful on a virtual X display, used by the
|
||||
# remote browser feature over the DevTools protocol
|
||||
FROM debian:bookworm-slim@sha256:7b140f374b289a7c2befc338f42ebe6441b7ea838a042bbd5acbfca6ec875818
|
||||
|
||||
# chrome is pinned by version and checksum. to upgrade read the current values from
|
||||
# https://dl.google.com/linux/chrome/deb/dists/stable/main/binary-amd64/Packages
|
||||
ARG CHROME_VERSION=150.0.7871.186-1
|
||||
ARG CHROME_SHA256=4193e00b6d5d5969ee63f7a69596868f546aa0e8cb077b3e0bf9cc1e2c719d00
|
||||
|
||||
# xvfb provides the display, openbox gives chrome a focused window, x11vnc plus
|
||||
# novnc make the display watchable from a browser, socat republishes the loopback
|
||||
# only devtools port on the container network
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
ca-certificates \
|
||||
curl \
|
||||
xvfb \
|
||||
openbox \
|
||||
x11vnc \
|
||||
novnc \
|
||||
websockify \
|
||||
socat \
|
||||
tini \
|
||||
fonts-liberation \
|
||||
fonts-noto-color-emoji \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN curl -fsSL -o /tmp/chrome.deb \
|
||||
"https://dl.google.com/linux/chrome/deb/pool/main/g/google-chrome-stable/google-chrome-stable_${CHROME_VERSION}_amd64.deb" \
|
||||
&& echo "${CHROME_SHA256} /tmp/chrome.deb" | sha256sum -c - \
|
||||
&& apt-get update \
|
||||
&& apt-get install -y --no-install-recommends /tmp/chrome.deb \
|
||||
&& rm -f /tmp/chrome.deb \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# google ships a daily apt cron and a repo entry with the package. neither is
|
||||
# wanted here because the version is pinned above
|
||||
RUN rm -f /etc/cron.daily/google-chrome /etc/apt/sources.list.d/google-chrome.list
|
||||
|
||||
RUN groupadd -g 1000 chrome && \
|
||||
useradd -u 1000 -g chrome -m -d /home/chrome chrome
|
||||
|
||||
# an empty named volume inherits the permissions of the image directory it is
|
||||
# mounted over, so the socket dir must be world writable before the volume exists
|
||||
RUN mkdir -p /tmp/.X11-unix && chmod 1777 /tmp/.X11-unix
|
||||
|
||||
COPY start.sh /usr/local/bin/start.sh
|
||||
RUN chmod +x /usr/local/bin/start.sh
|
||||
|
||||
USER chrome
|
||||
WORKDIR /home/chrome
|
||||
|
||||
EXPOSE 8109 9222
|
||||
|
||||
# -g forwards signals to the whole process group so xvfb, openbox and the vnc
|
||||
# stack stop with chrome instead of lingering
|
||||
ENTRYPOINT ["/usr/bin/tini", "-g", "--", "/usr/local/bin/start.sh"]
|
||||
@@ -0,0 +1,71 @@
|
||||
#!/bin/sh
|
||||
# starts the virtual display, a window manager, the vnc viewer stack and chrome.
|
||||
# every background process is killed when this script exits so the container
|
||||
# stops cleanly instead of leaving a half dead display behind.
|
||||
set -eu
|
||||
|
||||
DISPLAY_NUM="${DISPLAY_NUM:-99}"
|
||||
SCREEN="${SCREEN:-1920x1080x24}"
|
||||
DEVTOOLS_PORT="${DEVTOOLS_PORT:-9222}"
|
||||
NOVNC_PORT="${NOVNC_PORT:-8109}"
|
||||
|
||||
# chrome binds devtools to loopback only, so it listens on a private port and
|
||||
# socat republishes it on the container network below
|
||||
CHROME_DEVTOOLS_PORT=9223
|
||||
VNC_PORT=5900
|
||||
|
||||
WIDTH="${SCREEN%%x*}"
|
||||
HEIGHT_DEPTH="${SCREEN#*x}"
|
||||
HEIGHT="${HEIGHT_DEPTH%%x*}"
|
||||
|
||||
export DISPLAY=":${DISPLAY_NUM}"
|
||||
|
||||
cleanup() {
|
||||
trap - EXIT INT TERM
|
||||
kill 0 2>/dev/null || true
|
||||
}
|
||||
trap cleanup EXIT INT TERM
|
||||
|
||||
# the X socket is created inside a shared volume so other containers can render
|
||||
# on this display too. access control is off because the socket is only reachable
|
||||
# through that volume, never over the network
|
||||
Xvfb "$DISPLAY" -screen 0 "$SCREEN" -ac -noreset \
|
||||
+extension RANDR +extension GLX +render &
|
||||
|
||||
for _ in $(seq 1 100); do
|
||||
[ -S "/tmp/.X11-unix/X${DISPLAY_NUM}" ] && break
|
||||
sleep 0.1
|
||||
done
|
||||
if [ ! -S "/tmp/.X11-unix/X${DISPLAY_NUM}" ]; then
|
||||
echo "xvfb failed to create a display socket" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# without a window manager the chrome window never takes X focus, which leaves
|
||||
# document.hasFocus() false for every page and is a strong bot signal
|
||||
openbox &
|
||||
|
||||
x11vnc -display "$DISPLAY" -rfbport "$VNC_PORT" -localhost -forever -shared -nopw -quiet &
|
||||
websockify --web=/usr/share/novnc "$NOVNC_PORT" "127.0.0.1:${VNC_PORT}" &
|
||||
|
||||
# devtools refuses requests whose Host header is neither localhost nor an IP,
|
||||
# so connect to this container by IP rather than by service name
|
||||
socat "TCP-LISTEN:${DEVTOOLS_PORT},fork,reuseaddr" "TCP:127.0.0.1:${CHROME_DEVTOOLS_PORT}" &
|
||||
|
||||
# launching chrome by hand rather than through an automation library keeps the
|
||||
# enable-automation switch off, so navigator.webdriver stays false
|
||||
exec google-chrome-stable \
|
||||
--remote-debugging-address=127.0.0.1 \
|
||||
--remote-debugging-port="${CHROME_DEVTOOLS_PORT}" \
|
||||
--user-data-dir=/home/chrome/profile \
|
||||
--window-position=0,0 \
|
||||
--window-size="${WIDTH},${HEIGHT}" \
|
||||
--no-first-run \
|
||||
--no-default-browser-check \
|
||||
--disable-blink-features=AutomationControlled \
|
||||
--password-store=basic \
|
||||
--use-mock-keychain \
|
||||
${CHROME_LANG:+--lang=${CHROME_LANG}} \
|
||||
${CHROME_PROXY:+--proxy-server=${CHROME_PROXY}} \
|
||||
${CHROME_EXTRA_FLAGS:-} \
|
||||
about:blank
|
||||
@@ -1112,7 +1112,7 @@ declare var Infinity: number;
|
||||
{#if cfgMode === 'local'}
|
||||
Spawns an isolated Chrome process per session.
|
||||
{:else}
|
||||
Connect to a Chrome you launched yourself — real OS fingerprint, GPU, profile, and extensions. Best for bypassing bot detection.
|
||||
Connect to a Chrome you launched yourself - real OS fingerprint, GPU, profile, and extensions. Best for bypassing bot detection.
|
||||
{/if}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user