New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Need JSONP polling support #64

Open
StoneMoe opened this Issue Mar 5, 2018 · 3 comments

Comments

Projects
None yet
2 participants
@StoneMoe
Copy link

StoneMoe commented Mar 5, 2018

I tried to write a monkey patch
but finally come here to make a feature request

@miguelgrinberg

This comment has been minimized.

Copy link
Owner

miguelgrinberg commented Mar 5, 2018

I"m not currently planning to implement JSONP. Any reason to prefer that over XHR?

@StoneMoe

This comment has been minimized.

Copy link

StoneMoe commented Mar 5, 2018

xhr not work on ie9 and below

@StoneMoe

This comment has been minimized.

Copy link

StoneMoe commented Mar 5, 2018

Just found an undocument socket.io client-side option
We can use XDR on client side to support the browsers which don't support XHR.

And I just finished a monkey patch for Python-engineio to support jsonp-polling.
(only for v2.0.2)

def engine_io_patcher():
    from engineio.payload import Payload as eio_Payload
    from engineio.server import Server as eio_Server
    original_handle_request = eio_Server.handle_request

    def new_eioserv__ok(self, packets=None, headers=None, b64=False, jsonp_seq=None):
        """response generator."""
        if packets is not None:
            if headers is None:
                headers = []
            if b64:
                headers += [('Content-Type', 'text/plain; charset=UTF-8')]
            else:
                headers += [('Content-Type', 'application/octet-stream')]
            return {'status': '200 OK',
                    'headers': headers,
                    'response': eio_Payload(packets=packets).encode(b64=b64, jsonp_seq=jsonp_seq)}
        else:
            return {'status': '200 OK',
                    'headers': [('Content-Type', 'text/plain')],
                    'response': b'OK'}

    def new_eiopayload_decode(self, encoded_payload):
        """Decode a transmitted payload."""
        import six
        from engineio import packet
        self.packets = []
        while encoded_payload:
            if encoded_payload.startswith(b'd='):  # is form submit
                encoded_payload = encoded_payload[2:]
                encoded_payload = parse.unquote_to_bytes(encoded_payload.decode('utf-8'))
            if six.byte2int(encoded_payload[0:1]) <= 1:
                packet_len = 0
                i = 1
                while six.byte2int(encoded_payload[i:i + 1]) != 255:
                    packet_len = packet_len * 10 + six.byte2int(encoded_payload[i:i + 1])
                    i += 1
                self.packets.append(packet.Packet(encoded_packet=encoded_payload[i + 1:i + 1 + packet_len]))
            else:
                i = encoded_payload.find(b':')
                if i == -1:
                    raise ValueError('invalid payload')

                # extracting the packet out of the payload is extremely
                # inefficient, because the payload needs to be treated as
                # binary, but the non-binary packets have to be parsed as
                # unicode. Luckily this complication only applies to long
                # polling, as the websocket transport sends packets
                # individually wrapped.
                packet_len = int(encoded_payload[0:i])
                pkt = encoded_payload.decode('utf-8', errors='ignore')[
                    i + 1: i + 1 + packet_len].encode('utf-8')
                self.packets.append(packet.Packet(encoded_packet=pkt))

                # the engine.io protocol sends the packet length in
                # utf-8 characters, but we need it in bytes to be able to
                # jump to the next packet in the payload
                packet_len = len(pkt)
            encoded_payload = encoded_payload[i + 1 + packet_len:]

    def new_eiopayload_encode(self, b64=False, jsonp_seq=None):
        """Encode the payload for transmission."""
        import six
        encoded_payload = b''
        for pkt in self.packets:
            encoded_packet = pkt.encode(b64=b64)
            packet_len = len(encoded_packet)
            if b64:
                encoded_payload += str(packet_len).encode('utf-8') + b':' + encoded_packet
            else:
                binary_len = b''
                while packet_len != 0:
                    binary_len = six.int2byte(packet_len % 10) + binary_len
                    packet_len = int(packet_len / 10)
                if not pkt.binary:
                    encoded_payload += b'\0'
                else:
                    encoded_payload += b'\1'
                encoded_payload += binary_len + b'\xff' + encoded_packet
        # Patch start
        if jsonp_seq is not None:
            return '___eio[%s]("%s");' % (str(jsonp_seq), str(encoded_payload, encoding='utf-8').replace('"', '\\"'))
        # Patch end
        return encoded_payload

    def new_eioserv__handle_connect(self, environ, start_response, transport, b64=False, jsonp_seq=None):
        """handshake entry"""
        from engineio import socket
        from engineio import packet
        sid = self._generate_id()
        s = socket.Socket(self, sid)
        # Patch start
        # save jsonp status to engineio::socket instance
        setattr(s, 'is_jsonp', True if jsonp_seq is not None else False)
        setattr(s, 'jsonp_seq', jsonp_seq)
        # Patch end
        self.sockets[sid] = s

        pkt = packet.Packet(
            packet.OPEN, {'sid': sid,
                          'upgrades': self._upgrades(sid, transport),
                          'pingTimeout': int(self.ping_timeout * 1000),
                          'pingInterval': int(self.ping_interval * 1000)})

        s.send(pkt)

        ret = self._trigger_event('connect', sid, environ, run_async=False)
        if ret is False:
            del self.sockets[sid]
            self.logger.warning('Application rejected connection')
            return self._unauthorized()

        if transport == 'websocket':
            ret = s.handle_get_request(environ, start_response)
            if s.closed:
                # websocket connection ended, so we are done
                del self.sockets[sid]
            return ret
        else:
            s.connected = True
            headers = None
            if self.cookie:
                headers = [('Set-Cookie', self.cookie + '=' + sid)]
            return self._ok(s.poll(), headers=headers, b64=b64, jsonp_seq=jsonp_seq)

    def new_eioserv_handle_request(self, environ, start_response, patched_call=False):
        # Patch start
        # bypass jsonp not supported bad request
        # handle jsonp request with regular polling code
        if not patched_call:
            from six.moves import urllib
            from engineio.exceptions import EngineIOError
            method = environ['REQUEST_METHOD']
            query = urllib.parse.parse_qs(environ.get('QUERY_STRING', ''))
            if 'j' in query:
                print('JSONP Connection found')
                sid = query['sid'][0] if 'sid' in query else None
                b64 = False
                if 'b64' in query:
                    if query['b64'][0] == "1" or query['b64'][0].lower() == "true":
                        b64 = True
                if method == 'GET':
                    if sid is None:  # Need handshake
                        transport = query.get('transport', ['polling'])[0]
                        if transport != 'polling' and transport != 'websocket':
                            self.logger.warning('Invalid transport %s', transport)
                            r = self._bad_request()
                        else:
                            r = self._handle_connect(environ, start_response, transport, b64, jsonp_seq=int(query['j'][0]))
                    else:
                        if sid not in self.sockets:
                            self.logger.warning('Invalid session %s', sid)
                            r = self._bad_request()
                        else:
                            socket = self._get_socket(sid)
                            try:
                                packets = socket.handle_get_request(environ, start_response)
                                if isinstance(packets, list):
                                    r = self._ok(packets, b64=b64, jsonp_seq=int(query['j'][0]))
                                else:
                                    r = packets
                            except EngineIOError:
                                if sid in self.sockets:  # pragma: no cover
                                    self.disconnect(sid)
                                r = self._bad_request()
                            if sid in self.sockets and self.sockets[sid].closed:
                                del self.sockets[sid]
                elif method == 'POST':
                    if sid is None or sid not in self.sockets:
                        self.logger.warning('Invalid session %s', sid)
                        r = self._bad_request()
                    else:
                        socket = self._get_socket(sid)
                        try:
                            socket.handle_post_request(environ)
                            r = self._ok(jsonp_seq=int(query['j'][0]))
                        except EngineIOError:
                            if sid in self.sockets:  # pragma: no cover
                                self.disconnect(sid)
                            r = self._bad_request()
                        except:  # pragma: no cover
                            # for any other unexpected errors, we log the error
                            # and keep going
                            self.logger.exception('post request handler error')
                            r = self._ok(jsonp_seq=int(query['j'][0]))
                else:
                    self.logger.warning('Method %s not supported', method)
                    r = self._method_not_found()
                if not isinstance(r, dict):
                    return r or []
                if self.http_compression and len(r['response']) >= self.compression_threshold:
                    encodings = [e.split(';')[0].strip() for e in environ.get('HTTP_ACCEPT_ENCODING', '').split(',')]
                    for encoding in encodings:
                        if encoding in self.compression_methods:
                            r['response'] = getattr(self, '_' + encoding)(r['response'])
                            r['headers'] += [('Content-Encoding', encoding)]
                            break
                cors_headers = self._cors_headers(environ)
                start_response(r['status'], r['headers'] + cors_headers)
                return [r['response']]

            else:
                return original_handle_request(self, environ, start_response)
        # Patch end

    eio_Server._ok = new_eioserv__ok
    eio_Payload.decode = new_eiopayload_decode
    eio_Payload.encode = new_eiopayload_encode
    eio_Server.handle_request = new_eioserv_handle_request
    eio_Server._handle_connect = new_eioserv__handle_connect
    print('EngineIO JSONP support patch done.')

@miguelgrinberg miguelgrinberg self-assigned this Jan 20, 2019

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment