| 1 | #!/usr/bin/env python |
|---|
| 2 | |
|---|
| 3 | # Copyright (C) 2005-2007, Glenn Washburn |
|---|
| 4 | # |
|---|
| 5 | # This program is free software; you can redistribute it and/or modify |
|---|
| 6 | # it under the terms of the GNU General Public License as published by |
|---|
| 7 | # the Free Software Foundation; either version 2 of the License, or |
|---|
| 8 | # (at your option) any later version. |
|---|
| 9 | # |
|---|
| 10 | # This program is distributed in the hope that it will be useful, |
|---|
| 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|---|
| 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|---|
| 13 | # GNU General Public License for more details. |
|---|
| 14 | # |
|---|
| 15 | # You should have received a copy of the GNU General Public License |
|---|
| 16 | # along with this program; if not, write to the Free Software |
|---|
| 17 | # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA |
|---|
| 18 | # |
|---|
| 19 | # In addition, as a special exception, the copyright holders give |
|---|
| 20 | # permission to link the code of portions of this program with the |
|---|
| 21 | # OpenSSL library under certain conditions as described in each |
|---|
| 22 | # individual source file, and distribute linked combinations |
|---|
| 23 | # including the two. |
|---|
| 24 | # |
|---|
| 25 | # You must obey the GNU General Public License in all respects for |
|---|
| 26 | # all of the code used other than OpenSSL. If you modify file(s) |
|---|
| 27 | # with this exception, you may extend this exception to your version |
|---|
| 28 | # of the file(s), but you are not obligated to do so. If you do not |
|---|
| 29 | # wish to do so, delete this exception statement from your version. |
|---|
| 30 | # If you delete this exception statement from all source files in the |
|---|
| 31 | # program, then also delete it here. |
|---|
| 32 | # |
|---|
| 33 | # Contact: Glenn Washburn <crass@berlios.de> |
|---|
| 34 | |
|---|
| 35 | import sys, cStringIO as StringIO |
|---|
| 36 | import xmlrpclib, urllib, urlparse, socket |
|---|
| 37 | |
|---|
| 38 | # this allows us to parse scgi urls just like http ones |
|---|
| 39 | from urlparse import uses_netloc |
|---|
| 40 | uses_netloc.append('scgi') |
|---|
| 41 | |
|---|
| 42 | def do_scgi_xmlrpc_request(host, methodname, params=()): |
|---|
| 43 | """ |
|---|
| 44 | Send an xmlrpc request over scgi to host. |
|---|
| 45 | host: scgi://host:port/path |
|---|
| 46 | methodname: xmlrpc method name |
|---|
| 47 | params: tuple of simple python objects |
|---|
| 48 | returns: xmlrpc response |
|---|
| 49 | """ |
|---|
| 50 | xmlreq = xmlrpclib.dumps(params, methodname) |
|---|
| 51 | xmlresp = SCGIRequest(host).send(xmlreq) |
|---|
| 52 | #~ print xmlresp |
|---|
| 53 | |
|---|
| 54 | return xmlresp |
|---|
| 55 | |
|---|
| 56 | def do_scgi_xmlrpc_request_py(host, methodname, params=()): |
|---|
| 57 | """ |
|---|
| 58 | Send an xmlrpc request over scgi to host. |
|---|
| 59 | host: scgi://host:port/path |
|---|
| 60 | methodname: xmlrpc method name |
|---|
| 61 | params: tuple of simple python objects |
|---|
| 62 | returns: xmlrpc response converted to python |
|---|
| 63 | """ |
|---|
| 64 | xmlresp = do_scgi_xmlrpc_request(host, methodname, params) |
|---|
| 65 | return xmlrpclib.loads(xmlresp)[0][0] |
|---|
| 66 | |
|---|
| 67 | class SCGIRequest(object): |
|---|
| 68 | """ See spec at: http://python.ca/scgi/protocol.txt |
|---|
| 69 | Send an SCGI request. |
|---|
| 70 | |
|---|
| 71 | Use tcp socket |
|---|
| 72 | SCGIRequest('scgi://host:port').send(data) |
|---|
| 73 | |
|---|
| 74 | Or use the named unix domain socket |
|---|
| 75 | SCGIRequest('scgi:///tmp/rtorrent.sock').send(data) |
|---|
| 76 | """ |
|---|
| 77 | |
|---|
| 78 | def __init__(self, url): |
|---|
| 79 | self.url=url |
|---|
| 80 | self.resp_headers=[] |
|---|
| 81 | |
|---|
| 82 | def __send(self, scgireq): |
|---|
| 83 | scheme, netloc, path, query, frag = urlparse.urlsplit(self.url) |
|---|
| 84 | host, port = urllib.splitport(netloc) |
|---|
| 85 | #~ print '>>>', (netloc, host, port) |
|---|
| 86 | |
|---|
| 87 | if netloc: |
|---|
| 88 | addrinfo = socket.getaddrinfo(host, port, socket.AF_INET, socket.SOCK_STREAM) |
|---|
| 89 | |
|---|
| 90 | assert len(addrinfo) == 1, "There's more than one? %r"%addrinfo |
|---|
| 91 | #~ print addrinfo |
|---|
| 92 | |
|---|
| 93 | sock = socket.socket(*addrinfo[0][:3]) |
|---|
| 94 | sock.connect(addrinfo[0][4]) |
|---|
| 95 | else: |
|---|
| 96 | # if no host then assume unix domain socket |
|---|
| 97 | sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) |
|---|
| 98 | sock.connect(path) |
|---|
| 99 | |
|---|
| 100 | sock.send(scgireq) |
|---|
| 101 | recvdata = resp = sock.recv(1024) |
|---|
| 102 | while recvdata != '': |
|---|
| 103 | recvdata = sock.recv(1024) |
|---|
| 104 | #~ print 'Trying to receive more: %r'%recvdata |
|---|
| 105 | resp += recvdata |
|---|
| 106 | sock.close() |
|---|
| 107 | return resp |
|---|
| 108 | |
|---|
| 109 | def send(self, data): |
|---|
| 110 | "Send data over scgi to url and get response" |
|---|
| 111 | scgiresp = self.__send(self.add_required_scgi_headers(data)) |
|---|
| 112 | resp, self.resp_headers = self.get_scgi_resp(scgiresp) |
|---|
| 113 | return resp |
|---|
| 114 | |
|---|
| 115 | @staticmethod |
|---|
| 116 | def encode_netstring(string): |
|---|
| 117 | "Encode string as netstring" |
|---|
| 118 | return '%d:%s,'%(len(string), string) |
|---|
| 119 | |
|---|
| 120 | @staticmethod |
|---|
| 121 | def make_headers(headers): |
|---|
| 122 | "Make scgi header list" |
|---|
| 123 | #~ return '\x00'.join(headers)+'\x00' |
|---|
| 124 | return '\x00'.join(['%s\x00%s'%t for t in headers])+'\x00' |
|---|
| 125 | |
|---|
| 126 | @staticmethod |
|---|
| 127 | def add_required_scgi_headers(data, headers=[]): |
|---|
| 128 | "Wrap data in an scgi request,\nsee spec at: http://python.ca/scgi/protocol.txt" |
|---|
| 129 | # See spec at: http://python.ca/scgi/protocol.txt |
|---|
| 130 | headers = SCGIRequest.make_headers([ |
|---|
| 131 | ('CONTENT_LENGTH', str(len(data))), |
|---|
| 132 | ('SCGI', '1'), |
|---|
| 133 | ] + headers) |
|---|
| 134 | |
|---|
| 135 | enc_headers = SCGIRequest.encode_netstring(headers) |
|---|
| 136 | |
|---|
| 137 | return enc_headers+data |
|---|
| 138 | |
|---|
| 139 | @staticmethod |
|---|
| 140 | def gen_headers(file): |
|---|
| 141 | "Get header lines from scgi response" |
|---|
| 142 | line = file.readline().rstrip() |
|---|
| 143 | while line.strip(): |
|---|
| 144 | yield line |
|---|
| 145 | line = file.readline().rstrip() |
|---|
| 146 | |
|---|
| 147 | @staticmethod |
|---|
| 148 | def get_scgi_resp(resp): |
|---|
| 149 | "Get xmlrpc response from scgi response" |
|---|
| 150 | fresp = StringIO.StringIO(resp) |
|---|
| 151 | headers = [] |
|---|
| 152 | for line in SCGIRequest.gen_headers(fresp): |
|---|
| 153 | #~ print "Header: %r"%line |
|---|
| 154 | headers.append(line.split(': ', 1)) |
|---|
| 155 | |
|---|
| 156 | xmlresp = fresp.read() |
|---|
| 157 | return (xmlresp, headers) |
|---|
| 158 | |
|---|
| 159 | class RTorrentXMLRPCClient(object): |
|---|
| 160 | """ |
|---|
| 161 | The following is an exmple of how to use this class. |
|---|
| 162 | rtorrent_host='http://localhost:33000' |
|---|
| 163 | rtc = RTorrentXMLRPCClient(rtorrent_host) |
|---|
| 164 | for infohash in rtc.download_list('complete'): |
|---|
| 165 | if rtc.d.get_ratio(infohash) > 500: |
|---|
| 166 | print "%s has a ratio of over 0.5"%(rtc.d.get_name(infohash)) |
|---|
| 167 | """ |
|---|
| 168 | |
|---|
| 169 | def __init__(self, url, methodname=''): |
|---|
| 170 | self.url = url |
|---|
| 171 | self.methodname = methodname |
|---|
| 172 | |
|---|
| 173 | def __call__(self, *args): |
|---|
| 174 | #~ print "%s%r"%(self.methodname, args) |
|---|
| 175 | scheme, netloc, path, query, frag = urlparse.urlsplit(self.url) |
|---|
| 176 | xmlreq = xmlrpclib.dumps(args, self.methodname) |
|---|
| 177 | if scheme == 'scgi': |
|---|
| 178 | xmlresp = SCGIRequest(self.url).send(xmlreq) |
|---|
| 179 | return xmlrpclib.loads(xmlresp)[0][0] |
|---|
| 180 | #~ return do_scgi_xmlrpc_request_py(self.url, self.methodname, args) |
|---|
| 181 | elif scheme == 'http': |
|---|
| 182 | raise Exception('Unsupported protocol') |
|---|
| 183 | elif scheme == '': |
|---|
| 184 | raise Exception('Unsupported protocol') |
|---|
| 185 | else: |
|---|
| 186 | raise Exception('Unsupported protocol') |
|---|
| 187 | |
|---|
| 188 | def __getattr__(self, attr): |
|---|
| 189 | methodname = self.methodname and '.'.join([self.methodname,attr]) or attr |
|---|
| 190 | return RTorrentXMLRPCClient(self.url, methodname) |
|---|
| 191 | |
|---|
| 192 | def convert_params_to_native(params): |
|---|
| 193 | "Parse xmlrpc-c command line arg syntax" |
|---|
| 194 | #~ print 'convert_params_to_native', params |
|---|
| 195 | cparams = [] |
|---|
| 196 | # parse parameters |
|---|
| 197 | for param in params: |
|---|
| 198 | if len(param) < 2 or param[1] != '/': |
|---|
| 199 | cparams.append(param) |
|---|
| 200 | continue |
|---|
| 201 | |
|---|
| 202 | if param[0] == 'i': |
|---|
| 203 | ptype = int |
|---|
| 204 | elif param[0] == 'b': |
|---|
| 205 | ptype = bool |
|---|
| 206 | elif param[0] == 's': |
|---|
| 207 | ptype = str |
|---|
| 208 | else: |
|---|
| 209 | cparams.append(param) |
|---|
| 210 | continue |
|---|
| 211 | |
|---|
| 212 | cparams.append(ptype(param[2:])) |
|---|
| 213 | |
|---|
| 214 | return tuple(cparams) |
|---|
| 215 | |
|---|
| 216 | def main(argv): |
|---|
| 217 | output_python=False |
|---|
| 218 | |
|---|
| 219 | if argv[0] == '-p': |
|---|
| 220 | output_python=True |
|---|
| 221 | argv.pop(0) |
|---|
| 222 | |
|---|
| 223 | host, methodname = argv[:2] |
|---|
| 224 | |
|---|
| 225 | respxml = do_scgi_xmlrpc_request(host, methodname, |
|---|
| 226 | convert_params_to_native(argv[2:])) |
|---|
| 227 | #~ respxml = RTorrentXMLRPCClient(host, methodname)(convert_params_to_native(argv[2:])) |
|---|
| 228 | |
|---|
| 229 | if not output_python: |
|---|
| 230 | print respxml |
|---|
| 231 | else: |
|---|
| 232 | print xmlrpclib.loads(respxml)[0][0] |
|---|
| 233 | |
|---|
| 234 | if __name__ == "__main__": |
|---|
| 235 | main(sys.argv[1:]) |
|---|