Obxcura

A small Ruby client for the Obscura headless browser, driven over the Chrome DevTools Protocol.

A Browser owns one WebSocket connection; each Page is a CDP target with its own attached session. One connection, many pages.

Obxcura vs Obscura. This gem is Obxcura. The browser it drives is obscura — a separate binary you run yourself. The x keeps them apart.

Installation

bundle add obxcura

You also need the obscura browser binary, which is a separate project. The repo ships an installer for it:

scripts/obscura/install.sh

That pulls the latest release for your platform into /usr/local/bin (asking for sudo only if the prefix is not writable), then verifies the install and prints the checksums.

scripts/obscura/install.sh --version v0.2.0      # pin a release
scripts/obscura/install.sh --prefix ~/.local/bin # no sudo
scripts/obscura/install.sh --no-stealth          # plain build
scripts/obscura/install.sh --no-render           # smaller, no screenshot/pdf
scripts/obscura/install.sh --help

Prefer it to a manual download, because the packaging has sharp edges:

  • Since 0.2.0 there are four builds per platform, and the suffix decides what works:

    Asset suffix Render engine Stealth
    (none) yes no
    -stealth yes yes
    -no-render no no
    -no-render-stealth no yes

    The script defaults to -stealth: it carries the render engine that #screenshot and #pdf need, and it is the build that gets past commercial bot detection. --no-render trades both away for a smaller download.

  • --version cannot tell the builds apart — all four of a release report the same string. That is why the script prints checksums: they are the only way to identify later which build is actually installed.

  • The archive holds two binaries, obscura and obscura-worker, and serve looks for the worker as a sibling. The script always moves the pair, since installing one alone silently mixes builds.

  • On macOS it removes the target before copying. Overwriting a binary in place leaves a stale cached code signature on that inode, and the kernel then SIGKILLs it on exec — even though the bytes are fine.

Driving anything on localhost also needs --allow-private-network, which is off by default:

obscura serve --allow-private-network

Usage

Start the browser first (defaults to port 9222):

obscura serve

Then:

require "obxcura"

browser = Obxcura::Browser.new              # or Obxcura.start
begin
  page = browser.go_to("https://example.com")

  page.title                                # => "Example Domain"
  page.current_url                          # => "https://example.com/"
  page.html                                 # rendered DOM, post-JS
  page.evaluate("1 + 2")                    # => 3

  page.at_css("h1").text                    # => "Example Domain"
  page.css("a").map { |a| a["href"] }

  page.at_css("#username").type("guillermo")
  page.at_css("form").submit

  page.screenshot(path: "shot.png")
  page.pdf(path: "page.pdf", paper: :a4)
ensure
  browser.quit                              # always, so targets don't leak
end

That is the whole shape of it: a Browser owns the connection, a Page is a target on it, and #at_css / #css hand back live Node handles you can read from and act on.

Documentation

Full reference in docs/, one page per area:

| | | |---|---| | Connecting | Obxcura.start, Browser.new, ports, timeouts, shutdown | | Pages | #goto, #refresh, #close, raw #command | | Reading content | #html, #title, #current_url | | Querying the DOM | #at_css, #css, and every Node method | | Running JavaScript | #evaluate, #evaluate_func, #call_on | | Forms and input | #focus, #type, #submit, #value | | HTTP from the page | #post, #network_log | | Cookies | #cookies — read, set, remove, replay a session | | Headers | #headers — read and set extra HTTP headers | | Screenshots | #screenshot and every option | | PDF | #pdf and every option | | Errors | The exception hierarchy, and what does not raise | | Browser constraints | Obscura limits that shape this API |

Runnable examples live in examples/.

Two constraints are worth knowing before you write anything, both covered in Browser constraints: Obscura stops running background JavaScript shortly after load, so goto + sleep does not do what you expect; and an in-page throw comes back as nil rather than raising.

Development

bin/setup
bundle exec rake          # rubocop + rspec
bundle exec rake doc      # YARD docs into doc/
bin/console               # IRB with the gem loaded

The integration specs boot a real obscura serve against a local WEBrick test site. Point them at your binary:

OBSCURA_BIN=/path/to/obscura bundle exec rspec

Without the binary those specs skip cleanly, so bundle exec rspec still passes. The #screenshot and #pdf specs skip the same way on a -no-render build, so run them against a build that has the render engine — either the default from scripts/obscura/install.sh or the unsuffixed asset — if you touch either.

Contributing

Bug reports and pull requests are welcome at https://github.com/memoxmrdl/obxcura.

License

MIT. See LICENSE.txt.