pax_global_header00006660000000000000000000000064123157707340014523gustar00rootroot0000000000000052 comment=7f43f2a6bff1aabafad023652042439d7b9b8d84 python-ws4py-0.3.4/000077500000000000000000000000001231577073400141345ustar00rootroot00000000000000python-ws4py-0.3.4/.gitignore000066400000000000000000000000511231577073400161200ustar00rootroot00000000000000build *.egg-info *.pyc .tox docs/_build/ python-ws4py-0.3.4/.travis.yml000066400000000000000000000010751231577073400162500ustar00rootroot00000000000000language: python python: - 2.7 - 3.3 before_install: - sudo apt-get install python-dev libevent-dev - pip install --use-mirrors Cython install: - if [[ $TRAVIS_PYTHON_VERSION == '2.7' ]]; then pip install --use-mirrors -r requirements/py2kreqs.txt; fi - if [[ $TRAVIS_PYTHON_VERSION == 3* ]]; then pip install --use-mirrors -r requirements/py3kreqs.txt; fi - python setup.py install script: - if [[ $TRAVIS_PYTHON_VERSION == '2.7' ]]; then python -m unittest discover; fi - if [[ $TRAVIS_PYTHON_VERSION == 3* ]]; then python -m unittest discover; fipython-ws4py-0.3.4/CHANGELOG.txt000066400000000000000000000020161231577073400161630ustar00rootroot00000000000000ws4py changelog =============== Version 0.3.4 ------------- * Setup build was broken [#123] Version 0.3.3 ------------- * Using `gevent.Pool` rather than `gevent.Group` so that the `stop` method can be called upon it [#114]. * Added support for asyncio and :pep:`3156` * Now using xrange in both Python 3 and Python 2 [#113] Version 0.3.2 ------------- * Use of unicode and bytes litterals to make the code easier to read * Python 2.6 and 3.2 are not supported anymore * Internal cleanup to rely solely on bytes rather than on bytearrays * Improved unit testing coverage (still some way to go) * Reworked the gevent server interface slightly so that the chaussette project can now use it * Support for unix-domain socket * Support for client side certificates and other SSL options * Fixed many various bugs since 0.3.0 Version 0.3.0 ------------- * Introduced a neutral socket manager that deals with their events. The manager uses a loop-event based interface to handle sockets; select and epoll are currently implemented. python-ws4py-0.3.4/LICENSE000066400000000000000000000027531231577073400151500ustar00rootroot00000000000000Copyright (c) 2011-2014, Sylvain Hellegouarch All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of ws4py nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. python-ws4py-0.3.4/README.md000066400000000000000000000005771231577073400154240ustar00rootroot00000000000000WebSocket for Python (ws4py) ============================ Python library providing an implementation of the WebSocket protocol defined in [RFC 6455](http://tools.ietf.org/html/rfc6455). Read the [documentation](https://ws4py.readthedocs.org/en/latest/) for more information. You can also join the [ws4py mailing-list](http://groups.google.com/group/ws4py) to discuss the library. python-ws4py-0.3.4/docs/000077500000000000000000000000001231577073400150645ustar00rootroot00000000000000python-ws4py-0.3.4/docs/Makefile000066400000000000000000000126701231577073400165320ustar00rootroot00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: -rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/ws4py.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/ws4py.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/ws4py" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/ws4py" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." python-ws4py-0.3.4/docs/conf.py000066400000000000000000000224601231577073400163670ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # ws4py documentation build configuration file, created by # sphinx-quickstart on Sat Feb 18 19:54:37 2012. # # This file is execfile()d with the current directory set to its containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import sys, os # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. sys.path.insert(0, os.path.abspath(os.path.join( os.path.dirname(__file__), '..'))) from ws4py import __version__ # -- General configuration ----------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. #needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = ['sphinx.ext.autodoc', 'sphinx.ext.autosummary', 'sphinx.ext.coverage', 'sphinx.ext.viewcode', 'sphinx.ext.intersphinx', 'sphinxcontrib.seqdiag'] autoclass_content = 'init' autodoc_member_order = 'bysource' intersphinx_mapping = {'python': ('http://docs.python.org/2.7', None)} # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The encoding of source files. source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = u'ws4py' copyright = u'2011 - 2014, Sylvain Hellegouarch' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = __version__ # The full version, including alpha/beta/rc tags. release = __version__ # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. #language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. #today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ['_build'] # The reST default role (used for this markup: `text`) to use for all documents. #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. add_function_parentheses = False # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. show_authors = True # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' highlight_language = 'python' # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] # -- Options for HTML output --------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. if os.environ.get('READTHEDOCS', None) == 'True': html_theme = 'default' else: html_theme = 'sphinxdoc' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. #html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. #html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". #html_title = None # A shorter title for the navigation bar. Default is the same as html_title. #html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. #html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. #html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. #html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. #html_domain_indices = True # If false, no index is generated. #html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, links to the reST sources are added to the pages. #html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. #html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. #html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = 'ws4pydoc' # -- Options for LaTeX output -------------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). #'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). #'pointsize': '10pt', # Additional stuff for the LaTeX preamble. #'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ ('index', 'ws4py.tex', u'ws4py Documentation', u'Author', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. #latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # If true, show page references after internal links. #latex_show_pagerefs = False # If true, show URL addresses after external links. #latex_show_urls = False # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_domain_indices = True # -- Options for manual page output -------------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ('index', 'ws4py', u'ws4py Documentation', [u'Author'], 1) ] # If true, show URL addresses after external links. #man_show_urls = False # -- Options for Texinfo output ------------------------------------------------ # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ('index', 'ws4py', u'ws4py Documentation', u'Author', 'ws4py', 'One line description of project.', 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. #texinfo_appendices = [] # If false, no module index is generated. #texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. #texinfo_show_urls = 'footnote' # -- Options for Epub output --------------------------------------------------- # Bibliographic Dublin Core info. epub_title = u'ws4py' epub_author = u'Sylvain Hellegouarch' epub_publisher = u'Sylvain Hellegouarch' epub_copyright = u'2011 - 2013, Sylvain Hellegouarch' # The language of the text. It defaults to the language option # or en if the language is not set. #epub_language = '' # The scheme of the identifier. Typical schemes are ISBN or URL. #epub_scheme = '' # The unique identifier of the text. This can be a ISBN number # or the project homepage. #epub_identifier = '' # A unique identification for the text. #epub_uid = '' # A tuple containing the cover image and cover page html template filenames. #epub_cover = () # HTML files that should be inserted before the pages created by sphinx. # The format is a list of tuples containing the path and title. #epub_pre_files = [] # HTML files shat should be inserted after the pages created by sphinx. # The format is a list of tuples containing the path and title. #epub_post_files = [] # A list of files that should not be packed into the epub file. #epub_exclude_files = [] # The depth of the table of contents in toc.ncx. #epub_tocdepth = 3 # Allow duplicate toc entries. #epub_tocdup = True python-ws4py-0.3.4/docs/index.rst000066400000000000000000000026401231577073400167270ustar00rootroot00000000000000ws4py - A WebSocket package for Python ====================================== :Author: `Sylvain Hellegouarch `_ :Release: |version| :License: `BSD `_ :Source code: https://github.com/Lawouach/WebSocket-for-Python :Build status: https://travis-ci.org/Lawouach/WebSocket-for-Python ws4py is a Python package implementing the WebSocket protocol as defined in `RFC 6455 `_. It comes with various server and client implementations and runs on CPython 2/3, PyPy and Android. Overview ======== .. toctree:: :maxdepth: 1 sources/requirements sources/install sources/conformance sources/browser sources/performance sources/credits Tutorial ======== .. toctree:: :maxdepth: 2 sources/basics sources/clienttutorial sources/servertutorial sources/managertutorial sources/examples Maintainer Guide ================ This section describes the steps to work on ws4py itself and its release process, as well as other conventions and best practices. .. toctree:: :maxdepth: 1 sources/maintainer/rules sources/maintainer/design sources/maintainer/testing sources/maintainer/documenting sources/maintainer/releasing Packages ======== .. toctree:: :maxdepth: 6 sources/ws4py Indices and tables ================== * :ref:`modindex` * :ref:`search` python-ws4py-0.3.4/docs/make.bat000066400000000000000000000117461231577073400165020ustar00rootroot00000000000000@ECHO OFF REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set BUILDDIR=_build set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . set I18NSPHINXOPTS=%SPHINXOPTS% . if NOT "%PAPER%" == "" ( set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% ) if "%1" == "" goto help if "%1" == "help" ( :help echo.Please use `make ^` where ^ is one of echo. html to make standalone HTML files echo. dirhtml to make HTML files named index.html in directories echo. singlehtml to make a single large HTML file echo. pickle to make pickle files echo. json to make JSON files echo. htmlhelp to make HTML files and a HTML help project echo. qthelp to make HTML files and a qthelp project echo. devhelp to make HTML files and a Devhelp project echo. epub to make an epub echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter echo. text to make text files echo. man to make manual pages echo. texinfo to make Texinfo files echo. gettext to make PO message catalogs echo. changes to make an overview over all changed/added/deprecated items echo. linkcheck to check all external links for integrity echo. doctest to run all doctests embedded in the documentation if enabled goto end ) if "%1" == "clean" ( for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i del /q /s %BUILDDIR%\* goto end ) if "%1" == "html" ( %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/html. goto end ) if "%1" == "dirhtml" ( %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. goto end ) if "%1" == "singlehtml" ( %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. goto end ) if "%1" == "pickle" ( %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the pickle files. goto end ) if "%1" == "json" ( %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the JSON files. goto end ) if "%1" == "htmlhelp" ( %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run HTML Help Workshop with the ^ .hhp project file in %BUILDDIR%/htmlhelp. goto end ) if "%1" == "qthelp" ( %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run "qcollectiongenerator" with the ^ .qhcp project file in %BUILDDIR%/qthelp, like this: echo.^> qcollectiongenerator %BUILDDIR%\qthelp\ws4py.qhcp echo.To view the help file: echo.^> assistant -collectionFile %BUILDDIR%\qthelp\ws4py.ghc goto end ) if "%1" == "devhelp" ( %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp if errorlevel 1 exit /b 1 echo. echo.Build finished. goto end ) if "%1" == "epub" ( %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub if errorlevel 1 exit /b 1 echo. echo.Build finished. The epub file is in %BUILDDIR%/epub. goto end ) if "%1" == "latex" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex if errorlevel 1 exit /b 1 echo. echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. goto end ) if "%1" == "text" ( %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text if errorlevel 1 exit /b 1 echo. echo.Build finished. The text files are in %BUILDDIR%/text. goto end ) if "%1" == "man" ( %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man if errorlevel 1 exit /b 1 echo. echo.Build finished. The manual pages are in %BUILDDIR%/man. goto end ) if "%1" == "texinfo" ( %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo if errorlevel 1 exit /b 1 echo. echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. goto end ) if "%1" == "gettext" ( %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale if errorlevel 1 exit /b 1 echo. echo.Build finished. The message catalogs are in %BUILDDIR%/locale. goto end ) if "%1" == "changes" ( %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes if errorlevel 1 exit /b 1 echo. echo.The overview file is in %BUILDDIR%/changes. goto end ) if "%1" == "linkcheck" ( %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck if errorlevel 1 exit /b 1 echo. echo.Link check complete; look for any errors in the above output ^ or in %BUILDDIR%/linkcheck/output.txt. goto end ) if "%1" == "doctest" ( %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest if errorlevel 1 exit /b 1 echo. echo.Testing of doctests in the sources finished, look at the ^ results in %BUILDDIR%/doctest/output.txt. goto end ) :end python-ws4py-0.3.4/docs/sources/000077500000000000000000000000001231577073400165475ustar00rootroot00000000000000python-ws4py-0.3.4/docs/sources/basics.rst000066400000000000000000000031251231577073400205460ustar00rootroot00000000000000Basics ====== ws4py provides a high-level, yet simple, interface to provide your application with WebSocket support. It is simple as: .. code-block:: python from ws4py.websocket import WebSocket The :class:`WebSocket ` class should be sub-classed by your application. To the very least we suggest you override the :func:`received_message(message) ` method so that you can process incoming messages. For instance a straightforward echo application would look like this: .. code-block:: python class EchoWebSocket(WebSocket): def received_message(self, message): self.send(message.data, message.is_binary) Other useful methods to implement are: * :func:`opened() ` which is called whenever the WebSocket handshake is done. * :func:`closed(code, reason=None) ` which is called whenever the WebSocket connection is terminated. You may want to know if the connection is currently usable or :attr:`terminated `. At that stage, the subclass is still not connected to any data source. The way ws4py is designed, you don't necessarily a connected socket, in fact, you don't even need a socket at all. .. code-block:: python >>> from ws4py.messaging import TextMessage >>> def data_source(): >>> yield TextMessage(u'hello world') >>> from mock import MagicMock >>> source = MagicMock(side_effect=data_source) >>> ws = EchoWebSocket(sock=source) >>> ws.send(u'hello there') python-ws4py-0.3.4/docs/sources/browser.rst000066400000000000000000000006301231577073400207630ustar00rootroot00000000000000.. _browser: Browser Support =============== ws4py has been tested using: - Chromium 22 - Firefox 16 See http://caniuse.com/websockets to determine the current implementation's status of various browser vendors. Bear in mind that time is a premium and maintaining obsolete and unsecure protocols is not one of ws4py's goals. It's therefore unlikely it will ever support older versions of the protocol. python-ws4py-0.3.4/docs/sources/clienttutorial.rst000066400000000000000000000070411231577073400223450ustar00rootroot00000000000000Client ====== ws4py comes with various client implementations and they roughly share the same interface. Built-in -------- The built-in client relies only on modules provided by the Python stdlib. The client's inner loop runs within a thread and therefore holds the thread alive until the websocket is closed. .. code-block:: python :linenos: from ws4py.client.threadedclient import WebSocketClient class DummyClient(WebSocketClient): def opened(self): def data_provider(): for i in range(1, 200, 25): yield "#" * i self.send(data_provider()) for i in range(0, 200, 25): print i self.send("*" * i) def closed(self, code, reason=None): print "Closed down", code, reason def received_message(self, m): print m if len(m) == 175: self.close(reason='Bye bye') if __name__ == '__main__': try: ws = DummyClient('ws://localhost:9000/', protocols=['http-only', 'chat']) ws.connect() ws.run_forever() except KeyboardInterrupt: ws.close() In this snippet, when the handshake is successful, the :meth:`opened() ` method is called and within this method we immediately send a bunch of messages to the server. First we demonstrate how you can use a generator to do so, then we simply send strings. Assuming the server echoes messages as they arrive, the :func:`received_message(message) ` method will print out the messages returned by the server and simply close the connection once it receives the last sent messages, which length is 175. Finally the :func:`closed(code, reason=None) ` method is called with the code and reason given by the server. .. seealso:: :ref:`manager`. Tornado ------- If you are using a Tornado backend you may use the Tornado client that ws4py provides as follow: .. code-block:: python from ws4py.client.tornadoclient import TornadoWebSocketClient from tornado import ioloop class MyClient(TornadoWebSocketClient): def opened(self): for i in range(0, 200, 25): self.send("*" * i) def received_message(self, m): print m if len(m) == 175: self.close(reason='Bye bye') def closed(self, code, reason=None): ioloop.IOLoop.instance().stop() ws = MyClient('ws://localhost:9000/echo', protocols=['http-only', 'chat']) ws.connect() ioloop.IOLoop.instance().start() gevent ------ If you are using a gevent backend you may use the gevent client that ws4py provides as follow: .. code-block:: python from ws4py.client.geventclient import WebSocketClient This client can benefit from gevent's concepts as demonstrated below: .. code-block:: python ws = WebSocketClient('ws://localhost:9000/echo', protocols=['http-only', 'chat']) ws.connect() def incoming(): """ Greenlet waiting for incoming messages until ``None`` is received, indicating we can leave the loop. """ while True: m = ws.receive() if m is not None: print str(m) else: break def send_a_bunch(): for i in range(0, 40, 5): ws.send("*" * i) greenlets = [ gevent.spawn(incoming), gevent.spawn(send_a_bunch), ] gevent.joinall(greenlets) python-ws4py-0.3.4/docs/sources/conformance.rst000066400000000000000000000006131231577073400215730ustar00rootroot00000000000000.. _conformance: Conformance =========== ws4py tries hard to be as conformant as it can to the specification. In order to validate this conformance, each release is run against the `Autobahn testsuite `_ which provides an extensive coverage of various aspects of the protocol. Online test reports can be found at: http://www.defuze.org/oss/ws4py/testreports/servers/ python-ws4py-0.3.4/docs/sources/credits.rst000066400000000000000000000006111231577073400207340ustar00rootroot00000000000000.. _credits: Credits ======= Many thanks to the pywebsocket and Tornado projects which have provided a the starting point for ws4py. Thanks also to Jeff Lindsay (progrium) for the initial gevent server support. A well deserved thank you to Tobias Oberstein for `Autobahn `_ test suite. Obviously thanks to all the various folks who have provided bug reports and fixes. python-ws4py-0.3.4/docs/sources/examples.rst000066400000000000000000000014701231577073400211210ustar00rootroot00000000000000.. _examples: Built-in Examples ================= Real-time chat -------------- The ``echo_cherrypy_server`` example provides a simple Echo server. It requires CherryPy 3.2.3. Open a couple of tabs pointing at http://localhost:9000 and chat around. Android sensors and HTML5 ------------------------- The ``droid_sensor_cherrypy_server`` broadcasts sensor metrics to clients. Point your browser to http://localhost:9000 Then run the ``droid_sensor`` module from your Android device using `SL4A `_. A screenshot of what this renders to can be found `here `_. You will find a lovely `video `_ of this demo in action on YouTube thanks to Mat Bettinson for this. python-ws4py-0.3.4/docs/sources/install.rst000066400000000000000000000014071231577073400207510ustar00rootroot00000000000000Install ws4py ============= Get the code ------------ ws4py is hosted on `github `_ and can be retrieved from there: .. code-block:: console $ git clone git@github.com:Lawouach/WebSocket-for-Python.git Installing the ws4py package is performed as usual: .. code-block:: console $ python setup.py install However, since ws4py is referenced in `PyPI `_, it can also be installed through easy_install, distribute or pip: .. code-block:: console $ pip install ws4py $ easy_install ws4py .. note:: ws4py explicitely will not automatically pull out its dependencies. Please install them manually depending on which imlementation you'll be using. python-ws4py-0.3.4/docs/sources/maintainer/000077500000000000000000000000001231577073400206765ustar00rootroot00000000000000python-ws4py-0.3.4/docs/sources/maintainer/design.rst000066400000000000000000000060701231577073400227040ustar00rootroot00000000000000.. _design: Design ====== Workflow -------- ws4py's design is actually fairly simple and straigtforward. At a high level this is what is going on: .. seqdiag:: seqdiag { === HTTP Upgrade exchange === client -> webserver [label = "GET /ws"]; client <- webserver [label = "101 Switching Protocols"]; === WebSocket exchange === webserver --> ws4py_manager; ws4py_manager --> ws4py_websocket [label = "opened"]; ws4py_manager --> ws4py_manager [label = "poll"]; client -> ws4py_manager [label = "send"]; ws4py_manager --> ws4py_websocket [label = "received"]; client <- ws4py_websocket [label = "send"]; ws4py_manager --> ws4py_manager [label = "poll"]; client <- ws4py_websocket [label = "send"]; === WebSocket closing exchange (client initiated) === client -> ws4py_manager [label = "close"]; ws4py_manager --> ws4py_websocket [label = "close"]; client <- ws4py_websocket [label = "close"]; ws4py_manager --> ws4py_manager [label = "poll"]; ws4py_manager --> ws4py_websocket [label = "closed"]; } The initial connection is made by a WebSocket aware client to a WebSocket aware web server. The first exchange is dedicated to perform the upgrade handshake as defined by :rfc:`6455#section-4`. If the exchange succeeds, the socket is kept opened, wrapped into a :class:`ws4py.websocket.WebSocket` instance which is passed on to the global ws4py :class:`ws4py.manager.WebSocketManager` instance which will handle its lifecycle. Most notably, the manager will poll for the socket's receive events so that, when bytes are available, the websocket object can read and process them. Implementation -------------- ws4py data model is rather simple and follows the protocol itself: - a highlevel :class:`ws4py.websocket.WebSocket` class that determines actions to carry based on messages that are parsed. - a :class:`ws4py.streaming.Stream` class that handles a single message at a time - a :class:`ws4py.framing.Frame` class that performs the low level protocol parsing of frames Each are inter-connected as russian dolls generators. The process heavily relies on the capacity to send to a generator. So everytime one of those layers requires something, it yields and then its holder sends it back whatever was required. The Frame parser yields the number of bytes it needs at any time, the stream parser forwards it back to the WebSocket class which gets data from the underlying data provider it holds a reference to (a socket typically). The WebSocket class sends bytes as they are read from the socket down to the stream parser which forwards them to the frame parser. Eventually a frame is parsed and handled by the stream parser which in turns yields a complete message made of all parsed frames. The interesting aspect here is that the socket provider is totally abstracted from the protocol implementation which simply requires bytes as they come. This means one could write a ws4py socket provider that doesn't read from the wire but from any other source. It's also pretty fast and easy to read. python-ws4py-0.3.4/docs/sources/maintainer/documenting.rst000066400000000000000000000035451231577073400237530ustar00rootroot00000000000000.. _documenting: Documentation process ===================== Basic principles ---------------- Document in that order: why, what, how ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Documenting ws4py is an important process since the code doesn't always carry enough information to understand its design and context. Thus, documenting should target the question "why?" first then the "what?" and "how?". It's actually trickier than it sound. Explicit is better than implicit ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ When you have your nose in the code it may sound straightforward enough not to document certain aspects of pyalm. Remember that :pep:`20` principle: **Explicit is better than implicit**. Be clear, not verbose ^^^^^^^^^^^^^^^^^^^^^ Finding the right balance between too much and not enough is hard. Writing good documentation is just difficult. However, you should not be too verbose either. Add enough details to a section to provide context but don't flood the reader with irrelevant information. Show me where you come from ^^^^^^^^^^^^^^^^^^^^^^^^^^^ Every piece of code should be contextualized and almost every time you should explicitely indicate the import statement so that the reader doesn't wonder where an object comes from. Consistency for the greater good ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ A user of ws4py should feel at ease with any part of the library and shouldn't have to switch to a completely new mental model when going through the library or documentation. Please refer again to :pep:`8`. Documentation Toolkit --------------------- pyalm uses the Sphinx documentation generator toolkit so refer to its `documentation `_ to learn more on its usage. Building the documentation is as simple as: .. code-block:: console cd docs make html The generated documentation will be available in ``docs\_build\html``. python-ws4py-0.3.4/docs/sources/maintainer/releasing.rst000066400000000000000000000025111231577073400234000ustar00rootroot00000000000000.. _releasing: Release Process =============== ws4py's release process is as follow: 1. Update the release minor or micro version. If necessary change also the major version. This should be saved only for major modifications and/or API compatibility breakup. Edit ``ws4py/__init__.py`` accordingly. This will propagate to the ``setup.py`` and ``docs/conf.py`` appropriately on its own. .. seealso:: `How to version? `_ You should read this. 2. Run the unit test suites It's simple, fast and will make you sleep well at night. So `do it `_. If the test suite fails, do not release. It's a simple rule we constantly fail for some reason. So if it fails, go back and fix it. 3. Rebuild the documentation It may sound funny but a release with an out of date documentation has little value. Keeping your documentation up to date is as important as having no failing unit tests. Add to subversion any new documentation pages, both their sources and the resulting HTML files. 4. Build the source package First delete the ``build`` directory. Run the following command: .. code-block:: console python setup.py sdist --formats=gztar This will produce a tarball in the ``dist`` directory. 5. Push the release to PyPI 6. Tag the release in github 7. Announce it to the world :) python-ws4py-0.3.4/docs/sources/maintainer/rules.rst000066400000000000000000000007021231577073400225610ustar00rootroot00000000000000.. _rules: Coding Rules ============ Python is a rather flexible language which favors conventions over configurations. This is why, over the years, some community rules were published that most Python developers follow. ws4py tries to follow those principles for the most part and therefore. Therefore please carefully read: - :pep:`8` that suggests a set of coding rules and naming conventions - :pep:`20` for the spirit behind Python coding python-ws4py-0.3.4/docs/sources/maintainer/testing.rst000066400000000000000000000050321231577073400231050ustar00rootroot00000000000000.. _testing: Testing Overview ================ ws4py is a Python library which means it can be tested in various fashion: - unit testing - functional testing - load testing Though all of them are of useful, ws4py mostly relies on functional testing. Unit Testing ------------ Unit testing solves complex issues in a simple fashion: - Micro-validation of classes and functions - Ensure non-regression after modifications - Critique the design as early as possible for minimum impact Too often, developers focus solely on the first two and fail to realise how much feedback they can get by writing a simple unit tests. Usually starting writing unit tests can take time because your code is too tightly coupled with itself or external dependencies. This should not the case, most of the time anyway. So make sure to reflect on your code design whenever you have difficulties setting up proper unit tests. .. note:: Unfortunately, for now ws4py has a rather shallow coverage as it relies more on the functional testing to ensure the package is sane. I hope to change this in the future. Framework ^^^^^^^^^ ws4py uses the Python built-in :mod:`unittest` module. Make sure you read its extensive documentation. Execution ^^^^^^^^^ Test execution can be done as follow: .. code-block:: console cd test python -m unittest discover # execute all tests in the current directory Tests can obviously be executed via nose, unittest2 or py.test if you prefer. Functional Testing ------------------ ws4py relies heavily on the extensive `testing suite `_ provided by the `Autobahn `_ project. The server test suite is used by many other WebSocket implementation out there and provides a great way to validate interopability so it must be executed before each release to the least. Please refer to the :ref:`requirements` page to install the test suite. Execution ^^^^^^^^^ - Start the CherryPy server with PyPy 1.9 .. code-block:: console pypy test/autobahn_test_servers.py --run-cherrypy-server-pypy - Start the CherryPy server with Python 3.2 and/or 3.3 if you can. .. code-block:: console python3 test/autobahn_test_servers.py --run-cherrypy-server-py3k - Start all servers with Python 2.7 .. code-block:: console python2 test/autobahn_test_servers.py --run-all - Finally, execute the test suite as follow: .. code-block:: console wstest -m fuzzingclient -s test/fuzzingclient.json The whole test suite will take a while to complete so be patient. python-ws4py-0.3.4/docs/sources/managertutorial.rst000066400000000000000000000061771231577073400225120ustar00rootroot00000000000000.. _manager: Managing a pool of WebSockets ============================= ws4py provides a :class:`ws4py.manager.WebSocketManager` class that takes care of :class:`ws4py.websocket.WebSocket` instances once they the HTTP upgrade handshake has been performed. The manager is not compulsory but makes it simpler to track and let them run in your application's process. When you :meth:`add(websocket) ` a websocket to the manager, the file-descriptor is registered with the manager's poller and the :meth:`opened() ` method on is called. Polling ------- The manager uses a polling mechanism to dispatch on socket incoming events. Two pollers are implemented, one using the traditionnal :py:mod:`select` and another one based on `select.epoll `_ which is used only if available on the system. The polling is executed in its own thread, it keeps looping until the manager :meth:`stop() ` method. On every loop, the poller is called to poll for all registered file-descriptors. If any one of them is ready, we retrieve the websocket using that descriptor and, if the websocket is not yet terminated, we call its `once` method so that the incoming bytes are processed. If the processing fails in anyway, the manager terminates the websocket and remove it from itself. Client example -------------- Below is a simple example on how to start 2000 clients against a single server. .. code-block:: python :linenos: from ws4py.client import WebSocketBaseClient from ws4py.manager import WebSocketManager from ws4py import format_addresses, configure_logger logger = configure_logger() m = WebSocketManager() class EchoClient(WebSocketBaseClient): def handshake_ok(self): logger.info("Opening %s" % format_addresses(self)) m.add(self) def received_message(self, msg): logger.info(str(msg)) if __name__ == '__main__': import time try: m.start() for i in range(2000): client = EchoClient('ws://localhost:9000/ws') client.connect() logger.info("%d clients are connected" % i) while True: for ws in m.websockets.itervalues(): if not ws.terminated: break else: break time.sleep(3) except KeyboardInterrupt: m.close_all() m.stop() m.join() Once those are created against the ``echo_cherrypy_server`` example for instance, point your browser to http://localhost:9000/ and enter a message. It will be broadcasted to all connected peers. When a peer is closed, its connection is automatically removed from the manager so you should never need to explicitely remove it. .. note:: The CherryPy and wsgiref servers internally use a manager to handle connected websockets. The gevent server relies only on a greenlet `group `_ instead. python-ws4py-0.3.4/docs/sources/performance.rst000066400000000000000000000010021231577073400215730ustar00rootroot00000000000000.. _perf: Performances ============ ws4py doesn't perform too bad but it's far from being the fastest WebSocket lib under heavy load. The reason is that it was first designed to implement the protocol with simplicity and clarity in mind. Future developments will look at performances. .. note:: ws4py runs faster in some cases on PyPy than it does on CPython. .. note:: The `wsaccel `_ package replaces some internal bottleneck with a Cython implementation. python-ws4py-0.3.4/docs/sources/requirements.rst000066400000000000000000000024041231577073400220240ustar00rootroot00000000000000.. _requirements: Requirements ============ Python ------ Tested environments: - Python 2.7+ - Python 3.3+ - PyPy 1.8+ - Android 2.3 via `SL4A `_ .. note:: ws4py will not try to automatically install dependencies and will let you decide which one you need. Client ------ ws4py comes with three client implementations: - Built-in: That client is solely based on the Python stdlib, it uses a thread to run the inner processing loop. - Tornado: The Tornado client requires `Tornado `_ 2.0+ - gevent: The gevent client requires `gevent `_ 0.13.6 or 1.0.0-dev Server ------ ws4py comes with three server implementations: - Built-in: The server relies on the built-in :mod:`wsgiref` module. - CherryPy: The `CherryPy `_ server requires 3.2.2+ - gevent: The gevent server requires `gevent `_ 1.0.0 - asyncio: :pep:`3156` implementation for Python 3.3+ Testing ------- ws4py uses the Autobahn functional test suite to ensure it respects the standard. You must install it to run that test suite against ws4py. - Autobahn `python `_ - Autobahn `test suite `_ python-ws4py-0.3.4/docs/sources/servertutorial.rst000066400000000000000000000112521231577073400223740ustar00rootroot00000000000000Server ====== ws4py comes with a few server implementations built around the main :class:`WebSocket ` class. CherryPy -------- ws4py provides an extension to CherryPy 3 to enable WebSocket from the framework layer. It is based on the CherryPy `plugin `_ and `tool `_ mechanisms. The :class:`WebSocket tool ` plays at the request level on every request received by the server. Its goal is to perform the WebSocket handshake and, if it succeeds, to create the :class:`WebSocket ` instance (well a subclass you will be implementing) and push it to the plugin. The :class:`WebSocket plugin ` works at the CherryPy system level and has a single instance throughout. Its goal is to track websocket instances created by the tool and free their resources when connections are closed. Here is a simple example of an echo server: .. code-block:: python :linenos: import cherrypy from ws4py.server.cherrypyserver import WebSocketPlugin, WebSocketTool from ws4py.websocket import EchoWebSocket cherrypy.config.update({'server.socket_port': 9000}) WebSocketPlugin(cherrypy.engine).subscribe() cherrypy.tools.websocket = WebSocketTool() class Root(object): @cherrypy.expose def index(self): return 'some HTML with a websocket javascript connection' @cherrypy.expose def ws(self): # you can access the class instance through handler = cherrypy.request.ws_handler cherrypy.quickstart(Root(), '/', config={'/ws': {'tools.websocket.on': True, 'tools.websocket.handler_cls': EchoWebSocket}}) Note how we specify the class which should be instanciated by the server on each connection. The great aspect of the tool mechanism is that you can specify a different class on a per-path basis. gevent ------ `gevent `_ is a coroutine, called greenlets, implementation for very concurrent applications. ws4py offers a server implementation for this library on top of the `WSGI protocol `_. Using it is as simple as: .. code-block:: python :linenos: from gevent import monkey; monkey.patch_all() from ws4py.websocket import EchoWebSocket from ws4py.server.geventserver import WSGIServer from ws4py.server.wsgiutils import WebSocketWSGIApplication server = WSGIServer(('localhost', 9000), WebSocketWSGIApplication(handler_cls=EchoWebSocket)) server.serve_forever() First we patch all the standard modules so that the stdlib runs well with as gevent. Then we simply create a `WSGI `_ server and specify the class which will be instanciated internally each time a connection is successful. wsgiref ------- :py:mod:`wsgiref` is a built-in WSGI package that provides various classes and helpers to develop against WSGI. Mostly it provides a basic WSGI server that can be usedfor testing or simple demos. ws4py provides support for websocket on wsgiref for testing purpose as well. It's not meant to be used in production. .. code-block:: python :linenos: from wsgiref.simple_server import make_server from ws4py.websocket import EchoWebSocket from ws4py.server.wsgirefserver import WSGIServer, WebSocketWSGIRequestHandler from ws4py.server.wsgiutils import WebSocketWSGIApplication server = make_server('', 9000, server_class=WSGIServer, handler_class=WebSocketWSGIRequestHandler, app=WebSocketWSGIApplication(handler_cls=EchoWebSocket)) server.initialize_websockets_manager() server.serve_forever() asyncio ------- :py:mod:`asyncio` is the implementation of :pep:`3156`, the new asynchronous framework for concurrent applications. .. code-block:: python :linenos: from ws4py.async_websocket import EchoWebSocket loop = asyncio.get_event_loop() def start_server(): proto_factory = lambda: WebSocketProtocol(EchoWebSocket) return loop.create_server(proto_factory, '', 9007) s = loop.run_until_complete(start_server()) print('serving on', s.sockets[0].getsockname()) loop.run_forever() .. warning:: The provided HTTP server used for the handshake is clearly not production ready. However, once the handshake is performed, the rest of the code runs the same stack as the other server implementations. It should be easy to replace the HTTP interface with any asyncio aware HTTP framework. python-ws4py-0.3.4/docs/sources/ws4py.client.rst000066400000000000000000000011751231577073400216500ustar00rootroot00000000000000client Package ============== :mod:`client` Package --------------------- .. automodule:: ws4py.client :members: :undoc-members: :show-inheritance: :mod:`geventclient` Module -------------------------- .. automodule:: ws4py.client.geventclient :members: :undoc-members: :show-inheritance: :mod:`threadedclient` Module ---------------------------- .. automodule:: ws4py.client.threadedclient :members: :undoc-members: :show-inheritance: :mod:`tornadoclient` Module --------------------------- .. automodule:: ws4py.client.tornadoclient :members: :undoc-members: :show-inheritance: python-ws4py-0.3.4/docs/sources/ws4py.rst000066400000000000000000000022461231577073400203730ustar00rootroot00000000000000ws4py Package ============= :mod:`ws4py` Package -------------------- .. automodule:: ws4py.__init__ :members: :undoc-members: :show-inheritance: :mod:`exc` Module ----------------- .. automodule:: ws4py.exc :members: :undoc-members: :show-inheritance: :mod:`framing` Module --------------------- .. automodule:: ws4py.framing :members: :undoc-members: :show-inheritance: :mod:`manager` Module --------------------- .. automodule:: ws4py.manager :members: :undoc-members: :show-inheritance: :mod:`messaging` Module ----------------------- .. automodule:: ws4py.messaging :members: :undoc-members: :show-inheritance: :mod:`streaming` Module ----------------------- .. automodule:: ws4py.streaming :members: :undoc-members: :show-inheritance: :mod:`utf8validator` Module --------------------------- .. automodule:: ws4py.utf8validator :members: :undoc-members: :show-inheritance: :mod:`websocket` Module ----------------------- .. automodule:: ws4py.websocket :members: :undoc-members: :show-inheritance: Subpackages ----------- .. toctree:: ws4py.client ws4py.server python-ws4py-0.3.4/docs/sources/ws4py.server.rst000066400000000000000000000012151231577073400216730ustar00rootroot00000000000000server Package ============== :mod:`cherrypyserver` Module ---------------------------- .. automodule:: ws4py.server.cherrypyserver :members: :undoc-members: :show-inheritance: :mod:`geventserver` Module -------------------------- .. automodule:: ws4py.server.geventserver :members: :undoc-members: :show-inheritance: :mod:`wsgirefserver` Module --------------------------- .. automodule:: ws4py.server.wsgirefserver :members: :undoc-members: :show-inheritance: :mod:`wsgitutils` Module ------------------------ .. automodule:: ws4py.server.wsgiutils :members: :undoc-members: :show-inheritance: python-ws4py-0.3.4/example/000077500000000000000000000000001231577073400155675ustar00rootroot00000000000000python-ws4py-0.3.4/example/droid_sensor.py000066400000000000000000000063661231577073400206460ustar00rootroot00000000000000# -*- coding: utf-8 -*- __doc__ = """ WebSocket client that pushes Android sensor metrics to the websocket server it is connected to. In order to set this up: 1. Install SL4A http://code.google.com/p/android-scripting/ 2. Install the Python package for SL4A 3. Build ws4py and copy the package built into a directory called com.googlecode.pythonforandroid/extras/python on your Android device. 4. a. Either copy the droid_sensor.py module into the directory called sl4a/scripts on your Android device. b. Or set up the remote control so that you can run the module from your computer directly on your device: http://code.google.com/p/android-scripting/wiki/RemoteControl 5. Setup the device so that it has an IP address on the same network as the computer running the server. Run the example: 1. Start the echo_cherrypy_server module: $ python example/droid_sensor_cherrypy_server.py 2. From a local browser, go to: http://localhost:9000/ 3. Edit the droid_sensor module to set the appropriate IP address where your server is running. 4. Run the droid_sensor module (from the device or your computer depending on your setup): $ python example/droid_sensor.py 5. If your device isn't idled, just move ir around for the metrics to be sent to the server which will dispatch them to the browser's client. 6. Profit??? """ import time import math import android from ws4py.client.threadedclient import WebSocketClient class AirPongSensor(object): def __init__(self, host): self.droid = android.Android() #self.droid.startSensingThreshold(1, 0, 7) #self.droid.startSensingThreshold(2, 1, 2) self.droid.startSensingTimed(1, 100) self.running = False self.client = AirPongWebSocketClient(host) self.client.connect() def run(self): try: self.running = True last = [None, None, None] while self.running: azimuth, pitch, roll = self.droid.sensorsReadOrientation().result accel = x, y, z = self.droid.sensorsReadAccelerometer().result if azimuth is None: continue c = lambda rad: rad * 360.0 / math.pi print c(azimuth), c(pitch), c(roll), x, y, z if self.client.terminated: break if accel != [None, None, None] and accel != last: last = accel self.client.send("%s %s %s %s %s %s" % (c(azimuth), c(pitch), c(roll), x, y, z)) time.sleep(0.15) finally: self.terminate() def terminate(self): if not self.droid: return self.running = False self.droid.stopSensing() self.droid = None if not self.client.terminated: self.client.close() self.client._th.join() self.client = None class AirPongWebSocketClient(WebSocketClient): def received_message(self, m): pass if __name__ == '__main__': aps = AirPongSensor(host='http://192.168.0.10:9000/ws') try: aps.run() except KeyboardInterrupt: aps.terminate() python-ws4py-0.3.4/example/droid_sensor_cherrypy_server.py000066400000000000000000000056041231577073400241530ustar00rootroot00000000000000# -*- coding: utf-8 -*- import os.path import cherrypy from ws4py.server.cherrypyserver import WebSocketPlugin, WebSocketTool from ws4py.websocket import WebSocket class BroadcastWebSocketHandler(WebSocket): def received_message(self, m): cherrypy.engine.publish('websocket-broadcast', str(m)) class Root(object): @cherrypy.expose def display(self): return """ WebSocket example displaying Android device sensors
""" @cherrypy.expose def ws(self): cherrypy.log("Handler created: %s" % repr(cherrypy.request.ws_handler)) @cherrypy.expose def index(self): return """ WebSocket example displaying Android device sensors """ if __name__ == '__main__': cherrypy.config.update({ 'server.socket_host': '0.0.0.0', 'server.socket_port': 9000, 'tools.staticdir.root': os.path.abspath(os.path.join(os.path.dirname(__file__), 'static')) } ) print os.path.abspath(os.path.join(__file__, 'static')) WebSocketPlugin(cherrypy.engine).subscribe() cherrypy.tools.websocket = WebSocketTool() cherrypy.quickstart(Root(), '', config={ '/js': { 'tools.staticdir.on': True, 'tools.staticdir.dir': 'js' }, '/css': { 'tools.staticdir.on': True, 'tools.staticdir.dir': 'css' }, '/images': { 'tools.staticdir.on': True, 'tools.staticdir.dir': 'images' }, '/ws': { 'tools.websocket.on': True, 'tools.websocket.handler_cls': BroadcastWebSocketHandler } } ) python-ws4py-0.3.4/example/echo_chaussette_server.py000066400000000000000000000024211231577073400226740ustar00rootroot00000000000000# -*- coding: utf-8 -*- import logging from gevent import monkey; monkey.patch_all() import gevent from ws4py.server.geventserver import WebSocketWSGIApplication, \ WebSocketWSGIHandler, GEventWebSocketPool from ws4py.websocket import EchoWebSocket from chaussette.backend._gevent import Server as GeventServer class ws4pyServer(GeventServer): handler_class = WebSocketWSGIHandler def __init__(self, *args, **kwargs): GeventServer.__init__(self, *args, **kwargs) self.pool = GEventWebSocketPool() def stop(self, *args, **kwargs): self.pool.clear() self.pool = None GeventServer.stop(self, *args, **kwargs) if __name__ == '__main__': import os, socket, sys from ws4py import configure_logger logger = configure_logger() from chaussette.backend import register register('ws4py', ws4pyServer) from chaussette.server import make_server server = make_server(app=WebSocketWSGIApplication(handler_cls=EchoWebSocket), host='unix:///%s/ws.sock' % os.getcwd(), address_family=socket.AF_UNIX, backend='ws4py', logger=logger) try: server.serve_forever() except KeyboardInterrupt: sys.exit(0) python-ws4py-0.3.4/example/echo_cherrypy_server.py000066400000000000000000000077641231577073400224100ustar00rootroot00000000000000# -*- coding: utf-8 -*- import argparse import random import os import cherrypy from ws4py.server.cherrypyserver import WebSocketPlugin, WebSocketTool from ws4py.websocket import WebSocket from ws4py.messaging import TextMessage class ChatWebSocketHandler(WebSocket): def received_message(self, m): cherrypy.engine.publish('websocket-broadcast', m) def closed(self, code, reason="A client left the room without a proper explanation."): cherrypy.engine.publish('websocket-broadcast', TextMessage(reason)) class Root(object): def __init__(self, host, port, ssl=False): self.host = host self.port = port self.scheme = 'wss' if ssl else 'ws' @cherrypy.expose def index(self): return """

""" % {'username': "User%d" % random.randint(0, 100), 'host': self.host, 'port': self.port, 'scheme': self.scheme} @cherrypy.expose def ws(self): cherrypy.log("Handler created: %s" % repr(cherrypy.request.ws_handler)) if __name__ == '__main__': import logging from ws4py import configure_logger configure_logger(level=logging.DEBUG) parser = argparse.ArgumentParser(description='Echo CherryPy Server') parser.add_argument('--host', default='127.0.0.1') parser.add_argument('-p', '--port', default=9000, type=int) parser.add_argument('--ssl', action='store_true') args = parser.parse_args() cherrypy.config.update({'server.socket_host': args.host, 'server.socket_port': args.port, 'tools.staticdir.root': os.path.abspath(os.path.join(os.path.dirname(__file__), 'static'))}) if args.ssl: cherrypy.config.update({'server.ssl_certificate': './server.crt', 'server.ssl_private_key': './server.key'}) WebSocketPlugin(cherrypy.engine).subscribe() cherrypy.tools.websocket = WebSocketTool() cherrypy.quickstart(Root(args.host, args.port, args.ssl), '', config={ '/ws': { 'tools.websocket.on': True, 'tools.websocket.handler_cls': ChatWebSocketHandler }, '/js': { 'tools.staticdir.on': True, 'tools.staticdir.dir': 'js' } } ) python-ws4py-0.3.4/example/echo_client.py000066400000000000000000000014701231577073400204170ustar00rootroot00000000000000# -*- coding: utf-8 -*- from ws4py.client.threadedclient import WebSocketClient class EchoClient(WebSocketClient): def opened(self): def data_provider(): for i in range(1, 200, 25): yield "#" * i self.send(data_provider()) for i in range(0, 200, 25): print(i) self.send("*" * i) def closed(self, code, reason): print(("Closed down", code, reason)) def received_message(self, m): print("=> %d %s" % (len(m), str(m))) if len(m) == 175: self.close(reason='Bye bye') if __name__ == '__main__': try: ws = EchoClient('ws://localhost:9000/ws', protocols=['http-only', 'chat']) ws.daemon = False ws.connect() except KeyboardInterrupt: ws.close() python-ws4py-0.3.4/example/echo_gevent_client.py000066400000000000000000000017221231577073400217670ustar00rootroot00000000000000# -*- coding: utf-8 -*- from gevent import monkey; monkey.patch_all() import gevent from ws4py.client.geventclient import WebSocketClient if __name__ == '__main__': ws = WebSocketClient('ws://localhost:9000/ws', protocols=['http-only', 'chat']) ws.connect() ws.send("Hello world") print((ws.receive(),)) ws.send("Hello world again") print((ws.receive(),)) def incoming(): while True: m = ws.receive() if m is not None: m = str(m) print((m, len(m))) if len(m) == 35: ws.close() break else: break print(("Connection closed!",)) def outgoing(): for i in range(0, 40, 5): ws.send("*" * i) # We won't get this back ws.send("Foobar") greenlets = [ gevent.spawn(incoming), gevent.spawn(outgoing), ] gevent.joinall(greenlets) python-ws4py-0.3.4/example/echo_gevent_server.py000066400000000000000000000116451231577073400220240ustar00rootroot00000000000000# -*- coding: utf-8 -*- from gevent import monkey; monkey.patch_all() import argparse import random import os import gevent import gevent.pywsgi from ws4py.server.geventserver import WebSocketWSGIApplication, \ WebSocketWSGIHandler, WSGIServer from ws4py.websocket import EchoWebSocket class BroadcastWebSocket(EchoWebSocket): def opened(self): app = self.environ['ws4py.app'] app.clients.append(self) def received_message(self, m): # self.clients is set from within the server # and holds the list of all connected servers # we can dispatch to app = self.environ['ws4py.app'] for client in app.clients: client.send(m) def closed(self, code, reason="A client left the room without a proper explanation."): app = self.environ.pop('ws4py.app') if self in app.clients: app.clients.remove(self) for client in app.clients: try: client.send(reason) except: pass class EchoWebSocketApplication(object): def __init__(self, host, port): self.host = host self.port = port self.ws = WebSocketWSGIApplication(handler_cls=BroadcastWebSocket) # keep track of connected websocket clients # so that we can brodcasts messages sent by one # to all of them. Aren't we cool? self.clients = [] def __call__(self, environ, start_response): """ Good ol' WSGI application. This is a simple demo so I tried to stay away from dependencies. """ if environ['PATH_INFO'] == '/favicon.ico': return self.favicon(environ, start_response) if environ['PATH_INFO'] == '/ws': environ['ws4py.app'] = self return self.ws(environ, start_response) return self.webapp(environ, start_response) def favicon(self, environ, start_response): """ Don't care about favicon, let's send nothing. """ status = '200 OK' headers = [('Content-type', 'text/plain')] start_response(status, headers) return "" def webapp(self, environ, start_response): """ Our main webapp that'll display the chat form """ status = '200 OK' headers = [('Content-type', 'text/html')] start_response(status, headers) return """

""" % {'username': "User%d" % random.randint(0, 100), 'host': self.host, 'port': self.port} if __name__ == '__main__': from ws4py import configure_logger configure_logger() parser = argparse.ArgumentParser(description='Echo gevent Server') parser.add_argument('--host', default='127.0.0.1') parser.add_argument('-p', '--port', default=9000, type=int) args = parser.parse_args() server = WSGIServer((args.host, args.port), EchoWebSocketApplication(args.host, args.port)) server.serve_forever() python-ws4py-0.3.4/example/mocking_data_source.py000066400000000000000000000027301231577073400221430ustar00rootroot00000000000000from ws4py import configure_logger from ws4py.websocket import WebSocket from ws4py.messaging import Message, TextMessage logger = configure_logger(stdout=True) class DataSource(object): def __init__(self): self.frames = set() self.frame = None self.remaining_bytes = None def setblocking(self, flag): pass def feed(self, message): if isinstance(message, Message): message = message.single(mask=True) else: message = TextMessage(message).single(mask=True) self.frames.add(message) def recv(self, size): if not self.frame: if not self.frames: return b'' self.frame = self.frames.pop() self.remaining_bytes = self.frame current_bytes = self.remaining_bytes[:size] self.remaining_bytes = self.remaining_bytes[size:] if self.remaining_bytes is b'': self.frame = None self.remaining_bytes = None return current_bytes class LogWebSocket(WebSocket): def opened(self): logger.info("WebSocket now ready") def closed(self, code=1000, reason="Burp, done!"): logger.info("Terminated with reason '%s'" % reason) def received_message(self, m): logger.info("Received message: %s" % m) if __name__ == '__main__': source = DataSource() ws = LogWebSocket(sock=source) source.feed(u'hello there') source.feed(u'a bit more') ws.run() python-ws4py-0.3.4/example/server.crt000066400000000000000000000013651231577073400176140ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIICATCCAWoCCQDYwq5UbWJ62zANBgkqhkiG9w0BAQUFADBFMQswCQYDVQQGEwJB VTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0 cyBQdHkgTHRkMB4XDTEyMDMxNDIxMDMyMloXDTEzMDMxNDIxMDMyMlowRTELMAkG A1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0 IFdpZGdpdHMgUHR5IEx0ZDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAu8ic FFJZbp+U6L2HIEFPnblWGaECUQvBIjNAQqzmrMc7GY6BEAolC9g7mNi2D1JaYcxs WgqjiypfW9tiOsybmoGDh2b+HsxkmtIWDM0anHCxbncJitCIrpqHp9Tu7Fvk8SrA kKfu+3fQ/9ge8yDoa2t23/WKnG+nO7G/JuLyEJkCAwEAATANBgkqhkiG9w0BAQUF AAOBgQCD5nRO8nzqlF/Q3wo+pJtHJjS88I6nZxhZTecmhZ3R4J6jo1V/RMysYYNt zLK8m5QEv2P0SLmf5ysGam6Kd8PBGRxMWW1wbWnwxKyDoaso7vTK4eHzwrxgQk9w pKnxiyt1k4P2vars13hdxAEQu11yJkzoMieHTyS+FxIUexUvTg== -----END CERTIFICATE----- python-ws4py-0.3.4/example/server.key000066400000000000000000000015671231577073400176200ustar00rootroot00000000000000-----BEGIN RSA PRIVATE KEY----- MIICXAIBAAKBgQC7yJwUUllun5TovYcgQU+duVYZoQJRC8EiM0BCrOasxzsZjoEQ CiUL2DuY2LYPUlphzGxaCqOLKl9b22I6zJuagYOHZv4ezGSa0hYMzRqccLFudwmK 0Iiumoen1O7sW+TxKsCQp+77d9D/2B7zIOhra3bf9Yqcb6c7sb8m4vIQmQIDAQAB AoGAfSF92BDT5WJToQ+Cdpzux8RTunpPB+CUTwzl2khK4oFUQYBzQlPwQcdSV1S5 ZNZUweytmwaR2k9fAd/bwiDL4m/iGfmerwD1LPaWlYufKPEKG2XBzCAytNTuAU3J /UXqlQK5Ee4aCcmVrXPsjkPgunQM4hs7qQFJI+s0vPPRYT0CQQD3nFys2ScKfszP DLMQelWbo+kxqYrThfnCzsHFWA7fJXsyBX23CR8L1Dy5RcBxUa4I8VATwpfv8V7a MQtDJciLAkEAwiVXGq0agnC5I3xEx2b14ezuXDBWK/Ld+QH6ocTFLxa6oy+YY4Sd 4dilZwWAgef8QBkXJbGxkbw4xKDxDo8L6wJAHSj26QwxwtSn/gI63Efr6QZmogib ZsmyXjTHMRxrs+/QEFYBNhsG4ve9pvwF69J4smjoy0rxZbqBNyTrdJ7wfQJAFwzh 45vrytLhWFI3xEj4JoO/5RgkEwG50wemHzDCjI2xSRCskhw7toXHVYz0rffCHkYc VnBbeccUIlxNYoIfUwJBAKdrypQG/vu6xvRm4Y70K+IiOxzOCIflyT8KYI106jqt 0LfwMlVmtiGUr3tj+GEyi+FlisEJ7xaEHIu41yrcR/4= -----END RSA PRIVATE KEY----- python-ws4py-0.3.4/example/static/000077500000000000000000000000001231577073400170565ustar00rootroot00000000000000python-ws4py-0.3.4/example/static/css/000077500000000000000000000000001231577073400176465ustar00rootroot00000000000000python-ws4py-0.3.4/example/static/css/style.css000066400000000000000000000004341231577073400215210ustar00rootroot00000000000000 body { margin: 0; padding: 0; text-align: left; padding-top: 20px; } #game { background: #eee; margin: auto; overflow: hidden; width: 900px; height: 620px; box-shadow: 8px 8px 12px #020100; } #pong { }python-ws4py-0.3.4/example/static/js/000077500000000000000000000000001231577073400174725ustar00rootroot00000000000000python-ws4py-0.3.4/example/static/js/droidsensor.js000066400000000000000000000050641231577073400223700ustar00rootroot00000000000000 var initWebSocket = function() { var ws = new WebSocket('ws://localhost:9000/ws'); $(window).unload(function() { ws.close(); }); ws.onmessage = function (evt) { console.log(evt.data.split(' ')); var sensors = evt.data.split(' '); $("#canvas").clearCanvas(); drawAzimuth(Math.floor(parseFloat(sensors[0]))); drawPitch(Math.floor(parseFloat(sensors[1]))); drawRoll(Math.floor(parseFloat(sensors[2]))); drawX(Math.abs(Math.floor(parseFloat(sensors[3])))); drawY(Math.abs(Math.floor(parseFloat(sensors[4])))); drawZ(Math.abs(Math.floor(parseFloat(sensors[5])))); }; }; var initWebSocketAndSensors = function() { var ws = new WebSocket('ws://localhost:9000/ws'); $(window).unload(function() { ws.close(); }); if (window.DeviceOrientationEvent) { window.addEventListener("deviceorientation", function(event) { ws.send(event.alpha + " " + event.beta + " " + event.gamma + " 0 0 0"); }, false); } }; var drawArc = function(pos, value, label) { $("#canvas").drawText( { strokeStyle: "#000", text: label, x: pos, y: 100 } ); $("#canvas").drawArc( { strokeStyle: "#F77B00", strokeWidth: 15, x: pos, y: 200, radius: 50, start: 0, end: value } ); }; var drawSegment = function(lblx, lbly, x1, x2, y1, y2, label) { $("#canvas").drawText( { strokeStyle: "#000", text: label, x: lblx, y: lbly } ); $("#canvas").drawLine( { strokeStyle: "#F77B00", strokeWidth: 15, x1: x1, y1: y1, x2: x2, y2: y2 } ); }; var drawAll = function() { drawAzimuth(Math.floor(Math.random() * 360)); drawPitch(Math.floor(Math.random() * 360)); drawRoll(Math.floor(Math.random() * 360)); drawX(Math.floor(Math.random() * 20)); drawY(Math.floor(Math.random() * 20)); drawZ(Math.floor(Math.random() * 20)); }; var drawAzimuth = function(azimuth) { var offset = 75; drawArc(offset, azimuth, 'azimuth'); }; var drawPitch = function(pitch) { drawArc(200, pitch, 'pitch'); }; var drawRoll = function(roll) { drawArc(350, roll, 'roll'); }; var drawX = function(x) { x = 650 + Math.abs(Math.floor(200 * x / 20)); drawSegment(880, 300, 650, x, 300, 300, 'x'); }; var drawY = function(y) { y = 300 - Math.abs(Math.floor(170 * y / 20)); drawSegment(650, 100, 650, 650, 300, y, 'y'); }; var drawZ = function(z) { var x = 650 - Math.abs(Math.floor(130 * z / 20)); var y = 300 + Math.abs(Math.floor(100 * z / 20)); drawSegment(500, 410, 650, x, 300, y, 'z'); }; python-ws4py-0.3.4/requirements/000077500000000000000000000000001231577073400166575ustar00rootroot00000000000000python-ws4py-0.3.4/requirements/py2kreqs.txt000066400000000000000000000001761231577073400212040ustar00rootroot00000000000000CherryPy==3.2.4 tornado==3.1 greenlet==0.4.1 mock==1.0.1 Cython==0.19.1 -e git+git://github.com/surfly/gevent.git#egg=Package python-ws4py-0.3.4/requirements/py3kreqs.txt000066400000000000000000000000511231577073400211750ustar00rootroot00000000000000CherryPy==3.2.4 tornado==3.1 mock==1.0.1 python-ws4py-0.3.4/requirements/rtd.txt000066400000000000000000000002311231577073400202050ustar00rootroot00000000000000https://bitbucket.org/cherrypy/cherrypy/downloads/CherryPy-3.2.3.tar.gz Sphinx==1.1.3 funcparserlib==0.3.5 sphinxcontrib-seqdiag sphinxcontrib-blockdiag python-ws4py-0.3.4/setup.py000066400000000000000000000051131231577073400156460ustar00rootroot00000000000000# -*- coding: utf-8 -*- import os, os.path from glob import iglob import sys try: from setuptools import setup except ImportError: from distutils.core import setup from distutils.command.build_py import build_py class buildfor2or3(build_py): def find_package_modules(self, package, package_dir): """ Lookup modules to be built before install. Because we only use a single source distribution for Python 2 and 3, we want to avoid specific modules to be built and deployed on Python 2.x. By overriding this method, we filter out those modules before distutils process them. This is in reference to issue #123. """ modules = build_py.find_package_modules(self, package, package_dir) amended_modules = [] for (package_, module, module_file) in modules: if sys.version_info < (3,): if module in ['async_websocket', 'tulipserver']: continue amended_modules.append((package_, module, module_file)) return amended_modules setup(name = "ws4py", version = '0.3.4', description = "WebSocket client and server library for Python 2 and 3 as well as PyPy", maintainer = "Sylvain Hellegouarch", maintainer_email = "sh@defuze.org", url = "https://github.com/Lawouach/WebSocket-for-Python", download_url = "https://pypi.python.org/pypi/ws4py", packages = ['ws4py', 'ws4py.client', 'ws4py.server'], platforms = ["any"], license = 'BSD', long_description = "WebSocket client and server library for Python 2 and 3 as well as PyPy", classifiers=[ 'Development Status :: 5 - Production/Stable', 'Framework :: CherryPy', 'Intended Audience :: Developers', 'License :: OSI Approved :: BSD License', 'Operating System :: OS Independent', 'Programming Language :: Python', 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: PyPy', 'Topic :: Communications', 'Topic :: Internet :: WWW/HTTP :: WSGI :: Middleware', 'Topic :: Internet :: WWW/HTTP :: WSGI :: Server', 'Topic :: Software Development :: Libraries :: Python Modules' ], cmdclass=dict(build_py=buildfor2or3) ) python-ws4py-0.3.4/test/000077500000000000000000000000001231577073400151135ustar00rootroot00000000000000python-ws4py-0.3.4/test/__init__.py000066400000000000000000000000001231577073400172120ustar00rootroot00000000000000python-ws4py-0.3.4/test/autobahn_test_servers.py000066400000000000000000000171511231577073400221030ustar00rootroot00000000000000# -*- coding: utf-8 -*- import logging def run_cherrypy_server(host="127.0.0.1", port=9008): """ Runs a CherryPy server on Python 2.x. """ import cherrypy from ws4py.server.cherrypyserver import WebSocketPlugin, WebSocketTool from ws4py.websocket import EchoWebSocket cherrypy.config.update({'server.socket_host': host, 'server.socket_port': port, 'engine.autoreload.on': False, 'log.screen': False}) WebSocketPlugin(cherrypy.engine).subscribe() cherrypy.tools.websocket = WebSocketTool() class Root(object): @cherrypy.expose def index(self): pass config = { '/': { 'tools.encode.on': False, 'tools.websocket.on': True, 'tools.websocket.handler_cls': EchoWebSocket } } logger = logging.getLogger('autobahn_testsuite') logger.warning("Serving CherryPy server on %s:%s" % (host, port)) cherrypy.quickstart(Root(), '/', config) def run_cherrypy_server_with_wsaccel(host="127.0.0.1", port=9006): """ Runs a CherryPy server on Python 2.x with a cython driver for some internal operations. """ import wsaccel wsaccel.patch_ws4py() run_cherrypy_server(host, port) def run_cherrypy_server_with_python3(host="127.0.0.1", port=9004): """ Runs a CherryPy server on Python 3.x """ run_cherrypy_server(host, port) def run_cherrypy_server_with_pypy(host="127.0.0.1", port=9005): """ Runs a CherryPy server on PyPy """ run_cherrypy_server(host, port) def run_gevent_server(host="127.0.0.1", port=9001): """ Runs a gevent server on Python 2.x """ from gevent import monkey; monkey.patch_all() from ws4py.websocket import EchoWebSocket from ws4py.server.geventserver import WebSocketWSGIApplication, WSGIServer server = WSGIServer((host, port), WebSocketWSGIApplication(handler_cls=EchoWebSocket)) logger = logging.getLogger('autobahn_testsuite') logger.warning("Serving gevent server on %s:%s" % (host, port)) server.serve_forever() def run_python3_asyncio(host="127.0.0.1", port=9009): """ Runs a server using asyncio and Python 3.3+ """ import asyncio from ws4py.async_websocket import EchoWebSocket from ws4py.server.tulipserver import WebSocketProtocol loop = asyncio.get_event_loop() def start_server(): proto_factory = lambda: WebSocketProtocol(EchoWebSocket) return loop.create_server(proto_factory, host, port) s = loop.run_until_complete(start_server()) logger = logging.getLogger('asyncio_testsuite') logger.warning("Serving asyncio server on %s:%s" % s.sockets[0].getsockname()) loop.run_forever() def run_tornado_server(host="127.0.0.1", port=9007): """ Runs a Tornado server on Python 2.x """ from tornado import ioloop, web, websocket class EchoWebSocket(websocket.WebSocketHandler): def on_message(self, message): self.write_message(message) app = web.Application([(r"/", EchoWebSocket)]) app.listen(port, address=host) logger = logging.getLogger('autobahn_testsuite') logger.warning("Serving Tornado server on %s:%s" % (host, port)) ioloop.IOLoop.instance().start() def run_autobahn_server(host="127.0.0.1", port=9003): """ Runs a Autobahn server on Python 2.x """ from twisted.internet import reactor from autobahn.twisted.websocket import WebSocketServerProtocol, \ WebSocketServerFactory class MyServerProtocol(WebSocketServerProtocol): def onMessage(self, payload, isBinary): self.sendMessage(payload, isBinary) logger = logging.getLogger('autobahn_testsuite') logger.warning("Serving Autobahn server on %s:%s" % (host, port)) factory = WebSocketServerFactory("ws://%s:%d" % (host, port)) factory.protocol = MyServerProtocol reactor.listenTCP(port, factory) reactor.run() if __name__ == '__main__': import argparse from multiprocessing import Process logging.basicConfig(format='%(asctime)s %(message)s') logger = logging.getLogger('autobahn_testsuite') logger.setLevel(logging.WARNING) parser = argparse.ArgumentParser() parser.add_argument('--run-all', dest='run_all', action='store_true', help='Run all servers backend') parser.add_argument('--run-cherrypy-server', dest='run_cherrypy', action='store_true', help='Run the CherryPy server backend') parser.add_argument('--run-cherrypy-server-wsaccel', dest='run_cherrypy_wsaccel', action='store_true', help='Run the CherryPy server backend and wsaccel driver') parser.add_argument('--run-cherrypy-server-pypy', dest='run_cherrypy_pypy', action='store_true', help='Run the CherryPy server backend with PyPy') parser.add_argument('--run-cherrypy-server-py3k', dest='run_cherrypy_py3k', action='store_true', help='Run the CherryPy server backend with Python 3') parser.add_argument('--run-gevent-server', dest='run_gevent', action='store_true', help='Run the gevent server backend') parser.add_argument('--run-tornado-server', dest='run_tornado', action='store_true', help='Run the Tornado server backend') parser.add_argument('--run-autobahn-server', dest='run_autobahn', action='store_true', help='Run the Autobahn server backend') parser.add_argument('--run-asyncio-server', dest='run_asyncio', action='store_true', help='Run the asyncio server backend') args = parser.parse_args() if args.run_all: args.run_cherrypy = True args.run_cherrypy_wsaccel = True args.run_gevent = True args.run_tornado = True args.run_autobahn = True args.run_asyncio = True procs = [] logger.warning("CherryPy server: %s" % args.run_cherrypy) if args.run_cherrypy: p0 = Process(target=run_cherrypy_server) p0.daemon = True procs.append(p0) logger.warning("Gevent server: %s" % args.run_gevent) if args.run_gevent: p1 = Process(target=run_gevent_server) p1.daemon = True procs.append(p1) logger.warning("Tornado server: %s" % args.run_tornado) if args.run_tornado: p2 = Process(target=run_tornado_server) p2.daemon = True procs.append(p2) logger.warning("Autobahn server: %s" % args.run_autobahn) if args.run_autobahn: p3 = Process(target=run_autobahn_server) p3.daemon = True procs.append(p3) logger.warning("CherryPy server on PyPy: %s" % args.run_cherrypy_pypy) if args.run_cherrypy_pypy: p4 = Process(target=run_cherrypy_server_with_pypy) p4.daemon = True procs.append(p4) logger.warning("CherryPy server on Python 3: %s" % args.run_cherrypy_py3k) if args.run_cherrypy_py3k: p5 = Process(target=run_cherrypy_server_with_python3) p5.daemon = True procs.append(p5) logger.warning("CherryPy server on Python 2/wsaccel: %s" % args.run_cherrypy_wsaccel) if args.run_cherrypy_wsaccel: p6 = Process(target=run_cherrypy_server_with_wsaccel) p6.daemon = True procs.append(p6) logger.warning("asyncio server on Python 3: %s" % args.run_asyncio) if args.run_asyncio: p7 = Process(target=run_python3_asyncio) p7.daemon = True procs.append(p7) for p in procs: p.start() logging.info("Starting process... %d" % p.pid) for p in procs: p.join() python-ws4py-0.3.4/test/fuzzingclient.json000066400000000000000000000030071231577073400207010ustar00rootroot00000000000000{ "options": {"failByDrop": false}, "outdir": "./reports/servers", "servers": [ {"agent": "ws4py (CherryPy 3.2.5) for Python 2.7.5", "url": "ws://127.0.0.1:9008/", "options": {"version": 18}}, {"agent": "ws4py (CherryPy 3.2.5)/wsaccel for Python 2.7.5", "url": "ws://127.0.0.1:9006/", "options": {"version": 18}}, {"agent": "ws4py (CherryPy 3.2.5) for Python 3.3.3", "url": "ws://127.0.0.1:9004/", "options": {"version": 18}}, {"agent": "ws4py (CherryPy 3.2.5) for PyPy 2.2.1", "url": "ws://127.0.0.1:9005/", "options": {"version": 18}}, {"agent": "ws4py (gevent 1.0.0)", "url": "ws://127.0.0.1:9001/", "options": {"version": 18}}, {"agent": "Tornado 3.2 for Python 2.7.5", "url": "ws://127.0.0.1:9007/", "options": {"version": 18}}, {"agent": "Autobahn 0.8.6 for Python 2.7.5", "url": "ws://127.0.0.1:9003/", "options": {"version": 18}}, {"agent": "ws4py (asyncio 0.2.1) for Python 3.3.3", "url": "ws://127.0.0.1:9009/", "options": {"version": 18}} ], "cases": ["*"], "exclude-cases": ["6.4.3", "6.4.4", "12.*", "13.*"], "exclude-agent-cases": {} } python-ws4py-0.3.4/test/test_cherrypy.py000066400000000000000000000055711231577073400204010ustar00rootroot00000000000000# -*- coding: utf-8 -*- import os import time import unittest from mock import MagicMock, call import cherrypy from ws4py.server.cherrypyserver import WebSocketPlugin, WebSocketTool from ws4py.websocket import EchoWebSocket from ws4py.framing import Frame, OPCODE_TEXT, OPCODE_CLOSE class FakePoller(object): def __init__(self, timeout=0.1): self._fds = [] def release(self): self._fds = [] def register(self, fd): if fd not in self._fds: self._fds.append(fd) def unregister(self, fd): if fd in self._fds: self._fds.remove(fd) def poll(self): return self._fds class App(object): @cherrypy.expose def ws(self): assert cherrypy.request.ws_handler != None def setup_engine(): # we don't need a HTTP server for this test cherrypy.server.unsubscribe() cherrypy.config.update({'log.screen': False}) cherrypy.engine.websocket = WebSocketPlugin(cherrypy.engine) cherrypy.engine.websocket.subscribe() cherrypy.engine.websocket.manager.poller = FakePoller() cherrypy.tools.websocket = WebSocketTool() config={'/ws': {'tools.websocket.on': True, 'tools.websocket.handler_cls': EchoWebSocket}} cherrypy.tree.mount(App(), '/', config) cherrypy.engine.start() def teardown_engine(): cherrypy.engine.exit() class CherryPyTest(unittest.TestCase): def setUp(self): setup_engine() def tearDown(self): teardown_engine() def test_plugin(self): manager = cherrypy.engine.websocket.manager self.assertEqual(len(manager), 0) s = MagicMock() s.recv.return_value = Frame(opcode=OPCODE_TEXT, body=b'hello', fin=1, masking_key=os.urandom(4)).build() h = EchoWebSocket(s, [], []) cherrypy.engine.publish('handle-websocket', h, ('127.0.0.1', 0)) self.assertEqual(len(manager), 1) self.assertTrue(h in manager) # the following call to .close() on the # websocket object will initiate # the closing handshake # This next line mocks the response # from the client to actually # complete the handshake. # The manager will then remove the websocket # from its pool s.recv.return_value = Frame(opcode=OPCODE_CLOSE, body=b"ok we're done", fin=1, masking_key=os.urandom(4)).build() h.close() # the poller runs a thread, give it time to get there time.sleep(1) # TODO: Implement a fake poller so that works... self.assertEqual(len(manager), 0) if __name__ == '__main__': suite = unittest.TestSuite() loader = unittest.TestLoader() for testcase in [CherryPyTest]: tests = loader.loadTestsFromTestCase(testcase) suite.addTests(tests) unittest.TextTestRunner(verbosity=2).run(suite) python-ws4py-0.3.4/test/test_client.py000066400000000000000000000014561231577073400200100ustar00rootroot00000000000000# -*- coding: utf-8 -*- import time import unittest from mock import MagicMock, call, patch from ws4py.manager import WebSocketManager from ws4py.websocket import WebSocket from ws4py.client import WebSocketBaseClient class BasicClientTest(unittest.TestCase): def test_invalid_hostname_in_url(self): self.assertRaises(ValueError, WebSocketBaseClient, url="qsdfqsd65qsd354") def test_invalid_scheme_in_url(self): self.assertRaises(ValueError, WebSocketBaseClient, url="ftp://localhost") if __name__ == '__main__': suite = unittest.TestSuite() loader = unittest.TestLoader() for testcase in [BasicClientTest]: tests = loader.loadTestsFromTestCase(testcase) suite.addTests(tests) unittest.TextTestRunner(verbosity=2).run(suite) python-ws4py-0.3.4/test/test_frame.py000066400000000000000000000231541231577073400176230ustar00rootroot00000000000000# -*- coding: utf-8 -*- import os import unittest import types import random from ws4py.framing import Frame, \ OPCODE_CONTINUATION, OPCODE_TEXT, \ OPCODE_BINARY, OPCODE_CLOSE, OPCODE_PING, OPCODE_PONG from ws4py.exc import FrameTooLargeException, ProtocolException from ws4py.compat import * def map_on_bytes(f, bytes): for index, byte in enumerate(bytes): f(bytes[index:index+1]) class WSFrameBuilderTest(unittest.TestCase): def test_7_bit_length(self): f = Frame(opcode=OPCODE_TEXT, body=b'', fin=1) self.assertEqual(len(f.build()), 2) f = Frame(opcode=OPCODE_TEXT, body=b'*' * 125, fin=1) self.assertEqual(len(f.build()), 127) mask = os.urandom(4) f = Frame(opcode=OPCODE_TEXT, body=b'', masking_key=mask, fin=1) self.assertEqual(len(f.build()), 6) f = Frame(opcode=OPCODE_TEXT, body=b'*' * 125, masking_key=mask, fin=1) self.assertEqual(len(f.build()), 131) def test_16_bit_length(self): f = Frame(opcode=OPCODE_TEXT, body=b'*' * 126, fin=1) self.assertEqual(len(f.build()), 130) f = Frame(opcode=OPCODE_TEXT, body=b'*' * 65535, fin=1) self.assertEqual(len(f.build()), 65539) mask = os.urandom(4) f = Frame(opcode=OPCODE_TEXT, body=b'*' * 126, masking_key=mask, fin=1) self.assertEqual(len(f.build()), 134) f = Frame(opcode=OPCODE_TEXT, body=b'*' * 65535, masking_key=mask, fin=1) self.assertEqual(len(f.build()), 65543) def test_63_bit_length(self): f = Frame(opcode=OPCODE_TEXT, body=b'*' * 65536, fin=1) self.assertEqual(len(f.build()), 65546) mask = os.urandom(4) f = Frame(opcode=OPCODE_TEXT, body=b'*' * 65536, masking_key=mask, fin=1) self.assertEqual(len(f.build()), 65550) def test_non_zero_nor_one_fin(self): f = Frame(opcode=OPCODE_TEXT, body=b'', fin=2) self.assertRaises(ValueError, f.build) def test_opcodes(self): for opcode in [OPCODE_CONTINUATION, OPCODE_TEXT, OPCODE_BINARY, OPCODE_CLOSE, OPCODE_PING, OPCODE_PONG]: f = Frame(opcode=opcode, body=b'', fin=1) byte = ord(f.build()[0]) self.assertTrue(byte & opcode == opcode) f = Frame(opcode=0x3, body=b'', fin=1) self.assertRaises(ValueError, f.build) def test_masking(self): if py3k: mask = b"7\xfa!=" else: mask = "7\xfa!=" f = Frame(opcode=OPCODE_TEXT, body=b'Hello', masking_key=mask, fin=1) if py3k: spec_says = b'\x81\x857\xfa!=\x7f\x9fMQX' else: spec_says = '\x81\x857\xfa!=\x7f\x9fMQX' self.assertEqual(f.build(), spec_says) def test_frame_too_large(self): f = Frame(opcode=OPCODE_TEXT, body=b'', fin=1) # fake huge length f.payload_length = 1 << 63 self.assertRaises(FrameTooLargeException, f.build) def test_passing_encoded_string(self): # once encoded the u'\xe9' character will be of length 2 f = Frame(opcode=OPCODE_TEXT, body=u'\xe9trange'.encode('utf-8'), fin=1) self.assertEqual(len(f.build()), 10) def test_passing_unencoded_string_raises_type_error(self): self.assertRaises(TypeError, Frame, opcode=OPCODE_TEXT, body=u'\xe9', fin=1) class WSFrameParserTest(unittest.TestCase): def test_frame_parser_is_a_generator(self): f = Frame() self.assertEqual(type(f.parser), types.GeneratorType) f.parser.close() self.assertRaises(StopIteration, next, f.parser) def test_frame_header_parsing(self): bytes = Frame(opcode=OPCODE_TEXT, body=b'hello', fin=1).build() f = Frame() self.assertEqual(f.parser.send(bytes[0:1]), 1) self.assertEqual(f.fin, 1) self.assertEqual(f.rsv1, 0) self.assertEqual(f.rsv2, 0) self.assertEqual(f.rsv3, 0) self.assertEqual(f.parser.send(bytes[1:2]), 5) self.assertTrue(f.masking_key is None) self.assertEqual(f.payload_length, 5) f.parser.close() def test_frame_payload_parsing(self): bytes = Frame(opcode=OPCODE_TEXT, body=b'hello', fin=1).build() f = Frame() self.assertEqual(f.parser.send(bytes[0:1]), 1) self.assertEqual(f.parser.send(bytes[1:2]), 5) f.parser.send(bytes[2:]) self.assertEqual(f.body, b'hello') f = Frame() f.parser.send(bytes) self.assertRaises(StopIteration, next, f.parser) self.assertEqual(f.body, b'hello') def test_incremental_parsing_small_7_bit_length(self): bytes = Frame(opcode=OPCODE_TEXT, body=b'hello', fin=1).build() f = Frame() map_on_bytes(f.parser.send, bytes) self.assertTrue(f.masking_key is None) self.assertEqual(f.payload_length, 5) def test_incremental_parsing_16_bit_length(self): bytes = Frame(opcode=OPCODE_TEXT, body=b'*' * 126, fin=1).build() f = Frame() map_on_bytes(f.parser.send, bytes) self.assertTrue(f.masking_key is None) self.assertEqual(f.payload_length, 126) def test_incremental_parsing_63_bit_length(self): bytes = Frame(opcode=OPCODE_TEXT, body=b'*' * 65536, fin=1).build() f = Frame() map_on_bytes(f.parser.send, bytes) self.assertTrue(f.masking_key is None) self.assertEqual(f.payload_length, 65536) def test_rsv1_bits_set(self): f = Frame() self.assertRaises(ProtocolException, f.parser.send, b'\x40') def test_rsv2_bits_set(self): f = Frame() self.assertRaises(ProtocolException, f.parser.send, b'\x20') def test_rsv3_bits_set(self): f = Frame() self.assertRaises(ProtocolException, f.parser.send, b'\x10') def test_invalid_opcode(self): for opcode in range(3, 9): f = Frame() self.assertRaises(ProtocolException, f.parser.send, chr(opcode)) f = Frame() self.assertRaises(ProtocolException, f.parser.send, chr(10)) def test_fragmented_control_frame_is_invalid(self): f = Frame() self.assertRaises(ProtocolException, f.parser.send, b'0x9') def test_fragmented_control_frame_is_too_large(self): bytes = Frame(opcode=OPCODE_PING, body=b'*'*65536, fin=1).build() f = Frame() self.assertRaises(FrameTooLargeException, f.parser.send, bytes) def test_frame_sized_127(self): body = b'*'*65536 bytes = Frame(opcode=OPCODE_TEXT, body=body, fin=1).build() f = Frame() # determine how the size is stored f.parser.send(bytes[:3]) self.assertTrue(f.masking_key is None) # that's a large frame indeed self.assertEqual(f.payload_length, 127) # this will compute the actual application data size # it will also read the first byte of data # indeed the length is found from byte 3 to 10 f.parser.send(bytes[3:11]) self.assertEqual(f.payload_length, 65536) # parse the rest of our data f.parser.send(bytes[11:]) self.assertEqual(f.body, body) # The same but this time we provide enough # bytes so that the application's data length # can be computed from the first generator's send call f = Frame() f.parser.send(bytes[:10]) self.assertTrue(f.masking_key is None) self.assertEqual(f.payload_length, 65536) # parse the rest of our data f.parser.send(bytes[10:]) self.assertEqual(f.body, body) # The same with masking given out gradually mask = os.urandom(4) bytes = Frame(opcode=OPCODE_TEXT, body=body, fin=1, masking_key=mask).build() f = Frame() f.parser.send(bytes[:10]) self.assertTrue(f.masking_key is None) self.assertEqual(f.payload_length, 65536) # parse the mask gradually f.parser.send(bytes[10:12]) f.parser.send(bytes[12:]) self.assertEqual(f.unmask(f.body), body) def test_frame_sized_126(self): body = b'*'*256 bytes = Frame(opcode=OPCODE_TEXT, body=body, fin=1).build() f = Frame() # determine how the size is stored f.parser.send(bytes[:3]) self.assertTrue(f.masking_key is None) # that's a large frame indeed self.assertEqual(f.payload_length, 126) # this will compute the actual application data size # it will also read the first byte of data # indeed the length is found from byte 3 to 10 f.parser.send(bytes[3:11]) self.assertEqual(f.payload_length, 256) # parse the rest of our data f.parser.send(bytes[11:]) self.assertEqual(f.body, body) # The same but this time we provide enough # bytes so that the application's data length # can be computed from the first generator's send call f = Frame() f.parser.send(bytes[:10]) self.assertTrue(f.masking_key is None) self.assertEqual(f.payload_length, 256) # parse the rest of our data f.parser.send(bytes[10:]) self.assertEqual(f.body, body) if __name__ == '__main__': suite = unittest.TestSuite() loader = unittest.TestLoader() for testcase in (WSFrameBuilderTest, WSFrameParserTest,): tests = loader.loadTestsFromTestCase(testcase) suite.addTests(tests) unittest.TextTestRunner(verbosity=2).run(suite) python-ws4py-0.3.4/test/test_manager.py000066400000000000000000000103461231577073400201420ustar00rootroot00000000000000# -*- coding: utf-8 -*- import time import unittest from mock import MagicMock, call, patch from ws4py.manager import WebSocketManager from ws4py.websocket import WebSocket class WSManagerTest(unittest.TestCase): @patch('ws4py.manager.SelectPoller') def test_add_and_remove_websocket(self, MockSelectPoller): m = WebSocketManager(poller=MockSelectPoller()) ws = MagicMock() ws.sock.fileno.return_value = 1 m.add(ws) m.poller.register.assert_call_once_with(ws) m.remove(ws) m.poller.unregister.assert_call_once_with(ws) @patch('ws4py.manager.SelectPoller') def test_cannot_add_websocket_more_than_once(self, MockSelectPoller): m = WebSocketManager(poller=MockSelectPoller()) ws = MagicMock() ws.sock.fileno.return_value = 1 m.add(ws) self.assertEqual(len(m), 1) m.add(ws) self.assertEqual(len(m), 1) @patch('ws4py.manager.SelectPoller') def test_cannot_remove_unregistered_websocket(self, MockSelectPoller): m = WebSocketManager(poller=MockSelectPoller()) ws = MagicMock() ws.sock.fileno.return_value = 1 m.remove(ws) self.assertEqual(len(m), 0) self.assertFalse(m.poller.unregister.called) m.add(ws) self.assertEqual(len(m), 1) m.remove(ws) self.assertEqual(len(m), 0) m.poller.unregister.assert_call_once_with(ws) m.poller.reset_mock() m.remove(ws) self.assertEqual(len(m), 0) self.assertFalse(m.poller.unregister.called) @patch('ws4py.manager.SelectPoller') def test_mainloop_can_be_stopped_when_no_websocket_were_registered(self, MockSelectPoller): m = WebSocketManager(poller=MockSelectPoller()) self.assertFalse(m.running) m.start() self.assertTrue(m.running) m.stop() self.assertFalse(m.running) @patch('ws4py.manager.SelectPoller') def test_mainloop_can_be_stopped(self, MockSelectPoller): m = WebSocketManager(poller=MockSelectPoller()) def poll(): yield 1 m.stop() yield 2 m.poller.poll.return_value = poll() self.assertFalse(m.running) m.start() # just make sure it had the time to finish time.sleep(0.1) self.assertFalse(m.running) @patch('ws4py.manager.SelectPoller') def test_websocket_terminated_from_mainloop(self, MockSelectPoller): m = WebSocketManager(poller=MockSelectPoller()) m.poller.poll.return_value = [1] ws = MagicMock() ws.terminated = False ws.sock.fileno.return_value = 1 ws.once.return_value = False m.add(ws) m.start() ws.terminate.assert_call_once_with() m.stop() @patch('ws4py.manager.SelectPoller') def test_websocket_close_all(self, MockSelectPoller): m = WebSocketManager(poller=MockSelectPoller()) ws = MagicMock() m.add(ws) m.close_all() ws.terminate.assert_call_once_with(1001, 'Server is shutting down') @patch('ws4py.manager.SelectPoller') def test_broadcast(self, MockSelectPoller): m = WebSocketManager(poller=MockSelectPoller()) ws = MagicMock() ws.terminated = False m.add(ws) m.broadcast(b'hello there') ws.send.assert_call_once_with(b'hello there') @patch('ws4py.manager.SelectPoller') def test_broadcast_failure_must_not_break_caller(self, MockSelectPoller): m = WebSocketManager(poller=MockSelectPoller()) ws = MagicMock() ws.terminated = False ws.send.side_effect = RuntimeError m.add(ws) try: m.broadcast(b'hello there') except: self.fail("Broadcasting shouldn't have failed") if __name__ == '__main__': suite = unittest.TestSuite() loader = unittest.TestLoader() for testcase in [WSManagerTest]: tests = loader.loadTestsFromTestCase(testcase) suite.addTests(tests) unittest.TextTestRunner(verbosity=2).run(suite) python-ws4py-0.3.4/test/test_messaging.py000066400000000000000000000113501231577073400205010ustar00rootroot00000000000000# -*- coding: utf-8 -*- import os import unittest from ws4py.framing import Frame, \ OPCODE_CONTINUATION, OPCODE_TEXT, \ OPCODE_BINARY, OPCODE_CLOSE, OPCODE_PING, OPCODE_PONG from ws4py.messaging import * from ws4py.compat import * class WSMessagingTest(unittest.TestCase): def test_bytearray_text_message(self): m = TextMessage(bytearray(u'\xe9trange', 'utf-8')) self.assertFalse(m.is_binary) self.assertTrue(m.is_text) self.assertEqual(m.opcode, OPCODE_TEXT) self.assertEqual(m.encoding, 'utf-8') self.assertIsInstance(m.data, bytes) # length is compted on the unicode representation self.assertEqual(len(m), 7) # but once encoded it's actually taking 8 bytes in UTF-8 self.assertEqual(len(m.data), 8) self.assertEqual(m.data, u'\xe9trange'.encode('utf-8')) f = m.single() self.assertIsInstance(f, bytes) self.assertEqual(len(f), 10) # no masking f = m.single(mask=True) self.assertIsInstance(f, bytes) self.assertEqual(len(f), 14) # mask takes 4 bytes self.assertEqual(m.fragment(first=True, last=True), m.single()) m.extend(bytearray(' oui', 'utf-8')) self.assertEqual(m.data, u'\xe9trange oui'.encode('utf-8')) def test_bytes_text_message(self): m = TextMessage(u'\xe9trange'.encode('utf-8')) self.assertEqual(m.opcode, OPCODE_TEXT) self.assertEqual(m.encoding, 'utf-8') self.assertIsInstance(m.data, bytes) self.assertFalse(m.is_binary) self.assertTrue(m.is_text) # length is compted on the unicode representation self.assertEqual(len(m), 7) # but once encoded it's actually taking 8 bytes in UTF-8 self.assertEqual(len(m.data), 8) self.assertEqual(m.data, u'\xe9trange'.encode('utf-8')) f = m.single() self.assertIsInstance(f, bytes) self.assertEqual(len(f), 10) # no masking f = m.single(mask=True) self.assertIsInstance(f, bytes) self.assertEqual(len(f), 14) # mask takes 4 bytes self.assertEqual(m.fragment(first=True, last=True), m.single()) m.extend(b' oui') self.assertEqual(m.data, u'\xe9trange oui'.encode('utf-8')) def test_unicode_text_message(self): m = TextMessage(u'\xe9trange') self.assertEqual(m.opcode, OPCODE_TEXT) self.assertEqual(m.encoding, 'utf-8') self.assertIsInstance(m.data, bytes) self.assertFalse(m.is_binary) self.assertTrue(m.is_text) # length is compted on the unicode representation self.assertEqual(len(m), 7) # but once encoded it's actually taking 8 bytes in UTF-8 self.assertEqual(len(m.data), 8) self.assertEqual(m.data, u'\xe9trange'.encode('utf-8')) f = m.single() self.assertIsInstance(f, bytes) self.assertEqual(len(f), 10) # no masking f = m.single(mask=True) self.assertIsInstance(f, bytes) self.assertEqual(len(f), 14) # mask takes 4 bytes self.assertEqual(m.fragment(first=True, last=True), m.single()) m.extend(u' oui') self.assertEqual(m.data, u'\xe9trange oui'.encode('utf-8')) def test_unicode_text_message_with_no_encoding(self): self.assertRaises(TypeError, Message, OPCODE_TEXT, u'\xe9trange', encoding=None) def test_invalid_text_message_data_type(self): self.assertRaises(TypeError, TextMessage, ['something else']) m = TextMessage(u'\xe9trange') self.assertRaises(TypeError, m.extend, ["list aren't supported types"]) def test_close_control_message(self): m = CloseControlMessage(reason=u'bye bye') self.assertEqual(m.opcode, OPCODE_CLOSE) self.assertEqual(m.encoding, 'utf-8') self.assertIsInstance(m.reason, bytes) self.assertEqual(len(m), 7) self.assertEqual(m.code, 1000) self.assertEqual(m.reason, b'bye bye') def test_ping_control_message(self): m = PingControlMessage(data=u'are you there?') self.assertEqual(m.opcode, OPCODE_PING) self.assertEqual(m.encoding, 'utf-8') self.assertIsInstance(m.data, bytes) self.assertEqual(len(m), 14) def test_pong_control_message(self): m = PongControlMessage(data=u'yes, I am') self.assertEqual(m.opcode, OPCODE_PONG) self.assertEqual(m.encoding, 'utf-8') self.assertIsInstance(m.data, bytes) self.assertEqual(len(m), 9) if __name__ == '__main__': suite = unittest.TestSuite() loader = unittest.TestLoader() for testcase in [WSMessagingTest]: tests = loader.loadTestsFromTestCase(testcase) suite.addTests(tests) unittest.TextTestRunner(verbosity=2).run(suite) python-ws4py-0.3.4/test/test_stream.py000066400000000000000000000363301231577073400200240ustar00rootroot00000000000000# -*- coding: utf-8 -*- import unittest import os import struct from ws4py.framing import Frame, \ OPCODE_CONTINUATION, OPCODE_TEXT, \ OPCODE_BINARY, OPCODE_CLOSE, OPCODE_PING, OPCODE_PONG from ws4py.streaming import Stream from ws4py.messaging import TextMessage, BinaryMessage, \ CloseControlMessage, PingControlMessage, PongControlMessage from ws4py.compat import * class WSStreamTest(unittest.TestCase): def test_empty_close_message(self): f = Frame(opcode=OPCODE_CLOSE, body=b'', fin=1, masking_key=os.urandom(4)).build() s = Stream() self.assertEqual(s.closing, None) s.parser.send(f) self.assertEqual(type(s.closing), CloseControlMessage) def test_missing_masking_key_when_expected(self): f = Frame(opcode=OPCODE_TEXT, body=b'hello', fin=1, masking_key=None).build() s = Stream(expect_masking=True) s.parser.send(f) next(s.parser) self.assertNotEqual(s.errors, []) self.assertIsInstance(s.errors[0], CloseControlMessage) self.assertEqual(s.errors[0].code, 1002) def test_using_masking_key_when_unexpected(self): f = Frame(opcode=OPCODE_TEXT, body=b'hello', fin=1, masking_key=os.urandom(4)).build() s = Stream(expect_masking=False) s.parser.send(f) next(s.parser) self.assertNotEqual(s.errors, []) self.assertIsInstance(s.errors[0], CloseControlMessage) self.assertEqual(s.errors[0].code, 1002) def test_text_messages_cannot_interleave(self): s = Stream() f = Frame(opcode=OPCODE_TEXT, body=b'hello', fin=0, masking_key=os.urandom(4)).build() s.parser.send(f) next(s.parser) f = Frame(opcode=OPCODE_TEXT, body=b'there', fin=1, masking_key=os.urandom(4)).build() s.parser.send(f) next(s.parser) self.assertNotEqual(s.errors, []) self.assertIsInstance(s.errors[0], CloseControlMessage) self.assertEqual(s.errors[0].code, 1002) def test_binary_messages_cannot_interleave(self): s = Stream() f = Frame(opcode=OPCODE_BINARY, body=os.urandom(2), fin=0, masking_key=os.urandom(4)).build() s.parser.send(f) next(s.parser) f = Frame(opcode=OPCODE_BINARY, body=os.urandom(7), fin=1, masking_key=os.urandom(4)).build() s.parser.send(f) next(s.parser) self.assertNotEqual(s.errors, []) self.assertIsInstance(s.errors[0], CloseControlMessage) self.assertEqual(s.errors[0].code, 1002) def test_binary_and_text_messages_cannot_interleave(self): s = Stream() f = Frame(opcode=OPCODE_TEXT, body=b'hello', fin=0, masking_key=os.urandom(4)).build() s.parser.send(f) next(s.parser) f = Frame(opcode=OPCODE_BINARY, body=os.urandom(7), fin=1, masking_key=os.urandom(4)).build() s.parser.send(f) next(s.parser) self.assertNotEqual(s.errors, []) self.assertIsInstance(s.errors[0], CloseControlMessage) self.assertEqual(s.errors[0].code, 1002) def test_continuation_frame_before_message_started_is_invalid(self): f = Frame(opcode=OPCODE_CONTINUATION, body=b'hello', fin=1, masking_key=os.urandom(4)).build() s = Stream() s.parser.send(f) next(s.parser) self.assertNotEqual(s.errors, []) self.assertIsInstance(s.errors[0], CloseControlMessage) self.assertEqual(s.errors[0].code, 1002) def test_invalid_encoded_bytes(self): f = Frame(opcode=OPCODE_TEXT, body=b'h\xc3llo', fin=1, masking_key=os.urandom(4)).build() s = Stream() s.parser.send(f) next(s.parser) self.assertNotEqual(s.errors, []) self.assertIsInstance(s.errors[0], CloseControlMessage) self.assertEqual(s.errors[0].code, 1007) def test_invalid_encoded_bytes_on_continuation(self): s = Stream() f = Frame(opcode=OPCODE_TEXT, body=b'hello', fin=0, masking_key=os.urandom(4)).build() s.parser.send(f) next(s.parser) f = Frame(opcode=OPCODE_CONTINUATION, body=b'h\xc3llo', fin=1, masking_key=os.urandom(4)).build() s.parser.send(f) next(s.parser) self.assertNotEqual(s.errors, []) self.assertIsInstance(s.errors[0], CloseControlMessage) self.assertEqual(s.errors[0].code, 1007) def test_too_large_close_message(self): payload = struct.pack("!H", 1000) + b'*' * 330 f = Frame(opcode=OPCODE_CLOSE, body=payload, fin=1, masking_key=os.urandom(4)).build() s = Stream() self.assertEqual(len(s.errors), 0) self.assertEqual(s.closing, None) s.parser.send(f) self.assertEqual(s.closing, None) self.assertEqual(len(s.errors), 1) self.assertEqual(type(s.errors[0]), CloseControlMessage) self.assertEqual(s.errors[0].code, 1002) def test_invalid_sized_close_message(self): payload = b'boom' f = Frame(opcode=OPCODE_CLOSE, body=payload, fin=1, masking_key=os.urandom(4)).build() s = Stream() self.assertEqual(len(s.errors), 0) self.assertEqual(s.closing, None) s.parser.send(f) self.assertEqual(type(s.closing), CloseControlMessage) self.assertEqual(s.closing.code, 1002) def test_close_message_of_size_one_are_invalid(self): payload = b'*' f = Frame(opcode=OPCODE_CLOSE, body=payload, fin=1, masking_key=os.urandom(4)).build() s = Stream() self.assertEqual(len(s.errors), 0) self.assertEqual(s.closing, None) s.parser.send(f) self.assertEqual(type(s.closing), CloseControlMessage) self.assertEqual(s.closing.code, 1002) def test_invalid_close_message_type(self): payload = struct.pack("!H", 1500) + b'hello' f = Frame(opcode=OPCODE_CLOSE, body=payload, fin=1, masking_key=os.urandom(4)).build() s = Stream() self.assertEqual(len(s.errors), 0) self.assertEqual(s.closing, None) s.parser.send(f) self.assertEqual(type(s.closing), CloseControlMessage) self.assertEqual(s.closing.code, 1002) def test_invalid_close_message_reason_encoding(self): payload = struct.pack("!H", 1000) + b'h\xc3llo' f = Frame(opcode=OPCODE_CLOSE, body=payload, fin=1, masking_key=os.urandom(4)).build() s = Stream() self.assertEqual(len(s.errors), 0) self.assertEqual(s.closing, None) s.parser.send(f) self.assertEqual(s.closing, None) self.assertEqual(type(s.errors[0]), CloseControlMessage) self.assertEqual(s.errors[0].code, 1007) def test_protocol_exception_from_frame_parsing(self): payload = struct.pack("!H", 1000) + b'hello' f = Frame(opcode=OPCODE_CLOSE, body=payload, fin=1, masking_key=os.urandom(4)) f.rsv1 = 1 f = f.build() s = Stream() self.assertEqual(len(s.errors), 0) self.assertEqual(s.closing, None) s.parser.send(f) self.assertEqual(s.closing, None) self.assertEqual(type(s.errors[0]), CloseControlMessage) self.assertEqual(s.errors[0].code, 1002) def test_close_message_received(self): payload = struct.pack("!H", 1000) + b'hello' f = Frame(opcode=OPCODE_CLOSE, body=payload, fin=1, masking_key=os.urandom(4)).build() s = Stream() self.assertEqual(s.closing, None) s.parser.send(f) self.assertEqual(type(s.closing), CloseControlMessage) self.assertEqual(s.closing.code, 1000) self.assertEqual(s.closing.reason, b'hello') def test_ping_message_received(self): msg = b'ping me' f = Frame(opcode=OPCODE_PING, body=msg, fin=1, masking_key=os.urandom(4)).build() s = Stream() self.assertEqual(len(s.pings), 0) s.parser.send(f) self.assertEqual(len(s.pings), 1) def test_pong_message_received(self): msg = b'pong!' f = Frame(opcode=OPCODE_PONG, body=msg, fin=1, masking_key=os.urandom(4)).build() s = Stream() self.assertEqual(len(s.pongs), 0) s.parser.send(f) self.assertEqual(len(s.pongs), 1) def test_text_message_received(self): msg = b'hello there' f = Frame(opcode=OPCODE_TEXT, body=msg, fin=1, masking_key=os.urandom(4)).build() s = Stream() self.assertEqual(len(s.messages), 0) s.parser.send(f) self.assertEqual(len(s.messages), 1) def test_incremental_text_message_received(self): msg = b'hello there' f = Frame(opcode=OPCODE_TEXT, body=msg, fin=1, masking_key=os.urandom(4)).build() s = Stream() self.assertEqual(s.has_message, False) bytes = f for index, byte in enumerate(bytes): s.parser.send(bytes[index:index+1]) self.assertEqual(s.has_message, True) def test_text_message_received(self): msg = b'hello there' f = Frame(opcode=OPCODE_TEXT, body=msg, fin=1, masking_key=os.urandom(4)).build() s = Stream() self.assertEqual(s.has_message, False) s.parser.send(f) self.assertEqual(s.message.completed, True) def test_text_message_with_continuation_received(self): msg = b'hello there' f = Frame(opcode=OPCODE_TEXT, body=msg, fin=0, masking_key=os.urandom(4)).build() s = Stream() self.assertEqual(s.has_message, False) s.parser.send(f) self.assertEqual(s.message.completed, False) for i in range(3): f = Frame(opcode=OPCODE_CONTINUATION, body=msg, fin=0, masking_key=os.urandom(4)).build() s.parser.send(f) self.assertEqual(s.has_message, False) self.assertEqual(s.message.completed, False) self.assertEqual(s.message.opcode, OPCODE_TEXT) f = Frame(opcode=OPCODE_CONTINUATION, body=msg, fin=1, masking_key=os.urandom(4)).build() s.parser.send(f) self.assertEqual(s.has_message, True) self.assertEqual(s.message.completed, True) self.assertEqual(s.message.opcode, OPCODE_TEXT) def test_text_message_with_continuation_and_ping_in_between(self): msg = b'hello there' key = os.urandom(4) f = Frame(opcode=OPCODE_TEXT, body=msg, fin=0, masking_key=os.urandom(4)).build() s = Stream() self.assertEqual(s.has_message, False) s.parser.send(f) self.assertEqual(s.message.completed, False) for i in range(3): f = Frame(opcode=OPCODE_CONTINUATION, body=msg, fin=0, masking_key=os.urandom(4)).build() s.parser.send(f) self.assertEqual(s.has_message, False) self.assertEqual(s.message.completed, False) self.assertEqual(s.message.opcode, OPCODE_TEXT) f = Frame(opcode=OPCODE_PING, body=b'ping me', fin=1, masking_key=os.urandom(4)).build() self.assertEqual(len(s.pings), i) s.parser.send(f) self.assertEqual(len(s.pings), i+1) f = Frame(opcode=OPCODE_CONTINUATION, body=msg, fin=1, masking_key=os.urandom(4)).build() s.parser.send(f) self.assertEqual(s.has_message, True) self.assertEqual(s.message.opcode, OPCODE_TEXT) self.assertEqual(s.message.completed, True) def test_binary_message_received(self): msg = os.urandom(16) f = Frame(opcode=OPCODE_BINARY, body=msg, fin=1, masking_key=os.urandom(4)).build() s = Stream() self.assertEqual(s.has_message, False) s.parser.send(f) self.assertEqual(s.message.completed, True) def test_binary_message_with_continuation_received(self): msg = os.urandom(16) key = os.urandom(4) f = Frame(opcode=OPCODE_BINARY, body=msg, fin=0, masking_key=key).build() s = Stream() self.assertEqual(s.has_message, False) s.parser.send(f) self.assertEqual(s.has_message, False) for i in range(3): f = Frame(opcode=OPCODE_CONTINUATION, body=msg, fin=0, masking_key=key).build() s.parser.send(f) self.assertEqual(s.has_message, False) self.assertEqual(s.message.completed, False) self.assertEqual(s.message.opcode, OPCODE_BINARY) f = Frame(opcode=OPCODE_CONTINUATION, body=msg, fin=1, masking_key=key).build() s.parser.send(f) self.assertEqual(s.has_message, True) self.assertEqual(s.message.completed, True) self.assertEqual(s.message.opcode, OPCODE_BINARY) def test_helper_with_unicode_text_message(self): s = Stream() m = s.text_message(u'hello there!') self.assertIsInstance(m, TextMessage) self.assertFalse(m.is_binary) self.assertTrue(m.is_text) self.assertEqual(m.opcode, OPCODE_TEXT) self.assertEqual(m.encoding, 'utf-8') self.assertIsInstance(m.data, bytes) self.assertEqual(len(m), 12) self.assertEqual(len(m.data), 12) self.assertEqual(m.data, b'hello there!') def test_helper_with_bytes_text_message(self): s = Stream() m = s.text_message('hello there!') self.assertIsInstance(m, TextMessage) self.assertFalse(m.is_binary) self.assertTrue(m.is_text) self.assertEqual(m.opcode, OPCODE_TEXT) self.assertEqual(m.encoding, 'utf-8') self.assertIsInstance(m.data, bytes) self.assertEqual(len(m), 12) self.assertEqual(len(m.data), 12) self.assertEqual(m.data, b'hello there!') def test_helper_with_binary_message(self): msg = os.urandom(16) s = Stream() m = s.binary_message(msg) self.assertIsInstance(m, BinaryMessage) self.assertTrue(m.is_binary) self.assertFalse(m.is_text) self.assertEqual(m.opcode, OPCODE_BINARY) self.assertIsInstance(m.data, bytes) self.assertEqual(len(m), 16) self.assertEqual(len(m.data), 16) self.assertEqual(m.data, msg) def test_helper_ping_message(self): s = Stream() m = s.ping('sos') self.assertIsInstance(m, bytes) self.assertEqual(len(m), 5) def test_helper_masked_ping_message(self): s = Stream(always_mask=True) m = s.ping('sos') self.assertIsInstance(m, bytes) self.assertEqual(len(m), 9) def test_helper_pong_message(self): s = Stream() m = s.pong('sos') self.assertIsInstance(m, bytes) self.assertEqual(len(m), 5) def test_helper_masked_pong_message(self): s = Stream(always_mask=True) m = s.pong('sos') self.assertIsInstance(m, bytes) self.assertEqual(len(m), 9) def test_closing_parser_should_release_resources(self): f = Frame(opcode=OPCODE_TEXT, body=b'hello', fin=1, masking_key=os.urandom(4)).build() s = Stream() s.parser.send(f) s.parser.close() if __name__ == '__main__': suite = unittest.TestSuite() loader = unittest.TestLoader() for testcase in [WSStreamTest]: tests = loader.loadTestsFromTestCase(testcase) suite.addTests(tests) unittest.TextTestRunner(verbosity=2).run(suite) python-ws4py-0.3.4/test/test_websocket.py000066400000000000000000000134461231577073400205220ustar00rootroot00000000000000# -*- coding: utf-8 -*- import unittest import os import socket import struct from mock import MagicMock, call, patch from ws4py.framing import Frame, \ OPCODE_CONTINUATION, OPCODE_TEXT, \ OPCODE_BINARY, OPCODE_CLOSE, OPCODE_PING, OPCODE_PONG from ws4py.websocket import WebSocket from ws4py.messaging import TextMessage, BinaryMessage, \ CloseControlMessage, PingControlMessage, PongControlMessage from ws4py.compat import * class WSWebSocketTest(unittest.TestCase): def test_get_ipv4_addresses(self): m = MagicMock() m.getsockname.return_value = ('127.0.0.1', 52300) m.getpeername.return_value = ('127.0.0.1', 4800) ws = WebSocket(sock=m) self.assertEqual(ws.local_address, ('127.0.0.1', 52300)) self.assertEqual(ws.peer_address, ('127.0.0.1', 4800)) def test_get_ipv6_addresses(self): m = MagicMock() m.getsockname.return_value = ('127.0.0.1', 52300, None, None) m.getpeername.return_value = ('127.0.0.1', 4800, None, None) ws = WebSocket(sock=m) self.assertEqual(ws.local_address, ('127.0.0.1', 52300)) self.assertEqual(ws.peer_address, ('127.0.0.1', 4800)) def test_get_underlying_connection(self): m = MagicMock() ws = WebSocket(sock=m) self.assertEqual(ws.connection, m) def test_close_connection(self): m = MagicMock() ws = WebSocket(sock=m) ws.close_connection() m.shutdown.assert_called_once_with(socket.SHUT_RDWR) m.close.assert_called_once_with() self.assertIsNone(ws.connection) m = MagicMock() m.close = MagicMock(side_effect=RuntimeError) ws = WebSocket(sock=m) ws.close_connection() self.assertIsNone(ws.connection) def test_terminate_with_closing(self): m = MagicMock() s = MagicMock() c = MagicMock() cc = MagicMock() ws = WebSocket(sock=m) with patch.multiple(ws, closed=c, close_connection=cc): ws.stream = s ws.stream.closing = CloseControlMessage(code=1000, reason='test closing') ws.terminate() self.assertTrue(ws.client_terminated) self.assertTrue(ws.server_terminated) self.assertTrue(ws.terminated) c.assert_called_once_with(1000, b'test closing') cc.assert_called_once_with() self.assertIsNone(ws.stream) self.assertIsNone(ws.environ) def test_terminate_without_closing(self): m = MagicMock() s = MagicMock() c = MagicMock() cc = MagicMock() ws = WebSocket(sock=m) with patch.multiple(ws, closed=c, close_connection=cc): ws.stream = s ws.stream.closing = None ws.terminate() self.assertTrue(ws.client_terminated) self.assertTrue(ws.server_terminated) self.assertTrue(ws.terminated) c.assert_called_once_with(1006, "Going away") cc.assert_called_once_with() self.assertIsNone(ws.stream) self.assertIsNone(ws.environ) def test_cannot_process_more_data_when_stream_is_terminated(self): m = MagicMock() ws = WebSocket(sock=m) ws.client_terminated = True ws.server_terminated = True self.assertFalse(ws.once()) def test_socket_error_on_receiving_more_bytes(self): m = MagicMock() m.recv = MagicMock(side_effect=socket.error) ws = WebSocket(sock=m) self.assertFalse(ws.once()) def test_no_bytes_were_read(self): m = MagicMock() m.recv.return_value = b'' ws = WebSocket(sock=m) self.assertFalse(ws.once()) def test_send_bytes_without_masking(self): tm = TextMessage(b'hello world').single() m = MagicMock() ws = WebSocket(sock=m) ws.send(b'hello world') m.sendall.assert_called_once_with(tm) def test_send_bytes_with_masking(self): tm = TextMessage(b'hello world').single(mask=True) m = MagicMock() ws = WebSocket(sock=m) ws.stream = MagicMock() ws.stream.always_mask = True ws.stream.text_message.return_value.single.return_value = tm ws.send(b'hello world') m.sendall.assert_called_once_with(tm) def test_send_message_without_masking(self): tm = TextMessage(b'hello world') m = MagicMock() ws = WebSocket(sock=m) ws.send(tm) m.sendall.assert_called_once_with(tm.single()) def test_send_generator_without_masking(self): tm0 = b'hello' tm1 = b'world' def datasource(): yield tm0 yield tm1 gen = datasource() m = MagicMock() ws = WebSocket(sock=m) ws.send(gen) self.assertEqual(m.sendall.call_count, 2) self.assertRaises(StopIteration, next, gen) def test_sending_unknown_datetype(self): m = MagicMock() ws = WebSocket(sock=m) self.assertRaises(ValueError, ws.send, 123) def test_closing_message_received(self): s = MagicMock() m = MagicMock() c = MagicMock() ws = WebSocket(sock=m) with patch.multiple(ws, close=c): ws.stream = s ws.stream.closing = CloseControlMessage(code=1000, reason='test closing') ws.process(b'unused for this test') c.assert_called_once_with(1000, b'test closing') if __name__ == '__main__': suite = unittest.TestSuite() loader = unittest.TestLoader() for testcase in [WSWebSocketTest]: tests = loader.loadTestsFromTestCase(testcase) suite.addTests(tests) unittest.TextTestRunner(verbosity=2).run(suite) python-ws4py-0.3.4/tox.ini000066400000000000000000000004631231577073400154520ustar00rootroot00000000000000# Tox (http://tox.testrun.org/) is a tool for running tests # in multiple virtualenvs. This configuration file will run the # test suite on all supported python versions. To use it, "pip install tox" # and then run "tox" from this directory. [tox] envlist = py27 [testenv] commands = python setup.py test python-ws4py-0.3.4/ws4py/000077500000000000000000000000001231577073400152225ustar00rootroot00000000000000python-ws4py-0.3.4/ws4py/__init__.py000066400000000000000000000051351231577073400173370ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following disclaimer # in the documentation and/or other materials provided with the # distribution. # * Neither the name of ws4py nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # import logging __author__ = "Sylvain Hellegouarch" __version__ = "0.3.4" __all__ = ['WS_KEY', 'WS_VERSION', 'configure_logger', 'format_addresses'] WS_KEY = b"258EAFA5-E914-47DA-95CA-C5AB0DC85B11" WS_VERSION = (8, 13) def configure_logger(stdout=True, filepath=None, level=logging.INFO): logger = logging.getLogger('ws4py') logger.setLevel(level) logfmt = logging.Formatter("[%(asctime)s] %(levelname)s %(message)s") if filepath: h = handlers.RotatingFileHandler(filepath, maxBytes=10485760, backupCount=3) h.setLevel(level) h.setFormatter(logfmt) logger.addHandler(h) if stdout: import sys h = logging.StreamHandler(sys.stdout) h.setLevel(level) h.setFormatter(logfmt) logger.addHandler(h) return logger def format_addresses(ws): me = ws.local_address peer = ws.peer_address if isinstance(me, tuple) and isinstance(peer, tuple): me_ip, me_port = ws.local_address peer_ip, peer_port = ws.peer_address return "[Local => %s:%d | Remote => %s:%d]" % (me_ip, me_port, peer_ip, peer_port) return "[Bound to '%s']" % me python-ws4py-0.3.4/ws4py/async_websocket.py000066400000000000000000000075771231577073400207770ustar00rootroot00000000000000# -*- coding: utf-8 -*- __doc__ = """ WebSocket implementation that relies on two new Python features: * asyncio to provide the high-level interface above transports * yield from to delegate to the reading stream whenever more bytes are required You can use these implementations in that context and benefit from those features whilst using ws4py. Strictly speaking this module probably doesn't have to be called async_websocket but it feels this will be its typical usage and is probably more readable than delegated_generator_websocket_on_top_of_asyncio.py """ import asyncio import types from ws4py.websocket import WebSocket as _WebSocket from ws4py.messaging import Message __all__ = ['WebSocket', 'EchoWebSocket'] class WebSocket(_WebSocket): def __init__(self, proto): """ A :pep:`3156` ready websocket handler that works well in a coroutine-aware loop such as the one provided by the asyncio module. The provided `proto` instance is a :class:`asyncio.Protocol` subclass instance that will be used internally to read and write from the underlying transport. Because the base :class:`ws4py.websocket.WebSocket` class is still coupled a bit to the socket interface, we have to override a little more than necessary to play nice with the :pep:`3156` interface. Hopefully, some day this will be cleaned out. """ _WebSocket.__init__(self, None) self.started = False self.proto = proto @property def local_address(self): """ Local endpoint address as a tuple """ if not self._local_address: self._local_address = self.proto.reader.transport.get_extra_info('sockname') if len(self._local_address) == 4: self._local_address = self._local_address[:2] return self._local_address @property def peer_address(self): """ Peer endpoint address as a tuple """ if not self._peer_address: self._peer_address = self.proto.reader.transport.get_extra_info('peername') if len(self._peer_address) == 4: self._peer_address = self._peer_address[:2] return self._peer_address def once(self): """ The base class directly is used in conjunction with the :class:`ws4py.manager.WebSocketManager` which is not actually used with the asyncio implementation of ws4py. So let's make it clear it shan't be used. """ raise NotImplemented() def close_connection(self): """ Close the underlying transport """ @asyncio.coroutine def closeit(): yield from self.proto.writer.drain() self.proto.writer.close() asyncio.async(closeit()) def _write(self, data): """ Write to the underlying transport """ @asyncio.coroutine def sendit(data): self.proto.writer.write(data) yield from self.proto.writer.drain() asyncio.async(sendit(data)) @asyncio.coroutine def run(self): """ Coroutine that runs until the websocket exchange is terminated. It also calls the `opened()` method to indicate the exchange has started. """ self.started = True try: self.opened() reader = self.proto.reader while True: data = yield from reader.read(self.reading_buffer_size) if not self.process(data): return False finally: self.terminate() return True class EchoWebSocket(WebSocket): def received_message(self, message): """ Automatically sends back the provided ``message`` to its originating endpoint. """ self.send(message.data, message.is_binary) python-ws4py-0.3.4/ws4py/client/000077500000000000000000000000001231577073400165005ustar00rootroot00000000000000python-ws4py-0.3.4/ws4py/client/__init__.py000066400000000000000000000250111231577073400206100ustar00rootroot00000000000000# -*- coding: utf-8 -*- from base64 import b64encode from hashlib import sha1 import os import socket import ssl from ws4py import WS_KEY, WS_VERSION from ws4py.exc import HandshakeError from ws4py.websocket import WebSocket from ws4py.compat import urlsplit __all__ = ['WebSocketBaseClient'] class WebSocketBaseClient(WebSocket): def __init__(self, url, protocols=None, extensions=None, heartbeat_freq=None, ssl_options=None, headers=None): """ A websocket client that implements :rfc:`6455` and provides a simple interface to communicate with a websocket server. This class works on its own but will block if not run in its own thread. When an instance of this class is created, a :py:mod:`socket` is created. If the connection is a TCP socket, the nagle's algorithm is disabled. The address of the server will be extracted from the given websocket url. The websocket key is randomly generated, reset the `key` attribute if you want to provide yours. For instance to create a TCP client: .. code-block:: python >>> from websocket.client import WebSocketBaseClient >>> ws = WebSocketBaseClient('ws://localhost/ws') Here is an example for a TCP client over SSL: .. code-block:: python >>> from websocket.client import WebSocketBaseClient >>> ws = WebSocketBaseClient('wss://localhost/ws') Finally an example of a Unix-domain connection: .. code-block:: python >>> from websocket.client import WebSocketBaseClient >>> ws = WebSocketBaseClient('ws+unix:///tmp/my.sock') Note that in this case, the initial Upgrade request will be sent to ``/``. You may need to change this by setting the resource explicitely before connecting: .. code-block:: python >>> from websocket.client import WebSocketBaseClient >>> ws = WebSocketBaseClient('ws+unix:///tmp/my.sock') >>> ws.resource = '/ws' >>> ws.connect() You may provide extra headers by passing a list of tuples which must be unicode objects. """ self.url = url self.host = None self.scheme = None self.port = None self.unix_socket_path = None self.resource = None self.ssl_options = ssl_options or {} self.extra_headers = headers or [] self._parse_url() if self.unix_socket_path: sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM, 0) else: # Let's handle IPv4 and IPv6 addresses # Simplified from CherryPy's code try: family, socktype, proto, canonname, sa = socket.getaddrinfo(self.host, self.port, socket.AF_UNSPEC, socket.SOCK_STREAM, 0, socket.AI_PASSIVE)[0] except socket.gaierror: family = socket.AF_INET if self.host.startswith('::'): family = socket.AF_INET6 socktype = socket.SOCK_STREAM proto = 0 canonname = "" sa = (self.host, self.port, 0, 0) sock = socket.socket(family, socktype, proto) sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) if hasattr(socket, 'AF_INET6') and family == socket.AF_INET6 and \ self.host.startswith('::'): try: sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0) except (AttributeError, socket.error): pass WebSocket.__init__(self, sock, protocols=protocols, extensions=extensions, heartbeat_freq=heartbeat_freq) self.stream.always_mask = True self.stream.expect_masking = False self.key = b64encode(os.urandom(16)) # Adpated from: https://github.com/liris/websocket-client/blob/master/websocket.py#L105 def _parse_url(self): """ Parses a URL which must have one of the following forms: - ws://host[:port][path] - wss://host[:port][path] - ws+unix:///path/to/my.socket In the first two cases, the ``host`` and ``port`` attributes will be set to the parsed values. If no port is explicitely provided, it will be either 80 or 443 based on the scheme. Also, the ``resource`` attribute is set to the path segment of the URL (alongside any querystring). In addition, if the scheme is ``ws+unix``, the ``unix_socket_path`` attribute is set to the path to the Unix socket while the ``resource`` attribute is set to ``/``. """ # Python 2.6.1 and below don't parse ws or wss urls properly. netloc is empty. # See: https://github.com/Lawouach/WebSocket-for-Python/issues/59 scheme, url = self.url.split(":", 1) parsed = urlsplit(url, scheme="http") if parsed.hostname: self.host = parsed.hostname elif '+unix' in scheme: self.host = 'localhost' else: raise ValueError("Invalid hostname from: %s", self.url) if parsed.port: self.port = parsed.port if scheme == "ws": if not self.port: self.port = 80 elif scheme == "wss": if not self.port: self.port = 443 elif scheme in ('ws+unix', 'wss+unix'): pass else: raise ValueError("Invalid scheme: %s" % scheme) if parsed.path: resource = parsed.path else: resource = "/" if '+unix' in scheme: self.unix_socket_path = resource resource = '/' if parsed.query: resource += "?" + parsed.query self.scheme = scheme self.resource = resource @property def bind_addr(self): """ Returns the Unix socket path if or a tuple ``(host, port)`` depending on the initial URL's scheme. """ return self.unix_socket_path or (self.host, self.port) def close(self, code=1000, reason=''): """ Initiate the closing handshake with the server. """ if not self.client_terminated: self.client_terminated = True self._write(self.stream.close(code=code, reason=reason).single(mask=True)) def connect(self): """ Connects this websocket and starts the upgrade handshake with the remote endpoint. """ if self.scheme == "wss": # default port is now 443; upgrade self.sender to send ssl self.sock = ssl.wrap_socket(self.sock, **self.ssl_options) self.sock.connect(self.bind_addr) self._write(self.handshake_request) response = b'' doubleCLRF = b'\r\n\r\n' while True: bytes = self.sock.recv(128) if not bytes: break response += bytes if doubleCLRF in response: break if not response: self.close_connection() raise HandshakeError("Invalid response") headers, _, body = response.partition(doubleCLRF) response_line, _, headers = headers.partition(b'\r\n') try: self.process_response_line(response_line) self.protocols, self.extensions = self.process_handshake_header(headers) except HandshakeError: self.close_connection() raise self.handshake_ok() if body: self.process(body) @property def handshake_headers(self): """ List of headers appropriate for the upgrade handshake. """ headers = [ ('Host', self.host), ('Connection', 'Upgrade'), ('Upgrade', 'websocket'), ('Sec-WebSocket-Key', self.key.decode('utf-8')), ('Origin', self.url), ('Sec-WebSocket-Version', str(max(WS_VERSION))) ] if self.protocols: headers.append(('Sec-WebSocket-Protocol', ','.join(self.protocols))) if self.extra_headers: headers.extend(self.extra_headers) return headers @property def handshake_request(self): """ Prepare the request to be sent for the upgrade handshake. """ headers = self.handshake_headers request = [("GET %s HTTP/1.1" % self.resource).encode('utf-8')] for header, value in headers: request.append(("%s: %s" % (header, value)).encode('utf-8')) request.append(b'\r\n') return b'\r\n'.join(request) def process_response_line(self, response_line): """ Ensure that we received a HTTP `101` status code in response to our request and if not raises :exc:`HandshakeError`. """ protocol, code, status = response_line.split(b' ', 2) if code != b'101': raise HandshakeError("Invalid response status: %s %s" % (code, status)) def process_handshake_header(self, headers): """ Read the upgrade handshake's response headers and validate them against :rfc:`6455`. """ protocols = [] extensions = [] headers = headers.strip() for header_line in headers.split(b'\r\n'): header, value = header_line.split(b':', 1) header = header.strip().lower() value = value.strip().lower() if header == 'upgrade' and value != 'websocket': raise HandshakeError("Invalid Upgrade header: %s" % value) elif header == 'connection' and value != 'upgrade': raise HandshakeError("Invalid Connection header: %s" % value) elif header == 'sec-websocket-accept': match = b64encode(sha1(self.key.encode('utf-8') + WS_KEY).digest()) if value != match.lower(): raise HandshakeError("Invalid challenge response: %s" % value) elif header == 'sec-websocket-protocol': protocols = ','.join(value) elif header == 'sec-websocket-extensions': extensions = ','.join(value) return protocols, extensions python-ws4py-0.3.4/ws4py/client/geventclient.py000066400000000000000000000052001231577073400215360ustar00rootroot00000000000000# -*- coding: utf-8 -*- import copy import gevent from gevent import Greenlet from gevent.queue import Queue from ws4py.client import WebSocketBaseClient __all__ = ['WebSocketClient'] class WebSocketClient(WebSocketBaseClient): def __init__(self, url, protocols=None, extensions=None, ssl_options=None, headers=None): """ WebSocket client that executes the :meth:`run() ` into a gevent greenlet. .. code-block:: python ws = WebSocketClient('ws://localhost:9000/echo', protocols=['http-only', 'chat']) ws.connect() ws.send("Hello world") def incoming(): while True: m = ws.receive() if m is not None: print str(m) else: break def outgoing(): for i in range(0, 40, 5): ws.send("*" * i) greenlets = [ gevent.spawn(incoming), gevent.spawn(outgoing), ] gevent.joinall(greenlets) """ WebSocketBaseClient.__init__(self, url, protocols, extensions, ssl_options=ssl_options, headers=headers) self._th = Greenlet(self.run) self.messages = Queue() """ Queue that will hold received messages. """ def handshake_ok(self): """ Called when the upgrade handshake has completed successfully. Starts the client's thread. """ self._th.start() def received_message(self, message): """ Override the base class to store the incoming message in the `messages` queue. """ self.messages.put(copy.deepcopy(message)) def closed(self, code, reason=None): """ Puts a :exc:`StopIteration` as a message into the `messages` queue. """ # When the connection is closed, put a StopIteration # on the message queue to signal there's nothing left # to wait for self.messages.put(StopIteration) def receive(self): """ Returns messages that were stored into the `messages` queue and returns `None` when the websocket is terminated or closed. """ # If the websocket was terminated and there are no messages # left in the queue, return None immediately otherwise the client # will block forever if self.terminated and self.messages.empty(): return None message = self.messages.get() if message is StopIteration: return None return message python-ws4py-0.3.4/ws4py/client/threadedclient.py000066400000000000000000000054341231577073400220370ustar00rootroot00000000000000# -*- coding: utf-8 -*- import threading from ws4py.client import WebSocketBaseClient __all__ = ['WebSocketClient'] class WebSocketClient(WebSocketBaseClient): def __init__(self, url, protocols=None, extensions=None, heartbeat_freq=None, ssl_options=None, headers=None): """ .. code-block:: python from ws4py.client.threadedclient import WebSocketClient class EchoClient(WebSocketClient): def opened(self): for i in range(0, 200, 25): self.send("*" * i) def closed(self, code, reason): print(("Closed down", code, reason)) def received_message(self, m): print("=> %d %s" % (len(m), str(m))) try: ws = EchoClient('ws://localhost:9000/echo', protocols=['http-only', 'chat']) ws.connect() except KeyboardInterrupt: ws.close() """ WebSocketBaseClient.__init__(self, url, protocols, extensions, heartbeat_freq, ssl_options, headers=headers) self._th = threading.Thread(target=self.run, name='WebSocketClient') self._th.daemon = True @property def daemon(self): """ `True` if the client's thread is set to be a daemon thread. """ return self._th.daemon @daemon.setter def daemon(self, flag): """ Set to `True` if the client's thread should be a daemon. """ self._th.daemon = flag def run_forever(self): """ Simply blocks the thread until the websocket has terminated. """ while not self.terminated: self._th.join(timeout=0.1) def handshake_ok(self): """ Called when the upgrade handshake has completed successfully. Starts the client's thread. """ self._th.start() if __name__ == '__main__': from ws4py.client.threadedclient import WebSocketClient class EchoClient(WebSocketClient): def opened(self): def data_provider(): for i in range(0, 200, 25): yield "#" * i self.send(data_provider()) for i in range(0, 200, 25): self.send("*" * i) def closed(self, code, reason): print(("Closed down", code, reason)) def received_message(self, m): print("#%d" % len(m)) if len(m) == 175: self.close(reason='bye bye') try: ws = EchoClient('ws://localhost:9000/ws', protocols=['http-only', 'chat'], headers=[('X-Test', 'hello there')]) ws.connect() ws.run_forever() except KeyboardInterrupt: ws.close() python-ws4py-0.3.4/ws4py/client/tornadoclient.py000066400000000000000000000115431231577073400217230ustar00rootroot00000000000000# -*- coding: utf-8 -*- import ssl from tornado import iostream, escape from ws4py.client import WebSocketBaseClient from ws4py.exc import HandshakeError __all__ = ['TornadoWebSocketClient'] class TornadoWebSocketClient(WebSocketBaseClient): def __init__(self, url, protocols=None, extensions=None, io_loop=None, ssl_options=None, headers=None): """ .. code-block:: python from tornado import ioloop class MyClient(TornadoWebSocketClient): def opened(self): for i in range(0, 200, 25): self.send("*" * i) def received_message(self, m): print((m, len(str(m)))) def closed(self, code, reason=None): ioloop.IOLoop.instance().stop() ws = MyClient('ws://localhost:9000/echo', protocols=['http-only', 'chat']) ws.connect() ioloop.IOLoop.instance().start() """ WebSocketBaseClient.__init__(self, url, protocols, extensions, ssl_options=ssl_options, headers=headers) self.ssl_options["do_handshake_on_connect"] = False if self.scheme == "wss": self.sock = ssl.wrap_socket(self.sock, **self.ssl_options) self.io = iostream.SSLIOStream(self.sock, io_loop) else: self.io = iostream.IOStream(self.sock, io_loop) self.io_loop = io_loop def connect(self): """ Connects the websocket and initiate the upgrade handshake. """ self.io.set_close_callback(self.__connection_refused) self.io.connect((self.host, int(self.port)), self.__send_handshake) def _write(self, b): """ Trying to prevent a write operation on an already closed websocket stream. This cannot be bullet proof but hopefully will catch almost all use cases. """ if self.terminated: raise RuntimeError("Cannot send on a terminated websocket") self.io.write(b) def __connection_refused(self, *args, **kwargs): self.server_terminated = True self.closed(1005, 'Connection refused') def __send_handshake(self): self.io.set_close_callback(self.__connection_closed) self.io.write(escape.utf8(self.handshake_request), self.__handshake_sent) def __connection_closed(self, *args, **kwargs): self.server_terminated = True self.closed(1006, 'Connection closed during handshake') def __handshake_sent(self): self.io.read_until(b"\r\n\r\n", self.__handshake_completed) def __handshake_completed(self, data): self.io.set_close_callback(None) try: response_line, _, headers = data.partition(b'\r\n') self.process_response_line(response_line) protocols, extensions = self.process_handshake_header(headers) except HandshakeError: self.close_connection() raise self.opened() self.io.set_close_callback(self.__stream_closed) self.io.read_bytes(self.reading_buffer_size, self.__fetch_more) def __fetch_more(self, bytes): try: should_continue = self.process(bytes) except: should_continue = False if should_continue: self.io.read_bytes(self.reading_buffer_size, self.__fetch_more) else: self.__gracefully_terminate() def __gracefully_terminate(self): self.client_terminated = self.server_terminated = True try: if not self.stream.closing: self.closed(1006) finally: self.close_connection() def __stream_closed(self, *args, **kwargs): self.io.set_close_callback(None) code = 1006 reason = None if self.stream.closing: code, reason = self.stream.closing.code, self.stream.closing.reason self.closed(code, reason) self.stream._cleanup() def close_connection(self): """ Close the underlying connection """ self.io.close() if __name__ == '__main__': from tornado import ioloop class MyClient(TornadoWebSocketClient): def opened(self): def data_provider(): for i in range(0, 200, 25): yield "#" * i self.send(data_provider()) for i in range(0, 200, 25): self.send("*" * i) def received_message(self, m): print("#%d" % len(m)) if len(m) == 175: self.close() def closed(self, code, reason=None): ioloop.IOLoop.instance().stop() print(("Closed down", code, reason)) ws = MyClient('ws://localhost:9000/ws', protocols=['http-only', 'chat']) ws.connect() ioloop.IOLoop.instance().start() python-ws4py-0.3.4/ws4py/compat.py000066400000000000000000000021231231577073400170550ustar00rootroot00000000000000# -*- coding: utf-8 -*- __doc__ = """ This compatibility module is inspired by the one found in CherryPy. It provides a common entry point for the various functions and types that are used with ws4py but which differ from Python 2.x to Python 3.x There are likely better ways for some of them so feel free to provide patches. Note this has been tested against 2.7 and 3.3 only but should hopefully work fine with other versions too. """ import sys if sys.version_info >= (3, 0): py3k = True from urllib.parse import urlsplit range = range unicode = str basestring = (bytes, str) _ord = ord def get_connection(fileobj): return fileobj.raw._sock def detach_connection(fileobj): fileobj.detach() def ord(c): if isinstance(c, int): return c return _ord(c) else: py3k = False from urlparse import urlsplit range = xrange unicode = unicode basestring = basestring ord = ord def get_connection(fileobj): return fileobj._sock def detach_connection(fileobj): fileobj._sock = None python-ws4py-0.3.4/ws4py/exc.py000066400000000000000000000014671231577073400163630ustar00rootroot00000000000000# -*- coding: utf-8 -*- __all__ = ['WebSocketException', 'FrameTooLargeException', 'ProtocolException', 'UnsupportedFrameTypeException', 'TextFrameEncodingException', 'UnsupportedFrameTypeException', 'TextFrameEncodingException', 'StreamClosed', 'HandshakeError', 'InvalidBytesError'] class WebSocketException(Exception): pass class ProtocolException(WebSocketException): pass class FrameTooLargeException(WebSocketException): pass class UnsupportedFrameTypeException(WebSocketException): pass class TextFrameEncodingException(WebSocketException): pass class InvalidBytesError(WebSocketException): pass class StreamClosed(Exception): pass class HandshakeError(WebSocketException): def __init__(self, msg): self.msg = msg def __str__(self): return self.msg python-ws4py-0.3.4/ws4py/framing.py000066400000000000000000000236611231577073400172270ustar00rootroot00000000000000# -*- coding: utf-8 -*- from struct import pack, unpack from ws4py.exc import FrameTooLargeException, ProtocolException from ws4py.compat import py3k, ord, range # Frame opcodes defined in the spec. OPCODE_CONTINUATION = 0x0 OPCODE_TEXT = 0x1 OPCODE_BINARY = 0x2 OPCODE_CLOSE = 0x8 OPCODE_PING = 0x9 OPCODE_PONG = 0xa __all__ = ['Frame'] class Frame(object): def __init__(self, opcode=None, body=b'', masking_key=None, fin=0, rsv1=0, rsv2=0, rsv3=0): """ Implements the framing protocol as defined by RFC 6455. .. code-block:: python :linenos: >>> test_mask = 'XXXXXX' # perhaps from os.urandom(4) >>> f = Frame(OPCODE_TEXT, 'hello world', masking_key=test_mask, fin=1) >>> bytes = f.build() >>> bytes.encode('hex') '818bbe04e66ad6618a06d1249105cc6882' >>> f = Frame() >>> f.parser.send(bytes[0]) 1 >>> f.parser.send(bytes[1]) 4 .. seealso:: Data Framing http://tools.ietf.org/html/rfc6455#section-5.2 """ if not isinstance(body, bytes): raise TypeError("The body must be properly encoded") self.opcode = opcode self.body = body self.masking_key = masking_key self.fin = fin self.rsv1 = rsv1 self.rsv2 = rsv2 self.rsv3 = rsv3 self.payload_length = len(body) self._parser = None @property def parser(self): if self._parser is None: self._parser = self._parsing() # Python generators must be initialized once. next(self.parser) return self._parser def _cleanup(self): if self._parser: self._parser.close() self._parser = None def build(self): """ Builds a frame from the instance's attributes and returns its bytes representation. """ header = b'' if self.fin > 0x1: raise ValueError('FIN bit parameter must be 0 or 1') if 0x3 <= self.opcode <= 0x7 or 0xB <= self.opcode: raise ValueError('Opcode cannot be a reserved opcode') ## +-+-+-+-+-------+ ## |F|R|R|R| opcode| ## |I|S|S|S| (4) | ## |N|V|V|V| | ## | |1|2|3| | ## +-+-+-+-+-------+ header = pack('!B', ((self.fin << 7) | (self.rsv1 << 6) | (self.rsv2 << 5) | (self.rsv3 << 4) | self.opcode)) ## +-+-------------+-------------------------------+ ## |M| Payload len | Extended payload length | ## |A| (7) | (16/63) | ## |S| | (if payload len==126/127) | ## |K| | | ## +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - + ## | Extended payload length continued, if payload len == 127 | ## + - - - - - - - - - - - - - - - +-------------------------------+ if self.masking_key: mask_bit = 1 << 7 else: mask_bit = 0 length = self.payload_length if length < 126: header += pack('!B', (mask_bit | length)) elif length < (1 << 16): header += pack('!B', (mask_bit | 126)) + pack('!H', length) elif length < (1 << 63): header += pack('!B', (mask_bit | 127)) + pack('!Q', length) else: raise FrameTooLargeException() ## + - - - - - - - - - - - - - - - +-------------------------------+ ## | |Masking-key, if MASK set to 1 | ## +-------------------------------+-------------------------------+ ## | Masking-key (continued) | Payload Data | ## +-------------------------------- - - - - - - - - - - - - - - - + ## : Payload Data continued ... : ## + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + ## | Payload Data continued ... | ## +---------------------------------------------------------------+ body = self.body if not self.masking_key: return bytes(header + body) return bytes(header + self.masking_key + self.mask(body)) def _parsing(self): """ Generator to parse bytes into a frame. Yields until enough bytes have been read or an error is met. """ buf = b'' some_bytes = b'' # yield until we get the first header's byte while not some_bytes: some_bytes = (yield 1) first_byte = some_bytes[0] if isinstance(some_bytes, bytearray) else ord(some_bytes[0]) # frame-fin = %x0 ; more frames of this message follow # / %x1 ; final frame of this message self.fin = (first_byte >> 7) & 1 self.rsv1 = (first_byte >> 6) & 1 self.rsv2 = (first_byte >> 5) & 1 self.rsv3 = (first_byte >> 4) & 1 self.opcode = first_byte & 0xf # frame-rsv1 = %x0 ; 1 bit, MUST be 0 unless negotiated otherwise # frame-rsv2 = %x0 ; 1 bit, MUST be 0 unless negotiated otherwise # frame-rsv3 = %x0 ; 1 bit, MUST be 0 unless negotiated otherwise if self.rsv1 or self.rsv2 or self.rsv3: raise ProtocolException() # control frames between 3 and 7 as well as above 0xA are currently reserved if 2 < self.opcode < 8 or self.opcode > 0xA: raise ProtocolException() # control frames cannot be fragmented if self.opcode > 0x7 and self.fin == 0: raise ProtocolException() # do we already have enough some_bytes to continue? some_bytes = some_bytes[1:] if some_bytes and len(some_bytes) > 1 else b'' # Yield until we get the second header's byte while not some_bytes: some_bytes = (yield 1) second_byte = some_bytes[0] if isinstance(some_bytes, bytearray) else ord(some_bytes[0]) mask = (second_byte >> 7) & 1 self.payload_length = second_byte & 0x7f # All control frames MUST have a payload length of 125 some_bytes or less if self.opcode > 0x7 and self.payload_length > 125: raise FrameTooLargeException() if some_bytes and len(some_bytes) > 1: buf = some_bytes[1:] some_bytes = buf else: buf = b'' some_bytes = b'' if self.payload_length == 127: # This will compute the actual application data size if len(buf) < 8: nxt_buf_size = 8 - len(buf) some_bytes = (yield nxt_buf_size) some_bytes = buf + (some_bytes or b'') while len(some_bytes) < 8: b = (yield 8 - len(some_bytes)) if b is not None: some_bytes = some_bytes + b if len(some_bytes) > 8: buf = some_bytes[8:] some_bytes = some_bytes[:8] else: some_bytes = buf[:8] buf = buf[8:] extended_payload_length = some_bytes self.payload_length = unpack( '!Q', extended_payload_length)[0] if self.payload_length > 0x7FFFFFFFFFFFFFFF: raise FrameTooLargeException() elif self.payload_length == 126: if len(buf) < 2: nxt_buf_size = 2 - len(buf) some_bytes = (yield nxt_buf_size) some_bytes = buf + (some_bytes or b'') while len(some_bytes) < 2: b = (yield 2 - len(some_bytes)) if b is not None: some_bytes = some_bytes + b if len(some_bytes) > 2: buf = some_bytes[2:] some_bytes = some_bytes[:2] else: some_bytes = buf[:2] buf = buf[2:] extended_payload_length = some_bytes self.payload_length = unpack( '!H', extended_payload_length)[0] if mask: if len(buf) < 4: nxt_buf_size = 4 - len(buf) some_bytes = (yield nxt_buf_size) some_bytes = buf + (some_bytes or b'') while not some_bytes or len(some_bytes) < 4: b = (yield 4 - len(some_bytes)) if b is not None: some_bytes = some_bytes + b if len(some_bytes) > 4: buf = some_bytes[4:] else: some_bytes = buf[:4] buf = buf[4:] self.masking_key = some_bytes if len(buf) < self.payload_length: nxt_buf_size = self.payload_length - len(buf) some_bytes = (yield nxt_buf_size) some_bytes = buf + (some_bytes or b'') while len(some_bytes) < self.payload_length: l = self.payload_length - len(some_bytes) b = (yield l) if b is not None: some_bytes = some_bytes + b else: if self.payload_length == len(buf): some_bytes = buf else: some_bytes = buf[:self.payload_length] self.body = some_bytes yield def mask(self, data): """ Performs the masking or unmasking operation on data using the simple masking algorithm: .. j = i MOD 4 transformed-octet-i = original-octet-i XOR masking-key-octet-j """ masked = bytearray(data) if py3k: key = self.masking_key else: key = map(ord, self.masking_key) for i in range(len(data)): masked[i] = masked[i] ^ key[i%4] return masked unmask = mask python-ws4py-0.3.4/ws4py/manager.py000066400000000000000000000243051231577073400172120ustar00rootroot00000000000000# -*- coding: utf-8 -*- __doc__ = """ The manager module provides a selected classes to handle websocket's execution. Initially the rationale was to: - Externalize the way the CherryPy server had been setup as its websocket management was too tightly coupled with the plugin implementation. - Offer a management that could be used by other server or client implementations. - Move away from the threaded model to the event-based model by relying on `select` or `epoll` (when available). A simple usage for handling websocket clients: .. code-block:: python from ws4py.client import WebSocketBaseClient from ws4py.manager import WebSocketManager m = WebSocketManager() class EchoClient(WebSocketBaseClient): def handshake_ok(self): m.add(self) # register the client once the handshake is done def received_message(self, msg): print str(msg) m.start() client = EchoClient('ws://localhost:9000/ws') client.connect() m.join() # blocks forever Managers are not compulsory but hopefully will help your workflow. For clients, you can still rely on threaded, gevent or tornado based implementations of course. """ import logging import select import threading import time from ws4py import format_addresses from ws4py.compat import py3k logger = logging.getLogger('ws4py') class SelectPoller(object): def __init__(self, timeout=0.1): """ A socket poller that uses the `select` implementation to determines which file descriptors have data available to read. It is available on all platforms. """ self._fds = [] self.timeout = timeout def release(self): """ Cleanup resources. """ self._fds = [] def register(self, fd): """ Register a new file descriptor to be part of the select polling next time around. """ if fd not in self._fds: self._fds.append(fd) def unregister(self, fd): """ Unregister the given file descriptor. """ if fd in self._fds: self._fds.remove(fd) def poll(self): """ Polls once and returns a list of ready-to-be-read file descriptors. """ if not self._fds: time.sleep(self.timeout) return [] r, w, x = select.select(self._fds, [], [], self.timeout) return r class EPollPoller(object): def __init__(self, timeout=0.1): """ An epoll poller that uses the ``epoll`` implementation to determines which file descriptors have data available to read. Available on Unix flavors mostly. """ self.poller = select.epoll() self.timeout = timeout def release(self): """ Cleanup resources. """ self.poller.close() def register(self, fd): """ Register a new file descriptor to be part of the select polling next time around. """ try: self.poller.register(fd, select.EPOLLIN | select.EPOLLPRI) except IOError: pass def unregister(self, fd): """ Unregister the given file descriptor. """ self.poller.unregister(fd) def poll(self): """ Polls once and yields each ready-to-be-read file-descriptor """ events = self.poller.poll(timeout=self.timeout) for fd, event in events: if event | select.EPOLLIN | select.EPOLLPRI: yield fd class KQueuePoller(object): def __init__(self, timeout=0.1): """ An epoll poller that uses the ``epoll`` implementation to determines which file descriptors have data available to read. Available on Unix flavors mostly. """ self.poller = select.epoll() self.timeout = timeout def release(self): """ Cleanup resources. """ self.poller.close() def register(self, fd): """ Register a new file descriptor to be part of the select polling next time around. """ try: self.poller.register(fd, select.EPOLLIN | select.EPOLLPRI) except IOError: pass def unregister(self, fd): """ Unregister the given file descriptor. """ self.poller.unregister(fd) def poll(self): """ Polls once and yields each ready-to-be-read file-descriptor """ events = self.poller.poll(timeout=self.timeout) for fd, event in events: if event | select.EPOLLIN | select.EPOLLPRI: yield fd class WebSocketManager(threading.Thread): def __init__(self, poller=None): """ An event-based websocket manager. By event-based, we mean that the websockets will be called when their sockets have data to be read from. The manager itself runs in its own thread as not to be the blocking mainloop of your application. The poller's implementation is automatically chosen with ``epoll`` if available else ``select`` unless you provide your own ``poller``. """ threading.Thread.__init__(self) self.lock = threading.Lock() self.websockets = {} self.running = False if poller: self.poller = poller else: if hasattr(select, "epoll"): self.poller = EPollPoller() logger.info("Using epoll") else: self.poller = SelectPoller() logger.info("Using select as epoll is not available") def __len__(self): return len(self.websockets) def __iter__(self): if py3k: return iter(self.websockets.values()) else: return self.websockets.itervalues() def __contains__(self, ws): fd = ws.sock.fileno() # just in case the file descriptor was reused # we actually check the instance (well, this might # also have been reused...) return self.websockets.get(fd) is ws def add(self, websocket): """ Manage a new websocket. First calls its :meth:`opened() ` method and register its socket against the poller for reading events. """ if websocket in self: return logger.info("Managing websocket %s" % format_addresses(websocket)) websocket.opened() with self.lock: fd = websocket.sock.fileno() self.websockets[fd] = websocket self.poller.register(fd) def remove(self, websocket): """ Remove the given ``websocket`` from the manager. This does not call its :meth:`closed() ` method as it's out-of-band by your application or from within the manager's run loop. """ if websocket not in self: return logger.info("Removing websocket %s" % format_addresses(websocket)) with self.lock: fd = websocket.sock.fileno() self.websockets.pop(fd, None) self.poller.unregister(fd) def stop(self): """ Mark the manager as terminated and releases its resources. """ self.running = False with self.lock: self.websockets.clear() self.poller.release() def run(self): """ Manager's mainloop executed from within a thread. Constantly poll for read events and, when available, call related websockets' `once` method to read and process the incoming data. If the :meth:`once() ` method returns a `False` value, its :meth:`terminate() ` method is also applied to properly close the websocket and its socket is unregistered from the poller. Note that websocket shouldn't take long to process their data or they will block the remaining websockets with data to be handled. As for what long means, it's up to your requirements. """ self.running = True while self.running: with self.lock: polled = self.poller.poll() if not self.running: break for fd in polled: if not self.running: break ws = self.websockets.get(fd) if ws and not ws.terminated: if not ws.once(): with self.lock: fd = ws.sock.fileno() self.websockets.pop(fd, None) self.poller.unregister(fd) if not ws.terminated: logger.info("Terminating websocket %s" % format_addresses(ws)) ws.terminate() def close_all(self, code=1001, message='Server is shutting down'): """ Execute the :meth:`close() ` method of each registered websockets to initiate the closing handshake. It doesn't wait for the handshake to complete properly. """ with self.lock: logger.info("Closing all websockets with [%d] '%s'" % (code, message)) for ws in iter(self): ws.close(code=code, reason=message) def broadcast(self, message, binary=False): """ Broadcasts the given message to all registered websockets, at the time of the call. Broadcast may fail on a given registered peer but this is silent as it's not the method's purpose to handle websocket's failures. """ with self.lock: websockets = self.websockets.copy() if py3k: ws_iter = iter(websockets.values()) else: ws_iter = websockets.itervalues() for ws in ws_iter: if not ws.terminated: try: ws.send(message, binary) except: pass python-ws4py-0.3.4/ws4py/messaging.py000066400000000000000000000120511231577073400175500ustar00rootroot00000000000000# -*- coding: utf-8 -*- import os import struct from ws4py.framing import Frame, OPCODE_CONTINUATION, OPCODE_TEXT, \ OPCODE_BINARY, OPCODE_CLOSE, OPCODE_PING, OPCODE_PONG from ws4py.compat import unicode, py3k __all__ = ['Message', 'TextMessage', 'BinaryMessage', 'CloseControlMessage', 'PingControlMessage', 'PongControlMessage'] class Message(object): def __init__(self, opcode, data=b'', encoding='utf-8'): """ A message is a application level entity. It's usually built from one or many frames. The protocol defines several kind of messages which are grouped into two sets: * data messages which can be text or binary typed * control messages which provide a mechanism to perform in-band control communication between peers The ``opcode`` indicates the message type and ``data`` is the possible message payload. The payload is held internally as a a :func:`bytearray` as they are faster than pure strings for append operations. Unicode data will be encoded using the provided ``encoding``. """ self.opcode = opcode self._completed = False self.encoding = encoding if isinstance(data, unicode): if not encoding: raise TypeError("unicode data without an encoding") data = data.encode(encoding) elif isinstance(data, bytearray): data = bytes(data) elif not isinstance(data, bytes): raise TypeError("%s is not a supported data type" % type(data)) self.data = data def single(self, mask=False): """ Returns a frame bytes with the fin bit set and a random mask. If ``mask`` is set, automatically mask the frame using a generated 4-byte token. """ mask = os.urandom(4) if mask else None return Frame(body=self.data, opcode=self.opcode, masking_key=mask, fin=1).build() def fragment(self, first=False, last=False, mask=False): """ Returns a :class:`ws4py.framing.Frame` bytes. The behavior depends on the given flags: * ``first``: the frame uses ``self.opcode`` else a continuation opcode * ``last``: the frame has its ``fin`` bit set * ``mask``: the frame is masked using a automatically generated 4-byte token """ fin = 1 if last is True else 0 opcode = self.opcode if first is True else OPCODE_CONTINUATION mask = os.urandom(4) if mask else None return Frame(body=self.data, opcode=opcode, masking_key=mask, fin=fin).build() @property def completed(self): """ Indicates the the message is complete, meaning the frame's ``fin`` bit was set. """ return self._completed @completed.setter def completed(self, state): """ Sets the state for this message. Usually set by the stream's parser. """ self._completed = state def extend(self, data): """ Add more ``data`` to the message. """ if isinstance(data, bytes): self.data += data elif isinstance(data, bytearray): self.data += bytes(data) elif isinstance(data, unicode): self.data += data.encode(self.encoding) else: raise TypeError("%s is not a supported data type" % type(data)) def __len__(self): return len(self.__unicode__()) def __str__(self): if py3k: return self.data.decode(self.encoding) return self.data def __unicode__(self): return self.data.decode(self.encoding) class TextMessage(Message): def __init__(self, text=None): Message.__init__(self, OPCODE_TEXT, text) @property def is_binary(self): return False @property def is_text(self): return True class BinaryMessage(Message): def __init__(self, bytes=None): Message.__init__(self, OPCODE_BINARY, bytes, encoding=None) @property def is_binary(self): return True @property def is_text(self): return False def __len__(self): return len(self.data) class CloseControlMessage(Message): def __init__(self, code=1000, reason=''): data = b"" if code: data += struct.pack("!H", code) if reason is not None: if isinstance(reason, unicode): reason = reason.encode('utf-8') data += reason Message.__init__(self, OPCODE_CLOSE, data, 'utf-8') self.code = code self.reason = reason def __str__(self): if py3k: return self.reason.decode('utf-8') return self.reason def __unicode__(self): return self.reason.decode(self.encoding) class PingControlMessage(Message): def __init__(self, data=None): Message.__init__(self, OPCODE_PING, data) class PongControlMessage(Message): def __init__(self, data): Message.__init__(self, OPCODE_PONG, data) python-ws4py-0.3.4/ws4py/server/000077500000000000000000000000001231577073400165305ustar00rootroot00000000000000python-ws4py-0.3.4/ws4py/server/__init__.py000066400000000000000000000000001231577073400206270ustar00rootroot00000000000000python-ws4py-0.3.4/ws4py/server/cherrypyserver.py000066400000000000000000000336471231577073400222130ustar00rootroot00000000000000# -*- coding: utf-8 -*- __doc__ = """ WebSocket within CherryPy is a tricky bit since CherryPy is a threaded server which would choke quickly if each thread of the server were kept attached to a long living connection that WebSocket expects. In order to work around this constraint, we take some advantage of some internals of CherryPy as well as the introspection Python provides. Basically, when the WebSocket handshake is complete, we take over the socket and let CherryPy take back the thread that was associated with the upgrade request. These operations require a bit of work at various levels of the CherryPy framework but this module takes care of them and from your application's perspective, this is abstracted. Here are the various utilities provided by this module: * WebSocketTool: The tool is in charge to perform the HTTP upgrade and detach the socket from CherryPy. It runs at various hook points of the request's processing. Enable that tool at any path you wish to handle as a WebSocket handler. * WebSocketPlugin: The plugin tracks the instanciated web socket handlers. It also cleans out websocket handler which connection have been closed down. The websocket connection then runs in its own thread that this plugin manages. Simple usage example: .. code-block:: python :linenos: import cherrypy from ws4py.server.cherrypyserver import WebSocketPlugin, WebSocketTool from ws4py.websocket import EchoWebSocket cherrypy.config.update({'server.socket_port': 9000}) WebSocketPlugin(cherrypy.engine).subscribe() cherrypy.tools.websocket = WebSocketTool() class Root(object): @cherrypy.expose def index(self): return 'some HTML with a websocket javascript connection' @cherrypy.expose def ws(self): pass cherrypy.quickstart(Root(), '/', config={'/ws': {'tools.websocket.on': True, 'tools.websocket.handler_cls': EchoWebSocket}}) Note that you can set the handler class on per-path basis, meaning you could also dynamically change the class based on other envrionmental settings (is the user authenticated for ex). """ import base64 from hashlib import sha1 import inspect import threading import cherrypy from cherrypy import Tool from cherrypy.process import plugins from cherrypy.wsgiserver import HTTPConnection, HTTPRequest from ws4py import WS_KEY, WS_VERSION from ws4py.exc import HandshakeError from ws4py.websocket import WebSocket from ws4py.compat import py3k, get_connection, detach_connection from ws4py.manager import WebSocketManager __all__ = ['WebSocketTool', 'WebSocketPlugin'] class WebSocketTool(Tool): def __init__(self): Tool.__init__(self, 'before_request_body', self.upgrade) def _setup(self): conf = self._merged_args() hooks = cherrypy.serving.request.hooks p = conf.pop("priority", getattr(self.callable, "priority", self._priority)) hooks.attach(self._point, self.callable, priority=p, **conf) hooks.attach('before_finalize', self.complete, priority=p) hooks.attach('on_end_resource', self.cleanup_headers, priority=70) hooks.attach('on_end_request', self.start_handler, priority=70) def upgrade(self, protocols=None, extensions=None, version=WS_VERSION, handler_cls=WebSocket, heartbeat_freq=None): """ Performs the upgrade of the connection to the WebSocket protocol. The provided protocols may be a list of WebSocket protocols supported by the instance of the tool. When no list is provided and no protocol is either during the upgrade, then the protocol parameter is not taken into account. On the other hand, if the protocol from the handshake isn't part of the provided list, the upgrade fails immediatly. """ request = cherrypy.serving.request request.process_request_body = False ws_protocols = None ws_location = None ws_version = version ws_key = None ws_extensions = [] if request.method != 'GET': raise HandshakeError('HTTP method must be a GET') for key, expected_value in [('Upgrade', 'websocket'), ('Connection', 'upgrade')]: actual_value = request.headers.get(key, '').lower() if not actual_value: raise HandshakeError('Header %s is not defined' % key) if expected_value not in actual_value: raise HandshakeError('Illegal value for header %s: %s' % (key, actual_value)) version = request.headers.get('Sec-WebSocket-Version') supported_versions = ', '.join([str(v) for v in ws_version]) version_is_valid = False if version: try: version = int(version) except: pass else: version_is_valid = version in ws_version if not version_is_valid: cherrypy.response.headers['Sec-WebSocket-Version'] = supported_versions raise HandshakeError('Unhandled or missing WebSocket version') key = request.headers.get('Sec-WebSocket-Key') if key: ws_key = base64.b64decode(key.encode('utf-8')) if len(ws_key) != 16: raise HandshakeError("WebSocket key's length is invalid") protocols = protocols or [] subprotocols = request.headers.get('Sec-WebSocket-Protocol') if subprotocols: ws_protocols = [] for s in subprotocols.split(','): s = s.strip() if s in protocols: ws_protocols.append(s) exts = extensions or [] extensions = request.headers.get('Sec-WebSocket-Extensions') if extensions: for ext in extensions.split(','): ext = ext.strip() if ext in exts: ws_extensions.append(ext) location = [] include_port = False if request.scheme == "https": location.append("wss://") include_port = request.local.port != 443 else: location.append("ws://") include_port = request.local.port != 80 location.append('localhost') if include_port: location.append(":%d" % request.local.port) location.append(request.path_info) if request.query_string != "": location.append("?%s" % request.query_string) ws_location = ''.join(location) response = cherrypy.serving.response response.stream = True response.status = '101 Switching Protocols' response.headers['Content-Type'] = 'text/plain' response.headers['Upgrade'] = 'websocket' response.headers['Connection'] = 'Upgrade' response.headers['Sec-WebSocket-Version'] = str(version) response.headers['Sec-WebSocket-Accept'] = base64.b64encode(sha1(key.encode('utf-8') + WS_KEY).digest()) if ws_protocols: response.headers['Sec-WebSocket-Protocol'] = ', '.join(ws_protocols) if ws_extensions: response.headers['Sec-WebSocket-Extensions'] = ','.join(ws_extensions) addr = (request.remote.ip, request.remote.port) ws_conn = get_connection(request.rfile.rfile) request.ws_handler = handler_cls(ws_conn, ws_protocols, ws_extensions, request.wsgi_environ.copy(), heartbeat_freq=heartbeat_freq) def complete(self): """ Sets some internal flags of CherryPy so that it doesn't close the socket down. """ self._set_internal_flags() def cleanup_headers(self): """ Some clients aren't that smart when it comes to headers lookup. """ response = cherrypy.response if not response.header_list: return headers = response.header_list[:] for (k, v) in headers: if k[:7] == 'Sec-Web': response.header_list.remove((k, v)) response.header_list.append((k.replace('Sec-Websocket', 'Sec-WebSocket'), v)) def start_handler(self): """ Runs at the end of the request processing by calling the opened method of the handler. """ request = cherrypy.request if not hasattr(request, 'ws_handler'): return addr = (request.remote.ip, request.remote.port) ws_handler = request.ws_handler request.ws_handler = None delattr(request, 'ws_handler') # By doing this we detach the socket from # the CherryPy stack avoiding memory leaks detach_connection(request.rfile.rfile) cherrypy.engine.publish('handle-websocket', ws_handler, addr) def _set_internal_flags(self): """ CherryPy has two internal flags that we are interested in to enable WebSocket within the server. They can't be set via a public API and considering I'd want to make this extension as compatible as possible whilst refraining in exposing more than should be within CherryPy, I prefer performing a bit of introspection to set those flags. Even by Python standards such introspection isn't the cleanest but it works well enough in this case. This also means that we do that only on WebSocket connections rather than globally and therefore we do not harm the rest of the HTTP server. """ current = inspect.currentframe() while True: if not current: break _locals = current.f_locals if 'self' in _locals: if type(_locals['self']) == HTTPRequest: _locals['self'].close_connection = True if type(_locals['self']) == HTTPConnection: _locals['self'].linger = True # HTTPConnection is more inner than # HTTPRequest so we can leave once # we're done here return _locals = None current = current.f_back class WebSocketPlugin(plugins.SimplePlugin): def __init__(self, bus): plugins.SimplePlugin.__init__(self, bus) self.manager = WebSocketManager() def start(self): self.bus.log("Starting WebSocket processing") self.bus.subscribe('stop', self.cleanup) self.bus.subscribe('handle-websocket', self.handle) self.bus.subscribe('websocket-broadcast', self.broadcast) self.manager.start() def stop(self): self.bus.log("Terminating WebSocket processing") self.bus.unsubscribe('stop', self.cleanup) self.bus.unsubscribe('handle-websocket', self.handle) self.bus.unsubscribe('websocket-broadcast', self.broadcast) def handle(self, ws_handler, peer_addr): """ Tracks the provided handler. :param ws_handler: websocket handler instance :param peer_addr: remote peer address for tracing purpose """ self.manager.add(ws_handler) def cleanup(self): """ Terminate all connections and clear the pool. Executed when the engine stops. """ self.manager.close_all() self.manager.stop() self.manager.join() def broadcast(self, message, binary=False): """ Broadcasts a message to all connected clients known to the server. :param message: a message suitable to pass to the send() method of the connected handler. :param binary: whether or not the message is a binary one """ self.manager.broadcast(message, binary) if __name__ == '__main__': import random cherrypy.config.update({'server.socket_host': '127.0.0.1', 'server.socket_port': 9000}) WebSocketPlugin(cherrypy.engine).subscribe() cherrypy.tools.websocket = WebSocketTool() class Root(object): @cherrypy.expose @cherrypy.tools.websocket(on=False) def ws(self): return """

""" % {'username': "User%d" % random.randint(0, 100)} @cherrypy.expose def index(self): cherrypy.log("Handler created: %s" % repr(cherrypy.request.ws_handler)) cherrypy.quickstart(Root(), '/', config={'/': {'tools.websocket.on': True, 'tools.websocket.handler_cls': EchoWebSocketHandler}}) python-ws4py-0.3.4/ws4py/server/geventserver.py000066400000000000000000000071711231577073400216270ustar00rootroot00000000000000# -*- coding: utf-8 -*- __doc__ = """ WSGI entities to support WebSocket from within gevent. Its usage is rather simple: .. code-block: python from gevent import monkey; monkey.patch_all() from ws4py.websocket import EchoWebSocket from ws4py.server.geventserver import WSGIServer from ws4py.server.wsgiutils import WebSocketWSGIApplication server = WSGIServer(('localhost', 9000), WebSocketWSGIApplication(handler_cls=EchoWebSocket)) server.serve_forever() """ import logging import sys import gevent from gevent.pywsgi import WSGIHandler, WSGIServer as _WSGIServer from gevent.pool import Pool from ws4py import format_addresses from ws4py.server.wsgiutils import WebSocketWSGIApplication logger = logging.getLogger('ws4py') __all__ = ['WebSocketWSGIHandler', 'WSGIServer', 'GEventWebSocketPool'] class WebSocketWSGIHandler(WSGIHandler): """ A WSGI handler that will perform the :rfc:`6455` upgrade and handshake before calling the WSGI application. If the incoming request doesn't have a `'Upgrade'` header, the handler will simply fallback to the gevent builtin's handler and process it as per usual. """ def run_application(self): upgrade_header = self.environ.get('HTTP_UPGRADE', '').lower() if upgrade_header: try: # Build and start the HTTP response self.environ['ws4py.socket'] = self.socket or self.environ['wsgi.input'].rfile._sock self.result = self.application(self.environ, self.start_response) or [] self.process_result() except: raise else: del self.environ['ws4py.socket'] self.socket = None self.rfile.close() ws = self.environ.pop('ws4py.websocket') if ws: self.server.pool.track(ws) else: gevent.pywsgi.WSGIHandler.run_application(self) class GEventWebSocketPool(Pool): """ Simple pool of bound websockets. Internally it uses a gevent group to track the websockets. The server should call the ``clear`` method to initiate the closing handshake when the server is shutdown. """ def track(self, websocket): logger.info("Managing websocket %s" % format_addresses(websocket)) return self.spawn(websocket.run) def clear(self): logger.info("Terminating server and all connected websockets") for greenlet in self: try: websocket = greenlet._run.im_self if websocket: websocket.close(1001, 'Server is shutting down') except: pass finally: self.discard(greenlet) class WSGIServer(_WSGIServer): handler_class = WebSocketWSGIHandler def __init__(self, *args, **kwargs): """ WSGI server that simply tracks websockets and send them a proper closing handshake when the server terminates. Other than that, the server is the same as its :class:`gevent.pywsgi.WSGIServer` base. """ _WSGIServer.__init__(self, *args, **kwargs) self.pool = GEventWebSocketPool() def stop(self, *args, **kwargs): self.pool.clear() _WSGIServer.stop(self, *args, **kwargs) if __name__ == '__main__': import os from ws4py import configure_logger configure_logger() from ws4py.websocket import EchoWebSocket server = WSGIServer(('127.0.0.1', 9000), WebSocketWSGIApplication(handler_cls=EchoWebSocket)) server.serve_forever() python-ws4py-0.3.4/ws4py/server/tulipserver.py000066400000000000000000000167461231577073400215040ustar00rootroot00000000000000# -*- coding: utf-8 -*- import base64 from hashlib import sha1 from email.parser import BytesHeaderParser import io import asyncio from ws4py import WS_KEY, WS_VERSION from ws4py.exc import HandshakeError from ws4py.websocket import WebSocket LF = b'\n' CRLF = b'\r\n' SPACE = b' ' EMPTY = b'' __all__ = ['WebSocketProtocol'] class WebSocketProtocol(asyncio.StreamReaderProtocol): def __init__(self, handler_cls): asyncio.StreamReaderProtocol.__init__(self, asyncio.StreamReader(), self._pseudo_connected) self.ws = handler_cls(self) def _pseudo_connected(self, reader, writer): pass def connection_made(self, transport): """ A peer is now connected and we receive an instance of the underlying :class:`asyncio.Transport`. We :class:`asyncio.StreamReader` is created and the transport is associated before the initial HTTP handshake is undertaken. """ #self.transport = transport #self.stream = asyncio.StreamReader() #self.stream.set_transport(transport) asyncio.StreamReaderProtocol.connection_made(self, transport) # Let make it concurrent for others to tag along f = asyncio.async(self.handle_initial_handshake()) f.add_done_callback(self.terminated) @property def writer(self): return self._stream_writer @property def reader(self): return self._stream_reader def terminated(self, f): if f.done() and not f.cancelled(): ex = f.exception() if ex: response = [b'HTTP/1.0 400 Bad Request'] response.append(b'Content-Length: 0') response.append(b'Connection: close') response.append(b'') response.append(b'') self.writer.write(CRLF.join(response)) self.ws.close_connection() def close(self): """ Initiate the websocket closing handshake which will eventuall lead to the underlying transport. """ self.ws.close() def timeout(self): self.ws.close_connection() if self.ws.started: self.ws.closed(1002, "Peer connection timed-out") def connection_lost(self, exc): """ The peer connection is now, the closing handshake won't work so let's not even try. However let's make the websocket handler be aware of it by calling its `closed` method. """ if exc is not None: self.ws.close_connection() if self.ws.started: self.ws.closed(1002, "Peer connection was lost") @asyncio.coroutine def handle_initial_handshake(self): """ Performs the HTTP handshake described in :rfc:`6455`. Note that this implementation is really basic and it is strongly advised against using it in production. It would probably break for most clients. If you want a better support for HTTP, please use a more reliable HTTP server implemented using asyncio. """ request_line = yield from self.next_line() method, uri, req_protocol = request_line.strip().split(SPACE, 2) # GET required if method.upper() != b'GET': raise HandshakeError('HTTP method must be a GET') headers = yield from self.read_headers() if req_protocol == b'HTTP/1.1' and 'Host' not in headers: raise ValueError("Missing host header") for key, expected_value in [('Upgrade', 'websocket'), ('Connection', 'upgrade')]: actual_value = headers.get(key, '').lower() if not actual_value: raise HandshakeError('Header %s is not defined' % str(key)) if expected_value not in actual_value: raise HandshakeError('Illegal value for header %s: %s' % (key, actual_value)) response_headers = {} ws_version = WS_VERSION version = headers.get('Sec-WebSocket-Version') supported_versions = ', '.join([str(v) for v in ws_version]) version_is_valid = False if version: try: version = int(version) except: pass else: version_is_valid = version in ws_version if not version_is_valid: response_headers['Sec-WebSocket-Version'] = supported_versions raise HandshakeError('Unhandled or missing WebSocket version') key = headers.get('Sec-WebSocket-Key') if key: ws_key = base64.b64decode(key.encode('utf-8')) if len(ws_key) != 16: raise HandshakeError("WebSocket key's length is invalid") protocols = [] ws_protocols = [] subprotocols = headers.get('Sec-WebSocket-Protocol') if subprotocols: for s in subprotocols.split(','): s = s.strip() if s in protocols: ws_protocols.append(s) exts = [] ws_extensions = [] extensions = headers.get('Sec-WebSocket-Extensions') if extensions: for ext in extensions.split(','): ext = ext.strip() if ext in exts: ws_extensions.append(ext) response = [req_protocol + b' 101 Switching Protocols'] response.append(b'Upgrade: websocket') response.append(b'Content-Type: text/plain') response.append(b'Content-Length: 0') response.append(b'Connection: Upgrade') response.append(b'Sec-WebSocket-Version:' + bytes(str(version), 'utf-8')) response.append(b'Sec-WebSocket-Accept:' + base64.b64encode(sha1(key.encode('utf-8') + WS_KEY).digest())) if ws_protocols: response.append(b'Sec-WebSocket-Protocol:' + b', '.join(ws_protocols)) if ws_extensions: response.append(b'Sec-WebSocket-Extensions:' + b','.join(ws_extensions)) response.append(b'') response.append(b'') self.writer.write(CRLF.join(response)) yield from self.handle_websocket() @asyncio.coroutine def handle_websocket(self): """ Starts the websocket process until the exchange is completed and terminated. """ yield from self.ws.run() @asyncio.coroutine def read_headers(self): """ Read all HTTP headers from the HTTP request and returns a dictionary of them. """ headers = b'' while True: line = yield from self.next_line() headers += line if line == CRLF: break return BytesHeaderParser().parsebytes(headers) @asyncio.coroutine def next_line(self): """ Reads data until \r\n is met and then return all read bytes. """ line = yield from self.reader.readline() if not line.endswith(CRLF): raise ValueError("Missing mandatory trailing CRLF") return line if __name__ == '__main__': from ws4py.async_websocket import EchoWebSocket loop = asyncio.get_event_loop() def start_server(): proto_factory = lambda: WebSocketProtocol(EchoWebSocket) return loop.create_server(proto_factory, '', 9007) s = loop.run_until_complete(start_server()) print('serving on', s.sockets[0].getsockname()) loop.run_forever() python-ws4py-0.3.4/ws4py/server/wsgirefserver.py000066400000000000000000000113701231577073400220010ustar00rootroot00000000000000# -*- coding: utf-8 -*- __doc__ = """ Add WebSocket support to the built-in WSGI server provided by the :py:mod:`wsgiref`. This is clearly not meant to be a production server so please consider this only for testing purpose. Mostly, this module overrides bits and pieces of the built-in classes so that it supports the WebSocket workflow. .. code-block:: python from wsgiref.simple_server import make_server from ws4py.websocket import EchoWebSocket from ws4py.server.wsgirefserver import WSGIServer, WebSocketWSGIRequestHandler from ws4py.server.wsgiutils import WebSocketWSGIApplication server = make_server('', 9000, server_class=WSGIServer, handler_class=WebSocketWSGIRequestHandler, app=WebSocketWSGIApplication(handler_cls=EchoWebSocket)) server.initialize_websockets_manager() server.serve_forever() .. note:: For some reason this server may fail against autobahntestsuite. """ import logging import sys from wsgiref.handlers import SimpleHandler from wsgiref.simple_server import WSGIRequestHandler, WSGIServer as _WSGIServer from wsgiref import util util._hoppish = {}.__contains__ from ws4py.manager import WebSocketManager from ws4py import format_addresses from ws4py.server.wsgiutils import WebSocketWSGIApplication from ws4py.compat import get_connection __all__ = ['WebSocketWSGIHandler', 'WebSocketWSGIRequestHandler', 'WSGIServer'] logger = logging.getLogger('ws4py') class WebSocketWSGIHandler(SimpleHandler): def setup_environ(self): """ Setup the environ dictionary and add the `'ws4py.socket'` key. Its associated value is the real socket underlying socket. """ SimpleHandler.setup_environ(self) self.environ['ws4py.socket'] = get_connection(self.environ['wsgi.input']) def finish_response(self): """ Completes the response and performs the following tasks: - Remove the `'ws4py.socket'` and `'ws4py.websocket'` environ keys. - Attach the returned websocket, if any, to the WSGI server using its ``link_websocket_to_server`` method. """ ws = None if self.environ: self.environ.pop('ws4py.socket', None) ws = self.environ.pop('ws4py.websocket', None) try: SimpleHandler.finish_response(self) except: if ws: ws.close(1011, reason='Something broke') raise else: if ws: self.request_handler.server.link_websocket_to_server(ws) class WebSocketWSGIRequestHandler(WSGIRequestHandler): def handle(self): """ Unfortunately the base class forces us to override the whole method to actually provide our wsgi handler. """ self.raw_requestline = self.rfile.readline() if not self.parse_request(): # An error code has been sent, just exit return # next line is where we'd have expect a configuration key somehow handler = WebSocketWSGIHandler( self.rfile, self.wfile, self.get_stderr(), self.get_environ() ) handler.request_handler = self # backpointer for logging handler.run(self.server.get_app()) class WSGIServer(_WSGIServer): def initialize_websockets_manager(self): """ Call thos to start the underlying websockets manager. Make sure to call it once your server is created. """ self.manager = WebSocketManager() self.manager.start() def shutdown_request(self, request): """ The base class would close our socket if we didn't override it. """ pass def link_websocket_to_server(self, ws): """ Call this from your WSGI handler when a websocket has been created. """ self.manager.add(ws) def server_close(self): """ Properly initiate closing handshakes on all websockets when the WSGI server terminates. """ if hasattr(self, 'manager'): self.manager.close_all() self.manager.stop() self.manager.join() delattr(self, 'manager') _WSGIServer.server_close(self) if __name__ == '__main__': from ws4py import configure_logger configure_logger() from wsgiref.simple_server import make_server from ws4py.websocket import EchoWebSocket server = make_server('', 9000, server_class=WSGIServer, handler_class=WebSocketWSGIRequestHandler, app=WebSocketWSGIApplication(handler_cls=EchoWebSocket)) server.initialize_websockets_manager() try: server.serve_forever() except KeyboardInterrupt: server.server_close() python-ws4py-0.3.4/ws4py/server/wsgiutils.py000066400000000000000000000136211231577073400211370ustar00rootroot00000000000000# -*- coding: utf-8 -*- __doc__ = """ This module provides a WSGI application suitable for a WSGI server such as gevent or wsgiref for instance. :pep:`333` couldn't foresee a protocol such as WebSockets but luckily the way the initial protocol upgrade was designed means that we can fit the handshake in a WSGI flow. The handshake validates the request against some internal or user-provided values and fails the request if the validation doesn't complete. On success, the provided WebSocket subclass is instanciated and stored into the `'ws4py.websocket'` environ key so that the WSGI server can handle it. The WSGI application returns an empty iterable since there is little value to return some content within the response to the handshake. A server wishing to support WebSocket via ws4py should: - Provide the real socket object to ws4py through the `'ws4py.socket'` environ key. We can't use `'wsgi.input'` as it may be wrapper to the socket we wouldn't know how to extract the socket from. - Look for the `'ws4py.websocket'` key in the environ when the application has returned and probably attach it to a :class:`ws4py.manager.WebSocketManager` instance so that the websocket runs its life. - Remove the `'ws4py.websocket'` and `'ws4py.socket'` environ keys once the application has returned. No need for these keys to persist. - Not close the underlying socket otherwise, well, your websocket will also shutdown. .. warning:: The WSGI application sets the `'Upgrade'` header response as specified by :rfc:`6455`. This is not tolerated by :pep:`333` since it's a hop-by-hop header. We expect most servers won't mind. """ import base64 from hashlib import sha1 import logging import sys from ws4py.websocket import WebSocket from ws4py.exc import HandshakeError from ws4py.compat import unicode, py3k from ws4py import WS_VERSION, WS_KEY, format_addresses logger = logging.getLogger('ws4py') __all__ = ['WebSocketWSGIApplication'] class WebSocketWSGIApplication(object): def __init__(self, protocols=None, extensions=None, handler_cls=WebSocket): """ WSGI application usable to complete the upgrade handshake by validating the requested protocols and extensions as well as the websocket version. If the upgrade validates, the `handler_cls` class is instanciated and stored inside the WSGI `environ` under the `'ws4py.websocket'` key to make it available to the WSGI handler. """ self.protocols = protocols self.extensions = extensions self.handler_cls = handler_cls def make_websocket(self, sock, protocols, extensions, environ): """ Initialize the `handler_cls` instance with the given negociated sets of protocols and extensions as well as the `environ` and `sock`. Stores then the instance in the `environ` dict under the `'ws4py.websocket'` key. """ websocket = self.handler_cls(sock, protocols, extensions, environ.copy()) environ['ws4py.websocket'] = websocket return websocket def __call__(self, environ, start_response): if environ.get('REQUEST_METHOD') != 'GET': raise HandshakeError('HTTP method must be a GET') for key, expected_value in [('HTTP_UPGRADE', 'websocket'), ('HTTP_CONNECTION', 'upgrade')]: actual_value = environ.get(key, '').lower() if not actual_value: raise HandshakeError('Header %s is not defined' % key) if expected_value not in actual_value: raise HandshakeError('Illegal value for header %s: %s' % (key, actual_value)) key = environ.get('HTTP_SEC_WEBSOCKET_KEY') if key: ws_key = base64.b64decode(key.encode('utf-8')) if len(ws_key) != 16: raise HandshakeError("WebSocket key's length is invalid") version = environ.get('HTTP_SEC_WEBSOCKET_VERSION') supported_versions = b', '.join([unicode(v).encode('utf-8') for v in WS_VERSION]) version_is_valid = False if version: try: version = int(version) except: pass else: version_is_valid = version in WS_VERSION if not version_is_valid: environ['websocket.version'] = unicode(version).encode('utf-8') raise HandshakeError('Unhandled or missing WebSocket version') ws_protocols = [] protocols = self.protocols or [] subprotocols = environ.get('HTTP_SEC_WEBSOCKET_PROTOCOL') if subprotocols: for s in subprotocols.split(','): s = s.strip() if s in protocols: ws_protocols.append(s) ws_extensions = [] exts = self.extensions or [] extensions = environ.get('HTTP_SEC_WEBSOCKET_EXTENSIONS') if extensions: for ext in extensions.split(','): ext = ext.strip() if ext in exts: ws_extensions.append(ext) accept_value = base64.b64encode(sha1(key.encode('utf-8') + WS_KEY).digest()) if py3k: accept_value = accept_value.decode('utf-8') upgrade_headers = [ ('Upgrade', 'websocket'), ('Connection', 'Upgrade'), ('Sec-WebSocket-Version', '%s' % version), ('Sec-WebSocket-Accept', accept_value), ] if ws_protocols: upgrade_headers.append(('Sec-WebSocket-Protocol', ', '.join(ws_protocols))) if ws_extensions: upgrade_headers.append(('Sec-WebSocket-Extensions', ','.join(ws_extensions))) start_response("101 Switching Protocols", upgrade_headers) self.make_websocket(environ['ws4py.socket'], ws_protocols, ws_extensions, environ) return [] python-ws4py-0.3.4/ws4py/streaming.py000066400000000000000000000321341231577073400175700ustar00rootroot00000000000000# -*- coding: utf-8 -*- import struct from struct import unpack from ws4py.utf8validator import Utf8Validator from ws4py.messaging import TextMessage, BinaryMessage, CloseControlMessage,\ PingControlMessage, PongControlMessage from ws4py.framing import Frame, OPCODE_CONTINUATION, OPCODE_TEXT, \ OPCODE_BINARY, OPCODE_CLOSE, OPCODE_PING, OPCODE_PONG from ws4py.exc import FrameTooLargeException, ProtocolException, InvalidBytesError,\ TextFrameEncodingException, UnsupportedFrameTypeException, StreamClosed from ws4py.compat import py3k VALID_CLOSING_CODES = [1000, 1001, 1002, 1003, 1007, 1008, 1009, 1010, 1011] class Stream(object): def __init__(self, always_mask=False, expect_masking=True): """ Represents a websocket stream of bytes flowing in and out. The stream doesn't know about the data provider itself and doesn't even know about sockets. Instead the stream simply yields for more bytes whenever it requires them. The stream owner is responsible to provide the stream with those bytes until a frame can be interpreted. .. code-block:: python :linenos: >>> s = Stream() >>> s.parser.send(BYTES) >>> s.has_messages False >>> s.parser.send(MORE_BYTES) >>> s.has_messages True >>> s.message Set ``always_mask`` to mask all frames built. Set ``expect_masking`` to indicate masking will be checked on all parsed frames. """ self.message = None """ Parsed test or binary messages. Whenever the parser reads more bytes from a fragment message, those bytes are appended to the most recent message. """ self.pings = [] """ Parsed ping control messages. They are instances of :class:`ws4py.messaging.PingControlMessage` """ self.pongs = [] """ Parsed pong control messages. They are instances of :class:`ws4py.messaging.PongControlMessage` """ self.closing = None """ Parsed close control messsage. Instance of :class:`ws4py.messaging.CloseControlMessage` """ self.errors = [] """ Detected errors while parsing. Instances of :class:`ws4py.messaging.CloseControlMessage` """ self._parser = None """ Parser in charge to process bytes it is fed with. """ self.always_mask = always_mask self.expect_masking = expect_masking @property def parser(self): if self._parser is None: self._parser = self.receiver() # Python generators must be initialized once. next(self.parser) return self._parser def _cleanup(self): """ Frees the stream's resources rendering it unusable. """ self.message = None if self._parser is not None: if not self._parser.gi_running: self._parser.close() self._parser = None self.errors = None self.pings = None self.pongs = None self.closing = None def text_message(self, text): """ Returns a :class:`ws4py.messaging.TextMessage` instance ready to be built. Convenience method so that the caller doesn't need to import the :class:`ws4py.messaging.TextMessage` class itself. """ return TextMessage(text=text) def binary_message(self, bytes): """ Returns a :class:`ws4py.messaging.BinaryMessage` instance ready to be built. Convenience method so that the caller doesn't need to import the :class:`ws4py.messaging.BinaryMessage` class itself. """ return BinaryMessage(bytes) @property def has_message(self): """ Checks if the stream has received any message which, if fragmented, is now completed. """ if self.message is not None: return self.message.completed return False def close(self, code=1000, reason=''): """ Returns a close control message built from a :class:`ws4py.messaging.CloseControlMessage` instance, using the given status ``code`` and ``reason`` message. """ return CloseControlMessage(code=code, reason=reason) def ping(self, data=''): """ Returns a ping control message built from a :class:`ws4py.messaging.PingControlMessage` instance. """ return PingControlMessage(data).single(mask=self.always_mask) def pong(self, data=''): """ Returns a ping control message built from a :class:`ws4py.messaging.PongControlMessage` instance. """ return PongControlMessage(data).single(mask=self.always_mask) def receiver(self): """ Parser that keeps trying to interpret bytes it is fed with as incoming frames part of a message. Control message are single frames only while data messages, like text and binary, may be fragmented accross frames. The way it works is by instanciating a :class:`wspy.framing.Frame` object, then running its parser generator which yields how much bytes it requires to performs its task. The stream parser yields this value to its caller and feeds the frame parser. When the frame parser raises :exc:`StopIteration`, the stream parser tries to make sense of the parsed frame. It dispatches the frame's bytes to the most appropriate message type based on the frame's opcode. Overall this makes the stream parser totally agonstic to the data provider. """ utf8validator = Utf8Validator() running = True frame = None while running: frame = Frame() while 1: try: some_bytes = (yield next(frame.parser)) frame.parser.send(some_bytes) except GeneratorExit: running = False break except StopIteration: frame._cleanup() some_bytes = frame.body # Let's avoid unmasking when there is no payload if some_bytes: if frame.masking_key and self.expect_masking: some_bytes = frame.unmask(some_bytes) elif not frame.masking_key and self.expect_masking: msg = CloseControlMessage(code=1002, reason='Missing masking when expected') self.errors.append(msg) break elif frame.masking_key and not self.expect_masking: msg = CloseControlMessage(code=1002, reason='Masked when not expected') self.errors.append(msg) break else: # If we reach this stage, it's because # the frame wasn't masked and we didn't expect # it anyway. Therefore, on py2k, the bytes # are actually a str object and can't be used # in the utf8 validator as we need integers # when we get each byte one by one. # Our only solution here is to convert our # string to a bytearray. some_bytes = bytearray(some_bytes) if frame.opcode == OPCODE_TEXT: if self.message and not self.message.completed: # We got a text frame before we completed the previous one msg = CloseControlMessage(code=1002, reason='Received a new message before completing previous') self.errors.append(msg) break m = TextMessage(some_bytes) m.completed = (frame.fin == 1) self.message = m if some_bytes: is_valid, end_on_code_point, _, _ = utf8validator.validate(some_bytes) if not is_valid or (m.completed and not end_on_code_point): self.errors.append(CloseControlMessage(code=1007, reason='Invalid UTF-8 bytes')) break elif frame.opcode == OPCODE_BINARY: if self.message and not self.message.completed: # We got a text frame before we completed the previous one msg = CloseControlMessage(code=1002, reason='Received a new message before completing previous') self.errors.append(msg) break m = BinaryMessage(some_bytes) m.completed = (frame.fin == 1) self.message = m elif frame.opcode == OPCODE_CONTINUATION: m = self.message if m is None: self.errors.append(CloseControlMessage(code=1002, reason='Message not started yet')) break m.extend(some_bytes) m.completed = (frame.fin == 1) if m.opcode == OPCODE_TEXT: if some_bytes: is_valid, end_on_code_point, _, _ = utf8validator.validate(some_bytes) if not is_valid or (m.completed and not end_on_code_point): self.errors.append(CloseControlMessage(code=1007, reason='Invalid UTF-8 bytes')) break elif frame.opcode == OPCODE_CLOSE: code = 1000 reason = "" if frame.payload_length == 0: self.closing = CloseControlMessage(code=1000) elif frame.payload_length == 1: self.closing = CloseControlMessage(code=1002, reason='Payload has invalid length') else: try: # at this stage, some_bytes have been unmasked # so actually are held in a bytearray code = int(unpack("!H", bytes(some_bytes[0:2]))[0]) except struct.error: code = 1002 reason = 'Failed at decoding closing code' else: # Those codes are reserved or plainly forbidden if code not in VALID_CLOSING_CODES and not (2999 < code < 5000): reason = 'Invalid Closing Frame Code: %d' % code code = 1002 elif frame.payload_length > 1: reason = some_bytes[2:] if frame.masking_key else frame.body[2:] if not py3k: reason = bytearray(reason) is_valid, end_on_code_point, _, _ = utf8validator.validate(reason) if not is_valid or not end_on_code_point: self.errors.append(CloseControlMessage(code=1007, reason='Invalid UTF-8 bytes')) break reason = bytes(reason) self.closing = CloseControlMessage(code=code, reason=reason) elif frame.opcode == OPCODE_PING: self.pings.append(PingControlMessage(some_bytes)) elif frame.opcode == OPCODE_PONG: self.pongs.append(PongControlMessage(some_bytes)) else: self.errors.append(CloseControlMessage(code=1003)) break except ProtocolException: self.errors.append(CloseControlMessage(code=1002)) break except FrameTooLargeException: self.errors.append(CloseControlMessage(code=1002, reason="Frame was too large")) break frame._cleanup() frame.body = None frame = None if self.message is not None and self.message.completed: utf8validator.reset() utf8validator.reset() utf8validator = None self._cleanup() python-ws4py-0.3.4/ws4py/utf8validator.py000066400000000000000000000114151231577073400203720ustar00rootroot00000000000000# coding=utf-8 ############################################################################### ## ## Copyright 2011 Tavendo GmbH ## ## Note: ## ## This code is a Python implementation of the algorithm ## ## "Flexible and Economical UTF-8 Decoder" ## ## by Bjoern Hoehrmann ## ## bjoern@hoehrmann.de ## http://bjoern.hoehrmann.de/utf-8/decoder/dfa/ ## ## Licensed under the Apache License, Version 2.0 (the "License"); ## you may not use this file except in compliance with the License. ## You may obtain a copy of the License at ## ## http://www.apache.org/licenses/LICENSE-2.0 ## ## Unless required by applicable law or agreed to in writing, software ## distributed under the License is distributed on an "AS IS" BASIS, ## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. ## See the License for the specific language governing permissions and ## limitations under the License. ## ############################################################################### class Utf8Validator(object): """ Incremental UTF-8 validator with constant memory consumption (minimal state). Implements the algorithm "Flexible and Economical UTF-8 Decoder" by Bjoern Hoehrmann (http://bjoern.hoehrmann.de/utf-8/decoder/dfa/). """ ## DFA transitions UTF8VALIDATOR_DFA = [ 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, # 00..1f 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, # 20..3f 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, # 40..5f 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, # 60..7f 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, # 80..9f 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, # a0..bf 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, # c0..df 0xa,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x4,0x3,0x3, # e0..ef 0xb,0x6,0x6,0x6,0x5,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8, # f0..ff 0x0,0x1,0x2,0x3,0x5,0x8,0x7,0x1,0x1,0x1,0x4,0x6,0x1,0x1,0x1,0x1, # s0..s0 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,1,1,1,0,1,0,1,1,1,1,1,1, # s1..s2 1,2,1,1,1,1,1,2,1,2,1,1,1,1,1,1,1,1,1,1,1,1,1,2,1,1,1,1,1,1,1,1, # s3..s4 1,2,1,1,1,1,1,1,1,2,1,1,1,1,1,1,1,1,1,1,1,1,1,3,1,3,1,1,1,1,1,1, # s5..s6 1,3,1,1,1,1,1,3,1,3,1,1,1,1,1,1,1,3,1,1,1,1,1,1,1,1,1,1,1,1,1,1, # s7..s8 ] UTF8_ACCEPT = 0 UTF8_REJECT = 1 def __init__(self): self.reset() def decode(self, b): """ Eat one UTF-8 octet, and validate on the fly. Returns UTF8_ACCEPT when enough octets have been consumed, in which case self.codepoint contains the decoded Unicode code point. Returns UTF8_REJECT when invalid UTF-8 was encountered. Returns some other positive integer when more octets need to be eaten. """ type = Utf8Validator.UTF8VALIDATOR_DFA[b] if self.state != Utf8Validator.UTF8_ACCEPT: self.codepoint = (b & 0x3f) | (self.codepoint << 6) else: self.codepoint = (0xff >> type) & b self.state = Utf8Validator.UTF8VALIDATOR_DFA[256 + self.state * 16 + type] return self.state def reset(self): """ Reset validator to start new incremental UTF-8 decode/validation. """ self.state = Utf8Validator.UTF8_ACCEPT self.codepoint = 0 self.i = 0 def validate(self, ba): """ Incrementally validate a chunk of bytes provided as bytearray. Will return a quad (valid?, endsOnCodePoint?, currentIndex, totalIndex). As soon as an octet is encountered which renders the octet sequence invalid, a quad with valid? == False is returned. currentIndex returns the index within the currently consumed chunk, and totalIndex the index within the total consumed sequence that was the point of bail out. When valid? == True, currentIndex will be len(ba) and totalIndex the total amount of consumed bytes. """ state = self.state DFA = Utf8Validator.UTF8VALIDATOR_DFA i = 0 # make sure 'i' is set if when 'ba' is empty for i, b in enumerate(ba): ## optimized version of decode(), since we are not interested in actual code points state = DFA[256 + (state << 4) + DFA[b]] if state == Utf8Validator.UTF8_REJECT: self.i += i self.state = state return False, False, i, self.i self.i += i self.state = state return True, state == Utf8Validator.UTF8_ACCEPT, i, self.i python-ws4py-0.3.4/ws4py/websocket.py000066400000000000000000000327411231577073400175710ustar00rootroot00000000000000# -*- coding: utf-8 -*- import logging import socket import time import threading import types from ws4py import WS_KEY, WS_VERSION from ws4py.exc import HandshakeError, StreamClosed from ws4py.streaming import Stream from ws4py.messaging import Message, PongControlMessage from ws4py.compat import basestring, unicode DEFAULT_READING_SIZE = 2 logger = logging.getLogger('ws4py') __all__ = ['WebSocket', 'EchoWebSocket', 'Heartbeat'] class Heartbeat(threading.Thread): def __init__(self, websocket, frequency=2.0): """ Runs at a periodic interval specified by `frequency` by sending an unsolicitated pong message to the connected peer. If the message fails to be sent and a socket error is raised, we close the websocket socket automatically, triggering the `closed` handler. """ threading.Thread.__init__(self) self.websocket = websocket self.frequency = frequency def __enter__(self): if self.frequency: self.start() return self def __exit__(self, exc_type, exc_value, exc_tb): self.stop() def stop(self): self.running = False def run(self): self.running = True while self.running: time.sleep(self.frequency) if self.websocket.terminated: break try: self.websocket.send(PongControlMessage(data='beep')) except socket.error: logger.info("Heartbeat failed") self.websocket.server_terminated = True self.websocket.close_connection() break class WebSocket(object): """ Represents a websocket endpoint and provides a high level interface to drive the endpoint. """ def __init__(self, sock, protocols=None, extensions=None, environ=None, heartbeat_freq=None): """ The ``sock`` is an opened connection resulting from the websocket handshake. If ``protocols`` is provided, it is a list of protocols negotiated during the handshake as is ``extensions``. If ``environ`` is provided, it is a copy of the WSGI environ dictionnary from the underlying WSGI server. """ self.stream = Stream(always_mask=False) """ Underlying websocket stream that performs the websocket parsing to high level objects. By default this stream never masks its messages. Clients using this class should set the ``stream.always_mask`` fields to ``True`` and ``stream.expect_masking`` fields to ``False``. """ self.protocols = protocols """ List of protocols supported by this endpoint. Unused for now. """ self.extensions = extensions """ List of extensions supported by this endpoint. Unused for now. """ self.sock = sock """ Underlying connection. """ self.client_terminated = False """ Indicates if the client has been marked as terminated. """ self.server_terminated = False """ Indicates if the server has been marked as terminated. """ self.reading_buffer_size = DEFAULT_READING_SIZE """ Current connection reading buffer size. """ self.environ = environ """ WSGI environ dictionary. """ self.heartbeat_freq = heartbeat_freq """ At which interval the heartbeat will be running. Set this to `0` or `None` to disable it entirely. """ self._local_address = None self._peer_address = None @property def local_address(self): """ Local endpoint address as a tuple """ if not self._local_address: self._local_address = self.sock.getsockname() if len(self._local_address) == 4: self._local_address = self._local_address[:2] return self._local_address @property def peer_address(self): """ Peer endpoint address as a tuple """ if not self._peer_address: self._peer_address = self.sock.getpeername() if len(self._peer_address) == 4: self._peer_address = self._peer_address[:2] return self._peer_address def opened(self): """ Called by the server when the upgrade handshake has succeeeded. """ pass def close(self, code=1000, reason=''): """ Call this method to initiate the websocket connection closing by sending a close frame to the connected peer. The ``code`` is the status code representing the termination's reason. Once this method is called, the ``server_terminated`` attribute is set. Calling this method several times is safe as the closing frame will be sent only the first time. .. seealso:: Defined Status Codes http://tools.ietf.org/html/rfc6455#section-7.4.1 """ if not self.server_terminated: self.server_terminated = True self._write(self.stream.close(code=code, reason=reason).single(mask=self.stream.always_mask)) def closed(self, code, reason=None): """ Called when the websocket stream and connection are finally closed. The provided ``code`` is status set by the other point and ``reason`` is a human readable message. .. seealso:: Defined Status Codes http://tools.ietf.org/html/rfc6455#section-7.4.1 """ pass @property def terminated(self): """ Returns ``True`` if both the client and server have been marked as terminated. """ return self.client_terminated is True and self.server_terminated is True @property def connection(self): return self.sock def close_connection(self): """ Shutdowns then closes the underlying connection. """ if self.sock: try: self.sock.shutdown(socket.SHUT_RDWR) self.sock.close() except: pass finally: self.sock = None def ponged(self, pong): """ Pong message, as a :class:`messaging.PongControlMessage` instance, received on the stream. """ pass def received_message(self, message): """ Called whenever a complete ``message``, binary or text, is received and ready for application's processing. The passed message is an instance of :class:`messaging.TextMessage` or :class:`messaging.BinaryMessage`. .. note:: You should override this method in your subclass. """ pass def _write(self, b): """ Trying to prevent a write operation on an already closed websocket stream. This cannot be bullet proof but hopefully will catch almost all use cases. """ if self.terminated or self.sock is None: raise RuntimeError("Cannot send on a terminated websocket") self.sock.sendall(b) def send(self, payload, binary=False): """ Sends the given ``payload`` out. If ``payload`` is some bytes or a bytearray, then it is sent as a single message not fragmented. If ``payload`` is a generator, each chunk is sent as part of fragmented message. If ``binary`` is set, handles the payload as a binary message. """ message_sender = self.stream.binary_message if binary else self.stream.text_message if isinstance(payload, basestring) or isinstance(payload, bytearray): m = message_sender(payload).single(mask=self.stream.always_mask) self._write(m) elif isinstance(payload, Message): data = payload.single(mask=self.stream.always_mask) self._write(data) elif type(payload) == types.GeneratorType: bytes = next(payload) first = True for chunk in payload: self._write(message_sender(bytes).fragment(first=first, mask=self.stream.always_mask)) bytes = chunk first = False self._write(message_sender(bytes).fragment(last=True, mask=self.stream.always_mask)) else: raise ValueError("Unsupported type '%s' passed to send()" % type(payload)) def once(self): """ Performs the operation of reading from the underlying connection in order to feed the stream of bytes. We start with a small size of two bytes to be read from the connection so that we can quickly parse an incoming frame header. Then the stream indicates whatever size must be read from the connection since it knows the frame payload length. It returns `False` if an error occurred at the socket level or during the bytes processing. Otherwise, it returns `True`. """ if self.terminated: logger.debug("WebSocket is already terminated") return False try: b = self.sock.recv(self.reading_buffer_size) except socket.error: logger.exception("Failed to receive data") return False else: if not self.process(b): return False return True def terminate(self): """ Completes the websocket by calling the `closed` method either using the received closing code and reason, or when none was received, using the special `1006` code. Finally close the underlying connection for good and cleanup resources by unsetting the `environ` and `stream` attributes. """ s = self.stream self.client_terminated = self.server_terminated = True try: if not s.closing: self.closed(1006, "Going away") else: self.closed(s.closing.code, s.closing.reason) finally: self.close_connection() # Cleaning up resources s._cleanup() self.stream = None self.environ = None def process(self, bytes): """ Takes some bytes and process them through the internal stream's parser. If a message of any kind is found, performs one of these actions: * A closing message will initiate the closing handshake * Errors will initiate a closing handshake * A message will be passed to the ``received_message`` method * Pings will see pongs be sent automatically * Pongs will be passed to the ``ponged`` method The process should be terminated when this method returns ``False``. """ s = self.stream if not bytes and self.reading_buffer_size > 0: return False self.reading_buffer_size = s.parser.send(bytes) or DEFAULT_READING_SIZE if s.closing is not None: logger.debug("Closing message received (%d) '%s'" % (s.closing.code, s.closing.reason)) if not self.server_terminated: self.close(s.closing.code, s.closing.reason) else: self.client_terminated = True s = None return False if s.errors: for error in s.errors: logger.debug("Error message received (%d) '%s'" % (error.code, error.reason)) self.close(error.code, error.reason) s.errors = [] s = None return False if s.has_message: self.received_message(s.message) if s.message is not None: s.message.data = None s.message = None s = None return True if s.pings: for ping in s.pings: self._write(s.pong(ping.data)) s.pings = [] if s.pongs: for pong in s.pongs: self.ponged(pong) s.pongs = [] s = None return True def run(self): """ Performs the operation of reading from the underlying connection in order to feed the stream of bytes. We start with a small size of two bytes to be read from the connection so that we can quickly parse an incoming frame header. Then the stream indicates whatever size must be read from the connection since it knows the frame payload length. Note that we perform some automatic opererations: * On a closing message, we respond with a closing message and finally close the connection * We respond to pings with pong messages. * Whenever an error is raised by the stream parsing, we initiate the closing of the connection with the appropiate error code. This method is blocking and should likely be run in a thread. """ self.sock.setblocking(True) with Heartbeat(self, frequency=self.heartbeat_freq): s = self.stream try: self.opened() while not self.terminated: if not self.once(): break finally: self.terminate() class EchoWebSocket(WebSocket): def received_message(self, message): """ Automatically sends back the provided ``message`` to its originating endpoint. """ self.send(message.data, message.is_binary)