Skip to content

Commit 37cc758

Browse files
authored
Merge pull request #221 from seleniumbase/proxy-with-auth
Ability to use Proxy servers that require auth
2 parents 2011a15 + 3b0a2b8 commit 37cc758

16 files changed

+195
-24
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ report.html
6262

6363
# Other
6464
selenium-server-standalone.jar
65+
proxy.zip
6566
verbose_hub_server.dat
6667
verbose_node_server.dat
6768
ip_of_grid_hub.dat

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,12 @@ If you wish to use a proxy server for your browser tests (Chrome and Firefox onl
336336
pytest proxy_test.py --proxy=IP_ADDRESS:PORT
337337
```
338338

339+
If the proxy server that you wish to use requires authentication, you can do the following (Chrome only):
340+
341+
```
342+
pytest proxy_test.py --proxy=USERNAME:PASSWORD@IP_ADDRESS:PORT
343+
```
344+
339345
To make things easier, you can add your frequently-used proxies to PROXY_LIST in [proxy_list.py](https://github.com/seleniumbase/SeleniumBase/blob/master/seleniumbase/config/proxy_list.py), and then use ``--proxy=KEY_FROM_PROXY_LIST`` to use the IP_ADDRESS:PORT of that key.
340346

341347
```

examples/my_first_test.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
class MyTestClass(BaseCase):
55

66
def test_basic(self):
7-
self.open('https://xkcd.com/353/') # Navigate to the web page
7+
self.open('https://xkcd.com/353/') # Navigate to the web page
88
self.assert_element('img[alt="Python"]') # Assert element on page
99
self.click('a[rel="license"]') # Click element on page
1010
self.assert_text('free to copy', 'div center') # Assert text on page

examples/proxy_test.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,6 @@ def test_proxy(self):
1010
self.open('https://ipinfo.io/%s' % ip_address)
1111
print("\n\nIP Address = %s\n" % ip_address)
1212
print("Displaying Host Info:")
13-
print(self.get_text('table.table'))
13+
print(self.get_text('ul.address-list'))
1414
print("\nThe browser will close automatically in 7 seconds...")
1515
time.sleep(7)

examples/tour_examples/xkcd_tour.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,5 @@ def test_basic(self):
1717
self.add_tour_step("Click for the license here.", 'a[rel="license"]')
1818
self.add_tour_step("This selects a random comic.", 'a[href*="random"]')
1919
self.add_tour_step("Thanks for taking this tour!")
20-
# self.export_tour() # Use this to export the tour as a .js file
20+
# self.export_tour() # Use this to export the tour as [my_tour.js]
2121
self.play_tour()
22-

help_docs/customizing_test_runs.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ pytest my_test_suite.py --server=IP_ADDRESS --port=4444
3636

3737
pytest my_test_suite.py --proxy=IP_ADDRESS:PORT
3838

39+
pytest my_test_suite.py --proxy=USERNAME:PASSWORD@IP_ADDRESS:PORT
40+
3941
pytest test_fail.py -s --pdb --pdb-failures
4042
```
4143

help_docs/features_list.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
* Uses a [global config file](https://github.com/seleniumbase/SeleniumBase/blob/master/seleniumbase/config/settings.py) for configuring SeleniumBase to your specific needs.
1212
* Backwards-compatible with [WebDriver](http://www.seleniumhq.org/projects/webdriver/). (Use ``self.driver`` anywhere.)
1313
* Can run tests through a proxy server. (Use ``--proxy=IP_ADDRESS:PORT``)
14+
* Can use an authenticated proxy server. (``--proxy=USERNAME:PASSWORD@IP_ADDRESS:PORT``)
1415
* Can handle Google Authenticator logins by using the [Python one-time password library](https://pyotp.readthedocs.io/en/latest/).
1516
* Includes a hybrid-automation solution called **[MasterQA](https://github.com/seleniumbase/SeleniumBase/blob/master/seleniumbase/masterqa/ReadMe.md)** to speed up manual testing.
1617
* Includes integrations with [MySQL](https://github.com/seleniumbase/SeleniumBase/blob/master/seleniumbase/core/testcase_manager.py), [Selenium Grid](https://github.com/seleniumbase/SeleniumBase/tree/master/seleniumbase/utilities/selenium_grid), [Google Cloud](https://github.com/seleniumbase/SeleniumBase/tree/master/integrations/google_cloud/ReadMe.md), [Amazon S3](https://github.com/seleniumbase/SeleniumBase/blob/master/seleniumbase/plugins/s3_logging_plugin.py), and [NodeJS](https://github.com/seleniumbase/SeleniumBase/tree/master/integrations/node_js).

requirements.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ pip
22
ipython
33
setuptools
44
selenium==3.14.1
5-
pytest>=3.8.2
5+
pytest>=3.9.1
66
pytest-cov>=2.6.0
77
pytest-html>=1.19.0
88
pytest-rerunfailures>=4.2
@@ -14,8 +14,8 @@ pyotp>=2.2.6
1414
requests>=2.19.1
1515
unittest2>=1.1.0
1616
chardet>=3.0.4
17-
urllib3>=1.23
1817
boto>=2.49.0
18+
urllib3==1.23
1919
nose==1.3.7
2020
ipdb==0.11
2121
flake8==3.5.0

seleniumbase/config/proxy_list.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,14 @@
1515
Example proxies in PROXY_LIST below are not guaranteed to be active or secure.
1616
If you don't already have a proxy server to connect to,
1717
you can try finding one from one of following sites:
18+
* https://www.proxynova.com/proxy-server-list/port-8080/
1819
* https://www.us-proxy.org/
1920
* https://hidemy.name/en/proxy-list/?country=US&type=h#list
2021
* http://proxyservers.pro/proxy/list/protocol/http/country/US/
2122
"""
2223

2324
PROXY_LIST = {
24-
# "example1": "64.33.247.157:3128", # (Example) - set your own proxy here
25+
"example1": "104.248.122.30:8080", # (Example) - set your own proxy here
2526
"proxy1": None,
2627
"proxy2": None,
2728
"proxy3": None,

seleniumbase/core/browser_launcher.py

Lines changed: 76 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,20 @@
11
import os
22
import re
33
import sys
4+
import threading
5+
import time
46
import warnings
57
from selenium import webdriver
68
from selenium.common.exceptions import WebDriverException
79
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
810
from seleniumbase.config import proxy_list
911
from seleniumbase.core import download_helper
12+
from seleniumbase.core import proxy_helper
1013
from seleniumbase.fixtures import constants
1114
from seleniumbase.fixtures import page_utils
1215
from seleniumbase import drivers # webdriver storage folder for SeleniumBase
1316
DRIVER_DIR = os.path.dirname(os.path.realpath(drivers.__file__))
17+
PROXY_ZIP_PATH = proxy_helper.PROXY_ZIP_PATH
1418
PLATFORM = sys.platform
1519
IS_WINDOWS = False
1620
LOCAL_CHROMEDRIVER = None
@@ -49,7 +53,31 @@ def make_driver_executable_if_not(driver_path):
4953
make_executable(driver_path)
5054

5155

52-
def _set_chrome_options(downloads_path, proxy_string):
56+
def _add_chrome_proxy_extension(
57+
chrome_options, proxy_string, proxy_user, proxy_pass):
58+
""" Implementation of https://stackoverflow.com/a/35293284 for
59+
https://stackoverflow.com/questions/12848327/
60+
(Run Selenium on a proxy server that requires authentication.)
61+
The retry_on_exception is only needed for multithreaded runs
62+
because proxy.zip is a common file shared between all tests
63+
in a single run. """
64+
if not "".join(sys.argv) == "-c":
65+
# Single-threaded
66+
proxy_helper.create_proxy_zip(proxy_string, proxy_user, proxy_pass)
67+
else:
68+
# Pytest multi-threaded test
69+
lock = threading.Lock()
70+
with lock:
71+
if not os.path.exists(PROXY_ZIP_PATH):
72+
proxy_helper.create_proxy_zip(
73+
proxy_string, proxy_user, proxy_pass)
74+
time.sleep(0.3)
75+
chrome_options.add_extension(PROXY_ZIP_PATH)
76+
return chrome_options
77+
78+
79+
def _set_chrome_options(
80+
downloads_path, proxy_string, proxy_auth, proxy_user, proxy_pass):
5381
chrome_options = webdriver.ChromeOptions()
5482
prefs = {
5583
"download.default_directory": downloads_path,
@@ -71,6 +99,10 @@ def _set_chrome_options(downloads_path, proxy_string):
7199
chrome_options.add_argument("--disable-translate")
72100
chrome_options.add_argument("--disable-web-security")
73101
if proxy_string:
102+
if proxy_auth:
103+
chrome_options = _add_chrome_proxy_extension(
104+
chrome_options, proxy_string, proxy_user, proxy_pass)
105+
chrome_options.add_extension(DRIVER_DIR + "/proxy.zip")
74106
chrome_options.add_argument('--proxy-server=%s' % proxy_string)
75107
if "win32" in sys.platform or "win64" in sys.platform:
76108
chrome_options.add_argument("--log-level=3")
@@ -155,22 +187,55 @@ def validate_proxy_string(proxy_string):
155187

156188
def get_driver(browser_name, headless=False, use_grid=False,
157189
servername='localhost', port=4444, proxy_string=None):
190+
proxy_auth = False
191+
proxy_user = None
192+
proxy_pass = None
158193
if proxy_string:
194+
username_and_password = None
195+
if "@" in proxy_string:
196+
# Format => username:password@hostname:port
197+
try:
198+
username_and_password = proxy_string.split('@')[0]
199+
proxy_string = proxy_string.split('@')[1]
200+
proxy_user = username_and_password.split(':')[0]
201+
proxy_pass = username_and_password.split(':')[1]
202+
except Exception:
203+
raise Exception(
204+
'The format for using a proxy server with authentication '
205+
'is: "username:password@hostname:port". If using a proxy '
206+
'server without auth, the format is: "hostname:port".')
207+
if browser_name != constants.Browser.GOOGLE_CHROME:
208+
raise Exception(
209+
"Chrome is required when using a proxy server that has "
210+
"authentication! (If using a proxy server without auth, "
211+
"either Chrome or Firefox may be used.)")
159212
proxy_string = validate_proxy_string(proxy_string)
213+
if proxy_string and proxy_user and proxy_pass:
214+
if not os.path.exists(PROXY_ZIP_PATH):
215+
proxy_helper.create_proxy_zip(
216+
proxy_string, proxy_user, proxy_pass)
217+
proxy_auth = True
160218
if use_grid:
161219
return get_remote_driver(
162-
browser_name, headless, servername, port, proxy_string)
220+
browser_name, headless, servername, port, proxy_string, proxy_auth,
221+
proxy_user, proxy_pass)
163222
else:
164-
return get_local_driver(browser_name, headless, proxy_string)
223+
return get_local_driver(
224+
browser_name, headless, proxy_string, proxy_auth,
225+
proxy_user, proxy_pass)
165226

166227

167-
def get_remote_driver(browser_name, headless, servername, port, proxy_string):
228+
def get_remote_driver(
229+
browser_name, headless, servername, port, proxy_string, proxy_auth,
230+
proxy_user, proxy_pass):
168231
downloads_path = download_helper.get_downloads_folder()
169232
download_helper.reset_downloads_folder()
170233
address = "http://%s:%s/wd/hub" % (servername, port)
171234

172235
if browser_name == constants.Browser.GOOGLE_CHROME:
173-
chrome_options = _set_chrome_options(downloads_path, proxy_string)
236+
chrome_options = _set_chrome_options(
237+
downloads_path, proxy_string, proxy_auth,
238+
proxy_user, proxy_pass)
174239
if headless:
175240
chrome_options.add_argument("--headless")
176241
chrome_options.add_argument("--disable-gpu")
@@ -237,7 +302,9 @@ def get_remote_driver(browser_name, headless, servername, port, proxy_string):
237302
webdriver.DesiredCapabilities.PHANTOMJS))
238303

239304

240-
def get_local_driver(browser_name, headless, proxy_string):
305+
def get_local_driver(
306+
browser_name, headless, proxy_string, proxy_auth,
307+
proxy_user, proxy_pass):
241308
'''
242309
Spins up a new web browser and returns the driver.
243310
Can also be used to spin up additional browsers for the same test.
@@ -326,7 +393,9 @@ def get_local_driver(browser_name, headless, proxy_string):
326393
return webdriver.PhantomJS()
327394
elif browser_name == constants.Browser.GOOGLE_CHROME:
328395
try:
329-
chrome_options = _set_chrome_options(downloads_path, proxy_string)
396+
chrome_options = _set_chrome_options(
397+
downloads_path, proxy_string, proxy_auth,
398+
proxy_user, proxy_pass)
330399
if headless:
331400
chrome_options.add_argument("--headless")
332401
chrome_options.add_argument("--disable-gpu")

seleniumbase/core/log_helper.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ def log_test_failure_data(test, test_logpath, driver, browser):
3333
if sys.version.startswith('3') and hasattr(test, '_outcome'):
3434
if test._outcome.errors:
3535
try:
36-
exc_message = test._outcome.errors[0][1][1].msg
36+
exc_message = test._outcome.errors[0][1][1]
3737
traceback_address = test._outcome.errors[0][1][2]
3838
traceback_list = traceback.format_list(
3939
traceback.extract_tb(traceback_address)[1:])
@@ -42,7 +42,7 @@ def log_test_failure_data(test, test_logpath, driver, browser):
4242
exc_message = "(Unknown Exception)"
4343
traceback_message = "(Unknown Traceback)"
4444
data_to_save.append("Traceback: " + traceback_message)
45-
data_to_save.append("Exception: " + exc_message)
45+
data_to_save.append("Exception: " + str(exc_message))
4646
else:
4747
data_to_save.append("Traceback: " + ''.join(
4848
traceback.format_exception(sys.exc_info()[0],

seleniumbase/core/proxy_helper.py

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import os
2+
import threading
3+
import zipfile
4+
from seleniumbase import drivers
5+
DRIVER_DIR = os.path.dirname(os.path.realpath(drivers.__file__))
6+
PROXY_ZIP_PATH = "%s/%s" % (DRIVER_DIR, "proxy.zip")
7+
8+
9+
def create_proxy_zip(proxy_string, proxy_user, proxy_pass):
10+
""" Implementation of https://stackoverflow.com/a/35293284 for
11+
https://stackoverflow.com/questions/12848327/
12+
(Run Selenium on a proxy server that requires authentication.)
13+
Solution involves creating & adding a Chrome extension on the fly.
14+
* CHROME-ONLY for now! *
15+
"""
16+
proxy_host = proxy_string.split(':')[0]
17+
proxy_port = proxy_string.split(':')[1]
18+
proxy_zip = DRIVER_DIR + '/proxy.zip'
19+
background_js = (
20+
"""var config = {\n"""
21+
""" mode: "fixed_servers",\n"""
22+
""" rules: {\n"""
23+
""" singleProxy: {\n"""
24+
""" scheme: "http",\n"""
25+
""" host: "%s",\n"""
26+
""" port: parseInt("%s")\n"""
27+
""" },\n"""
28+
""" }\n"""
29+
""" };\n"""
30+
"""chrome.proxy.settings.set("""
31+
"""{value: config, scope: "regular"}, function() {"""
32+
"""});\n"""
33+
"""function callbackFn(details) {\n"""
34+
""" return {\n"""
35+
""" authCredentials: {\n"""
36+
""" username: "%s",\n"""
37+
""" password: "%s"\n"""
38+
""" }\n"""
39+
""" };\n"""
40+
"""}\n"""
41+
"""chrome.webRequest.onAuthRequired.addListener(\n"""
42+
""" callbackFn,\n"""
43+
""" {urls: ["<all_urls>"]},\n"""
44+
""" ['blocking']\n"""
45+
""");""" % (proxy_host, proxy_port, proxy_user, proxy_pass))
46+
manifest_json = (
47+
'''{\n'''
48+
'''"version": "1.0.0",\n'''
49+
'''"manifest_version": 2,\n'''
50+
'''"name": "Chrome Proxy",\n'''
51+
'''"permissions": [\n'''
52+
''' "proxy",\n'''
53+
''' "tabs",\n'''
54+
''' "unlimitedStorage",\n'''
55+
''' "storage",\n'''
56+
''' "<all_urls>",\n'''
57+
''' "webRequest",\n'''
58+
''' "webRequestBlocking"\n'''
59+
'''],\n'''
60+
'''"background": {\n'''
61+
''' "scripts": ["background.js"]\n'''
62+
'''},\n'''
63+
'''"minimum_chrome_version":"22.0.0"\n'''
64+
'''}''')
65+
lock = threading.RLock() # Support multi-threaded test runs with Pytest
66+
with lock:
67+
zf = zipfile.ZipFile(proxy_zip, mode='w')
68+
zf.writestr("background.js", background_js)
69+
zf.writestr("manifest.json", manifest_json)
70+
zf.close()
71+
72+
73+
def remove_proxy_zip_if_present():
74+
""" Remove Chrome extension zip file used for proxy server authentication.
75+
Used in the implementation of https://stackoverflow.com/a/35293284
76+
for https://stackoverflow.com/questions/12848327/
77+
"""
78+
try:
79+
if os.path.exists(PROXY_ZIP_PATH):
80+
os.remove(PROXY_ZIP_PATH)
81+
except Exception:
82+
pass

seleniumbase/plugins/pytest_plugin.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import optparse
44
import pytest
55
from seleniumbase.core import log_helper
6+
from seleniumbase.core import proxy_helper
67
from seleniumbase.fixtures import constants
78

89

@@ -84,6 +85,7 @@ def pytest_addoption(parser):
8485
default=None,
8586
help="""Designates the proxy server:port to use.
8687
Format: servername:port. OR
88+
username:password@servername:port OR
8789
A dict key from proxy_list.PROXY_LIST
8890
Default: None.""")
8991
parser.addoption('--headless', action="store_true",
@@ -138,11 +140,12 @@ def pytest_configure(config):
138140
if with_testing_base:
139141
log_path = config.getoption('log_path')
140142
log_helper.log_folder_setup(log_path)
143+
proxy_helper.remove_proxy_zip_if_present()
141144

142145

143146
def pytest_unconfigure():
144-
""" This runs after all tests have completed with pytest """
145-
pass
147+
""" This runs after all tests have completed with pytest. """
148+
proxy_helper.remove_proxy_zip_if_present()
146149

147150

148151
def pytest_runtest_setup():

0 commit comments

Comments
 (0)