# external import
import os
import json
import logging
try:
from urllib import quote_plus # Python2
except ImportError:
from urllib.parse import quote_plus # Python3
try:
from httplib import HTTPResponse as http_HTTPResponse # Python2
except ImportError:
from http.client import HTTPResponse as http_HTTPResponse # Python3
try:
from StringIO import StringIO as FakeSocketParam # Python2
except ImportError:
from io import BytesIO as FakeSocketParam # Python3
from . import exception, SSHSession
logger = logging.getLogger(__name__)
[docs]class RestSshClient(object):
def __init__(
self,
ssh_session=None,
**kwargs
):
"""
:param ssh_session:
:param host:
:param username:
:param kwargs:
"""
if ssh_session:
self.ssh_session = ssh_session
else:
self.ssh_session = SSHSession(host=kwargs.pop('host'),
username=kwargs.pop('username'), **kwargs)
self.host = self.ssh_session.host
self.username = self.ssh_session.username
def __enter__(self):
self.ssh_session.open()
return self
def __exit__(self, *args):
self.ssh_session.close()
def __repr__(self):
return '%s(host=%s, username=%s, ssh_session=%s)' \
% (self.__class__.__name__, self.host, self.username, repr(self.ssh_session))
[docs] def request(self, method, uri, **kwargs):
"""Perform http request and send back http response.
:param method: http method.
:param uri: remote URL to target.
:param params: (optional) Dictionary to be sent in the query string.
:param data: (optional) Content to send in the body of the http request.
:param headers: (optional) Dictionary of HTTP Headers to send with the http request.
:param remote_file: (optional) File on the remote host with content to send in the body of the http request.
:param local_file: (optional) Local file with content to send in the body of the http request.
:param document_info_only: (optional) if True, only HTTP Headers are returned in http response (default=False).
:param auth: (optional) Auth tuple to enable Basic/Digest/Custom HTTP Auth.
:param verify: (optional) whether the SSL cert will be verified.
:return: :class:`HTTPResponse <HTTPResponse>` object
:rtype: restclient.HTTPResponse
Usage::
>>> from jumpssh import RestSshClient
>>> with RestSshClient(host='gateway.example.com', username='my_user') as rest_client:
>>> ... http_response = rest_client.request('GET', 'http://remote.example.com')
>>> ... http_response.status_code
200
"""
# build curl command
# force usage of http 1.0 as chunked transfer encoding not yet supported here
cmd = 'curl -is --http1.0 '
# disable verification of SSL cert
if not kwargs.get('verify', True):
cmd += "-k "
# only return status code and headers (no body)
if kwargs.get('document_info_only'):
cmd += "-I "
# basic authentication
if kwargs.get('auth'):
auth = kwargs.get('auth')
if len(auth) != 2:
raise exception.RestClientError("Invalid auth parameter. "
"Tuple with 2 elements (user, password) is expected.")
cmd += "-u %s:%s " % (auth[0], auth[1])
# specify http method
cmd += '-X %s ' % method.upper()
# build headers list
if kwargs.get('headers'):
headers = kwargs.get('headers')
for key, value in headers.items():
cmd += '-H "%s:%s" ' % (key, value)
# add targeted uri
cmd += '"%s' % uri
# add uri parameters
if kwargs.get('params'):
params = kwargs.get('params')
if any(params):
cmd += '?'
cmd += '&'.join(['%s=%s' % (quote_plus(key), quote_plus(value)) for key, value in params.items()])
# close quotes around uri with params
cmd += '" '
# build body
if kwargs.get('local_file'):
local_file = kwargs.get('local_file')
if not os.path.exists(local_file) or not os.path.isfile(local_file):
raise exception.RestClientError("Invalid file path given '%s'" % local_file)
sftp_client = self.ssh_session.get_sftp_client()
file_name = os.path.basename(local_file)
sftp_client.put(local_file, file_name)
cmd += '-d @%s ' % file_name
elif kwargs.get('remote_file'):
remote_file = kwargs.get('remote_file')
if not self.ssh_session.exists(remote_file):
raise exception.RestClientError("Invalid remote file path given '%s' on host '%s'"
% (remote_file, self.ssh_session.host))
cmd += '-d @%s ' % remote_file
elif kwargs.get('data'):
data = kwargs.get('data')
cmd += "-d '%s' " % data.replace("'", "\'")
# execute remote http query and get raw response
# do not raise exception if error code different from 0 as some query can be successful with other exit codes
exit_code, output = self.ssh_session.run_cmd(cmd, raise_if_error=False)
# check exit code has a proper value
# most of successful commands will return exit code 0
# except when using HEAD http method as file transfer is shorter or larger than expected
# curl is returning exit code 18 : CURLE_PARTIAL_FILE (18)
if exit_code != 0 and not (exit_code == 18 and method.upper() == 'HEAD'):
raise exception.RestClientError(
'"Remote command ({command})" returned exit status ({exit_code}): {error}'.format(
exit_code=exit_code, command=cmd, error=output)
)
# cleanup file copied once rest query has been done
if kwargs.get('local_file'):
sftp_client.remove(file_name)
# return structured http response
return HTTPResponse(output)
[docs] def get(self, uri, **kwargs):
"""Sends a GET request.
:param uri: URL of the http request.
:param \*\*kwargs: Optional arguments that :func:`~request` takes.
:return: :class:`HTTPResponse <HTTPResponse>` object
:rtype: restclient.HTTPResponse
"""
return self.request('get', uri, **kwargs)
[docs] def options(self, uri, **kwargs):
"""Sends a OPTIONS request.
:param uri: URL of the http request.
:param \*\*kwargs: Optional arguments that :func:`~request` takes.
:return: :class:`HTTPResponse <HTTPResponse>` object
:rtype: restclient.HTTPResponse
"""
return self.request('options', uri, **kwargs)
[docs] def head(self, uri, **kwargs):
"""Sends a HEAD request.
:param uri: URL of the http request.
:param \*\*kwargs: Optional arguments that :func:`~request` takes.
:return: :class:`HTTPResponse <HTTPResponse>` object
:rtype: restclient.HTTPResponse
"""
return self.request('head', uri, **kwargs)
[docs] def post(self, uri, **kwargs):
"""Sends a POST request.
:param uri: URL of the http request.
:param \*\*kwargs: Optional arguments that :func:`~request` takes.
:return: :class:`HTTPResponse <HTTPResponse>` object
:rtype: restclient.HTTPResponse
"""
return self.request('post', uri, **kwargs)
[docs] def put(self, uri, **kwargs):
"""Sends a PUT request.
:param uri: URL of the http request.
:param \*\*kwargs: Optional arguments that :func:`~request` takes.
:return: :class:`HTTPResponse <HTTPResponse>` object
:rtype: restclient.HTTPResponse
"""
return self.request('put', uri, **kwargs)
[docs] def patch(self, uri, **kwargs):
"""Sends a PATCH request.
:param uri: URL of the http request.
:param \*\*kwargs: Optional arguments that :func:`~request` takes.
:return: :class:`HTTPResponse <HTTPResponse>` object
:rtype: restclient.HTTPResponse
"""
return self.request('patch', uri, **kwargs)
[docs] def delete(self, uri, **kwargs):
"""Sends a DELETE request.
:param uri: URL of the http request.
:param \*\*kwargs: Optional arguments that :func:`~request` takes.
:return: :class:`HTTPResponse <HTTPResponse>` object
:rtype: restclient.HTTPResponse
"""
return self.request('delete', uri, **kwargs)
[docs]class HTTPResponse:
def __init__(self, http_response_str):
httplib_HTTPResponse = self.__parse_response(http_response_str)
self.headers = {}
self.status_code = httplib_HTTPResponse.status
self.reason = httplib_HTTPResponse.reason
# to get full response body from httplib, force read of full payload, and manually override length
received_length = httplib_HTTPResponse.length
httplib_HTTPResponse.length = None
self.text = httplib_HTTPResponse.read().strip().decode('utf-8')
httplib_HTTPResponse.length = received_length
for key, value in httplib_HTTPResponse.getheaders():
self.headers[key] = value
def __parse_response(self, str_response):
class FakeSocket(FakeSocketParam):
def makefile(self, *args, **kw):
return self
# remove double '\r' as http.client only searching for the following sequences
# to identify end of headers list : (b'\r\n', b'\n', b'')
# so this is failing when receiving for example b'\r\r\n'
# the change python made to break it in 2.7.13 can be seen here
# https://fossies.org/diffs/Python/2.7.12_vs_2.7.13/Lib/httplib.py-diff.html
raw_response = str_response.replace('\r\r\n', '\r\n').encode('utf-8')
socket = FakeSocket(raw_response)
response = http_HTTPResponse(socket)
response.begin()
return response
[docs] def check_for_success(self):
if self.status_code not in [200, 201]:
raise exception.RestClientError("http error received: %s" % self)
[docs] def is_valid_json_body(self):
try:
json.loads(self.text)
except ValueError:
return False
return True
[docs] def json(self, **kwargs):
try:
return json.loads(self.text, **kwargs)
except ValueError:
raise exception.RestClientError("http response body is not in a valid json format : %s" + self.text)
def __str__(self):
result = "%s %s\n" % (self.status_code, self.reason)
try:
result += json.dumps(json.loads(self.text), indent=4, sort_keys=True)
except ValueError:
result += self.text
return result