miguelgrinberg / flask-sock

Modern WebSocket support for Flask.
MIT License
271 stars 24 forks source link

connection error with werkzeug #71

Open gbrault opened 10 months ago

gbrault commented 10 months ago

I am using flask-sock with flask-appbuilder. From python side I have one main sock route: /clock

It provides access to the ws to a user if it has the 'ResetMyPasswordView' access within flask-appbuilder (a user which has the right to change is password)

Here is the code

@sock.route('/clock')
def clock(ws):
    """Accepts a websocket connection
        - checks if the user has access to the websocket
        - adds the user and the socket to the list of listeners
        - echo received messages back to the client (could be extended to take some actions on the server side)
        - sends a welcome message if the href is '/'
        - and sends the current time every second
        - when closed break the echo loop
        - the user will be removed by the clock from the list of listeners
        - each time a user changes the page the websocket is closed and a new one is opened (so checking '/' is enough to know if the user is on the welcome page)"""
    from urllib.parse import urlparse
    try:
        show_welcome = False
        if 'href' in request.values:
            url = urlparse(request.values['href'])
            if url.path == '/':
                show_welcome = True
        if isinstance(g.user, AnonymousUserMixin):
            ws.send(json.dumps({"type": "log", "text": "Anonymous User has no access"}))
            ws.close()
            return
        app.logger.info(g.user.username)
        user = app.appbuilder.sm.find_user(username=g.user.username)
        if user is None:
            ws.send(json.dumps({"type": "log", "text": "User not found"}))
            ws.close()
            return
        has_access = appbuilder.sm.has_access('can_this_form_post', 'ResetMyPasswordView') # if the guy can reset his password, he can access the websocket
        if has_access:
            #send a welcome message to the user if href path is '/' (show_welcome)
            if show_welcome:
                    ws.send(json.dumps({"type": "alert", "text": f"Welcome to {app.config['APP_NAME']}", "alerttype" : "alert-success"}))
            # create a unique key for the user,socket pair (the key is the current date-time)
            key = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
            # add the user and the socket to the list of listeners with the above key
            listners[key] = {"user": user, "socket": ws}
        else:
            # only user with a profile benefit from the websocket service
            ws.send(json.dumps({"type": "log", "text": "User not allowed"}))
            ws.close()
            return
        while True:
            # this means the webserver thread is active till the websocket is closed (the user changes or close the page)
            data = ws.receive()
            if data == 'close':
                break
            ws.send(json.dumps({"type": "log", "text": data}))
    except Exception as e:
        app.logger.error(e)
gbrault commented 10 months ago

I use the following python functions to interact over the ws:

def send_time():
    """Send the current time to all connected clients"""
    while True:
        if app.sock_running == False:
            break
        time.sleep(1)
        keys = list(listners.keys())
        for key in keys:
            try:
                listners[key]["socket"].send(json.dumps({"type": "clock", "text": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")}))
            except:
                del listners[key]

def send_modal(username,title,message,modal_type="info"):
    """Send a modal to a user
        - username: the username of the user to send the modal to
        - title: the title of the modal
        - message: the message to send
        - modal_type: the type of modal (info, warning, error, success)"""
    keys = list(listners.keys())
    for key in keys:
        if username == "*" or listners[key]["user"].username == username:
            try:
                message = message.replace("\n","<br>")
                listners[key]["socket"].send(json.dumps({"type": "modal", "title": title, "text": message, "modaltype": modal_type}))
            except:
                del listners[key]

def send_alert(username,message,alert_type="alert-info"):
    """Send an alert to a user
        - username: the username of the user to send the alert to
        - message: the message to send
        - alert_type: the type of alert (alert-primary, alert-secondary, alert-success, alert-info, alert-warning, alert-danger, alert-light, alert-dark)"""
    keys = list(listners.keys())
    for key in keys:
        if username == "*" or listners[key]["user"].username == username:
            try:
                message = message.replace("\n","<br>")
                listners[key]["socket"].send(json.dumps({"type": "alert", "alerttype": alert_type, "text": message}))
            except:
                del listners[key]
gbrault commented 10 months ago

The clock is sent over the ws thanks to a thread

t = threading.Thread(target=send_time,args=())
t.start()
gbrault commented 10 months ago

The application works pretty well, I only have some small glitches

gbrault commented 10 months ago

From the client side here is the code

    $(document).ready(function() {
        // =====================
        let socket = null
        // this is the websocket connection
        if (location.protocol === 'https:') {
            socket = new WebSocket('wss://' + location.host + '/clock?href='+encodeURIComponent(location.href));
        } else {
            socket = new WebSocket('ws://' + location.host + '/clock?href='+encodeURIComponent(location.href));
        }
        // =====================
        if (socket !== null) {
            // this is the websocket event listener
            socket.addEventListener('message', ev => {  // this is the websocket event listener
                var msg = JSON.parse(ev.data);
                if (msg.type === 'clock') {
                    if (document.getElementById('clock') === null){
                        // if the clock span doesnt exist then create it 
                        $('body > header > div > div.container').prepend('<span id="clock" style="font-size: 7px; color: white;"></span>');
                    }
                    document.getElementById('clock').innerHTML = msg.text;
                    document.getElementById('clock').title = msg.text;
                } else if (msg.type === 'alert') {
                    showalert(msg.text,msg.alerttype)
                } else {
                    console.log('<<<', ev.data);
                }
            });

            socket.addEventListener('close', ev => {
                console.log('<<< closed');
            });
            // =====================
            // this is the function that will show the alert
            function showalert(message,alerttype) {
                // showalert("Invalid Login","alert-error")
                var idx = Math.floor(Math.random() * 1000);
                var alertdiv = "alertdiv_"+idx
                $("body > div.container").prepend('<div id="' + alertdiv + '"" class="alert ' +  alerttype + '"><a class="close" data-dismiss="alert">×</a><span>'+message+'</span></div>')

                    setTimeout(function() { // this will automatically close the alert and remove this if the users doesnt close it in 10 secs
                            $("#"+alertdiv).remove();
                    }, 10000);
            }
        }
    });
miguelgrinberg commented 10 months ago

connection error with werkzeug I only have some small glitches

This is not sufficient description of the problem for me to investigate. Please be specific.

gbrault commented 10 months ago

The non debug

When I launch my app: flask run -h 0.0.0.0 -p 5000

If I get connected to the server, as I am not authenticated with flask-appbuilder, the connection is nuturally closed.

2023-09-13 09:35:19,751:ERROR:werkzeug:172.18.0.12 - - [13/Sep/2023 09:35:19] code 400, message Bad request syntax ('\x88\x82?"\x8f¬<Ê')
2023-09-13 09:35:19,751:INFO:werkzeug:172.18.0.12 - - [13/Sep/2023 09:35:19] "None /clock?href=https://bbfta.ilexascenseurs.fr/ HTTP/0.9" 400 -
2
gbrault commented 10 months ago

The debug

When I launch my app: flask run -h 0.0.0.0 -p 5000 --debug

If I connect with the browser I get this in the Flask log:

2023-09-13 09:42:50,311:INFO:werkzeug:172.18.0.12 - - [13/Sep/2023 09:42:50] "GET /clock?href=https://bbfta.ilexascenseurs.fr/ HTTP/1.1" 500 -
Traceback (most recent call last):
  File "/home/appuser/venv/lib/python3.11/site-packages/flask/app.py", line 2213, in __call__
    return self.wsgi_app(environ, start_response)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/appuser/venv/lib/python3.11/site-packages/flask/app.py", line 2197, in wsgi_app
    return response(environ, start_response)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/appuser/venv/lib/python3.11/site-packages/flask_sock/__init__.py", line 86, in __call__
    raise ConnectionError()
ConnectionError
gbrault commented 10 months ago

If I authenticate, everything works great. But when I quit the browser, I get the same message again.

gbrault commented 10 months ago

@miguelgrinberg if you need some more details, don't hesitate to ask

miguelgrinberg commented 10 months ago

Yes, please provide the output of pip freeze

gbrault commented 10 months ago

This is the freeze on Linux, where I have the issue

aiohttp==3.8.5
aiosignal==1.3.1
anyio==3.7.1
apispec==5.2.2
argon2-cffi==21.3.0
argon2-cffi-bindings==21.2.0
arrow==1.2.3
asttokens==2.2.1
async-lru==2.0.3
async-timeout==4.0.2
attrs==23.1.0
Babel==2.12.1
backcall==0.2.0
beautifulsoup4==4.12.2
binaryornot==0.4.4
bleach==6.0.0
blinker==1.6.2
certifi==2023.7.22
cffi==1.15.1
chardet==5.1.0
charset-normalizer==3.2.0
click==8.1.6
colorama==0.4.6
comm==0.1.3
cookiecutter @ git+https://github.com/cookiecutter/cookiecutter.git@33a36b382776aeb713f4cc66f68cd3c20633f297
cryptography==41.0.2
dataclasses-json==0.5.13
dateparser==1.1.8
debugpy==1.6.7
decorator==5.1.1
defusedxml==0.7.1
Deprecated==1.2.14
dnspython==2.4.0
docxcompose==1.4.0
docxtpl==0.16.7
email-validator==1.3.1
ERAlchemy @ git+https://github.com/gbrault/eralchemy.git@32845d27ee08d62c582d08f576f88a02dcaf98e3
et-xmlfile==1.1.0
executing==1.2.0
fastjsonschema==2.18.0
Flask==2.3.2
Flask-AppBuilder==4.3.1
Flask-Babel==2.0.0
Flask-Cors==4.0.0
Flask-JWT-Extended==4.5.2
Flask-Limiter==3.3.1
Flask-Login==0.6.2
flask-sock==0.6.0
Flask-SQLAlchemy==2.5.1
Flask-WTF==1.1.1
fqdn==1.5.1
frozenlist==1.4.0
ftputil==5.0.4
gitdb==4.0.10
GitPython==3.1.32
greenlet==2.0.2
h11==0.14.0
htmlmin==0.1.12
httpcore==0.17.3
idna==3.4
importlib-resources==6.0.0
ipyfilechooser==0.6.0
ipykernel==6.24.0
ipython==8.14.0
ipywidgets==8.0.7
isoduration==20.11.0
itsdangerous==2.1.2
jedi==0.18.2
Jinja2==3.1.2
json-schema-for-humans @ git+https://github.com/gbrault/json-schema-for-humans.git@172d53f4ccb360a875f645733dac5bb44419dd40
json5==0.9.14
jsonpointer==2.4
jsonschema==4.18.4
jsonschema-specifications==2023.7.1
jupyter-events==0.6.3
jupyter-lsp==2.2.0
jupyter_client==8.3.0
jupyter_core==5.3.1
jupyter_server==2.7.0
jupyter_server_proxy==4.0.0
jupyter_server_terminals==0.4.4
jupyterlab==4.0.3
jupyterlab-pygments==0.2.2
jupyterlab-widgets==3.0.8
jupyterlab_server==2.23.0
limits==3.5.0
lxml==4.9.3
Markdown==3.4.3
markdown-it-py==3.0.0
markdown2==2.4.9
MarkupSafe==2.1.3
marshmallow==3.20.1
marshmallow-enum==1.5.1
marshmallow-sqlalchemy==0.26.1
matplotlib-inline==0.1.6
mdurl==0.1.2
mistune==3.0.1
multidict==6.0.4
mypy-extensions==1.0.0
mysql-connector-python==8.1.0
nbclient==0.8.0
nbconvert==7.7.2
nbformat==5.9.1
nest-asyncio==1.5.6
notebook_shim==0.2.3
numpy==1.25.1
openpyxl==3.1.2
ordered-set==4.1.0
overrides==7.3.1
packaging==23.1
pandas==2.0.3
pandocfilters==1.5.0
parso==0.8.3
pexpect==4.8.0
pickleshare==0.7.5
Pillow==10.0.0
platformdirs==3.9.1
prison==0.2.1
prometheus-client==0.17.1
prompt-toolkit==3.0.39
protobuf==4.21.12
psutil==5.9.5
ptyprocess==0.7.0
pure-eval==0.2.2
pycparser==2.21
Pygments==2.15.1
pygraphviz==1.11
PyJWT==2.8.0
PyMuPDF==1.22.5
pyOpenSSL==23.2.0
python-dateutil==2.8.2
python-docx==0.8.11
python-dotenv==1.0.0
python-json-logger==2.0.7
python-slugify==8.0.1
pytz==2021.3
PyYAML==6.0.1
pyzmq==25.1.0
referencing==0.30.0
regex==2023.6.3
requests==2.31.0
rfc3339-validator==0.1.4
rfc3986-validator==0.1.1
rich==13.4.2
rpds-py==0.9.2
Send2Trash==1.8.2
simpervisor==1.0.0
simple-websocket==0.10.1
six==1.16.0
smmap==5.0.0
sniffio==1.3.0
soupsieve==2.4.1
SQLAlchemy==1.4.0
SQLAlchemy-Utils==0.41.1
stack-data==0.6.2
supervisor==4.2.5
terminado==0.17.1
text-unidecode==1.3
tinycss2==1.2.1
tornado==6.3.2
traitlets==5.9.0
typing-inspect==0.9.0
typing_extensions==4.7.1
tzdata==2023.3
tzlocal==5.0.1
uri-template==1.3.0
urllib3==2.0.4
wcwidth==0.2.6
webcolors==1.13
webencodings==0.5.1
websocket-client==1.6.1
Werkzeug==2.3.6
widgetsnbextension==4.0.8
wrapt==1.15.0
wsproto==1.2.0
WTForms==3.0.1
xlrd==2.0.1
XlsxWriter==3.1.2
yarl==1.9.2
gbrault commented 10 months ago

Something I just notice is that I don't have the error on windows on my dev computer here is the freeze for windows

(venv) PS D:\Dev\Ilex Projects\BBFTA> pip freeze
aiofiles==22.1.0
aiohttp==3.8.4
aiosignal==1.3.1
aiosqlite==0.18.0
anyio==3.6.2
apispec==5.2.2
argon2-cffi==21.3.0
argon2-cffi-bindings==21.2.0
arrow==1.2.3
aspose-words-cloud==23.4.0
asttokens==2.2.1
async-timeout==4.0.2
attrs==22.2.0
Babel==2.12.1
backcall==0.2.0
beautifulsoup4==4.12.0
binaryornot==0.4.4
bleach==6.0.0
Brotli==1.0.9
certifi==2022.12.7
cffi==1.15.1
chardet==5.1.0
charset-normalizer==3.1.0
click==8.1.3
cobble==0.1.3
colorama==0.4.6
comm==0.1.3
convertapi==1.6.0
cookiecutter @ git+https://github.com/cookiecutter/cookiecutter.git@1b8520e7075175db4a3deae85e71081730ca7ad1
cryptography==40.0.1
cssselect2==0.7.0
dataclasses-json==0.5.7
dateparser==1.1.8
debugpy==1.6.6
decorator==5.1.1
defusedxml==0.7.1
Deprecated==1.2.13
dnspython==2.3.0
docopt==0.6.2
docx2pdf==0.1.8
docxcompose==1.4.0
docxtpl==0.16.6
doxypypy==0.8.8.7
email-validator==1.3.1
entrypoints==0.4
ERAlchemy @ git+https://github.com/gbrault/eralchemy.git@32845d27ee08d62c582d08f576f88a02dcaf98e3
et-xmlfile==1.1.0
executing==1.2.0
fastjsonschema==2.16.3
Flask==2.2.3
Flask-AppBuilder==4.3.1
Flask-Babel==2.0.0
Flask-Cors==4.0.0
Flask-JWT-Extended==4.4.4
Flask-Limiter==3.3.0
Flask-Login==0.6.2
flask-sock==0.6.0
Flask-SQLAlchemy==2.5.1
Flask-WTF==1.1.1
fonttools==4.42.1
fqdn==1.5.1
frozenlist==1.3.3
ftputil==5.0.4
gitdb==4.0.10
GitPython==3.1.31
greenlet==2.0.2
h11==0.14.0
html5lib==1.1
htmlmin==0.1.12
idna==3.4
importlib-resources==5.12.0
ipyfilechooser==0.6.0
ipykernel==6.22.0
ipython==8.12.0
ipython-genutils==0.2.0
ipywidgets==8.0.6
isoduration==20.11.0
itsdangerous==2.1.2
jedi==0.18.2
Jinja2==3.1.2
jinja2-time==0.2.0
json-schema-for-humans @ git+https://github.com/gbrault/json-schema-for-humans.git@172d53f4ccb360a875f645733dac5bb44419dd40
json5==0.9.11
jsonpointer==2.3
jsonschema==4.17.3
jupyter-events==0.6.3
jupyter-server==1.23.6
jupyter-server-proxy==3.2.2
jupyter-ydoc==0.2.3
jupyter_client==7.4.1
jupyter_core==5.3.0
jupyter_server_fileid==0.8.0
jupyter_server_ydoc==0.8.0
jupyterlab==3.6.3
jupyterlab-pygments==0.2.2
jupyterlab-widgets==3.0.7
jupyterlab_server==2.22.0
limits==3.3.1
lxml==4.9.2
Mako==1.2.4
mammoth==1.5.1
Markdown==3.4.3
markdown-it-py==2.2.0
markdown2==2.4.8
MarkupSafe==2.1.2
marshmallow==3.19.0
marshmallow-enum==1.5.1
marshmallow-sqlalchemy==0.26.1
matplotlib-inline==0.1.6
md2pdf==1.0.1
mdurl==0.1.2
mistune==2.0.5
multidict==6.0.4
mypy-extensions==1.0.0
mysql-connector-python==8.1.0
nbclassic==0.5.4
nbclient==0.7.2
nbconvert==7.2.10
nbformat==5.8.0
nest-asyncio==1.5.6
notebook==6.5.3
notebook_shim==0.2.2
numpy==1.24.2
openpyxl==3.1.2
ordered-set==4.1.0
packaging==23.0
pandas==1.5.3
pandocfilters==1.5.0
parso==0.8.3
pdoc3==0.10.0
pickleshare==0.7.5
Pillow==9.5.0
platformdirs==3.2.0
prison==0.2.1
prometheus-client==0.16.0
prompt-toolkit==3.0.38
protobuf==4.21.12
psutil==5.9.4
pure-eval==0.2.2
pycparser==2.21
pycryptodome==3.17
pydyf==0.7.0
Pygments==2.14.0
pygraphviz==1.10
PyJWT==2.6.0
PyMuPDF==1.22.5
pypandoc-binary==1.11
pyphen==0.14.0
pyrsistent==0.19.3
pystache==0.6.0
python-dateutil==2.8.2
python-docx==0.8.11
python-dotenv==1.0.0
python-json-logger==2.0.7
python-slugify==8.0.1
pytz==2021.3
pywin32==306
pywinpty==2.0.10
PyYAML==6.0
pyzmq==25.0.2
regex==2023.6.3
requests==2.28.2
requests-toolbelt==1.0.0
rfc3339-validator==0.1.4
rfc3986-validator==0.1.1
rich==13.3.3
Send2Trash==1.8.0
simpervisor==0.4
simple-websocket==0.9.0
six==1.16.0
smartypants==2.0.1
smmap==5.0.0
sniffio==1.3.0
soupsieve==2.4
SQLAlchemy==1.4.16
SQLAlchemy-Utils==0.40.0
stack-data==0.6.2
supervisor-win==4.7.0
terminado==0.17.1
text-unidecode==1.3
tinycss2==1.2.1
tornado==6.2
tqdm==4.65.0
traitlets==5.9.0
typing-inspect==0.8.0
typing_extensions==4.5.0
tzdata==2023.3
tzlocal==5.0.1
uri-template==1.2.0
urllib3==1.26.15
voila==0.4.0
wcwidth==0.2.6
weasyprint==59.0
webcolors==1.13
webencodings==0.5.1
websocket-client==1.5.1
websockets==11.0
Werkzeug==2.2.3
widgetsnbextension==4.0.7
wrapt==1.15.0
wsproto==1.2.0
WTForms==3.0.1
xlrd==2.0.1
XlsxWriter==3.1.2
y-py==0.5.9
yarl==1.8.2
ypy-websocket==0.8.2
zopfli==0.2.2
(venv) PS D:\Dev\Ilex Projects\BBFTA> 
miguelgrinberg commented 10 months ago

The ConnectionError problem should be addressed in the Flask-Sock 0.7.0 release. For the other problem I'm not sure. You are comparing two machines that have different versions of packages, so you can try to figure our a combination of requirements that work well. For this problem I'd say the packages that matter are Flask, Werkzeug, simple-websocket and Flask-Sock. If you see issues with the latest versions of these packages, then please report exactly the issue and I'll investigate.

gbrault commented 9 months ago

Thanks @miguelgrinberg , will do!