Skip to content

Commit f2d2fdd

Browse files
committed
Add ability to use a proxy server with authentication on Chrome
1 parent 2011a15 commit f2d2fdd

File tree

2 files changed

+158
-7
lines changed

2 files changed

+158
-7
lines changed

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/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

0 commit comments

Comments
 (0)