Skip to content

Commit

Permalink
Handle https proxy values (#276)
Browse files Browse the repository at this point in the history
* add https proxy handling and tests
* update log messages to be more useful
* Surface the assumed https url for tableau server
  • Loading branch information
jacalata authored Feb 21, 2024
1 parent 3065f3d commit 3b75f3a
Show file tree
Hide file tree
Showing 4 changed files with 85 additions and 32 deletions.
46 changes: 33 additions & 13 deletions tabcmd/commands/auth/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,13 @@ def _update_session_data(self, args):
self.username = self.username.lower()
self.server_url = args.server or self.server_url or "http://localhost"
self.server_url = self.server_url.lower()
if not self.server_url.find('http') == 0:
# This was being secretly done by TSC already - just making it obvious to the user.
self.logger.info("No protocol given for the server URL. Assuming https.")
self.server_url = "https://" + self.server_url
if args.server is not None:
self.site_name = None

self.site_name = args.site_name or self.site_name or ""
self.site_name = self.site_name.lower()
if self.site_name == "default":
Expand Down Expand Up @@ -152,27 +157,33 @@ def _create_new_token_credential(self):
else:
Errors.exit_with_error(self.logger, _("session.errors.missing_arguments").format("token name"))

def _open_connection_with_opts(self) -> TSC.Server:
def _set_connection_options(self) -> Dict[str, Any]:
self.logger.debug("Setting up request options")
http_options: Dict[str, Any] = {"headers": {"User-Agent": "Tabcmd/{}".format(version)}}

if self.no_certcheck:
http_options["verify"] = False
urllib3.disable_warnings(category=InsecureRequestWarning)

"""
Do we want to do the same format check as old tabcmd?
For now I think we can trust requests to handle a bad proxy
Pattern pattern = Pattern.compile("([^:]*):([0-9]*)");
if not matches:
throw new ReportableException(m_i18n.getString("sessionoptions.errors.bad_proxy_format", proxyArg));
"""
# proxy debugging:
# 1. verify that the proxy url is right: 'curl --proxy="<the same proxy>" "https://google.com/" '
# 2. in urllib3: https://urllib3.readthedocs.io/en/stable/advanced-usage.html#your-proxy-appears-to-only-use-http-and-not-https
if self.proxy:
self.logger.debug("Setting http proxy: {}".format(self.proxy))
proxies = {"http": self.proxy}
proxies = None
if self.proxy.find("https") == 0:
self.logger.debug("Setting https proxy: {}".format(self.proxy))
proxies = {"https": self.proxy}
else:
if not self.proxy.find("http") == 0:
self.logger.debug("Proxy URL must include a protocol. Since there was none given, assuming http only.")
self.proxy = "http://" + self.proxy
# proxy is now a http url
self.logger.debug("Setting http proxy: {}".format(self.proxy))
proxies = {"http": self.proxy}
print(self.proxy)
http_options["proxies"] = proxies
if self.no_proxy:
# override any proxy that was set
# override any proxy that was set. This will ignore a proxy in environment variables.
http_options["proxies"] = None

if self.timeout:
Expand All @@ -181,12 +192,15 @@ def _open_connection_with_opts(self) -> TSC.Server:
if self.certificate:
http_options["cert"] = self.certificate

self.logger.debug(http_options)
return http_options

def _open_connection_with_opts(self) -> TSC.Server:
http_options: Dict[str, Any] = self._set_connection_options()
try:
self.logger.debug(http_options)
# this is the only place we open a connection to the server
# so the request options are all set for the session now
tableau_server = TSC.Server(self.server_url, http_options=http_options)

except Exception as e:
self.logger.debug(
"Connection args: server {}, site {}, proxy {}/no-proxy {}, cert {}".format(
Expand Down Expand Up @@ -263,6 +277,12 @@ def _sign_in(self, tableau_auth) -> TSC.Server:
self.logger.debug(_("listsites.output").format("", self.username or self.token_name, self.site_name))
try:
self.tableau_server.auth.sign_in(tableau_auth) # it's the same call for token or user-pass
except requests.exceptions.SSLError as ssl_error:
error_message = "There was a problem creating a secure connection to the server. \n \
It is likely caused by an issue configuring your network proxy or server certificate, either \
on the server or on this machine."

Errors.exit_with_error(self.logger, message=error_message, exception=ssl_error)
except Exception as e:
Errors.exit_with_error(self.logger, exception=e)
try:
Expand Down
17 changes: 10 additions & 7 deletions tabcmd/commands/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,23 +58,23 @@ def log_stack(logger):

@staticmethod
def exit_with_error(logger, message: Optional[str] = None, exception: Optional[Exception] = None):
suggest_logs_message = "Exiting with error...\nSee logs for more information."
try:
if message and not exception:
logger.error(message)
Errors.log_stack(logger)
elif exception:
if message:
logger.info("Error message: " + message)
logger.info("\nError message: " + message)
Errors.check_common_error_codes_and_explain(logger, exception)
else:
logger.info("No exception or message provided")

except Exception as exc:
print(sys.stderr, "Error during log call from exception - {} {}".format(exc.__class__, message))
try:
logger.info("Exiting...")
except Exception:
print(sys.stderr, "Exiting...")
print("Error during log call from exception - {} {}".format(exc.__class__, message))

print("")
print(suggest_logs_message)
sys.exit(1)

@staticmethod
Expand All @@ -90,7 +90,10 @@ def check_common_error_codes_and_explain(logger, exception: Exception):
# session.renew_session()
return
if exception.__class__ == tableauserverclient.ServerResponseError:
logger.debug("Server response error")

logger.error(exception)
else:
logger.exception(exception)
# logger.exception prints a really long ugly stack, generally not useful to users.
# Should print that to the log, but not console.
logger.error(exception)
2 changes: 1 addition & 1 deletion tabcmd/execution/parent_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ def run_command(self, args):
"{<command> [command args]}", # 5
"Tableau Server Command Line Utility", # 6
"Show version information and exit.", # 7
"Use the specified logging level. The default level is INFO.", # 8
"Use the specified logging level. The default level is INFO. Logs are stored at {user}/AppData/Local/Tableau/Tabcmd/ or ~/.tableau/tabcmd/", # 8
"Treat resource conflicts as item creation success e.g project already exists", # 9
"Set the language to use. Exported data will be returned in this lang/locale.\n \
If not set, the client will use your computer locale, and the server will use \
Expand Down
52 changes: 41 additions & 11 deletions tests/commands/test_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -420,45 +420,75 @@ class ConnectionOptionsTest(unittest.TestCase):
def test_user_agent(self):
mock_session = Session()
mock_session.server_url = "fakehost"
connection = mock_session._open_connection_with_opts()
assert connection._http_options["headers"]["User-Agent"].startswith("Tabcmd/")
connection = mock_session._set_connection_options()
assert connection["headers"]["User-Agent"].startswith("Tabcmd/")

def test_no_certcheck(self):
mock_session = Session()
mock_session.server_url = "fakehost"
mock_session.no_certcheck = True
mock_session.site_id = "s"
mock_session.user_id = "u"
connection = mock_session._open_connection_with_opts()
assert connection._http_options["verify"] == False
connection = mock_session._set_connection_options()
assert connection["verify"] == False

def test_cert(self):
mock_session = Session()
mock_session.server_url = "fakehost"
mock_session.site_id = "s"
mock_session.user_id = "u"
mock_session.certificate = "my-cert-info"
connection = mock_session._open_connection_with_opts()
assert connection._http_options["cert"] == mock_session.certificate
connection = mock_session._set_connection_options()
assert connection["cert"] == mock_session.certificate

def test_proxy_stuff(self):
def test_proxy_http(self):
mock_session = Session()
mock_session.server_url = "fakehost"
mock_session.site_id = "s"
mock_session.user_id = "u"
mock_session.proxy = "proxy:port"
connection = mock_session._open_connection_with_opts()
assert connection._http_options["proxies"] == {"http": mock_session.proxy}
connection = mock_session._set_connection_options()
fixed_proxy = "http://proxy:port"
assert connection["proxies"] == {"http": mock_session.proxy}
assert fixed_proxy == mock_session.proxy

def test_proxy_https(self):
mock_session = Session()
mock_session.server_url = "fakehost"
mock_session.site_id = "s"
mock_session.user_id = "u"
mock_session.proxy = "https://proxy:port"
connection = mock_session._set_connection_options()
assert connection["proxies"] == {"https": mock_session.proxy}

def test_no_proxy(self):
mock_session = Session()
mock_session.server_url = "fakehost"
mock_session.site_id = "s"
mock_session.user_id = "u"
mock_session.no_proxy = True
connection = mock_session._set_connection_options()
assert connection["proxies"] == None


def test_timeout(self):
mock_session = Session()
mock_session.server_url = "fakehost"
mock_session.site_id = "s"
mock_session.user_id = "u"
mock_session.timeout = 10
connection = mock_session._open_connection_with_opts()
assert connection._http_options["timeout"] == 10
connection = mock_session._set_connection_options()
assert connection["timeout"] == 10


def test_setting_options_on_connection(self):
mock_session = Session()
mock_session.server_url = "fakehost"
mock_session.site_id = "s"
mock_session.user_id = "u"
mock_session.timeout = 10
connection = mock_session._open_connection_with_opts()
assert connection.http_options["timeout"] == 10

"""
class CookieTests(unittest.TestCase):
Expand Down

0 comments on commit 3b75f3a

Please sign in to comment.