fix(core): 3.0 cluster setup (#20439)

* Add graphql API endpoint to BBB API

This parameter will be required for the HTML5 client to discover the API
endpoint of the GraphQL API so it can fetch its settings.

* Fix: ensure API calls are headed towards the BBB Server

In a cluster setup the URL that is visible in the browser is different
from the URL of the BBB Server.

The clients needs to contact the BBB server to retrieve information
about the graphQL websocket and the graphql API.

This patch adds the GraphQL API endpoint to the BBB API and changes the
Client to use it.

* Make GraphQL API endpoint configurable

The default value should work for single node and cluster setups

* Update docs to reflect changes required by cluster setup

* fix eslint and typescript errors

* Fix cluster setup docs

- add missing parts for hasura
- use consistent domain example.com

* rename variable

Co-authored-by: Gustavo Trott <gustavo@trott.com.br>

* rename variable

Co-authored-by: Gustavo Trott <gustavo@trott.com.br>

* rename variable

Co-authored-by: Gustavo Trott <gustavo@trott.com.br>

* Update bigbluebutton-web/grails-app/controllers/org/bigbluebutton/web/controllers/ApiController.groovy

Co-authored-by: Gustavo Trott <gustavo@trott.com.br>

* Update bigbluebutton-web/grails-app/conf/bigbluebutton.properties

Co-authored-by: Gustavo Trott <gustavo@trott.com.br>

---------

Co-authored-by: Daniel Schreiber <daniel.schreiber@hrz.tu-chemnitz.de>
Co-authored-by: Gustavo Trott <gustavo@trott.com.br>
This commit is contained in:
schrd 2024-06-17 20:26:13 +02:00 committed by GitHub
parent 52a548b93e
commit 64e43e4331
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 104 additions and 40 deletions

View File

@ -77,6 +77,7 @@ public class ParamsProcessorUtil {
private String defaultHTML5ClientUrl; private String defaultHTML5ClientUrl;
private String graphqlWebsocketUrl; private String graphqlWebsocketUrl;
private String graphqlApiUrl;
private Boolean allowRequestsWithoutSession = false; private Boolean allowRequestsWithoutSession = false;
private Integer defaultHttpSessionTimeout = 14400; private Integer defaultHttpSessionTimeout = 14400;
private Boolean useDefaultAvatar = false; private Boolean useDefaultAvatar = false;
@ -886,6 +887,10 @@ public class ParamsProcessorUtil {
return graphqlWebsocketUrl; return graphqlWebsocketUrl;
} }
public String getGraphqlApiUrl() {
return graphqlApiUrl;
}
public Boolean getUseDefaultLogo() { public Boolean getUseDefaultLogo() {
return useDefaultLogo; return useDefaultLogo;
} }
@ -1249,6 +1254,10 @@ public class ParamsProcessorUtil {
this.graphqlWebsocketUrl = graphqlWebsocketUrl.replace("https://","wss://"); this.graphqlWebsocketUrl = graphqlWebsocketUrl.replace("https://","wss://");
} }
public void setGraphqlApiUrl(String graphqlApiUrl) {
this.graphqlApiUrl = graphqlApiUrl;
}
public void setUseDefaultLogo(Boolean value) { public void setUseDefaultLogo(Boolean value) {
this.useDefaultLogo = value; this.useDefaultLogo = value;
} }

View File

@ -52,7 +52,7 @@ public class ResponseBuilder {
return new Date(timestamp).toString(); return new Date(timestamp).toString();
} }
public String buildMeetingVersion(String apiVersion, String bbbVersion, String graphqlWebsocketUrl, String returnCode) { public String buildMeetingVersion(String apiVersion, String bbbVersion, String graphqlWebsocketUrl, String graphqlApiUrl, String returnCode) {
StringWriter xmlText = new StringWriter(); StringWriter xmlText = new StringWriter();
Map<String, Object> data = new HashMap<String, Object>(); Map<String, Object> data = new HashMap<String, Object>();
@ -61,6 +61,7 @@ public class ResponseBuilder {
data.put("apiVersion", apiVersion); data.put("apiVersion", apiVersion);
data.put("bbbVersion", bbbVersion); data.put("bbbVersion", bbbVersion);
data.put("graphqlWebsocketUrl", graphqlWebsocketUrl); data.put("graphqlWebsocketUrl", graphqlWebsocketUrl);
data.put("graphqlApiUrl", graphqlApiUrl);
processData(getTemplate("api-version.ftlx"), data, xmlText); processData(getTemplate("api-version.ftlx"), data, xmlText);

View File

@ -70,7 +70,12 @@ const ConnectionManager: React.FC<ConnectionManagerProps> = ({ children }): Reac
const numberOfAttempts = useRef(20); const numberOfAttempts = useRef(20);
const [errorCounts, setErrorCounts] = React.useState(0); const [errorCounts, setErrorCounts] = React.useState(0);
useEffect(() => { useEffect(() => {
fetch(`https://${window.location.hostname}/bigbluebutton/api`, { const pathMatch = window.location.pathname.match('^(.*)/html5client/join$');
if (pathMatch == null) {
throw new Error('Failed to match BBB client URI');
}
const serverPathPrefix = pathMatch[1];
fetch(`https://${window.location.hostname}${serverPathPrefix}/bigbluebutton/api`, {
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },

View File

@ -1,5 +1,5 @@
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import Session from '/imports/ui/services/storage/in-memory'; import { Session } from 'meteor/session';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
import { ErrorScreen } from '../../error-screen/component'; import { ErrorScreen } from '../../error-screen/component';
import LoadingScreen from '../../common/loading-screen/component'; import LoadingScreen from '../../common/loading-screen/component';
@ -65,9 +65,19 @@ const StartupDataFetch: React.FC<StartupDataFetchProps> = ({
setLoading(false); setLoading(false);
return; return;
} }
const clientStartupSettings = `/api/rest/clientStartupSettings/?sessionToken=${sessionToken}`; const pathMatch = window.location.pathname.match('^(.*)/html5client/join$');
const url = new URL(`${window.location.origin}${clientStartupSettings}`); if (pathMatch == null) {
fetch(url, { method: 'get' }) throw new Error('Failed to match BBB client URI');
}
const serverPathPrefix = pathMatch[1];
fetch(`https://${window.location.hostname}${serverPathPrefix}/bigbluebutton/api`, {
headers: {
'Content-Type': 'application/json',
},
}).then((resp) => resp.json())
.then((data) => {
const url = `${data.response.graphqlApiUrl}/clientStartupSettings/?sessionToken=${sessionToken}`;
fetch(url, { method: 'get', credentials: 'include' })
.then((resp) => resp.json()) .then((resp) => resp.json())
.then((data: Response) => { .then((data: Response) => {
const settings = data.meeting_clientSettings[0]; const settings = data.meeting_clientSettings[0];
@ -76,10 +86,14 @@ const StartupDataFetch: React.FC<StartupDataFetchProps> = ({
clearTimeout(timeoutRef.current); clearTimeout(timeoutRef.current);
setLoading(false); setLoading(false);
}).catch(() => { }).catch(() => {
Session.setItem('errorMessageDescription', 'meeting_ended'); Session.set('errorMessageDescription', 'meeting_ended');
setError('Error fetching startup data'); setError('Error fetching startup data');
setLoading(false); setLoading(false);
}); });
}).catch((error) => {
setLoading(false);
throw new Error('Error fetching GraphQL URL: '.concat(error.message || ''));
});
}, []); }, []);
return ( return (

View File

@ -27,11 +27,22 @@ const SettingsLoader: React.FC = () => {
}, []); }, []);
useEffect(() => { useEffect(() => {
const pathMatch = window.location.pathname.match('^(.*)/html5client/join$');
if (pathMatch == null) {
throw new Error('Failed to match BBB client URI');
}
const serverPathPrefix = pathMatch[1];
fetch(`https://${window.location.hostname}${serverPathPrefix}/bigbluebutton/api`, {
headers: {
'Content-Type': 'application/json',
},
}).then((resp) => resp.json())
.then((data) => {
const urlParams = new URLSearchParams(window.location.search); const urlParams = new URLSearchParams(window.location.search);
const sessionToken = urlParams.get('sessionToken'); const sessionToken = urlParams.get('sessionToken');
const clientStartupSettings = `/api/rest/clientSettings/?sessionToken=${sessionToken}`; const clientStartupSettings = `/clientSettings/?sessionToken=${sessionToken}`;
const url = new URL(`${window.location.origin}${clientStartupSettings}`); const url = new URL(`${data.response.graphqlApiUrl}${clientStartupSettings}`);
fetch(url, { method: 'get' }) fetch(url, { method: 'get', credentials: 'include' })
.then((resp) => resp.json()) .then((resp) => resp.json())
.then((data: Response) => { .then((data: Response) => {
const settings = data?.meeting_clientSettings[0].clientSettingsJson; const settings = data?.meeting_clientSettings[0].clientSettingsJson;
@ -45,7 +56,10 @@ const SettingsLoader: React.FC = () => {
loadingContextInfo.setLoading(false, ''); loadingContextInfo.setLoading(false, '');
throw new Error('Error on requesting client settings data.'); throw new Error('Error on requesting client settings data.');
}); });
// } }).catch((error) => {
loadingContextInfo.setLoading(false, '');
throw new Error('Error fetching GraphQL API URL: '.concat(error.message || ''));
});
}, []); }, []);
return ( return (
(allowToRender) (allowToRender)

View File

@ -308,6 +308,9 @@ defaultHTML5ClientUrl=${bigbluebutton.web.serverURL}/html5client/join
# Using `serverURL` as default, so `https` will be automatically replaced by `wss` # Using `serverURL` as default, so `https` will be automatically replaced by `wss`
graphqlWebsocketUrl=${bigbluebutton.web.serverURL}/graphql graphqlWebsocketUrl=${bigbluebutton.web.serverURL}/graphql
# Graphql API url (it's necessary to change for cluster setup)
graphqlApiUrl=${bigbluebutton.web.serverURL}/api/rest
# This parameter defines the duration (in minutes) to wait before removing user sessions after a meeting has ended. # This parameter defines the duration (in minutes) to wait before removing user sessions after a meeting has ended.
# During this delay, users can still access information indicating that the "Meeting has ended". # During this delay, users can still access information indicating that the "Meeting has ended".
# Setting this value to 0 will result in the sessions being kept alive indefinitely (permanent availability). # Setting this value to 0 will result in the sessions being kept alive indefinitely (permanent availability).

View File

@ -142,6 +142,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
<property name="defaultNumDigitsForTelVoice" value="${defaultNumDigitsForTelVoice}"/> <property name="defaultNumDigitsForTelVoice" value="${defaultNumDigitsForTelVoice}"/>
<property name="defaultHTML5ClientUrl" value="${defaultHTML5ClientUrl}"/> <property name="defaultHTML5ClientUrl" value="${defaultHTML5ClientUrl}"/>
<property name="graphqlWebsocketUrl" value="${graphqlWebsocketUrl}"/> <property name="graphqlWebsocketUrl" value="${graphqlWebsocketUrl}"/>
<property name="graphqlApiUrl" value="${graphqlApiUrl}"/>
<property name="useDefaultLogo" value="${useDefaultLogo}"/> <property name="useDefaultLogo" value="${useDefaultLogo}"/>
<property name="defaultLogoURL" value="${defaultLogoURL}"/> <property name="defaultLogoURL" value="${defaultLogoURL}"/>
<property name="allowRequestsWithoutSession" value="${allowRequestsWithoutSession}"/> <property name="allowRequestsWithoutSession" value="${allowRequestsWithoutSession}"/>

View File

@ -97,6 +97,7 @@ class ApiController {
apiVersion paramsProcessorUtil.getApiVersion() apiVersion paramsProcessorUtil.getApiVersion()
bbbVersion paramsProcessorUtil.getBbbVersion() bbbVersion paramsProcessorUtil.getBbbVersion()
graphqlWebsocketUrl paramsProcessorUtil.getGraphqlWebsocketUrl() graphqlWebsocketUrl paramsProcessorUtil.getGraphqlWebsocketUrl()
graphqlApiUrl paramsProcessorUtil.getGraphqlApiUrl()
} }
render(contentType: "application/json", text: builder.toPrettyString()) render(contentType: "application/json", text: builder.toPrettyString())
} }
@ -108,6 +109,7 @@ class ApiController {
paramsProcessorUtil.getApiVersion(), paramsProcessorUtil.getApiVersion(),
paramsProcessorUtil.getBbbVersion(), paramsProcessorUtil.getBbbVersion(),
paramsProcessorUtil.getGraphqlWebsocketUrl(), paramsProcessorUtil.getGraphqlWebsocketUrl(),
paramsProcessorUtil.getGraphqlApiUrl(),
RESP_CODE_SUCCESS), RESP_CODE_SUCCESS),
contentType: "text/xml") contentType: "text/xml")
} }

View File

@ -7,5 +7,6 @@
<apiVersion>${apiVersion}</apiVersion> <apiVersion>${apiVersion}</apiVersion>
<bbbVersion>${bbbVersion}</bbbVersion> <bbbVersion>${bbbVersion}</bbbVersion>
<graphqlWebsocketUrl>${graphqlWebsocketUrl}</graphqlWebsocketUrl> <graphqlWebsocketUrl>${graphqlWebsocketUrl}</graphqlWebsocketUrl>
<graphqlApiUrl>${graphqlApiUrl}</graphqlApiUrl>
</response> </response>
</#compress> </#compress>

View File

@ -77,6 +77,12 @@ location /bbb-01/html5client/ {
proxy_set_header Upgrade $http_upgrade; proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade"; proxy_set_header Connection "Upgrade";
} }
location /bbb-01/bigbluebutton/api {
proxy_pass https://bbb-01.example.com/bigbluebutton/api;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
}
``` ```
Repeat this `location` directive for every BigBlueButton server. Repeat this `location` directive for every BigBlueButton server.
@ -123,9 +129,9 @@ public:
url: 'https://bbb-01.example.com/pad' url: 'https://bbb-01.example.com/pad'
``` ```
Create (or edit if it already exists) these unit file overrides: Create (or edit if it already exists) this unit override file:
* `/usr/lib/systemd/system/bbb-html5.service` * `/etc/systemd/system/bbb-html5.service.d/cluster.conf`
It should have the following content: It should have the following content:
@ -165,7 +171,7 @@ Create the file `/etc/bigbluebutton/etherpad.json` with the following content:
```json ```json
{ {
"cluster_proxies": [ "cluster_proxies": [
"https://bbb-proxy.example.org" "https://bbb-proxy.example.com"
] ]
} }
``` ```
@ -173,7 +179,7 @@ Create the file `/etc/bigbluebutton/etherpad.json` with the following content:
Adjust the CORS settings in `/etc/default/bbb-web`: Adjust the CORS settings in `/etc/default/bbb-web`:
```shell ```shell
JDK_JAVA_OPTIONS="-Dgrails.cors.enabled=true -Dgrails.cors.allowCredentials=true -Dgrails.cors.allowedOrigins=https://bbb-proxy.example.org,https://https://bbb-01.example.com" JDK_JAVA_OPTIONS="-Dgrails.cors.enabled=true -Dgrails.cors.allowCredentials=true -Dgrails.cors.allowedOrigins=https://bbb-proxy.example.com,https://https://bbb-01.example.com"
``` ```
Adjust the CORS setting in `/etc/default/bbb-graphql-middleware`: Adjust the CORS setting in `/etc/default/bbb-graphql-middleware`:
@ -182,16 +188,24 @@ Adjust the CORS setting in `/etc/default/bbb-graphql-middleware`:
BBB_GRAPHQL_MIDDLEWARE_LISTEN_PORT=8378 BBB_GRAPHQL_MIDDLEWARE_LISTEN_PORT=8378
# If you are running a cluster proxy setup, you need to configure the Origin of # If you are running a cluster proxy setup, you need to configure the Origin of
# the frontend. See https://docs.bigbluebutton.org/administration/cluster-proxy # the frontend. See https://docs.bigbluebutton.org/administration/cluster-proxy
BBB_GRAPHQL_MIDDLEWARE_ORIGIN=bbb-proxy.example.org BBB_GRAPHQL_MIDDLEWARE_ORIGIN=bbb-proxy.example.com
``` ```
Pay attention that this one is without protocol, just the hostname. Pay attention that this one is without protocol, just the hostname.
Adjust the CORS setting in `/etc/default/bbb-graphql-server`:
Restart BigBlueButton:
```shell ```shell
$ bbb-conf --restart HASURA_GRAPHQL_CORS_DOMAIN="https://bbb-proxy.example.com"
```
This one includes the protocol.
Reload systemd and restart BigBlueButton:
```shell
# systemctl daemon-reload
# bbb-conf --restart
``` ```
Now, opening a new session should show Now, opening a new session should show