Skip to content
Snippets Groups Projects
Commit be1ea4d3 authored by tsm-odoo's avatar tsm-odoo
Browse files

[FIX] bus: handle session expired in peek notifications


In order to know whether or not the websocket session is up to date,
the session is saved when opening a websocket connection. Then, for
each incoming/outgoing message, we check if the session still exists
on the file system. When it does not exist anymore, the websocket
connection is refreshed. This ensures the session is always up to date.

Odoo sh proxies the websocket connection and needs a way to tell
when a websocket session is expired. This commit adds the same
check that is done for each incoming websocket message in the
websocket peek route in order to raise a `SessionExpiredException`
when the session is outdated.

This will allow odoo sh to catch this error and to refresh their
websocket connection accordingly.

closes odoo/odoo#100416

Signed-off-by: default avatarJulien Castiaux <juc@odoo.com>
parent 8ec96343
No related branches found
No related tags found
No related merge requests found
......@@ -3,7 +3,7 @@
import json
from werkzeug.exceptions import ServiceUnavailable
from odoo.http import Controller, request, route
from odoo.http import Controller, request, route, SessionExpiredException
from odoo.addons.base.models.assetsbundle import AssetsBundle
from ..models.bus import channel_with_db
from ..websocket import WebsocketConnectionHandler
......@@ -32,9 +32,14 @@ class WebsocketController(Controller):
return request.make_response(data, headers)
@route('/websocket/peek_notifications', type='json', auth='public', cors='*')
def peek_notifications(self, channels, last):
def peek_notifications(self, channels, last, is_first_poll=False):
if not all(isinstance(c, str) for c in channels):
raise ValueError("bus.Bus only string channels are allowed.")
if is_first_poll:
# Used to detect when the current session is expired.
request.session['is_websocket_session'] = True
elif 'is_websocket_session' not in request.session:
raise SessionExpiredException()
channels = list(set(
channel_with_db(request.db, c)
for c in request.env['ir.websocket']._build_bus_channel_list(channels)
......
......@@ -3,4 +3,5 @@ from . import test_assetsbundle
from . import test_health
from . import test_ir_websocket
from . import test_websocket_caryall
from . import test_websocket_controller
from . import test_websocket_rate_limiting
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import json
from odoo.tests import common
class TestWebsocketController(common.HttpCase):
def _make_rpc(self, route, params, headers=None):
data = json.dumps({
'id': 0,
'jsonrpc': '2.0',
'method': 'call',
'params': params,
}).encode()
headers = headers or {}
headers['Content-Type'] = 'application/json'
return self.url_open(route, data, headers=headers)
def test_websocket_peek(self):
response = json.loads(
self._make_rpc('/websocket/peek_notifications', {
'channels': [],
'last': 0,
'is_first_poll': True,
}).content.decode()
)
# Response containing channels/notifications is retrieved and is
# conform to excpectations.
result = response.get('result')
self.assertIsNotNone(result)
channels = result.get('channels')
self.assertIsNotNone(channels)
self.assertIsInstance(channels, list)
notifications = result.get('notifications')
self.assertIsNotNone(notifications)
self.assertIsInstance(notifications, list)
response = json.loads(
self._make_rpc('/websocket/peek_notifications', {
'channels': [],
'last': 0,
'is_first_poll': False,
}).content.decode()
)
# Reponse is received as long as the session is valid.
self.assertIn('result', response)
def test_websocket_peek_session_expired_login(self):
session = self.authenticate(None, None)
# first rpc should be fine
self._make_rpc('/websocket/peek_notifications', {
'channels': [],
'last': 0,
'is_first_poll': True,
})
self.authenticate('admin', 'admin')
# rpc with outdated session should lead to error.
headers = {'Cookie': f'session_id={session.sid};'}
response = json.loads(
self._make_rpc('/websocket/peek_notifications', {
'channels': [],
'last': 0,
'is_first_poll': False,
}, headers=headers).content.decode()
)
error = response.get('error')
self.assertIsNotNone(error, 'Sending a poll with an outdated session should lead to error')
self.assertEqual('odoo.http.SessionExpiredException', error['data']['name'])
def test_websocket_peek_session_expired_logout(self):
session = self.authenticate('demo', 'demo')
# first rpc should be fine
self._make_rpc('/websocket/peek_notifications', {
'channels': [],
'last': 0,
'is_first_poll': True,
})
self.url_open('/web/session/logout')
# rpc with outdated session should lead to error.
headers = {'Cookie': f'session_id={session.sid};'}
response = json.loads(
self._make_rpc('/websocket/peek_notifications', {
'channels': [],
'last': 0,
'is_first_poll': False,
}, headers=headers).content.decode()
)
error = response.get('error')
self.assertIsNotNone(error, 'Sending a poll with an outdated session should lead to error')
self.assertEqual('odoo.http.SessionExpiredException', error['data']['name'])
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment