From 092befda38602e000a87b6b1f47d552567ecea95 Mon Sep 17 00:00:00 2001 From: maximumultraist Date: Thu, 14 May 2026 16:09:43 -0400 Subject: [PATCH 1/2] First pass at removing static IPs in services --- compose.yml | 102 ++- mod/apps-akka/Dockerfile | 43 ++ mod/apps-akka/entrypoint.sh | 17 + mod/apps-akka/logback.xml | 18 + mod/base-java/Dockerfile | 24 + mod/bbb-apps-akka.conf | 25 + mod/bbb-export-annotations/Dockerfile | 23 + .../config/settings.json | 40 + mod/bbb-exporter.nginx | 4 + mod/bbb-graphql-actions/Dockerfile | 34 + mod/bbb-graphql-actions/Dockerfile.dev | 16 + mod/bbb-graphql-actions/dev-entrypoint.sh | 12 + mod/bbb-graphql-middleware/Dockerfile | 12 + mod/bbb-graphql-middleware/Dockerfile.dev | 8 + mod/bbb-graphql-middleware/config.yml | 15 + mod/bbb-graphql-server/Dockerfile | 25 + mod/bbb-graphql-server/config.yaml | 7 + mod/bbb-graphql-server/entrypoint.sh | 27 + mod/bbb-graphql-server/start.sh | 39 + mod/bbb-html5.dev.nginx | 23 + mod/bbb-pads/Dockerfile | 18 + mod/bbb-pads/entrypoint.sh | 15 + mod/bbb-resync-freeswitch | 5 + mod/bbb-web/Dockerfile | 60 ++ mod/bbb-web/bbb-web.properties | 24 + mod/bbb-web/entrypoint.sh | 31 + mod/bbb-web/logback.xml | 28 + mod/bbb-web/office-convert.sh | 36 + mod/bbb-web/run-in-systemd.sh | 10 + mod/bbb-web/turn-stun-servers.xml | 59 ++ mod/bbb-webrtc-recorder/Dockerfile | 40 + mod/coturn/turnserver.conf | 28 + mod/etherpad/Dockerfile | 29 + mod/etherpad/entrypoint.sh | 3 + mod/etherpad/etherpad-export.sh | 9 + mod/etherpad/settings.json | 723 ++++++++++++++++++ mod/freeswitch/Dockerfile | 71 ++ .../autoload_configs/conference.conf.xml.tmpl | 285 +++++++ .../autoload_configs/event_socket.conf.xml | 10 + .../conf/autoload_configs/modules.conf.xml | 32 + .../conf/dialplan/public/dialin.xml | 31 + .../conf/sip_profiles/external-dialin.xml | 86 +++ mod/freeswitch/conf/sip_profiles/external.xml | 115 +++ mod/freeswitch/conf/vars.xml.tmpl | 453 +++++++++++ mod/freeswitch/entrypoint.sh | 72 ++ mod/fsesl-akka/Dockerfile | 30 + mod/fsesl-akka/bbb-fsesl-akka.conf | 18 + mod/fsesl-akka/entrypoint.sh | 9 + mod/fsesl-akka/logback.xml | 17 + mod/haproxy.cfg | 80 ++ mod/haproxy/Dockerfile | 4 + mod/haproxy/bootstrap.sh | 30 + mod/haproxy/protocolmap | 52 ++ mod/html5-dev/Dockerfile | 13 + mod/html5-dev/entrypoint.sh | 11 + mod/initdb.sh | 21 + mod/livekit/livekit.yaml | 15 + mod/nginx.conf | 53 ++ mod/nginx/Dockerfile | 49 ++ .../bbb-graphql-client-settings-cache.conf | 1 + mod/nginx/bbb/bbb-html5.nginx | 21 + mod/nginx/bbb/demo.nginx | 26 + mod/nginx/bbb/graphql.nginx | 71 ++ mod/nginx/bbb/learning-dashboard.nginx | 4 + mod/nginx/bbb/livekit.nginx | 11 + mod/nginx/bbb/loadbalancer.nginx | 4 + mod/nginx/bbb/notes-playback.nginx | 4 + mod/nginx/bbb/notes.nginx | 77 ++ mod/nginx/bbb/playback-video.nginx | 21 + mod/nginx/bbb/podcast.nginx | 22 + mod/nginx/bbb/presentation-slides.nginx | 46 ++ mod/nginx/bbb/presentation.nginx | 28 + mod/nginx/bbb/recording-screenshare.nginx | 22 + mod/nginx/bbb/slides.nginx | 28 + mod/nginx/bbb/web.nginx | 186 +++++ mod/nginx/bigbluebutton | 85 ++ mod/nginx/default.pdf | Bin 0 -> 42107 bytes mod/periodic/Dockerfile | 10 + mod/periodic/bbb-remove-old-recordings | 41 + mod/periodic/entrypoint.sh | 23 + mod/recordings/Dockerfile | 126 +++ mod/recordings/bbb-web.properties | 2 + mod/recordings/entrypoint.sh | 16 + mod/recordings/recording.yml | 4 + mod/recordings/supervisord.conf | 34 + mod/webhooks.nginx | 9 + mod/webhooks/Dockerfile | 25 + mod/webhooks/entrypoint.sh | 17 + mod/webrtc-sfu-config.yaml | 32 + mod/webrtc-sfu.nginx | 25 + mod/webrtc-sfu/Dockerfile | 42 + 91 files changed, 4230 insertions(+), 22 deletions(-) create mode 100644 mod/apps-akka/Dockerfile create mode 100644 mod/apps-akka/entrypoint.sh create mode 100644 mod/apps-akka/logback.xml create mode 100644 mod/base-java/Dockerfile create mode 100644 mod/bbb-apps-akka.conf create mode 100644 mod/bbb-export-annotations/Dockerfile create mode 100644 mod/bbb-export-annotations/config/settings.json create mode 100644 mod/bbb-exporter.nginx create mode 100644 mod/bbb-graphql-actions/Dockerfile create mode 100644 mod/bbb-graphql-actions/Dockerfile.dev create mode 100644 mod/bbb-graphql-actions/dev-entrypoint.sh create mode 100644 mod/bbb-graphql-middleware/Dockerfile create mode 100644 mod/bbb-graphql-middleware/Dockerfile.dev create mode 100644 mod/bbb-graphql-middleware/config.yml create mode 100644 mod/bbb-graphql-server/Dockerfile create mode 100644 mod/bbb-graphql-server/config.yaml create mode 100644 mod/bbb-graphql-server/entrypoint.sh create mode 100644 mod/bbb-graphql-server/start.sh create mode 100644 mod/bbb-html5.dev.nginx create mode 100644 mod/bbb-pads/Dockerfile create mode 100644 mod/bbb-pads/entrypoint.sh create mode 100644 mod/bbb-resync-freeswitch create mode 100644 mod/bbb-web/Dockerfile create mode 100644 mod/bbb-web/bbb-web.properties create mode 100644 mod/bbb-web/entrypoint.sh create mode 100644 mod/bbb-web/logback.xml create mode 100644 mod/bbb-web/office-convert.sh create mode 100644 mod/bbb-web/run-in-systemd.sh create mode 100644 mod/bbb-web/turn-stun-servers.xml create mode 100644 mod/bbb-webrtc-recorder/Dockerfile create mode 100644 mod/coturn/turnserver.conf create mode 100644 mod/etherpad/Dockerfile create mode 100644 mod/etherpad/entrypoint.sh create mode 100644 mod/etherpad/etherpad-export.sh create mode 100644 mod/etherpad/settings.json create mode 100644 mod/freeswitch/Dockerfile create mode 100644 mod/freeswitch/conf/autoload_configs/conference.conf.xml.tmpl create mode 100644 mod/freeswitch/conf/autoload_configs/event_socket.conf.xml create mode 100644 mod/freeswitch/conf/autoload_configs/modules.conf.xml create mode 100644 mod/freeswitch/conf/dialplan/public/dialin.xml create mode 100644 mod/freeswitch/conf/sip_profiles/external-dialin.xml create mode 100644 mod/freeswitch/conf/sip_profiles/external.xml create mode 100644 mod/freeswitch/conf/vars.xml.tmpl create mode 100644 mod/freeswitch/entrypoint.sh create mode 100644 mod/fsesl-akka/Dockerfile create mode 100644 mod/fsesl-akka/bbb-fsesl-akka.conf create mode 100644 mod/fsesl-akka/entrypoint.sh create mode 100644 mod/fsesl-akka/logback.xml create mode 100644 mod/haproxy.cfg create mode 100644 mod/haproxy/Dockerfile create mode 100644 mod/haproxy/bootstrap.sh create mode 100644 mod/haproxy/protocolmap create mode 100644 mod/html5-dev/Dockerfile create mode 100644 mod/html5-dev/entrypoint.sh create mode 100644 mod/initdb.sh create mode 100644 mod/livekit/livekit.yaml create mode 100644 mod/nginx.conf create mode 100644 mod/nginx/Dockerfile create mode 100644 mod/nginx/bbb-graphql-client-settings-cache.conf create mode 100644 mod/nginx/bbb/bbb-html5.nginx create mode 100644 mod/nginx/bbb/demo.nginx create mode 100644 mod/nginx/bbb/graphql.nginx create mode 100644 mod/nginx/bbb/learning-dashboard.nginx create mode 100644 mod/nginx/bbb/livekit.nginx create mode 100644 mod/nginx/bbb/loadbalancer.nginx create mode 100644 mod/nginx/bbb/notes-playback.nginx create mode 100644 mod/nginx/bbb/notes.nginx create mode 100644 mod/nginx/bbb/playback-video.nginx create mode 100644 mod/nginx/bbb/podcast.nginx create mode 100644 mod/nginx/bbb/presentation-slides.nginx create mode 100644 mod/nginx/bbb/presentation.nginx create mode 100644 mod/nginx/bbb/recording-screenshare.nginx create mode 100644 mod/nginx/bbb/slides.nginx create mode 100644 mod/nginx/bbb/web.nginx create mode 100644 mod/nginx/bigbluebutton create mode 100644 mod/nginx/default.pdf create mode 100644 mod/periodic/Dockerfile create mode 100644 mod/periodic/bbb-remove-old-recordings create mode 100644 mod/periodic/entrypoint.sh create mode 100644 mod/recordings/Dockerfile create mode 100644 mod/recordings/bbb-web.properties create mode 100644 mod/recordings/entrypoint.sh create mode 100644 mod/recordings/recording.yml create mode 100644 mod/recordings/supervisord.conf create mode 100644 mod/webhooks.nginx create mode 100644 mod/webhooks/Dockerfile create mode 100644 mod/webhooks/entrypoint.sh create mode 100644 mod/webrtc-sfu-config.yaml create mode 100644 mod/webrtc-sfu.nginx create mode 100644 mod/webrtc-sfu/Dockerfile diff --git a/compose.yml b/compose.yml index 118feb2..409509b 100644 --- a/compose.yml +++ b/compose.yml @@ -3,7 +3,7 @@ version: '3.8' services: bbb-web: - image: alangecker/bbb-docker-web:v3.0.4 + image: alangecker/bbb-docker-web:v3.0.19 depends_on: - redis - etherpad @@ -43,7 +43,7 @@ services: # freeswitch (audio input streaming and telco dial-in support) freeswitch: - image: alangecker/bbb-docker-freeswitch:v1.10.12-v3.0.4 + image: alangecker/bbb-docker-freeswitch:v1.10.12-v3.0.19 cap_add: - IPC_LOCK - NET_ADMIN @@ -86,16 +86,43 @@ services: # nginx (webserver gateway for a bunch of things) nginx: - image: alangecker/bbb-docker-nginx:v3.0.4-v5.3.1-1.25 + image: alangecker/bbb-docker-nginx:v3.0.19-v5.4.3-1.29 volumes: - bigbluebutton:/var/bigbluebutton - ${DEFAULT_PRESENTATION:-/dev/null}:/www/default.pdf tmpfs: - /tmp + configs: + - source: nginx_conf + target: /etc/nginx/nginx.conf + mode: 0555 + - source: bbb_exporter_nginx_conf + target: /etc/nginx/bbb/bbb-exporter.nginx + mode: 0555 + - source: bbb_html5_dev_nginx_conf + target: /etc/nginx/bbb/bbb-html5.dev.nginx + mode: 0555 + - source: webhooks_nginx_conf + target: /etc/nginx/bbb/webhooks.nginx + mode: 0555 + - source: webrtc_sfu_nginx_conf + target: /etc/nginx/bbb/webrtc-sfu.nginx + mode: 0555 networks: bbb-net: aliases: - nginx + deploy: + labels: + - "traefik.enable=true" + - "traefik.docker.network=proxy" + - "traefik.http.services.${STACK_NAME}.loadbalancer.server.port=80" + - "traefik.http.routers.${STACK_NAME}.rule=Host(`${DOMAIN}`${EXTRA_DOMAINS})" + - "traefik.http.routers.${STACK_NAME}.tls.certresolver=${LETS_ENCRYPT_ENV}" + - "traefik.http.routers.${STACK_NAME}.entrypoints=web-secure" + - "traefik.http.routers.${STACK_NAME}.middlewares=${STACK_NAME}-redirect" + - "traefik.http.middlewares.${STACK_NAME}-redirect.redirectscheme.scheme=https" + - "traefik.http.middlewares.${STACK_NAME}-redirect.redirectscheme.permanent=true" # extra_hosts: # - "host.docker.internal:10.7.7.1" # - "bbb-web:10.7.7.2" @@ -107,7 +134,7 @@ services: # etherpad (collaborative notepad) etherpad: - image: alangecker/bbb-docker-etherpad:2.2.7-s8328b77-p88f3f6b + image: alangecker/bbb-docker-etherpad:2.4.2-s8328b77-p88f3f6b depends_on: - redis - collabora @@ -127,7 +154,7 @@ services: # bbb-pads (bbb etherpad integration) bbb-pads: - image: alangecker/bbb-docker-pads:v1.5.3 + image: alangecker/bbb-docker-pads:v1.5.6 depends_on: - redis - etherpad @@ -147,7 +174,7 @@ services: # redis (caching) redis: - image: redis:7.2-alpine + image: redis:8.4-alpine #healthcheck: # test: ["CMD", "redis-cli", "ping"] # interval: 1s @@ -159,7 +186,7 @@ services: - redis bbb-export-annotations: - image: alangecker/bbb-docker-bbb-export-annotations:v3.0.4 + image: alangecker/bbb-docker-bbb-export-annotations:v3.0.19 depends_on: - redis - etherpad @@ -179,7 +206,7 @@ services: # webrtc-sfu (webrtc controller for negotiation and media stream) webrtc-sfu: - image: alangecker/bbb-docker-webrtc-sfu:v2.17.0-beta.6 + image: alangecker/bbb-docker-webrtc-sfu:v2.21.2 depends_on: - redis - freeswitch @@ -190,6 +217,9 @@ services: - source: entrypoint_webrtcsfu target: /usr/local/bin/docker-entrypoint.sh mode: 0555 + - source: webrtc_sfu_conf + target: /etc/bigbluebutton/bbb-webrtc-sfu/production.yml + mode: 0555 entrypoint: /usr/local/bin/docker-entrypoint.sh volumes: - vol-mediasoup:/var/mediasoup @@ -206,7 +236,7 @@ services: # fsesl-akka (bbb freeswitch integration) fsesl-akka: - image: alangecker/bbb-docker-fsesl-akka:v3.0.4 + image: alangecker/bbb-docker-fsesl-akka:v3.0.19 depends_on: - redis - freeswitch @@ -226,7 +256,7 @@ services: # apps-akka (seems to be an bbb app integration endpoint) apps-akka: - image: alangecker/bbb-docker-apps-akka:v3.0.4 + image: alangecker/bbb-docker-apps-akka:v3.0.19 depends_on: - redis environment: @@ -241,6 +271,9 @@ services: - source: bbbhtml5_conf target: /etc/bigbluebutton/bbb-html5.yml mode: 0555 + - source: apps_akka_conf + target: /etc/bigbluebutton/mod/bbb-apps-akka.conf.tmpl + mode: 0555 secrets: - shared_secret - postgres_password @@ -251,7 +284,7 @@ services: - apps-akka bbb-graphql-server: - image: alangecker/bbb-docker-graphql-server:v3.0.4 + image: alangecker/bbb-docker-graphql-server:v3.0.19 depends_on: - postgres - bbb-web @@ -267,7 +300,7 @@ services: - bbb-graphql-server bbb-graphql-actions: - image: alangecker/bbb-docker-graphql-actions:v3.0.4 + image: alangecker/bbb-docker-graphql-actions:v3.0.19 depends_on: - redis - apps-akka @@ -277,7 +310,7 @@ services: - bbb-graphql-actions bbb-graphql-middleware: - image: alangecker/bbb-docker-graphql-middleware:v3.0.4 + image: alangecker/bbb-docker-graphql-middleware:v3.0.19 depends_on: - bbb-graphql-server - bbb-graphql-actions @@ -323,7 +356,7 @@ services: - periodic bbb-webrtc-recorder: - image: alangecker/bbb-docker-webrtc-recorder:v0.8.0 + image: alangecker/bbb-docker-webrtc-recorder:v0.13.1 depends_on: - redis volumes: @@ -339,7 +372,7 @@ services: # coturn coturn: - image: coturn/coturn:4.6-alpine + image: coturn/coturn:4.8-alpine command: - "--external-ip=${EXTERNAL_IPv4}/${EXTERNAL_IPv4}" - "--external-ip=${EXTERNAL_IPv6:-::1}/${EXTERNAL_IPv6:-::1}" @@ -363,7 +396,7 @@ services: # greenlight greenlight: - image: bigbluebutton/greenlight:v3.5.0 + image: bigbluebutton/greenlight:v3.6.3 depends_on: - postgres - redis @@ -394,16 +427,17 @@ services: postgres: image: postgres:16-alpine environment: - POSTGRES_DB: greenlight-v3 + POSTGRES_MULTIPLE_DATABASES: bbb_graphql,hasura_app,greenlight POSTGRES_USER: postgres POSTGRES_PASSWORD_FILE: /run/secret/postgres_password - #healthcheck: - #test: ["CMD-SHELL", "pg_isready -U postgres"] - #interval: 10s - #timeout: 5s - #retries: 5 + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 10s + timeout: 5s + retries: 5 volumes: - "postgres_data:/var/lib/postgresql/data" + - ./mod/initdb.sh:/docker-entrypoint-initdb.d/initdb.sh secrets: - postgres_password networks: @@ -467,6 +501,30 @@ configs: entrypoint_bbbweb: name: ${STACK_NAME}_entrypoint_bbbweb_${ENTRYPOINT_BBBWEB_VERSION} file: ./entrypoint.bbb-web.sh + apps_akka_conf: + name: ${STACK_NAME}_appsakka_conf_${APPSAKKA_CONF_VERSION} + file: ./mod/bbb-apps-akka.conf + bbb_exporter_nginx_conf: + name: ${STACK_NAME}_bbb_exporter_conf_${BBB_EXPORTER_CONF_VERSION} + file: ./mod/bbb-exporter.nginx + bbb_html5_dev_nginx_conf: + name: ${STACK_NAME}_bbb_html5_nginx_conf_${BBB_HTML5_NGINX_CONF_VERSION} + file: ./mod/bbb-html5.dev.nginx + bbb_resync_freeswitch_conf: + name: ${STACK_NAME}_bbb_resync_freeswitch_conf_${BBB_RESYNC_FREESWITCH_CONF_VERSION} + file: ./mod/bbb-resync-freeswitch + nginx_conf: + name: ${STACK_NAME}_nginx_conf_${NGINX_CONF_VERSION} + file: ./mod/nginx.conf + webhooks_nginx_conf: + name: ${STACK_NAME}_webhooks_nginx_conf_${WEBHOOKS_NGINX_CONF_VERSION} + file: ./mod/webhooks.nginx + webrtc_sfu_nginx_conf: + name: ${STACK_NAME}_webrtc_sfu_nginx_conf_${WEBRTC_SFU_NGINX_CONF_VERSION} + file: ./mod/webrtc-sfu.nginx + webrtc_sfu_conf: + name: ${STACK_NAME}_webrtc_sfu_conf_${WEBRTC_SFU_CONF_VERSION} + file: ./mod/webrtc-sfu-config.yaml secrets: postgres_password: diff --git a/mod/apps-akka/Dockerfile b/mod/apps-akka/Dockerfile new file mode 100644 index 0000000..6d4bf57 --- /dev/null +++ b/mod/apps-akka/Dockerfile @@ -0,0 +1,43 @@ +ARG BBB_BUILD_TAG +FROM bigbluebutton/bbb-build:$BBB_BUILD_TAG AS builder + +COPY --from=src-common-message / /bbb-common-message + +# build bbb-common-message +RUN cd /bbb-common-message && ./deploy.sh + +# =================================================== + +ARG TAG_APPS_AKKA + +COPY --from=src-apps-akka / /source + +# compile and unzip bin +RUN cd /source \ + && sbt universal:packageBin \ + && unzip /source/target/universal/bbb-apps-akka-0.0.4.zip -d / + +# =================================================== + + +FROM bigbluebutton/bbb-build:$BBB_BUILD_TAG AS builder-settings +RUN wget -qO /usr/local/bin/yq https://github.com/mikefarah/yq/releases/download/v4.44.3/yq_linux_amd64 && chmod a+x /usr/local/bin/yq +COPY --from=src-config /settings.yml /settings.yml +ARG TAG_BBB +RUN yq e -i ".public.app.bbbServerVersion = \"$TAG_BBB\"" /settings.yml +RUN yq e -i ".public.app.html5ClientBuild = \"$TAG_BBB\"" /settings.yml + + +# =================================================== + +FROM alangecker/bbb-docker-base-java + +COPY --from=builder-settings /usr/local/bin/yq /usr/local/bin/yq +COPY --from=builder /bbb-apps-akka-0.0.4 /bbb-apps-akka +COPY bbb-apps-akka.conf /etc/bigbluebutton/bbb-apps-akka.conf.tmpl +COPY logback.xml /bbb-apps-akka/conf/logback.xml +COPY entrypoint.sh /entrypoint.sh +COPY --from=builder-settings --chown=bigbluebutton:bigbluebutton /settings.yml /usr/share/bigbluebutton/html5-client/private/config/settings.yml + +USER bigbluebutton +ENTRYPOINT /entrypoint.sh diff --git a/mod/apps-akka/entrypoint.sh b/mod/apps-akka/entrypoint.sh new file mode 100644 index 0000000..033429e --- /dev/null +++ b/mod/apps-akka/entrypoint.sh @@ -0,0 +1,17 @@ +#!/bin/sh -e + +# bbb-apps-akka.conf +TARGET=/etc/bigbluebutton/bbb-apps-akka.conf +cp /etc/bigbluebutton/bbb-apps-akka.conf.tmpl $TARGET +sed -i "s/DOMAIN/$DOMAIN/" $TARGET +sed -i "s/SHARED_SECRET/$SHARED_SECRET/" $TARGET +sed -i "s/POSTGRES_PASSWORD/$POSTGRES_PASSWORD/" $TARGET + + +# settings.yml +TARGET=/usr/share/bigbluebutton/html5-client/private/config/settings.yml +yq e -i ".public.kurento.wsUrl = \"wss://$DOMAIN/bbb-webrtc-sfu\"" $TARGET +yq e -i ".public.pads.url = \"https://$DOMAIN/pad\"" $TARGET + +cd /bbb-apps-akka +/bbb-apps-akka/bin/bbb-apps-akka \ No newline at end of file diff --git a/mod/apps-akka/logback.xml b/mod/apps-akka/logback.xml new file mode 100644 index 0000000..c29b1fb --- /dev/null +++ b/mod/apps-akka/logback.xml @@ -0,0 +1,18 @@ + + + + + %d{"yyyy-MM-dd'T'HH:mm:ss.SSSXXX"} %-5level %logger{35} - %msg%n + + + + + + + + + + + + + diff --git a/mod/base-java/Dockerfile b/mod/base-java/Dockerfile new file mode 100644 index 0000000..810feee --- /dev/null +++ b/mod/base-java/Dockerfile @@ -0,0 +1,24 @@ +FROM eclipse-temurin:17-jre-jammy + +RUN apt-get update && apt-get install -y \ + wget unzip gosu locales \ + imagemagick xpdf-utils curl \ + && sed -i '/en_US.UTF-8/s/^# //g' /etc/locale.gen && locale-gen + +ENV LANG en_US.UTF-8 +ENV LANGUAGE en_US:en +ENV LC_ALL en_US.UTF-8 + +# add user & group +RUN groupadd -g 998 bigbluebutton \ + && useradd -m -u 998 -g bigbluebutton bigbluebutton \ + && mkdir /etc/bigbluebutton \ + && chown bigbluebutton:bigbluebutton /etc/bigbluebutton + +# add dockerize +ENV DOCKERIZE_VERSION v0.7.0 +RUN wget -q https://github.com/jwilder/dockerize/releases/download/$DOCKERIZE_VERSION/dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz \ + && tar -C /usr/local/bin -xzvf dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz \ + && rm dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz + + diff --git a/mod/bbb-apps-akka.conf b/mod/bbb-apps-akka.conf new file mode 100644 index 0000000..ae21ad1 --- /dev/null +++ b/mod/bbb-apps-akka.conf @@ -0,0 +1,25 @@ +// include default config from upstream +include "/bbb-apps-akka/conf/application.conf" + +redis { + host="redis" +} + +services { + bbbWebAPI="https://DOMAIN/bigbluebutton/api" + sharedSecret="SHARED_SECRET" + graphqlMiddlewareAPI = "http://bbb-graphql-middleware:8378" +} +http { + interface = "0.0.0.0" +} + +postgres { + properties = { + serverName = "postgres" + portNumber = "5432" + databaseName = "bbb_graphql" + user = "postgres" + password = "POSTGRES_PASSWORD" + } +} \ No newline at end of file diff --git a/mod/bbb-export-annotations/Dockerfile b/mod/bbb-export-annotations/Dockerfile new file mode 100644 index 0000000..d6c591f --- /dev/null +++ b/mod/bbb-export-annotations/Dockerfile @@ -0,0 +1,23 @@ +FROM node:22-bookworm-slim AS builder + +COPY --from=src / /bbb-export-annotations +RUN cd /bbb-export-annotations && npm ci && npm install + +# -------------------- + +FROM node:22-bookworm-slim + +RUN groupadd -g 998 bigbluebutton \ + && useradd -m -u 998 -g bigbluebutton bigbluebutton + +RUN apt update && apt install -y \ + nodejs npm cairosvg ghostscript imagemagick nodejs poppler-utils + +COPY --from=builder /bbb-export-annotations /bbb-export-annotations +COPY ./config/settings.json /bbb-export-annotations/config/settings.json + +USER bigbluebutton +WORKDIR /bbb-export-annotations +ENV NODE_ENV=production + +ENTRYPOINT npm start diff --git a/mod/bbb-export-annotations/config/settings.json b/mod/bbb-export-annotations/config/settings.json new file mode 100644 index 0000000..294c3be --- /dev/null +++ b/mod/bbb-export-annotations/config/settings.json @@ -0,0 +1,40 @@ +{ + "log": { + "level": "info", + "msgName": "PresAnnStatusMsg" + }, + "shared": { + "presAnnDropboxDir": "/tmp/pres-ann-dropbox", + "cairosvg": "/usr/bin/cairosvg", + "ghostscript": "/usr/bin/gs" + }, + "process": { + "maxImageWidth": 1440, + "maxImageHeight": 1080, + "pointsPerInch": 72, + "pixelsPerInch": 96, + "cairoSVGUnsafeFlag": false + }, + "notifier": { + "pod_id": "DEFAULT_PRESENTATION_POD", + "is_downloadable": "false", + "msgName": "NewPresFileAvailableMsg" + }, + "bbbWebAPI": "http://bbb-web:8090", + "bbbPadsAPI": "http://bbb-pads:9002", + "redis": { + "host": "redis", + "port": 6379, + "password": null, + "channels": { + "queue": "exportJobs", + "publish": "to-akka-apps-redis-channel" + } + }, + "fonts": { + "draw": "/usr/local/share/fonts/Shantell_Sans-Tldrawish.woff2", + "sans": "/usr/local/share/fonts/IBMPlexSans-Medium.woff2", + "serif": "/usr/local/share/fonts/IBMPlexSerif-Medium.woff2", + "mono": "/usr/local/share/fonts/IBMPlexMono-Medium.woff2" + } +} \ No newline at end of file diff --git a/mod/bbb-exporter.nginx b/mod/bbb-exporter.nginx new file mode 100644 index 0000000..7b1dfaa --- /dev/null +++ b/mod/bbb-exporter.nginx @@ -0,0 +1,4 @@ +location /bbb-exporter { + proxy_pass http://prometheus-exporter:9688; + proxy_http_version 1.1; +} \ No newline at end of file diff --git a/mod/bbb-graphql-actions/Dockerfile b/mod/bbb-graphql-actions/Dockerfile new file mode 100644 index 0000000..66ac9b8 --- /dev/null +++ b/mod/bbb-graphql-actions/Dockerfile @@ -0,0 +1,34 @@ +ARG BBB_BUILD_TAG +FROM bigbluebutton/bbb-build:$BBB_BUILD_TAG AS builder + +COPY --from=src ./ /src +RUN cd /src && \ + npm ci --no-progress && \ + npm run build + +# delete node_modules (it should create a fresh one inside /src/dist/) +RUN rm -rf /src/node_modules + +RUN cd /src/dist && \ + mv index.js bbb-graphql-actions.js && \ + cp ../package.json ../package-lock.json . && \ + npm ci --no-progress --omit=dev + + +# ------------------------------ +FROM node:22-bookworm-slim + +RUN groupadd -g 2062 app \ + && useradd -m -u 2063 -g app app + +USER app + +WORKDIR /app + +ENV SERVER_HOST 0.0.0.0 +ENV BBB_REDIS_HOST redis +ENV NODE_ENV=production + +COPY --from=builder /src/dist /app + +CMD [ "node", "/app/bbb-graphql-actions.js" ] \ No newline at end of file diff --git a/mod/bbb-graphql-actions/Dockerfile.dev b/mod/bbb-graphql-actions/Dockerfile.dev new file mode 100644 index 0000000..22babbb --- /dev/null +++ b/mod/bbb-graphql-actions/Dockerfile.dev @@ -0,0 +1,16 @@ +ARG BBB_BUILD_TAG +FROM bigbluebutton/bbb-build:$BBB_BUILD_TAG AS builder + +RUN apt-get update && apt-get install -y gosu + +# allow any user to use node in /root/.nvm +RUN chmod 755 /root + +COPY dev-entrypoint.sh /dev-entrypoint.sh +ENTRYPOINT [ "/dev-entrypoint.sh" ] + +WORKDIR /app +ENV SERVER_HOST 0.0.0.0 +ENV BBB_REDIS_HOST redis + +CMD [ "npm install && npm start" ] \ No newline at end of file diff --git a/mod/bbb-graphql-actions/dev-entrypoint.sh b/mod/bbb-graphql-actions/dev-entrypoint.sh new file mode 100644 index 0000000..7c3700c --- /dev/null +++ b/mod/bbb-graphql-actions/dev-entrypoint.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +# get owner of /app +OWNER="$(stat -c '%u' "/app")" +GROUP="$(stat -c '%g' "/app")" +useradd --home-dir /tmp -u $OWNER user || /bin/true + +# run with same user to avoid any issues +# with file permissions +. /root/.nvm/nvm.sh +gosu $OWNER:$GROUP bash -c "$@" + diff --git a/mod/bbb-graphql-middleware/Dockerfile b/mod/bbb-graphql-middleware/Dockerfile new file mode 100644 index 0000000..763ee1a --- /dev/null +++ b/mod/bbb-graphql-middleware/Dockerfile @@ -0,0 +1,12 @@ +ARG BBB_BUILD_TAG +FROM bigbluebutton/bbb-build:$BBB_BUILD_TAG AS builder + +COPY --from=src / /src/ +RUN cd /src/ && CGO_ENABLED=0 go build -o bbb-graphql-middleware cmd/bbb-graphql-middleware/main.go + +# ------------------------------ +FROM alpine +COPY --from=builder /src/bbb-graphql-middleware /app/bbb-graphql-middleware +COPY --from=builder /src/config/config.yml /usr/share/bbb-graphql-middleware/config.yml +COPY config.yml /etc/bigbluebutton/bbb-graphql-middleware.yml +CMD [ "/app/bbb-graphql-middleware" ] \ No newline at end of file diff --git a/mod/bbb-graphql-middleware/Dockerfile.dev b/mod/bbb-graphql-middleware/Dockerfile.dev new file mode 100644 index 0000000..d1952d4 --- /dev/null +++ b/mod/bbb-graphql-middleware/Dockerfile.dev @@ -0,0 +1,8 @@ +ARG BBB_BUILD_TAG +FROM bigbluebutton/bbb-build:$BBB_BUILD_TAG AS builder + +WORKDIR /app + +ENV GOPATH /gopath + +CMD ["go", "run", "cmd/bbb-graphql-middleware/main.go", "--signal", "SIGTERM"] \ No newline at end of file diff --git a/mod/bbb-graphql-middleware/config.yml b/mod/bbb-graphql-middleware/config.yml new file mode 100644 index 0000000..f323157 --- /dev/null +++ b/mod/bbb-graphql-middleware/config.yml @@ -0,0 +1,15 @@ +server: + listen_host: 0.0.0.0 + listen_port: 8378 +redis: + host: redis + port: 6379 + password: "" +hasura: + url: ws://nginx:8185/v1/graphql +graphql-actions: + url: http://bbb-graphql-actions:8093 +auth_hook: + url: http://bbb-web:8090/bigbluebutton/connection/checkGraphqlAuthorization +session_vars_hook: + url: http://apps-akka:8901/userInfo \ No newline at end of file diff --git a/mod/bbb-graphql-server/Dockerfile b/mod/bbb-graphql-server/Dockerfile new file mode 100644 index 0000000..3c087cc --- /dev/null +++ b/mod/bbb-graphql-server/Dockerfile @@ -0,0 +1,25 @@ +ARG BBB_BUILD_TAG +ARG GRAPHQL_ENGINE_TAG +FROM bigbluebutton/bbb-build:$BBB_BUILD_TAG AS builder + +RUN curl -L https://github.com/hasura/graphql-engine/raw/stable/cli/get.sh | INSTALL_PATH=/usr/local/bin VERSION=v2.44.0 bash +RUN wget -qO /usr/local/bin/yq https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 && chmod a+x /usr/local/bin/yq + +# ---------------------------- +FROM hasura/graphql-engine:$GRAPHQL_ENGINE_TAG + +# install netstat, required for start script +RUN apt-get update && apt-get install -y net-tools gosu + +COPY --from=builder /usr/local/bin/yq /usr/local/bin/yq +COPY --from=builder /usr/local/bin/hasura /usr/local/bin/hasura + +COPY --from=src /bbb_schema.sql /app/ +COPY --from=src /metadata /app/metadata + +COPY config.yaml /app/config.yaml +COPY entrypoint.sh /entrypoint.sh +COPY start.sh /app/start.sh + +ENTRYPOINT [ "/entrypoint.sh" ] +CMD [ "/app/start.sh" ] diff --git a/mod/bbb-graphql-server/config.yaml b/mod/bbb-graphql-server/config.yaml new file mode 100644 index 0000000..cd39dc2 --- /dev/null +++ b/mod/bbb-graphql-server/config.yaml @@ -0,0 +1,7 @@ +version: 3 +endpoint: http://localhost:8085 +admin_secret: bigbluebutton +metadata_directory: metadata +actions: + kind: synchronous + handler_webhook_baseurl: http://localhost:3000 diff --git a/mod/bbb-graphql-server/entrypoint.sh b/mod/bbb-graphql-server/entrypoint.sh new file mode 100644 index 0000000..f14e2f4 --- /dev/null +++ b/mod/bbb-graphql-server/entrypoint.sh @@ -0,0 +1,27 @@ +#!/bin/bash + +# for psql +export PGHOST=postgres +export PGUSER="${POSTGRES_USER}" +export PGPASSWORD="${POSTGRES_PASSWORD}" + + +# for hasura +export HASURA_GRAPHQL_DATABASE_URL=postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/hasura_app +export HASURA_GRAPHQL_METADATA_DATABASE_URL=postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/hasura_app +export HASURA_GRAPHQL_LOG_LEVEL=warn +export HASURA_GRAPHQL_ENABLE_CONSOLE=false +export HASURA_GRAPHQL_LIVE_QUERIES_MULTIPLEXED_REFETCH_INTERVAL=333 +export HASURA_GRAPHQL_LIVE_QUERIES_MULTIPLEXED_BATCH_SIZE=1000 +export HASURA_GRAPHQL_STREAMING_QUERIES_MULTIPLEXED_REFETCH_INTERVAL=150 +export HASURA_GRAPHQL_STREAMING_QUERIES_MULTIPLEXED_BATCH_SIZE=1000 +export HASURA_GRAPHQL_SERVER_PORT=8085 +export HASURA_GRAPHQL_ENABLE_TELEMETRY=false +export HASURA_GRAPHQL_WEBSOCKET_KEEPALIVE=10 +export HASURA_GRAPHQL_AUTH_HOOK=http://apps-akka:8901/userInfo +export HASURA_BBB_GRAPHQL_ACTIONS_ADAPTER_URL=http://bbb-graphql-actions:8093 + + +export HASURA_GRAPHQL_BBB_DATABASE_URL=postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/bbb_graphql + +exec $@ \ No newline at end of file diff --git a/mod/bbb-graphql-server/start.sh b/mod/bbb-graphql-server/start.sh new file mode 100644 index 0000000..5fc3354 --- /dev/null +++ b/mod/bbb-graphql-server/start.sh @@ -0,0 +1,39 @@ +#!/bin/bash +set -e + +cd /app/ + +# patch database url +# TODO: this should be possible upstream in BBB via an environment variable +yq e -i ".[1].configuration.connection_info.database_url = \"$HASURA_GRAPHQL_BBB_DATABASE_URL\"" metadata/databases/databases.yaml + +sed -i "s/^admin_secret: .*/admin_secret: $HASURA_GRAPHQL_ADMIN_SECRET/g" /app/config.yaml + +echo "SELECT 'CREATE DATABASE hasura_app' WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'hasura_app')\gexec" | psql + +echo "Restarting database bbb_graphql" +psql -c "SELECT pg_terminate_backend(pg_stat_activity.pid) FROM pg_stat_activity WHERE datname = 'bbb_graphql'" > /dev/null +psql -c "drop database if exists bbb_graphql with (force)" +psql -c "create database bbb_graphql WITH TEMPLATE template0 LC_COLLATE 'C.UTF-8'" +psql -c "alter database bbb_graphql set timezone to 'UTC'" + +echo "Creating tables in bbb_graphql" +psql -U postgres -d bbb_graphql -q -f bbb_schema.sql --set ON_ERROR_STOP=on + +echo "Starting hasura-graphql-engine" +gosu nobody graphql-engine serve & +PID=$! + +sleep 1 + + +#Check if Hasura is ready before applying metadata +while ! netstat -tuln | grep ":$HASURA_GRAPHQL_SERVER_PORT " > /dev/null; do + echo "Waiting for Hasura's port ($HASURA_GRAPHQL_SERVER_PORT) to be ready..." + sleep 1 +done + +echo "Applying new metadata to Hasura" +/usr/local/bin/hasura metadata apply --skip-update-check + +wait "$PID" \ No newline at end of file diff --git a/mod/bbb-html5.dev.nginx b/mod/bbb-html5.dev.nginx new file mode 100644 index 0000000..5b059ad --- /dev/null +++ b/mod/bbb-html5.dev.nginx @@ -0,0 +1,23 @@ +# serve locale index from prebuilt static files +location = /html5client/locales/ { + alias /usr/share/bigbluebutton/html5-client/locales/; + autoindex on; + autoindex_format json; + + # Prevent browsers from caching + add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0"; + add_header Pragma "no-cache"; + add_header Expires 0; +} + +# running from source (npm start) +location /html5client/ { + rewrite /html5client/(.*) /$1 break; + gzip_static on; + proxy_pass http://webrtc-sfu:3000/; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "Upgrade"; + proxy_set_header Host $host; +} + diff --git a/mod/bbb-pads/Dockerfile b/mod/bbb-pads/Dockerfile new file mode 100644 index 0000000..e366366 --- /dev/null +++ b/mod/bbb-pads/Dockerfile @@ -0,0 +1,18 @@ +FROM node:22-bookworm-slim AS builder + +COPY --from=src / /bbb-pads +RUN cd /bbb-pads && npm install --production + + +RUN chmod 777 /bbb-pads/config +# ------------------------------ + +FROM node:22-bookworm-slim + +RUN apt update && apt install -y jq moreutils \ + && useradd --uid 2003 --create-home --user-group bbb-pads + +COPY --from=builder /bbb-pads /bbb-pads +USER bbb-pads +COPY entrypoint.sh /entrypoint.sh +ENTRYPOINT /entrypoint.sh \ No newline at end of file diff --git a/mod/bbb-pads/entrypoint.sh b/mod/bbb-pads/entrypoint.sh new file mode 100644 index 0000000..9f93722 --- /dev/null +++ b/mod/bbb-pads/entrypoint.sh @@ -0,0 +1,15 @@ +#!/bin/sh +set -e + +TARGET=/bbb-pads/config/settings.json + +cp /bbb-pads/config/settings.json.template $TARGET +sed -i "s/ETHERPAD_API_KEY/\"$ETHERPAD_API_KEY\"/g" $TARGET +jq '.etherpad.host = "etherpad"' $TARGET | sponge $TARGET +jq '.express.host = "0.0.0.0"' $TARGET | sponge $TARGET +jq '.redis.host = "redis"' $TARGET | sponge $TARGET + + +cd /bbb-pads +export NODE_ENV=production +npm start \ No newline at end of file diff --git a/mod/bbb-resync-freeswitch b/mod/bbb-resync-freeswitch new file mode 100644 index 0000000..8b73c06 --- /dev/null +++ b/mod/bbb-resync-freeswitch @@ -0,0 +1,5 @@ +#!/bin/bash + +# https://github.com/bigbluebutton/bigbluebutton/pull/9597/files + +docker exec -it bbb-freeswitch fs_cli -H webrtc-sfu -P 8021 -x 'fsctl sync_clock_when_idle' \ No newline at end of file diff --git a/mod/bbb-web/Dockerfile b/mod/bbb-web/Dockerfile new file mode 100644 index 0000000..5a08075 --- /dev/null +++ b/mod/bbb-web/Dockerfile @@ -0,0 +1,60 @@ +ARG BBB_BUILD_TAG +FROM bigbluebutton/bbb-build:$BBB_BUILD_TAG AS builder + +COPY --from=src-common-message / /bbb-common-message + +# build bbb-common-message +RUN cd /bbb-common-message && ./deploy.sh + +# =================================================== + + +COPY --from=src-common-web / /bbb-common-web +# build bbb-common-web +RUN cd /bbb-common-web && ./deploy.sh + +COPY --from=src-web / /bbb-web + +# compile bbb-web +RUN cd /bbb-web && sh ./gradlew assemble + +# compile pres-checker +RUN cd /bbb-web/pres-checker && gradle jar +RUN mkdir -p /bbb-web/pres-checker/lib && find /bbb-web/pres-checker && \ + cp /bbb-web/pres-checker/build/libs/bbb-pres-check-0.0.1.jar /bbb-web/pres-checker/lib + +# extract .war +RUN unzip -q /bbb-web/build/libs/bigbluebutton-0.10.0.war -d /dist + + +# =================================================== +FROM alangecker/bbb-docker-base-java + +# add blank presentation files and allow conversion to pdf/svg +RUN mkdir -p /usr/share/bigbluebutton/blank \ + && cd /usr/share/bigbluebutton/blank \ + && wget \ + https://raw.githubusercontent.com/bigbluebutton/bigbluebutton/v3.0.19/bigbluebutton-config/slides/blank-svg.svg \ + https://raw.githubusercontent.com/bigbluebutton/bigbluebutton/v3.0.19/bigbluebutton-config/slides/blank-thumb.png \ + https://raw.githubusercontent.com/bigbluebutton/bigbluebutton/v3.0.19/bigbluebutton-config/slides/blank-presentation.pdf \ + https://raw.githubusercontent.com/bigbluebutton/bigbluebutton/v3.0.19/bigbluebutton-config/slides/blank-png.png \ + && sed -i 's///g' /etc/ImageMagick-6/policy.xml \ + && sed -i '/potrace/d' /etc/ImageMagick-6/delegates.xml + + +# get bbb-web +COPY --from=builder /dist /usr/share/bbb-web + +# get pres-checker +COPY --from=builder /bbb-web/pres-checker/lib /usr/share/prescheck/lib +COPY --from=builder /bbb-web/pres-checker/run.sh /usr/share/prescheck/prescheck.sh + +# add entrypoint and templates +COPY entrypoint.sh /entrypoint.sh +COPY bbb-web.properties /etc/bigbluebutton/bbb-web.properties.tmpl +COPY turn-stun-servers.xml /etc/bigbluebutton/turn-stun-servers.xml.tmpl +COPY logback.xml /usr/share/bbb-web/WEB-INF/classes/logback.xml +COPY office-convert.sh /usr/share/bbb-libreoffice-conversion/convert.sh +COPY run-in-systemd.sh /usr/share/bbb-web/run-in-systemd.sh + +ENTRYPOINT ["/entrypoint.sh"] diff --git a/mod/bbb-web/bbb-web.properties b/mod/bbb-web/bbb-web.properties new file mode 100644 index 0000000..7c653cd --- /dev/null +++ b/mod/bbb-web/bbb-web.properties @@ -0,0 +1,24 @@ +defaultWelcomeMessage={{ .Env.WELCOME_MESSAGE }} +defaultWelcomeMessageFooter={{ .Env.WELCOME_FOOTER }} + +{{ if isTrue .Env.ENABLE_RECORDING }} +disableRecordingDefault=false +{{ else }} +disableRecordingDefault=true +{{ end }} + +bigbluebutton.web.serverURL=https://{{ .Env.DOMAIN }} + +securitySalt={{ .Env.SHARED_SECRET }} + +redisHost=redis + +{{ if isTrue .Env.IGNORE_TLS_CERT_ERRORS }} +beans.presentationService.defaultUploadedPresentation=https://raw.githubusercontent.com/bigbluebutton/bigbluebutton/v3.0.19/bigbluebutton-config/assets/default.pdf +{{else}} +beans.presentationService.defaultUploadedPresentation=${bigbluebutton.web.serverURL}/default.pdf +{{end}} + +learningDashboardEnabled={{ .Env.ENABLE_LEARNING_DASHBOARD }} + +defaultNumDigitsForTelVoice=9 \ No newline at end of file diff --git a/mod/bbb-web/entrypoint.sh b/mod/bbb-web/entrypoint.sh new file mode 100644 index 0000000..f9182f2 --- /dev/null +++ b/mod/bbb-web/entrypoint.sh @@ -0,0 +1,31 @@ +#!/bin/bash +set -e + +# create recording directory structure if it doesn't exist yet +mkdir -p /var/bigbluebutton/recording/status +mkdir -p /var/bigbluebutton/events +mkdir -p /var/bigbluebutton/recording +mkdir -p /var/bigbluebutton/recording/raw +mkdir -p /var/bigbluebutton/recording/process +mkdir -p /var/bigbluebutton/recording/publish +mkdir -p /var/bigbluebutton/recording/status/recorded +mkdir -p /var/bigbluebutton/recording/status/archived +mkdir -p /var/bigbluebutton/recording/status/processed +mkdir -p /var/bigbluebutton/recording/status/ended +mkdir -p /var/bigbluebutton/recording/status/sanity +mkdir -p /var/bigbluebutton/recording/status/published +mkdir -p /var/bigbluebutton/captions +mkdir -p /var/bigbluebutton/captions/inbox +mkdir -p /var/bigbluebutton/published +mkdir -p /var/bigbluebutton/deleted +mkdir -p /var/bigbluebutton/unpublished +mkdir -p /var/bigbluebutton/basic_stats +chown -R bigbluebutton:bigbluebutton /var/bigbluebutton + +cd /usr/share/bbb-web/ +dockerize \ + -template /etc/bigbluebutton/bbb-web.properties.tmpl:/etc/bigbluebutton/bbb-web.properties \ + -template /etc/bigbluebutton/turn-stun-servers.xml.tmpl:/etc/bigbluebutton/turn-stun-servers.xml \ + gosu bigbluebutton java -Dgrails.env=prod -Dserver.address=0.0.0.0 -Dserver.port=8090 -Dspring.main.allow-circular-references=true -Xms384m -Xmx384m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/bigbluebutton/diagnostics -cp WEB-INF/lib/*:/:WEB-INF/classes/:. org.springframework.boot.loader.launch.WarLauncher + + diff --git a/mod/bbb-web/logback.xml b/mod/bbb-web/logback.xml new file mode 100644 index 0000000..def4f59 --- /dev/null +++ b/mod/bbb-web/logback.xml @@ -0,0 +1,28 @@ + + + + + %d{"yyyy-MM-dd'T'HH:mm:ss.SSSXXX"} %-5level %logger{35} - %msg%n + + + + + + + + + + + + + + + + + + + + + + diff --git a/mod/bbb-web/office-convert.sh b/mod/bbb-web/office-convert.sh new file mode 100644 index 0000000..ee88b34 --- /dev/null +++ b/mod/bbb-web/office-convert.sh @@ -0,0 +1,36 @@ +#!/bin/bash +set -e +set -u +PATH="/bin/:/usr/bin/" + +# This script receives three params +# Param 1: Input office file path (e.g. "/tmp/test.odt") +# Param 2: Output pdf file path (e.g. "/tmp/test.pdf") +# Param 3: Destination Format (pdf default) +# Param 4: Timeout (secs) (optional) + +if (( $# == 0 )); then + echo "Missing parameter 1 (Input office file path)"; + exit 1 +elif (( $# == 1 )); then + echo "Missing parameter 2 (Output pdf file path)"; + exit 1 +fi; + + +source="$1" +dest="$2" + +# If output format is missing, define PDF +convertTo="${3:-pdf}" + +# If timeout is missing, define 60 +timeoutSecs="${4:-60}" +# Truncate timeout to max 3 digits (as expected by sudoers) +timeoutSecs="${timeoutSecs:0:3}" + +# The timeout is important. + +timeout $(printf %03d $timeoutSecs)s curl -F "data=@${source}" -k $COLLABORA_URL/convert-to/$convertTo > "${dest}" + +exit 0 diff --git a/mod/bbb-web/run-in-systemd.sh b/mod/bbb-web/run-in-systemd.sh new file mode 100644 index 0000000..79018d1 --- /dev/null +++ b/mod/bbb-web/run-in-systemd.sh @@ -0,0 +1,10 @@ +#!/bin/sh -e + +# bbb-web requires a script under /usr/share/bbb-web/run-in-systemd.sh to run certain tasks +# this is used for sandboxing, which is in our case a bit more difficult, because +# we run it without systemd in an unprivileged container, with currently no extra sandboxing capabilities + +# TODO: exploring better ways! (e. g. firejail) + +timeout_secs="$1"; shift +exec timeout "${timeout_secs}" "$@" \ No newline at end of file diff --git a/mod/bbb-web/turn-stun-servers.xml b/mod/bbb-web/turn-stun-servers.xml new file mode 100644 index 0000000..de92838 --- /dev/null +++ b/mod/bbb-web/turn-stun-servers.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + {{if and (isTrue .Env.ENABLE_HTTPS_PROXY) (not (isTrue .Env.IGNORE_TLS_CERT_ERRORS)) }} + {{/* ignore when using a self signed certificate in dev mode */}} + + + + + + {{end}} + + + {{if .Env.TURN_EXT_SERVER }} + + + + + + {{end}} + + + + + + + + + + + + {{if and (isTrue .Env.ENABLE_HTTPS_PROXY) (not (isTrue .Env.IGNORE_TLS_CERT_ERRORS)) }} + + {{end}} + + {{if .Env.TURN_EXT_SERVER }} + + {{end}} + + + + + + + + diff --git a/mod/bbb-webrtc-recorder/Dockerfile b/mod/bbb-webrtc-recorder/Dockerfile new file mode 100644 index 0000000..61fd252 --- /dev/null +++ b/mod/bbb-webrtc-recorder/Dockerfile @@ -0,0 +1,40 @@ +# Build stage +FROM golang:1.25 as builder + +ARG APP_VERSION=devel +ARG GOMOD=github.com/bigbluebutton/bbb-webrtc-recorder + +WORKDIR /app + +COPY --from=src go.* ./ + +RUN go mod tidy + +COPY --from=src . ./ + +RUN APP_VERSION=$(cat ./VERSION | sed 's/ /-/g') \ + go build -o ./build/bbb-webrtc-recorder \ + -ldflags="-X '$GOMOD/internal.AppVersion=v${APP_VERSION1}'" \ + ./cmd/bbb-webrtc-recorder + + +RUN mv /app/build/bbb-webrtc-recorder /usr/bin/bbb-webrtc-recorder + +# Running stage +FROM debian:bookworm-slim + +RUN apt-get update && apt-get install -y gosu + +# use same UID as in the recordings container +RUN groupadd -g 998 bigbluebutton && useradd -m -u 998 -g bigbluebutton bigbluebutton + +# config +ENV BBBRECORDER_PUBSUB_ADAPTERS_REDIS_ADDRESS=redis:6379 +ENV BBBRECORDER_PUBSUB_ADAPTERS_REDIS_NETWORK=tcp +ENV BBBRECORDER_DEBUG=true + +# Copy the binary to the production image from the builder stage. +COPY --from=builder /usr/bin/bbb-webrtc-recorder /usr/bin/bbb-webrtc-recorder +COPY --from=builder /app/config/bbb-webrtc-recorder.yml /etc/bbb-webrtc-recorder/bbb-webrtc-recorder.yml + +CMD ["/bin/sh", "-c", "chown -R bigbluebutton:bigbluebutton /var/lib/bbb-webrtc-recorder && gosu bigbluebutton /usr/bin/bbb-webrtc-recorder"] \ No newline at end of file diff --git a/mod/coturn/turnserver.conf b/mod/coturn/turnserver.conf new file mode 100644 index 0000000..55cdf9d --- /dev/null +++ b/mod/coturn/turnserver.conf @@ -0,0 +1,28 @@ +listening-port=3478 + +# listening-ip=${INTERNAL_IP:-$IP} +# relay-ip=${INTERNAL_IP:-$IP} + +min-port=32769 +max-port=65535 +# verbose + +fingerprint +lt-cred-mech +use-auth-secret +realm=bbb-docker + +keep-address-family + +no-cli +no-tlsv1 +no-tlsv1_1 + +# Block connections to IP ranges which shouldn't be reachable +no-loopback-peers +no-multicast-peers + + +# we only need to allow peer connections from the machine itself (from mediasoup or freeswitch). +denied-peer-ip=0.0.0.0-255.255.255.255 +denied-peer-ip=::-ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff \ No newline at end of file diff --git a/mod/etherpad/Dockerfile b/mod/etherpad/Dockerfile new file mode 100644 index 0000000..1633561 --- /dev/null +++ b/mod/etherpad/Dockerfile @@ -0,0 +1,29 @@ +ARG TAG_ETHERPAD +FROM etherpad/etherpad:$TAG_ETHERPAD + +USER root + +RUN apk add git curl + +USER etherpad + +RUN pnpm run plugins i \ + ep_disable_chat@0.0.13 \ + ep_auth_session@1.1.1 \ + --github \ + mconf/ep_cursortrace#56fb8c2b211cdda4fc8715ec99e1cb7b7d9eb851 \ + mconf/ep_pad_ttl#360136cd38493dd698435631f2373cbb7089082d \ + mconf/ep_redis_publisher#2b6e47c1c59362916a0b2961a29b259f2977b694 + + +# add skin from git submodule +COPY --chown=etherpad:0 --from=skin / /opt/etherpad-lite/src/static/skins/bigbluebutton + +# add plugin from git submodule +COPY --chown=etherpad:0 --from=plugin / /ep_bigbluebutton_patches +RUN pnpm run plugins i --path /ep_bigbluebutton_patches + +COPY settings.json /opt/etherpad-lite/settings.json +COPY etherpad-export.sh /etherpad-export.sh +COPY entrypoint.sh /entrypoint.sh +ENTRYPOINT ["/entrypoint.sh"] \ No newline at end of file diff --git a/mod/etherpad/entrypoint.sh b/mod/etherpad/entrypoint.sh new file mode 100644 index 0000000..acc2392 --- /dev/null +++ b/mod/etherpad/entrypoint.sh @@ -0,0 +1,3 @@ +#!/bin/sh +echo $ETHERPAD_API_KEY > /tmp/apikey +pnpm run prod --apikey /tmp/apikey \ No newline at end of file diff --git a/mod/etherpad/etherpad-export.sh b/mod/etherpad/etherpad-export.sh new file mode 100644 index 0000000..0ebd733 --- /dev/null +++ b/mod/etherpad/etherpad-export.sh @@ -0,0 +1,9 @@ +#!/bin/sh +src="$8" +dest="$(echo $8 | sed -E -e 's/html|odt/'$7'/')" +convertTo="$7" + + +curl -v -F "data=@${src}" -k $COLLABORA_URL/convert-to/$convertTo > "${dest}" + +exit 0 \ No newline at end of file diff --git a/mod/etherpad/settings.json b/mod/etherpad/settings.json new file mode 100644 index 0000000..d3c1c4e --- /dev/null +++ b/mod/etherpad/settings.json @@ -0,0 +1,723 @@ +/* + * This file must be valid JSON. But comments are allowed + * + * Please edit settings.json, not settings.json.template + * + * Please note that starting from Etherpad 1.6.0 you can store DB credentials in + * a separate file (credentials.json). + * + * + * ENVIRONMENT VARIABLE SUBSTITUTION + * ================================= + * + * All the configuration values can be read from environment variables using the + * syntax "${ENV_VAR}" or "${ENV_VAR:default_value}". + * + * This is useful, for example, when running in a Docker container. + * + * DETAILED RULES: + * - If the environment variable is set to the string "true" or "false", the + * value becomes Boolean true or false. + * - If the environment variable is set to the string "null", the value + * becomes null. + * - If the environment variable is set to the string "undefined", the setting + * is removed entirely, except when used as the member of an array in which + * case it becomes null. + * - If the environment variable is set to a string representation of a finite + * number, the string is converted to that number. + * - If the environment variable is set to any other string, including the + * empty string, the value is that string. + * - If the environment variable is unset and a default value is provided, the + * value is as if the environment variable was set to the provided default: + * - "${UNSET_VAR:}" becomes the empty string. + * - "${UNSET_VAR:foo}" becomes the string "foo". + * - "${UNSET_VAR:true}" and "${UNSET_VAR:false}" become true and false. + * - "${UNSET_VAR:null}" becomes null. + * - "${UNSET_VAR:undefined}" causes the setting to be removed (or be set + * to null, if used as a member of an array). + * - If the environment variable is unset and no default value is provided, + * the value becomes null. THIS BEHAVIOR MAY CHANGE IN A FUTURE VERSION OF + * ETHERPAD; if you want the default value to be null, you should explicitly + * specify "null" as the default value. + * + * EXAMPLE: + * "port": "${PORT:9001}" + * "minify": "${MINIFY}" + * "skinName": "${SKIN_NAME:colibris}" + * + * Would read the configuration values for those items from the environment + * variables PORT, MINIFY and SKIN_NAME. + * + * If PORT and SKIN_NAME variables were not defined, the default values 9001 and + * "colibris" would be used. + * The configuration value "minify", on the other hand, does not have a + * designated default value. Thus, if the environment variable MINIFY were + * undefined, "minify" would be null. + * + * REMARKS: + * 1) please note that variable substitution always needs to be quoted. + * + * "port": 9001, <-- Literal values. When not using + * "minify": false substitution, only strings must be + * "skinName": "colibris" quoted. Booleans and numbers must not. + * + * "port": "${PORT:9001}" <-- CORRECT: if you want to use a variable + * "minify": "${MINIFY:true}" substitution, put quotes around its name, + * "skinName": "${SKIN_NAME}" even if the required value is a number or + * a boolean. + * Etherpad will take care of rewriting it + * to the proper type if necessary. + * + * "port": ${PORT:9001} <-- ERROR: this is not valid json. Quotes + * "minify": ${MINIFY} around variable names are missing. + * "skinName": ${SKIN_NAME} + * + * 2) Beware of undefined variables and default values: nulls and empty strings + * are different! + * + * This is particularly important for user's passwords (see the relevant + * section): + * + * "password": "${PASSW}" // if PASSW is not defined would result in password === null + * "password": "${PASSW:}" // if PASSW is not defined would result in password === '' + * + * If you want to use an empty value (null) as default value for a variable, + * simply do not set it, without putting any colons: "${ABIWORD}". + * + * 3) if you want to use newlines in the default value of a string parameter, + * use "\n" as usual. + * + * "defaultPadText" : "${DEFAULT_PAD_TEXT}Line 1\nLine 2" + */ + { + /* + * Name your instance! + */ + "title": "Etherpad", + + /* + * Pathname of the favicon you want to use. If null, the skin's favicon is + * used if one is provided by the skin, otherwise the default Etherpad favicon + * is used. If this is a relative path it is interpreted as relative to the + * Etherpad root directory. + */ + "favicon": null, + + /* + * Skin name. + * + * Its value has to be an existing directory under src/static/skins. + * You can write your own, or use one of the included ones: + * + * - "no-skin": an empty skin (default). This yields the unmodified, + * traditional Etherpad theme. + * - "colibris": the new experimental skin (since Etherpad 1.8), candidate to + * become the default in Etherpad 2.0 + */ + "skinName": "bigbluebutton", + + /* + * Skin Variants + * + * Use the UI skin variants builder at /p/test#skinvariantsbuilder + * + * For the colibris skin only, you can choose how to render the three main + * containers: + * - toolbar (top menu with icons) + * - editor (containing the text of the pad) + * - background (area outside of editor, mostly visible when using page style) + * + * For each of the 3 containers you can choose 4 color combinations: + * super-light, light, dark, super-dark. + * + * For example, to make the toolbar dark, you will include "dark-toolbar" into + * skinVariants. + * + * You can provide multiple skin variants separated by spaces. Default + * skinVariant is "super-light-toolbar super-light-editor light-background". + * + * For the editor container, you can also make it full width by adding + * "full-width-editor" variant (by default editor is rendered as a page, with + * a max-width of 900px). + */ + "skinVariants": "", + + /* + * IP and port which Etherpad should bind at. + * + * Binding to a Unix socket is also supported: just use an empty string for + * the ip, and put the full path to the socket in the port parameter. + * + * EXAMPLE USING UNIX SOCKET: + * "ip": "", // <-- has to be an empty string + * "port" : "/somepath/etherpad.socket", // <-- path to a Unix socket + */ + "ip": "0.0.0.0", + "port": 9001, + + /* + * Option to hide/show the settings.json in admin page. + * + * Default option is set to true + */ + "showSettingsInAdminPage": true, + + /* + * Settings for cleanup of pads + */ + "cleanup": { + "enabled": false, + "keepRevisions": 5 + }, + + /* + * Node native SSL support + * + * This is disabled by default. + * Make sure to have the minimum and correct file access permissions set so + * that the Etherpad server can access them + */ + + /* + "ssl" : { + "key" : "/path-to-your/epl-server.key", + "cert" : "/path-to-your/epl-server.crt", + "ca": ["/path-to-your/epl-intermediate-cert1.crt", "/path-to-your/epl-intermediate-cert2.crt"] + }, + */ + + /* + * The type of the database. + * + * You can choose between many DB drivers, for example: dirty, postgres, + * sqlite, mysql. + * + * You shouldn't use "dirty" for for anything else than testing or + * development. + * + * + * Database specific settings are dependent on dbType, and go in dbSettings. + * Remember that since Etherpad 1.6.0 you can also store this information in + * credentials.json. + * + * For a complete list of the supported drivers, please refer to: + * https://www.npmjs.com/package/ueberdb2 + */ + + "dbType": "redis", + "dbSettings": { + "url": "redis://redis:6379" + }, + + /* + * An Example of MySQL Configuration (commented out). + * + * See: https://github.com/ether/etherpad-lite/wiki/How-to-use-Etherpad-Lite-with-MySQL + */ + + /* + "dbType" : "redis", + "dbSettings" : { + "host": "127.0.0.1", + "port": 6379, + "client_options": { + "password": "PASSWORD" + } + }, + */ + + /* + * The default text of a pad: A zero-width-space is used to work around an issue with Etherpad 1.9.1 where empty pads are not being created. + * See: https://github.com/ether/etherpad-lite/issues/5787 + */ + "defaultPadText" : "\u200b", + + /* + * Default Pad behavior. + * + * Change them if you want to override. + */ + "padOptions": { + "noColors": true, + "showControls": true, + "showChat": false, + "showLineNumbers": false, + "useMonospaceFont": false, + "userName": false, + "userColor": false, + "rtl": false, + "alwaysShowChat": false, + "chatAndUsers": false, + "lang": "en" + }, + + /* + * Pad Shortcut Keys + */ + "padShortcutEnabled" : { + "altF9": true, /* focus on the File Menu and/or editbar */ + "altC": false, /* focus on the Chat window */ + "cmdShift2": true, /* shows a gritter popup showing a line author */ + "delete": true, + "return": true, + "esc": true, /* in mozilla versions 14-19 avoid reconnecting pad */ + "cmdS": true, /* save a revision */ + "tab": true, /* indent */ + "cmdZ": true, /* undo/redo */ + "cmdY": true, /* redo */ + "cmdI": true, /* italic */ + "cmdB": true, /* bold */ + "cmdU": true, /* underline */ + "cmd5": true, /* strike through */ + "cmdShiftL": true, /* unordered list */ + "cmdShiftN": true, /* ordered list */ + "cmdShift1": true, /* ordered list */ + "cmdShiftC": true, /* clear authorship */ + "cmdH": true, /* backspace */ + "ctrlHome": true, /* scroll to top of pad */ + "pageUp": true, + "pageDown": true + }, + + /* + * Enables the use of a different server. We have a different one that syncs changes from the original server. + * It is hosted on GitHub and should not be blocked by many firewalls. + * https://etherpad.org/ep_infos + */ + + "updateServer": "https://etherpad.org/ep_infos", + + /* + * Should we suppress errors from being visible in the default Pad Text? + */ + "suppressErrorsInPadText": true, + + /* + * If this option is enabled, a user must have a session to access pads. + * This effectively allows only group pads to be accessed. + */ + "requireSession": true, + + /* + * Users may edit pads but not create new ones. + * + * Pad creation is only via the API. + * This applies both to group pads and regular pads. + */ + "editOnly": true, + + /* + * If true, all css & js will be minified before sending to the client. + * + * This will improve the loading performance massively, but makes it difficult + * to debug the javascript/css + */ + "minify": true, + + /* + * How long may clients use served javascript code (in seconds)? + * + * Not setting this may cause problems during deployment. + * Set to 0 to disable caching. + */ + "maxAge": 21600, // 60 * 60 * 6 = 6 hours + + /* + * Absolute path to the Abiword executable. + * + * Abiword is needed to get advanced import/export features of pads. Setting + * it to null disables Abiword and will only allow plain text and HTML + * import/exports. + */ + "abiword": null, + + /* + * This is the absolute path to the soffice executable. + * + * LibreOffice can be used in lieu of Abiword to export pads. + * Setting it to null disables LibreOffice exporting. + */ + "soffice": "/etherpad-export.sh", + + /* + * Allow import of file types other than the supported ones: + * txt, doc, docx, rtf, odt, html & htm + */ + "allowUnknownFileEnds": false, + + /* + * This setting is used if you require authentication of all users. + * + * Note: "/admin" always requires authentication. + */ + "requireAuthentication": false, + + /* + * Require authorization by a module, or a user with is_admin set, see below. + */ + "requireAuthorization": false, + + /* + * When you use NGINX or another proxy/load-balancer set this to true. + * + * This is especially necessary when the reverse proxy performs SSL + * termination, otherwise the cookies will not have the "secure" flag. + * + * The other effect will be that the logs will contain the real client's IP, + * instead of the reverse proxy's IP. + */ + "trustProxy": true, + + /* + * Settings controlling the session cookie issued by Etherpad. + */ + "cookie": { + /* + * How often (in milliseconds) the key used to sign the express_sid cookie + * should be rotated. Long rotation intervals reduce signature verification + * overhead (because there are fewer historical keys to check) and database + * load (fewer historical keys to store, and less frequent queries to + * get/update the keys). Short rotation intervals are slightly more secure. + * + * Multiple Etherpad processes sharing the same database (table) is + * supported as long as the clock sync error is significantly less than this + * value. + * + * Key rotation can be disabled (not recommended) by setting this to 0 or + * null, or by disabling session expiration (see sessionLifetime). + */ + "keyRotationInterval": 86400000, // = 1d * 24h/d * 60m/h * 60s/m * 1000ms/s + + /* + * Value of the SameSite cookie property. "Lax" is recommended unless + * Etherpad will be embedded in an iframe from another site, in which case + * this must be set to "None". Note: "None" will not work (the browser will + * not send the cookie to Etherpad) unless https is used to access Etherpad + * (either directly or via a reverse proxy with "trustProxy" set to true). + * + * "Strict" is not recommended because it has few security benefits but + * significant usability drawbacks vs. "Lax". See + * https://stackoverflow.com/q/41841880 for discussion. + */ + "sameSite": "None", + + /* + * How long (in milliseconds) after navigating away from Etherpad before the + * user is required to log in again. (The express_sid cookie is set to + * expire at time now + sessionLifetime when first created, and its + * expiration time is periodically refreshed to a new now + sessionLifetime + * value.) If requireAuthentication is false then this value does not really + * matter. + * + * The "best" value depends on your users' usage patterns and the amount of + * convenience you desire. A long lifetime is more convenient (users won't + * have to log back in as often) but has some drawbacks: + * - It increases the amount of state kept in the database. + * - It might weaken security somewhat: The cookie expiration is refreshed + * indefinitely without consulting authentication or authorization + * hooks, so once a user has accessed a pad, the user can continue to + * use the pad until the user leaves for longer than sessionLifetime. + * - More historical keys (sessionLifetime / keyRotationInterval) must be + * checked when verifying signatures. + * + * Session lifetime can be set to infinity (not recommended) by setting this + * to null or 0. Note that if the session does not expire, most browsers + * will delete the cookie when the browser exits, but a session record is + * kept in the database forever. + */ + "sessionLifetime": 864000000, // = 10d * 24h/d * 60m/h * 60s/m * 1000ms/s + + /* + * How long (in milliseconds) before the expiration time of an active user's + * session is refreshed (to now + sessionLifetime). This setting affects the + * following: + * - How often a new session expiration time will be written to the + * database. + * - How often each user's browser will ping the Etherpad server to + * refresh the expiration time of the session cookie. + * + * High values reduce the load on the database and the load from browsers, + * but can shorten the effective session lifetime if Etherpad is restarted + * or the user navigates away. + * + * Automatic session refreshes can be disabled (not recommended) by setting + * this to null. + */ + "sessionRefreshInterval": 86400000 // = 1d * 24h/d * 60m/h * 60s/m * 1000ms/s + }, + + /* + * Privacy: disable IP logging + */ + "disableIPlogging": true, + + /* + * Time (in seconds) to automatically reconnect pad when a "Force reconnect" + * message is shown to user. + * + * Set to 0 to disable automatic reconnection. + */ + "automaticReconnectionTimeout": 0, + + /* + * By default, when caret is moved out of viewport, it scrolls the minimum + * height needed to make this line visible. + */ + "scrollWhenFocusLineIsOutOfViewport": { + + /* + * Percentage of viewport height to be additionally scrolled. + * + * E.g.: use "percentage.editionAboveViewport": 0.5, to place caret line in + * the middle of viewport, when user edits a line above of the + * viewport + * + * Set to 0 to disable extra scrolling + */ + "percentage": { + "editionAboveViewport": 0, + "editionBelowViewport": 0 + }, + + /* + * Time (in milliseconds) used to animate the scroll transition. + * Set to 0 to disable animation + */ + "duration": 0, + + /* + * Flag to control if it should scroll when user places the caret in the + * last line of the viewport + */ + "scrollWhenCaretIsInTheLastLineOfViewport": false, + + /* + * Percentage of viewport height to be additionally scrolled when user + * presses arrow up in the line of the top of the viewport. + * + * Set to 0 to let the scroll to be handled as default by Etherpad + */ + "percentageToScrollWhenUserPressesArrowUp": 0 + }, + + /* + * User accounts. These accounts are used by: + * - default HTTP basic authentication if no plugin handles authentication + * - some but not all authentication plugins + * - some but not all authorization plugins + * + * User properties: + * - password: The user's password. Some authentication plugins will ignore + * this. + * - is_admin: true gives access to /admin. Defaults to false. If you do not + * uncomment this, /admin will not be available! + * - readOnly: If true, this user will not be able to create new pads or + * modify existing pads. Defaults to false. + * - canCreate: If this is true and readOnly is false, this user can create + * new pads. Defaults to true. + * + * Authentication and authorization plugins may define additional properties. + * + * WARNING: passwords should not be stored in plaintext in this file. + * If you want to mitigate this, please install ep_hash_auth and + * follow the section "secure your installation" in README.md + */ + + /* + "users": { + "admin": { + // 1) "password" can be replaced with "hash" if you install ep_hash_auth + // 2) please note that if password is null, the user will not be created + "password": "changeme1", + "is_admin": true + }, + "user": { + // 1) "password" can be replaced with "hash" if you install ep_hash_auth + // 2) please note that if password is null, the user will not be created + "password": "changeme1", + "is_admin": false + } + }, + */ + + /* + * Restrict socket.io transport methods + */ + "socketTransportProtocols" : ["websocket", "polling"], + + "socketIo": { + /* + * Maximum permitted client message size (in bytes). All messages from + * clients that are larger than this will be rejected. Large values make it + * possible to paste large amounts of text, and plugins may require a larger + * value to work properly, but increasing the value increases susceptibility + * to denial of service attacks (malicious clients can exhaust memory). + */ + "maxHttpBufferSize": 50000 + }, + + /* + * Allow Load Testing tools to hit the Etherpad Instance. + * + * WARNING: this will disable security on the instance. + */ + "loadTest": false, + + /** + * Disable dump of objects preventing a clean exit + */ + "dumpOnUncleanExit": false, + + /* + * Disable indentation on new line when previous line ends with some special + * chars (':', '[', '(', '{') + */ + + /* + "indentationOnNewLine": false, + */ + + "ep_pad_ttl": { + "ttl": 86400, // 24 hours + "timeout": 30, + "interval": 21600 // 6 hours + }, + + /* + * Redis publisher plugin configuration. + * npm i git+https://git@github.com/mconf/ep_redis_publisher.git + */ + + "ep_redis_publisher": { + "host": "redis", + "port": 6379 + }, + + /* + * From Etherpad 1.8.3 onwards, import and export of pads is always rate + * limited. + * + * The default is to allow at most 10 requests per IP in a 90 seconds window. + * After that the import/export request is rejected. + * + * See https://github.com/nfriedly/express-rate-limit for more options + */ + "importExportRateLimiting": { + // duration of the rate limit window (milliseconds) + "windowMs": 90000, + + // maximum number of requests per IP to allow during the rate limit window + "max": 32 + }, + + /* + * From Etherpad 1.8.3 onwards, the maximum allowed size for a single imported + * file is always bounded. + * + * File size is specified in bytes. Default is 50 MB. + */ + "importMaxFileSize": 52428800, // 50 * 1024 * 1024 + + /* + The authentication method used by the server. + The default value is sso + If you want to use the old authentication system, change this to apikey + */ + "authenticationMethod": "apikey", + + /* + * From Etherpad 1.8.5 onwards, when Etherpad is in production mode commits from individual users are rate limited + * + * The default is to allow at most 10 changes per IP in a 1 second window. + * After that the change is rejected. + * + * See https://github.com/animir/node-rate-limiter-flexible/wiki/Overall-example#websocket-single-connection-prevent-flooding for more options + */ + "commitRateLimiting": { + // duration of the rate limit window (seconds) + "duration": 1, + + // maximum number of changes per IP to allow during the rate limit window + "points": 100 + }, + + /* + * Toolbar buttons configuration. + * + * Uncomment to customize. + */ + + "toolbar": { + "left": [ + ["bold", "italic", "underline", "strikethrough"], + ["orderedlist", "unorderedlist", "undo", "redo"], + ["importexport"] + ], + "right": [[]] + }, + + /* + * Expose Etherpad version in the web interface and in the Server http header. + * + * Do not enable on production machines. + */ + "exposeVersion": false, + + /* + * The log level we are using. + * + * Valid values: DEBUG, INFO, WARN, ERROR + */ + "loglevel": "INFO", + + /* + * The log layout type to use. + * + * Valid values: basic, colored + */ + "logLayoutType": "colored", + + /* Override any strings found in locale directories */ + "customLocaleStrings": { + "de": { + "pad.importExport.import_export": "Export", + "pad.toolbar.import_export.title": "Export zu verschiedenen Dateiformaten" + }, + "en-gb": { + "pad.importExport.import_export": "Export", + "pad.toolbar.import_export.title": "Export to different file formats" + }, + "en": { + "pad.importExport.import_export": "Export", + "pad.toolbar.import_export.title": "Export to different file formats" + }, + "es": { + "pad.importExport.import_export": "Exportar", + "pad.toolbar.import_export.title": "Exportar a diferentes formatos de archivos" + }, + "fr": { + "pad.importExport.import_export": "Exporter", + "pad.toolbar.import_export.title": "Exporter vers un format de fichier différent" + }, + "it": { + "pad.importExport.import_export": "Esportazione", + "pad.toolbar.import_export.title": "Esporta a diversi formati di file" + }, + "pt-br": { + "pad.importExport.import_export": "Exportar", + "pad.toolbar.import_export.title": "Exportar para diferentes formatos de arquivo" + }, + "pt": { + "pad.importExport.import_export": "Exportar", + "pad.toolbar.import_export.title": "Exportar para diferentes formatos de ficheiro" + } + }, + + /* Disable Admin UI tests */ + "enableAdminUITests": false, + + /* + * Enable/Disable case-insensitive pad names. + */ + "lowerCasePadIds": false +} \ No newline at end of file diff --git a/mod/freeswitch/Dockerfile b/mod/freeswitch/Dockerfile new file mode 100644 index 0000000..0153763 --- /dev/null +++ b/mod/freeswitch/Dockerfile @@ -0,0 +1,71 @@ +ARG BBB_BUILD_TAG +FROM bigbluebutton/bbb-build:$BBB_BUILD_TAG AS builder + +COPY --from=freeswitch / /build/freeswitch + +# install most recent git version for proper sparse-checkout support +# https://stackoverflow.com/questions/72223738/failed-to-initialize-sparse-checkout +RUN echo 'deb https://ppa.launchpadcontent.net/git-core/ppa/ubuntu focal main' > /etc/apt/sources.list.d/git-core-ppa.list && \ + apt-key adv --keyserver keyserver.ubuntu.com --recv-keys A1715D88E1DF1F24 && \ + apt-get update && \ + apt-get install -y git + +# get build files for bbb-freeswitch (build/packages-template/bbb-freeswitch-core/) +COPY --from=build-files / /build/ + +# mock files expected by build.sh +RUN mkdir -p /build/bbb-voice-conference/config/freeswitch/conf/ && \ + touch \ + /build/opts-build.sh \ + /build/freeswitch.service.build \ + /build/bbb-voice-conference/config/freeswitch/conf/a \ + && \ + echo "" > /usr/local/bin/fpm + +# build freeswitch +RUN cd /build && ./build.sh + + +# add english sounds +RUN mkdir -p /build/staging/opt/freeswitch/share/freeswitch && \ + wget https://ubuntu.bigbluebutton.org/sounds.tar.gz -O sounds.tar.gz && \ + tar xvfz sounds.tar.gz -C /build/staging/opt/freeswitch/share/freeswitch && \ + wget https://gitlab.senfcall.de/senfcall-public/mute-and-unmute-sounds/-/archive/master/mute-and-unmute-sounds-master.zip && \ + unzip mute-and-unmute-sounds-master.zip && \ + cd mute-and-unmute-sounds-master/sounds && \ + find . -name "*.wav" -exec /bin/bash -c "sox -v 0.3 {} /tmp/tmp.wav; cp /tmp/tmp.wav /build/staging/opt/freeswitch/share/freeswitch/sounds/en/us/callie/conference/{}" \; + +# add bigblugbutton config +ARG TAG_FS_CONFIG +COPY --from=fs-config / /build/staging/opt/freeswitch/etc/freeswitch/ + +# =============================================== + +# we are using ubuntu here, because libjpeg8 is required, but not available in debian +FROM ubuntu:22.04 +RUN apt-get update && \ + apt-get install -y \ + xmlstarlet wget iptables curl \ + libfreetype6 libcurl4 libspeex1 libspeexdsp1 libopus0 libsndfile1 libopusfile0 liblua5.2-0 libjbig0 libldns3 libedit2 libtiff5 libpng16-16 libsqlite3-0 \ + && \ +# install libopusenc0 + wget -O /tmp/libopusenc0_0.2.1-1bbb2_amd64.deb https://launchpad.net/~bigbluebutton/+archive/ubuntu/support/+files/libopusenc0_0.2.1-1bbb2_amd64.deb \ + && dpkg -i /tmp/libopusenc0_0.2.1-1bbb2_amd64.deb \ + && rm /tmp/libopusenc0_0.2.1-1bbb2_amd64.deb + +# add dockerize +COPY --from=alangecker/bbb-docker-base-java /usr/local/bin/dockerize /usr/local/bin/dockerize + +# copy over built freeswitch & config +COPY --from=builder /build/staging/opt /opt +COPY --from=builder /build/staging/etc /etc + +RUN ldconfig && \ + ln -s /opt/freeswitch/conf /etc/freeswitch && \ + groupadd freeswitch && \ + useradd --home-dir /opt/freeswitch --shell /usr/sbin/nologin -g freeswitch freeswitch + +COPY ./entrypoint.sh /entrypoint.sh +COPY ./conf /etc/freeswitch/ + +ENTRYPOINT /entrypoint.sh \ No newline at end of file diff --git a/mod/freeswitch/conf/autoload_configs/conference.conf.xml.tmpl b/mod/freeswitch/conf/autoload_configs/conference.conf.xml.tmpl new file mode 100644 index 0000000..1171614 --- /dev/null +++ b/mod/freeswitch/conf/autoload_configs/conference.conf.xml.tmpl @@ -0,0 +1,285 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {{if not (isTrue .Env.DISABLE_SOUND_MUTED) }} + + + {{end}} + {{if not (isTrue .Env.DISABLE_SOUND_ALONE) }} + + {{end}} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mod/freeswitch/conf/autoload_configs/event_socket.conf.xml b/mod/freeswitch/conf/autoload_configs/event_socket.conf.xml new file mode 100644 index 0000000..6d7d4d7 --- /dev/null +++ b/mod/freeswitch/conf/autoload_configs/event_socket.conf.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/mod/freeswitch/conf/autoload_configs/modules.conf.xml b/mod/freeswitch/conf/autoload_configs/modules.conf.xml new file mode 100644 index 0000000..f05aa52 --- /dev/null +++ b/mod/freeswitch/conf/autoload_configs/modules.conf.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mod/freeswitch/conf/dialplan/public/dialin.xml b/mod/freeswitch/conf/dialplan/public/dialin.xml new file mode 100644 index 0000000..02c4ed7 --- /dev/null +++ b/mod/freeswitch/conf/dialplan/public/dialin.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/mod/freeswitch/conf/sip_profiles/external-dialin.xml b/mod/freeswitch/conf/sip_profiles/external-dialin.xml new file mode 100644 index 0000000..fbe7ca3 --- /dev/null +++ b/mod/freeswitch/conf/sip_profiles/external-dialin.xml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mod/freeswitch/conf/sip_profiles/external.xml b/mod/freeswitch/conf/sip_profiles/external.xml new file mode 100644 index 0000000..53c9de1 --- /dev/null +++ b/mod/freeswitch/conf/sip_profiles/external.xml @@ -0,0 +1,115 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mod/freeswitch/conf/vars.xml.tmpl b/mod/freeswitch/conf/vars.xml.tmpl new file mode 100644 index 0000000..b86502d --- /dev/null +++ b/mod/freeswitch/conf/vars.xml.tmpl @@ -0,0 +1,453 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mod/freeswitch/entrypoint.sh b/mod/freeswitch/entrypoint.sh new file mode 100644 index 0000000..bf6dae1 --- /dev/null +++ b/mod/freeswitch/entrypoint.sh @@ -0,0 +1,72 @@ +#!/bin/bash -e + +# remove all SIP (port 5060) iptable rules +iptables -S INPUT | grep "\-\-dport 5060 " | cut -d " " -f 2- | xargs -rL1 iptables -D + +# block requests to 5060 (tcp/udp) +iptables -A INPUT -p tcp --dport 5060 -s 0.0.0.0/0 -j REJECT +iptables -A INPUT -p udp --dport 5060 -s 0.0.0.0/0 -j REJECT + +# allow some IPs +IFS=',' read -ra ADDR <<< "$SIP_IP_ALLOWLIST" +for IP in "${ADDR[@]}"; do + # process "$i" + echo "allow port 5060/udp for $IP" + iptables -I INPUT -p udp --dport 5060 -s $IP -j ACCEPT +done + +mkdir -p /var/freeswitch/meetings +chown -R freeswitch:daemon /var/freeswitch/meetings +chmod 777 /var/freeswitch/meetings +chown -R freeswitch:daemon /opt/freeswitch/var +chown -R freeswitch:daemon /opt/freeswitch/etc +chmod -R g-rwx,o-rwx /opt/freeswitch/etc + +# install freeswitch sounds if missing +SOUNDS_DIR=/opt/freeswitch/share/freeswitch/sounds +if [ "$SOUNDS_LANGUAGE" == "en-us-callie" ]; then + # default, is already installed + echo "" +elif [ "$SOUNDS_LANGUAGE" == "de-de-daedalus3" ]; then + if [ ! -d "$SOUNDS_DIR/de/de/daedalus3" ]; then + echo "sounds package for de-de-daedalus3 not installed yet" + wget -O /tmp/freeswitch-german-soundfiles.zip https://github.com/Daedalus3/freeswitch-german-soundfiles/archive/master.zip + mkdir -p $SOUNDS_DIR/de/de/daedalus3 + unzip /tmp/freeswitch-german-soundfiles.zip -d /tmp/ + mv /tmp/freeswitch-german-soundfiles-master $SOUNDS_DIR/de/de/daedalus3/conference + + # symlink other folders + for folder in "digits" "ivr" "misc"; do + ln -s $SOUNDS_DIR/en/us/callie/$folder $SOUNDS_DIR/de/de/daedalus3/$folder + done + + fi +else + if [ ! -f $SOUNDS_DIR/$SOUNDS_LANGUAGE.installed ]; then + echo "sounds package for $SOUNDS_LANGUAGE not installed yet" + + # get filename of latest release for this sound package + FILENAME=$(curl -s https://files.freeswitch.org/releases/sounds/ | grep -i $SOUNDS_LANGUAGE 2> /dev/null | awk -F'\"' '{print $8}' | grep -E '\-48000-.*\.gz$' | sort -V | tail -n 1) + + if [ "$FILENAME" = "" ]; then + echo "Error: could not find sounds for language '$SOUNDS_LANGUAGE'" + echo "make sure to specify a value for SOUNDS_LANGUAGE which exists on https://files.freeswitch.org/releases/sounds/" + exit 1 + fi + for bitrate in 8000 16000 32000 48000; do + URL=https://files.freeswitch.org/releases/sounds/$(echo $FILENAME | sed "s/48000/$bitrate/") + wget -O /tmp/sounds.tar.gz $URL + tar xvfz /tmp/sounds.tar.gz -C $SOUNDS_DIR + done + + touch $SOUNDS_DIR/$SOUNDS_LANGUAGE.installed + fi +fi + + +export SOUNDS_PATH=$SOUNDS_DIR/$(echo "$SOUNDS_LANGUAGE" | sed 's|-|/|g') + +dockerize \ + -template /etc/freeswitch/vars.xml.tmpl:/etc/freeswitch/vars.xml \ + -template /etc/freeswitch/autoload_configs/conference.conf.xml.tmpl:/etc/freeswitch/autoload_configs/conference.conf.xml \ + /opt/freeswitch/bin/freeswitch -u freeswitch -g daemon -nonat -nf diff --git a/mod/fsesl-akka/Dockerfile b/mod/fsesl-akka/Dockerfile new file mode 100644 index 0000000..8f66f59 --- /dev/null +++ b/mod/fsesl-akka/Dockerfile @@ -0,0 +1,30 @@ +ARG BBB_BUILD_TAG +FROM bigbluebutton/bbb-build:$BBB_BUILD_TAG AS builder + +COPY --from=src-common-message / /bbb-common-message + +# build bbb-common-message +RUN cd /bbb-common-message && ./deploy.sh + +# =================================================== +COPY --from=src-fsesl-client / /bbb-fsesl-client +RUN cd /bbb-fsesl-client && ./deploy.sh + +COPY --from=src-fsesl-akka / /source + +# compile and unzip bin +RUN cd /source \ + && sbt universal:packageBin +RUN unzip /source/target/universal/bbb-fsesl-akka-0.0.2.zip -d / + +# # =================================================== + +FROM alangecker/bbb-docker-base-java + +COPY --from=builder /bbb-fsesl-akka-0.0.2 /bbb-fsesl-akka +COPY bbb-fsesl-akka.conf /etc/bigbluebutton/bbb-fsesl-akka.conf.tmpl +COPY logback.xml /bbb-fsesl-akka/conf/logback.xml +COPY entrypoint.sh /entrypoint.sh + +USER bigbluebutton +ENTRYPOINT /entrypoint.sh \ No newline at end of file diff --git a/mod/fsesl-akka/bbb-fsesl-akka.conf b/mod/fsesl-akka/bbb-fsesl-akka.conf new file mode 100644 index 0000000..12acb3e --- /dev/null +++ b/mod/fsesl-akka/bbb-fsesl-akka.conf @@ -0,0 +1,18 @@ +// include default config from upstream +include "/bbb-fsesl-akka/conf/application.conf" + + +freeswitch { + esl { + host="freeswitch" + password="FSESL_PASSWORD" + } +} + +redis { + host="redis" +} + +http { + interface = "0.0.0.0" +} \ No newline at end of file diff --git a/mod/fsesl-akka/entrypoint.sh b/mod/fsesl-akka/entrypoint.sh new file mode 100644 index 0000000..2000ce3 --- /dev/null +++ b/mod/fsesl-akka/entrypoint.sh @@ -0,0 +1,9 @@ +#!/bin/sh -e + +TARGET=/etc/bigbluebutton/bbb-fsesl-akka.conf + +cp /etc/bigbluebutton/bbb-fsesl-akka.conf.tmpl $TARGET +sed -i "s/FSESL_PASSWORD/$FSESL_PASSWORD/" $TARGET + +cd /bbb-fsesl-akka +/bbb-fsesl-akka/bin/bbb-fsesl-akka \ No newline at end of file diff --git a/mod/fsesl-akka/logback.xml b/mod/fsesl-akka/logback.xml new file mode 100644 index 0000000..9e587d9 --- /dev/null +++ b/mod/fsesl-akka/logback.xml @@ -0,0 +1,17 @@ + + + + + %d{"yyyy-MM-dd'T'HH:mm:ss.SSSXXX"} %-5level %logger{35} - %msg%n + + + + + + + + + + + + diff --git a/mod/haproxy.cfg b/mod/haproxy.cfg new file mode 100644 index 0000000..feb3532 --- /dev/null +++ b/mod/haproxy.cfg @@ -0,0 +1,80 @@ +global + log stdout format raw local0 debug + + maxconn 20480 + ############# IMPORTANT ################################# + ## DO NOT SET CHROOT OTHERWISE YOU HAVE TO CHANGE THE ## + ## acme-http01-webroot.lua file ## + # chroot /jail ## + ######################################################### + lua-load /etc/haproxy/acme-http01-webroot.lua + # + # SSL options + ssl-default-bind-ciphers ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:RSA+AESGCM:RSA+AES:!aNULL:!MD5:!DSS + ssl-default-bind-options ssl-min-ver TLSv1.2 + tune.ssl.default-dh-param 4096 + + + # workaround for bug #14 (Cert renewal blocks HAProxy indefinitely with Websocket connections) + hard-stop-after 3s + + +# DNS runt-time resolution on backend hosts +resolvers docker + nameserver dns "127.0.0.11:53" + +defaults + log global + mode http + timeout connect 5000ms + timeout client 50000ms + timeout server 50000ms + # option forwardfor + option httplog + + option dontlognull + timeout connect 5000 + timeout client 50000 + timeout server 50000 + + # never fail on address resolution + default-server init-addr last,libc,none + +frontend http + bind *:80,[::]:80 + mode http + acl url_acme_http01 path_beg /.well-known/acme-challenge/ + http-request use-service lua.acme-http01 if METH_GET url_acme_http01 + redirect scheme https code 301 if !{ ssl_fc } + +frontend nginx_or_turn + bind *:443,:::443 ssl crt /etc/haproxy/certs/ ssl-min-ver TLSv1.2 alpn h2,http/1.1,stun.turn + mode tcp + option tcplog + tcp-request content capture req.payload(0,1) len 1 + log-format "%ci:%cp [%t] %ft %b/%s %Tw/%Tc/%Tt %B %ts %ac/%fc/%bc/%sc/%rc %sq/%bq captured_user:%{+X}[capture.req.hdr(0)]" + tcp-request inspect-delay 30s + # We terminate SSL on haproxy. HTTP2 is a binary protocol. haproxy has to + # decide which protocol is spoken. This is negotiated by ALPN. + # + # Depending on the ALPN value traffic is redirected to either port 82 (HTTP2, + # ALPN value h2) or 81 (HTTP 1.0 or HTTP 1.1, ALPN value http/1.1 or no value) + # If no ALPN value is set, the first byte is inspected and depending on the + # value traffic is sent to either port 81 or coturn. + use_backend nginx-http2 if { ssl_fc_alpn h2 } + use_backend nginx if { ssl_fc_alpn http/1.1 } + use_backend turn if { ssl_fc_alpn stun.turn } + use_backend %[capture.req.hdr(0),map_str(/etc/haproxy/protocolmap,turn)] + default_backend turn + +backend turn + mode tcp + server localhost webrtc-sfu:3478 check + +backend nginx + mode tcp + server localhost nginx:48081 send-proxy check + +backend nginx-http2 + mode tcp + server localhost nginx:48082 send-proxy check diff --git a/mod/haproxy/Dockerfile b/mod/haproxy/Dockerfile new file mode 100644 index 0000000..f172a4b --- /dev/null +++ b/mod/haproxy/Dockerfile @@ -0,0 +1,4 @@ +FROM ghcr.io/tomdess/docker-haproxy-certbot:2.8.10 + +# overwrite bootstrap.sh +COPY bootstrap.sh /bootstrap.sh \ No newline at end of file diff --git a/mod/haproxy/bootstrap.sh b/mod/haproxy/bootstrap.sh new file mode 100644 index 0000000..c17e9f9 --- /dev/null +++ b/mod/haproxy/bootstrap.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash + +set -e + +# save container environment variables to use it +# in cron scripts + +declare -p | grep -Ev '^declare -[[:alpha:]]*r' > /container.env + +# when used with an IP, we'll also disable certbot +if [[ "$CERT1" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + IGNORE_TLS_CERT_ERRORS=true +fi + +if [ "$IGNORE_TLS_CERT_ERRORS" ] && [ "$IGNORE_TLS_CERT_ERRORS" != "false" ]; then + # use self signed certificate + if [ ! -f /etc/haproxy/certs/haproxy-10.7.7.1.pem ]; then + mkdir -p /etc/haproxy/certs + # generate self signed certificate + openssl req -x509 -nodes -days 700 -newkey rsa:2048 \ + -keyout /tmp/domain.key -out /tmp/domain.crt \ + -subj "/C=CA/ST=Quebec/L=Montreal/O=BigBlueButton Development/OU=bbb-docker/CN=10.7.7.1" + + cat /tmp/domain.key /tmp/domain.crt | tee /etc/haproxy/certs/haproxy-10.7.7.1.pem >/dev/null + fi +else + # obtain certificates from lets encrypt + /certs.sh +fi +supervisord -c /etc/supervisord.conf -n \ No newline at end of file diff --git a/mod/haproxy/protocolmap b/mod/haproxy/protocolmap new file mode 100644 index 0000000..0f85dd0 --- /dev/null +++ b/mod/haproxy/protocolmap @@ -0,0 +1,52 @@ +a nginx +b nginx +c nginx +d nginx +e nginx +f nginx +g nginx +h nginx +i nginx +j nginx +k nginx +l nginx +m nginx +n nginx +o nginx +p nginx +q nginx +r nginx +s nginx +t nginx +u nginx +v nginx +w nginx +x nginx +y nginx +z nginx +A nginx +B nginx +C nginx +D nginx +E nginx +F nginx +G nginx +H nginx +I nginx +J nginx +K nginx +L nginx +M nginx +N nginx +O nginx +P nginx +Q nginx +R nginx +S nginx +T nginx +U nginx +V nginx +W nginx +X nginx +Y nginx +Z nginx \ No newline at end of file diff --git a/mod/html5-dev/Dockerfile b/mod/html5-dev/Dockerfile new file mode 100644 index 0000000..474410a --- /dev/null +++ b/mod/html5-dev/Dockerfile @@ -0,0 +1,13 @@ +ARG BBB_BUILD_TAG +FROM bigbluebutton/bbb-build:$BBB_BUILD_TAG + + +# use /tmp as home dir as writeable directory for whatever UID we get +ENV HOME /tmp + +# allow all user to access .nvm in root +RUN chmod 755 /root + +WORKDIR /app +COPY /entrypoint.sh /entrypoint.sh +ENTRYPOINT /entrypoint.sh diff --git a/mod/html5-dev/entrypoint.sh b/mod/html5-dev/entrypoint.sh new file mode 100644 index 0000000..09c1ee1 --- /dev/null +++ b/mod/html5-dev/entrypoint.sh @@ -0,0 +1,11 @@ +set -e + +# enable nvm +. /root/.nvm/nvm.sh + +if [ -n "$1" ]; then + exec "$@" +else + npm install + npm start -- --host 0.0.0.0 +fi \ No newline at end of file diff --git a/mod/initdb.sh b/mod/initdb.sh new file mode 100644 index 0000000..fb0560e --- /dev/null +++ b/mod/initdb.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +set -e +set -u + +function create_user_and_database() { + local database=$1 + echo " Creating user and database '$database'" + psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" <<-EOSQL + CREATE DATABASE $database; + GRANT ALL PRIVILEGES ON DATABASE $database TO $POSTGRES_USER; +EOSQL +} + +if [ -n "$POSTGRES_MULTIPLE_DATABASES" ]; then + echo "Multiple database creation requested: $POSTGRES_MULTIPLE_DATABASES" + for db in $(echo $POSTGRES_MULTIPLE_DATABASES | tr ',' ' '); do + create_user_and_database $db + done + echo "Multiple databases created" +fi \ No newline at end of file diff --git a/mod/livekit/livekit.yaml b/mod/livekit/livekit.yaml new file mode 100644 index 0000000..55653f3 --- /dev/null +++ b/mod/livekit/livekit.yaml @@ -0,0 +1,15 @@ +port: 7880 +log_level: debug +# when enabled, LiveKit will expose prometheus metrics on :6789/metrics +#prometheus_port: 6789 +rtc: + port_range_start: 16384 + port_range_end: 32768 + use_external_ip: false +redis: + # redis is recommended for production deploys + address: redis:6379 + +keys: + # TODO: change keys + TEST: TEST diff --git a/mod/nginx.conf b/mod/nginx.conf new file mode 100644 index 0000000..0b06b1f --- /dev/null +++ b/mod/nginx.conf @@ -0,0 +1,53 @@ +user nginx; +worker_processes auto; + +error_log /var/log/nginx/error.log warn; +pid /var/run/nginx.pid; + +worker_rlimit_nofile 20000; + +events { + worker_connections 10000; +} + + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /var/log/nginx/access.log main; + + sendfile on; + #tcp_nopush on; + + keepalive_timeout 65; + + #gzip on; + + include /etc/nginx/conf.d/*.conf; + + + server { + # additional server only used for greenlight in dev mode + # allows it to use the BBB API without failing + # due to the self signed certificates + # + # all other requests (e.g. /join) is then redirected + listen 48083 http2; + + location /bigbluebutton/api/join { + return 301 https://haproxy$request_uri; + } + location /bigbluebutton/api { + proxy_pass http://127.0.0.1:48087; + } + location / { + return 301 https://haproxy$request_uri; + } + } + +} diff --git a/mod/nginx/Dockerfile b/mod/nginx/Dockerfile new file mode 100644 index 0000000..dc92c41 --- /dev/null +++ b/mod/nginx/Dockerfile @@ -0,0 +1,49 @@ +ARG BBB_BUILD_TAG +FROM bigbluebutton/bbb-build:$BBB_BUILD_TAG AS builder-learning-dashboard +COPY --from=src-learning-dashboard / /bbb-learning-dashboard +RUN cd /bbb-learning-dashboard && npm ci && npm run build + + +FROM bigbluebutton/bbb-build:$BBB_BUILD_TAG AS builder-playback +COPY --from=src-playback / /bbb-playback +RUN cd /bbb-playback && npm install && npm run-script build + +FROM bigbluebutton/bbb-build:$BBB_BUILD_TAG AS builder-html5 +COPY --from=src-html5 / /source +RUN cd /source && CI=true npm ci +RUN cd /source && DISABLE_ESLINT_PLUGIN=true npm run build-safari && npm run build +RUN cd /source/dist && \ + HASH=$(ls | grep -Eo 'bundle\.[a-f0-9]{20}\.js' | head -n 1 | grep -Eo '[a-f0-9]{20}') && \ + if [ -z "$HASH" ]; then \ + echo "Bundle hash not found."; \ + else \ + for FILE in *.safari.js *.safari.js.map; do \ + if [[ "$FILE" == *"$HASH"* ]]; then \ + continue; \ + fi; \ + PREFIX="${FILE%%.safari.js*}"; \ + SUFFIX="${FILE#*.safari.js}"; \ + NEW_NAME="${PREFIX}.${HASH}.safari.js${SUFFIX}"; \ + echo "Renaming $FILE → $NEW_NAME"; \ + mv "$FILE" "$NEW_NAME"; \ + done; \ + fi + +RUN find /source/dist -name '*.js' -exec gzip -k -f -9 '{}' \; \ + && find /source/dist -name '*.css' -exec gzip -k -f -9 '{}' \; \ + && find /source/dist -name '*.wasm' -exec gzip -k -f -9 '{}' \; + +RUN sed -i "s/?v=VERSION/?v=$BBB_BUILD_TAG/g" /source/dist/index.html && \ + sed -i "s/?v=VERSION/?v=$BBB_BUILD_TAG/g" /source/dist/stylesheets/fonts.css + +# -------------------- + +FROM nginx:1.29-alpine + +COPY --from=builder-learning-dashboard /bbb-learning-dashboard/build /www/learning-analytics-dashboard/ +COPY --from=builder-playback /bbb-playback/build /www/playback/presentation/2.3 +COPY --from=builder-html5 /source/dist /usr/share/bigbluebutton/html5-client/ +COPY ./bbb /etc/nginx/bbb +COPY ./bigbluebutton /etc/nginx/conf.d/default.conf +COPY ./bbb-graphql-client-settings-cache.conf /etc/nginx/conf.d/bbb-graphql-client-settings-cache.conf +COPY ./nginx.conf /etc/nginx/nginx.conf diff --git a/mod/nginx/bbb-graphql-client-settings-cache.conf b/mod/nginx/bbb-graphql-client-settings-cache.conf new file mode 100644 index 0000000..fbea8b7 --- /dev/null +++ b/mod/nginx/bbb-graphql-client-settings-cache.conf @@ -0,0 +1 @@ +proxy_cache_path /tmp/hasura-client-settings-cache levels=1:2 keys_zone=client_settings_cache:64m inactive=2880m use_temp_path=off; \ No newline at end of file diff --git a/mod/nginx/bbb/bbb-html5.nginx b/mod/nginx/bbb/bbb-html5.nginx new file mode 100644 index 0000000..fc73a84 --- /dev/null +++ b/mod/nginx/bbb/bbb-html5.nginx @@ -0,0 +1,21 @@ +# running in production (static assets) +location /html5client { + gzip_static on; + alias /usr/share/bigbluebutton/html5-client/; + index index.html; + try_files $uri $uri/ =404; +} + +location /html5client/locales { + alias /usr/share/bigbluebutton/html5-client/locales; + autoindex on; + autoindex_format json; +} + +location /html5client/wasm { + types { + application/wasm wasm; + } + gzip_static on; + alias /usr/share/bigbluebutton/html5-client/wasm; +} \ No newline at end of file diff --git a/mod/nginx/bbb/demo.nginx b/mod/nginx/bbb/demo.nginx new file mode 100644 index 0000000..8b68775 --- /dev/null +++ b/mod/nginx/bbb/demo.nginx @@ -0,0 +1,26 @@ + # Forward request to /demo to tomcat. This is for + # the BigBlueButton api demos. + location = /demo/ { + return 301 /demo/demo1.jsp; + } + location /demo { + proxy_pass http://host.docker.internal:8001; + proxy_redirect default; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + + # Allow 30M uploaded presentation document. + client_max_body_size 30m; + client_body_buffer_size 128k; + + proxy_connect_timeout 90; + proxy_send_timeout 90; + proxy_read_timeout 90; + + proxy_buffer_size 4k; + proxy_buffers 4 32k; + proxy_busy_buffers_size 64k; + proxy_temp_file_write_size 64k; + + include fastcgi_params; + } + diff --git a/mod/nginx/bbb/graphql.nginx b/mod/nginx/bbb/graphql.nginx new file mode 100644 index 0000000..be1f8cd --- /dev/null +++ b/mod/nginx/bbb/graphql.nginx @@ -0,0 +1,71 @@ +# Websocket connection +location /graphql { + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "Upgrade"; + proxy_set_header Host $host; + #proxy_pass http://bbb-graphql-server:8085; #Hasura (it requires to change the location to /v1/graphql) + proxy_pass http://bbb-graphql-middleware:8378; #Graphql Middleware +} + +#DEPRECATED: +#This endpoint is being replaced by /api/rest/meetingStaticData (which contain clientSettings and more) +#It will be removed in BBB 3.1 +location /api/rest/clientSettings { + auth_request /bigbluebutton/connection/checkGraphqlAuthorization; + auth_request_set $meeting_id $sent_http_meeting_id; + + proxy_cache client_settings_cache; + proxy_cache_key "$uri|$meeting_id"; + proxy_cache_use_stale updating; + proxy_cache_valid 24h; + proxy_cache_lock on; + proxy_cache_lock_timeout 5s; # how long other requests may wait for the first one holding the cache lock + proxy_cache_lock_age 10s; # consider the lock stale after this time (prevents a stuck lock if upstream hangs) + # proxy_cache_background_update on; # optional: serve stale while refreshing the cache in background + + add_header X-Cached $upstream_cache_status; + + proxy_http_version 1.1; + proxy_set_header Connection ""; + proxy_connect_timeout 3s; # max time to establish TCP connection to Hasura + proxy_send_timeout 15s; # max time to send the request to Hasura + proxy_read_timeout 30s; # max time to wait for Hasura’s response + proxy_set_header Host $host; + proxy_pass http://bbb-graphql-server:8085; #Hasura +} + +#Set cache system for meeting static data +location /api/rest/meetingStaticData { + auth_request /bigbluebutton/connection/checkGraphqlAuthorization; + auth_request_set $meeting_id $sent_http_meeting_id; + + proxy_cache client_settings_cache; + proxy_cache_key "$uri|$meeting_id"; + proxy_cache_use_stale updating; + proxy_cache_valid 24h; + proxy_cache_lock on; + proxy_cache_lock_timeout 5s; # how long other requests may wait for the first one holding the cache lock + proxy_cache_lock_age 10s; # consider the lock stale after this time (prevents a stuck lock if upstream hangs) + # proxy_cache_background_update on; # optional: serve stale while refreshing the cache in background + + add_header X-Cached $upstream_cache_status; + + proxy_http_version 1.1; + proxy_set_header Connection ""; + proxy_connect_timeout 3s; # max time to establish TCP connection to Hasura + proxy_send_timeout 15s; # max time to send the request to Hasura + proxy_read_timeout 30s; # max time to wait for Hasura’s response + proxy_set_header Host $host; + proxy_pass http://bbb-graphql-server:8085; #Hasura +} + +location /api/rest/userMetadata { + auth_request /bigbluebutton/connection/checkGraphqlAuthorization; + auth_request_set $meeting_id $sent_http_meeting_id; + + proxy_http_version 1.1; + proxy_set_header Connection ""; + proxy_set_header Host $host; + proxy_pass http://bbb-graphql-server:8085; #Hasura +} diff --git a/mod/nginx/bbb/learning-dashboard.nginx b/mod/nginx/bbb/learning-dashboard.nginx new file mode 100644 index 0000000..f0b4fc3 --- /dev/null +++ b/mod/nginx/bbb/learning-dashboard.nginx @@ -0,0 +1,4 @@ +location /learning-analytics-dashboard/ { + alias /www/learning-analytics-dashboard/; + autoindex off; +} diff --git a/mod/nginx/bbb/livekit.nginx b/mod/nginx/bbb/livekit.nginx new file mode 100644 index 0000000..c295562 --- /dev/null +++ b/mod/nginx/bbb/livekit.nginx @@ -0,0 +1,11 @@ +location /livekit/ { + proxy_pass http://127.0.0.1:7880/; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "Upgrade"; + + proxy_read_timeout 60s; + proxy_send_timeout 60s; + client_body_timeout 60s; + send_timeout 60s; +} diff --git a/mod/nginx/bbb/loadbalancer.nginx b/mod/nginx/bbb/loadbalancer.nginx new file mode 100644 index 0000000..e254118 --- /dev/null +++ b/mod/nginx/bbb/loadbalancer.nginx @@ -0,0 +1,4 @@ +# If you run this BBB node behind a proxy loadbalancer set the hostname of +# the loadbalancer here. This will be used to add CORS headers so requesting +# slides and API calls won't fail +set $bbb_loadbalancer_node ''; diff --git a/mod/nginx/bbb/notes-playback.nginx b/mod/nginx/bbb/notes-playback.nginx new file mode 100644 index 0000000..16f7c43 --- /dev/null +++ b/mod/nginx/bbb/notes-playback.nginx @@ -0,0 +1,4 @@ +location /notes { + root /var/bigbluebutton/published; + index index.html index.htm; +} \ No newline at end of file diff --git a/mod/nginx/bbb/notes.nginx b/mod/nginx/bbb/notes.nginx new file mode 100644 index 0000000..4965d2d --- /dev/null +++ b/mod/nginx/bbb/notes.nginx @@ -0,0 +1,77 @@ +# https://github.com/ether/etherpad-lite/wiki/How-to-put-Etherpad-Lite-behind-a-reverse-Proxy +location /pad/p/ { + # Avoid setting the user name from the embedded URL + if ($arg_userName) { + return 401; + } + + rewrite /pad/p/(.*) /p/$1 break; + rewrite ^/pad/p$ /pad/p/ permanent; + proxy_pass http://etherpad:9001/p; + proxy_pass_header Server; + proxy_redirect /p /pad/p; + proxy_set_header Host $host; + proxy_buffering off; + + proxy_set_header X-Real-IP $remote_addr; # http://wiki.nginx.org/HttpProxyModule + proxy_set_header X-Forwarded-For $remote_addr; # EP logs to show the actual remote IP + proxy_set_header X-Forwarded-Proto $real_scheme; # for EP to set secure cookie flag when https is used + proxy_http_version 1.1; + + auth_request /bigbluebutton/connection/checkAuthorization; + auth_request_set $auth_status $upstream_status; +} + +location /pad/auth_session { + rewrite /pad/auth_session(.*) /auth_session$1 break; + proxy_pass http://etherpad:9001/; + proxy_pass_header Server; + proxy_set_header Host $host; + proxy_buffering off; + auth_request /bigbluebutton/connection/checkAuthorization; + auth_request_set $auth_status $upstream_status; +} + +location /pad { + rewrite /pad/(.*) /$1 break; + rewrite ^/pad$ /pad/ permanent; + proxy_pass http://etherpad:9001/; + proxy_pass_header Server; + proxy_redirect / /pad/; + proxy_set_header Host $host; + proxy_buffering off; +} + +location /pad/socket.io/socket.io.js { + rewrite /pad/socket.io/socket.io.js /socket.io/socket.io.js break; + proxy_pass http://etherpad:9001/; + proxy_set_header Host $host; + proxy_buffering off; +} + +location /pad/socket.io { + rewrite /pad/socket.io/(.*) /socket.io/$1 break; + proxy_pass http://etherpad:9001/; + proxy_redirect / /pad/; + proxy_set_header Host $host; + proxy_buffering off; + proxy_set_header X-Real-IP $remote_addr; # http://wiki.nginx.org/HttpProxyModule + proxy_set_header X-Forwarded-For $remote_addr; # EP logs to show the actual remote IP + proxy_set_header X-Forwarded-Proto $real_scheme; # for EP to set secure cookie flag when https is used + proxy_set_header Host $host; # pass the host header + proxy_http_version 1.1; # recommended with keepalive connections + # WebSocket proxying - from http://nginx.org/en/docs/http/websocket.html + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "Upgrade"; + + auth_request /bigbluebutton/connection/checkAuthorization; + auth_request_set $auth_status $upstream_status; +} + +location /static { + rewrite /static/(.*) /static/$1 break; + proxy_pass http://etherpad:9001/; + proxy_set_header Host $host; + proxy_buffering off; +} + diff --git a/mod/nginx/bbb/playback-video.nginx b/mod/nginx/bbb/playback-video.nginx new file mode 100644 index 0000000..39d0c68 --- /dev/null +++ b/mod/nginx/bbb/playback-video.nginx @@ -0,0 +1,21 @@ +# This file is part of BigBlueButton. +# +# Copyright © BigBlueButton Inc. and by respective authors. +# +# BigBlueButton is free software: you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by the +# Free Software Foundation, either version 3.0 of the License, or (at your +# option) any later version. +# +# BigBlueButton is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with BigBlueButton. If not, see . + +location /playback/video/ { + alias /var/bigbluebutton/published/video/; + index index.html index.htm; +} diff --git a/mod/nginx/bbb/podcast.nginx b/mod/nginx/bbb/podcast.nginx new file mode 100644 index 0000000..7d8c0a8 --- /dev/null +++ b/mod/nginx/bbb/podcast.nginx @@ -0,0 +1,22 @@ +# +# BigBlueButton open source conferencing system - http://www.bigbluebutton.org/ +# +# Copyright (c) 2012 BigBlueButton Inc. and by respective authors (see below). +# +# This program is free software; you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free Software +# Foundation; either version 3.0 of the License, or (at your option) any later +# version. +# +# BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +# PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License along +# with BigBlueButton; if not, see . +# + + location /podcast { + root /var/bigbluebutton/published; + index index.html index.htm; + } \ No newline at end of file diff --git a/mod/nginx/bbb/presentation-slides.nginx b/mod/nginx/bbb/presentation-slides.nginx new file mode 100644 index 0000000..d90adeb --- /dev/null +++ b/mod/nginx/bbb/presentation-slides.nginx @@ -0,0 +1,46 @@ +# +# BigBlueButton open source conferencing system - http://www.bigbluebutton.org/ +# +# Copyright (c) 2012 BigBlueButton Inc. and by respective authors (see below). +# +# This program is free software; you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free Software +# Foundation; either version 3.0 of the License, or (at your option) any later +# version. +# +# BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +# PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License along +# with BigBlueButton; if not, see . +# + +# Have nginx serve the presentation slides instead of tomcat as large files +# causes tomcat to OOM. (ralam sept 20, 2018) + + location ~^\/bigbluebutton\/presentation\/(?[A-Za-z0-9\-]+)\/(?[A-Za-z0-9\-]+)\/(?[A-Za-z0-9\-]+)\/svg\/(?\d+)$ { + default_type image/svg+xml; + alias /var/bigbluebutton/$meeting_id_2/$meeting_id_2/$pres_id/svgs/slide$page_num.svg; + add_header 'Access-Control-Allow-Origin' '*' always; + } + + location ~^\/bigbluebutton\/presentation\/(?[A-Za-z0-9\-]+)\/(?[A-Za-z0-9\-]+)\/(?[A-Za-z0-9\-]+)\/pdf\/(?[A-Za-z0-9]+)\/annotated_slides.pdf$ { + default_type application/pdf; + alias /var/bigbluebutton/$meeting_id_2/$meeting_id_2/$pres_id/pdfs/$job_id/annotated_slides.pdf; + add_header 'Access-Control-Allow-Origin' '*' always; + } + + location ~^\/bigbluebutton\/presentation\/(?[A-Za-z0-9\-]+)\/(?[A-Za-z0-9\-]+)\/(?[A-Za-z0-9\-]+)\/thumbnail\/(?\d+)$ { + default_type image/png; + alias /var/bigbluebutton/$meeting_id_2/$meeting_id_2/$pres_id/thumbnails/thumb-$page_num.png; + add_header 'Access-Control-Allow-Origin' '*' always; + } + + location ~^\/bigbluebutton\/presentation\/(?[A-Za-z0-9\-]+)\/(?[A-Za-z0-9\-]+)\/(?[A-Za-z0-9\-]+)\/textfiles\/(?\d+)$ { + default_type text/plain; + alias /var/bigbluebutton/$meeting_id_2/$meeting_id_2/$pres_id/textfiles/slide-$page_num.txt; + add_header 'Access-Control-Allow-Origin' '*' always; + } + + diff --git a/mod/nginx/bbb/presentation.nginx b/mod/nginx/bbb/presentation.nginx new file mode 100644 index 0000000..2199332 --- /dev/null +++ b/mod/nginx/bbb/presentation.nginx @@ -0,0 +1,28 @@ +# +# BigBlueButton open source conferencing system - http://www.bigbluebutton.org/ +# +# Copyright (c) 2012 BigBlueButton Inc. and by respective authors (see below). +# +# This program is free software; you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free Software +# Foundation; either version 3.0 of the License, or (at your option) any later +# version. +# +# BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +# PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License along +# with BigBlueButton; if not, see . +# + location /playback/presentation/2.0/playback.html { + return 301 /playback/presentation/2.3/$arg_meetingId?$query_string; + } + location /playback/presentation/2.3 { + try_files $uri /playback/presentation/2.3/index.html; + } + + location /presentation { + root /var/bigbluebutton/published; + index index.html index.htm; + } diff --git a/mod/nginx/bbb/recording-screenshare.nginx b/mod/nginx/bbb/recording-screenshare.nginx new file mode 100644 index 0000000..d639110 --- /dev/null +++ b/mod/nginx/bbb/recording-screenshare.nginx @@ -0,0 +1,22 @@ +# +# BigBlueButton open source conferencing system - http://www.bigbluebutton.org/ +# +# Copyright (c) 2012 BigBlueButton Inc. and by respective authors (see below). +# +# This program is free software; you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free Software +# Foundation; either version 3.0 of the License, or (at your option) any later +# version. +# +# BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +# PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License along +# with BigBlueButton; if not, see . +# + + location /recording/screenshare { + alias /var/bigbluebutton/published/screenshare; + index index.html index.htm; + } \ No newline at end of file diff --git a/mod/nginx/bbb/slides.nginx b/mod/nginx/bbb/slides.nginx new file mode 100644 index 0000000..1c3d170 --- /dev/null +++ b/mod/nginx/bbb/slides.nginx @@ -0,0 +1,28 @@ + +# +# BigBlueButton open source conferencing system - http://www.bigbluebutton.org/ +# +# Copyright (c) 2012 BigBlueButton Inc. and by respective authors (see below). +# +# This program is free software; you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free Software +# Foundation; either version 3.0 of the License, or (at your option) any later +# version. +# +# BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +# PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License along +# with BigBlueButton; if not, see . +# + location /playback/slides { + root /var/bigbluebutton; + index index.html index.htm; + } + + location /slides { + root /var/bigbluebutton/published; + index index.html index.htm; + } + diff --git a/mod/nginx/bbb/web.nginx b/mod/nginx/bbb/web.nginx new file mode 100644 index 0000000..d854219 --- /dev/null +++ b/mod/nginx/bbb/web.nginx @@ -0,0 +1,186 @@ + # Handle request to bbb-web running within a SpringBoot Tomcat embedded servlet container. This is for BBB-API and Presentation. + location /bigbluebutton { + proxy_http_version 1.1; + + location /bigbluebutton { + proxy_pass http://bbb-web:8090; + proxy_redirect default; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + + # Workaround IE refusal to set cookies in iframe + add_header P3P 'CP="No P3P policy available"'; + } + + + location ~ "^\/bigbluebutton\/presentation\/(?[a-zA-Z0-9_-]+)/upload$" { + proxy_pass http://bbb-web:8090; + proxy_redirect default; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + + # Workaround IE refusal to set cookies in iframe + add_header P3P 'CP="No P3P policy available"'; + + # high limit for presentation as bbb-web will reject upload if larger than configured + client_max_body_size 1000m; + client_body_buffer_size 128k; + + proxy_connect_timeout 90; + proxy_send_timeout 90; + proxy_read_timeout 90; + + proxy_buffer_size 4k; + proxy_buffers 4 32k; + proxy_busy_buffers_size 64k; + proxy_temp_file_write_size 64k; + + include fastcgi_params; + + proxy_request_buffering off; + + # Send a sub-request to allow bbb-web to refuse before loading + # If file is larger than configured bbb-web will return with code 403 and Header: x-file-too-large = 1 + auth_request /bigbluebutton/presentation/checkPresentation; + error_page 403 = @error403; + auth_request_set $file_too_large_header $upstream_http_x_file_too_large; + + } + + location /bigbluebutton/presentation/download { + return 404; + } + + location ~ "^/bigbluebutton/presentation/download\/[0-9a-f]+-[0-9]+/[0-9a-f]+-[0-9]+$" { + if ($arg_presFilename !~ "^[0-9a-f]+-[0-9]+\.[0-9a-zA-Z]+$") { + return 404; + } + proxy_pass http://bbb-web:8090$uri$is_args$args; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + # Workaround IE refusal to set cookies in iframe + add_header P3P 'CP="No P3P policy available"'; + } + + location = /bigbluebutton/presentation/checkPresentation { + proxy_pass http://bbb-web:8090; + proxy_redirect default; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + + proxy_set_header X-Presentation-Token $prestoken; + proxy_set_header X-Original-URI $request_uri; + proxy_set_header Content-Length ""; + proxy_set_header X-Original-Content-Length $http_content_length; + proxy_set_header X-Original-Method $request_method; + + # high limit for presentation as bbb-web will reject upload if larger than configured + client_max_body_size 1000m; + client_body_buffer_size 128k; + + proxy_pass_request_body off; + proxy_request_buffering off; + + } + + # To check connection authentication, include: + # auth_request /bigbluebutton/connection/checkAuthorization; + # auth_request_set $auth_status $upstream_status; + # + # and make sure to add sessionToken param in the request URI + location = /bigbluebutton/connection/checkAuthorization { + internal; + proxy_pass http://bbb-web:8090; + proxy_pass_request_body off; + proxy_set_header Content-Length ""; + proxy_set_header X-Original-URI $request_uri; + } + + location = /bigbluebutton/connection/checkGraphqlAuthorization { + internal; + proxy_pass http://bbb-web:8090; + proxy_pass_request_body off; + proxy_set_header Content-Length ""; + proxy_set_header X-Original-URI $request_uri; + # this is required for CORS preflight checks in cluster setup + proxy_set_header X-Original-Method $request_method; + } + + location = /bigbluebutton/connection/legacyCheckAuthorization { + internal; + proxy_pass http://bbb-web:8090; + proxy_pass_request_body off; + proxy_set_header Content-Length ""; + proxy_set_header X-Original-URI $request_uri; + } + + location = /bigbluebutton/connection/validatePad { + internal; + proxy_pass http://bbb-web:8090; + proxy_pass_request_body off; + proxy_set_header Content-Length ""; + proxy_set_header X-Original-URI $request_uri; + } + + location ~ "^/bigbluebutton\/textTrack\/(?[a-zA-Z0-9]+)\/(?[a-zA-Z0-9_-]+)\/(?.+)$" { + # Workaround IE refusal to set cookies in iframe + add_header P3P 'CP="No P3P policy available"'; + + # Allow 30M uploaded presentation document. + client_max_body_size 30m; + client_body_buffer_size 128k; + + proxy_connect_timeout 90; + proxy_send_timeout 90; + proxy_read_timeout 90; + + proxy_buffer_size 4k; + proxy_buffers 4 32k; + proxy_busy_buffers_size 64k; + proxy_temp_file_write_size 64k; + + include fastcgi_params; + + proxy_request_buffering off; + + # Send a sub-request to allow bbb-web to refuse before loading + auth_request /bigbluebutton/textTrack/validateAuthToken; + + default_type text/plain; + alias /var/bigbluebutton/captions/$recordId/$textTrack; + + } + + location = /bigbluebutton/textTrack/validateAuthToken { + internal; + proxy_pass http://bbb-web:8090; + proxy_redirect default; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + + proxy_set_header X-textTrack-token $textTrackToken; + proxy_set_header X-textTrack-recordId $recordId; + proxy_set_header X-textTrack-track $textTrack; + proxy_set_header X-Original-URI $request_uri; + } + + location /bigbluebutton/rtt-check { + default_type text/plain; + add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0"; + add_header Pragma "no-cache"; + add_header Expires "0"; + add_header X-Server-Epoch-Msec $msec; + add_header X-Request-Id $request_id; + + # this Header is required for cluster setups as the ping check is a + # CORS request. No cookies are required so we can just allow anyone + # to use this endpoint. + add_header 'Access-Control-Allow-Origin' '*'; + add_header 'Access-Control-Expose-Headers' 'X-Server-Epoch-Msec,X-Request-Id'; + return 200 ""; + } + + } + + location @error403 { + if ($file_too_large_header = '1') { + return 413; + } + + return 403; + } diff --git a/mod/nginx/bigbluebutton b/mod/nginx/bigbluebutton new file mode 100644 index 0000000..a7ff2f0 --- /dev/null +++ b/mod/nginx/bigbluebutton @@ -0,0 +1,85 @@ +server { + # proxied from HAProxy + listen 48082 http2 proxy_protocol; + listen 48081 proxy_protocol; + + # optional ports for other reverse proxies + listen 48087 default_server; + listen [::]:48087 default_server; + + server_name _; + access_log /dev/stdout; + absolute_redirect off; + root /www/; + + # This variable is used instead of $scheme by bigbluebutton nginx include + # files, so $scheme can be overridden in reverse-proxy configurations. + set $real_scheme $scheme; + + # opt-out of google's floc tracking + # https://www.eff.org/deeplinks/2021/03/googles-floc-terrible-idea + add_header Permissions-Policy "interest-cohort=()"; + + + # Include specific rules for record and playback + include /etc/nginx/bbb/*.nginx; + + # redirect old greenlight v2 room links + location ~ "/b/([a-z0-9\-]+)" { + return 302 /rooms/$1; + } + + # serve default.pdf from /www/ + location = /default.pdf { + try_files $uri =404; + } + + location / { + proxy_pass http://greenlight:3000; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For "127.0.0.1"; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Ssl on; + proxy_http_version 1.1; + client_max_body_size 1000m; + } + + location /cable { + proxy_pass http://greenlight:3000; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For "127.0.0.1"; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Ssl on; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "Upgrade"; + proxy_http_version 1.1; + proxy_read_timeout 6h; + proxy_send_timeout 6h; + client_body_timeout 6h; + send_timeout 6h; + } +} + +upstream hasura { + least_conn; + server bbb-graphql-server:8085; + # you might want to add more bbb-graphql-server@ instances to balance the + # load to multiple bbb-graphql-server instances. Execute + # `systemctl enable --now bbb-graphql-server@8086` and uncomment the + # following line: + # server 127.0.0.1:8086; +} +server { + listen 8185; + + root /var/www/html; + + location / { + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "Upgrade"; + proxy_set_header Host $host; + + proxy_pass http://hasura; + } +} \ No newline at end of file diff --git a/mod/nginx/default.pdf b/mod/nginx/default.pdf new file mode 100644 index 0000000000000000000000000000000000000000..8a23582439fe4bf0703c51fc7451d28059b7a410 GIT binary patch literal 42107 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"${LOGFILE}" + +# Find the name of recordings container in order to access `bbb-record` utility +BBB_RECORDINGS_CONTAINER_NAME=$(docker ps --filter "name=recordings" --filter "status=running" --format "{{.Names}}") +if [ $BBB_RECORDINGS_CONTAINER_NAME == "" ]; then + echo "$(date --rfc-3339=seconds) ERROR: recordings container is not running" >>"${LOGFILE}" + exit 1 +fi + +for donefile in /var/bigbluebutton/recording/status/published/*-presentation.done ; do + MTIME=$(stat -c %Y "${donefile}") + # Check the age of the recording + if [ $(( ( $NOW - $MTIME ) / 86400 )) -gt $RECORDING_MAX_AGE_DAYS ]; then + MEETING_ID=$(basename "${donefile}") + MEETING_ID=${MEETING_ID%-presentation.done} + echo "${MEETING_ID}" >> "${LOGFILE}" + + docker exec "$BBB_RECORDINGS_CONTAINER_NAME" bbb-record --delete "${MEETING_ID}" >>"${LOGFILE}" + fi +done + +for eventsfile in /var/bigbluebutton/recording/raw/*/events.xml ; do + MTIME=$(stat -c %Y "${eventsfile}") + # Check the age of the recording + if [ $(( ( $NOW - $MTIME ) / 86400 )) -gt $RECORDING_MAX_AGE_DAYS ]; then + MEETING_ID="${eventsfile%/events.xml}" + MEETING_ID="${MEETING_ID##*/}" + echo "${MEETING_ID}" >> "${LOGFILE}" + + docker exec "$BBB_RECORDINGS_CONTAINER_NAME" bbb-record --delete "${MEETING_ID}" >>"${LOGFILE}" + fi +done diff --git a/mod/periodic/entrypoint.sh b/mod/periodic/entrypoint.sh new file mode 100644 index 0000000..cc3bbb0 --- /dev/null +++ b/mod/periodic/entrypoint.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +# +# How N days back to keep files +# +history=5 + +while : +do + + # resync freeswitch + /bbb-resync-freeswitch + + # delete presentations older than N days + find /var/bigbluebutton/ -maxdepth 1 -type d -name "*-[0-9]*" -mtime +$history -exec rm -rf '{}' + + + # delete recordings older than $RECORDING_MAX_AGE_DAYS + if [ "$ENABLE_RECORDING" == true ] && [ "$REMOVE_OLD_RECORDING" == true ]; then + /bbb-remove-old-recordings + fi + + sleep 30m +done diff --git a/mod/recordings/Dockerfile b/mod/recordings/Dockerfile new file mode 100644 index 0000000..cd6fe87 --- /dev/null +++ b/mod/recordings/Dockerfile @@ -0,0 +1,126 @@ +ARG BBB_BUILD_TAG +FROM bigbluebutton/bbb-build:$BBB_BUILD_TAG AS builder + +# compile and install mkclean +RUN cd /tmp \ + && wget https://sourceforge.net/projects/matroska/files/mkclean/mkclean-0.8.10.tar.bz2/download -O mkclean-0.8.10.tar.bz2 \ + && tar -xf /tmp/mkclean-0.8.10.tar.bz2 \ + && cd /tmp/mkclean-0.8.10 \ + && sed -i 's/\r//g' ./mkclean/configure.compiled \ + && ./mkclean/configure.compiled \ + && make -C mkclean \ + && cp ./release/gcc_linux_x64/mkclean /usr/bin/mkclean \ + && rm -r /tmp/mkclean-* + + +FROM ruby:3.2-slim-trixie + +# install apt dependencies +RUN apt-get update && apt-get install -y \ + wget \ + subversion \ + rsync \ + build-essential \ + libsystemd-dev \ + python3 \ + python3-attr \ + python3-cairo \ + python3-gi \ + python3-gi-cairo \ + python3-lxml \ + python3-icu \ + python3-pyinotify \ + python3-sortedcollections \ + python3-packaging \ + gir1.2-pangocairo-1.0 \ + gir1.2-poppler-0.18 \ + gir1.2-gtk-3.0 \ + gir1.2-gdkpixbuf-2.0 \ + fonts-croscore \ + ffmpeg \ + poppler-utils \ + imagemagick \ + supervisor \ + unzip \ + locales \ + locales-all +# TODO: missing packages + +ENV LC_ALL en_US.UTF-8 +ENV LANG en_US.UTF-8 +ENV LANGUAGE en_US.UTF-8 + +COPY --from=builder /usr/bin/mkclean /usr/bin/mkclean + +# add dockerize +ENV DOCKERIZE_VERSION v0.6.1 +RUN wget -q https://github.com/jwilder/dockerize/releases/download/$DOCKERIZE_VERSION/dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz \ + && tar -C /usr/local/bin -xzvf dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz \ + && rm dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz + +# add yq for bbb-record +RUN wget -q https://github.com/mikefarah/yq/releases/download/v4.45.1/yq_linux_amd64 -O /usr/bin/yq \ + && chmod +x /usr/bin/yq + +RUN mkdir -p \ + /usr/local/bigbluebutton \ + /usr/local/bigbluebutton/core \ + /etc/bigbluebutton \ + /etc/bigbluebutton/recording + +# dependency for bbb-presentation-video +RUN wget -q -O /tmp/python3-perfect-freehand.deb https://github.com/bigbluebutton/perfect-freehand-python/releases/download/1.2.0/python3-perfect-freehand_1.2.0_all.deb && \ + dpkg -i /tmp/python3-perfect-freehand.deb && \ + rm /tmp/python3-perfect-freehand.deb + +ARG TAG_RECORDINGS + +COPY --from=record-core / /usr/local/bigbluebutton/core + +# add bbb-playback-presentation scripts +COPY --from=presentation /scripts /usr/local/bigbluebutton/core/scripts/ + +# install ruby dependencies +RUN cd /usr/local/bigbluebutton/core \ + && gem install builder \ + && gem install bundler -v 2.4.22 --no-document \ + && bundle config set --local deployment true \ + && bundle install \ + && bundle clean \ + && rm -r vendor/bundle/ruby/*/cache \ + && find vendor/bundle -name '*.o' -delete + +# log to file instead of journald +RUN sed -i 's|Journald::Logger\.new.*|Logger.new("/var/log/bigbluebutton/recording.log")|g' /usr/local/bigbluebutton/core/lib/recordandplayback.rb && \ + sed -i 's|Journald::Logger\.new.*|Logger.new("/var/log/bigbluebutton/recording.log")|g' /usr/local/bigbluebutton/core/scripts/rap-caption-inbox.rb && \ + sed -i 's|Logger\.new.*|Logger.new("/var/log/bigbluebutton/recording.log")|g' /usr/local/bigbluebutton/core/scripts/rap-process-worker.rb && \ + sed -i 's|Logger\.new.*|Logger.new("/var/log/bigbluebutton/recording.log")|g' /usr/local/bigbluebutton/core/scripts/archive/archive.rb && \ + sed -i 's|Logger\.new.*|Logger.new("/var/log/bigbluebutton/recording.log")|g' /usr/local/bigbluebutton/core/scripts/publish/presentation.rb && \ + sed -i 's|Logger\.new.*|Logger.new("/var/log/bigbluebutton/recording.log")|g' /usr/local/bigbluebutton/core/scripts/utils/captions.rb && \ + sed -i 's|Logger\.new.*|Logger.new("/var/log/bigbluebutton/recording.log")|g' /usr/local/bigbluebutton/core/scripts/process/presentation.rb + +# add bbb-record with some adjustments so bbb-record works in this environment +COPY --from=bbb-conf /bin/bbb-record /usr/bin/bbb-record +RUN cd /usr/bin \ + && chmod +x /usr/bin/bbb-record \ + && sed -i 's/^BBB_WEB.*/BBB_WEB=""/' /usr/bin/bbb-record \ + && sed -i 's/systemctl.*//' /usr/bin/bbb-record \ + && echo "BIGBLUEBUTTON_RELEASE=$TAG_RECORDINGS" > /etc/bigbluebutton/bigbluebutton-release + +# change owner +# https://github.com/alangecker/bigbluebutton-docker/issues/63 +RUN chown -R 998:998 /usr/local/bigbluebutton + +ARG TAG_BBB_PRESENTATION_VIDEO +RUN cd /tmp/ && \ + wget -q -O bbb-presentation-video.zip "https://github.com/bigbluebutton/bbb-presentation-video/releases/download/${TAG_BBB_PRESENTATION_VIDEO}/ubuntu-22.04.zip" && \ + unzip -o bbb-presentation-video.zip -d bbb-presentation-video && \ + dpkg -i bbb-presentation-video/bbb-presentation-video*.deb && \ + rm -rf /tmp/* + +COPY bbb-web.properties /etc/bigbluebutton/bbb-web.properties.tmpl +COPY recording.yml /etc/bigbluebutton/recording/recording.yml.tmpl +COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf +COPY entrypoint.sh /entrypoint.sh + +ENTRYPOINT /entrypoint.sh diff --git a/mod/recordings/bbb-web.properties b/mod/recordings/bbb-web.properties new file mode 100644 index 0000000..e891d1c --- /dev/null +++ b/mod/recordings/bbb-web.properties @@ -0,0 +1,2 @@ +bigbluebutton.web.serverURL=https://{{ .Env.DOMAIN }} +securitySalt={{ .Env.SHARED_SECRET }} \ No newline at end of file diff --git a/mod/recordings/entrypoint.sh b/mod/recordings/entrypoint.sh new file mode 100644 index 0000000..2a70f42 --- /dev/null +++ b/mod/recordings/entrypoint.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +touch /var/log/bigbluebutton/recording.log +touch /var/log/bigbluebutton/bbb-web.log +touch /var/log/bigbluebutton/sanity.log +touch /var/log/bigbluebutton/post_publish.log +mkdir -p /var/log/bigbluebutton/presentation +chown -R 998:998 /var/log/bigbluebutton + +dockerize \ + -template /etc/bigbluebutton/recording/recording.yml.tmpl:/etc/bigbluebutton/recording/recording.yml \ + -template /etc/bigbluebutton/bbb-web.properties.tmpl:/etc/bigbluebutton/bbb-web.properties \ + -stdout /var/log/bigbluebutton/recording.log \ + -stdout /var/log/bigbluebutton/post_publish.log \ + -stdout /var/log/bigbluebutton/sanity.log \ + /usr/bin/supervisord --nodaemon \ No newline at end of file diff --git a/mod/recordings/recording.yml b/mod/recordings/recording.yml new file mode 100644 index 0000000..2e79bdf --- /dev/null +++ b/mod/recordings/recording.yml @@ -0,0 +1,4 @@ +redis_host: redis +notes_endpoint: http://bbb-pads:9002/p +playback_host: {{ .Env.DOMAIN }} +playback_protocol: https diff --git a/mod/recordings/supervisord.conf b/mod/recordings/supervisord.conf new file mode 100644 index 0000000..168999d --- /dev/null +++ b/mod/recordings/supervisord.conf @@ -0,0 +1,34 @@ +[supervisord] +user=root + +[program:rasque_workers] +command=bundle exec rake -f Rakefile resque:workers +directory=/usr/local/bigbluebutton/core/scripts +environment=QUEUE="rap:archive,rap:publish,rap:process,rap:sanity,rap:captions,rap:events",COUNT="1",HOME="/tmp" +user=998 +stdout_logfile=/dev/fd/1 +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/fd/2 +stderr_logfile_maxbytes=0 + + +[program:rap_starter] +command=bundle exec ruby /usr/local/bigbluebutton/core/scripts/rap-starter.rb +directory=/usr/local/bigbluebutton/core/scripts +environment=HOME="/tmp" +user=998 +stdout_logfile=/dev/fd/1 +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/fd/2 +stderr_logfile_maxbytes=0 + + +[program:rap_caption_inbox] +command=bundle exec ruby /usr/local/bigbluebutton/core/scripts/rap-caption-inbox.rb +directory=/usr/local/bigbluebutton/core/scripts +environment=HOME="/tmp" +user=998 +stdout_logfile=/dev/fd/1 +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/fd/2 +stderr_logfile_maxbytes=0 diff --git a/mod/webhooks.nginx b/mod/webhooks.nginx new file mode 100644 index 0000000..003e35c --- /dev/null +++ b/mod/webhooks.nginx @@ -0,0 +1,9 @@ +# Pass to the webhooks app all requests made to the webhooks API. +location /bigbluebutton/api/hooks { + proxy_pass http://10.7.7.17:3005; + proxy_redirect default; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header Host $http_host; + proxy_set_header X-NginX-Proxy true; +} diff --git a/mod/webhooks/Dockerfile b/mod/webhooks/Dockerfile new file mode 100644 index 0000000..e4b7ce3 --- /dev/null +++ b/mod/webhooks/Dockerfile @@ -0,0 +1,25 @@ +FROM node:22-bookworm-slim AS builder + + +RUN apt-get update && apt-get install -y git wget + +RUN wget -q https://github.com/mikefarah/yq/releases/download/v4.25.1/yq_linux_amd64 -O /usr/bin/yq \ + && chmod +x /usr/bin/yq + +COPY --from=src / /bbb-webhooks +RUN cd /bbb-webhooks && npm ci --omit=dev && rm -rf /bbb-webhooks./.git + +RUN chmod 777 /bbb-webhooks/config +# ------------------------------ + +FROM node:22-bookworm-slim +RUN useradd --uid 2004 --user-group bbb-webhooks + +COPY --from=builder /usr/bin/yq /usr/bin/yq +COPY --from=builder /bbb-webhooks /bbb-webhooks +COPY entrypoint.sh /entrypoint.sh + +RUN mkdir /bbb-webhooks/log && chmod 777 /bbb-webhooks/log +USER bbb-webhooks +ENTRYPOINT /entrypoint.sh + diff --git a/mod/webhooks/entrypoint.sh b/mod/webhooks/entrypoint.sh new file mode 100644 index 0000000..1cd89e0 --- /dev/null +++ b/mod/webhooks/entrypoint.sh @@ -0,0 +1,17 @@ +#!/bin/sh +set -e +TARGET=/bbb-webhooks/config/production.yml +cp /bbb-webhooks/config/default.example.yml $TARGET + +yq e -i ".hooks.getRaw = false" $TARGET +yq e -i '.modules."../out/webhooks/index.js".config.getRaw = false' $TARGET + +export NODE_ENV=production +export REDIS_HOST=redis +export SERVER_DOMAIN=$DOMAIN +export BEARER_AUTH=true +export SERVER_BIND_IP=0.0.0.0 + +cd /bbb-webhooks +node app.js + diff --git a/mod/webrtc-sfu-config.yaml b/mod/webrtc-sfu-config.yaml new file mode 100644 index 0000000..804f648 --- /dev/null +++ b/mod/webrtc-sfu-config.yaml @@ -0,0 +1,32 @@ +kurento: [] +redisHost: redis +clientHost: webrtc-sfu +recordingAdapter: bbb-webrtc-recorder + +mcs-host: webrtc-sfu +mcs-address: webrtc-sfu +freeswitch: + ip: freeswitch + sip_ip: freeswitch + port: 5066 + esl_ip: freeswitch + esl_port: 8021 + +log: + # trace|debug|info|warn|error + level: debug + # Whether to log to stdout + stdout: true + # Whether to log to a file + file: false + +mediasoup: + dedicatedMediaTypeWorkers: + audio: auto + workerBalancing: + strategy: least-loaded + + plainRtp: + listenIp: + ip: "0.0.0.0" + announcedIp: "webrtc-sfu" \ No newline at end of file diff --git a/mod/webrtc-sfu.nginx b/mod/webrtc-sfu.nginx new file mode 100644 index 0000000..daa67bc --- /dev/null +++ b/mod/webrtc-sfu.nginx @@ -0,0 +1,25 @@ +location /bbb-webrtc-sfu { + auth_request /bigbluebutton/connection/checkAuthorization; + auth_request_set $auth_status $upstream_status; + # Extra variables are annotated by bbb-web as custom headers + auth_request_set $user_id $sent_http_user_id; + auth_request_set $meeting_id $sent_http_meeting_id; + auth_request_set $voice_bridge $sent_http_voice_bridge; + auth_request_set $user_name $sent_http_user_name; + + proxy_pass http://webrtc-sfu:3008; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "Upgrade"; + # Following custom headers are used by bbb-webrtc-sfu + proxy_set_header User-Id $user_id; + proxy_set_header Meeting-Id $meeting_id; + proxy_set_header Voice-Bridge $voice_bridge; + proxy_set_header User-Name $user_name; + + proxy_read_timeout 60s; + proxy_send_timeout 60s; + client_body_timeout 60s; + send_timeout 60s; +} + diff --git a/mod/webrtc-sfu/Dockerfile b/mod/webrtc-sfu/Dockerfile new file mode 100644 index 0000000..cd2ee00 --- /dev/null +++ b/mod/webrtc-sfu/Dockerfile @@ -0,0 +1,42 @@ +ARG BBB_BUILD_TAG +FROM bigbluebutton/bbb-build:$BBB_BUILD_TAG AS builder + + +RUN useradd --uid 2004 --user-group webrtc-sfu + +# cache packages +COPY --from=source /package.json /cache/package.json +RUN cd /cache && npm install --unsafe-perm + +COPY --from=source / /app + +ENV NODE_ENV production + + +RUN cd /app \ + && rm -rf /app/node_modules && cp -a /cache/node_modules /app/node_modules \ + && cp config/default.example.yml config/production.yml \ + && npm install --unsafe-perm \ + && npm cache clear --force \ + && rm -rf node_modules/mediasoup/worker/out/Release/subprojects \ + && rm -rf node_modules/mediasoup/worker/out/Release/mediasoup-worker.p \ + && rm -rf node_modules/mediasoup/worker/out/Release/deps + + +# ============================= +FROM node:22-bookworm-slim +RUN useradd --uid 2004 --user-group webrtc-sfu +ENV NODE_ENV production + +COPY --from=builder /app /app +RUN mkdir /home/webrtc-sfu && chown -R webrtc-sfu:webrtc-sfu /app/config /home/webrtc-sfu + +USER webrtc-sfu +WORKDIR /app + + +COPY config.yaml /etc/bigbluebutton/bbb-webrtc-sfu/production.yml +ENV NODE_ENV=production +ENV NODE_CONFIG_DIR=/app/config/:/etc/bigbluebutton/bbb-webrtc-sfu/ +ENV ALLOW_CONFIG_MUTATIONS=true +CMD [ "npm", "start" ] From fdbdd4764ca567565a5cfca70ce340872d9eb227 Mon Sep 17 00:00:00 2001 From: maximumultraist Date: Thu, 4 Jun 2026 14:29:59 -0400 Subject: [PATCH 2/2] Second pass at removing static IPs --- .env.sample | 13 +- ...e-original.yml => compose-original.yml.old | 1 + compose.yml | 188 ++++++++---------- entrypoint.bbb-graphql-server.sh | 34 ++++ entrypoint.coturn.sh | 14 +- entrypoint.greenlight.sh | 2 +- init-replica.sh | 26 --- mod/nginx.conf | 20 -- mod/webhooks.nginx | 10 +- mod/webrtc-sfu-config.yaml | 4 + mongod.conf | 33 --- 11 files changed, 145 insertions(+), 200 deletions(-) rename compose-original.yml => compose-original.yml.old (99%) create mode 100644 entrypoint.bbb-graphql-server.sh delete mode 100755 init-replica.sh delete mode 100644 mongod.conf diff --git a/.env.sample b/.env.sample index 31d6c9a..1037f0c 100644 --- a/.env.sample +++ b/.env.sample @@ -16,6 +16,7 @@ SECRET_SHARED_SECRET_VERSION=v1 SECRET_RAILS_SECRET_VERSION=v1 SECRET_TURN_SECRET_VERSION=v1 SECRET_FSESL_PASSWORD_VERSION=v1 +SECRET_HASURA_ADMIN_SECRET_VERSION=v1 ETHERPAD_API_KEY_VERSION=v1 ENTRYPOINT_GREENLIGHT_VERSION=v1 @@ -25,12 +26,20 @@ BBBHTML5_CONF_VERSION=v1 ENTRYPOINT_APPSAKKA_VERSION=v1 ENTRYPOINT_FSESLAKKA_VERSION=v1 ENTRYPOINT_WEBRTCSFU_VERSION=v1 -MONGOD_CONF_VERSION=v1 -INIT_REPLICA_VERSION=v1 +ENTRYPOINT_GRAPHQLSERVER_VERSION=v1 +INITDB_SH_VERSION=v1 ENTRYPOINT_BBBPADS_VERSION=v1 ENTRYPOINT_ETHERPAD_VERSION=v1 ENTRYPOINT_FREESWITCH_VERSION=v1 ENTRYPOINT_BBBWEB_VERSION=v1 +WS_CONF_VERSION=v1 +WN_CONF_VERSION=v1 +WSN_CONF_VERSION=v1 +NGINX_CONF_VERSION=v1 +BRF_CONF_VERSION=v1 +BHDN_CONF_VERSION=v1 +BEG_CONF_VERSION=v1 +AA_CONF_VERSION=v1 # ==================================== # ADDITIONS to BigBlueButton diff --git a/compose-original.yml b/compose-original.yml.old similarity index 99% rename from compose-original.yml rename to compose-original.yml.old index 9dbf617..0be0804 100644 --- a/compose-original.yml +++ b/compose-original.yml.old @@ -431,6 +431,7 @@ services: extra_hosts: - "redis:10.7.7.5" + # HAPROXY NOT NECESSARY FOR CO-OP CLOUD haproxy: build: mod/haproxy image: alangecker/bbb-haproxy:3.0.9 diff --git a/compose.yml b/compose.yml index 409509b..b93d5b5 100644 --- a/compose.yml +++ b/compose.yml @@ -24,6 +24,7 @@ services: # SHARED_SECRET is set via entrypoint.bbb-web.sh # TURN_SECRET is set via entrypoint.bbb-web.sh ENABLE_LEARNING_DASHBOARD: ${ENABLE_LEARNING_DASHBOARD:-true} + COLLABORA_URL: ${COLLABORA_URL:-https://collabora:9980/cool} # NUMBER_OF_BACKEND_NODEJS_PROCESSES: 2 # look for containers? configs: - source: entrypoint_bbbweb @@ -37,9 +38,7 @@ services: - bigbluebutton:/var/bigbluebutton - vol-freeswitch:/var/freeswitch/meetings networks: - bbb-net: - aliases: - - bbb-web + - internal # freeswitch (audio input streaming and telco dial-in support) freeswitch: @@ -73,9 +72,7 @@ services: - vol-freeswitch:/var/freeswitch/meetings # network_mode: host networks: - bbb-net: - aliases: - - freeswitch + - internal logging: # reduce logs to a minimum, so `docker compose logs -f` still works driver: "local" @@ -96,22 +93,21 @@ services: - source: nginx_conf target: /etc/nginx/nginx.conf mode: 0555 - - source: bbb_exporter_nginx_conf + - source: beg_conf target: /etc/nginx/bbb/bbb-exporter.nginx mode: 0555 - - source: bbb_html5_dev_nginx_conf + - source: bhdn_conf target: /etc/nginx/bbb/bbb-html5.dev.nginx mode: 0555 - - source: webhooks_nginx_conf + - source: wn_conf target: /etc/nginx/bbb/webhooks.nginx mode: 0555 - - source: webrtc_sfu_nginx_conf + - source: wsn_conf target: /etc/nginx/bbb/webrtc-sfu.nginx mode: 0555 networks: - bbb-net: - aliases: - - nginx + - proxy + - internal deploy: labels: - "traefik.enable=true" @@ -138,7 +134,8 @@ services: depends_on: - redis - collabora - # environment: + environment: + COLLABORA_URL: ${COLLABORA_URL:-https://collabora:9980/cool} # ETHERPAD_API_KEY is set via entrypoint configs: - source: entrypoint_etherpad @@ -148,9 +145,7 @@ services: - etherpad_api_key entrypoint: /usr/local/bin/docker-entrypoint.sh networks: - bbb-net: - aliases: - - etherpad + - internal # bbb-pads (bbb etherpad integration) bbb-pads: @@ -168,9 +163,7 @@ services: - etherpad_api_key entrypoint: /usr/local/bin/docker-entrypoint.sh networks: - bbb-net: - aliases: - - bbb-pads + - internal # redis (caching) redis: @@ -181,9 +174,7 @@ services: # timeout: 3s # retries: 30 networks: - bbb-net: - aliases: - - redis + - internal bbb-export-annotations: image: alangecker/bbb-docker-bbb-export-annotations:v3.0.19 @@ -196,9 +187,7 @@ services: # https://github.com/bigbluebutton/bigbluebutton/blob/v2.7.0/bbb-export-annotations/config/settings.json # "bbbWebAPI": "http://127.0.0.1:8090", -> bbb-web # "bbbPadsAPI": "http://127.0.0.1:9002", -> bbb-pads - bbb-net: - aliases: - - bbb-export-annotations + - internal volumes: - bigbluebutton:/var/bigbluebutton tmpfs: @@ -210,14 +199,18 @@ services: depends_on: - redis - freeswitch + ports: + # mediasoup UDP port range — must match mediasoup.worker.rtcMinPort/rtcMaxPort in webrtc-sfu-config.yaml + - "24577-24676:24577-24676/udp" environment: # ESL_PASSWORD is set via entrypoint.webrtc-sfu.sh - MS_WEBRTC_LISTEN_IPS: '[{"ip":"${EXTERNAL_IPv4}", "announcedIp":"${EXTERNAL_IPv4}"}]' + # ip must be 0.0.0.0 (container can't bind to external host IP) + MS_WEBRTC_LISTEN_IPS: '[{"ip":"0.0.0.0", "announcedIp":"${EXTERNAL_IPv4}"}]' configs: - source: entrypoint_webrtcsfu target: /usr/local/bin/docker-entrypoint.sh mode: 0555 - - source: webrtc_sfu_conf + - source: ws_conf target: /etc/bigbluebutton/bbb-webrtc-sfu/production.yml mode: 0555 entrypoint: /usr/local/bin/docker-entrypoint.sh @@ -227,11 +220,14 @@ services: - /var/log/bbb-webrtc-sfu secrets: - fsesl_password - # network_mode: host + # network_mode: host would be needed for mediasoup UDP to work from outside; + # keeping on overlay network for now so other services can reach it by name. # security_opt: # - seccomp:unconfined # allow io_uring access for mediasoup ulimits: memlock: -1 # allow io_uring_register_buffers to allocate enough ram + networks: + - internal # fsesl-akka (bbb freeswitch integration) @@ -250,9 +246,7 @@ services: - fsesl_password entrypoint: /usr/local/bin/docker-entrypoint.sh networks: - bbb-net: - aliases: - - fsesl-akka + - internal # apps-akka (seems to be an bbb app integration endpoint) apps-akka: @@ -271,7 +265,7 @@ services: - source: bbbhtml5_conf target: /etc/bigbluebutton/bbb-html5.yml mode: 0555 - - source: apps_akka_conf + - source: aa_conf target: /etc/bigbluebutton/mod/bbb-apps-akka.conf.tmpl mode: 0555 secrets: @@ -279,9 +273,7 @@ services: - postgres_password entrypoint: /usr/local/bin/docker-entrypoint.sh networks: - bbb-net: - aliases: - - apps-akka + - internal bbb-graphql-server: image: alangecker/bbb-docker-graphql-server:v3.0.19 @@ -292,12 +284,18 @@ services: - bbb-graphql-actions environment: POSTGRES_USER: postgres - POSTGRES_PASSWORD: ${POSTGRESQL_SECRET:-password} - HASURA_GRAPHQL_ADMIN_SECRET: TODO_CHANGE_ME + # POSTGRES_PASSWORD and HASURA_GRAPHQL_ADMIN_SECRET are set via entrypoint + configs: + - source: entrypoint_graphqlserver + target: /usr/local/bin/docker-entrypoint.sh + mode: 0555 + entrypoint: /usr/local/bin/docker-entrypoint.sh + command: ["graphql-engine", "serve"] + secrets: + - postgres_password + - hasura_admin_secret networks: - bbb-net: - aliases: - - bbb-graphql-server + - internal bbb-graphql-actions: image: alangecker/bbb-docker-graphql-actions:v3.0.19 @@ -305,9 +303,7 @@ services: - redis - apps-akka networks: - bbb-net: - aliases: - - bbb-graphql-actions + - internal bbb-graphql-middleware: image: alangecker/bbb-docker-graphql-middleware:v3.0.19 @@ -317,9 +313,7 @@ services: - bbb-web - redis networks: - bbb-net: - aliases: - - bbb-graphql-middleware + - internal # extra_hosts: # - "nginx:10.7.7.1" @@ -328,9 +322,7 @@ services: tmpfs: - /tmp networks: - bbb-net: - aliases: - - collabora + - internal # disable logging (way to verbose) logging: driver: none @@ -339,7 +331,7 @@ services: periodic: image: alangecker/bbb-docker-periodic:v3.0.0 depends_on: - - mongodb + - postgres volumes: - /var/run/docker.sock:/var/run/docker.sock # not sure what prupose this has? - bigbluebutton:/var/bigbluebutton # access bbb data @@ -351,32 +343,23 @@ services: REMOVE_OLD_RECORDING: ${REMOVE_OLD_RECORDING} RECORDING_MAX_AGE_DAYS: ${RECORDING_MAX_AGE_DAYS} networks: - bbb-net: - aliases: - - periodic + - internal bbb-webrtc-recorder: image: alangecker/bbb-docker-webrtc-recorder:v0.13.1 depends_on: - redis - volumes: - - ./data/bbb-webrtc-recorder:/var/lib/bbb-webrtc-recorder - # WebRTC connection to bbb-webrtc-sfu seem to - # only to work via the external IP - # extra_hosts: - # - "redis:10.7.7.5" + volumes: + - vol-webrtc-recorder:/var/lib/bbb-webrtc-recorder networks: - bbb-net: - aliases: - - bbb-webrtc-recorder + - internal - # coturn + # coturn (TURN server — must use host networking for UDP relay to reach clients) coturn: image: coturn/coturn:4.8-alpine command: - "--external-ip=${EXTERNAL_IPv4}/${EXTERNAL_IPv4}" - "--external-ip=${EXTERNAL_IPv6:-::1}/${EXTERNAL_IPv6:-::1}" - - "--static-auth-secret=${TURN_SECRET}" - "--allowed-peer-ip=${EXTERNAL_IPv4}" - "--relay-ip=${EXTERNAL_IPv4}" - "--relay-ip=${EXTERNAL_IPv6:-::1}" @@ -390,7 +373,7 @@ services: - turn_secret environment: ENABLE_HTTPS_PROXY: "false" - # network_mode: host + network_mode: host user: root entrypoint: /usr/local/bin/docker-entrypoint.sh @@ -419,17 +402,15 @@ services: - rails_secret entrypoint: /usr/local/bin/docker-entrypoint.sh networks: - bbb-net: - aliases: - - greenlight + - internal - # postgres for greenlight + # postgres for greenlight postgres: image: postgres:16-alpine environment: POSTGRES_MULTIPLE_DATABASES: bbb_graphql,hasura_app,greenlight POSTGRES_USER: postgres - POSTGRES_PASSWORD_FILE: /run/secret/postgres_password + POSTGRES_PASSWORD_FILE: /run/secrets/postgres_password healthcheck: test: ["CMD-SHELL", "pg_isready -U postgres"] interval: 10s @@ -437,22 +418,21 @@ services: retries: 5 volumes: - "postgres_data:/var/lib/postgresql/data" - - ./mod/initdb.sh:/docker-entrypoint-initdb.d/initdb.sh + configs: + - source: initdb_sh + target: /docker-entrypoint-initdb.d/initdb.sh + mode: 0555 secrets: - postgres_password networks: - bbb-net: - aliases: - - postgres + - internal netshoot: image: nicolaka/netshoot entrypoint: /bin/bash command: -c "sleep infinity" networks: - bbb-net: - aliases: - - netshoot + - internal volumes: greenlight_data: @@ -460,6 +440,7 @@ volumes: bigbluebutton: vol-mediasoup: vol-freeswitch: + vol-webrtc-recorder: configs: entrypoint_greenlight: @@ -483,12 +464,12 @@ configs: entrypoint_webrtcsfu: name: ${STACK_NAME}_entrypoint_webrtcsfu_${ENTRYPOINT_WEBRTCSFU_VERSION} file: ./entrypoint.webrtc-sfu.sh - mongod_conf: - name: ${STACK_NAME}_mongod_conf_${MONGOD_CONF_VERSION} - file: ./mongod.conf - init_replica: - name: ${STACK_NAME}_init_replica_${INIT_REPLICA_VERSION} - file: ./init-replica.sh + entrypoint_graphqlserver: + name: ${STACK_NAME}_entrypoint_graphqlserver_${ENTRYPOINT_GRAPHQLSERVER_VERSION} + file: ./entrypoint.bbb-graphql-server.sh + initdb_sh: + name: ${STACK_NAME}_initdb_sh_${INITDB_SH_VERSION} + file: ./mod/initdb.sh entrypoint_bbbpads: name: ${STACK_NAME}_entrypoint_bbbpads_${ENTRYPOINT_BBBPADS_VERSION} file: ./entrypoint.bbb-pads.sh @@ -501,29 +482,29 @@ configs: entrypoint_bbbweb: name: ${STACK_NAME}_entrypoint_bbbweb_${ENTRYPOINT_BBBWEB_VERSION} file: ./entrypoint.bbb-web.sh - apps_akka_conf: - name: ${STACK_NAME}_appsakka_conf_${APPSAKKA_CONF_VERSION} + aa_conf: + name: ${STACK_NAME}_aa_conf_${AA_CONF_VERSION} file: ./mod/bbb-apps-akka.conf - bbb_exporter_nginx_conf: - name: ${STACK_NAME}_bbb_exporter_conf_${BBB_EXPORTER_CONF_VERSION} + beg_conf: + name: ${STACK_NAME}_beg_conf_${BEG_CONF_VERSION} file: ./mod/bbb-exporter.nginx - bbb_html5_dev_nginx_conf: - name: ${STACK_NAME}_bbb_html5_nginx_conf_${BBB_HTML5_NGINX_CONF_VERSION} + bhdn_conf: + name: ${STACK_NAME}_bhdn_conf_${BHDN_CONF_VERSION} file: ./mod/bbb-html5.dev.nginx - bbb_resync_freeswitch_conf: - name: ${STACK_NAME}_bbb_resync_freeswitch_conf_${BBB_RESYNC_FREESWITCH_CONF_VERSION} + brf_conf: + name: ${STACK_NAME}_brf_conf_${BRF_CONF_VERSION} file: ./mod/bbb-resync-freeswitch nginx_conf: name: ${STACK_NAME}_nginx_conf_${NGINX_CONF_VERSION} file: ./mod/nginx.conf - webhooks_nginx_conf: - name: ${STACK_NAME}_webhooks_nginx_conf_${WEBHOOKS_NGINX_CONF_VERSION} + wn_conf: + name: ${STACK_NAME}_wn_conf_${WN_CONF_VERSION} file: ./mod/webhooks.nginx - webrtc_sfu_nginx_conf: - name: ${STACK_NAME}_webrtc_sfu_nginx_conf_${WEBRTC_SFU_NGINX_CONF_VERSION} + wsn_conf: + name: ${STACK_NAME}_wsn_conf_${WSN_CONF_VERSION} file: ./mod/webrtc-sfu.nginx - webrtc_sfu_conf: - name: ${STACK_NAME}_webrtc_sfu_conf_${WEBRTC_SFU_CONF_VERSION} + ws_conf: + name: ${STACK_NAME}_ws_conf_${WS_CONF_VERSION} file: ./mod/webrtc-sfu-config.yaml secrets: @@ -545,10 +526,11 @@ secrets: etherpad_api_key: external: true name: ${STACK_NAME}_etherpad_api_key_${ETHERPAD_API_KEY_VERSION} + hasura_admin_secret: + external: true + name: ${STACK_NAME}_hasura_admin_secret_${SECRET_HASURA_ADMIN_SECRET_VERSION} networks: - bbb-net: - ipam: - driver: default - config: - - subnet: "10.7.7.0/24" + proxy: + external: true + internal: \ No newline at end of file diff --git a/entrypoint.bbb-graphql-server.sh b/entrypoint.bbb-graphql-server.sh new file mode 100644 index 0000000..fad2e3b --- /dev/null +++ b/entrypoint.bbb-graphql-server.sh @@ -0,0 +1,34 @@ +#!/bin/sh +set -e + +# set POSTGRES_PASSWORD from Docker secret +if test -f "/run/secrets/postgres_password"; then + pwd=$(cat /run/secrets/postgres_password) + if [ -z "$pwd" ]; then + echo >&2 "error: /run/secrets/postgres_password is empty" + exit 1 + fi + echo "entrypoint.bbb-graphql-server.sh setting POSTGRES_PASSWORD" + export POSTGRES_PASSWORD="${pwd}" + unset pwd +else + echo >&2 "error: /run/secrets/postgres_password does not exist" + exit 1 +fi + +# set HASURA_GRAPHQL_ADMIN_SECRET from Docker secret +if test -f "/run/secrets/hasura_admin_secret"; then + pwd=$(cat /run/secrets/hasura_admin_secret) + if [ -z "$pwd" ]; then + echo >&2 "error: /run/secrets/hasura_admin_secret is empty" + exit 1 + fi + echo "entrypoint.bbb-graphql-server.sh setting HASURA_GRAPHQL_ADMIN_SECRET" + export HASURA_GRAPHQL_ADMIN_SECRET="${pwd}" + unset pwd +else + echo >&2 "error: /run/secrets/hasura_admin_secret does not exist" + exit 1 +fi + +exec "$@" diff --git a/entrypoint.coturn.sh b/entrypoint.coturn.sh index 561c70b..b23e73d 100755 --- a/entrypoint.coturn.sh +++ b/entrypoint.coturn.sh @@ -5,8 +5,13 @@ apk add jq su-exec # create certs for proxy if [ "$ENABLE_HTTPS_PROXY" == true ]; then - while [ ! -f /etc/resty-auto-ssl/storage/file/*latest ] + while true do + for cert_file in /etc/resty-auto-ssl/storage/file/*latest; do + if [ -f "$cert_file" ]; then + break 2 + fi + done echo "ERROR: certificate doesn't exist yet." echo "Certificate gets create on the first request to the HTTPS proxy." echo "We will try again..." @@ -18,13 +23,6 @@ if [ "$ENABLE_HTTPS_PROXY" == true ]; then cat /etc/resty-auto-ssl/storage/file/*%3Alatest | jq -r '.privkey_pem' > /tmp/key.pem fi -if [ ! -f /tmp/cert.pem ] || [ ! -f /tmp/key.pem ]; then - echo "ERROR: certificate not found, but coturn relies on it." - echo "Use either auto HTTPS proxy or" - echo "provide path to certificates in .env file" - exit 1 -fi - # set turn_secret variable if test -f "/run/secrets/turn_secret"; then pwd=$(cat /run/secrets/turn_secret) diff --git a/entrypoint.greenlight.sh b/entrypoint.greenlight.sh index 86a0dbf..8510412 100644 --- a/entrypoint.greenlight.sh +++ b/entrypoint.greenlight.sh @@ -9,7 +9,7 @@ if test -f "/run/secrets/postgres_password"; then exit 1 fi echo "entrypoint.greenlight.sh setting DATABASE_URL" - export "DATABASE_URL"="postgres://postgres:${pwd}@postgres:5432/greenlight-v3" + export "DATABASE_URL"="postgres://postgres:${pwd}@postgres:5432/greenlight" unset "pwd" else echo >&2 "error: /run/secrets/postgres_password does not exist" diff --git a/init-replica.sh b/init-replica.sh deleted file mode 100755 index 188cdda..0000000 --- a/init-replica.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/bin/sh -set -e - - -host=${HOSTNAME:-$(hostname -f)} - -# shut down again -mongod --pidfilepath /tmp/docker-entrypoint-temp-mongod.pid --shutdown -# restart again binding to 0.0.0.0 to allow a replset with 10.7.7.6 -mongod --oplogSize 8 --replSet rs0 --noauth \ - --config /tmp/docker-entrypoint-temp-config.json \ - --bind_ip 0.0.0.0 --port 27017 \ - --tlsMode disabled \ - --logpath /proc/1/fd/1 --logappend \ - --pidfilepath /tmp/docker-entrypoint-temp-mongod.pid --fork - -# init replset with defaults -mongo 10.7.7.6 --eval "rs.initiate({ - _id: 'rs0', - members: [ { _id: 0, host: '10.7.7.6:27017' } ] -})" - -echo "Waiting to become a master" -echo 'while (!db.isMaster().ismaster) { sleep(100); }' | mongo - -echo "I'm the master!" \ No newline at end of file diff --git a/mod/nginx.conf b/mod/nginx.conf index 0b06b1f..d1e9b3d 100644 --- a/mod/nginx.conf +++ b/mod/nginx.conf @@ -30,24 +30,4 @@ http { include /etc/nginx/conf.d/*.conf; - - server { - # additional server only used for greenlight in dev mode - # allows it to use the BBB API without failing - # due to the self signed certificates - # - # all other requests (e.g. /join) is then redirected - listen 48083 http2; - - location /bigbluebutton/api/join { - return 301 https://haproxy$request_uri; - } - location /bigbluebutton/api { - proxy_pass http://127.0.0.1:48087; - } - location / { - return 301 https://haproxy$request_uri; - } - } - } diff --git a/mod/webhooks.nginx b/mod/webhooks.nginx index 003e35c..e2e3f5f 100644 --- a/mod/webhooks.nginx +++ b/mod/webhooks.nginx @@ -1,9 +1,5 @@ -# Pass to the webhooks app all requests made to the webhooks API. +# Webhooks service is not deployed; return 503 if the endpoint is hit. +# To enable webhooks, add a webhooks service and restore the proxy_pass block. location /bigbluebutton/api/hooks { - proxy_pass http://10.7.7.17:3005; - proxy_redirect default; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header Host $http_host; - proxy_set_header X-NginX-Proxy true; + return 503; } diff --git a/mod/webrtc-sfu-config.yaml b/mod/webrtc-sfu-config.yaml index 804f648..45c72da 100644 --- a/mod/webrtc-sfu-config.yaml +++ b/mod/webrtc-sfu-config.yaml @@ -25,6 +25,10 @@ mediasoup: audio: auto workerBalancing: strategy: least-loaded + worker: + # Must match the port range published in compose.yml (24577-24676) + rtcMinPort: 24577 + rtcMaxPort: 24676 plainRtp: listenIp: diff --git a/mongod.conf b/mongod.conf deleted file mode 100644 index f791065..0000000 --- a/mongod.conf +++ /dev/null @@ -1,33 +0,0 @@ -# mongod.conf - -# for documentation of all options, see: -# http://docs.mongodb.org/manual/reference/configuration-options/ - -storage: - dbPath: /data/db - journal: - enabled: true - wiredTiger: - engineConfig: - cacheSizeGB: 1 - journalCompressor: none - directoryForIndexes: true - collectionConfig: - blockCompressor: none - indexConfig: - prefixCompression: false - - -net: - port: 27017 - bindIp: 0.0.0.0 - - -replication: - replSetName: rs0 - -setParameter: - diagnosticDataCollectionEnabled: false - -security: - javascriptEnabled: false