Handle URL host of the bus on login instead of using a redirect

Browsers remove authorization header upon cross-origin redirect.

Closes #12329
This commit is contained in:
Nicolas Évrard
2025-10-06 17:03:41 +02:00
parent f0199c149b
commit 736eda2333
9 changed files with 41 additions and 27 deletions
+4
View File
@@ -156,6 +156,9 @@ def acs(request, pool, identity):
with Transaction().set_user(user_id):
session = Session.new()
allow_subscribe = config.getboolean(
'bus', 'allow_subscribe', default=False)
bus_url_host = config.get('bus', 'url_host', default=request.host_url)
redirect_url = request.form.get('RelayState')
if not redirect_url:
redirect_url = http_host()
@@ -165,6 +168,7 @@ def acs(request, pool, identity):
query.append(('login', login))
query.append(('user_id', user_id))
query.append(('session', session))
query.append(('bus_url_host', bus_url_host if allow_subscribe else ''))
parts = list(parts)
parts[3] = urllib.parse.urlencode(query)
return redirect(urllib.parse.urlunsplit(parts))
+3 -2
View File
@@ -16,12 +16,13 @@
wait = wait || 1;
last_message = last_message || Sao.Bus.last_message;
var session = Sao.Session.current_session;
if (!session) {
if (!session || !session.bus_url_host) {
return;
}
Sao.Bus.listening = true;
let channels = Object.keys(Sao.Bus.channel_actions);
let url = new URL(`${session.database}/bus`, session.bus_url_host);
Sao.Bus.last_message = last_message;
Sao.Bus.request = jQuery.ajax({
headers: {
@@ -33,7 +34,7 @@
channels: channels,
}),
dataType: 'json',
url: '/' + session.database + '/bus',
url: url,
type: 'POST',
timeout: Sao.config.bus_timeout,
});
+1
View File
@@ -1135,6 +1135,7 @@ var Sao = {
login: url.searchParams.get('login'),
user_id: parseInt(url.searchParams.get('user_id'), 10),
session: url.searchParams.get('session'),
bus_url_host: url.searchParams.get('bus_url_host'),
};
if (url.searchParams.has('renew')) {
var renew_id = parseInt(url.searchParams.get('renew'), 10);
+5
View File
@@ -13,6 +13,7 @@
this.login_service = null;
this.user_id = null;
this.session = null;
this.bus_url_host = null;
this.cache = new Cache();
this.prm = jQuery.when(); // renew promise
this.database = database;
@@ -47,12 +48,14 @@
this.login = login;
this.user_id = result[0];
this.session = result[1];
this.bus_url_host = result[2];
this.store();
this.renew_device_cookie();
dfd.resolve();
}, () => {
this.user_id = null;
this.session = null;
this.bus_url_host = null;
this.store();
dfd.reject();
});
@@ -148,6 +151,7 @@
this.login = session_data.login;
this.user_id = session_data.user_id;
this.session = session_data.session;
this.bus_url_host = session_data.bus_url_host;
}
}
}
@@ -157,6 +161,7 @@
'login': this.login,
'user_id': this.user_id,
'session': this.session,
'bus_url_host': this.bus_url_host,
};
session = JSON.stringify(session);
localStorage.setItem('sao_session_' + this.database, session);
+16 -12
View File
@@ -9,6 +9,7 @@ import time
import uuid
from collections import defaultdict
from urllib.error import HTTPError
from urllib.parse import urljoin
from urllib.request import Request, urlopen
from gi.repository import GLib
@@ -25,20 +26,25 @@ class Bus:
current_thread = None
channel_actions = defaultdict(list)
listening = False
connection = None
host = None
@classmethod
def listen(cls, connection):
def listen(cls, connection, host):
if not CONFIG['thread']:
return
listener = threading.Thread(
target=cls._listen, args=(connection,), daemon=True)
cls.connection = connection
cls.host = host
if not host:
return
listener = threading.Thread(target=cls._listen, daemon=True)
listener.start()
cls.current_thread = listener.ident
@classmethod
def _listen(cls, connection):
def _listen(cls):
bus_timeout = CONFIG['client.bus_timeout']
session = connection.session
session = cls.connection.session
authorization = base64.b64encode(session.encode('utf-8'))
headers = {
'Content-Type': 'application/json',
@@ -49,12 +55,12 @@ class Bus:
wait = 1
last_message = None
url = None
while connection.session == session:
while cls.connection.session == session:
if url is None:
if connection.url is None:
if cls.connection._database is None:
time.sleep(1)
continue
url = connection.url + '/bus'
url = urljoin(cls.host, f'{cls.connection._database}/bus')
cls.listening = True
channels = list(cls.channel_actions)
@@ -91,7 +97,7 @@ class Bus:
wait *= 2
continue
if connection.session != session:
if cls.connection.session != session:
break
if thread_id != cls.current_thread:
break
@@ -109,14 +115,12 @@ class Bus:
@classmethod
def register(cls, channel, function):
from tryton import rpc
restart = channel not in cls.channel_actions
cls.channel_actions[channel].append(function)
if restart:
# We can not really abort a thread, so we will just start a new one
# and ignore the result of the one already running
Bus.listen(rpc.CONNECTION)
Bus.listen(cls.connection, cls.host)
@classmethod
def unregister(cls, channel, function):
+5 -3
View File
@@ -102,12 +102,13 @@ def set_service_session(parameters):
if _USER != renew_id:
raise ValueError
_USER = user_id
bus_url_host = parameters.get('bus_url_host', [''])[0]
session = ':'.join(map(str, [username, user_id, session]))
if CONNECTION is not None:
CONNECTION.close()
CONNECTION = ServerPool(
hostname, port, database, session=session, cache=not CONFIG['dev'])
Bus.listen(CONNECTION)
Bus.listen(CONNECTION, bus_url_host)
def login(parameters):
@@ -126,13 +127,14 @@ def login(parameters):
result = connection.common.db.login(username, parameters, language)
logger.debug('%r', result)
_USER = result[0]
session = ':'.join(map(str, [username] + result))
session = ':'.join(map(str, [username] + result[:2]))
bus_url_host = result[2]
if CONNECTION is not None:
CONNECTION.close()
CONNECTION = ServerPool(
hostname, port, database, session=session, cache=not CONFIG['dev'])
device_cookie.renew()
Bus.listen(CONNECTION)
Bus.listen(CONNECTION, bus_url_host)
def logout():
+1
View File
@@ -1,3 +1,4 @@
* Handle URL host of the bus on login
* Restrict sessions to the client's IP address
* Use ``tryton.cfg`` to register in the Pool
* Use the FILTER syntax with SQL aggregate functions
+2 -9
View File
@@ -9,13 +9,11 @@ import selectors
import threading
import time
import uuid
from urllib.parse import urljoin
from trytond import backend
from trytond.config import config
from trytond.protocols.jsonrpc import JSONDecoder, JSONEncoder
from trytond.protocols.wrappers import (
HTTPStatus, Response, exceptions, redirect)
from trytond.protocols.wrappers import HTTPStatus, Response, abort, exceptions
from trytond.tools import resolve
from trytond.transaction import Transaction
from trytond.wsgi import app
@@ -231,12 +229,7 @@ def subscribe(request, database_name):
if not _allow_subscribe:
raise exceptions.NotImplemented
if _url_host and _url_host != request.host_url:
response = redirect(
urljoin(_url_host, request.path), HTTPStatus.PERMANENT_REDIRECT)
# Allow to change the redirection after some time
response.headers['Cache-Control'] = (
'private, max-age=%s' % _web_cache_timeout)
return response
abort(HTTPStatus.UNAUTHORIZED)
user = request.authorization.get('userid')
channels = request.parsed_data.get('channels', [])
if user is None:
+4 -1
View File
@@ -62,7 +62,10 @@ def login(request, database_name, user, parameters, language=None):
code = HTTPStatus.TOO_MANY_REQUESTS
if not session:
abort(code)
return session
allow_subscribe = config.getboolean(
'bus', 'allow_subscribe', default=False)
bus_url_host = config.get('bus', 'url_host', default=request.host_url)
return (*session, bus_url_host if allow_subscribe else None)
@app.auth_required