Files
entropy/libraries/entropy/services/client.py
T

1049 lines
40 KiB
Python

# -*- coding: utf-8 -*-
"""
@author: Fabio Erculiani <lxnay@sabayon.org>
@contact: lxnay@sabayon.org
@copyright: Fabio Erculiani
@license: GPL-2
B{Entropy Base Repository Web Services client interface}.
"""
__all__ = ["WebServiceFactory", "WebService"]
import sys
import os
import errno
import json
import tempfile
import threading
import hashlib
import urllib
import socket
if sys.hexversion >= 0x3000000:
import http.client as httplib
from io import StringIO
else:
import httplib
from cStringIO import StringIO
import entropy.dump
from entropy.core import Singleton
from entropy.cache import EntropyCacher
from entropy.const import const_debug_write, const_setup_file, etpConst, \
const_convert_to_rawstring, const_isunicode, const_isstring, \
const_convert_to_unicode, const_isstring, const_debug_enabled
from entropy.exceptions import EntropyException
import entropy.tools
import entropy.dep
class WebServiceFactory(object):
"""
Base Entropy Repository Web Services Factory. Generates
WebService objects that can be used to communicate with the established
web service.
This is a base class and subclasses should be preferred (example:
entropy.client.services.interfaces.ClientWebServiceFactory)
"""
class InvalidWebServiceFactory(EntropyException):
"""
Raised when an invalid WebService based class is passed.
"""
def __init__(self, web_service_class, entropy_client):
"""
WebServiceFactory constructor.
@param entropy_client: Entropy Client interface
@type entropy_client: entropy.client.interfaces.client.Client
"""
object.__init__(self)
if not issubclass(web_service_class, WebService):
raise InvalidWebServiceFactory("invalid web_service_class")
self._entropy = entropy_client
self._service_class = web_service_class
def new(self, repository_id):
"""
Return a new WebService object for given repository identifier.
@param repository_id: repository identifier
@rtype repository_id: string
@raise WebService.UnsupportedService: if web service is
explicitly unsupported by repository
"""
return self._service_class(self._entropy, repository_id)
class WebService(object):
"""
This is the Entropy Repository Web Services that proxies requests over
an Web Services answering to HTTP POST requests
(either over HTTP or over HTTPS) in the following form:
Given that repositories must ship with a file (in their repository
meta file "packages.db.meta", coming from the server-side repository
directory) called "packages.db.webservices"
(etpConst['etpdatabasewebservicesfile']) containing the HTTP POST base
URL (example: http://packages.sabayon.org/api).
The POST url is composed as follows:
<base_url>/<method name>
Function arguments are then appended in JSON format, so that float, int,
strings and lists are correctly represented.
So, for example, if WebService exposes a method with the following
signature (with the base URL in example above):
float get_vote(string package_name)
The URL referenced will be:
http://packages.sabayon.org/api/get_vote
And the JSON dictionary will contain a key called "package_name" with
package_name value.
For methods requiring authentication, the JSON object will contain
"username" and "password" fields (clear text, so make sure to use HTTPS).
The Response depends on each specific method and it is given in JSON format
too, that is afterwards interpreted by the caller function, that will
always return the expected data format (see respective API documentation).
For more information about how to implement the Web Service, please see
the packages.git Sabayon repository, which contains a Pylons MVC web app.
In general, every JSON response must provide a 'code' field, representing
an HTTP-response alike return code (200 is ok, 500 is server error, 400 is
bad request, etc) and a 'message' field, containing the error message (if
no error, 'message' is usually empty). The RPC result is put inside the
'r' field.
This is a base class, and you should really implement a subclass providing
your own API methods, and use _method_getter().
"""
# Supported communcation protocols
SUPPORTED_URL_SCHEMAS = ("http", "https")
# package icon metadata identifier
PKG_ICON_IDENTIFIER = "__icon__"
# Currently supported Web Service API level
# an API level defines a set of available remote calls and their data
# structure
SUPPORTED_API_LEVEL = 1
# Default common Web Service responses, please use these when
# implementing your web service
WEB_SERVICE_RESPONSE_CODE_OK = 200
WEB_SERVICE_INVALID_CREDENTIALS_CODE = 450
WEB_SERVICE_INVALID_REQUEST_CODE = 400
WEB_SERVICE_NOT_FOUND_CODE = 404
WEB_SERVICE_RESPONSE_ERROR_CODE = 503
class WebServiceException(EntropyException):
"""
Base WebService exception class.
"""
def __init__(self, value, method = None, message = None):
self.value = value
self.method = method
self.message = message
Exception.__init__(self)
def __get_method(self):
if self.method is None:
method = const_convert_to_unicode("")
else:
method = const_convert_to_unicode(self.method)
return method
def __get_message(self):
if self.message is None:
message = const_convert_to_unicode("")
else:
message = const_convert_to_unicode(self.message)
return message
def __unicode__(self):
method = self.__get_method()
message = self.__get_message()
if const_isstring(self.value):
return const_convert_to_unicode(method + " " + self.value) \
+ ", " + message
return const_convert_to_unicode(method + " " + repr(self.value)) \
+ ", " + message
def __str__(self):
method = self.__get_method()
message = self.__get_message()
if const_isstring(self.value):
return method + " " + self.value + ", " + message
return method + " " + repr(self.value) + ", " + message
class UnsupportedService(WebServiceException):
"""
Raised when Repository doesn't seem to support any Web Service
feature.
"""
class UnsupportedParameters(WebServiceException):
"""
Raised when input parameters cannot be converted to JSON.
Probably due to invalid input data.
"""
class RequestError(WebServiceException):
"""
If the request cannot be satisfied by the remote web service.
"""
class AuthenticationRequired(WebServiceException):
"""
When a method requiring valid user credentials is called without
being logged in.
"""
class AuthenticationFailed(WebServiceException):
"""
When credentials are stored locally but don't seem to work against
the Web Service.
"""
class MethodNotAvailable(WebServiceException):
"""
When calling a remote method that is not available.
"""
class MalformedResponse(WebServiceException):
"""
If JSON response cannot be converted back to dict.
"""
class UnsupportedAPILevel(WebServiceException):
"""
If this client and the Web Service expose a different API level.
"""
class MethodResponseError(WebServiceException):
"""
If the request has been accepted, but its computation stopped for
some reason. The encapsulated data contains the error code.
"""
class CacheMiss(WebServiceException):
"""
If the request is not available in the on-disk cache.
"""
def __init__(self, entropy_client, repository_id):
"""
WebService constructor.
@param entropy_client: Entropy Client interface
@type entropy_client: entropy.client.interfaces.client.Client
@param repository_id: repository identifier
@rtype repository_id: string
"""
self._cache_dir_lock = threading.RLock()
self._transfer_callback = None
self._entropy = entropy_client
self._repository_id = repository_id
self.__auth_storage = None
self.__settings = None
self.__cacher = None
# some data requires huge timeout (videos)
self._default_timeout_secs = 300.0
self.__credentials_validated = False
# check availability
_repository_data = self._settings['repositories']['available'].get(
self._repository_id)
if _repository_data is None:
raise WebService.UnsupportedService("unsupported")
self._repository_data = _repository_data
web_services_conf = self._repository_data['webservices_config']
# mainly for debugging purposes, hidden and undocumented
override_request_url = os.getenv("ETP_OVERRIDE_REQUEST_URL")
if override_request_url is not None:
_remote_url = override_request_url
else:
try:
with open(web_services_conf, "r") as web_f:
# currently, in this file there is only the remote base URL
_remote_url = web_f.readline().strip()
except (OSError, IOError) as err:
const_debug_write(__name__, "WebService.__init__ error: %s" % (
err,))
raise WebService.UnsupportedService(err)
url_obj = entropy.tools.spliturl(_remote_url)
if url_obj.scheme not in WebService.SUPPORTED_URL_SCHEMAS:
raise WebService.UnsupportedService("unsupported url")
self._request_url = _remote_url
self._request_protocol = url_obj.scheme
self._request_host = url_obj.netloc
self._request_path = url_obj.path
const_debug_write(__name__, "WebService loaded, url: %s" % (
self._request_url,))
def _set_timeout(self, secs):
"""
Override default timeout setting a new one (in seconds).
"""
self._default_timeout_secs = int(secs)
def _set_transfer_callback(self, callback):
"""
Set a transfer progress callback function.
@param transfer_callback: this callback function can be used to
show a progress status to user, if passed, it must be a function
accepting 3 input parameters: (int transfered, int total,
bool download). The last parameter is True, when progress is about
download, False if upload. If no transfer information is declared,
total might be -1.
@param transfer_callback: callable
"""
self._transfer_callback = callback
@property
def _settings(self):
"""
Get SystemSettings instance
"""
if self.__settings is None:
self.__settings = self._entropy.Settings()
return self.__settings
@property
def _arch(self):
"""
Get currently running Entropy architecture
"""
return self._settings['repositories']['arch']
@property
def _product(self):
"""
Get currently running Entropy product
"""
return self._settings['repositories']['product']
@property
def _branch(self):
"""
Get currently running Entropy branch
"""
return self._settings['repositories']['branch']
@property
def _authstore(self):
"""
Repository authentication configuration storage interface.
Makes possible to retrieve on-disk stored user credentials.
"""
if self.__auth_storage is None:
self.__auth_storage = AuthenticationStorage()
return self.__auth_storage
@property
def _cacher(self):
if self.__cacher is None:
self.__cacher = EntropyCacher()
return self.__cacher
def _generate_user_agent(self, function_name):
"""
Generate a standard (entropy services centric) HTTP User Agent.
"""
uname = os.uname()
user_agent = "Entropy.Services/%s (compatible; %s; %s: %s %s %s)" % (
etpConst['entropyversion'],
"Entropy",
function_name,
uname[0],
uname[4],
uname[2],
)
return user_agent
def _encode_multipart_form(self, params, file_params, boundary):
"""
Encode parameters and files into a valid HTTP multipart form data.
NOTE: this method loads the whole file in RAM, HTTP post doesn't work
well for big files anyway.
"""
def _cast_to_str(value):
if value is None:
return const_convert_to_rawstring("")
elif isinstance(value, (int, float, long)):
return const_convert_to_rawstring(value)
elif isinstance(value, (list, tuple)):
return repr(value)
return value
tmp_fd, tmp_path = tempfile.mkstemp()
tmp_f = os.fdopen(tmp_fd, "ab+")
tmp_f.truncate(0)
crlf = '\r\n'
for key, value in params.items():
tmp_f.write("--" + boundary + crlf)
tmp_f.write("Content-Disposition: form-data; name=\"%s\"" % (
key,))
tmp_f.write(crlf + crlf + _cast_to_str(value) + crlf)
for key, (f_name, f_obj) in file_params.items():
tmp_f.write("--" + boundary + crlf)
tmp_f.write(
"Content-Disposition: form-data; name=\"%s\"; filename=\"%s\"" % (
key, f_name,))
tmp_f.write(crlf)
tmp_f.write("Content-Type: application/octet-stream" + crlf)
tmp_f.write("Content-Transfer-Encoding: binary" + crlf + crlf)
f_obj.seek(0)
while True:
chunk = f_obj.read(65536)
if not chunk:
break
tmp_f.write(chunk)
tmp_f.write(crlf)
tmp_f.write("--" + boundary + "--" + crlf + crlf)
tmp_f.flush()
return tmp_f, tmp_path
def _generic_post_handler(self, function_name, params, file_params):
"""
Given a function name and the request data (dict format), do the actual
HTTP request and return the response object to caller.
WARNING: params and file_params dict keys must be ASCII string only.
@param function_name: name of the function that called this method
@type function_name: string
@param params: POST parameters
@type params: dict
@param file_params: mapping composed by file names as key and tuple
composed by (file_name, file object) as values
@type file_params: dict
@return: tuple composed by the server response string or None
(in case of empty response) and the HTTPResponse object (useful
for checking response status)
@rtype: tuple
"""
multipart_boundary = "---entropy.services,boundary---"
request_path = self._request_path.rstrip("/") + "/" + function_name
const_debug_write(__name__,
"WebService _generic_post_handler, calling: %s at %s -- %s,"
" tx_callback: %s" % (self._request_host, request_path,
params, self._transfer_callback,))
connection = None
try:
if self._request_protocol == "http":
connection = httplib.HTTPConnection(self._request_host,
timeout = self._default_timeout_secs)
elif self._request_protocol == "https":
connection = httplib.HTTPSConnection(self._request_host,
timeout = self._default_timeout_secs)
else:
raise WebService.RequestError("invalid request protocol",
method = function_name)
headers = {
"Accept": "text/plain",
"User-Agent": self._generate_user_agent(function_name),
}
if file_params is None:
file_params = {}
# autodetect file parameters in params
for k in list(params.keys()):
if isinstance(params[k], (tuple, list)) \
and (len(params[k]) == 2):
f_name, f_obj = params[k]
if isinstance(f_obj, file):
file_params[k] = params[k]
del params[k]
elif const_isunicode(params[k]):
# convert to raw string
params[k] = const_convert_to_rawstring(params[k],
from_enctype = "utf-8")
elif not const_isstring(params[k]):
# invalid ?
if params[k] is None:
# will be converted to ""
continue
supported_types = (float, long, int, list, tuple)
if not isinstance(params[k], supported_types):
raise WebService.UnsupportedParameters(
"%s is unsupported type %s" % (k, type(params[k])))
list_types = (list, tuple)
if isinstance(params[k], list_types):
# not supporting nested lists
non_str = [x for x in params[k] if not \
const_isstring(x)]
if non_str:
raise WebService.UnsupportedParameters(
"%s is unsupported type %s" % (k,
type(params[k])))
body = None
if not file_params:
headers["Content-Type"] = "application/x-www-form-urlencoded"
encoded_params = urllib.urlencode(params)
data_size = len(encoded_params)
if self._transfer_callback is not None:
self._transfer_callback(0, data_size, False)
if data_size < 65536:
try:
connection.request("POST", request_path, encoded_params,
headers)
except socket.error as err:
raise WebService.RequestError(err,
method = function_name)
else:
try:
connection.request("POST", request_path, None, headers)
except socket.error as err:
raise WebService.RequestError(err,
method = function_name)
sio = StringIO(encoded_params)
data_size = len(encoded_params)
while True:
chunk = sio.read(65535)
if not chunk:
break
try:
connection.send(chunk)
except socket.error as err:
raise WebService.RequestError(err,
method = function_name)
if self._transfer_callback is not None:
self._transfer_callback(sio.tell(),
data_size, False)
# for both ways, send a signal through the callback
if self._transfer_callback is not None:
self._transfer_callback(data_size, data_size, False)
else:
headers["Content-Type"] = "multipart/form-data; boundary=" + \
multipart_boundary
body_file, body_fpath = self._encode_multipart_form(params,
file_params, multipart_boundary)
try:
data_size = body_file.tell()
headers["Content-Length"] = str(data_size)
body_file.seek(0)
if self._transfer_callback is not None:
self._transfer_callback(0, data_size, False)
try:
connection.request("POST", request_path, None, headers)
except socket.error as err:
raise WebService.RequestError(err,
method = function_name)
while True:
chunk = body_file.read(65535)
if not chunk:
break
try:
connection.send(chunk)
except socket.error as err:
raise WebService.RequestError(err,
method = function_name)
if self._transfer_callback is not None:
self._transfer_callback(body_file.tell(),
data_size, False)
if self._transfer_callback is not None:
self._transfer_callback(data_size, data_size, False)
finally:
body_file.close()
os.remove(body_fpath)
try:
response = connection.getresponse()
except socket.error as err:
raise WebService.RequestError(err,
method = function_name)
const_debug_write(__name__, "WebService.%s(%s), "
"response header: %s" % (
function_name, params, response.getheaders(),))
total_length = response.getheader("Content-Length", "-1")
try:
total_length = int(total_length)
except ValueError:
total_length = -1
outcome = ""
current_len = 0
if self._transfer_callback is not None:
self._transfer_callback(current_len, total_length, True)
while True:
chunk = response.read(65536)
if not chunk:
break
outcome += chunk
current_len += len(chunk)
if self._transfer_callback is not None:
self._transfer_callback(current_len, total_length, True)
if self._transfer_callback is not None:
self._transfer_callback(total_length, total_length, True)
if not outcome:
return None, response
return outcome, response
except httplib.HTTPException as err:
raise WebService.RequestError(err,
method = function_name)
finally:
if connection is not None:
connection.close()
def _setup_credentials(self, request_params):
"""
This method is automatically called by public API functions to setup
credentials data if available, otherwise user interaction will be
triggered by raising WebService.AuthenticationRequired
"""
creds = self._authstore.get(self._repository_id)
if creds is None:
raise WebService.AuthenticationRequired(self._repository_id)
username, password = creds
request_params['username'], request_params['password'] = \
username, password
def _setup_generic_params(self, request_params):
"""
This methods adds some generic parameters to the HTTP request metadata.
Any parameter added by this method is prefixed with __, to avoid
name collisions.
"""
request_params["__repository_id__"] = self._repository_id
def add_credentials(self, username, password):
"""
Add credentials for this repository and store the information into
an user-protected location.
"""
self.__credentials_validated = False
self._authstore.add(self._repository_id, username, password)
self._authstore.save()
def validate_credentials(self):
"""
Validate currently stored credentials (if available) against the
remote service. If credentials are not available,
WebService.AuthenticationRequired is raised.
If credentials are not valid, WebService.AuthenticationFailed is
raised.
@raise WebService.AuthenticationRequired: if credentials are not
available
@raise WebService.AuthenticationFailed: if credentials are not valid
"""
if not self.credentials_available():
raise WebService.AuthenticationRequired("credentials not available")
if not self.__credentials_validated:
# this will raise WebService.AuthenticationFailed if credentials
# are invalid
self._method_getter("validate_credentials", {}, cache = False,
require_credentials = True)
self.__credentials_validated = True
def credentials_available(self):
"""
Return whether credentials are stored locally or not.
Please note that credentials can be stored properly but considered
invalid remotely.
@return: True, if credentials are available
@rtype: bool
"""
return self._authstore.get(self._repository_id) is not None
def get_credentials(self):
"""
Return the username string stored in the authentication storage, if any.
Otherwise return None.
@return: the username string stored in the authentication storage
@rtype: string or None
"""
creds = self._authstore.get(self._repository_id)
if creds is not None:
username, _pass = creds
return username
def remove_credentials(self):
"""
Remove any credential bound to the repository from on-disk storage.
@return: True, if credentials existed and got removed
@rtype: bool
"""
self.__credentials_validated = False
res = self._authstore.remove(self._repository_id)
self._authstore.save()
return res
CACHE_DIR = os.path.join(etpConst['entropyworkdir'], "websrv_cache")
def _get_cache_key(self, method, params):
"""
Return on disk cache file name as key, given a method name and its
parameters.
"""
sorted_data = [(x, params[x]) for x in sorted(params.keys())]
hash_str = repr(sorted_data) + ", " + self._request_url
if sys.hexversion >= 0x3000000:
hash_str = hash_str.encode("utf-8")
sha = hashlib.sha1()
sha.update(hash_str)
return method + "_" + sha.hexdigest()
def _get_cached(self, cache_key):
"""
Return an on-disk cached object for given cache key.
"""
with self._cache_dir_lock:
return self._cacher.pop(cache_key, cache_dir = WebService.CACHE_DIR)
def _set_cached(self, cache_key, data):
"""
Save a cache item to disk.
"""
with self._cache_dir_lock:
return self._cacher.save(cache_key, data,
cache_dir = WebService.CACHE_DIR)
def _drop_cached(self, method):
"""
Drop all on-disk cache for given method.
"""
with self._cache_dir_lock:
cache_dir = WebService.CACHE_DIR
for currentdir, subdirs, files in os.walk(cache_dir):
hostile_files = [os.path.join(currentdir, x) for x in \
files if x.startswith(method + "_")]
for path in hostile_files:
try:
os.remove(path)
except OSError as err:
# avoid race conditions
if err.errno != errno.ENOENT:
raise
def _method_cached(self, func_name, params, cache_key = None):
"""
Try to fetch on-disk cached object and return it. If error or not
found, None is returned.
"""
# setup generic request parameters
self._setup_generic_params(params)
if cache_key is None:
cache_key = self._get_cache_key(func_name, params)
return self._get_cached(cache_key)
def _method_getter(self, func_name, params, cache = True,
cached = False, require_credentials = False, file_params = None):
"""
Given a function name and request parameters, do all the duties required
to get a response from the Web Service. This method raises several
exceptions, that have to be advertised on public methods as well.
@param func_name: API function name
@type func_name: string
@param params: dictionary object that will be converted into a JSON
request string
@type params: dict
@keyword cache: True means use on-disk cache if available?
@type cache: bool
@keyword cached: if True, it will only use the on-disk cached call
result and raise WebService.CacheMiss if not found.
@type cached: bool
@keyword require_credentials: True means that credentials will be added
to the request, if credentials are not available in the local
authentication storage, WebService.AuthenticationRequired is
raised
@type require_credentials: bool
@param file_params: mapping composed by file names as key and tuple
composed by (file_name, file object) as values
@type file_params: dict
@return: the JSON response (dict format)
@rtype: dict
@raise WebService.UnsupportedParameters: if input parameters are invalid
@raise WebService.RequestError: if request cannot be satisfied
@raise WebService.MethodNotAvailable: if API method is not available
remotely and an error occured (error code passed as exception
argument)
@raise WebService.AuthenticationRequired: if require_credentials is True
and credentials are required.
@raise WebService.AuthenticationFailed: if credentials are not valid
@raise WebService.MalformedResponse: if JSON response cannot be
converted back to dict.
@raise WebService.UnsupportedAPILevel: if client API and Web Service
API do not match
@raise WebService.MethodResponseError; if method execution failed
@raise WebService.CacheMiss: if cached=True and cached object is not
available
"""
cache_key = self._get_cache_key(func_name, params)
if cache or cached:
# this does call: _setup_generic_params()
obj = self._method_cached(func_name, params, cache_key = cache_key)
if (obj is None) and cached:
raise WebService.CacheMiss(
WebService.WEB_SERVICE_NOT_FOUND_CODE, method = func_name)
if obj is not None:
if const_debug_enabled():
const_debug_write(__name__,
"WebService.%s(%s) = CACHED!" % (
func_name, params,))
return obj
if const_debug_enabled():
const_debug_write(__name__, "WebService.%s(%s) = NOT cached" % (
func_name, params,))
else:
self._setup_generic_params(params)
if require_credentials:
# this can raise AuthenticationRequired
self._setup_credentials(params)
obj = None
try:
json_response, response = self._generic_post_handler(func_name,
params, file_params)
http_status = response.status
if http_status not in (httplib.OK,):
raise WebService.MethodNotAvailable(http_status,
method = func_name)
# try to convert the JSON response
try:
data = json.loads(json_response)
except (ValueError, TypeError) as err:
raise WebService.MalformedResponse(err,
method = func_name)
# check API
if data.get("api_rev") != WebService.SUPPORTED_API_LEVEL:
raise WebService.UnsupportedAPILevel(data['api_rev'],
method = func_name, message = data.get("message"))
code = data.get("code", -1)
if code == WebService.WEB_SERVICE_INVALID_CREDENTIALS_CODE:
# invalid credentials, ask again login data
raise WebService.AuthenticationFailed(code,
method = func_name, message = data.get("message"))
if code != WebService.WEB_SERVICE_RESPONSE_CODE_OK:
raise WebService.MethodResponseError(code,
method = func_name, message = data.get("message"))
if "r" not in data:
raise WebService.MalformedResponse("r not found",
method = func_name, message = data.get("message"))
obj = data["r"]
if const_debug_enabled():
const_debug_write(__name__, "WebService.%s(%s) = fetched!" % (
func_name, params,))
return obj
finally:
if obj is not None:
# store cache
self._set_cached(cache_key, obj)
def service_available(self, cache = True, cached = False):
"""
Return whether the Web Service is correctly able to answer our requests.
@keyword cache: True means use on-disk cache if available?
@type cache: bool
@keyword cached: if True, it will only use the on-disk cached call
result and raise WebService.CacheMiss if not found.
@type cached: bool
@return: True, if service is available
@rtype: bool
@raise WebService.UnsupportedParameters: if input parameters are invalid
@raise WebService.RequestError: if request cannot be satisfied
@raise WebService.MethodNotAvailable: if API method is not available
remotely and an error occured (error code passed as exception
argument)
@raise WebService.AuthenticationRequired: if require_credentials is True
and credentials are required.
@raise WebService.AuthenticationFailed: if credentials are not valid
@raise WebService.MalformedResponse: if JSON response cannot be
converted back to dict.
@raise WebService.UnsupportedAPILevel: if client API and Web Service
API do not match
@raise WebService.MethodResponseError; if method execution failed
@raise WebService.CacheMiss: if cached=True and cached object is not
available
"""
params = locals().copy()
params.pop("self")
params.pop("cache")
return self._method_getter("service_available", params, cache = cache,
cached = cached, require_credentials = False)
def data_send_available(self):
"""
Return whether data send is correctly working. A temporary file with
random content is sent to the service, that would need to calculate
its md5 hash. For security reason, data will be accepted remotely if,
and only if its size is < 256 bytes.
"""
md5 = hashlib.md5()
test_str = const_convert_to_rawstring("")
for x in range(256):
test_str += chr(x)
md5.update(test_str)
expected_hash = md5.hexdigest()
func_name = "data_send_available"
tmp_fd, tmp_path = tempfile.mkstemp()
try:
with os.fdopen(tmp_fd, "ab+") as tmp_f:
tmp_f.write(test_str)
tmp_f.seek(0)
params = {
"test_param": "hello",
}
file_params = {
"test_file": ("test_file.txt", tmp_f),
}
remote_hash = self._method_getter(func_name, params,
cache = False, require_credentials = False,
file_params = file_params)
finally:
os.remove(tmp_path)
const_debug_write(__name__,
"WebService.%s, expected: %s, got: %s" % (
func_name, repr(expected_hash), repr(remote_hash),))
return expected_hash == remote_hash
class AuthenticationStorage(Singleton):
"""
Entropy Web Service authentication credentials storage class.
"""
_AUTH_FILE = ".entropy/id_entropy"
def init_singleton(self):
self.__dump_lock = threading.Lock()
# not loaded, load at very last moment
self.__store = None
def _get_authfile(self):
"""
Try to get the auth file. If it fails, return None.
"""
# setup auth file path
home = os.getenv("HOME")
auth_file = None
if home is not None:
if os.path.isdir(home) and os.access(home, os.W_OK):
auth_file = os.path.join(home,
AuthenticationStorage._AUTH_FILE)
auth_dir = os.path.dirname(auth_file)
if not os.path.isdir(auth_dir):
try:
os.makedirs(auth_dir, 0o700)
const_setup_file(auth_dir, etpConst['entropygid'],
0o700)
except (OSError, IOError):
# ouch, no permissions
auth_file = None
return auth_file
@property
def _authstore(self):
"""
Authentication data object automatically loaded from disk if needed.
"""
if self.__store is None:
auth_file = self._get_authfile()
store = {}
if auth_file is not None:
store = entropy.dump.loadobj(auth_file, complete_path = True)
if store is None:
store = {}
elif not isinstance(store, dict):
store = {}
self.__store = store
return self.__store
def save(self):
"""
Save currently loaded authentication configuration to disk.
@return: True, if save was effectively run
@rtype: bool
"""
with self.__dump_lock:
auth_file = self._get_authfile()
if auth_file is not None:
entropy.dump.dumpobj(auth_file, self._authstore,
complete_path = True, custom_permissions = 0o600)
# make sure
if auth_file is not None:
try:
const_setup_file(auth_file, etpConst['entropygid'],
0o600)
return True
except (OSError, IOError):
return False
def add(self, repository_id, username, password):
"""
Add authentication credentials to Authentication configuration.
@param repository_id: repository identifier
@type repository_id: string
@param username: the username
@type username: string
@param password: the password
@type password: string
"""
self._authstore[repository_id] = {
'username': username,
'password': password,
}
def remove(self, repository_id):
"""
Remove any credential for given repository identifier.
@param repository_id: repository identifier
@type repository_id: string
@return: True, if removal went fine (if there was something to remove)
@rtype: bool
"""
try:
del self._authstore[repository_id]
return True
except KeyError:
return False
def get(self, repository_id):
"""
Get authentication credentials for given repository identifier.
@return: tuple composed by username, password, or None, if credentials
are not found
@rtype: tuple or None
"""
data = self._authstore.get(repository_id)
if data is None:
return None
return data['username'], data['password']