Navigation Menu

Skip to content

Instantly share code, notes, and snippets.

@artizirk
Last active September 1, 2022 17:12
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 5 You must be signed in to fork a gist
  • Save artizirk/04eb23d957d7916c01ca632bb27d5436 to your computer and use it in GitHub Desktop.
Save artizirk/04eb23d957d7916c01ca632bb27d5436 to your computer and use it in GitHub Desktop.
Python asyncio websockets http static file server, aka http and websocket server on the same port: python-websockets/websockets#116
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>WebSocket demo</title>
</head>
<body>
<script>
var ws = new WebSocket("ws://127.0.0.1:8765/"),
messages = document.createElement('ul');
ws.onmessage = function (event) {
var messages = document.getElementsByTagName('ul')[0],
message = document.createElement('li'),
content = document.createTextNode(event.data);
message.appendChild(content);
messages.appendChild(message);
};
document.body.appendChild(messages);
</script>
</body>
</html>
#!/usr/bin/env python3
"""
This code allows you to serve static files from the same port as the websocket connection
This is only suitable for small files and as a development server!
open(full_path, 'rb').read() call that is used to send files will block the whole asyncio loop!
"""
import os
import asyncio
import datetime
import random
import functools
import websockets
from http import HTTPStatus
MIME_TYPES = {
"html": "text/html",
"js": "text/javascript",
"css": "text/css"
}
async def process_request(sever_root, path, request_headers):
"""Serves a file when doing a GET request with a valid path."""
if "Upgrade" in request_headers:
return # Probably a WebSocket connection
if path == '/':
path = '/index.html'
response_headers = [
('Server', 'asyncio websocket server'),
('Connection', 'close'),
]
# Derive full system path
full_path = os.path.realpath(os.path.join(sever_root, path[1:]))
# Validate the path
if os.path.commonpath((sever_root, full_path)) != sever_root or \
not os.path.exists(full_path) or not os.path.isfile(full_path):
print("HTTP GET {} 404 NOT FOUND".format(path))
return HTTPStatus.NOT_FOUND, [], b'404 NOT FOUND'
# Guess file content type
extension = full_path.split(".")[-1]
mime_type = MIME_TYPES.get(extension, "application/octet-stream")
response_headers.append(('Content-Type', mime_type))
# Read the whole file into memory and send it out
body = open(full_path, 'rb').read()
response_headers.append(('Content-Length', str(len(body))))
print("HTTP GET {} 200 OK".format(path))
return HTTPStatus.OK, response_headers, body
async def time(websocket, path):
print("New WebSocket connection from", websocket.remote_address)
while websocket.open:
now = datetime.datetime.utcnow().isoformat() + 'Z'
await websocket.send(now)
await asyncio.sleep(random.random() * 2)
# This print will not run when abrnomal websocket close happens
# for example when tcp connection dies and no websocket close frame is sent
print("WebSocket connection closed for", websocket.remote_address)
if __name__ == "__main__":
# set first argument for the handler to current working directory
handler = functools.partial(process_request, os.getcwd())
start_server = websockets.serve(time, '127.0.0.1', 8765,
process_request=handler)
print("Running server at http://127.0.0.1:8765/")
asyncio.get_event_loop().run_until_complete(start_server)
asyncio.get_event_loop().run_forever()
@ahmetuludag
Copy link

ahmetuludag commented Feb 15, 2020

Thank you, it's useful for development purposes. One thing I needed to add after line #46
response_headers.append(('Content-Type', 'text/html'))
Otherwise Firefox was handling that page as text/plain and shows the html file source as-is.

@artizirk
Copy link
Author

Thank you, it's useful for development purposes. One thing I needed to add after line #46
response_headers.append(('Content-Type', 'text/html'))
Otherwise Firefox was handling that page as text/plain and shows the html file source as-is.

Good call, I have updated the gist to reflect that and also did some other cleanups to the code.

@dg9vh
Copy link

dg9vh commented Nov 27, 2020

Hi, thank you for the code but:
Running your example above I get following:

Error in opening handshake
Traceback (most recent call last):
  File "/usr/lib/python3/dist-packages/websockets/server.py", line 117, in handler
    extra_headers=self.extra_headers,
  File "/usr/lib/python3/dist-packages/websockets/server.py", line 483, in handshake
    raise AbortHandshake(*early_response)
TypeError: type object argument after * must be an iterable, not coroutine
/usr/lib/python3/dist-packages/websockets/server.py:166: RuntimeWarning: coroutine 'process_request' was never awaited
  return
RuntimeWarning: Enable tracemalloc to get the object allocation traceback
Error in opening handshake
Traceback (most recent call last):
  File "/usr/lib/python3/dist-packages/websockets/server.py", line 117, in handler
    extra_headers=self.extra_headers,
  File "/usr/lib/python3/dist-packages/websockets/server.py", line 483, in handshake
    raise AbortHandshake(*early_response)
TypeError: type object argument after * must be an iterable, not coroutine

What's going wrong here? An idea?

@artizirk
Copy link
Author

I think libraries have changed. When I some free time I will try to fix it.

@dg9vh
Copy link

dg9vh commented Nov 28, 2020

This will be very nice... actually I have to start a separate webserver for handling the html-file :-)

@artizirk
Copy link
Author

@dg9vh It looks like that error only happens when using websockets version 7.0. And looks like Debian 10 is currently shipping that version in the repos. If possible, use venv and install latest 8.x release of websockets from pypi.

@dg9vh
Copy link

dg9vh commented Dec 14, 2020

Hi,
tnx for your info... will try it if I get it managed :-) tnx!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment