├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── examples ├── bridge.py ├── check_tpo.py ├── headless.py ├── onion_service.py ├── parallel.py ├── screenshot.py ├── stem_adv.py └── stem_simple.py ├── requirements-dev.txt ├── requirements-travis.txt ├── run_tests.py ├── setup.cfg ├── setup.py ├── tbselenium ├── .coveragerc ├── __init__.py ├── common.py ├── exceptions.py ├── tbbinary.py ├── tbdriver.py ├── test │ ├── __init__.py │ ├── conftest.py │ ├── fixtures.py │ ├── test_addons.py │ ├── test_bridge.py │ ├── test_browser.py │ ├── test_context_switch.py │ ├── test_data │ │ ├── borderify.xpi │ │ ├── img_test.html │ │ └── js_test.html │ ├── test_disable_features.py │ ├── test_env.py │ ├── test_exceptions.py │ ├── test_screenshot.py │ ├── test_set_security_level.py │ ├── test_stem.py │ ├── test_tbdriver.py │ ├── test_tor.py │ └── test_utils.py └── utils.py └── travis.sh /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | build 3 | __pycache__ 4 | tbselenium.egg-info 5 | geckodriver.log 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | os: linux 3 | dist: focal 4 | services: 5 | - xvfb 6 | addons: 7 | apt: 8 | sources: 9 | - sourceline: 'deb https://deb.torproject.org/torproject.org focal main' 10 | packages: 11 | - tor 12 | - tor-geoipdb 13 | env: 14 | global: 15 | - DOWNLOAD_DIR=${HOME}/download 16 | - STABLE_V=14.5.3 17 | - ALPHA_V=14.5a6 18 | - TBB_DIST_URL=https://www.torproject.org/dist/torbrowser/ 19 | matrix: 20 | - TRAVIS_EXTRA_JOB_WORKAROUND=true 21 | cache: 22 | directories: 23 | - ${DOWNLOAD_DIR} 24 | matrix: 25 | include: 26 | - python: "3.8" 27 | env: VERSION_ARCH="${ALPHA_V}/tor-browser-linux-x86_64-${ALPHA_V}.tar.xz" 28 | - python: "3.8" 29 | env: VERSION_ARCH="${STABLE_V}/tor-browser-linux64-${STABLE_V}_ALL.tar.xz" 30 | exclude: 31 | - env: TRAVIS_EXTRA_JOB_WORKAROUND=true 32 | install: 33 | - pip install -r requirements-travis.txt 34 | - pip install . 35 | - TARBALL=`echo ${VERSION_ARCH} |cut -d'/' -f 2` 36 | - . ./travis.sh 37 | - export TBB_PATH=${HOME}/tor-browser 38 | 39 | before_script: 40 | - cd tbselenium 41 | script: travis_retry py.test -s -v --cov=tbselenium --cov-report term-missing --durations=10 test 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-present Gunes Acar, Marc Juarez and other contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tor-browser-selenium [](https://app.travis-ci.com/webfp/tor-browser-selenium) 2 | 3 | 4 | A Python library to automate Tor Browser with Selenium WebDriver. 5 | 6 | ## 📦 Installation 7 | 8 | > [!WARNING] 9 | > Windows and macOS are currently not supported. 10 | 11 | ``` 12 | pip install tbselenium 13 | ``` 14 | 15 | Download `geckodriver` v0.31.0 from the [geckodriver releases page](https://github.com/mozilla/geckodriver/releases/) and add it to PATH. 16 | 17 | ## 🚀 Usage 18 | 19 | Download and extract [Tor Browser](https://www.torproject.org/projects/torbrowser.html.en), and pass its folder's path when you initialize `TorBrowserDriver`. In the examples below, you should not pass "/path/to/tor-browser/", but the (Tor Browser) folder that contains the directory called `Browser`: 20 | 21 | 22 | ### Using with system `tor` 23 | 24 | `tor` needs to be installed (`apt install tor`) and running on port 9050. 25 | 26 | ```python 27 | from tbselenium.tbdriver import TorBrowserDriver 28 | with TorBrowserDriver("/path/to/tor-browser/") as driver: 29 | driver.get('https://check.torproject.org') 30 | ``` 31 | 32 | ### Using with `Stem` 33 | You can use `Stem` to start a new tor process programmatically, and connect to it from `tor-browser-selenium`. Make sure you have `Stem` installed: `pip install stem`: 34 | 35 | 36 | ```python 37 | import tbselenium.common as cm 38 | from tbselenium.tbdriver import TorBrowserDriver 39 | from tbselenium.utils import launch_tbb_tor_with_stem 40 | 41 | tbb_dir = "/path/to/tor-browser/" 42 | tor_process = launch_tbb_tor_with_stem(tbb_path=tbb_dir) 43 | with TorBrowserDriver(tbb_dir, tor_cfg=cm.USE_STEM) as driver: 44 | driver.load_url("https://check.torproject.org") 45 | 46 | tor_process.kill() 47 | ``` 48 | 49 | 50 | ## 💡 Examples 51 | Check the [examples](https://github.com/webfp/tor-browser-selenium/tree/master/examples) to discover different ways to use `tor-browser-selenium` 52 | * [check_tpo.py](https://github.com/webfp/tor-browser-selenium/tree/master/examples/check_tpo.py): Visit the `check.torproject.org` website and print the network status message 53 | * [headless.py](https://github.com/webfp/tor-browser-selenium/tree/master/examples/headless.py): Headless visit and screenshot of check.torproject.org using [PyVirtualDisplay](https://pypi.org/project/PyVirtualDisplay/) 54 | * [onion_service.py](https://github.com/webfp/tor-browser-selenium/blob/main/examples/onion_service.py): Search using DuckDuckGo's Onion service 55 | * [parallel.py](https://github.com/webfp/tor-browser-selenium/tree/master/examples/parallel.py): Visit `check.torproject.org`` with 3 browsers running in parallel 56 | * [screenshot.py](https://github.com/webfp/tor-browser-selenium/tree/master/examples/screenshot.py): Take a screenshot 57 | * [stem_simple.py](https://github.com/webfp/tor-browser-selenium/tree/master/examples/stem_simple.py): Use Stem to start a `tor` process 58 | * [stem_adv.py](https://github.com/webfp/tor-browser-selenium/tree/master/examples/stem_adv.py): Use Stem to launch `tor` with more advanced configuration 59 | 60 | 61 | 62 | ## 🛠️ Test and development 63 | 64 | * Browse the [existing tests](https://github.com/webfp/tor-browser-selenium/tree/main/tbselenium/test) to find out about different ways you can use `tor-browser-selenium`. 65 | 66 | * For development and testing first install the necessary Python packages: 67 | `pip install -r requirements-dev.txt` 68 | 69 | * Install the `xvfb` package by running `apt-get install xvfb` or using your distro's package manager. 70 | 71 | * Run the following to launch the tests: 72 | 73 | `./run_tests.py /path/to/tor-browser/` 74 | 75 | * By default, tests will be run using `Xvfb`, so the browser window will not be visible. 76 | You may disable `Xvfb` by setting the `NO_XVFB` environment variable: 77 | 78 | `export NO_XVFB=1` 79 | 80 | 81 | ### Running individual tests 82 | * First, export the path to Tor Browser folder in the `TBB_PATH` environment variable. 83 | 84 | `export TBB_PATH=/path/to/tbb/tor-browser/` 85 | 86 | * Then, use `py.test` to launch the tests you want, e.g.: 87 | 88 | * `py.test tbselenium/test/test_tbdriver.py` 89 | * `py.test tbselenium/test/test_tbdriver.py::TBDriverTest::test_should_load_check_tpo` 90 | 91 | 92 | ### Using a custom `geckodriver` 93 | A custom `geckodriver` binary can be set via the `executable_path` argument: 94 | 95 | ```python 96 | TorBrowserDriver(executable_path="/path/to/geckodriver") 97 | ``` 98 | 99 | ### Disabling console logs 100 | You can redirect the logs to `/dev/null` by passing the `tbb_logfile_path` initialization parameter: 101 | ```python 102 | TorBrowserDriver(..., tbb_logfile_path='/dev/null') 103 | ``` 104 | 105 | ## ⚙️ Compatibility 106 | 107 | Warning: **Windows and macOS are not supported.** 108 | 109 | [Tested](https://travis-ci.org/webfp/tor-browser-selenium) with the following Tor Browser versions on Ubuntu: 110 | 111 | * **Stable**: 14.5.3 112 | * **Alpha**: 14.5a6 113 | 114 | If you need to use a different version of Tor Browser, [view the past test runs](https://travis-ci.org/webfp/tor-browser-selenium) to find out the compatible `selenium` and `geckodriver` versions. 115 | 116 | ## 🔧 Troubleshooting 117 | 118 | Solutions to potential issues: 119 | 120 | * Make sure you have compatible dependencies. While older or newer versions may work, they may cause issues. 121 | - [Tor Browser](https://www.torproject.org/download/) needs to be downloaded and extracted. 122 | - Python [`selenium`](https://www.selenium.dev/) (`pip install -U selenium`). 123 | - `geckodriver` [version 0.31.0](https://github.com/mozilla/geckodriver/releases/tag/v0.31.0). 124 | 125 | * Running Firefox on the same system may help diagnose issues such as missing libraries and displays. 126 | * `Process unexpectedly closed with status 1`: If you encounter this on a remote machine you connect via SSH, you may need to enable the [headless mode](https://github.com/webfp/tor-browser-selenium/blob/master/examples/headless.py). 127 | * Port conflict with other (`Tor`) process: Pick a different SOCKS and controller port using the `socks_port` argument. 128 | * Use `tbb_logfile_path` argument of TorBrowserDriver to debug obscure errors. This can help with problems due to missing display, missing libraries (e.g. when the LD_LIBRARY_PATH is not set correctly) or other errors that Tor Browser logs to standard output/error. 129 | * `driver.get_cookies()` returns an empty list. This is due to Private Browsing Mode (PBM), which Selenium uses under the hood. See [#79](https://github.com/webfp/tor-browser-selenium/issues/79) for a possible solution. 130 | * WebGL is not supported in the headless mode started with `headless=True` due to a Firefox bug ([#1375585](https://bugzilla.mozilla.org/show_bug.cgi?id=1375585)). To enable WebGL in a headless setting, use `pyvirtualdisplay` following the [headless.py](https://github.com/webfp/tor-browser-selenium/tree/master/examples/headless.py) example. 131 | 132 | ## 📚 Reference 133 | Please use the following reference if you use `tor-browser-selenium` in your academic publications. 134 | 135 | ``` 136 | @misc{tor-browser-selenium, 137 | author = {Gunes Acar and Marc Juarez and individual contributors}, 138 | title = {tor-browser-selenium - Tor Browser automation with Selenium}, 139 | year = {2023}, 140 | publisher = {GitHub}, 141 | howpublished = {\url{https://github.com/webfp/tor-browser-selenium}} 142 | } 143 | ``` 144 | 145 | ## 🙌 Credits 146 | We greatly benefited from the [tor-browser-bundle-testsuite](https://gitlab.torproject.org/tpo/applications/tor-browser-bundle-testsuite) and [tor-browser-selenium](https://github.com/isislovecruft/tor-browser-selenium) projects. 147 | -------------------------------------------------------------------------------- /examples/bridge.py: -------------------------------------------------------------------------------- 1 | from argparse import ArgumentParser 2 | from tbselenium.tbdriver import TorBrowserDriver 3 | from time import sleep 4 | 5 | # Usage: python bridge.py /path/to/tor-browser-bundle bridge_type 6 | # bridge_type is one of: obfs3, obfs4, fte, meek-amazon, meek-azure 7 | 8 | 9 | def visit_using_bridge(tbb_dir, bridge_type="meek-amazon"): 10 | url = "https://check.torproject.org" 11 | with TorBrowserDriver( 12 | tbb_dir, default_bridge_type=bridge_type) as driver: 13 | driver.load_url(url) 14 | print(driver.find_element_by("h1.on").text) # status text 15 | sleep(10) # To verify that the bridge is indeed uses, go to 16 | # Tor Network Settings dialog 17 | 18 | 19 | def main(): 20 | desc = "Visit check.torproject.org website using a Tor bridge" 21 | parser = ArgumentParser(description=desc) 22 | parser.add_argument('tbb_path') 23 | parser.add_argument('bridge_type', nargs='?', default="meek-amazon") 24 | args = parser.parse_args() 25 | visit_using_bridge(args.tbb_path, args.bridge_type) 26 | 27 | 28 | if __name__ == '__main__': 29 | main() 30 | -------------------------------------------------------------------------------- /examples/check_tpo.py: -------------------------------------------------------------------------------- 1 | from argparse import ArgumentParser 2 | from tbselenium.tbdriver import TorBrowserDriver 3 | from selenium.webdriver.support.ui import Select 4 | from time import sleep 5 | 6 | 7 | def visit(tbb_dir): 8 | url = "https://check.torproject.org" 9 | with TorBrowserDriver(tbb_dir) as driver: 10 | driver.load_url(url) 11 | # Iterate over a bunch of locales from the drop-down menu 12 | for lang_code in ["en_US", "fr", "zh_CN", "th", "tr"]: 13 | select = Select(driver.find_element_by_id("cl")) 14 | select.select_by_value(lang_code) 15 | sleep(1) 16 | print("\n======== Locale: %s ========" % lang_code) 17 | print(driver.find_element_by("h1.on").text) # status text 18 | print(driver.find_element_by(".content > p").text) # IP address 19 | 20 | 21 | def main(): 22 | desc = "Visit check.torproject.org website" 23 | parser = ArgumentParser(description=desc) 24 | parser.add_argument('tbb_path') 25 | args = parser.parse_args() 26 | visit(args.tbb_path) 27 | 28 | 29 | if __name__ == '__main__': 30 | main() 31 | -------------------------------------------------------------------------------- /examples/headless.py: -------------------------------------------------------------------------------- 1 | from argparse import ArgumentParser 2 | from tbselenium.tbdriver import TorBrowserDriver 3 | from tbselenium.utils import start_xvfb, stop_xvfb 4 | from os.path import join, dirname, realpath 5 | 6 | 7 | def headless_visit(tbb_dir): 8 | out_img = join(dirname(realpath(__file__)), "headless_screenshot.png") 9 | # start a virtual display 10 | xvfb_display = start_xvfb() 11 | with TorBrowserDriver(tbb_dir) as driver: 12 | driver.load_url("https://check.torproject.org") 13 | driver.get_screenshot_as_file(out_img) 14 | print("Screenshot is saved as %s" % out_img) 15 | 16 | stop_xvfb(xvfb_display) 17 | 18 | 19 | def main(): 20 | desc = "Headless visit and screenshot of check.torproject.org using XVFB" 21 | parser = ArgumentParser(description=desc) 22 | parser.add_argument('tbb_path') 23 | args = parser.parse_args() 24 | headless_visit(args.tbb_path) 25 | 26 | 27 | if __name__ == '__main__': 28 | main() 29 | -------------------------------------------------------------------------------- /examples/onion_service.py: -------------------------------------------------------------------------------- 1 | from argparse import ArgumentParser 2 | from tbselenium.tbdriver import TorBrowserDriver 3 | from selenium.webdriver.common.keys import Keys 4 | from selenium.webdriver.common.by import By 5 | from selenium.webdriver.support.wait import WebDriverWait 6 | from selenium.webdriver.support import expected_conditions as EC 7 | from time import time 8 | 9 | 10 | def search_with_ddg_onion_service(tbb_dir): 11 | """Search using DuckDuckGo's Onion service""" 12 | 13 | # https://gitlab.torproject.org/tpo/applications/tor-browser/-/issues/40524#note_2744494 14 | ddg_hs_url = "https://duckduckgogg42xjoc72x3sjasowoarfbgcmvfimaftt6twagswzczad.onion/" 15 | with TorBrowserDriver(tbb_dir) as driver: 16 | driver.load_url(ddg_hs_url, wait_for_page_body=True) 17 | search_box = WebDriverWait(driver, 10).until( 18 | EC.presence_of_element_located((By.ID, "search_form_input_homepage")) 19 | ) 20 | print(f'Found search box: {search_box}') 21 | search_box.send_keys("Citizenfour by Laura Poitras") 22 | search_box.send_keys(Keys.RETURN) 23 | t0 = time() 24 | wp_link = WebDriverWait(driver, 30).until( 25 | EC.presence_of_element_located( 26 | (By.XPATH, f'//a[@href="https://en.wikipedia.org/wiki/Citizenfour"]')) 27 | ) 28 | print(f'Loaded the search results and found the Wikipedia link in {time() - t0} seconds') 29 | assert "Citizenfour" in wp_link.text 30 | 31 | 32 | def main(): 33 | desc = "Search using DuckDuckGo's Onion service" 34 | parser = ArgumentParser(description=desc) 35 | parser.add_argument('tbb_path') 36 | args = parser.parse_args() 37 | search_with_ddg_onion_service(args.tbb_path) 38 | 39 | 40 | if __name__ == '__main__': 41 | main() 42 | -------------------------------------------------------------------------------- /examples/parallel.py: -------------------------------------------------------------------------------- 1 | from multiprocessing import Pool 2 | from argparse import ArgumentParser 3 | from tbselenium.tbdriver import TorBrowserDriver 4 | from tbselenium.utils import launch_tbb_tor_with_stem 5 | from tbselenium.common import STEM_SOCKS_PORT, USE_RUNNING_TOR,\ 6 | STEM_CONTROL_PORT 7 | 8 | JOBS_IN_PARALLEL = 3 9 | 10 | 11 | def run_in_parallel(inputs, worker, no_of_processes=JOBS_IN_PARALLEL): 12 | p = Pool(no_of_processes) 13 | p.map(worker, inputs) 14 | 15 | 16 | def visit_check_tpo_with_stem(tbb_dir): 17 | url = "https://check.torproject.org" 18 | with TorBrowserDriver(tbb_dir, 19 | socks_port=STEM_SOCKS_PORT, 20 | control_port=STEM_CONTROL_PORT, 21 | tor_cfg=USE_RUNNING_TOR) as driver: 22 | driver.load_url(url, wait_on_page=3) 23 | print(driver.find_element_by("h1.on").text) 24 | print(driver.find_element_by(".content > p").text) 25 | 26 | 27 | def launch_browsers_in_parallel(tbb_path): 28 | tor_process = launch_tbb_tor_with_stem(tbb_path=tbb_path) 29 | run_in_parallel(JOBS_IN_PARALLEL * [tbb_path], 30 | visit_check_tpo_with_stem) 31 | tor_process.kill() 32 | 33 | 34 | def main(): 35 | desc = "Visit check.torproject.org website running 3 browsers in parallel" 36 | parser = ArgumentParser(description=desc) 37 | parser.add_argument('tbb_path') 38 | args = parser.parse_args() 39 | launch_browsers_in_parallel(args.tbb_path) 40 | 41 | 42 | if __name__ == '__main__': 43 | main() 44 | -------------------------------------------------------------------------------- /examples/screenshot.py: -------------------------------------------------------------------------------- 1 | from argparse import ArgumentParser 2 | from os.path import dirname, join, realpath, getsize 3 | from tbselenium.tbdriver import TorBrowserDriver 4 | 5 | 6 | def visit_and_capture(tbb_dir, url): 7 | """Take screenshot of the page.""" 8 | out_img = join(dirname(realpath(__file__)), "screenshot.png") 9 | with TorBrowserDriver(tbb_dir) as driver: 10 | driver.load_url(url, wait_for_page_body=True) 11 | driver.get_screenshot_as_file(out_img) 12 | print("Screenshot is saved as %s (%s bytes)" % (out_img, getsize(out_img))) 13 | 14 | 15 | def main(): 16 | desc = "Take a screenshot using TorBrowserDriver" 17 | default_url = "https://check.torproject.org" 18 | parser = ArgumentParser(description=desc) 19 | parser.add_argument('tbb_path') 20 | parser.add_argument('url', nargs='?', default=default_url) 21 | args = parser.parse_args() 22 | visit_and_capture(args.tbb_path, args.url) 23 | 24 | 25 | if __name__ == '__main__': 26 | main() 27 | -------------------------------------------------------------------------------- /examples/stem_adv.py: -------------------------------------------------------------------------------- 1 | from argparse import ArgumentParser 2 | from stem.control import Controller 3 | from stem import CircStatus 4 | from tbselenium.tbdriver import TorBrowserDriver 5 | import tbselenium.common as cm 6 | from tbselenium.utils import launch_tbb_tor_with_stem 7 | from selenium.webdriver.common.utils import free_port 8 | import tempfile 9 | from os.path import join 10 | 11 | 12 | def print_tor_circuits(controller): 13 | """Print built Tor circuits using Stem. 14 | From https://stem.torproject.org/tutorials/examples/list_circuits.html 15 | """ 16 | for circ in sorted(controller.get_circuits()): 17 | if circ.status != CircStatus.BUILT: 18 | continue 19 | 20 | print("\nCircuit %s (%s)" % (circ.id, circ.purpose)) 21 | 22 | for i, entry in enumerate(circ.path): 23 | div = '+' if (i == len(circ.path) - 1) else '|' 24 | fingerprint, nickname = entry 25 | 26 | desc = controller.get_network_status(fingerprint, None) 27 | address = desc.address if desc else 'unknown' 28 | 29 | print(" %s- %s (%s, %s)" % (div, fingerprint, nickname, address)) 30 | 31 | 32 | def launch_tb_with_custom_stem(tbb_dir): 33 | socks_port = free_port() 34 | control_port = free_port() 35 | tor_data_dir = tempfile.mkdtemp() 36 | tor_binary = join(tbb_dir, cm.DEFAULT_TOR_BINARY_PATH) 37 | print("SOCKS port: %s, Control port: %s" % (socks_port, control_port)) 38 | 39 | torrc = {'ControlPort': str(control_port), 40 | 'SOCKSPort': str(socks_port), 41 | 'DataDirectory': tor_data_dir} 42 | tor_process = launch_tbb_tor_with_stem(tbb_path=tbb_dir, torrc=torrc, 43 | tor_binary=tor_binary) 44 | with Controller.from_port(port=control_port) as controller: 45 | controller.authenticate() 46 | with TorBrowserDriver(tbb_dir, socks_port=socks_port, 47 | control_port=control_port, 48 | tor_cfg=cm.USE_STEM) as driver: 49 | driver.load_url("https://check.torproject.org", wait_on_page=3) 50 | print(driver.find_element_by("h1.on").text) 51 | print(driver.find_element_by(".content > p").text) 52 | print_tor_circuits(controller) 53 | 54 | tor_process.kill() 55 | 56 | 57 | def main(): 58 | desc = "Use TorBrowserDriver with Stem" 59 | parser = ArgumentParser(description=desc) 60 | parser.add_argument('tbb_path') 61 | args = parser.parse_args() 62 | launch_tb_with_custom_stem(args.tbb_path) 63 | 64 | 65 | if __name__ == '__main__': 66 | main() 67 | -------------------------------------------------------------------------------- /examples/stem_simple.py: -------------------------------------------------------------------------------- 1 | from argparse import ArgumentParser 2 | from tbselenium.tbdriver import TorBrowserDriver 3 | import tbselenium.common as cm 4 | from tbselenium.utils import launch_tbb_tor_with_stem 5 | 6 | 7 | def launch_tb_with_stem(tbb_dir): 8 | tor_process = launch_tbb_tor_with_stem(tbb_path=tbb_dir) 9 | # get the Tor process pid 10 | tor_pid = tor_process.pid 11 | print(f"Tor is running with PID={tor_pid}") 12 | with TorBrowserDriver(tbb_dir, 13 | tor_cfg=cm.USE_STEM) as driver: 14 | driver.load_url("https://check.torproject.org", wait_on_page=3) 15 | print(driver.find_element_by("h1.on").text) 16 | print(driver.find_element_by(".content > p").text) 17 | 18 | tor_process.kill() 19 | 20 | 21 | def main(): 22 | desc = "Use TorBrowserDriver with Stem" 23 | parser = ArgumentParser(description=desc) 24 | parser.add_argument('tbb_path') 25 | args = parser.parse_args() 26 | launch_tb_with_stem(args.tbb_path) 27 | 28 | 29 | if __name__ == '__main__': 30 | main() 31 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | pyvirtualdisplay 2 | stem 3 | pytest 4 | pytest-cov 5 | psutil 6 | -------------------------------------------------------------------------------- /requirements-travis.txt: -------------------------------------------------------------------------------- 1 | stem 2 | pytest>=3.10 3 | pytest-cov 4 | pytest-rerunfailures 5 | psutil 6 | -------------------------------------------------------------------------------- /run_tests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | from argparse import ArgumentParser 3 | from subprocess import call 4 | from os import environ 5 | from os.path import isdir, realpath, dirname, join 6 | 7 | 8 | desc = "Run all the TorBrowserDriver tests" 9 | parser = ArgumentParser(description=desc) 10 | parser.add_argument('tbb_path') 11 | args = parser.parse_args() 12 | 13 | if not isdir(args.tbb_path): 14 | raise IOError("Please pass the path to Tor Browser Bundle") 15 | 16 | # TBB_PATH environment variable is used by the tests 17 | environ['TBB_PATH'] = args.tbb_path 18 | 19 | # Get test directory from path of this script 20 | file_path = dirname(realpath(__file__)) 21 | test_dir = join(file_path, 'tbselenium', 'test') 22 | 23 | # Run all the tests using py.test 24 | call(["py.test", "-s", "-v", "--cov=tbselenium", "--cov-report", 25 | "term-missing", "--durations=10", test_dir]) 26 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description_file = README.md 3 | 4 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | with open('README.md', encoding='utf-8') as f: 4 | long_description = f.read() 5 | 6 | setup( 7 | author='Gunes Acar', 8 | name="tbselenium", 9 | description="Tor Browser automation with Selenium", 10 | keywords=["tor", "selenium", "tor browser"], 11 | version="0.9.0", 12 | long_description=long_description, 13 | long_description_content_type='text/markdown', 14 | url='https://github.com/webfp/tor-browser-selenium', 15 | packages=["tbselenium", "tbselenium.test"], 16 | install_requires=[ 17 | "selenium>=4" 18 | ] 19 | ) 20 | -------------------------------------------------------------------------------- /tbselenium/.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | omit = 3 | test/* 4 | -------------------------------------------------------------------------------- /tbselenium/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webfp/tor-browser-selenium/a1f8d9e0ea1f6fb1da45e4f94646af0709a73bec/tbselenium/__init__.py -------------------------------------------------------------------------------- /tbselenium/common.py: -------------------------------------------------------------------------------- 1 | from os.path import join, dirname, abspath 2 | from os import environ 3 | 4 | # DEFAULT TBB PATHS works for TBB versions v4.x and above 5 | # Old TBB versions (V3.X or below) have different directory structures 6 | DEFAULT_TBB_BROWSER_DIR = 'Browser' 7 | DEFAULT_TBB_TORBROWSER_DIR = join('Browser', 'TorBrowser') 8 | DEFAULT_TBB_FX_BINARY_PATH = join('Browser', 'firefox') 9 | DEFAULT_TOR_BINARY_DIR = join(DEFAULT_TBB_TORBROWSER_DIR, 'Tor') 10 | DEFAULT_TOR_BINARY_PATH = join(DEFAULT_TOR_BINARY_DIR, 'tor') 11 | DEFAULT_TBB_DATA_DIR = join(DEFAULT_TBB_TORBROWSER_DIR, 'Data') 12 | DEFAULT_TBB_PROFILE_PATH = join(DEFAULT_TBB_DATA_DIR, 'Browser', 13 | 'profile.default') 14 | DEFAULT_TOR_DATA_PATH = join(DEFAULT_TBB_DATA_DIR, 'Tor') 15 | TB_CHANGE_LOG_PATH = join(DEFAULT_TBB_TORBROWSER_DIR, 16 | 'Docs', 'ChangeLog.txt') 17 | # noscript .xpi path as found in the TBB distributions 18 | DEFAULT_TBB_NO_SCRIPT_XPI_PATH = join( 19 | DEFAULT_TBB_PROFILE_PATH, 'extensions', 20 | '{73a6fe31-595d-460b-a920-fcc0f8843232}.xpi') 21 | 22 | # Directories for bundled fonts - Linux only 23 | DEFAULT_FONTCONFIG_PATH = join(DEFAULT_TBB_DATA_DIR, 'fontconfig') 24 | FONTCONFIG_FILE = "fonts.conf" 25 | DEFAULT_FONTS_CONF_PATH = join(DEFAULT_FONTCONFIG_PATH, FONTCONFIG_FILE) 26 | DEFAULT_BUNDLED_FONTS_PATH = join('Browser', 'fonts') 27 | 28 | # SYSTEM TOR PORTS 29 | DEFAULT_SOCKS_PORT = 9050 30 | DEFAULT_CONTROL_PORT = 9051 31 | 32 | # TBB TOR PORTS 33 | TBB_SOCKS_PORT = 9150 34 | TBB_CONTROL_PORT = 9151 35 | 36 | # pick 9250, 9251 to avoid conflict 37 | STEM_SOCKS_PORT = 9250 38 | STEM_CONTROL_PORT = 9251 39 | 40 | KNOWN_SOCKS_PORTS = [DEFAULT_SOCKS_PORT, TBB_SOCKS_PORT] 41 | PORT_BAN_PREFS = ["extensions.torbutton.banned_ports", 42 | "network.security.ports.banned"] 43 | 44 | 45 | # Test constants 46 | CHECK_TPO_URL = "http://check.torproject.org" 47 | CHECK_TPO_HOST = "check.torproject.org" 48 | TEST_URL = CHECK_TPO_URL 49 | ABOUT_TOR_URL = "about:tor" 50 | 51 | # Which tor process/binary to use 52 | LAUNCH_NEW_TBB_TOR = 0 # Not supported (use tor in TBB, launch a new process) 53 | USE_RUNNING_TOR = 1 # use system tor or tor started with stem 54 | USE_STEM = 2 # use tor started with Stem 55 | 56 | TRAVIS = "CI" in environ and "TRAVIS" in environ 57 | 58 | TB_SELENIUM_DIR = dirname(abspath(__file__)) 59 | TEST_DATA_DIR = join(TB_SELENIUM_DIR, "test", "test_data") 60 | LOCAL_JS_TEST_URL = 'file://' + join(TEST_DATA_DIR, "js_test.html") 61 | LOCAL_IMG_TEST_URL = 'file://' + join(TEST_DATA_DIR, "img_test.html") 62 | -------------------------------------------------------------------------------- /tbselenium/exceptions.py: -------------------------------------------------------------------------------- 1 | 2 | class TBDriverPathError(Exception): 3 | pass 4 | 5 | 6 | class TBTestEnvVarError(Exception): 7 | pass 8 | 9 | 10 | class TBDriverPortError(Exception): 11 | pass 12 | 13 | 14 | class TBDriverConfigError(Exception): 15 | pass 16 | 17 | 18 | class TimeExceededError(Exception): 19 | pass 20 | 21 | 22 | class TorBrowserDriverInitError(Exception): 23 | pass 24 | 25 | 26 | class StemLaunchError(Exception): 27 | pass 28 | -------------------------------------------------------------------------------- /tbselenium/tbbinary.py: -------------------------------------------------------------------------------- 1 | from selenium.webdriver.firefox.firefox_binary import FirefoxBinary 2 | 3 | 4 | class TBBinary(FirefoxBinary): 5 | ''' 6 | Extend FirefoxBinary to better handle terminated browser processes. 7 | ''' 8 | 9 | def kill(self): 10 | """Kill the browser. 11 | 12 | This is useful when the browser is stuck. 13 | """ 14 | if self.process and self.process.poll() is None: 15 | self.process.kill() 16 | self.process.wait() 17 | -------------------------------------------------------------------------------- /tbselenium/tbdriver.py: -------------------------------------------------------------------------------- 1 | import shutil 2 | from os import environ, chdir 3 | from os.path import isdir, isfile, join, abspath, dirname 4 | from time import sleep 5 | from http.client import CannotSendRequest 6 | from selenium.webdriver.support.ui import WebDriverWait 7 | from selenium.webdriver.support import expected_conditions as EC 8 | from selenium.webdriver.common.by import By 9 | from selenium.webdriver.firefox.service import Service 10 | from selenium.webdriver.firefox.webdriver import WebDriver as FirefoxDriver 11 | from selenium.webdriver.firefox.options import Options 12 | from selenium.common.exceptions import WebDriverException 13 | import tbselenium.common as cm 14 | from tbselenium.utils import prepend_to_env_var, is_busy 15 | from tbselenium.tbbinary import TBBinary 16 | from tbselenium.exceptions import ( 17 | TBDriverConfigError, TBDriverPortError, TBDriverPathError) 18 | 19 | 20 | DEFAULT_BANNED_PORTS = "9050,9051,9150,9151" 21 | GECKO_DRIVER_EXE_PATH = shutil.which("geckodriver") 22 | 23 | class TorBrowserDriver(FirefoxDriver): 24 | """ 25 | Extend Firefox webdriver to automate Tor Browser. 26 | """ 27 | def __init__(self, 28 | tbb_path="", 29 | tor_cfg=cm.USE_RUNNING_TOR, 30 | tbb_fx_binary_path="", 31 | tbb_profile_path="", 32 | tbb_logfile_path="", 33 | tor_data_dir="", 34 | executable_path=GECKO_DRIVER_EXE_PATH, 35 | pref_dict={}, 36 | socks_port=None, 37 | control_port=None, 38 | extensions=[], 39 | default_bridge_type="", 40 | headless=False, 41 | options=None, 42 | use_custom_profile=False, 43 | geckodriver_port=0 # by default a random port will be used 44 | ): 45 | 46 | # use_custom_profile: whether to launch from and *write to* the given 47 | # profile 48 | # False: copy the profile to a tempdir; remove the temp folder on quit 49 | # True: use the given profile without copying. This can be used to keep 50 | # a stateful profile across different launches of the Tor Browser. 51 | # It uses firefox's `-profile`` command line parameter under the hood 52 | 53 | self.use_custom_profile = use_custom_profile 54 | self.tor_cfg = tor_cfg 55 | self.setup_tbb_paths(tbb_path, tbb_fx_binary_path, 56 | tbb_profile_path, tor_data_dir) 57 | self.options = Options() if options is None else options 58 | install_noscript = False 59 | 60 | USE_DEPRECATED_PROFILE_METHOD = True 61 | if self.use_custom_profile: 62 | # launch from and write to this custom profile 63 | self.options.add_argument("-profile") 64 | self.options.add_argument(self.tbb_profile_path) 65 | elif USE_DEPRECATED_PROFILE_METHOD: 66 | # launch from this custom profile 67 | self.options.profile = self.tbb_profile_path 68 | else: 69 | # Launch with no profile at all. This should be used with caution. 70 | # NoScript does not come installed on browsers launched by this 71 | # method, so we install it ourselves 72 | install_noscript = True 73 | 74 | self.init_ports(tor_cfg, socks_port, control_port) 75 | self.init_prefs(pref_dict, default_bridge_type) 76 | self.export_env_vars() 77 | # TODO: 78 | # self.binary = self.get_tb_binary(logfile=tbb_logfile_path) 79 | if use_custom_profile: 80 | print(f'Using custom profile: {self.tbb_profile_path}') 81 | tbb_service = Service( 82 | executable_path=executable_path, 83 | log_path=tbb_logfile_path, # TODO: deprecated, use log_output 84 | service_args=["--marionette-port", "2828"], 85 | port=geckodriver_port 86 | ) 87 | else: 88 | tbb_service = Service( 89 | executable_path=executable_path, 90 | log_path=tbb_logfile_path, 91 | port=geckodriver_port 92 | ) 93 | # options.binary is path to the Firefox binary and it can be a string 94 | # or a FirefoxBinary object. If it's a string, it will be converted to 95 | # a FirefoxBinary object. 96 | # https://github.com/SeleniumHQ/selenium/blob/7cfd137085fcde932cd71af78642a15fd56fe1f1/py/selenium/webdriver/firefox/options.py#L54 97 | self.options.binary = self.tbb_fx_binary_path 98 | self.options.add_argument('--class') 99 | self.options.add_argument('"Tor Browser"') 100 | if headless: 101 | self.options.add_argument('-headless') 102 | 103 | super(TorBrowserDriver, self).__init__( 104 | service=tbb_service, 105 | options=self.options, 106 | ) 107 | self.is_running = True 108 | self.install_extensions(extensions, install_noscript) 109 | self.temp_profile_dir = self.capabilities["moz:profile"] 110 | sleep(1) 111 | 112 | def install_extensions(self, extensions, install_noscript): 113 | """Install the given extensions to the profile we are launching.""" 114 | if install_noscript: 115 | no_script_xpi = join( 116 | self.tbb_path, cm.DEFAULT_TBB_NO_SCRIPT_XPI_PATH) 117 | extensions.append(no_script_xpi) 118 | 119 | for extension in extensions: 120 | self.install_addon(extension) 121 | 122 | def init_ports(self, tor_cfg, socks_port, control_port): 123 | """Check SOCKS port and Tor config inputs.""" 124 | if tor_cfg == cm.LAUNCH_NEW_TBB_TOR: 125 | raise TBDriverConfigError( 126 | """`LAUNCH_NEW_TBB_TOR` config is not supported anymore. 127 | Use USE_RUNNING_TOR or USE_STEM""") 128 | 129 | if tor_cfg not in [cm.USE_RUNNING_TOR, cm.USE_STEM]: 130 | raise TBDriverConfigError("Unrecognized tor_cfg: %s" % tor_cfg) 131 | 132 | if socks_port is None: 133 | if tor_cfg == cm.USE_RUNNING_TOR: 134 | socks_port = cm.DEFAULT_SOCKS_PORT # 9050 135 | else: 136 | socks_port = cm.STEM_SOCKS_PORT 137 | if control_port is None: 138 | if tor_cfg == cm.USE_RUNNING_TOR: 139 | control_port = cm.DEFAULT_CONTROL_PORT 140 | else: 141 | control_port = cm.STEM_CONTROL_PORT 142 | 143 | if not is_busy(socks_port): 144 | raise TBDriverPortError("SOCKS port %s is not listening" 145 | % socks_port) 146 | 147 | self.socks_port = socks_port 148 | self.control_port = control_port 149 | 150 | def setup_tbb_paths(self, tbb_path, tbb_fx_binary_path, tbb_profile_path, 151 | tor_data_dir): 152 | """Update instance variables based on the passed paths. 153 | 154 | TorBrowserDriver can be initialized by passing either 155 | 1) path to TBB directory (tbb_path) 156 | 2) path to TBB directory and profile (tbb_path, tbb_profile_path) 157 | 3) path to TBB's Firefox binary and profile (tbb_fx_binary_path, tbb_profile_path) 158 | """ 159 | if not (tbb_path or (tbb_fx_binary_path and tbb_profile_path)): 160 | raise TBDriverPathError("Either TBB path or Firefox profile" 161 | " and binary path should be provided" 162 | " %s" % tbb_path) 163 | 164 | if tbb_path: 165 | if not isdir(tbb_path): 166 | raise TBDriverPathError("TBB path is not a directory %s" 167 | % tbb_path) 168 | tbb_fx_binary_path = join(tbb_path, cm.DEFAULT_TBB_FX_BINARY_PATH) 169 | else: 170 | # based on https://github.com/webfp/tor-browser-selenium/issues/159#issue-1016463002 171 | tbb_path = dirname(dirname(tbb_fx_binary_path)) 172 | 173 | if not tbb_profile_path: 174 | # fall back to the default profile path in TBB 175 | tbb_profile_path = join(tbb_path, cm.DEFAULT_TBB_PROFILE_PATH) 176 | 177 | if not isfile(tbb_fx_binary_path): 178 | raise TBDriverPathError("Invalid Firefox binary %s" 179 | % tbb_fx_binary_path) 180 | if not isdir(tbb_profile_path): 181 | raise TBDriverPathError("Invalid Firefox profile dir %s" 182 | % tbb_profile_path) 183 | self.tbb_path = abspath(tbb_path) 184 | self.tbb_profile_path = abspath(tbb_profile_path) 185 | self.tbb_fx_binary_path = abspath(tbb_fx_binary_path) 186 | self.tbb_browser_dir = abspath(join(tbb_path, 187 | cm.DEFAULT_TBB_BROWSER_DIR)) 188 | if tor_data_dir: 189 | self.tor_data_dir = tor_data_dir # only relevant if we launch tor 190 | else: 191 | # fall back to default tor data dir in TBB 192 | self.tor_data_dir = join(tbb_path, cm.DEFAULT_TOR_DATA_PATH) 193 | # TB can't find bundled "fonts" if we don't switch to tbb_browser_dir 194 | chdir(self.tbb_browser_dir) 195 | 196 | def load_url(self, url, wait_on_page=0, wait_for_page_body=False): 197 | """Load a URL and wait before returning. 198 | 199 | If you query/manipulate DOM or execute a script immediately 200 | after the page load, you may get the following error: 201 | 202 | "WebDriverException: Message: waiting for doc.body failed" 203 | 204 | To prevent this, set wait_for_page_body to True, and driver 205 | will wait for the page body to become available before it returns. 206 | 207 | """ 208 | self.get(url) 209 | if wait_for_page_body: 210 | # if the page can't be loaded this will raise a TimeoutException 211 | self.find_element_by("body", find_by=By.TAG_NAME) 212 | sleep(wait_on_page) 213 | 214 | def find_element_by(self, selector, timeout=30, 215 | find_by=By.CSS_SELECTOR): 216 | """Wait until the element matching the selector appears or timeout.""" 217 | return WebDriverWait(self, timeout).until( 218 | EC.presence_of_element_located((find_by, selector))) 219 | 220 | def add_ports_to_fx_banned_ports(self, socks_port, control_port): 221 | """By default, ports 9050,9051,9150,9151 are banned in TB. 222 | 223 | If we use a tor process running on a custom SOCKS port, we add SOCKS 224 | and control ports to the following prefs: 225 | network.security.ports.banned 226 | extensions.torbutton.banned_ports 227 | """ 228 | if socks_port in cm.KNOWN_SOCKS_PORTS: 229 | return 230 | tb_prefs = self.options.preferences 231 | set_pref = self.options.set_preference 232 | 233 | for port_ban_pref in cm.PORT_BAN_PREFS: 234 | banned_ports = tb_prefs.get(port_ban_pref, DEFAULT_BANNED_PORTS) 235 | set_pref(port_ban_pref, "%s,%s,%s" % 236 | (banned_ports, socks_port, control_port)) 237 | 238 | def set_tb_prefs_for_using_system_tor(self, control_port): 239 | """Set the preferences suggested by start-tor-browser script 240 | to run TB with system-installed Tor. 241 | 242 | We set these prefs for running with Tor started with Stem as well. 243 | """ 244 | set_pref = self.options.set_preference 245 | # Prevent Tor Browser running its own Tor process 246 | set_pref('extensions.torlauncher.start_tor', False) 247 | # TODO: investigate whether these prefs are up to date or not 248 | set_pref('extensions.torbutton.block_disk', False) 249 | set_pref('extensions.torbutton.custom.socks_host', '127.0.0.1') 250 | set_pref('extensions.torbutton.custom.socks_port', self.socks_port) 251 | set_pref('extensions.torbutton.inserted_button', True) 252 | set_pref('extensions.torbutton.launch_warning', False) 253 | set_pref('privacy.spoof_english', 2) 254 | set_pref('extensions.torbutton.loglevel', 2) 255 | set_pref('extensions.torbutton.logmethod', 0) 256 | set_pref('extensions.torbutton.settings_method', 'custom') 257 | set_pref('extensions.torbutton.use_privoxy', False) 258 | set_pref('extensions.torlauncher.control_port', control_port) 259 | set_pref('extensions.torlauncher.loglevel', 2) 260 | set_pref('extensions.torlauncher.logmethod', 0) 261 | set_pref('extensions.torlauncher.prompt_at_startup', False) 262 | # disable XPI signature checking 263 | set_pref('xpinstall.signatures.required', False) 264 | set_pref('xpinstall.whitelist.required', False) 265 | 266 | def init_prefs(self, pref_dict, default_bridge_type): 267 | self.add_ports_to_fx_banned_ports(self.socks_port, self.control_port) 268 | set_pref = self.options.set_preference 269 | set_pref('browser.startup.page', "0") 270 | set_pref('torbrowser.settings.quickstart.enabled', True) 271 | set_pref('browser.startup.homepage', 'about:newtab') 272 | set_pref('extensions.torlauncher.prompt_at_startup', 0) 273 | # load strategy normal is equivalent to "onload" 274 | set_pref('webdriver.load.strategy', 'normal') 275 | # disable auto-update 276 | set_pref('app.update.enabled', False) 277 | set_pref('extensions.torbutton.versioncheck_enabled', False) 278 | if default_bridge_type: 279 | # to use a non-default bridge, overwrite the relevant pref, e.g.: 280 | # extensions.torlauncher.default_bridge.meek-azure.1 = meek 0.0.... 281 | set_pref('extensions.torlauncher.default_bridge_type', 282 | default_bridge_type) 283 | 284 | set_pref('extensions.torbutton.prompted_language', True) 285 | # https://gitlab.torproject.org/tpo/applications/tor-browser/-/issues/41378 286 | set_pref('intl.language_notification.shown', True) 287 | # Configure Firefox to use Tor SOCKS proxy 288 | set_pref('network.proxy.socks_port', self.socks_port) 289 | set_pref('extensions.torbutton.socks_port', self.socks_port) 290 | set_pref('extensions.torlauncher.control_port', self.control_port) 291 | self.set_tb_prefs_for_using_system_tor(self.control_port) 292 | # pref_dict overwrites above preferences 293 | for pref_name, pref_val in pref_dict.items(): 294 | set_pref(pref_name, pref_val) 295 | # self.profile.update_preferences() 296 | 297 | def export_env_vars(self): 298 | """Setup LD_LIBRARY_PATH and HOME environment variables. 299 | 300 | We follow start-tor-browser script. 301 | """ 302 | tor_binary_dir = join(self.tbb_path, cm.DEFAULT_TOR_BINARY_DIR) 303 | environ["LD_LIBRARY_PATH"] = tor_binary_dir 304 | environ["FONTCONFIG_PATH"] = join(self.tbb_path, 305 | cm.DEFAULT_FONTCONFIG_PATH) 306 | environ["FONTCONFIG_FILE"] = cm.FONTCONFIG_FILE 307 | environ["HOME"] = self.tbb_browser_dir 308 | # Add "TBB_DIR/Browser" to the PATH, see issue #10. 309 | prepend_to_env_var("PATH", self.tbb_browser_dir) 310 | 311 | def get_tb_binary(self, logfile=None): 312 | """Return FirefoxBinary pointing to the TBB's firefox binary.""" 313 | tbb_logfile = open(logfile, 'a+') if logfile else None 314 | return TBBinary(firefox_path=self.tbb_fx_binary_path, 315 | log_file=tbb_logfile) 316 | 317 | @property 318 | def is_connection_error_page(self): 319 | """Check if we get a connection error, i.e. 'Problem loading page'.""" 320 | return "ENTITY connectionFailure.title" in self.page_source 321 | 322 | def clean_up_profile_dirs(self): 323 | """Remove temporary profile directories. 324 | Only called when WebDriver.quit() is interrupted 325 | """ 326 | if self.use_custom_profile: 327 | # don't remove the profile if we are writing into it 328 | # i.e. stateful mode 329 | return 330 | 331 | if self.temp_profile_dir and isdir(self.temp_profile_dir): 332 | shutil.rmtree(self.temp_profile_dir) 333 | 334 | def quit(self): 335 | """Quit the driver. Clean up if the parent's quit fails.""" 336 | self.is_running = False 337 | try: 338 | super(TorBrowserDriver, self).quit() 339 | except (CannotSendRequest, AttributeError, WebDriverException): 340 | try: # Clean up if webdriver.quit() throws 341 | if hasattr(self, "service"): 342 | self.service.stop() 343 | if hasattr(self, "options") and hasattr( 344 | self.options, "profile"): 345 | self.clean_up_profile_dirs() 346 | except Exception as e: 347 | print("[tbselenium] Exception while quitting: %s" % e) 348 | 349 | def __enter__(self): 350 | return self 351 | 352 | def __exit__(self, *args): 353 | self.quit() 354 | -------------------------------------------------------------------------------- /tbselenium/test/__init__.py: -------------------------------------------------------------------------------- 1 | # Environment variable that points to TBB directory: 2 | from os import environ 3 | from os.path import abspath, isdir 4 | from tbselenium.exceptions import TBTestEnvVarError 5 | 6 | TBB_PATH = environ.get('TBB_PATH') 7 | 8 | if TBB_PATH is None: 9 | raise TBTestEnvVarError("Environment variable `TBB_PATH` can't be found.") 10 | 11 | TBB_PATH = abspath(TBB_PATH) 12 | if not isdir(TBB_PATH): 13 | raise TBTestEnvVarError("TBB_PATH is not a directory: %s" % TBB_PATH) 14 | -------------------------------------------------------------------------------- /tbselenium/test/conftest.py: -------------------------------------------------------------------------------- 1 | from os import environ 2 | import tempfile 3 | from tbselenium.utils import start_xvfb, stop_xvfb, is_busy 4 | from tbselenium.test.fixtures import launch_tbb_tor_with_stem_fixture 5 | import tbselenium.common as cm 6 | from tbselenium.test import TBB_PATH 7 | from shutil import rmtree 8 | 9 | test_conf = {} 10 | 11 | 12 | def launch_tor(): 13 | # TODO: Consider using system tor (when available) to speed tests up 14 | tor_process = None 15 | temp_data_dir = tempfile.mkdtemp() 16 | torrc = {'ControlPort': str(cm.STEM_CONTROL_PORT), 17 | 'SOCKSPort': str(cm.STEM_SOCKS_PORT), 18 | 'DataDirectory': temp_data_dir} 19 | if not is_busy(cm.STEM_SOCKS_PORT): 20 | tor_process = launch_tbb_tor_with_stem_fixture(tbb_path=TBB_PATH, 21 | torrc=torrc) 22 | return (temp_data_dir, tor_process) 23 | 24 | 25 | def pytest_sessionstart(session): 26 | if ("TRAVIS" not in environ and 27 | ("NO_XVFB" not in environ or environ["NO_XVFB"] != "1")): 28 | test_conf["xvfb_display"] = start_xvfb() 29 | test_conf["temp_data_dir"], test_conf["tor_process"] = launch_tor() 30 | 31 | 32 | def pytest_sessionfinish(session, exitstatus): 33 | xvfb_display = test_conf.get("xvfb_display") 34 | tor_process = test_conf.get("tor_process") 35 | if xvfb_display: 36 | stop_xvfb(xvfb_display) 37 | 38 | if tor_process: 39 | tor_process.kill() 40 | rmtree(test_conf["temp_data_dir"], ignore_errors=True) 41 | -------------------------------------------------------------------------------- /tbselenium/test/fixtures.py: -------------------------------------------------------------------------------- 1 | import os 2 | import tempfile 3 | from socket import error as socket_error 4 | from tbselenium.tbdriver import TorBrowserDriver 5 | import tbselenium.common as cm 6 | from tbselenium.exceptions import StemLaunchError, TorBrowserDriverInitError 7 | from tbselenium.utils import launch_tbb_tor_with_stem, is_busy, read_file 8 | from selenium.common.exceptions import TimeoutException, WebDriverException 9 | 10 | from http.client import CannotSendRequest 11 | 12 | 13 | MAX_FIXTURE_TRIES = 3 14 | 15 | # make sure TB logs to a file, print if init fails 16 | FORCE_TB_LOGS_DURING_TESTS = True 17 | ERR_MSG_NETERROR_NETTIMEOUT = "Reached error page: about:neterror?e=netTimeout" 18 | 19 | 20 | class TBDriverFixture(TorBrowserDriver): 21 | """Extend TorBrowserDriver to have fixtures for tests.""" 22 | def __init__(self, *args, **kwargs): 23 | self.change_default_tor_cfg(kwargs) 24 | last_err = None 25 | log_file = kwargs.get("tbb_logfile_path") 26 | 27 | for tries in range(MAX_FIXTURE_TRIES): 28 | try: 29 | return super(TBDriverFixture, self).__init__(*args, **kwargs) 30 | except (TimeoutException, WebDriverException, 31 | socket_error) as last_err: 32 | print("\nERROR: TBDriver init error. Attempt %s %s" % 33 | ((tries + 1), last_err)) 34 | if FORCE_TB_LOGS_DURING_TESTS: 35 | logs = read_file(log_file) 36 | if len(logs): 37 | print("TB logs:\n%s\n(End of TB logs)" % logs) 38 | super(TBDriverFixture, self).quit() # clean up 39 | continue 40 | # Raise if we didn't return yet 41 | try: 42 | raise last_err 43 | except Exception: 44 | raise TorBrowserDriverInitError("Cannot initialize") 45 | 46 | def __del__(self): 47 | # remove the temp log file if we created 48 | if FORCE_TB_LOGS_DURING_TESTS and hasattr(self, "log_file") and os.path.isfile(self.log_file): 49 | os.remove(self.log_file) 50 | 51 | def change_default_tor_cfg(self, kwargs): 52 | """Use the Tor process that we started with at the beginning of the 53 | tests if the caller doesn't want to launch a new TBB Tor. 54 | 55 | This makes tests faster and more robust against network 56 | issues since otherwise we'd have to launch a new Tor process 57 | for each test. 58 | 59 | if FORCE_TB_LOGS_DURING_TESTS is True add a log file arg to make 60 | it easier to debug the failures. 61 | 62 | """ 63 | 64 | if kwargs.get("tor_cfg") is None and is_busy(cm.STEM_SOCKS_PORT): 65 | kwargs["tor_cfg"] = cm.USE_STEM 66 | kwargs["socks_port"] = cm.STEM_SOCKS_PORT 67 | kwargs["control_port"] = cm.STEM_CONTROL_PORT 68 | 69 | if FORCE_TB_LOGS_DURING_TESTS and\ 70 | kwargs.get("tbb_logfile_path") is None: 71 | _, self.log_file = tempfile.mkstemp() 72 | kwargs["tbb_logfile_path"] = self.log_file 73 | 74 | def load_url_ensure(self, *args, **kwargs): 75 | """Make sure the requested URL is loaded. Retry if necessary.""" 76 | last_err = None 77 | for tries in range(MAX_FIXTURE_TRIES): 78 | try: 79 | self.load_url(*args, **kwargs) 80 | if self.current_url != "about:newtab" and \ 81 | not self.is_connection_error_page: 82 | return 83 | except (TimeoutException, 84 | CannotSendRequest) as last_err: 85 | print("\nload_url timed out. Attempt %s %s" % 86 | ((tries + 1), last_err)) 87 | continue 88 | except WebDriverException as wd_err: 89 | if ERR_MSG_NETERROR_NETTIMEOUT in str(wd_err): 90 | print("\nload_url timed out (WebDriverException). " 91 | "Attempt %s %s" % ((tries + 1), last_err)) 92 | continue 93 | raise wd_err 94 | 95 | # Raise if we didn't return yet 96 | try: 97 | raise last_err 98 | except Exception: 99 | raise WebDriverException("Can't load the page") 100 | 101 | 102 | def launch_tbb_tor_with_stem_fixture(*args, **kwargs): 103 | last_err = None 104 | for tries in range(MAX_FIXTURE_TRIES): 105 | try: 106 | return launch_tbb_tor_with_stem(*args, **kwargs) 107 | except OSError as last_err: 108 | print("\nlaunch_tor try %s %s" % ((tries + 1), last_err)) 109 | if "timeout without success" in str(last_err): 110 | continue 111 | else: # we don't want to retry if this is not a timeout 112 | raise 113 | # Raise if we didn't return yet 114 | try: 115 | raise last_err 116 | except Exception: 117 | raise StemLaunchError("Cannot start Tor") 118 | -------------------------------------------------------------------------------- /tbselenium/test/test_addons.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import pytest 3 | from os.path import dirname, realpath, join 4 | from selenium.webdriver.common.by import By 5 | 6 | from selenium.webdriver.support import expected_conditions as EC 7 | from selenium.webdriver.support.ui import WebDriverWait 8 | 9 | from tbselenium import common as cm 10 | from tbselenium.test import TBB_PATH 11 | from tbselenium.test.fixtures import TBDriverFixture 12 | 13 | 14 | class TBAddonsTest(unittest.TestCase): 15 | 16 | def get_list_of_installed_addons(self, driver): 17 | found_addons = [] 18 | driver.load_url_ensure("about:addons") 19 | addons = driver.find_elements(By.CLASS_NAME, "addon-name") 20 | for addon in addons: 21 | found_addons.append(addon.text) 22 | return found_addons 23 | 24 | def test_builtin_addons_should_come_installed(self): 25 | """Make sure that the built-in addons come installed.""" 26 | EXPECTED_ADDONS = set(['NoScript']) 27 | found_addons = [] 28 | with TBDriverFixture(TBB_PATH) as driver: 29 | found_addons = self.get_list_of_installed_addons(driver) 30 | assert EXPECTED_ADDONS == set(found_addons) 31 | 32 | def test_should_install_custom_extension(self): 33 | XPI_NAME = "borderify.xpi" # sample extension based on: 34 | # https://github.com/mdn/webextensions-examples/tree/master/borderify 35 | test_dir = dirname(realpath(__file__)) 36 | xpi_path = join(test_dir, "test_data", XPI_NAME) 37 | 38 | with TBDriverFixture(TBB_PATH, extensions=[xpi_path]) as driver: 39 | assert 'Borderify' in self.get_list_of_installed_addons(driver) 40 | 41 | 42 | if __name__ == "__main__": 43 | unittest.main() 44 | -------------------------------------------------------------------------------- /tbselenium/test/test_bridge.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | # from time import sleep 3 | 4 | from tbselenium import common as cm 5 | from tbselenium.test import TBB_PATH 6 | from tbselenium.test.fixtures import TBDriverFixture 7 | 8 | CONGRATS = "Congratulations. This browser is configured to use Tor." 9 | 10 | 11 | class TBDriverBridgeTest(unittest.TestCase): 12 | def tearDown(self): 13 | self.tb_driver.quit() 14 | 15 | def should_load_check_tpo_via_bridge(self, bridge_type): 16 | self.tb_driver = TBDriverFixture(TBB_PATH, 17 | default_bridge_type=bridge_type) 18 | self.tb_driver.load_url_ensure(cm.CHECK_TPO_URL) 19 | status = self.tb_driver.find_element_by("h1.on") 20 | self.assertEqual(status.text, CONGRATS) 21 | # sleep(0) # the bridge type in use is manually verified by 22 | # checking the Tor Network Settings dialog. 23 | # We set a sleep of a few seconds and exported NO_XVFB=1 to do that 24 | # manually. 25 | 26 | # TODO: find a way to automatically verify the bridge in use 27 | # This may be possible with geckodriver, since it can interact 28 | # with chrome as well. 29 | 30 | def test_should_load_check_tpo_via_meek_azure_bridge(self): 31 | self.should_load_check_tpo_via_bridge("meek-azure") 32 | 33 | def test_should_load_check_tpo_via_meek_obfs3_bridge(self): 34 | self.should_load_check_tpo_via_bridge("obfs3") 35 | 36 | def test_should_load_check_tpo_via_obfs4_bridge(self): 37 | self.should_load_check_tpo_via_bridge("obfs4") 38 | 39 | def test_should_load_check_tpo_via_fte_bridge(self): 40 | self.should_load_check_tpo_via_bridge("fte") 41 | -------------------------------------------------------------------------------- /tbselenium/test/test_browser.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import sys 3 | import unittest 4 | import tempfile 5 | import psutil 6 | from tbselenium.test.fixtures import TBDriverFixture 7 | from tbselenium import common as cm 8 | from tbselenium.test import TBB_PATH 9 | from tbselenium.utils import read_file 10 | 11 | from os.path import exists, getmtime, join 12 | from os import remove 13 | 14 | 15 | class TorBrowserTest(unittest.TestCase): 16 | 17 | @classmethod 18 | def setUpClass(cls): 19 | _, cls.log_file = tempfile.mkstemp() 20 | cls.driver = TBDriverFixture(TBB_PATH, tbb_logfile_path=cls.log_file) 21 | 22 | @classmethod 23 | def tearDownClass(cls): 24 | if cls.driver: 25 | cls.driver.quit() 26 | if exists(cls.log_file): 27 | remove(cls.log_file) 28 | 29 | def test_correct_firefox_binary(self): 30 | """Make sure we use the Firefox binary from the TBB directory.""" 31 | try: 32 | # driver.binary was removed in selenium-4.10.0 33 | # https://github.com/SeleniumHQ/selenium/pull/12030/files#diff-89ba579445647535b74423c7bf4b8be79ef1ce33847a2768e623c3083a33545dL127 34 | tbbinary = self.driver.options.binary 35 | except AttributeError: 36 | tbbinary = self.driver.binary 37 | self.assertTrue(tbbinary.which('firefox').startswith(TBB_PATH)) 38 | 39 | # TODO: log output is always empty 40 | @pytest.mark.xfail 41 | def test_tbb_logfile(self): 42 | log_txt = read_file(self.log_file) 43 | self.assertIn("Torbutton INFO", log_txt) 44 | 45 | @pytest.mark.skipif(sys.platform != 'linux', reason='Requires Linux') 46 | def test_should_load_tbb_firefox_libs(self): 47 | """Make sure we load the Firefox libraries from the TBB directories. 48 | We only test libxul (main Firefox/Gecko library) and libstdc++. 49 | 50 | The memory map of the TB process is used to find loaded libraries. 51 | http://man7.org/linux/man-pages/man5/proc.5.html 52 | """ 53 | FF_BINARY_SUFFIX = '.real' 54 | driver = self.driver 55 | geckodriver_pid = driver.service.process.pid 56 | process = psutil.Process(geckodriver_pid) 57 | try: 58 | # driver.binary was removed in selenium-4.10.0 59 | # https://github.com/SeleniumHQ/selenium/pull/12030/files#diff-89ba579445647535b74423c7bf4b8be79ef1ce33847a2768e623c3083a33545dL127 60 | tbbinary = self.driver.options.binary 61 | except AttributeError: 62 | tbbinary = self.driver.binary 63 | tbbinary_path = tbbinary.which('firefox') + FF_BINARY_SUFFIX 64 | for child in process.children(): 65 | if tbbinary_path == child.exe(): 66 | tb_pid = child.pid 67 | break 68 | else: 69 | self.fail("Cannot find the firefox process") 70 | xul_lib_path = join(driver.tbb_browser_dir, "libxul.so") 71 | std_c_lib_path = join(driver.tbb_browser_dir, "libssl3.so") 72 | proc_mem_map_file = "/proc/%d/maps" % (tb_pid) 73 | mem_map = read_file(proc_mem_map_file) 74 | self.assertIn(xul_lib_path, mem_map) 75 | self.assertIn(std_c_lib_path, mem_map) 76 | 77 | def test_tbdriver_fx_profile_not_be_modified(self): 78 | """Visiting a site should not modify the original profile contents.""" 79 | profile_path = join(TBB_PATH, cm.DEFAULT_TBB_PROFILE_PATH) 80 | mtime_before = getmtime(profile_path) 81 | self.driver.load_url_ensure(cm.CHECK_TPO_URL) 82 | mtime_after = getmtime(profile_path) 83 | self.assertEqual(mtime_before, mtime_after) 84 | -------------------------------------------------------------------------------- /tbselenium/test/test_context_switch.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from tbselenium.test import TBB_PATH 3 | from tbselenium.test.fixtures import TBDriverFixture 4 | from tbselenium.common import CHECK_TPO_URL 5 | from selenium.common.exceptions import WebDriverException 6 | 7 | 8 | # based on https://stackoverflow.com/a/6549765 9 | ENUMERATE_FONTS = """ 10 | var font_enumerator = Components.classes["@mozilla.org/gfx/fontenumerator;1"] 11 | .getService(Components.interfaces.nsIFontEnumerator); 12 | return font_enumerator.EnumerateAllFonts({});""" 13 | 14 | COMPONENT_CLASSES_JS = "var c = Components.classes; return c;" 15 | 16 | 17 | class TestGeckoDriverChromeScript(unittest.TestCase): 18 | 19 | def test_should_not_run_chrome_script_without_context_switch(self): 20 | with self.assertRaises(WebDriverException): 21 | with TBDriverFixture(TBB_PATH) as driver: 22 | driver.execute_script(COMPONENT_CLASSES_JS) 23 | 24 | def test_should_run_chrome_script(self): 25 | with TBDriverFixture(TBB_PATH) as driver: 26 | driver.set_context(driver.CONTEXT_CHROME) 27 | driver.execute_script(COMPONENT_CLASSES_JS) 28 | 29 | def test_bundled_fonts_via_chrome_script(self): 30 | # do not disable this pref when crawling untrusted sites 31 | pref_dict = {"dom.ipc.cpows.forbid-unsafe-from-browser": False} 32 | 33 | with TBDriverFixture(TBB_PATH, pref_dict=pref_dict) as driver: 34 | driver.load_url_ensure(CHECK_TPO_URL) 35 | driver.set_context(driver.CONTEXT_CHROME) 36 | components = driver.execute_script(COMPONENT_CLASSES_JS) 37 | self.assertIn("@mozilla.org/gfx/fontenumerator", 38 | str(components)) 39 | used_fonts = driver.execute_script(ENUMERATE_FONTS) 40 | self.assertIn("Arimo", str(used_fonts)) 41 | -------------------------------------------------------------------------------- /tbselenium/test/test_data/borderify.xpi: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webfp/tor-browser-selenium/a1f8d9e0ea1f6fb1da45e4f94646af0709a73bec/tbselenium/test/test_data/borderify.xpi -------------------------------------------------------------------------------- /tbselenium/test/test_data/img_test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 |