blob: 873845b5da91d1beffc4da2221d698338794a08f [file] [log] [blame]
# Copyright 2017 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Utility for accessing web services."""
import json
import logging
import xmlrpclib
# This is a top level helper so it can't use cros.factory.external.
try:
import zeep
import zeep.cache
import zeep.helpers
import zeep.transports
HAVE_ZEEP = True
except ImportError:
HAVE_ZEEP = False
try:
import jsonrpclib
HAVE_JSONRPCLIB = True
except ImportError:
HAVE_JSONRPCLIB = False
try:
from twisted.internet import defer
from twisted.internet import threads
from twisted.web import xmlrpc
HAVE_TWISTED = True
except ImportError:
HAVE_TWISTED = False
try:
from txjsonrpc.web import jsonrpc
HAVE_TXJSONRPC = True
except ImportError:
HAVE_TXJSONRPC = False
# URL prefixes if you want to override RPC protocol.
PREFIX_XMLRPC = 'xmlrpc:'
PREFIX_WSDL = 'wsdl:'
PREFIX_JSONRPC = 'jsonrpc:'
PREFIX_JSON_FILTER = 'json:'
PROTOCOL_PREFIXES = [PREFIX_XMLRPC,
PREFIX_WSDL,
PREFIX_JSONRPC,
PREFIX_JSON_FILTER]
def CheckPackage(url, has_package, package_name):
if not has_package:
raise ImportError(
'The URL %s needs Python package "%s" installed. '
'Please install that by command: "sudo pip install %s"' %
(url, package_name, package_name))
def ParseURL(url):
"""Parses the URL to extract protocol prefixes from real URL."""
protocols = []
while ':' in url:
if any(map(url.startswith, PROTOCOL_PREFIXES)):
protocol, colon, url = url.partition(':')
protocols.append(protocol + colon)
else:
break
return protocols, url
class WebServiceProxy(object):
"""An abstract class for proxy to web services.
Most web services are using HTTP as transport instance, which may cause race
condition (CannotSendRequest) if we try to send new requests before some
previous response was fully processed.
As a result, most WebServiceProxy implementations should try to keep only
URL in constructor, and create the real proxy object when the callRemote is
invoked.
"""
def callRemote(self, method, *args, **kargs):
raise NotImplementedError
def __getattr__(self, name):
def _wrapper(*args, **kargs):
return self.callRemote(name, *args, **kargs)
return _wrapper
class XMLRPCProxy(WebServiceProxy):
"""A proxy for web service implemented in XML-RPC."""
def __init__(self, url):
self._url = url
def callRemote(self, method, *args, **kargs):
proxy = xmlrpclib.ServerProxy(self._url, allow_none=True)
return getattr(proxy, method)(*args, **kargs)
class TwistedXMLRPCProxy(WebServiceProxy):
""" A proxy for web service implemented in XML-RPC, powered by Twisted."""
def __init__(self, url):
CheckPackage(url, HAVE_TWISTED, 'twisted')
self._url = url
def callRemote(self, method, *args, **kargs):
proxy = xmlrpc.Proxy(self._url, allowNone=True)
return proxy.callRemote(method, *args, **kargs)
class JSONRPCProxy(WebServiceProxy):
"""A proxy for web service implemented in JSON-RPC."""
def __init__(self, url):
CheckPackage(url, HAVE_JSONRPCLIB, 'jsonrpclib')
self._url = url
def callRemote(self, method, *args, **kargs):
proxy = jsonrpclib.Server(self._url)
return getattr(proxy, method)(*args, **kargs)
class TXJSONRPCProxy(WebServiceProxy):
def __init__(self, url):
CheckPackage(url, HAVE_TXJSONRPC, 'txJSON-RPC')
self._url = url
def callRemote(self, method, *args, **kargs):
proxy = jsonrpc.Proxy(self._url)
return proxy.callRemote(method, *args, **kargs)
class ZeepProxy(WebServiceProxy):
"""A proxy for web service implemented as Zeep Client (WSDL/SOAP)."""
def __init__(self, url):
CheckPackage(url, HAVE_ZEEP, 'zeep')
self._url = url
# Without cache, zeep will retrieve WSDL on every connection.
self._cache = zeep.cache.InMemoryCache()
def callRemote(self, method, *args, **kargs):
transport = zeep.transports.Transport(cache=self._cache)
proxy = zeep.Client(self._url, transport=transport).service
result = proxy[method](*args, **kargs)
# By default zeep returns collections.OrderedDict.
return zeep.helpers.serialize_object(result, target_cls=dict)
class TwistedProxy(WebServiceProxy):
"""A virtual proxy to turn a proxy into twisted deferred proxy."""
def __init__(self, proxy):
CheckPackage('<unknown>', HAVE_TWISTED, 'twisted')
assert isinstance(proxy, WebServiceProxy)
self._proxy = proxy
def callRemote(self, method, *args, **kargs):
return threads.deferToThread(
self._proxy.callRemote, method, *args, **kargs)
class JSONProxyFilter(WebServiceProxy):
"""A proxy that converts input and output for chained proxies."""
def __init__(self, proxy):
assert isinstance(proxy, WebServiceProxy)
self._proxy = proxy
def callRemote(self, method, *args, **kargs):
if kargs:
raise TypeError(
'Keyword arguments (%r) not allowed for web service method: %s.' %
(kargs, method))
result = self._proxy.callRemote(method, *map(json.dumps, args))
if HAVE_TWISTED and isinstance(result, defer.Deferred):
return result.addCallback(json.loads)
return json.loads(result)
def CreateWebServiceProxy(url, use_twisted=False):
"""Returns a web service proxy that will work against specified URL.
The URL should be a reference to web service, with optional prefixes:
- json: Translate input and output into JSON string.
- wsdl: Assume the URL itself returns a WSDL document.
- jsonrpc: Assume the URL refers to a JSON-RPC server.
- xmlrpc: Assume the URL refers to a XML-RPC server.
URLs ends with 'wsdl' will be recognized as WSDL service as well.
For example,
- ``json:http://10.3.0.11/proxy`` will connect to a XML-RPC service that both
arguments and return values should be JSON simple strings.
- ``http://10.3.0.11/?wsdl' will connect to a WSDL (usually SOAP) service.
The input and output must be encoded as generic compound types.
- ``json:wsdl:http://10.3.0.11' will connect to a WSDL service, and encode
the argument and return values as JSON simple strings.
Args:
url: A string to web service URL.
use_twisted: Set to True to create Twisted-friendly proxies.
"""
enable_json_filter = False
force_wsdl = False
force_jsonrpc = False
force_xmlrpc = False
protocols, url = ParseURL(url)
if PREFIX_XMLRPC in protocols:
force_xmlrpc = True
if PREFIX_JSONRPC in protocols:
force_jsonrpc = True
if PREFIX_WSDL in protocols:
force_wsdl = True
if PREFIX_JSON_FILTER in protocols:
enable_json_filter = True
forced = [force_wsdl, force_jsonrpc, force_xmlrpc]
if forced.count(True) > 1:
raise ValueError('URL %s has too many protocol identifiers.' % url)
if not any(forced) and url.lower().endswith('wsdl'):
force_wsdl = True
# Now, start building the proxy or filters.
if force_wsdl:
proxy = ZeepProxy(url)
if use_twisted:
proxy = TwistedProxy(proxy)
elif force_jsonrpc:
proxy = TXJSONRPCProxy(url) if use_twisted else JSONRPCProxy(url)
else:
proxy = TwistedXMLRPCProxy(url) if use_twisted else XMLRPCProxy(url)
if enable_json_filter:
logging.info('Enabled JSON input/output filter for web service: %s', url)
proxy = JSONProxyFilter(proxy)
return proxy