Merge pull request #1410 from croneter/new-websocket
Fix instantaneous background sync and Alexa not working
This commit is contained in:
commit
92daa29592
25 changed files with 3856 additions and 1158 deletions
|
@ -4,6 +4,7 @@
|
||||||
<import addon="xbmc.python" version="3.0.0"/>
|
<import addon="xbmc.python" version="3.0.0"/>
|
||||||
<import addon="script.module.requests" version="2.22.0+matrix.1" />
|
<import addon="script.module.requests" version="2.22.0+matrix.1" />
|
||||||
<import addon="script.module.defusedxml" version="0.6.0+matrix.1"/>
|
<import addon="script.module.defusedxml" version="0.6.0+matrix.1"/>
|
||||||
|
<import addon="script.module.six" />
|
||||||
<import addon="plugin.video.plexkodiconnect.movies" version="3.0.0" />
|
<import addon="plugin.video.plexkodiconnect.movies" version="3.0.0" />
|
||||||
<import addon="plugin.video.plexkodiconnect.tvshows" version="3.0.0" />
|
<import addon="plugin.video.plexkodiconnect.tvshows" version="3.0.0" />
|
||||||
<import addon="metadata.themoviedb.org.python" version="1.3.1+matrix.1" />
|
<import addon="metadata.themoviedb.org.python" version="1.3.1+matrix.1" />
|
||||||
|
|
|
@ -1134,11 +1134,6 @@ msgctxt "#39089"
|
||||||
msgid "Alexa connection status:"
|
msgid "Alexa connection status:"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
# PKC Settings - Connection - Background sync connection status
|
|
||||||
msgctxt "#39090"
|
|
||||||
msgid "Suspended - not connected"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
# PKC Settings - Connection - Background sync connection status
|
# PKC Settings - Connection - Background sync connection status
|
||||||
msgctxt "#39091"
|
msgctxt "#39091"
|
||||||
msgid "Timeout - not connected"
|
msgid "Timeout - not connected"
|
||||||
|
@ -1149,6 +1144,16 @@ msgctxt "#39092"
|
||||||
msgid "IOError - not connected"
|
msgid "IOError - not connected"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
# PKC Settings - Connection - Background sync connection status
|
||||||
|
msgctxt "#39093"
|
||||||
|
msgid "Suspended - not connected"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
# PKC Settings - Connection - Background sync connection status
|
||||||
|
msgctxt "#39094"
|
||||||
|
msgid "Managed Plex User - not connected"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgctxt "#39200"
|
msgctxt "#39200"
|
||||||
msgid "Log-out Plex Home User "
|
msgid "Log-out Plex Home User "
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
|
@ -55,8 +55,6 @@ class Sync(object):
|
||||||
|
|
||||||
# How often shall we sync?
|
# How often shall we sync?
|
||||||
self.full_sync_intervall = None
|
self.full_sync_intervall = None
|
||||||
# Background Sync disabled?
|
|
||||||
self.background_sync_disabled = None
|
|
||||||
# How long shall we wait with synching a new item to make sure Plex got all
|
# How long shall we wait with synching a new item to make sure Plex got all
|
||||||
# metadata?
|
# metadata?
|
||||||
self.backgroundsync_saftymargin = None
|
self.backgroundsync_saftymargin = None
|
||||||
|
@ -79,7 +77,6 @@ class Sync(object):
|
||||||
# List of section_ids we're synching to Kodi - will be automatically
|
# List of section_ids we're synching to Kodi - will be automatically
|
||||||
# re-built if sections are set a-new
|
# re-built if sections are set a-new
|
||||||
self.section_ids = set()
|
self.section_ids = set()
|
||||||
self.enable_alexa = None
|
|
||||||
|
|
||||||
self.load()
|
self.load()
|
||||||
|
|
||||||
|
@ -120,8 +117,6 @@ class Sync(object):
|
||||||
Any settings unrelated to syncs to the Kodi database - can thus be
|
Any settings unrelated to syncs to the Kodi database - can thus be
|
||||||
safely reset without a Kodi reboot
|
safely reset without a Kodi reboot
|
||||||
"""
|
"""
|
||||||
self.background_sync_disabled = utils.settings('enableBackgroundSync') == 'false'
|
|
||||||
self.enable_alexa = utils.settings('enable_alexa') == 'true'
|
|
||||||
self.sync_dialog = utils.settings('dbSyncIndicator') == 'true'
|
self.sync_dialog = utils.settings('dbSyncIndicator') == 'true'
|
||||||
self.full_sync_intervall = int(utils.settings('fullSyncInterval')) * 60
|
self.full_sync_intervall = int(utils.settings('fullSyncInterval')) * 60
|
||||||
self.backgroundsync_saftymargin = int(utils.settings('backgroundsync_saftyMargin'))
|
self.backgroundsync_saftymargin = int(utils.settings('backgroundsync_saftyMargin'))
|
||||||
|
|
|
@ -98,7 +98,8 @@ class Service(object):
|
||||||
self.welcome_msg = True
|
self.welcome_msg = True
|
||||||
self.connection_check_counter = 0
|
self.connection_check_counter = 0
|
||||||
self.setup = None
|
self.setup = None
|
||||||
self.alexa = None
|
self.pms_ws = None
|
||||||
|
self.alexa_ws = None
|
||||||
self.playqueue = None
|
self.playqueue = None
|
||||||
# Flags for other threads
|
# Flags for other threads
|
||||||
self.connection_check_running = False
|
self.connection_check_running = False
|
||||||
|
@ -444,8 +445,8 @@ class Service(object):
|
||||||
self.setup.setup()
|
self.setup.setup()
|
||||||
|
|
||||||
# Initialize important threads
|
# Initialize important threads
|
||||||
self.ws = websocket_client.PMS_Websocket()
|
self.pms_ws = websocket_client.get_pms_websocketapp()
|
||||||
self.alexa = websocket_client.Alexa_Websocket()
|
self.alexa_ws = websocket_client.get_alexa_websocketapp()
|
||||||
self.sync = sync.Sync()
|
self.sync = sync.Sync()
|
||||||
self.plexcompanion = plex_companion.PlexCompanion()
|
self.plexcompanion = plex_companion.PlexCompanion()
|
||||||
self.playqueue = playqueue.PlayqueueMonitor()
|
self.playqueue = playqueue.PlayqueueMonitor()
|
||||||
|
@ -545,11 +546,11 @@ class Service(object):
|
||||||
continue
|
continue
|
||||||
elif not self.startup_completed:
|
elif not self.startup_completed:
|
||||||
self.startup_completed = True
|
self.startup_completed = True
|
||||||
self.ws.start()
|
self.pms_ws.start()
|
||||||
self.sync.start()
|
self.sync.start()
|
||||||
self.plexcompanion.start()
|
self.plexcompanion.start()
|
||||||
self.playqueue.start()
|
self.playqueue.start()
|
||||||
self.alexa.start()
|
self.alexa_ws.start()
|
||||||
|
|
||||||
elif app.APP.is_playing:
|
elif app.APP.is_playing:
|
||||||
skip_plex_intro.check()
|
skip_plex_intro.check()
|
||||||
|
|
|
@ -224,7 +224,7 @@ class Sync(backgroundthread.KillableThread):
|
||||||
not app.APP.is_playing_video):
|
not app.APP.is_playing_video):
|
||||||
LOG.info('Doing scheduled full library scan')
|
LOG.info('Doing scheduled full library scan')
|
||||||
self.start_library_sync()
|
self.start_library_sync()
|
||||||
elif not app.SYNC.background_sync_disabled:
|
else:
|
||||||
# Check back whether we should process something Only do
|
# Check back whether we should process something Only do
|
||||||
# this once a while (otherwise, potentially many screen
|
# this once a while (otherwise, potentially many screen
|
||||||
# refreshes lead to flickering)
|
# refreshes lead to flickering)
|
||||||
|
|
|
@ -1,917 +0,0 @@
|
||||||
#!/usr/bin/env python
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""
|
|
||||||
websocket - WebSocket client library for Python
|
|
||||||
|
|
||||||
Copyright (C) 2010 Hiroki Ohtani(liris)
|
|
||||||
|
|
||||||
This library is free software; you can redistribute it and/or
|
|
||||||
modify it under the terms of the GNU Lesser General Public
|
|
||||||
License as published by the Free Software Foundation; either
|
|
||||||
version 2.1 of the License, or (at your option) any later version.
|
|
||||||
|
|
||||||
This library is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
||||||
Lesser General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU Lesser General Public
|
|
||||||
License along with this library; if not, write to the Free Software
|
|
||||||
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
|
||||||
|
|
||||||
"""
|
|
||||||
import socket
|
|
||||||
|
|
||||||
try:
|
|
||||||
import ssl
|
|
||||||
from ssl import SSLError
|
|
||||||
HAVE_SSL = True
|
|
||||||
except ImportError:
|
|
||||||
class SSLError(Exception):
|
|
||||||
"""
|
|
||||||
Dummy class of SSLError for ssl none-support environment.
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
HAVE_SSL = False
|
|
||||||
|
|
||||||
from urllib.parse import urlparse
|
|
||||||
import os
|
|
||||||
import array
|
|
||||||
import struct
|
|
||||||
import uuid
|
|
||||||
import hashlib
|
|
||||||
import base64
|
|
||||||
import threading
|
|
||||||
import logging
|
|
||||||
import traceback
|
|
||||||
import sys
|
|
||||||
|
|
||||||
from . import app
|
|
||||||
|
|
||||||
###############################################################################
|
|
||||||
|
|
||||||
LOG = logging.getLogger('PLEX.websocket')
|
|
||||||
|
|
||||||
###############################################################################
|
|
||||||
|
|
||||||
"""
|
|
||||||
websocket python client.
|
|
||||||
=========================
|
|
||||||
|
|
||||||
This version support only hybi-13.
|
|
||||||
Please see http://tools.ietf.org/html/rfc6455 for protocol.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
# websocket supported version.
|
|
||||||
VERSION = 13
|
|
||||||
|
|
||||||
# closing frame status codes.
|
|
||||||
STATUS_NORMAL = 1000
|
|
||||||
STATUS_GOING_AWAY = 1001
|
|
||||||
STATUS_PROTOCOL_ERROR = 1002
|
|
||||||
STATUS_UNSUPPORTED_DATA_TYPE = 1003
|
|
||||||
STATUS_STATUS_NOT_AVAILABLE = 1005
|
|
||||||
STATUS_ABNORMAL_CLOSED = 1006
|
|
||||||
STATUS_INVALID_PAYLOAD = 1007
|
|
||||||
STATUS_POLICY_VIOLATION = 1008
|
|
||||||
STATUS_MESSAGE_TOO_BIG = 1009
|
|
||||||
STATUS_INVALID_EXTENSION = 1010
|
|
||||||
STATUS_UNEXPECTED_CONDITION = 1011
|
|
||||||
STATUS_TLS_HANDSHAKE_ERROR = 1015
|
|
||||||
|
|
||||||
|
|
||||||
class WebSocketException(Exception):
|
|
||||||
"""
|
|
||||||
websocket exeception class.
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class WebSocketConnectionClosedException(WebSocketException):
|
|
||||||
"""
|
|
||||||
If remote host closed the connection or some network error happened,
|
|
||||||
this exception will be raised.
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class WebSocketTimeoutException(WebSocketException):
|
|
||||||
"""
|
|
||||||
WebSocketTimeoutException will be raised at socket timeout during read and
|
|
||||||
write data.
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class WebsocketRedirect(WebSocketException):
|
|
||||||
"""
|
|
||||||
WebsocketRedirect will be raised if a status code 301 is returned
|
|
||||||
The Exception will be instantiated with a dict containing all response
|
|
||||||
headers; which should contain the redirect address under the key 'location'
|
|
||||||
|
|
||||||
Access the headers via the attribute headers
|
|
||||||
"""
|
|
||||||
def __init__(self, headers):
|
|
||||||
self.headers = headers
|
|
||||||
super(WebsocketRedirect, self).__init__()
|
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_TIMEOUT = None
|
|
||||||
TRACE_ENABLED = False
|
|
||||||
|
|
||||||
|
|
||||||
def enable_trace(tracable):
|
|
||||||
"""
|
|
||||||
turn on/off the tracability.
|
|
||||||
|
|
||||||
tracable: boolean value. if set True, tracability is enabled.
|
|
||||||
"""
|
|
||||||
global TRACE_ENABLED
|
|
||||||
TRACE_ENABLED = tracable
|
|
||||||
if tracable:
|
|
||||||
if not LOG.handlers:
|
|
||||||
LOG.addHandler(logging.StreamHandler())
|
|
||||||
LOG.setLevel(logging.DEBUG)
|
|
||||||
|
|
||||||
|
|
||||||
def setdefaulttimeout(timeout):
|
|
||||||
"""
|
|
||||||
Set the global timeout setting to connect.
|
|
||||||
|
|
||||||
timeout: default socket timeout time. This value is second.
|
|
||||||
"""
|
|
||||||
global DEFAULT_TIMEOUT
|
|
||||||
DEFAULT_TIMEOUT = timeout
|
|
||||||
|
|
||||||
|
|
||||||
def getdefaulttimeout():
|
|
||||||
"""
|
|
||||||
Return the global timeout setting(second) to connect.
|
|
||||||
"""
|
|
||||||
return DEFAULT_TIMEOUT
|
|
||||||
|
|
||||||
|
|
||||||
def _parse_url(url):
|
|
||||||
"""
|
|
||||||
parse url and the result is tuple of
|
|
||||||
(hostname, port, resource path and the flag of secure mode)
|
|
||||||
|
|
||||||
url: url string.
|
|
||||||
"""
|
|
||||||
if ":" not in url:
|
|
||||||
raise ValueError("url is invalid")
|
|
||||||
|
|
||||||
scheme, url = url.split(":", 1)
|
|
||||||
|
|
||||||
parsed = urlparse(url, scheme="http")
|
|
||||||
if parsed.hostname:
|
|
||||||
hostname = parsed.hostname
|
|
||||||
else:
|
|
||||||
raise ValueError("hostname is invalid")
|
|
||||||
port = 0
|
|
||||||
if parsed.port:
|
|
||||||
port = parsed.port
|
|
||||||
|
|
||||||
is_secure = False
|
|
||||||
if scheme == "ws" or scheme == 'http':
|
|
||||||
if not port:
|
|
||||||
port = 80
|
|
||||||
elif scheme == "wss" or scheme == 'https':
|
|
||||||
is_secure = True
|
|
||||||
if not port:
|
|
||||||
port = 443
|
|
||||||
else:
|
|
||||||
raise ValueError("scheme %s is invalid" % scheme)
|
|
||||||
|
|
||||||
if parsed.path:
|
|
||||||
resource = parsed.path
|
|
||||||
else:
|
|
||||||
resource = "/"
|
|
||||||
|
|
||||||
if parsed.query:
|
|
||||||
resource += "?" + parsed.query
|
|
||||||
|
|
||||||
return (hostname, port, resource, is_secure)
|
|
||||||
|
|
||||||
|
|
||||||
def create_connection(url, timeout=None, **options):
|
|
||||||
"""
|
|
||||||
connect to url and return websocket object.
|
|
||||||
|
|
||||||
Connect to url and return the WebSocket object.
|
|
||||||
Passing optional timeout parameter will set the timeout on the socket.
|
|
||||||
If no timeout is supplied, the global default timeout setting returned by
|
|
||||||
getdefauttimeout() is used.
|
|
||||||
You can customize using 'options'.
|
|
||||||
If you set "header" list object, you can set your own custom header.
|
|
||||||
|
|
||||||
>>> conn = create_connection("ws://echo.websocket.org/",
|
|
||||||
... header=["User-Agent: MyProgram",
|
|
||||||
... "x-custom: header"])
|
|
||||||
|
|
||||||
|
|
||||||
timeout: socket timeout time. This value is integer.
|
|
||||||
if you set None for this value, it means "use DEFAULT_TIMEOUT
|
|
||||||
value"
|
|
||||||
|
|
||||||
options: current support option is only "header".
|
|
||||||
if you set header as dict value, the custom HTTP headers are added
|
|
||||||
"""
|
|
||||||
sockopt = options.get("sockopt", [])
|
|
||||||
sslopt = options.get("sslopt", {})
|
|
||||||
websock = WebSocket(sockopt=sockopt, sslopt=sslopt)
|
|
||||||
websock.settimeout(timeout if timeout is not None else DEFAULT_TIMEOUT)
|
|
||||||
websock.connect(url, **options)
|
|
||||||
return websock
|
|
||||||
|
|
||||||
|
|
||||||
_MAX_INTEGER = (1 << 32) - 1
|
|
||||||
_AVAILABLE_KEY_CHARS = list(range(0x21, 0x2f + 1)) + list(range(0x3a, 0x7e + 1))
|
|
||||||
_MAX_CHAR_BYTE = (1 << 8) - 1
|
|
||||||
|
|
||||||
# ref. Websocket gets an update, and it breaks stuff.
|
|
||||||
# http://axod.blogspot.com/2010/06/websocket-gets-update-and-it-breaks.html
|
|
||||||
|
|
||||||
|
|
||||||
def _create_sec_websocket_key():
|
|
||||||
uid = uuid.uuid4()
|
|
||||||
return base64.encodestring(uid.bytes).strip()
|
|
||||||
|
|
||||||
|
|
||||||
_HEADERS_TO_CHECK = {"upgrade": "websocket", "connection": "upgrade"}
|
|
||||||
|
|
||||||
|
|
||||||
class ABNF(object):
|
|
||||||
"""
|
|
||||||
ABNF frame class.
|
|
||||||
see http://tools.ietf.org/html/rfc5234
|
|
||||||
and http://tools.ietf.org/html/rfc6455#section-5.2
|
|
||||||
"""
|
|
||||||
|
|
||||||
# operation code values.
|
|
||||||
OPCODE_CONT = 0x0
|
|
||||||
OPCODE_TEXT = 0x1
|
|
||||||
OPCODE_BINARY = 0x2
|
|
||||||
OPCODE_CLOSE = 0x8
|
|
||||||
OPCODE_PING = 0x9
|
|
||||||
OPCODE_PONG = 0xa
|
|
||||||
|
|
||||||
# available operation code value tuple
|
|
||||||
OPCODES = (OPCODE_CONT, OPCODE_TEXT, OPCODE_BINARY, OPCODE_CLOSE,
|
|
||||||
OPCODE_PING, OPCODE_PONG)
|
|
||||||
|
|
||||||
# opcode human readable string
|
|
||||||
OPCODE_MAP = {
|
|
||||||
OPCODE_CONT: "cont",
|
|
||||||
OPCODE_TEXT: "text",
|
|
||||||
OPCODE_BINARY: "binary",
|
|
||||||
OPCODE_CLOSE: "close",
|
|
||||||
OPCODE_PING: "ping",
|
|
||||||
OPCODE_PONG: "pong"
|
|
||||||
}
|
|
||||||
|
|
||||||
# data length threashold.
|
|
||||||
LENGTH_7 = 0x7d
|
|
||||||
LENGTH_16 = 1 << 16
|
|
||||||
LENGTH_63 = 1 << 63
|
|
||||||
|
|
||||||
def __init__(self, fin=0, rsv1=0, rsv2=0, rsv3=0,
|
|
||||||
opcode=OPCODE_TEXT, mask=1, data=""):
|
|
||||||
"""
|
|
||||||
Constructor for ABNF.
|
|
||||||
please check RFC for arguments.
|
|
||||||
"""
|
|
||||||
self.fin = fin
|
|
||||||
self.rsv1 = rsv1
|
|
||||||
self.rsv2 = rsv2
|
|
||||||
self.rsv3 = rsv3
|
|
||||||
self.opcode = opcode
|
|
||||||
self.mask = mask
|
|
||||||
self.data = data
|
|
||||||
self.get_mask_key = os.urandom
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return "fin=" + str(self.fin) \
|
|
||||||
+ " opcode=" + str(self.opcode) \
|
|
||||||
+ " data=" + str(self.data)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def create_frame(data, opcode):
|
|
||||||
"""
|
|
||||||
create frame to send text, binary and other data.
|
|
||||||
|
|
||||||
data: data to send. This is string value(byte array).
|
|
||||||
if opcode is OPCODE_TEXT and this value is uniocde,
|
|
||||||
data value is conveted into unicode string, automatically.
|
|
||||||
|
|
||||||
opcode: operation code. please see OPCODE_XXX.
|
|
||||||
"""
|
|
||||||
if opcode == ABNF.OPCODE_TEXT and isinstance(data, str):
|
|
||||||
data = data.encode()
|
|
||||||
# mask must be set if send data from client
|
|
||||||
return ABNF(1, 0, 0, 0, opcode, 1, data)
|
|
||||||
|
|
||||||
def format(self):
|
|
||||||
"""
|
|
||||||
format this object to string(byte array) to send data to server.
|
|
||||||
"""
|
|
||||||
if any(x not in (0, 1) for x in [self.fin, self.rsv1, self.rsv2, self.rsv3]):
|
|
||||||
raise ValueError("not 0 or 1")
|
|
||||||
if self.opcode not in ABNF.OPCODES:
|
|
||||||
raise ValueError("Invalid OPCODE")
|
|
||||||
length = len(self.data)
|
|
||||||
if length >= ABNF.LENGTH_63:
|
|
||||||
raise ValueError("data is too long")
|
|
||||||
|
|
||||||
frame_header = chr(self.fin << 7 |
|
|
||||||
self.rsv1 << 6 | self.rsv2 << 5 | self.rsv3 << 4 |
|
|
||||||
self.opcode)
|
|
||||||
if length < ABNF.LENGTH_7:
|
|
||||||
frame_header += chr(self.mask << 7 | length)
|
|
||||||
elif length < ABNF.LENGTH_16:
|
|
||||||
frame_header += chr(self.mask << 7 | 0x7e)
|
|
||||||
frame_header += struct.pack("!H", length)
|
|
||||||
else:
|
|
||||||
frame_header += chr(self.mask << 7 | 0x7f)
|
|
||||||
frame_header += struct.pack("!Q", length)
|
|
||||||
|
|
||||||
if not self.mask:
|
|
||||||
return frame_header + self.data
|
|
||||||
else:
|
|
||||||
mask_key = self.get_mask_key(4)
|
|
||||||
return frame_header + self._get_masked(mask_key)
|
|
||||||
|
|
||||||
def _get_masked(self, mask_key):
|
|
||||||
s = ABNF.mask(mask_key, self.data)
|
|
||||||
return mask_key + "".join(s)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def mask(mask_key, data):
|
|
||||||
"""
|
|
||||||
mask or unmask data. Just do xor for each byte
|
|
||||||
|
|
||||||
mask_key: 4 byte string(byte).
|
|
||||||
|
|
||||||
data: data to mask/unmask.
|
|
||||||
"""
|
|
||||||
_m = array.array("B", mask_key)
|
|
||||||
_d = array.array("B", data)
|
|
||||||
for i, _ in enumerate(_d):
|
|
||||||
_d[i] ^= _m[i % 4]
|
|
||||||
return _d.tostring()
|
|
||||||
|
|
||||||
|
|
||||||
class WebSocket(object):
|
|
||||||
"""
|
|
||||||
Low level WebSocket interface.
|
|
||||||
This class is based on
|
|
||||||
The WebSocket protocol draft-hixie-thewebsocketprotocol-76
|
|
||||||
http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-76
|
|
||||||
|
|
||||||
We can connect to the websocket server and send/recieve data.
|
|
||||||
The following example is a echo client.
|
|
||||||
|
|
||||||
>>> import websocket
|
|
||||||
>>> ws = websocket.WebSocket()
|
|
||||||
>>> ws.connect("ws://echo.websocket.org")
|
|
||||||
>>> ws.send("Hello, Server")
|
|
||||||
>>> ws.recv()
|
|
||||||
'Hello, Server'
|
|
||||||
>>> ws.close()
|
|
||||||
|
|
||||||
get_mask_key: a callable to produce new mask keys, see the set_mask_key
|
|
||||||
function's docstring for more details
|
|
||||||
sockopt: values for socket.setsockopt.
|
|
||||||
sockopt must be tuple and each element is argument of sock.setscokopt.
|
|
||||||
sslopt: dict object for ssl socket option.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, get_mask_key=None, sockopt=None, sslopt=None):
|
|
||||||
"""
|
|
||||||
Initalize WebSocket object.
|
|
||||||
"""
|
|
||||||
if sockopt is None:
|
|
||||||
sockopt = []
|
|
||||||
if sslopt is None:
|
|
||||||
sslopt = {}
|
|
||||||
self.connected = False
|
|
||||||
self.sock = socket.socket()
|
|
||||||
for opts in sockopt:
|
|
||||||
self.sock.setsockopt(*opts)
|
|
||||||
self.sslopt = sslopt
|
|
||||||
self.get_mask_key = get_mask_key
|
|
||||||
# Buffers over the packets from the layer beneath until desired amount
|
|
||||||
# bytes of bytes are received.
|
|
||||||
self._recv_buffer = []
|
|
||||||
# These buffer over the build-up of a single frame.
|
|
||||||
self._frame_header = None
|
|
||||||
self._frame_length = None
|
|
||||||
self._frame_mask = None
|
|
||||||
self._cont_data = None
|
|
||||||
|
|
||||||
def fileno(self):
|
|
||||||
"""
|
|
||||||
Returns sock.fileno()
|
|
||||||
"""
|
|
||||||
return self.sock.fileno()
|
|
||||||
|
|
||||||
def set_mask_key(self, func):
|
|
||||||
"""
|
|
||||||
set function to create musk key. You can custumize mask key generator.
|
|
||||||
Mainly, this is for testing purpose.
|
|
||||||
|
|
||||||
func: callable object. the fuct must 1 argument as integer.
|
|
||||||
The argument means length of mask key.
|
|
||||||
This func must be return string(byte array),
|
|
||||||
which length is argument specified.
|
|
||||||
"""
|
|
||||||
self.get_mask_key = func
|
|
||||||
|
|
||||||
def gettimeout(self):
|
|
||||||
"""
|
|
||||||
Get the websocket timeout(second).
|
|
||||||
"""
|
|
||||||
return self.sock.gettimeout()
|
|
||||||
|
|
||||||
def settimeout(self, timeout):
|
|
||||||
"""
|
|
||||||
Set the timeout to the websocket.
|
|
||||||
|
|
||||||
timeout: timeout time(second).
|
|
||||||
"""
|
|
||||||
self.sock.settimeout(timeout)
|
|
||||||
|
|
||||||
timeout = property(gettimeout, settimeout)
|
|
||||||
|
|
||||||
def connect(self, url, **options):
|
|
||||||
"""
|
|
||||||
Connect to url. url is websocket url scheme. ie. ws://host:port/resource
|
|
||||||
You can customize using 'options'.
|
|
||||||
If you set "header" dict object, you can set your own custom header.
|
|
||||||
|
|
||||||
>>> ws = WebSocket()
|
|
||||||
>>> ws.connect("ws://echo.websocket.org/",
|
|
||||||
... header={"User-Agent: MyProgram",
|
|
||||||
... "x-custom: header"})
|
|
||||||
|
|
||||||
timeout: socket timeout time. This value is integer.
|
|
||||||
if you set None for this value,
|
|
||||||
it means "use DEFAULT_TIMEOUT value"
|
|
||||||
|
|
||||||
options: current support option is only "header".
|
|
||||||
if you set header as dict value,
|
|
||||||
the custom HTTP headers are added.
|
|
||||||
|
|
||||||
"""
|
|
||||||
hostname, port, resource, is_secure = _parse_url(url)
|
|
||||||
# TODO: we need to support proxy
|
|
||||||
self.sock.connect((hostname, port))
|
|
||||||
if is_secure:
|
|
||||||
if HAVE_SSL:
|
|
||||||
if self.sslopt is None:
|
|
||||||
sslopt = {}
|
|
||||||
else:
|
|
||||||
sslopt = self.sslopt
|
|
||||||
self.sock = ssl.wrap_socket(self.sock, **sslopt)
|
|
||||||
else:
|
|
||||||
raise WebSocketException("SSL not available.")
|
|
||||||
|
|
||||||
self._handshake(hostname, port, resource, **options)
|
|
||||||
|
|
||||||
def _handshake(self, host, port, resource, **options):
|
|
||||||
headers = []
|
|
||||||
headers.append("GET %s HTTP/1.1" % resource)
|
|
||||||
headers.append("Upgrade: websocket")
|
|
||||||
headers.append("Connection: Upgrade")
|
|
||||||
if port == 80:
|
|
||||||
hostport = host
|
|
||||||
else:
|
|
||||||
hostport = "%s:%d" % (host, port)
|
|
||||||
headers.append("Host: %s" % hostport)
|
|
||||||
|
|
||||||
if "origin" in options:
|
|
||||||
headers.append("Origin: %s" % options["origin"])
|
|
||||||
else:
|
|
||||||
headers.append("Origin: http://%s" % hostport)
|
|
||||||
|
|
||||||
key = _create_sec_websocket_key()
|
|
||||||
headers.append("Sec-WebSocket-Key: %s" % key)
|
|
||||||
headers.append("Sec-WebSocket-Version: %s" % VERSION)
|
|
||||||
if "header" in options:
|
|
||||||
headers.extend(options["header"])
|
|
||||||
|
|
||||||
headers.append("")
|
|
||||||
headers.append("")
|
|
||||||
|
|
||||||
header_str = "\r\n".join(headers)
|
|
||||||
self._send(header_str)
|
|
||||||
if TRACE_ENABLED:
|
|
||||||
LOG.debug("--- request header ---")
|
|
||||||
LOG.debug(header_str)
|
|
||||||
LOG.debug("-----------------------")
|
|
||||||
|
|
||||||
status, resp_headers = self._read_headers()
|
|
||||||
if status == 301:
|
|
||||||
# Redirect
|
|
||||||
raise WebsocketRedirect(resp_headers)
|
|
||||||
if status != 101:
|
|
||||||
self.close()
|
|
||||||
raise WebSocketException("Handshake Status %d" % status)
|
|
||||||
|
|
||||||
success = self._validate_header(resp_headers, key)
|
|
||||||
if not success:
|
|
||||||
self.close()
|
|
||||||
raise WebSocketException("Invalid WebSocket Header")
|
|
||||||
|
|
||||||
self.connected = True
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _validate_header(headers, key):
|
|
||||||
for k, v in _HEADERS_TO_CHECK.items():
|
|
||||||
r = headers.get(k, None)
|
|
||||||
if not r:
|
|
||||||
return False
|
|
||||||
r = r.lower()
|
|
||||||
if v != r:
|
|
||||||
return False
|
|
||||||
|
|
||||||
result = headers.get("sec-websocket-accept", None)
|
|
||||||
if not result:
|
|
||||||
return False
|
|
||||||
result = result.lower()
|
|
||||||
|
|
||||||
value = key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
|
|
||||||
hashed = base64.encodestring(hashlib.sha1(value).digest()).strip().lower()
|
|
||||||
return hashed == result
|
|
||||||
|
|
||||||
def _read_headers(self):
|
|
||||||
status = None
|
|
||||||
headers = {}
|
|
||||||
if TRACE_ENABLED:
|
|
||||||
LOG.debug("--- response header ---")
|
|
||||||
|
|
||||||
while True:
|
|
||||||
line = self._recv_line()
|
|
||||||
if line == "\r\n":
|
|
||||||
break
|
|
||||||
line = line.strip()
|
|
||||||
if TRACE_ENABLED:
|
|
||||||
LOG.debug(line)
|
|
||||||
if not status:
|
|
||||||
status_info = line.split(" ", 2)
|
|
||||||
status = int(status_info[1])
|
|
||||||
else:
|
|
||||||
kv = line.split(":", 1)
|
|
||||||
if len(kv) == 2:
|
|
||||||
key, value = kv
|
|
||||||
headers[key.lower()] = value.strip().lower()
|
|
||||||
else:
|
|
||||||
raise WebSocketException("Invalid header")
|
|
||||||
|
|
||||||
if TRACE_ENABLED:
|
|
||||||
LOG.debug("-----------------------")
|
|
||||||
|
|
||||||
return status, headers
|
|
||||||
|
|
||||||
def send(self, payload, opcode=ABNF.OPCODE_TEXT):
|
|
||||||
"""
|
|
||||||
Send the data as string.
|
|
||||||
|
|
||||||
payload: Payload must be utf-8 string or unicoce,
|
|
||||||
if the opcode is OPCODE_TEXT.
|
|
||||||
Otherwise, it must be string(byte array)
|
|
||||||
|
|
||||||
opcode: operation code to send. Please see OPCODE_XXX.
|
|
||||||
"""
|
|
||||||
frame = ABNF.create_frame(payload, opcode)
|
|
||||||
if self.get_mask_key:
|
|
||||||
frame.get_mask_key = self.get_mask_key
|
|
||||||
data = frame.format()
|
|
||||||
length = len(data)
|
|
||||||
if TRACE_ENABLED:
|
|
||||||
LOG.debug("send: %s", repr(data))
|
|
||||||
while data:
|
|
||||||
l = self._send(data)
|
|
||||||
data = data[l:]
|
|
||||||
return length
|
|
||||||
|
|
||||||
def send_binary(self, payload):
|
|
||||||
"""
|
|
||||||
send the payload
|
|
||||||
"""
|
|
||||||
return self.send(payload, ABNF.OPCODE_BINARY)
|
|
||||||
|
|
||||||
def ping(self, payload=""):
|
|
||||||
"""
|
|
||||||
send ping data.
|
|
||||||
|
|
||||||
payload: data payload to send server.
|
|
||||||
"""
|
|
||||||
self.send(payload, ABNF.OPCODE_PING)
|
|
||||||
|
|
||||||
def pong(self, payload):
|
|
||||||
"""
|
|
||||||
send pong data.
|
|
||||||
|
|
||||||
payload: data payload to send server.
|
|
||||||
"""
|
|
||||||
self.send(payload, ABNF.OPCODE_PONG)
|
|
||||||
|
|
||||||
def recv(self):
|
|
||||||
"""
|
|
||||||
Receive string data(byte array) from the server.
|
|
||||||
|
|
||||||
return value: string(byte array) value.
|
|
||||||
"""
|
|
||||||
_, data = self.recv_data()
|
|
||||||
return data
|
|
||||||
|
|
||||||
def recv_data(self):
|
|
||||||
"""
|
|
||||||
Recieve data with operation code.
|
|
||||||
|
|
||||||
return value: tuple of operation code and string(byte array) value.
|
|
||||||
"""
|
|
||||||
while True:
|
|
||||||
frame = self.recv_frame()
|
|
||||||
if not frame:
|
|
||||||
# handle error:
|
|
||||||
# 'NoneType' object has no attribute 'opcode'
|
|
||||||
raise WebSocketException("Not a valid frame %s" % frame)
|
|
||||||
elif frame.opcode in (ABNF.OPCODE_TEXT, ABNF.OPCODE_BINARY, ABNF.OPCODE_CONT):
|
|
||||||
if frame.opcode == ABNF.OPCODE_CONT and not self._cont_data:
|
|
||||||
raise WebSocketException("Illegal frame")
|
|
||||||
if self._cont_data:
|
|
||||||
self._cont_data[1] += frame.data
|
|
||||||
else:
|
|
||||||
self._cont_data = [frame.opcode, frame.data]
|
|
||||||
if frame.fin:
|
|
||||||
data = self._cont_data
|
|
||||||
self._cont_data = None
|
|
||||||
return data
|
|
||||||
elif frame.opcode == ABNF.OPCODE_CLOSE:
|
|
||||||
self.send_close()
|
|
||||||
return (frame.opcode, None)
|
|
||||||
elif frame.opcode == ABNF.OPCODE_PING:
|
|
||||||
self.pong(frame.data)
|
|
||||||
|
|
||||||
def recv_frame(self):
|
|
||||||
"""
|
|
||||||
recieve data as frame from server.
|
|
||||||
|
|
||||||
return value: ABNF frame object.
|
|
||||||
"""
|
|
||||||
# Header
|
|
||||||
if self._frame_header is None:
|
|
||||||
self._frame_header = self._recv_strict(2)
|
|
||||||
b1 = ord(self._frame_header[0])
|
|
||||||
fin = b1 >> 7 & 1
|
|
||||||
rsv1 = b1 >> 6 & 1
|
|
||||||
rsv2 = b1 >> 5 & 1
|
|
||||||
rsv3 = b1 >> 4 & 1
|
|
||||||
opcode = b1 & 0xf
|
|
||||||
b2 = ord(self._frame_header[1])
|
|
||||||
has_mask = b2 >> 7 & 1
|
|
||||||
# Frame length
|
|
||||||
if self._frame_length is None:
|
|
||||||
length_bits = b2 & 0x7f
|
|
||||||
if length_bits == 0x7e:
|
|
||||||
length_data = self._recv_strict(2)
|
|
||||||
self._frame_length = struct.unpack("!H", length_data)[0]
|
|
||||||
elif length_bits == 0x7f:
|
|
||||||
length_data = self._recv_strict(8)
|
|
||||||
self._frame_length = struct.unpack("!Q", length_data)[0]
|
|
||||||
else:
|
|
||||||
self._frame_length = length_bits
|
|
||||||
# Mask
|
|
||||||
if self._frame_mask is None:
|
|
||||||
self._frame_mask = self._recv_strict(4) if has_mask else ""
|
|
||||||
# Payload
|
|
||||||
payload = self._recv_strict(self._frame_length)
|
|
||||||
if has_mask:
|
|
||||||
payload = ABNF.mask(self._frame_mask, payload)
|
|
||||||
# Reset for next frame
|
|
||||||
self._frame_header = None
|
|
||||||
self._frame_length = None
|
|
||||||
self._frame_mask = None
|
|
||||||
return ABNF(fin, rsv1, rsv2, rsv3, opcode, has_mask, payload)
|
|
||||||
|
|
||||||
|
|
||||||
def send_close(self, status=STATUS_NORMAL, reason=""):
|
|
||||||
"""
|
|
||||||
send close data to the server.
|
|
||||||
|
|
||||||
status: status code to send. see STATUS_XXX.
|
|
||||||
|
|
||||||
reason: the reason to close. This must be string.
|
|
||||||
"""
|
|
||||||
if status < 0 or status >= ABNF.LENGTH_16:
|
|
||||||
raise ValueError("code is invalid range")
|
|
||||||
self.send(struct.pack('!H', status) + reason, ABNF.OPCODE_CLOSE)
|
|
||||||
|
|
||||||
def close(self, status=STATUS_NORMAL, reason=""):
|
|
||||||
"""
|
|
||||||
Close Websocket object
|
|
||||||
|
|
||||||
status: status code to send. see STATUS_XXX.
|
|
||||||
|
|
||||||
reason: the reason to close. This must be string.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
self.sock.shutdown(socket.SHUT_RDWR)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
self._closeInternal()
|
|
||||||
|
|
||||||
def _closeInternal(self):
|
|
||||||
self.connected = False
|
|
||||||
self.sock.close()
|
|
||||||
|
|
||||||
def _send(self, data):
|
|
||||||
try:
|
|
||||||
return self.sock.send(data.encode('utf-8'))
|
|
||||||
except socket.timeout as e:
|
|
||||||
raise WebSocketTimeoutException(e.args[0])
|
|
||||||
except Exception as e:
|
|
||||||
if "timed out" in e.args[0]:
|
|
||||||
raise WebSocketTimeoutException(e.args[0])
|
|
||||||
else:
|
|
||||||
raise e
|
|
||||||
|
|
||||||
def _recv(self, bufsize):
|
|
||||||
try:
|
|
||||||
bytes_ = self.sock.recv(bufsize)
|
|
||||||
except socket.timeout as e:
|
|
||||||
raise WebSocketTimeoutException(e.args[0])
|
|
||||||
except SSLError as e:
|
|
||||||
if e.args[0] == "The read operation timed out":
|
|
||||||
raise WebSocketTimeoutException(e.args[0])
|
|
||||||
else:
|
|
||||||
raise
|
|
||||||
if not bytes_:
|
|
||||||
raise WebSocketConnectionClosedException()
|
|
||||||
return bytes_
|
|
||||||
|
|
||||||
def _recv_strict(self, bufsize):
|
|
||||||
shortage = bufsize - sum(len(x) for x in self._recv_buffer)
|
|
||||||
while shortage > 0:
|
|
||||||
bytes_ = self._recv(shortage)
|
|
||||||
self._recv_buffer.append(bytes_)
|
|
||||||
shortage -= len(bytes_)
|
|
||||||
unified = "".join(self._recv_buffer)
|
|
||||||
if shortage == 0:
|
|
||||||
self._recv_buffer = []
|
|
||||||
return unified
|
|
||||||
else:
|
|
||||||
self._recv_buffer = [unified[bufsize:]]
|
|
||||||
return unified[:bufsize]
|
|
||||||
|
|
||||||
def _recv_line(self):
|
|
||||||
line = []
|
|
||||||
while True:
|
|
||||||
c = self._recv(1)
|
|
||||||
line.append(c)
|
|
||||||
if c == "\n":
|
|
||||||
break
|
|
||||||
return "".join(line)
|
|
||||||
|
|
||||||
|
|
||||||
class WebSocketApp(object):
|
|
||||||
"""
|
|
||||||
Higher level of APIs are provided.
|
|
||||||
The interface is like JavaScript WebSocket object.
|
|
||||||
"""
|
|
||||||
def __init__(self, url, header=None,
|
|
||||||
on_open=None, on_message=None, on_error=None,
|
|
||||||
on_close=None, keep_running=True, get_mask_key=None):
|
|
||||||
"""
|
|
||||||
url: websocket url.
|
|
||||||
header: custom header for websocket handshake.
|
|
||||||
on_open: callable object which is called at opening websocket.
|
|
||||||
this function has one argument. The arugment is this class object.
|
|
||||||
on_message: callbale object which is called when recieved data.
|
|
||||||
on_message has 2 arguments.
|
|
||||||
The 1st arugment is this class object.
|
|
||||||
The passing 2nd arugment is utf-8 string which we get from the server.
|
|
||||||
on_error: callable object which is called when we get error.
|
|
||||||
on_error has 2 arguments.
|
|
||||||
The 1st arugment is this class object.
|
|
||||||
The passing 2nd arugment is exception object.
|
|
||||||
on_close: callable object which is called when closed the connection.
|
|
||||||
this function has one argument. The arugment is this class object.
|
|
||||||
keep_running: a boolean flag indicating whether the app's main loop should
|
|
||||||
keep running, defaults to True
|
|
||||||
get_mask_key: a callable to produce new mask keys, see the WebSocket.set_mask_key's
|
|
||||||
docstring for more information
|
|
||||||
"""
|
|
||||||
self.url = url
|
|
||||||
self.header = [] if header is None else header
|
|
||||||
self.on_open = on_open
|
|
||||||
self.on_message = on_message
|
|
||||||
self.on_error = on_error
|
|
||||||
self.on_close = on_close
|
|
||||||
self.keep_running = keep_running
|
|
||||||
self.get_mask_key = get_mask_key
|
|
||||||
self.sock = None
|
|
||||||
|
|
||||||
def send(self, data, opcode=ABNF.OPCODE_TEXT):
|
|
||||||
"""
|
|
||||||
send message.
|
|
||||||
data: message to send. If you set opcode to OPCODE_TEXT, data must be utf-8 string or unicode.
|
|
||||||
opcode: operation code of data. default is OPCODE_TEXT.
|
|
||||||
"""
|
|
||||||
if self.sock.send(data, opcode) == 0:
|
|
||||||
raise WebSocketConnectionClosedException()
|
|
||||||
|
|
||||||
def close(self):
|
|
||||||
"""
|
|
||||||
close websocket connection.
|
|
||||||
"""
|
|
||||||
self.keep_running = False
|
|
||||||
if self.sock != None:
|
|
||||||
self.sock.close()
|
|
||||||
|
|
||||||
def _send_ping(self, interval):
|
|
||||||
while True:
|
|
||||||
for _ in range(interval):
|
|
||||||
app.APP.monitor.waitForAbort(1)
|
|
||||||
if not self.keep_running:
|
|
||||||
return
|
|
||||||
self.sock.ping()
|
|
||||||
|
|
||||||
def run_forever(self, sockopt=None, sslopt=None, ping_interval=0):
|
|
||||||
"""
|
|
||||||
run event loop for WebSocket framework.
|
|
||||||
This loop is infinite loop and is alive during websocket is available.
|
|
||||||
sockopt: values for socket.setsockopt.
|
|
||||||
sockopt must be tuple and each element is argument of
|
|
||||||
sock.setscokopt.
|
|
||||||
sslopt: ssl socket optional dict.
|
|
||||||
ping_interval: automatically send "ping" command every specified
|
|
||||||
period(second)
|
|
||||||
if set to 0, not send automatically.
|
|
||||||
"""
|
|
||||||
if sockopt is None:
|
|
||||||
sockopt = []
|
|
||||||
if sslopt is None:
|
|
||||||
sslopt = {}
|
|
||||||
if self.sock:
|
|
||||||
raise WebSocketException("socket is already opened")
|
|
||||||
thread = None
|
|
||||||
self.keep_running = True
|
|
||||||
|
|
||||||
try:
|
|
||||||
self.sock = WebSocket(self.get_mask_key,
|
|
||||||
sockopt=sockopt,
|
|
||||||
sslopt=sslopt)
|
|
||||||
self.sock.settimeout(DEFAULT_TIMEOUT)
|
|
||||||
self.sock.connect(self.url, header=self.header)
|
|
||||||
self._callback(self.on_open)
|
|
||||||
|
|
||||||
if ping_interval:
|
|
||||||
thread = threading.Thread(target=self._send_ping,
|
|
||||||
args=(ping_interval,))
|
|
||||||
thread.setDaemon(True)
|
|
||||||
thread.start()
|
|
||||||
|
|
||||||
while self.keep_running:
|
|
||||||
try:
|
|
||||||
data = self.sock.recv()
|
|
||||||
if data is None or self.keep_running is False:
|
|
||||||
break
|
|
||||||
self._callback(self.on_message, data)
|
|
||||||
except Exception as e:
|
|
||||||
if "timed out" not in e.args[0]:
|
|
||||||
raise e
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self._callback(self.on_error, e)
|
|
||||||
finally:
|
|
||||||
if thread:
|
|
||||||
self.keep_running = False
|
|
||||||
self.sock.close()
|
|
||||||
self._callback(self.on_close)
|
|
||||||
self.sock = None
|
|
||||||
|
|
||||||
def _callback(self, callback, *args):
|
|
||||||
if callback:
|
|
||||||
try:
|
|
||||||
callback(self, *args)
|
|
||||||
except Exception as e:
|
|
||||||
LOG.error(e)
|
|
||||||
_, _, tb = sys.exc_info()
|
|
||||||
traceback.print_tb(tb)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
enable_trace(True)
|
|
||||||
WEBSOCKET = create_connection("ws://echo.websocket.org/")
|
|
||||||
LOG.info("Sending 'Hello, World'...")
|
|
||||||
WEBSOCKET.send("Hello, World")
|
|
||||||
LOG.info("Sent")
|
|
||||||
LOG.info("Receiving...")
|
|
||||||
RESULT = WEBSOCKET.recv()
|
|
||||||
LOG.info("Received '%s'", RESULT)
|
|
||||||
WEBSOCKET.close()
|
|
28
resources/lib/websocket/__init__.py
Normal file
28
resources/lib/websocket/__init__.py
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
"""
|
||||||
|
websocket - WebSocket client library for Python
|
||||||
|
|
||||||
|
Copyright (C) 2010 Hiroki Ohtani(liris)
|
||||||
|
|
||||||
|
This library is free software; you can redistribute it and/or
|
||||||
|
modify it under the terms of the GNU Lesser General Public
|
||||||
|
License as published by the Free Software Foundation; either
|
||||||
|
version 2.1 of the License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This library is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
Lesser General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Lesser General Public
|
||||||
|
License along with this library; if not, write to the Free Software
|
||||||
|
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
|
||||||
|
"""
|
||||||
|
from ._abnf import *
|
||||||
|
from ._app import WebSocketApp
|
||||||
|
from ._core import *
|
||||||
|
from ._exceptions import *
|
||||||
|
from ._logging import *
|
||||||
|
from ._socket import *
|
||||||
|
|
||||||
|
__version__ = "0.58.0"
|
458
resources/lib/websocket/_abnf.py
Normal file
458
resources/lib/websocket/_abnf.py
Normal file
|
@ -0,0 +1,458 @@
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""
|
||||||
|
websocket - WebSocket client library for Python
|
||||||
|
|
||||||
|
Copyright (C) 2010 Hiroki Ohtani(liris)
|
||||||
|
|
||||||
|
This library is free software; you can redistribute it and/or
|
||||||
|
modify it under the terms of the GNU Lesser General Public
|
||||||
|
License as published by the Free Software Foundation; either
|
||||||
|
version 2.1 of the License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This library is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
Lesser General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Lesser General Public
|
||||||
|
License along with this library; if not, write to the Free Software
|
||||||
|
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
|
||||||
|
"""
|
||||||
|
import array
|
||||||
|
import os
|
||||||
|
import struct
|
||||||
|
|
||||||
|
import six
|
||||||
|
|
||||||
|
from ._exceptions import *
|
||||||
|
from ._utils import validate_utf8
|
||||||
|
from threading import Lock
|
||||||
|
|
||||||
|
try:
|
||||||
|
if six.PY3:
|
||||||
|
import numpy
|
||||||
|
else:
|
||||||
|
numpy = None
|
||||||
|
except ImportError:
|
||||||
|
numpy = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
# If wsaccel is available we use compiled routines to mask data.
|
||||||
|
if not numpy:
|
||||||
|
from wsaccel.xormask import XorMaskerSimple
|
||||||
|
|
||||||
|
def _mask(_m, _d):
|
||||||
|
return XorMaskerSimple(_m).process(_d)
|
||||||
|
except ImportError:
|
||||||
|
# wsaccel is not available, we rely on python implementations.
|
||||||
|
def _mask(_m, _d):
|
||||||
|
for i in range(len(_d)):
|
||||||
|
_d[i] ^= _m[i % 4]
|
||||||
|
|
||||||
|
if six.PY3:
|
||||||
|
return _d.tobytes()
|
||||||
|
else:
|
||||||
|
return _d.tostring()
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
'ABNF', 'continuous_frame', 'frame_buffer',
|
||||||
|
'STATUS_NORMAL',
|
||||||
|
'STATUS_GOING_AWAY',
|
||||||
|
'STATUS_PROTOCOL_ERROR',
|
||||||
|
'STATUS_UNSUPPORTED_DATA_TYPE',
|
||||||
|
'STATUS_STATUS_NOT_AVAILABLE',
|
||||||
|
'STATUS_ABNORMAL_CLOSED',
|
||||||
|
'STATUS_INVALID_PAYLOAD',
|
||||||
|
'STATUS_POLICY_VIOLATION',
|
||||||
|
'STATUS_MESSAGE_TOO_BIG',
|
||||||
|
'STATUS_INVALID_EXTENSION',
|
||||||
|
'STATUS_UNEXPECTED_CONDITION',
|
||||||
|
'STATUS_BAD_GATEWAY',
|
||||||
|
'STATUS_TLS_HANDSHAKE_ERROR',
|
||||||
|
]
|
||||||
|
|
||||||
|
# closing frame status codes.
|
||||||
|
STATUS_NORMAL = 1000
|
||||||
|
STATUS_GOING_AWAY = 1001
|
||||||
|
STATUS_PROTOCOL_ERROR = 1002
|
||||||
|
STATUS_UNSUPPORTED_DATA_TYPE = 1003
|
||||||
|
STATUS_STATUS_NOT_AVAILABLE = 1005
|
||||||
|
STATUS_ABNORMAL_CLOSED = 1006
|
||||||
|
STATUS_INVALID_PAYLOAD = 1007
|
||||||
|
STATUS_POLICY_VIOLATION = 1008
|
||||||
|
STATUS_MESSAGE_TOO_BIG = 1009
|
||||||
|
STATUS_INVALID_EXTENSION = 1010
|
||||||
|
STATUS_UNEXPECTED_CONDITION = 1011
|
||||||
|
STATUS_BAD_GATEWAY = 1014
|
||||||
|
STATUS_TLS_HANDSHAKE_ERROR = 1015
|
||||||
|
|
||||||
|
VALID_CLOSE_STATUS = (
|
||||||
|
STATUS_NORMAL,
|
||||||
|
STATUS_GOING_AWAY,
|
||||||
|
STATUS_PROTOCOL_ERROR,
|
||||||
|
STATUS_UNSUPPORTED_DATA_TYPE,
|
||||||
|
STATUS_INVALID_PAYLOAD,
|
||||||
|
STATUS_POLICY_VIOLATION,
|
||||||
|
STATUS_MESSAGE_TOO_BIG,
|
||||||
|
STATUS_INVALID_EXTENSION,
|
||||||
|
STATUS_UNEXPECTED_CONDITION,
|
||||||
|
STATUS_BAD_GATEWAY,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ABNF(object):
|
||||||
|
"""
|
||||||
|
ABNF frame class.
|
||||||
|
See http://tools.ietf.org/html/rfc5234
|
||||||
|
and http://tools.ietf.org/html/rfc6455#section-5.2
|
||||||
|
"""
|
||||||
|
|
||||||
|
# operation code values.
|
||||||
|
OPCODE_CONT = 0x0
|
||||||
|
OPCODE_TEXT = 0x1
|
||||||
|
OPCODE_BINARY = 0x2
|
||||||
|
OPCODE_CLOSE = 0x8
|
||||||
|
OPCODE_PING = 0x9
|
||||||
|
OPCODE_PONG = 0xa
|
||||||
|
|
||||||
|
# available operation code value tuple
|
||||||
|
OPCODES = (OPCODE_CONT, OPCODE_TEXT, OPCODE_BINARY, OPCODE_CLOSE,
|
||||||
|
OPCODE_PING, OPCODE_PONG)
|
||||||
|
|
||||||
|
# opcode human readable string
|
||||||
|
OPCODE_MAP = {
|
||||||
|
OPCODE_CONT: "cont",
|
||||||
|
OPCODE_TEXT: "text",
|
||||||
|
OPCODE_BINARY: "binary",
|
||||||
|
OPCODE_CLOSE: "close",
|
||||||
|
OPCODE_PING: "ping",
|
||||||
|
OPCODE_PONG: "pong"
|
||||||
|
}
|
||||||
|
|
||||||
|
# data length threshold.
|
||||||
|
LENGTH_7 = 0x7e
|
||||||
|
LENGTH_16 = 1 << 16
|
||||||
|
LENGTH_63 = 1 << 63
|
||||||
|
|
||||||
|
def __init__(self, fin=0, rsv1=0, rsv2=0, rsv3=0,
|
||||||
|
opcode=OPCODE_TEXT, mask=1, data=""):
|
||||||
|
"""
|
||||||
|
Constructor for ABNF. Please check RFC for arguments.
|
||||||
|
"""
|
||||||
|
self.fin = fin
|
||||||
|
self.rsv1 = rsv1
|
||||||
|
self.rsv2 = rsv2
|
||||||
|
self.rsv3 = rsv3
|
||||||
|
self.opcode = opcode
|
||||||
|
self.mask = mask
|
||||||
|
if data is None:
|
||||||
|
data = ""
|
||||||
|
self.data = data
|
||||||
|
self.get_mask_key = os.urandom
|
||||||
|
|
||||||
|
def validate(self, skip_utf8_validation=False):
|
||||||
|
"""
|
||||||
|
Validate the ABNF frame.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
skip_utf8_validation: skip utf8 validation.
|
||||||
|
"""
|
||||||
|
if self.rsv1 or self.rsv2 or self.rsv3:
|
||||||
|
raise WebSocketProtocolException("rsv is not implemented, yet")
|
||||||
|
|
||||||
|
if self.opcode not in ABNF.OPCODES:
|
||||||
|
raise WebSocketProtocolException("Invalid opcode %r", self.opcode)
|
||||||
|
|
||||||
|
if self.opcode == ABNF.OPCODE_PING and not self.fin:
|
||||||
|
raise WebSocketProtocolException("Invalid ping frame.")
|
||||||
|
|
||||||
|
if self.opcode == ABNF.OPCODE_CLOSE:
|
||||||
|
l = len(self.data)
|
||||||
|
if not l:
|
||||||
|
return
|
||||||
|
if l == 1 or l >= 126:
|
||||||
|
raise WebSocketProtocolException("Invalid close frame.")
|
||||||
|
if l > 2 and not skip_utf8_validation and not validate_utf8(self.data[2:]):
|
||||||
|
raise WebSocketProtocolException("Invalid close frame.")
|
||||||
|
|
||||||
|
code = 256 * \
|
||||||
|
six.byte2int(self.data[0:1]) + six.byte2int(self.data[1:2])
|
||||||
|
if not self._is_valid_close_status(code):
|
||||||
|
raise WebSocketProtocolException("Invalid close opcode.")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _is_valid_close_status(code):
|
||||||
|
return code in VALID_CLOSE_STATUS or (3000 <= code < 5000)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "fin=" + str(self.fin) \
|
||||||
|
+ " opcode=" + str(self.opcode) \
|
||||||
|
+ " data=" + str(self.data)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def create_frame(data, opcode, fin=1):
|
||||||
|
"""
|
||||||
|
Create frame to send text, binary and other data.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
data: <type>
|
||||||
|
data to send. This is string value(byte array).
|
||||||
|
If opcode is OPCODE_TEXT and this value is unicode,
|
||||||
|
data value is converted into unicode string, automatically.
|
||||||
|
opcode: <type>
|
||||||
|
operation code. please see OPCODE_XXX.
|
||||||
|
fin: <type>
|
||||||
|
fin flag. if set to 0, create continue fragmentation.
|
||||||
|
"""
|
||||||
|
if opcode == ABNF.OPCODE_TEXT and isinstance(data, six.text_type):
|
||||||
|
data = data.encode("utf-8")
|
||||||
|
# mask must be set if send data from client
|
||||||
|
return ABNF(fin, 0, 0, 0, opcode, 1, data)
|
||||||
|
|
||||||
|
def format(self):
|
||||||
|
"""
|
||||||
|
Format this object to string(byte array) to send data to server.
|
||||||
|
"""
|
||||||
|
if any(x not in (0, 1) for x in [self.fin, self.rsv1, self.rsv2, self.rsv3]):
|
||||||
|
raise ValueError("not 0 or 1")
|
||||||
|
if self.opcode not in ABNF.OPCODES:
|
||||||
|
raise ValueError("Invalid OPCODE")
|
||||||
|
length = len(self.data)
|
||||||
|
if length >= ABNF.LENGTH_63:
|
||||||
|
raise ValueError("data is too long")
|
||||||
|
|
||||||
|
frame_header = chr(self.fin << 7
|
||||||
|
| self.rsv1 << 6 | self.rsv2 << 5 | self.rsv3 << 4
|
||||||
|
| self.opcode)
|
||||||
|
if length < ABNF.LENGTH_7:
|
||||||
|
frame_header += chr(self.mask << 7 | length)
|
||||||
|
frame_header = six.b(frame_header)
|
||||||
|
elif length < ABNF.LENGTH_16:
|
||||||
|
frame_header += chr(self.mask << 7 | 0x7e)
|
||||||
|
frame_header = six.b(frame_header)
|
||||||
|
frame_header += struct.pack("!H", length)
|
||||||
|
else:
|
||||||
|
frame_header += chr(self.mask << 7 | 0x7f)
|
||||||
|
frame_header = six.b(frame_header)
|
||||||
|
frame_header += struct.pack("!Q", length)
|
||||||
|
|
||||||
|
if not self.mask:
|
||||||
|
return frame_header + self.data
|
||||||
|
else:
|
||||||
|
mask_key = self.get_mask_key(4)
|
||||||
|
return frame_header + self._get_masked(mask_key)
|
||||||
|
|
||||||
|
def _get_masked(self, mask_key):
|
||||||
|
s = ABNF.mask(mask_key, self.data)
|
||||||
|
|
||||||
|
if isinstance(mask_key, six.text_type):
|
||||||
|
mask_key = mask_key.encode('utf-8')
|
||||||
|
|
||||||
|
return mask_key + s
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def mask(mask_key, data):
|
||||||
|
"""
|
||||||
|
Mask or unmask data. Just do xor for each byte
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
mask_key: <type>
|
||||||
|
4 byte string(byte).
|
||||||
|
data: <type>
|
||||||
|
data to mask/unmask.
|
||||||
|
"""
|
||||||
|
if data is None:
|
||||||
|
data = ""
|
||||||
|
|
||||||
|
if isinstance(mask_key, six.text_type):
|
||||||
|
mask_key = six.b(mask_key)
|
||||||
|
|
||||||
|
if isinstance(data, six.text_type):
|
||||||
|
data = six.b(data)
|
||||||
|
|
||||||
|
if numpy:
|
||||||
|
origlen = len(data)
|
||||||
|
_mask_key = mask_key[3] << 24 | mask_key[2] << 16 | mask_key[1] << 8 | mask_key[0]
|
||||||
|
|
||||||
|
# We need data to be a multiple of four...
|
||||||
|
data += bytes(" " * (4 - (len(data) % 4)), "us-ascii")
|
||||||
|
a = numpy.frombuffer(data, dtype="uint32")
|
||||||
|
masked = numpy.bitwise_xor(a, [_mask_key]).astype("uint32")
|
||||||
|
if len(data) > origlen:
|
||||||
|
return masked.tobytes()[:origlen]
|
||||||
|
return masked.tobytes()
|
||||||
|
else:
|
||||||
|
_m = array.array("B", mask_key)
|
||||||
|
_d = array.array("B", data)
|
||||||
|
return _mask(_m, _d)
|
||||||
|
|
||||||
|
|
||||||
|
class frame_buffer(object):
|
||||||
|
_HEADER_MASK_INDEX = 5
|
||||||
|
_HEADER_LENGTH_INDEX = 6
|
||||||
|
|
||||||
|
def __init__(self, recv_fn, skip_utf8_validation):
|
||||||
|
self.recv = recv_fn
|
||||||
|
self.skip_utf8_validation = skip_utf8_validation
|
||||||
|
# Buffers over the packets from the layer beneath until desired amount
|
||||||
|
# bytes of bytes are received.
|
||||||
|
self.recv_buffer = []
|
||||||
|
self.clear()
|
||||||
|
self.lock = Lock()
|
||||||
|
|
||||||
|
def clear(self):
|
||||||
|
self.header = None
|
||||||
|
self.length = None
|
||||||
|
self.mask = None
|
||||||
|
|
||||||
|
def has_received_header(self):
|
||||||
|
return self.header is None
|
||||||
|
|
||||||
|
def recv_header(self):
|
||||||
|
header = self.recv_strict(2)
|
||||||
|
b1 = header[0]
|
||||||
|
|
||||||
|
if six.PY2:
|
||||||
|
b1 = ord(b1)
|
||||||
|
|
||||||
|
fin = b1 >> 7 & 1
|
||||||
|
rsv1 = b1 >> 6 & 1
|
||||||
|
rsv2 = b1 >> 5 & 1
|
||||||
|
rsv3 = b1 >> 4 & 1
|
||||||
|
opcode = b1 & 0xf
|
||||||
|
b2 = header[1]
|
||||||
|
|
||||||
|
if six.PY2:
|
||||||
|
b2 = ord(b2)
|
||||||
|
|
||||||
|
has_mask = b2 >> 7 & 1
|
||||||
|
length_bits = b2 & 0x7f
|
||||||
|
|
||||||
|
self.header = (fin, rsv1, rsv2, rsv3, opcode, has_mask, length_bits)
|
||||||
|
|
||||||
|
def has_mask(self):
|
||||||
|
if not self.header:
|
||||||
|
return False
|
||||||
|
return self.header[frame_buffer._HEADER_MASK_INDEX]
|
||||||
|
|
||||||
|
def has_received_length(self):
|
||||||
|
return self.length is None
|
||||||
|
|
||||||
|
def recv_length(self):
|
||||||
|
bits = self.header[frame_buffer._HEADER_LENGTH_INDEX]
|
||||||
|
length_bits = bits & 0x7f
|
||||||
|
if length_bits == 0x7e:
|
||||||
|
v = self.recv_strict(2)
|
||||||
|
self.length = struct.unpack("!H", v)[0]
|
||||||
|
elif length_bits == 0x7f:
|
||||||
|
v = self.recv_strict(8)
|
||||||
|
self.length = struct.unpack("!Q", v)[0]
|
||||||
|
else:
|
||||||
|
self.length = length_bits
|
||||||
|
|
||||||
|
def has_received_mask(self):
|
||||||
|
return self.mask is None
|
||||||
|
|
||||||
|
def recv_mask(self):
|
||||||
|
self.mask = self.recv_strict(4) if self.has_mask() else ""
|
||||||
|
|
||||||
|
def recv_frame(self):
|
||||||
|
|
||||||
|
with self.lock:
|
||||||
|
# Header
|
||||||
|
if self.has_received_header():
|
||||||
|
self.recv_header()
|
||||||
|
(fin, rsv1, rsv2, rsv3, opcode, has_mask, _) = self.header
|
||||||
|
|
||||||
|
# Frame length
|
||||||
|
if self.has_received_length():
|
||||||
|
self.recv_length()
|
||||||
|
length = self.length
|
||||||
|
|
||||||
|
# Mask
|
||||||
|
if self.has_received_mask():
|
||||||
|
self.recv_mask()
|
||||||
|
mask = self.mask
|
||||||
|
|
||||||
|
# Payload
|
||||||
|
payload = self.recv_strict(length)
|
||||||
|
if has_mask:
|
||||||
|
payload = ABNF.mask(mask, payload)
|
||||||
|
|
||||||
|
# Reset for next frame
|
||||||
|
self.clear()
|
||||||
|
|
||||||
|
frame = ABNF(fin, rsv1, rsv2, rsv3, opcode, has_mask, payload)
|
||||||
|
frame.validate(self.skip_utf8_validation)
|
||||||
|
|
||||||
|
return frame
|
||||||
|
|
||||||
|
def recv_strict(self, bufsize):
|
||||||
|
shortage = bufsize - sum(len(x) for x in self.recv_buffer)
|
||||||
|
while shortage > 0:
|
||||||
|
# Limit buffer size that we pass to socket.recv() to avoid
|
||||||
|
# fragmenting the heap -- the number of bytes recv() actually
|
||||||
|
# reads is limited by socket buffer and is relatively small,
|
||||||
|
# yet passing large numbers repeatedly causes lots of large
|
||||||
|
# buffers allocated and then shrunk, which results in
|
||||||
|
# fragmentation.
|
||||||
|
bytes_ = self.recv(min(16384, shortage))
|
||||||
|
self.recv_buffer.append(bytes_)
|
||||||
|
shortage -= len(bytes_)
|
||||||
|
|
||||||
|
unified = six.b("").join(self.recv_buffer)
|
||||||
|
|
||||||
|
if shortage == 0:
|
||||||
|
self.recv_buffer = []
|
||||||
|
return unified
|
||||||
|
else:
|
||||||
|
self.recv_buffer = [unified[bufsize:]]
|
||||||
|
return unified[:bufsize]
|
||||||
|
|
||||||
|
|
||||||
|
class continuous_frame(object):
|
||||||
|
|
||||||
|
def __init__(self, fire_cont_frame, skip_utf8_validation):
|
||||||
|
self.fire_cont_frame = fire_cont_frame
|
||||||
|
self.skip_utf8_validation = skip_utf8_validation
|
||||||
|
self.cont_data = None
|
||||||
|
self.recving_frames = None
|
||||||
|
|
||||||
|
def validate(self, frame):
|
||||||
|
if not self.recving_frames and frame.opcode == ABNF.OPCODE_CONT:
|
||||||
|
raise WebSocketProtocolException("Illegal frame")
|
||||||
|
if self.recving_frames and \
|
||||||
|
frame.opcode in (ABNF.OPCODE_TEXT, ABNF.OPCODE_BINARY):
|
||||||
|
raise WebSocketProtocolException("Illegal frame")
|
||||||
|
|
||||||
|
def add(self, frame):
|
||||||
|
if self.cont_data:
|
||||||
|
self.cont_data[1] += frame.data
|
||||||
|
else:
|
||||||
|
if frame.opcode in (ABNF.OPCODE_TEXT, ABNF.OPCODE_BINARY):
|
||||||
|
self.recving_frames = frame.opcode
|
||||||
|
self.cont_data = [frame.opcode, frame.data]
|
||||||
|
|
||||||
|
if frame.fin:
|
||||||
|
self.recving_frames = None
|
||||||
|
|
||||||
|
def is_fire(self, frame):
|
||||||
|
return frame.fin or self.fire_cont_frame
|
||||||
|
|
||||||
|
def extract(self, frame):
|
||||||
|
data = self.cont_data
|
||||||
|
self.cont_data = None
|
||||||
|
frame.data = data[1]
|
||||||
|
if not self.fire_cont_frame and data[0] == ABNF.OPCODE_TEXT and not self.skip_utf8_validation and not validate_utf8(frame.data):
|
||||||
|
raise WebSocketPayloadException(
|
||||||
|
"cannot decode: " + repr(frame.data))
|
||||||
|
|
||||||
|
return [data[0], frame]
|
394
resources/lib/websocket/_app.py
Normal file
394
resources/lib/websocket/_app.py
Normal file
|
@ -0,0 +1,394 @@
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""
|
||||||
|
websocket - WebSocket client library for Python
|
||||||
|
|
||||||
|
Copyright (C) 2010 Hiroki Ohtani(liris)
|
||||||
|
|
||||||
|
This library is free software; you can redistribute it and/or
|
||||||
|
modify it under the terms of the GNU Lesser General Public
|
||||||
|
License as published by the Free Software Foundation; either
|
||||||
|
version 2.1 of the License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This library is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
Lesser General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Lesser General Public
|
||||||
|
License along with this library; if not, write to the Free Software
|
||||||
|
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
|
||||||
|
"""
|
||||||
|
import inspect
|
||||||
|
import select
|
||||||
|
import sys
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
import six
|
||||||
|
|
||||||
|
from ._abnf import ABNF
|
||||||
|
from ._core import WebSocket, getdefaulttimeout
|
||||||
|
from ._exceptions import *
|
||||||
|
from . import _logging
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ["WebSocketApp"]
|
||||||
|
|
||||||
|
class Dispatcher:
|
||||||
|
"""
|
||||||
|
Dispatcher
|
||||||
|
"""
|
||||||
|
def __init__(self, app, ping_timeout):
|
||||||
|
self.app = app
|
||||||
|
self.ping_timeout = ping_timeout
|
||||||
|
|
||||||
|
def read(self, sock, read_callback, check_callback):
|
||||||
|
while self.app.keep_running:
|
||||||
|
r, w, e = select.select(
|
||||||
|
(self.app.sock.sock, ), (), (), self.ping_timeout)
|
||||||
|
if r:
|
||||||
|
if not read_callback():
|
||||||
|
break
|
||||||
|
check_callback()
|
||||||
|
|
||||||
|
class SSLDispatcher:
|
||||||
|
"""
|
||||||
|
SSLDispatcher
|
||||||
|
"""
|
||||||
|
def __init__(self, app, ping_timeout):
|
||||||
|
self.app = app
|
||||||
|
self.ping_timeout = ping_timeout
|
||||||
|
|
||||||
|
def read(self, sock, read_callback, check_callback):
|
||||||
|
while self.app.keep_running:
|
||||||
|
r = self.select()
|
||||||
|
if r:
|
||||||
|
if not read_callback():
|
||||||
|
break
|
||||||
|
check_callback()
|
||||||
|
|
||||||
|
def select(self):
|
||||||
|
sock = self.app.sock.sock
|
||||||
|
if sock.pending():
|
||||||
|
return [sock,]
|
||||||
|
|
||||||
|
r, w, e = select.select((sock, ), (), (), self.ping_timeout)
|
||||||
|
return r
|
||||||
|
|
||||||
|
|
||||||
|
class WebSocketApp(object):
|
||||||
|
"""
|
||||||
|
Higher level of APIs are provided. The interface is like JavaScript WebSocket object.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, url, header=None,
|
||||||
|
on_open=None, on_message=None, on_error=None,
|
||||||
|
on_close=None, on_ping=None, on_pong=None,
|
||||||
|
on_cont_message=None,
|
||||||
|
keep_running=True, get_mask_key=None, cookie=None,
|
||||||
|
subprotocols=None,
|
||||||
|
on_data=None):
|
||||||
|
"""
|
||||||
|
WebSocketApp initialization
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
url: <type>
|
||||||
|
websocket url.
|
||||||
|
header: list or dict
|
||||||
|
custom header for websocket handshake.
|
||||||
|
on_open: <type>
|
||||||
|
callable object which is called at opening websocket.
|
||||||
|
this function has one argument. The argument is this class object.
|
||||||
|
on_message: <type>
|
||||||
|
callable object which is called when received data.
|
||||||
|
on_message has 2 arguments.
|
||||||
|
The 1st argument is this class object.
|
||||||
|
The 2nd argument is utf-8 string which we get from the server.
|
||||||
|
on_error: <type>
|
||||||
|
callable object which is called when we get error.
|
||||||
|
on_error has 2 arguments.
|
||||||
|
The 1st argument is this class object.
|
||||||
|
The 2nd argument is exception object.
|
||||||
|
on_close: <type>
|
||||||
|
callable object which is called when closed the connection.
|
||||||
|
this function has one argument. The argument is this class object.
|
||||||
|
on_cont_message: <type>
|
||||||
|
callback object which is called when receive continued
|
||||||
|
frame data.
|
||||||
|
on_cont_message has 3 arguments.
|
||||||
|
The 1st argument is this class object.
|
||||||
|
The 2nd argument is utf-8 string which we get from the server.
|
||||||
|
The 3rd argument is continue flag. if 0, the data continue
|
||||||
|
to next frame data
|
||||||
|
on_data: <type>
|
||||||
|
callback object which is called when a message received.
|
||||||
|
This is called before on_message or on_cont_message,
|
||||||
|
and then on_message or on_cont_message is called.
|
||||||
|
on_data has 4 argument.
|
||||||
|
The 1st argument is this class object.
|
||||||
|
The 2nd argument is utf-8 string which we get from the server.
|
||||||
|
The 3rd argument is data type. ABNF.OPCODE_TEXT or ABNF.OPCODE_BINARY will be came.
|
||||||
|
The 4th argument is continue flag. if 0, the data continue
|
||||||
|
keep_running: <type>
|
||||||
|
this parameter is obsolete and ignored.
|
||||||
|
get_mask_key: func
|
||||||
|
a callable to produce new mask keys,
|
||||||
|
see the WebSocket.set_mask_key's docstring for more information
|
||||||
|
cookie: str
|
||||||
|
cookie value.
|
||||||
|
subprotocols: <type>
|
||||||
|
array of available sub protocols. default is None.
|
||||||
|
"""
|
||||||
|
self.url = url
|
||||||
|
self.header = header if header is not None else []
|
||||||
|
self.cookie = cookie
|
||||||
|
|
||||||
|
self.on_open = on_open
|
||||||
|
self.on_message = on_message
|
||||||
|
self.on_data = on_data
|
||||||
|
self.on_error = on_error
|
||||||
|
self.on_close = on_close
|
||||||
|
self.on_ping = on_ping
|
||||||
|
self.on_pong = on_pong
|
||||||
|
self.on_cont_message = on_cont_message
|
||||||
|
self.keep_running = False
|
||||||
|
self.get_mask_key = get_mask_key
|
||||||
|
self.sock = None
|
||||||
|
self.last_ping_tm = 0
|
||||||
|
self.last_pong_tm = 0
|
||||||
|
self.subprotocols = subprotocols
|
||||||
|
|
||||||
|
def send(self, data, opcode=ABNF.OPCODE_TEXT):
|
||||||
|
"""
|
||||||
|
send message
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
data: <type>
|
||||||
|
Message to send. If you set opcode to OPCODE_TEXT,
|
||||||
|
data must be utf-8 string or unicode.
|
||||||
|
opcode: <type>
|
||||||
|
Operation code of data. default is OPCODE_TEXT.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not self.sock or self.sock.send(data, opcode) == 0:
|
||||||
|
raise WebSocketConnectionClosedException(
|
||||||
|
"Connection is already closed.")
|
||||||
|
|
||||||
|
def close(self, **kwargs):
|
||||||
|
"""
|
||||||
|
Close websocket connection.
|
||||||
|
"""
|
||||||
|
self.keep_running = False
|
||||||
|
if self.sock:
|
||||||
|
self.sock.close(**kwargs)
|
||||||
|
self.sock = None
|
||||||
|
|
||||||
|
def _send_ping(self, interval, event):
|
||||||
|
while not event.wait(interval):
|
||||||
|
self.last_ping_tm = time.time()
|
||||||
|
if self.sock:
|
||||||
|
try:
|
||||||
|
self.sock.ping()
|
||||||
|
except Exception as ex:
|
||||||
|
_logging.warning("send_ping routine terminated: {}".format(ex))
|
||||||
|
break
|
||||||
|
|
||||||
|
def run_forever(self, sockopt=None, sslopt=None,
|
||||||
|
ping_interval=0, ping_timeout=None,
|
||||||
|
http_proxy_host=None, http_proxy_port=None,
|
||||||
|
http_no_proxy=None, http_proxy_auth=None,
|
||||||
|
skip_utf8_validation=False,
|
||||||
|
host=None, origin=None, dispatcher=None,
|
||||||
|
suppress_origin=False, proxy_type=None):
|
||||||
|
"""
|
||||||
|
Run event loop for WebSocket framework.
|
||||||
|
|
||||||
|
This loop is an infinite loop and is alive while websocket is available.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
sockopt: tuple
|
||||||
|
values for socket.setsockopt.
|
||||||
|
sockopt must be tuple
|
||||||
|
and each element is argument of sock.setsockopt.
|
||||||
|
sslopt: dict
|
||||||
|
optional dict object for ssl socket option.
|
||||||
|
ping_interval: int or float
|
||||||
|
automatically send "ping" command
|
||||||
|
every specified period (in seconds)
|
||||||
|
if set to 0, not send automatically.
|
||||||
|
ping_timeout: int or float
|
||||||
|
timeout (in seconds) if the pong message is not received.
|
||||||
|
http_proxy_host: <type>
|
||||||
|
http proxy host name.
|
||||||
|
http_proxy_port: <type>
|
||||||
|
http proxy port. If not set, set to 80.
|
||||||
|
http_no_proxy: <type>
|
||||||
|
host names, which doesn't use proxy.
|
||||||
|
skip_utf8_validation: bool
|
||||||
|
skip utf8 validation.
|
||||||
|
host: str
|
||||||
|
update host header.
|
||||||
|
origin: str
|
||||||
|
update origin header.
|
||||||
|
dispatcher: <type>
|
||||||
|
customize reading data from socket.
|
||||||
|
suppress_origin: bool
|
||||||
|
suppress outputting origin header.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
teardown: bool
|
||||||
|
False if caught KeyboardInterrupt, True if other exception was raised during a loop
|
||||||
|
"""
|
||||||
|
|
||||||
|
if ping_timeout is not None and ping_timeout <= 0:
|
||||||
|
ping_timeout = None
|
||||||
|
if ping_timeout and ping_interval and ping_interval <= ping_timeout:
|
||||||
|
raise WebSocketException("Ensure ping_interval > ping_timeout")
|
||||||
|
if not sockopt:
|
||||||
|
sockopt = []
|
||||||
|
if not sslopt:
|
||||||
|
sslopt = {}
|
||||||
|
if self.sock:
|
||||||
|
raise WebSocketException("socket is already opened")
|
||||||
|
thread = None
|
||||||
|
self.keep_running = True
|
||||||
|
self.last_ping_tm = 0
|
||||||
|
self.last_pong_tm = 0
|
||||||
|
|
||||||
|
def teardown(close_frame=None):
|
||||||
|
"""
|
||||||
|
Tears down the connection.
|
||||||
|
|
||||||
|
If close_frame is set, we will invoke the on_close handler with the
|
||||||
|
statusCode and reason from there.
|
||||||
|
"""
|
||||||
|
if thread and thread.is_alive():
|
||||||
|
event.set()
|
||||||
|
thread.join()
|
||||||
|
self.keep_running = False
|
||||||
|
if self.sock:
|
||||||
|
self.sock.close()
|
||||||
|
close_args = self._get_close_args(
|
||||||
|
close_frame.data if close_frame else None)
|
||||||
|
self._callback(self.on_close, *close_args)
|
||||||
|
self.sock = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.sock = WebSocket(
|
||||||
|
self.get_mask_key, sockopt=sockopt, sslopt=sslopt,
|
||||||
|
fire_cont_frame=self.on_cont_message is not None,
|
||||||
|
skip_utf8_validation=skip_utf8_validation,
|
||||||
|
enable_multithread=True if ping_interval else False)
|
||||||
|
self.sock.settimeout(getdefaulttimeout())
|
||||||
|
self.sock.connect(
|
||||||
|
self.url, header=self.header, cookie=self.cookie,
|
||||||
|
http_proxy_host=http_proxy_host,
|
||||||
|
http_proxy_port=http_proxy_port, http_no_proxy=http_no_proxy,
|
||||||
|
http_proxy_auth=http_proxy_auth, subprotocols=self.subprotocols,
|
||||||
|
host=host, origin=origin, suppress_origin=suppress_origin,
|
||||||
|
proxy_type=proxy_type)
|
||||||
|
if not dispatcher:
|
||||||
|
dispatcher = self.create_dispatcher(ping_timeout)
|
||||||
|
|
||||||
|
self._callback(self.on_open)
|
||||||
|
|
||||||
|
if ping_interval:
|
||||||
|
event = threading.Event()
|
||||||
|
thread = threading.Thread(
|
||||||
|
target=self._send_ping, args=(ping_interval, event))
|
||||||
|
thread.setDaemon(True)
|
||||||
|
thread.start()
|
||||||
|
|
||||||
|
def read():
|
||||||
|
if not self.keep_running:
|
||||||
|
return teardown()
|
||||||
|
|
||||||
|
op_code, frame = self.sock.recv_data_frame(True)
|
||||||
|
if op_code == ABNF.OPCODE_CLOSE:
|
||||||
|
return teardown(frame)
|
||||||
|
elif op_code == ABNF.OPCODE_PING:
|
||||||
|
self._callback(self.on_ping, frame.data)
|
||||||
|
elif op_code == ABNF.OPCODE_PONG:
|
||||||
|
self.last_pong_tm = time.time()
|
||||||
|
self._callback(self.on_pong, frame.data)
|
||||||
|
elif op_code == ABNF.OPCODE_CONT and self.on_cont_message:
|
||||||
|
self._callback(self.on_data, frame.data,
|
||||||
|
frame.opcode, frame.fin)
|
||||||
|
self._callback(self.on_cont_message,
|
||||||
|
frame.data, frame.fin)
|
||||||
|
else:
|
||||||
|
data = frame.data
|
||||||
|
if six.PY3 and op_code == ABNF.OPCODE_TEXT:
|
||||||
|
data = data.decode("utf-8")
|
||||||
|
self._callback(self.on_data, data, frame.opcode, True)
|
||||||
|
self._callback(self.on_message, data)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def check():
|
||||||
|
if (ping_timeout):
|
||||||
|
has_timeout_expired = time.time() - self.last_ping_tm > ping_timeout
|
||||||
|
has_pong_not_arrived_after_last_ping = self.last_pong_tm - self.last_ping_tm < 0
|
||||||
|
has_pong_arrived_too_late = self.last_pong_tm - self.last_ping_tm > ping_timeout
|
||||||
|
|
||||||
|
if (self.last_ping_tm
|
||||||
|
and has_timeout_expired
|
||||||
|
and (has_pong_not_arrived_after_last_ping or has_pong_arrived_too_late)):
|
||||||
|
raise WebSocketTimeoutException("ping/pong timed out")
|
||||||
|
return True
|
||||||
|
|
||||||
|
dispatcher.read(self.sock.sock, read, check)
|
||||||
|
except (Exception, KeyboardInterrupt, SystemExit) as e:
|
||||||
|
self._callback(self.on_error, e)
|
||||||
|
if isinstance(e, SystemExit):
|
||||||
|
# propagate SystemExit further
|
||||||
|
raise
|
||||||
|
teardown()
|
||||||
|
return not isinstance(e, KeyboardInterrupt)
|
||||||
|
|
||||||
|
def create_dispatcher(self, ping_timeout):
|
||||||
|
timeout = ping_timeout or 10
|
||||||
|
if self.sock.is_ssl():
|
||||||
|
return SSLDispatcher(self, timeout)
|
||||||
|
|
||||||
|
return Dispatcher(self, timeout)
|
||||||
|
|
||||||
|
def _get_close_args(self, data):
|
||||||
|
"""
|
||||||
|
_get_close_args extracts the code, reason from the close body
|
||||||
|
if they exists, and if the self.on_close except three arguments
|
||||||
|
"""
|
||||||
|
# if the on_close callback is "old", just return empty list
|
||||||
|
if sys.version_info < (3, 0):
|
||||||
|
if not self.on_close or len(inspect.getargspec(self.on_close).args) != 3:
|
||||||
|
return []
|
||||||
|
else:
|
||||||
|
if not self.on_close or len(inspect.getfullargspec(self.on_close).args) != 3:
|
||||||
|
return []
|
||||||
|
|
||||||
|
if data and len(data) >= 2:
|
||||||
|
code = 256 * six.byte2int(data[0:1]) + six.byte2int(data[1:2])
|
||||||
|
reason = data[2:].decode('utf-8')
|
||||||
|
return [code, reason]
|
||||||
|
|
||||||
|
return [None, None]
|
||||||
|
|
||||||
|
def _callback(self, callback, *args):
|
||||||
|
if callback:
|
||||||
|
try:
|
||||||
|
callback(self, *args)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
_logging.error("error from callback {}: {}".format(callback, e))
|
||||||
|
if _logging.isEnabledForDebug():
|
||||||
|
_, _, tb = sys.exc_info()
|
||||||
|
traceback.print_tb(tb)
|
76
resources/lib/websocket/_cookiejar.py
Normal file
76
resources/lib/websocket/_cookiejar.py
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""
|
||||||
|
websocket - WebSocket client library for Python
|
||||||
|
|
||||||
|
Copyright (C) 2010 Hiroki Ohtani(liris)
|
||||||
|
|
||||||
|
This library is free software; you can redistribute it and/or
|
||||||
|
modify it under the terms of the GNU Lesser General Public
|
||||||
|
License as published by the Free Software Foundation; either
|
||||||
|
version 2.1 of the License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This library is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
Lesser General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Lesser General Public
|
||||||
|
License along with this library; if not, write to the Free Software
|
||||||
|
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
import Cookie
|
||||||
|
except:
|
||||||
|
import http.cookies as Cookie
|
||||||
|
|
||||||
|
|
||||||
|
class SimpleCookieJar(object):
|
||||||
|
def __init__(self):
|
||||||
|
self.jar = dict()
|
||||||
|
|
||||||
|
def add(self, set_cookie):
|
||||||
|
if set_cookie:
|
||||||
|
try:
|
||||||
|
simpleCookie = Cookie.SimpleCookie(set_cookie)
|
||||||
|
except:
|
||||||
|
simpleCookie = Cookie.SimpleCookie(set_cookie.encode('ascii', 'ignore'))
|
||||||
|
|
||||||
|
for k, v in simpleCookie.items():
|
||||||
|
domain = v.get("domain")
|
||||||
|
if domain:
|
||||||
|
if not domain.startswith("."):
|
||||||
|
domain = "." + domain
|
||||||
|
cookie = self.jar.get(domain) if self.jar.get(domain) else Cookie.SimpleCookie()
|
||||||
|
cookie.update(simpleCookie)
|
||||||
|
self.jar[domain.lower()] = cookie
|
||||||
|
|
||||||
|
def set(self, set_cookie):
|
||||||
|
if set_cookie:
|
||||||
|
try:
|
||||||
|
simpleCookie = Cookie.SimpleCookie(set_cookie)
|
||||||
|
except:
|
||||||
|
simpleCookie = Cookie.SimpleCookie(set_cookie.encode('ascii', 'ignore'))
|
||||||
|
|
||||||
|
for k, v in simpleCookie.items():
|
||||||
|
domain = v.get("domain")
|
||||||
|
if domain:
|
||||||
|
if not domain.startswith("."):
|
||||||
|
domain = "." + domain
|
||||||
|
self.jar[domain.lower()] = simpleCookie
|
||||||
|
|
||||||
|
def get(self, host):
|
||||||
|
if not host:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
cookies = []
|
||||||
|
for domain, simpleCookie in self.jar.items():
|
||||||
|
host = host.lower()
|
||||||
|
if host.endswith(domain) or host == domain[1:]:
|
||||||
|
cookies.append(self.jar.get(domain))
|
||||||
|
|
||||||
|
return "; ".join(filter(None, ["%s=%s" % (k, v.value) for cookie in filter(None, sorted(cookies)) for k, v in
|
||||||
|
sorted(cookie.items())]))
|
595
resources/lib/websocket/_core.py
Normal file
595
resources/lib/websocket/_core.py
Normal file
|
@ -0,0 +1,595 @@
|
||||||
|
from __future__ import print_function
|
||||||
|
"""
|
||||||
|
_core.py
|
||||||
|
====================================
|
||||||
|
WebSocket Python client
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""
|
||||||
|
websocket - WebSocket client library for Python
|
||||||
|
|
||||||
|
Copyright (C) 2010 Hiroki Ohtani(liris)
|
||||||
|
|
||||||
|
This library is free software; you can redistribute it and/or
|
||||||
|
modify it under the terms of the GNU Lesser General Public
|
||||||
|
License as published by the Free Software Foundation; either
|
||||||
|
version 2.1 of the License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This library is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
Lesser General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Lesser General Public
|
||||||
|
License along with this library; if not, write to the Free Software
|
||||||
|
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
|
||||||
|
"""
|
||||||
|
import socket
|
||||||
|
import struct
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
|
||||||
|
import six
|
||||||
|
|
||||||
|
# websocket modules
|
||||||
|
from ._abnf import *
|
||||||
|
from ._exceptions import *
|
||||||
|
from ._handshake import *
|
||||||
|
from ._http import *
|
||||||
|
from ._logging import *
|
||||||
|
from ._socket import *
|
||||||
|
from ._ssl_compat import *
|
||||||
|
from ._utils import *
|
||||||
|
|
||||||
|
__all__ = ['WebSocket', 'create_connection']
|
||||||
|
|
||||||
|
class WebSocket(object):
|
||||||
|
"""
|
||||||
|
Low level WebSocket interface.
|
||||||
|
|
||||||
|
This class is based on the WebSocket protocol `draft-hixie-thewebsocketprotocol-76 <http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-76>`_
|
||||||
|
|
||||||
|
We can connect to the websocket server and send/receive data.
|
||||||
|
The following example is an echo client.
|
||||||
|
|
||||||
|
>>> import websocket
|
||||||
|
>>> ws = websocket.WebSocket()
|
||||||
|
>>> ws.connect("ws://echo.websocket.org")
|
||||||
|
>>> ws.send("Hello, Server")
|
||||||
|
>>> ws.recv()
|
||||||
|
'Hello, Server'
|
||||||
|
>>> ws.close()
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
get_mask_key: func
|
||||||
|
a callable to produce new mask keys, see the set_mask_key
|
||||||
|
function's docstring for more details
|
||||||
|
sockopt: tuple
|
||||||
|
values for socket.setsockopt.
|
||||||
|
sockopt must be tuple and each element is argument of sock.setsockopt.
|
||||||
|
sslopt: dict
|
||||||
|
optional dict object for ssl socket option.
|
||||||
|
fire_cont_frame: bool
|
||||||
|
fire recv event for each cont frame. default is False
|
||||||
|
enable_multithread: bool
|
||||||
|
if set to True, lock send method.
|
||||||
|
skip_utf8_validation: bool
|
||||||
|
skip utf8 validation.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, get_mask_key=None, sockopt=None, sslopt=None,
|
||||||
|
fire_cont_frame=False, enable_multithread=False,
|
||||||
|
skip_utf8_validation=False, **_):
|
||||||
|
"""
|
||||||
|
Initialize WebSocket object.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
sslopt: specify ssl certification verification options
|
||||||
|
"""
|
||||||
|
self.sock_opt = sock_opt(sockopt, sslopt)
|
||||||
|
self.handshake_response = None
|
||||||
|
self.sock = None
|
||||||
|
|
||||||
|
self.connected = False
|
||||||
|
self.get_mask_key = get_mask_key
|
||||||
|
# These buffer over the build-up of a single frame.
|
||||||
|
self.frame_buffer = frame_buffer(self._recv, skip_utf8_validation)
|
||||||
|
self.cont_frame = continuous_frame(
|
||||||
|
fire_cont_frame, skip_utf8_validation)
|
||||||
|
|
||||||
|
if enable_multithread:
|
||||||
|
self.lock = threading.Lock()
|
||||||
|
self.readlock = threading.Lock()
|
||||||
|
else:
|
||||||
|
self.lock = NoLock()
|
||||||
|
self.readlock = NoLock()
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
"""
|
||||||
|
Allow iteration over websocket, implying sequential `recv` executions.
|
||||||
|
"""
|
||||||
|
while True:
|
||||||
|
yield self.recv()
|
||||||
|
|
||||||
|
def __next__(self):
|
||||||
|
return self.recv()
|
||||||
|
|
||||||
|
def next(self):
|
||||||
|
return self.__next__()
|
||||||
|
|
||||||
|
def fileno(self):
|
||||||
|
return self.sock.fileno()
|
||||||
|
|
||||||
|
def set_mask_key(self, func):
|
||||||
|
"""
|
||||||
|
Set function to create mask key. You can customize mask key generator.
|
||||||
|
Mainly, this is for testing purpose.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
func: func
|
||||||
|
callable object. the func takes 1 argument as integer.
|
||||||
|
The argument means length of mask key.
|
||||||
|
This func must return string(byte array),
|
||||||
|
which length is argument specified.
|
||||||
|
"""
|
||||||
|
self.get_mask_key = func
|
||||||
|
|
||||||
|
def gettimeout(self):
|
||||||
|
"""
|
||||||
|
Get the websocket timeout (in seconds) as an int or float
|
||||||
|
|
||||||
|
Returns
|
||||||
|
----------
|
||||||
|
timeout: int or float
|
||||||
|
returns timeout value (in seconds). This value could be either float/integer.
|
||||||
|
"""
|
||||||
|
return self.sock_opt.timeout
|
||||||
|
|
||||||
|
def settimeout(self, timeout):
|
||||||
|
"""
|
||||||
|
Set the timeout to the websocket.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
timeout: int or float
|
||||||
|
timeout time (in seconds). This value could be either float/integer.
|
||||||
|
"""
|
||||||
|
self.sock_opt.timeout = timeout
|
||||||
|
if self.sock:
|
||||||
|
self.sock.settimeout(timeout)
|
||||||
|
|
||||||
|
timeout = property(gettimeout, settimeout)
|
||||||
|
|
||||||
|
def getsubprotocol(self):
|
||||||
|
"""
|
||||||
|
Get subprotocol
|
||||||
|
"""
|
||||||
|
if self.handshake_response:
|
||||||
|
return self.handshake_response.subprotocol
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
subprotocol = property(getsubprotocol)
|
||||||
|
|
||||||
|
def getstatus(self):
|
||||||
|
"""
|
||||||
|
Get handshake status
|
||||||
|
"""
|
||||||
|
if self.handshake_response:
|
||||||
|
return self.handshake_response.status
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
status = property(getstatus)
|
||||||
|
|
||||||
|
def getheaders(self):
|
||||||
|
"""
|
||||||
|
Get handshake response header
|
||||||
|
"""
|
||||||
|
if self.handshake_response:
|
||||||
|
return self.handshake_response.headers
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def is_ssl(self):
|
||||||
|
return isinstance(self.sock, ssl.SSLSocket)
|
||||||
|
|
||||||
|
headers = property(getheaders)
|
||||||
|
|
||||||
|
def connect(self, url, **options):
|
||||||
|
"""
|
||||||
|
Connect to url. url is websocket url scheme.
|
||||||
|
ie. ws://host:port/resource
|
||||||
|
You can customize using 'options'.
|
||||||
|
If you set "header" list object, you can set your own custom header.
|
||||||
|
|
||||||
|
>>> ws = WebSocket()
|
||||||
|
>>> ws.connect("ws://echo.websocket.org/",
|
||||||
|
... header=["User-Agent: MyProgram",
|
||||||
|
... "x-custom: header"])
|
||||||
|
|
||||||
|
timeout: <type>
|
||||||
|
socket timeout time. This value is an integer or float.
|
||||||
|
if you set None for this value, it means "use default_timeout value"
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
options:
|
||||||
|
- header: list or dict
|
||||||
|
custom http header list or dict.
|
||||||
|
- cookie: str
|
||||||
|
cookie value.
|
||||||
|
- origin: str
|
||||||
|
custom origin url.
|
||||||
|
- suppress_origin: bool
|
||||||
|
suppress outputting origin header.
|
||||||
|
- host: str
|
||||||
|
custom host header string.
|
||||||
|
- http_proxy_host: <type>
|
||||||
|
http proxy host name.
|
||||||
|
- http_proxy_port: <type>
|
||||||
|
http proxy port. If not set, set to 80.
|
||||||
|
- http_no_proxy: <type>
|
||||||
|
host names, which doesn't use proxy.
|
||||||
|
- http_proxy_auth: <type>
|
||||||
|
http proxy auth information. tuple of username and password. default is None
|
||||||
|
- redirect_limit: <type>
|
||||||
|
number of redirects to follow.
|
||||||
|
- subprotocols: <type>
|
||||||
|
array of available sub protocols. default is None.
|
||||||
|
- socket: <type>
|
||||||
|
pre-initialized stream socket.
|
||||||
|
"""
|
||||||
|
self.sock_opt.timeout = options.get('timeout', self.sock_opt.timeout)
|
||||||
|
self.sock, addrs = connect(url, self.sock_opt, proxy_info(**options),
|
||||||
|
options.pop('socket', None))
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.handshake_response = handshake(self.sock, *addrs, **options)
|
||||||
|
for attempt in range(options.pop('redirect_limit', 3)):
|
||||||
|
if self.handshake_response.status in SUPPORTED_REDIRECT_STATUSES:
|
||||||
|
url = self.handshake_response.headers['location']
|
||||||
|
self.sock.close()
|
||||||
|
self.sock, addrs = connect(url, self.sock_opt, proxy_info(**options),
|
||||||
|
options.pop('socket', None))
|
||||||
|
self.handshake_response = handshake(self.sock, *addrs, **options)
|
||||||
|
self.connected = True
|
||||||
|
except:
|
||||||
|
if self.sock:
|
||||||
|
self.sock.close()
|
||||||
|
self.sock = None
|
||||||
|
raise
|
||||||
|
|
||||||
|
def send(self, payload, opcode=ABNF.OPCODE_TEXT):
|
||||||
|
"""
|
||||||
|
Send the data as string.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
payload: <type>
|
||||||
|
Payload must be utf-8 string or unicode,
|
||||||
|
if the opcode is OPCODE_TEXT.
|
||||||
|
Otherwise, it must be string(byte array)
|
||||||
|
opcode: <type>
|
||||||
|
operation code to send. Please see OPCODE_XXX.
|
||||||
|
"""
|
||||||
|
|
||||||
|
frame = ABNF.create_frame(payload, opcode)
|
||||||
|
return self.send_frame(frame)
|
||||||
|
|
||||||
|
def send_frame(self, frame):
|
||||||
|
"""
|
||||||
|
Send the data frame.
|
||||||
|
|
||||||
|
>>> ws = create_connection("ws://echo.websocket.org/")
|
||||||
|
>>> frame = ABNF.create_frame("Hello", ABNF.OPCODE_TEXT)
|
||||||
|
>>> ws.send_frame(frame)
|
||||||
|
>>> cont_frame = ABNF.create_frame("My name is ", ABNF.OPCODE_CONT, 0)
|
||||||
|
>>> ws.send_frame(frame)
|
||||||
|
>>> cont_frame = ABNF.create_frame("Foo Bar", ABNF.OPCODE_CONT, 1)
|
||||||
|
>>> ws.send_frame(frame)
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
frame: <type>
|
||||||
|
frame data created by ABNF.create_frame
|
||||||
|
"""
|
||||||
|
if self.get_mask_key:
|
||||||
|
frame.get_mask_key = self.get_mask_key
|
||||||
|
data = frame.format()
|
||||||
|
length = len(data)
|
||||||
|
if (isEnabledForTrace()):
|
||||||
|
trace("send: " + repr(data))
|
||||||
|
|
||||||
|
with self.lock:
|
||||||
|
while data:
|
||||||
|
l = self._send(data)
|
||||||
|
data = data[l:]
|
||||||
|
|
||||||
|
return length
|
||||||
|
|
||||||
|
def send_binary(self, payload):
|
||||||
|
return self.send(payload, ABNF.OPCODE_BINARY)
|
||||||
|
|
||||||
|
def ping(self, payload=""):
|
||||||
|
"""
|
||||||
|
Send ping data.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
payload: <type>
|
||||||
|
data payload to send server.
|
||||||
|
"""
|
||||||
|
if isinstance(payload, six.text_type):
|
||||||
|
payload = payload.encode("utf-8")
|
||||||
|
self.send(payload, ABNF.OPCODE_PING)
|
||||||
|
|
||||||
|
def pong(self, payload=""):
|
||||||
|
"""
|
||||||
|
Send pong data.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
payload: <type>
|
||||||
|
data payload to send server.
|
||||||
|
"""
|
||||||
|
if isinstance(payload, six.text_type):
|
||||||
|
payload = payload.encode("utf-8")
|
||||||
|
self.send(payload, ABNF.OPCODE_PONG)
|
||||||
|
|
||||||
|
def recv(self):
|
||||||
|
"""
|
||||||
|
Receive string data(byte array) from the server.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
----------
|
||||||
|
data: string (byte array) value.
|
||||||
|
"""
|
||||||
|
with self.readlock:
|
||||||
|
opcode, data = self.recv_data()
|
||||||
|
if six.PY3 and opcode == ABNF.OPCODE_TEXT:
|
||||||
|
return data.decode("utf-8")
|
||||||
|
elif opcode == ABNF.OPCODE_TEXT or opcode == ABNF.OPCODE_BINARY:
|
||||||
|
return data
|
||||||
|
else:
|
||||||
|
return ''
|
||||||
|
|
||||||
|
def recv_data(self, control_frame=False):
|
||||||
|
"""
|
||||||
|
Receive data with operation code.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
control_frame: bool
|
||||||
|
a boolean flag indicating whether to return control frame
|
||||||
|
data, defaults to False
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
opcode, frame.data: tuple
|
||||||
|
tuple of operation code and string(byte array) value.
|
||||||
|
"""
|
||||||
|
opcode, frame = self.recv_data_frame(control_frame)
|
||||||
|
return opcode, frame.data
|
||||||
|
|
||||||
|
def recv_data_frame(self, control_frame=False):
|
||||||
|
"""
|
||||||
|
Receive data with operation code.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
control_frame: bool
|
||||||
|
a boolean flag indicating whether to return control frame
|
||||||
|
data, defaults to False
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
frame.opcode, frame: tuple
|
||||||
|
tuple of operation code and string(byte array) value.
|
||||||
|
"""
|
||||||
|
while True:
|
||||||
|
frame = self.recv_frame()
|
||||||
|
if not frame:
|
||||||
|
# handle error:
|
||||||
|
# 'NoneType' object has no attribute 'opcode'
|
||||||
|
raise WebSocketProtocolException(
|
||||||
|
"Not a valid frame %s" % frame)
|
||||||
|
elif frame.opcode in (ABNF.OPCODE_TEXT, ABNF.OPCODE_BINARY, ABNF.OPCODE_CONT):
|
||||||
|
self.cont_frame.validate(frame)
|
||||||
|
self.cont_frame.add(frame)
|
||||||
|
|
||||||
|
if self.cont_frame.is_fire(frame):
|
||||||
|
return self.cont_frame.extract(frame)
|
||||||
|
|
||||||
|
elif frame.opcode == ABNF.OPCODE_CLOSE:
|
||||||
|
self.send_close()
|
||||||
|
return frame.opcode, frame
|
||||||
|
elif frame.opcode == ABNF.OPCODE_PING:
|
||||||
|
if len(frame.data) < 126:
|
||||||
|
self.pong(frame.data)
|
||||||
|
else:
|
||||||
|
raise WebSocketProtocolException(
|
||||||
|
"Ping message is too long")
|
||||||
|
if control_frame:
|
||||||
|
return frame.opcode, frame
|
||||||
|
elif frame.opcode == ABNF.OPCODE_PONG:
|
||||||
|
if control_frame:
|
||||||
|
return frame.opcode, frame
|
||||||
|
|
||||||
|
def recv_frame(self):
|
||||||
|
"""
|
||||||
|
Receive data as frame from server.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
self.frame_buffer.recv_frame(): ABNF frame object
|
||||||
|
"""
|
||||||
|
return self.frame_buffer.recv_frame()
|
||||||
|
|
||||||
|
def send_close(self, status=STATUS_NORMAL, reason=six.b("")):
|
||||||
|
"""
|
||||||
|
Send close data to the server.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
status: <type>
|
||||||
|
status code to send. see STATUS_XXX.
|
||||||
|
reason: str or bytes
|
||||||
|
the reason to close. This must be string or bytes.
|
||||||
|
"""
|
||||||
|
if status < 0 or status >= ABNF.LENGTH_16:
|
||||||
|
raise ValueError("code is invalid range")
|
||||||
|
self.connected = False
|
||||||
|
self.send(struct.pack('!H', status) + reason, ABNF.OPCODE_CLOSE)
|
||||||
|
|
||||||
|
def close(self, status=STATUS_NORMAL, reason=six.b(""), timeout=3):
|
||||||
|
"""
|
||||||
|
Close Websocket object
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
status: <type>
|
||||||
|
status code to send. see STATUS_XXX.
|
||||||
|
reason: <type>
|
||||||
|
the reason to close. This must be string.
|
||||||
|
timeout: int or float
|
||||||
|
timeout until receive a close frame.
|
||||||
|
If None, it will wait forever until receive a close frame.
|
||||||
|
"""
|
||||||
|
if self.connected:
|
||||||
|
if status < 0 or status >= ABNF.LENGTH_16:
|
||||||
|
raise ValueError("code is invalid range")
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.connected = False
|
||||||
|
self.send(struct.pack('!H', status) +
|
||||||
|
reason, ABNF.OPCODE_CLOSE)
|
||||||
|
sock_timeout = self.sock.gettimeout()
|
||||||
|
self.sock.settimeout(timeout)
|
||||||
|
start_time = time.time()
|
||||||
|
while timeout is None or time.time() - start_time < timeout:
|
||||||
|
try:
|
||||||
|
frame = self.recv_frame()
|
||||||
|
if frame.opcode != ABNF.OPCODE_CLOSE:
|
||||||
|
continue
|
||||||
|
if isEnabledForError():
|
||||||
|
recv_status = struct.unpack("!H", frame.data[0:2])[0]
|
||||||
|
if recv_status >= 3000 and recv_status <= 4999:
|
||||||
|
debug("close status: " + repr(recv_status))
|
||||||
|
elif recv_status != STATUS_NORMAL:
|
||||||
|
error("close status: " + repr(recv_status))
|
||||||
|
break
|
||||||
|
except:
|
||||||
|
break
|
||||||
|
self.sock.settimeout(sock_timeout)
|
||||||
|
self.sock.shutdown(socket.SHUT_RDWR)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
self.shutdown()
|
||||||
|
|
||||||
|
def abort(self):
|
||||||
|
"""
|
||||||
|
Low-level asynchronous abort, wakes up other threads that are waiting in recv_*
|
||||||
|
"""
|
||||||
|
if self.connected:
|
||||||
|
self.sock.shutdown(socket.SHUT_RDWR)
|
||||||
|
|
||||||
|
def shutdown(self):
|
||||||
|
"""
|
||||||
|
close socket, immediately.
|
||||||
|
"""
|
||||||
|
if self.sock:
|
||||||
|
self.sock.close()
|
||||||
|
self.sock = None
|
||||||
|
self.connected = False
|
||||||
|
|
||||||
|
def _send(self, data):
|
||||||
|
return send(self.sock, data)
|
||||||
|
|
||||||
|
def _recv(self, bufsize):
|
||||||
|
try:
|
||||||
|
return recv(self.sock, bufsize)
|
||||||
|
except WebSocketConnectionClosedException:
|
||||||
|
if self.sock:
|
||||||
|
self.sock.close()
|
||||||
|
self.sock = None
|
||||||
|
self.connected = False
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def create_connection(url, timeout=None, class_=WebSocket, **options):
|
||||||
|
"""
|
||||||
|
Connect to url and return websocket object.
|
||||||
|
|
||||||
|
Connect to url and return the WebSocket object.
|
||||||
|
Passing optional timeout parameter will set the timeout on the socket.
|
||||||
|
If no timeout is supplied,
|
||||||
|
the global default timeout setting returned by getdefaulttimeout() is used.
|
||||||
|
You can customize using 'options'.
|
||||||
|
If you set "header" list object, you can set your own custom header.
|
||||||
|
|
||||||
|
>>> conn = create_connection("ws://echo.websocket.org/",
|
||||||
|
... header=["User-Agent: MyProgram",
|
||||||
|
... "x-custom: header"])
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
timeout: int or float
|
||||||
|
socket timeout time. This value could be either float/integer.
|
||||||
|
if you set None for this value,
|
||||||
|
it means "use default_timeout value"
|
||||||
|
class_: <type>
|
||||||
|
class to instantiate when creating the connection. It has to implement
|
||||||
|
settimeout and connect. It's __init__ should be compatible with
|
||||||
|
WebSocket.__init__, i.e. accept all of it's kwargs.
|
||||||
|
options: <type>
|
||||||
|
- header: list or dict
|
||||||
|
custom http header list or dict.
|
||||||
|
- cookie: str
|
||||||
|
cookie value.
|
||||||
|
- origin: str
|
||||||
|
custom origin url.
|
||||||
|
- suppress_origin: bool
|
||||||
|
suppress outputting origin header.
|
||||||
|
- host: <type>
|
||||||
|
custom host header string.
|
||||||
|
- http_proxy_host: <type>
|
||||||
|
http proxy host name.
|
||||||
|
- http_proxy_port: <type>
|
||||||
|
http proxy port. If not set, set to 80.
|
||||||
|
- http_no_proxy: <type>
|
||||||
|
host names, which doesn't use proxy.
|
||||||
|
- http_proxy_auth: <type>
|
||||||
|
http proxy auth information. tuple of username and password. default is None
|
||||||
|
- enable_multithread: bool
|
||||||
|
enable lock for multithread.
|
||||||
|
- redirect_limit: <type>
|
||||||
|
number of redirects to follow.
|
||||||
|
- sockopt: <type>
|
||||||
|
socket options
|
||||||
|
- sslopt: <type>
|
||||||
|
ssl option
|
||||||
|
- subprotocols: <type>
|
||||||
|
array of available sub protocols. default is None.
|
||||||
|
- skip_utf8_validation: bool
|
||||||
|
skip utf8 validation.
|
||||||
|
- socket: <type>
|
||||||
|
pre-initialized stream socket.
|
||||||
|
"""
|
||||||
|
sockopt = options.pop("sockopt", [])
|
||||||
|
sslopt = options.pop("sslopt", {})
|
||||||
|
fire_cont_frame = options.pop("fire_cont_frame", False)
|
||||||
|
enable_multithread = options.pop("enable_multithread", False)
|
||||||
|
skip_utf8_validation = options.pop("skip_utf8_validation", False)
|
||||||
|
websock = class_(sockopt=sockopt, sslopt=sslopt,
|
||||||
|
fire_cont_frame=fire_cont_frame,
|
||||||
|
enable_multithread=enable_multithread,
|
||||||
|
skip_utf8_validation=skip_utf8_validation, **options)
|
||||||
|
websock.settimeout(timeout if timeout is not None else getdefaulttimeout())
|
||||||
|
websock.connect(url, **options)
|
||||||
|
return websock
|
85
resources/lib/websocket/_exceptions.py
Normal file
85
resources/lib/websocket/_exceptions.py
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
"""
|
||||||
|
Define WebSocket exceptions
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""
|
||||||
|
websocket - WebSocket client library for Python
|
||||||
|
|
||||||
|
Copyright (C) 2010 Hiroki Ohtani(liris)
|
||||||
|
|
||||||
|
This library is free software; you can redistribute it and/or
|
||||||
|
modify it under the terms of the GNU Lesser General Public
|
||||||
|
License as published by the Free Software Foundation; either
|
||||||
|
version 2.1 of the License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This library is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
Lesser General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Lesser General Public
|
||||||
|
License along with this library; if not, write to the Free Software
|
||||||
|
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
class WebSocketException(Exception):
|
||||||
|
"""
|
||||||
|
WebSocket exception class.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class WebSocketProtocolException(WebSocketException):
|
||||||
|
"""
|
||||||
|
If the WebSocket protocol is invalid, this exception will be raised.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class WebSocketPayloadException(WebSocketException):
|
||||||
|
"""
|
||||||
|
If the WebSocket payload is invalid, this exception will be raised.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class WebSocketConnectionClosedException(WebSocketException):
|
||||||
|
"""
|
||||||
|
If remote host closed the connection or some network error happened,
|
||||||
|
this exception will be raised.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class WebSocketTimeoutException(WebSocketException):
|
||||||
|
"""
|
||||||
|
WebSocketTimeoutException will be raised at socket timeout during read/write data.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class WebSocketProxyException(WebSocketException):
|
||||||
|
"""
|
||||||
|
WebSocketProxyException will be raised when proxy error occurred.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class WebSocketBadStatusException(WebSocketException):
|
||||||
|
"""
|
||||||
|
WebSocketBadStatusException will be raised when we get bad handshake status code.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, message, status_code, status_message=None, resp_headers=None):
|
||||||
|
msg = message % (status_code, status_message)
|
||||||
|
super(WebSocketBadStatusException, self).__init__(msg)
|
||||||
|
self.status_code = status_code
|
||||||
|
self.resp_headers = resp_headers
|
||||||
|
|
||||||
|
|
||||||
|
class WebSocketAddressException(WebSocketException):
|
||||||
|
"""
|
||||||
|
If the websocket address info cannot be found, this exception will be raised.
|
||||||
|
"""
|
||||||
|
pass
|
211
resources/lib/websocket/_handshake.py
Normal file
211
resources/lib/websocket/_handshake.py
Normal file
|
@ -0,0 +1,211 @@
|
||||||
|
"""
|
||||||
|
websocket - WebSocket client library for Python
|
||||||
|
|
||||||
|
Copyright (C) 2010 Hiroki Ohtani(liris)
|
||||||
|
|
||||||
|
This library is free software; you can redistribute it and/or
|
||||||
|
modify it under the terms of the GNU Lesser General Public
|
||||||
|
License as published by the Free Software Foundation; either
|
||||||
|
version 2.1 of the License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This library is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
Lesser General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Lesser General Public
|
||||||
|
License along with this library; if not, write to the Free Software
|
||||||
|
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
|
||||||
|
"""
|
||||||
|
import hashlib
|
||||||
|
import hmac
|
||||||
|
import os
|
||||||
|
|
||||||
|
import six
|
||||||
|
|
||||||
|
from ._cookiejar import SimpleCookieJar
|
||||||
|
from ._exceptions import *
|
||||||
|
from ._http import *
|
||||||
|
from ._logging import *
|
||||||
|
from ._socket import *
|
||||||
|
|
||||||
|
if hasattr(six, 'PY3') and six.PY3:
|
||||||
|
from base64 import encodebytes as base64encode
|
||||||
|
else:
|
||||||
|
from base64 import encodestring as base64encode
|
||||||
|
|
||||||
|
if hasattr(six, 'PY3') and six.PY3:
|
||||||
|
if hasattr(six, 'PY34') and six.PY34:
|
||||||
|
from http import client as HTTPStatus
|
||||||
|
else:
|
||||||
|
from http import HTTPStatus
|
||||||
|
else:
|
||||||
|
import httplib as HTTPStatus
|
||||||
|
|
||||||
|
__all__ = ["handshake_response", "handshake", "SUPPORTED_REDIRECT_STATUSES"]
|
||||||
|
|
||||||
|
if hasattr(hmac, "compare_digest"):
|
||||||
|
compare_digest = hmac.compare_digest
|
||||||
|
else:
|
||||||
|
def compare_digest(s1, s2):
|
||||||
|
return s1 == s2
|
||||||
|
|
||||||
|
# websocket supported version.
|
||||||
|
VERSION = 13
|
||||||
|
|
||||||
|
SUPPORTED_REDIRECT_STATUSES = (HTTPStatus.MOVED_PERMANENTLY, HTTPStatus.FOUND, HTTPStatus.SEE_OTHER,)
|
||||||
|
SUCCESS_STATUSES = SUPPORTED_REDIRECT_STATUSES + (HTTPStatus.SWITCHING_PROTOCOLS,)
|
||||||
|
|
||||||
|
CookieJar = SimpleCookieJar()
|
||||||
|
|
||||||
|
|
||||||
|
class handshake_response(object):
|
||||||
|
|
||||||
|
def __init__(self, status, headers, subprotocol):
|
||||||
|
self.status = status
|
||||||
|
self.headers = headers
|
||||||
|
self.subprotocol = subprotocol
|
||||||
|
CookieJar.add(headers.get("set-cookie"))
|
||||||
|
|
||||||
|
|
||||||
|
def handshake(sock, hostname, port, resource, **options):
|
||||||
|
headers, key = _get_handshake_headers(resource, hostname, port, options)
|
||||||
|
|
||||||
|
header_str = "\r\n".join(headers)
|
||||||
|
send(sock, header_str)
|
||||||
|
dump("request header", header_str)
|
||||||
|
|
||||||
|
status, resp = _get_resp_headers(sock)
|
||||||
|
if status in SUPPORTED_REDIRECT_STATUSES:
|
||||||
|
return handshake_response(status, resp, None)
|
||||||
|
success, subproto = _validate(resp, key, options.get("subprotocols"))
|
||||||
|
if not success:
|
||||||
|
raise WebSocketException("Invalid WebSocket Header")
|
||||||
|
|
||||||
|
return handshake_response(status, resp, subproto)
|
||||||
|
|
||||||
|
|
||||||
|
def _pack_hostname(hostname):
|
||||||
|
# IPv6 address
|
||||||
|
if ':' in hostname:
|
||||||
|
return '[' + hostname + ']'
|
||||||
|
|
||||||
|
return hostname
|
||||||
|
|
||||||
|
def _get_handshake_headers(resource, host, port, options):
|
||||||
|
headers = [
|
||||||
|
"GET %s HTTP/1.1" % resource,
|
||||||
|
"Upgrade: websocket"
|
||||||
|
]
|
||||||
|
if port == 80 or port == 443:
|
||||||
|
hostport = _pack_hostname(host)
|
||||||
|
else:
|
||||||
|
hostport = "%s:%d" % (_pack_hostname(host), port)
|
||||||
|
if "host" in options and options["host"] is not None:
|
||||||
|
headers.append("Host: %s" % options["host"])
|
||||||
|
else:
|
||||||
|
headers.append("Host: %s" % hostport)
|
||||||
|
|
||||||
|
if "suppress_origin" not in options or not options["suppress_origin"]:
|
||||||
|
if "origin" in options and options["origin"] is not None:
|
||||||
|
headers.append("Origin: %s" % options["origin"])
|
||||||
|
else:
|
||||||
|
headers.append("Origin: http://%s" % hostport)
|
||||||
|
|
||||||
|
key = _create_sec_websocket_key()
|
||||||
|
|
||||||
|
# Append Sec-WebSocket-Key & Sec-WebSocket-Version if not manually specified
|
||||||
|
if not 'header' in options or 'Sec-WebSocket-Key' not in options['header']:
|
||||||
|
key = _create_sec_websocket_key()
|
||||||
|
headers.append("Sec-WebSocket-Key: %s" % key)
|
||||||
|
else:
|
||||||
|
key = options['header']['Sec-WebSocket-Key']
|
||||||
|
|
||||||
|
if not 'header' in options or 'Sec-WebSocket-Version' not in options['header']:
|
||||||
|
headers.append("Sec-WebSocket-Version: %s" % VERSION)
|
||||||
|
|
||||||
|
if not 'connection' in options or options['connection'] is None:
|
||||||
|
headers.append('Connection: Upgrade')
|
||||||
|
else:
|
||||||
|
headers.append(options['connection'])
|
||||||
|
|
||||||
|
subprotocols = options.get("subprotocols")
|
||||||
|
if subprotocols:
|
||||||
|
headers.append("Sec-WebSocket-Protocol: %s" % ",".join(subprotocols))
|
||||||
|
|
||||||
|
if "header" in options:
|
||||||
|
header = options["header"]
|
||||||
|
if isinstance(header, dict):
|
||||||
|
header = [
|
||||||
|
": ".join([k, v])
|
||||||
|
for k, v in header.items()
|
||||||
|
if v is not None
|
||||||
|
]
|
||||||
|
headers.extend(header)
|
||||||
|
|
||||||
|
server_cookie = CookieJar.get(host)
|
||||||
|
client_cookie = options.get("cookie", None)
|
||||||
|
|
||||||
|
cookie = "; ".join(filter(None, [server_cookie, client_cookie]))
|
||||||
|
|
||||||
|
if cookie:
|
||||||
|
headers.append("Cookie: %s" % cookie)
|
||||||
|
|
||||||
|
headers.append("")
|
||||||
|
headers.append("")
|
||||||
|
|
||||||
|
return headers, key
|
||||||
|
|
||||||
|
|
||||||
|
def _get_resp_headers(sock, success_statuses=SUCCESS_STATUSES):
|
||||||
|
status, resp_headers, status_message = read_headers(sock)
|
||||||
|
if status not in success_statuses:
|
||||||
|
raise WebSocketBadStatusException("Handshake status %d %s", status, status_message, resp_headers)
|
||||||
|
return status, resp_headers
|
||||||
|
|
||||||
|
|
||||||
|
_HEADERS_TO_CHECK = {
|
||||||
|
"upgrade": "websocket",
|
||||||
|
"connection": "upgrade",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _validate(headers, key, subprotocols):
|
||||||
|
subproto = None
|
||||||
|
for k, v in _HEADERS_TO_CHECK.items():
|
||||||
|
r = headers.get(k, None)
|
||||||
|
if not r:
|
||||||
|
return False, None
|
||||||
|
r = r.lower()
|
||||||
|
if v != r:
|
||||||
|
return False, None
|
||||||
|
|
||||||
|
if subprotocols:
|
||||||
|
subproto = headers.get("sec-websocket-protocol", None)
|
||||||
|
if not subproto or subproto.lower() not in [s.lower() for s in subprotocols]:
|
||||||
|
error("Invalid subprotocol: " + str(subprotocols))
|
||||||
|
return False, None
|
||||||
|
subproto = subproto.lower()
|
||||||
|
|
||||||
|
result = headers.get("sec-websocket-accept", None)
|
||||||
|
if not result:
|
||||||
|
return False, None
|
||||||
|
result = result.lower()
|
||||||
|
|
||||||
|
if isinstance(result, six.text_type):
|
||||||
|
result = result.encode('utf-8')
|
||||||
|
|
||||||
|
value = (key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11").encode('utf-8')
|
||||||
|
hashed = base64encode(hashlib.sha1(value).digest()).strip().lower()
|
||||||
|
success = compare_digest(hashed, result)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
return True, subproto
|
||||||
|
else:
|
||||||
|
return False, None
|
||||||
|
|
||||||
|
|
||||||
|
def _create_sec_websocket_key():
|
||||||
|
randomness = os.urandom(16)
|
||||||
|
return base64encode(randomness).decode('utf-8').strip()
|
329
resources/lib/websocket/_http.py
Normal file
329
resources/lib/websocket/_http.py
Normal file
|
@ -0,0 +1,329 @@
|
||||||
|
"""
|
||||||
|
websocket - WebSocket client library for Python
|
||||||
|
|
||||||
|
Copyright (C) 2010 Hiroki Ohtani(liris)
|
||||||
|
|
||||||
|
This library is free software; you can redistribute it and/or
|
||||||
|
modify it under the terms of the GNU Lesser General Public
|
||||||
|
License as published by the Free Software Foundation; either
|
||||||
|
version 2.1 of the License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This library is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
Lesser General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Lesser General Public
|
||||||
|
License along with this library; if not, write to the Free Software
|
||||||
|
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
|
||||||
|
"""
|
||||||
|
import errno
|
||||||
|
import os
|
||||||
|
import socket
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import six
|
||||||
|
|
||||||
|
from ._exceptions import *
|
||||||
|
from ._logging import *
|
||||||
|
from ._socket import*
|
||||||
|
from ._ssl_compat import *
|
||||||
|
from ._url import *
|
||||||
|
|
||||||
|
if six.PY3:
|
||||||
|
from base64 import encodebytes as base64encode
|
||||||
|
else:
|
||||||
|
from base64 import encodestring as base64encode
|
||||||
|
|
||||||
|
__all__ = ["proxy_info", "connect", "read_headers"]
|
||||||
|
|
||||||
|
try:
|
||||||
|
import socks
|
||||||
|
ProxyConnectionError = socks.ProxyConnectionError
|
||||||
|
HAS_PYSOCKS = True
|
||||||
|
except:
|
||||||
|
class ProxyConnectionError(BaseException):
|
||||||
|
pass
|
||||||
|
HAS_PYSOCKS = False
|
||||||
|
|
||||||
|
class proxy_info(object):
|
||||||
|
|
||||||
|
def __init__(self, **options):
|
||||||
|
self.type = options.get("proxy_type") or "http"
|
||||||
|
if not(self.type in ['http', 'socks4', 'socks5', 'socks5h']):
|
||||||
|
raise ValueError("proxy_type must be 'http', 'socks4', 'socks5' or 'socks5h'")
|
||||||
|
self.host = options.get("http_proxy_host", None)
|
||||||
|
if self.host:
|
||||||
|
self.port = options.get("http_proxy_port", 0)
|
||||||
|
self.auth = options.get("http_proxy_auth", None)
|
||||||
|
self.no_proxy = options.get("http_no_proxy", None)
|
||||||
|
else:
|
||||||
|
self.port = 0
|
||||||
|
self.auth = None
|
||||||
|
self.no_proxy = None
|
||||||
|
|
||||||
|
|
||||||
|
def _open_proxied_socket(url, options, proxy):
|
||||||
|
hostname, port, resource, is_secure = parse_url(url)
|
||||||
|
|
||||||
|
if not HAS_PYSOCKS:
|
||||||
|
raise WebSocketException("PySocks module not found.")
|
||||||
|
|
||||||
|
ptype = socks.SOCKS5
|
||||||
|
rdns = False
|
||||||
|
if proxy.type == "socks4":
|
||||||
|
ptype = socks.SOCKS4
|
||||||
|
if proxy.type == "http":
|
||||||
|
ptype = socks.HTTP
|
||||||
|
if proxy.type[-1] == "h":
|
||||||
|
rdns = True
|
||||||
|
|
||||||
|
sock = socks.create_connection(
|
||||||
|
(hostname, port),
|
||||||
|
proxy_type = ptype,
|
||||||
|
proxy_addr = proxy.host,
|
||||||
|
proxy_port = proxy.port,
|
||||||
|
proxy_rdns = rdns,
|
||||||
|
proxy_username = proxy.auth[0] if proxy.auth else None,
|
||||||
|
proxy_password = proxy.auth[1] if proxy.auth else None,
|
||||||
|
timeout = options.timeout,
|
||||||
|
socket_options = DEFAULT_SOCKET_OPTION + options.sockopt
|
||||||
|
)
|
||||||
|
|
||||||
|
if is_secure:
|
||||||
|
if HAVE_SSL:
|
||||||
|
sock = _ssl_socket(sock, options.sslopt, hostname)
|
||||||
|
else:
|
||||||
|
raise WebSocketException("SSL not available.")
|
||||||
|
|
||||||
|
return sock, (hostname, port, resource)
|
||||||
|
|
||||||
|
|
||||||
|
def connect(url, options, proxy, socket):
|
||||||
|
if proxy.host and not socket and not (proxy.type == 'http'):
|
||||||
|
return _open_proxied_socket(url, options, proxy)
|
||||||
|
|
||||||
|
hostname, port, resource, is_secure = parse_url(url)
|
||||||
|
|
||||||
|
if socket:
|
||||||
|
return socket, (hostname, port, resource)
|
||||||
|
|
||||||
|
addrinfo_list, need_tunnel, auth = _get_addrinfo_list(
|
||||||
|
hostname, port, is_secure, proxy)
|
||||||
|
if not addrinfo_list:
|
||||||
|
raise WebSocketException(
|
||||||
|
"Host not found.: " + hostname + ":" + str(port))
|
||||||
|
|
||||||
|
sock = None
|
||||||
|
try:
|
||||||
|
sock = _open_socket(addrinfo_list, options.sockopt, options.timeout)
|
||||||
|
if need_tunnel:
|
||||||
|
sock = _tunnel(sock, hostname, port, auth)
|
||||||
|
|
||||||
|
if is_secure:
|
||||||
|
if HAVE_SSL:
|
||||||
|
sock = _ssl_socket(sock, options.sslopt, hostname)
|
||||||
|
else:
|
||||||
|
raise WebSocketException("SSL not available.")
|
||||||
|
|
||||||
|
return sock, (hostname, port, resource)
|
||||||
|
except:
|
||||||
|
if sock:
|
||||||
|
sock.close()
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def _get_addrinfo_list(hostname, port, is_secure, proxy):
|
||||||
|
phost, pport, pauth = get_proxy_info(
|
||||||
|
hostname, is_secure, proxy.host, proxy.port, proxy.auth, proxy.no_proxy)
|
||||||
|
try:
|
||||||
|
# when running on windows 10, getaddrinfo without socktype returns a socktype 0.
|
||||||
|
# This generates an error exception: `_on_error: exception Socket type must be stream or datagram, not 0`
|
||||||
|
# or `OSError: [Errno 22] Invalid argument` when creating socket. Force the socket type to SOCK_STREAM.
|
||||||
|
if not phost:
|
||||||
|
addrinfo_list = socket.getaddrinfo(
|
||||||
|
hostname, port, 0, socket.SOCK_STREAM, socket.SOL_TCP)
|
||||||
|
return addrinfo_list, False, None
|
||||||
|
else:
|
||||||
|
pport = pport and pport or 80
|
||||||
|
# when running on windows 10, the getaddrinfo used above
|
||||||
|
# returns a socktype 0. This generates an error exception:
|
||||||
|
# _on_error: exception Socket type must be stream or datagram, not 0
|
||||||
|
# Force the socket type to SOCK_STREAM
|
||||||
|
addrinfo_list = socket.getaddrinfo(phost, pport, 0, socket.SOCK_STREAM, socket.SOL_TCP)
|
||||||
|
return addrinfo_list, True, pauth
|
||||||
|
except socket.gaierror as e:
|
||||||
|
raise WebSocketAddressException(e)
|
||||||
|
|
||||||
|
|
||||||
|
def _open_socket(addrinfo_list, sockopt, timeout):
|
||||||
|
err = None
|
||||||
|
for addrinfo in addrinfo_list:
|
||||||
|
family, socktype, proto = addrinfo[:3]
|
||||||
|
sock = socket.socket(family, socktype, proto)
|
||||||
|
sock.settimeout(timeout)
|
||||||
|
for opts in DEFAULT_SOCKET_OPTION:
|
||||||
|
sock.setsockopt(*opts)
|
||||||
|
for opts in sockopt:
|
||||||
|
sock.setsockopt(*opts)
|
||||||
|
|
||||||
|
address = addrinfo[4]
|
||||||
|
err = None
|
||||||
|
while not err:
|
||||||
|
try:
|
||||||
|
sock.connect(address)
|
||||||
|
except ProxyConnectionError as error:
|
||||||
|
err = WebSocketProxyException(str(error))
|
||||||
|
err.remote_ip = str(address[0])
|
||||||
|
continue
|
||||||
|
except socket.error as error:
|
||||||
|
error.remote_ip = str(address[0])
|
||||||
|
try:
|
||||||
|
eConnRefused = (errno.ECONNREFUSED, errno.WSAECONNREFUSED)
|
||||||
|
except:
|
||||||
|
eConnRefused = (errno.ECONNREFUSED, )
|
||||||
|
if error.errno == errno.EINTR:
|
||||||
|
continue
|
||||||
|
elif error.errno in eConnRefused:
|
||||||
|
err = error
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
raise error
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
if err:
|
||||||
|
raise err
|
||||||
|
|
||||||
|
return sock
|
||||||
|
|
||||||
|
|
||||||
|
def _can_use_sni():
|
||||||
|
return six.PY2 and sys.version_info >= (2, 7, 9) or sys.version_info >= (3, 2)
|
||||||
|
|
||||||
|
|
||||||
|
def _wrap_sni_socket(sock, sslopt, hostname, check_hostname):
|
||||||
|
context = ssl.SSLContext(sslopt.get('ssl_version', ssl.PROTOCOL_SSLv23))
|
||||||
|
|
||||||
|
if sslopt.get('cert_reqs', ssl.CERT_NONE) != ssl.CERT_NONE:
|
||||||
|
cafile = sslopt.get('ca_certs', None)
|
||||||
|
capath = sslopt.get('ca_cert_path', None)
|
||||||
|
if cafile or capath:
|
||||||
|
context.load_verify_locations(cafile=cafile, capath=capath)
|
||||||
|
elif hasattr(context, 'load_default_certs'):
|
||||||
|
context.load_default_certs(ssl.Purpose.SERVER_AUTH)
|
||||||
|
if sslopt.get('certfile', None):
|
||||||
|
context.load_cert_chain(
|
||||||
|
sslopt['certfile'],
|
||||||
|
sslopt.get('keyfile', None),
|
||||||
|
sslopt.get('password', None),
|
||||||
|
)
|
||||||
|
# see
|
||||||
|
# https://github.com/liris/websocket-client/commit/b96a2e8fa765753e82eea531adb19716b52ca3ca#commitcomment-10803153
|
||||||
|
context.verify_mode = sslopt['cert_reqs']
|
||||||
|
if HAVE_CONTEXT_CHECK_HOSTNAME:
|
||||||
|
context.check_hostname = check_hostname
|
||||||
|
if 'ciphers' in sslopt:
|
||||||
|
context.set_ciphers(sslopt['ciphers'])
|
||||||
|
if 'cert_chain' in sslopt:
|
||||||
|
certfile, keyfile, password = sslopt['cert_chain']
|
||||||
|
context.load_cert_chain(certfile, keyfile, password)
|
||||||
|
if 'ecdh_curve' in sslopt:
|
||||||
|
context.set_ecdh_curve(sslopt['ecdh_curve'])
|
||||||
|
|
||||||
|
return context.wrap_socket(
|
||||||
|
sock,
|
||||||
|
do_handshake_on_connect=sslopt.get('do_handshake_on_connect', True),
|
||||||
|
suppress_ragged_eofs=sslopt.get('suppress_ragged_eofs', True),
|
||||||
|
server_hostname=hostname,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _ssl_socket(sock, user_sslopt, hostname):
|
||||||
|
sslopt = dict(cert_reqs=ssl.CERT_REQUIRED)
|
||||||
|
sslopt.update(user_sslopt)
|
||||||
|
|
||||||
|
certPath = os.environ.get('WEBSOCKET_CLIENT_CA_BUNDLE')
|
||||||
|
if certPath and os.path.isfile(certPath) \
|
||||||
|
and user_sslopt.get('ca_certs', None) is None \
|
||||||
|
and user_sslopt.get('ca_cert', None) is None:
|
||||||
|
sslopt['ca_certs'] = certPath
|
||||||
|
elif certPath and os.path.isdir(certPath) \
|
||||||
|
and user_sslopt.get('ca_cert_path', None) is None:
|
||||||
|
sslopt['ca_cert_path'] = certPath
|
||||||
|
|
||||||
|
check_hostname = sslopt["cert_reqs"] != ssl.CERT_NONE and sslopt.pop(
|
||||||
|
'check_hostname', True)
|
||||||
|
|
||||||
|
if _can_use_sni():
|
||||||
|
sock = _wrap_sni_socket(sock, sslopt, hostname, check_hostname)
|
||||||
|
else:
|
||||||
|
sslopt.pop('check_hostname', True)
|
||||||
|
sock = ssl.wrap_socket(sock, **sslopt)
|
||||||
|
|
||||||
|
if not HAVE_CONTEXT_CHECK_HOSTNAME and check_hostname:
|
||||||
|
match_hostname(sock.getpeercert(), hostname)
|
||||||
|
|
||||||
|
return sock
|
||||||
|
|
||||||
|
|
||||||
|
def _tunnel(sock, host, port, auth):
|
||||||
|
debug("Connecting proxy...")
|
||||||
|
connect_header = "CONNECT %s:%d HTTP/1.0\r\n" % (host, port)
|
||||||
|
# TODO: support digest auth.
|
||||||
|
if auth and auth[0]:
|
||||||
|
auth_str = auth[0]
|
||||||
|
if auth[1]:
|
||||||
|
auth_str += ":" + auth[1]
|
||||||
|
encoded_str = base64encode(auth_str.encode()).strip().decode().replace('\n', '')
|
||||||
|
connect_header += "Proxy-Authorization: Basic %s\r\n" % encoded_str
|
||||||
|
connect_header += "\r\n"
|
||||||
|
dump("request header", connect_header)
|
||||||
|
|
||||||
|
send(sock, connect_header)
|
||||||
|
|
||||||
|
try:
|
||||||
|
status, resp_headers, status_message = read_headers(sock)
|
||||||
|
except Exception as e:
|
||||||
|
raise WebSocketProxyException(str(e))
|
||||||
|
|
||||||
|
if status != 200:
|
||||||
|
raise WebSocketProxyException(
|
||||||
|
"failed CONNECT via proxy status: %r" % status)
|
||||||
|
|
||||||
|
return sock
|
||||||
|
|
||||||
|
|
||||||
|
def read_headers(sock):
|
||||||
|
status = None
|
||||||
|
status_message = None
|
||||||
|
headers = {}
|
||||||
|
trace("--- response header ---")
|
||||||
|
|
||||||
|
while True:
|
||||||
|
line = recv_line(sock)
|
||||||
|
line = line.decode('utf-8').strip()
|
||||||
|
if not line:
|
||||||
|
break
|
||||||
|
trace(line)
|
||||||
|
if not status:
|
||||||
|
|
||||||
|
status_info = line.split(" ", 2)
|
||||||
|
status = int(status_info[1])
|
||||||
|
if len(status_info) > 2:
|
||||||
|
status_message = status_info[2]
|
||||||
|
else:
|
||||||
|
kv = line.split(":", 1)
|
||||||
|
if len(kv) == 2:
|
||||||
|
key, value = kv
|
||||||
|
headers[key.lower()] = value.strip()
|
||||||
|
else:
|
||||||
|
raise WebSocketException("Invalid header")
|
||||||
|
|
||||||
|
trace("-----------------------")
|
||||||
|
|
||||||
|
return status, headers, status_message
|
90
resources/lib/websocket/_logging.py
Normal file
90
resources/lib/websocket/_logging.py
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""
|
||||||
|
websocket - WebSocket client library for Python
|
||||||
|
|
||||||
|
Copyright (C) 2010 Hiroki Ohtani(liris)
|
||||||
|
|
||||||
|
This library is free software; you can redistribute it and/or
|
||||||
|
modify it under the terms of the GNU Lesser General Public
|
||||||
|
License as published by the Free Software Foundation; either
|
||||||
|
version 2.1 of the License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This library is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
Lesser General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Lesser General Public
|
||||||
|
License along with this library; if not, write to the Free Software
|
||||||
|
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
|
||||||
|
_logger = logging.getLogger('websocket')
|
||||||
|
try:
|
||||||
|
from logging import NullHandler
|
||||||
|
except ImportError:
|
||||||
|
class NullHandler(logging.Handler):
|
||||||
|
def emit(self, record):
|
||||||
|
pass
|
||||||
|
|
||||||
|
_logger.addHandler(NullHandler())
|
||||||
|
|
||||||
|
_traceEnabled = False
|
||||||
|
|
||||||
|
__all__ = ["enableTrace", "dump", "error", "warning", "debug", "trace",
|
||||||
|
"isEnabledForError", "isEnabledForDebug", "isEnabledForTrace"]
|
||||||
|
|
||||||
|
|
||||||
|
def enableTrace(traceable, handler = logging.StreamHandler()):
|
||||||
|
"""
|
||||||
|
Turn on/off the traceability.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
traceable: bool
|
||||||
|
If set to True, traceability is enabled.
|
||||||
|
"""
|
||||||
|
global _traceEnabled
|
||||||
|
_traceEnabled = traceable
|
||||||
|
if traceable:
|
||||||
|
_logger.addHandler(handler)
|
||||||
|
_logger.setLevel(logging.DEBUG)
|
||||||
|
|
||||||
|
def dump(title, message):
|
||||||
|
if _traceEnabled:
|
||||||
|
_logger.debug("--- " + title + " ---")
|
||||||
|
_logger.debug(message)
|
||||||
|
_logger.debug("-----------------------")
|
||||||
|
|
||||||
|
|
||||||
|
def error(msg):
|
||||||
|
_logger.error(msg)
|
||||||
|
|
||||||
|
|
||||||
|
def warning(msg):
|
||||||
|
_logger.warning(msg)
|
||||||
|
|
||||||
|
|
||||||
|
def debug(msg):
|
||||||
|
_logger.debug(msg)
|
||||||
|
|
||||||
|
|
||||||
|
def trace(msg):
|
||||||
|
if _traceEnabled:
|
||||||
|
_logger.debug(msg)
|
||||||
|
|
||||||
|
|
||||||
|
def isEnabledForError():
|
||||||
|
return _logger.isEnabledFor(logging.ERROR)
|
||||||
|
|
||||||
|
|
||||||
|
def isEnabledForDebug():
|
||||||
|
return _logger.isEnabledFor(logging.DEBUG)
|
||||||
|
|
||||||
|
def isEnabledForTrace():
|
||||||
|
return _traceEnabled
|
177
resources/lib/websocket/_socket.py
Normal file
177
resources/lib/websocket/_socket.py
Normal file
|
@ -0,0 +1,177 @@
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""
|
||||||
|
websocket - WebSocket client library for Python
|
||||||
|
|
||||||
|
Copyright (C) 2010 Hiroki Ohtani(liris)
|
||||||
|
|
||||||
|
This library is free software; you can redistribute it and/or
|
||||||
|
modify it under the terms of the GNU Lesser General Public
|
||||||
|
License as published by the Free Software Foundation; either
|
||||||
|
version 2.1 of the License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This library is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
Lesser General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Lesser General Public
|
||||||
|
License along with this library; if not, write to the Free Software
|
||||||
|
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
|
||||||
|
"""
|
||||||
|
import errno
|
||||||
|
import select
|
||||||
|
import socket
|
||||||
|
|
||||||
|
import six
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from ._exceptions import *
|
||||||
|
from ._ssl_compat import *
|
||||||
|
from ._utils import *
|
||||||
|
|
||||||
|
DEFAULT_SOCKET_OPTION = [(socket.SOL_TCP, socket.TCP_NODELAY, 1)]
|
||||||
|
if hasattr(socket, "SO_KEEPALIVE"):
|
||||||
|
DEFAULT_SOCKET_OPTION.append((socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1))
|
||||||
|
if hasattr(socket, "TCP_KEEPIDLE"):
|
||||||
|
DEFAULT_SOCKET_OPTION.append((socket.SOL_TCP, socket.TCP_KEEPIDLE, 30))
|
||||||
|
if hasattr(socket, "TCP_KEEPINTVL"):
|
||||||
|
DEFAULT_SOCKET_OPTION.append((socket.SOL_TCP, socket.TCP_KEEPINTVL, 10))
|
||||||
|
if hasattr(socket, "TCP_KEEPCNT"):
|
||||||
|
DEFAULT_SOCKET_OPTION.append((socket.SOL_TCP, socket.TCP_KEEPCNT, 3))
|
||||||
|
|
||||||
|
_default_timeout = None
|
||||||
|
|
||||||
|
__all__ = ["DEFAULT_SOCKET_OPTION", "sock_opt", "setdefaulttimeout", "getdefaulttimeout",
|
||||||
|
"recv", "recv_line", "send"]
|
||||||
|
|
||||||
|
|
||||||
|
class sock_opt(object):
|
||||||
|
|
||||||
|
def __init__(self, sockopt, sslopt):
|
||||||
|
if sockopt is None:
|
||||||
|
sockopt = []
|
||||||
|
if sslopt is None:
|
||||||
|
sslopt = {}
|
||||||
|
self.sockopt = sockopt
|
||||||
|
self.sslopt = sslopt
|
||||||
|
self.timeout = None
|
||||||
|
|
||||||
|
|
||||||
|
def setdefaulttimeout(timeout):
|
||||||
|
"""
|
||||||
|
Set the global timeout setting to connect.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
timeout: int or float
|
||||||
|
default socket timeout time (in seconds)
|
||||||
|
"""
|
||||||
|
global _default_timeout
|
||||||
|
_default_timeout = timeout
|
||||||
|
|
||||||
|
|
||||||
|
def getdefaulttimeout():
|
||||||
|
"""
|
||||||
|
Get default timeout
|
||||||
|
|
||||||
|
Returns
|
||||||
|
----------
|
||||||
|
_default_timeout: int or float
|
||||||
|
Return the global timeout setting (in seconds) to connect.
|
||||||
|
"""
|
||||||
|
return _default_timeout
|
||||||
|
|
||||||
|
|
||||||
|
def recv(sock, bufsize):
|
||||||
|
if not sock:
|
||||||
|
raise WebSocketConnectionClosedException("socket is already closed.")
|
||||||
|
|
||||||
|
def _recv():
|
||||||
|
try:
|
||||||
|
return sock.recv(bufsize)
|
||||||
|
except SSLWantReadError:
|
||||||
|
pass
|
||||||
|
except socket.error as exc:
|
||||||
|
error_code = extract_error_code(exc)
|
||||||
|
if error_code is None:
|
||||||
|
raise
|
||||||
|
if error_code != errno.EAGAIN or error_code != errno.EWOULDBLOCK:
|
||||||
|
raise
|
||||||
|
|
||||||
|
r, w, e = select.select((sock, ), (), (), sock.gettimeout())
|
||||||
|
if r:
|
||||||
|
return sock.recv(bufsize)
|
||||||
|
|
||||||
|
try:
|
||||||
|
if sock.gettimeout() == 0:
|
||||||
|
bytes_ = sock.recv(bufsize)
|
||||||
|
else:
|
||||||
|
bytes_ = _recv()
|
||||||
|
except socket.timeout as e:
|
||||||
|
message = extract_err_message(e)
|
||||||
|
raise WebSocketTimeoutException(message)
|
||||||
|
except SSLError as e:
|
||||||
|
message = extract_err_message(e)
|
||||||
|
if isinstance(message, str) and 'timed out' in message:
|
||||||
|
raise WebSocketTimeoutException(message)
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
|
||||||
|
if not bytes_:
|
||||||
|
raise WebSocketConnectionClosedException(
|
||||||
|
"Connection is already closed.")
|
||||||
|
|
||||||
|
return bytes_
|
||||||
|
|
||||||
|
|
||||||
|
def recv_line(sock):
|
||||||
|
line = []
|
||||||
|
while True:
|
||||||
|
c = recv(sock, 1)
|
||||||
|
line.append(c)
|
||||||
|
if c == six.b("\n"):
|
||||||
|
break
|
||||||
|
return six.b("").join(line)
|
||||||
|
|
||||||
|
|
||||||
|
def send(sock, data):
|
||||||
|
if isinstance(data, six.text_type):
|
||||||
|
data = data.encode('utf-8')
|
||||||
|
|
||||||
|
if not sock:
|
||||||
|
raise WebSocketConnectionClosedException("socket is already closed.")
|
||||||
|
|
||||||
|
def _send():
|
||||||
|
try:
|
||||||
|
return sock.send(data)
|
||||||
|
except SSLWantWriteError:
|
||||||
|
pass
|
||||||
|
except socket.error as exc:
|
||||||
|
error_code = extract_error_code(exc)
|
||||||
|
if error_code is None:
|
||||||
|
raise
|
||||||
|
if error_code != errno.EAGAIN or error_code != errno.EWOULDBLOCK:
|
||||||
|
raise
|
||||||
|
|
||||||
|
r, w, e = select.select((), (sock, ), (), sock.gettimeout())
|
||||||
|
if w:
|
||||||
|
return sock.send(data)
|
||||||
|
|
||||||
|
try:
|
||||||
|
if sock.gettimeout() == 0:
|
||||||
|
return sock.send(data)
|
||||||
|
else:
|
||||||
|
return _send()
|
||||||
|
except socket.timeout as e:
|
||||||
|
message = extract_err_message(e)
|
||||||
|
raise WebSocketTimeoutException(message)
|
||||||
|
except Exception as e:
|
||||||
|
message = extract_err_message(e)
|
||||||
|
if isinstance(message, str) and "timed out" in message:
|
||||||
|
raise WebSocketTimeoutException(message)
|
||||||
|
else:
|
||||||
|
raise
|
53
resources/lib/websocket/_ssl_compat.py
Normal file
53
resources/lib/websocket/_ssl_compat.py
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
"""
|
||||||
|
websocket - WebSocket client library for Python
|
||||||
|
|
||||||
|
Copyright (C) 2010 Hiroki Ohtani(liris)
|
||||||
|
|
||||||
|
This library is free software; you can redistribute it and/or
|
||||||
|
modify it under the terms of the GNU Lesser General Public
|
||||||
|
License as published by the Free Software Foundation; either
|
||||||
|
version 2.1 of the License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This library is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
Lesser General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Lesser General Public
|
||||||
|
License along with this library; if not, write to the Free Software
|
||||||
|
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
|
||||||
|
"""
|
||||||
|
__all__ = ["HAVE_SSL", "ssl", "SSLError", "SSLWantReadError", "SSLWantWriteError"]
|
||||||
|
|
||||||
|
try:
|
||||||
|
import ssl
|
||||||
|
from ssl import SSLError
|
||||||
|
from ssl import SSLWantReadError
|
||||||
|
from ssl import SSLWantWriteError
|
||||||
|
if hasattr(ssl, 'SSLContext') and hasattr(ssl.SSLContext, 'check_hostname'):
|
||||||
|
HAVE_CONTEXT_CHECK_HOSTNAME = True
|
||||||
|
else:
|
||||||
|
HAVE_CONTEXT_CHECK_HOSTNAME = False
|
||||||
|
if hasattr(ssl, "match_hostname"):
|
||||||
|
from ssl import match_hostname
|
||||||
|
else:
|
||||||
|
from backports.ssl_match_hostname import match_hostname
|
||||||
|
__all__.append("match_hostname")
|
||||||
|
__all__.append("HAVE_CONTEXT_CHECK_HOSTNAME")
|
||||||
|
|
||||||
|
HAVE_SSL = True
|
||||||
|
except ImportError:
|
||||||
|
# dummy class of SSLError for ssl none-support environment.
|
||||||
|
class SSLError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class SSLWantReadError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class SSLWantWriteError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
ssl = lambda: None
|
||||||
|
|
||||||
|
HAVE_SSL = False
|
172
resources/lib/websocket/_url.py
Normal file
172
resources/lib/websocket/_url.py
Normal file
|
@ -0,0 +1,172 @@
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""
|
||||||
|
"""
|
||||||
|
websocket - WebSocket client library for Python
|
||||||
|
|
||||||
|
Copyright (C) 2010 Hiroki Ohtani(liris)
|
||||||
|
|
||||||
|
This library is free software; you can redistribute it and/or
|
||||||
|
modify it under the terms of the GNU Lesser General Public
|
||||||
|
License as published by the Free Software Foundation; either
|
||||||
|
version 2.1 of the License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This library is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
Lesser General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Lesser General Public
|
||||||
|
License along with this library; if not, write to the Free Software
|
||||||
|
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import socket
|
||||||
|
import struct
|
||||||
|
|
||||||
|
from six.moves.urllib.parse import urlparse
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ["parse_url", "get_proxy_info"]
|
||||||
|
|
||||||
|
|
||||||
|
def parse_url(url):
|
||||||
|
"""
|
||||||
|
parse url and the result is tuple of
|
||||||
|
(hostname, port, resource path and the flag of secure mode)
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
url: str
|
||||||
|
url string.
|
||||||
|
"""
|
||||||
|
if ":" not in url:
|
||||||
|
raise ValueError("url is invalid")
|
||||||
|
|
||||||
|
scheme, url = url.split(":", 1)
|
||||||
|
|
||||||
|
parsed = urlparse(url, scheme="ws")
|
||||||
|
if parsed.hostname:
|
||||||
|
hostname = parsed.hostname
|
||||||
|
else:
|
||||||
|
raise ValueError("hostname is invalid")
|
||||||
|
port = 0
|
||||||
|
if parsed.port:
|
||||||
|
port = parsed.port
|
||||||
|
|
||||||
|
is_secure = False
|
||||||
|
if scheme == "ws":
|
||||||
|
if not port:
|
||||||
|
port = 80
|
||||||
|
elif scheme == "wss":
|
||||||
|
is_secure = True
|
||||||
|
if not port:
|
||||||
|
port = 443
|
||||||
|
else:
|
||||||
|
raise ValueError("scheme %s is invalid" % scheme)
|
||||||
|
|
||||||
|
if parsed.path:
|
||||||
|
resource = parsed.path
|
||||||
|
else:
|
||||||
|
resource = "/"
|
||||||
|
|
||||||
|
if parsed.query:
|
||||||
|
resource += "?" + parsed.query
|
||||||
|
|
||||||
|
return hostname, port, resource, is_secure
|
||||||
|
|
||||||
|
|
||||||
|
DEFAULT_NO_PROXY_HOST = ["localhost", "127.0.0.1"]
|
||||||
|
|
||||||
|
|
||||||
|
def _is_ip_address(addr):
|
||||||
|
try:
|
||||||
|
socket.inet_aton(addr)
|
||||||
|
except socket.error:
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def _is_subnet_address(hostname):
|
||||||
|
try:
|
||||||
|
addr, netmask = hostname.split("/")
|
||||||
|
return _is_ip_address(addr) and 0 <= int(netmask) < 32
|
||||||
|
except ValueError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _is_address_in_network(ip, net):
|
||||||
|
ipaddr = struct.unpack('I', socket.inet_aton(ip))[0]
|
||||||
|
netaddr, bits = net.split('/')
|
||||||
|
netmask = struct.unpack('I', socket.inet_aton(netaddr))[0] & ((2 << int(bits) - 1) - 1)
|
||||||
|
return ipaddr & netmask == netmask
|
||||||
|
|
||||||
|
|
||||||
|
def _is_no_proxy_host(hostname, no_proxy):
|
||||||
|
if not no_proxy:
|
||||||
|
v = os.environ.get("no_proxy", "").replace(" ", "")
|
||||||
|
if v:
|
||||||
|
no_proxy = v.split(",")
|
||||||
|
if not no_proxy:
|
||||||
|
no_proxy = DEFAULT_NO_PROXY_HOST
|
||||||
|
|
||||||
|
if hostname in no_proxy:
|
||||||
|
return True
|
||||||
|
elif _is_ip_address(hostname):
|
||||||
|
return any([_is_address_in_network(hostname, subnet) for subnet in no_proxy if _is_subnet_address(subnet)])
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def get_proxy_info(
|
||||||
|
hostname, is_secure, proxy_host=None, proxy_port=0, proxy_auth=None,
|
||||||
|
no_proxy=None, proxy_type='http'):
|
||||||
|
"""
|
||||||
|
Try to retrieve proxy host and port from environment
|
||||||
|
if not provided in options.
|
||||||
|
Result is (proxy_host, proxy_port, proxy_auth).
|
||||||
|
proxy_auth is tuple of username and password
|
||||||
|
of proxy authentication information.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
hostname: <type>
|
||||||
|
websocket server name.
|
||||||
|
is_secure: <type>
|
||||||
|
is the connection secure? (wss) looks for "https_proxy" in env
|
||||||
|
before falling back to "http_proxy"
|
||||||
|
options: <type>
|
||||||
|
- http_proxy_host: <type>
|
||||||
|
http proxy host name.
|
||||||
|
- http_proxy_port: <type>
|
||||||
|
http proxy port.
|
||||||
|
- http_no_proxy: <type>
|
||||||
|
host names, which doesn't use proxy.
|
||||||
|
- http_proxy_auth: <type>
|
||||||
|
http proxy auth information. tuple of username and password. default is None
|
||||||
|
- proxy_type: <type>
|
||||||
|
if set to "socks5" PySocks wrapper will be used in place of a http proxy. default is "http"
|
||||||
|
"""
|
||||||
|
if _is_no_proxy_host(hostname, no_proxy):
|
||||||
|
return None, 0, None
|
||||||
|
|
||||||
|
if proxy_host:
|
||||||
|
port = proxy_port
|
||||||
|
auth = proxy_auth
|
||||||
|
return proxy_host, port, auth
|
||||||
|
|
||||||
|
env_keys = ["http_proxy"]
|
||||||
|
if is_secure:
|
||||||
|
env_keys.insert(0, "https_proxy")
|
||||||
|
|
||||||
|
for key in env_keys:
|
||||||
|
value = os.environ.get(key, None)
|
||||||
|
if value:
|
||||||
|
proxy = urlparse(value)
|
||||||
|
auth = (proxy.username, proxy.password) if proxy.username else None
|
||||||
|
return proxy.hostname, proxy.port, auth
|
||||||
|
|
||||||
|
return None, 0, None
|
110
resources/lib/websocket/_utils.py
Normal file
110
resources/lib/websocket/_utils.py
Normal file
|
@ -0,0 +1,110 @@
|
||||||
|
"""
|
||||||
|
websocket - WebSocket client library for Python
|
||||||
|
|
||||||
|
Copyright (C) 2010 Hiroki Ohtani(liris)
|
||||||
|
|
||||||
|
This library is free software; you can redistribute it and/or
|
||||||
|
modify it under the terms of the GNU Lesser General Public
|
||||||
|
License as published by the Free Software Foundation; either
|
||||||
|
version 2.1 of the License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This library is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
Lesser General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Lesser General Public
|
||||||
|
License along with this library; if not, write to the Free Software
|
||||||
|
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
|
||||||
|
"""
|
||||||
|
import six
|
||||||
|
|
||||||
|
__all__ = ["NoLock", "validate_utf8", "extract_err_message", "extract_error_code"]
|
||||||
|
|
||||||
|
|
||||||
|
class NoLock(object):
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def __exit__(self, exc_type, exc_value, traceback):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
# If wsaccel is available we use compiled routines to validate UTF-8
|
||||||
|
# strings.
|
||||||
|
from wsaccel.utf8validator import Utf8Validator
|
||||||
|
|
||||||
|
def _validate_utf8(utfbytes):
|
||||||
|
return Utf8Validator().validate(utfbytes)[0]
|
||||||
|
|
||||||
|
except ImportError:
|
||||||
|
# UTF-8 validator
|
||||||
|
# python implementation of http://bjoern.hoehrmann.de/utf-8/decoder/dfa/
|
||||||
|
|
||||||
|
_UTF8_ACCEPT = 0
|
||||||
|
_UTF8_REJECT = 12
|
||||||
|
|
||||||
|
_UTF8D = [
|
||||||
|
# The first part of the table maps bytes to character classes that
|
||||||
|
# to reduce the size of the transition table and create bitmasks.
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, 9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,
|
||||||
|
7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, 7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,
|
||||||
|
8,8,2,2,2,2,2,2,2,2,2,2,2,2,2,2, 2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,
|
||||||
|
10,3,3,3,3,3,3,3,3,3,3,3,3,4,3,3, 11,6,6,6,5,8,8,8,8,8,8,8,8,8,8,8,
|
||||||
|
|
||||||
|
# The second part is a transition table that maps a combination
|
||||||
|
# of a state of the automaton and a character class to a state.
|
||||||
|
0,12,24,36,60,96,84,12,12,12,48,72, 12,12,12,12,12,12,12,12,12,12,12,12,
|
||||||
|
12, 0,12,12,12,12,12, 0,12, 0,12,12, 12,24,12,12,12,12,12,24,12,24,12,12,
|
||||||
|
12,12,12,12,12,12,12,24,12,12,12,12, 12,24,12,12,12,12,12,12,12,24,12,12,
|
||||||
|
12,12,12,12,12,12,12,36,12,36,12,12, 12,36,12,12,12,12,12,36,12,36,12,12,
|
||||||
|
12,36,12,12,12,12,12,12,12,12,12,12, ]
|
||||||
|
|
||||||
|
def _decode(state, codep, ch):
|
||||||
|
tp = _UTF8D[ch]
|
||||||
|
|
||||||
|
codep = (ch & 0x3f) | (codep << 6) if (
|
||||||
|
state != _UTF8_ACCEPT) else (0xff >> tp) & ch
|
||||||
|
state = _UTF8D[256 + state + tp]
|
||||||
|
|
||||||
|
return state, codep
|
||||||
|
|
||||||
|
def _validate_utf8(utfbytes):
|
||||||
|
state = _UTF8_ACCEPT
|
||||||
|
codep = 0
|
||||||
|
for i in utfbytes:
|
||||||
|
if six.PY2:
|
||||||
|
i = ord(i)
|
||||||
|
state, codep = _decode(state, codep, i)
|
||||||
|
if state == _UTF8_REJECT:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def validate_utf8(utfbytes):
|
||||||
|
"""
|
||||||
|
validate utf8 byte string.
|
||||||
|
utfbytes: utf byte string to check.
|
||||||
|
return value: if valid utf8 string, return true. Otherwise, return false.
|
||||||
|
"""
|
||||||
|
return _validate_utf8(utfbytes)
|
||||||
|
|
||||||
|
|
||||||
|
def extract_err_message(exception):
|
||||||
|
if exception.args:
|
||||||
|
return exception.args[0]
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def extract_error_code(exception):
|
||||||
|
if exception.args and len(exception.args) > 1:
|
||||||
|
return exception.args[0] if isinstance(exception.args[0], int) else None
|
0
resources/lib/websocket/tests/__init__.py
Normal file
0
resources/lib/websocket/tests/__init__.py
Normal file
6
resources/lib/websocket/tests/data/header01.txt
Normal file
6
resources/lib/websocket/tests/data/header01.txt
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
HTTP/1.1 101 WebSocket Protocol Handshake
|
||||||
|
Connection: Upgrade
|
||||||
|
Upgrade: WebSocket
|
||||||
|
Sec-WebSocket-Accept: Kxep+hNu9n51529fGidYu7a3wO0=
|
||||||
|
some_header: something
|
||||||
|
|
6
resources/lib/websocket/tests/data/header02.txt
Normal file
6
resources/lib/websocket/tests/data/header02.txt
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
HTTP/1.1 101 WebSocket Protocol Handshake
|
||||||
|
Connection: Upgrade
|
||||||
|
Upgrade WebSocket
|
||||||
|
Sec-WebSocket-Accept: Kxep+hNu9n51529fGidYu7a3wO0=
|
||||||
|
some_header: something
|
||||||
|
|
122
resources/lib/websocket/tests/test_cookiejar.py
Normal file
122
resources/lib/websocket/tests/test_cookiejar.py
Normal file
|
@ -0,0 +1,122 @@
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""
|
||||||
|
websocket - WebSocket client library for Python
|
||||||
|
|
||||||
|
Copyright (C) 2010 Hiroki Ohtani(liris)
|
||||||
|
|
||||||
|
This library is free software; you can redistribute it and/or
|
||||||
|
modify it under the terms of the GNU Lesser General Public
|
||||||
|
License as published by the Free Software Foundation; either
|
||||||
|
version 2.1 of the License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This library is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
Lesser General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Lesser General Public
|
||||||
|
License along with this library; if not, write to the Free Software
|
||||||
|
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
|
||||||
|
"""
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from websocket._cookiejar import SimpleCookieJar
|
||||||
|
|
||||||
|
try:
|
||||||
|
import Cookie
|
||||||
|
except:
|
||||||
|
import http.cookies as Cookie
|
||||||
|
|
||||||
|
|
||||||
|
class CookieJarTest(unittest.TestCase):
|
||||||
|
def testAdd(self):
|
||||||
|
cookie_jar = SimpleCookieJar()
|
||||||
|
cookie_jar.add("")
|
||||||
|
self.assertFalse(cookie_jar.jar, "Cookie with no domain should not be added to the jar")
|
||||||
|
|
||||||
|
cookie_jar = SimpleCookieJar()
|
||||||
|
cookie_jar.add("a=b")
|
||||||
|
self.assertFalse(cookie_jar.jar, "Cookie with no domain should not be added to the jar")
|
||||||
|
|
||||||
|
cookie_jar = SimpleCookieJar()
|
||||||
|
cookie_jar.add("a=b; domain=.abc")
|
||||||
|
self.assertTrue(".abc" in cookie_jar.jar)
|
||||||
|
|
||||||
|
cookie_jar = SimpleCookieJar()
|
||||||
|
cookie_jar.add("a=b; domain=abc")
|
||||||
|
self.assertTrue(".abc" in cookie_jar.jar)
|
||||||
|
self.assertTrue("abc" not in cookie_jar.jar)
|
||||||
|
|
||||||
|
cookie_jar = SimpleCookieJar()
|
||||||
|
cookie_jar.add("a=b; c=d; domain=abc")
|
||||||
|
self.assertEqual(cookie_jar.get("abc"), "a=b; c=d")
|
||||||
|
|
||||||
|
cookie_jar = SimpleCookieJar()
|
||||||
|
cookie_jar.add("a=b; c=d; domain=abc")
|
||||||
|
cookie_jar.add("e=f; domain=abc")
|
||||||
|
self.assertEqual(cookie_jar.get("abc"), "a=b; c=d; e=f")
|
||||||
|
|
||||||
|
cookie_jar = SimpleCookieJar()
|
||||||
|
cookie_jar.add("a=b; c=d; domain=abc")
|
||||||
|
cookie_jar.add("e=f; domain=.abc")
|
||||||
|
self.assertEqual(cookie_jar.get("abc"), "a=b; c=d; e=f")
|
||||||
|
|
||||||
|
cookie_jar = SimpleCookieJar()
|
||||||
|
cookie_jar.add("a=b; c=d; domain=abc")
|
||||||
|
cookie_jar.add("e=f; domain=xyz")
|
||||||
|
self.assertEqual(cookie_jar.get("abc"), "a=b; c=d")
|
||||||
|
self.assertEqual(cookie_jar.get("xyz"), "e=f")
|
||||||
|
self.assertEqual(cookie_jar.get("something"), "")
|
||||||
|
|
||||||
|
def testSet(self):
|
||||||
|
cookie_jar = SimpleCookieJar()
|
||||||
|
cookie_jar.set("a=b")
|
||||||
|
self.assertFalse(cookie_jar.jar, "Cookie with no domain should not be added to the jar")
|
||||||
|
|
||||||
|
cookie_jar = SimpleCookieJar()
|
||||||
|
cookie_jar.set("a=b; domain=.abc")
|
||||||
|
self.assertTrue(".abc" in cookie_jar.jar)
|
||||||
|
|
||||||
|
cookie_jar = SimpleCookieJar()
|
||||||
|
cookie_jar.set("a=b; domain=abc")
|
||||||
|
self.assertTrue(".abc" in cookie_jar.jar)
|
||||||
|
self.assertTrue("abc" not in cookie_jar.jar)
|
||||||
|
|
||||||
|
cookie_jar = SimpleCookieJar()
|
||||||
|
cookie_jar.set("a=b; c=d; domain=abc")
|
||||||
|
self.assertEqual(cookie_jar.get("abc"), "a=b; c=d")
|
||||||
|
|
||||||
|
cookie_jar = SimpleCookieJar()
|
||||||
|
cookie_jar.set("a=b; c=d; domain=abc")
|
||||||
|
cookie_jar.set("e=f; domain=abc")
|
||||||
|
self.assertEqual(cookie_jar.get("abc"), "e=f")
|
||||||
|
|
||||||
|
cookie_jar = SimpleCookieJar()
|
||||||
|
cookie_jar.set("a=b; c=d; domain=abc")
|
||||||
|
cookie_jar.set("e=f; domain=.abc")
|
||||||
|
self.assertEqual(cookie_jar.get("abc"), "e=f")
|
||||||
|
|
||||||
|
cookie_jar = SimpleCookieJar()
|
||||||
|
cookie_jar.set("a=b; c=d; domain=abc")
|
||||||
|
cookie_jar.set("e=f; domain=xyz")
|
||||||
|
self.assertEqual(cookie_jar.get("abc"), "a=b; c=d")
|
||||||
|
self.assertEqual(cookie_jar.get("xyz"), "e=f")
|
||||||
|
self.assertEqual(cookie_jar.get("something"), "")
|
||||||
|
|
||||||
|
def testGet(self):
|
||||||
|
cookie_jar = SimpleCookieJar()
|
||||||
|
cookie_jar.set("a=b; c=d; domain=abc.com")
|
||||||
|
self.assertEqual(cookie_jar.get("abc.com"), "a=b; c=d")
|
||||||
|
self.assertEqual(cookie_jar.get("x.abc.com"), "a=b; c=d")
|
||||||
|
self.assertEqual(cookie_jar.get("abc.com.es"), "")
|
||||||
|
self.assertEqual(cookie_jar.get("xabc.com"), "")
|
||||||
|
|
||||||
|
cookie_jar.set("a=b; c=d; domain=.abc.com")
|
||||||
|
self.assertEqual(cookie_jar.get("abc.com"), "a=b; c=d")
|
||||||
|
self.assertEqual(cookie_jar.get("x.abc.com"), "a=b; c=d")
|
||||||
|
self.assertEqual(cookie_jar.get("abc.com.es"), "")
|
||||||
|
self.assertEqual(cookie_jar.get("xabc.com"), "")
|
691
resources/lib/websocket/tests/test_websocket.py
Normal file
691
resources/lib/websocket/tests/test_websocket.py
Normal file
|
@ -0,0 +1,691 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""
|
||||||
|
websocket - WebSocket client library for Python
|
||||||
|
|
||||||
|
Copyright (C) 2010 Hiroki Ohtani(liris)
|
||||||
|
|
||||||
|
This library is free software; you can redistribute it and/or
|
||||||
|
modify it under the terms of the GNU Lesser General Public
|
||||||
|
License as published by the Free Software Foundation; either
|
||||||
|
version 2.1 of the License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This library is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
Lesser General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Lesser General Public
|
||||||
|
License along with this library; if not, write to the Free Software
|
||||||
|
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
sys.path[0:0] = [""]
|
||||||
|
|
||||||
|
import os
|
||||||
|
import os.path
|
||||||
|
import socket
|
||||||
|
|
||||||
|
import six
|
||||||
|
|
||||||
|
# websocket-client
|
||||||
|
import websocket as ws
|
||||||
|
from websocket._handshake import _create_sec_websocket_key, \
|
||||||
|
_validate as _validate_header
|
||||||
|
from websocket._http import read_headers
|
||||||
|
from websocket._url import get_proxy_info, parse_url
|
||||||
|
from websocket._utils import validate_utf8
|
||||||
|
|
||||||
|
if six.PY3:
|
||||||
|
from base64 import decodebytes as base64decode
|
||||||
|
else:
|
||||||
|
from base64 import decodestring as base64decode
|
||||||
|
|
||||||
|
if sys.version_info[0] == 2 and sys.version_info[1] < 7:
|
||||||
|
import unittest2 as unittest
|
||||||
|
else:
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
try:
|
||||||
|
from ssl import SSLError
|
||||||
|
except ImportError:
|
||||||
|
# dummy class of SSLError for ssl none-support environment.
|
||||||
|
class SSLError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Skip test to access the internet.
|
||||||
|
TEST_WITH_INTERNET = os.environ.get('TEST_WITH_INTERNET', '0') == '1'
|
||||||
|
|
||||||
|
# Skip Secure WebSocket test.
|
||||||
|
TEST_SECURE_WS = True
|
||||||
|
TRACEABLE = True
|
||||||
|
|
||||||
|
|
||||||
|
def create_mask_key(_):
|
||||||
|
return "abcd"
|
||||||
|
|
||||||
|
|
||||||
|
class SockMock(object):
|
||||||
|
def __init__(self):
|
||||||
|
self.data = []
|
||||||
|
self.sent = []
|
||||||
|
|
||||||
|
def add_packet(self, data):
|
||||||
|
self.data.append(data)
|
||||||
|
|
||||||
|
def gettimeout(self):
|
||||||
|
return None
|
||||||
|
|
||||||
|
def recv(self, bufsize):
|
||||||
|
if self.data:
|
||||||
|
e = self.data.pop(0)
|
||||||
|
if isinstance(e, Exception):
|
||||||
|
raise e
|
||||||
|
if len(e) > bufsize:
|
||||||
|
self.data.insert(0, e[bufsize:])
|
||||||
|
return e[:bufsize]
|
||||||
|
|
||||||
|
def send(self, data):
|
||||||
|
self.sent.append(data)
|
||||||
|
return len(data)
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class HeaderSockMock(SockMock):
|
||||||
|
|
||||||
|
def __init__(self, fname):
|
||||||
|
SockMock.__init__(self)
|
||||||
|
path = os.path.join(os.path.dirname(__file__), fname)
|
||||||
|
with open(path, "rb") as f:
|
||||||
|
self.add_packet(f.read())
|
||||||
|
|
||||||
|
|
||||||
|
class WebSocketTest(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
ws.enableTrace(TRACEABLE)
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def testDefaultTimeout(self):
|
||||||
|
self.assertEqual(ws.getdefaulttimeout(), None)
|
||||||
|
ws.setdefaulttimeout(10)
|
||||||
|
self.assertEqual(ws.getdefaulttimeout(), 10)
|
||||||
|
ws.setdefaulttimeout(None)
|
||||||
|
|
||||||
|
def testParseUrl(self):
|
||||||
|
p = parse_url("ws://www.example.com/r")
|
||||||
|
self.assertEqual(p[0], "www.example.com")
|
||||||
|
self.assertEqual(p[1], 80)
|
||||||
|
self.assertEqual(p[2], "/r")
|
||||||
|
self.assertEqual(p[3], False)
|
||||||
|
|
||||||
|
p = parse_url("ws://www.example.com/r/")
|
||||||
|
self.assertEqual(p[0], "www.example.com")
|
||||||
|
self.assertEqual(p[1], 80)
|
||||||
|
self.assertEqual(p[2], "/r/")
|
||||||
|
self.assertEqual(p[3], False)
|
||||||
|
|
||||||
|
p = parse_url("ws://www.example.com/")
|
||||||
|
self.assertEqual(p[0], "www.example.com")
|
||||||
|
self.assertEqual(p[1], 80)
|
||||||
|
self.assertEqual(p[2], "/")
|
||||||
|
self.assertEqual(p[3], False)
|
||||||
|
|
||||||
|
p = parse_url("ws://www.example.com")
|
||||||
|
self.assertEqual(p[0], "www.example.com")
|
||||||
|
self.assertEqual(p[1], 80)
|
||||||
|
self.assertEqual(p[2], "/")
|
||||||
|
self.assertEqual(p[3], False)
|
||||||
|
|
||||||
|
p = parse_url("ws://www.example.com:8080/r")
|
||||||
|
self.assertEqual(p[0], "www.example.com")
|
||||||
|
self.assertEqual(p[1], 8080)
|
||||||
|
self.assertEqual(p[2], "/r")
|
||||||
|
self.assertEqual(p[3], False)
|
||||||
|
|
||||||
|
p = parse_url("ws://www.example.com:8080/")
|
||||||
|
self.assertEqual(p[0], "www.example.com")
|
||||||
|
self.assertEqual(p[1], 8080)
|
||||||
|
self.assertEqual(p[2], "/")
|
||||||
|
self.assertEqual(p[3], False)
|
||||||
|
|
||||||
|
p = parse_url("ws://www.example.com:8080")
|
||||||
|
self.assertEqual(p[0], "www.example.com")
|
||||||
|
self.assertEqual(p[1], 8080)
|
||||||
|
self.assertEqual(p[2], "/")
|
||||||
|
self.assertEqual(p[3], False)
|
||||||
|
|
||||||
|
p = parse_url("wss://www.example.com:8080/r")
|
||||||
|
self.assertEqual(p[0], "www.example.com")
|
||||||
|
self.assertEqual(p[1], 8080)
|
||||||
|
self.assertEqual(p[2], "/r")
|
||||||
|
self.assertEqual(p[3], True)
|
||||||
|
|
||||||
|
p = parse_url("wss://www.example.com:8080/r?key=value")
|
||||||
|
self.assertEqual(p[0], "www.example.com")
|
||||||
|
self.assertEqual(p[1], 8080)
|
||||||
|
self.assertEqual(p[2], "/r?key=value")
|
||||||
|
self.assertEqual(p[3], True)
|
||||||
|
|
||||||
|
self.assertRaises(ValueError, parse_url, "http://www.example.com/r")
|
||||||
|
|
||||||
|
if sys.version_info[0] == 2 and sys.version_info[1] < 7:
|
||||||
|
return
|
||||||
|
|
||||||
|
p = parse_url("ws://[2a03:4000:123:83::3]/r")
|
||||||
|
self.assertEqual(p[0], "2a03:4000:123:83::3")
|
||||||
|
self.assertEqual(p[1], 80)
|
||||||
|
self.assertEqual(p[2], "/r")
|
||||||
|
self.assertEqual(p[3], False)
|
||||||
|
|
||||||
|
p = parse_url("ws://[2a03:4000:123:83::3]:8080/r")
|
||||||
|
self.assertEqual(p[0], "2a03:4000:123:83::3")
|
||||||
|
self.assertEqual(p[1], 8080)
|
||||||
|
self.assertEqual(p[2], "/r")
|
||||||
|
self.assertEqual(p[3], False)
|
||||||
|
|
||||||
|
p = parse_url("wss://[2a03:4000:123:83::3]/r")
|
||||||
|
self.assertEqual(p[0], "2a03:4000:123:83::3")
|
||||||
|
self.assertEqual(p[1], 443)
|
||||||
|
self.assertEqual(p[2], "/r")
|
||||||
|
self.assertEqual(p[3], True)
|
||||||
|
|
||||||
|
p = parse_url("wss://[2a03:4000:123:83::3]:8080/r")
|
||||||
|
self.assertEqual(p[0], "2a03:4000:123:83::3")
|
||||||
|
self.assertEqual(p[1], 8080)
|
||||||
|
self.assertEqual(p[2], "/r")
|
||||||
|
self.assertEqual(p[3], True)
|
||||||
|
|
||||||
|
def testWSKey(self):
|
||||||
|
key = _create_sec_websocket_key()
|
||||||
|
self.assertTrue(key != 24)
|
||||||
|
self.assertTrue(six.u("¥n") not in key)
|
||||||
|
|
||||||
|
def testWsUtils(self):
|
||||||
|
key = "c6b8hTg4EeGb2gQMztV1/g=="
|
||||||
|
required_header = {
|
||||||
|
"upgrade": "websocket",
|
||||||
|
"connection": "upgrade",
|
||||||
|
"sec-websocket-accept": "Kxep+hNu9n51529fGidYu7a3wO0=",
|
||||||
|
}
|
||||||
|
self.assertEqual(_validate_header(required_header, key, None), (True, None))
|
||||||
|
|
||||||
|
header = required_header.copy()
|
||||||
|
header["upgrade"] = "http"
|
||||||
|
self.assertEqual(_validate_header(header, key, None), (False, None))
|
||||||
|
del header["upgrade"]
|
||||||
|
self.assertEqual(_validate_header(header, key, None), (False, None))
|
||||||
|
|
||||||
|
header = required_header.copy()
|
||||||
|
header["connection"] = "something"
|
||||||
|
self.assertEqual(_validate_header(header, key, None), (False, None))
|
||||||
|
del header["connection"]
|
||||||
|
self.assertEqual(_validate_header(header, key, None), (False, None))
|
||||||
|
|
||||||
|
header = required_header.copy()
|
||||||
|
header["sec-websocket-accept"] = "something"
|
||||||
|
self.assertEqual(_validate_header(header, key, None), (False, None))
|
||||||
|
del header["sec-websocket-accept"]
|
||||||
|
self.assertEqual(_validate_header(header, key, None), (False, None))
|
||||||
|
|
||||||
|
header = required_header.copy()
|
||||||
|
header["sec-websocket-protocol"] = "sub1"
|
||||||
|
self.assertEqual(_validate_header(header, key, ["sub1", "sub2"]), (True, "sub1"))
|
||||||
|
self.assertEqual(_validate_header(header, key, ["sub2", "sub3"]), (False, None))
|
||||||
|
|
||||||
|
header = required_header.copy()
|
||||||
|
header["sec-websocket-protocol"] = "sUb1"
|
||||||
|
self.assertEqual(_validate_header(header, key, ["Sub1", "suB2"]), (True, "sub1"))
|
||||||
|
|
||||||
|
header = required_header.copy()
|
||||||
|
self.assertEqual(_validate_header(header, key, ["Sub1", "suB2"]), (False, None))
|
||||||
|
|
||||||
|
def testReadHeader(self):
|
||||||
|
status, header, status_message = read_headers(HeaderSockMock("data/header01.txt"))
|
||||||
|
self.assertEqual(status, 101)
|
||||||
|
self.assertEqual(header["connection"], "Upgrade")
|
||||||
|
|
||||||
|
HeaderSockMock("data/header02.txt")
|
||||||
|
self.assertRaises(ws.WebSocketException, read_headers, HeaderSockMock("data/header02.txt"))
|
||||||
|
|
||||||
|
def testSend(self):
|
||||||
|
# TODO: add longer frame data
|
||||||
|
sock = ws.WebSocket()
|
||||||
|
sock.set_mask_key(create_mask_key)
|
||||||
|
s = sock.sock = HeaderSockMock("data/header01.txt")
|
||||||
|
sock.send("Hello")
|
||||||
|
self.assertEqual(s.sent[0], six.b("\x81\x85abcd)\x07\x0f\x08\x0e"))
|
||||||
|
|
||||||
|
sock.send("こんにちは")
|
||||||
|
self.assertEqual(s.sent[1], six.b("\x81\x8fabcd\x82\xe3\xf0\x87\xe3\xf1\x80\xe5\xca\x81\xe2\xc5\x82\xe3\xcc"))
|
||||||
|
|
||||||
|
sock.send(u"こんにちは")
|
||||||
|
self.assertEqual(s.sent[1], six.b("\x81\x8fabcd\x82\xe3\xf0\x87\xe3\xf1\x80\xe5\xca\x81\xe2\xc5\x82\xe3\xcc"))
|
||||||
|
|
||||||
|
sock.send("x" * 127)
|
||||||
|
|
||||||
|
def testRecv(self):
|
||||||
|
# TODO: add longer frame data
|
||||||
|
sock = ws.WebSocket()
|
||||||
|
s = sock.sock = SockMock()
|
||||||
|
something = six.b("\x81\x8fabcd\x82\xe3\xf0\x87\xe3\xf1\x80\xe5\xca\x81\xe2\xc5\x82\xe3\xcc")
|
||||||
|
s.add_packet(something)
|
||||||
|
data = sock.recv()
|
||||||
|
self.assertEqual(data, "こんにちは")
|
||||||
|
|
||||||
|
s.add_packet(six.b("\x81\x85abcd)\x07\x0f\x08\x0e"))
|
||||||
|
data = sock.recv()
|
||||||
|
self.assertEqual(data, "Hello")
|
||||||
|
|
||||||
|
@unittest.skipUnless(TEST_WITH_INTERNET, "Internet-requiring tests are disabled")
|
||||||
|
def testIter(self):
|
||||||
|
count = 2
|
||||||
|
for _ in ws.create_connection('wss://stream.meetup.com/2/rsvps'):
|
||||||
|
count -= 1
|
||||||
|
if count == 0:
|
||||||
|
break
|
||||||
|
|
||||||
|
@unittest.skipUnless(TEST_WITH_INTERNET, "Internet-requiring tests are disabled")
|
||||||
|
def testNext(self):
|
||||||
|
sock = ws.create_connection('wss://stream.meetup.com/2/rsvps')
|
||||||
|
self.assertEqual(str, type(next(sock)))
|
||||||
|
|
||||||
|
def testInternalRecvStrict(self):
|
||||||
|
sock = ws.WebSocket()
|
||||||
|
s = sock.sock = SockMock()
|
||||||
|
s.add_packet(six.b("foo"))
|
||||||
|
s.add_packet(socket.timeout())
|
||||||
|
s.add_packet(six.b("bar"))
|
||||||
|
# s.add_packet(SSLError("The read operation timed out"))
|
||||||
|
s.add_packet(six.b("baz"))
|
||||||
|
with self.assertRaises(ws.WebSocketTimeoutException):
|
||||||
|
sock.frame_buffer.recv_strict(9)
|
||||||
|
# if six.PY2:
|
||||||
|
# with self.assertRaises(ws.WebSocketTimeoutException):
|
||||||
|
# data = sock._recv_strict(9)
|
||||||
|
# else:
|
||||||
|
# with self.assertRaises(SSLError):
|
||||||
|
# data = sock._recv_strict(9)
|
||||||
|
data = sock.frame_buffer.recv_strict(9)
|
||||||
|
self.assertEqual(data, six.b("foobarbaz"))
|
||||||
|
with self.assertRaises(ws.WebSocketConnectionClosedException):
|
||||||
|
sock.frame_buffer.recv_strict(1)
|
||||||
|
|
||||||
|
def testRecvTimeout(self):
|
||||||
|
sock = ws.WebSocket()
|
||||||
|
s = sock.sock = SockMock()
|
||||||
|
s.add_packet(six.b("\x81"))
|
||||||
|
s.add_packet(socket.timeout())
|
||||||
|
s.add_packet(six.b("\x8dabcd\x29\x07\x0f\x08\x0e"))
|
||||||
|
s.add_packet(socket.timeout())
|
||||||
|
s.add_packet(six.b("\x4e\x43\x33\x0e\x10\x0f\x00\x40"))
|
||||||
|
with self.assertRaises(ws.WebSocketTimeoutException):
|
||||||
|
sock.recv()
|
||||||
|
with self.assertRaises(ws.WebSocketTimeoutException):
|
||||||
|
sock.recv()
|
||||||
|
data = sock.recv()
|
||||||
|
self.assertEqual(data, "Hello, World!")
|
||||||
|
with self.assertRaises(ws.WebSocketConnectionClosedException):
|
||||||
|
sock.recv()
|
||||||
|
|
||||||
|
def testRecvWithSimpleFragmentation(self):
|
||||||
|
sock = ws.WebSocket()
|
||||||
|
s = sock.sock = SockMock()
|
||||||
|
# OPCODE=TEXT, FIN=0, MSG="Brevity is "
|
||||||
|
s.add_packet(six.b("\x01\x8babcd#\x10\x06\x12\x08\x16\x1aD\x08\x11C"))
|
||||||
|
# OPCODE=CONT, FIN=1, MSG="the soul of wit"
|
||||||
|
s.add_packet(six.b("\x80\x8fabcd\x15\n\x06D\x12\r\x16\x08A\r\x05D\x16\x0b\x17"))
|
||||||
|
data = sock.recv()
|
||||||
|
self.assertEqual(data, "Brevity is the soul of wit")
|
||||||
|
with self.assertRaises(ws.WebSocketConnectionClosedException):
|
||||||
|
sock.recv()
|
||||||
|
|
||||||
|
def testRecvWithFireEventOfFragmentation(self):
|
||||||
|
sock = ws.WebSocket(fire_cont_frame=True)
|
||||||
|
s = sock.sock = SockMock()
|
||||||
|
# OPCODE=TEXT, FIN=0, MSG="Brevity is "
|
||||||
|
s.add_packet(six.b("\x01\x8babcd#\x10\x06\x12\x08\x16\x1aD\x08\x11C"))
|
||||||
|
# OPCODE=CONT, FIN=0, MSG="Brevity is "
|
||||||
|
s.add_packet(six.b("\x00\x8babcd#\x10\x06\x12\x08\x16\x1aD\x08\x11C"))
|
||||||
|
# OPCODE=CONT, FIN=1, MSG="the soul of wit"
|
||||||
|
s.add_packet(six.b("\x80\x8fabcd\x15\n\x06D\x12\r\x16\x08A\r\x05D\x16\x0b\x17"))
|
||||||
|
|
||||||
|
_, data = sock.recv_data()
|
||||||
|
self.assertEqual(data, six.b("Brevity is "))
|
||||||
|
_, data = sock.recv_data()
|
||||||
|
self.assertEqual(data, six.b("Brevity is "))
|
||||||
|
_, data = sock.recv_data()
|
||||||
|
self.assertEqual(data, six.b("the soul of wit"))
|
||||||
|
|
||||||
|
# OPCODE=CONT, FIN=0, MSG="Brevity is "
|
||||||
|
s.add_packet(six.b("\x80\x8babcd#\x10\x06\x12\x08\x16\x1aD\x08\x11C"))
|
||||||
|
|
||||||
|
with self.assertRaises(ws.WebSocketException):
|
||||||
|
sock.recv_data()
|
||||||
|
|
||||||
|
with self.assertRaises(ws.WebSocketConnectionClosedException):
|
||||||
|
sock.recv()
|
||||||
|
|
||||||
|
def testClose(self):
|
||||||
|
sock = ws.WebSocket()
|
||||||
|
sock.sock = SockMock()
|
||||||
|
sock.connected = True
|
||||||
|
sock.close()
|
||||||
|
self.assertEqual(sock.connected, False)
|
||||||
|
|
||||||
|
sock = ws.WebSocket()
|
||||||
|
s = sock.sock = SockMock()
|
||||||
|
sock.connected = True
|
||||||
|
s.add_packet(six.b('\x88\x80\x17\x98p\x84'))
|
||||||
|
sock.recv()
|
||||||
|
self.assertEqual(sock.connected, False)
|
||||||
|
|
||||||
|
def testRecvContFragmentation(self):
|
||||||
|
sock = ws.WebSocket()
|
||||||
|
s = sock.sock = SockMock()
|
||||||
|
# OPCODE=CONT, FIN=1, MSG="the soul of wit"
|
||||||
|
s.add_packet(six.b("\x80\x8fabcd\x15\n\x06D\x12\r\x16\x08A\r\x05D\x16\x0b\x17"))
|
||||||
|
self.assertRaises(ws.WebSocketException, sock.recv)
|
||||||
|
|
||||||
|
def testRecvWithProlongedFragmentation(self):
|
||||||
|
sock = ws.WebSocket()
|
||||||
|
s = sock.sock = SockMock()
|
||||||
|
# OPCODE=TEXT, FIN=0, MSG="Once more unto the breach, "
|
||||||
|
s.add_packet(six.b("\x01\x9babcd.\x0c\x00\x01A\x0f\x0c\x16\x04B\x16\n\x15"
|
||||||
|
"\rC\x10\t\x07C\x06\x13\x07\x02\x07\tNC"))
|
||||||
|
# OPCODE=CONT, FIN=0, MSG="dear friends, "
|
||||||
|
s.add_packet(six.b("\x00\x8eabcd\x05\x07\x02\x16A\x04\x11\r\x04\x0c\x07"
|
||||||
|
"\x17MB"))
|
||||||
|
# OPCODE=CONT, FIN=1, MSG="once more"
|
||||||
|
s.add_packet(six.b("\x80\x89abcd\x0e\x0c\x00\x01A\x0f\x0c\x16\x04"))
|
||||||
|
data = sock.recv()
|
||||||
|
self.assertEqual(
|
||||||
|
data,
|
||||||
|
"Once more unto the breach, dear friends, once more")
|
||||||
|
with self.assertRaises(ws.WebSocketConnectionClosedException):
|
||||||
|
sock.recv()
|
||||||
|
|
||||||
|
def testRecvWithFragmentationAndControlFrame(self):
|
||||||
|
sock = ws.WebSocket()
|
||||||
|
sock.set_mask_key(create_mask_key)
|
||||||
|
s = sock.sock = SockMock()
|
||||||
|
# OPCODE=TEXT, FIN=0, MSG="Too much "
|
||||||
|
s.add_packet(six.b("\x01\x89abcd5\r\x0cD\x0c\x17\x00\x0cA"))
|
||||||
|
# OPCODE=PING, FIN=1, MSG="Please PONG this"
|
||||||
|
s.add_packet(six.b("\x89\x90abcd1\x0e\x06\x05\x12\x07C4.,$D\x15\n\n\x17"))
|
||||||
|
# OPCODE=CONT, FIN=1, MSG="of a good thing"
|
||||||
|
s.add_packet(six.b("\x80\x8fabcd\x0e\x04C\x05A\x05\x0c\x0b\x05B\x17\x0c"
|
||||||
|
"\x08\x0c\x04"))
|
||||||
|
data = sock.recv()
|
||||||
|
self.assertEqual(data, "Too much of a good thing")
|
||||||
|
with self.assertRaises(ws.WebSocketConnectionClosedException):
|
||||||
|
sock.recv()
|
||||||
|
self.assertEqual(
|
||||||
|
s.sent[0],
|
||||||
|
six.b("\x8a\x90abcd1\x0e\x06\x05\x12\x07C4.,$D\x15\n\n\x17"))
|
||||||
|
|
||||||
|
@unittest.skipUnless(TEST_WITH_INTERNET, "Internet-requiring tests are disabled")
|
||||||
|
def testWebSocket(self):
|
||||||
|
s = ws.create_connection("ws://echo.websocket.org/")
|
||||||
|
self.assertNotEqual(s, None)
|
||||||
|
s.send("Hello, World")
|
||||||
|
result = s.recv()
|
||||||
|
self.assertEqual(result, "Hello, World")
|
||||||
|
|
||||||
|
s.send(u"こにゃにゃちは、世界")
|
||||||
|
result = s.recv()
|
||||||
|
self.assertEqual(result, "こにゃにゃちは、世界")
|
||||||
|
s.close()
|
||||||
|
|
||||||
|
@unittest.skipUnless(TEST_WITH_INTERNET, "Internet-requiring tests are disabled")
|
||||||
|
def testPingPong(self):
|
||||||
|
s = ws.create_connection("ws://echo.websocket.org/")
|
||||||
|
self.assertNotEqual(s, None)
|
||||||
|
s.ping("Hello")
|
||||||
|
s.pong("Hi")
|
||||||
|
s.close()
|
||||||
|
|
||||||
|
@unittest.skipUnless(TEST_WITH_INTERNET, "Internet-requiring tests are disabled")
|
||||||
|
@unittest.skipUnless(TEST_SECURE_WS, "wss://echo.websocket.org doesn't work well.")
|
||||||
|
def testSecureWebSocket(self):
|
||||||
|
if 1:
|
||||||
|
import ssl
|
||||||
|
s = ws.create_connection("wss://echo.websocket.org/")
|
||||||
|
self.assertNotEqual(s, None)
|
||||||
|
self.assertTrue(isinstance(s.sock, ssl.SSLSocket))
|
||||||
|
s.send("Hello, World")
|
||||||
|
result = s.recv()
|
||||||
|
self.assertEqual(result, "Hello, World")
|
||||||
|
s.send(u"こにゃにゃちは、世界")
|
||||||
|
result = s.recv()
|
||||||
|
self.assertEqual(result, "こにゃにゃちは、世界")
|
||||||
|
s.close()
|
||||||
|
#except:
|
||||||
|
# pass
|
||||||
|
|
||||||
|
@unittest.skipUnless(TEST_WITH_INTERNET, "Internet-requiring tests are disabled")
|
||||||
|
def testWebSocketWithCustomHeader(self):
|
||||||
|
s = ws.create_connection("ws://echo.websocket.org/",
|
||||||
|
headers={"User-Agent": "PythonWebsocketClient"})
|
||||||
|
self.assertNotEqual(s, None)
|
||||||
|
s.send("Hello, World")
|
||||||
|
result = s.recv()
|
||||||
|
self.assertEqual(result, "Hello, World")
|
||||||
|
s.close()
|
||||||
|
|
||||||
|
@unittest.skipUnless(TEST_WITH_INTERNET, "Internet-requiring tests are disabled")
|
||||||
|
def testAfterClose(self):
|
||||||
|
s = ws.create_connection("ws://echo.websocket.org/")
|
||||||
|
self.assertNotEqual(s, None)
|
||||||
|
s.close()
|
||||||
|
self.assertRaises(ws.WebSocketConnectionClosedException, s.send, "Hello")
|
||||||
|
self.assertRaises(ws.WebSocketConnectionClosedException, s.recv)
|
||||||
|
|
||||||
|
def testNonce(self):
|
||||||
|
""" WebSocket key should be a random 16-byte nonce.
|
||||||
|
"""
|
||||||
|
key = _create_sec_websocket_key()
|
||||||
|
nonce = base64decode(key.encode("utf-8"))
|
||||||
|
self.assertEqual(16, len(nonce))
|
||||||
|
|
||||||
|
|
||||||
|
class WebSocketAppTest(unittest.TestCase):
|
||||||
|
|
||||||
|
class NotSetYet(object):
|
||||||
|
""" A marker class for signalling that a value hasn't been set yet.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
ws.enableTrace(TRACEABLE)
|
||||||
|
|
||||||
|
WebSocketAppTest.keep_running_open = WebSocketAppTest.NotSetYet()
|
||||||
|
WebSocketAppTest.keep_running_close = WebSocketAppTest.NotSetYet()
|
||||||
|
WebSocketAppTest.get_mask_key_id = WebSocketAppTest.NotSetYet()
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
WebSocketAppTest.keep_running_open = WebSocketAppTest.NotSetYet()
|
||||||
|
WebSocketAppTest.keep_running_close = WebSocketAppTest.NotSetYet()
|
||||||
|
WebSocketAppTest.get_mask_key_id = WebSocketAppTest.NotSetYet()
|
||||||
|
|
||||||
|
@unittest.skipUnless(TEST_WITH_INTERNET, "Internet-requiring tests are disabled")
|
||||||
|
def testKeepRunning(self):
|
||||||
|
""" A WebSocketApp should keep running as long as its self.keep_running
|
||||||
|
is not False (in the boolean context).
|
||||||
|
"""
|
||||||
|
|
||||||
|
def on_open(self, *args, **kwargs):
|
||||||
|
""" Set the keep_running flag for later inspection and immediately
|
||||||
|
close the connection.
|
||||||
|
"""
|
||||||
|
WebSocketAppTest.keep_running_open = self.keep_running
|
||||||
|
|
||||||
|
self.close()
|
||||||
|
|
||||||
|
def on_close(self, *args, **kwargs):
|
||||||
|
""" Set the keep_running flag for the test to use.
|
||||||
|
"""
|
||||||
|
WebSocketAppTest.keep_running_close = self.keep_running
|
||||||
|
|
||||||
|
app = ws.WebSocketApp('ws://echo.websocket.org/', on_open=on_open, on_close=on_close)
|
||||||
|
app.run_forever()
|
||||||
|
|
||||||
|
# if numpy is installed, this assertion fail
|
||||||
|
# self.assertFalse(isinstance(WebSocketAppTest.keep_running_open,
|
||||||
|
# WebSocketAppTest.NotSetYet))
|
||||||
|
|
||||||
|
# self.assertFalse(isinstance(WebSocketAppTest.keep_running_close,
|
||||||
|
# WebSocketAppTest.NotSetYet))
|
||||||
|
|
||||||
|
# self.assertEqual(True, WebSocketAppTest.keep_running_open)
|
||||||
|
# self.assertEqual(False, WebSocketAppTest.keep_running_close)
|
||||||
|
|
||||||
|
@unittest.skipUnless(TEST_WITH_INTERNET, "Internet-requiring tests are disabled")
|
||||||
|
def testSockMaskKey(self):
|
||||||
|
""" A WebSocketApp should forward the received mask_key function down
|
||||||
|
to the actual socket.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def my_mask_key_func():
|
||||||
|
pass
|
||||||
|
|
||||||
|
def on_open(self, *args, **kwargs):
|
||||||
|
""" Set the value so the test can use it later on and immediately
|
||||||
|
close the connection.
|
||||||
|
"""
|
||||||
|
WebSocketAppTest.get_mask_key_id = id(self.get_mask_key)
|
||||||
|
self.close()
|
||||||
|
|
||||||
|
app = ws.WebSocketApp('ws://echo.websocket.org/', on_open=on_open, get_mask_key=my_mask_key_func)
|
||||||
|
app.run_forever()
|
||||||
|
|
||||||
|
# if numpu is installed, this assertion fail
|
||||||
|
# Note: We can't use 'is' for comparing the functions directly, need to use 'id'.
|
||||||
|
# self.assertEqual(WebSocketAppTest.get_mask_key_id, id(my_mask_key_func))
|
||||||
|
|
||||||
|
|
||||||
|
class SockOptTest(unittest.TestCase):
|
||||||
|
@unittest.skipUnless(TEST_WITH_INTERNET, "Internet-requiring tests are disabled")
|
||||||
|
def testSockOpt(self):
|
||||||
|
sockopt = ((socket.IPPROTO_TCP, socket.TCP_NODELAY, 1),)
|
||||||
|
s = ws.create_connection("ws://echo.websocket.org", sockopt=sockopt)
|
||||||
|
self.assertNotEqual(s.sock.getsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY), 0)
|
||||||
|
s.close()
|
||||||
|
|
||||||
|
|
||||||
|
class UtilsTest(unittest.TestCase):
|
||||||
|
def testUtf8Validator(self):
|
||||||
|
state = validate_utf8(six.b('\xf0\x90\x80\x80'))
|
||||||
|
self.assertEqual(state, True)
|
||||||
|
state = validate_utf8(six.b('\xce\xba\xe1\xbd\xb9\xcf\x83\xce\xbc\xce\xb5\xed\xa0\x80edited'))
|
||||||
|
self.assertEqual(state, False)
|
||||||
|
state = validate_utf8(six.b(''))
|
||||||
|
self.assertEqual(state, True)
|
||||||
|
|
||||||
|
|
||||||
|
class ProxyInfoTest(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.http_proxy = os.environ.get("http_proxy", None)
|
||||||
|
self.https_proxy = os.environ.get("https_proxy", None)
|
||||||
|
if "http_proxy" in os.environ:
|
||||||
|
del os.environ["http_proxy"]
|
||||||
|
if "https_proxy" in os.environ:
|
||||||
|
del os.environ["https_proxy"]
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
if self.http_proxy:
|
||||||
|
os.environ["http_proxy"] = self.http_proxy
|
||||||
|
elif "http_proxy" in os.environ:
|
||||||
|
del os.environ["http_proxy"]
|
||||||
|
|
||||||
|
if self.https_proxy:
|
||||||
|
os.environ["https_proxy"] = self.https_proxy
|
||||||
|
elif "https_proxy" in os.environ:
|
||||||
|
del os.environ["https_proxy"]
|
||||||
|
|
||||||
|
def testProxyFromArgs(self):
|
||||||
|
self.assertEqual(get_proxy_info("echo.websocket.org", False, proxy_host="localhost"), ("localhost", 0, None))
|
||||||
|
self.assertEqual(get_proxy_info("echo.websocket.org", False, proxy_host="localhost", proxy_port=3128), ("localhost", 3128, None))
|
||||||
|
self.assertEqual(get_proxy_info("echo.websocket.org", True, proxy_host="localhost"), ("localhost", 0, None))
|
||||||
|
self.assertEqual(get_proxy_info("echo.websocket.org", True, proxy_host="localhost", proxy_port=3128), ("localhost", 3128, None))
|
||||||
|
|
||||||
|
self.assertEqual(get_proxy_info("echo.websocket.org", False, proxy_host="localhost", proxy_auth=("a", "b")),
|
||||||
|
("localhost", 0, ("a", "b")))
|
||||||
|
self.assertEqual(get_proxy_info("echo.websocket.org", False, proxy_host="localhost", proxy_port=3128, proxy_auth=("a", "b")),
|
||||||
|
("localhost", 3128, ("a", "b")))
|
||||||
|
self.assertEqual(get_proxy_info("echo.websocket.org", True, proxy_host="localhost", proxy_auth=("a", "b")),
|
||||||
|
("localhost", 0, ("a", "b")))
|
||||||
|
self.assertEqual(get_proxy_info("echo.websocket.org", True, proxy_host="localhost", proxy_port=3128, proxy_auth=("a", "b")),
|
||||||
|
("localhost", 3128, ("a", "b")))
|
||||||
|
|
||||||
|
self.assertEqual(get_proxy_info("echo.websocket.org", True, proxy_host="localhost", proxy_port=3128, no_proxy=["example.com"], proxy_auth=("a", "b")),
|
||||||
|
("localhost", 3128, ("a", "b")))
|
||||||
|
self.assertEqual(get_proxy_info("echo.websocket.org", True, proxy_host="localhost", proxy_port=3128, no_proxy=["echo.websocket.org"], proxy_auth=("a", "b")),
|
||||||
|
(None, 0, None))
|
||||||
|
|
||||||
|
def testProxyFromEnv(self):
|
||||||
|
os.environ["http_proxy"] = "http://localhost/"
|
||||||
|
self.assertEqual(get_proxy_info("echo.websocket.org", False), ("localhost", None, None))
|
||||||
|
os.environ["http_proxy"] = "http://localhost:3128/"
|
||||||
|
self.assertEqual(get_proxy_info("echo.websocket.org", False), ("localhost", 3128, None))
|
||||||
|
|
||||||
|
os.environ["http_proxy"] = "http://localhost/"
|
||||||
|
os.environ["https_proxy"] = "http://localhost2/"
|
||||||
|
self.assertEqual(get_proxy_info("echo.websocket.org", False), ("localhost", None, None))
|
||||||
|
os.environ["http_proxy"] = "http://localhost:3128/"
|
||||||
|
os.environ["https_proxy"] = "http://localhost2:3128/"
|
||||||
|
self.assertEqual(get_proxy_info("echo.websocket.org", False), ("localhost", 3128, None))
|
||||||
|
|
||||||
|
os.environ["http_proxy"] = "http://localhost/"
|
||||||
|
os.environ["https_proxy"] = "http://localhost2/"
|
||||||
|
self.assertEqual(get_proxy_info("echo.websocket.org", True), ("localhost2", None, None))
|
||||||
|
os.environ["http_proxy"] = "http://localhost:3128/"
|
||||||
|
os.environ["https_proxy"] = "http://localhost2:3128/"
|
||||||
|
self.assertEqual(get_proxy_info("echo.websocket.org", True), ("localhost2", 3128, None))
|
||||||
|
|
||||||
|
|
||||||
|
os.environ["http_proxy"] = "http://a:b@localhost/"
|
||||||
|
self.assertEqual(get_proxy_info("echo.websocket.org", False), ("localhost", None, ("a", "b")))
|
||||||
|
os.environ["http_proxy"] = "http://a:b@localhost:3128/"
|
||||||
|
self.assertEqual(get_proxy_info("echo.websocket.org", False), ("localhost", 3128, ("a", "b")))
|
||||||
|
|
||||||
|
os.environ["http_proxy"] = "http://a:b@localhost/"
|
||||||
|
os.environ["https_proxy"] = "http://a:b@localhost2/"
|
||||||
|
self.assertEqual(get_proxy_info("echo.websocket.org", False), ("localhost", None, ("a", "b")))
|
||||||
|
os.environ["http_proxy"] = "http://a:b@localhost:3128/"
|
||||||
|
os.environ["https_proxy"] = "http://a:b@localhost2:3128/"
|
||||||
|
self.assertEqual(get_proxy_info("echo.websocket.org", False), ("localhost", 3128, ("a", "b")))
|
||||||
|
|
||||||
|
os.environ["http_proxy"] = "http://a:b@localhost/"
|
||||||
|
os.environ["https_proxy"] = "http://a:b@localhost2/"
|
||||||
|
self.assertEqual(get_proxy_info("echo.websocket.org", True), ("localhost2", None, ("a", "b")))
|
||||||
|
os.environ["http_proxy"] = "http://a:b@localhost:3128/"
|
||||||
|
os.environ["https_proxy"] = "http://a:b@localhost2:3128/"
|
||||||
|
self.assertEqual(get_proxy_info("echo.websocket.org", True), ("localhost2", 3128, ("a", "b")))
|
||||||
|
|
||||||
|
os.environ["http_proxy"] = "http://a:b@localhost/"
|
||||||
|
os.environ["https_proxy"] = "http://a:b@localhost2/"
|
||||||
|
os.environ["no_proxy"] = "example1.com,example2.com"
|
||||||
|
self.assertEqual(get_proxy_info("example.1.com", True), ("localhost2", None, ("a", "b")))
|
||||||
|
os.environ["http_proxy"] = "http://a:b@localhost:3128/"
|
||||||
|
os.environ["https_proxy"] = "http://a:b@localhost2:3128/"
|
||||||
|
os.environ["no_proxy"] = "example1.com,example2.com, echo.websocket.org"
|
||||||
|
self.assertEqual(get_proxy_info("echo.websocket.org", True), (None, 0, None))
|
||||||
|
|
||||||
|
os.environ["http_proxy"] = "http://a:b@localhost:3128/"
|
||||||
|
os.environ["https_proxy"] = "http://a:b@localhost2:3128/"
|
||||||
|
os.environ["no_proxy"] = "127.0.0.0/8, 192.168.0.0/16"
|
||||||
|
self.assertEqual(get_proxy_info("127.0.0.1", False), (None, 0, None))
|
||||||
|
self.assertEqual(get_proxy_info("192.168.1.1", False), (None, 0, None))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
|
@ -1,57 +1,158 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
from json import loads
|
import json
|
||||||
|
|
||||||
from . import backgroundthread, websocket, utils, companion, app, variables as v
|
from . import websocket
|
||||||
|
from . import backgroundthread, app, variables as v, utils, companion
|
||||||
|
|
||||||
###############################################################################
|
log = getLogger('PLEX.websocket')
|
||||||
|
|
||||||
LOG = getLogger('PLEX.websocket_client')
|
PMS_PATH = '/:/websockets/notifications'
|
||||||
|
|
||||||
###############################################################################
|
PMS_INTERESTING_MESSAGE_TYPES = ('playing', 'timeline', 'activity')
|
||||||
|
SETTINGS_STRING = '_status'
|
||||||
|
|
||||||
|
|
||||||
class WebSocket(backgroundthread.KillableThread):
|
def get_pms_uri():
|
||||||
opcode_data = (websocket.ABNF.OPCODE_TEXT, websocket.ABNF.OPCODE_BINARY)
|
uri = app.CONN.server
|
||||||
status_setting = None
|
if not uri:
|
||||||
|
return
|
||||||
|
# Get the appropriate prefix for the websocket
|
||||||
|
if uri.startswith('https'):
|
||||||
|
uri = "wss%s" % uri[5:]
|
||||||
|
else:
|
||||||
|
uri = "ws%s" % uri[4:]
|
||||||
|
uri += PMS_PATH
|
||||||
|
log.debug('uri to connect pms websocket: %s', uri)
|
||||||
|
if app.ACCOUNT.pms_token:
|
||||||
|
uri += '?X-Plex-Token=' + app.ACCOUNT.pms_token
|
||||||
|
return uri
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.ws = None
|
|
||||||
self.redirect_uri = None
|
|
||||||
self.sleeptime = 0.0
|
|
||||||
super(WebSocket, self).__init__()
|
|
||||||
|
|
||||||
def close_websocket(self):
|
def get_alexa_uri():
|
||||||
if self.ws is not None:
|
if not app.ACCOUNT.plex_token:
|
||||||
self.ws.close()
|
return
|
||||||
self.ws = None
|
return (f'wss://pubsub.plex.tv/sub/websockets/{app.ACCOUNT.plex_user_id}/'
|
||||||
|
f'{v.PKC_MACHINE_IDENTIFIER}?'
|
||||||
|
f'X-Plex-Token={app.ACCOUNT.plex_token}')
|
||||||
|
|
||||||
def process(self, opcode, message):
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def receive(self, ws):
|
def pms_on_message(ws, message):
|
||||||
# Not connected yet
|
"""
|
||||||
if ws is None:
|
Called when we receive a message from the PMS, e.g. when a new library
|
||||||
raise websocket.WebSocketConnectionClosedException
|
item has been added.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
message = json.loads(message)
|
||||||
|
except ValueError as err:
|
||||||
|
log.error('Error decoding PMS websocket message: %s', err)
|
||||||
|
log.error('message: %s', message)
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
message = message['NotificationContainer']
|
||||||
|
typus = message['type']
|
||||||
|
except KeyError:
|
||||||
|
log.error('Could not parse PMS message: %s', message)
|
||||||
|
return
|
||||||
|
# Triage
|
||||||
|
if typus not in PMS_INTERESTING_MESSAGE_TYPES:
|
||||||
|
# Drop everything we're not interested in
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
# Put PMS message on queue and let libsync take care of it
|
||||||
|
app.APP.websocket_queue.put(message)
|
||||||
|
|
||||||
frame = ws.recv_frame()
|
|
||||||
|
|
||||||
if not frame:
|
def alexa_on_message(ws, message):
|
||||||
raise websocket.WebSocketException("Not a valid frame %s" % frame)
|
"""
|
||||||
elif frame.opcode in self.opcode_data:
|
Called when we receive a message from Alexa
|
||||||
return frame.opcode, frame.data
|
"""
|
||||||
elif frame.opcode == websocket.ABNF.OPCODE_CLOSE:
|
log.debug('alexa message received: %s', message)
|
||||||
ws.send_close()
|
try:
|
||||||
return frame.opcode, None
|
message = utils.etree.fromstring(message)
|
||||||
elif frame.opcode == websocket.ABNF.OPCODE_PING:
|
except Exception as err:
|
||||||
ws.pong("Hi!")
|
log.error('Error decoding message from Alexa: %s %s', type(err), err)
|
||||||
return None, None
|
log.error('message from Alexa: %s', message)
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
if message.attrib['command'] == 'processRemoteControlCommand':
|
||||||
|
message = message[0]
|
||||||
|
else:
|
||||||
|
log.error('Unknown Alexa message received: %s', message)
|
||||||
|
return
|
||||||
|
companion.process_command(message.attrib['path'][1:], message.attrib)
|
||||||
|
except Exception as err:
|
||||||
|
log.exception('Could not parse Alexa message, error: %s %s',
|
||||||
|
type(err), err)
|
||||||
|
log.error('message: %s', message)
|
||||||
|
|
||||||
def getUri(self):
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def _sleep_cycle(self):
|
def on_error(ws, error):
|
||||||
|
status = ws.name + SETTINGS_STRING
|
||||||
|
if isinstance(error, IOError):
|
||||||
|
# We are probably offline
|
||||||
|
log.debug('%s: IOError connecting', ws.name)
|
||||||
|
# Status = IOError - not connected
|
||||||
|
utils.settings(status, value=utils.lang(39092))
|
||||||
|
ws.sleep_cycle()
|
||||||
|
elif isinstance(error, websocket.WebSocketTimeoutException):
|
||||||
|
log.debug('%s: WebSocketTimeoutException', ws.name)
|
||||||
|
# Status = 'Timeout - not connected'
|
||||||
|
utils.settings(status, value=utils.lang(39091))
|
||||||
|
ws.sleep_cycle()
|
||||||
|
elif isinstance(error, websocket.WebSocketConnectionClosedException):
|
||||||
|
log.debug('%s: WebSocketConnectionClosedException', ws.name)
|
||||||
|
# Status = Not connected
|
||||||
|
utils.settings(ws.name + SETTINGS_STRING, value=utils.lang(15208))
|
||||||
|
elif isinstance(error, websocket.WebSocketBadStatusException):
|
||||||
|
# Most likely Alexa not connecting, throwing a 403
|
||||||
|
log.debug('%s: got a bad HTTP status: %s', ws.name, error)
|
||||||
|
# Status = <value of exception>
|
||||||
|
utils.settings(status, value=str(error))
|
||||||
|
ws.sleep_cycle()
|
||||||
|
elif isinstance(error, websocket.WebSocketException):
|
||||||
|
log.error('%s: got another websocket exception %s: %s',
|
||||||
|
ws.name, type(error), error)
|
||||||
|
# Status = Error
|
||||||
|
utils.settings(status, value=utils.lang(257))
|
||||||
|
ws.sleep_cycle()
|
||||||
|
elif isinstance(error, SystemExit):
|
||||||
|
log.debug('%s: SystemExit detected', ws.name)
|
||||||
|
# Status = Not connected
|
||||||
|
utils.settings(ws.name + SETTINGS_STRING, value=utils.lang(15208))
|
||||||
|
else:
|
||||||
|
log.exception('%s: got an unexpected exception of type %s: %s',
|
||||||
|
ws.name, type(error), error)
|
||||||
|
# Status = Error
|
||||||
|
utils.settings(status, value=utils.lang(257))
|
||||||
|
raise RuntimeError
|
||||||
|
|
||||||
|
|
||||||
|
def on_close(ws):
|
||||||
|
"""
|
||||||
|
This does not seem to get called by our websocket client :-(
|
||||||
|
"""
|
||||||
|
log.debug('%s: connection closed', ws.name)
|
||||||
|
# Status = Not connected
|
||||||
|
utils.settings(ws.name + SETTINGS_STRING, value=utils.lang(15208))
|
||||||
|
|
||||||
|
|
||||||
|
def on_open(ws):
|
||||||
|
log.debug('%s: connected', ws.name)
|
||||||
|
# Status = Connected
|
||||||
|
utils.settings(ws.name + SETTINGS_STRING, value=utils.lang(13296))
|
||||||
|
ws.sleeptime = 0
|
||||||
|
|
||||||
|
|
||||||
|
class PlexWebSocketApp(websocket.WebSocketApp,
|
||||||
|
backgroundthread.KillableThread):
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
self.sleeptime = 0
|
||||||
|
backgroundthread.KillableThread.__init__(self)
|
||||||
|
websocket.WebSocketApp.__init__(self, self.get_uri(), **kwargs)
|
||||||
|
|
||||||
|
def sleep_cycle(self):
|
||||||
"""
|
"""
|
||||||
Sleeps for 2^self.sleeptime where sleeping period will be doubled with
|
Sleeps for 2^self.sleeptime where sleeping period will be doubled with
|
||||||
each unsuccessful connection attempt.
|
each unsuccessful connection attempt.
|
||||||
|
@ -59,224 +160,133 @@ class WebSocket(backgroundthread.KillableThread):
|
||||||
"""
|
"""
|
||||||
self.sleep(2 ** self.sleeptime)
|
self.sleep(2 ** self.sleeptime)
|
||||||
if self.sleeptime < 6:
|
if self.sleeptime < 6:
|
||||||
self.sleeptime += 1.0
|
self.sleeptime += 1
|
||||||
|
|
||||||
|
def suspend(self, block=False, timeout=None):
|
||||||
|
"""
|
||||||
|
Call this method from another thread to suspend this websocket thread
|
||||||
|
"""
|
||||||
|
self.close()
|
||||||
|
backgroundthread.KillableThread.suspend(self, block, timeout)
|
||||||
|
|
||||||
|
def cancel(self):
|
||||||
|
"""
|
||||||
|
Call this method from another thread to cancel this websocket thread
|
||||||
|
"""
|
||||||
|
self.close()
|
||||||
|
backgroundthread.KillableThread.cancel(self)
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
LOG.info("----===## Starting %s ##===----", self.__class__.__name__)
|
"""
|
||||||
|
Ensure that sockets will be closed no matter what
|
||||||
|
"""
|
||||||
|
log.info("----===## Starting %s ##===----", self.name)
|
||||||
app.APP.register_thread(self)
|
app.APP.register_thread(self)
|
||||||
try:
|
try:
|
||||||
self._run()
|
self._run()
|
||||||
|
except RuntimeError:
|
||||||
|
pass
|
||||||
|
except Exception as err:
|
||||||
|
log.exception('Exception of type %s occured: %s', type(err), err)
|
||||||
finally:
|
finally:
|
||||||
self.close_websocket()
|
self.close()
|
||||||
|
# Status = Not connected
|
||||||
|
utils.settings(self.name + SETTINGS_STRING,
|
||||||
|
value=utils.lang(15208))
|
||||||
app.APP.deregister_thread(self)
|
app.APP.deregister_thread(self)
|
||||||
LOG.info("##===---- %s Stopped ----===##", self.__class__.__name__)
|
log.info("----===## %s stopped ##===----", self.name)
|
||||||
|
|
||||||
def _run(self):
|
def _run(self):
|
||||||
while not self.should_cancel():
|
while not self.should_cancel():
|
||||||
# In the event the server goes offline
|
# In the event the server goes offline
|
||||||
if self.should_suspend():
|
while self.should_suspend():
|
||||||
# Set in service.py
|
# We will be caught in this loop if either another thread
|
||||||
self.close_websocket()
|
# called the suspend() method, thus setting _suspended = True
|
||||||
# Status = 'Suspended - not connected'
|
# OR if there any other conditions to not open a websocket
|
||||||
utils.settings(self.status_setting, value=utils.lang(39090))
|
# connection - see methods should_suspend() below
|
||||||
|
# Status = Suspended - not connected
|
||||||
|
self.set_suspension_settings_status()
|
||||||
if self.wait_while_suspended():
|
if self.wait_while_suspended():
|
||||||
# Abort was requested while waiting. We should exit
|
# Abort was requested while waiting. We should exit
|
||||||
return
|
return
|
||||||
try:
|
if not self._suspended:
|
||||||
self.process(*self.receive(self.ws))
|
# because wait_while_suspended will return instantly if
|
||||||
except websocket.WebSocketTimeoutException:
|
# this thread did not get suspended from another thread
|
||||||
# No worries if read timed out
|
self.sleep_cycle()
|
||||||
pass
|
self.url = self.get_uri()
|
||||||
except websocket.WebSocketConnectionClosedException:
|
if not self.url:
|
||||||
LOG.debug("%s: connection closed, (re)connecting",
|
self.sleep_cycle()
|
||||||
self.__class__.__name__)
|
continue
|
||||||
uri, sslopt = self.getUri()
|
self.run_forever()
|
||||||
try:
|
|
||||||
# Low timeout - let's us shut this thread down!
|
|
||||||
self.ws = websocket.create_connection(
|
|
||||||
uri,
|
|
||||||
timeout=1,
|
|
||||||
sslopt=sslopt,
|
|
||||||
enable_multithread=True)
|
|
||||||
except IOError:
|
|
||||||
# Server is probably offline
|
|
||||||
LOG.debug("%s: IOError connecting", self.__class__.__name__)
|
|
||||||
self.ws = None
|
|
||||||
# Status = IOError - not connected
|
|
||||||
utils.settings(self.status_setting,
|
|
||||||
value=utils.lang(39092))
|
|
||||||
self._sleep_cycle()
|
|
||||||
except websocket.WebSocketTimeoutException:
|
|
||||||
LOG.debug("%s: WebSocketTimeoutException", self.__class__.__name__)
|
|
||||||
self.ws = None
|
|
||||||
# Status = 'Timeout - not connected'
|
|
||||||
utils.settings(self.status_setting,
|
|
||||||
value=utils.lang(39091))
|
|
||||||
self._sleep_cycle()
|
|
||||||
except websocket.WebsocketRedirect as e:
|
|
||||||
LOG.debug('301 redirect detected: %s', e)
|
|
||||||
self.redirect_uri = e.headers.get('location',
|
|
||||||
e.headers.get('Location'))
|
|
||||||
if self.redirect_uri:
|
|
||||||
self.redirect_uri = self.redirect_uri.decode('utf-8')
|
|
||||||
self.ws = None
|
|
||||||
self._sleep_cycle()
|
|
||||||
except websocket.WebSocketException as e:
|
|
||||||
LOG.debug('%s: WebSocketException: %s', self.__class__.__name__, e)
|
|
||||||
self.ws = None
|
|
||||||
# Status = Error
|
|
||||||
utils.settings(self.status_setting,
|
|
||||||
value=utils.lang(257))
|
|
||||||
self._sleep_cycle()
|
|
||||||
except Exception as e:
|
|
||||||
LOG.error('%s: Unknown exception encountered when '
|
|
||||||
'connecting: %s', self.__class__.__name__, e)
|
|
||||||
import traceback
|
|
||||||
LOG.error("%s: Traceback:\n%s",
|
|
||||||
self.__class__.__name__, traceback.format_exc())
|
|
||||||
self.ws = None
|
|
||||||
# Status = Error
|
|
||||||
utils.settings(self.status_setting,
|
|
||||||
value=utils.lang(257))
|
|
||||||
self._sleep_cycle()
|
|
||||||
else:
|
|
||||||
self.sleeptime = 0.0
|
|
||||||
# Status = Connected
|
|
||||||
utils.settings(self.status_setting,
|
|
||||||
value=utils.lang(13296))
|
|
||||||
except Exception as e:
|
|
||||||
LOG.error("%s: Unknown exception encountered: %s",
|
|
||||||
self.__class__.__name__, e)
|
|
||||||
import traceback
|
|
||||||
LOG.error("%s: Traceback:\n%s",
|
|
||||||
self.__class__.__name__, traceback.format_exc())
|
|
||||||
self.close_websocket()
|
|
||||||
# Status = Error
|
|
||||||
utils.settings(self.status_setting, value=utils.lang(257))
|
|
||||||
|
|
||||||
|
|
||||||
class PMS_Websocket(WebSocket):
|
class PMSWebsocketApp(PlexWebSocketApp):
|
||||||
"""
|
name = 'pms_websocket'
|
||||||
Websocket connection with the PMS for Plex Companion
|
|
||||||
"""
|
def get_uri(self):
|
||||||
status_setting = 'pms_websocket_status'
|
return get_pms_uri()
|
||||||
|
|
||||||
def should_suspend(self):
|
def should_suspend(self):
|
||||||
"""
|
"""
|
||||||
Returns True if the thread is suspended.
|
Returns True if the thread needs to suspend.
|
||||||
"""
|
"""
|
||||||
suspend = self._suspended or app.SYNC.background_sync_disabled
|
return (self._suspended or
|
||||||
if suspend:
|
utils.settings('enableBackgroundSync') != 'true')
|
||||||
# This thread needs to clear the Event() _is_not_suspended itself!
|
|
||||||
self.suspend()
|
|
||||||
return suspend
|
|
||||||
|
|
||||||
def getUri(self):
|
def set_suspension_settings_status(self):
|
||||||
if self.redirect_uri:
|
if utils.settings('enableBackgroundSync') != 'true':
|
||||||
uri = self.redirect_uri
|
# Status = Disabled
|
||||||
self.redirect_uri = None
|
utils.settings(self.name + SETTINGS_STRING,
|
||||||
|
value=utils.lang(24023))
|
||||||
else:
|
else:
|
||||||
server = app.CONN.server
|
# Status = 'Suspended - not connected'
|
||||||
# Get the appropriate prefix for the websocket
|
utils.settings(self.name + SETTINGS_STRING,
|
||||||
if server.startswith('https'):
|
value=utils.lang(39093))
|
||||||
server = "wss%s" % server[5:]
|
|
||||||
else:
|
|
||||||
server = "ws%s" % server[4:]
|
|
||||||
uri = "%s/:/websockets/notifications" % server
|
|
||||||
if app.ACCOUNT.pms_token:
|
|
||||||
uri += '?X-Plex-Token=%s' % app.ACCOUNT.pms_token
|
|
||||||
sslopt = {}
|
|
||||||
LOG.debug("%s: Uri: %s, sslopt: %s",
|
|
||||||
self.__class__.__name__, uri, sslopt)
|
|
||||||
return uri, sslopt
|
|
||||||
|
|
||||||
def process(self, opcode, message):
|
|
||||||
if opcode not in self.opcode_data:
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
message = loads(message)
|
|
||||||
except ValueError as err:
|
|
||||||
LOG.error('%s: Error decoding message from websocket: %s',
|
|
||||||
self.__class__.__name__, err)
|
|
||||||
LOG.error(message)
|
|
||||||
return
|
|
||||||
try:
|
|
||||||
message = message['NotificationContainer']
|
|
||||||
except KeyError:
|
|
||||||
LOG.error('%s: Could not parse PMS message: %s',
|
|
||||||
self.__class__.__name__, message)
|
|
||||||
return
|
|
||||||
# Triage
|
|
||||||
typus = message.get('type')
|
|
||||||
if typus is None:
|
|
||||||
LOG.error('%s: No message type, dropping message: %s',
|
|
||||||
self.__class__.__name__, message)
|
|
||||||
return
|
|
||||||
LOG.debug('%s: Received message from PMS server: %s',
|
|
||||||
self.__class__.__name__, message)
|
|
||||||
# Drop everything we're not interested in
|
|
||||||
if typus not in ('playing', 'timeline', 'activity'):
|
|
||||||
return
|
|
||||||
else:
|
|
||||||
# Put PMS message on queue and let libsync take care of it
|
|
||||||
app.APP.websocket_queue.put(message)
|
|
||||||
|
|
||||||
|
|
||||||
class Alexa_Websocket(WebSocket):
|
class AlexaWebsocketApp(PlexWebSocketApp):
|
||||||
"""
|
name = 'alexa_websocket'
|
||||||
Websocket connection to talk to Amazon Alexa.
|
|
||||||
"""
|
def get_uri(self):
|
||||||
status_setting = 'alexa_websocket_status'
|
return get_alexa_uri()
|
||||||
|
|
||||||
def should_suspend(self):
|
def should_suspend(self):
|
||||||
"""
|
"""
|
||||||
Overwrite method since we need to check for plex token
|
Returns True if the thread needs to suspend.
|
||||||
"""
|
"""
|
||||||
suspend = self._suspended or \
|
return self._suspended or \
|
||||||
not app.SYNC.enable_alexa or \
|
utils.settings('enable_alexa') != 'true' or \
|
||||||
app.ACCOUNT.restricted_user or \
|
app.ACCOUNT.restricted_user or \
|
||||||
not app.ACCOUNT.plex_token
|
not app.ACCOUNT.plex_token
|
||||||
if suspend:
|
|
||||||
# This thread needs to clear the Event() _is_not_suspended itself!
|
|
||||||
self.suspend()
|
|
||||||
return suspend
|
|
||||||
|
|
||||||
def getUri(self):
|
def set_suspension_settings_status(self):
|
||||||
if self.redirect_uri:
|
if utils.settings('enable_alexa') != 'true':
|
||||||
uri = self.redirect_uri
|
# Status = Disabled
|
||||||
self.redirect_uri = None
|
utils.settings(self.name + SETTINGS_STRING,
|
||||||
|
value=utils.lang(24023))
|
||||||
|
elif app.ACCOUNT.restricted_user:
|
||||||
|
# Status = Managed Plex User - not connected
|
||||||
|
utils.settings(self.name + SETTINGS_STRING,
|
||||||
|
value=utils.lang(39094))
|
||||||
|
elif not app.ACCOUNT.plex_token:
|
||||||
|
# Status = Not logged in to plex.tv
|
||||||
|
utils.settings(self.name + SETTINGS_STRING,
|
||||||
|
value=utils.lang(39226))
|
||||||
else:
|
else:
|
||||||
uri = ('wss://pubsub.plex.tv/sub/websockets/%s/%s?X-Plex-Token=%s'
|
# Status = 'Suspended - not connected'
|
||||||
% (app.ACCOUNT.plex_user_id,
|
utils.settings(self.name + SETTINGS_STRING,
|
||||||
v.PKC_MACHINE_IDENTIFIER,
|
value=utils.lang(39093))
|
||||||
app.ACCOUNT.plex_token))
|
|
||||||
sslopt = {}
|
|
||||||
LOG.debug("%s: Uri: %s, sslopt: %s",
|
|
||||||
self.__class__.__name__, uri, sslopt)
|
|
||||||
return uri, sslopt
|
|
||||||
|
|
||||||
def process(self, opcode, message):
|
|
||||||
if opcode not in self.opcode_data:
|
def get_pms_websocketapp():
|
||||||
return
|
return PMSWebsocketApp(on_open=on_open,
|
||||||
LOG.debug('%s: Received the following message from Alexa:',
|
on_message=pms_on_message,
|
||||||
self.__class__.__name__)
|
on_error=on_error,
|
||||||
LOG.debug('%s: %s', self.__class__.__name__, message)
|
on_close=on_close)
|
||||||
try:
|
|
||||||
message = utils.etree.fromstring(message)
|
|
||||||
except Exception as ex:
|
def get_alexa_websocketapp():
|
||||||
LOG.error('%s: Error decoding message from Alexa: %s',
|
return AlexaWebsocketApp(on_open=on_open,
|
||||||
self.__class__.__name__, ex)
|
on_message=alexa_on_message,
|
||||||
return
|
on_error=on_error,
|
||||||
try:
|
on_close=on_close)
|
||||||
if message.attrib['command'] == 'processRemoteControlCommand':
|
|
||||||
message = message[0]
|
|
||||||
else:
|
|
||||||
LOG.error('%s: Unknown Alexa message received',
|
|
||||||
self.__class__.__name__)
|
|
||||||
return
|
|
||||||
except Exception:
|
|
||||||
LOG.error('%s: Could not parse Alexa message',
|
|
||||||
self.__class__.__name__)
|
|
||||||
return
|
|
||||||
companion.process_command(message.attrib['path'][1:], message.attrib)
|
|
||||||
|
|
Loading…
Reference in a new issue