├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
└── labeler.yml
├── .gitignore
├── LICENSE
├── MANIFEST.in
├── README.md
├── assests
└── icon.png
├── client.html
├── docs
├── README.md
├── _config.yml
├── app.md
├── framework.md
├── handler.md
├── middleware.md
├── server.md
└── websocket.md
├── pyproject.toml
├── setup.cfg
├── setup.py
├── test.py
├── tox.ini
└── wsocket.py
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: BUG
5 | labels: bug
6 | assignees: Ksenginew
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
23 | **Screenshots**
24 | If applicable, add screenshots to help explain your problem.
25 |
26 | **Additional context**
27 | Add any other context about the problem here.
28 |
29 | ---
30 |
31 | **Practicalities**
32 | - YES/NO I am willing to work on this issue myself.
33 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: FEATURE
5 | labels: enhancement, feature
6 | assignees: Ksenginew
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/.github/labeler.yml:
--------------------------------------------------------------------------------
1 | # Add 'repo' label to any root file changes
2 | repo:
3 | - ./*
4 |
5 | docs:
6 | - ./docs/*
7 |
8 | source:
9 | - ./*.py
10 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | pip-wheel-metadata/
24 | share/python-wheels/
25 | *.egg-info/
26 | .installed.cfg
27 | *.egg
28 | MANIFEST
29 |
30 | # PyInstaller
31 | # Usually these files are written by a python script from a template
32 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
33 | *.manifest
34 | *.spec
35 |
36 | # Installer logs
37 | pip-log.txt
38 | pip-delete-this-directory.txt
39 |
40 | # Unit test / coverage reports
41 | htmlcov/
42 | .tox/
43 | .nox/
44 | .coverage
45 | .coverage.*
46 | .cache
47 | nosetests.xml
48 | coverage.xml
49 | *.cover
50 | *.py,cover
51 | .hypothesis/
52 | .pytest_cache/
53 |
54 | # Translations
55 | *.mo
56 | *.pot
57 |
58 | # Django stuff:
59 | *.log
60 | local_settings.py
61 | db.sqlite3
62 | db.sqlite3-journal
63 |
64 | # Flask stuff:
65 | instance/
66 | .webassets-cache
67 |
68 | # Scrapy stuff:
69 | .scrapy
70 |
71 | # Sphinx documentation
72 | docs/_build/
73 |
74 | # PyBuilder
75 | target/
76 |
77 | # Jupyter Notebook
78 | .ipynb_checkpoints
79 |
80 | # IPython
81 | profile_default/
82 | ipython_config.py
83 |
84 | # pyenv
85 | .python-version
86 |
87 | # pipenv
88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
91 | # install all needed dependencies.
92 | #Pipfile.lock
93 |
94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow
95 | __pypackages__/
96 |
97 | # Celery stuff
98 | celerybeat-schedule
99 | celerybeat.pid
100 |
101 | # SageMath parsed files
102 | *.sage.py
103 |
104 | # Environments
105 | .env
106 | .venv
107 | env/
108 | venv/
109 | ENV/
110 | env.bak/
111 | venv.bak/
112 |
113 | # Spyder project settings
114 | .spyderproject
115 | .spyproject
116 |
117 | # Rope project settings
118 | .ropeproject
119 |
120 | # mkdocs documentation
121 | /site
122 |
123 | # mypy
124 | .mypy_cache/
125 | .dmypy.json
126 | dmypy.json
127 |
128 | # Pyre type checker
129 | .pyre/
130 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Ksengine
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include wsocket.py
2 | include setup.py
3 | include README.rst
4 | include LICENSE
5 | include test/views/*.tpl
6 | include test/*.py
7 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://pepy.tech/project/wsocket)
2 | [](https://github.com/Ksengine/WSocket/issues)
3 | [](https://github.com/Ksengine/WSocket/network)
4 | [](https://github.com/Ksengine/WSocket/stargazers)
5 | [](https://github.com/Ksengine/WSocket/blob/master/LICENSE)
6 | [](https://twitter.com/intent/tweet?text=Wow:&url=https%3A%2F%2Fgithub.com%2FKsengine%2FWSocket)
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
17 | Simple WSGI HTTP + Websocket Server, Framework, Middleware And App.
18 |
19 |
20 | Explore the docs »
21 |
22 |
23 | PyPI
24 | ·
25 | Report Bug
26 | ·
27 | Request Feature
28 |
ZeroDivisionError :division by zero
Traceback (most recent call last): 252 | > File "wsocket.py", line 881, in process_response 253 | > results = self.app(self.environ, self.start_response) 254 | > File "wsocket.py", line 1068, in wsgi 255 | > 1/0 256 | > ZeroDivisionError: division by zero 257 | >258 | 259 | report button starts reporting issue 260 | and logger will print error to python console 261 | -------------------------------------------------------------------------------- /docs/handler.md: -------------------------------------------------------------------------------- 1 | 2 | # Server 3 | Server([WSGI](http://www.wsgi.org/)) creates and listens at the HTTP socket, dispatching the requests to a handler. WSGIRef server but uses threads to handle requests by using the ThreadingMixIn. This is useful to handle web browsers pre-opening sockets, on which Server would wait indefinitely. **can used with any WSGI compatible web framework** 4 | > this is a wsgiref based server 5 | 6 | [`wsgiref`](https://docs.python.org/3/library/wsgiref.html "(in Python v3.x)") is a built-in WSGI package that provides various classes and helpers to develop against WSGI. Mostly it provides a basic WSGI server that can be used for testing or simple demos. WSocket provides support for websocket on wsgiref for testing purpose. It can only initiate connections one at a time, as a result of being single threaded. 7 | **but WSocket WSGI server is multi threaded HTTP server. So it can handle many connections at a time.** 8 | 9 | ## `wsocket.run(app=WSocketApp(), host="127.0.0.1", port=8080, handler_cls=FixedHandler, server_cls=ThreadingWSGIServer)` 10 | if app not given it runs demo app 11 | you can use following values as `host` to run local server(named localhost) 12 | - `"localhost"` 13 | - `"127.0.0.1"` 14 | - `""` 15 | 16 | default `host` is "127.0.0.1". 17 | default `port` is 8080. If host is 0, It will choose random port 18 | default `handler_cls` is [`FixedHandler`](handler.md) 19 | default `server_cls` is [`ThreadingWSGIServer`](#`wsocket.ThreadingWSGIServer`) 20 | `app` should be a valid [WSGI](http://www.wsgi.org/) application. 21 | **example :** 22 | ```python 23 | from wsocket import run, WSocketApp 24 | app = WSocketApp() 25 | run('', 8080, app) 26 | ``` 27 | 28 | ## `wsocket.make_server(host, port, app, server_class, handler_class)` 29 | Create a new WSGIServer server listening on _host_ and _port_, accepting connections for _app_. The return value is an instance of the supplied _server_class_, and will process requests using the specified _handler_class_. _app_ must be a WSGI application object, as defined by [**PEP 3333**](https://www.python.org/dev/peps/pep-3333). 30 | 31 | **example :** 32 | ```python 33 | from wsocket import WebSocketHandler, WSocketApp, make_server, ThreadingWSGIServer 34 | 35 | server = make_server('', 8080, server_class=ThreadingWSGIServer, 36 | handler_class=WebSocketHandler, 37 | app=WSocketApp()) 38 | server.serve_forever() 39 | ``` 40 | 41 | ## `wsocket.ThreadingWSGIServer(server_address, RequestHandlerClass)` 42 | Create a `ThreadingWSGIServer` instance. _server_address_ should be a `(host,port)` tuple, and _RequestHandlerClass_ should be the subclass of [`http.server.BaseHTTPRequestHandler`](https://docs.python.org/3/library/http.server.html#http.server.BaseHTTPRequestHandler "http.server.BaseHTTPRequestHandler") that will be used to process requests. 43 | 44 | You do not normally need to call this constructor, as the [`make_server()`](#make_server) function can handle all the details for you. 45 | 46 | `ThreadingWSGIServer` is a subclass of [`WSGIServer`](https://docs.python.org/3/library/wsgiref.html#wsgiref.simple_server.WSGIServer "wsgiref.simple_server.WSGIServer"). [`ThreadingWSGIServer`] also provides these WSGI-specific methods: 47 | 48 | `set_app(application)` - Sets the callable _application_ as the WSGI application that will receive requests. 49 | 50 | `get_app()` - Returns the currently-set application callable. 51 | 52 | Normally, however, you do not need to use these additional methods, as [`set_app()`] is normally called by [`make_server()`](#make_server), and the [`get_app()`] exists mainly for the benefit of request handler instances. 53 | -------------------------------------------------------------------------------- /docs/middleware.md: -------------------------------------------------------------------------------- 1 | ## Middleware 2 | 3 | convert any WSGI compatible web framework to Websocket+HTTP framework using middleware. **works with many WSGI compatible servers** 4 | 5 | **can used with any [WSGI](http://www.wsgi.org/) compatible web framework** 6 | > Flask, Django, Pyramid, Bottle, ... supported 7 | 8 | This middleware adds [`wsgi.websocket`](https://github.com/Ksengine/WSocket/tree/master/docs/websocket.md) variable to [WSGI](http://www.wsgi.org/) environment dictionary. 9 | 10 | example with [bottle](https://github.com/bottlepy/bottle): 11 | ```python 12 | from bottle import request, Bottle 13 | from wsocket import WSocketApp, WebSocketError, logger, run 14 | from time import sleep 15 | 16 | logger.setLevel(10) # for debugging 17 | 18 | bottle = Bottle() 19 | app = WSocketApp(bottle) 20 | # app = WSocketApp(bottle, "WAMP") 21 | 22 | @bottle.route("/") 23 | def handle_websocket(): 24 | wsock = request.environ.get("wsgi.websocket") 25 | if not wsock: 26 | return "Hello World!" 27 | 28 | while True: 29 | try: 30 | message = wsock.receive() 31 | if message != None: 32 | print("participator : " + message) 33 | wsock.send("you : "+message) 34 | sleep(2) 35 | wsock.send("you : "+message) 36 | except WebSocketError: 37 | break 38 | run(app) 39 | ``` 40 | > you should use HTTP version `1.1` Server with your [WSGI](http://www.wsgi.org/) framework for some clients like Firefox browser 41 | 42 | **for examples on other web frameworks visit [`examples/frameworks`](https://github.com/Ksengine/WSocket/tree/master/examples/frameworks) folder 43 | ## `class WSocketApp(app=None, protocol=None)` 44 | `app` should be a valid [WSGI](http://www.wsgi.org/) web application. 45 | `protocol` is websocket sub protocol to accept (ex: [WAMP](https://wamp-proto.org/)) 46 | 47 | ### Class variables 48 | 49 | `GUID` - unique ID to generate websocket accept key 50 | 51 | `SUPPORTED_VERSIONS` - 13, 8 or 7 52 | 53 | `websocket_class` - `"wsgi.websocket"` in WSGI Environ 54 | -------------------------------------------------------------------------------- /docs/server.md: -------------------------------------------------------------------------------- 1 | # Server 2 | Server([WSGI](http://www.wsgi.org/)) creates and listens at the HTTP socket, dispatching the requests to a handler. WSGIRef server but uses threads to handle requests by using the ThreadingMixIn. This is useful to handle web browsers pre-opening sockets, on which Server would wait indefinitely. **can used with any WSGI compatible web framework** 3 | > this is a wsgiref based server 4 | 5 | [`wsgiref`](https://docs.python.org/3/library/wsgiref.html "(in Python v3.x)") is a built-in WSGI package that provides various classes and helpers to develop against WSGI. Mostly it provides a basic WSGI server that can be used for testing or simple demos. WSocket provides support for websocket on wsgiref for testing purpose. It can only initiate connections one at a time, as a result of being single threaded. 6 | **but WSocket WSGI server is multi threaded HTTP server. So it can handle many connections at a time.** 7 | 8 | ## `wsocket.run(app=WSocketApp(), host="127.0.0.1", port=8080, handler_cls=FixedHandler, server_cls=ThreadingWSGIServer)` 9 | if app not given it runs demo app 10 | you can use following values as `host` to run local server(named localhost) 11 | - `"localhost"` 12 | - `"127.0.0.1"` 13 | - `""` 14 | 15 | default `host` is "127.0.0.1". 16 | default `port` is 8080. If host is 0, It will choose random port 17 | default `handler_cls` is [`FixedHandler`](handler.md) 18 | default `server_cls` is [`ThreadingWSGIServer`](#`wsocket.ThreadingWSGIServer`) 19 | `app` should be a valid [WSGI](http://www.wsgi.org/) application. 20 | **example :** 21 | ```python 22 | from wsocket import run, WSocketApp 23 | app = WSocketApp() 24 | run('', 8080, app) 25 | ``` 26 | 27 | ## `wsocket.make_server(host, port, app, server_class, handler_class)` 28 | Create a new WSGIServer server listening on _host_ and _port_, accepting connections for _app_. The return value is an instance of the supplied _server_class_, and will process requests using the specified _handler_class_. _app_ must be a WSGI application object, as defined by [**PEP 3333**](https://www.python.org/dev/peps/pep-3333). 29 | 30 | **example :** 31 | ```python 32 | from wsocket import WebSocketHandler, WSocketApp, make_server, ThreadingWSGIServer 33 | 34 | server = make_server('', 8080, server_class=ThreadingWSGIServer, 35 | handler_class=WebSocketHandler, 36 | app=WSocketApp()) 37 | server.serve_forever() 38 | ``` 39 | 40 | ## `wsocket.ThreadingWSGIServer(server_address, RequestHandlerClass)` 41 | Create a `ThreadingWSGIServer` instance. _server_address_ should be a `(host,port)` tuple, and _RequestHandlerClass_ should be the subclass of [`http.server.BaseHTTPRequestHandler`](https://docs.python.org/3/library/http.server.html#http.server.BaseHTTPRequestHandler "http.server.BaseHTTPRequestHandler") that will be used to process requests. 42 | 43 | You do not normally need to call this constructor, as the [`make_server()`](#make_server) function can handle all the details for you. 44 | 45 | `ThreadingWSGIServer` is a subclass of [`WSGIServer`](https://docs.python.org/3/library/wsgiref.html#wsgiref.simple_server.WSGIServer "wsgiref.simple_server.WSGIServer"). [`ThreadingWSGIServer`] also provides these WSGI-specific methods: 46 | 47 | `set_app(application)` - Sets the callable _application_ as the WSGI application that will receive requests. 48 | 49 | `get_app()` - Returns the currently-set application callable. 50 | 51 | Normally, however, you do not need to use these additional methods, as [`set_app()`] is normally called by [`make_server()`](#make_server), and the [`get_app()`] exists mainly for the benefit of request handler instances. 52 | -------------------------------------------------------------------------------- /docs/websocket.md: -------------------------------------------------------------------------------- 1 | 2 | # Websocket 3 | ### Class variables 4 | - `origin` - HTTP `Origin` header 5 | 6 | - `protocol` - supported websocket sub protocol 7 | 8 | - `version` - websocket version(1, 8 or 7) 9 | 10 | - `path` - required path by client(eg:-if websocket url which client opened is `ws://localhost/hello/world?user=Ksengine&pass=1234`, path is `/hello/world`) 11 | 12 | - `logger` = default logger([Python Docs](https://docs.python.org/3/library/logging.html)) 13 | 14 | - `do_compress` - is compressed messages required by client 15 | 16 | ### Class methods 17 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | # These are the assumed default build requirements from pip: 3 | # https://pip.pypa.io/en/stable/reference/pip/#pep-517-and-518-support 4 | requires = ["setuptools>=40.8.0", "wheel"] 5 | build-backend = "setuptools.build_meta" 6 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal = 1 3 | 4 | [metadata] 5 | license_file = LICENSE 6 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """A setuptools based setup module. 5 | See: 6 | https://packaging.python.org/guides/distributing-packages-using-setuptools/ 7 | https://github.com/pypa/sampleproject 8 | """ 9 | 10 | # Always prefer setuptools over distutils 11 | 12 | from setuptools import setup 13 | from os import path 14 | 15 | import wsocket 16 | devstatus=[ 17 | 'Planning', 18 | 'Pre-Alpha', 19 | 'Alpha', 20 | 'Beta', 21 | 'Production/Stable', 22 | 'Mature', 23 | 'Inactive' 24 | ] 25 | 26 | here = path.dirname(__file__) 27 | # Get the long description from the README file 28 | long_description = open(path.join(here, 'README.md')).read() 29 | 30 | setup( 31 | 32 | # https://packaging.python.org/specifications/core-metadata/#name 33 | name='WSocket', 34 | 35 | # https://www.python.org/dev/peps/pep-0440/ 36 | # https://packaging.python.org/en/latest/single_source_version.html 37 | version=wsocket.__version__, 38 | 39 | # https://packaging.python.org/specifications/core-metadata/#summary 40 | description='Simple WSGI Websocket Server, Framework, Middleware And App', 41 | 42 | # https://packaging.python.org/specifications/core-metadata/#description-optional 43 | long_description=long_description, 44 | 45 | # text/plain, text/x-rst, and text/markdown 46 | # https://packaging.python.org/specifications/core-metadata/#description-content-type-optional 47 | long_description_content_type="text/markdown", 48 | 49 | author=wsocket.__author__, 50 | author_email='kavindusanthusa@gmail.com', 51 | 52 | # https://packaging.python.org/specifications/core-metadata/#home-page-optional 53 | url='https://github.com/Ksengine/wsocket/', 54 | 55 | keywords='sample, setuptools, development', # Optional 56 | 57 | py_modules=['wsocket'], 58 | scripts=['wsocket.py'], 59 | license='MIT', 60 | platforms='any', 61 | classifiers=['Development Status :: {} - {}'.format(wsocket.__status__, devstatus[wsocket.__status__-1]), 62 | "Operating System :: OS Independent", 63 | 'Intended Audience :: Developers', 64 | 'License :: OSI Approved :: MIT License', 65 | 'Topic :: Internet :: WWW/HTTP :: Dynamic Content :: CGI Tools/Libraries', 66 | 'Topic :: Internet :: WWW/HTTP :: HTTP Servers', 67 | 'Topic :: Internet :: WWW/HTTP :: WSGI', 68 | 'Topic :: Internet :: WWW/HTTP :: WSGI :: Application', 69 | 'Topic :: Internet :: WWW/HTTP :: WSGI :: Middleware', 70 | 'Topic :: Internet :: WWW/HTTP :: WSGI :: Server', 71 | 'Topic :: Software Development :: Libraries :: Application Frameworks', 72 | 'Programming Language :: Python', 73 | 'Programming Language :: Python :: 2', 74 | 'Programming Language :: Python :: 2.3', 75 | 'Programming Language :: Python :: 2.4', 76 | 'Programming Language :: Python :: 2.5', 77 | 'Programming Language :: Python :: 2.6', 78 | 'Programming Language :: Python :: 2.7', 79 | 'Programming Language :: Python :: 3', 80 | 'Programming Language :: Python :: 3.0', 81 | 'Programming Language :: Python :: 3.1', 82 | 'Programming Language :: Python :: 3.2', 83 | 'Programming Language :: Python :: 3.3', 84 | 'Programming Language :: Python :: 3.4', 85 | 'Programming Language :: Python :: 3.5', 86 | 'Programming Language :: Python :: 3.6', 87 | 'Programming Language :: Python :: 3.7', 88 | 'Programming Language :: Python :: 3.8', 89 | 'Programming Language :: Python :: 3.9', 90 | 'Programming Language :: Python :: 3.10', 91 | 'Programming Language :: Python :: Implementation', 92 | 'Programming Language :: Python :: Implementation :: CPython', 93 | 'Programming Language :: Python :: Implementation :: IronPython', 94 | 'Programming Language :: Python :: Implementation :: Jython', 95 | 'Programming Language :: Python :: Implementation :: PyPy', 96 | 'Programming Language :: Python :: Implementation :: Stackless', 97 | 'Framework :: WSocket', 98 | 'Topic :: Communications :: Chat', 99 | 'Topic :: Internet', 100 | 'Topic :: Internet :: WWW/HTTP', 101 | 'Topic :: Internet :: WWW/HTTP :: Browsers', 102 | 'Topic :: Internet :: WWW/HTTP :: HTTP Servers', 103 | 'Topic :: Internet :: WWW/HTTP :: WSGI', 104 | 'Topic :: Internet :: WWW/HTTP :: WSGI :: Application', 105 | 'Topic :: Internet :: WWW/HTTP :: WSGI :: Middleware', 106 | 'Topic :: Internet :: WWW/HTTP :: WSGI :: Server', 107 | ], 108 | 109 | # https://packaging.python.org/specifications/core-metadata/#project-url-multiple-use 110 | project_urls={ 111 | 'Bug Reports': 'https://github.com/Ksengine/wsocket/issues/', 112 | 'Documentation': 'https://wsocket.gitbook.io/', 113 | 'Source': 'https://github.com/Ksengine/wsocket/', 114 | }, 115 | 116 | ) 117 | 118 | -------------------------------------------------------------------------------- /test.py: -------------------------------------------------------------------------------- 1 | from bottle import request, Bottle 2 | from wsocket import WebSocketHandler,logger 3 | from sl.server import ThreadingWSGIServer 4 | from time import sleep 5 | logger.setLevel(10) 6 | app = Bottle() 7 | 8 | @app.route('/') 9 | def handle_websocket(): 10 | wsock = request.environ.get('wsgi.websocket') 11 | if not wsock: 12 | return 'Hello World!' 13 | while True: 14 | message = wsock.receive() 15 | if not message: 16 | break 17 | print(message) 18 | wsock.send('Your message was: %r' % message) 19 | sleep(3) 20 | wsock.send('Your message was: %r' % message) 21 | 22 | httpd = ThreadingWSGIServer(('localhost',9001),WebSocketHandler) 23 | httpd.set_app(app) 24 | print('WSGIServer: Serving HTTP on port 9001 ...\n') 25 | try: 26 | httpd.serve_forever() 27 | except: 28 | print('WSGIServer: Server Stopped') 29 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | # this file is *not* meant to cover or endorse the use of tox or pytest or 2 | # testing in general, 3 | # 4 | # It's meant to show the use of: 5 | # 6 | # - check-manifest 7 | # confirm items checked into vcs are in your sdist 8 | # - python setup.py check 9 | # confirm required package meta-data in setup.py 10 | # - readme_renderer (when using a ReStructuredText README) 11 | # confirms your long_description will render correctly on PyPI. 12 | # 13 | # and also to help confirm pull requests to this project. 14 | 15 | [tox] 16 | envlist = py{35,36,37,38} 17 | 18 | # Define the minimal tox version required to run; 19 | # if the host tox is less than this the tool with create an environment and 20 | # provision it with a tox that satisfies it under provision_tox_env. 21 | # At least this version is needed for PEP 517/518 support. 22 | minversion = 3.3.0 23 | 24 | # Activate isolated build environment. tox will use a virtual environment 25 | # to build a source distribution from the source tree. For build tools and 26 | # arguments use the pyproject.toml file as specified in PEP-517 and PEP-518. 27 | isolated_build = true 28 | 29 | [testenv] 30 | deps = 31 | check-manifest >= 0.42 32 | # If your project uses README.rst, uncomment the following: 33 | # readme_renderer 34 | flake8 35 | pytest 36 | commands = 37 | check-manifest --ignore 'tox.ini,tests/**' 38 | # This repository uses a Markdown long_description, so the -r flag to 39 | # `setup.py check` is not needed. If your project contains a README.rst, 40 | # use `python setup.py check -m -r -s` instead. 41 | python setup.py check -m -s 42 | flake8 . 43 | py.test tests {posargs} 44 | 45 | [flake8] 46 | exclude = .tox,*.egg,build,data 47 | select = E,W,F 48 | -------------------------------------------------------------------------------- /wsocket.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | WSocket is a Simple WSGI Websocket Server, Framework, Middleware And 5 | App. It also offers a basic WSGI framework with routes handler, a 6 | built-in HTTP Server and event based websocket application. all in a 7 | single file and with no dependencies other than the Python Standard 8 | Library. 9 | 10 | Homepage and documentation: https://wsocket.gitbook.io 11 | 12 | Copyright (c) 2020, Kavindu Santhusa. 13 | License: MIT 14 | """ 15 | # Imports 16 | from __future__ import absolute_import, division, print_function 17 | 18 | from base64 import b64decode, b64encode 19 | from hashlib import sha1 20 | from sys import version_info, exc_info 21 | from os import urandom 22 | from threading import Thread 23 | from time import sleep 24 | import traceback 25 | import logging 26 | import zlib 27 | import struct 28 | import socket 29 | from socket import error as socket_error 30 | from wsgiref.simple_server import make_server, ServerHandler, WSGIRequestHandler, WSGIServer 31 | 32 | try: # Py3 33 | from socketserver import ThreadingMixIn 34 | from urllib.parse import urlencode 35 | 36 | except ImportError: # Py2 37 | from SocketServer import ThreadingMixIn 38 | from urllib import urlencode 39 | 40 | try: 41 | import ssl 42 | except ImportError as e: 43 | ssl_err = e 44 | 45 | class ssl(): 46 | def __getattr__(self, name): 47 | raise ssl_err 48 | 49 | 50 | __author__ = "Kavindu Santhusa" 51 | __version__ = "2.1.1" 52 | __license__ = "MIT" 53 | __status__ = 4 # see setup.py 54 | 55 | logger = logging.getLogger(__name__) 56 | logging.basicConfig() 57 | 58 | # python compatability 59 | PY3 = version_info[0] >= 3 60 | if PY3: 61 | import http.client as httplib 62 | from urllib.parse import urlparse 63 | text_type = str 64 | string_types = (str, ) 65 | range_type = range 66 | 67 | else: 68 | import httplib 69 | from urlparse import urlparse 70 | bytes = str 71 | text_type = unicode 72 | string_types = basestring 73 | range_type = xrange 74 | 75 | # websocket OPCODES 76 | OPCODE_CONTINUATION = 0x00 77 | OPCODE_TEXT = 0x01 78 | OPCODE_BINARY = 0x02 79 | OPCODE_CLOSE = 0x08 80 | OPCODE_PING = 0x09 81 | OPCODE_PONG = 0x0A 82 | FIN_MASK = 0x80 83 | OPCODE_MASK = 0x0F 84 | MASK_MASK = 0x80 85 | LENGTH_MASK = 0x7F 86 | RSV0_MASK = 0x40 87 | RSV1_MASK = 0x20 88 | RSV2_MASK = 0x10 89 | HEADER_FLAG_MASK = RSV0_MASK | RSV1_MASK | RSV2_MASK 90 | 91 | # default messages 92 | MSG_SOCKET_DEAD = "Socket is dead" 93 | MSG_ALREADY_CLOSED = "Connection is already closed" 94 | MSG_CLOSED = "Connection closed" 95 | 96 | # from bottlepy/bottle 97 | #: A dict to map HTTP status codes (e.g. 404) to phrases (e.g. 'Not Found') 98 | HTTP_CODES = httplib.responses.copy() 99 | HTTP_CODES[418] = "I'm a teapot" # RFC 2324 100 | HTTP_CODES[428] = "Precondition Required" 101 | HTTP_CODES[429] = "Too Many Requests" 102 | HTTP_CODES[431] = "Request Header Fields Too Large" 103 | HTTP_CODES[451] = "Unavailable For Legal Reasons" # RFC 7725 104 | HTTP_CODES[511] = "Network Authentication Required" 105 | _HTTP_STATUS_LINES = dict( 106 | (k, "%d %s" % (k, v)) for (k, v) in HTTP_CODES.items()) 107 | 108 | 109 | def log_traceback(ex): 110 | """generates error log from Exception object.""" 111 | if PY3: 112 | ex_traceback = ex.__traceback__ 113 | else: 114 | _, _, ex_traceback = exc_info() 115 | tb_lines = '' 116 | for line in traceback.format_exception(ex.__class__, ex, ex_traceback): 117 | tb_lines += str(line) 118 | return tb_lines 119 | 120 | 121 | class WebSocketError(socket_error): 122 | """ 123 | Base class for all websocket errors. 124 | """ 125 | 126 | pass 127 | 128 | 129 | class ProtocolError(WebSocketError): 130 | """ 131 | Raised if an error occurs when de/encoding the websocket protocol. 132 | """ 133 | 134 | pass 135 | 136 | 137 | class FrameTooLargeException(ProtocolError): 138 | """ 139 | Raised if a frame is received that is too large. 140 | """ 141 | 142 | pass 143 | 144 | 145 | class ThreadingWSGIServer(ThreadingMixIn, WSGIServer): 146 | """This class is identical to WSGIServer but uses threads to handle 147 | requests by using the ThreadingMixIn. This is useful to handle web 148 | browsers pre-opening sockets, on which Server would wait indefinitely. 149 | """ 150 | 151 | multithread = True 152 | daemon_threads = True 153 | 154 | 155 | class FixedServerHandler(ServerHandler): # fixed serverhandler 156 | http_version = "1.1" # http versions below 1.1 is not supported by some clients such as Firefox 157 | 158 | def _convert_string_type(self, value, 159 | title): # not in old versions of wsgiref 160 | """Convert/check value type.""" 161 | if isinstance(value, string_types): 162 | return value 163 | 164 | raise AssertionError("{0} must be of type str (got {1})".format( 165 | title, repr(value))) 166 | 167 | def start_response(self, status, headers, exc_info=None): 168 | """'start_response()' callable as specified by PEP 3333""" 169 | 170 | if exc_info: 171 | try: 172 | if self.headers_sent: 173 | # Re-raise original exception if headers sent 174 | raise exc_info[0](exc_info[1]).with_traceback(exc_info[2]) 175 | 176 | finally: 177 | exc_info = None # avoid dangling circular ref 178 | 179 | elif self.headers is not None: 180 | raise AssertionError("Headers already set!") 181 | 182 | self.status = status 183 | self.headers = self.headers_class(headers) 184 | status = self._convert_string_type(status, "Status") 185 | assert len(status) >= 4, "Status must be at least 4 characters" 186 | assert status[:3].isdigit(), "Status message must begin w/3-digit code" 187 | assert status[3] == " ", "Status message must have a space after code" 188 | 189 | if __debug__: 190 | for name, val in headers: 191 | name = self._convert_string_type(name, "Header name") 192 | val = self._convert_string_type(val, "Header value") 193 | # removed hop by hop headers check otherwise it raises AssertionError for Upgrade and Connection headers 194 | # assert not is_hop_by_hop( 195 | # name 196 | # ), "Hop-by-hop header, '{}: {}', not allowed".format(name, val) 197 | 198 | self.send_headers() 199 | return self.write 200 | 201 | 202 | class FixedHandler(WSGIRequestHandler): # fixed request handler 203 | def address_string(self): # Prevent reverse DNS lookups please. 204 | return self.client_address[0] 205 | 206 | def log_request(self, *args, **kw): 207 | if not self.quiet: 208 | return WSGIRequestHandler.log_request(self, *args, **kw) 209 | 210 | def get_app(self): 211 | return self.server.get_app() 212 | 213 | def handle(self 214 | ): # to add FixedServerHandler we had to override entire method 215 | """Handle a single HTTP request""" 216 | 217 | self.raw_requestline = self.rfile.readline(65537) 218 | if len(self.raw_requestline) > 65536: 219 | self.requestline = "" 220 | self.request_version = "" 221 | self.command = "" 222 | self.send_error(414) 223 | return 224 | 225 | if not self.parse_request(): # An error code has been sent, just exit 226 | return 227 | 228 | handler = FixedServerHandler(self.rfile, self.wfile, self.get_stderr(), 229 | self.get_environ()) 230 | handler.request_handler = self # backpointer for logging 231 | handler.run(self.get_app()) 232 | 233 | 234 | class WebSocket(object): 235 | """ 236 | Base class for supporting websocket operations. 237 | """ 238 | 239 | origin = None 240 | protocol = None 241 | version = None 242 | path = None 243 | logger = logger 244 | 245 | def __init__(self, environ, read, write, handler, do_compress): 246 | self.environ = environ 247 | self.closed = False 248 | self.write = write 249 | self.read = read 250 | self.handler = handler 251 | self.do_compress = do_compress 252 | self.origin = self.environ.get( 253 | "HTTP_SEC_WEBSOCKET_ORIGIN") or self.environ.get("HTTP_ORIGIN") 254 | self.protocols = list( 255 | map(str.strip, 256 | self.environ.get("HTTP_SEC_WEBSOCKET_PROTOCOL", 257 | "").split(","))) 258 | self.version = int( 259 | self.environ.get("HTTP_SEC_WEBSOCKET_VERSION", "0").strip()) 260 | self.path = self.environ.get("PATH_INFO", "/") 261 | if do_compress: 262 | self.compressor = zlib.compressobj(7, zlib.DEFLATED, 263 | -zlib.MAX_WBITS) 264 | self.decompressor = zlib.decompressobj(-zlib.MAX_WBITS) 265 | 266 | def __del__(self): 267 | try: 268 | self.close() 269 | except: 270 | # close() may fail if __init__ didn't complete 271 | pass 272 | 273 | def _decode_bytes(self, bytestring): 274 | if not bytestring: 275 | return "" 276 | 277 | try: 278 | return bytestring.decode("utf-8") 279 | 280 | except UnicodeDecodeError as e: 281 | print('UnicodeDecodeError') 282 | self.close(1007, str(e)) 283 | raise 284 | 285 | def _encode_bytes(self, text): 286 | if not isinstance(text, str): 287 | text = text_type(text or "") 288 | 289 | return text.encode("utf-8") 290 | 291 | def _is_valid_close_code(self, code): 292 | # valid hybi close code? 293 | if (code < 1000 or 1004 <= code <= 1006 or 1012 <= code <= 1016 294 | or code == 295 | 1100 # not sure about this one but the autobahn fuzzer requires it. 296 | or 2000 <= code <= 2999): 297 | return False 298 | 299 | return True 300 | 301 | def handle_close(self, payload): 302 | if not payload: 303 | self.close(1000, "") 304 | return 305 | 306 | if len(payload) < 2: 307 | raise ProtocolError("Invalid close frame: %s" % payload) 308 | 309 | code = struct.unpack("!H", payload[:2])[0] 310 | payload = payload[2:] 311 | if payload: 312 | payload.decode("utf-8") 313 | 314 | if not self._is_valid_close_code(code): 315 | raise ProtocolError("Invalid close code %s" % code) 316 | 317 | self.close(code, payload) 318 | 319 | def handle_ping(self, payload): 320 | self.send_frame(payload, self.OPCODE_PONG) 321 | 322 | def handle_pong(self, payload): 323 | pass 324 | 325 | def mask_payload(self, mask, length, payload): 326 | payload = bytearray(payload) 327 | mask = bytearray(mask) 328 | for i in range_type(length): 329 | payload[i] ^= mask[i % 4] 330 | 331 | return payload 332 | 333 | def read_message(self): 334 | opcode = None 335 | message = bytearray() 336 | 337 | while True: 338 | data = self.read(2) 339 | 340 | if len(data) != 2: 341 | first_byte, second_byte = 0, 0 342 | 343 | else: 344 | first_byte, second_byte = struct.unpack("!BB", data) 345 | 346 | fin = first_byte & FIN_MASK 347 | f_opcode = first_byte & OPCODE_MASK 348 | flags = first_byte & HEADER_FLAG_MASK 349 | length = second_byte & LENGTH_MASK 350 | has_mask = second_byte & MASK_MASK == MASK_MASK 351 | 352 | if f_opcode > 0x07: 353 | if not fin: 354 | raise ProtocolError( 355 | "Received fragmented control frame: {0!r}".format( 356 | data)) 357 | # Control frames MUST have a payload length of 125 bytes or less 358 | if length > 125: 359 | raise FrameTooLargeException( 360 | "Control frame cannot be larger than 125 bytes: " 361 | "{0!r}".format(data)) 362 | 363 | if length == 126: 364 | # 16 bit length 365 | data = self.read(2) 366 | if len(data) != 2: 367 | raise WebSocketError( 368 | "Unexpected EOF while decoding header") 369 | length = struct.unpack("!H", data)[0] 370 | 371 | elif length == 127: 372 | # 64 bit length 373 | data = self.read(8) 374 | if len(data) != 8: 375 | raise WebSocketError( 376 | "Unexpected EOF while decoding header") 377 | length = struct.unpack("!Q", data)[0] 378 | 379 | if has_mask: 380 | mask = self.read(4) 381 | if len(mask) != 4: 382 | raise WebSocketError( 383 | "Unexpected EOF while decoding header") 384 | 385 | if self.do_compress and (flags & RSV0_MASK): 386 | flags &= ~RSV0_MASK 387 | compressed = True 388 | 389 | else: 390 | compressed = False 391 | 392 | if flags: 393 | raise ProtocolError(str(flags)) 394 | 395 | if not length: 396 | payload = b"" 397 | 398 | else: 399 | try: 400 | payload = self.read(length) 401 | 402 | except socket.error: 403 | payload = b"" 404 | 405 | except Exception: 406 | raise WebSocketError("Could not read payload") 407 | 408 | if len(payload) != length: 409 | raise WebSocketError( 410 | "Unexpected EOF reading frame payload") 411 | 412 | if has_mask: 413 | payload = self.mask_payload(mask, length, payload) 414 | 415 | if compressed: 416 | payload = b"".join(( 417 | self.decompressor.decompress(bytes(payload)), 418 | self.decompressor.decompress(b"\0\0\xff\xff"), 419 | self.decompressor.flush(), 420 | )) 421 | 422 | if f_opcode in (OPCODE_TEXT, OPCODE_BINARY): 423 | # a new frame 424 | if opcode: 425 | raise ProtocolError("The opcode in non-fin frame is " 426 | "expected to be zero, got " 427 | "{0!r}".format(f_opcode)) 428 | 429 | opcode = f_opcode 430 | 431 | elif f_opcode == OPCODE_CONTINUATION: 432 | if not opcode: 433 | raise ProtocolError("Unexpected frame with opcode=0") 434 | 435 | elif f_opcode == OPCODE_PING: 436 | self.handle_ping(payload) 437 | continue 438 | 439 | elif f_opcode == OPCODE_PONG: 440 | self.handle_pong(payload) 441 | continue 442 | 443 | elif f_opcode == OPCODE_CLOSE: 444 | print('opcode close') 445 | self.handle_close(payload) 446 | return 447 | 448 | else: 449 | raise ProtocolError("Unexpected opcode={0!r}".format(f_opcode)) 450 | 451 | if opcode == OPCODE_TEXT: 452 | payload.decode("utf-8") 453 | 454 | message += payload 455 | 456 | if fin: 457 | break 458 | 459 | if opcode == OPCODE_TEXT: 460 | return self._decode_bytes(message) 461 | 462 | else: 463 | return message 464 | 465 | def receive(self): 466 | """ 467 | Read and return a message from the stream. If `None` is returned, then 468 | the socket is considered closed/errored. 469 | """ 470 | if self.closed: 471 | print('receive closed') 472 | self.handler.on_close(MSG_ALREADY_CLOSED) 473 | raise WebSocketError(MSG_ALREADY_CLOSED) 474 | 475 | try: 476 | return self.read_message() 477 | 478 | except UnicodeError as e: 479 | print('UnicodeDecodeError') 480 | self.close(1007, str(e).encode()) 481 | 482 | except ProtocolError as e: 483 | print('Protocol err', e) 484 | self.close(1002, str(e).encode()) 485 | 486 | except socket.timeout as e: 487 | print('timeout') 488 | self.close(message=str(e)) 489 | self.handler.on_close(MSG_CLOSED) 490 | 491 | except socket.error as e: 492 | print('spcket err') 493 | self.close(message=str(e)) 494 | self.handler.on_close(MSG_CLOSED) 495 | 496 | return None 497 | 498 | def encode_header(self, fin, opcode, mask, length, flags): 499 | first_byte = opcode 500 | second_byte = 0 501 | extra = b"" 502 | result = bytearray() 503 | 504 | if fin: 505 | first_byte |= FIN_MASK 506 | 507 | if flags & RSV0_MASK: 508 | first_byte |= RSV0_MASK 509 | 510 | if flags & RSV1_MASK: 511 | first_byte |= RSV1_MASK 512 | 513 | if flags & RSV2_MASK: 514 | first_byte |= RSV2_MASK 515 | 516 | if length < 126: 517 | second_byte += length 518 | 519 | elif length <= 0xFFFF: 520 | second_byte += 126 521 | extra = struct.pack("!H", length) 522 | 523 | elif length <= 0xFFFFFFFFFFFFFFFF: 524 | second_byte += 127 525 | extra = struct.pack("!Q", length) 526 | 527 | else: 528 | raise FrameTooLargeException 529 | 530 | if mask: 531 | second_byte |= MASK_MASK 532 | 533 | result.append(first_byte) 534 | result.append(second_byte) 535 | result.extend(extra) 536 | 537 | if mask: 538 | result.extend(mask) 539 | 540 | return result 541 | 542 | def send_frame(self, message, opcode, do_compress=False): 543 | if self.closed: 544 | print('receive closed') 545 | self.handler.on_close(MSG_ALREADY_CLOSED) 546 | raise WebSocketError(MSG_ALREADY_CLOSED) 547 | 548 | if not message: 549 | return 550 | 551 | if opcode in (OPCODE_TEXT, OPCODE_PING): 552 | message = self._encode_bytes(message) 553 | 554 | elif opcode == OPCODE_BINARY: 555 | message = bytes(message) 556 | 557 | if do_compress and self.do_compress: 558 | message = self.compressor.compress(message) 559 | message += self.compressor.flush(zlib.Z_SYNC_FLUSH) 560 | 561 | if message.endswith(b"\x00\x00\xff\xff"): 562 | message = message[:-4] 563 | 564 | flags = RSV0_MASK 565 | 566 | else: 567 | flags = 0 568 | 569 | header = self.encode_header(True, opcode, b"", len(message), flags) 570 | 571 | try: 572 | self.write(bytes(header + message)) 573 | 574 | except socket.error as e: 575 | raise WebSocketError(MSG_SOCKET_DEAD + " : " + str(e)) 576 | 577 | def send(self, message, binary=None, do_compress=True): 578 | """ 579 | Send a frame over the websocket with message as its payload 580 | """ 581 | 582 | if binary is None: 583 | binary = not isinstance(message, string_types) 584 | 585 | opcode = OPCODE_BINARY if binary else OPCODE_TEXT 586 | 587 | try: 588 | self.send_frame(message, opcode, do_compress) 589 | 590 | except WebSocketError: 591 | self.handler.on_close(MSG_SOCKET_DEAD) 592 | raise WebSocketError(MSG_SOCKET_DEAD) 593 | 594 | def close(self, code=1000, message=b""): 595 | """ 596 | Close the websocket and connection, sending the specified code and 597 | message. The underlying socket object is _not_ closed, that is the 598 | responsibility of the initiator. 599 | """ 600 | print("close called") 601 | if self.closed: 602 | print('receive closed') 603 | self.handler.on_close(MSG_ALREADY_CLOSED) 604 | 605 | try: 606 | message = self._encode_bytes(message) 607 | self.send_frame(struct.pack("!H%ds" % len(message), code, message), 608 | opcode=OPCODE_CLOSE) 609 | 610 | except WebSocketError: 611 | self.logger.debug( 612 | "Failed to write closing frame -> closing socket") 613 | 614 | finally: 615 | self.logger.debug("Closed WebSocket") 616 | self.closed = True 617 | self.write = None 618 | self.read = None 619 | self.environ = None 620 | 621 | 622 | class Response(object): 623 | # Header blacklist for specific response codes 624 | # (rfc2616 section 10.2.3 and 10.3.5) 625 | bad_headers = { 626 | 204: 627 | frozenset(("Content-Type", "Content-Length")), 628 | 304: 629 | frozenset(( 630 | "Allow", 631 | "Content-Encoding", 632 | "Content-Language", 633 | "Content-Length", 634 | "Content-Range", 635 | "Content-Type", 636 | "Content-Md5", 637 | "Last-Modified", 638 | )), 639 | } 640 | headers_sent = False 641 | 642 | def __init__(self, environ, start_response, app): 643 | self.environ = environ 644 | self._start_response = start_response 645 | self.app = app 646 | 647 | def process_response(self, allow_write=True): 648 | try: 649 | results = self.app(self.environ, self.start_response) 650 | 651 | except Exception as e: 652 | self.start_response() 653 | log = log_traceback(e) 654 | err = "
%s :%s
%s
%s
" % ( 821 | environ.get("PATH_INFO") + "?" + environ.get("QUERY_STRING", "\b")) 822 | 823 | def wsgi(self, environ, start_response): 824 | if len(self.routes): 825 | for route in self.routes: 826 | if route == environ.get("PATH_INFO"): 827 | r = Response(environ, start_response, self.routes[route]) 828 | return r.process_response() 829 | 830 | if route.endswith("*") and environ.get( 831 | "PATH_INFO", "").startswith(route[:-1]): 832 | r = Response(environ, start_response, self.routes[route]) 833 | return r.process_response() 834 | 835 | r = Response(environ, start_response, self.not_found) 836 | return r.process_response() 837 | 838 | wsock = environ.get("wsgi.websocket") 839 | if not wsock: 840 | start_response() 841 | return "