From 38f74fa28d0300989ef04bbbd9bf92d5dfc3298c Mon Sep 17 00:00:00 2001 From: mi4aux <63875263+mi4aux@users.noreply.github.com> Date: Tue, 20 Apr 2021 20:59:48 +0200 Subject: [PATCH 01/57] Keep avatar as a circle regardless of image aspect ratio Keep avatar as a circle regardless of image aspect ratio. Otherwise horizontal or vertical images result in a ellipse as avatar. --- app/src/components/MeetingDrawer/Chat/Message.js | 2 ++ app/src/components/MeetingDrawer/FileSharing/File.js | 4 +++- app/src/components/MeetingDrawer/ParticipantList/ListMe.js | 2 ++ app/src/components/MeetingDrawer/ParticipantList/ListPeer.js | 2 ++ 4 files changed, 9 insertions(+), 1 deletion(-) diff --git a/app/src/components/MeetingDrawer/Chat/Message.js b/app/src/components/MeetingDrawer/Chat/Message.js index d1ad32fa..731a91c0 100644 --- a/app/src/components/MeetingDrawer/Chat/Message.js +++ b/app/src/components/MeetingDrawer/Chat/Message.js @@ -50,6 +50,8 @@ const styles = (theme) => { borderRadius : '50%', height : '2rem', + width : '2rem', + objectFit : 'cover', alignSelf : 'center' } }); diff --git a/app/src/components/MeetingDrawer/FileSharing/File.js b/app/src/components/MeetingDrawer/FileSharing/File.js index fbcad8b9..d8e98fe8 100644 --- a/app/src/components/MeetingDrawer/FileSharing/File.js +++ b/app/src/components/MeetingDrawer/FileSharing/File.js @@ -25,7 +25,9 @@ const styles = (theme) => avatar : { borderRadius : '50%', - height : '2rem' + height : '2rem', + width : '2rem', + objectFit : 'cover' }, text : { diff --git a/app/src/components/MeetingDrawer/ParticipantList/ListMe.js b/app/src/components/MeetingDrawer/ParticipantList/ListMe.js index 33873d20..a3c7825a 100644 --- a/app/src/components/MeetingDrawer/ParticipantList/ListMe.js +++ b/app/src/components/MeetingDrawer/ParticipantList/ListMe.js @@ -24,6 +24,8 @@ const styles = (theme) => { borderRadius : '50%', height : '2rem', + width : '2rem', + objectFit : 'cover', marginTop : theme.spacing(0.5) }, peerInfo : diff --git a/app/src/components/MeetingDrawer/ParticipantList/ListPeer.js b/app/src/components/MeetingDrawer/ParticipantList/ListPeer.js index 29f582a4..dbf8be44 100644 --- a/app/src/components/MeetingDrawer/ParticipantList/ListPeer.js +++ b/app/src/components/MeetingDrawer/ParticipantList/ListPeer.js @@ -49,6 +49,8 @@ const styles = (theme) => { borderRadius : '50%', height : '2rem', + width : '2rem', + objectFit : 'cover', marginTop : theme.spacing(0.5) }, peerInfo : From 832831bf4a2ef5cbfbdbd3e5d8de147e849d724c Mon Sep 17 00:00:00 2001 From: mi4aux <63875263+mi4aux@users.noreply.github.com> Date: Tue, 20 Apr 2021 21:36:25 +0200 Subject: [PATCH 02/57] Update service path to new dist/ folder --- edumeet.service | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/edumeet.service b/edumeet.service index 94b940e6..bf58c6c1 100644 --- a/edumeet.service +++ b/edumeet.service @@ -3,7 +3,7 @@ Description=edumeet is a audio / video meeting service running in the browser an After=network.target [Service] -ExecStart=/usr/local/src/edumeet/server/server.js +ExecStart=/usr/local/src/edumeet/server/dist/server.js Restart=always User=nobody Group=nogroup From 357941c6f6a5d6d6e8c9d7dc56290a97b3aeb118 Mon Sep 17 00:00:00 2001 From: mi4aux <63875263+mi4aux@users.noreply.github.com> Date: Tue, 20 Apr 2021 21:37:54 +0200 Subject: [PATCH 03/57] Fix access permissions for dist/server.js --- server/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/package.json b/server/package.json index ef7ad3ed..4109742a 100644 --- a/server/package.json +++ b/server/package.json @@ -15,7 +15,7 @@ "main": "lib/index.js", "scripts": { "start": "yarn build && node dist/server.js", - "build": "rm -rf dist && tsc && ln -s ../certs dist/certs && ln -s ../public dist/public", + "build": "rm -rf dist && tsc && ln -s ../certs dist/certs && ln -s ../public dist/public && chmod 755 dist/server.js", "dev": "nodemon --exec ts-node --ignore dist/ -e js,ts server.js", "connect": "ts-node connect.js", "lint": "eslint -c .eslintrc.json --ext .js,.ts *.js *.ts lib/", From 1ac1f7ca92b7a15fbcc8c33b82bfce7fa79b8f89 Mon Sep 17 00:00:00 2001 From: Vittorio Palmisano Date: Tue, 27 Apr 2021 19:57:36 +0200 Subject: [PATCH 04/57] using eslint settings from mediasoup-client --- app/.eslintrc.json | 113 +++----- app/.prettierrc | 2 +- app/src/config.ts | 499 ++++++++++++++++++++++++++++++++ app/src/translations/locales.ts | 276 +++++++++--------- 4 files changed, 676 insertions(+), 214 deletions(-) create mode 100644 app/src/config.ts diff --git a/app/.eslintrc.json b/app/.eslintrc.json index c4590994..c2bb6eaf 100644 --- a/app/.eslintrc.json +++ b/app/.eslintrc.json @@ -6,91 +6,48 @@ }, "overrides": [ { - "files":["**/*.{ts,tsx}"], - "plugins": ["prettier", "@typescript-eslint"], - "extends": ["airbnb-typescript", "react-app", "prettier"], + "files": ["**/*.{ts,tsx}"], + "plugins": ["@typescript-eslint"], + "extends":[ + "eslint:recommended", + "plugin:@typescript-eslint/eslint-recommended", + "plugin:@typescript-eslint/recommended" + ], "parser": "@typescript-eslint/parser", "parserOptions": { - "project": "./tsconfig.json" + "project": "./tsconfig.json" }, "settings": { - "import/resolver": { - "typescript": { - "alwaysTryTypes": true - } - } + "import/resolver": { + "typescript": { + "alwaysTryTypes": true + } + } }, "rules": { - "object-curly-spacing": ["warn", "always"], - "no-unused-vars": [ - "warn", - { - "vars": "all", - "args": "none" - } - ], - "@typescript-eslint/semi": [ - "off" - ], - "@typescript-eslint/no-unused-vars": [ - "warn", - { - "vars": "all", - "args": "none" - } - ], - "max-len": [ - "warn", - { - "code": 100, - "ignoreStrings": true, - "ignoreTemplateLiterals": true, - "ignoreComments": true - } - ], - "prefer-destructuring": [ - "error", - { - "VariableDeclarator": { - "array": false, - "object": true - }, - "AssignmentExpression": { - "array": false, - "object": false - } - }, { - "enforceForRenamedProperties": false - } - ], + "no-unused-vars" : 0, + "@typescript-eslint/ban-types" : 0, + "@typescript-eslint/ban-ts-comment" : 0, + "@typescript-eslint/ban-ts-ignore" : 0, + "@typescript-eslint/explicit-module-boundary-types" : 0, + "@typescript-eslint/member-delimiter-style" : [ 2, - "no-plusplus": [ - "error", - { - "allowForLoopAfterthoughts": true - } - ], - "react/jsx-key": "error", - "import/no-extraneous-dependencies": [ - "error", - { - "devDependencies": [ - "**/*.test.js", - "**/*.test.jsx", - "**/*.test.ts", - "**/*.test.tsx", - "src/tests/**/*" - ] - } - ], - "react/jsx-props-no-spreading": "off", - "import/prefer-default-export": "off", - "react/jsx-boolean-value": "off", - "react/prop-types": "off", - "react/no-unescaped-entities": "off", - "react/jsx-one-expression-per-line": "off", - "react/jsx-wrap-multilines": "off", - "react/destructuring-assignment": "off" + { + "multiline" : { "delimiter": "semi", "requireLast": true }, + "singleline" : { "delimiter": "semi", "requireLast": false } + } + ], + "@typescript-eslint/no-explicit-any" : 0, + "@typescript-eslint/no-unused-vars" : [ 2, + { + "vars" : "all", + "args" : "after-used", + "ignoreRestSiblings" : false + } + ], + "@typescript-eslint/no-use-before-define" : [ 2, { "functions": false } ], + "@typescript-eslint/no-empty-function" : 0, + "@typescript-eslint/no-non-null-assertion" : 0 } } ], diff --git a/app/.prettierrc b/app/.prettierrc index bcbc4f22..41222071 100644 --- a/app/.prettierrc +++ b/app/.prettierrc @@ -3,5 +3,5 @@ "printWidth": 100, "semi": false, "singleQuote": true, - "tabWidth": 2 + "tabWidth": 4 } \ No newline at end of file diff --git a/app/src/config.ts b/app/src/config.ts new file mode 100644 index 00000000..c6269b2d --- /dev/null +++ b/app/src/config.ts @@ -0,0 +1,499 @@ +import convict from 'convict'; +import * as convictFormatWithValidator from 'convict-format-with-validator'; + +convict.addFormat(convictFormatWithValidator.ipaddress); + +const config = convict({ + loginEnabled : + { + doc: 'The login is enabled.', + format: Boolean, + default: false + }, + developmentPort : + { + doc: 'The development server listening port.', + format: Number, + default: 3443 + }, + productionPort : + { + doc: 'The production server listening port.', + format: Number, + default: 443 + }, + serverHostname : + { + doc: 'If the server component runs on a different host than the app you can specify the host name.', + format: '*', + default: null + }, + /** + * Supported browsers version in bowser satisfy format. + * See more: + * https://www.npmjs.com/package/bowser#filtering-browsers + * Otherwise you got a unsupported browser page + */ + supportedBrowsers : + { + doc: 'Supported browsers version in bowser satisfy format.', + format: Object, + default: + { + 'windows' : { + 'internet explorer' : '>12', + 'microsoft edge' : '>18' + }, + 'safari' : '>12', + 'firefox' : '>=60', + 'chrome' : '>=74', + 'chromium' : '>=74', + 'opera' : '>=62', + 'samsung internet for android' : '>=11.1.1.52' + } + }, + /** + * Network priorities + * DSCP bits set by browser according this priority values. + * ("high" means actually: EF for audio, and AF41 for Video in chrome) + * https://en.wikipedia.org/wiki/Differentiated_services + */ + networkPriorities : + { + doc: 'Network priorities.', + format: Object, + default : + { + audio : 'high', + mainVideo : 'high', + additionalVideos : 'medium', + screenShare : 'medium' + } + }, + // The aspect ratio of the videos as shown on the screen. + // This is changeable in client settings. + // This value must match one of the defined values in + // viewAspectRatios EXACTLY (e.g. 1.333) + viewAspectRatio : + { + doc: 'The aspect ratio of the videos as shown on the screen.', + format: Number, + default: 1.777 + }, + viewAspectRatios : + { + doc : 'The selectable aspect ratios in the settings.', + format : Array, + default : + [ + { + value : 1.333, // 4 / 3 + label : '4 : 3' + }, + { + value : 1.777, // 16 / 9 + label : '16 : 9' + } + ], + }, + // The aspect ratio of the video from the camera + // this is not changeable in settings, only config + videoAspectRatio : + { + doc: 'The aspect ratio of the video from the camera.', + format: Number, + default: 1.777 + }, + defaultResolution : + { + doc: 'The default video camera capture resolution.', + format: String, + default: 'medium' + }, + defaultFrameRate : + { + doc: 'The default video camera capture framerate.', + format: Number, + default: 15 + }, + defaultScreenResolution : + { + doc: 'The default screen sharing resolution.', + format: String, + default: 'veryhigh' + }, + defaultScreenSharingFrameRate : + { + doc: 'The default screen sharing framerate.', + format: Number, + default: 5 + }, + simulcast : + { + doc: 'Enable or disable simulcast for webcam video.', + format: Boolean, + default: true + }, + simulcastSharing : + { + doc: 'Enable or disable simulcast for screen sharing video.', + format: Boolean, + default: false + }, + simulcastProfiles : + { + doc: 'Define different encodings for various resolutions of the video.', + format: Object, + default: + { + 3840 : + [ + { scaleResolutionDownBy: 4, maxBitRate: 1500000 }, + { scaleResolutionDownBy: 2, maxBitRate: 4000000 }, + { scaleResolutionDownBy: 1, maxBitRate: 10000000 } + ], + 1920 : + [ + { scaleResolutionDownBy: 4, maxBitRate: 750000 }, + { scaleResolutionDownBy: 2, maxBitRate: 1500000 }, + { scaleResolutionDownBy: 1, maxBitRate: 4000000 } + ], + 1280 : + [ + { scaleResolutionDownBy: 4, maxBitRate: 250000 }, + { scaleResolutionDownBy: 2, maxBitRate: 900000 }, + { scaleResolutionDownBy: 1, maxBitRate: 3000000 } + ], + 640 : + [ + { scaleResolutionDownBy: 2, maxBitRate: 250000 }, + { scaleResolutionDownBy: 1, maxBitRate: 900000 } + ], + 320 : + [ + { scaleResolutionDownBy: 1, maxBitRate: 250000 } + ] + } + }, + // The adaptive spatial layer selection scaling factor (in the range [0.5, 1.0]) + // example: + // with level width=640px, the minimum width required to trigger the + // level change will be: 640 * 0.75 = 480px + adaptiveScalingFactor : + { + doc: 'The adaptive spatial layer selection scaling factor.', + format: (value: number) => value >= 0.5 && value <= 1.0, + default: 0.75 + }, + /** + * White listing browsers that support audio output device selection. + * It is not yet fully implemented in Firefox. + * See: https://bugzilla.mozilla.org/show_bug.cgi?id=1498512 + */ + audioOutputSupportedBrowsers : + { + doc: 'White listing browsers that support audio output device selection.', + format: Array, + default: [ + 'chrome', + 'opera' + ] + }, + requestTimeout : + { + doc: 'The Socket.io request timeout.', + format: Number, + default: 20000 + }, + requestRetries : + { + doc: 'The Socket.io request maximum retries.', + format: Number, + default: 3 + }, + transportOptions : + { + doc: '', + format: Object, + default: { + tcp : true + } + }, + // defaults for audio setting on new clients / can be customized and overruled from client side + defaultAudio : + { + doc: 'Defaults audio settings.', + format: Object, + default : + { + autoGainControl : true, // default : true + echoCancellation : true, // default : true + noiseSuppression : true, // default : true + // Automatically unmute speaking above noisThereshold + voiceActivatedUnmute : false, // default : false + // This is only for voiceActivatedUnmute and audio-indicator + noiseThreshold : -60 // default -60 + } + }, + // Audio options for now only centrally from config file: + centralAudioOptions : + { + doc: 'Defaults audio settings.', + format: Object, + default : + { + // will not eat that much bandwith thanks to opus + sampleRate : 48000, // default : 48000 and don't go higher + // usually mics are mono so this saves bandwidth + channelCount : 1, // default : 1 + volume : 1.0, // default : 1.0 + sampleSize : 16, // default : 16 + // usually mics are mono so this saves bandwidth + opusStereo : false, // default : false + opusDtx : true, // default : true / will save bandwidth + opusFec : true, // default : true / forward error correction + opusPtime : '20', // default : 20 / minimum packet time (3, 5, 10, 20, 40, 60, 120) + opusMaxPlaybackRate : 48000 // default : 48000 and don't go higher + } + }, + /** + * Set max number participants in one room that join + * unmuted. Next participant will join automatically muted + * Default value is 4 + * + * Set it to 0 to auto mute all, + * Set it to negative (-1) to never automatically auto mute + * but use it with caution + * full mesh audio strongly decrease room capacity! + */ + autoMuteThreshold : + { + doc: 'Set max number participants in one room that join unmuted.', + format: Number, + default: 4 + }, + background : + { + doc: 'The page background image URL', + format: String, + default: 'images/background.jpg' + }, + defaultLayout : + { + doc: 'The default layout', + format: [ 'democratic', 'filmstrip' ], + default: 'democratic' + }, + buttonControlBar : + { + doc: 'If true, will show media control buttons in separate control bar, not in the ME container.', + format: Boolean, + default: false + }, + drawerOverlayed : + { + doc: 'If false, will push videos away to make room for side drawer. If true, will overlay side drawer over videos.', + format: Boolean, + default: true + }, + notificationPosition : + { + doc: 'The position of notifications.', + format: [ 'left', 'right' ], + default: 'right' + }, + /** + * Set the notificationSounds. Valid keys are: + * 'parkedPeer', 'parkedPeers', 'raisedHand', 'chatMessage', + * 'sendFile', 'newPeer' and 'default'. + * + * Not defining a key is equivalent to using the default notification sound. + * Setting 'play' to null disables the sound notification. + */ + notificationSounds : + { + doc: 'Set the notifications sounds.', + format: Object, + default : + { + chatMessage : { + play : '/sounds/notify-chat.mp3' + }, + raisedHand : { + play : '/sounds/notify-hand.mp3' + }, + default : { + delay : 5000, // minimum delay between alert sounds [ms] + play : '/sounds/notify.mp3' + } + } + }, + hideTimeout : + { + doc: 'Timeout for autohiding topbar and button control bar.', + format: Number, + default: 3000 + }, + lastN : + { + doc: 'The max number of participant that will be visible in as speaker.', + format: Number, + default: 4 + }, + mobileLastN : + { + doc: 'The max number of participant that will be visible in as speaker for mobile users.', + format: Number, + default: 1 + }, + maxLastN : + { + doc: 'The highest number of lastN the user can select manually in the user interface.', + format: Number, + default: 5 + }, + lockLastN : + { + doc: 'If truthy, users can NOT change number of speakers visible.', + format: Boolean, + default: false + }, + // Show logo if "logo" is not null, else show title + // Set logo file name using logo.* pattern like "logo.png" to not track it by git + logo : + { + doc: 'If not null, it shows the logo loaded from URL, else it shows the title.', + format: String, + default: 'images/logo.edumeet.svg' + }, + title : + { + doc: 'The title to show if the logo is not specified.', + format: String, + default: 'edumeet' + }, + supportUrl : + { + doc: 'Service & Support URL if not set then not displayed on the about modals.', + format: String, + default: 'https://support.example.com' + }, + // Privacy and data protection URL or path by default privacy/privacy.html + // that is a placeholder for your policies + // + // but an external url could be also used here + privacyUrl : + { + doc: 'Privacy and data protection URL or path by default privacy/privacy.html.', + format: String, + default: 'privacy/privacy.html' + }, + // UI theme elements + theme : + { + palette : + { + primary : + { + main : { default: '#313131' } + } + }, + overrides : + { + MuiAppBar : + { + colorPrimary : + { + backgroundColor : { default: '#313131' } + } + }, + MuiButton : + { + containedPrimary : + { + backgroundColor : { default: '#5F9B2D' }, + '&:hover' : + { + backgroundColor : { default: '#5F9B2D' } + } + }, + containedSecondary : + { + backgroundColor : { default: '#f50057' }, + '&:hover' : + { + backgroundColor : { default: '#f50057' } + } + } + + }, + + /* + MuiIconButton : + { + colorPrimary : + { + backgroundColor : '#5F9B2D', + '&:hover' : + { + backgroundColor : '#5F9B2D' + } + }, + colorSecondary : + { + backgroundColor : '#f50057', + '&:hover' : + { + backgroundColor : '#f50057' + } + } + + }, + */ + + MuiFab : + { + primary : + { + backgroundColor : { default: '#5F9B2D' }, + '&:hover' : + { + backgroundColor : { default: '#5F9B2D' } + } + }, + secondary : + { + backgroundColor : { default: '#f50057' }, + '&:hover' : + { + backgroundColor : { default: '#f50057' } + } + } + + }, + MuiBadge : + { + colorPrimary : + { + backgroundColor : { default: '#5F9B2D' }, + '&:hover' : + { + backgroundColor : { default: '#518029' } + } + } + } + }, + typography : + { + useNextVariants : { default: true } + } + } +}); + +// Load config from window object +config.load((window as any).config); + +// Perform validation +config.validate({ allowed: 'strict' }); + +export default config; diff --git a/app/src/translations/locales.ts b/app/src/translations/locales.ts index fd055b09..ac3b2e29 100644 --- a/app/src/translations/locales.ts +++ b/app/src/translations/locales.ts @@ -2,151 +2,157 @@ /* eslint-disable import/no-dynamic-require */ const list = [ - { - name: 'English', - file: 'en', - locale: ['en', 'en-en'], - }, - { - name: 'Czech', - file: 'cs', - locale: ['cs', 'cs-cs'], - }, - { - name: 'Chinese (Simplified)', - file: 'cn', - locale: ['zn', 'zn-zn', 'zn-cn'], - }, // hans - { - name: 'Chinese (Traditional)', - file: 'tw', - locale: ['zn-tw', 'zn-hk', 'zn-sg'], - }, // hant - { - name: 'Croatian', - file: 'hr', - locale: ['hr', 'hr-hr'], - }, - { - name: 'Danish', - file: 'dk', - locale: ['dk', 'dk-dk'], - }, - { - name: 'French', - file: 'fr', - locale: ['fr', 'fr-fr'], - }, - { - name: 'German', - file: 'de', - locale: ['de', 'de-de'], - }, - { - name: 'Greek', - file: 'el', - locale: ['el', 'el-el'], - }, - { - name: 'Hindi', - file: 'hi', - locale: ['hi', 'hi-hi'], - }, - { - name: 'Hungarian', - file: 'hu', - locale: ['hu', 'hu-hu'], - }, - { - name: 'Italian', - file: 'it', - locale: ['it', 'it-it'], - }, - { - name: 'Kazakh', - file: 'kk', - locale: ['kk', 'kk-kz '], - }, - { - name: 'Latvian', - file: 'lv', - locale: ['lv', 'lv-lv'], - }, - { - name: 'Norwegian', - file: 'nb', - locale: ['nb', 'nb-no'], - }, - { - name: 'Polish', - file: 'pl', - locale: ['pl', 'pl-pl'], - }, - { - name: 'Portuguese', - file: 'pt', - locale: ['pt', 'pt-pt'], - }, - { - name: 'Romanian', - file: 'ro', - locale: ['ro', 'ro-ro'], - }, - { - name: 'Russian', - file: 'ru', - locale: ['ru', 'ru-ru'], - }, - { - name: 'Spanish', - file: 'es', - locale: ['es', 'es-es'], - }, - { - name: 'Turkish', - file: 'tr', - locale: ['tr', 'tr-tr'], - }, - { - name: 'Ukrainian', - file: 'uk', - locale: ['uk', 'uk-uk'], - }, -] + { + name : 'English', + file : 'en', + locale : [ 'en', 'en-en' ] + }, + { + name : 'Czech', + file : 'cs', + locale : [ 'cs', 'cs-cs' ] + }, + { + name : 'Chinese (Simplified)', + file : 'cn', + locale : [ 'zn', 'zn-zn', 'zn-cn' ] + }, // hans + { + name : 'Chinese (Traditional)', + file : 'tw', + locale : [ 'zn-tw', 'zn-hk', 'zn-sg' ] + }, // hant + { + name : 'Croatian', + file : 'hr', + locale : [ 'hr', 'hr-hr' ] + }, + { + name : 'Danish', + file : 'dk', + locale : [ 'dk', 'dk-dk' ] + }, + { + name : 'French', + file : 'fr', + locale : [ 'fr', 'fr-fr' ] + }, + { + name : 'German', + file : 'de', + locale : [ 'de', 'de-de' ] + }, + { + name : 'Greek', + file : 'el', + locale : [ 'el', 'el-el' ] + }, + { + name : 'Hindi', + file : 'hi', + locale : [ 'hi', 'hi-hi' ] + }, + { + name : 'Hungarian', + file : 'hu', + locale : [ 'hu', 'hu-hu' ] + }, + { + name : 'Italian', + file : 'it', + locale : [ 'it', 'it-it' ] + }, + { + name : 'Kazakh', + file : 'kk', + locale : [ 'kk', 'kk-kz ' ] + }, + { + name : 'Latvian', + file : 'lv', + locale : [ 'lv', 'lv-lv' ] + }, + { + name : 'Norwegian', + file : 'nb', + locale : [ 'nb', 'nb-no' ] + }, + { + name : 'Polish', + file : 'pl', + locale : [ 'pl', 'pl-pl' ] + }, + { + name : 'Portuguese', + file : 'pt', + locale : [ 'pt', 'pt-pt' ] + }, + { + name : 'Romanian', + file : 'ro', + locale : [ 'ro', 'ro-ro' ] + }, + { + name : 'Russian', + file : 'ru', + locale : [ 'ru', 'ru-ru' ] + }, + { + name : 'Spanish', + file : 'es', + locale : [ 'es', 'es-es' ] + }, + { + name : 'Turkish', + file : 'tr', + locale : [ 'tr', 'tr-tr' ] + }, + { + name : 'Ukrainian', + file : 'uk', + locale : [ 'uk', 'uk-uk' ] + } +]; -export const detect = () => { - const localeFull = (navigator.language || (navigator as any).browserLanguage).toLowerCase() +export const detect = () => +{ + const localeFull = (navigator.language || + (navigator as any).browserLanguage).toLowerCase(); - // const localeCountry = localeFull.split(/[-_]/)[0]; + // const localeCountry = localeFull.split(/[-_]/)[0]; - // const localeRegion = localeFull.split(/[-_]/)[1] || null; + // const localeRegion = localeFull.split(/[-_]/)[1] || null; - return localeFull -} + return localeFull; +}; -export const getList = () => list +export const getList = () => list; export interface ILocale { - name: string - file: string - locale: string[] - messages: any + name: string; + file: string; + locale: string[]; + messages: any; } -export const loadOne = (locale: string): ILocale => { - let res: any = {} +export const loadOne = (locale: string): ILocale => +{ + let res: any = {}; - try { - res = list.filter( - (item) => item.locale.includes(locale) || item.locale.includes(locale.split(/[-_]/)[0]) - )[0] + try + { + res = list.filter( + (item) => item.locale.includes(locale) || item.locale.includes(locale.split(/[-_]/)[0]) + )[0]; - res.messages = require(`./${res.file}`) - } catch { - res = list.filter((item) => item.locale.includes('en'))[0] + res.messages = require(`./${res.file}`); + } + catch + { + res = list.filter((item) => item.locale.includes('en'))[0]; - res.messages = require(`./${res.file}`) - } + res.messages = require(`./${res.file}`); + } - return res -} + return res; +}; From 6b46663276181f700db76a3d629768329d0068c4 Mon Sep 17 00:00:00 2001 From: Vittorio Palmisano Date: Tue, 27 Apr 2021 19:58:07 +0200 Subject: [PATCH 05/57] WIP app config validation --- app/package.json | 4 + app/src/config.ts | 915 +++++++++++++++++++++++----------------------- app/src/index.js | 2 + app/yarn.lock | 36 +- 4 files changed, 506 insertions(+), 451 deletions(-) diff --git a/app/package.json b/app/package.json index 817a7c80..e3fa9b32 100644 --- a/app/package.json +++ b/app/package.json @@ -24,6 +24,8 @@ "@types/react-dom": "^17.0.3", "bowser": "^2.7.0", "classnames": "^2.2.6", + "convict": "^6.0.1", + "convict-format-with-validator": "^6.0.1", "create-torrent": "^4.4.1", "dompurify": "^2.0.7", "domready": "^1.0.8", @@ -79,6 +81,8 @@ "not op_mini all" ], "devDependencies": { + "@types/convict": "^6.0.1", + "@types/convict-format-with-validator": "^6.0.2", "@typescript-eslint/eslint-plugin": "^4.20.0", "@typescript-eslint/parser": "^4.20.0", "babel-eslint": "^10.1.0", diff --git a/app/src/config.ts b/app/src/config.ts index c6269b2d..ad5b2ab2 100644 --- a/app/src/config.ts +++ b/app/src/config.ts @@ -1,493 +1,501 @@ import convict from 'convict'; import * as convictFormatWithValidator from 'convict-format-with-validator'; +import Logger from './Logger'; + +const logger = new Logger('config'); convict.addFormat(convictFormatWithValidator.ipaddress); const config = convict({ - loginEnabled : - { - doc: 'The login is enabled.', - format: Boolean, - default: false - }, + loginEnabled : + { + doc : 'The login is enabled.', + format : Boolean, + default : false + }, developmentPort : - { - doc: 'The development server listening port.', - format: Number, - default: 3443 - }, - productionPort : - { - doc: 'The production server listening port.', - format: Number, - default: 443 - }, - serverHostname : - { - doc: 'If the server component runs on a different host than the app you can specify the host name.', - format: '*', - default: null - }, + { + doc : 'The development server listening port.', + format : Number, + default : 3443 + }, + productionPort : + { + doc : 'The production server listening port.', + format : Number, + default : 443 + }, + serverHostname : + { + doc : 'If the server component runs on a different host than the app you can specify the host name.', + format : '*', + default : null + }, + /** * Supported browsers version in bowser satisfy format. * See more: * https://www.npmjs.com/package/bowser#filtering-browsers * Otherwise you got a unsupported browser page */ - supportedBrowsers : - { - doc: 'Supported browsers version in bowser satisfy format.', - format: Object, - default: - { - 'windows' : { - 'internet explorer' : '>12', - 'microsoft edge' : '>18' - }, - 'safari' : '>12', - 'firefox' : '>=60', - 'chrome' : '>=74', - 'chromium' : '>=74', - 'opera' : '>=62', - 'samsung internet for android' : '>=11.1.1.52' - } - }, + supportedBrowsers : + { + doc : 'Supported browsers version in bowser satisfy format.', + format : Object, + default : + { + 'windows' : { + 'internet explorer' : '>12', + 'microsoft edge' : '>18' + }, + 'safari' : '>12', + 'firefox' : '>=60', + 'chrome' : '>=74', + 'chromium' : '>=74', + 'opera' : '>=62', + 'samsung internet for android' : '>=11.1.1.52' + } + }, + /** * Network priorities * DSCP bits set by browser according this priority values. * ("high" means actually: EF for audio, and AF41 for Video in chrome) * https://en.wikipedia.org/wiki/Differentiated_services */ - networkPriorities : - { - doc: 'Network priorities.', - format: Object, - default : - { - audio : 'high', - mainVideo : 'high', - additionalVideos : 'medium', - screenShare : 'medium' - } - }, - // The aspect ratio of the videos as shown on the screen. - // This is changeable in client settings. + networkPriorities : + { + doc : 'Network priorities.', + format : Object, + default : + { + audio : 'high', + mainVideo : 'high', + additionalVideos : 'medium', + screenShare : 'medium' + } + }, + // The aspect ratio of the videos as shown on the screen. + // This is changeable in client settings. // This value must match one of the defined values in // viewAspectRatios EXACTLY (e.g. 1.333) viewAspectRatio : - { - doc: 'The aspect ratio of the videos as shown on the screen.', - format: Number, - default: 1.777 - }, + { + doc : 'The aspect ratio of the videos as shown on the screen.', + format : Number, + default : 1.777 + }, viewAspectRatios : - { - doc : 'The selectable aspect ratios in the settings.', - format : Array, - default : - [ - { - value : 1.333, // 4 / 3 - label : '4 : 3' - }, - { - value : 1.777, // 16 / 9 - label : '16 : 9' - } - ], - }, - // The aspect ratio of the video from the camera + { + doc : 'The selectable aspect ratios in the settings.', + format : Array, + default : + [ + { + value : 1.333, // 4 / 3 + label : '4 : 3' + }, + { + value : 1.777, // 16 / 9 + label : '16 : 9' + } + ] + }, + // The aspect ratio of the video from the camera // this is not changeable in settings, only config videoAspectRatio : - { - doc: 'The aspect ratio of the video from the camera.', - format: Number, - default: 1.777 - }, - defaultResolution : - { - doc: 'The default video camera capture resolution.', - format: String, - default: 'medium' - }, - defaultFrameRate : - { - doc: 'The default video camera capture framerate.', - format: Number, - default: 15 - }, - defaultScreenResolution : - { - doc: 'The default screen sharing resolution.', - format: String, - default: 'veryhigh' - }, - defaultScreenSharingFrameRate : - { - doc: 'The default screen sharing framerate.', - format: Number, - default: 5 - }, - simulcast : - { - doc: 'Enable or disable simulcast for webcam video.', - format: Boolean, - default: true - }, - simulcastSharing : - { - doc: 'Enable or disable simulcast for screen sharing video.', - format: Boolean, - default: false - }, + { + doc : 'The aspect ratio of the video from the camera.', + format : Number, + default : 1.777 + }, + defaultResolution : + { + doc : 'The default video camera capture resolution.', + format : String, + default : 'medium' + }, + defaultFrameRate : + { + doc : 'The default video camera capture framerate.', + format : Number, + default : 15 + }, + defaultScreenResolution : + { + doc : 'The default screen sharing resolution.', + format : String, + default : 'veryhigh' + }, + defaultScreenSharingFrameRate : + { + doc : 'The default screen sharing framerate.', + format : Number, + default : 5 + }, + simulcast : + { + doc : 'Enable or disable simulcast for webcam video.', + format : Boolean, + default : true + }, + simulcastSharing : + { + doc : 'Enable or disable simulcast for screen sharing video.', + format : Boolean, + default : false + }, simulcastProfiles : - { - doc: 'Define different encodings for various resolutions of the video.', - format: Object, - default: - { - 3840 : - [ - { scaleResolutionDownBy: 4, maxBitRate: 1500000 }, - { scaleResolutionDownBy: 2, maxBitRate: 4000000 }, - { scaleResolutionDownBy: 1, maxBitRate: 10000000 } - ], - 1920 : - [ - { scaleResolutionDownBy: 4, maxBitRate: 750000 }, - { scaleResolutionDownBy: 2, maxBitRate: 1500000 }, - { scaleResolutionDownBy: 1, maxBitRate: 4000000 } - ], - 1280 : - [ - { scaleResolutionDownBy: 4, maxBitRate: 250000 }, - { scaleResolutionDownBy: 2, maxBitRate: 900000 }, - { scaleResolutionDownBy: 1, maxBitRate: 3000000 } - ], - 640 : - [ - { scaleResolutionDownBy: 2, maxBitRate: 250000 }, - { scaleResolutionDownBy: 1, maxBitRate: 900000 } - ], - 320 : - [ - { scaleResolutionDownBy: 1, maxBitRate: 250000 } - ] - } - }, - // The adaptive spatial layer selection scaling factor (in the range [0.5, 1.0]) + { + doc : 'Define different encodings for various resolutions of the video.', + format : Object, + default : + { + 3840 : + [ + { scaleResolutionDownBy: 4, maxBitRate: 1500000 }, + { scaleResolutionDownBy: 2, maxBitRate: 4000000 }, + { scaleResolutionDownBy: 1, maxBitRate: 10000000 } + ], + 1920 : + [ + { scaleResolutionDownBy: 4, maxBitRate: 750000 }, + { scaleResolutionDownBy: 2, maxBitRate: 1500000 }, + { scaleResolutionDownBy: 1, maxBitRate: 4000000 } + ], + 1280 : + [ + { scaleResolutionDownBy: 4, maxBitRate: 250000 }, + { scaleResolutionDownBy: 2, maxBitRate: 900000 }, + { scaleResolutionDownBy: 1, maxBitRate: 3000000 } + ], + 640 : + [ + { scaleResolutionDownBy: 2, maxBitRate: 250000 }, + { scaleResolutionDownBy: 1, maxBitRate: 900000 } + ], + 320 : + [ + { scaleResolutionDownBy: 1, maxBitRate: 250000 } + ] + } + }, + // The adaptive spatial layer selection scaling factor (in the range [0.5, 1.0]) // example: // with level width=640px, the minimum width required to trigger the // level change will be: 640 * 0.75 = 480px adaptiveScalingFactor : - { - doc: 'The adaptive spatial layer selection scaling factor.', - format: (value: number) => value >= 0.5 && value <= 1.0, - default: 0.75 - }, - /** + { + doc : 'The adaptive spatial layer selection scaling factor.', + format : (value: number) => value >= 0.5 && value <= 1.0, + default : 0.75 + }, + + /** * White listing browsers that support audio output device selection. * It is not yet fully implemented in Firefox. * See: https://bugzilla.mozilla.org/show_bug.cgi?id=1498512 */ audioOutputSupportedBrowsers : - { - doc: 'White listing browsers that support audio output device selection.', - format: Array, - default: [ - 'chrome', - 'opera' - ] - }, + { + doc : 'White listing browsers that support audio output device selection.', + format : Array, + default : [ + 'chrome', + 'opera' + ] + }, requestTimeout : - { - doc: 'The Socket.io request timeout.', - format: Number, - default: 20000 - }, - requestRetries : - { - doc: 'The Socket.io request maximum retries.', - format: Number, - default: 3 - }, - transportOptions : - { - doc: '', - format: Object, - default: { - tcp : true - } - }, - // defaults for audio setting on new clients / can be customized and overruled from client side - defaultAudio : - { - doc: 'Defaults audio settings.', - format: Object, - default : - { - autoGainControl : true, // default : true - echoCancellation : true, // default : true - noiseSuppression : true, // default : true - // Automatically unmute speaking above noisThereshold - voiceActivatedUnmute : false, // default : false - // This is only for voiceActivatedUnmute and audio-indicator - noiseThreshold : -60 // default -60 - } - }, - // Audio options for now only centrally from config file: - centralAudioOptions : - { - doc: 'Defaults audio settings.', - format: Object, - default : - { - // will not eat that much bandwith thanks to opus - sampleRate : 48000, // default : 48000 and don't go higher - // usually mics are mono so this saves bandwidth - channelCount : 1, // default : 1 - volume : 1.0, // default : 1.0 - sampleSize : 16, // default : 16 - // usually mics are mono so this saves bandwidth - opusStereo : false, // default : false - opusDtx : true, // default : true / will save bandwidth - opusFec : true, // default : true / forward error correction - opusPtime : '20', // default : 20 / minimum packet time (3, 5, 10, 20, 40, 60, 120) - opusMaxPlaybackRate : 48000 // default : 48000 and don't go higher - } - }, - /** - * Set max number participants in one room that join - * unmuted. Next participant will join automatically muted - * Default value is 4 - * - * Set it to 0 to auto mute all, - * Set it to negative (-1) to never automatically auto mute - * but use it with caution - * full mesh audio strongly decrease room capacity! - */ - autoMuteThreshold : - { - doc: 'Set max number participants in one room that join unmuted.', - format: Number, - default: 4 - }, - background : - { - doc: 'The page background image URL', - format: String, - default: 'images/background.jpg' - }, - defaultLayout : - { - doc: 'The default layout', - format: [ 'democratic', 'filmstrip' ], - default: 'democratic' - }, - buttonControlBar : - { - doc: 'If true, will show media control buttons in separate control bar, not in the ME container.', - format: Boolean, - default: false - }, - drawerOverlayed : - { - doc: 'If false, will push videos away to make room for side drawer. If true, will overlay side drawer over videos.', - format: Boolean, - default: true - }, - notificationPosition : - { - doc: 'The position of notifications.', - format: [ 'left', 'right' ], - default: 'right' - }, - /** - * Set the notificationSounds. Valid keys are: - * 'parkedPeer', 'parkedPeers', 'raisedHand', 'chatMessage', - * 'sendFile', 'newPeer' and 'default'. - * - * Not defining a key is equivalent to using the default notification sound. - * Setting 'play' to null disables the sound notification. - */ - notificationSounds : - { - doc: 'Set the notifications sounds.', - format: Object, - default : - { - chatMessage : { - play : '/sounds/notify-chat.mp3' - }, - raisedHand : { - play : '/sounds/notify-hand.mp3' - }, - default : { - delay : 5000, // minimum delay between alert sounds [ms] - play : '/sounds/notify.mp3' - } - } - }, - hideTimeout : - { - doc: 'Timeout for autohiding topbar and button control bar.', - format: Number, - default: 3000 - }, - lastN : - { - doc: 'The max number of participant that will be visible in as speaker.', - format: Number, - default: 4 - }, - mobileLastN : - { - doc: 'The max number of participant that will be visible in as speaker for mobile users.', - format: Number, - default: 1 - }, - maxLastN : - { - doc: 'The highest number of lastN the user can select manually in the user interface.', - format: Number, - default: 5 - }, - lockLastN : - { - doc: 'If truthy, users can NOT change number of speakers visible.', - format: Boolean, - default: false - }, - // Show logo if "logo" is not null, else show title - // Set logo file name using logo.* pattern like "logo.png" to not track it by git - logo : - { - doc: 'If not null, it shows the logo loaded from URL, else it shows the title.', - format: String, - default: 'images/logo.edumeet.svg' - }, - title : - { - doc: 'The title to show if the logo is not specified.', - format: String, - default: 'edumeet' - }, - supportUrl : - { - doc: 'Service & Support URL if not set then not displayed on the about modals.', - format: String, - default: 'https://support.example.com' - }, - // Privacy and data protection URL or path by default privacy/privacy.html - // that is a placeholder for your policies - // - // but an external url could be also used here - privacyUrl : - { - doc: 'Privacy and data protection URL or path by default privacy/privacy.html.', - format: String, - default: 'privacy/privacy.html' - }, - // UI theme elements - theme : - { - palette : - { - primary : - { - main : { default: '#313131' } - } - }, - overrides : - { - MuiAppBar : - { - colorPrimary : - { - backgroundColor : { default: '#313131' } - } - }, - MuiButton : - { - containedPrimary : - { - backgroundColor : { default: '#5F9B2D' }, - '&:hover' : - { - backgroundColor : { default: '#5F9B2D' } - } - }, - containedSecondary : - { - backgroundColor : { default: '#f50057' }, - '&:hover' : - { - backgroundColor : { default: '#f50057' } - } - } + { + doc : 'The Socket.io request timeout.', + format : Number, + default : 20000 + }, + requestRetries : + { + doc : 'The Socket.io request maximum retries.', + format : Number, + default : 3 + }, + transportOptions : + { + doc : '', + format : Object, + default : { + tcp : true + } + }, + // defaults for audio setting on new clients / can be customized and overruled from client side + defaultAudio : + { + doc : 'Defaults audio settings.', + format : Object, + default : + { + autoGainControl : true, // default : true + echoCancellation : true, // default : true + noiseSuppression : true, // default : true + // Automatically unmute speaking above noisThereshold + voiceActivatedUnmute : false, // default : false + // This is only for voiceActivatedUnmute and audio-indicator + noiseThreshold : -60 // default -60 + } + }, + // Audio options for now only centrally from config file: + centralAudioOptions : + { + doc : 'Defaults audio settings.', + format : Object, + default : + { + // will not eat that much bandwith thanks to opus + sampleRate : 48000, // default : 48000 and don't go higher + // usually mics are mono so this saves bandwidth + channelCount : 1, // default : 1 + volume : 1.0, // default : 1.0 + sampleSize : 16, // default : 16 + // usually mics are mono so this saves bandwidth + opusStereo : false, // default : false + opusDtx : true, // default : true / will save bandwidth + opusFec : true, // default : true / forward error correction + opusPtime : '20', // default : 20 / minimum packet time (3, 5, 10, 20, 40, 60, 120) + opusMaxPlaybackRate : 48000 // default : 48000 and don't go higher + } + }, - }, + /** + * Set max number participants in one room that join + * unmuted. Next participant will join automatically muted + * Default value is 4 + * + * Set it to 0 to auto mute all, + * Set it to negative (-1) to never automatically auto mute + * but use it with caution + * full mesh audio strongly decrease room capacity! + */ + autoMuteThreshold : + { + doc : 'Set max number participants in one room that join unmuted.', + format : Number, + default : 4 + }, + background : + { + doc : 'The page background image URL', + format : String, + default : 'images/background.jpg' + }, + defaultLayout : + { + doc : 'The default layout', + format : [ 'democratic', 'filmstrip' ], + default : 'democratic' + }, + buttonControlBar : + { + doc : 'If true, will show media control buttons in separate control bar, not in the ME container.', + format : Boolean, + default : false + }, + drawerOverlayed : + { + doc : 'If false, will push videos away to make room for side drawer. If true, will overlay side drawer over videos.', + format : Boolean, + default : true + }, + notificationPosition : + { + doc : 'The position of notifications.', + format : [ 'left', 'right' ], + default : 'right' + }, - /* - MuiIconButton : - { - colorPrimary : - { - backgroundColor : '#5F9B2D', - '&:hover' : - { - backgroundColor : '#5F9B2D' - } - }, - colorSecondary : - { - backgroundColor : '#f50057', - '&:hover' : - { - backgroundColor : '#f50057' - } - } + /** + * Set the notificationSounds. Valid keys are: + * 'parkedPeer', 'parkedPeers', 'raisedHand', 'chatMessage', + * 'sendFile', 'newPeer' and 'default'. + * + * Not defining a key is equivalent to using the default notification sound. + * Setting 'play' to null disables the sound notification. + */ + notificationSounds : + { + doc : 'Set the notifications sounds.', + format : Object, + default : + { + chatMessage : { + play : '/sounds/notify-chat.mp3' + }, + raisedHand : { + play : '/sounds/notify-hand.mp3' + }, + default : { + delay : 5000, // minimum delay between alert sounds [ms] + play : '/sounds/notify.mp3' + } + } + }, + hideTimeout : + { + doc : 'Timeout for autohiding topbar and button control bar.', + format : Number, + default : 3000 + }, + lastN : + { + doc : 'The max number of participant that will be visible in as speaker.', + format : Number, + default : 4 + }, + mobileLastN : + { + doc : 'The max number of participant that will be visible in as speaker for mobile users.', + format : Number, + default : 1 + }, + maxLastN : + { + doc : 'The highest number of lastN the user can select manually in the user interface.', + format : Number, + default : 5 + }, + lockLastN : + { + doc : 'If truthy, users can NOT change number of speakers visible.', + format : Boolean, + default : false + }, + // Show logo if "logo" is not null, else show title + // Set logo file name using logo.* pattern like "logo.png" to not track it by git + logo : + { + doc : 'If not null, it shows the logo loaded from URL, else it shows the title.', + format : String, + default : 'images/logo.edumeet.svg' + }, + title : + { + doc : 'The title to show if the logo is not specified.', + format : String, + default : 'edumeet' + }, + supportUrl : + { + doc : 'Service & Support URL if not set then not displayed on the about modals.', + format : String, + default : 'https://support.example.com' + }, + // Privacy and data protection URL or path by default privacy/privacy.html + // that is a placeholder for your policies + // + // but an external url could be also used here + privacyUrl : + { + doc : 'Privacy and data protection URL or path by default privacy/privacy.html.', + format : String, + default : 'privacy/privacy.html' + }, + // UI theme elements + theme : + { + palette : + { + primary : + { + main : { format: String, default: '#313131' } + } + }, + overrides : + { + MuiAppBar : + { + colorPrimary : + { + backgroundColor : { format: String, default: '#313131' } + } + }, + MuiButton : + { + containedPrimary : + { + backgroundColor : { format: String, default: '#5F9B2D' }, + '&:hover' : + { + backgroundColor : { format: String, default: '#5F9B2D' } + } + }, + containedSecondary : + { + backgroundColor : { format: String, default: '#f50057' }, + '&:hover' : + { + backgroundColor : { format: String, default: '#f50057' } + } + } - }, - */ + }, - MuiFab : - { - primary : - { - backgroundColor : { default: '#5F9B2D' }, - '&:hover' : - { - backgroundColor : { default: '#5F9B2D' } - } - }, - secondary : - { - backgroundColor : { default: '#f50057' }, - '&:hover' : - { - backgroundColor : { default: '#f50057' } - } - } + /* + MuiIconButton : + { + colorPrimary : + { + backgroundColor : '#5F9B2D', + '&:hover' : + { + backgroundColor : '#5F9B2D' + } + }, + colorSecondary : + { + backgroundColor : '#f50057', + '&:hover' : + { + backgroundColor : '#f50057' + } + } - }, - MuiBadge : - { - colorPrimary : - { - backgroundColor : { default: '#5F9B2D' }, - '&:hover' : - { - backgroundColor : { default: '#518029' } - } - } - } - }, - typography : - { - useNextVariants : { default: true } - } - } + }, + */ + + MuiFab : + { + primary : + { + backgroundColor : { format: String, default: '#5F9B2D' }, + '&:hover' : + { + backgroundColor : { format: String, default: '#5F9B2D' } + } + }, + secondary : + { + backgroundColor : { format: String, default: '#f50057' }, + '&:hover' : + { + backgroundColor : { format: String, default: '#f50057' } + } + } + + }, + MuiBadge : + { + colorPrimary : + { + backgroundColor : { format: String, default: '#5F9B2D' }, + '&:hover' : + { + backgroundColor : { format: String, default: '#518029' } + } + } + } + }, + typography : + { + useNextVariants : { format: Boolean, default: true } + } + } }); // Load config from window object @@ -496,4 +504,11 @@ config.load((window as any).config); // Perform validation config.validate({ allowed: 'strict' }); -export default config; +const properties = config.getProperties(); + +logger.debug('properties:', properties); + +// Override the window config with the validated properties. +(window as any)['config'] = properties; + +export default properties; diff --git a/app/src/index.js b/app/src/index.js index 7fcfca83..a7e5df61 100644 --- a/app/src/index.js +++ b/app/src/index.js @@ -29,6 +29,8 @@ import { detectDevice } from 'mediasoup-client'; import './index.css'; +import config from './config'; + const App = LazyPreload(() => import(/* webpackChunkName: "app" */ './components/App')); // const cache = createIntlCache(); diff --git a/app/yarn.lock b/app/yarn.lock index 62a6c3fd..0ec87fdf 100644 --- a/app/yarn.lock +++ b/app/yarn.lock @@ -1843,6 +1843,20 @@ dependencies: "@babel/types" "^7.3.0" +"@types/convict-format-with-validator@^6.0.2": + version "6.0.2" + resolved "https://registry.yarnpkg.com/@types/convict-format-with-validator/-/convict-format-with-validator-6.0.2.tgz#5275be2815a955f1891e0065c6c2be85c79f9a56" + integrity sha512-H8AfvvijYaFRE5sLje5ugldjGcwOqzre/TzrrWfrMHtVUhoN5eaUf6BqkFB9QJ24iUUoCDCceej2KTmbS/GaTg== + dependencies: + "@types/convict" "*" + +"@types/convict@*", "@types/convict@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/@types/convict/-/convict-6.0.1.tgz#224832262e1e3b5a7cca5bcc88e550ca70b6b230" + integrity sha512-5094Bw9nSvkUNV7qqUvzDt6lgWZpKbpR/AJu8lmtojKa12nxtHHGrPFwK3YP4YZA1rCc2tbpI5TUEBU35eqVxg== + dependencies: + "@types/node" "*" + "@types/debug@^4.1.5": version "4.1.5" resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.5.tgz#b14efa8852b7768d898906613c23f688713e02cd" @@ -4094,6 +4108,21 @@ convert-source-map@^0.3.3: resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-0.3.5.tgz#f1d802950af7dd2631a1febe0596550c86ab3190" integrity sha1-8dgClQr33SYxof6+BZZVDIarMZA= +convict-format-with-validator@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/convict-format-with-validator/-/convict-format-with-validator-6.0.1.tgz#a0ff08b663c40beac480d1a0d39b40382b9e39a3" + integrity sha512-6/O0W9/0MESdL+0fY7glQlWHs+wnP9xXlB1tjY/Wb9ujCCEBqMP8VCjtLvRlBdfwgONrpb+DTPSHQRhUO5aTlQ== + dependencies: + validator "^11.1.0" + +convict@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/convict/-/convict-6.0.1.tgz#d909a844d8a1187d1366d47d9bc21ec052e8bf0d" + integrity sha512-M4YNNq5NV4/VS8JhvBSHAokwvQRL4evEuU0VFe1GNPiqnj9TAkLXpf39ImCCVZlsp3CFp04bc/kRSWPGsJGJWg== + dependencies: + lodash.clonedeep "^4.5.0" + yargs-parser "^18.1.3" + cookie-signature@1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" @@ -13442,6 +13471,11 @@ validate-npm-package-license@^3.0.1: spdx-correct "^3.0.0" spdx-expression-parse "^3.0.0" +validator@^11.1.0: + version "11.1.0" + resolved "https://registry.yarnpkg.com/validator/-/validator-11.1.0.tgz#ac18cac42e0aa5902b603d7a5d9b7827e2346ac4" + integrity sha512-qiQ5ktdO7CD6C/5/mYV4jku/7qnqzjrxb3C/Q5wR3vGGinHTgJZN/TdFT3ZX4vXhX2R1PXx42fB1cn5W+uJ4lg== + value-equal@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/value-equal/-/value-equal-1.0.1.tgz#1e0b794c734c5c0cade179c437d356d931a34d6c" @@ -14076,7 +14110,7 @@ yargs-parser@^13.1.2: camelcase "^5.0.0" decamelize "^1.2.0" -yargs-parser@^18.1.2: +yargs-parser@^18.1.2, yargs-parser@^18.1.3: version "18.1.3" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0" integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ== From eea8a84f03087df43363050f840a23c6d6ecdf44 Mon Sep 17 00:00:00 2001 From: Vittorio Palmisano Date: Tue, 27 Apr 2021 20:30:25 +0200 Subject: [PATCH 06/57] using new config --- compose/config/edumeet-app-config.js | 334 +-------------------------- 1 file changed, 1 insertion(+), 333 deletions(-) diff --git a/compose/config/edumeet-app-config.js b/compose/config/edumeet-app-config.js index 9829b033..dc87a31a 100644 --- a/compose/config/edumeet-app-config.js +++ b/compose/config/edumeet-app-config.js @@ -1,338 +1,6 @@ // eslint-disable-next-line var config = { - loginEnabled : false, developmentPort : 8443, - productionPort : 3443, - - /* - // If the server component runs on a different host than the app - // you can uncomment the following line and specify the host name. - serverHostname : 'external-server.com', - */ - - /** - * Supported browsers version - * in bowser satisfy format. - * See more: - * https://www.npmjs.com/package/bowser#filtering-browsers - * Otherwise you got a unsupported browser page - */ - supportedBrowsers : - { - 'windows' : { - 'internet explorer' : '>12', - 'microsoft edge' : '>18' - }, - 'safari' : '>12', - 'firefox' : '>=60', - 'chrome' : '>=74', - 'chromium' : '>=74', - 'opera' : '>=62', - 'samsung internet for android' : '>=11.1.1.52' - }, - - /** - * Network priorities - * DSCP bits set by browser according this priority values. - * ("high" means actually: EF for audio, and AF41 for Video in chrome) - * https://en.wikipedia.org/wiki/Differentiated_services - */ - networkPriorities : - { - 'audio' : 'high', - 'mainVideo' : 'high', - 'additionalVideos' : 'medium', - 'screenShare' : 'medium' - }, - - /** - * Resolutions: - * - * low ~ 320x240 - * medium ~ 640x480 - * high ~ 1280x720 - * veryhigh ~ 1920x1080 - * ultra ~ 3840x2560 - * - **/ - - /** - * Frame rates: - * - * 1, 5, 10, 15, 20, 25, 30 - * - **/ - // The aspect ratio of the videos as shown on - // the screen. This is changeable in client settings. - // This value must match one of the defined values in - // viewAspectRatios EXACTLY (e.g. 1.333) - viewAspectRatio : 1.777, - // These are the selectable aspect ratios in the settings - viewAspectRatios : [ { - value : 1.333, // 4 / 3 - label : '4 : 3' - }, { - value : 1.777, // 16 / 9 - label : '16 : 9' - } ], - // The aspect ratio of the video from the camera - // this is not changeable in settings, only config - videoAspectRatio : 1.777, - defaultResolution : 'medium', - defaultFrameRate : 15, - defaultScreenResolution : 'veryhigh', - defaultScreenSharingFrameRate : 5, - // Enable or disable simulcast for webcam video - simulcast : true, - // Enable or disable simulcast for screen sharing video - simulcastSharing : false, - // Define different encodings for various resolutions of the video - simulcastProfiles : - { - 3840 : - [ - { scaleResolutionDownBy: 4, maxBitRate: 1500000 }, - { scaleResolutionDownBy: 2, maxBitRate: 4000000 }, - { scaleResolutionDownBy: 1, maxBitRate: 10000000 } - ], - 1920 : - [ - { scaleResolutionDownBy: 4, maxBitRate: 750000 }, - { scaleResolutionDownBy: 2, maxBitRate: 1500000 }, - { scaleResolutionDownBy: 1, maxBitRate: 4000000 } - ], - 1280 : - [ - { scaleResolutionDownBy: 4, maxBitRate: 250000 }, - { scaleResolutionDownBy: 2, maxBitRate: 900000 }, - { scaleResolutionDownBy: 1, maxBitRate: 3000000 } - ], - 640 : - [ - { scaleResolutionDownBy: 2, maxBitRate: 250000 }, - { scaleResolutionDownBy: 1, maxBitRate: 900000 } - ], - 320 : - [ - { scaleResolutionDownBy: 1, maxBitRate: 250000 } - ] - }, - - // The adaptive spatial layer selection scaling factor (in the range [0.5, 1.0]) - // example: - // with level width=640px, the minimum width required to trigger the - // level change will be: 640 * 0.75 = 480px - adaptiveScalingFactor : 0.75, - - /** - * White listing browsers that support audio output device selection. - * It is not yet fully implemented in Firefox. - * See: https://bugzilla.mozilla.org/show_bug.cgi?id=1498512 - */ - audioOutputSupportedBrowsers : - [ - 'chrome', - 'opera' - ], - // Socket.io request timeout - requestTimeout : 20000, - requestRetries : 3, - transportOptions : - { - tcp : true - }, - // defaults for audio setting on new clients / can be customized and overruled from client side - defaultAudio : - { - autoGainControl : true, // default : true - echoCancellation : true, // default : true - noiseSuppression : true, // default : true - // Automatically unmute speaking above noisThereshold - voiceActivatedUnmute : false, // default : false - // This is only for voiceActivatedUnmute and audio-indicator - noiseThreshold : -60 // default -60 - }, - // Audio options for now only centrally from config file: - centralAudioOptions : - { - // will not eat that much bandwith thanks to opus - sampleRate : 48000, // default : 48000 and don't go higher - // usually mics are mono so this saves bandwidth - channelCount : 1, // default : 1 - volume : 1.0, // default : 1.0 - sampleSize : 16, // default : 16 - // usually mics are mono so this saves bandwidth - opusStereo : false, // default : false - opusDtx : true, // default : true / will save bandwidth - opusFec : true, // default : true / forward error correction - opusPtime : '20', // default : 20 / minimum packet time (3, 5, 10, 20, 40, 60, 120) - opusMaxPlaybackRate : 48000 // default : 48000 and don't go higher - }, - - /** - * Set max number participants in one room that join - * unmuted. Next participant will join automatically muted - * Default value is 4 - * - * Set it to 0 to auto mute all, - * Set it to negative (-1) to never automatically auto mute - * but use it with caution - * full mesh audio strongly decrease room capacity! - */ - autoMuteThreshold : 4, - background : 'images/background.jpg', - defaultLayout : 'democratic', // democratic, filmstrip - // If true, will show media control buttons in separate - // control bar, not in the ME container. - buttonControlBar : false, - // If false, will push videos away to make room for side - // drawer. If true, will overlay side drawer over videos - drawerOverlayed : true, - // Position of notifications - notificationPosition : 'right', - - /** - * Set the notificationSounds. Valid keys are: - * 'parkedPeer', 'parkedPeers', 'raisedHand', 'chatMessage', - * 'sendFile', 'newPeer' and 'default'. - * - * Not defining a key is equivalent to using the default notification sound. - * Setting 'play' to null disables the sound notification. - */ - notificationSounds : { - chatMessage : { - play : '/sounds/notify-chat.mp3' - }, - raisedHand : { - play : '/sounds/notify-hand.mp3' - }, - default : { - delay : 5000, // minimum delay between alert sounds [ms] - play : '/sounds/notify.mp3' - } - }, - // Timeout for autohiding topbar and button control bar - hideTimeout : 3000, - // max number of participant that will be visible in - // as speaker - lastN : 4, - mobileLastN : 1, - // Highest number of lastN the user can select manually in - // userinteface - maxLastN : 5, - // If truthy, users can NOT change number of speakers visible - lockLastN : false, - // Show logo if "logo" is not null, else show title - // Set logo file name using logo.* pattern like "logo.png" to not track it by git - logo : 'images/logo.edumeet.svg', - title : 'edumeet', - // Service & Support URL - // if not set then not displayed on the about modals - supportUrl : 'https://support.example.com', - // Privacy and dataprotection URL or path - // by default privacy/privacy.html - // that is a placeholder for your policies - // - // but an external url could be also used here - privacyUrl : 'privacy/privacy.html', - theme : - { - palette : - { - primary : - { - main : '#313131' - } - }, - overrides : - { - MuiAppBar : - { - colorPrimary : - { - backgroundColor : '#313131' - } - }, - MuiButton : - { - containedPrimary : - { - backgroundColor : '#5F9B2D', - '&:hover' : - { - backgroundColor : '#5F9B2D' - } - }, - containedSecondary : - { - backgroundColor : '#f50057', - '&:hover' : - { - backgroundColor : '#f50057' - } - } - - }, - - /* - MuiIconButton : - { - colorPrimary : - { - backgroundColor : '#5F9B2D', - '&:hover' : - { - backgroundColor : '#5F9B2D' - } - }, - colorSecondary : - { - backgroundColor : '#f50057', - '&:hover' : - { - backgroundColor : '#f50057' - } - } - - }, - */ - - MuiFab : - { - primary : - { - backgroundColor : '#5F9B2D', - '&:hover' : - { - backgroundColor : '#5F9B2D' - } - }, - secondary : - { - backgroundColor : '#f50057', - '&:hover' : - { - backgroundColor : '#f50057' - } - } - - }, - MuiBadge : - { - colorPrimary : - { - backgroundColor : '#5F9B2D', - '&:hover' : - { - backgroundColor : '#518029' - } - } - } - }, - typography : - { - useNextVariants : true - } - } + productionPort : 3443 }; From 822215ddce75219c9282c6c65da925c8cf358215 Mon Sep 17 00:00:00 2001 From: Vittorio Palmisano Date: Tue, 27 Apr 2021 20:30:40 +0200 Subject: [PATCH 07/57] added more validators --- app/src/config.ts | 76 ++++++++++++++++++++++++++++------------------- 1 file changed, 45 insertions(+), 31 deletions(-) diff --git a/app/src/config.ts b/app/src/config.ts index ad5b2ab2..d04e36b1 100644 --- a/app/src/config.ts +++ b/app/src/config.ts @@ -4,25 +4,37 @@ import Logger from './Logger'; const logger = new Logger('config'); -convict.addFormat(convictFormatWithValidator.ipaddress); +convict.addFormats(convictFormatWithValidator); + +function assert(assertion: Boolean, msg: string) +{ + if (!assertion) + throw new Error(msg); +} + +convict.addFormat({ + name : 'float', + coerce : (v: string) => parseFloat(v), + validate : (v: number) => assert(Number.isFinite(v), 'must be a number') +}); const config = convict({ loginEnabled : { - doc : 'The login is enabled.', - format : Boolean, + doc : 'If the login is enabled.', + format : 'Boolean', default : false }, developmentPort : { doc : 'The development server listening port.', - format : Number, + format : 'port', default : 3443 }, productionPort : { doc : 'The production server listening port.', - format : Number, + format : 'port', default : 443 }, serverHostname : @@ -69,10 +81,10 @@ const config = convict({ format : Object, default : { - audio : 'high', - mainVideo : 'high', + audio : 'high', + mainVideo : 'high', additionalVideos : 'medium', - screenShare : 'medium' + screenShare : 'medium' } }, // The aspect ratio of the videos as shown on the screen. @@ -82,7 +94,7 @@ const config = convict({ viewAspectRatio : { doc : 'The aspect ratio of the videos as shown on the screen.', - format : Number, + format : 'float', default : 1.777 }, viewAspectRatios : @@ -106,43 +118,43 @@ const config = convict({ videoAspectRatio : { doc : 'The aspect ratio of the video from the camera.', - format : Number, + format : 'float', default : 1.777 }, defaultResolution : { doc : 'The default video camera capture resolution.', - format : String, + format : [ 'low', 'medium', 'high', 'veryhigh', 'ultra' ], default : 'medium' }, defaultFrameRate : { doc : 'The default video camera capture framerate.', - format : Number, + format : 'nat', default : 15 }, defaultScreenResolution : { doc : 'The default screen sharing resolution.', - format : String, + format : [ 'low', 'medium', 'high', 'veryhigh', 'ultra' ], default : 'veryhigh' }, defaultScreenSharingFrameRate : { doc : 'The default screen sharing framerate.', - format : Number, + format : 'nat', default : 5 }, simulcast : { doc : 'Enable or disable simulcast for webcam video.', - format : Boolean, + format : 'Boolean', default : true }, simulcastSharing : { doc : 'Enable or disable simulcast for screen sharing video.', - format : Boolean, + format : 'Boolean', default : false }, simulcastProfiles : @@ -208,13 +220,13 @@ const config = convict({ requestTimeout : { doc : 'The Socket.io request timeout.', - format : Number, + format : 'nat', default : 20000 }, requestRetries : { doc : 'The Socket.io request maximum retries.', - format : Number, + format : 'nat', default : 3 }, transportOptions : @@ -264,7 +276,7 @@ const config = convict({ }, /** - * Set max number participants in one room that join + * Set max 'int' participants in one room that join * unmuted. Next participant will join automatically muted * Default value is 4 * @@ -275,8 +287,8 @@ const config = convict({ */ autoMuteThreshold : { - doc : 'Set max number participants in one room that join unmuted.', - format : Number, + doc : 'Set the max number of participants in one room that join unmuted.', + format : 'nat', default : 4 }, background : @@ -294,13 +306,13 @@ const config = convict({ buttonControlBar : { doc : 'If true, will show media control buttons in separate control bar, not in the ME container.', - format : Boolean, + format : 'Boolean', default : false }, drawerOverlayed : { doc : 'If false, will push videos away to make room for side drawer. If true, will overlay side drawer over videos.', - format : Boolean, + format : 'Boolean', default : true }, notificationPosition : @@ -339,31 +351,31 @@ const config = convict({ hideTimeout : { doc : 'Timeout for autohiding topbar and button control bar.', - format : Number, + format : 'int', default : 3000 }, lastN : { doc : 'The max number of participant that will be visible in as speaker.', - format : Number, + format : 'nat', default : 4 }, mobileLastN : { doc : 'The max number of participant that will be visible in as speaker for mobile users.', - format : Number, + format : 'nat', default : 1 }, maxLastN : { doc : 'The highest number of lastN the user can select manually in the user interface.', - format : Number, + format : 'nat', default : 5 }, lockLastN : { doc : 'If truthy, users can NOT change number of speakers visible.', - format : Boolean, + format : 'Boolean', default : false }, // Show logo if "logo" is not null, else show title @@ -371,7 +383,7 @@ const config = convict({ logo : { doc : 'If not null, it shows the logo loaded from URL, else it shows the title.', - format : String, + format : 'url', default : 'images/logo.edumeet.svg' }, title : @@ -383,7 +395,7 @@ const config = convict({ supportUrl : { doc : 'Service & Support URL if not set then not displayed on the about modals.', - format : String, + format : 'url', default : 'https://support.example.com' }, // Privacy and data protection URL or path by default privacy/privacy.html @@ -493,11 +505,13 @@ const config = convict({ }, typography : { - useNextVariants : { format: Boolean, default: true } + useNextVariants : { format: 'Boolean', default: true } } } }); +logger.debug('schema:', config.getSchemaString()); + // Load config from window object config.load((window as any).config); From 5eadcc6bb76bf6cbef54bcfd1a3f2b2b7b914d7d Mon Sep 17 00:00:00 2001 From: Vittorio Palmisano Date: Wed, 28 Apr 2021 18:28:31 +0200 Subject: [PATCH 08/57] fixed docker image file and docs --- compose/README.md | 2 +- compose/edumeet/Dockerfile | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/compose/README.md b/compose/README.md index 50a809aa..1383f43d 100644 --- a/compose/README.md +++ b/compose/README.md @@ -28,5 +28,5 @@ also for the websocket connection. Rebuild the web application bundle: ```sh -docker-compose exec edumeet sh -c "cd app && yarn && yarn build" +docker-compose exec -u $UID edumeet sh -c "cd app && yarn && yarn build" ``` diff --git a/compose/edumeet/Dockerfile b/compose/edumeet/Dockerfile index 8744c1d1..d7b52c3b 100644 --- a/compose/edumeet/Dockerfile +++ b/compose/edumeet/Dockerfile @@ -1,4 +1,4 @@ -FROM node:lts-buster-slim +FROM node:14-buster-slim RUN apt-get update && \ apt-get install -y git build-essential python pkg-config libssl-dev && \ apt-get clean @@ -6,5 +6,7 @@ WORKDIR /edumeet ENV DEBUG=edumeet*,mediasoup* RUN npm install -g nodemon && \ npm install -g concurrently -CMD cd app && yarn && yarn build -CMD concurrently --names "server,app" "cd server && yarn && yarn dev" "cd app && yarn && yarn start" \ No newline at end of file +RUN touch /.yarnrc && mkdir -p /.yarn /.cache/yarn && chmod 775 /.yarn /.yarnrc /.cache/yarn +CMD concurrently --names "server,app" \ + "cd server && yarn && yarn dev" \ + "cd app && yarn && yarn build && yarn start" From da53b3465ea50c99b735bd4edc962c4f70631e2c Mon Sep 17 00:00:00 2001 From: Vittorio Palmisano Date: Wed, 28 Apr 2021 18:28:54 +0200 Subject: [PATCH 09/57] adding config docs generator --- app/package.json | 4 +++- app/src/config.ts | 46 ++++++++++++++++++++++++++++++++++------------ app/src/index.js | 2 +- app/tsconfig.json | 5 ++--- app/yarn.lock | 39 ++++++++++++++++++++++++++++++++++++++- 5 files changed, 78 insertions(+), 18 deletions(-) diff --git a/app/package.json b/app/package.json index e3fa9b32..33264dab 100644 --- a/app/package.json +++ b/app/package.json @@ -72,6 +72,7 @@ "electron": "electron --no-sandbox .", "dev": "nf start -p 3000", "lint": "eslint -c .eslintrc.json --ext .js,.ts,.tsx src", + "lint:fix": "eslint -c .eslintrc.json --ext .js,.ts,.tsx src --fix", "format": "prettier --write 'src/**/*.{ts,tsx}'" }, "browserslist": [ @@ -103,6 +104,7 @@ "prettier": "^2.2.1", "prettier-eslint": "^12.0.0", "prettier-eslint-cli": "^5.0.1", - "redux-mock-store": "^1.5.3" + "redux-mock-store": "^1.5.3", + "ts-node": "^9.1.1" } } diff --git a/app/src/config.ts b/app/src/config.ts index d04e36b1..1c8e7820 100644 --- a/app/src/config.ts +++ b/app/src/config.ts @@ -1,8 +1,5 @@ import convict from 'convict'; import * as convictFormatWithValidator from 'convict-format-with-validator'; -import Logger from './Logger'; - -const logger = new Logger('config'); convict.addFormats(convictFormatWithValidator); @@ -18,7 +15,7 @@ convict.addFormat({ validate : (v: number) => assert(Number.isFinite(v), 'must be a number') }); -const config = convict({ +const configSchema = convict({ loginEnabled : { doc : 'If the login is enabled.', @@ -510,19 +507,44 @@ const config = convict({ } }); -logger.debug('schema:', config.getSchemaString()); - // Load config from window object -config.load((window as any).config); +configSchema.load((window as any).config); + +function formatDocs(docs: any, property: string | null, schema: any) +{ + if (schema._cvtProperties) + { + Object.entries(schema._cvtProperties).forEach(([ name, value ]) => + { + formatDocs(docs, `${property ? `${property}.` : ''}${name}`, value); + }); + + return; + } + else if (property) + { + docs[property] = + { + doc : schema.doc, + format : schema.format, + default : schema.default + }; + } +} // Perform validation -config.validate({ allowed: 'strict' }); +configSchema.validate({ allowed: 'strict' }); -const properties = config.getProperties(); +const config = configSchema.getProperties(); -logger.debug('properties:', properties); +// eslint-disable-next-line +console.log('Using config:', config); // Override the window config with the validated properties. -(window as any)['config'] = properties; +(window as any)['config'] = config; -export default properties; +export { + configSchema, + config, + formatDocs +}; diff --git a/app/src/index.js b/app/src/index.js index a7e5df61..66e4d060 100644 --- a/app/src/index.js +++ b/app/src/index.js @@ -29,7 +29,7 @@ import { detectDevice } from 'mediasoup-client'; import './index.css'; -import config from './config'; +import { config } from './config'; const App = LazyPreload(() => import(/* webpackChunkName: "app" */ './components/App')); diff --git a/app/tsconfig.json b/app/tsconfig.json index a273b0cf..0114a3ba 100644 --- a/app/tsconfig.json +++ b/app/tsconfig.json @@ -20,7 +20,6 @@ "noEmit": true, "jsx": "react-jsx" }, - "include": [ - "src" - ] + "include": ["src/**/*.ts"], + "exclude": ["node_modules", "**/*.spec.ts"] } diff --git a/app/yarn.lock b/app/yarn.lock index 0ec87fdf..1abff4ad 100644 --- a/app/yarn.lock +++ b/app/yarn.lock @@ -2603,6 +2603,11 @@ aproba@^1.1.1: resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw== +arg@^4.1.0: + version "4.1.3" + resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" + integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== + argparse@^1.0.7: version "1.0.10" resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" @@ -4249,6 +4254,11 @@ create-react-class@^15.6.0: loose-envify "^1.3.1" object-assign "^4.1.1" +create-require@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" + integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== + create-torrent@^4.4.1, create-torrent@^4.4.2: version "4.6.0" resolved "https://registry.yarnpkg.com/create-torrent/-/create-torrent-4.6.0.tgz#63d639d0f7c23e65ba9c18a053e57c71f9cda35b" @@ -4765,6 +4775,11 @@ diff-sequences@^26.6.2: resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-26.6.2.tgz#48ba99157de1923412eed41db6b6d4aa9ca7c0b1" integrity sha512-Mv/TDa3nZ9sbc5soK+OoA74BsS3mL37yixCvUAQkiuA4Wz6YtwP/K47n2rv2ovzHZvoiQeA5FTQOschKkEwB0Q== +diff@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" + integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== + diffie-hellman@^5.0.0: version "5.0.3" resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875" @@ -8554,6 +8569,11 @@ make-dir@^3.0.0, make-dir@^3.0.2: dependencies: semver "^6.0.0" +make-error@^1.1.1: + version "1.3.6" + resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" + integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== + make-plural@^4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/make-plural/-/make-plural-4.3.0.tgz#f23de08efdb0cac2e0c9ba9f315b0dff6b4c2735" @@ -12262,7 +12282,7 @@ source-map-resolve@^0.5.0, source-map-resolve@^0.5.2: source-map-url "^0.4.0" urix "^0.1.0" -source-map-support@^0.5.6, source-map-support@~0.5.12, source-map-support@~0.5.19: +source-map-support@^0.5.17, source-map-support@^0.5.6, source-map-support@~0.5.12, source-map-support@~0.5.19: version "0.5.19" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61" integrity sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw== @@ -13055,6 +13075,18 @@ tryer@^1.0.1: resolved "https://registry.yarnpkg.com/tryer/-/tryer-1.0.1.tgz#f2c85406800b9b0f74c9f7465b81eaad241252f8" integrity sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA== +ts-node@^9.1.1: + version "9.1.1" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-9.1.1.tgz#51a9a450a3e959401bda5f004a72d54b936d376d" + integrity sha512-hPlt7ZACERQGf03M253ytLY3dHbGNGrAq9qIHWUY9XHYl1z7wYngSr3OQ5xmui8o2AaxsONxIzjafLUiWBo1Fg== + dependencies: + arg "^4.1.0" + create-require "^1.1.0" + diff "^4.0.1" + make-error "^1.1.1" + source-map-support "^0.5.17" + yn "3.1.1" + ts-pnp@1.2.0, ts-pnp@^1.1.6: version "1.2.0" resolved "https://registry.yarnpkg.com/ts-pnp/-/ts-pnp-1.2.0.tgz#a500ad084b0798f1c3071af391e65912c86bca92" @@ -14182,6 +14214,11 @@ yeast@0.1.2: resolved "https://registry.yarnpkg.com/yeast/-/yeast-0.1.2.tgz#008e06d8094320c372dbc2f8ed76a0ca6c8ac419" integrity sha1-AI4G2AlDIMNy28L47XagymyKxBk= +yn@3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" + integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== + yocto-queue@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" From 956aadd1af8fefbcfa2e6874eb13cec8143a721b Mon Sep 17 00:00:00 2001 From: Vittorio Palmisano Date: Thu, 29 Apr 2021 15:17:33 +0200 Subject: [PATCH 10/57] added logic for updating the settings in the store when the app config changes --- app/package.json | 1 + app/public/config/config.example.js | 16 ++++----- app/src/config.ts | 50 ++++++++++++++++++---------- app/src/reducers/config.js | 22 ++++++++++++ app/src/reducers/rootReducer.js | 4 ++- app/src/reducers/settings.js | 34 +++++++++++-------- app/src/store.js | 31 +++++++++++++++-- app/yarn.lock | 5 +++ compose/config/edumeet-app-config.js | 2 +- 9 files changed, 120 insertions(+), 45 deletions(-) create mode 100644 app/src/reducers/config.js diff --git a/app/package.json b/app/package.json index 33264dab..fbdcfbd5 100644 --- a/app/package.json +++ b/app/package.json @@ -27,6 +27,7 @@ "convict": "^6.0.1", "convict-format-with-validator": "^6.0.1", "create-torrent": "^4.4.1", + "deep-object-diff": "^1.1.0", "dompurify": "^2.0.7", "domready": "^1.0.8", "end-of-stream": "1.4.1", diff --git a/app/public/config/config.example.js b/app/public/config/config.example.js index 4bde4936..3c275bc0 100644 --- a/app/public/config/config.example.js +++ b/app/public/config/config.example.js @@ -78,17 +78,17 @@ var config = } ], // The aspect ratio of the video from the camera // this is not changeable in settings, only config - videoAspectRatio : 1.777, - defaultResolution : 'medium', - defaultFrameRate : 15, - defaultScreenResolution : 'veryhigh', - defaultScreenSharingFrameRate : 5, + videoAspectRatio : 1.777, + resolution : 'medium', + frameRate : 15, + screenResolution : 'veryhigh', + screenSharingFrameRate : 5, // Enable or disable simulcast for webcam video - simulcast : true, + simulcast : true, // Enable or disable simulcast for screen sharing video - simulcastSharing : false, + simulcastSharing : false, // Define different encodings for various resolutions of the video - simulcastProfiles : + simulcastProfiles : { 3840 : [ diff --git a/app/src/config.ts b/app/src/config.ts index 1c8e7820..ee07d4e7 100644 --- a/app/src/config.ts +++ b/app/src/config.ts @@ -118,25 +118,25 @@ const configSchema = convict({ format : 'float', default : 1.777 }, - defaultResolution : + resolution : { doc : 'The default video camera capture resolution.', format : [ 'low', 'medium', 'high', 'veryhigh', 'ultra' ], default : 'medium' }, - defaultFrameRate : + frameRate : { doc : 'The default video camera capture framerate.', format : 'nat', default : 15 }, - defaultScreenResolution : + screenResolution : { doc : 'The default screen sharing resolution.', format : [ 'low', 'medium', 'high', 'veryhigh', 'ultra' ], default : 'veryhigh' }, - defaultScreenSharingFrameRate : + screenSharingFrameRate : { doc : 'The default screen sharing framerate.', format : 'nat', @@ -234,21 +234,35 @@ const configSchema = convict({ tcp : true } }, - // defaults for audio setting on new clients / can be customized and overruled from client side - defaultAudio : + autoGainControl : { - doc : 'Defaults audio settings.', - format : Object, - default : - { - autoGainControl : true, // default : true - echoCancellation : true, // default : true - noiseSuppression : true, // default : true - // Automatically unmute speaking above noisThereshold - voiceActivatedUnmute : false, // default : false - // This is only for voiceActivatedUnmute and audio-indicator - noiseThreshold : -60 // default -60 - } + doc : 'Auto gain control enabled.', + format : 'Boolean', + default : true + }, + echoCancellation : + { + doc : 'Echo cancellation enabled.', + format : 'Boolean', + default : true + }, + noiseSuppression : + { + doc : 'Noise suppression enabled.', + format : 'Boolean', + default : true + }, + voiceActivatedUnmute : + { + doc : 'Automatically unmute speaking above noiseThreshold.', + format : 'Boolean', + default : false + }, + noiseThreshold : + { + doc : 'This is only for voiceActivatedUnmute and audio-indicator.', + format : 'int', + default : -60 }, // Audio options for now only centrally from config file: centralAudioOptions : diff --git a/app/src/reducers/config.js b/app/src/reducers/config.js new file mode 100644 index 00000000..975ca03c --- /dev/null +++ b/app/src/reducers/config.js @@ -0,0 +1,22 @@ +import { config as defaultConfig } from '../config'; + +const initialState = +{ + ...defaultConfig +}; + +const config = (state = initialState, action) => +{ + switch (action.type) + { + case 'CONFIG_SET': + { + return { ...action.payload }; + } + + default: + return state; + } +}; + +export default config; diff --git a/app/src/reducers/rootReducer.js b/app/src/reducers/rootReducer.js index ce82e306..d837c7f9 100644 --- a/app/src/reducers/rootReducer.js +++ b/app/src/reducers/rootReducer.js @@ -13,6 +13,7 @@ import files from './files'; import settings from './settings'; import transports from './transports'; import intl from './intl'; +import config from './config'; // import { intlReducer } from 'react-intl-redux'; export default combineReducers({ @@ -30,5 +31,6 @@ export default combineReducers({ files, settings, // intl : intlReducer - intl + intl, + config }); diff --git a/app/src/reducers/settings.js b/app/src/reducers/settings.js index e9a65f05..2f20f0da 100644 --- a/app/src/reducers/settings.js +++ b/app/src/reducers/settings.js @@ -1,21 +1,23 @@ +import { config } from '../config'; + const initialState = { displayName : '', selectedWebcam : null, selectedAudioDevice : null, advancedMode : false, - autoGainControl : true, - echoCancellation : true, - noiseSuppression : true, - voiceActivatedUnmute : false, - noiseThreshold : -50, + autoGainControl : config.autoGainControl, + echoCancellation : config.echoCancellation, + noiseSuppression : config.noiseSuppression, + voiceActivatedUnmute : config.voiceActivatedUnmute, + noiseThreshold : config.noiseThreshold, audioMuted : false, videoMuted : false, // low, medium, high, veryhigh, ultra - resolution : window.config.defaultResolution || 'medium', - frameRate : window.config.defaultFrameRate || 15, - screenSharingResolution : window.config.defaultScreenResolution || 'veryhigh', - screenSharingFrameRate : window.config.defaultScreenSharingFrameRate || 5, + resolution : config.resolution, + frameRate : config.frameRate, + screenSharingResolution : config.screenResolution, + screenSharingFrameRate : config.screenSharingFrameRate, lastN : 4, permanentTopBar : true, hiddenControls : false, @@ -23,12 +25,11 @@ const initialState = notificationSounds : true, mirrorOwnVideo : true, hideNoVideoParticipants : false, - buttonControlBar : window.config.buttonControlBar || false, - drawerOverlayed : (typeof window.config.drawerOverlayed === 'undefined') ? true : window.config.drawerOverlayed, - aspectRatio : window.config.viewAspectRatio || 1.777, // 16 : 9 + buttonControlBar : config.buttonControlBar, + drawerOverlayed : config.drawerOverlayed, + aspectRatio : config.viewAspectRatio || 1.777, // 16 : 9 mediaPerms : { audio: true, video: true }, - localPicture : null, - ...window.config.defaultAudio + localPicture : null }; const settings = (state = initialState, action) => @@ -260,6 +261,11 @@ const settings = (state = initialState, action) => return { ...state, localPicture }; } + case 'SETTINGS_UPDATE': + { + return { ...state, ...action.payload }; + } + default: return state; } diff --git a/app/src/store.js b/app/src/store.js index 9d620940..bdd222c6 100644 --- a/app/src/store.js +++ b/app/src/store.js @@ -8,8 +8,13 @@ import { createLogger } from 'redux-logger'; import { createMigrate, persistStore, persistReducer } from 'redux-persist'; import storage from 'redux-persist/lib/storage'; import autoMergeLevel2 from 'redux-persist/lib/stateReconciler/autoMergeLevel2'; -import rootReducer from './reducers/rootReducer'; import { createFilter } from 'redux-persist-transform-filter'; +import { diff } from 'deep-object-diff'; +import rootReducer from './reducers/rootReducer'; +import Logger from './Logger'; +import { config } from './config'; + +const logger = new Logger('store'); const migrations = { @@ -60,7 +65,7 @@ const persistConfig = version : 2, migrate : createMigrate(migrations, { debug: false }), stateReconciler : autoMergeLevel2, - whitelist : [ 'settings', 'intl' ] + whitelist : [ 'settings', 'intl', 'config' ] }; const saveSubsetFilter = createFilter( @@ -113,7 +118,27 @@ export const store = createStore( enhancer ); -export const persistor = persistStore(store); +export const persistor = persistStore(store, null, () => +{ + // Check if the app config differs from the stored version. + const currentConfig = store.getState().config; + const changed = diff(currentConfig, config); + const changedKeys = Object.keys(changed); + + if (changedKeys.length) + { + logger.debug('store config changed:', changed); + const changedSettings = {}; + + changedKeys.forEach((key) => + { + changedSettings[key] = config[key]; + }); + + store.dispatch({ type: 'SETTINGS_UPDATE', payload: changedSettings }); + store.dispatch({ type: 'CONFIG_SET', payload: config }); + } +}); /* diff --git a/app/yarn.lock b/app/yarn.lock index 1abff4ad..610c2b63 100644 --- a/app/yarn.lock +++ b/app/yarn.lock @@ -4669,6 +4669,11 @@ deep-is@^0.1.3, deep-is@~0.1.3: resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ= +deep-object-diff@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/deep-object-diff/-/deep-object-diff-1.1.0.tgz#d6fabf476c2ed1751fc94d5ca693d2ed8c18bc5a" + integrity sha512-b+QLs5vHgS+IoSNcUE4n9HP2NwcHj7aqnJWsjPtuG75Rh5TOaGt0OjAYInh77d5T16V5cRDC+Pw/6ZZZiETBGw== + deepmerge@^4.2.2: version "4.2.2" resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955" diff --git a/compose/config/edumeet-app-config.js b/compose/config/edumeet-app-config.js index dc87a31a..ab4434ef 100644 --- a/compose/config/edumeet-app-config.js +++ b/compose/config/edumeet-app-config.js @@ -2,5 +2,5 @@ var config = { developmentPort : 8443, - productionPort : 3443 + productionPort : 3443, }; From a40831e9df819a19acc7179dc95dd84565d65d14 Mon Sep 17 00:00:00 2001 From: Vittorio Palmisano Date: Thu, 29 Apr 2021 15:17:42 +0200 Subject: [PATCH 11/57] fixed type errors --- app/src/components/Controls/TopBar.js | 2 +- app/src/components/JoinDialog.js | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/src/components/Controls/TopBar.js b/app/src/components/Controls/TopBar.js index 64336cc9..c0b5ce7f 100644 --- a/app/src/components/Controls/TopBar.js +++ b/app/src/components/Controls/TopBar.js @@ -514,7 +514,7 @@ const TopBar = (props) => aria-label={locale.split(/[-_]/)[0]} className={classes.actionButton} color='secondary' - disableRipple='true' + disableRipple onClick={(event) => handleMenuOpen(event, 'localeMenu')} > {locale.split(/[-_]/)[0]} diff --git a/app/src/components/JoinDialog.js b/app/src/components/JoinDialog.js index 2409e4cb..4d291cd6 100644 --- a/app/src/components/JoinDialog.js +++ b/app/src/components/JoinDialog.js @@ -357,7 +357,7 @@ const JoinDialog = ({ className={classes.actionButton} aria-label={locale.split(/[-_]/)[0]} color='secondary' - disableRipple='true' + disableRipple style={{ backgroundColor: 'transparent' }} {...bindTrigger(popupState)} > @@ -773,10 +773,10 @@ JoinDialog.propTypes = setMediaPerms : PropTypes.func.isRequired, classes : PropTypes.object.isRequired, mediaPerms : PropTypes.object.isRequired, - setAudioMuted : PropTypes.bool.isRequired, - setVideoMuted : PropTypes.bool.isRequired, - locale : PropTypes.object.isRequired, - localesList : PropTypes.object.isRequired + setAudioMuted : PropTypes.func.isRequired, + setVideoMuted : PropTypes.func.isRequired, + locale : PropTypes.string.isRequired, + localesList : PropTypes.array.isRequired }; From f40c62abd1cc375ffd36c56bc313facdee4f17e0 Mon Sep 17 00:00:00 2001 From: Vittorio Palmisano Date: Fri, 30 Apr 2021 12:41:15 +0200 Subject: [PATCH 12/57] fixed warnings --- app/src/components/ConfigDocumentation.tsx | 102 +++++++++++++++++++++ app/src/components/Controls/TopBar.js | 6 +- 2 files changed, 105 insertions(+), 3 deletions(-) create mode 100644 app/src/components/ConfigDocumentation.tsx diff --git a/app/src/components/ConfigDocumentation.tsx b/app/src/components/ConfigDocumentation.tsx new file mode 100644 index 00000000..251d8b45 --- /dev/null +++ b/app/src/components/ConfigDocumentation.tsx @@ -0,0 +1,102 @@ +import React from 'react'; +import { withStyles } from '@material-ui/core/styles'; +import PropTypes from 'prop-types'; +import { FormattedMessage } from 'react-intl'; + +import Dialog from '@material-ui/core/Dialog'; +import DialogTitle from '@material-ui/core/DialogTitle'; +import DialogContent from '@material-ui/core/DialogContent'; +import Grid from '@material-ui/core/Grid'; +import List from '@material-ui/core/List'; +import ListItem from '@material-ui/core/ListItem'; +import ListItemText from '@material-ui/core/ListItemText'; +import ListItemAvatar from '@material-ui/core/ListItemAvatar'; +import Avatar from '@material-ui/core/Avatar'; +import WebAssetIcon from '@material-ui/icons/WebAsset'; +import ErrorIcon from '@material-ui/icons/Error'; +import Hidden from '@material-ui/core/Hidden'; + +const styles = (theme: any) => + ({ + dialogPaper : + { + width : '40vw', + [theme.breakpoints.down('lg')] : + { + width : '40vw' + }, + [theme.breakpoints.down('md')] : + { + width : '50vw' + }, + [theme.breakpoints.down('sm')] : + { + width : '70vw' + }, + [theme.breakpoints.down('xs')] : + { + width : '90vw' + } + // display : 'flex', + // flexDirection : 'column' + }, + list : { + backgroundColor : theme.palette.background.paper + }, + errorAvatar : { + width : theme.spacing(20), + height : theme.spacing(20) + } + }); + +let dense = false; + +const ConfigDocumentation = ({ + platform, + classes +}) => +{ + if (platform !== 'desktop') + dense = true; + + return ( + + + + + + + + + +
+ + + +
+
+
+
+
+ ); +}; + +ConfigDocumentation.propTypes = +{ + platform : PropTypes.string.isRequired, + classes : PropTypes.object.isRequired +}; + +export default withStyles(styles)(ConfigDocumentation); diff --git a/app/src/components/Controls/TopBar.js b/app/src/components/Controls/TopBar.js index c0b5ce7f..8a289933 100644 --- a/app/src/components/Controls/TopBar.js +++ b/app/src/components/Controls/TopBar.js @@ -944,9 +944,9 @@ TopBar.propTypes = canPromote : PropTypes.bool.isRequired, classes : PropTypes.object.isRequired, theme : PropTypes.object.isRequired, - intl : PropTypes.object.isRequired, - locale : PropTypes.object.isRequired, - localesList : PropTypes.object.isRequired + intl : PropTypes.object, + locale : PropTypes.string.isRequired, + localesList : PropTypes.array.isRequired }; const makeMapStateToProps = () => From 7074bbafb577925ff032ee32a92589bccb44be4f Mon Sep 17 00:00:00 2001 From: Vittorio Palmisano Date: Fri, 30 Apr 2021 12:41:45 +0200 Subject: [PATCH 13/57] added /config-doc page --- app/src/components/ConfigDocumentation.tsx | 152 ++++++++++----------- app/src/config.ts | 14 +- app/src/index.js | 8 ++ app/tsconfig.json | 2 +- 4 files changed, 94 insertions(+), 82 deletions(-) diff --git a/app/src/components/ConfigDocumentation.tsx b/app/src/components/ConfigDocumentation.tsx index 251d8b45..e94efc5e 100644 --- a/app/src/components/ConfigDocumentation.tsx +++ b/app/src/components/ConfigDocumentation.tsx @@ -1,102 +1,102 @@ -import React from 'react'; +import React from 'react'; // eslint-disable-line no-use-before-define import { withStyles } from '@material-ui/core/styles'; import PropTypes from 'prop-types'; import { FormattedMessage } from 'react-intl'; -import Dialog from '@material-ui/core/Dialog'; -import DialogTitle from '@material-ui/core/DialogTitle'; -import DialogContent from '@material-ui/core/DialogContent'; -import Grid from '@material-ui/core/Grid'; -import List from '@material-ui/core/List'; -import ListItem from '@material-ui/core/ListItem'; -import ListItemText from '@material-ui/core/ListItemText'; -import ListItemAvatar from '@material-ui/core/ListItemAvatar'; -import Avatar from '@material-ui/core/Avatar'; -import WebAssetIcon from '@material-ui/icons/WebAsset'; -import ErrorIcon from '@material-ui/icons/Error'; -import Hidden from '@material-ui/core/Hidden'; +import Card from '@material-ui/core/Card'; +import CardActions from '@material-ui/core/CardActions'; +import CardContent from '@material-ui/core/CardContent'; +import Typography from '@material-ui/core/Typography'; +import Table from '@material-ui/core/Table'; +import TableBody from '@material-ui/core/TableBody'; +import TableCell from '@material-ui/core/TableCell'; +import TableContainer from '@material-ui/core/TableContainer'; +import TableHead from '@material-ui/core/TableHead'; +import TableRow from '@material-ui/core/TableRow'; +import Paper from '@material-ui/core/Paper'; +import Button from '@material-ui/core/Button'; + +import { configDocs } from '../config'; const styles = (theme: any) => ({ - dialogPaper : - { - width : '40vw', - [theme.breakpoints.down('lg')] : - { - width : '40vw' - }, - [theme.breakpoints.down('md')] : - { - width : '50vw' - }, - [theme.breakpoints.down('sm')] : - { - width : '70vw' - }, - [theme.breakpoints.down('xs')] : - { - width : '90vw' - } - // display : 'flex', - // flexDirection : 'column' + table : { + minWidth : 700 + }, + pre : { + fontSize : '0.8rem' + }, + cell : { + maxWidth : '25vw', + overflow : 'auto' }, list : { backgroundColor : theme.palette.background.paper - }, - errorAvatar : { - width : theme.spacing(20), - height : theme.spacing(20) } }); -let dense = false; - const ConfigDocumentation = ({ - platform, classes +}: { + classes : any; }) => { - if (platform !== 'desktop') - dense = true; - return ( - - - - - - - - - -
- - - -
-
-
-
-
+ + + + + + + + + + + Property + Description + Format + Default value + + + + {Object.entries(configDocs).map(([ name, value ] : [ string, any ]) => + { + return ( + + {name} + {value.doc} + +
{value.format}
+
+ +
{value.default}
+
+
+ ); + })} +
+
+
+
+
+ + + +
); }; ConfigDocumentation.propTypes = { - platform : PropTypes.string.isRequired, - classes : PropTypes.object.isRequired + classes : PropTypes.object.isRequired }; export default withStyles(styles)(ConfigDocumentation); diff --git a/app/src/config.ts b/app/src/config.ts index ee07d4e7..1f7044c6 100644 --- a/app/src/config.ts +++ b/app/src/config.ts @@ -533,26 +533,30 @@ function formatDocs(docs: any, property: string | null, schema: any) formatDocs(docs, `${property ? `${property}.` : ''}${name}`, value); }); - return; + return docs; } else if (property) { docs[property] = { doc : schema.doc, - format : schema.format, - default : schema.default + format : JSON.stringify(schema.format, null, 2), + default : JSON.stringify(schema.default, null, 2) }; } + + return docs; } +const configDocs = formatDocs({}, null, configSchema.getSchema()); + // Perform validation configSchema.validate({ allowed: 'strict' }); const config = configSchema.getProperties(); // eslint-disable-next-line -console.log('Using config:', config); +console.log('Using config:', config, configDocs); // Override the window config with the validated properties. (window as any)['config'] = config; @@ -560,5 +564,5 @@ console.log('Using config:', config); export { configSchema, config, - formatDocs + configDocs }; diff --git a/app/src/index.js b/app/src/index.js index 66e4d060..e9e3558c 100644 --- a/app/src/index.js +++ b/app/src/index.js @@ -16,6 +16,7 @@ import RoomContext from './RoomContext'; import deviceInfo from './deviceInfo'; import * as meActions from './actions/meActions'; import UnsupportedBrowser from './components/UnsupportedBrowser'; +import ConfigDocumentation from './components/ConfigDocumentation'; import JoinDialog from './components/JoinDialog'; import LoginDialog from './components/AccessControl/LoginDialog'; import LoadingView from './components/Loader/LoadingView'; @@ -192,6 +193,13 @@ function run() + ( + + )} + /> diff --git a/app/tsconfig.json b/app/tsconfig.json index 0114a3ba..3d23e5ac 100644 --- a/app/tsconfig.json +++ b/app/tsconfig.json @@ -20,6 +20,6 @@ "noEmit": true, "jsx": "react-jsx" }, - "include": ["src/**/*.ts"], + "include": ["src/**/*.ts", "src/**/*.tsx"], "exclude": ["node_modules", "**/*.spec.ts"] } From ada75eb979e29ad80011315d498d7a35ecbc9d08 Mon Sep 17 00:00:00 2001 From: Vittorio Palmisano Date: Fri, 30 Apr 2021 15:17:45 +0200 Subject: [PATCH 14/57] added note --- compose/README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/compose/README.md b/compose/README.md index 1383f43d..816913ab 100644 --- a/compose/README.md +++ b/compose/README.md @@ -30,3 +30,13 @@ Rebuild the web application bundle: ```sh docker-compose exec -u $UID edumeet sh -c "cd app && yarn && yarn build" ``` + +## Known issues + +The docker virtual network used by this compose configuration (`172.22.0.0/24`) +should be reacheble from all the started services. If iptables is filtering the +INPUT chain, this rule is required to make the services communicating with each other: + +``` +iptables -A INPUT --src 172.22.0.0/16 -j ACCEPT +``` From 4a73ca8741875ddcff6c3626750f068f5140394f Mon Sep 17 00:00:00 2001 From: Vittorio Palmisano Date: Fri, 30 Apr 2021 17:08:19 +0200 Subject: [PATCH 15/57] added config error page, fixed browser error display --- app/src/components/ConfigDocumentation.tsx | 5 +- app/src/components/ConfigError.tsx | 75 ++++++++++++++++++++++ app/src/config.ts | 17 ++++- app/src/index.js | 63 +++++++++++++----- compose/config/edumeet-app-config.js | 2 +- 5 files changed, 137 insertions(+), 25 deletions(-) create mode 100644 app/src/components/ConfigError.tsx diff --git a/app/src/components/ConfigDocumentation.tsx b/app/src/components/ConfigDocumentation.tsx index e94efc5e..d6b535b8 100644 --- a/app/src/components/ConfigDocumentation.tsx +++ b/app/src/components/ConfigDocumentation.tsx @@ -18,7 +18,7 @@ import Button from '@material-ui/core/Button'; import { configDocs } from '../config'; -const styles = (theme: any) => +const styles = () => ({ table : { minWidth : 700 @@ -29,9 +29,6 @@ const styles = (theme: any) => cell : { maxWidth : '25vw', overflow : 'auto' - }, - list : { - backgroundColor : theme.palette.background.paper } }); diff --git a/app/src/components/ConfigError.tsx b/app/src/components/ConfigError.tsx new file mode 100644 index 00000000..3b33e6cb --- /dev/null +++ b/app/src/components/ConfigError.tsx @@ -0,0 +1,75 @@ +import React from 'react'; // eslint-disable-line no-use-before-define +import { withStyles } from '@material-ui/core/styles'; +import PropTypes from 'prop-types'; +import { FormattedMessage } from 'react-intl'; + +import Dialog from '@material-ui/core/Dialog'; +import DialogTitle from '@material-ui/core/DialogTitle'; +import DialogContent from '@material-ui/core/DialogContent'; +import Grid from '@material-ui/core/Grid'; +import ErrorIcon from '@material-ui/icons/Error'; +import Button from '@material-ui/core/Button'; + +const styles = () => + ({ + error : { + color : 'red' + } + }); + +const ConfigError = ({ + classes, + configError +}: { + classes : any; + configError : string; +}) => +{ + return ( + + + + + + + + + +

{configError}

+
+ +
+
+
+ ); +}; + +ConfigError.propTypes = +{ + classes : PropTypes.object.isRequired, + configError : PropTypes.string.isRequired +}; + +export default withStyles(styles)(ConfigError); diff --git a/app/src/config.ts b/app/src/config.ts index 1f7044c6..00488a7a 100644 --- a/app/src/config.ts +++ b/app/src/config.ts @@ -548,12 +548,22 @@ function formatDocs(docs: any, property: string | null, schema: any) return docs; } -const configDocs = formatDocs({}, null, configSchema.getSchema()); +let config: any = {}; +let configError = ''; // Perform validation -configSchema.validate({ allowed: 'strict' }); +try +{ + configSchema.validate({ allowed: 'strict' }); + config = configSchema.getProperties(); +} +catch (error: any) +{ + configError = error.message; +} -const config = configSchema.getProperties(); +// format docs +const configDocs = formatDocs({}, null, configSchema.getSchema()); // eslint-disable-next-line console.log('Using config:', config, configDocs); @@ -564,5 +574,6 @@ console.log('Using config:', config, configDocs); export { configSchema, config, + configError, configDocs }; diff --git a/app/src/index.js b/app/src/index.js index e9e3558c..e86a6482 100644 --- a/app/src/index.js +++ b/app/src/index.js @@ -17,6 +17,7 @@ import deviceInfo from './deviceInfo'; import * as meActions from './actions/meActions'; import UnsupportedBrowser from './components/UnsupportedBrowser'; import ConfigDocumentation from './components/ConfigDocumentation'; +import ConfigError from './components/ConfigError'; import JoinDialog from './components/JoinDialog'; import LoginDialog from './components/AccessControl/LoginDialog'; import LoadingView from './components/Loader/LoadingView'; @@ -30,13 +31,14 @@ import { detectDevice } from 'mediasoup-client'; import './index.css'; -import { config } from './config'; +import { config, configError } from './config'; const App = LazyPreload(() => import(/* webpackChunkName: "app" */ './components/App')); // const cache = createIntlCache(); -const supportedBrowsers={ +const supportedBrowsers = +{ 'windows' : { 'internet explorer' : '>12', 'microsoft edge' : '>18' @@ -144,14 +146,48 @@ function run() if (unsupportedBrowser || webrtcUnavailable) { render( - - - - - , + + + + + + + , + document.getElementById('edumeet') + ); + + return; + } + + if (basePath === '/config') + { + render( + + + + + + + , + document.getElementById('edumeet') + ); + + return; + } + + if (configError) + { + render( + + + + + + + , document.getElementById('edumeet') ); @@ -193,13 +229,6 @@ function run() - ( - - )} - /> diff --git a/compose/config/edumeet-app-config.js b/compose/config/edumeet-app-config.js index ab4434ef..dc87a31a 100644 --- a/compose/config/edumeet-app-config.js +++ b/compose/config/edumeet-app-config.js @@ -2,5 +2,5 @@ var config = { developmentPort : 8443, - productionPort : 3443, + productionPort : 3443 }; From 5c24fe56cb892b9442c197a23d3740287132713f Mon Sep 17 00:00:00 2001 From: Vittorio Palmisano Date: Wed, 5 May 2021 11:39:53 +0200 Subject: [PATCH 16/57] Updating the manual install steps --- README.md | 53 +++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 37 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 029eefbe..32cc5b47 100644 --- a/README.md +++ b/README.md @@ -60,31 +60,39 @@ $ sudo systemctl start edumeet ## Manual installation -* Prerequisites: -Currently edumeet will run on nodejs v14.x -To install see here [here](https://github.com/nodesource/distributions/blob/master/README.md#debinstall). +* Install all the required dependencies and NodeJS v14 (Debian/Ubuntu): ```bash -$ sudo apt install git npm yarnpkg build-essential redis libssl-dev openssl pkg-config +sudo apt install -y curl git build-essential redis openssl libssl-dev pkg-config +curl -fsSL https://deb.nodesource.com/setup_14.x | sudo bash - +sudo apt update && sudo apt install -y nodejs +``` + +* Install the Yarn package manager (recommended): + +```bash +curl -sL https://dl.yarnpkg.com/debian/pubkey.gpg | gpg --dearmor | sudo tee /usr/share/keyrings/yarnkey.gpg >/dev/null +echo "deb [signed-by=/usr/share/keyrings/yarnkey.gpg] https://dl.yarnpkg.com/debian stable main" | sudo tee /etc/apt/sources.list.d/yarn.list +sudo apt update && sudo apt install -y yarn ``` * Clone the project: ```bash -$ git clone https://github.com/edumeet/edumeet.git -$ cd edumeet +git clone https://github.com/edumeet/edumeet.git +cd edumeet ``` * Copy `server/config/config.example.js` to `server/config/config.js` : ```bash -$ cp server/config/config.example.js server/config/config.js +cp server/config/config.example.js server/config/config.js ``` * Copy `app/public/config/config.example.js` to `app/public/config/config.js` : ```bash -$ cp app/public/config/config.example.js app/public/config/config.js +cp app/public/config/config.example.js app/public/config/config.js ``` * Edit your two `config.js` with appropriate settings (listening IP/port, logging options, **valid** TLS certificate, don't forget ip setting in last section in server config: (webRtcTransport), etc). @@ -92,9 +100,13 @@ $ cp app/public/config/config.example.js app/public/config/config.js * Set up the browser app: ```bash -$ cd app -$ yarn -$ yarn build +cd app + +# using Yarn (recommended) +yarn && yarn build + +# using NPM +npm i && npm run build ``` This will build the client application and copy everythink to `server/public` from where the server can host client code to browser requests. @@ -102,9 +114,13 @@ This will build the client application and copy everythink to `server/public` fr * Set up the server: ```bash -$ cd .. -$ cd server -$ yarn +cd ../server + +# using Yarn (recommended) +yarn + +# using NPM +npm i ``` ## Run it locally @@ -112,8 +128,13 @@ $ yarn * Run the Node.js server application in a terminal: ```bash -$ cd server -$ yarn start +cd server + +# using Yarn (recommended) +yarn start + +# using NPM +npm run start ``` * Note: Do not run the server as root. If you need to use port 80/443 make a iptables-mapping for that or use systemd configuration for that (see further down this doc). From 6c19667ce3d127b80e421f6c2e78b398798da956 Mon Sep 17 00:00:00 2001 From: Vittorio Palmisano Date: Wed, 5 May 2021 11:58:19 +0200 Subject: [PATCH 17/57] Update README.md --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 32cc5b47..306670b5 100644 --- a/README.md +++ b/README.md @@ -81,6 +81,9 @@ sudo apt update && sudo apt install -y yarn ```bash git clone https://github.com/edumeet/edumeet.git cd edumeet + +# switch to the "develop" branch to get the latest version for developing +git checkout develop ``` * Copy `server/config/config.example.js` to `server/config/config.js` : From 88e571cb03ef9434aa42684403bbe49406651ae1 Mon Sep 17 00:00:00 2001 From: Vittorio Palmisano Date: Wed, 5 May 2021 12:01:58 +0200 Subject: [PATCH 18/57] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 306670b5..ca3afff5 100644 --- a/README.md +++ b/README.md @@ -112,7 +112,7 @@ yarn && yarn build npm i && npm run build ``` -This will build the client application and copy everythink to `server/public` from where the server can host client code to browser requests. +This will build the client application and copy everything to `server/public` from where the server can host client code to browser requests. * Set up the server: From 99de19269b5c81b5e8de25359a09319c9cfb3f98 Mon Sep 17 00:00:00 2001 From: Vittorio Palmisano Date: Wed, 5 May 2021 12:56:15 +0200 Subject: [PATCH 19/57] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ca3afff5..16203845 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,7 @@ $ sudo systemctl start edumeet * Install all the required dependencies and NodeJS v14 (Debian/Ubuntu): ```bash -sudo apt install -y curl git build-essential redis openssl libssl-dev pkg-config +sudo apt install -y curl git python build-essential redis openssl libssl-dev pkg-config curl -fsSL https://deb.nodesource.com/setup_14.x | sudo bash - sudo apt update && sudo apt install -y nodejs ``` From 6cefacf450155a53853b1b6177049af4b87db1ca Mon Sep 17 00:00:00 2001 From: Vittorio Palmisano Date: Wed, 5 May 2021 13:02:12 +0200 Subject: [PATCH 20/57] Update package.json --- server/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/package.json b/server/package.json index 4109742a..1b9405b5 100644 --- a/server/package.json +++ b/server/package.json @@ -14,7 +14,7 @@ "license": "MIT", "main": "lib/index.js", "scripts": { - "start": "yarn build && node dist/server.js", + "start": "node dist/server.js", "build": "rm -rf dist && tsc && ln -s ../certs dist/certs && ln -s ../public dist/public && chmod 755 dist/server.js", "dev": "nodemon --exec ts-node --ignore dist/ -e js,ts server.js", "connect": "ts-node connect.js", From 331a2368047e3db217a2b2ab87ba8c1836c8c40a Mon Sep 17 00:00:00 2001 From: Vittorio Palmisano Date: Wed, 5 May 2021 13:03:14 +0200 Subject: [PATCH 21/57] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 16203845..5d2831a6 100644 --- a/README.md +++ b/README.md @@ -120,10 +120,10 @@ This will build the client application and copy everything to `server/public` fr cd ../server # using Yarn (recommended) -yarn +yarn && yarn build # using NPM -npm i +npm i && npm run build ``` ## Run it locally From 01ffcafab4785c7555d8c344711f19239c172b62 Mon Sep 17 00:00:00 2001 From: Vittorio Palmisano Date: Wed, 5 May 2021 13:14:16 +0200 Subject: [PATCH 22/57] Update edumeet.service --- edumeet.service | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/edumeet.service b/edumeet.service index bf58c6c1..f5f648c1 100644 --- a/edumeet.service +++ b/edumeet.service @@ -3,13 +3,18 @@ Description=edumeet is a audio / video meeting service running in the browser an After=network.target [Service] +# modify the paths accordingly with your installation ExecStart=/usr/local/src/edumeet/server/dist/server.js +WorkingDirectory=/usr/local/src/edumeet/server Restart=always +RestartSec=1 User=nobody Group=nogroup Environment=PATH=/usr/bin:/usr/local/bin Environment=NODE_ENV=production -WorkingDirectory=/usr/local/src/edumeet/server +Environment=DEBUG="*ERROR*,*WARN*,*INFO*" +StandardOutput=syslog +StandardError=syslog AmbientCapabilities=CAP_NET_BIND_SERVICE [Install] From fa09c49f5a32f341b745ea4b7b44dfed3f2cdbd3 Mon Sep 17 00:00:00 2001 From: Vittorio Palmisano Date: Wed, 5 May 2021 13:17:32 +0200 Subject: [PATCH 23/57] Update README.md --- README.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 5d2831a6..e44664d2 100644 --- a/README.md +++ b/README.md @@ -148,21 +148,23 @@ npm run start * Stop your locally running server. Copy systemd-service file `edumeet.service` to `/etc/systemd/system/` and check location path settings: ```bash -$ cp edumeet.service /etc/systemd/system/ -$ edit /etc/systemd/system/edumeet.service +cp edumeet.service /etc/systemd/system/ + +# modify the install paths, if required +sudo edit /etc/systemd/system/edumeet.service ``` * Reload systemd configuration and start service: ```bash -$ systemctl daemon-reload -$ systemctl start edumeet +sudo systemctl daemon-reload +sudo systemctl start edumeet ``` * If you want to start edumeet at boot time: ```bash -$ systemctl enable edumeet +sudo systemctl enable edumeet ``` ## Ports and firewall From e819f3a1b343ff9656450d725cf977854541c2d9 Mon Sep 17 00:00:00 2001 From: Vittorio Palmisano Date: Wed, 5 May 2021 14:25:59 +0200 Subject: [PATCH 24/57] Changed: app/src/reducers/settings.js --- app/src/reducers/settings.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/reducers/settings.js b/app/src/reducers/settings.js index 2f20f0da..50f1b2f2 100644 --- a/app/src/reducers/settings.js +++ b/app/src/reducers/settings.js @@ -27,7 +27,7 @@ const initialState = hideNoVideoParticipants : false, buttonControlBar : config.buttonControlBar, drawerOverlayed : config.drawerOverlayed, - aspectRatio : config.viewAspectRatio || 1.777, // 16 : 9 + aspectRatio : config.viewAspectRatio, mediaPerms : { audio: true, video: true }, localPicture : null }; From b50c6d27655b5c8ec7f3d313e3f545b8d067378b Mon Sep 17 00:00:00 2001 From: Vittorio Palmisano Date: Wed, 5 May 2021 15:46:37 +0200 Subject: [PATCH 25/57] lint fixes --- app/src/config.ts | 74 +++++++++++++++++++++++------------------------ 1 file changed, 37 insertions(+), 37 deletions(-) diff --git a/app/src/config.ts b/app/src/config.ts index 00488a7a..0f4ed176 100644 --- a/app/src/config.ts +++ b/app/src/config.ts @@ -18,25 +18,25 @@ convict.addFormat({ const configSchema = convict({ loginEnabled : { - doc : 'If the login is enabled.', + doc : 'If the login is enabled.', format : 'Boolean', default : false }, developmentPort : { - doc : 'The development server listening port.', + doc : 'The development server listening port.', format : 'port', default : 3443 }, productionPort : { - doc : 'The production server listening port.', + doc : 'The production server listening port.', format : 'port', default : 443 }, serverHostname : { - doc : 'If the server component runs on a different host than the app you can specify the host name.', + doc : 'If the server component runs on a different host than the app you can specify the host name.', format : '*', default : null }, @@ -74,7 +74,7 @@ const configSchema = convict({ */ networkPriorities : { - doc : 'Network priorities.', + doc : 'Network priorities.', format : Object, default : { @@ -90,13 +90,13 @@ const configSchema = convict({ // viewAspectRatios EXACTLY (e.g. 1.333) viewAspectRatio : { - doc : 'The aspect ratio of the videos as shown on the screen.', + doc : 'The aspect ratio of the videos as shown on the screen.', format : 'float', default : 1.777 }, viewAspectRatios : { - doc : 'The selectable aspect ratios in the settings.', + doc : 'The selectable aspect ratios in the settings.', format : Array, default : [ @@ -114,49 +114,49 @@ const configSchema = convict({ // this is not changeable in settings, only config videoAspectRatio : { - doc : 'The aspect ratio of the video from the camera.', + doc : 'The aspect ratio of the video from the camera.', format : 'float', default : 1.777 }, resolution : { - doc : 'The default video camera capture resolution.', + doc : 'The default video camera capture resolution.', format : [ 'low', 'medium', 'high', 'veryhigh', 'ultra' ], default : 'medium' }, frameRate : { - doc : 'The default video camera capture framerate.', + doc : 'The default video camera capture framerate.', format : 'nat', default : 15 }, screenResolution : { - doc : 'The default screen sharing resolution.', + doc : 'The default screen sharing resolution.', format : [ 'low', 'medium', 'high', 'veryhigh', 'ultra' ], default : 'veryhigh' }, screenSharingFrameRate : { - doc : 'The default screen sharing framerate.', + doc : 'The default screen sharing framerate.', format : 'nat', default : 5 }, simulcast : { - doc : 'Enable or disable simulcast for webcam video.', + doc : 'Enable or disable simulcast for webcam video.', format : 'Boolean', default : true }, simulcastSharing : { - doc : 'Enable or disable simulcast for screen sharing video.', + doc : 'Enable or disable simulcast for screen sharing video.', format : 'Boolean', default : false }, simulcastProfiles : { - doc : 'Define different encodings for various resolutions of the video.', + doc : 'Define different encodings for various resolutions of the video.', format : Object, default : { @@ -195,7 +195,7 @@ const configSchema = convict({ // level change will be: 640 * 0.75 = 480px adaptiveScalingFactor : { - doc : 'The adaptive spatial layer selection scaling factor.', + doc : 'The adaptive spatial layer selection scaling factor.', format : (value: number) => value >= 0.5 && value <= 1.0, default : 0.75 }, @@ -207,7 +207,7 @@ const configSchema = convict({ */ audioOutputSupportedBrowsers : { - doc : 'White listing browsers that support audio output device selection.', + doc : 'White listing browsers that support audio output device selection.', format : Array, default : [ 'chrome', @@ -216,19 +216,19 @@ const configSchema = convict({ }, requestTimeout : { - doc : 'The Socket.io request timeout.', + doc : 'The Socket.io request timeout.', format : 'nat', default : 20000 }, requestRetries : { - doc : 'The Socket.io request maximum retries.', + doc : 'The Socket.io request maximum retries.', format : 'nat', default : 3 }, transportOptions : { - doc : '', + doc : '', format : Object, default : { tcp : true @@ -267,7 +267,7 @@ const configSchema = convict({ // Audio options for now only centrally from config file: centralAudioOptions : { - doc : 'Defaults audio settings.', + doc : 'Defaults audio settings.', format : Object, default : { @@ -298,37 +298,37 @@ const configSchema = convict({ */ autoMuteThreshold : { - doc : 'Set the max number of participants in one room that join unmuted.', + doc : 'Set the max number of participants in one room that join unmuted.', format : 'nat', default : 4 }, background : { - doc : 'The page background image URL', + doc : 'The page background image URL', format : String, default : 'images/background.jpg' }, defaultLayout : { - doc : 'The default layout', + doc : 'The default layout', format : [ 'democratic', 'filmstrip' ], default : 'democratic' }, buttonControlBar : { - doc : 'If true, will show media control buttons in separate control bar, not in the ME container.', + doc : 'If true, will show media control buttons in separate control bar, not in the ME container.', format : 'Boolean', default : false }, drawerOverlayed : { - doc : 'If false, will push videos away to make room for side drawer. If true, will overlay side drawer over videos.', + doc : 'If false, will push videos away to make room for side drawer. If true, will overlay side drawer over videos.', format : 'Boolean', default : true }, notificationPosition : { - doc : 'The position of notifications.', + doc : 'The position of notifications.', format : [ 'left', 'right' ], default : 'right' }, @@ -343,7 +343,7 @@ const configSchema = convict({ */ notificationSounds : { - doc : 'Set the notifications sounds.', + doc : 'Set the notifications sounds.', format : Object, default : { @@ -361,31 +361,31 @@ const configSchema = convict({ }, hideTimeout : { - doc : 'Timeout for autohiding topbar and button control bar.', + doc : 'Timeout for autohiding topbar and button control bar.', format : 'int', default : 3000 }, lastN : { - doc : 'The max number of participant that will be visible in as speaker.', + doc : 'The max number of participant that will be visible in as speaker.', format : 'nat', default : 4 }, mobileLastN : { - doc : 'The max number of participant that will be visible in as speaker for mobile users.', + doc : 'The max number of participant that will be visible in as speaker for mobile users.', format : 'nat', default : 1 }, maxLastN : { - doc : 'The highest number of lastN the user can select manually in the user interface.', + doc : 'The highest number of lastN the user can select manually in the user interface.', format : 'nat', default : 5 }, lockLastN : { - doc : 'If truthy, users can NOT change number of speakers visible.', + doc : 'If truthy, users can NOT change number of speakers visible.', format : 'Boolean', default : false }, @@ -393,19 +393,19 @@ const configSchema = convict({ // Set logo file name using logo.* pattern like "logo.png" to not track it by git logo : { - doc : 'If not null, it shows the logo loaded from URL, else it shows the title.', + doc : 'If not null, it shows the logo loaded from URL, else it shows the title.', format : 'url', default : 'images/logo.edumeet.svg' }, title : { - doc : 'The title to show if the logo is not specified.', + doc : 'The title to show if the logo is not specified.', format : String, default : 'edumeet' }, supportUrl : { - doc : 'Service & Support URL if not set then not displayed on the about modals.', + doc : 'Service & Support URL if not set then not displayed on the about modals.', format : 'url', default : 'https://support.example.com' }, @@ -415,7 +415,7 @@ const configSchema = convict({ // but an external url could be also used here privacyUrl : { - doc : 'Privacy and data protection URL or path by default privacy/privacy.html.', + doc : 'Privacy and data protection URL or path by default privacy/privacy.html.', format : String, default : 'privacy/privacy.html' }, From 6ce0e1ffc0d1c5d6793c037780f5437dcc70a0a6 Mon Sep 17 00:00:00 2001 From: Vittorio Palmisano Date: Wed, 5 May 2021 15:56:55 +0200 Subject: [PATCH 26/57] handle edge browser on non-windows platform --- app/src/config.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/config.ts b/app/src/config.ts index 0f4ed176..8d2b21a7 100644 --- a/app/src/config.ts +++ b/app/src/config.ts @@ -57,6 +57,7 @@ const configSchema = convict({ 'internet explorer' : '>12', 'microsoft edge' : '>18' }, + 'microsoft edge' : '>=74', 'safari' : '>12', 'firefox' : '>=60', 'chrome' : '>=74', From 7e4f97b22e7ae244df4c2220af599d2fabad1203 Mon Sep 17 00:00:00 2001 From: Vittorio Palmisano Date: Wed, 5 May 2021 20:03:38 +0200 Subject: [PATCH 27/57] WIP using new config for server --- app/src/config.ts | 6 +- server/.eslintrc.json | 82 ++----- server/lib/config.ts | 441 +++++++++++++++++++++++++++++++++++++ server/lib/configLoader.ts | 0 server/package.json | 7 +- server/server.js | 20 +- server/yarn.lock | 52 ++++- 7 files changed, 534 insertions(+), 74 deletions(-) create mode 100644 server/lib/config.ts delete mode 100644 server/lib/configLoader.ts diff --git a/app/src/config.ts b/app/src/config.ts index 8d2b21a7..f33b0193 100644 --- a/app/src/config.ts +++ b/app/src/config.ts @@ -522,9 +522,6 @@ const configSchema = convict({ } }); -// Load config from window object -configSchema.load((window as any).config); - function formatDocs(docs: any, property: string | null, schema: any) { if (schema._cvtProperties) @@ -552,6 +549,9 @@ function formatDocs(docs: any, property: string | null, schema: any) let config: any = {}; let configError = ''; +// Load config from window object +configSchema.load((window as any).config); + // Perform validation try { diff --git a/server/.eslintrc.json b/server/.eslintrc.json index 7df6a6db..4d490dcf 100644 --- a/server/.eslintrc.json +++ b/server/.eslintrc.json @@ -7,8 +7,12 @@ "overrides": [ { "files":["**/*.ts"], - "plugins": ["prettier", "@typescript-eslint"], - "extends": ["airbnb-typescript/base", "prettier"], + "plugins": ["@typescript-eslint"], + "extends":[ + "eslint:recommended", + "plugin:@typescript-eslint/eslint-recommended", + "plugin:@typescript-eslint/recommended" + ], "parser": "@typescript-eslint/parser", "parserOptions": { "project": "./tsconfig.json" @@ -21,69 +25,29 @@ // } // }, "rules": { - "no-underscore-dangle":"off", - "object-curly-spacing": [ - "warn", - "always" - ], - "no-unused-vars": [ - "warn", + "no-unused-vars" : 0, + "@typescript-eslint/ban-types" : 0, + "@typescript-eslint/ban-ts-comment" : 0, + "@typescript-eslint/ban-ts-ignore" : 0, + "@typescript-eslint/explicit-module-boundary-types" : 0, + "@typescript-eslint/member-delimiter-style" : [ 2, + { - "vars": "all", - "args": "none" + "multiline" : { "delimiter": "semi", "requireLast": true }, + "singleline" : { "delimiter": "semi", "requireLast": false } } ], - "@typescript-eslint/semi": [ - "off" - ], - "@typescript-eslint/no-unused-vars": [ - "warn", + "@typescript-eslint/no-explicit-any" : 0, + "@typescript-eslint/no-unused-vars" : [ 2, { - "vars": "all", - "args": "none" + "vars" : "all", + "args" : "after-used", + "ignoreRestSiblings" : false } ], - "max-len": [ - "warn", - { - "code": 100, - "ignoreStrings": true, - "ignoreTemplateLiterals": true, - "ignoreComments": true - } - ], - "prefer-destructuring": [ - "error", - { - "VariableDeclarator": { - "array": false, - "object": true - }, - "AssignmentExpression": { - "array": false, - "object": false - } - }, - { - "enforceForRenamedProperties": false - } - ], - "no-plusplus": [ - "error", - { - "allowForLoopAfterthoughts": true - } - ], - "import/no-extraneous-dependencies": [ - "error", - { - "devDependencies": [ - "**/*.test.js", - "**/*.test.ts", - "src/tests/**/*" - ] - } - ] + "@typescript-eslint/no-use-before-define" : [ 2, { "functions": false } ], + "@typescript-eslint/no-empty-function" : 0, + "@typescript-eslint/no-non-null-assertion" : 0 } } ], diff --git a/server/lib/config.ts b/server/lib/config.ts new file mode 100644 index 00000000..d35c40ee --- /dev/null +++ b/server/lib/config.ts @@ -0,0 +1,441 @@ +import * as fs from 'fs'; +import convict from 'convict'; +import { ipaddress, url } from 'convict-format-with-validator'; +import json5 from 'json5'; +import yaml from 'yaml'; +import toml from 'toml'; +import { cpus } from 'os'; + +import Logger from './Logger'; + +const logger = new Logger('config'); + +// add parsers +convict.addParser([ + { extension: 'json', parse: JSON.parse }, + { extension: 'json5', parse: json5.parse }, + { extension: [ 'yml', 'yaml' ], parse: yaml.parse }, + { extension: 'toml', parse: toml.parse } +]); + +// add formats +function assert(assertion: Boolean, msg: string) +{ + if (!assertion) + throw new Error(msg); +} + +const isFloat = { + name : 'float', + coerce : (v: string) => parseFloat(v), + validate : (v: number) => assert(Number.isFinite(v), 'must be a number') +}; + +convict.addFormats({ ipaddress, url, isFloat }); + +// config schema +const configSchema = convict({ + turnAPIKey : + { + doc : 'TURN server key for requesting a geoip-based TURN server closest to the client.', + format : String, + default : '' + }, + turnAPIURI : + { + doc : 'TURN server URL for requesting a geoip-based TURN server closest to the client.', + format : 'url', + default : '' + }, + turnAPIparams : + { + 'uri_schema' : { + doc : 'TURN server URL schema.', + format : String, + default : 'turn' + }, + 'transport' : { + doc : 'TURN server transport.', + format : [ 'tcp', 'udp' ], + default : 'tcp' + }, + 'ip_ver' : { + doc : 'TURN server IP version.', + format : [ 'ipv4', 'ipv6' ], + default : 'ipv4' + }, + 'servercount' : { + doc : 'TURN server count.', + format : 'nat', + default : 2 + } + }, + turnAPITimeout : { + doc : 'TURN server API timeout (seconds).', + format : 'nat', + default : 2 * 1000 + }, + backupTurnServers : { + doc : 'Backup TURN servers if REST fails or is not configured', + format : '*', + default : [ + { + urls : [ + 'turn:turn.example.com:443?transport=tcp' + ], + username : 'example', + credential : 'example' + } + ] + }, + fileTracker : { + doc : 'Bittorrent tracker.', + format : String, + default : 'wss://tracker.lab.vvc.niif.hu:443' + }, + redisOptions : { + doc : 'Redis server options.', + format : Object, + default : {} + }, + cookieSecret : { + doc : 'Session cookie secret.', + format : String, + default : 'T0P-S3cR3t_cook!e' + }, + cookieName : { + doc : 'Session cookie name.', + format : String, + default : 'edumeet.sid' + }, + tls : { + cert : { + doc : 'SSL certificate path.', + format : String, + default : `${__dirname}/../certs/mediasoup-demo.localhost.cert.pem` + }, + key : { + doc : 'SSL key path.', + format : String, + default : `${__dirname}/../certs/mediasoup-demo.localhost.key.pem` + } + }, + listeningHost : { + doc : 'The listening Host or IP address. If omitted listens on every IP. ("0.0.0.0" and "::").', + format : String, + default : null + }, + listeningPort : { + doc : 'The HTTPS listening port.', + format : 'port', + default : 8443 + }, + listeningRedirectPort : { + doc : 'The HTTP listening port. Any HTTP request is redirected to HTTPS.', + format : 'port', + default : 8080 + }, + httpOnly : { + doc : 'Listens only on HTTP on listeningPort; listeningRedirectPort disabled. Use case: load balancer backend.', + format : 'Boolean', + default : false + }, + trustProxy : { + doc : 'WebServer/Express trust proxy config for httpOnly mode. More infos: https://expressjs.com/en/guide/behind-proxies.html, https://www.npmjs.com/package/proxy-addr', + format : String, + default : '' + }, + activateOnHostJoin : { + doc : 'When true, the room will be open to all users since there are users in the room.', + format : 'Boolean', + default : true + }, + roomsUnlocked : { + doc : 'An array of rooms users can enter without waiting in the lobby.', + format : Array, + default : null + }, + maxUsersPerRoom : { + doc : 'It defines how many users can join a single room. If not set, no limit is applied.', + format : 'nat', + default : null + }, + routerScaleSize : { + doc : 'Room size before spreading to a new router.', + format : 'nat', + default : 40 + }, + requestTimeout : { + doc : 'Socket timeout value (ms).', + format : 'nat', + default : 20000 + }, + requestRetries : { + doc : 'Socket retries when a timeout occurs.', + format : 'nat', + default : 3 + }, + // Mediasoup settings + mediasoup : + { + numWorkers : { + doc : 'The number of Mediasoup workers to spawn.', + format : 'nat', + default : Object.keys(cpus()).length + }, + worker : + { + logLevel : { + doc : 'The Mediasoup log level.', + format : String, + default : 'warn' + }, + logTags : { + doc : 'The Mediasoup log tags.', + format : Array, + default : [ + 'info', + 'ice', + 'dtls', + 'rtp', + 'srtp', + 'rtcp' + ] + }, + rtcMinPort : { + doc : 'The Mediasoup start listening port number.', + format : 'port', + default : 40000 + }, + rtcMaxPort : { + doc : 'The Mediasoup end listening port number.', + format : 'port', + default : 49999 + } + }, + // mediasoup Router settings. + router : + { + // Router media codecs. + mediaCodecs : { + doc : 'The Mediasoup codecs settings.', + format : Object, + default : + [ + { + kind : 'audio', + mimeType : 'audio/opus', + clockRate : 48000, + channels : 2 + }, + { + kind : 'video', + mimeType : 'video/VP8', + clockRate : 90000, + parameters : + { + 'x-google-start-bitrate' : 1000 + } + }, + { + kind : 'video', + mimeType : 'video/VP9', + clockRate : 90000, + parameters : + { + 'profile-id' : 2, + 'x-google-start-bitrate' : 1000 + } + }, + { + kind : 'video', + mimeType : 'video/h264', + clockRate : 90000, + parameters : + { + 'packetization-mode' : 1, + 'profile-level-id' : '4d0032', + 'level-asymmetry-allowed' : 1, + 'x-google-start-bitrate' : 1000 + } + }, + { + kind : 'video', + mimeType : 'video/h264', + clockRate : 90000, + parameters : + { + 'packetization-mode' : 1, + 'profile-level-id' : '42e01f', + 'level-asymmetry-allowed' : 1, + 'x-google-start-bitrate' : 1000 + } + } + ] + } + }, + // mediasoup WebRtcTransport settings. + webRtcTransport : + { + listenIps : { + doc : 'The Mediasoup listen IPs. https://mediasoup.org/documentation/v3/mediasoup/api/#TransportListenIp', + format : Array, + default : [ + { ip: '0.0.0.0', announcedIp: null } + ] + }, + initialAvailableOutgoingBitrate : { + doc : 'The Mediasoup initial available outgoing bitrate (in bps). https://mediasoup.org/documentation/v3/mediasoup/api/#WebRtcTransportOptions', + format : 'nat', + default : 1000000 + }, + maxIncomingBitrate : { + doc : 'The Mediasoup maximum incoming bitrate for each transport. (in bps). https://mediasoup.org/documentation/v3/mediasoup/api/#transport-setMaxIncomingBitrate', + format : 'nat', + default : 1500000 + } + } + }, + // Prometheus exporter + prometheus : { + deidentify : { + doc : 'De-identify IP addresses in Prometheus logs.', + format : 'Boolean', + default : false + }, + listen : { + doc : 'Prometheus exporter listening address.', + format : 'String', + default : 'localhost' + }, + numeric : { + doc : 'Show numeric IP addresses in Prometheus logs.', + format : 'Boolean', + default : false + }, + port : { + doc : 'The Prometheus exporter listening port.', + format : 'port', + default : 8889 + }, + quiet : { + doc : 'Include fewer labels in Prometheus logs.', + format : 'Boolean', + default : false + }, + // aggregated metrics options + period : { + doc : 'The Prometheus exporter update period (seconds).', + format : 'nat', + default : 15 + }, + secret : { + doc : 'The Prometheus exporter authorization header: `Bearer ` required to allow scraping.', + format : String, + default : null + } + } +}); + +/** + * Formats the schema documentation, calling the same function recursively. + * @param docs the documentation object to extend + * @param property the root property + * @param schema the config schema fragment + * @returns the documentation object + */ +function formatDocs(docs: any, property: string | null, schema: any) +{ + if (schema._cvtProperties) + { + Object.entries(schema._cvtProperties).forEach(([ name, value ]) => + { + formatDocs(docs, `${property ? `${property}.` : ''}${name}`, value); + }); + + return docs; + } + + if (property) + { + docs[property] = // eslint-disable-line no-param-reassign + { + doc : schema.doc, + format : JSON.stringify(schema.format, null, 2), + default : JSON.stringify(schema.default, null, 2) + }; + } + + return docs; +} + +// format docs +const configDocs = formatDocs({}, null, configSchema.getSchema()); + +let config: any = {}; +let configError = ''; +let configLoaded = false; + +// Load config from window object +for (const format of [ 'json', 'json5', 'yaml', 'yml', 'toml' ]) // eslint-disable-line no-restricted-syntax +{ + const filepath = `./config/config.${format}`; + + if (fs.existsSync(filepath)) + { + try + { + logger.debug(`Loading config from ${filepath}`); + configSchema.loadFile(filepath); + configLoaded = true; + break; + } + catch (err) + { + logger.debug(`Loading config from ${filepath} failed: ${err.message}`); + } + } +} + +if (!configLoaded) +{ + logger.warn('No config file found, using defaults.'); + configSchema.load({}); +} + +// Perform validation +try +{ + configSchema.validate({ allowed: 'strict' }); + config = configSchema.getProperties(); +} +catch (error: any) +{ + configError = error.message; +} + +// load additional config module (no validation is performed) +if (fs.existsSync(`${__dirname}/../config/config.js`)) +{ + try + { + const configModule = require('../config/config.js'); // eslint-disable-line @typescript-eslint/no-var-requires + + Object.assign(config, configModule); + } + catch (err) + { + logger.error(`Error loading ${config.configFile} module: ${err.message}`); + } +} + +// eslint-disable-next-line +logger.info('Using config:', config); + +// +export { + configSchema, + config, + configError, + configDocs +}; diff --git a/server/lib/configLoader.ts b/server/lib/configLoader.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/server/package.json b/server/package.json index 1b9405b5..10a465f1 100644 --- a/server/package.json +++ b/server/package.json @@ -30,6 +30,8 @@ "colors": "^1.4.0", "compression": "^1.7.4", "connect-redis": "^4.0.3", + "convict": "^6.1.0", + "convict-format-with-validator": "^6.0.1", "cookie-parser": "^1.4.4", "debug": "^4.3.1", "express": "^4.17.1", @@ -38,6 +40,7 @@ "fast-stats": "^0.0.6", "helmet": "^3.21.2", "ims-lti": "^3.0.2", + "json5": "^2.2.0", "jsonwebtoken": "^8.5.1", "mediasoup": "vpalmisano/mediasoup#3.7.1-build-with-system-openssl", "openid-client": "^3.7.3", @@ -50,7 +53,9 @@ "redis": "^2.8.0", "socket.io": "^2.4.0", "spdy": "^4.0.1", - "uuid": "^7.0.2" + "toml": "^3.0.0", + "uuid": "^7.0.2", + "yaml": "^1.10.2" }, "devDependencies": { "@types/base-64": "^0.1.3", diff --git a/server/server.js b/server/server.js index b8306e77..98222e36 100755 --- a/server/server.js +++ b/server/server.js @@ -3,9 +3,18 @@ process.title = 'edumeet-server'; import Logger from './lib/Logger'; +const Room = require('./lib/Room'); +const Peer = require('./lib/Peer'); +const userRoles = require('./userRoles'); +const { + loginHelper, + logoutHelper +} = require('./httpHelper'); +const { config } = require('./lib/config'); +const interactiveServer = require('./lib/interactiveServer'); +const promExporter = require('./lib/promExporter'); const bcrypt = require('bcrypt'); -const config = require('./config/config'); const fs = require('fs'); const http = require('http'); const spdy = require('spdy'); @@ -15,15 +24,8 @@ const cookieParser = require('cookie-parser'); const compression = require('compression'); const mediasoup = require('mediasoup'); const AwaitQueue = require('awaitqueue'); -const Room = require('./lib/Room'); -const Peer = require('./lib/Peer'); const base64 = require('base-64'); const helmet = require('helmet'); -const userRoles = require('./userRoles'); -const { - loginHelper, - logoutHelper -} = require('./httpHelper'); // auth const passport = require('passport'); const LTIStrategy = require('passport-lti'); @@ -36,8 +38,6 @@ const { Issuer, Strategy } = require('openid-client'); const expressSession = require('express-session'); const RedisStore = require('connect-redis')(expressSession); const sharedSession = require('express-socket.io-session'); -const interactiveServer = require('./lib/interactiveServer'); -const promExporter = require('./lib/promExporter'); const { v4: uuidv4 } = require('uuid'); /* eslint-disable no-console */ diff --git a/server/yarn.lock b/server/yarn.lock index 31fce029..af0571ce 100644 --- a/server/yarn.lock +++ b/server/yarn.lock @@ -801,6 +801,11 @@ camelcase@^2.0.0: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f" integrity sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8= +camelcase@^5.0.0: + version "5.3.1" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" + integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== + camelize@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/camelize/-/camelize-1.0.0.tgz#164a5483e630fa4321e5af07020e531831b2609b" @@ -1011,6 +1016,21 @@ content-type@~1.0.4: resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== +convict-format-with-validator@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/convict-format-with-validator/-/convict-format-with-validator-6.0.1.tgz#a0ff08b663c40beac480d1a0d39b40382b9e39a3" + integrity sha512-6/O0W9/0MESdL+0fY7glQlWHs+wnP9xXlB1tjY/Wb9ujCCEBqMP8VCjtLvRlBdfwgONrpb+DTPSHQRhUO5aTlQ== + dependencies: + validator "^11.1.0" + +convict@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/convict/-/convict-6.1.0.tgz#5b393bd675a0e743390abd0c5bf9e2b9a2edf4b5" + integrity sha512-8dzppr6Z9URlm6P8N9NiydFRq2NWtQyf4RZOK5m0Q48fWWuKamHLXD7Qz/SiLvRXnjQcKCuHayIk9Fk51sax0w== + dependencies: + lodash.clonedeep "^4.5.0" + yargs-parser "^18.1.3" + cookie-parser@^1.4.4: version "1.4.5" resolved "https://registry.yarnpkg.com/cookie-parser/-/cookie-parser-1.4.5.tgz#3e572d4b7c0c80f9c61daf604e4336831b5d1d49" @@ -1138,7 +1158,7 @@ debug@~4.1.0: dependencies: ms "^2.1.1" -decamelize@^1.1.2: +decamelize@^1.1.2, decamelize@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= @@ -2528,6 +2548,13 @@ json5@^1.0.1: dependencies: minimist "^1.2.0" +json5@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.0.tgz#2dfefe720c6ba525d9ebd909950f0515316c89a3" + integrity sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA== + dependencies: + minimist "^1.2.5" + jsonwebtoken@^8.5.1: version "8.5.1" resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz#00e71e0b8df54c2121a1f26137df2280673bcc0d" @@ -4261,6 +4288,11 @@ toidentifier@1.0.0: resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553" integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw== +toml@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/toml/-/toml-3.0.0.tgz#342160f1af1904ec9d204d03a5d61222d762c5ee" + integrity sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w== + tough-cookie@~2.5.0: version "2.5.0" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" @@ -4441,6 +4473,11 @@ validate-npm-package-license@^3.0.1: spdx-correct "^3.0.0" spdx-expression-parse "^3.0.0" +validator@^11.1.0: + version "11.1.0" + resolved "https://registry.yarnpkg.com/validator/-/validator-11.1.0.tgz#ac18cac42e0aa5902b603d7a5d9b7827e2346ac4" + integrity sha512-qiQ5ktdO7CD6C/5/mYV4jku/7qnqzjrxb3C/Q5wR3vGGinHTgJZN/TdFT3ZX4vXhX2R1PXx42fB1cn5W+uJ4lg== + vary@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" @@ -4608,6 +4645,19 @@ yallist@^4.0.0: resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== +yaml@^1.10.2: + version "1.10.2" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" + integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== + +yargs-parser@^18.1.3: + version "18.1.3" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0" + integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ== + dependencies: + camelcase "^5.0.0" + decamelize "^1.2.0" + yeast@0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/yeast/-/yeast-0.1.2.tgz#008e06d8094320c372dbc2f8ed76a0ca6c8ac419" From d0881873df62c0720a409940deebd7ac563dc0c4 Mon Sep 17 00:00:00 2001 From: Vittorio Palmisano Date: Wed, 5 May 2021 20:07:28 +0200 Subject: [PATCH 28/57] WIP using new config for server --- server/config/config.example.js | 183 -------------------------------- 1 file changed, 183 deletions(-) diff --git a/server/config/config.example.js b/server/config/config.example.js index 1deea6ba..aec872fb 100644 --- a/server/config/config.example.js +++ b/server/config/config.example.js @@ -28,7 +28,6 @@ const { module.exports = { - // Auth conf /* auth : @@ -104,58 +103,6 @@ module.exports = } }, */ - // URI and key for requesting geoip-based TURN server closest to the client - turnAPIKey : 'examplekey', - turnAPIURI : 'https://example.com/api/turn', - turnAPIparams : { - 'uri_schema' : 'turn', - 'transport' : 'tcp', - 'ip_ver' : 'ipv4', - 'servercount' : '2' - }, - turnAPITimeout : 2 * 1000, - // Backup turnservers if REST fails or is not configured - backupTurnServers : [ - { - urls : [ - 'turn:turn.example.com:443?transport=tcp' - ], - username : 'example', - credential : 'example' - } - ], - // bittorrent tracker - fileTracker : 'wss://tracker.lab.vvc.niif.hu:443', - // redis server options - redisOptions : {}, - // session cookie secret - cookieSecret : 'T0P-S3cR3t_cook!e', - cookieName : 'edumeet.sid', - // if you use encrypted private key the set the passphrase - tls : - { - cert : `${__dirname}/../certs/mediasoup-demo.localhost.cert.pem`, - // passphrase: 'key_password' - key : `${__dirname}/../certs/mediasoup-demo.localhost.key.pem` - }, - // listening Host or IP - // If omitted listens on every IP. ("0.0.0.0" and "::") - // listeningHost: 'localhost', - // Listening port for https server. - listeningPort : 443, - // Any http request is redirected to https. - // Listening port for http server. - listeningRedirectPort : 80, - // Listens only on http, only on listeningPort - // listeningRedirectPort disabled - // use case: loadbalancer backend - httpOnly : false, - // WebServer/Express trust proxy config for httpOnly mode - // You can find more info: - // - https://expressjs.com/en/guide/behind-proxies.html - // - https://www.npmjs.com/package/proxy-addr - // use case: loadbalancer backend - trustProxy : '', // This logger class will have the log function // called every time there is a room created or destroyed, // or peer created or destroyed. This would then be able @@ -346,134 +293,4 @@ module.exports = // action as soon as a peer with the permission joins. In this example // everyone will be able to lock/unlock room until a MODERATOR joins. allowWhenRoleMissing : [ CHANGE_ROOM_LOCK ], - // When true, the room will be open to all users as long as there - // are allready users in the room - activateOnHostJoin : true, - // roomsUnlocked is an array of rooms users can enter without waiting - // in the lobby. If the array is undefined or null, users can enter - // any room without waiting in the lobby. This is the default. The - // aim of roomsUnlocked is to enforce moderated access to all rooms - // with the exception of the rooms defined in the array. - // roomsUnlocked : [ 'unlocked1', 'unlocked2', 'unlocked3' ], - // When set, maxUsersPerRoom defines how many users can join - // a single room. If not set, there is no limit. - // maxUsersPerRoom : 20, - // Room size before spreading to new router - routerScaleSize : 40, - // Socket timeout value - requestTimeout : 20000, - // Socket retries when timeout - requestRetries : 3, - // If > 0, sets a cache-control max-age (in seconds) to static files responses. - staticFilesCachePeriod : 0, - // Mediasoup settings - mediasoup : - { - numWorkers : Object.keys(os.cpus()).length, - // mediasoup Worker settings. - worker : - { - logLevel : 'warn', - logTags : - [ - 'info', - 'ice', - 'dtls', - 'rtp', - 'srtp', - 'rtcp' - ], - rtcMinPort : 40000, - rtcMaxPort : 49999 - }, - // mediasoup Router settings. - router : - { - // Router media codecs. - mediaCodecs : - [ - { - kind : 'audio', - mimeType : 'audio/opus', - clockRate : 48000, - channels : 2 - }, - { - kind : 'video', - mimeType : 'video/VP8', - clockRate : 90000, - parameters : - { - 'x-google-start-bitrate' : 1000 - } - }, - { - kind : 'video', - mimeType : 'video/VP9', - clockRate : 90000, - parameters : - { - 'profile-id' : 2, - 'x-google-start-bitrate' : 1000 - } - }, - { - kind : 'video', - mimeType : 'video/h264', - clockRate : 90000, - parameters : - { - 'packetization-mode' : 1, - 'profile-level-id' : '4d0032', - 'level-asymmetry-allowed' : 1, - 'x-google-start-bitrate' : 1000 - } - }, - { - kind : 'video', - mimeType : 'video/h264', - clockRate : 90000, - parameters : - { - 'packetization-mode' : 1, - 'profile-level-id' : '42e01f', - 'level-asymmetry-allowed' : 1, - 'x-google-start-bitrate' : 1000 - } - } - ] - }, - // mediasoup WebRtcTransport settings. - webRtcTransport : - { - listenIps : - [ - // change 192.0.2.1 IPv4 to your server's IPv4 address!! - { ip: '192.0.2.1', announcedIp: null } - - // Can have multiple listening interfaces - // change 2001:DB8::1 IPv6 to your server's IPv6 address!! - // { ip: '2001:DB8::1', announcedIp: null } - ], - initialAvailableOutgoingBitrate : 1000000, - minimumAvailableOutgoingBitrate : 600000, - // Additional options that are not part of WebRtcTransportOptions. - maxIncomingBitrate : 1500000 - } - } - - /* - , - // Prometheus exporter - prometheus : { - deidentify : false, // deidentify IP addresses - // listen : 'localhost', // exporter listens on this address - numeric : true, // show numeric IP addresses - port : 8889, // allocated port - quiet : false // include fewer labels - // aggregated metrics options - period : 15 // update period (seconds) - secret : null // if set, checks the authorization header: `Bearer ` - } - */ }; From 639dfe2831923636d824d7af3b92bec9dc439d4c Mon Sep 17 00:00:00 2001 From: Vittorio Palmisano Date: Thu, 6 May 2021 10:11:19 +0200 Subject: [PATCH 29/57] removed example config --- server/config/config.example.js | 296 -------------------------------- 1 file changed, 296 deletions(-) delete mode 100644 server/config/config.example.js diff --git a/server/config/config.example.js b/server/config/config.example.js deleted file mode 100644 index aec872fb..00000000 --- a/server/config/config.example.js +++ /dev/null @@ -1,296 +0,0 @@ -const os = require('os'); -// const fs = require('fs'); - -const userRoles = require('../userRoles'); - -const { - BYPASS_ROOM_LOCK, - BYPASS_LOBBY -} = require('../access'); - -const { - CHANGE_ROOM_LOCK, - PROMOTE_PEER, - MODIFY_ROLE, - SEND_CHAT, - MODERATE_CHAT, - SHARE_AUDIO, - SHARE_VIDEO, - SHARE_SCREEN, - EXTRA_VIDEO, - SHARE_FILE, - MODERATE_FILES, - MODERATE_ROOM -} = require('../permissions'); - -// const AwaitQueue = require('awaitqueue'); -// const axios = require('axios'); - -module.exports = -{ - // Auth conf - /* - auth : - { - // Always enabled if configured - lti : - { - consumerKey : 'key', - consumerSecret : 'secret' - }, - - // Auth strategy to use (default oidc) - strategy : 'oidc', - oidc : - { - // The issuer URL for OpenID Connect discovery - // The OpenID Provider Configuration Document - // could be discovered on: - // issuerURL + '/.well-known/openid-configuration' - - // e.g. google OIDC config - // Follow this guide to get credential: - // https://developers.google.com/identity/protocols/oauth2/openid-connect - // use this issuerURL - // issuerURL : 'https://accounts.google.com/', - - issuerURL : 'https://example.com', - clientOptions : - { - client_id : '', - client_secret : '', - scope : 'openid email profile', - // where client.example.com is your edumeet server - redirect_uri : 'https://client.example.com/auth/callback' - } - - }, - saml : - { - // where edumeet.example.com is your edumeet server - callbackUrl : 'https://edumeet.example.com/auth/callback', - issuer : 'https://edumeet.example.com', - entryPoint : 'https://openidp.feide.no/simplesaml/saml2/idp/SSOService.php', - privateCert : fs.readFileSync('config/saml_privkey.pem', 'utf-8'), - signingCert : fs.readFileSync('config/saml_cert.pem', 'utf-8'), - decryptionPvk : fs.readFileSync('config/saml_privkey.pem', 'utf-8'), - decryptionCert : fs.readFileSync('config/saml_cert.pem', 'utf-8'), - // Federation cert - cert : fs.readFileSync('config/federation_cert.pem', 'utf-8') - }, - - // to create password hash use: node server/utils/password_encode.js cleartextpassword - local : - { - users : [ - { - id : 1, - username : 'alice', - passwordHash : '$2b$10$PAXXw.6cL3zJLd7ZX.AnL.sFg2nxjQPDmMmGSOQYIJSa0TrZ9azG6', - displayName : 'Alice', - emails : [ { value: 'alice@atlanta.com' } ], - meet_roles : [ ] - }, - { - id : 2, - username : 'bob', - passwordHash : '$2b$10$BzAkXcZ54JxhHTqCQcFn8.H6klY/G48t4jDBeTE2d2lZJk/.tvv0G', - displayName : 'Bob', - emails : [ { value: 'bob@biloxi.com' } ], - meet_roles : [ ] - } - ] - } - }, - */ - // This logger class will have the log function - // called every time there is a room created or destroyed, - // or peer created or destroyed. This would then be able - // to log to a file or external service. - /* StatusLogger : class - { - constructor() - { - this._queue = new AwaitQueue(); - } - - // rooms: rooms object - // peers: peers object - // eslint-disable-next-line no-unused-vars - async log({ rooms, peers }) - { - this._queue.push(async () => - { - // Do your logging in here, use queue to keep correct order - - // eslint-disable-next-line no-console - console.log('Number of rooms: ', rooms.size); - // eslint-disable-next-line no-console - console.log('Number of peers: ', peers.size); - }) - .catch((error) => - { - // eslint-disable-next-line no-console - console.log('error in log', error); - }); - } - }, */ - // This function will be called on successful login through oidc. - // Use this function to map your oidc userinfo to the Peer object. - // The roomId is equal to the room name. - // See examples below. - // Examples: - /* - // All authenticated users will be MODERATOR and AUTHENTICATED - userMapping : async ({ peer, room, roomId, userinfo }) => - { - peer.addRole(userRoles.MODERATOR); - peer.addRole(userRoles.AUTHENTICATED); - }, - // All authenticated users will be AUTHENTICATED, - // and those with the moderator role set in the userinfo - // will also be MODERATOR - userMapping : async ({ peer, room, roomId, userinfo }) => - { - if ( - Array.isArray(userinfo.meet_roles) && - userinfo.meet_roles.includes('moderator') - ) - { - peer.addRole(userRoles.MODERATOR); - } - - if ( - Array.isArray(userinfo.meet_roles) && - userinfo.meet_roles.includes('meetingadmin') - ) - { - peer.addRole(userRoles.ADMIN); - } - - peer.addRole(userRoles.AUTHENTICATED); - }, - // First authenticated user will be moderator, - // all others will be AUTHENTICATED - userMapping : async ({ peer, room, roomId, userinfo }) => - { - if (room) - { - const peers = room.getJoinedPeers(); - - if (peers.some((_peer) => _peer.authenticated)) - peer.addRole(userRoles.AUTHENTICATED); - else - { - peer.addRole(userRoles.MODERATOR); - peer.addRole(userRoles.AUTHENTICATED); - } - } - }, - // All authenticated users will be AUTHENTICATED, - // and those with email ending with @example.com - // will also be MODERATOR - userMapping : async ({ peer, room, roomId, userinfo }) => - { - if (userinfo.email && userinfo.email.endsWith('@example.com')) - { - peer.addRole(userRoles.MODERATOR); - } - - peer.addRole(userRoles.AUTHENTICATED); - }, - */ - // eslint-disable-next-line no-unused-vars - userMapping : async ({ peer, room, roomId, userinfo }) => - { - if (userinfo.picture != null) - { - if (!userinfo.picture.match(/^http/g)) - { - peer.picture = `data:image/jpeg;base64, ${userinfo.picture}`; - } - else - { - peer.picture = userinfo.picture; - } - } - if (userinfo['urn:oid:0.9.2342.19200300.100.1.60'] != null) - { - peer.picture = `data:image/jpeg;base64, ${userinfo['urn:oid:0.9.2342.19200300.100.1.60']}`; - } - - if (userinfo.nickname != null) - { - peer.displayName = userinfo.nickname; - } - - if (userinfo.name != null) - { - peer.displayName = userinfo.name; - } - - if (userinfo.displayName != null) - { - peer.displayName = userinfo.displayName; - } - - if (userinfo['urn:oid:2.16.840.1.113730.3.1.241'] != null) - { - peer.displayName = userinfo['urn:oid:2.16.840.1.113730.3.1.241']; - } - - if (userinfo.email != null) - { - peer.email = userinfo.email; - } - }, - // All users have the role "NORMAL" by default. Other roles need to be - // added in the "userMapping" function. The following accesses and - // permissions are arrays of roles. Roles can be changed in userRoles.js - // - // Example: - // [ userRoles.MODERATOR, userRoles.AUTHENTICATED ] - accessFromRoles : { - // The role(s) will gain access to the room - // even if it is locked (!) - [BYPASS_ROOM_LOCK] : [ userRoles.ADMIN ], - // The role(s) will gain access to the room without - // going into the lobby. If you want to restrict access to your - // server to only directly allow authenticated users, you could - // add the userRoles.AUTHENTICATED to the user in the userMapping - // function, and change to BYPASS_LOBBY : [ userRoles.AUTHENTICATED ] - [BYPASS_LOBBY] : [ userRoles.NORMAL ] - }, - permissionsFromRoles : { - // The role(s) have permission to lock/unlock a room - [CHANGE_ROOM_LOCK] : [ userRoles.MODERATOR ], - // The role(s) have permission to promote a peer from the lobby - [PROMOTE_PEER] : [ userRoles.NORMAL ], - // The role(s) have permission to give/remove other peers roles - [MODIFY_ROLE] : [ userRoles.NORMAL ], - // The role(s) have permission to send chat messages - [SEND_CHAT] : [ userRoles.NORMAL ], - // The role(s) have permission to moderate chat - [MODERATE_CHAT] : [ userRoles.MODERATOR ], - // The role(s) have permission to share audio - [SHARE_AUDIO] : [ userRoles.NORMAL ], - // The role(s) have permission to share video - [SHARE_VIDEO] : [ userRoles.NORMAL ], - // The role(s) have permission to share screen - [SHARE_SCREEN] : [ userRoles.NORMAL ], - // The role(s) have permission to produce extra video - [EXTRA_VIDEO] : [ userRoles.NORMAL ], - // The role(s) have permission to share files - [SHARE_FILE] : [ userRoles.NORMAL ], - // The role(s) have permission to moderate files - [MODERATE_FILES] : [ userRoles.MODERATOR ], - // The role(s) have permission to moderate room (e.g. kick user) - [MODERATE_ROOM] : [ userRoles.MODERATOR ] - }, - // Array of permissions. If no peer with the permission in question - // is in the room, all peers are permitted to do the action. The peers - // that are allowed because of this rule will not be able to do this - // action as soon as a peer with the permission joins. In this example - // everyone will be able to lock/unlock room until a MODERATOR joins. - allowWhenRoleMissing : [ CHANGE_ROOM_LOCK ], -}; From 16c4c28fced78bc25e2ab8e8b89eaeaa13dc7673 Mon Sep 17 00:00:00 2001 From: Vittorio Palmisano Date: Thu, 6 May 2021 10:38:49 +0200 Subject: [PATCH 30/57] added doc auto generator --- server/config.md | 50 +++++++++++++++++++++++++++++++++++++++ server/gen-config-docs.ts | 35 +++++++++++++++++++++++++++ server/lib/config.ts | 4 ++-- server/package.json | 3 ++- 4 files changed, 89 insertions(+), 3 deletions(-) create mode 100644 server/config.md create mode 100644 server/gen-config-docs.ts diff --git a/server/config.md b/server/config.md new file mode 100644 index 00000000..3e96c595 --- /dev/null +++ b/server/config.md @@ -0,0 +1,50 @@ +# Edumeet Server Config + +| Property | Description | Format | Default value | +| :------- | :---------- | :----- | :------------ | +| turnAPIKey | TURN server key for requesting a geoip-based TURN server closest to the client. | `"string"` | ``""`` | +| turnAPIURI | TURN server URL for requesting a geoip-based TURN server closest to the client. | `"url"` | ``""`` | +| turnAPIparams.uri_schema | TURN server URL schema. | `"string"` | ``"turn"`` | +| turnAPIparams.transport | TURN server transport. | `[ "tcp", "udp"]` | ``"tcp"`` | +| turnAPIparams.ip_ver | TURN server IP version. | `[ "ipv4", "ipv6"]` | ``"ipv4"`` | +| turnAPIparams.servercount | TURN server count. | `"nat"` | ``2`` | +| turnAPITimeout | TURN server API timeout (seconds). | `"nat"` | ``2000`` | +| backupTurnServers | Backup TURN servers if REST fails or is not configured | `"*"` | ``[ { "urls": [ "turn:turn.example.com:443?transport=tcp" ], "username": "example", "credential": "example" }]`` | +| fileTracker | Bittorrent tracker. | `"string"` | ``"wss://tracker.lab.vvc.niif.hu:443"`` | +| redisOptions | Redis server options. | `"object"` | ``{}`` | +| cookieSecret | Session cookie secret. | `"string"` | ``"T0P-S3cR3t_cook!e"`` | +| cookieName | Session cookie name. | `"string"` | ``"edumeet.sid"`` | +| tls.cert | SSL certificate path. | `"string"` | ``"./certs/mediasoup-demo.localhost.cert.pem"`` | +| tls.key | SSL key path. | `"string"` | ``"./certs/mediasoup-demo.localhost.key.pem"`` | +| listeningHost | The listening Host or IP address. If omitted listens on every IP. ("0.0.0.0" and "::"). | `"string"` | ``null`` | +| listeningPort | The HTTPS listening port. | `"port"` | ``8443`` | +| listeningRedirectPort | The HTTP listening port. Any HTTP request is redirected to HTTPS. | `"port"` | ``8080`` | +| httpOnly | Listens only on HTTP on listeningPort; listeningRedirectPort disabled. Use case: load balancer backend. | `"boolean"` | ``false`` | +| trustProxy | WebServer/Express trust proxy config for httpOnly mode. More infos: https://expressjs.com/en/guide/behind-proxies.html, https://www.npmjs.com/package/proxy-addr | `"string"` | ``""`` | +| activateOnHostJoin | When true, the room will be open to all users since there are users in the room. | `"boolean"` | ``true`` | +| roomsUnlocked | An array of rooms users can enter without waiting in the lobby. | `"array"` | ``null`` | +| maxUsersPerRoom | It defines how many users can join a single room. If not set, no limit is applied. | `"nat"` | ``null`` | +| routerScaleSize | Room size before spreading to a new router. | `"nat"` | ``40`` | +| requestTimeout | Socket timeout value (ms). | `"nat"` | ``20000`` | +| requestRetries | Socket retries when a timeout occurs. | `"nat"` | ``3`` | +| mediasoup.numWorkers | The number of Mediasoup workers to spawn. | `"nat"` | ``8`` | +| mediasoup.worker.logLevel | The Mediasoup log level. | `"string"` | ``"warn"`` | +| mediasoup.worker.logTags | The Mediasoup log tags. | `"array"` | ``[ "info", "ice", "dtls", "rtp", "srtp", "rtcp"]`` | +| mediasoup.worker.rtcMinPort | The Mediasoup start listening port number. | `"port"` | ``40000`` | +| mediasoup.worker.rtcMaxPort | The Mediasoup end listening port number. | `"port"` | ``49999`` | +| mediasoup.router.mediaCodecs | The Mediasoup codecs settings. | `"object"` | ``[ { "kind": "audio", "mimeType": "audio/opus", "clockRate": 48000, "channels": 2 }, { "kind": "video", "mimeType": "video/VP8", "clockRate": 90000, "parameters": { "x-google-start-bitrate": 1000 } }, { "kind": "video", "mimeType": "video/VP9", "clockRate": 90000, "parameters": { "profile-id": 2, "x-google-start-bitrate": 1000 } }, { "kind": "video", "mimeType": "video/h264", "clockRate": 90000, "parameters": { "packetization-mode": 1, "profile-level-id": "4d0032", "level-asymmetry-allowed": 1, "x-google-start-bitrate": 1000 } }, { "kind": "video", "mimeType": "video/h264", "clockRate": 90000, "parameters": { "packetization-mode": 1, "profile-level-id": "42e01f", "level-asymmetry-allowed": 1, "x-google-start-bitrate": 1000 } }]`` | +| mediasoup.webRtcTransport.listenIps | The Mediasoup listen IPs. https://mediasoup.org/documentation/v3/mediasoup/api/#TransportListenIp | `"array"` | ``[ { "ip": "0.0.0.0", "announcedIp": null }]`` | +| mediasoup.webRtcTransport.initialAvailableOutgoingBitrate | The Mediasoup initial available outgoing bitrate (in bps). https://mediasoup.org/documentation/v3/mediasoup/api/#WebRtcTransportOptions | `"nat"` | ``1000000`` | +| mediasoup.webRtcTransport.maxIncomingBitrate | The Mediasoup maximum incoming bitrate for each transport. (in bps). https://mediasoup.org/documentation/v3/mediasoup/api/#transport-setMaxIncomingBitrate | `"nat"` | ``1500000`` | +| prometheus.deidentify | De-identify IP addresses in Prometheus logs. | `"boolean"` | ``false`` | +| prometheus.listen | Prometheus exporter listening address. | `"string"` | ``"localhost"`` | +| prometheus.numeric | Show numeric IP addresses in Prometheus logs. | `"boolean"` | ``false`` | +| prometheus.port | The Prometheus exporter listening port. | `"port"` | ``8889`` | +| prometheus.quiet | Include fewer labels in Prometheus logs. | `"boolean"` | ``false`` | +| prometheus.period | The Prometheus exporter update period (seconds). | `"nat"` | ``15`` | +| prometheus.secret | The Prometheus exporter authorization header: `Bearer ` required to allow scraping. | `"string"` | ``null`` | + + +--- + +*Document generated with:* `yarn gen-config-docs` diff --git a/server/gen-config-docs.ts b/server/gen-config-docs.ts new file mode 100644 index 00000000..515643cc --- /dev/null +++ b/server/gen-config-docs.ts @@ -0,0 +1,35 @@ +import { configDocs } from './lib/config'; +import { writeFile } from 'fs/promises'; + +function formatJson(data) +{ + return `\`${data.replace(/\n/g, '')}\``; +} + +let data = `# Edumeet Server Configuration + +| Property | Description | Format | Default value | +| :------- | :---------- | :----- | :------------ | +`; + +Object.entries(configDocs).forEach((entry: [string, any]) => +{ + const [ name, value ] = entry; + + data += `| ${name} | ${value.doc} | ${formatJson(value.format)} | \`${formatJson(value.default)}\` |\n`; +}); + +data += ` + +--- + +*Document generated with:* \`yarn gen-config-docs\` +`; + +writeFile('config.md', data).then(() => +{ + console.log('done'); // eslint-disable-line +}, (err) => +{ + console.error(`Error writing file: ${err.message}`); // eslint-disable-line +}); diff --git a/server/lib/config.ts b/server/lib/config.ts index d35c40ee..7dfb254e 100644 --- a/server/lib/config.ts +++ b/server/lib/config.ts @@ -112,12 +112,12 @@ const configSchema = convict({ cert : { doc : 'SSL certificate path.', format : String, - default : `${__dirname}/../certs/mediasoup-demo.localhost.cert.pem` + default : './certs/mediasoup-demo.localhost.cert.pem' }, key : { doc : 'SSL key path.', format : String, - default : `${__dirname}/../certs/mediasoup-demo.localhost.key.pem` + default : './certs/mediasoup-demo.localhost.key.pem' } }, listeningHost : { diff --git a/server/package.json b/server/package.json index 10a465f1..13ee8f07 100644 --- a/server/package.json +++ b/server/package.json @@ -19,7 +19,8 @@ "dev": "nodemon --exec ts-node --ignore dist/ -e js,ts server.js", "connect": "ts-node connect.js", "lint": "eslint -c .eslintrc.json --ext .js,.ts *.js *.ts lib/", - "format": "prettier --write '**/*.ts' && npm run lint --fix" + "format": "prettier --write '**/*.ts' && npm run lint --fix", + "gen-config-docs": "ts-node gen-config-docs.ts" }, "dependencies": { "awaitqueue": "^1.0.0", From d3557b8133b7fbc2a2b7b0cd88703d801b0ab39d Mon Sep 17 00:00:00 2001 From: Vittorio Palmisano Date: Thu, 6 May 2021 10:44:14 +0200 Subject: [PATCH 31/57] renamed doc file; using links formatting --- server/{config.md => README.md} | 10 +++++----- server/gen-config-docs.ts | 2 +- server/lib/config.ts | 8 ++++---- 3 files changed, 10 insertions(+), 10 deletions(-) rename server/{config.md => README.md} (87%) diff --git a/server/config.md b/server/README.md similarity index 87% rename from server/config.md rename to server/README.md index 3e96c595..4738531c 100644 --- a/server/config.md +++ b/server/README.md @@ -1,4 +1,4 @@ -# Edumeet Server Config +# Edumeet Server Configuration | Property | Description | Format | Default value | | :------- | :---------- | :----- | :------------ | @@ -20,7 +20,7 @@ | listeningPort | The HTTPS listening port. | `"port"` | ``8443`` | | listeningRedirectPort | The HTTP listening port. Any HTTP request is redirected to HTTPS. | `"port"` | ``8080`` | | httpOnly | Listens only on HTTP on listeningPort; listeningRedirectPort disabled. Use case: load balancer backend. | `"boolean"` | ``false`` | -| trustProxy | WebServer/Express trust proxy config for httpOnly mode. More infos: https://expressjs.com/en/guide/behind-proxies.html, https://www.npmjs.com/package/proxy-addr | `"string"` | ``""`` | +| trustProxy | WebServer/Express trust proxy config for httpOnly mode. More infos: [expressjs](https://expressjs.com/en/guide/behind-proxies.html), [proxy-addr](https://www.npmjs.com/package/proxy-addr) | `"string"` | ``""`` | | activateOnHostJoin | When true, the room will be open to all users since there are users in the room. | `"boolean"` | ``true`` | | roomsUnlocked | An array of rooms users can enter without waiting in the lobby. | `"array"` | ``null`` | | maxUsersPerRoom | It defines how many users can join a single room. If not set, no limit is applied. | `"nat"` | ``null`` | @@ -33,9 +33,9 @@ | mediasoup.worker.rtcMinPort | The Mediasoup start listening port number. | `"port"` | ``40000`` | | mediasoup.worker.rtcMaxPort | The Mediasoup end listening port number. | `"port"` | ``49999`` | | mediasoup.router.mediaCodecs | The Mediasoup codecs settings. | `"object"` | ``[ { "kind": "audio", "mimeType": "audio/opus", "clockRate": 48000, "channels": 2 }, { "kind": "video", "mimeType": "video/VP8", "clockRate": 90000, "parameters": { "x-google-start-bitrate": 1000 } }, { "kind": "video", "mimeType": "video/VP9", "clockRate": 90000, "parameters": { "profile-id": 2, "x-google-start-bitrate": 1000 } }, { "kind": "video", "mimeType": "video/h264", "clockRate": 90000, "parameters": { "packetization-mode": 1, "profile-level-id": "4d0032", "level-asymmetry-allowed": 1, "x-google-start-bitrate": 1000 } }, { "kind": "video", "mimeType": "video/h264", "clockRate": 90000, "parameters": { "packetization-mode": 1, "profile-level-id": "42e01f", "level-asymmetry-allowed": 1, "x-google-start-bitrate": 1000 } }]`` | -| mediasoup.webRtcTransport.listenIps | The Mediasoup listen IPs. https://mediasoup.org/documentation/v3/mediasoup/api/#TransportListenIp | `"array"` | ``[ { "ip": "0.0.0.0", "announcedIp": null }]`` | -| mediasoup.webRtcTransport.initialAvailableOutgoingBitrate | The Mediasoup initial available outgoing bitrate (in bps). https://mediasoup.org/documentation/v3/mediasoup/api/#WebRtcTransportOptions | `"nat"` | ``1000000`` | -| mediasoup.webRtcTransport.maxIncomingBitrate | The Mediasoup maximum incoming bitrate for each transport. (in bps). https://mediasoup.org/documentation/v3/mediasoup/api/#transport-setMaxIncomingBitrate | `"nat"` | ``1500000`` | +| mediasoup.webRtcTransport.listenIps | The Mediasoup listen IPs. [TransportListenIp](https://mediasoup.org/documentation/v3/mediasoup/api/#TransportListenIp) | `"array"` | ``[ { "ip": "0.0.0.0", "announcedIp": null }]`` | +| mediasoup.webRtcTransport.initialAvailableOutgoingBitrate | The Mediasoup initial available outgoing bitrate (in bps). [WebRtcTransportOptions](https://mediasoup.org/documentation/v3/mediasoup/api/#WebRtcTransportOptions) | `"nat"` | ``1000000`` | +| mediasoup.webRtcTransport.maxIncomingBitrate | The Mediasoup maximum incoming bitrate for each transport. (in bps). [setMaxIncomingBitrate](https://mediasoup.org/documentation/v3/mediasoup/api/#transport-setMaxIncomingBitrate) | `"nat"` | ``1500000`` | | prometheus.deidentify | De-identify IP addresses in Prometheus logs. | `"boolean"` | ``false`` | | prometheus.listen | Prometheus exporter listening address. | `"string"` | ``"localhost"`` | | prometheus.numeric | Show numeric IP addresses in Prometheus logs. | `"boolean"` | ``false`` | diff --git a/server/gen-config-docs.ts b/server/gen-config-docs.ts index 515643cc..7139f3f4 100644 --- a/server/gen-config-docs.ts +++ b/server/gen-config-docs.ts @@ -26,7 +26,7 @@ data += ` *Document generated with:* \`yarn gen-config-docs\` `; -writeFile('config.md', data).then(() => +writeFile('README.md', data).then(() => { console.log('done'); // eslint-disable-line }, (err) => diff --git a/server/lib/config.ts b/server/lib/config.ts index 7dfb254e..ce32e6b0 100644 --- a/server/lib/config.ts +++ b/server/lib/config.ts @@ -141,7 +141,7 @@ const configSchema = convict({ default : false }, trustProxy : { - doc : 'WebServer/Express trust proxy config for httpOnly mode. More infos: https://expressjs.com/en/guide/behind-proxies.html, https://www.npmjs.com/package/proxy-addr', + doc : 'WebServer/Express trust proxy config for httpOnly mode. More infos: [expressjs](https://expressjs.com/en/guide/behind-proxies.html), [proxy-addr](https://www.npmjs.com/package/proxy-addr)', format : String, default : '' }, @@ -278,19 +278,19 @@ const configSchema = convict({ webRtcTransport : { listenIps : { - doc : 'The Mediasoup listen IPs. https://mediasoup.org/documentation/v3/mediasoup/api/#TransportListenIp', + doc : 'The Mediasoup listen IPs. [TransportListenIp](https://mediasoup.org/documentation/v3/mediasoup/api/#TransportListenIp)', format : Array, default : [ { ip: '0.0.0.0', announcedIp: null } ] }, initialAvailableOutgoingBitrate : { - doc : 'The Mediasoup initial available outgoing bitrate (in bps). https://mediasoup.org/documentation/v3/mediasoup/api/#WebRtcTransportOptions', + doc : 'The Mediasoup initial available outgoing bitrate (in bps). [WebRtcTransportOptions](https://mediasoup.org/documentation/v3/mediasoup/api/#WebRtcTransportOptions)', format : 'nat', default : 1000000 }, maxIncomingBitrate : { - doc : 'The Mediasoup maximum incoming bitrate for each transport. (in bps). https://mediasoup.org/documentation/v3/mediasoup/api/#transport-setMaxIncomingBitrate', + doc : 'The Mediasoup maximum incoming bitrate for each transport. (in bps). [setMaxIncomingBitrate](https://mediasoup.org/documentation/v3/mediasoup/api/#transport-setMaxIncomingBitrate)', format : 'nat', default : 1500000 } From 1107faa73b5ac1875768cf134582126c0e91b29b Mon Sep 17 00:00:00 2001 From: Vittorio Palmisano Date: Thu, 6 May 2021 11:31:16 +0200 Subject: [PATCH 32/57] using new server config in compose environment --- compose/config/edumeet-server-config.js | 371 ---------------------- compose/config/edumeet-server-config.yaml | 8 + compose/docker-compose.yml | 1 + server/lib/Room.js | 2 +- server/lib/config.ts | 34 +- server/server.js | 12 +- 6 files changed, 43 insertions(+), 385 deletions(-) create mode 100644 compose/config/edumeet-server-config.yaml diff --git a/compose/config/edumeet-server-config.js b/compose/config/edumeet-server-config.js index f38a04a5..685fc5b7 100644 --- a/compose/config/edumeet-server-config.js +++ b/compose/config/edumeet-server-config.js @@ -53,351 +53,6 @@ function getListenIps() { module.exports = { - - // Auth conf - /* - auth : - { - // Always enabled if configured - lti : - { - consumerKey : 'key', - consumerSecret : 'secret' - }, - - // Auth strategy to use (default oidc) - strategy : 'oidc', - oidc : - { - // The issuer URL for OpenID Connect discovery - // The OpenID Provider Configuration Document - // could be discovered on: - // issuerURL + '/.well-known/openid-configuration' - - // e.g. google OIDC config - // Follow this guide to get credential: - // https://developers.google.com/identity/protocols/oauth2/openid-connect - // use this issuerURL - // issuerURL : 'https://accounts.google.com/', - - issuerURL : 'https://example.com', - clientOptions : - { - client_id : '', - client_secret : '', - scope : 'openid email profile', - // where client.example.com is your edumeet server - redirect_uri : 'https://client.example.com/auth/callback' - } - - }, - saml : - { - // where edumeet.example.com is your edumeet server - callbackUrl : 'https://edumeet.example.com/auth/callback', - issuer : 'https://edumeet.example.com', - entryPoint : 'https://openidp.feide.no/simplesaml/saml2/idp/SSOService.php', - privateCert : fs.readFileSync('config/saml_privkey.pem', 'utf-8'), - signingCert : fs.readFileSync('config/saml_cert.pem', 'utf-8'), - decryptionPvk : fs.readFileSync('config/saml_privkey.pem', 'utf-8'), - decryptionCert : fs.readFileSync('config/saml_cert.pem', 'utf-8'), - // Federation cert - cert : fs.readFileSync('config/federation_cert.pem', 'utf-8') - }, - - // to create password hash use: node server/utils/password_encode.js cleartextpassword - local : - { - users : [ - { - id : 1, - username : 'alice', - passwordHash : '$2b$10$PAXXw.6cL3zJLd7ZX.AnL.sFg2nxjQPDmMmGSOQYIJSa0TrZ9azG6', - displayName : 'Alice', - emails : [ { value: 'alice@atlanta.com' } ] - }, - { - id : 2, - username : 'bob', - passwordHash : '$2b$10$BzAkXcZ54JxhHTqCQcFn8.H6klY/G48t4jDBeTE2d2lZJk/.tvv0G', - displayName : 'Bob', - emails : [ { value: 'bob@biloxi.com' } ] - } - ] - } - }, - */ - // URI and key for requesting geoip-based TURN server closest to the client - //turnAPIKey : 'examplekey', - //turnAPIURI : 'https://example.com/api/turn', - //turnAPIparams : { - // 'uri_schema' : 'turn', - // 'transport' : 'tcp', - // 'ip_ver' : 'ipv4', - // 'servercount' : '2' - //}, - //turnAPITimeout : 2 * 1000, - // Backup turnservers if REST fails or is not configured - //backupTurnServers : [ - // { - // urls : [ - // 'turn:turn.example.com:443?transport=tcp' - // ], - // username : 'example', - // credential : 'example' - // } - //], - // bittorrent tracker - fileTracker : 'wss://tracker.lab.vvc.niif.hu:443', - // redis server options - redisOptions : { - host: 'redis', - port: 6379 - }, - // session cookie secret - cookieSecret : 'T0P-S3cR3t_cook!e', - cookieName : 'edumeet.sid', - // if you use encrypted private key the set the passphrase - tls : - { - cert : `${__dirname}/../certs/mediasoup-demo.localhost.cert.pem`, - // passphrase: 'key_password' - key : `${__dirname}/../certs/mediasoup-demo.localhost.key.pem` - }, - // listening Host or IP - // If omitted listens on every IP. ("0.0.0.0" and "::") - // listeningHost: 'localhost', - // Listening port for https server. - listeningPort : 3443, - // Any http request is redirected to https. - // Listening port for http server. - listeningRedirectPort : 8080, - // Listens only on http, only on listeningPort - // listeningRedirectPort disabled - // use case: loadbalancer backend - httpOnly : false, - // WebServer/Express trust proxy config for httpOnly mode - // You can find more info: - // - https://expressjs.com/en/guide/behind-proxies.html - // - https://www.npmjs.com/package/proxy-addr - // use case: loadbalancer backend - trustProxy : '', - // This logger class will have the log function - // called every time there is a room created or destroyed, - // or peer created or destroyed. This would then be able - // to log to a file or external service. - /* StatusLogger : class - { - constructor() - { - this._queue = new AwaitQueue(); - } - - // rooms: rooms object - // peers: peers object - // eslint-disable-next-line no-unused-vars - async log({ rooms, peers }) - { - this._queue.push(async () => - { - // Do your logging in here, use queue to keep correct order - - // eslint-disable-next-line no-console - console.log('Number of rooms: ', rooms.size); - // eslint-disable-next-line no-console - console.log('Number of peers: ', peers.size); - }) - .catch((error) => - { - // eslint-disable-next-line no-console - console.log('error in log', error); - }); - } - }, */ - // This function will be called on successful login through oidc. - // Use this function to map your oidc userinfo to the Peer object. - // The roomId is equal to the room name. - // See examples below. - // Examples: - /* - // All authenicated users will be MODERATOR and AUTHENTICATED - userMapping : async ({ peer, room, roomId, userinfo }) => - { - peer.addRole(userRoles.MODERATOR); - peer.addRole(userRoles.AUTHENTICATED); - }, - // All authenicated users will be AUTHENTICATED, - // and those with the moderator role set in the userinfo - // will also be MODERATOR - userMapping : async ({ peer, room, roomId, userinfo }) => - { - if ( - Array.isArray(userinfo.meet_roles) && - userinfo.meet_roles.includes('moderator') - ) - { - peer.addRole(userRoles.MODERATOR); - } - - if ( - Array.isArray(userinfo.meet_roles) && - userinfo.meet_roles.includes('meetingadmin') - ) - { - peer.addRole(userRoles.ADMIN); - } - - peer.addRole(userRoles.AUTHENTICATED); - }, - // First authenticated user will be moderator, - // all others will be AUTHENTICATED - userMapping : async ({ peer, room, roomId, userinfo }) => - { - if (room) - { - const peers = room.getJoinedPeers(); - - if (peers.some((_peer) => _peer.authenticated)) - peer.addRole(userRoles.AUTHENTICATED); - else - { - peer.addRole(userRoles.MODERATOR); - peer.addRole(userRoles.AUTHENTICATED); - } - } - }, - // All authenicated users will be AUTHENTICATED, - // and those with email ending with @example.com - // will also be MODERATOR - userMapping : async ({ peer, room, roomId, userinfo }) => - { - if (userinfo.email && userinfo.email.endsWith('@example.com')) - { - peer.addRole(userRoles.MODERATOR); - } - - peer.addRole(userRoles.AUTHENTICATED); - }, - // All authenicated users will be AUTHENTICATED, - // and those with email ending with @example.com - // will also be MODERATOR - userMapping : async ({ peer, room, roomId, userinfo }) => - { - if (userinfo.email && userinfo.email.endsWith('@example.com')) - { - peer.addRole(userRoles.MODERATOR); - } - - peer.addRole(userRoles.AUTHENTICATED); - }, - */ - // eslint-disable-next-line no-unused-vars - userMapping : async ({ peer, room, roomId, userinfo }) => - { - if (userinfo.picture != null) - { - if (!userinfo.picture.match(/^http/g)) - { - peer.picture = `data:image/jpeg;base64, ${userinfo.picture}`; - } - else - { - peer.picture = userinfo.picture; - } - } - if (userinfo['urn:oid:0.9.2342.19200300.100.1.60'] != null) - { - peer.picture = `data:image/jpeg;base64, ${userinfo['urn:oid:0.9.2342.19200300.100.1.60']}`; - } - - if (userinfo.nickname != null) - { - peer.displayName = userinfo.nickname; - } - - if (userinfo.name != null) - { - peer.displayName = userinfo.name; - } - - if (userinfo.displayName != null) - { - peer.displayName = userinfo.displayName; - } - - if (userinfo['urn:oid:2.16.840.1.113730.3.1.241'] != null) - { - peer.displayName = userinfo['urn:oid:2.16.840.1.113730.3.1.241']; - } - - if (userinfo.email != null) - { - peer.email = userinfo.email; - } - }, - // All users have the role "NORMAL" by default. Other roles need to be - // added in the "userMapping" function. The following accesses and - // permissions are arrays of roles. Roles can be changed in userRoles.js - // - // Example: - // [ userRoles.MODERATOR, userRoles.AUTHENTICATED ] - accessFromRoles : { - // The role(s) will gain access to the room - // even if it is locked (!) - [BYPASS_ROOM_LOCK] : [ userRoles.ADMIN ], - // The role(s) will gain access to the room without - // going into the lobby. If you want to restrict access to your - // server to only directly allow authenticated users, you could - // add the userRoles.AUTHENTICATED to the user in the userMapping - // function, and change to BYPASS_LOBBY : [ userRoles.AUTHENTICATED ] - [BYPASS_LOBBY] : [ userRoles.NORMAL ] - }, - permissionsFromRoles : { - // The role(s) have permission to lock/unlock a room - [CHANGE_ROOM_LOCK] : [ userRoles.MODERATOR ], - // The role(s) have permission to promote a peer from the lobby - [PROMOTE_PEER] : [ userRoles.NORMAL ], - // The role(s) have permission to give/remove other peers roles - [MODIFY_ROLE] : [ userRoles.NORMAL ], - // The role(s) have permission to send chat messages - [SEND_CHAT] : [ userRoles.NORMAL ], - // The role(s) have permission to moderate chat - [MODERATE_CHAT] : [ userRoles.MODERATOR ], - // The role(s) have permission to share audio - [SHARE_AUDIO] : [ userRoles.NORMAL ], - // The role(s) have permission to share video - [SHARE_VIDEO] : [ userRoles.NORMAL ], - // The role(s) have permission to share screen - [SHARE_SCREEN] : [ userRoles.NORMAL ], - // The role(s) have permission to produce extra video - [EXTRA_VIDEO] : [ userRoles.NORMAL ], - // The role(s) have permission to share files - [SHARE_FILE] : [ userRoles.NORMAL ], - // The role(s) have permission to moderate files - [MODERATE_FILES] : [ userRoles.MODERATOR ], - // The role(s) have permission to moderate room (e.g. kick user) - [MODERATE_ROOM] : [ userRoles.MODERATOR ] - }, - // Array of permissions. If no peer with the permission in question - // is in the room, all peers are permitted to do the action. The peers - // that are allowed because of this rule will not be able to do this - // action as soon as a peer with the permission joins. In this example - // everyone will be able to lock/unlock room until a MODERATOR joins. - allowWhenRoleMissing : [ CHANGE_ROOM_LOCK ], - // When truthy, the room will be open to all users when as long as there - // are allready users in the room - activateOnHostJoin : true, - // When set, maxUsersPerRoom defines how many users can join - // a single room. If not set, there is no limit. - // maxUsersPerRoom : 20, - // Room size before spreading to new router - routerScaleSize : 40, - // Socket timout value - requestTimeout : 20000, - // Socket retries when timeout - requestRetries : 3, - // If > 0, sets a cache-control max-age (in seconds) to static files responses. - staticFilesCachePeriod : 0, // Mediasoup settings mediasoup : { @@ -479,32 +134,6 @@ module.exports = webRtcTransport : { listenIps : getListenIps(), - /*[ - // change 192.0.2.1 IPv4 to your server's IPv4 address!! - //{ ip: '192.0.2.1', announcedIp: null } - // Can have multiple listening interfaces - // change 2001:DB8::1 IPv6 to your server's IPv6 address!! - // { ip: '2001:DB8::1', announcedIp: null } - ],*/ - initialAvailableOutgoingBitrate : 1000000, - minimumAvailableOutgoingBitrate : 600000, - // Additional options that are not part of WebRtcTransportOptions. - maxIncomingBitrate : 1500000 } } - - - , - // Prometheus exporter - prometheus : { - deidentify : false, // deidentify IP addresses - // listen : 'localhost', // exporter listens on this address - numeric : true, // show numeric IP addresses - port : 8889, // allocated port - quiet : false, // include fewer labels - // aggregated metrics options - period : 15, // update period (seconds) - secret : null // if set, checks the authorization header: `Bearer ` - } - }; diff --git a/compose/config/edumeet-server-config.yaml b/compose/config/edumeet-server-config.yaml new file mode 100644 index 00000000..37ef667d --- /dev/null +++ b/compose/config/edumeet-server-config.yaml @@ -0,0 +1,8 @@ +redisOptions: + host: redis + port: 6379 + +listeningPort: 3443 +listeningRedirectPort: 8080 +httpOnly: false +trustProxy: '' diff --git a/compose/docker-compose.yml b/compose/docker-compose.yml index ee58f3b2..f4e39467 100644 --- a/compose/docker-compose.yml +++ b/compose/docker-compose.yml @@ -18,6 +18,7 @@ services: volumes: - ${PWD}/..:/edumeet - ${PWD}/config/edumeet-server-config.js:/edumeet/server/config/config.js:ro + - ${PWD}/config/edumeet-server-config.yaml:/edumeet/server/config/config.yaml:ro - ${PWD}/config/edumeet-app-config.js:/edumeet/app/public/config/config.js:ro network_mode: "host" extra_hosts: diff --git a/server/lib/Room.js b/server/lib/Room.js index 2216b1c8..2b22616d 100644 --- a/server/lib/Room.js +++ b/server/lib/Room.js @@ -395,7 +395,7 @@ class Room extends EventEmitter else if (this._hasAccess(peer, BYPASS_ROOM_LOCK)) this._peerJoining(peer); else if ( - 'maxUsersPerRoom' in config && + config.maxUsersPerRoom && ( Object.keys(this._peers).length + this._lobby.peerList().length diff --git a/server/lib/config.ts b/server/lib/config.ts index ce32e6b0..956ac176 100644 --- a/server/lib/config.ts +++ b/server/lib/config.ts @@ -44,7 +44,7 @@ const configSchema = convict({ turnAPIURI : { doc : 'TURN server URL for requesting a geoip-based TURN server closest to the client.', - format : 'url', + format : String, default : '' }, turnAPIparams : @@ -94,9 +94,21 @@ const configSchema = convict({ default : 'wss://tracker.lab.vvc.niif.hu:443' }, redisOptions : { - doc : 'Redis server options.', - format : Object, - default : {} + host : { + doc : 'Redis server host.', + format : String, + default : 'localhost' + }, + port : { + doc : 'Redis server port.', + format : 'port', + default : 6379 + }, + password : { + doc : 'Redis server password.', + format : String, + default : '' + } }, cookieSecret : { doc : 'Session cookie secret.', @@ -123,7 +135,7 @@ const configSchema = convict({ listeningHost : { doc : 'The listening Host or IP address. If omitted listens on every IP. ("0.0.0.0" and "::").', format : String, - default : null + default : '' }, listeningPort : { doc : 'The HTTPS listening port.', @@ -153,12 +165,12 @@ const configSchema = convict({ roomsUnlocked : { doc : 'An array of rooms users can enter without waiting in the lobby.', format : Array, - default : null + default : [] }, maxUsersPerRoom : { doc : 'It defines how many users can join a single room. If not set, no limit is applied.', format : 'nat', - default : null + default : 0 }, routerScaleSize : { doc : 'Room size before spreading to a new router.', @@ -219,7 +231,7 @@ const configSchema = convict({ // Router media codecs. mediaCodecs : { doc : 'The Mediasoup codecs settings.', - format : Object, + format : '*', default : [ { @@ -332,7 +344,7 @@ const configSchema = convict({ secret : { doc : 'The Prometheus exporter authorization header: `Bearer ` required to allow scraping.', format : String, - default : null + default : '' } } }); @@ -376,7 +388,7 @@ let config: any = {}; let configError = ''; let configLoaded = false; -// Load config from window object +// Load config from file for (const format of [ 'json', 'json5', 'yaml', 'yml', 'toml' ]) // eslint-disable-line no-restricted-syntax { const filepath = `./config/config.${format}`; @@ -430,7 +442,7 @@ if (fs.existsSync(`${__dirname}/../config/config.js`)) } // eslint-disable-next-line -logger.info('Using config:', config); +// logger.debug('Using config:', config); // export { diff --git a/server/server.js b/server/server.js index 98222e36..c0d22299 100755 --- a/server/server.js +++ b/server/server.js @@ -10,7 +10,7 @@ const { loginHelper, logoutHelper } = require('./httpHelper'); -const { config } = require('./lib/config'); +const { config, configError } = require('./lib/config'); const interactiveServer = require('./lib/interactiveServer'); const promExporter = require('./lib/promExporter'); @@ -33,13 +33,21 @@ const imsLti = require('ims-lti'); const SAMLStrategy = require('passport-saml').Strategy; const LocalStrategy = require('passport-local').Strategy; const redis = require('redis'); -const redisClient = redis.createClient(config.redisOptions); const { Issuer, Strategy } = require('openid-client'); const expressSession = require('express-session'); const RedisStore = require('connect-redis')(expressSession); const sharedSession = require('express-socket.io-session'); const { v4: uuidv4 } = require('uuid'); +if (configError) +{ + /* eslint-disable no-console */ + console.error(`Invalid config file: ${configError}`); + process.exit(-1); +} + +const redisClient = redis.createClient(config.redisOptions); + /* eslint-disable no-console */ console.log('- process.env.DEBUG:', process.env.DEBUG); console.log('- config.mediasoup.worker.logLevel:', config.mediasoup.worker.logLevel); From 834152ca53f6078641bc0075528a715e46dea2fe Mon Sep 17 00:00:00 2001 From: Vittorio Palmisano Date: Thu, 6 May 2021 11:39:22 +0200 Subject: [PATCH 33/57] adde more docs --- server/README.md | 44 +++++++++++++++++++++++++++++++-------- server/gen-config-docs.ts | 29 ++++++++++++++++++++++++-- 2 files changed, 62 insertions(+), 11 deletions(-) diff --git a/server/README.md b/server/README.md index 4738531c..06c2e011 100644 --- a/server/README.md +++ b/server/README.md @@ -1,9 +1,33 @@ # Edumeet Server Configuration -| Property | Description | Format | Default value | -| :------- | :---------- | :----- | :------------ | +The server configuration file can use one of the following formats: + +- config/config.json +- config/config.json5 +- config/config.yaml +- config/config.yml +- config/config.toml + +Example `config.yaml`: + +```yaml +redisOptions: + host: redis + port: 6379 + +listeningPort: 3443 +``` + +Additionally, the `config/config.js` can be modified to overwrite specific properties +with runtime generated values and to add configuration functions and classes. +Look at the default `config/config.js` source file for documentation. + +## Configuration properties + +| Name | Description | Format | Default value | +| :--- | :---------- | :----- | :------------ | | turnAPIKey | TURN server key for requesting a geoip-based TURN server closest to the client. | `"string"` | ``""`` | -| turnAPIURI | TURN server URL for requesting a geoip-based TURN server closest to the client. | `"url"` | ``""`` | +| turnAPIURI | TURN server URL for requesting a geoip-based TURN server closest to the client. | `"string"` | ``""`` | | turnAPIparams.uri_schema | TURN server URL schema. | `"string"` | ``"turn"`` | | turnAPIparams.transport | TURN server transport. | `[ "tcp", "udp"]` | ``"tcp"`` | | turnAPIparams.ip_ver | TURN server IP version. | `[ "ipv4", "ipv6"]` | ``"ipv4"`` | @@ -11,19 +35,21 @@ | turnAPITimeout | TURN server API timeout (seconds). | `"nat"` | ``2000`` | | backupTurnServers | Backup TURN servers if REST fails or is not configured | `"*"` | ``[ { "urls": [ "turn:turn.example.com:443?transport=tcp" ], "username": "example", "credential": "example" }]`` | | fileTracker | Bittorrent tracker. | `"string"` | ``"wss://tracker.lab.vvc.niif.hu:443"`` | -| redisOptions | Redis server options. | `"object"` | ``{}`` | +| redisOptions.host | Redis server host. | `"string"` | ``"localhost"`` | +| redisOptions.port | Redis server port. | `"port"` | ``6379`` | +| redisOptions.password | Redis server password. | `"string"` | ``""`` | | cookieSecret | Session cookie secret. | `"string"` | ``"T0P-S3cR3t_cook!e"`` | | cookieName | Session cookie name. | `"string"` | ``"edumeet.sid"`` | | tls.cert | SSL certificate path. | `"string"` | ``"./certs/mediasoup-demo.localhost.cert.pem"`` | | tls.key | SSL key path. | `"string"` | ``"./certs/mediasoup-demo.localhost.key.pem"`` | -| listeningHost | The listening Host or IP address. If omitted listens on every IP. ("0.0.0.0" and "::"). | `"string"` | ``null`` | +| listeningHost | The listening Host or IP address. If omitted listens on every IP. ("0.0.0.0" and "::"). | `"string"` | ``""`` | | listeningPort | The HTTPS listening port. | `"port"` | ``8443`` | | listeningRedirectPort | The HTTP listening port. Any HTTP request is redirected to HTTPS. | `"port"` | ``8080`` | | httpOnly | Listens only on HTTP on listeningPort; listeningRedirectPort disabled. Use case: load balancer backend. | `"boolean"` | ``false`` | | trustProxy | WebServer/Express trust proxy config for httpOnly mode. More infos: [expressjs](https://expressjs.com/en/guide/behind-proxies.html), [proxy-addr](https://www.npmjs.com/package/proxy-addr) | `"string"` | ``""`` | | activateOnHostJoin | When true, the room will be open to all users since there are users in the room. | `"boolean"` | ``true`` | -| roomsUnlocked | An array of rooms users can enter without waiting in the lobby. | `"array"` | ``null`` | -| maxUsersPerRoom | It defines how many users can join a single room. If not set, no limit is applied. | `"nat"` | ``null`` | +| roomsUnlocked | An array of rooms users can enter without waiting in the lobby. | `"array"` | ``[]`` | +| maxUsersPerRoom | It defines how many users can join a single room. If not set, no limit is applied. | `"nat"` | ``0`` | | routerScaleSize | Room size before spreading to a new router. | `"nat"` | ``40`` | | requestTimeout | Socket timeout value (ms). | `"nat"` | ``20000`` | | requestRetries | Socket retries when a timeout occurs. | `"nat"` | ``3`` | @@ -32,7 +58,7 @@ | mediasoup.worker.logTags | The Mediasoup log tags. | `"array"` | ``[ "info", "ice", "dtls", "rtp", "srtp", "rtcp"]`` | | mediasoup.worker.rtcMinPort | The Mediasoup start listening port number. | `"port"` | ``40000`` | | mediasoup.worker.rtcMaxPort | The Mediasoup end listening port number. | `"port"` | ``49999`` | -| mediasoup.router.mediaCodecs | The Mediasoup codecs settings. | `"object"` | ``[ { "kind": "audio", "mimeType": "audio/opus", "clockRate": 48000, "channels": 2 }, { "kind": "video", "mimeType": "video/VP8", "clockRate": 90000, "parameters": { "x-google-start-bitrate": 1000 } }, { "kind": "video", "mimeType": "video/VP9", "clockRate": 90000, "parameters": { "profile-id": 2, "x-google-start-bitrate": 1000 } }, { "kind": "video", "mimeType": "video/h264", "clockRate": 90000, "parameters": { "packetization-mode": 1, "profile-level-id": "4d0032", "level-asymmetry-allowed": 1, "x-google-start-bitrate": 1000 } }, { "kind": "video", "mimeType": "video/h264", "clockRate": 90000, "parameters": { "packetization-mode": 1, "profile-level-id": "42e01f", "level-asymmetry-allowed": 1, "x-google-start-bitrate": 1000 } }]`` | +| mediasoup.router.mediaCodecs | The Mediasoup codecs settings. | `"*"` | ``[ { "kind": "audio", "mimeType": "audio/opus", "clockRate": 48000, "channels": 2 }, { "kind": "video", "mimeType": "video/VP8", "clockRate": 90000, "parameters": { "x-google-start-bitrate": 1000 } }, { "kind": "video", "mimeType": "video/VP9", "clockRate": 90000, "parameters": { "profile-id": 2, "x-google-start-bitrate": 1000 } }, { "kind": "video", "mimeType": "video/h264", "clockRate": 90000, "parameters": { "packetization-mode": 1, "profile-level-id": "4d0032", "level-asymmetry-allowed": 1, "x-google-start-bitrate": 1000 } }, { "kind": "video", "mimeType": "video/h264", "clockRate": 90000, "parameters": { "packetization-mode": 1, "profile-level-id": "42e01f", "level-asymmetry-allowed": 1, "x-google-start-bitrate": 1000 } }]`` | | mediasoup.webRtcTransport.listenIps | The Mediasoup listen IPs. [TransportListenIp](https://mediasoup.org/documentation/v3/mediasoup/api/#TransportListenIp) | `"array"` | ``[ { "ip": "0.0.0.0", "announcedIp": null }]`` | | mediasoup.webRtcTransport.initialAvailableOutgoingBitrate | The Mediasoup initial available outgoing bitrate (in bps). [WebRtcTransportOptions](https://mediasoup.org/documentation/v3/mediasoup/api/#WebRtcTransportOptions) | `"nat"` | ``1000000`` | | mediasoup.webRtcTransport.maxIncomingBitrate | The Mediasoup maximum incoming bitrate for each transport. (in bps). [setMaxIncomingBitrate](https://mediasoup.org/documentation/v3/mediasoup/api/#transport-setMaxIncomingBitrate) | `"nat"` | ``1500000`` | @@ -42,7 +68,7 @@ | prometheus.port | The Prometheus exporter listening port. | `"port"` | ``8889`` | | prometheus.quiet | Include fewer labels in Prometheus logs. | `"boolean"` | ``false`` | | prometheus.period | The Prometheus exporter update period (seconds). | `"nat"` | ``15`` | -| prometheus.secret | The Prometheus exporter authorization header: `Bearer ` required to allow scraping. | `"string"` | ``null`` | +| prometheus.secret | The Prometheus exporter authorization header: `Bearer ` required to allow scraping. | `"string"` | ``""`` | --- diff --git a/server/gen-config-docs.ts b/server/gen-config-docs.ts index 7139f3f4..32a84a35 100644 --- a/server/gen-config-docs.ts +++ b/server/gen-config-docs.ts @@ -1,5 +1,6 @@ import { configDocs } from './lib/config'; import { writeFile } from 'fs/promises'; +import config from './config/config'; function formatJson(data) { @@ -8,8 +9,32 @@ function formatJson(data) let data = `# Edumeet Server Configuration -| Property | Description | Format | Default value | -| :------- | :---------- | :----- | :------------ | +The server configuration file can use one of the following formats: + +- config/config.json +- config/config.json5 +- config/config.yaml +- config/config.yml +- config/config.toml + +Example \`config.yaml\`: + +\`\`\`yaml +redisOptions: + host: redis + port: 6379 + +listeningPort: 3443 +\`\`\` + +Additionally, the \`config/config.js\` can be modified to overwrite specific properties +with runtime generated values and to add configuration functions and classes. +Look at the default \`config/config.js\` source file for documentation. + +## Configuration properties + +| Name | Description | Format | Default value | +| :--- | :---------- | :----- | :------------ | `; Object.entries(configDocs).forEach((entry: [string, any]) => From 8a2895b83cf38608cc9ef2aa4b8571938cece85f Mon Sep 17 00:00:00 2001 From: Vittorio Palmisano Date: Thu, 6 May 2021 11:41:54 +0200 Subject: [PATCH 34/57] add default config.js --- .gitignore | 2 +- server/config/config.js | 296 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 297 insertions(+), 1 deletion(-) create mode 100644 server/config/config.js diff --git a/.gitignore b/.gitignore index 78330af6..5ee1d5c1 100644 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,7 @@ node_modules/ /app/public/images/logo.* !/app/public/images/logo.edumeet.svg /server/config/ -!/server/config/config.example.js +!/server/config/config.js /server/public/ /server/certs/ /server/dist/ diff --git a/server/config/config.js b/server/config/config.js new file mode 100644 index 00000000..0a925839 --- /dev/null +++ b/server/config/config.js @@ -0,0 +1,296 @@ +// const os = require('os'); +// const fs = require('fs'); + +const userRoles = require('../userRoles'); + +const { + BYPASS_ROOM_LOCK, + BYPASS_LOBBY +} = require('../access'); + +const { + CHANGE_ROOM_LOCK, + PROMOTE_PEER, + MODIFY_ROLE, + SEND_CHAT, + MODERATE_CHAT, + SHARE_AUDIO, + SHARE_VIDEO, + SHARE_SCREEN, + EXTRA_VIDEO, + SHARE_FILE, + MODERATE_FILES, + MODERATE_ROOM +} = require('../permissions'); + +// const AwaitQueue = require('awaitqueue'); +// const axios = require('axios'); + +module.exports = +{ + // Auth conf + /* + auth : + { + // Always enabled if configured + lti : + { + consumerKey : 'key', + consumerSecret : 'secret' + }, + + // Auth strategy to use (default oidc) + strategy : 'oidc', + oidc : + { + // The issuer URL for OpenID Connect discovery + // The OpenID Provider Configuration Document + // could be discovered on: + // issuerURL + '/.well-known/openid-configuration' + + // e.g. google OIDC config + // Follow this guide to get credential: + // https://developers.google.com/identity/protocols/oauth2/openid-connect + // use this issuerURL + // issuerURL : 'https://accounts.google.com/', + + issuerURL : 'https://example.com', + clientOptions : + { + client_id : '', + client_secret : '', + scope : 'openid email profile', + // where client.example.com is your edumeet server + redirect_uri : 'https://client.example.com/auth/callback' + } + + }, + saml : + { + // where edumeet.example.com is your edumeet server + callbackUrl : 'https://edumeet.example.com/auth/callback', + issuer : 'https://edumeet.example.com', + entryPoint : 'https://openidp.feide.no/simplesaml/saml2/idp/SSOService.php', + privateCert : fs.readFileSync('config/saml_privkey.pem', 'utf-8'), + signingCert : fs.readFileSync('config/saml_cert.pem', 'utf-8'), + decryptionPvk : fs.readFileSync('config/saml_privkey.pem', 'utf-8'), + decryptionCert : fs.readFileSync('config/saml_cert.pem', 'utf-8'), + // Federation cert + cert : fs.readFileSync('config/federation_cert.pem', 'utf-8') + }, + + // to create password hash use: node server/utils/password_encode.js cleartextpassword + local : + { + users : [ + { + id : 1, + username : 'alice', + passwordHash : '$2b$10$PAXXw.6cL3zJLd7ZX.AnL.sFg2nxjQPDmMmGSOQYIJSa0TrZ9azG6', + displayName : 'Alice', + emails : [ { value: 'alice@atlanta.com' } ], + meet_roles : [ ] + }, + { + id : 2, + username : 'bob', + passwordHash : '$2b$10$BzAkXcZ54JxhHTqCQcFn8.H6klY/G48t4jDBeTE2d2lZJk/.tvv0G', + displayName : 'Bob', + emails : [ { value: 'bob@biloxi.com' } ], + meet_roles : [ ] + } + ] + } + }, + */ + // This logger class will have the log function + // called every time there is a room created or destroyed, + // or peer created or destroyed. This would then be able + // to log to a file or external service. + /* StatusLogger : class + { + constructor() + { + this._queue = new AwaitQueue(); + } + + // rooms: rooms object + // peers: peers object + // eslint-disable-next-line no-unused-vars + async log({ rooms, peers }) + { + this._queue.push(async () => + { + // Do your logging in here, use queue to keep correct order + + // eslint-disable-next-line no-console + console.log('Number of rooms: ', rooms.size); + // eslint-disable-next-line no-console + console.log('Number of peers: ', peers.size); + }) + .catch((error) => + { + // eslint-disable-next-line no-console + console.log('error in log', error); + }); + } + }, */ + // This function will be called on successful login through oidc. + // Use this function to map your oidc userinfo to the Peer object. + // The roomId is equal to the room name. + // See examples below. + // Examples: + /* + // All authenticated users will be MODERATOR and AUTHENTICATED + userMapping : async ({ peer, room, roomId, userinfo }) => + { + peer.addRole(userRoles.MODERATOR); + peer.addRole(userRoles.AUTHENTICATED); + }, + // All authenticated users will be AUTHENTICATED, + // and those with the moderator role set in the userinfo + // will also be MODERATOR + userMapping : async ({ peer, room, roomId, userinfo }) => + { + if ( + Array.isArray(userinfo.meet_roles) && + userinfo.meet_roles.includes('moderator') + ) + { + peer.addRole(userRoles.MODERATOR); + } + + if ( + Array.isArray(userinfo.meet_roles) && + userinfo.meet_roles.includes('meetingadmin') + ) + { + peer.addRole(userRoles.ADMIN); + } + + peer.addRole(userRoles.AUTHENTICATED); + }, + // First authenticated user will be moderator, + // all others will be AUTHENTICATED + userMapping : async ({ peer, room, roomId, userinfo }) => + { + if (room) + { + const peers = room.getJoinedPeers(); + + if (peers.some((_peer) => _peer.authenticated)) + peer.addRole(userRoles.AUTHENTICATED); + else + { + peer.addRole(userRoles.MODERATOR); + peer.addRole(userRoles.AUTHENTICATED); + } + } + }, + // All authenticated users will be AUTHENTICATED, + // and those with email ending with @example.com + // will also be MODERATOR + userMapping : async ({ peer, room, roomId, userinfo }) => + { + if (userinfo.email && userinfo.email.endsWith('@example.com')) + { + peer.addRole(userRoles.MODERATOR); + } + + peer.addRole(userRoles.AUTHENTICATED); + }, + */ + // eslint-disable-next-line no-unused-vars + userMapping : async ({ peer, room, roomId, userinfo }) => + { + if (userinfo.picture != null) + { + if (!userinfo.picture.match(/^http/g)) + { + peer.picture = `data:image/jpeg;base64, ${userinfo.picture}`; + } + else + { + peer.picture = userinfo.picture; + } + } + if (userinfo['urn:oid:0.9.2342.19200300.100.1.60'] != null) + { + peer.picture = `data:image/jpeg;base64, ${userinfo['urn:oid:0.9.2342.19200300.100.1.60']}`; + } + + if (userinfo.nickname != null) + { + peer.displayName = userinfo.nickname; + } + + if (userinfo.name != null) + { + peer.displayName = userinfo.name; + } + + if (userinfo.displayName != null) + { + peer.displayName = userinfo.displayName; + } + + if (userinfo['urn:oid:2.16.840.1.113730.3.1.241'] != null) + { + peer.displayName = userinfo['urn:oid:2.16.840.1.113730.3.1.241']; + } + + if (userinfo.email != null) + { + peer.email = userinfo.email; + } + }, + // All users have the role "NORMAL" by default. Other roles need to be + // added in the "userMapping" function. The following accesses and + // permissions are arrays of roles. Roles can be changed in userRoles.js + // + // Example: + // [ userRoles.MODERATOR, userRoles.AUTHENTICATED ] + accessFromRoles : { + // The role(s) will gain access to the room + // even if it is locked (!) + [BYPASS_ROOM_LOCK] : [ userRoles.ADMIN ], + // The role(s) will gain access to the room without + // going into the lobby. If you want to restrict access to your + // server to only directly allow authenticated users, you could + // add the userRoles.AUTHENTICATED to the user in the userMapping + // function, and change to BYPASS_LOBBY : [ userRoles.AUTHENTICATED ] + [BYPASS_LOBBY] : [ userRoles.NORMAL ] + }, + permissionsFromRoles : { + // The role(s) have permission to lock/unlock a room + [CHANGE_ROOM_LOCK] : [ userRoles.MODERATOR ], + // The role(s) have permission to promote a peer from the lobby + [PROMOTE_PEER] : [ userRoles.NORMAL ], + // The role(s) have permission to give/remove other peers roles + [MODIFY_ROLE] : [ userRoles.NORMAL ], + // The role(s) have permission to send chat messages + [SEND_CHAT] : [ userRoles.NORMAL ], + // The role(s) have permission to moderate chat + [MODERATE_CHAT] : [ userRoles.MODERATOR ], + // The role(s) have permission to share audio + [SHARE_AUDIO] : [ userRoles.NORMAL ], + // The role(s) have permission to share video + [SHARE_VIDEO] : [ userRoles.NORMAL ], + // The role(s) have permission to share screen + [SHARE_SCREEN] : [ userRoles.NORMAL ], + // The role(s) have permission to produce extra video + [EXTRA_VIDEO] : [ userRoles.NORMAL ], + // The role(s) have permission to share files + [SHARE_FILE] : [ userRoles.NORMAL ], + // The role(s) have permission to moderate files + [MODERATE_FILES] : [ userRoles.MODERATOR ], + // The role(s) have permission to moderate room (e.g. kick user) + [MODERATE_ROOM] : [ userRoles.MODERATOR ] + }, + // Array of permissions. If no peer with the permission in question + // is in the room, all peers are permitted to do the action. The peers + // that are allowed because of this rule will not be able to do this + // action as soon as a peer with the permission joins. In this example + // everyone will be able to lock/unlock room until a MODERATOR joins. + allowWhenRoleMissing : [ CHANGE_ROOM_LOCK ] +}; From 249ce992f5df5e6a86208d292025ca80a721f719 Mon Sep 17 00:00:00 2001 From: Vittorio Palmisano Date: Thu, 6 May 2021 11:58:15 +0200 Subject: [PATCH 35/57] rever default config settings --- .gitignore | 2 +- server/config/{config.js => config.example.js} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename server/config/{config.js => config.example.js} (100%) diff --git a/.gitignore b/.gitignore index 5ee1d5c1..78330af6 100644 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,7 @@ node_modules/ /app/public/images/logo.* !/app/public/images/logo.edumeet.svg /server/config/ -!/server/config/config.js +!/server/config/config.example.js /server/public/ /server/certs/ /server/dist/ diff --git a/server/config/config.js b/server/config/config.example.js similarity index 100% rename from server/config/config.js rename to server/config/config.example.js From e202a29138184b2a4a2c72c13f5bacd3e50314fa Mon Sep 17 00:00:00 2001 From: Vittorio Palmisano Date: Thu, 6 May 2021 12:00:34 +0200 Subject: [PATCH 36/57] Changed: server/README.md server/gen-config-docs.ts --- server/README.md | 6 +++--- server/gen-config-docs.ts | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/server/README.md b/server/README.md index 06c2e011..66a88db7 100644 --- a/server/README.md +++ b/server/README.md @@ -18,9 +18,9 @@ redisOptions: listeningPort: 3443 ``` -Additionally, the `config/config.js` can be modified to overwrite specific properties -with runtime generated values and to add configuration functions and classes. -Look at the default `config/config.js` source file for documentation. +Additionally, a `config/config.js` can be used to override specific properties +with runtime generated values and to set additional configuration functions and classes. +Look at the default `config/config.example.js` file for documentation. ## Configuration properties diff --git a/server/gen-config-docs.ts b/server/gen-config-docs.ts index 32a84a35..fb59eefc 100644 --- a/server/gen-config-docs.ts +++ b/server/gen-config-docs.ts @@ -1,6 +1,6 @@ import { configDocs } from './lib/config'; import { writeFile } from 'fs/promises'; -import config from './config/config'; +import config from './config/config.example'; function formatJson(data) { @@ -27,9 +27,9 @@ redisOptions: listeningPort: 3443 \`\`\` -Additionally, the \`config/config.js\` can be modified to overwrite specific properties -with runtime generated values and to add configuration functions and classes. -Look at the default \`config/config.js\` source file for documentation. +Additionally, a \`config/config.js\` can be used to override specific properties +with runtime generated values and to set additional configuration functions and classes. +Look at the default \`config/config.example.js\` file for documentation. ## Configuration properties From 8ad5d1b52c1202b494e86a9bff07ceb6846593be Mon Sep 17 00:00:00 2001 From: Vittorio Palmisano Date: Thu, 6 May 2021 12:03:33 +0200 Subject: [PATCH 37/57] Changed: server/README.md server/lib/config.ts --- server/README.md | 2 +- server/lib/config.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/server/README.md b/server/README.md index 66a88db7..da9df7e3 100644 --- a/server/README.md +++ b/server/README.md @@ -58,7 +58,7 @@ Look at the default `config/config.example.js` file for documentation. | mediasoup.worker.logTags | The Mediasoup log tags. | `"array"` | ``[ "info", "ice", "dtls", "rtp", "srtp", "rtcp"]`` | | mediasoup.worker.rtcMinPort | The Mediasoup start listening port number. | `"port"` | ``40000`` | | mediasoup.worker.rtcMaxPort | The Mediasoup end listening port number. | `"port"` | ``49999`` | -| mediasoup.router.mediaCodecs | The Mediasoup codecs settings. | `"*"` | ``[ { "kind": "audio", "mimeType": "audio/opus", "clockRate": 48000, "channels": 2 }, { "kind": "video", "mimeType": "video/VP8", "clockRate": 90000, "parameters": { "x-google-start-bitrate": 1000 } }, { "kind": "video", "mimeType": "video/VP9", "clockRate": 90000, "parameters": { "profile-id": 2, "x-google-start-bitrate": 1000 } }, { "kind": "video", "mimeType": "video/h264", "clockRate": 90000, "parameters": { "packetization-mode": 1, "profile-level-id": "4d0032", "level-asymmetry-allowed": 1, "x-google-start-bitrate": 1000 } }, { "kind": "video", "mimeType": "video/h264", "clockRate": 90000, "parameters": { "packetization-mode": 1, "profile-level-id": "42e01f", "level-asymmetry-allowed": 1, "x-google-start-bitrate": 1000 } }]`` | +| mediasoup.router.mediaCodecs | The Mediasoup codecs settings. [supportedRtpCapabilities](https://github.com/versatica/mediasoup/blob/v3/src/supportedRtpCapabilities.ts) | `"*"` | ``[ { "kind": "audio", "mimeType": "audio/opus", "clockRate": 48000, "channels": 2 }, { "kind": "video", "mimeType": "video/VP8", "clockRate": 90000, "parameters": { "x-google-start-bitrate": 1000 } }, { "kind": "video", "mimeType": "video/VP9", "clockRate": 90000, "parameters": { "profile-id": 2, "x-google-start-bitrate": 1000 } }, { "kind": "video", "mimeType": "video/h264", "clockRate": 90000, "parameters": { "packetization-mode": 1, "profile-level-id": "4d0032", "level-asymmetry-allowed": 1, "x-google-start-bitrate": 1000 } }, { "kind": "video", "mimeType": "video/h264", "clockRate": 90000, "parameters": { "packetization-mode": 1, "profile-level-id": "42e01f", "level-asymmetry-allowed": 1, "x-google-start-bitrate": 1000 } }]`` | | mediasoup.webRtcTransport.listenIps | The Mediasoup listen IPs. [TransportListenIp](https://mediasoup.org/documentation/v3/mediasoup/api/#TransportListenIp) | `"array"` | ``[ { "ip": "0.0.0.0", "announcedIp": null }]`` | | mediasoup.webRtcTransport.initialAvailableOutgoingBitrate | The Mediasoup initial available outgoing bitrate (in bps). [WebRtcTransportOptions](https://mediasoup.org/documentation/v3/mediasoup/api/#WebRtcTransportOptions) | `"nat"` | ``1000000`` | | mediasoup.webRtcTransport.maxIncomingBitrate | The Mediasoup maximum incoming bitrate for each transport. (in bps). [setMaxIncomingBitrate](https://mediasoup.org/documentation/v3/mediasoup/api/#transport-setMaxIncomingBitrate) | `"nat"` | ``1500000`` | diff --git a/server/lib/config.ts b/server/lib/config.ts index 956ac176..4af92371 100644 --- a/server/lib/config.ts +++ b/server/lib/config.ts @@ -230,7 +230,7 @@ const configSchema = convict({ { // Router media codecs. mediaCodecs : { - doc : 'The Mediasoup codecs settings.', + doc : 'The Mediasoup codecs settings. [supportedRtpCapabilities](https://github.com/versatica/mediasoup/blob/v3/src/supportedRtpCapabilities.ts)', format : '*', default : [ From 8578021fb464c7dbfbff7b115d467b02679caf3c Mon Sep 17 00:00:00 2001 From: Vittorio Palmisano Date: Thu, 6 May 2021 12:06:34 +0200 Subject: [PATCH 38/57] Changed: server/README.md server/lib/config.ts --- server/README.md | 2 +- server/lib/config.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/server/README.md b/server/README.md index da9df7e3..55d49523 100644 --- a/server/README.md +++ b/server/README.md @@ -53,7 +53,7 @@ Look at the default `config/config.example.js` file for documentation. | routerScaleSize | Room size before spreading to a new router. | `"nat"` | ``40`` | | requestTimeout | Socket timeout value (ms). | `"nat"` | ``20000`` | | requestRetries | Socket retries when a timeout occurs. | `"nat"` | ``3`` | -| mediasoup.numWorkers | The number of Mediasoup workers to spawn. | `"nat"` | ``8`` | +| mediasoup.numWorkers | The number of Mediasoup workers to spawn. Defaults to the available CPUs count. | `"nat"` | ``8`` | | mediasoup.worker.logLevel | The Mediasoup log level. | `"string"` | ``"warn"`` | | mediasoup.worker.logTags | The Mediasoup log tags. | `"array"` | ``[ "info", "ice", "dtls", "rtp", "srtp", "rtcp"]`` | | mediasoup.worker.rtcMinPort | The Mediasoup start listening port number. | `"port"` | ``40000`` | diff --git a/server/lib/config.ts b/server/lib/config.ts index 4af92371..8d9cbb6f 100644 --- a/server/lib/config.ts +++ b/server/lib/config.ts @@ -191,7 +191,7 @@ const configSchema = convict({ mediasoup : { numWorkers : { - doc : 'The number of Mediasoup workers to spawn.', + doc : 'The number of Mediasoup workers to spawn. Defaults to the available CPUs count.', format : 'nat', default : Object.keys(cpus()).length }, From 7eb5921b61113a8afddc26ad285be7138cb78343 Mon Sep 17 00:00:00 2001 From: Vittorio Palmisano Date: Thu, 6 May 2021 12:12:01 +0200 Subject: [PATCH 39/57] removed unsed vars --- compose/config/edumeet-server-config.js | 28 +------------------------ 1 file changed, 1 insertion(+), 27 deletions(-) diff --git a/compose/config/edumeet-server-config.js b/compose/config/edumeet-server-config.js index 685fc5b7..1da42fb4 100644 --- a/compose/config/edumeet-server-config.js +++ b/compose/config/edumeet-server-config.js @@ -1,33 +1,7 @@ const os = require('os'); -// const fs = require('fs'); - -const userRoles = require('../userRoles'); - -import { - BYPASS_ROOM_LOCK, - BYPASS_LOBBY -} from '../access'; - -const { - CHANGE_ROOM_LOCK, - PROMOTE_PEER, - MODIFY_ROLE, - SEND_CHAT, - MODERATE_CHAT, - SHARE_AUDIO, - SHARE_VIDEO, - SHARE_SCREEN, - EXTRA_VIDEO, - SHARE_FILE, - MODERATE_FILES, - MODERATE_ROOM -} = require('../permissions'); - -// const AwaitQueue = require('awaitqueue'); -// const axios = require('axios'); // To gather ip address only on interface like eth0, ens0p3 -const ifaceWhiteListRegex = /^(eth.*)|(ens.*)|(tun.*)/ +const ifaceWhiteListRegex = /^(eth.*)|(ens.*)|(tun.*)|(wlp.*)/ function getListenIps() { let listenIP = []; From 19fd6d2629b1fb27dfb4545e447fdfd32bb86d29 Mon Sep 17 00:00:00 2001 From: Vittorio Palmisano Date: Thu, 6 May 2021 12:13:06 +0200 Subject: [PATCH 40/57] removed unsed vars --- server/gen-config-docs.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/server/gen-config-docs.ts b/server/gen-config-docs.ts index fb59eefc..b1663ff0 100644 --- a/server/gen-config-docs.ts +++ b/server/gen-config-docs.ts @@ -1,6 +1,5 @@ import { configDocs } from './lib/config'; import { writeFile } from 'fs/promises'; -import config from './config/config.example'; function formatJson(data) { From ea327d69c6573c72050be26e0dee11893e70957e Mon Sep 17 00:00:00 2001 From: Vittorio Palmisano Date: Thu, 6 May 2021 12:15:11 +0200 Subject: [PATCH 41/57] fixed config import path --- server/lib/Room.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/lib/Room.js b/server/lib/Room.js index 2b22616d..907ad800 100644 --- a/server/lib/Room.js +++ b/server/lib/Room.js @@ -29,7 +29,7 @@ const permissions = require('../permissions'), { MODERATE_ROOM } = permissions; -const config = require('../config/config'); +const { config } = require('./config'); const logger = new Logger('Room'); From 44b0a02571aa4701fb20c8c69b11cfd660f9284c Mon Sep 17 00:00:00 2001 From: Vittorio Palmisano Date: Thu, 6 May 2021 12:20:05 +0200 Subject: [PATCH 42/57] replaced config references --- server/lib/Room.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/server/lib/Room.js b/server/lib/Room.js index 907ad800..c9a4ffa3 100644 --- a/server/lib/Room.js +++ b/server/lib/Room.js @@ -674,7 +674,7 @@ class Room extends EventEmitter let turnServers; - if ('turnAPIURI' in config) + if (config.turnAPIURI) { try { @@ -697,13 +697,13 @@ class Room extends EventEmitter } catch (error) { - if ('backupTurnServers' in config) + if (config.backupTurnServers) turnServers = config.backupTurnServers; logger.error('_peerJoining() | error on REST turn [error:"%o"]', error); } } - else if ('backupTurnServers' in config) + else if (config.backupTurnServers) { turnServers = config.backupTurnServers; } From 88f484d294551ca8cbfd23035b3cfcace9e80c57 Mon Sep 17 00:00:00 2001 From: Vittorio Palmisano Date: Thu, 6 May 2021 12:31:25 +0200 Subject: [PATCH 43/57] replaced config references --- server/config/config.example.js | 51 +------------------- server/lib/Room.js | 2 +- server/lib/config.ts | 84 +++++++++++++++++++++++++++++++++ 3 files changed, 86 insertions(+), 51 deletions(-) diff --git a/server/config/config.example.js b/server/config/config.example.js index 0a925839..6794c85e 100644 --- a/server/config/config.example.js +++ b/server/config/config.example.js @@ -243,54 +243,5 @@ module.exports = { peer.email = userinfo.email; } - }, - // All users have the role "NORMAL" by default. Other roles need to be - // added in the "userMapping" function. The following accesses and - // permissions are arrays of roles. Roles can be changed in userRoles.js - // - // Example: - // [ userRoles.MODERATOR, userRoles.AUTHENTICATED ] - accessFromRoles : { - // The role(s) will gain access to the room - // even if it is locked (!) - [BYPASS_ROOM_LOCK] : [ userRoles.ADMIN ], - // The role(s) will gain access to the room without - // going into the lobby. If you want to restrict access to your - // server to only directly allow authenticated users, you could - // add the userRoles.AUTHENTICATED to the user in the userMapping - // function, and change to BYPASS_LOBBY : [ userRoles.AUTHENTICATED ] - [BYPASS_LOBBY] : [ userRoles.NORMAL ] - }, - permissionsFromRoles : { - // The role(s) have permission to lock/unlock a room - [CHANGE_ROOM_LOCK] : [ userRoles.MODERATOR ], - // The role(s) have permission to promote a peer from the lobby - [PROMOTE_PEER] : [ userRoles.NORMAL ], - // The role(s) have permission to give/remove other peers roles - [MODIFY_ROLE] : [ userRoles.NORMAL ], - // The role(s) have permission to send chat messages - [SEND_CHAT] : [ userRoles.NORMAL ], - // The role(s) have permission to moderate chat - [MODERATE_CHAT] : [ userRoles.MODERATOR ], - // The role(s) have permission to share audio - [SHARE_AUDIO] : [ userRoles.NORMAL ], - // The role(s) have permission to share video - [SHARE_VIDEO] : [ userRoles.NORMAL ], - // The role(s) have permission to share screen - [SHARE_SCREEN] : [ userRoles.NORMAL ], - // The role(s) have permission to produce extra video - [EXTRA_VIDEO] : [ userRoles.NORMAL ], - // The role(s) have permission to share files - [SHARE_FILE] : [ userRoles.NORMAL ], - // The role(s) have permission to moderate files - [MODERATE_FILES] : [ userRoles.MODERATOR ], - // The role(s) have permission to moderate room (e.g. kick user) - [MODERATE_ROOM] : [ userRoles.MODERATOR ] - }, - // Array of permissions. If no peer with the permission in question - // is in the room, all peers are permitted to do the action. The peers - // that are allowed because of this rule will not be able to do this - // action as soon as a peer with the permission joins. In this example - // everyone will be able to lock/unlock room until a MODERATOR joins. - allowWhenRoleMissing : [ CHANGE_ROOM_LOCK ] + } }; diff --git a/server/lib/Room.js b/server/lib/Room.js index c9a4ffa3..12a1f530 100644 --- a/server/lib/Room.js +++ b/server/lib/Room.js @@ -263,7 +263,7 @@ class Room extends EventEmitter this._queue = new AwaitQueue(); // Locked flag. - this._locked = config.roomsUnlocked && Array.isArray(config.roomsUnlocked) + this._locked = config.roomsUnlocked.length && !config.roomsUnlocked.includes(roomId); // if true: accessCode is a possibility to open the room diff --git a/server/lib/config.ts b/server/lib/config.ts index 8d9cbb6f..37c74b68 100644 --- a/server/lib/config.ts +++ b/server/lib/config.ts @@ -10,6 +10,28 @@ import Logger from './Logger'; const logger = new Logger('config'); +const userRoles = require('../userRoles'); + +const { + BYPASS_ROOM_LOCK, + BYPASS_LOBBY +} = require('../access'); + +const { + CHANGE_ROOM_LOCK, + PROMOTE_PEER, + MODIFY_ROLE, + SEND_CHAT, + MODERATE_CHAT, + SHARE_AUDIO, + SHARE_VIDEO, + SHARE_SCREEN, + EXTRA_VIDEO, + SHARE_FILE, + MODERATE_FILES, + MODERATE_ROOM +} = require('../permissions'); + // add parsers convict.addParser([ { extension: 'json', parse: JSON.parse }, @@ -346,6 +368,68 @@ const configSchema = convict({ format : String, default : '' } + }, + // User roles + // All users have the role "NORMAL" by default. Other roles need to be + // added in the "userMapping" function. The following accesses and + // permissions are arrays of roles. Roles can be changed in userRoles.js + // + // Example: + // [ userRoles.MODERATOR, userRoles.AUTHENTICATED ] + accessFromRoles : { + doc : 'User roles.', + format : '*', + default : { + // The role(s) will gain access to the room + // even if it is locked (!) + [BYPASS_ROOM_LOCK] : [ userRoles.ADMIN ], + // The role(s) will gain access to the room without + // going into the lobby. If you want to restrict access to your + // server to only directly allow authenticated users, you could + // add the userRoles.AUTHENTICATED to the user in the userMapping + // function, and change to BYPASS_LOBBY : [ userRoles.AUTHENTICATED ] + [BYPASS_LOBBY] : [ userRoles.NORMAL ] + } + }, + permissionsFromRoles : { + doc : 'User permissions from roles.', + format : '*', + default : { + // The role(s) have permission to lock/unlock a room + [CHANGE_ROOM_LOCK] : [ userRoles.MODERATOR ], + // The role(s) have permission to promote a peer from the lobby + [PROMOTE_PEER] : [ userRoles.NORMAL ], + // The role(s) have permission to give/remove other peers roles + [MODIFY_ROLE] : [ userRoles.NORMAL ], + // The role(s) have permission to send chat messages + [SEND_CHAT] : [ userRoles.NORMAL ], + // The role(s) have permission to moderate chat + [MODERATE_CHAT] : [ userRoles.MODERATOR ], + // The role(s) have permission to share audio + [SHARE_AUDIO] : [ userRoles.NORMAL ], + // The role(s) have permission to share video + [SHARE_VIDEO] : [ userRoles.NORMAL ], + // The role(s) have permission to share screen + [SHARE_SCREEN] : [ userRoles.NORMAL ], + // The role(s) have permission to produce extra video + [EXTRA_VIDEO] : [ userRoles.NORMAL ], + // The role(s) have permission to share files + [SHARE_FILE] : [ userRoles.NORMAL ], + // The role(s) have permission to moderate files + [MODERATE_FILES] : [ userRoles.MODERATOR ], + // The role(s) have permission to moderate room (e.g. kick user) + [MODERATE_ROOM] : [ userRoles.MODERATOR ] + } + }, + // Array of permissions. If no peer with the permission in question + // is in the room, all peers are permitted to do the action. The peers + // that are allowed because of this rule will not be able to do this + // action as soon as a peer with the permission joins. In this example + // everyone will be able to lock/unlock room until a MODERATOR joins. + allowWhenRoleMissing : { + doc : 'Allow when role missing.', + format : Array, + default : [ CHANGE_ROOM_LOCK ] } }); From 5f71f0af8bf7999ebac2791eeee84277d993cf00 Mon Sep 17 00:00:00 2001 From: Vittorio Palmisano Date: Thu, 6 May 2021 12:34:46 +0200 Subject: [PATCH 44/57] fixed user roles in config --- server/README.md | 3 +++ server/lib/config.ts | 16 +++++++--------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/server/README.md b/server/README.md index 55d49523..24bf7895 100644 --- a/server/README.md +++ b/server/README.md @@ -69,6 +69,9 @@ Look at the default `config/config.example.js` file for documentation. | prometheus.quiet | Include fewer labels in Prometheus logs. | `"boolean"` | ``false`` | | prometheus.period | The Prometheus exporter update period (seconds). | `"nat"` | ``15`` | | prometheus.secret | The Prometheus exporter authorization header: `Bearer ` required to allow scraping. | `"string"` | ``""`` | +| accessFromRoles | User roles. | `"*"` | ``{ "BYPASS_ROOM_LOCK": [ { "id": 2529, "label": "admin", "level": 50, "promotable": true } ], "BYPASS_LOBBY": [ { "id": 4261, "label": "normal", "level": 10, "promotable": false } ]}`` | +| permissionsFromRoles | User permissions from roles. | `"*"` | ``{ "CHANGE_ROOM_LOCK": [ { "id": 5337, "label": "moderator", "level": 40, "promotable": true } ], "PROMOTE_PEER": [ { "id": 4261, "label": "normal", "level": 10, "promotable": false } ], "MODIFY_ROLE": [ { "id": 4261, "label": "normal", "level": 10, "promotable": false } ], "SEND_CHAT": [ { "id": 4261, "label": "normal", "level": 10, "promotable": false } ], "MODERATE_CHAT": [ { "id": 5337, "label": "moderator", "level": 40, "promotable": true } ], "SHARE_AUDIO": [ { "id": 4261, "label": "normal", "level": 10, "promotable": false } ], "SHARE_VIDEO": [ { "id": 4261, "label": "normal", "level": 10, "promotable": false } ], "SHARE_SCREEN": [ { "id": 4261, "label": "normal", "level": 10, "promotable": false } ], "EXTRA_VIDEO": [ { "id": 4261, "label": "normal", "level": 10, "promotable": false } ], "SHARE_FILE": [ { "id": 4261, "label": "normal", "level": 10, "promotable": false } ], "MODERATE_FILES": [ { "id": 5337, "label": "moderator", "level": 40, "promotable": true } ], "MODERATE_ROOM": [ { "id": 5337, "label": "moderator", "level": 40, "promotable": true } ]}`` | +| allowWhenRoleMissing | Allow when role missing. | `"array"` | ``[ "CHANGE_ROOM_LOCK"]`` | --- diff --git a/server/lib/config.ts b/server/lib/config.ts index 37c74b68..e2d532f8 100644 --- a/server/lib/config.ts +++ b/server/lib/config.ts @@ -8,16 +8,12 @@ import { cpus } from 'os'; import Logger from './Logger'; -const logger = new Logger('config'); - -const userRoles = require('../userRoles'); - -const { +import * as userRoles from '../userRoles'; +import { BYPASS_ROOM_LOCK, BYPASS_LOBBY -} = require('../access'); - -const { +} from '../access'; +import { CHANGE_ROOM_LOCK, PROMOTE_PEER, MODIFY_ROLE, @@ -30,7 +26,9 @@ const { SHARE_FILE, MODERATE_FILES, MODERATE_ROOM -} = require('../permissions'); +} from '../permissions'; + +const logger = new Logger('config'); // add parsers convict.addParser([ From 868ab40c2562bfcd6ae5dbe875165c8493f25cd1 Mon Sep 17 00:00:00 2001 From: Vittorio Palmisano Date: Thu, 6 May 2021 12:43:13 +0200 Subject: [PATCH 45/57] re-added user roles to config example --- server/config/config.example.js | 51 ++++++++++++++++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/server/config/config.example.js b/server/config/config.example.js index 6794c85e..0a925839 100644 --- a/server/config/config.example.js +++ b/server/config/config.example.js @@ -243,5 +243,54 @@ module.exports = { peer.email = userinfo.email; } - } + }, + // All users have the role "NORMAL" by default. Other roles need to be + // added in the "userMapping" function. The following accesses and + // permissions are arrays of roles. Roles can be changed in userRoles.js + // + // Example: + // [ userRoles.MODERATOR, userRoles.AUTHENTICATED ] + accessFromRoles : { + // The role(s) will gain access to the room + // even if it is locked (!) + [BYPASS_ROOM_LOCK] : [ userRoles.ADMIN ], + // The role(s) will gain access to the room without + // going into the lobby. If you want to restrict access to your + // server to only directly allow authenticated users, you could + // add the userRoles.AUTHENTICATED to the user in the userMapping + // function, and change to BYPASS_LOBBY : [ userRoles.AUTHENTICATED ] + [BYPASS_LOBBY] : [ userRoles.NORMAL ] + }, + permissionsFromRoles : { + // The role(s) have permission to lock/unlock a room + [CHANGE_ROOM_LOCK] : [ userRoles.MODERATOR ], + // The role(s) have permission to promote a peer from the lobby + [PROMOTE_PEER] : [ userRoles.NORMAL ], + // The role(s) have permission to give/remove other peers roles + [MODIFY_ROLE] : [ userRoles.NORMAL ], + // The role(s) have permission to send chat messages + [SEND_CHAT] : [ userRoles.NORMAL ], + // The role(s) have permission to moderate chat + [MODERATE_CHAT] : [ userRoles.MODERATOR ], + // The role(s) have permission to share audio + [SHARE_AUDIO] : [ userRoles.NORMAL ], + // The role(s) have permission to share video + [SHARE_VIDEO] : [ userRoles.NORMAL ], + // The role(s) have permission to share screen + [SHARE_SCREEN] : [ userRoles.NORMAL ], + // The role(s) have permission to produce extra video + [EXTRA_VIDEO] : [ userRoles.NORMAL ], + // The role(s) have permission to share files + [SHARE_FILE] : [ userRoles.NORMAL ], + // The role(s) have permission to moderate files + [MODERATE_FILES] : [ userRoles.MODERATOR ], + // The role(s) have permission to moderate room (e.g. kick user) + [MODERATE_ROOM] : [ userRoles.MODERATOR ] + }, + // Array of permissions. If no peer with the permission in question + // is in the room, all peers are permitted to do the action. The peers + // that are allowed because of this rule will not be able to do this + // action as soon as a peer with the permission joins. In this example + // everyone will be able to lock/unlock room until a MODERATOR joins. + allowWhenRoleMissing : [ CHANGE_ROOM_LOCK ] }; From b1c3b74767971e67b0e2a372499f5482e113fb10 Mon Sep 17 00:00:00 2001 From: Vittorio Palmisano Date: Thu, 6 May 2021 13:40:12 +0200 Subject: [PATCH 46/57] changed edge supported version (#803) --- app/src/config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/config.ts b/app/src/config.ts index f33b0193..0694e489 100644 --- a/app/src/config.ts +++ b/app/src/config.ts @@ -57,7 +57,7 @@ const configSchema = convict({ 'internet explorer' : '>12', 'microsoft edge' : '>18' }, - 'microsoft edge' : '>=74', + 'microsoft edge' : '>18', 'safari' : '>12', 'firefox' : '>=60', 'chrome' : '>=74', From f547ccff0eda9b340c1227d60fb052ddfa64b615 Mon Sep 17 00:00:00 2001 From: Vittorio Palmisano Date: Fri, 7 May 2021 11:57:45 +0200 Subject: [PATCH 47/57] make listeningRedirectPort optional --- compose/config/edumeet-server-config.yaml | 2 +- server/server.js | 15 +++++++++------ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/compose/config/edumeet-server-config.yaml b/compose/config/edumeet-server-config.yaml index 37ef667d..a49d4f0e 100644 --- a/compose/config/edumeet-server-config.yaml +++ b/compose/config/edumeet-server-config.yaml @@ -3,6 +3,6 @@ redisOptions: port: 6379 listeningPort: 3443 -listeningRedirectPort: 8080 +listeningRedirectPort: 0 httpOnly: false trustProxy: '' diff --git a/server/server.js b/server/server.js index c0d22299..1c28f161 100755 --- a/server/server.js +++ b/server/server.js @@ -655,13 +655,16 @@ async function runHttpsServer() // https mainListener = spdy.createServer(tls, app); - // http - const redirectListener = http.createServer(app); + // http -> https redirect server + if (config.listeningRedirectPort) + { + const redirectListener = http.createServer(app); - if (config.listeningHost) - redirectListener.listen(config.listeningRedirectPort, config.listeningHost); - else - redirectListener.listen(config.listeningRedirectPort); + if (config.listeningHost) + redirectListener.listen(config.listeningRedirectPort, config.listeningHost); + else + redirectListener.listen(config.listeningRedirectPort); + } } // https or http From 98d817723406092d2e4e50cc97389c528e65b708 Mon Sep 17 00:00:00 2001 From: Vittorio Palmisano Date: Fri, 7 May 2021 12:03:55 +0200 Subject: [PATCH 48/57] added staticFilesCachePeriod config param --- server/lib/config.ts | 7 ++++++- server/server.js | 4 ++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/server/lib/config.ts b/server/lib/config.ts index e2d532f8..4cd9ce1f 100644 --- a/server/lib/config.ts +++ b/server/lib/config.ts @@ -163,7 +163,7 @@ const configSchema = convict({ default : 8443 }, listeningRedirectPort : { - doc : 'The HTTP listening port. Any HTTP request is redirected to HTTPS.', + doc : 'The HTTP server listening port used for redirecting any HTTP request to HTTPS. If 0, the redirect server is disabled.', format : 'port', default : 8080 }, @@ -177,6 +177,11 @@ const configSchema = convict({ format : String, default : '' }, + staticFilesCachePeriod : { + doc : 'The max-age in milliseconds for HTTP caching of static resources. This can also be a string accepted by the [ms module](https://www.npmjs.com/package/ms#readme).', + format : '*', + default : 0 + }, activateOnHostJoin : { doc : 'When true, the room will be open to all users since there are users in the room.', format : 'Boolean', diff --git a/server/server.js b/server/server.js index 1c28f161..b766b4ed 100755 --- a/server/server.js +++ b/server/server.js @@ -638,11 +638,11 @@ async function runHttpsServer() // Serve all files in the public folder as static files. app.use(express.static('public', { - maxAge : (config.staticFilesCachePeriod || 0) * 1000 + maxAge : config.staticFilesCachePeriod })); app.use((req, res) => res.sendFile(`${__dirname}/public/index.html`, { - maxAge : (config.staticFilesCachePeriod || 0) * 1000 + maxAge : config.staticFilesCachePeriod })); if (config.httpOnly === true) From 2f0465821336c6a5e03ede28691f06806f34c7fc Mon Sep 17 00:00:00 2001 From: Vittorio Palmisano Date: Fri, 7 May 2021 12:12:02 +0200 Subject: [PATCH 49/57] added staticFilesCachePeriod config param --- server/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server/README.md b/server/README.md index 24bf7895..bda4d3e5 100644 --- a/server/README.md +++ b/server/README.md @@ -44,9 +44,10 @@ Look at the default `config/config.example.js` file for documentation. | tls.key | SSL key path. | `"string"` | ``"./certs/mediasoup-demo.localhost.key.pem"`` | | listeningHost | The listening Host or IP address. If omitted listens on every IP. ("0.0.0.0" and "::"). | `"string"` | ``""`` | | listeningPort | The HTTPS listening port. | `"port"` | ``8443`` | -| listeningRedirectPort | The HTTP listening port. Any HTTP request is redirected to HTTPS. | `"port"` | ``8080`` | +| listeningRedirectPort | The HTTP server listening port used for redirecting any HTTP request to HTTPS. If 0, the redirect server is disabled. | `"port"` | ``8080`` | | httpOnly | Listens only on HTTP on listeningPort; listeningRedirectPort disabled. Use case: load balancer backend. | `"boolean"` | ``false`` | | trustProxy | WebServer/Express trust proxy config for httpOnly mode. More infos: [expressjs](https://expressjs.com/en/guide/behind-proxies.html), [proxy-addr](https://www.npmjs.com/package/proxy-addr) | `"string"` | ``""`` | +| staticFilesCachePeriod | The max-age in milliseconds for HTTP caching of static resources. This can also be a string accepted by the [ms module](https://www.npmjs.com/package/ms#readme). | `"*"` | ``0`` | | activateOnHostJoin | When true, the room will be open to all users since there are users in the room. | `"boolean"` | ``true`` | | roomsUnlocked | An array of rooms users can enter without waiting in the lobby. | `"array"` | ``[]`` | | maxUsersPerRoom | It defines how many users can join a single room. If not set, no limit is applied. | `"nat"` | ``0`` | From 3e617803946de8987fdaf3776a2f633576af93f3 Mon Sep 17 00:00:00 2001 From: Vittorio Palmisano Date: Fri, 7 May 2021 13:52:59 +0200 Subject: [PATCH 50/57] fixed prometheus expoter classes with linting --- compose/config/edumeet-server-config.yaml | 4 + server/lib/config.ts | 28 +- server/lib/metrics/aggregated.js | 552 +++++++++++----------- server/lib/metrics/default.js | 376 +++++++-------- server/lib/promExporter.js | 40 +- server/server.js | 12 +- 6 files changed, 515 insertions(+), 497 deletions(-) diff --git a/compose/config/edumeet-server-config.yaml b/compose/config/edumeet-server-config.yaml index a49d4f0e..0779a2a5 100644 --- a/compose/config/edumeet-server-config.yaml +++ b/compose/config/edumeet-server-config.yaml @@ -6,3 +6,7 @@ listeningPort: 3443 listeningRedirectPort: 0 httpOnly: false trustProxy: '' + +prometheus: + enabled: true + listen: 0.0.0.0 diff --git a/server/lib/config.ts b/server/lib/config.ts index 4cd9ce1f..201a08dc 100644 --- a/server/lib/config.ts +++ b/server/lib/config.ts @@ -335,39 +335,45 @@ const configSchema = convict({ }, // Prometheus exporter prometheus : { - deidentify : { - doc : 'De-identify IP addresses in Prometheus logs.', + enabled : { + doc : 'Enables the Prometheus metrics exporter.', format : 'Boolean', default : false }, listen : { - doc : 'Prometheus exporter listening address.', + doc : 'Prometheus metrics exporter listening address.', format : 'String', default : 'localhost' }, + port : { + doc : 'The Prometheus metrics exporter listening port.', + format : 'port', + default : 8889 + }, + // default metrics options + deidentify : { + doc : 'De-identify IP addresses in Prometheus logs.', + format : 'Boolean', + default : false + }, numeric : { doc : 'Show numeric IP addresses in Prometheus logs.', format : 'Boolean', default : false }, - port : { - doc : 'The Prometheus exporter listening port.', - format : 'port', - default : 8889 - }, quiet : { - doc : 'Include fewer labels in Prometheus logs.', + doc : 'Include fewer labels in Prometheus metrics.', format : 'Boolean', default : false }, // aggregated metrics options period : { - doc : 'The Prometheus exporter update period (seconds).', + doc : 'The Prometheus metrics exporter update period (seconds).', format : 'nat', default : 15 }, secret : { - doc : 'The Prometheus exporter authorization header: `Bearer ` required to allow scraping.', + doc : 'The Prometheus metrics exporter authorization header: `Bearer ` required to allow scraping.', format : String, default : '' } diff --git a/server/lib/metrics/aggregated.js b/server/lib/metrics/aggregated.js index 4124c589..2e9f1112 100644 --- a/server/lib/metrics/aggregated.js +++ b/server/lib/metrics/aggregated.js @@ -9,83 +9,88 @@ const logger = new Logger('metrics:aggregated'); // module.exports = function(workers, rooms_, peers_, config) { - const register = new promClient.Registry(); - promClient.collectDefaultMetrics({ prefix: 'mediasoup_', register }); - - const mediasoupStats = {}; - let mediasoupStatsUpdate = 0; + const register = new promClient.Registry(); - const formatStats = (s) => - { - return { - length: s.length || 0, - sum: s.sum || 0, - mean: s.amean() || 0, - stddev: s.stddev() || 0, - p25: s.percentile(25) || 0, - min: s.min || 0, - max: s.max || 0, - }; - }; + promClient.collectDefaultMetrics({ prefix: 'mediasoup_', register }); - const collectStats = async () => - { - const now = Date.now(); + const mediasoupStats = {}; + let mediasoupStatsUpdate = 0; - if (now - mediasoupStatsUpdate < config.period * 1000) - { - return; - } - mediasoupStatsUpdate = now; + const formatStats = (s) => + { + return { + length : s.length || 0, + sum : s.sum || 0, + mean : s.amean() || 0, + stddev : s.stddev() || 0, + p25 : s.percentile(25) || 0, + min : s.min || 0, + max : s.max || 0 + }; + }; - const start = process.hrtime(); + const collectStats = async () => + { + const now = Date.now(); - let workers_cpu = new Stats(); - let workers_memory = new Stats(); + if (now - mediasoupStatsUpdate < config.period * 1000) + { + return; + } + mediasoupStatsUpdate = now; - let rooms = new Stats(); - rooms.push(rooms_.size); + const start = process.hrtime(); - let peers = new Stats(); - peers.push(peers_.size); + const workersCpu = new Stats(); + const workersMemory = new Stats(); - // in - let video_bitrates_in = new Stats(); - let audio_bitrates_in = new Stats(); - let video_scores_in = new Stats(); - let audio_scores_in = new Stats(); + const rooms = new Stats(); - let packets_counts_in = new Stats(); - let packets_losts_in = new Stats(); - let packets_retransmitted_in = new Stats(); + rooms.push(rooms_.size); - // out - let video_bitrates_out = new Stats(); - let audio_bitrates_out = new Stats(); + const peers = new Stats(); - let round_trip_times_out = new Stats(); - let packets_counts_out = new Stats(); - let packets_losts_out = new Stats(); + peers.push(peers_.size); - let spatial_layers_out = new Stats(); - let temporal_layers_out = new Stats(); + // in + const videoBitratesIn = new Stats(); + const audioBitratesIn = new Stats(); + const videoScoresIn = new Stats(); + const audioScoresIn = new Stats(); - try { - // iterate workers - for (const worker of workers.values()) - { - // worker process stats - const workerStats = await pidusage(worker._pid); - workers_cpu.push(workerStats.cpu / 100); - workers_memory.push(workerStats.memory); - - // iterate routers - for (const router of worker._routers.values()) - { - // iterate transports - for (const transport of router._transports.values()) - { - /* let stats = []; + const packetsCountsIn = new Stats(); + const packetsLostsIn = new Stats(); + const packetsRetransmittedIn = new Stats(); + + // out + const videoBitratesOut = new Stats(); + const audioBitratesOut = new Stats(); + + const roundTripTimesOut = new Stats(); + const packetsCountsOut = new Stats(); + const packetsLostsOut = new Stats(); + + const spatialLayersOut = new Stats(); + const temporalLayersOut = new Stats(); + + try + { + // iterate workers + for (const worker of workers) + { + // worker process stats + const workerStats = await pidusage(worker.pid); + + workersCpu.push(workerStats.cpu / 100); + workersMemory.push(workerStats.memory); + + // iterate routers + for (const router of worker._routers.values()) + { + // iterate transports + for (const transport of router._transports.values()) + { + /* let stats = []; try { stats = await transport.getStats(); @@ -102,212 +107,225 @@ module.exports = function(workers, rooms_, peers_, config) } } */ - // iterate producers - for (const producer of transport._producers.values()) - { - let stats = []; - try - { - stats = await producer.getStats(); - } - catch(err) - { - logger.error('producer.getStats error:', err.message); - continue; - } - for (const s of stats) - { - if (s.type !== 'inbound-rtp') - { - continue; - } - if (s.kind === 'video') - { - video_bitrates_in.push(s.bitrate); - video_scores_in.push(s.score); - } - else if (s.kind === 'audio') - { - audio_bitrates_in.push(s.bitrate); - audio_scores_in.push(s.score); - } - packets_counts_in.push(s.packetCount || 0); - packets_losts_in.push(s.packetsLost || 0); - packets_retransmitted_in.push(s.packetsRetransmitted || 0); - } - } + // iterate producers + for (const producer of transport._producers.values()) + { + let stats = []; - // iterate consumers - for (const consumer of transport._consumers.values()) - { - if (consumer.type === 'pipe') - { - continue; - } - let stats = []; - try - { - stats = await consumer.getStats(); - } - catch(err) - { - logger.error('consumer.getStats error:', err.message); - continue; - } - for (const s of stats) - { - if(s.type !== 'outbound-rtp'){ - continue; - } - if (s.kind === 'video') - { - video_bitrates_out.push(s.bitrate || 0); - spatial_layers_out.push(consumer.currentLayers ? consumer.currentLayers.spatialLayer : 0); - temporal_layers_out.push(consumer.currentLayers ? consumer.currentLayers.temporalLayer : 0); - } - else if(s.kind === 'audio') - { - audio_bitrates_out.push(s.bitrate || 0); - } - round_trip_times_out.push(s.roundTripTime || 0); - packets_counts_out.push(s.packetCount || 0); - packets_losts_out.push(s.packetsLost || 0); - } - } - } - } - } - } - catch(err) - { - logger.error('collectStats error:', err.message); - } - finally - { - Object.assign(mediasoupStats, { - workers_cpu: formatStats(workers_cpu), - workers_memory: formatStats(workers_memory), - rooms: formatStats(rooms), - peers: formatStats(peers), - // in - video_bitrates_in: formatStats(video_bitrates_in), - video_scores_in: formatStats(video_scores_in), - audio_bitrates_in: formatStats(audio_bitrates_in), - audio_scores_in: formatStats(audio_scores_in), - packets_counts_in: formatStats(packets_counts_in), - packets_losts_in: formatStats(packets_losts_in), - packets_retransmitted_in: formatStats(packets_retransmitted_in), - // out - video_bitrates_out: formatStats(video_bitrates_out), - audio_bitrates_out: formatStats(audio_bitrates_out), - round_trip_times_out: formatStats(round_trip_times_out), - packets_counts_out: formatStats(packets_counts_out), - packets_losts_out: formatStats(packets_losts_out), - spatial_layers_out: formatStats(spatial_layers_out), - temporal_layers_out: formatStats(temporal_layers_out), - }); - } + try + { + stats = await producer.getStats(); + } + catch (err) + { + logger.error('producer.getStats error:', err.message); + continue; + } + for (const s of stats) + { + if (s.type !== 'inbound-rtp') + { + continue; + } + if (s.kind === 'video') + { + videoBitratesIn.push(s.bitrate); + videoScoresIn.push(s.score); + } + else if (s.kind === 'audio') + { + audioBitratesIn.push(s.bitrate); + audioScoresIn.push(s.score); + } + packetsCountsIn.push(s.packetCount || 0); + packetsLostsIn.push(s.packetsLost || 0); + packetsRetransmittedIn.push(s.packetsRetransmitted || 0); + } + } - const end = process.hrtime(start); - logger.info(`collectStats (elapsed: ${end[0] * 1e3 + end[1] * 1e-6} ms)`); - } + // iterate consumers + for (const consumer of transport._consumers.values()) + { + if (consumer.type === 'pipe') + { + continue; + } + let stats = []; - // mediasoup metrics - [ - { name: 'workers_count', statName: 'workers_cpu', statValue: 'length' }, - { name: 'workers_cpu', statName: 'workers_cpu', statValue: 'sum' }, - { name: 'workers_memory', statName: 'workers_memory', statValue: 'sum' }, - // - { name: 'rooms', statName: 'rooms', statValue: 'sum' }, - { name: 'peers', statName: 'peers', statValue: 'sum' }, - // audio in - { name: 'audio_in_count', statName: 'audio_bitrates_in', statValue: 'length' }, - // audio in bitrates - { name: 'audio_bitrates_in_sum', statName: 'audio_bitrates_in', statValue: 'sum' }, - { name: 'audio_bitrates_in_mean', statName: 'audio_bitrates_in', statValue: 'mean' }, - { name: 'audio_bitrates_in_min', statName: 'audio_bitrates_in', statValue: 'min' }, - { name: 'audio_bitrates_in_max', statName: 'audio_bitrates_in', statValue: 'max' }, - { name: 'audio_bitrates_in_p25', statName: 'audio_bitrates_in', statValue: 'p25' }, - // audio in scores - { name: 'audio_scores_in_mean', statName: 'audio_scores_in', statValue: 'mean' }, - { name: 'audio_scores_in_min', statName: 'audio_scores_in', statValue: 'min' }, - { name: 'audio_scores_in_max', statName: 'audio_scores_in', statValue: 'max' }, - { name: 'audio_scores_in_p25', statName: 'audio_scores_in', statValue: 'p25' }, - // video in - { name: 'video_in_count', statName: 'video_bitrates_in', statValue: 'length' }, - // video in bitrates - { name: 'video_bitrates_in_sum', statName: 'video_bitrates_in', statValue: 'sum' }, - { name: 'video_bitrates_in_mean', statName: 'video_bitrates_in', statValue: 'mean' }, - { name: 'video_bitrates_in_min', statName: 'video_bitrates_in', statValue: 'min' }, - { name: 'video_bitrates_in_max', statName: 'video_bitrates_in', statValue: 'max' }, - { name: 'video_bitrates_in_p25', statName: 'video_bitrates_in', statValue: 'p25' }, - // video in scores - { name: 'video_scores_in_mean', statName: 'video_scores_in', statValue: 'mean' }, - { name: 'video_scores_in_min', statName: 'video_scores_in', statValue: 'min' }, - { name: 'video_scores_in_max', statName: 'video_scores_in', statValue: 'max' }, - { name: 'video_scores_in_p25', statName: 'video_scores_in', statValue: 'p25' }, - // packets in - { name: 'packets_counts_in_sum', statName: 'packets_counts_in', statValue: 'sum' }, - { name: 'packets_counts_in_mean', statName: 'packets_counts_in', statValue: 'mean' }, - { name: 'packets_counts_in_min', statName: 'packets_counts_in', statValue: 'min' }, - { name: 'packets_counts_in_max', statName: 'packets_counts_in', statValue: 'max' }, - { name: 'packets_counts_in_p25', statName: 'packets_counts_in', statValue: 'p25' }, - { name: 'packets_losts_in_sum', statName: 'packets_losts_in', statValue: 'sum' }, - { name: 'packets_losts_in_mean', statName: 'packets_losts_in', statValue: 'mean' }, - { name: 'packets_losts_in_min', statName: 'packets_losts_in', statValue: 'min' }, - { name: 'packets_losts_in_max', statName: 'packets_losts_in', statValue: 'max' }, - { name: 'packets_losts_in_p25', statName: 'packets_losts_in', statValue: 'p25' }, - { name: 'packets_retransmitted_in_sum', statName: 'packets_retransmitted_in', statValue: 'sum' }, - { name: 'packets_retransmitted_in_mean', statName: 'packets_retransmitted_in', statValue: 'mean' }, - { name: 'packets_retransmitted_in_min', statName: 'packets_retransmitted_in', statValue: 'min' }, - { name: 'packets_retransmitted_in_max', statName: 'packets_retransmitted_in', statValue: 'max' }, - { name: 'packets_retransmitted_in_p25', statName: 'packets_retransmitted_in', statValue: 'p25' }, - // audio out - { name: 'audio_out_count', statName: 'audio_bitrates_out', statValue: 'length' }, - { name: 'audio_bitrates_out_sum', statName: 'audio_bitrates_out', statValue: 'sum' }, - { name: 'audio_bitrates_out_mean', statName: 'audio_bitrates_out', statValue: 'mean' }, - { name: 'audio_bitrates_out_min', statName: 'audio_bitrates_out', statValue: 'min' }, - { name: 'audio_bitrates_out_max', statName: 'audio_bitrates_out', statValue: 'max' }, - { name: 'audio_bitrates_out_p25', statName: 'audio_bitrates_out', statValue: 'p25' }, - // video out - { name: 'video_out_count', statName: 'video_bitrates_out', statValue: 'length' }, - { name: 'video_bitrates_out_sum', statName: 'video_bitrates_out', statValue: 'sum' }, - { name: 'video_bitrates_out_mean', statName: 'video_bitrates_out', statValue: 'mean' }, - { name: 'video_bitrates_out_min', statName: 'video_bitrates_out', statValue: 'min' }, - { name: 'video_bitrates_out_max', statName: 'video_bitrates_out', statValue: 'max' }, - { name: 'video_bitrates_out_p25', statName: 'video_bitrates_out', statValue: 'p25' }, - // sl - { name: 'spatial_layers_out_mean', statName: 'spatial_layers_out', statValue: 'mean' }, - { name: 'spatial_layers_out_min', statName: 'spatial_layers_out', statValue: 'min' }, - { name: 'spatial_layers_out_max', statName: 'spatial_layers_out', statValue: 'max' }, - { name: 'spatial_layers_out_p25', statName: 'spatial_layers_out', statValue: 'p25' }, - // tl - { name: 'temporal_layers_out_mean', statName: 'temporal_layers_out', statValue: 'mean' }, - { name: 'temporal_layers_out_min', statName: 'temporal_layers_out', statValue: 'min' }, - { name: 'temporal_layers_out_max', statName: 'temporal_layers_out', statValue: 'max' }, - { name: 'temporal_layers_out_p25', statName: 'temporal_layers_out', statValue: 'p25' }, - // rtt out - { name: 'round_trip_times_out_mean', statName: 'round_trip_times_out', statValue: 'mean' }, - { name: 'round_trip_times_out_min', statName: 'round_trip_times_out', statValue: 'min' }, - { name: 'round_trip_times_out_max', statName: 'round_trip_times_out', statValue: 'max' }, - { name: 'round_trip_times_out_p25', statName: 'round_trip_times_out', statValue: 'p25' }, + try + { + stats = await consumer.getStats(); + } + catch (err) + { + logger.error('consumer.getStats error:', err.message); + continue; + } + for (const s of stats) + { + if (s.type !== 'outbound-rtp') + { + continue; + } + if (s.kind === 'video') + { + videoBitratesOut.push(s.bitrate || 0); + spatialLayersOut.push(consumer.currentLayers + ? consumer.currentLayers.spatialLayer : 0); + temporalLayersOut.push(consumer.currentLayers + ? consumer.currentLayers.temporalLayer : 0); + } + else if (s.kind === 'audio') + { + audioBitratesOut.push(s.bitrate || 0); + } + roundTripTimesOut.push(s.roundTripTime || 0); + packetsCountsOut.push(s.packetCount || 0); + packetsLostsOut.push(s.packetsLost || 0); + } + } + } + } + } + } + catch (err) + { + logger.error('collectStats error:', err.message); + } + finally + { + Object.assign(mediasoupStats, { + workersCpu : formatStats(workersCpu), + workersMemory : formatStats(workersMemory), + rooms : formatStats(rooms), + peers : formatStats(peers), + // in + videoBitratesIn : formatStats(videoBitratesIn), + videoScoresIn : formatStats(videoScoresIn), + audioBitratesIn : formatStats(audioBitratesIn), + audioScoresIn : formatStats(audioScoresIn), + packetsCountsIn : formatStats(packetsCountsIn), + packetsLostsIn : formatStats(packetsLostsIn), + packetsRetransmittedIn : formatStats(packetsRetransmittedIn), + // out + videoBitratesOut : formatStats(videoBitratesOut), + audioBitratesOut : formatStats(audioBitratesOut), + roundTripTimesOut : formatStats(roundTripTimesOut), + packetsCountsOut : formatStats(packetsCountsOut), + packetsLostsOut : formatStats(packetsLostsOut), + spatialLayersOut : formatStats(spatialLayersOut), + temporalLayersOut : formatStats(temporalLayersOut) + }); + } - ].forEach(({ name, statName, statValue }) => { - new promClient.Gauge({ - name: `mediasoup_${name}`, - help: `MediaSoup ${name}`, - labelNames: [], - registers: [ register ], - async collect() - { - await collectStats(); - if (mediasoupStats[statName] !== undefined && mediasoupStats[statName][statValue] !== undefined) - { - this.set({}, mediasoupStats[statName][statValue]); - } - } - }); - }); + const end = process.hrtime(start); - return register; + logger.info(`collectStats (elapsed: ${(end[0] * 1e3) + (end[1] * 1e-6)} ms)`); + }; + + // mediasoup metrics + [ + { name: 'workers_count', statName: 'workersCpu', statValue: 'length' }, + { name: 'workers_cpu', statName: 'workersCpu', statValue: 'sum' }, + { name: 'workers_memory', statName: 'workersMemory', statValue: 'sum' }, + // + { name: 'rooms', statName: 'rooms', statValue: 'sum' }, + { name: 'peers', statName: 'peers', statValue: 'sum' }, + // audio in + { name: 'audio_in_count', statName: 'audioBitratesIn', statValue: 'length' }, + // audio in bitrates + { name: 'audio_bitrates_in_sum', statName: 'audioBitratesIn', statValue: 'sum' }, + { name: 'audio_bitrates_in_mean', statName: 'audioBitratesIn', statValue: 'mean' }, + { name: 'audio_bitrates_in_min', statName: 'audioBitratesIn', statValue: 'min' }, + { name: 'audio_bitrates_in_max', statName: 'audioBitratesIn', statValue: 'max' }, + { name: 'audio_bitrates_in_p25', statName: 'audioBitratesIn', statValue: 'p25' }, + // audio in scores + { name: 'audio_scores_in_mean', statName: 'audioScoresIn', statValue: 'mean' }, + { name: 'audio_scores_in_min', statName: 'audioScoresIn', statValue: 'min' }, + { name: 'audio_scores_in_max', statName: 'audioScoresIn', statValue: 'max' }, + { name: 'audio_scores_in_p25', statName: 'audioScoresIn', statValue: 'p25' }, + // video in + { name: 'video_in_count', statName: 'videoBitratesIn', statValue: 'length' }, + // video in bitrates + { name: 'video_bitrates_in_sum', statName: 'videoBitratesIn', statValue: 'sum' }, + { name: 'video_bitrates_in_mean', statName: 'videoBitratesIn', statValue: 'mean' }, + { name: 'video_bitrates_in_min', statName: 'videoBitratesIn', statValue: 'min' }, + { name: 'video_bitrates_in_max', statName: 'videoBitratesIn', statValue: 'max' }, + { name: 'video_bitrates_in_p25', statName: 'videoBitratesIn', statValue: 'p25' }, + // video in scores + { name: 'video_scores_in_mean', statName: 'videoScoresIn', statValue: 'mean' }, + { name: 'video_scores_in_min', statName: 'videoScoresIn', statValue: 'min' }, + { name: 'video_scores_in_max', statName: 'videoScoresIn', statValue: 'max' }, + { name: 'video_scores_in_p25', statName: 'videoScoresIn', statValue: 'p25' }, + // packets in + { name: 'packets_counts_in_sum', statName: 'packetsCountsIn', statValue: 'sum' }, + { name: 'packets_counts_in_mean', statName: 'packetsCountsIn', statValue: 'mean' }, + { name: 'packets_counts_in_min', statName: 'packetsCountsIn', statValue: 'min' }, + { name: 'packets_counts_in_max', statName: 'packetsCountsIn', statValue: 'max' }, + { name: 'packets_counts_in_p25', statName: 'packetsCountsIn', statValue: 'p25' }, + { name: 'packets_losts_in_sum', statName: 'packetsLostsIn', statValue: 'sum' }, + { name: 'packets_losts_in_mean', statName: 'packetsLostsIn', statValue: 'mean' }, + { name: 'packets_losts_in_min', statName: 'packetsLostsIn', statValue: 'min' }, + { name: 'packets_losts_in_max', statName: 'packetsLostsIn', statValue: 'max' }, + { name: 'packets_losts_in_p25', statName: 'packetsLostsIn', statValue: 'p25' }, + { name: 'packets_retransmitted_in_sum', statName: 'packetsRetransmittedIn', statValue: 'sum' }, + { name: 'packets_retransmitted_in_mean', statName: 'packetsRetransmittedIn', statValue: 'mean' }, + { name: 'packets_retransmitted_in_min', statName: 'packetsRetransmittedIn', statValue: 'min' }, + { name: 'packets_retransmitted_in_max', statName: 'packetsRetransmittedIn', statValue: 'max' }, + { name: 'packets_retransmitted_in_p25', statName: 'packetsRetransmittedIn', statValue: 'p25' }, + // audio out + { name: 'audio_out_count', statName: 'audioBitratesOut', statValue: 'length' }, + { name: 'audio_bitrates_out_sum', statName: 'audioBitratesOut', statValue: 'sum' }, + { name: 'audio_bitrates_out_mean', statName: 'audioBitratesOut', statValue: 'mean' }, + { name: 'audio_bitrates_out_min', statName: 'audioBitratesOut', statValue: 'min' }, + { name: 'audio_bitrates_out_max', statName: 'audioBitratesOut', statValue: 'max' }, + { name: 'audio_bitrates_out_p25', statName: 'audioBitratesOut', statValue: 'p25' }, + // video out + { name: 'video_out_count', statName: 'videoBitratesOut', statValue: 'length' }, + { name: 'video_bitrates_out_sum', statName: 'videoBitratesOut', statValue: 'sum' }, + { name: 'video_bitrates_out_mean', statName: 'videoBitratesOut', statValue: 'mean' }, + { name: 'video_bitrates_out_min', statName: 'videoBitratesOut', statValue: 'min' }, + { name: 'video_bitrates_out_max', statName: 'videoBitratesOut', statValue: 'max' }, + { name: 'video_bitrates_out_p25', statName: 'videoBitratesOut', statValue: 'p25' }, + // sl + { name: 'spatial_layers_out_mean', statName: 'spatialLayersOut', statValue: 'mean' }, + { name: 'spatial_layers_out_min', statName: 'spatialLayersOut', statValue: 'min' }, + { name: 'spatial_layers_out_max', statName: 'spatialLayersOut', statValue: 'max' }, + { name: 'spatial_layers_out_p25', statName: 'spatialLayersOut', statValue: 'p25' }, + // tl + { name: 'temporal_layers_out_mean', statName: 'temporalLayersOut', statValue: 'mean' }, + { name: 'temporal_layers_out_min', statName: 'temporalLayersOut', statValue: 'min' }, + { name: 'temporal_layers_out_max', statName: 'temporalLayersOut', statValue: 'max' }, + { name: 'temporal_layers_out_p25', statName: 'temporalLayersOut', statValue: 'p25' }, + // rtt out + { name: 'round_trip_times_out_mean', statName: 'roundTripTimesOut', statValue: 'mean' }, + { name: 'round_trip_times_out_min', statName: 'roundTripTimesOut', statValue: 'min' }, + { name: 'round_trip_times_out_max', statName: 'roundTripTimesOut', statValue: 'max' }, + { name: 'round_trip_times_out_p25', statName: 'roundTripTimesOut', statValue: 'p25' } + + ].forEach(({ name, statName, statValue }) => + { + // eslint-disable-next-line no-new + new promClient.Gauge({ + name : `mediasoup_${name}`, + help : `MediaSoup ${name}`, + labelNames : [], + registers : [ register ], + async collect() + { + await collectStats(); + if (mediasoupStats[statName] !== undefined + && mediasoupStats[statName][statValue] !== undefined) + { + this.set({}, mediasoupStats[statName][statValue]); + } + else + { + logger.warn(`${statName}.${statValue} not found`); + } + } + }); + }); + + return register; }; diff --git a/server/lib/metrics/default.js b/server/lib/metrics/default.js index 4e9feeed..2062a61c 100644 --- a/server/lib/metrics/default.js +++ b/server/lib/metrics/default.js @@ -19,218 +19,218 @@ const metadata = { module.exports = async function(workers, rooms, peers, registry, config) { - const newMetrics = function(subsystem) - { - const namespace = 'mediasoup'; - const metrics = new Map(); + const newMetrics = function(subsystem) + { + const namespace = 'mediasoup'; + const metrics = new Map(); - for (const key in metadata) - { - if (Object.prototype.hasOwnProperty.call(metadata, key)) - { - const value = metadata[key]; - const name = key.split(/(?=[A-Z])/).join('_') - .toLowerCase(); - const unit = value.unit; - const metricType = value.metricType; - let s = `${namespace}_${subsystem}_${name}`; + for (const key in metadata) + { + if (Object.prototype.hasOwnProperty.call(metadata, key)) + { + const value = metadata[key]; + const name = key.split(/(?=[A-Z])/).join('_') + .toLowerCase(); + const unit = value.unit; + const metricType = value.metricType; + let s = `${namespace}_${subsystem}_${name}`; - if (unit) - { - s += `_${unit}`; - } - const m = new metricType({ - name : s, help : `${subsystem}.${key}`, labelNames : labelNames, registers : [ registry ] }); + if (unit) + { + s += `_${unit}`; + } + const m = new metricType({ + name : s, help : `${subsystem}.${key}`, labelNames : labelNames, registers : [ registry ] }); - metrics.set(key, m); - } - } + metrics.set(key, m); + } + } - return metrics; - }; + return metrics; + }; - const commonLabels = function(both, fn) - { - for (const roomId of rooms.keys()) - { - for (const [ peerId, peer ] of peers) - { - if (fn(peer)) - { - const displayName = peer._displayName; - const userAgent = peer._socket.client.request.headers['user-agent']; - const kind = both.kind; - const codec = both.rtpParameters.codecs[0].mimeType.split('/')[1]; + const commonLabels = function(both, fn) + { + for (const roomId of rooms.keys()) + { + for (const [ peerId, peer ] of peers) + { + if (fn(peer)) + { + const displayName = peer._displayName; + const userAgent = peer._socket.client.request.headers['user-agent']; + const kind = both.kind; + const codec = both.rtpParameters.codecs[0].mimeType.split('/')[1]; - return { roomId, peerId, displayName, userAgent, kind, codec }; - } - } - } - throw new Error('cannot find common labels'); - }; + return { roomId, peerId, displayName, userAgent, kind, codec }; + } + } + } + throw new Error('cannot find common labels'); + }; - const addr = async function(ip, port) - { - if (config.deidentify) - { - const a = ip.split('.'); + const addr = async function(ip, port) + { + if (config.deidentify) + { + const a = ip.split('.'); - for (let i = 0; i < a.length - 2; i++) - { - a[i] = 'xx'; - } + for (let i = 0; i < a.length - 2; i++) + { + a[i] = 'xx'; + } - return `${a.join('.')}:${port}`; - } - else if (config.numeric) - { - return `${ip}:${port}`; - } - else - { - try - { - const a = await resolver.reverse(ip); + return `${a.join('.')}:${port}`; + } + else if (config.numeric) + { + return `${ip}:${port}`; + } + else + { + try + { + const a = await resolver.reverse(ip); - ip = a[0]; - } - catch (err) - { - logger.error(`reverse DNS query failed: ${ip} ${err.code}`); - } + ip = a[0]; + } + catch (err) + { + logger.error(`reverse DNS query failed: ${ip} ${err.code}`); + } - return `${ip}:${port}`; - } - }; + return `${ip}:${port}`; + } + }; - const quiet = function(s) - { - return config.quiet ? '' : s; - }; + const quiet = function(s) + { + return config.quiet ? '' : s; + }; - const setValue = function(key, m, labels, v) - { - logger.debug(`setValue key=${key} v=${v}`); - switch (metadata[key].metricType) - { - case prom.Counter: - m.inc(labels, v); - break; - case prom.Gauge: - m.set(labels, v); - break; - default: - throw new Error(`unexpected metric: ${m}`); - } - }; + const setValue = function(key, m, labels, v) + { + logger.debug(`setValue key=${key} v=${v}`); + switch (metadata[key].metricType) + { + case prom.Counter: + m.inc(labels, v); + break; + case prom.Gauge: + m.set(labels, v); + break; + default: + throw new Error(`unexpected metric: ${m}`); + } + }; - logger.debug('collect'); - const mRooms = new prom.Gauge({ name: 'edumeet_rooms', help: '#rooms', registers: [ registry ] }); + logger.debug('collect'); + const mRooms = new prom.Gauge({ name: 'edumeet_rooms', help: '#rooms', registers: [ registry ] }); - mRooms.set(rooms.size); - const mPeers = new prom.Gauge({ name: 'edumeet_peers', help: '#peers', labelNames: [ 'room_id' ], registers: [ registry ] }); + mRooms.set(rooms.size); + const mPeers = new prom.Gauge({ name: 'edumeet_peers', help: '#peers', labelNames: [ 'room_id' ], registers: [ registry ] }); - for (const [ roomId, room ] of rooms) - { - mPeers.labels(roomId).set(Object.keys(room._peers).length); - } + for (const [ roomId, room ] of rooms) + { + mPeers.labels(roomId).set(Object.keys(room._peers).length); + } - const mConsumer = newMetrics('consumer'); - const mProducer = newMetrics('producer'); + const mConsumer = newMetrics('consumer'); + const mProducer = newMetrics('producer'); - for (const [ pid, worker ] of workers) - { - logger.debug(`visiting worker ${pid}`); - for (const router of worker._routers) - { - logger.debug(`visiting router ${router.id}`); - for (const [ transportId, transport ] of router._transports) - { - logger.debug(`visiting transport ${transportId}`); - const transportJson = await transport.dump(); + for (const worker of workers) + { + logger.debug(`visiting worker ${worker.pid}`); + for (const router of worker._routers) + { + logger.debug(`visiting router ${router.id}`); + for (const [ transportId, transport ] of router._transports) + { + logger.debug(`visiting transport ${transportId}`); + const transportJson = await transport.dump(); - if (transportJson.iceState != 'completed') - { - logger.debug(`skipping transport ${transportId}}: ${transportJson.iceState}`); - continue; - } - const iceSelectedTuple = transportJson.iceSelectedTuple; - const proto = iceSelectedTuple.protocol; - const localAddr = await addr(iceSelectedTuple.localIp, - iceSelectedTuple.localPort); - const remoteAddr = await addr(iceSelectedTuple.remoteIp, - iceSelectedTuple.remotePort); + if (transportJson.iceState != 'completed') + { + logger.debug(`skipping transport ${transportId}}: ${transportJson.iceState}`); + continue; + } + const iceSelectedTuple = transportJson.iceSelectedTuple; + const proto = iceSelectedTuple.protocol; + const localAddr = await addr(iceSelectedTuple.localIp, + iceSelectedTuple.localPort); + const remoteAddr = await addr(iceSelectedTuple.remoteIp, + iceSelectedTuple.remotePort); - for (const [ producerId, producer ] of transport._producers) - { - logger.debug(`visiting producer ${producerId}`); - const { roomId, peerId, displayName, userAgent, kind, codec } = + for (const [ producerId, producer ] of transport._producers) + { + logger.debug(`visiting producer ${producerId}`); + const { roomId, peerId, displayName, userAgent, kind, codec } = commonLabels(producer, (peer) => peer._producers.has(producerId)); - const a = await producer.getStats(); + const a = await producer.getStats(); - for (const x of a) - { - const type = x.type; - const labels = { - 'pid' : pid, - 'room_id' : roomId, - 'peer_id' : peerId, - 'display_name' : displayName, - 'user_agent' : userAgent, - 'transport_id' : quiet(transportId), - 'proto' : proto, - 'local_addr' : localAddr, - 'remote_addr' : remoteAddr, - 'id' : quiet(producerId), - 'kind' : kind, - 'codec' : codec, - 'type' : type - }; + for (const x of a) + { + const type = x.type; + const labels = { + 'pid' : worker.pid, + 'room_id' : roomId, + 'peer_id' : peerId, + 'display_name' : displayName, + 'user_agent' : userAgent, + 'transport_id' : quiet(transportId), + 'proto' : proto, + 'local_addr' : localAddr, + 'remote_addr' : remoteAddr, + 'id' : quiet(producerId), + 'kind' : kind, + 'codec' : codec, + 'type' : type + }; - for (const [ key, m ] of mProducer) - { - setValue(key, m, labels, x[key]); - } - } - } - for (const [ consumerId, consumer ] of transport._consumers) - { - logger.debug(`visiting consumer ${consumerId}`); - const { roomId, peerId, displayName, userAgent, kind, codec } = + for (const [ key, m ] of mProducer) + { + setValue(key, m, labels, x[key]); + } + } + } + for (const [ consumerId, consumer ] of transport._consumers) + { + logger.debug(`visiting consumer ${consumerId}`); + const { roomId, peerId, displayName, userAgent, kind, codec } = commonLabels(consumer, (peer) => peer._consumers.has(consumerId)); - const a = await consumer.getStats(); + const a = await consumer.getStats(); - for (const x of a) - { - if (x.type == 'inbound-rtp') - { - continue; - } - const type = x.type; - const labels = - { - 'pid' : pid, - 'room_id' : roomId, - 'peer_id' : peerId, - 'display_name' : displayName, - 'user_agent' : userAgent, - 'transport_id' : quiet(transportId), - 'proto' : proto, - 'local_addr' : localAddr, - 'remote_addr' : remoteAddr, - 'id' : quiet(consumerId), - 'kind' : kind, - 'codec' : codec, - 'type' : type - }; + for (const x of a) + { + if (x.type == 'inbound-rtp') + { + continue; + } + const type = x.type; + const labels = + { + 'pid' : worker.pid, + 'room_id' : roomId, + 'peer_id' : peerId, + 'display_name' : displayName, + 'user_agent' : userAgent, + 'transport_id' : quiet(transportId), + 'proto' : proto, + 'local_addr' : localAddr, + 'remote_addr' : remoteAddr, + 'id' : quiet(consumerId), + 'kind' : kind, + 'codec' : codec, + 'type' : type + }; - for (const [ key, m ] of mConsumer) - { - setValue(key, m, labels, x[key]); - } - } - } - } - } - } + for (const [ key, m ] of mConsumer) + { + setValue(key, m, labels, x[key]); + } + } + } + } + } + } }; \ No newline at end of file diff --git a/server/lib/promExporter.js b/server/lib/promExporter.js index fb8ba2ef..fa007622 100644 --- a/server/lib/promExporter.js +++ b/server/lib/promExporter.js @@ -8,28 +8,18 @@ const collectDefaultMetrics = require('./metrics/default'); const RegisterAggregated = require('./metrics/aggregated'); const logger = new Logger('promClient'); -const workers = new Map(); -module.exports = async function(rooms, peers, config) +import { config } from './config'; + +module.exports = async function(workers, rooms, peers) { try { - logger.debug(`config.deidentify=${config.deidentify}`); - logger.debug(`config.listen=${config.listen}`); - logger.debug(`config.numeric=${config.numeric}`); - logger.debug(`config.port=${config.port}`); - logger.debug(`config.quiet=${config.quiet}`); - - mediasoup.observer.on('newworker', (worker) => - { - logger.debug(`observing newworker ${worker.pid} #${workers.size}`); - workers.set(worker.pid, worker); - worker.observer.on('close', () => - { - logger.debug(`observing close worker ${worker.pid} #${workers.size - 1}`); - workers.delete(worker.pid); - }); - }); + logger.debug(`config.prometheus.deidentify=${config.prometheus.deidentify}`); + logger.debug(`config.prometheus.listen=${config.prometheus.listen}`); + logger.debug(`config.prometheus.numeric=${config.prometheus.numeric}`); + logger.debug(`config.prometheus.port=${config.prometheus.port}`); + logger.debug(`config.prometheus.quiet=${config.prometheus.quiet}`); const app = express(); @@ -39,7 +29,7 @@ module.exports = async function(rooms, peers, config) logger.debug(`GET ${req.originalUrl}`); const registry = new promClient.Registry(); - await collectDefaultMetrics(workers, rooms, peers, registry, config); + await collectDefaultMetrics(workers, rooms, peers, registry, config.prometheus); res.set('Content-Type', registry.contentType); const data = await registry.metrics(); @@ -47,26 +37,26 @@ module.exports = async function(rooms, peers, config) }); // aggregated register - const registerAggregated = RegisterAggregated(workers, rooms, peers, config); + const registerAggregated = RegisterAggregated(workers, rooms, peers, config.prometheus); app.get('/metrics', async (req, res) => { logger.debug(`GET ${req.originalUrl}`); - if (config.secret && req.headers.authorization !== 'Bearer ' + config.secret) + if (config.prometheus.secret && req.headers.authorization !== `Bearer ${ config.prometheus.secret}`) { - logger.error(`Invalid authorization header`); + logger.error('Invalid authorization header'); + return res.status(401).end(); } - + res.set('Content-Type', registerAggregated.contentType); const data = await registerAggregated.metrics(); res.end(data); }); - const server = app.listen(config.port || 8889, - config.listen || undefined, () => + const server = app.listen(config.prometheus.port, config.prometheus.listen, () => { const address = server.address(); diff --git a/server/server.js b/server/server.js index b766b4ed..e5c975f6 100755 --- a/server/server.js +++ b/server/server.js @@ -146,12 +146,6 @@ async function run() // Open the interactive server. await interactiveServer(rooms, peers); - // start Prometheus exporter - if (config.prometheus) - { - await promExporter(rooms, peers, config.prometheus); - } - if (typeof (config.auth) === 'undefined') { logger.warn('Auth is not configured properly!'); @@ -167,6 +161,12 @@ async function run() // Run HTTPS server. await runHttpsServer(); + // start Prometheus exporter + if (config.prometheus.enabled) + { + await promExporter(mediasoupWorkers, rooms, peers); + } + // Run WebSocketServer. await runWebSocketServer(); From 60ae01b58bbc7b33b9301d5cf215427bda241f17 Mon Sep 17 00:00:00 2001 From: Vittorio Palmisano Date: Fri, 7 May 2021 13:53:49 +0200 Subject: [PATCH 51/57] fixed prometheus expoter classes with linting --- server/lib/promExporter.js | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/server/lib/promExporter.js b/server/lib/promExporter.js index fa007622..6f4ed096 100644 --- a/server/lib/promExporter.js +++ b/server/lib/promExporter.js @@ -1,7 +1,6 @@ import Logger from './Logger'; const express = require('express'); -const mediasoup = require('mediasoup'); const promClient = require('prom-client'); const collectDefaultMetrics = require('./metrics/default'); @@ -29,7 +28,8 @@ module.exports = async function(workers, rooms, peers) logger.debug(`GET ${req.originalUrl}`); const registry = new promClient.Registry(); - await collectDefaultMetrics(workers, rooms, peers, registry, config.prometheus); + await collectDefaultMetrics( + workers, rooms, peers, registry, config.prometheus); res.set('Content-Type', registry.contentType); const data = await registry.metrics(); @@ -37,13 +37,15 @@ module.exports = async function(workers, rooms, peers) }); // aggregated register - const registerAggregated = RegisterAggregated(workers, rooms, peers, config.prometheus); + const registerAggregated = RegisterAggregated( + workers, rooms, peers, config.prometheus); app.get('/metrics', async (req, res) => { logger.debug(`GET ${req.originalUrl}`); - if (config.prometheus.secret && req.headers.authorization !== `Bearer ${ config.prometheus.secret}`) + if (config.prometheus.secret + && req.headers.authorization !== `Bearer ${ config.prometheus.secret}`) { logger.error('Invalid authorization header'); @@ -57,11 +59,11 @@ module.exports = async function(workers, rooms, peers) }); const server = app.listen(config.prometheus.port, config.prometheus.listen, () => - { - const address = server.address(); + { + const address = server.address(); - logger.info(`listening ${address.address}:${address.port}`); - }); + logger.info(`listening ${address.address}:${address.port}`); + }); } catch (err) { From 086adbd207efb30b1fa9d175e0e59282cc295f83 Mon Sep 17 00:00:00 2001 From: Vittorio Palmisano Date: Fri, 7 May 2021 13:54:42 +0200 Subject: [PATCH 52/57] updated docs --- server/README.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/server/README.md b/server/README.md index bda4d3e5..8962aca4 100644 --- a/server/README.md +++ b/server/README.md @@ -63,13 +63,14 @@ Look at the default `config/config.example.js` file for documentation. | mediasoup.webRtcTransport.listenIps | The Mediasoup listen IPs. [TransportListenIp](https://mediasoup.org/documentation/v3/mediasoup/api/#TransportListenIp) | `"array"` | ``[ { "ip": "0.0.0.0", "announcedIp": null }]`` | | mediasoup.webRtcTransport.initialAvailableOutgoingBitrate | The Mediasoup initial available outgoing bitrate (in bps). [WebRtcTransportOptions](https://mediasoup.org/documentation/v3/mediasoup/api/#WebRtcTransportOptions) | `"nat"` | ``1000000`` | | mediasoup.webRtcTransport.maxIncomingBitrate | The Mediasoup maximum incoming bitrate for each transport. (in bps). [setMaxIncomingBitrate](https://mediasoup.org/documentation/v3/mediasoup/api/#transport-setMaxIncomingBitrate) | `"nat"` | ``1500000`` | +| prometheus.enabled | Enables the Prometheus metrics exporter. | `"boolean"` | ``false`` | +| prometheus.listen | Prometheus metrics exporter listening address. | `"string"` | ``"localhost"`` | +| prometheus.port | The Prometheus metrics exporter listening port. | `"port"` | ``8889`` | | prometheus.deidentify | De-identify IP addresses in Prometheus logs. | `"boolean"` | ``false`` | -| prometheus.listen | Prometheus exporter listening address. | `"string"` | ``"localhost"`` | | prometheus.numeric | Show numeric IP addresses in Prometheus logs. | `"boolean"` | ``false`` | -| prometheus.port | The Prometheus exporter listening port. | `"port"` | ``8889`` | -| prometheus.quiet | Include fewer labels in Prometheus logs. | `"boolean"` | ``false`` | -| prometheus.period | The Prometheus exporter update period (seconds). | `"nat"` | ``15`` | -| prometheus.secret | The Prometheus exporter authorization header: `Bearer ` required to allow scraping. | `"string"` | ``""`` | +| prometheus.quiet | Include fewer labels in Prometheus metrics. | `"boolean"` | ``false`` | +| prometheus.period | The Prometheus metrics exporter update period (seconds). | `"nat"` | ``15`` | +| prometheus.secret | The Prometheus metrics exporter authorization header: `Bearer ` required to allow scraping. | `"string"` | ``""`` | | accessFromRoles | User roles. | `"*"` | ``{ "BYPASS_ROOM_LOCK": [ { "id": 2529, "label": "admin", "level": 50, "promotable": true } ], "BYPASS_LOBBY": [ { "id": 4261, "label": "normal", "level": 10, "promotable": false } ]}`` | | permissionsFromRoles | User permissions from roles. | `"*"` | ``{ "CHANGE_ROOM_LOCK": [ { "id": 5337, "label": "moderator", "level": 40, "promotable": true } ], "PROMOTE_PEER": [ { "id": 4261, "label": "normal", "level": 10, "promotable": false } ], "MODIFY_ROLE": [ { "id": 4261, "label": "normal", "level": 10, "promotable": false } ], "SEND_CHAT": [ { "id": 4261, "label": "normal", "level": 10, "promotable": false } ], "MODERATE_CHAT": [ { "id": 5337, "label": "moderator", "level": 40, "promotable": true } ], "SHARE_AUDIO": [ { "id": 4261, "label": "normal", "level": 10, "promotable": false } ], "SHARE_VIDEO": [ { "id": 4261, "label": "normal", "level": 10, "promotable": false } ], "SHARE_SCREEN": [ { "id": 4261, "label": "normal", "level": 10, "promotable": false } ], "EXTRA_VIDEO": [ { "id": 4261, "label": "normal", "level": 10, "promotable": false } ], "SHARE_FILE": [ { "id": 4261, "label": "normal", "level": 10, "promotable": false } ], "MODERATE_FILES": [ { "id": 5337, "label": "moderator", "level": 40, "promotable": true } ], "MODERATE_ROOM": [ { "id": 5337, "label": "moderator", "level": 40, "promotable": true } ]}`` | | allowWhenRoleMissing | Allow when role missing. | `"array"` | ``[ "CHANGE_ROOM_LOCK"]`` | From a19cc4ad86a2db5ef878d6f0bc8e209bc5b8d3eb Mon Sep 17 00:00:00 2001 From: Vittorio Palmisano Date: Fri, 14 May 2021 14:04:45 +0200 Subject: [PATCH 53/57] adjusted docs --- server/lib/config.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/lib/config.ts b/server/lib/config.ts index 201a08dc..40b1768a 100644 --- a/server/lib/config.ts +++ b/server/lib/config.ts @@ -153,9 +153,9 @@ const configSchema = convict({ } }, listeningHost : { - doc : 'The listening Host or IP address. If omitted listens on every IP. ("0.0.0.0" and "::").', + doc : 'The listening Host or IP address.', format : String, - default : '' + default : '0.0.0.0' }, listeningPort : { doc : 'The HTTPS listening port.', From b6bc72168e1a0770932e412b1df61fa6e790b282 Mon Sep 17 00:00:00 2001 From: Vittorio Palmisano Date: Fri, 14 May 2021 14:06:07 +0200 Subject: [PATCH 54/57] adjusted docs --- server/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/README.md b/server/README.md index 8962aca4..65cad0d3 100644 --- a/server/README.md +++ b/server/README.md @@ -42,7 +42,7 @@ Look at the default `config/config.example.js` file for documentation. | cookieName | Session cookie name. | `"string"` | ``"edumeet.sid"`` | | tls.cert | SSL certificate path. | `"string"` | ``"./certs/mediasoup-demo.localhost.cert.pem"`` | | tls.key | SSL key path. | `"string"` | ``"./certs/mediasoup-demo.localhost.key.pem"`` | -| listeningHost | The listening Host or IP address. If omitted listens on every IP. ("0.0.0.0" and "::"). | `"string"` | ``""`` | +| listeningHost | The listening Host or IP address. | `"string"` | ``"0.0.0.0"`` | | listeningPort | The HTTPS listening port. | `"port"` | ``8443`` | | listeningRedirectPort | The HTTP server listening port used for redirecting any HTTP request to HTTPS. If 0, the redirect server is disabled. | `"port"` | ``8080`` | | httpOnly | Listens only on HTTP on listeningPort; listeningRedirectPort disabled. Use case: load balancer backend. | `"boolean"` | ``false`` | From 8f1330a77d4a12bb0e687f248edee13c5a7270f1 Mon Sep 17 00:00:00 2001 From: toto6038 <50100922+toto6038@users.noreply.github.com> Date: Tue, 25 May 2021 14:33:02 +0800 Subject: [PATCH 55/57] Update tw.json The existing version is mostly translated by just swapping the characters from Simplified Chinese to Traditional Chinese, thus results in poor localization quality. This commit is intended to fix that problem. --- app/src/translations/tw.json | 122 +++++++++++++++++------------------ 1 file changed, 61 insertions(+), 61 deletions(-) diff --git a/app/src/translations/tw.json b/app/src/translations/tw.json index 66ca85ae..d1b52ccc 100644 --- a/app/src/translations/tw.json +++ b/app/src/translations/tw.json @@ -1,58 +1,58 @@ { - "socket.disconnected": "您已斷開連接", - "socket.reconnecting": "嘗試重新連接", - "socket.reconnected": "您已重新連接", - "socket.requestError": "服務器請求錯誤", + "socket.disconnected": "您已中斷連線", + "socket.reconnecting": "嘗試重新連線", + "socket.reconnected": "已恢復連線", + "socket.requestError": "伺服器請求發生錯誤", - "room.chooseRoom": "選擇您要加入的房間的名稱", - "room.cookieConsent": "這個網站使用Cookies來提升您的使用者體驗", + "room.chooseRoom": "選擇您要加入的會議室名稱", + "room.cookieConsent": "本網站使用 Cookies 技術來提升您的使用者體驗", "room.consentUnderstand": "了解", - "room.joined": "您已加入房間", - "room.cantJoin": "無法加入房間", - "room.youLocked": "您已鎖定房間", - "room.cantLock": "無法鎖定房間", - "room.youUnLocked": "您解鎖了房間", - "room.cantUnLock": "無法解鎖房間", - "room.locked": "房間已鎖定", - "room.unlocked": "房間現已解鎖", + "room.joined": "您已加入會議室", + "room.cantJoin": "無法加入會議室", + "room.youLocked": "您已鎖定會議室", + "room.cantLock": "無法鎖定會議室", + "room.youUnLocked": "您解鎖了會議室", + "room.cantUnLock": "無法解鎖會議室", + "room.locked": "會議室已鎖定", + "room.unlocked": "會議室現已解鎖", "room.newLobbyPeer": "新參與者進入大廳", "room.lobbyPeerLeft": "參與者離開大廳", "room.lobbyPeerChangedDisplayName": "大廳的參與者將名稱變更為 {displayName}", "room.lobbyPeerChangedPicture": "大廳的參與者變更了圖片", - "room.setAccessCode": "設置房間的進入密碼", - "room.accessCodeOn": "房間的進入密碼現已啟用", - "room.accessCodeOff": "房間的進入密碼已停用", - "room.peerChangedDisplayName": "{oldDisplayName} 已變更名稱為 {displayName}", + "room.setAccessCode": "設定會議室的密碼", + "room.accessCodeOn": "已啟用會議室密碼", + "room.accessCodeOff": "已停用會議室密碼", + "room.peerChangedDisplayName": "{oldDisplayName} 已更名為 {displayName}", "room.newPeer": "{displayName} 加入了會議室", - "room.newFile": "有新文件", + "room.newFile": "有新檔案", "room.toggleAdvancedMode": "切換進階模式", - "room.setDemocraticView": "已更改為使用者佈局", - "room.setFilmStripView": "已更改為投影片佈局", + "room.setDemocraticView": "已更改為使用者版面", + "room.setFilmStripView": "已更改為投影片版面", "room.loggedIn": "您已登入", "room.loggedOut": "您已登出", "room.changedDisplayName": "您的顯示名稱已變更為 {displayName}", "room.changeDisplayNameError": "更改顯示名稱時發生錯誤", "room.chatError": "無法發送聊天消息", "room.aboutToJoin": "您即將參加會議", - "room.roomId": "房間ID: {roomName}", - "room.setYourName": "設置您的顯示名稱,並選擇您想加入的方式:", + "room.roomId": "會議室 ID: {roomName}", + "room.setYourName": "設定您的顯示名稱,並選擇您想加入的方式:", "room.audioOnly": "僅通話", "room.audioVideo": "通話和視訊", "room.youAreReady": "準備完畢!", - "room.emptyRequireLogin": "房間是空的! 您可以登錄以開始會議或等待主持人加入", - "room.locketWait": "房間已鎖定! 請等待其他人允許您進入...", + "room.emptyRequireLogin": "會議室空蕩蕩的… 您可以登入以開始會議或等待主持人加入", + "room.locketWait": "會議室已上鎖! 請等待其他人允許您進入…", "room.lobbyAdministration": "大廳管理", "room.peersInLobby": "大廳的參與者", "room.lobbyEmpty": "大廳目前沒有人", "room.hiddenPeers": "{hiddenPeersCount, plural, one {participant} other {participants}}", "room.me": "我", - "room.spotlights": "Spotlight中的參與者", + "room.spotlights": "Spotlight 中的參與者", "room.passive": "被動參與者", "room.videoPaused": "視訊已關閉", "room.muteAll": "全部靜音", "room.stopAllVideo": "關閉全部視訊", "room.closeMeeting": "關閉會議", - "room.clearChat": "清除聊天", + "room.clearChat": "清除聊天訊息", "room.clearFileSharing": "清除檔案", "room.speechUnsupported": "您的瀏覽器不支援語音辨識", "room.moderatoractions": "管理員動作", @@ -60,7 +60,7 @@ "room.loweredHand": "{displayName} 放下了他的手", "room.extraVideo": "其他視訊", "room.extraVideoDuplication": null, - "room.overRoomLimit": "房間已滿,請稍後重試", + "room.overRoomLimit": "會議室已滿,請稍後重試", "room.help": "幫助", "room.about": "關於", "room.shortcutKeys": "鍵盤快速鍵", @@ -68,7 +68,7 @@ "room.hideSelfView": null, "room.showSelfView": null, - "me.mutedPTT": "您已靜音,請按下 空白鍵 來說話", + "me.mutedPTT": "您已將麥克風靜音,請按下「空白鍵」來發言", "roles.gotRole": "您已取得身份: {role}", "roles.lostRole": "您的 {role} 身份已被撤銷", @@ -76,14 +76,14 @@ "tooltip.login": "登入", "tooltip.logout": "登出", "tooltip.admitFromLobby": "從大廳允許", - "tooltip.lockRoom": "鎖定房間", - "tooltip.unLockRoom": "解鎖房間", + "tooltip.lockRoom": "鎖上會議室", + "tooltip.unLockRoom": "解鎖會議室", "tooltip.enterFullscreen": "進入全螢幕", "tooltip.leaveFullscreen": "退出全螢幕", "tooltip.lobby": "顯示大廳", - "tooltip.settings": "顯示設置", + "tooltip.settings": "設定", "tooltip.participants": "顯示參加者", - "tooltip.kickParticipant": "踢出", + "tooltip.kickParticipant": "踢出會議室", "tooltip.muteParticipant": "靜音", "tooltip.muteParticipantVideo": "隱藏視訊", "tooltip.inSpotlight": null, @@ -95,11 +95,11 @@ "tooltip.unMuteParticipant": null, "tooltip.addParticipantToSpotlight": null, "tooltip.removeParticipantFromSpotlight": null, - "tooltip.muteParticipantAudioModerator": "關閉聲音", + "tooltip.muteParticipantAudioModerator": "噤聲", "tooltip.muteParticipantVideoModerator": "關閉視訊", "tooltip.muteScreenSharingModerator": "關閉螢幕分享", - "label.roomName": "房間名稱", + "label.roomName": "會議室名稱", "label.chooseRoomButton": "繼續", "label.yourName": "您的名字", "label.newWindow": "新視窗", @@ -108,14 +108,14 @@ "label.leave": "離開", "label.chatInput": "輸入聊天訊息", "label.chat": "聊天", - "label.filesharing": "文件分享", + "label.filesharing": "檔案分享", "label.participants": "參與者", - "label.shareFile": "分享文件", + "label.shareFile": "分享檔案", "label.shareGalleryFile": "分享圖片", - "label.fileSharingUnsupported": "不支援文件分享", + "label.fileSharingUnsupported": "不支援檔案分享", "label.unknown": "未知", - "label.democratic": "使用者佈局", - "label.filmstrip": "投影片佈局", + "label.democratic": "使用者版面", + "label.filmstrip": "投影片版面", "label.low": "低", "label.medium": "中", "label.high": "高 (HD)", @@ -135,7 +135,7 @@ "label.logout": null, "label.join": null, - "settings.settings": "設置", + "settings.settings": "設定", "settings.camera": "視訊來源", "settings.selectCamera": "選擇視訊來源", "settings.cantSelectCamera": "無法選擇此視訊來源", @@ -149,8 +149,8 @@ "settings.frameRate": null, "settings.screenSharingResolution": null, "settings.screenSharingFrameRate": null, - "settings.layout": "房間佈局", - "settings.selectRoomLayout": "選擇房間佈局", + "settings.layout": "會議室版面", + "settings.selectRoomLayout": "選擇會議室版面", "settings.advancedMode": "進階模式", "settings.permanentTopBar": "固定頂端列", "settings.aspectRatio": null, @@ -162,10 +162,10 @@ "settings.buttonControlBar": "獨立控制按鈕", "settings.showAdvancedVideo": null, "settings.showAdvancedAudio": null, - "settings.echoCancellation": "回音消除", + "settings.echoCancellation": "抑制回音", "settings.autoGainControl": "自動增益控制", - "settings.noiseSuppression": "噪音消除", - "settings.drawerOverlayed": "側邊欄覆蓋畫面", + "settings.noiseSuppression": "消除噪音", + "settings.drawerOverlayed": "側邊欄覆蓋", "settings.voiceActivatedUnmute": null, "settings.noiseThreshold": null, "settings.mirrorOwnVideo": null, @@ -176,18 +176,18 @@ "settings.myPhotoSizeError": null, "settings.myPhotoTypeError": null, - "filesharing.saveFileError": "無法保存文件", - "filesharing.startingFileShare": "開始分享文件", - "filesharing.successfulFileShare": "文件已成功分享", - "filesharing.unableToShare": "無法分享文件", - "filesharing.error": "文件分享發生錯誤", - "filesharing.finished": "文件分享成功", - "filesharing.save": "保存文件", - "filesharing.sharedFile": "{displayName} 分享了一個文件", - "filesharing.download": "下載文件", - "filesharing.missingSeeds": "如果過了很久還是無法下載,則可能沒有人播種了。請讓上傳者重新上傳您想要的文件。", + "filesharing.saveFileError": "無法儲存檔案", + "filesharing.startingFileShare": "開始分享檔案", + "filesharing.successfulFileShare": "成功分享檔案", + "filesharing.unableToShare": "無法分享檔案", + "filesharing.error": "分享檔案時發生錯誤", + "filesharing.finished": "檔案分享完成", + "filesharing.save": "儲存檔案", + "filesharing.sharedFile": "{displayName} 分享了一份檔案", + "filesharing.download": "下載檔案", + "filesharing.missingSeeds": "如果過了很久還是無法下載,則可能沒有人播種了。請讓上傳者重新上傳您需要的檔案。", - "devices.devicesChanged": "您的設備已更改,請在設置中設定您的設備", + "devices.devicesChanged": "您已變更裝置,請在設定中調整您的裝置", "devices.enableOnlyMicrophone": null, "device.audioUnsupported": "不支援您的音訊格式", @@ -201,18 +201,18 @@ "device.stopVideo": "關閉視訊", "device.screenSharingUnsupported": "不支援您的螢幕分享格式", - "device.startScreenSharing": "開始螢幕分享", + "device.startScreenSharing": "開始分享螢幕", "device.stopScreenSharing": "停止螢幕分享", - "devices.microphoneDisconnected": "麥克風已斷開", + "devices.microphoneDisconnected": "麥克風連線中斷", "devices.microphoneError": "麥克風發生錯誤", "devices.microphoneMute": "麥克風靜音", "devices.microphoneUnMute": "取消麥克風靜音", - "devices.microphoneEnable": "麥克風已啟用", + "devices.microphoneEnable": "已啟用麥克風", "devices.microphoneMuteError": "無法使麥克風靜音", "devices.microphoneUnMuteError": "無法取消麥克風靜音", - "devices.screenSharingDisconnected": "螢幕分享已斷開", + "devices.screenSharingDisconnected": "螢幕分享已中斷", "devices.screenSharingError": "螢幕分享時發生錯誤", "devices.cameraDisconnected": "相機已斷開連接", From 30d35e02fb2caceb57cd89a7cf25c88ccb91d318 Mon Sep 17 00:00:00 2001 From: Roman Drozd Date: Thu, 27 May 2021 00:50:32 +0200 Subject: [PATCH 56/57] Fix language selection (wrong locale code setting) --- app/src/translations/locales.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/translations/locales.ts b/app/src/translations/locales.ts index fd055b09..7350592e 100644 --- a/app/src/translations/locales.ts +++ b/app/src/translations/locales.ts @@ -138,7 +138,8 @@ export const loadOne = (locale: string): ILocale => { try { res = list.filter( - (item) => item.locale.includes(locale) || item.locale.includes(locale.split(/[-_]/)[0]) + // (item) => item.locale.includes(locale) || item.locale.includes(locale.split(/[-_]/)[0]) + (item) => item.locale.includes(locale) )[0] res.messages = require(`./${res.file}`) From ff9163cf01ee1cd5038f92bcd5af4a2b60655085 Mon Sep 17 00:00:00 2001 From: Vittorio Palmisano Date: Fri, 28 May 2021 17:54:13 +0200 Subject: [PATCH 57/57] lint fix --- app/src/translations/locales.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/app/src/translations/locales.ts b/app/src/translations/locales.ts index f383aff0..c219ceb8 100644 --- a/app/src/translations/locales.ts +++ b/app/src/translations/locales.ts @@ -139,11 +139,12 @@ export const loadOne = (locale: string): ILocale => { let res: any = {}; - try { - res = list.filter( - // (item) => item.locale.includes(locale) || item.locale.includes(locale.split(/[-_]/)[0]) - (item) => item.locale.includes(locale) - )[0] + try + { + res = list.filter( + // (item) => item.locale.includes(locale) || item.locale.includes(locale.split(/[-_]/)[0]) + (item) => item.locale.includes(locale) + )[0]; res.messages = require(`./${res.file}`); }