Repository: Flexget/Flexget

Issue:
rtorrent SCGI transport looks like GPLed code with its license stripped
The [SCGI XML-RPC transport used in Flexget](https://github.com/Flexget/Flexget/blob/develop/flexget/plugins/clients/rtorrent.py#L99-L224) looks exactly like the GPLed one that's floating around. An example of this can be seen [here](https://github.com/TinyHTPC/xbmc-dev-repo/blob/a3184d3f9451b23865bcf1abce380099f9367876/script.service.audo/resources/SickBeard/lib/rtorrent/lib/xmlrpc/scgi.py) or [here](https://gist.github.com/query/899683/7e5d0c123cd5a6117b3e3d0a9d41a583260516fd)

This also matches my experience looking for a better licensed SCGI Transport back in start of 2015 (before this code was added to Flexget) which resulted in me writing my own transport as none existed.

I suggest replacing the plagiarized SCGI Transport with [my own version](https://github.com/JohnDoee/autotorrent/blob/develop/autotorrent/scgitransport.py) as it should have no license issues and be functionally equivalent.


Discussion:
Is yours a drop in replacement?

I'm not that familiar with the licencing. I thought we could re-use it but need to keep the license at the top of the file intact?  We could just drop in that file as is..?
I'm not sure what you're saying? You didn't keep the license you removed it.

You also can't use GPL code if you want to keep Flexget under MIT.
I'm not familiar with how the licenses work or what's allowed. We should re-add the licence back. But reading the license I didn't realise Flexget would also need to be GPL.

So onto yours.. Is it a drop in replacement?
@stevezau please read up about licenses

Code under GPL can not be included in FlexGet repository. A separately distributed plugin under GPL would be an option though. However for this we should switch to @JohnDoee implementation ASAP.
If you copy the code from scgitransport.py and use a function like this:
```
from six.moves.urllib.parse import quote, urlsplit
from six.moves.xmlrpc_client import ServerProxy

def create_proxy(url):
    parsed = urlsplit(url)
    proto = url.split(':')[0].lower()
    if proto == 'scgi':
        if parsed.netloc:
            url = 'http://%s' % parsed.netloc
            return ServerProxy(url, transport=SCGITransport())
        else:
            path = parsed.path
            return ServerProxy('http://1', transport=SCGITransport(socket_path=path))
    else:
        logger.debug('Creating Normal XMLRPC Proxy with url %r' % url)
    return ServerProxy(url)
```

then it's almost a drop-in replacement. When the proxy is created then functionally it should be equivalent at least.

A GPL problem also exist with Deluge implementation but there's already a pull request for that in the works.

GPL is literally the reason I've written both a Deluge client and an SCGI transport.

Resolving patch:
diff --git a/flexget/plugins/clients/rtorrent.py b/flexget/plugins/clients/rtorrent.py
--- a/flexget/plugins/clients/rtorrent.py
+++ b/flexget/plugins/clients/rtorrent.py
@@ -1,13 +1,14 @@
 from __future__ import unicode_literals, division, absolute_import
 from builtins import *  # noqa pylint: disable=unused-import, redefined-builtin
 from future.moves.xmlrpc import client as xmlrpc_client
-from future.moves.urllib.parse import urlparse, urljoin
+from future.moves.urllib.parse import urlparse, urljoin, urlsplit
 from future.utils import native_str
 
 import logging
 import os
 import socket
 import re
+from io import BytesIO
 from time import sleep
 
 from flexget.utils.template import RenderError
@@ -17,7 +18,6 @@
 from flexget.entry import Entry
 from flexget.config_schema import one_or_more
 from flexget.utils.bittorrent import Torrent, is_torrent_file
-from flexget.utils.tools import native_str_to_text
 
 from requests.auth import HTTPDigestAuth, HTTPBasicAuth
 
@@ -96,132 +96,69 @@ def parse_response(self, response):
         return u.close()
 
 
-class SCGITransport(xmlrpc_client.Transport):
-    """ Used to override the default xmlrpclib transport to support SCGI """
-
-    def __init__(self, *args, **kwargs):
-        self.verbose = 0
-        xmlrpc_client.Transport.__init__(self, *args, **kwargs)
+def encode_netstring(input):
+    return str(len(input)).encode() + b':' + input + b','
 
-    def request(self, host, handler, request_body, verbose=False):
-        return self.single_request(host, handler, request_body, verbose)
-
-    def single_request(self, host, handler, request_body, verbose=0):
-        # Add SCGI headers to the request.
-        headers = [('CONTENT_LENGTH', native_str(len(request_body))), ('SCGI', '1')]
-        header = '\x00'.join(['%s\x00%s' % (key, value) for key, value in headers]) + '\x00'
-        header = '%d:%s' % (len(header), header)
-        request_body = '%s,%s' % (header, request_body)
 
-        sock = None
+def encode_header(key, value):
+    return key + b'\x00' + value + b'\x00'
 
-        try:
-            if host:
-                parsed_host = urlparse(host)
-                host = parsed_host.hostname
-                port = parsed_host.port
-
-                addr_info = socket.getaddrinfo(host, port, socket.AF_INET, socket.SOCK_STREAM)
-                sock = socket.socket(*addr_info[0][:3])
-                sock.connect(addr_info[0][4])
-            else:
-                sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
-                sock.connect(handler)
 
-            self.verbose = verbose
+class SCGITransport(xmlrpc_client.Transport):
+    """
+    Public domain SCGITrannsport implementation from:
+    https://github.com/JohnDoee/autotorrent/blob/develop/autotorrent/scgitransport.py
+    """
+    def __init__(self, *args, **kwargs):
+        self.socket_path = kwargs.pop('socket_path', '')
+        xmlrpc_client.Transport.__init__(self, *args, **kwargs)
 
-            sock.sendall(request_body.encode())
+    def single_request(self, host, handler, request_body, verbose=False):
+        self.verbose = verbose
+        if self.socket_path:
+            s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
+            s.connect(self.socket_path)
+        else:
+            host, port = host.split(':')
+            s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+            s.connect((host, int(port)))
 
-            return self.parse_response(sock.makefile())
-        finally:
-            if sock:
-                sock.close()
+        request = encode_header(b'CONTENT_LENGTH', str(len(request_body)).encode())
+        request += encode_header(b'SCGI', b'1')
+        request += encode_header(b'REQUEST_METHOD', b'POST')
+        request += encode_header(b'REQUEST_URI', handler.encode())
 
-    def parse_response(self, response):
-        p, u = self.getparser()
+        request = encode_netstring(request)
+        request += request_body
 
-        response_body = ''
+        s.send(request)
 
+        response = b''
         while True:
-            data = response.read(1024)
-            if not data:
+            r = s.recv(1024)
+            if not r:
                 break
-            response_body += native_str_to_text(data, encoding='utf-8')
-
-        if self.verbose:
-            log.info('body: %s', repr(response_body))
-
-        # Remove SCGI headers from the response.
-        _, response_body = re.split(r'\n\s*?\n', response_body, maxsplit=1)
-        p.feed(response_body.encode('utf-8'))
-        p.close()
-
-        return u.close()
-
-
-class SCGIServerProxy(object):
-    """ Enable connection to SCGI proxy """
+            response += r
 
-    def __init__(self, uri, transport=None, encoding=None,
-                 verbose=False, allow_none=False, use_datetime=False):
-        parsed_url = urlparse(uri)
-        self.__host = uri if parsed_url.scheme else None
-        self.__handler = parsed_url.path
-        if not self.__handler:
-            self.__handler = '/'
+        response_body = BytesIO(b'\r\n\r\n'.join(response.split(b'\r\n\r\n')[1:]))
 
-        if not transport:
-            transport = SCGITransport(use_datetime=use_datetime)
-        self.__transport = transport
-        self.__encoding = encoding or 'utf-8'
-        self.__verbose = verbose
-        self.__allow_none = allow_none
+        return self.parse_response(response_body)
 
-    def __close(self):
-        self.__transport.close()
 
-    def __request(self, method_name, params):
-        # call a method on the remote server
+if not hasattr(xmlrpc_client.Transport, 'single_request'):
+    SCGITransport.request = SCGITransport.single_request
 
-        request = xmlrpc_client.dumps(params, method_name, encoding=self.__encoding,
-                                      allow_none=self.__allow_none).encode(self.__encoding)
 
-        response = self.__transport.request(
-            self.__host,
-            self.__handler,
-            request.decode('utf-8'),
-            verbose=self.__verbose
-        )
-
-        if len(response) == 1:
-            response = response[0]
-
-        return response
-
-    def __repr__(self):
-        return (
-            "<ServerProxy for %s%s>" %
-            (self.__host, self.__handler)
-        )
-
-    __str__ = __repr__
-
-    def __getattr__(self, name):
-        # magic method dispatcher
-        return _Method(self.__request, name)
-
-    # note: to call a remote object with an non-standard name, use
-    # result getattr(server, "strange-python-name")(args)
-
-    def __call__(self, attr):
-        """A workaround to get special attributes on the ServerProxy
-           without interfering with the magic __getattr__
-        """
-        if attr == "close":
-            return self.__close
-        elif attr == "transport":
-            return self.__transport
-        raise AttributeError("Attribute %r not found" % (attr,))
+def create_proxy(url):
+    parsed = urlsplit(url)
+    if not parsed.scheme:
+        path = parsed.path
+        return xmlrpc_client.ServerProxy('http://1', transport=SCGITransport(socket_path=path))
+    if parsed.scheme == 'scgi':
+        url = 'http://%s' % parsed.netloc
+        return xmlrpc_client.ServerProxy(url, transport=SCGITransport())
+    log.debug('Creating Normal XMLRPC Proxy with url %r' % url)
+    return xmlrpc_client.ServerProxy(url)
 
 
 class RTorrent(object):
@@ -268,10 +205,10 @@ def __init__(self, uri, username=None, password=None, digest_auth=None, session=
         if parsed_uri.scheme in ['http', 'https']:
             sp = xmlrpc_client.ServerProxy
         elif parsed_uri.scheme == 'scgi':
-            sp = SCGIServerProxy
+            sp = create_proxy
         elif parsed_uri.scheme == '' and parsed_uri.path:
             self.uri = parsed_uri.path
-            sp = SCGIServerProxy
+            sp = create_proxy
         else:
             raise IOError('Unsupported scheme %s for uri %s' % (parsed_uri.scheme, self.uri))
 


Test patch: