PageRenderTime 33ms CodeModel.GetById 15ms RepoModel.GetById 0ms app.codeStats 1ms

/tests/web/jsonrpclib.py

https://bitbucket.org/prologic/circuits/
Python | 454 lines | 319 code | 56 blank | 79 comment | 18 complexity | caa317051c53564764f7608a3de1d17a MD5 | raw file
  1. # a port of xmlrpclib to json....
  2. #
  3. #
  4. # The JSON-RPC client interface is based on the XML-RPC client
  5. #
  6. # Copyright (c) 1999-2002 by Secret Labs AB
  7. # Copyright (c) 1999-2002 by Fredrik Lundh
  8. # Copyright (c) 2006 by Matt Harrison
  9. #
  10. # By obtaining, using, and/or copying this software and/or its
  11. # associated documentation, you agree that you have read, understood,
  12. # and will comply with the following terms and conditions:
  13. #
  14. # Permission to use, copy, modify, and distribute this software and
  15. # its associated documentation for any purpose and without fee is
  16. # hereby granted, provided that the above copyright notice appears in
  17. # all copies, and that both that copyright notice and this permission
  18. # notice appear in supporting documentation, and that the name of
  19. # Secret Labs AB or the author not be used in advertising or publicity
  20. # pertaining to distribution of the software without specific, written
  21. # prior permission.
  22. #
  23. # SECRET LABS AB AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD
  24. # TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANT-
  25. # ABILITY AND FITNESS. IN NO EVENT SHALL SECRET LABS AB OR THE AUTHOR
  26. # BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY
  27. # DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
  28. # WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS
  29. # ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE
  30. # OF THIS SOFTWARE.
  31. # --------------------------------------------------------------------
  32. import sys
  33. import json
  34. import base64
  35. PY3 = sys.version_info[0] == 3
  36. try:
  37. from http.client import HTTPConnection
  38. from http.client import HTTPSConnection
  39. except ImportError:
  40. from httplib import HTTP as HTTPConnection # NOQA
  41. from httplib import HTTPS as HTTPSConnection # NOQA
  42. try:
  43. from urllib.parse import unquote
  44. from urllib.parse import splithost, splittype, splituser
  45. except ImportError:
  46. from urllib import unquote # NOQA
  47. from urllib import splithost, splittype, splituser # NOQA
  48. __version__ = "0.0.1"
  49. ID = 1
  50. def _gen_id():
  51. global ID
  52. ID = ID + 1
  53. return ID
  54. # --------------------------------------------------------------------
  55. # Exceptions
  56. ##
  57. # Base class for all kinds of client-side errors.
  58. class Error(Exception):
  59. """Base class for client errors."""
  60. def __str__(self):
  61. return repr(self)
  62. ##
  63. # Indicates an HTTP-level protocol error. This is raised by the HTTP
  64. # transport layer, if the server returns an error code other than 200
  65. # (OK).
  66. #
  67. # @param url The target URL.
  68. # @param errcode The HTTP error code.
  69. # @param errmsg The HTTP error message.
  70. # @param headers The HTTP header dictionary.
  71. class ProtocolError(Error):
  72. """Indicates an HTTP protocol error."""
  73. def __init__(self, url, errcode, errmsg, headers, response):
  74. Error.__init__(self)
  75. self.url = url
  76. self.errcode = errcode
  77. self.errmsg = errmsg
  78. self.headers = headers
  79. self.response = response
  80. def __repr__(self):
  81. return (
  82. "<ProtocolError for %s: %s %s>" %
  83. (self.url, self.errcode, self.errmsg)
  84. )
  85. def getparser(encoding):
  86. un = Unmarshaller(encoding)
  87. par = Parser(un)
  88. return par, un
  89. def dumps(params, methodname=None, methodresponse=None, encoding=None,
  90. allow_none=0):
  91. if methodname:
  92. request = {}
  93. request["method"] = methodname
  94. request["params"] = params
  95. request["id"] = _gen_id()
  96. return json.dumps(request)
  97. class Unmarshaller(object):
  98. def __init__(self, encoding):
  99. self.data = None
  100. self.encoding = encoding
  101. def feed(self, data):
  102. if self.data is None:
  103. self.data = data
  104. else:
  105. self.data = self.data + data
  106. def close(self):
  107. #try to convert string to json
  108. return json.loads(self.data.decode(self.encoding))
  109. class Parser(object):
  110. def __init__(self, unmarshaller):
  111. self._target = unmarshaller
  112. self.data = None
  113. def feed(self, data):
  114. if self.data is None:
  115. self.data = data
  116. else:
  117. self.data = self.data + data
  118. def close(self):
  119. self._target.feed(self.data)
  120. class _Method(object):
  121. # some magic to bind an JSON-RPC method to an RPC server.
  122. # supports "nested" methods (e.g. examples.getStateName)
  123. def __init__(self, send, name):
  124. self.__send = send
  125. self.__name = name
  126. def __getattr__(self, name):
  127. return _Method(self.__send, "%s.%s" % (self.__name, name))
  128. def __call__(self, *args):
  129. return self.__send(self.__name, args)
  130. ##
  131. # Standard transport class for JSON-RPC over HTTP.
  132. # <p>
  133. # You can create custom transports by subclassing this method, and
  134. # overriding selected methods.
  135. class Transport:
  136. """Handles an HTTP transaction to an JSON-RPC server."""
  137. # client identifier (may be overridden)
  138. user_agent = "jsonlib.py/%s (by matt harrison)" % __version__
  139. ##
  140. # Send a complete request, and parse the response.
  141. #
  142. # @param host Target host.
  143. # @param handler Target PRC handler.
  144. # @param request_body JSON-RPC request body.
  145. # @param verbose Debugging flag.
  146. # @return Parsed response.
  147. def request(self, host, handler, request_body, encoding, verbose=0):
  148. # issue JSON-RPC request
  149. h = self.make_connection(host)
  150. if verbose:
  151. h.set_debuglevel(1)
  152. self.send_request(h, handler, request_body)
  153. if not PY3:
  154. self.send_host(h, host)
  155. self.send_user_agent(h)
  156. self.send_content(h, request_body)
  157. try:
  158. errcode, errmsg, headers = h.getreply()
  159. r = h.getfile()
  160. except AttributeError:
  161. r = h.getresponse()
  162. errcode = r.status
  163. errmsg = r.reason
  164. headers = r.getheaders()
  165. if errcode != 200:
  166. response = r.read()
  167. raise ProtocolError(
  168. host + handler,
  169. errcode, errmsg,
  170. headers,
  171. response
  172. )
  173. self.verbose = verbose
  174. try:
  175. sock = h._conn.sock
  176. except AttributeError:
  177. sock = None
  178. return self._parse_response(r, sock, encoding)
  179. ##
  180. # Create parser.
  181. #
  182. # @return A 2-tuple containing a parser and a unmarshaller.
  183. def getparser(self, encoding):
  184. # get parser and unmarshaller
  185. return getparser(encoding)
  186. ##
  187. # Get authorization info from host parameter
  188. # Host may be a string, or a (host, x509-dict) tuple; if a string,
  189. # it is checked for a "user:pw@host" format, and a "Basic
  190. # Authentication" header is added if appropriate.
  191. #
  192. # @param host Host descriptor (URL or (URL, x509 info) tuple).
  193. # @return A 3-tuple containing (actual host, extra headers,
  194. # x509 info). The header and x509 fields may be None.
  195. def get_host_info(self, host):
  196. x509 = {}
  197. if isinstance(host, tuple):
  198. host, x509 = host
  199. auth, host = splituser(host)
  200. if auth:
  201. auth = base64.encodestring(unquote(auth))
  202. auth = "".join(auth.split()) # get rid of whitespace
  203. extra_headers = [
  204. ("Authorization", "Basic " + auth)
  205. ]
  206. else:
  207. extra_headers = None
  208. return host, extra_headers, x509
  209. ##
  210. # Connect to server.
  211. #
  212. # @param host Target host.
  213. # @return A connection handle.
  214. def make_connection(self, host):
  215. # create a HTTP connection object from a host descriptor
  216. host, extra_headers, x509 = self.get_host_info(host)
  217. return HTTPConnection(host)
  218. ##
  219. # Send request header.
  220. #
  221. # @param connection Connection handle.
  222. # @param handler Target RPC handler.
  223. # @param request_body JSON-RPC body.
  224. def send_request(self, connection, handler, request_body):
  225. connection.putrequest("POST", handler)
  226. ##
  227. # Send host name.
  228. #
  229. # @param connection Connection handle.
  230. # @param host Host name.
  231. def send_host(self, connection, host):
  232. host, extra_headers, x509 = self.get_host_info(host)
  233. connection.putheader("Host", host)
  234. if extra_headers:
  235. if isinstance(extra_headers, dict):
  236. extra_headers = list(extra_headers.items())
  237. for key, value in extra_headers:
  238. connection.putheader(key, value)
  239. ##
  240. # Send user-agent identifier.
  241. #
  242. # @param connection Connection handle.
  243. def send_user_agent(self, connection):
  244. connection.putheader("User-Agent", self.user_agent)
  245. ##
  246. # Send request body.
  247. #
  248. # @param connection Connection handle.
  249. # @param request_body JSON-RPC request body.
  250. def send_content(self, connection, request_body):
  251. connection.putheader("Content-Type", "text/xml")
  252. connection.putheader("Content-Length", str(len(request_body)))
  253. connection.endheaders()
  254. if request_body:
  255. connection.send(request_body)
  256. ##
  257. # Parse response.
  258. #
  259. # @param file Stream.
  260. # @return Response tuple and target method.
  261. def parse_response(self, file):
  262. # compatibility interface
  263. return self._parse_response(file, None)
  264. ##
  265. # Parse response (alternate interface). This is similar to the
  266. # parse_response method, but also provides direct access to the
  267. # underlying socket object (where available).
  268. #
  269. # @param file Stream.
  270. # @param sock Socket handle (or None, if the socket object
  271. # could not be accessed).
  272. # @return Response tuple and target method.
  273. def _parse_response(self, file, sock, encoding):
  274. # read response from input file/socket, and parse it
  275. p, u = self.getparser(encoding)
  276. while 1:
  277. if sock:
  278. response = sock.recv(1024)
  279. else:
  280. response = file.read(1024)
  281. if not response:
  282. break
  283. if self.verbose:
  284. print("body:", repr(response))
  285. p.feed(response)
  286. file.close()
  287. p.close()
  288. return u.close()
  289. ##
  290. # Standard transport class for JSON-RPC over HTTPS.
  291. class SafeTransport(Transport):
  292. """Handles an HTTPS transaction to an JSON-RPC server."""
  293. # FIXME: mostly untested
  294. def make_connection(self, host):
  295. # create a HTTPS connection object from a host descriptor
  296. # host may be a string, or a (host, x509-dict) tuple
  297. host, extra_headers, x509 = self.get_host_info(host)
  298. try:
  299. HTTPS = HTTPSConnection
  300. except AttributeError:
  301. raise NotImplementedError(
  302. "your version of httplib doesn't support HTTPS"
  303. )
  304. else:
  305. return HTTPS(host, None, **(x509 or {}))
  306. class ServerProxy(object):
  307. def __init__(self, uri, transport=None, encoding=None,
  308. verbose=None, allow_none=0):
  309. utype, uri = splittype(uri)
  310. if utype not in ("http", "https"):
  311. raise IOError("Unsupported JSONRPC protocol")
  312. self.__host, self.__handler = splithost(uri)
  313. if not self.__handler:
  314. self.__handler = "/RPC2"
  315. if transport is None:
  316. if utype == "https":
  317. transport = SafeTransport()
  318. else:
  319. transport = Transport()
  320. self.__transport = transport
  321. self.__encoding = encoding
  322. self.__verbose = verbose
  323. self.__allow_none = allow_none
  324. def __request(self, methodname, params):
  325. """call a method on the remote server
  326. """
  327. request = dumps(params, methodname, encoding=self.__encoding,
  328. allow_none=self.__allow_none)
  329. response = self.__transport.request(
  330. self.__host,
  331. self.__handler,
  332. request.encode(self.__encoding),
  333. self.__encoding,
  334. verbose=self.__verbose
  335. )
  336. if len(response) == 1:
  337. response = response[0]
  338. return response
  339. def __repr__(self):
  340. return ("<JSONProxy for %s%s>" %
  341. (self.__host, self.__handler)
  342. )
  343. __str__ = __repr__
  344. def __getattr__(self, name):
  345. #dispatch
  346. return _Method(self.__request, name)
  347. # note: to call a remote object with an non-standard name, use
  348. # result getattr(server, "strange-python-name")(args)
  349. if __name__ == "__main__":
  350. s = ServerProxy("http://localhost:8080/foo/", verbose=1)
  351. c = s.echo("foo bar")
  352. print(c)
  353. d = s.bad("other")
  354. print(d)
  355. e = s.echo("foo bar", "baz")
  356. print(e)
  357. f = s.echo(5)
  358. print(f)