├── .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 | [![Downloads](https://static.pepy.tech/personalized-badge/wsocket?period=total&units=none&left_color=black&right_color=blue&left_text=downloads)](https://pepy.tech/project/wsocket) 2 | [![GitHub issues](https://img.shields.io/github/issues/Ksengine/WSocket?style=flat-square)](https://github.com/Ksengine/WSocket/issues) 3 | [![GitHub forks](https://img.shields.io/github/forks/Ksengine/WSocket?style=flat-square)](https://github.com/Ksengine/WSocket/network) 4 | [![GitHub stars](https://img.shields.io/github/stars/Ksengine/WSocket?style=flat-square)](https://github.com/Ksengine/WSocket/stargazers) 5 | [![GitHub license](https://img.shields.io/github/license/Ksengine/WSocket?style=flat-square)](https://github.com/Ksengine/WSocket/blob/master/LICENSE) 6 | [![Twitter](https://img.shields.io/twitter/url?style=social&url=https%3A%2F%2Fgithub.com%2FKsengine%2FWSocket)](https://twitter.com/intent/tweet?text=Wow:&url=https%3A%2F%2Fgithub.com%2FKsengine%2FWSocket) 7 | 8 |
9 |

10 | 11 | Logo 12 | 13 | 14 |

WSocket

15 | 16 |

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 |

29 |

30 | 31 | 32 | ## About The Project 33 | 34 | This is a simple library to add websocket support to WSGI. 35 | 36 | - Server - Patched wsgiref server 37 | - Middleware - Add websocket support to Flask, Django, Pyramid, Bottle, ... 38 | - Handler - Websocket handler to wsgiref 39 | - Framework - Basic websocket + WSGI web application framework 40 | - App - Plug and play demo app. 41 | 42 | ## Getting Started 43 | 44 | This is an example of how you may give instructions on setting up your websocket connunication locally. 45 | 46 | ### Installation 47 | You can 48 | - Installing from PyPI 49 | - Download:down_arrow: and Include 50 | 51 | #### Installing from PyPI 52 | 53 | Install latest version or upgrade an already installed WSocket to the latest from PyPI. 54 | ```bash 55 | pip install --upgrade wsocket 56 | ``` 57 | 58 | #### Download and Include 59 | 60 | - Visit [wsocket.py](https://raw.githubusercontent.com/Ksengine/WSocket/master/wsocket.py/). 61 | - Save file (`ctrl+s` in browser). 62 | - Include in your package derectory. 63 | ``` 64 | my-app/ 65 | ├─ hello_world.py 66 | ├─ wsocket.py 67 | ``` 68 | 69 | ## Usage 70 | 71 | - Include following source on your test file(eg:-`hello_world.py`). 72 | ```python 73 | from wsocket import WSocketApp, WebSocketError, logger, run 74 | from time import sleep 75 | 76 | logger.setLevel(10) # for debugging 77 | 78 | def on_close(self, message, client): 79 | print(repr(client) + " : " + message) 80 | 81 | def on_connect(client): 82 | print(repr(client) + " connected") 83 | 84 | def on_message(message, client): 85 | print(repr(clent) + " : " + repr(message)) 86 | try: 87 | client.send("you said: " + message) 88 | sleep(2) 89 | client.send("you said: " + message) 90 | 91 | except WebSocketError: 92 | pass 93 | 94 | app = WSocketApp() 95 | app.onconnect += on_connect 96 | app.onmessage += on_message 97 | app.onclose += on_close 98 | 99 | run(app) 100 | ``` 101 | - Visit [client.html](https://github.com/Ksengine/WSocket/raw/master/client.html). 102 | - Save file (`ctrl+s` in browser). 103 | - Open it in browser(websocket supported). 104 | - Experience the two way websocket communication. :smile::smile::smile: 105 | 106 | 107 | ## Contributing 108 | 109 | Contributions are what make the open source community such an amazing place to be learn, inspire, and create. Any contributions you make are **greatly appreciated**. 110 | 111 | 1. Fork the Project 112 | 2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`) 113 | 3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`) 114 | 4. Push to the Branch (`git push origin feature/AmazingFeature`) 115 | 5. Open a Pull Request 116 | 117 | 118 | ## License 119 | Code and documentation are available according to the MIT License (see [LICENSE](https://github.com/Ksengine/WSocket/blob/master/LICENSE)). 120 | 121 | 122 | ## Contact 123 | **Report Bugs** - https://github.com/Ksengine/WSocket/issues/new/ 124 | -------------------------------------------------------------------------------- /assests/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ksenginew/WSocket/91d1c16cf3a1a7741d4d7f4fc88f7021949cabcd/assests/icon.png -------------------------------------------------------------------------------- /client.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Simple client 4 | 5 | 56 | 57 | 58 |
59 | 60 | 61 | 62 |
63 |
64 | 65 | 66 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # WSocket 2 | **Simple WSGI HTTP + Websocket Server, Framework, Middleware And App.** 3 | 4 | [![Downloads](https://pepy.tech/badge/wsocket)](https://pepy.tech/project/wsocket) 5 | 6 | **Note:** 7 | I am a 16 years old student.I have no enough knowledge. So can anyone [help me](https://github.com/Ksengine/WSocket/issues/2) to develop this library? 8 | ## Server 9 | Server([WSGI](http://www.wsgi.org/)) creates and listens at the HTTPsocket, 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. 10 | **can used with any WSGI compatible web framework** 11 | 12 | ## Middleware 13 | convert any WSGI compatible web framework to Websocket+HTTP framework 14 | using middleware. 15 | **works with many WSGI compatible servers** 16 | **can used with any WSGI compatible web framework** 17 | > Flask, Django, Pyramid, Bottle, ... supported 18 | 19 | ## Handler 20 | `wsgiref.simple_server.WSGIRequestHandler` like class named `FixedHandler` that always wrap WSGI app using Middleware. 21 | changes from `WSGIRequestHandler` : 22 | - Prevents reverse DNS lookups 23 | - errorless logger 24 | - use `ServerHandler` to make it WSGI 25 | 26 | > You can convert wsgiref to a websocket+HTTP server using this handler 27 | 28 | ### ServerHandler 29 | `wsgiref.simple_server.ServerHandler`(inherited from `wsgiref.handlers.ServerHandler` like handler named `FixedServerHandler` . 30 | changes from `ServerHandler` : 31 | - set HTTP version to `1.1` because versions below `1.1` are not supported some clients like Firefox. 32 | - removed hop-by-hop headers checker because it raise errors on `Upgrade` and `Connection` headers 33 | - check that all headers are strings 34 | 35 | ## Framework 36 | basic WSGI web application framework that uses Middleware. 37 | - simple routes handler 38 | - auto description to status code 39 | - headers checker 40 | - send data as soon as possible 41 | - send strings, bytes, lists(even bytes and strings mixed) or files directly 42 | - error catcher and error logger 43 | 44 | **works with many WSGI compatible servers** 45 | 46 | ## App 47 | Event based app for websocket communication. this is app that uses Framework 48 | if not events handled by developer. this app works like demo(echo) app. 49 | 50 | ## Features 51 | all Middleware, Handler, Framework and App has following features. 52 | - websocket sub protocol supported 53 | - websocket message compression supported (works if client asks) 54 | - receive and send pong and ping messages(with automatic pong sender) 55 | - receive and send binary or text messages 56 | - works for messages with or without mask 57 | - closing messages supported 58 | - auto and manual close 59 | 60 | **View Documentaion** - https://wsocket.gitbook.io/ 61 | **Report Bugs** - https://github.com/Ksengine/WSocket/issues/new/ 62 | 63 | ### License 64 | Code and documentation are available according to the MIT License (see [LICENSE](https://github.com/Ksengine/WSocket/blob/master/LICENSE)). 65 | -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /docs/app.md: -------------------------------------------------------------------------------- 1 | ## App 2 | 3 | Event based app for websocket communication. This is app that uses [Framework](framework.md). If not events handled by developer. this app works like demo(echo) app. 4 | This is a [WSGI](http://www.wsgi.org/) web app. So you can use any [WSGI](http://www.wsgi.org/) Server to host this app 5 | > you should use HTTP version `1.1` Server with your [WSGI](http://www.wsgi.org/) framework for some clients like Firefox browser 6 | 7 | ```python 8 | from wsocket import WSocketApp, WebSocketError, logger, run 9 | from time import sleep 10 | 11 | logger.setLevel(10) # for debugging 12 | 13 | def on_close(self, message, client): 14 | print(repr(client) + " : " + message) 15 | 16 | def on_connect(client): 17 | print(repr(client) + " connected") 18 | 19 | def on_message(message, client): 20 | print(repr(clent) + " : " + repr(message)) 21 | try: 22 | client.send("you said: " + message) 23 | sleep(2) 24 | client.send("you said: " + message) 25 | 26 | except WebSocketError: 27 | pass 28 | 29 | app = WSocketApp() 30 | app.onconnect += on_connect 31 | app.onmessage += on_message 32 | app.onclose += on_close 33 | 34 | run(app) 35 | ``` 36 | > for more info on `client` see - https://github.com/Ksengine/WSocket/tree/master/docs/websocket.md 37 | 38 | 39 | ## `class WSocketApp(app=None, protocol=None)` 40 | `app` should be a valid [WSGI](http://www.wsgi.org/) web application. 41 | `protocol` is websocket sub protocol to accept (ex: [WAMP](https://wamp-proto.org/)) 42 | 43 | ### Class variables 44 | 45 | `GUID` - unique ID to generate websocket accept key 46 | 47 | `SUPPORTED_VERSIONS` - 13, 8 or 7 48 | 49 | `websocket_class` - `"wsgi.websocket"` in WSGI Environ 50 | 51 | ### Events 52 | 53 | `onconnect` - fires when client sent a message 54 | `onmessage` - fires when client sent a message 55 | `onmessage` - fires when client sent a message 56 | 57 | you can attach event handler method to event using 58 | - `+=` operator 59 | ```python 60 | app = WSocketApp() 61 | app.onmessage += on_message 62 | app.onmessage += on_message2 63 | # both `on_message` and `on_message2` can handle event 64 | run(app) 65 | ``` 66 | 67 | - `+` operator 68 | ```python 69 | app = WSocketApp() 70 | app.onmessage + on_message 71 | app.onmessage + on_message2 72 | # both `on_message` and `on_message2` can handle event 73 | run(app) 74 | ``` 75 | 76 | - `=` operator 77 | ```python 78 | app = WSocketApp() 79 | app.onmessage = on_message 80 | # only `on_message` can handle event 81 | app.onmessage = on_message2 82 | # now, only `on_message2` can handle event 83 | run(app) 84 | ``` 85 | > You can't add new handlers to Event after `=` operator used. It replaces Event. But you can replace it again using another handler. 86 | -------------------------------------------------------------------------------- /docs/framework.md: -------------------------------------------------------------------------------- 1 | # Framework 2 | 3 | basic WSGI web application framework that uses Middleware. 4 | 5 | - simple routes handler 6 | - auto description to status code 7 | - headers checker 8 | - send data as soon as possible 9 | - send strings, bytes, lists(even bytes and strings mixed) or files directly 10 | - error catcher and error logger 11 | 12 | **works with many WSGI compatible servers** 13 | > you should use HTTP version **`1.1`** Server with your [WSGI](http://www.wsgi.org/) framework for some clients like Firefox browser 14 | 15 | ```python 16 | from wsocket import WSocketApp, WebSocketError, logger, run 17 | from time import sleep 18 | 19 | logger.setLevel(10) # for debugging 20 | 21 | app = WSocketApp() 22 | # app = WSocketApp(protocol="WAMP") 23 | 24 | @app.route("/") 25 | def handle_websocket(environ, start_response): 26 | 27 | wsock = environ.get("wsgi.websocket") 28 | 29 | if not wsock: 30 | start_response() 31 | return "Hello World!" 32 | 33 | while True: 34 | try: 35 | message = wsock.receive() 36 | if message != None: 37 | print("participator : " + message) 38 | wsock.send("you : "+message) 39 | sleep(2) 40 | wsock.send("you : "+message) 41 | except WebSocketError: 42 | break 43 | run(app) 44 | ``` 45 | > more info on `wsgi.websocket` variable in [WSGI](http://www.wsgi.org/) environment dictionary - https://github.com/Ksengine/WSocket/tree/master/docs/websocket.md 46 | 47 | ## `class WSocketApp(app=None, protocol=None)` 48 | 49 | 50 | ### Class variables 51 | 52 | `GUID` - unique ID to generate websocket accept key 53 | 54 | `SUPPORTED_VERSIONS` - 13, 8 or 7 55 | 56 | `routes` - dictionary for route handlers 57 | 58 | `websocket_class` - `"wsgi.websocket"` in WSGI Environ 59 | 60 | 61 | ### Methods 62 | 63 | `not_found(self, environ, start_response)` - handle `404 NOT FOUND` error 64 | 65 | `route(self, r)` - register routes 66 | 67 | ## Routes 68 | WSocket uses simple routes engine. 69 | How it works? 70 | - URL - `http://localhost:8080/hello/world?user=Ksengine&pass=1234` 71 | - divided into parts(by Server) 72 | | origin | host | port | path | query | 73 | |-- |-- |-- |-- |-- | 74 | | `http` | `localhost` | `8080` | `/hello/world` | `user=Ksengine&pass=1234` | 75 | **only path is used to find routes** 76 | - walk through routes dictionary 77 | - if `"/hello/world"` path found, trigger route handler 78 | ```python 79 | @app.route("/hello/world") 80 | ``` 81 | - else, if some route string ends with "*" and path starts with that string trigger route handler 82 | ```python 83 | @app.route("/hello/world") 84 | ``` 85 | 86 | ## Status and Headers 87 | call `start_response` to send status code and headers. 88 | if you returns without calling it. It will send `200 OK` status and some basic headers to client. 89 | if `start_response` is called without argsuments, it will send `200 OK` status and some basic headers to client. 90 | ```python 91 | start_response() 92 | ``` 93 | `start_response` has two arguments 94 | - status - status code as int(eg:-200) or str(eg:- "200 OK" or "200"). If status description(eg:-"OK") not supplied, it will find it. 95 | - headers - HTTP headers. can passed as, 96 | - list of tuples 97 | ```python 98 | [("header1 name", "header1 value"), 99 | ("header2 name", "header2 value") ] 100 | ``` 101 | - dictionary 102 | ```python 103 | {"header1 name": "header1 value", 104 | "header2 name": "header2 value"} 105 | ``` 106 | 107 | status code examples:- 108 | ```python 109 | start_response() # start_response("200 OK",[]) 110 | ``` 111 | ```python 112 | start_response(200) # start_response("200 OK",[]) 113 | ``` 114 | ```python 115 | start_response("200") # start_response("200 OK",[]) 116 | ``` 117 | ```python 118 | start_response("200 OK") # start_response("200 OK",[]) 119 | ``` 120 | send headers examples:- 121 | - list of tuples 122 | ```python 123 | start_response("200 OK",[ 124 | ("header1 name", "header1 value"), 125 | ("header2 name", "header2 value") 126 | ]) 127 | ``` 128 | 129 | - dictionary 130 | ```python 131 | start_response("200 OK",{ 132 | "header1 name": "header1 value", 133 | "header2 name": "header2 value" 134 | }) 135 | ``` 136 | 137 | ## Send data 138 | You can send following data types 139 | - `str` - string, text 140 | ```python 141 | def sender(environ, start_response) 142 | start_response() 143 | # ...some code here 144 | return "Hello World!" 145 | ``` 146 | - `bytes` - binary 147 | ```python 148 | def sender(environ, start_response) 149 | start_response() 150 | # ...some code here 151 | return b"Hello World!" # b"" is binary string 152 | ``` 153 | ```python 154 | def sender(environ, start_response) 155 | start_response() 156 | # ...some code here 157 | return "Hello World!".encode() # str.encode converts str to bytes 158 | ``` 159 | ```python 160 | def sender(environ, start_response) 161 | start_response() 162 | # ...some code here 163 | return b"Hello World!" # open file as text file 164 | ``` 165 | - `files` - opened files 166 | ```python 167 | def sender(environ, start_response) 168 | start_response() 169 | # ...some code here 170 | return open("hello.txt") 171 | ``` 172 | ```python 173 | def sender(environ, start_response) 174 | start_response() 175 | # ...some code here 176 | return open("hello.txt", "b") # "b" - open as binary file 177 | ``` 178 | - `file-like object` - streams(text, binary) like `StringIO`or `BytesIO` 179 | ```python 180 | try: 181 | from io import StringIO 182 | except ImportError: 183 | from StringIO import StringIO 184 | 185 | def sender(environ, start_response) 186 | start_response() 187 | # ...some code here 188 | file_like = StringIO() 189 | return file_like 190 | ``` 191 | ```python 192 | try: 193 | from io import BytesIO 194 | except ImportError: 195 | from StringIO import BytesIO 196 | 197 | def sender(environ, start_response) 198 | start_response() 199 | # ...some code here 200 | file_like = BytesIO() 201 | return file_like 202 | ``` 203 | - `iterables` - `list`, `tuple`, `set` `dict` `generators` etc. 204 | ```python 205 | def sender(environ, start_response) 206 | start_response() 207 | # ...some code here 208 | return ["Hello ", b"World", "!".encode(), 2020] # list 209 | ``` 210 | ```python 211 | def sender(environ, start_response) 212 | start_response() 213 | # ...some code here 214 | return ("Hello ", b"World", "!".encode(), 2020) # tuple 215 | ``` 216 | ```python 217 | def sender(environ, start_response) 218 | start_response() 219 | # ...some code here 220 | return {"Hello ", b"World", "!".encode(), 2020} # set 221 | ``` 222 | ```python 223 | def sender(environ, start_response) 224 | start_response() 225 | # ...some code here 226 | return {"Hello ":None, "World!":None} # dict 227 | ``` 228 | ```python 229 | import time 230 | def sender(environ, start_response) 231 | start_response() 232 | # ...some code here 233 | yield "Hello " 234 | time.sleep(2) 235 | yield b"World" 236 | time.sleep(5) 237 | yield "!".encode() 238 | yield 2020 # generators 239 | ``` 240 | **generators can send data one by one with time intervals. so it's like async Server** 241 | - other - 242 | ```python 243 | def sender(environ, start_response) 244 | start_response() 245 | # ...some code here 246 | return 3.3 # float 247 | ``` 248 | 249 | ## Errors 250 | example :- 251 | >

Internal Server Error(500)

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 = "

Internal Server Error(500)

%s :%s

%s

" % ( 655 | type(e).__name__, str(e), log, 656 | urlencode({ 657 | 'title': type(e).__name__, 658 | 'body': '```python\n' + log + '\n```' 659 | })) 660 | logger.debug(log) 661 | return [err.encode("utf-8")] 662 | 663 | if not allow_write: 664 | return [] 665 | 666 | if isinstance(results, string_types): 667 | return [results.encode("utf-8")] 668 | 669 | elif isinstance(results, bytes): 670 | return [results] 671 | 672 | elif hasattr(results, "__iter__"): 673 | while not self.headers_sent: 674 | pass 675 | 676 | for result in results: 677 | if isinstance(result, string_types): 678 | self.write(result.encode("utf-8")) 679 | 680 | elif isinstance(result, bytes): 681 | self.write(result) 682 | 683 | else: 684 | self.write(str(result).encode("utf-8")) 685 | 686 | return [] 687 | 688 | else: 689 | return [str(result).encode("utf-8")] 690 | 691 | def start_response(self, status="200 OK", headers=[]): 692 | if self.headers_sent: 693 | return 694 | 695 | status = self.process_status(status) 696 | 697 | if isinstance(headers, dict): 698 | headers = list(headers.items()) 699 | 700 | if self.code in self.bad_headers: 701 | bad_headers = self.bad_headers[self.code] 702 | headers = [h for h in headers if h[0] not in bad_headers] 703 | 704 | self.write = self._start_response(status, headers) 705 | self.headers_sent = True 706 | return self.write 707 | 708 | def process_status(self, status): 709 | if isinstance(status, int): 710 | code, status = status, _HTTP_STATUS_LINES.get(status) 711 | 712 | elif " " in status: 713 | if "\n" in status or "\r" in status or "\0" in status: 714 | raise ValueError("Status line must not include control chars.") 715 | 716 | status = status.strip() 717 | code = int(status.split()[0]) 718 | 719 | else: 720 | raise ValueError("String status line without a reason phrase.") 721 | 722 | if not 100 <= code <= 999: 723 | raise ValueError("Status code out of range.") 724 | 725 | self.code = code 726 | return str(status or ("%d Unknown" % code)) 727 | 728 | 729 | class Event: 730 | def __init__(self, default=None): 731 | self._items = [] 732 | self.default = default 733 | 734 | def __call__(self, *args, **kwargs): 735 | def execute(): 736 | for func in self._items: 737 | try: 738 | func(*args, **kwargs) 739 | 740 | except Exception as e: 741 | logger.exception(e) 742 | 743 | if not len(self._items): 744 | if self.default: 745 | t = Thread(target=self.default, args=args, kwargs=kwargs) 746 | t.start() 747 | return 748 | 749 | else: 750 | return 751 | 752 | t = Thread(target=execute) 753 | t.start() 754 | 755 | def clear(self): 756 | self._items = [] 757 | 758 | def __add__(self, item): 759 | self._items.append(item) 760 | return self 761 | 762 | def __sub__(self, item): 763 | self._items.remove(item) 764 | return self 765 | 766 | def __iadd__(self, item): 767 | self._items.append(item) 768 | return self 769 | 770 | def __isub__(self, item): 771 | self._items.remove(item) 772 | return self 773 | 774 | 775 | class WSocketApp: 776 | SUPPORTED_VERSIONS = ("13", "8", "7") 777 | GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" 778 | websocket_class = WebSocket 779 | send = None 780 | routes = {} 781 | 782 | def __init__(self, app=None, protocols=[]): 783 | self.protocols = protocols if isinstance(protocols, 784 | (list, tuple, 785 | set)) else [protocols] 786 | self.app = app or self.wsgi 787 | self.onclose = Event(self.on_close) 788 | self.onmessage = Event(self.on_message) 789 | self.onconnect = Event(self.on_connect) 790 | 791 | def on_close(self, message): 792 | print(message) 793 | 794 | def on_connect(self, client): 795 | print(client) 796 | client.send('you connected') 797 | 798 | def fake(*args, **kwargs): 799 | pass 800 | 801 | def on_message(self, message, client): 802 | print(repr(message)) 803 | try: 804 | client.send("you said: " + message) 805 | sleep(2) 806 | client.send("you said: " + message) 807 | 808 | except WebSocketError: 809 | pass 810 | 811 | def route(self, r): 812 | def decorator(callback): 813 | self.routes[r] = callback 814 | return callback 815 | 816 | return decorator 817 | 818 | def not_found(self, environ, start_response): 819 | start_response(404) 820 | return "

Page Not Found(404)

%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 "

Hello World!

" 842 | 843 | self.onconnect(wsock) 844 | while True: 845 | try: 846 | message = wsock.receive() 847 | if message != None: 848 | self.onmessage(message, wsock) 849 | 850 | except WebSocketError as e: 851 | break 852 | 853 | return [] 854 | 855 | def __call__(self, environ, start_response): 856 | if "wsgi.websocket" in environ or environ.get("REQUEST_METHOD", 857 | "") != "GET": 858 | r = Response(environ, start_response, self.app) 859 | return r.process_response() 860 | # Upgrade 861 | # Connection 862 | if "websocket" not in map( 863 | str.strip, 864 | environ.get("HTTP_UPGRADE", 865 | "").lower().split(",")) or "upgrade" not in map( 866 | str.strip, 867 | environ.get("HTTP_CONNECTION", 868 | "").lower().split(",")): 869 | r = Response(environ, start_response, self.app) 870 | return r.process_response() 871 | # Sec-WebSocket-Version PLUS determine mode: Hybi or Hixie 872 | if "HTTP_SEC_WEBSOCKET_VERSION" not in environ: 873 | logger.warning( 874 | "WebSocket connection denied - Hixie76 protocol not supported." 875 | ) 876 | start_response( 877 | "426 Upgrade Required", 878 | [("Sec-WebSocket-Version", ", ".join(self.SUPPORTED_VERSIONS)) 879 | ], 880 | ) 881 | return [b"No Websocket protocol version defined"] 882 | 883 | version = environ.get("HTTP_SEC_WEBSOCKET_VERSION") 884 | 885 | # respond with list of supported versions (descending order) 886 | if version not in self.SUPPORTED_VERSIONS: 887 | msg = "Unsupported WebSocket Version: %s" % version 888 | logger.warning(msg) 889 | start_response( 890 | "400 Bad Request", 891 | [("Sec-WebSocket-Version", ", ".join(self.SUPPORTED_VERSIONS)) 892 | ], 893 | ) 894 | return [msg.encode()] 895 | 896 | key = environ.get("HTTP_SEC_WEBSOCKET_KEY", "").strip() 897 | if not len(key): 898 | msg = "Sec-WebSocket-Key header is missing/empty" 899 | logger.warning(msg) 900 | start_response("400 Bad Request", []) 901 | return [msg.encode()] 902 | 903 | try: 904 | key_len = len(b64decode(key)) 905 | 906 | except TypeError: 907 | msg = "Invalid key: %s" % key 908 | logger.warning(msg) 909 | start_response("400 Bad Request", []) 910 | return [msg.encode()] 911 | 912 | if key_len != 16: 913 | msg = "Invalid key: %s" % key 914 | logger.warning(msg) 915 | start_response("400 Bad Request", []) 916 | return [msg.encode] 917 | 918 | # Sec-WebSocket-Protocol 919 | requested_protocols = list( 920 | map(str.strip, 921 | environ.get("HTTP_SEC_WEBSOCKET_PROTOCOL", "").split(","))) 922 | protocols = None 923 | protocols = set(requested_protocols) and set(self.protocols) 924 | logger.debug("Protocols allowed: {0}".format(", ".join(protocols))) 925 | 926 | extensions = list( 927 | map(lambda ext: ext.split(";")[0].strip(), 928 | environ.get("HTTP_SEC_WEBSOCKET_EXTENSIONS", "").split(","))) 929 | 930 | do_compress = "permessage-deflate" in extensions 931 | 932 | if PY3: 933 | accept = b64encode( 934 | sha1((key + 935 | self.GUID).encode("latin-1")).digest()).decode("latin-1") 936 | 937 | else: 938 | accept = b64encode(sha1(key + self.GUID).digest()) 939 | 940 | headers = [ 941 | ("Upgrade", "websocket"), 942 | ("Connection", "Upgrade"), 943 | ("Sec-WebSocket-Accept", accept), 944 | ] 945 | 946 | if do_compress: 947 | headers.append(("Sec-WebSocket-Extensions", "permessage-deflate")) 948 | 949 | if protocols: 950 | headers.append(("Sec-WebSocket-Protocol", ", ".join(protocols))) 951 | 952 | logger.debug("WebSocket request accepted, switching protocols") 953 | write = start_response("101 Switching Protocols", headers) 954 | read = environ["wsgi.input"].read 955 | write(b"") 956 | websocket = self.websocket_class(environ, read, write, self, 957 | do_compress) 958 | environ.update({ 959 | "wsgi.websocket_version": version, 960 | "wsgi.websocket": websocket 961 | }) 962 | r = Response(environ, start_response, self.app) 963 | r.start_response = self.fake 964 | return r.process_response(False) 965 | 966 | 967 | # for version compat 968 | class WebSocketHandler(FixedHandler): 969 | def get_app(self): 970 | return WSocketApp(self.server.get_app()) 971 | 972 | 973 | WSocketHandler = WebSocketHandler 974 | 975 | 976 | class WSocketServer(ThreadingWSGIServer): 977 | def set_app(self, app, *args, **kwargs): 978 | ThreadingWSGIServer.set_app(self, WSocketApp(app), *args, **kwargs) 979 | 980 | 981 | def run(app=WSocketApp(), host="127.0.0.1", port=8080, **options): 982 | handler_cls = options.get("handler_class", FixedHandler) 983 | server_cls = options.get("server_class", ThreadingWSGIServer) 984 | 985 | if ":" in host: # Fix wsgiref for IPv6 addresses. 986 | if getattr(server_cls, "address_family") == socket.AF_INET: 987 | 988 | class server_cls(server_cls): 989 | address_family = socket.AF_INET6 990 | 991 | srv = make_server(host, port, app, server_cls, handler_cls) 992 | port = srv.server_port # update port actual port (0 means random) 993 | print("Server started at http://%s:%i." % (host, port)) 994 | try: 995 | srv.serve_forever() 996 | 997 | except KeyboardInterrupt: 998 | print("\nServer stopped.") 999 | srv.server_close() # Prevent ResourceWarning: unclosed socket 1000 | 1001 | 1002 | if __name__ == "__main__": 1003 | run() 1004 | --------------------------------------------------------------------------------