Package pyrobase :: Package io :: Module xmlrpc2scgi
[hide private]
[frames] | no frames]

Source Code for Module pyrobase.io.xmlrpc2scgi

  1  # -*- coding: utf-8 -*- 
  2  # pylint: disable=too-few-public-methods 
  3  """ XMLRPC via SCGI client proxy over various transports. 
  4   
  5      Copyright (c) 2011 The PyroScope Project <pyroscope.project@gmail.com> 
  6   
  7      Losely based on code Copyright (C) 2005-2007, Glenn Washburn <crass@berlios.de> 
  8      SSH tunneling back-ported from https://github.com/Quantique 
  9  """ 
 10  # This program is free software; you can redistribute it and/or modify 
 11  # it under the terms of the GNU General Public License as published by 
 12  # the Free Software Foundation; either version 2 of the License, or 
 13  # (at your option) any later version. 
 14  # 
 15  # This program is distributed in the hope that it will be useful, 
 16  # but WITHOUT ANY WARRANTY; without even the implied warranty of 
 17  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
 18  # GNU General Public License for more details. 
 19  # 
 20  # You should have received a copy of the GNU General Public License along 
 21  # with this program; if not, write to the Free Software Foundation, Inc., 
 22  # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 
 23  import os 
 24  import time 
 25  import pipes 
 26  import socket 
 27  import urllib2 
 28  import urlparse 
 29  import xmlrpclib 
 30  import subprocess 
 31   
 32   
33 -class SCGIException(Exception):
34 """SCGI protocol error"""
35 36 # Types of exceptions thrown 37 ERRORS = (SCGIException, urllib2.URLError, xmlrpclib.Fault, socket.error) 38 39 40 # 41 # SCGI transports 42 # 43
44 -class LocalTransport(object):
45 """ Transport via TCP or a UNIX domain socket. 46 """ 47 48 # Amount of bytes to read at once 49 CHUNK_SIZE = 32768 50 51
52 - def __init__(self, url):
53 self.url = url 54 55 if url.netloc: 56 # TCP socket 57 addrinfo = list(set(socket.getaddrinfo(url.hostname, url.port, socket.AF_INET, socket.SOCK_STREAM))) 58 if len(addrinfo) != 1: 59 raise urllib2.URLError("Host of URL %r resolves to multiple addresses %r" % (url.geturl(), addrinfo)) 60 61 self.sock_args = addrinfo[0][:3] 62 self.sock_addr = addrinfo[0][4] 63 else: 64 # UNIX domain socket 65 path = url.path 66 if path.startswith("/~"): 67 path = os.path.expanduser(path) 68 self.sock_args = (socket.AF_UNIX, socket.SOCK_STREAM) 69 self.sock_addr = os.path.abspath(path)
70 71
72 - def send(self, data):
73 """ Open transport, send data, and yield response chunks. 74 """ 75 sock = socket.socket(*self.sock_args) 76 try: 77 sock.connect(self.sock_addr) 78 except socket.error, exc: 79 raise socket.error("Can't connect to %r (%s)" % (self.url.geturl(), exc)) 80 81 try: 82 # Send request 83 sock.send(data) 84 85 # Read response 86 while True: 87 chunk = sock.recv(self.CHUNK_SIZE) 88 if chunk: 89 yield chunk 90 else: 91 break 92 finally: 93 # Clean up 94 sock.close()
95 96
97 -class SSHTransport(object):
98 """ Transport via SSH to a UNIX domain socket. 99 """ 100
101 - def __init__(self, url):
102 self.url = url 103 self.cmd = ['ssh', '-T'] # no pseudo-tty 104 105 if not url.path.startswith('/'): 106 raise urllib2.URLError("Bad path in URL %r" % url.geturl()) 107 108 # pipes.quote is used because ssh always goes through a login shell. 109 # The only exception is for redirecting ports, which can't be used 110 # to access a domain socket. 111 if url.path.startswith("/~/"): 112 clean_path = "~/" + pipes.quote(url.path[3:]) 113 else: 114 clean_path = pipes.quote(url.path) 115 116 ssh_netloc = ''.join((url.username or '', '@' if url.username else '', url.hostname)) 117 if url.port: 118 reconstructed_netloc = '%s:%d' % (ssh_netloc, url.port) 119 self.cmd.extend(["-p", str(url.port)]) 120 else: 121 reconstructed_netloc = ssh_netloc 122 if reconstructed_netloc != url.netloc: 123 raise urllib2.URLError("Bad location in URL %r (expected %r)" % (url.geturl(), reconstructed_netloc)) 124 125 self.cmd.extend(["--", ssh_netloc]) 126 #self.cmd.extend(["/bin/nc", "-U", "--", clean_path]) 127 self.cmd.extend(["socat", "STDIO", "UNIX-CONNECT:" + clean_path])
128 129
130 - def send(self, data):
131 """ Open transport, send data, and yield response chunks. 132 """ 133 try: 134 proc = subprocess.Popen(self.cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 135 except OSError, exc: 136 raise urllib2.URLError("Calling %r failed (%s)!" % (' '.join(self.cmd), exc)) 137 else: 138 stdout, stderr = proc.communicate(data) 139 if proc.returncode: 140 raise urllib2.URLError("Calling %r failed with RC=%d!\n%s" % ( 141 ' '.join(self.cmd), proc.returncode, stderr, 142 )) 143 yield stdout
144 145 146 TRANSPORTS = { 147 "scgi": LocalTransport, 148 "scgi+ssh": SSHTransport, 149 } 150 151 # Register our schemes to be parsed as having a netloc 152 urlparse.uses_netloc.extend(TRANSPORTS.keys()) 153 154
155 -def transport_from_url(url):
156 """ Create a transport for the given URL. 157 """ 158 if '/' not in url and ':' in url and url.rsplit(':')[-1].isdigit(): 159 url = 'scgi://' + url 160 url = urlparse.urlsplit(url, "scgi", allow_fragments=False) 161 162 try: 163 transport = TRANSPORTS[url.scheme.lower()] 164 except KeyError: 165 if not any((url.netloc, url.query)) and url.path.isdigit(): 166 # Support simplified "domain:port" URLs 167 return transport_from_url("scgi://%s:%s" % (url.scheme, url.path)) 168 else: 169 raise urllib2.URLError("Unsupported scheme in URL %r" % url.geturl()) 170 else: 171 return transport(url)
172 173 174 # 175 # Helpers to handle SCGI data 176 # See spec at http://python.ca/scgi/protocol.txt 177 # 178
179 -def _encode_netstring(text):
180 "Encode text as netstring." 181 return "%d:%s," % (len(text), text)
182 183
184 -def _encode_headers(headers):
185 "Make SCGI header bytes from list of tuples." 186 return ''.join(['%s\0%s\0' % i for i in headers])
187 188
189 -def _encode_payload(data, headers=None):
190 "Wrap data in an SCGI request." 191 prolog = "CONTENT_LENGTH\0%d\0SCGI\x001\0" % len(data) 192 if headers: 193 prolog += _encode_headers(headers) 194 195 return _encode_netstring(prolog) + data
196 197
198 -def _parse_headers(headers):
199 "Get headers dict from header string." 200 try: 201 return dict(line.rstrip().split(": ", 1) 202 for line in headers.splitlines() 203 if line 204 ) 205 except (TypeError, ValueError), exc: 206 raise SCGIException("Error in SCGI headers %r (%s)" % (headers, exc,))
207 208
209 -def _parse_response(resp):
210 """ Get xmlrpc response from scgi response 211 """ 212 # Assume they care for standards and send us CRLF (not just LF) 213 try: 214 headers, payload = resp.split("\r\n\r\n", 1) 215 except (TypeError, ValueError), exc: 216 raise SCGIException("No header delimiter in SCGI response of length %d (%s)" % (len(resp), exc,)) 217 headers = _parse_headers(headers) 218 219 clen = headers.get("Content-Length") 220 if clen is not None: 221 # Check length, just in case the transport is bogus 222 assert len(payload) == int(clen) 223 224 return payload, headers
225 226 227 # 228 # SCGI request handling 229 #
230 -class SCGIRequest(object):
231 """ Send a SCGI request. 232 See spec at "http://python.ca/scgi/protocol.txt". 233 234 Use tcp socket 235 SCGIRequest('scgi://host:port').send(data) 236 237 Or use the named unix domain socket 238 SCGIRequest('scgi:///tmp/rtorrent.sock').send(data) 239 """ 240
241 - def __init__(self, url_or_transport):
242 try: 243 self.transport = transport_from_url(url_or_transport + "") 244 except TypeError: 245 self.transport = url_or_transport 246 247 self.resp_headers = {} 248 self.latency = 0.0
249 250
251 - def send(self, data):
252 """ Send data over scgi to URL and get response. 253 """ 254 start = time.time() 255 try: 256 scgi_resp = ''.join(self.transport.send(_encode_payload(data))) 257 finally: 258 self.latency = time.time() - start 259 260 resp, self.resp_headers = _parse_response(scgi_resp) 261 return resp
262 263
264 -def scgi_request(url, methodname, *params, **kw):
265 """ Send a XMLRPC request over SCGI to the given URL. 266 267 @param url: Endpoint URL. 268 @param methodname: XMLRPC method name. 269 @param params: Tuple of simple python objects. 270 @keyword deserialize: Parse XML result? (default is True) 271 @return: XMLRPC response, or the equivalent Python data. 272 """ 273 xmlreq = xmlrpclib.dumps(params, methodname) 274 xmlresp = SCGIRequest(url).send(xmlreq) 275 276 if kw.get("deserialize", True): 277 # This fixes a bug with the Python xmlrpclib module 278 # (has no handler for <i8> in some versions) 279 xmlresp = xmlresp.replace("<i8>", "<i4>").replace("</i8>", "</i4>") 280 281 # Return deserialized data 282 return xmlrpclib.loads(xmlresp)[0][0] 283 else: 284 # Return raw XML 285 return xmlresp
286