From aa0494dd29aea3fa9ad8f84c973e62656d84aad3 Mon Sep 17 00:00:00 2001 From: Nick Snell Date: Wed, 4 Nov 2015 17:44:15 +0000 Subject: [PATCH] Base fork of https://bitbucket.org/Jeffrey/gevent-websocket with Py3 support --- .gitignore | 80 ++ AUTHORS | 9 + LICENSE | 13 + MANIFEST.in | 4 + README.rst | 98 +++ docs/Makefile | 177 ++++ docs/conf.py | 255 ++++++ docs/index.rst | 151 ++++ examples/chat/chat.py | 67 ++ examples/chat/static/.bowerrc | 3 + examples/chat/static/.gitignore | 4 + examples/chat/static/bower.json | 8 + examples/chat/static/config.rb | 25 + examples/chat/static/js/app.js | 3 + examples/chat/static/scss/_settings.scss | 995 +++++++++++++++++++++++ examples/chat/static/scss/app.scss | 2 + examples/chat/templates/index.html | 111 +++ examples/echoserver.py | 38 + examples/plot_graph.html | 43 + examples/plot_graph.py | 41 + examples/wamp/wamp_example.html | 82 ++ examples/wamp/wamp_example.py | 49 ++ geventwebsocket/__init__.py | 21 + geventwebsocket/exceptions.py | 19 + geventwebsocket/gunicorn/__init__.py | 0 geventwebsocket/gunicorn/workers.py | 6 + geventwebsocket/handler.py | 285 +++++++ geventwebsocket/logging.py | 31 + geventwebsocket/protocols/__init__.py | 0 geventwebsocket/protocols/base.py | 35 + geventwebsocket/protocols/wamp.py | 236 ++++++ geventwebsocket/resource.py | 106 +++ geventwebsocket/server.py | 34 + geventwebsocket/utf8validator.py | 130 +++ geventwebsocket/utils.py | 45 + geventwebsocket/websocket.py | 544 +++++++++++++ perf-requirements.txt | 5 + requirements.txt | 3 + setup.py | 30 + test-requirements.txt | 3 + tests/run_autobahn_tests.py | 99 +++ 41 files changed, 3890 insertions(+) create mode 100644 .gitignore create mode 100644 AUTHORS create mode 100644 LICENSE create mode 100644 MANIFEST.in create mode 100644 README.rst create mode 100644 docs/Makefile create mode 100644 docs/conf.py create mode 100644 docs/index.rst create mode 100644 examples/chat/chat.py create mode 100644 examples/chat/static/.bowerrc create mode 100644 examples/chat/static/.gitignore create mode 100644 examples/chat/static/bower.json create mode 100644 examples/chat/static/config.rb create mode 100644 examples/chat/static/js/app.js create mode 100644 examples/chat/static/scss/_settings.scss create mode 100644 examples/chat/static/scss/app.scss create mode 100644 examples/chat/templates/index.html create mode 100644 examples/echoserver.py create mode 100644 examples/plot_graph.html create mode 100644 examples/plot_graph.py create mode 100644 examples/wamp/wamp_example.html create mode 100644 examples/wamp/wamp_example.py create mode 100644 geventwebsocket/__init__.py create mode 100644 geventwebsocket/exceptions.py create mode 100644 geventwebsocket/gunicorn/__init__.py create mode 100644 geventwebsocket/gunicorn/workers.py create mode 100644 geventwebsocket/handler.py create mode 100644 geventwebsocket/logging.py create mode 100644 geventwebsocket/protocols/__init__.py create mode 100644 geventwebsocket/protocols/base.py create mode 100644 geventwebsocket/protocols/wamp.py create mode 100644 geventwebsocket/resource.py create mode 100644 geventwebsocket/server.py create mode 100644 geventwebsocket/utf8validator.py create mode 100644 geventwebsocket/utils.py create mode 100644 geventwebsocket/websocket.py create mode 100644 perf-requirements.txt create mode 100644 requirements.txt create mode 100644 setup.py create mode 100644 test-requirements.txt create mode 100755 tests/run_autobahn_tests.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..84a42c1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,80 @@ +# Compiled source # +################### +*.com +*.class +*.dll +*.exe +*.o +*.so +*.pyc +__pycache__ + +# Packages # +############ +# it's better to unpack these files and commit the raw source +# git has its own built in compression methods +*.7z +*.dmg +*.gz +*.iso +*.jar +*.rar +*.tar +*.zip +*.csv + +# Logs and databases # +###################### +*.log +*.sql +*.sqlite + +# OS generated files # +###################### +.DS_Store +ehthumbs.db +Icon? +Thumbs.db + +# Crap +*~ + +.idea/workspace.xml + +# Don't include env folders/files +bin/ +build/ +include/ +lib/ +dev/ +.Python +_site/ +venv/ +#docs/_build +__py +.coverage +settings/local.py +docs/_build +local_settings.py +.idea/ +db/data.db +backup_test/ +.hg/ +.hgcheck/ +/utils/duff_ids.txt +/utils/breaders.txt +venv/ +venv/.Python +node_modules +/dump.rdb +/prod.psql +tiler +latest.dump +__pycache__ + +# Settings +exoticdirect/settings/local_settings.py + +# Static file ignore (ignore compiled files) +exoticdirect/static/ +dev/ diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 0000000..02de709 --- /dev/null +++ b/AUTHORS @@ -0,0 +1,9 @@ +This Websocket library for Gevent is written and maintained by + + Jeffrey Gelens + + +Contributors: + + Denis Bilenko + Lon Ingram diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..3101606 --- /dev/null +++ b/LICENSE @@ -0,0 +1,13 @@ + Copyright 2011-2013 Jeffrey Gelens + + 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. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..a0e8ea9 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,4 @@ +include LICENSE +include AUTHORS +include README.rst +include MANIFEST.in diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..73984ee --- /dev/null +++ b/README.rst @@ -0,0 +1,98 @@ +================ +gevent-websocket +================ + +`gevent-websocket`_ is a WebSocket library for the gevent_ networking library. + +Features include: + +- Integration on both socket level or using an abstract interface. +- RPC and PubSub framework using `WAMP`_ (WebSocket Application + Messaging Protocol). +- Easily extendible using a simple WebSocket protocol plugin API + + +:: + + from geventwebsocket import WebSocketServer, WebSocketApplication, Resource + + class EchoApplication(WebSocketApplication): + def on_open(self): + print "Connection opened" + + def on_message(self, message): + self.ws.send(message) + + def on_close(self, reason): + print reason + + WebSocketServer( + ('', 8000), + Resource({'/': EchoApplication}) + ).serve_forever() + +or a low level implementation:: + + from gevent import pywsgi + from geventwebsocket.handler import WebSocketHandler + + def websocket_app(environ, start_response): + if environ["PATH_INFO"] == '/echo': + ws = environ["wsgi.websocket"] + message = ws.receive() + ws.send(message) + + server = pywsgi.WSGIServer(("", 8000), websocket_app, + handler_class=WebSocketHandler) + server.serve_forever() + +More examples can be found in the ``examples`` directory. Hopefully more +documentation will be available soon. + +Installation +------------ + +The easiest way to install gevent-websocket is directly from PyPi_ using pip or +setuptools by running the commands below:: + + $ pip install gevent-websocket + + +Gunicorn Worker +^^^^^^^^^^^^^^^ + +Using Gunicorn it is even more easy to start a server. Only the +`websocket_app` from the previous example is required to start the server. +Start Gunicorn using the following command and worker class to enable WebSocket +funtionality for the application. + +:: + + gunicorn -k "geventwebsocket.gunicorn.workers.GeventWebSocketWorker" wsgi:websocket_app + +Performance +^^^^^^^^^^^ + +`gevent-websocket`_ is pretty fast, but can be accelerated further by +installing `wsaccel `_ and `ujson` or `simplejson`:: + + $ pip install wsaccel ujson + +`gevent-websocket`_ automatically detects ``wsaccell`` and uses the Cython +implementation for UTF8 validation and later also frame masking and +demasking. + +Get in touch +^^^^^^^^^^^^ + +Get in touch on IRC #gevent on Freenode or on the Gevent `mailinglist +`_. Issues can be created +on `Bitbucket `_. + +.. _WAMP: http://www.wamp.ws +.. _gevent-websocket: http://www.bitbucket.org/Jeffrey/gevent-websocket/ +.. _gevent: http://www.gevent.org/ +.. _Jeffrey Gelens: http://www.gelens.org/ +.. _PyPi: http://pypi.python.org/pypi/gevent-websocket/ +.. _repository: http://www.bitbucket.org/Jeffrey/gevent-websocket/ +.. _RFC6455: http://datatracker.ietf.org/doc/rfc6455/?include_text=1 diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..9dc07ae --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,177 @@ +# Makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +PAPER = +BUILDDIR = _build + +# User-friendly check for sphinx-build +ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) +$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) +endif + +# 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 " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" + @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 " xml to make Docutils-native XML files" + @echo " pseudoxml to make pseudoxml-XML files for display purposes" + @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/gevent-websocket.qhcp" + @echo "To view the help file:" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/gevent-websocket.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/gevent-websocket" + @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/gevent-websocket" + @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." + +latexpdfja: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through platex and dvipdfmx..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja + @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." + +xml: + $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml + @echo + @echo "Build finished. The XML files are in $(BUILDDIR)/xml." + +pseudoxml: + $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml + @echo + @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..4d74fdd --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,255 @@ +# -*- coding: utf-8 -*- +# +# gevent-websocket documentation build configuration file, created by +# sphinx-quickstart on Mon Apr 15 15:02:46 2013. +# +# 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('.')) + +# -- 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.todo', 'sphinx.ext.ifconfig', 'sphinx.ext.viewcode'] + +# 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 = 'gevent-websocket' +copyright = '2013, Jeffrey Gelens' + +# 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. +import pkg_resources +try: + release = pkg_resources.get_distribution('gevent-websocket').version +except pkg_resources.DistributionNotFound: + print("Install gevent-websocket in order to build the docs") + sys.exit(1) +del pkg_resources + +version = ".".join(release.split('.')[:2]) +# The full version, including alpha/beta/rc tags. + +# 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 = True + +# 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 = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# A list of ignored prefixes for module index sorting. +#modindex_common_prefix = [] + +# If true, keep warnings as "system message" paragraphs in the built documents. +#keep_warnings = False + + +# -- Options for HTML output --------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +html_theme = 'default' + +# 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 = 'gevent-websocketdoc' + + +# -- 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', 'gevent-websocket.tex', 'gevent-websocket Documentation', + 'Jeffrey Gelens', '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', 'gevent-websocket', 'gevent-websocket Documentation', + ['Jeffrey Gelens'], 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', 'gevent-websocket', 'gevent-websocket Documentation', + 'Jeffrey Gelens', 'gevent-websocket', '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' + +# If true, do not generate a @detailmenu in the "Top" node's menu. +#texinfo_no_detailmenu = False diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..97db84f --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,151 @@ +gevent-websocket +================ + +gevent-websocket is a `WebSocket`_ library for the gevent_ networking library +written written and maintained by `Jeffrey Gelens`_ It is licensed under the BSD license. + +:: + + from geventwebsocket import WebSocketServer, WebSocketApplication, Resource + + class EchoApplication(WebSocketApplication): + def on_message(self, message): + self.ws.send(message) + + WebSocketServer( + ('', 8000), + Resource({'/': EchoApplication}) + ) + + +Add WebSockets to your WSGI application +======================================= + +It isn't necessary to use the build-in `WebSocketServer` to start using +WebSockets. WebSockers can be added to existing applications very easy by +making the non-standard `wsgi.websocket` variable available in the WSGI +environment. An example using `Flask `_ follows:: + + from geventwebsocket import WebSocketServer, WebSocketError + from flask import Flask, request, render_template + + app = Flask(__name__) + + @app.route('/') + def index(): + return render_template('index.html') + + @app.route('/api') + def api(): + ws = request.environ.get('wsgi.websocket') + + if not ws: + abort(400, "Expected WebSocket request") + + while True: + try: + message = ws.receive() + ws.send("Your message was: {}".format(message)) + except WebSocketError: + # Possibility to execute code when connection is closed + break + + if __name__ == '__main__': + server = WebSocketServer(("", 8000), app) + server.serve_forever() + +Also the browser Javascript application can be very simple:: + + + + + + + + +Features +======== + +- Framework for WebSocket servers and WebSocket subprotocols +- Implementation of RFC6455_ and Hybi-10+ +- gevent_ based: high performance, asynchronous +- standards conformance (100% passes the `Autobahn Websocket Testsuite`_) + +Installation +============ + +Distribute & Pip +---------------- + +Installing gevent-websocket is simple with `pip `_:: + + $ pip install gevent-websocket + +Get the Code +------------ + +Requests is being developed on BitBucket. + +You can clone the repsistory:: + + hg clone https://www.bitbucket.org/Jeffrey/gevent-websocket + +or download the tarball:: + + curl -LO https://bitbucket.org/Jeffrey/gevent-websocket/TODO + +Once you have a copy, you can either embed it in your application, or installed +it on your system with:: + + $ python setup.py install + + +API +=== + +.. module:: geventwebsocket + +Main classes +------------ + +.. autoclass:: geventwebsocket.server.WebSocketServer + :inherited-members: + +.. autoclass:: geventwebsocket.resource.WebSocketApplication + :inherited-members: + +.. autoclass:: geventwebsocket.resource.Resource + :inherited-members: + +Exceptions +---------- + +.. module:: geventwebsocket.exceptions + +.. autoexception:: WebSocketError + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` + +.. _Autobahn Websocket Testsuite: http://autobahn.ws/testsuite +.. _RFC6455: http://datatracker.ietf.org/doc/rfc6455/?include_text=1 +.. _WebSocket: http://www.websocket.org/aboutwebsocket.html +.. _repository: http://www.bitbucket.org/Jeffrey/gevent-websocket/ +.. _PyPi: http://pypi.python.org/pypi/gevent-websocket/ +.. _gevent-websocket: http://www.bitbucket.org/Jeffrey/gevent-websocket/ +.. _gevent: http://www.gevent.org +.. _Jeffrey Gelens: http://www.noppo.pro diff --git a/examples/chat/chat.py b/examples/chat/chat.py new file mode 100644 index 0000000..646dd43 --- /dev/null +++ b/examples/chat/chat.py @@ -0,0 +1,67 @@ +import json + +from gevent import monkey +monkey.patch_all() + +from flask import Flask, app, render_template +from werkzeug.debug import DebuggedApplication + +from geventwebsocket import WebSocketServer, WebSocketApplication, Resource + +flask_app = Flask(__name__) +flask_app.debug = True + + +class ChatApplication(WebSocketApplication): + def on_open(self): + print("Some client connected!") + + def on_message(self, message): + if message is None: + return + + message = json.loads(message) + + if message['msg_type'] == 'message': + self.broadcast(message) + elif message['msg_type'] == 'update_clients': + self.send_client_list(message) + + def send_client_list(self, message): + current_client = self.ws.handler.active_client + current_client.nickname = message['nickname'] + + self.ws.send(json.dumps({ + 'msg_type': 'update_clients', + 'clients': [ + getattr(client, 'nickname', 'anonymous') + for client in list(self.ws.handler.server.clients.values()) + ] + })) + + def broadcast(self, message): + for client in list(self.ws.handler.server.clients.values()): + client.ws.send(json.dumps({ + 'msg_type': 'message', + 'nickname': message['nickname'], + 'message': message['message'] + })) + + def on_close(self, reason): + print("Connection closed! ") + + +@flask_app.route('/') +def index(): + return render_template('index.html') + +WebSocketServer( + ('0.0.0.0', 8000), + + Resource({ + '^/chat': ChatApplication, + '^/.*': DebuggedApplication(flask_app) + }), + + debug=False +).serve_forever() diff --git a/examples/chat/static/.bowerrc b/examples/chat/static/.bowerrc new file mode 100644 index 0000000..69fad35 --- /dev/null +++ b/examples/chat/static/.bowerrc @@ -0,0 +1,3 @@ +{ + "directory": "bower_components" +} diff --git a/examples/chat/static/.gitignore b/examples/chat/static/.gitignore new file mode 100644 index 0000000..0cca00f --- /dev/null +++ b/examples/chat/static/.gitignore @@ -0,0 +1,4 @@ +/bower_components/* +/.sass-cache/* +*.DS_Store +/stylesheets/* diff --git a/examples/chat/static/bower.json b/examples/chat/static/bower.json new file mode 100644 index 0000000..608c031 --- /dev/null +++ b/examples/chat/static/bower.json @@ -0,0 +1,8 @@ +{ + "name": "foundation-compass-app", + "version": "0.0.1", + "private": "true", + "dependencies": { + "foundation": "zurb/bower-foundation" + } +} diff --git a/examples/chat/static/config.rb b/examples/chat/static/config.rb new file mode 100644 index 0000000..1295d1e --- /dev/null +++ b/examples/chat/static/config.rb @@ -0,0 +1,25 @@ +# Require any additional compass plugins here. +add_import_path "bower_components/foundation/scss" + +# Set this to the root of your project when deployed: +http_path = "/" +css_dir = "stylesheets" +sass_dir = "scss" +images_dir = "images" +javascripts_dir = "javascripts" + +# You can select your preferred output style here (can be overridden via the command line): +# output_style = :expanded or :nested or :compact or :compressed + +# To enable relative paths to assets via compass helper functions. Uncomment: +# relative_assets = true + +# To disable debugging comments that display the original location of your selectors. Uncomment: +# line_comments = false + + +# If you prefer the indented syntax, you might want to regenerate this +# project again passing --syntax sass, or you can uncomment this: +# preferred_syntax = :sass +# and then run: +# sass-convert -R --from scss --to sass sass scss && rm -rf sass && mv scss sass diff --git a/examples/chat/static/js/app.js b/examples/chat/static/js/app.js new file mode 100644 index 0000000..3f4a470 --- /dev/null +++ b/examples/chat/static/js/app.js @@ -0,0 +1,3 @@ +// Foundation JavaScript +// Documentation can be found at: http://foundation.zurb.com/docs +$(document).foundation(); \ No newline at end of file diff --git a/examples/chat/static/scss/_settings.scss b/examples/chat/static/scss/_settings.scss new file mode 100644 index 0000000..47c9391 --- /dev/null +++ b/examples/chat/static/scss/_settings.scss @@ -0,0 +1,995 @@ +// +// FOUNDATION SETTINGS +// + +// Uncomment to use rem-calc() in your settings +// @import "foundation/functions"; + +// $experimental: true; + +// The default font-size is set to 100% of the browser style sheet (usually 16px) +// for compatibility with brower-based text zoom or user-set defaults. + +// Since the typical default browser font-size is 16px, that makes the calculation for grid size. +// If you want your base font-size to be different and not have it affect the grid breakpoints, +// set $rem-base to $base-font-size and make sure $base-font-size is a px value. +// $base-font-size: 100%; + +// The $base-line-height is 100% while $base-font-size is 150% +// $base-line-height: 150%; + +// This is the default html and body font-size for the base rem value. +// $rem-base: 16px; + +// We use this to control whether or not CSS classes come through in the gem files. +// $include-html-classes: true; +// $include-print-styles: true; +// $include-html-global-classes: $include-html-classes; + +// Grid + +// $include-html-grid-classes: $include-html-classes; + +// $row-width: rem-calc(1000); +// $column-gutter: rem-calc(30); +// $total-columns: 12; + +// We use these to control various global styles +// $body-bg: #fff; +// $body-font-color: #222; +// $body-font-family: "Helvetica Neue", "Helvetica", Helvetica, Arial, sans-serif; +// $body-font-weight: normal; +// $body-font-style: normal; + +// We use this to control font-smoothing +// $font-smoothing: antialiased; + +// We use these to control text direction settings +// $text-direction: ltr; +// $opposite-direction: right; +// $default-float: left; + +// We use these as default colors throughout +// $primary-color: #008CBA; +// $secondary-color: #e7e7e7; +// $alert-color: #f04124; +// $success-color: #43AC6A; +// $warning-color: #f08a24; +// $info-color: #a0d3e8; + +// We use these to make sure border radius matches unless we want it different. +// $global-radius: 3px; +// $global-rounded: 1000px; + +// We use these to control inset shadow shiny edges and depressions. +// $shiny-edge-size: 0 1px 0; +// $shiny-edge-color: rgba(#fff, .5); +// $shiny-edge-active-color: rgba(#000, .2); + +// Media Query Ranges +// $small-range: (0em, 40em); +// $medium-range: (40.063em, 64em); +// $large-range: (64.063em, 90em); +// $xlarge-range: (90.063em, 120em); +// $xxlarge-range: (120.063em); + +// $screen: "only screen"; + +// $landscape: "#{$screen} and (orientation: landscape)"; +// $portrait: "#{$screen} and (orientation: portrait)"; + +// $small-up: $screen; +// $small-only: "#{$screen} and (max-width: #{upper-bound($small-range)})"; + +// $medium-up: "#{$screen} and (min-width:#{lower-bound($medium-range)})"; +// $medium-only: "#{$screen} and (min-width:#{lower-bound($medium-range)}) and (max-width:#{upper-bound($medium-range)})"; + +// $large-up: "#{$screen} and (min-width:#{lower-bound($large-range)})"; +// $large-only: "#{$screen} and (min-width:#{lower-bound($large-range)}) and (max-width:#{upper-bound($large-range)})"; + +// $xlarge-up: "#{$screen} and (min-width:#{lower-bound($xlarge-range)})"; +// $xlarge-only: "#{$screen} and (min-width:#{lower-bound($xlarge-range)}) and (max-width:#{upper-bound($xlarge-range)})"; + +// $xxlarge-up: "#{$screen} and (min-width:#{lower-bound($xxlarge-range)})"; +// $xxlarge-only: "#{$screen} and (min-width:#{lower-bound($xxlarge-range)}) and (max-width:#{upper-bound($xxlarge-range)})"; + +// Legacy +// $small: $medium-up; +// $medium: $medium-up; +// $large: $large-up; + +//We use this as cursors values for enabling the option of having custom cursors in the whole site's stylesheet +// $cursor-crosshair-value: crosshair; +// $cursor-default-value: default; +// $cursor-pointer-value: pointer; +// $cursor-help-value: help; +// $cursor-text-value: text; + +// Accordion + +// $include-html-accordion-classes: $include-html-classes; + +// $accordion-navigation-padding: rem-calc(16); +// $accordion-navigation-bg-color: #efefef ; +// $accordion-navigation-hover-bg-color: darken($accordion-navigation-bg-color, 5%); +// $accordion-navigation-active-bg-color: darken($accordion-navigation-bg-color, 3%); +// $accordion-navigation-font-color: #222; +// $accordion-navigation-font-size: rem-calc(16); +// $accordion-navigation-font-family: $body-font-family; + +// $accordion-content-padding: $column-gutter/2; +// $accordion-content-active-bg-color: #fff; + +// Alert Boxes + +// $include-html-alert-classes: $include-html-classes; + +// We use this to control alert padding. +// $alert-padding-top: rem-calc(14); +// $alert-padding-default-float: $alert-padding-top; +// $alert-padding-opposite-direction: $alert-padding-top + rem-calc(10); +// $alert-padding-bottom: $alert-padding-top; + +// We use these to control text style. +// $alert-font-weight: normal; +// $alert-font-size: rem-calc(13); +// $alert-font-color: #fff; +// $alert-font-color-alt: darken($secondary-color, 60%); + +// We use this for close hover effect. +// $alert-function-factor: 5%; + +// We use these to control border styles. +// $alert-border-style: solid; +// $alert-border-width: 1px; +// $alert-border-color: darken($primary-color, $alert-function-factor); +// $alert-bottom-margin: rem-calc(20); + +// We use these to style the close buttons +// $alert-close-color: #333; +// $alert-close-top: 50%; +// $alert-close-position: rem-calc(5); +// $alert-close-font-size: rem-calc(22); +// $alert-close-opacity: 0.3; +// $alert-close-opacity-hover: 0.5; +// $alert-close-padding: 9px 6px 4px; + +// We use this to control border radius +// $alert-radius: $global-radius; + +// Block Grid + +// $include-html-grid-classes: $include-html-classes; + +// We use this to control the maximum number of block grid elements per row +// $block-grid-elements: 12; +// $block-grid-default-spacing: rem-calc(20); + +// Enables media queries for block-grid classes. Set to false if writing semantic HTML. +// $block-grid-media-queries: true; + +// Breadcrumbs + +// $include-html-nav-classes: $include-html-classes; + +// We use this to set the background color for the breadcrumb container. +// $crumb-bg: lighten($secondary-color, 5%); + +// We use these to set the padding around the breadcrumbs. +// $crumb-padding: rem-calc(9 14 9); +// $crumb-side-padding: rem-calc(12); + +// We use these to control border styles. +// $crumb-function-factor: 10%; +// $crumb-border-size: 1px; +// $crumb-border-style: solid; +// $crumb-border-color: darken($crumb-bg, $crumb-function-factor); +// $crumb-radius: $global-radius; + +// We use these to set various text styles for breadcrumbs. +// $crumb-font-size: rem-calc(11); +// $crumb-font-color: $primary-color; +// $crumb-font-color-current: #333; +// $crumb-font-color-unavailable: #999; +// $crumb-font-transform: uppercase; +// $crumb-link-decor: underline; + +// We use these to control the slash between breadcrumbs +// $crumb-slash-color: #aaa; +// $crumb-slash: "/"; + +// Button Groups + +// $include-html-button-classes: $include-html-classes; + +// Sets the margin for the right side by default, and the left margin if right-to-left direction is used +// $button-bar-margin-opposite: rem-calc(10); +// $button-group-border-width: 1px; + +// Clearing + +// $include-html-clearing-classes: $include-html-classes; + +// We use these to set the background colors for parts of Clearing. +// $clearing-bg: #333; +// $clearing-caption-bg: $clearing-bg; +// $clearing-carousel-bg: rgba (51,51,51,0.8); +// $clearing-img-bg: $clearing-bg; + +// We use these to style the close button +// $clearing-close-color: #ccc; +// $clearing-close-size: 30px; + +// We use these to style the arrows +// $clearing-arrow-size: 12px; +// $clearing-arrow-color: $clearing-close-color; + +// We use these to style captions +// $clearing-caption-font-color: #ccc; +// $clearing-caption-font-size: 0.875em; +// $clearing-caption-padding: 10px 30px 20px; + +// We use these to make the image and carousel height and style +// $clearing-active-img-height: 85%; +// $clearing-carousel-height: 120px; +// $clearing-carousel-thumb-width: 120px; +// $clearing-carousel-thumb-active-border: 1px solid rgb(255,255,255); + +// Dropdown + +// $include-html-button-classes: $include-html-classes; + +// We use these to controls height and width styles. +// $f-dropdown-max-width: 200px; +// $f-dropdown-height: auto; +// $f-dropdown-max-height: none; +// $f-dropdown-margin-top: 2px; + +// We use this to control the background color +// $f-dropdown-bg: #fff; + +// We use this to set the border styles for dropdowns. +// $f-dropdown-border-style: solid; +// $f-dropdown-border-width: 1px; +// $f-dropdown-border-color: darken(#fff, 20%); + +// We use these to style the triangle pip. +// $f-dropdown-triangle-size: 6px; +// $f-dropdown-triangle-color: #fff; +// $f-dropdown-triangle-side-offset: 10px; + +// We use these to control styles for the list elements. +// $f-dropdown-list-style: none; +// $f-dropdown-font-color: #555; +// $f-dropdown-font-size: rem-calc(14); +// $f-dropdown-list-padding: rem-calc(5, 10); +// $f-dropdown-line-height: rem-calc(18); +// $f-dropdown-list-hover-bg: #eeeeee ; +// $dropdown-mobile-default-float: 0; + +// We use this to control the styles for when the dropdown has custom content. +// $f-dropdown-content-padding: rem-calc(20); + +// Dropdown Buttons + +// $include-html-button-classes: $include-html-classes; + +// We use these to set the color of the pip in dropdown buttons +// $dropdown-button-pip-color: #fff; +// $dropdown-button-pip-color-alt: #333; + +// $button-pip-tny: rem-calc(6); +// $button-pip-sml: rem-calc(7); +// $button-pip-med: rem-calc(9); +// $button-pip-lrg: rem-calc(11); + +// We use these to style tiny dropdown buttons +// $dropdown-button-padding-tny: $button-pip-tny * 7; +// $dropdown-button-pip-size-tny: $button-pip-tny; +// $dropdown-button-pip-opposite-tny: $button-pip-tny * 3; +// $dropdown-button-pip-top-tny: -$button-pip-tny / 2 + rem-calc(1); + +// We use these to style small dropdown buttons +// $dropdown-button-padding-sml: $button-pip-sml * 7; +// $dropdown-button-pip-size-sml: $button-pip-sml; +// $dropdown-button-pip-opposite-sml: $button-pip-sml * 3; +// $dropdown-button-pip-top-sml: -$button-pip-sml / 2 + rem-calc(1); + +// We use these to style medium dropdown buttons +// $dropdown-button-padding-med: $button-pip-med * 6 + rem-calc(3); +// $dropdown-button-pip-size-med: $button-pip-med - rem-calc(3); +// $dropdown-button-pip-opposite-med: $button-pip-med * 2.5; +// $dropdown-button-pip-top-med: -$button-pip-med / 2 + rem-calc(2); + +// We use these to style large dropdown buttons +// $dropdown-button-padding-lrg: $button-pip-lrg * 5 + rem-calc(3); +// $dropdown-button-pip-size-lrg: $button-pip-lrg - rem-calc(6); +// $dropdown-button-pip-opposite-lrg: $button-pip-lrg * 2.5; +// $dropdown-button-pip-top-lrg: -$button-pip-lrg / 2 + rem-calc(3); + +// Flex Video + +// $include-html-media-classes: $include-html-classes; + +// We use these to control video container padding and margins +// $flex-video-padding-top: rem-calc(25); +// $flex-video-padding-bottom: 67.5%; +// $flex-video-margin-bottom: rem-calc(16); + +// We use this to control widescreen bottom padding +// $flex-video-widescreen-padding-bottom: 57.25%; + +// Forms + +// $include-html-form-classes: $include-html-classes; + +// We use this to set the base for lots of form spacing and positioning styles +// $form-spacing: rem-calc(16); + +// We use these to style the labels in different ways +// $form-label-pointer: pointer; +// $form-label-font-size: rem-calc(14); +// $form-label-font-weight: normal; +// $form-label-font-color: lighten(#000, 30%); +// $form-label-bottom-margin: rem-calc(8); +// $input-font-family: inherit; +// $input-font-color: rgba(0,0,0,0.75); +// $input-font-size: rem-calc(14); +// $input-bg-color: #fff; +// $input-focus-bg-color: darken(#fff, 2%); +// $input-border-color: darken(#fff, 20%); +// $input-focus-border-color: darken(#fff, 40%); +// $input-border-style: solid; +// $input-border-width: 1px; +// $input-disabled-bg: #ddd; +// $input-box-shadow: inset 0 1px 2px rgba(0,0,0,0.1); +// $input-include-glowing-effect: true; + +// We use these to style the fieldset border and spacing. +// $fieldset-border-style: solid; +// $fieldset-border-width: 1px; +// $fieldset-border-color: #ddd; +// $fieldset-padding: rem-calc(20); +// $fieldset-margin: rem-calc(18 0); + +// We use these to style the legends when you use them +// $legend-bg: #fff; +// $legend-font-weight: bold; +// $legend-padding: rem-calc(0 3); + +// We use these to style the prefix and postfix input elements +// $input-prefix-bg: darken(#fff, 5%); +// $input-prefix-border-color: darken(#fff, 20%); +// $input-prefix-border-size: 1px; +// $input-prefix-border-type: solid; +// $input-prefix-overflow: hidden; +// $input-prefix-font-color: #333; +// $input-prefix-font-color-alt: #fff; + +// We use these to style the error states for inputs and labels +// $input-error-message-padding: rem-calc(6 9 9); +// $input-error-message-top: -1px; +// $input-error-message-font-size: rem-calc(12); +// $input-error-message-font-weight: normal; +// $input-error-message-font-style: italic; +// $input-error-message-font-color: #fff; +// $input-error-message-font-color-alt: #333; + +// We use this to style the glowing effect of inputs when focused +// $glowing-effect-fade-time: 0.45s; +// $glowing-effect-color: $input-focus-border-color; + +// Select variables +// $select-bg-color: #fafafa ; + +// Inline Lists + +// $include-html-inline-list-classes: $include-html-classes; + +// We use this to control the margins and padding of the inline list. +// $inline-list-top-margin: 0; +// $inline-list-opposite-margin: 0; +// $inline-list-bottom-margin: rem-calc(17); +// $inline-list-default-float-margin: rem-calc(-22); + +// $inline-list-padding: 0; + +// We use this to control the overflow of the inline list. +// $inline-list-overflow: hidden; + +// We use this to control the list items +// $inline-list-display: block; + +// We use this to control any elments within list items +// $inline-list-children-display: block; + +// Joyride + +// $include-html-joyride-classes: $include-html-classes; + +// Controlling default Joyride styles +// $joyride-tip-bg: #333; +// $joyride-tip-default-width: 300px; +// $joyride-tip-padding: rem-calc(18 20 24); +// $joyride-tip-border: solid 1px #555; +// $joyride-tip-radius: 4px; +// $joyride-tip-position-offset: 22px; + +// Here, we're setting the tip dont styles +// $joyride-tip-font-color: #fff; +// $joyride-tip-font-size: rem-calc(14); +// $joyride-tip-header-weight: bold; + +// This changes the nub size +// $joyride-tip-nub-size: 10px; + +// This adjusts the styles for the timer when its enabled +// $joyride-tip-timer-width: 50px; +// $joyride-tip-timer-height: 3px; +// $joyride-tip-timer-color: #666; + +// This changes up the styles for the close button +// $joyride-tip-close-color: #777; +// $joyride-tip-close-size: 24px; +// $joyride-tip-close-weight: normal; + +// When Joyride is filling the screen, we use this style for the bg +// $joyride-screenfill: rgba(0,0,0,0.5); + +// Keystrokes + +// $include-html-type-classes: $include-html-classes; + +// We use these to control text styles. +// $keystroke-font: "Consolas", "Menlo", "Courier", monospace; +// $keystroke-font-size: rem-calc(14); +// $keystroke-font-color: #222; +// $keystroke-font-color-alt: #fff; +// $keystroke-function-factor: 7%; + +// We use this to control keystroke padding. +// $keystroke-padding: rem-calc(2 4 0); + +// We use these to control background and border styles. +// $keystroke-bg: darken(#fff, $keystroke-function-factor); +// $keystroke-border-style: solid; +// $keystroke-border-width: 1px; +// $keystroke-border-color: darken($keystroke-bg, $keystroke-function-factor); +// $keystroke-radius: $global-radius; + +// Labels + +// $include-html-label-classes: $include-html-classes; + +// We use these to style the labels +// $label-padding: rem-calc(4 8 6); +// $label-radius: $global-radius; + +// We use these to style the label text +// $label-font-sizing: rem-calc(11); +// $label-font-weight: normal; +// $label-font-color: #333; +// $label-font-color-alt: #fff; +// $label-font-family: $body-font-family; + +// Magellan + +// $include-html-magellan-classes: $include-html-classes; + +// $magellan-bg: #fff; +// $magellan-padding: 10px; + +// Off-canvas + +// $tabbar-bg: #333; +// $tabbar-height: rem-calc(45); +// $tabbar-line-height: $tabbar-height; +// $tabbar-color: #FFF; +// $tabbar-middle-padding: 0 rem-calc(10); + +// Off Canvas Divider Styles +// $tabbar-right-section-border: solid 1px lighten($tabbar-bg, 10%); +// $tabbar-left-section-border: solid 1px darken($tabbar-bg, 10%); + +// Off Canvas Tab Bar Headers +// $tabbar-header-color: #FFF; +// $tabbar-header-weight: bold; +// $tabbar-header-line-height: $tabbar-height; +// $tabbar-header-margin: 0; + +// Off Canvas Menu Variables +// $off-canvas-width: 250px; +// $off-canvas-bg: #333; + +// Off Canvas Menu List Variables +// $off-canvas-label-padding: 0.3rem rem-calc(15); +// $off-canvas-label-color: #999; +// $off-canvas-label-text-transform: uppercase; +// $off-canvas-label-font-weight: bold; +// $off-canvas-label-bg: #444; +// $off-canvas-label-border-top: 1px solid lighten(#444, 10%); +// $off-canvas-label-border-bottom: none; +// $off-canvas-label-margin:0; +// $off-canvas-link-padding: rem-calc(10, 15); +// $off-canvas-link-color: rgba(#FFF, 0.7); +// $off-canvas-link-border-bottom: 1px solid darken($off-canvas-bg, 5%); + +// Off Canvas Menu Icon Variables +// $tabbar-menu-icon-color: #FFF; +// $tabbar-menu-icon-hover: darken($tabbar-menu-icon-color, 30%); + +// $tabbar-menu-icon-text-indent: rem-calc(35); +// $tabbar-menu-icon-width: $tabbar-height; +// $tabbar-menu-icon-height: $tabbar-height; +// $tabbar-menu-icon-line-height: rem-calc(33); +// $tabbar-menu-icon-padding: 0; + +// $tabbar-hamburger-icon-width: rem-calc(16); +// $tabbar-hamburger-icon-left: rem-calc(13); +// $tabbar-hamburger-icon-top: rem-calc(5); + +// Off Canvas Back-Link Overlay +// $off-canvas-overlay-transition: background 300ms ease; +// $off-canvas-overlay-cursor: pointer; +// $off-canvas-overlay-box-shadow: -4px 0 4px rgba(#000, 0.5), 4px 0 4px rgba(#000, 0.5); +// $off-canvas-overlay-background: rgba(#FFF, 0.2); +// $off-canvas-overlay-background-hover: rgba(#FFF, 0.05); + +// Transition Variabls +// $menu-slide: "transform 500ms ease"; + +// Orbit + +// $include-html-orbit-classes: $include-html-classes; + +// We use these to control the caption styles +// $orbit-container-bg: none; +// $orbit-caption-bg: rgba(51,51,51, 0.8); +// $orbit-caption-font-color: #fff; +// $orbit-caption-font-size: rem-calc(14); +// $orbit-caption-position: "bottom"; // Supported values: "bottom", "under" +// $orbit-caption-padding: rem-calc(10,14); +// $orbit-caption-height: auto; + +// We use these to control the left/right nav styles +// $orbit-nav-bg: none; +// $orbit-nav-bg-hover: rgba(0,0,0,0.3); +// $orbit-nav-arrow-color: #fff; +// $orbit-nav-arrow-color-hover: #fff; + +// We use these to control the timer styles +// $orbit-timer-bg: rgba(255,255,255,0.3); +// $orbit-timer-show-progress-bar: true; + +// We use these to control the bullet nav styles +// $orbit-bullet-nav-color: #ccc; +// $orbit-bullet-nav-color-active: #999; +// $orbit-bullet-radius: rem-calc(9); + +// We use these to controls the style of slide numbers +// $orbit-slide-number-bg: rgba(0,0,0,0); +// $orbit-slide-number-font-color: #fff; +// $orbit-slide-number-padding: rem-calc(5); + +// Graceful Loading Wrapper and preloader +// $wrapper-class: "slideshow-wrapper"; +// $preloader-class: "preloader"; + +// Pagination + +// $include-html-nav-classes: $include-html-classes; + +// We use these to control the pagination container +// $pagination-height: rem-calc(24); +// $pagination-margin: rem-calc(-5); + +// We use these to set the list-item properties +// $pagination-li-float: $default-float; +// $pagination-li-height: rem-calc(24); +// $pagination-li-font-color: #222; +// $pagination-li-font-size: rem-calc(14); +// $pagination-li-margin: rem-calc(5); + +// We use these for the pagination anchor links +// $pagination-link-pad: rem-calc(1 10 1); +// $pagination-link-font-color: #999; +// $pagination-link-active-bg: darken(#fff, 10%); + +// We use these for disabled anchor links +// $pagination-link-unavailable-cursor: default; +// $pagination-link-unavailable-font-color: #999; +// $pagination-link-unavailable-bg-active: transparent; + +// We use these for currently selected anchor links +// $pagination-link-current-background: $primary-color; +// $pagination-link-current-font-color: #fff; +// $pagination-link-current-font-weight: bold; +// $pagination-link-current-cursor: default; +// $pagination-link-current-active-bg: $primary-color; + +// Panels + +// $include-html-panel-classes: $include-html-classes; + +// We use these to control the background and border styles +// $panel-bg: darken(#fff, 5%); +// $panel-border-style: solid; +// $panel-border-size: 1px; + +// We use this % to control how much we darken things on hover +// $panel-function-factor: 10%; +// $panel-border-color: darken($panel-bg, $panel-function-factor); + +// We use these to set default inner padding and bottom margin +// $panel-margin-bottom: rem-calc(20); +// $panel-padding: rem-calc(20); + +// We use these to set default font colors +// $panel-font-color: #333; +// $panel-font-color-alt: #fff; + +// $panel-header-adjust: true; +// $callout-panel-link-color: $primary-color; + +// Pricing Tables + +// $include-html-pricing-classes: $include-html-classes; + +// We use this to control the border color +// $price-table-border: solid 1px #ddd; + +// We use this to control the bottom margin of the pricing table +// $price-table-margin-bottom: rem-calc(20); + +// We use these to control the title styles +// $price-title-bg: #333; +// $price-title-padding: rem-calc(15 20); +// $price-title-align: center; +// $price-title-color: #eee; +// $price-title-weight: normal; +// $price-title-size: rem-calc(16); +// $price-title-font-family: $body-font-family; + +// We use these to control the price styles +// $price-money-bg: #f6f6f6 ; +// $price-money-padding: rem-calc(15 20); +// $price-money-align: center; +// $price-money-color: #333; +// $price-money-weight: normal; +// $price-money-size: rem-calc(32); +// $price-money-font-family: $body-font-family; + + +// We use these to control the description styles +// $price-bg: #fff; +// $price-desc-color: #777; +// $price-desc-padding: rem-calc(15); +// $price-desc-align: center; +// $price-desc-font-size: rem-calc(12); +// $price-desc-weight: normal; +// $price-desc-line-height: 1.4; +// $price-desc-bottom-border: dotted 1px #ddd; + +// We use these to control the list item styles +// $price-item-color: #333; +// $price-item-padding: rem-calc(15); +// $price-item-align: center; +// $price-item-font-size: rem-calc(14); +// $price-item-weight: normal; +// $price-item-bottom-border: dotted 1px #ddd; + +// We use these to control the CTA area styles +// $price-cta-bg: #fff; +// $price-cta-align: center; +// $price-cta-padding: rem-calc(20 20 0); + +// Progress Meters + +// $include-html-media-classes: $include-html-classes; + +// We use this to se the prog bar height +// $progress-bar-height: rem-calc(25); +// $progress-bar-color: #f6f6f6 ; + +// We use these to control the border styles +// $progress-bar-border-color: darken(#fff, 20%); +// $progress-bar-border-size: 1px; +// $progress-bar-border-style: solid; +// $progress-bar-border-radius: $global-radius; + +// We use these to control the margin & padding +// $progress-bar-pad: rem-calc(2); +// $progress-bar-margin-bottom: rem-calc(10); + +// We use these to set the meter colors +// $progress-meter-color: $primary-color; +// $progress-meter-secondary-color: $secondary-color; +// $progress-meter-success-color: $success-color; +// $progress-meter-alert-color: $alert-color; + +// Reveal + +// $include-html-reveal-classes: $include-html-classes; + +// We use these to control the style of the reveal overlay. +// $reveal-overlay-bg: rgba(#000, .45); +// $reveal-overlay-bg-old: #000; + +// We use these to control the style of the modal itself. +// $reveal-modal-bg: #fff; +// $reveal-position-top: 50px; +// $reveal-default-width: 80%; +// $reveal-modal-padding: rem-calc(20); +// $reveal-box-shadow: 0 0 10px rgba(#000,.4); + +// We use these to style the reveal close button +// $reveal-close-font-size: rem-calc(22); +// $reveal-close-top: rem-calc(8); +// $reveal-close-side: rem-calc(11); +// $reveal-close-color: #aaa; +// $reveal-close-weight: bold; + +// We use these to control the modal border +// $reveal-border-style: solid; +// $reveal-border-width: 1px; +// $reveal-border-color: #666; + +// $reveal-modal-class: "reveal-modal"; +// $close-reveal-modal-class: "close-reveal-modal"; + +// Side Nav + +// $include-html-nav-classes: $include-html-classes; + +// We use this to control padding. +// $side-nav-padding: rem-calc(14 0); + +// We use these to control list styles. +// $side-nav-list-type: none; +// $side-nav-list-position: inside; +// $side-nav-list-margin: rem-calc(0 0 7 0); + +// We use these to control link styles. +// $side-nav-link-color: $primary-color; +// $side-nav-link-color-active: lighten(#000, 30%); +// $side-nav-font-size: rem-calc(14); +// $side-nav-font-weight: normal; +// $side-nav-font-family: $body-font-family; +// $side-nav-active-font-family: $side-nav-font-family; + + + +// We use these to control border styles +// $side-nav-divider-size: 1px; +// $side-nav-divider-style: solid; +// $side-nav-divider-color: darken(#fff, 10%); + +// Split Buttons + +// $include-html-button-classes: $include-html-classes; + +// We use these to control different shared styles for Split Buttons +// $split-button-function-factor: 10%; +// $split-button-pip-color: #fff; +// $split-button-pip-color-alt: #333; +// $split-button-active-bg-tint: rgba(0,0,0,0.1); + +// We use these to control tiny split buttons +// $split-button-padding-tny: $button-pip-tny * 10; +// $split-button-span-width-tny: $button-pip-tny * 6; +// $split-button-pip-size-tny: $button-pip-tny; +// $split-button-pip-top-tny: $button-pip-tny * 2; +// $split-button-pip-default-float-tny: rem-calc(-6); + +// We use these to control small split buttons +// $split-button-padding-sml: $button-pip-sml * 10; +// $split-button-span-width-sml: $button-pip-sml * 6; +// $split-button-pip-size-sml: $button-pip-sml; +// $split-button-pip-top-sml: $button-pip-sml * 1.5; +// $split-button-pip-default-float-sml: rem-calc(-6); + +// We use these to control medium split buttons +// $split-button-padding-med: $button-pip-med * 9; +// $split-button-span-width-med: $button-pip-med * 5.5; +// $split-button-pip-size-med: $button-pip-med - rem-calc(3); +// $split-button-pip-top-med: $button-pip-med * 1.5; +// $split-button-pip-default-float-med: rem-calc(-6); + +// We use these to control large split buttons +// $split-button-padding-lrg: $button-pip-lrg * 8; +// $split-button-span-width-lrg: $button-pip-lrg * 5; +// $split-button-pip-size-lrg: $button-pip-lrg - rem-calc(6); +// $split-button-pip-top-lrg: $button-pip-lrg + rem-calc(5); +// $split-button-pip-default-float-lrg: rem-calc(-6); + +// Sub Nav + +// $include-html-nav-classes: $include-html-classes; + +// We use these to control margin and padding +// $sub-nav-list-margin: rem-calc(-4 0 18); +// $sub-nav-list-padding-top: rem-calc(4); + +// We use this to control the definition +// $sub-nav-font-family: $body-font-family; +// $sub-nav-font-size: rem-calc(14); +// $sub-nav-font-color: #999; +// $sub-nav-font-weight: normal; +// $sub-nav-text-decoration: none; +// $sub-nav-border-radius: 3px; +// $sub-nav-font-color-hover: darken($sub-nav-font-color, 15%); + + +// We use these to control the active item styles + +// $sub-nav-active-font-weight: normal; +// $sub-nav-active-bg: $primary-color; +// $sub-nav-active-bg-hover: darken($sub-nav-active-bg, 5%); +// $sub-nav-active-color: #fff; +// $sub-nav-active-padding: rem-calc(3 16); +// $sub-nav-active-cursor: default; + +// $sub-nav-item-divider: ""; +// $sub-nav-item-divider-margin: rem-calc(12); + +// Tables + +// $include-html-table-classes: $include-html-classes; + +// These control the background color for the table and even rows +// $table-bg: #fff; +// $table-even-row-bg: #f9f9f9 ; + +// These control the table cell border style +// $table-border-style: solid; +// $table-border-size: 1px; +// $table-border-color: #ddd; + +// These control the table head styles +// $table-head-bg: #f5f5f5 ; +// $table-head-font-size: rem-calc(14); +// $table-head-font-color: #222; +// $table-head-font-weight: bold; +// $table-head-padding: rem-calc(8 10 10); + +// These control the row padding and font styles +// $table-row-padding: rem-calc(9 10); +// $table-row-font-size: rem-calc(14); +// $table-row-font-color: #222; +// $table-line-height: rem-calc(18); + +// These are for controlling the display and margin of tables +// $table-display: table-cell; +// $table-margin-bottom: rem-calc(20); + +// +// TABS +// + +// $include-html-tabs-classes: $include-html-classes; + +// $tabs-navigation-padding: rem-calc(16); +// $tabs-navigation-bg-color: #efefef ; +// $tabs-navigation-hover-bg-color: darken($tabs-navigation-bg-color, 5%); +// $tabs-navigation-font-color: #222; +// $tabs-navigation-font-size: rem-calc(16); +// $tabs-navigation-font-family: $body-font-family; + +// $tabs-content-margin-bottom: rem-calc(24); +// $tabs-content-padding: $column-gutter/2; + +// $tabs-vertical-navigation-margin-bottom: 1.25rem; + +// +// THUMBNAILS +// + +// $include-html-media-classes: $include-html-classes; + +// We use these to control border styles +// $thumb-border-style: solid; +// $thumb-border-width: 4px; +// $thumb-border-color: #fff; +// $thumb-box-shadow: 0 0 0 1px rgba(#000,.2); +// $thumb-box-shadow-hover: 0 0 6px 1px rgba($primary-color,0.5); + +// Radius and transition speed for thumbs +// $thumb-radius: $global-radius; +// $thumb-transition-speed: 200ms; + +// +// TOOLTIPS +// + +// $include-html-tooltip-classes: $include-html-classes; + +// $has-tip-border-bottom: dotted 1px #ccc; +// $has-tip-font-weight: bold; +// $has-tip-font-color: #333; +// $has-tip-border-bottom-hover: dotted 1px darken($primary-color, 20%); +// $has-tip-font-color-hover: $primary-color; +// $has-tip-cursor-type: help; + +// $tooltip-padding: rem-calc(12); +// $tooltip-bg: #333; +// $tooltip-font-size: rem-calc(14); +// $tooltip-font-weight: normal; +// $tooltip-font-color: #fff; +// $tooltip-line-height: 1.3; +// $tooltip-close-font-size: rem-calc(10); +// $tooltip-close-font-weight: normal; +// $tooltip-close-font-color: #777; +// $tooltip-font-size-sml: rem-calc(14); +// $tooltip-radius: $global-radius; +// $tooltip-pip-size: 5px; + +// +// TOP BAR +// + +// $include-html-top-bar-classes: $include-html-classes; + +// Background color for the top bar +// $topbar-bg-color: #333; +// $topbar-bg: $topbar-bg-color; + +// Height and margin +// $topbar-height: 45px; +// $topbar-margin-bottom: 0; + +// Control Input height for top bar + +// Controlling the styles for the title in the top bar +// $topbar-title-weight: normal; +// $topbar-title-font-size: rem-calc(17); + +// Style the top bar dropdown elements +// $topbar-dropdown-bg: #333; +// $topbar-dropdown-link-color: #fff; +// $topbar-dropdown-link-bg: #333; +// $topbar-dropdown-toggle-size: 5px; +// $topbar-dropdown-toggle-color: #fff; +// $topbar-dropdown-toggle-alpha: 0.4; + +// Set the link colors and styles for top-level nav +// $topbar-link-color: #fff; +// $topbar-link-color-hover: #fff; +// $topbar-link-color-active: #fff; +// $topbar-link-weight: normal; +// $topbar-link-font-size: rem-calc(13); +// $topbar-link-hover-lightness: -10%; // Darken by 10% +// $topbar-link-bg-hover: #272727 ; +// $topbar-link-bg-active: $primary-color; +// $topbar-link-bg-active-hover: darken($primary-color, 5%); +// $topbar-link-font-family: $body-font-family; + +// $topbar-button-font-size: 0.75rem; + +// $topbar-dropdown-label-color: #777; +// $topbar-dropdown-label-text-transform: uppercase; +// $topbar-dropdown-label-font-weight: bold; +// $topbar-dropdown-label-font-size: rem-calc(10); +// $topbar-dropdown-label-bg: #333; + +// Top menu icon styles +// $topbar-menu-link-transform: uppercase; +// $topbar-menu-link-font-size: rem-calc(13); +// $topbar-menu-link-weight: bold; +// $topbar-menu-link-color: #fff; +// $topbar-menu-icon-color: #fff; +// $topbar-menu-link-color-toggled: #888; +// $topbar-menu-icon-color-toggled: #888; + +// Transitions and breakpoint styles +// $topbar-transition-speed: 300ms; +// Using rem-calc for the below breakpoint causes issues with top bar +// $topbar-breakpoint: #{upper-bound($medium-range)}; // Change to 9999px for always mobile layout +// $topbar-media-query: "only screen and (min-width: #{upper-bound($medium-range)})"; + +// Divider Styles +// $topbar-divider-border-bottom: solid 1px lighten($topbar-bg-color, 10%); +// $topbar-divider-border-top: solid 1px darken($topbar-bg-color, 10%); + +// Sticky Class +// $topbar-sticky-class: ".sticky"; +// $topbar-arrows: true; //Set false to remove the triangle icon from the menu item diff --git a/examples/chat/static/scss/app.scss b/examples/chat/static/scss/app.scss new file mode 100644 index 0000000..b1f39d2 --- /dev/null +++ b/examples/chat/static/scss/app.scss @@ -0,0 +1,2 @@ +@import "settings"; +@import "foundation"; diff --git a/examples/chat/templates/index.html b/examples/chat/templates/index.html new file mode 100644 index 0000000..7bea34f --- /dev/null +++ b/examples/chat/templates/index.html @@ -0,0 +1,111 @@ + + + + + + Chat Demo + + + + + + +
+
+

Websocket chat disconnected

+
+
+ +
+
+
+
+ +
+
+ +
+
+
+
    +
+
+
+
+ +
+
+ +
+
+
+
+ +
+
+ + + + + + + + diff --git a/examples/echoserver.py b/examples/echoserver.py new file mode 100644 index 0000000..b20ca9a --- /dev/null +++ b/examples/echoserver.py @@ -0,0 +1,38 @@ +import os + +import geventwebsocket + +from geventwebsocket.server import WebSocketServer + + + +def echo_app(environ, start_response): + websocket = environ.get("wsgi.websocket") + + if websocket is None: + return http_handler(environ, start_response) + try: + while True: + message = websocket.receive() + websocket.send(message) + websocket.close() + except geventwebsocket.WebSocketError as ex: + print("{0}: {1}".format(ex.__class__.__name__, ex)) + + +def http_handler(environ, start_response): + if environ["PATH_INFO"].strip("/") == "version": + start_response("200 OK", []) + return [agent] + + else: + start_response("400 Bad Request", []) + + return ["WebSocket connection is expected here."] + + +path = os.path.dirname(geventwebsocket.__file__) +agent = "gevent-websocket/%s" % (geventwebsocket.get_version()) + +print("Running %s from %s" % (agent, path)) +WebSocketServer(("", 8000), echo_app, debug=False).serve_forever() diff --git a/examples/plot_graph.html b/examples/plot_graph.html new file mode 100644 index 0000000..2d5d8e8 --- /dev/null +++ b/examples/plot_graph.html @@ -0,0 +1,43 @@ + + + + + + + + + +

Plot

+
+ + diff --git a/examples/plot_graph.py b/examples/plot_graph.py new file mode 100644 index 0000000..b8c20bd --- /dev/null +++ b/examples/plot_graph.py @@ -0,0 +1,41 @@ +""" +This example generates random data and plots a graph in the browser. + +Run it using Gevent directly using: + $ python plot_graph.py + +Or with an Gunicorn wrapper: + $ gunicorn -k "geventwebsocket.gunicorn.workers.GeventWebSocketWorker" \ + plot_graph:resource +""" + + +import gevent +import random + +from geventwebsocket import WebSocketServer, WebSocketApplication, Resource + + +class PlotApplication(WebSocketApplication): + def on_open(self): + for i in range(10000): + self.ws.send("0 %s %s\n" % (i, random.random())) + gevent.sleep(0.1) + + def on_close(self, reason): + print("Connection Closed!!!", reason) + + +def static_wsgi_app(environ, start_response): + start_response("200 OK", [("Content-Type", "text/html")]) + return open("plot_graph.html").readlines() + + +resource = Resource({ + '/': static_wsgi_app, + '/data': PlotApplication +}) + +if __name__ == "__main__": + server = WebSocketServer(('', 8000), resource, debug=True) + server.serve_forever() diff --git a/examples/wamp/wamp_example.html b/examples/wamp/wamp_example.html new file mode 100644 index 0000000..48d2c79 --- /dev/null +++ b/examples/wamp/wamp_example.html @@ -0,0 +1,82 @@ + + + + + + + +

RPCs with AutobahnJS

+

Addition

+

+ Input 1:
+ Input 2:
+ +

+ +
+ +

Key Value

+

+ Key:
+ Value:
+ + + + + + + diff --git a/examples/wamp/wamp_example.py b/examples/wamp/wamp_example.py new file mode 100644 index 0000000..3834e74 --- /dev/null +++ b/examples/wamp/wamp_example.py @@ -0,0 +1,49 @@ +from geventwebsocket.server import WebSocketServer +from geventwebsocket.resource import Resource, WebSocketApplication +from geventwebsocket.protocols.wamp import WampProtocol, export_rpc + + +db = {} + +class KeyValue(object): + @export_rpc + def set(self, key, val): + db[key] = val + + @export_rpc + def get_val(self, key): + return db.get(key) + + +class WampApplication(WebSocketApplication): + protocol_class = WampProtocol + + def on_open(self): + wamp = self.protocol + wamp.register_procedure("http://localhost:8000/calc#add", self.add) + wamp.register_object("http://localhost:8000/db#", KeyValue()) + print("opened") + + def on_message(self, message): + print("message: ", message) + super(WampApplication, self).on_message(message) + + def on_close(self, reason): + print("closed") + + def add(self, x, y): + return int(x) + int(y) + + +def static_wsgi_app(environ, start_response): + start_response("200 OK", [("Content-Type", "text/html")]) + return open("wamp_example.html").readlines() + +if __name__ == "__main__": + resource = Resource({ + '^/wamp_example$': WampApplication, + '^/$': static_wsgi_app + }) + + server = WebSocketServer(("", 8000), resource, debug=True) + server.serve_forever() diff --git a/geventwebsocket/__init__.py b/geventwebsocket/__init__.py new file mode 100644 index 0000000..7440a64 --- /dev/null +++ b/geventwebsocket/__init__.py @@ -0,0 +1,21 @@ +VERSION = (0, 9, 5, 'final', 0) + +__all__ = [ + 'WebSocketApplication', + 'Resource', + 'WebSocketServer', + 'WebSocketError', + 'get_version' +] + + +def get_version(*args, **kwargs): + from .utils import get_version + return get_version(*args, **kwargs) + +try: + from resource import WebSocketApplication, Resource + from server import WebSocketServer + from exceptions import WebSocketError +except ImportError: + pass diff --git a/geventwebsocket/exceptions.py b/geventwebsocket/exceptions.py new file mode 100644 index 0000000..e066727 --- /dev/null +++ b/geventwebsocket/exceptions.py @@ -0,0 +1,19 @@ +from socket import error as socket_error + + +class WebSocketError(socket_error): + """ + Base class for all websocket errors. + """ + + +class ProtocolError(WebSocketError): + """ + Raised if an error occurs when de/encoding the websocket protocol. + """ + + +class FrameTooLargeException(ProtocolError): + """ + Raised if a frame is received that is too large. + """ diff --git a/geventwebsocket/gunicorn/__init__.py b/geventwebsocket/gunicorn/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/geventwebsocket/gunicorn/workers.py b/geventwebsocket/gunicorn/workers.py new file mode 100644 index 0000000..d0aa136 --- /dev/null +++ b/geventwebsocket/gunicorn/workers.py @@ -0,0 +1,6 @@ +from geventwebsocket.handler import WebSocketHandler +from gunicorn.workers.ggevent import GeventPyWSGIWorker + + +class GeventWebSocketWorker(GeventPyWSGIWorker): + wsgi_handler = WebSocketHandler diff --git a/geventwebsocket/handler.py b/geventwebsocket/handler.py new file mode 100644 index 0000000..52a51dc --- /dev/null +++ b/geventwebsocket/handler.py @@ -0,0 +1,285 @@ +import base64 +import hashlib +import six +import warnings + +from gevent.pywsgi import WSGIHandler +from .websocket import WebSocket, Stream +from .logging import create_logger + + +class Client(object): + def __init__(self, address, ws): + self.address = address + self.ws = ws + + +class WebSocketHandler(WSGIHandler): + """ + Automatically upgrades the connection to a websocket. + + To prevent the WebSocketHandler to call the underlying WSGI application, + but only setup the WebSocket negotiations, do: + + mywebsockethandler.prevent_wsgi_call = True + + before calling run_application(). This is useful if you want to do more + things before calling the app, and want to off-load the WebSocket + negotiations to this library. Socket.IO needs this for example, to send + the 'ack' before yielding the control to your WSGI app. + """ + + SUPPORTED_VERSIONS = ('13', '8', '7') + GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" + + def run_websocket(self): + """ + Called when a websocket has been created successfully. + """ + + if getattr(self, 'prevent_wsgi_call', False): + return + + # In case WebSocketServer is not used + if not hasattr(self.server, 'clients'): + self.server.clients = {} + + # Since we're now a websocket connection, we don't care what the + # application actually responds with for the http response + + try: + self.server.clients[self.client_address] = Client( + self.client_address, self.websocket) + self.application(self.environ, lambda s, h, e=None: []) + finally: + del self.server.clients[self.client_address] + if not self.websocket.closed: + self.websocket.close() + self.environ.update({ + 'wsgi.websocket': None + }) + self.websocket = None + + def run_application(self): + if (hasattr(self.server, 'pre_start_hook') + and self.server.pre_start_hook): + self.logger.debug("Calling pre-start hook") + if self.server.pre_start_hook(self): + return super(WebSocketHandler, self).run_application() + + self.logger.debug("Initializing WebSocket") + self.result = self.upgrade_websocket() + + if hasattr(self, 'websocket'): + if self.status and not self.headers_sent: + self.write('') + + self.run_websocket() + else: + if self.status: + # A status was set, likely an error so just send the response + if not self.result: + self.result = [] + + self.process_result() + return + + # This handler did not handle the request, so defer it to the + # underlying application object + return super(WebSocketHandler, self).run_application() + + def upgrade_websocket(self): + """ + Attempt to upgrade the current environ into a websocket enabled + connection. If successful, the environ dict with be updated with two + new entries, `wsgi.websocket` and `wsgi.websocket_version`. + + :returns: Whether the upgrade was successful. + """ + + # Some basic sanity checks first + + self.logger.debug("Validating WebSocket request") + + if self.environ.get('REQUEST_METHOD', '') != 'GET': + # This is not a websocket request, so we must not handle it + self.logger.debug('Can only upgrade connection if using GET method.') + return + + upgrade = self.environ.get('HTTP_UPGRADE', '').lower() + + if upgrade == 'websocket': + connection = self.environ.get('HTTP_CONNECTION', '').lower() + + if 'upgrade' not in connection: + # This is not a websocket request, so we must not handle it + self.logger.warning("Client didn't ask for a connection " + "upgrade") + return + else: + # This is not a websocket request, so we must not handle it + return + + if self.request_version != 'HTTP/1.1': + self.start_response('402 Bad Request', []) + self.logger.warning("Bad server protocol in headers") + + return ['Bad protocol version'] + + if self.environ.get('HTTP_SEC_WEBSOCKET_VERSION'): + return self.upgrade_connection() + else: + self.logger.warning("No protocol defined") + self.start_response('426 Upgrade Required', [ + ('Sec-WebSocket-Version', ', '.join(self.SUPPORTED_VERSIONS))]) + + return ['No Websocket protocol version defined'] + + def upgrade_connection(self): + """ + Validate and 'upgrade' the HTTP request to a WebSocket request. + + If an upgrade succeeded then then handler will have `start_response` + with a status of `101`, the environ will also be updated with + `wsgi.websocket` and `wsgi.websocket_version` keys. + + :param environ: The WSGI environ dict. + :param start_response: The callable used to start the response. + :param stream: File like object that will be read from/written to by + the underlying WebSocket object, if created. + :return: The WSGI response iterator is something went awry. + """ + + self.logger.debug("Attempting to upgrade connection") + + version = self.environ.get("HTTP_SEC_WEBSOCKET_VERSION") + + if version not in self.SUPPORTED_VERSIONS: + msg = "Unsupported WebSocket Version: {0}".format(version) + + self.logger.warning(msg) + self.start_response('400 Bad Request', [ + ('Sec-WebSocket-Version', ', '.join(self.SUPPORTED_VERSIONS)) + ]) + + return [msg] + + key = self.environ.get("HTTP_SEC_WEBSOCKET_KEY", '').strip() + + if not key: + # 5.2.1 (3) + msg = "Sec-WebSocket-Key header is missing/empty" + + self.logger.warning(msg) + self.start_response('400 Bad Request', []) + + return [msg] + + try: + key_len = len(base64.b64decode(key)) + except TypeError: + msg = "Invalid key: {0}".format(key) + + self.logger.warning(msg) + self.start_response('400 Bad Request', []) + + return [msg] + + if key_len != 16: + # 5.2.1 (3) + msg = "Invalid key: {0}".format(key) + + self.logger.warning(msg) + self.start_response('400 Bad Request', []) + + return [msg] + + # Check for WebSocket Protocols + requested_protocols = self.environ.get( + 'HTTP_SEC_WEBSOCKET_PROTOCOL', '') + protocol = None + + if hasattr(self.application, 'app_protocol'): + allowed_protocol = self.application.app_protocol( + self.environ['PATH_INFO']) + + if allowed_protocol and allowed_protocol in requested_protocols: + protocol = allowed_protocol + self.logger.debug("Protocol allowed: {0}".format(protocol)) + + self.websocket = WebSocket(self.environ, Stream(self), self) + self.environ.update({ + 'wsgi.websocket_version': version, + 'wsgi.websocket': self.websocket + }) + + try: + # python2 + _hash = base64.b64encode(hashlib.sha1(key + self.GUID).digest()) + except: + # python3 + _hash = base64.b64encode(hashlib.sha1((key + self.GUID).encode()).digest()).decode() + + headers = [ + ("Upgrade", "websocket"), + ("Connection", "Upgrade"), + ("Sec-WebSocket-Accept", _hash), + ] + + if protocol: + headers.append(("Sec-WebSocket-Protocol", protocol)) + + self.logger.debug("WebSocket request accepted, switching protocols") + self.start_response("101 Switching Protocols", headers) + + @property + def logger(self): + if not hasattr(self.server, 'logger'): + self.server.logger = create_logger(__name__) + + return self.server.logger + + def log_request(self): + if '101' not in self.status.decode('utf-8'): + self.logger.info(self.format_request()) + + @property + def active_client(self): + return self.server.clients[self.client_address] + + def start_response(self, status, headers, exc_info=None): + """ + Called when the handler is ready to send a response back to the remote + endpoint. A websocket connection may have not been created. + """ + writer = super(WebSocketHandler, self).start_response( + status, headers, exc_info=exc_info) + + self._prepare_response() + + return writer + + def _prepare_response(self): + """ + Sets up the ``pywsgi.Handler`` to work with a websocket response. + + This is used by other projects that need to support WebSocket + connections as part of a larger effort. + """ + assert not self.headers_sent + + if not self.environ.get('wsgi.websocket'): + # a WebSocket connection is not established, do nothing + return + + # So that `finalize_headers` doesn't write a Content-Length header + self.provided_content_length = False + + # The websocket is now controlling the response + self.response_use_chunked = False + + # Once the request is over, the connection must be closed + self.close_connection = True + + # Prevents the Date header from being written + self.provided_date = True diff --git a/geventwebsocket/logging.py b/geventwebsocket/logging.py new file mode 100644 index 0000000..554ca02 --- /dev/null +++ b/geventwebsocket/logging.py @@ -0,0 +1,31 @@ +from __future__ import absolute_import + +from logging import getLogger, StreamHandler, getLoggerClass, Formatter, DEBUG + + +def create_logger(name, debug=False, format=None): + Logger = getLoggerClass() + + class DebugLogger(Logger): + def getEffectiveLevel(x): + if x.level == 0 and debug: + return DEBUG + else: + return Logger.getEffectiveLevel(x) + + class DebugHandler(StreamHandler): + def emit(x, record): + StreamHandler.emit(x, record) if debug else None + + handler = DebugHandler() + handler.setLevel(DEBUG) + + if format: + handler.setFormatter(Formatter(format)) + + logger = getLogger(name) + del logger.handlers[:] + logger.__class__ = DebugLogger + logger.addHandler(handler) + + return logger diff --git a/geventwebsocket/protocols/__init__.py b/geventwebsocket/protocols/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/geventwebsocket/protocols/base.py b/geventwebsocket/protocols/base.py new file mode 100644 index 0000000..1c05ab6 --- /dev/null +++ b/geventwebsocket/protocols/base.py @@ -0,0 +1,35 @@ +class BaseProtocol(object): + PROTOCOL_NAME = '' + + def __init__(self, app): + self._app = app + + def on_open(self): + self.app.on_open() + + def on_message(self, message): + self.app.on_message(message) + + def on_close(self, reason=None): + self.app.on_close(reason) + + @property + def app(self): + if self._app: + return self._app + else: + raise Exception("No application coupled") + + @property + def server(self): + if not hasattr(self.app, 'ws'): + return None + + return self.app.ws.handler.server + + @property + def handler(self): + if not hasattr(self.app, 'ws'): + return None + + return self.app.ws.handler diff --git a/geventwebsocket/protocols/wamp.py b/geventwebsocket/protocols/wamp.py new file mode 100644 index 0000000..591c4d7 --- /dev/null +++ b/geventwebsocket/protocols/wamp.py @@ -0,0 +1,236 @@ +import inspect +import random +import six +import string +import types +import six + +try: + import ujson as json +except ImportError: + try: + import simplejson as json + except ImportError: + import json + +from ..exceptions import WebSocketError +from .base import BaseProtocol + + +def export_rpc(arg=None): + if isinstance(arg, types.FunctionType): + arg._rpc = arg.__name__ + return arg + + +def serialize(data): + return json.dumps(data) + + +class Prefixes(object): + def __init__(self): + self.prefixes = {} + + def add(self, prefix, uri): + self.prefixes[prefix] = uri + + def resolve(self, curie_or_uri): + if "http://" in curie_or_uri: + return curie_or_uri + elif ':' in curie_or_uri: + prefix, proc = curie_or_uri.split(':', 1) + return self.prefixes[prefix] + proc + else: + raise Exception(curie_or_uri) + + +class RemoteProcedures(object): + def __init__(self): + self.calls = {} + + def register_procedure(self, uri, proc): + self.calls[uri] = proc + + def register_object(self, uri, obj): + for k in inspect.getmembers(obj, inspect.ismethod): + if '_rpc' in k[1].__dict__: + proc_uri = uri + k[1]._rpc + self.calls[proc_uri] = (obj, k[1]) + + def call(self, uri, args): + if uri in self.calls: + proc = self.calls[uri] + + # Do the correct call whether it's a function or instance method. + if isinstance(proc, tuple): + if proc[1].__self__ is None: + # Create instance of object and call method + return proc[1](proc[0](), *args) + else: + # Call bound method on instance + return proc[1](*args) + else: + return self.calls[uri](*args) + else: + raise Exception("no such uri '{}'".format(uri)) + + +class Channels(object): + def __init__(self): + self.channels = {} + + def create(self, uri, prefix_matching=False): + if uri not in self.channels: + self.channels[uri] = [] + + # TODO: implement prefix matching + + def subscribe(self, uri, client): + if uri in self.channels: + self.channels[uri].append(client) + + def unsubscribe(self, uri, client): + if uri not in self.channels: + return + + client_index = self.channels[uri].index(client) + self.channels[uri].pop(client_index) + + if len(self.channels[uri]) == 0: + del self.channels[uri] + + def publish(self, uri, event, exclude=None, eligible=None): + if uri not in self.channels: + return + + # TODO: exclude & eligible + + msg = [WampProtocol.MSG_EVENT, uri, event] + + for client in self.channels[uri]: + try: + client.ws.send(serialize(msg)) + except WebSocketError: + # Seems someone didn't unsubscribe before disconnecting + self.channels[uri].remove(client) + + +class WampProtocol(BaseProtocol): + MSG_WELCOME = 0 + MSG_PREFIX = 1 + MSG_CALL = 2 + MSG_CALL_RESULT = 3 + MSG_CALL_ERROR = 4 + MSG_SUBSCRIBE = 5 + MSG_UNSUBSCRIBE = 6 + MSG_PUBLISH = 7 + MSG_EVENT = 8 + + PROTOCOL_NAME = "wamp" + + def __init__(self, *args, **kwargs): + self.procedures = RemoteProcedures() + self.prefixes = Prefixes() + self.session_id = ''.join( + [random.choice(string.digits + string.letters) + for i in six.moves.xrange(16)]) + + super(WampProtocol, self).__init__(*args, **kwargs) + + def register_procedure(self, *args, **kwargs): + self.procedures.register_procedure(*args, **kwargs) + + def register_object(self, *args, **kwargs): + self.procedures.register_object(*args, **kwargs) + + def register_pubsub(self, *args, **kwargs): + if not hasattr(self.server, 'channels'): + self.server.channels = Channels() + + self.server.channels.create(*args, **kwargs) + + def do_handshake(self): + from geventwebsocket import get_version + + welcome = [ + self.MSG_WELCOME, + self.session_id, + 1, + 'gevent-websocket/' + get_version() + ] + self.app.ws.send(serialize(welcome)) + + def _get_exception_info(self, e): + uri = 'http://TODO#generic' + desc = str(type(e)) + details = str(e) + return [uri, desc, details] + + def rpc_call(self, data): + call_id, curie_or_uri = data[1:3] + args = data[3:] + + if not isinstance(call_id, six.string_types): + raise Exception() + if not isinstance(curie_or_uri, six.string_types): + raise Exception() + + uri = self.prefixes.resolve(curie_or_uri) + + try: + result = self.procedures.call(uri, args) + result_msg = [self.MSG_CALL_RESULT, call_id, result] + except Exception as e: + result_msg = [self.MSG_CALL_ERROR, + call_id] + self._get_exception_info(e) + + self.app.on_message(serialize(result_msg)) + + def pubsub_action(self, data): + action = data[0] + curie_or_uri = data[1] + + if not isinstance(action, int): + raise Exception() + if not isinstance(curie_or_uri, six.string_types): + raise Exception() + + uri = self.prefixes.resolve(curie_or_uri) + + if action == self.MSG_SUBSCRIBE and len(data) == 2: + self.server.channels.subscribe(data[1], self.handler.active_client) + + elif action == self.MSG_UNSUBSCRIBE and len(data) == 2: + self.server.channels.unsubscribe( + data[1], self.handler.active_client) + + elif action == self.MSG_PUBLISH and len(data) >= 3: + payload = data[2] if len(data) >= 3 else None + exclude = data[3] if len(data) >= 4 else None + eligible = data[4] if len(data) >= 5 else None + + self.server.channels.publish(uri, payload, exclude, eligible) + + def on_open(self): + self.app.on_open() + self.do_handshake() + + def on_message(self, message): + data = json.loads(message) + + if not isinstance(data, list): + raise Exception('incoming data is no list') + + if data[0] == self.MSG_PREFIX and len(data) == 3: + prefix, uri = data[1:3] + self.prefixes.add(prefix, uri) + + elif data[0] == self.MSG_CALL and len(data) >= 3: + return self.rpc_call(data) + + elif data[0] in (self.MSG_SUBSCRIBE, self.MSG_UNSUBSCRIBE, + self.MSG_PUBLISH): + return self.pubsub_action(data) + else: + raise Exception("Unknown call") + diff --git a/geventwebsocket/resource.py b/geventwebsocket/resource.py new file mode 100644 index 0000000..0de7b4a --- /dev/null +++ b/geventwebsocket/resource.py @@ -0,0 +1,106 @@ +import re +<<<<<<< destination:f4bc7c145b37dd0e8cecb4e6b415054dfc890bfe:geventwebsocket/resource.py +import warnings +======= +import six +>>>>>>> source:f6b3d40a61b90f5ef580d423778159d35a7d4b58:geventwebsocket/resource.py + +from collections import OrderedDict +from .protocols.base import BaseProtocol +from .exceptions import WebSocketError + + +class WebSocketApplication(object): + protocol_class = BaseProtocol + + def __init__(self, ws): + self.protocol = self.protocol_class(self) + self.ws = ws + + def handle(self): + self.protocol.on_open() + + while True: + try: + message = self.ws.receive() + except WebSocketError: + self.protocol.on_close() + break + + self.protocol.on_message(message) + + def on_open(self, *args, **kwargs): + pass + + def on_close(self, *args, **kwargs): + pass + + def on_message(self, message, *args, **kwargs): + self.ws.send(message, **kwargs) + + @classmethod + def protocol_name(cls): + return cls.protocol_class.PROTOCOL_NAME + + +class Resource(object): + def __init__(self, apps=None): + self.apps = apps if apps else [] + + if isinstance(apps, dict) and not isinstance(apps, OrderedDict): + warnings.warn("Using an unordered dictionary for the " + "app list is discouraged and may lead to " + "undefined behavior.", UserWarning) + + # Convert to a list of tuples + # The order is undefined, which can be very bad, but this keeps + # backwards compatibility. + self.apps = [(path, app) for path, app in list(apps.items())] + + # An app can either be a standard WSGI application (an object we call with + # __call__(self, environ, start_response)) or a class we instantiate + # (and which can handle websockets). This function tells them apart. + # Override this if you have apps that can handle websockets but don't + # fulfill these criteria. + def _is_websocket_app(self, app): + return isinstance(app, type) and issubclass(app, WebSocketApplication) + + def _app_by_path(self, environ_path, is_websocket_request): + # Which app matched the current path? +<<<<<<< destination:f4bc7c145b37dd0e8cecb4e6b415054dfc890bfe:geventwebsocket/resource.py + for path, app in self.apps.items(): +======= + + for path, app in six.iteritems(self.apps): +>>>>>>> source:f6b3d40a61b90f5ef580d423778159d35a7d4b58:geventwebsocket/resource.py + if re.match(path, environ_path): + if is_websocket_request == self._is_websocket_app(app): + return app + return None + + def app_protocol(self, path): + # app_protocol will only be called for websocket apps + app = self._app_by_path(path, True) + + if hasattr(app, 'protocol_name'): + return app.protocol_name() + else: + return '' + + def __call__(self, environ, start_response): + environ = environ + is_websocket_call = 'wsgi.websocket' in environ + current_app = self._app_by_path(environ['PATH_INFO'], is_websocket_call) + + if current_app is None: + raise Exception("No apps defined") + + if is_websocket_call: + ws = environ['wsgi.websocket'] + current_app = current_app(ws) + current_app.ws = ws # TODO: needed? + current_app.handle() + # Always return something, calling WSGI middleware may rely on it + return [] + else: + return current_app(environ, start_response) diff --git a/geventwebsocket/server.py b/geventwebsocket/server.py new file mode 100644 index 0000000..e939bd1 --- /dev/null +++ b/geventwebsocket/server.py @@ -0,0 +1,34 @@ +from gevent.pywsgi import WSGIServer + +from .handler import WebSocketHandler +from .logging import create_logger + + +class WebSocketServer(WSGIServer): + handler_class = WebSocketHandler + debug_log_format = ( + '-' * 80 + '\n' + + '%(levelname)s in %(module)s [%(pathname)s:%(lineno)d]:\n' + + '%(message)s\n' + + '-' * 80 + ) + + def __init__(self, *args, **kwargs): + self.debug = kwargs.pop('debug', False) + self.pre_start_hook = kwargs.pop('pre_start_hook', None) + self._logger = None + self.clients = {} + + super(WebSocketServer, self).__init__(*args, **kwargs) + + def handle(self, socket, address): + handler = self.handler_class(socket, address, self) + handler.handle() + + @property + def logger(self): + if not self._logger: + self._logger = create_logger( + __name__, self.debug, self.debug_log_format) + + return self._logger diff --git a/geventwebsocket/utf8validator.py b/geventwebsocket/utf8validator.py new file mode 100644 index 0000000..9712993 --- /dev/null +++ b/geventwebsocket/utf8validator.py @@ -0,0 +1,130 @@ +############################################################################### +## +## Copyright 2011-2013 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. +## +############################################################################### + +from six.moves import range + +## use Cython implementation of UTF8 validator if available +## +import six +try: + from wsaccel.utf8validator import Utf8Validator +except: + ## fallback to pure Python implementation + + class Utf8Validator: + """ + 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 string. + + 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. + """ + + l = len(ba) + + for i in six.moves.xrange(l): + ## optimized version of decode(), since we are not interested in actual code points + + self.state = Utf8Validator.UTF8VALIDATOR_DFA[256 + (self.state << 4) + Utf8Validator.UTF8VALIDATOR_DFA[ord(ba[i])]] + + if self.state == Utf8Validator.UTF8_REJECT: + self.i += i + return False, False, i, self.i + + self.i += l + + return True, self.state == Utf8Validator.UTF8_ACCEPT, l, self.i diff --git a/geventwebsocket/utils.py b/geventwebsocket/utils.py new file mode 100644 index 0000000..2e5bc3b --- /dev/null +++ b/geventwebsocket/utils.py @@ -0,0 +1,45 @@ +import subprocess + + +def get_version(version=None): + "Returns a PEP 386-compliant version number from VERSION." + + if version is None: + from geventwebsocket import VERSION as version + else: + assert len(version) == 5 + assert version[3] in ('alpha', 'beta', 'rc', 'final') + + # Now build the two parts of the version number: + # main = X.Y[.Z] + # sub = .devN - for pre-alpha releases + # | {a|b|c}N - for alpha, beta and rc releases + + parts = 2 if version[2] == 0 else 3 + main = '.'.join(str(x) for x in version[:parts]) + + sub = '' + if version[3] == 'alpha' and version[4] == 0: + hg_changeset = get_hg_changeset() + if hg_changeset: + sub = '.dev{0}'.format(hg_changeset) + + elif version[3] != 'final': + mapping = {'alpha': 'a', 'beta': 'b', 'rc': 'c'} + sub = mapping[version[3]] + str(version[4]) + + return str(main + sub) + + +def get_hg_changeset(): + rev, err = subprocess.Popen( + 'hg id -i', + shell=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE + ).communicate() + + if err: + return None + else: + return rev.strip().replace('+', '') diff --git a/geventwebsocket/websocket.py b/geventwebsocket/websocket.py new file mode 100644 index 0000000..71e57e9 --- /dev/null +++ b/geventwebsocket/websocket.py @@ -0,0 +1,544 @@ +import six +import struct +import six +import six + +from socket import error + +from .exceptions import ProtocolError +from .exceptions import WebSocketError +from .exceptions import FrameTooLargeException + +from .utf8validator import Utf8Validator + + +MSG_SOCKET_DEAD = "Socket is dead" +MSG_ALREADY_CLOSED = "Connection is already closed" +MSG_CLOSED = "Connection closed" + +class WebSocket(object): + """ + Base class for supporting websocket operations. + + :ivar environ: The http environment referenced by this connection. + :ivar closed: Whether this connection is closed/closing. + :ivar stream: The underlying file like object that will be read from / + written to by this WebSocket object. + """ + + __slots__ = ('utf8validator', 'utf8validate_last', 'environ', 'closed', + 'stream', 'raw_write', 'raw_read', 'handler') + + OPCODE_CONTINUATION = 0x00 + OPCODE_TEXT = 0x01 + OPCODE_BINARY = 0x02 + OPCODE_CLOSE = 0x08 + OPCODE_PING = 0x09 + OPCODE_PONG = 0x0a + + def __init__(self, environ, stream, handler): + self.environ = environ + self.closed = False + + self.stream = stream + + self.raw_write = stream.write + self.raw_read = stream.read + + self.utf8validator = Utf8Validator() + self.handler = handler + + def __del__(self): + try: + self.close() + except: + # close() may fail if __init__ didn't complete + pass + + def _decode_bytes(self, bytestring): + """ + Internal method used to convert the utf-8 encoded bytestring into + unicode. + + If the conversion fails, the socket will be closed. + """ + + if not bytestring: + return six.u('') + + try: + return bytestring.decode('utf-8') + except UnicodeDecodeError: + self.close(1007) + + raise + + def _encode_bytes(self, text): + """ + :returns: The utf-8 byte string equivalent of `text`. + """ + + if isinstance(text, six.binary_type): + return text + + if not isinstance(text, six.text_type): + text = six.text_type(text or '') + + return text.encode('utf-8') + + def _is_valid_close_code(self, code): + """ + :returns: Whether the returned close code is a valid hybi return code. + """ + if code < 1000: + return False + + if 1004 <= code <= 1006: + return False + + if 1012 <= code <= 1016: + return False + + if code == 1100: + # not sure about this one but the autobahn fuzzer requires it. + return False + + if 2000 <= code <= 2999: + return False + + return True + + @property + def current_app(self): + if hasattr(self.handler.server.application, 'current_app'): + return self.handler.server.application.current_app + else: + # For backwards compatibility reasons + class MockApp(): + def on_close(self, *args): + pass + + return MockApp() + + @property + def origin(self): + if not self.environ: + return + + return self.environ.get('HTTP_ORIGIN') + + @property + def protocol(self): + if not self.environ: + return + + return self.environ.get('HTTP_SEC_WEBSOCKET_PROTOCOL') + + @property + def version(self): + if not self.environ: + return + + return self.environ.get('HTTP_SEC_WEBSOCKET_VERSION') + + @property + def path(self): + if not self.environ: + return + + return self.environ.get('PATH_INFO') + + @property + def logger(self): + return self.handler.logger + + def handle_close(self, header, payload): + """ + Called when a close frame has been decoded from the stream. + + :param header: The decoded `Header`. + :param payload: The bytestring payload associated with the close frame. + """ + if not payload: + self.close(1000, None) + + return + + if len(payload) < 2: + raise ProtocolError('Invalid close frame: {0} {1}'.format( + header, payload)) + + code = struct.unpack('!H', payload[:2])[0] + payload = payload[2:] + + if payload: + validator = Utf8Validator() + val = validator.validate(payload) + + if not val[0]: + raise UnicodeError + + if not self._is_valid_close_code(code): + raise ProtocolError('Invalid close code {0}'.format(code)) + + self.close(code, payload) + + def handle_ping(self, header, payload): + self.send_frame(payload, self.OPCODE_PONG) + + def handle_pong(self, header, payload): + pass + + def read_frame(self): + """ + Block until a full frame has been read from the socket. + + This is an internal method as calling this will not cleanup correctly + if an exception is called. Use `receive` instead. + + :return: The header and payload as a tuple. + """ + + header = Header.decode_header(self.stream) + + if header.flags: + raise ProtocolError + + if not header.length: + return header, b'' + + try: + payload = self.raw_read(header.length) + except error: + payload = b'' + except Exception: + # TODO log out this exception + payload = b'' + + if len(payload) != header.length: + raise WebSocketError('Unexpected EOF reading frame payload') + + if header.mask: + payload = header.unmask_payload(payload) + + return header, payload + + def validate_utf8(self, payload): + # Make sure the frames are decodable independently + self.utf8validate_last = self.utf8validator.validate(payload) + + if not self.utf8validate_last[0]: + raise UnicodeError("Encountered invalid UTF-8 while processing " + "text message at payload octet index " + "{0:d}".format(self.utf8validate_last[3])) + + def read_message(self): + """ + Return the next text or binary message from the socket. + + This is an internal method as calling this will not cleanup correctly + if an exception is called. Use `receive` instead. + """ + opcode = None + message = b'' + + while True: + header, payload = self.read_frame() + f_opcode = header.opcode + + if f_opcode in (self.OPCODE_TEXT, self.OPCODE_BINARY): + # a new frame + if opcode: + raise ProtocolError("The opcode in non-fin frame is " + "expected to be zero, got " + "{0!r}".format(f_opcode)) + + # Start reading a new message, reset the validator + self.utf8validator.reset() + self.utf8validate_last = (True, True, 0, 0) + + opcode = f_opcode + + elif f_opcode == self.OPCODE_CONTINUATION: + if not opcode: + raise ProtocolError("Unexpected frame with opcode=0") + + elif f_opcode == self.OPCODE_PING: + self.handle_ping(header, payload) + continue + + elif f_opcode == self.OPCODE_PONG: + self.handle_pong(header, payload) + continue + + elif f_opcode == self.OPCODE_CLOSE: + self.handle_close(header, payload) + return + + else: + raise ProtocolError("Unexpected opcode={0!r}".format(f_opcode)) + + if opcode == self.OPCODE_TEXT: + self.validate_utf8(payload) + + message += payload + + if header.fin: + break + + if opcode == self.OPCODE_TEXT: + self.validate_utf8(message) + return self._decode_bytes(message) + else: + return bytearray(message) + + def receive(self): + """ + Read and return a message from the stream. If `None` is returned, then + the socket is considered closed/errored. + """ + + if self.closed: + self.current_app.on_close(MSG_ALREADY_CLOSED) + raise WebSocketError(MSG_ALREADY_CLOSED) + + try: + return self.read_message() + except UnicodeError: + self.close(1007) + except ProtocolError: + self.close(1002) + except error: + self.close() + self.current_app.on_close(MSG_CLOSED) + + return None + + def send_frame(self, message, opcode): + """ + Send a frame over the websocket with message as its payload + """ + if self.closed: + self.current_app.on_close(MSG_ALREADY_CLOSED) + raise WebSocketError(MSG_ALREADY_CLOSED) + + if opcode == self.OPCODE_TEXT: + message = self._encode_bytes(message) + elif opcode == self.OPCODE_BINARY: + message = str(message) + + header = Header.encode_header(True, opcode, 0, len(message), '') + + try: + self.raw_write(header+message) + except error: + raise WebSocketError(MSG_SOCKET_DEAD) + + def send(self, message, binary=None): + """ + Send a frame over the websocket with message as its payload + """ + if binary is None: + binary = not isinstance(message, six.string_types) + + opcode = self.OPCODE_BINARY if binary else self.OPCODE_TEXT + + try: + self.send_frame(message, opcode) + except WebSocketError: + self.current_app.on_close(MSG_SOCKET_DEAD) + raise WebSocketError(MSG_SOCKET_DEAD) + + def close(self, code=1000, message=''): + """ + Close the websocket and connection, sending the specified code and + message. The underlying socket object is _not_ closed, that is the + responsibility of the initiator. + """ + if self.closed: + self.current_app.on_close(MSG_ALREADY_CLOSED) + + try: + message = self._encode_bytes(message) + + _ = struct.pack('!H%ds' % len(message), code, message) + + self.send_frame(_, opcode=self.OPCODE_CLOSE) + except WebSocketError: + # Failed to write the closing frame but it's ok because we're + # closing the socket anyway. + self.logger.debug("Failed to write closing frame -> closing socket") + finally: + self.logger.debug("Closed WebSocket") + self.closed = True + + self.stream = None + self.raw_write = None + self.raw_read = None + + self.environ = None + + #self.current_app.on_close(MSG_ALREADY_CLOSED) + + +class Stream(object): + """ + Wraps the handler's socket/rfile attributes and makes it in to a file like + object that can be read from/written to by the lower level websocket api. + """ + + __slots__ = ('handler', 'read', 'write') + + def __init__(self, handler): + self.handler = handler + self.read = handler.rfile.read + self.write = handler.socket.sendall + + +class Header(object): + __slots__ = ('fin', 'mask', 'opcode', 'flags', 'length') + + FIN_MASK = 0x80 + OPCODE_MASK = 0x0f + MASK_MASK = 0x80 + LENGTH_MASK = 0x7f + + RSV0_MASK = 0x40 + RSV1_MASK = 0x20 + RSV2_MASK = 0x10 + + # bitwise mask that will determine the reserved bits for a frame header + HEADER_FLAG_MASK = RSV0_MASK | RSV1_MASK | RSV2_MASK + + def __init__(self, fin=0, opcode=0, flags=0, length=0): + self.mask = '' + self.fin = fin + self.opcode = opcode + self.flags = flags + self.length = length + + def mask_payload(self, payload): + payload = bytearray(payload) + mask = bytearray(self.mask) + + for i in six.moves.xrange(self.length): + payload[i] ^= mask[i % 4] + + return str(payload) + + # it's the same operation + unmask_payload = mask_payload + + def __repr__(self): + return ("

").format(self.fin, self.opcode, self.length, + self.flags, id(self)) + + @classmethod + def decode_header(cls, stream): + """ + Decode a WebSocket header. + + :param stream: A file like object that can be 'read' from. + :returns: A `Header` instance. + """ + read = stream.read + data = read(2) + + if len(data) != 2: + raise WebSocketError("Unexpected EOF while decoding header") + + first_byte, second_byte = struct.unpack(b'!BB', data) + + header = cls( + fin=first_byte & cls.FIN_MASK == cls.FIN_MASK, + opcode=first_byte & cls.OPCODE_MASK, + flags=first_byte & cls.HEADER_FLAG_MASK, + length=second_byte & cls.LENGTH_MASK) + + has_mask = second_byte & cls.MASK_MASK == cls.MASK_MASK + + if header.opcode > 0x07: + if not header.fin: + raise ProtocolError( + "Received fragmented control frame: {0!r}".format(data)) + + # Control frames MUST have a payload length of 125 bytes or less + if header.length > 125: + raise FrameTooLargeException( + "Control frame cannot be larger than 125 bytes: " + "{0!r}".format(data)) + + if header.length == 126: + # 16 bit length + data = read(2) + + if len(data) != 2: + raise WebSocketError('Unexpected EOF while decoding header') + + header.length = struct.unpack(b'!H', data)[0] + elif header.length == 127: + # 64 bit length + data = read(8) + + if len(data) != 8: + raise WebSocketError('Unexpected EOF while decoding header') + + header.length = struct.unpack(b'!Q', data)[0] + + if has_mask: + mask = read(4) + + if len(mask) != 4: + raise WebSocketError('Unexpected EOF while decoding header') + + header.mask = mask + + return header + + @classmethod + def encode_header(cls, fin, opcode, flags, length, mask): + """ + Encodes a WebSocket header. + + :param fin: Whether this is the final frame for this opcode. + :param opcode: The opcode of the payload, see `OPCODE_*` + :param length: The length of the frame. + :param flags: The RSV* flags. + :param mask: Whether the payload is masked. + :return: A bytestring encoded header. + """ + first_byte = opcode + second_byte = 0 + extra = b'' + + if fin: + first_byte |= cls.FIN_MASK + + if flags & cls.RSV0_MASK: + first_byte |= cls.RSV0_MASK + + if flags & cls.RSV1_MASK: + first_byte |= cls.RSV1_MASK + + if flags & cls.RSV2_MASK: + first_byte |= cls.RSV2_MASK + + # now deal with length complexities + if length < 126: + second_byte += length + elif length <= 0xffff: + second_byte += 126 + extra = struct.pack('!H', length) + elif length <= 0xffffffffffffffff: + second_byte += 127 + extra = struct.pack('!Q', length) + else: + raise FrameTooLargeException + + if mask: + second_byte |= cls.MASK_MASK + + extra += mask + + return six.b(chr(first_byte) + chr(second_byte) + extra.decode('utf-8')) diff --git a/perf-requirements.txt b/perf-requirements.txt new file mode 100644 index 0000000..70d3d4f --- /dev/null +++ b/perf-requirements.txt @@ -0,0 +1,5 @@ +cython +wsaccel +ujson + +-r requirements.txt diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..6b2c94d --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +gevent==1.0 +greenlet==0.4.1 +six diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..29e09d7 --- /dev/null +++ b/setup.py @@ -0,0 +1,30 @@ +from setuptools import setup, find_packages + + +version = __import__('geventwebsocket').get_version() + +setup( + name="gevent-websocket", + version=version, + url="https://bitbucket.org/Jeffrey/gevent-websocket", + author="Jeffrey Gelens", + author_email="jeffrey@noppo.pro", + description=("Websocket handler for the gevent pywsgi server, a Python " + "network library"), + long_description=open("README.rst").read(), + download_url="https://bitbucket.org/Jeffrey/gevent-websocket", + packages=find_packages(exclude=["examples", "tests"]), + license=open('LICENSE').read(), + zip_safe=False, + install_requires=("gevent", "six>=1.5.2"), + classifiers=[ + "Environment :: Web Environment", + "Intended Audience :: Developers", + "License :: OSI Approved :: Apache Software License", + "Operating System :: MacOS :: MacOS X", + "Operating System :: POSIX", + "Programming Language :: Python", + "Topic :: Internet", + "Topic :: Software Development :: Libraries :: Python Modules", + ] +) diff --git a/test-requirements.txt b/test-requirements.txt new file mode 100644 index 0000000..9a138cf --- /dev/null +++ b/test-requirements.txt @@ -0,0 +1,3 @@ +autobahntestsuite + +-r requirements.txt diff --git a/tests/run_autobahn_tests.py b/tests/run_autobahn_tests.py new file mode 100755 index 0000000..5798a64 --- /dev/null +++ b/tests/run_autobahn_tests.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python +""" +Test gevent-websocket with the test suite of Autobahn +http://autobahn.ws/testsuite +""" +import sys +import subprocess +import time +import six +from twisted.python import log +from twisted.internet import reactor +from autobahntestsuite.fuzzing import FuzzingClientFactory + +spec = { + "options": {"failByDrop": False}, + "enable-ssl": False, + "servers": []} + +default_args = ['*'] + + +class ProcessPool(object): + def __init__(self): + self.popens = [] + + def spawn(self, *args, **kwargs): + popen = subprocess.Popen(*args, **kwargs) + self.popens.append(popen) + time.sleep(0.2) + self.check() + return popen + + def check(self): + for popen in self.popens: + if popen.poll() is not None: + sys.exit(1) + + def wait(self, timeout): + end = time.time() + timeout + while True: + time.sleep(0.1) + pool.check() + if time.time() > end: + break + + def kill(self): + while self.popens: + popen = self.popens.pop() + try: + popen.kill() + except Exception as ex: + print(ex) + + +if __name__ == '__main__': + import optparse + parser = optparse.OptionParser() + parser.add_option('--geventwebsocket', default='../examples/echoserver.py') + options, args = parser.parse_args() + + # Load cases + cases = [] + exclude_cases = [] + + for arg in (args or default_args): + if arg.startswith('x'): + arg = arg[1:] + exclude_cases.append(arg) + else: + cases.append(arg) + + spec['cases'] = cases + spec['exclude-cases'] = exclude_cases + + pool = ProcessPool() + + try: + if options.geventwebsocket: + pool.spawn([sys.executable, options.geventwebsocket]) + + pool.wait(1) + + if options.geventwebsocket: + agent = six.moves.urllib.request.urlopen('http://127.0.0.1:8000/version').read().strip() + + assert agent and '\n' not in agent and 'gevent-websocket' in agent, agent + + spec['servers'].append({"url": "ws://localhost:8000", + "agent": agent, + "options": {"version": 18}}) + + log.startLogging(sys.stdout) + + # Start testing the server using the FuzzingClient + FuzzingClientFactory(spec) + + reactor.run() + finally: + pool.kill()