From cf235dbd4a3acadeeabf3ee73db9291e24de8886 Mon Sep 17 00:00:00 2001 From: Willy Douhard Date: Mon, 9 Dec 2024 09:39:44 +0100 Subject: [PATCH 01/10] fix: pass headers when connecting sockets --- libs/react-client/src/useChatSession.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/libs/react-client/src/useChatSession.ts b/libs/react-client/src/useChatSession.ts index 441e66d665..afb19d14b6 100644 --- a/libs/react-client/src/useChatSession.ts +++ b/libs/react-client/src/useChatSession.ts @@ -100,6 +100,7 @@ const useChatSession = () => { const socket = io(uri, { path, + withCredentials: true, extraHeaders: { Authorization: accessToken || '', 'X-Chainlit-Client-Type': client.type, From bae47824ce385031da8a5307df58608e3964cac0 Mon Sep 17 00:00:00 2001 From: Willy Douhard Date: Mon, 9 Dec 2024 13:33:12 +0100 Subject: [PATCH 02/10] fix: remove task start/end in window_message --- backend/chainlit/socket.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/backend/chainlit/socket.py b/backend/chainlit/socket.py index d79c76c16e..4b68323086 100644 --- a/backend/chainlit/socket.py +++ b/backend/chainlit/socket.py @@ -162,13 +162,13 @@ def emit_call_fn(event: Literal["ask", "call_fn"], data, timeout): async def connection_successful(sid): context = init_ws_context(sid) - if context.session.restored: - return - await context.emitter.task_end() await context.emitter.clear("clear_ask") await context.emitter.clear("clear_call_fn") + if context.session.restored: + return + if context.session.thread_id_to_resume and config.code.on_chat_resume: thread = await resume_thread(context.session) if thread: @@ -312,17 +312,13 @@ async def message(sid, payload: MessagePayload): async def window_message(sid, data): """Handle a message send by the host window.""" session = WebsocketSession.require(sid) - context = init_ws_context(session) - - await context.emitter.task_start() + init_ws_context(session) if config.code.on_window_message: try: await config.code.on_window_message(data) except asyncio.CancelledError: pass - finally: - await context.emitter.task_end() @sio.on("audio_start") From d5dc6c7de5986cedc0a8190e96169952056177fd Mon Sep 17 00:00:00 2001 From: Willy Douhard Date: Mon, 9 Dec 2024 13:45:50 +0100 Subject: [PATCH 03/10] fix: update cors --- backend/chainlit/server.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/backend/chainlit/server.py b/backend/chainlit/server.py index 5118f544a7..1936646f2b 100644 --- a/backend/chainlit/server.py +++ b/backend/chainlit/server.py @@ -187,7 +187,13 @@ def get_build_dir(local_target: str, packaged_target: str) -> str: app = FastAPI(lifespan=lifespan) -sio = socketio.AsyncServer(cors_allowed_origins=[], async_mode="asgi") +ws_cors = ( + "*" if config.project.allow_origins[0] == "*" else config.project.allow_origins +) + +sio = socketio.AsyncServer( + cors_allowed_origins=ws_cors, cors_credentials=True, async_mode="asgi" +) asgi_app = socketio.ASGIApp( socketio_server=sio, @@ -196,9 +202,11 @@ def get_build_dir(local_target: str, packaged_target: str) -> str: app.mount(f"{PREFIX}/ws/socket.io", asgi_app) +http_cors = config.project.allow_origins + app.add_middleware( CORSMiddleware, - allow_origins=config.project.allow_origins, + allow_origins=http_cors, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], From b50fd60e9c347afdc10432051292d51a44eada2f Mon Sep 17 00:00:00 2001 From: Willy Douhard Date: Mon, 9 Dec 2024 14:22:46 +0100 Subject: [PATCH 04/10] fix: cors --- backend/chainlit/server.py | 10 ++-------- cypress/e2e/copilot/.chainlit/config.toml | 2 +- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/backend/chainlit/server.py b/backend/chainlit/server.py index 1936646f2b..49dd5c7e40 100644 --- a/backend/chainlit/server.py +++ b/backend/chainlit/server.py @@ -187,12 +187,8 @@ def get_build_dir(local_target: str, packaged_target: str) -> str: app = FastAPI(lifespan=lifespan) -ws_cors = ( - "*" if config.project.allow_origins[0] == "*" else config.project.allow_origins -) - sio = socketio.AsyncServer( - cors_allowed_origins=ws_cors, cors_credentials=True, async_mode="asgi" + async_mode="asgi" ) asgi_app = socketio.ASGIApp( @@ -202,11 +198,9 @@ def get_build_dir(local_target: str, packaged_target: str) -> str: app.mount(f"{PREFIX}/ws/socket.io", asgi_app) -http_cors = config.project.allow_origins - app.add_middleware( CORSMiddleware, - allow_origins=http_cors, + allow_origins=config.project.allow_origins, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], diff --git a/cypress/e2e/copilot/.chainlit/config.toml b/cypress/e2e/copilot/.chainlit/config.toml index e2a93af08f..9183c3c84b 100644 --- a/cypress/e2e/copilot/.chainlit/config.toml +++ b/cypress/e2e/copilot/.chainlit/config.toml @@ -13,7 +13,7 @@ session_timeout = 3600 cache = false # Authorized origins -allow_origins = ["*"] +allow_origins = ["http://localhost:8080"] # Follow symlink for asset mount (see https://github.com/Chainlit/chainlit/issues/317) # follow_symlink = false From 96f68c9093268623ca8d2732f90cbb4342a5ebfb Mon Sep 17 00:00:00 2001 From: Willy Douhard Date: Mon, 9 Dec 2024 14:46:24 +0100 Subject: [PATCH 05/10] fix: update cors --- backend/chainlit/server.py | 1 + cypress/e2e/copilot/.chainlit/config.toml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/chainlit/server.py b/backend/chainlit/server.py index 49dd5c7e40..14d1605f9f 100644 --- a/backend/chainlit/server.py +++ b/backend/chainlit/server.py @@ -188,6 +188,7 @@ def get_build_dir(local_target: str, packaged_target: str) -> str: app = FastAPI(lifespan=lifespan) sio = socketio.AsyncServer( + cors_allowed_origins=[], async_mode="asgi" ) diff --git a/cypress/e2e/copilot/.chainlit/config.toml b/cypress/e2e/copilot/.chainlit/config.toml index 9183c3c84b..e2a93af08f 100644 --- a/cypress/e2e/copilot/.chainlit/config.toml +++ b/cypress/e2e/copilot/.chainlit/config.toml @@ -13,7 +13,7 @@ session_timeout = 3600 cache = false # Authorized origins -allow_origins = ["http://localhost:8080"] +allow_origins = ["*"] # Follow symlink for asset mount (see https://github.com/Chainlit/chainlit/issues/317) # follow_symlink = false From b599372f71830ae39fdcad9dc0bcff0f5111fa7b Mon Sep 17 00:00:00 2001 From: Willy Douhard Date: Mon, 9 Dec 2024 15:08:56 +0100 Subject: [PATCH 06/10] fix: ci --- backend/chainlit/server.py | 5 +---- cypress/e2e/copilot/.chainlit/config.toml | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/backend/chainlit/server.py b/backend/chainlit/server.py index 14d1605f9f..5118f544a7 100644 --- a/backend/chainlit/server.py +++ b/backend/chainlit/server.py @@ -187,10 +187,7 @@ def get_build_dir(local_target: str, packaged_target: str) -> str: app = FastAPI(lifespan=lifespan) -sio = socketio.AsyncServer( - cors_allowed_origins=[], - async_mode="asgi" -) +sio = socketio.AsyncServer(cors_allowed_origins=[], async_mode="asgi") asgi_app = socketio.ASGIApp( socketio_server=sio, diff --git a/cypress/e2e/copilot/.chainlit/config.toml b/cypress/e2e/copilot/.chainlit/config.toml index e2a93af08f..9c42755715 100644 --- a/cypress/e2e/copilot/.chainlit/config.toml +++ b/cypress/e2e/copilot/.chainlit/config.toml @@ -13,7 +13,7 @@ session_timeout = 3600 cache = false # Authorized origins -allow_origins = ["*"] +allow_origins = ["http://127.0.0.1:8000"] # Follow symlink for asset mount (see https://github.com/Chainlit/chainlit/issues/317) # follow_symlink = false From 7ad1706f2c5abcbe0e4b73eaa585c3d2e61bac30 Mon Sep 17 00:00:00 2001 From: Willy Douhard Date: Mon, 9 Dec 2024 16:10:50 +0100 Subject: [PATCH 07/10] fix: add transports: ['websocket'] --- libs/react-client/src/useChatSession.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/libs/react-client/src/useChatSession.ts b/libs/react-client/src/useChatSession.ts index afb19d14b6..e1632ee24b 100644 --- a/libs/react-client/src/useChatSession.ts +++ b/libs/react-client/src/useChatSession.ts @@ -101,6 +101,7 @@ const useChatSession = () => { const socket = io(uri, { path, withCredentials: true, + transports: ['websocket'], extraHeaders: { Authorization: accessToken || '', 'X-Chainlit-Client-Type': client.type, From 646d1bf91668a9df40bcce3947543ba4bcd77caa Mon Sep 17 00:00:00 2001 From: Willy Douhard Date: Mon, 9 Dec 2024 17:47:06 +0100 Subject: [PATCH 08/10] fix: test --- backend/chainlit/socket.py | 32 +++++++------------------ libs/react-client/src/useChatSession.ts | 21 ++++++++-------- 2 files changed, 18 insertions(+), 35 deletions(-) diff --git a/backend/chainlit/socket.py b/backend/chainlit/socket.py index 4b68323086..0424d4b793 100644 --- a/backend/chainlit/socket.py +++ b/backend/chainlit/socket.py @@ -77,24 +77,8 @@ def load_user_env(user_env): return user_env -def build_anon_user_identifier(environ): - scope = environ.get("asgi.scope", {}) - client_ip, _ = scope.get("client") - ip = environ.get("HTTP_X_FORWARDED_FOR", client_ip) - - try: - headers = scope.get("headers", {}) - user_agent = next( - (v.decode("utf-8") for k, v in headers if k.decode("utf-8") == "user-agent") - ) - return str(uuid.uuid5(uuid.NAMESPACE_DNS, user_agent + ip)) - - except StopIteration: - return str(uuid.uuid5(uuid.NAMESPACE_DNS, ip)) - - @sio.on("connect") -async def connect(sid, environ): +async def connect(sid, environ, auth): if ( not config.code.on_chat_start and not config.code.on_message @@ -110,8 +94,8 @@ async def connect(sid, environ): try: # Check if the authentication is required if login_required: - authorization_header = environ.get("HTTP_AUTHORIZATION") - token = authorization_header.split(" ")[1] if authorization_header else None + token = auth.get("token") + token = token.split(" ")[1] if token else None user = await get_current_user(token=token) except Exception: logger.info("Authentication failed") @@ -125,16 +109,16 @@ def emit_fn(event, data): def emit_call_fn(event: Literal["ask", "call_fn"], data, timeout): return sio.call(event, data, timeout=timeout, to=sid) - session_id = environ.get("HTTP_X_CHAINLIT_SESSION_ID") + session_id = auth.get("sessionId") if restore_existing_session(sid, session_id, emit_fn, emit_call_fn): return True - user_env_string = environ.get("HTTP_USER_ENV") + user_env_string = auth.get("userEnv") user_env = load_user_env(user_env_string) - client_type = environ.get("HTTP_X_CHAINLIT_CLIENT_TYPE") + client_type = auth.get("clientType") http_referer = environ.get("HTTP_REFERER") - url_encoded_chat_profile = environ.get("HTTP_X_CHAINLIT_CHAT_PROFILE") + url_encoded_chat_profile = auth.get("chatProfile") chat_profile = ( unquote(url_encoded_chat_profile) if url_encoded_chat_profile else None ) @@ -149,7 +133,7 @@ def emit_call_fn(event: Literal["ask", "call_fn"], data, timeout): user=user, token=token, chat_profile=chat_profile, - thread_id=environ.get("HTTP_X_CHAINLIT_THREAD_ID"), + thread_id=auth.get("threadId"), languages=environ.get("HTTP_ACCEPT_LANGUAGE"), http_referer=http_referer, ) diff --git a/libs/react-client/src/useChatSession.ts b/libs/react-client/src/useChatSession.ts index e1632ee24b..8d381ade23 100644 --- a/libs/react-client/src/useChatSession.ts +++ b/libs/react-client/src/useChatSession.ts @@ -78,7 +78,7 @@ const useChatSession = () => { // Use currentThreadId as thread id in websocket header useEffect(() => { if (session?.socket) { - session.socket.io.opts.extraHeaders!['X-Chainlit-Thread-Id'] = + session.socket.auth["threadId"] = currentThreadId || ''; } }, [currentThreadId]); @@ -102,16 +102,15 @@ const useChatSession = () => { path, withCredentials: true, transports: ['websocket'], - extraHeaders: { - Authorization: accessToken || '', - 'X-Chainlit-Client-Type': client.type, - 'X-Chainlit-Session-Id': sessionId, - 'X-Chainlit-Thread-Id': idToResume || '', - 'user-env': JSON.stringify(userEnv), - 'X-Chainlit-Chat-Profile': chatProfile - ? encodeURIComponent(chatProfile) - : '' - } + auth: { + token: accessToken, + clientType: client.type, + sessionId, + threadId: idToResume || '', + userEnv: JSON.stringify(userEnv), + chatProfile: chatProfile ? encodeURIComponent(chatProfile) : '' + } + }); setSession((old) => { old?.socket?.removeAllListeners(); From d1931bfcf3199b3fedc1c65824d668903f339d4b Mon Sep 17 00:00:00 2001 From: Willy Douhard Date: Mon, 9 Dec 2024 18:01:24 +0100 Subject: [PATCH 09/10] fix: lint --- backend/chainlit/socket.py | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/chainlit/socket.py b/backend/chainlit/socket.py index 0424d4b793..5053262e2f 100644 --- a/backend/chainlit/socket.py +++ b/backend/chainlit/socket.py @@ -1,7 +1,6 @@ import asyncio import json import time -import uuid from typing import Any, Dict, Literal from urllib.parse import unquote From 827201202fa3588d249f79ba59566d20e7265efd Mon Sep 17 00:00:00 2001 From: Willy Douhard Date: Tue, 10 Dec 2024 09:21:42 +0100 Subject: [PATCH 10/10] feat: make socket.io client transports option configurable --- backend/chainlit/config.py | 2 ++ backend/chainlit/server.py | 5 ++++- frontend/src/App.tsx | 2 ++ libs/copilot/src/chat/index.tsx | 1 + libs/react-client/src/useChatSession.ts | 4 +++- 5 files changed, 12 insertions(+), 2 deletions(-) diff --git a/backend/chainlit/config.py b/backend/chainlit/config.py index b90f162f07..18ee6be8db 100644 --- a/backend/chainlit/config.py +++ b/backend/chainlit/config.py @@ -311,6 +311,8 @@ class CodeSettings: @dataclass() class ProjectSettings(DataClassJsonMixin): allow_origins: List[str] = Field(default_factory=lambda: ["*"]) + # Socket.io client transports option + transports: Optional[List[str]] = None enable_telemetry: bool = True # List of environment variables to be provided by each user to use the app. If empty, no environment variables will be asked to the user. user_env: Optional[List[str]] = None diff --git a/backend/chainlit/server.py b/backend/chainlit/server.py index 5118f544a7..7aeabe5329 100644 --- a/backend/chainlit/server.py +++ b/backend/chainlit/server.py @@ -301,7 +301,10 @@ def get_html_template(): """ - js = f"""""" + js = f"""""" css = None if config.ui.custom_css: diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index cc80e03ac9..9238ca2519 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -42,6 +42,7 @@ declare global { light?: ThemOverride; dark?: ThemOverride; }; + transports?: string[] } } @@ -99,6 +100,7 @@ function App() { return; } else { connect({ + transports: window.transports, userEnv, accessToken }); diff --git a/libs/copilot/src/chat/index.tsx b/libs/copilot/src/chat/index.tsx index 5f0a0779e7..3cc4bd3289 100644 --- a/libs/copilot/src/chat/index.tsx +++ b/libs/copilot/src/chat/index.tsx @@ -12,6 +12,7 @@ export default function ChatWrapper() { useEffect(() => { if (session?.socket?.connected) return; connect({ + transports: window.transports, userEnv: {}, accessToken: `Bearer ${accessToken}` }); diff --git a/libs/react-client/src/useChatSession.ts b/libs/react-client/src/useChatSession.ts index 8d381ade23..b1079179f0 100644 --- a/libs/react-client/src/useChatSession.ts +++ b/libs/react-client/src/useChatSession.ts @@ -85,9 +85,11 @@ const useChatSession = () => { const _connect = useCallback( ({ + transports, userEnv, accessToken }: { + transports?: string[] userEnv: Record; accessToken?: string; }) => { @@ -101,7 +103,7 @@ const useChatSession = () => { const socket = io(uri, { path, withCredentials: true, - transports: ['websocket'], + transports, auth: { token: accessToken, clientType: client.type,