1
2
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
11
12
13
14
15
16
17
18
19
20
21
22
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
34 """SCGI protocol error"""
35
36
37 ERRORS = (SCGIException, urllib2.URLError, xmlrpclib.Fault, socket.error)
38
39
40
41
42
43
45 """ Transport via TCP or a UNIX domain socket.
46 """
47
48
49 CHUNK_SIZE = 32768
50
51
53 self.url = url
54
55 if url.netloc:
56
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
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
83 sock.send(data)
84
85
86 while True:
87 chunk = sock.recv(self.CHUNK_SIZE)
88 if chunk:
89 yield chunk
90 else:
91 break
92 finally:
93
94 sock.close()
95
96
98 """ Transport via SSH to a UNIX domain socket.
99 """
100
102 self.url = url
103 self.cmd = ['ssh', '-T']
104
105 if not url.path.startswith('/'):
106 raise urllib2.URLError("Bad path in URL %r" % url.geturl())
107
108
109
110
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
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
152 urlparse.uses_netloc.extend(TRANSPORTS.keys())
153
154
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
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
176
177
178
180 "Encode text as netstring."
181 return "%d:%s," % (len(text), text)
182
183
185 "Make SCGI header bytes from list of tuples."
186 return ''.join(['%s\0%s\0' % i for i in headers])
187
188
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
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
210 """ Get xmlrpc response from scgi response
211 """
212
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
222 assert len(payload) == int(clen)
223
224 return payload, headers
225
226
227
228
229
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
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
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
278
279 xmlresp = xmlresp.replace("<i8>", "<i4>").replace("</i8>", "</i4>")
280
281
282 return xmlrpclib.loads(xmlresp)[0][0]
283 else:
284
285 return xmlresp
286