diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9989684 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +jitsiinstallrc +.vagrant diff --git a/README.md b/README.md index 172674a..911c694 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,93 @@ -# jitsi-meet-video-conference-installer -Unattended setup for Jitsi meet video conference server +# Jitsi Meet Video Conference Installer + +This project includes a selection of scripts to facilitate the installation of +[Jitsi](https://jitsi.org/) from the official Debian packages. +In addition to that, it provides some configuration options to quickly and +repeatedly get your own out-of-the-box video conferencing server. + +It will most likely work on any Debian-based system, however, it was only +(successfully) tested on Ubuntu 18.04. + +It is mainly meant for disposable, temporary personal use on the Internet or +for use in private networks as security, scalability, or optimized asset delivery +has not been a main consideration in the implementation so far. + +## Getting started + +Create a `jitsiinstallrc` file in the project root by copying the +`jitsiinstallrc.example` file and adjust the the configuration options as needed. +Most configuration options are optional. If no value is provided, the defaults +will be used/no changes will be made. + +Subsequently run the installer script: `installer/install-jitsi.sh` + +**Vagrant** + +With `vagrant` installed, simply run `vagrant up` to create a virtual machine +that will automatically run the installer on boot. + +The IP address of the virtual machine will be `10.0.3.33`; to reach the Jitsi +server from the host system, ensure that the domain is properly mapped to that +IP address, for instance by executing: +``` +source jitsiinstallrc +echo "10.0.3.33 ${FULLY_QUALIFIED_HOSTNAME}" >> /etc/hosts +``` + +### Configuration features + +**Hooks** + +Hooks will load a URL via HTTP GET before Jitsi is installed as well +as after the installation. The post-install hook will also run on reboot. +These hooks can be useful in to dynamically update DNS records in an +unmanaged environment or when managing a dynamic machine inventory. + +**SSL** + +Jitsi supports letsencrypt SSL certificates out of the box, which is the +preferred and recommended option. This option requires to provide a valid +e-mail address and to agree with the terms. +Additionally, the certificate issuance process requires a valid DNS entry and +that the machine is reachable under the provided DNS name through the Internet +for verification purposes. However, there are certain domains from cloud +providers for which this process will not work. +There also seem to be other constraints such as the number of certificate +issuance requests per week, in case the server is re-installed multiple times +withing this timeframe. + +If letsencrypt is not an option, alternatively, an existing certificate and a +key can be provided as configuration parameters. +When providing the parameter it needs to be base64 encoded and should not have +any linebreaks; a quick way to achieve this is by running `base64 FILENAME | tr -d "\n"`. + +When none of the mentioned SSL options is specified, a self-signed certificate +will be generated. + +**Authentication for meeting creation** + +By default Jitsi does not require any authentication, i.e. anyone with the +URL can create and host meetings. +Configuring the username and password will only allow users will these credentials +to start a meeting. +Once the meeting is started, anyone with the conference URL can still join +without authenticating. + +**Jitsi configurations** + +A few selected features are customizable through the configuration options. +See config file for details on these options https://meet.jit.si/config.js +or compare the behavior to the reference installation at http://meet.jit.si/ + +**Phone dial in** + +Not implemented yet. + +## Sources/more info + +The installer is based on some articles and resources: +- https://github.com/jitsi/jitsi-meet/blob/master/doc/quick-install.md +- https://www.reddit.com/r/linux/comments/ayy0sj/jitsimeet_authentication_for_dummies/ +- https://dev.to/noandrea/self-hosted-jitsi-server-with-authentication-ie7 +- https://github.com/jitsi/jitsi-meet/blob/master/resources/install-letsencrypt-cert.sh +- https://aws.amazon.com/blogs/opensource/getting-started-with-jitsi-an-open-source-web-conferencing-solution/ diff --git a/Vagrantfile b/Vagrantfile new file mode 100644 index 0000000..93791d5 --- /dev/null +++ b/Vagrantfile @@ -0,0 +1,16 @@ + +Vagrant.configure("2") do |config| + config.vm.box = "ubuntu/bionic64" + config.vm.network "private_network", ip: "10.0.3.33" + config.vm.provider 'virtualbox' { |box| box.name = 'jitsi-meet' } + + config.vm.provision "install-jitsi", type: :shell, inline: <<-SHELL + cd /vagrant + + if [ ! -s "./jitsiinstallrc" ]; then + echo "Ensure to set up a jitsiinstallrc file before install!" >&2 + exit 1 + fi + ./installer/install-jitsi.sh + SHELL +end diff --git a/configs/auth/meet-config.js b/configs/auth/meet-config.js new file mode 100644 index 0000000..f644277 --- /dev/null +++ b/configs/auth/meet-config.js @@ -0,0 +1,522 @@ +/* eslint-disable no-unused-vars, no-var */ + +var config = { + // Connection + // + + hosts: { + // XMPP domain. + domain: 'JITSI_DOMAIN_NAME', + + // When using authentication, domain for guest users. + anonymousdomain: 'guest.JITSI_DOMAIN_NAME', + + // Domain for authenticated users. Defaults to . + authdomain: 'JITSI_DOMAIN_NAME', + + // Jirecon recording component domain. + // jirecon: 'jirecon.JITSI_DOMAIN_NAME', + + // Call control component (Jigasi). + // call_control: 'callcontrol.JITSI_DOMAIN_NAME', + + // Focus component domain. Defaults to focus.. + // focus: 'focus.JITSI_DOMAIN_NAME', + + // XMPP MUC domain. FIXME: use XEP-0030 to discover it. + muc: 'conference.JITSI_DOMAIN_NAME' + }, + + // BOSH URL. FIXME: use XEP-0156 to discover it. + bosh: '//JITSI_DOMAIN_NAME/http-bind', + + // Websocket URL + // websocket: 'wss://JITSI_DOMAIN_NAME/xmpp-websocket', + + // The name of client node advertised in XEP-0115 'c' stanza + clientNode: 'http://jitsi.org/jitsimeet', + + // The real JID of focus participant - can be overridden here + focusUserJid: 'focus@auth.JITSI_DOMAIN_NAME', + + + // Testing / experimental features. + // + + testing: { + // Enables experimental simulcast support on Firefox. + enableFirefoxSimulcast: false, + + // P2P test mode disables automatic switching to P2P when there are 2 + // participants in the conference. + p2pTestMode: false + + // Enables the test specific features consumed by jitsi-meet-torture + // testMode: false + + // Disables the auto-play behavior of *all* newly created video element. + // This is useful when the client runs on a host with limited resources. + // noAutoPlayVideo: false + }, + + // Disables ICE/UDP by filtering out local and remote UDP candidates in + // signalling. + // webrtcIceUdpDisable: false, + + // Disables ICE/TCP by filtering out local and remote TCP candidates in + // signalling. + // webrtcIceTcpDisable: false, + + + // Media + // + + // Audio + + // Disable measuring of audio levels. + // disableAudioLevels: false, + // audioLevelsInterval: 200, + + // Enabling this will run the lib-jitsi-meet no audio detection module which + // will notify the user if the current selected microphone has no audio + // input and will suggest another valid device if one is present. + enableNoAudioDetection: true, + + // Enabling this will run the lib-jitsi-meet noise detection module which will + // notify the user if there is noise, other than voice, coming from the current + // selected microphone. The purpose it to let the user know that the input could + // be potentially unpleasant for other meeting participants. + enableNoisyMicDetection: true, + + // Start the conference in audio only mode (no video is being received nor + // sent). + // startAudioOnly: false, + + // Every participant after the Nth will start audio muted. + startAudioMuted: 4, + + // Start calls with audio muted. Unlike the option above, this one is only + // applied locally. FIXME: having these 2 options is confusing. + // startWithAudioMuted: false, + + // Enabling it (with #params) will disable local audio output of remote + // participants and to enable it back a reload is needed. + // startSilent: false + + // Video + + // Sets the preferred resolution (height) for local video. Defaults to 720. + // resolution: 720, + + // w3c spec-compliant video constraints to use for video capture. Currently + // used by browsers that return true from lib-jitsi-meet's + // util#browser#usesNewGumFlow. The constraints are independent from + // this config's resolution value. Defaults to requesting an ideal aspect + // ratio of 16:9 with an ideal resolution of 720. + // constraints: { + // video: { + // aspectRatio: 16 / 9, + // height: { + // ideal: 720, + // max: 720, + // min: 240 + // } + // } + // }, + + // Enable / disable simulcast support. + // disableSimulcast: false, + + // Enable / disable layer suspension. If enabled, endpoints whose HD + // layers are not in use will be suspended (no longer sent) until they + // are requested again. + // enableLayerSuspension: false, + + // Every participant after the Nth will start video muted. + // startVideoMuted: 10, + + // Start calls with video muted. Unlike the option above, this one is only + // applied locally. FIXME: having these 2 options is confusing. + // startWithVideoMuted: false, + + // If set to true, prefer to use the H.264 video codec (if supported). + // Note that it's not recommended to do this because simulcast is not + // supported when using H.264. For 1-to-1 calls this setting is enabled by + // default and can be toggled in the p2p section. + // preferH264: true, + + // If set to true, disable H.264 video codec by stripping it out of the + // SDP. + // disableH264: false, + + // Desktop sharing + + // The ID of the jidesha extension for Chrome. + desktopSharingChromeExtId: null, + + // Whether desktop sharing should be disabled on Chrome. + // desktopSharingChromeDisabled: false, + + // The media sources to use when using screen sharing with the Chrome + // extension. + desktopSharingChromeSources: [ 'screen', 'window', 'tab' ], + + // Required version of Chrome extension + desktopSharingChromeMinExtVersion: '0.1', + + // Whether desktop sharing should be disabled on Firefox. + // desktopSharingFirefoxDisabled: false, + + // Optional desktop sharing frame rate options. Default value: min:5, max:5. + // desktopSharingFrameRate: { + // min: 5, + // max: 5 + // }, + + // Try to start calls with screen-sharing instead of camera video. + // startScreenSharing: false, + + // Recording + + // Whether to enable file recording or not. + // fileRecordingsEnabled: false, + // Enable the dropbox integration. + // dropbox: { + // appKey: '' // Specify your app key here. + // // A URL to redirect the user to, after authenticating + // // by default uses: + // // 'https://JITSI_DOMAIN_NAME/static/oauth.html' + // redirectURI: + // 'https://JITSI_DOMAIN_NAME/subfolder/static/oauth.html' + // }, + // When integrations like dropbox are enabled only that will be shown, + // by enabling fileRecordingsServiceEnabled, we show both the integrations + // and the generic recording service (its configuration and storage type + // depends on jibri configuration) + // fileRecordingsServiceEnabled: false, + // Whether to show the possibility to share file recording with other people + // (e.g. meeting participants), based on the actual implementation + // on the backend. + // fileRecordingsServiceSharingEnabled: false, + + // Whether to enable live streaming or not. + // liveStreamingEnabled: false, + + // Transcription (in interface_config, + // subtitles and buttons can be configured) + // transcribingEnabled: false, + + // Enables automatic turning on captions when recording is started + // autoCaptionOnRecord: false, + + // Misc + + // Default value for the channel "last N" attribute. -1 for unlimited. + channelLastN: -1, + + // Disables or enables RTX (RFC 4588) (defaults to false). + // disableRtx: false, + + // Disables or enables TCC (the default is in Jicofo and set to true) + // (draft-holmer-rmcat-transport-wide-cc-extensions-01). This setting + // affects congestion control, it practically enables send-side bandwidth + // estimations. + // enableTcc: true, + + // Disables or enables REMB (the default is in Jicofo and set to false) + // (draft-alvestrand-rmcat-remb-03). This setting affects congestion + // control, it practically enables recv-side bandwidth estimations. When + // both TCC and REMB are enabled, TCC takes precedence. When both are + // disabled, then bandwidth estimations are disabled. + // enableRemb: false, + + // Defines the minimum number of participants to start a call (the default + // is set in Jicofo and set to 2). + // minParticipants: 2, + + // Use XEP-0215 to fetch STUN and TURN servers. + // useStunTurn: true, + + // Enable IPv6 support. + // useIPv6: true, + + // Enables / disables a data communication channel with the Videobridge. + // Values can be 'datachannel', 'websocket', true (treat it as + // 'datachannel'), undefined (treat it as 'datachannel') and false (don't + // open any channel). + // openBridgeChannel: true, + + + // UI + // + + // Use display name as XMPP nickname. + // useNicks: false, + + // Require users to always specify a display name. + // requireDisplayName: true, + + // Whether to use a welcome page or not. In case it's false a random room + // will be joined when no room is specified. + enableWelcomePage: true, + + // Enabling the close page will ignore the welcome page redirection when + // a call is hangup. + // enableClosePage: false, + + // Disable hiding of remote thumbnails when in a 1-on-1 conference call. + // disable1On1Mode: false, + + // Default language for the user interface. + // defaultLanguage: 'en', + + // If true all users without a token will be considered guests and all users + // with token will be considered non-guests. Only guests will be allowed to + // edit their profile. + enableUserRolesBasedOnToken: false, + + // Whether or not some features are checked based on token. + // enableFeaturesBasedOnToken: false, + + // Enable lock room for all moderators, even when userRolesBasedOnToken is enabled and participants are guests. + // lockRoomGuestEnabled: false, + + // When enabled the password used for locking a room is restricted to up to the number of digits specified + // roomPasswordNumberOfDigits: 10, + // default: roomPasswordNumberOfDigits: false, + + // Message to show the users. Example: 'The service will be down for + // maintenance at 01:00 AM GMT, + // noticeMessage: '', + + // Enables calendar integration, depends on googleApiApplicationClientID + // and microsoftApiApplicationClientID + // enableCalendarIntegration: false, + + // Stats + // + + // Whether to enable stats collection or not in the TraceablePeerConnection. + // This can be useful for debugging purposes (post-processing/analysis of + // the webrtc stats) as it is done in the jitsi-meet-torture bandwidth + // estimation tests. + // gatherStats: false, + + // The interval at which PeerConnection.getStats() is called. Defaults to 10000 + // pcStatsInterval: 10000, + + // To enable sending statistics to callstats.io you must provide the + // Application ID and Secret. + // callStatsID: '', + // callStatsSecret: '', + + // enables sending participants display name to callstats + // enableDisplayNameInStats: false, + + // enables sending participants email if available to callstats and other analytics + // enableEmailInStats: false, + + // Privacy + // + + // If third party requests are disabled, no other server will be contacted. + // This means avatars will be locally generated and callstats integration + // will not function. + // disableThirdPartyRequests: false, + + + // Peer-To-Peer mode: used (if enabled) when there are just 2 participants. + // + + p2p: { + // Enables peer to peer mode. When enabled the system will try to + // establish a direct connection when there are exactly 2 participants + // in the room. If that succeeds the conference will stop sending data + // through the JVB and use the peer to peer connection instead. When a + // 3rd participant joins the conference will be moved back to the JVB + // connection. + enabled: true, + + // Use XEP-0215 to fetch STUN and TURN servers. + // useStunTurn: true, + + // The STUN servers that will be used in the peer to peer connections + stunServers: [ + + // { urls: 'stun:JITSI_DOMAIN_NAME:443' }, + { urls: 'stun:stun.l.google.com:19302' }, + { urls: 'stun:stun1.l.google.com:19302' }, + { urls: 'stun:stun2.l.google.com:19302' } + ], + + // Sets the ICE transport policy for the p2p connection. At the time + // of this writing the list of possible values are 'all' and 'relay', + // but that is subject to change in the future. The enum is defined in + // the WebRTC standard: + // https://www.w3.org/TR/webrtc/#rtcicetransportpolicy-enum. + // If not set, the effective value is 'all'. + // iceTransportPolicy: 'all', + + // If set to true, it will prefer to use H.264 for P2P calls (if H.264 + // is supported). + preferH264: true + + // If set to true, disable H.264 video codec by stripping it out of the + // SDP. + // disableH264: false, + + // How long we're going to wait, before going back to P2P after the 3rd + // participant has left the conference (to filter out page reload). + // backToP2PDelay: 5 + }, + + analytics: { + // The Google Analytics Tracking ID: + // googleAnalyticsTrackingId: 'your-tracking-id-UA-123456-1' + + // The Amplitude APP Key: + // amplitudeAPPKey: '' + + // Array of script URLs to load as lib-jitsi-meet "analytics handlers". + // scriptURLs: [ + // "libs/analytics-ga.min.js", // google-analytics + // "https://example.com/my-custom-analytics.js" + // ], + }, + + // Information about the jitsi-meet instance we are connecting to, including + // the user region as seen by the server. + deploymentInfo: { + // shard: "shard1", + // region: "europe", + // userRegion: "asia" + }, + + // Information for the chrome extension banner + // chromeExtensionBanner: { + // // The chrome extension to be installed address + // url: 'https://chrome.google.com/webstore/detail/jitsi-meetings/kglhbbefdnlheedjiejgomgmfplipfeb', + + // // Extensions info which allows checking if they are installed or not + // chromeExtensionsInfo: [ + // { + // id: 'kglhbbefdnlheedjiejgomgmfplipfeb', + // path: 'jitsi-logo-48x48.png' + // } + // ] + // }, + + // Local Recording + // + + // localRecording: { + // Enables local recording. + // Additionally, 'localrecording' (all lowercase) needs to be added to + // TOOLBAR_BUTTONS in interface_config.js for the Local Recording + // button to show up on the toolbar. + // + // enabled: true, + // + + // The recording format, can be one of 'ogg', 'flac' or 'wav'. + // format: 'flac' + // + + // }, + + // Options related to end-to-end (participant to participant) ping. + // e2eping: { + // // The interval in milliseconds at which pings will be sent. + // // Defaults to 10000, set to <= 0 to disable. + // pingInterval: 10000, + // + // // The interval in milliseconds at which analytics events + // // with the measured RTT will be sent. Defaults to 60000, set + // // to <= 0 to disable. + // analyticsInterval: 60000, + // }, + + // If set, will attempt to use the provided video input device label when + // triggering a screenshare, instead of proceeding through the normal flow + // for obtaining a desktop stream. + // NOTE: This option is experimental and is currently intended for internal + // use only. + // _desktopSharingSourceDevice: 'sample-id-or-label', + + // If true, any checks to handoff to another application will be prevented + // and instead the app will continue to display in the current browser. + // disableDeepLinking: false, + + // A property to disable the right click context menu for localVideo + // the menu has option to flip the locally seen video for local presentations + // disableLocalVideoFlip: false, + + // Deployment specific URLs. + // deploymentUrls: { + // // If specified a 'Help' button will be displayed in the overflow menu with a link to the specified URL for + // // user documentation. + // userDocumentationURL: 'https://docs.example.com/video-meetings.html', + // // If specified a 'Download our apps' button will be displayed in the overflow menu with a link + // // to the specified URL for an app download page. + // downloadAppsUrl: 'https://docs.example.com/our-apps.html' + // }, + + // List of undocumented settings used in jitsi-meet + /** + _immediateReloadThreshold + autoRecord + autoRecordToken + debug + debugAudioLevels + deploymentInfo + dialInConfCodeUrl + dialInNumbersUrl + dialOutAuthUrl + dialOutCodesUrl + disableRemoteControl + displayJids + etherpad_base + externalConnectUrl + firefox_fake_device + googleApiApplicationClientID + iAmRecorder + iAmSipGateway + microsoftApiApplicationClientID + peopleSearchQueryTypes + peopleSearchUrl + requireDisplayName + tokenAuthUrl + */ + + // List of undocumented settings used in lib-jitsi-meet + /** + _peerConnStatusOutOfLastNTimeout + _peerConnStatusRtcMuteTimeout + abTesting + avgRtpStatsN + callStatsConfIDNamespace + callStatsCustomScriptUrl + desktopSharingSources + disableAEC + disableAGC + disableAP + disableHPF + disableNS + enableLipSync + enableTalkWhileMuted + forceJVB121Ratio + hiddenDomain + ignoreStartMuted + nick + startBitrate + */ + + + // Allow all above example options to include a trailing comma and + // prevent fear when commenting out the last value. + makeJsonParserHappy: 'even if last key had a trailing comma' + + // no configuration value should follow this line. +}; + +/* eslint-enable no-unused-vars, no-var */ diff --git a/configs/auth/prosody.cfg.lua b/configs/auth/prosody.cfg.lua new file mode 100644 index 0000000..2259e73 --- /dev/null +++ b/configs/auth/prosody.cfg.lua @@ -0,0 +1,80 @@ +plugin_paths = { "/usr/share/jitsi-meet/prosody-plugins/" } + +-- domain mapper options, must at least have domain base set to use the mapper +muc_mapper_domain_base = "JITSI_DOMAIN_NAME"; + +turncredentials_secret = "TURN_SECRET"; + +turncredentials = { + { type = "stun", host = "JITSI_DOMAIN_NAME", port = "443" }, + { type = "turn", host = "JITSI_DOMAIN_NAME", port = "443", transport = "udp" }, + { type = "turns", host = "JITSI_DOMAIN_NAME", port = "443", transport = "tcp" } +}; + +cross_domain_bosh = false; +consider_bosh_secure = true; + +VirtualHost "JITSI_DOMAIN_NAME" + -- enabled = false -- Remove this line to enable this host + authentication = "internal_plain" + -- Properties below are modified by jitsi-meet-tokens package config + -- and authentication above is switched to "token" + --app_id="example_app_id" + --app_secret="example_app_secret" + -- Assign this host a certificate for TLS, otherwise it would use the one + -- set in the global section (if any). + -- Note that old-style SSL on port 5223 only supports one certificate, and will always + -- use the global one. + ssl = { + key = "/etc/prosody/certs/JITSI_DOMAIN_NAME.key"; + certificate = "/etc/prosody/certs/JITSI_DOMAIN_NAME.crt"; + } + speakerstats_component = "speakerstats.JITSI_DOMAIN_NAME" + conference_duration_component = "conferenceduration.JITSI_DOMAIN_NAME" + -- we need bosh + modules_enabled = { + "bosh"; + "pubsub"; + "ping"; -- Enable mod_ping + "speakerstats"; + "turncredentials"; + "conference_duration"; + } + c2s_require_encryption = false + +Component "conference.JITSI_DOMAIN_NAME" "muc" + storage = "none" + modules_enabled = { + "muc_meeting_id"; + "muc_domain_mapper"; + -- "token_verification"; + } + admins = { "focus@auth.JITSI_DOMAIN_NAME" } + +-- internal muc component +Component "internal.auth.JITSI_DOMAIN_NAME" "muc" + storage = "none" + modules_enabled = { + "ping"; + } + admins = { "focus@auth.JITSI_DOMAIN_NAME", "jvb@auth.JITSI_DOMAIN_NAME" } + +VirtualHost "auth.JITSI_DOMAIN_NAME" + ssl = { + key = "/etc/prosody/certs/auth.JITSI_DOMAIN_NAME.key"; + certificate = "/etc/prosody/certs/auth.JITSI_DOMAIN_NAME.crt"; + } + authentication = "internal_plain" + +Component "focus.JITSI_DOMAIN_NAME" + component_secret = "JICOFO_SECRET" + +Component "speakerstats.JITSI_DOMAIN_NAME" "speakerstats_component" + muc_component = "conference.JITSI_DOMAIN_NAME" + +Component "conferenceduration.JITSI_DOMAIN_NAME" "conference_duration_component" + muc_component = "conference.JITSI_DOMAIN_NAME" + +VirtualHost "guest.JITSI_DOMAIN_NAME" + authentication = "anonymous" + c2s_require_encryption = false diff --git a/configs/ssl/configure-certificate.sh b/configs/ssl/configure-certificate.sh new file mode 100644 index 0000000..b748005 --- /dev/null +++ b/configs/ssl/configure-certificate.sh @@ -0,0 +1,63 @@ +#!/usr/bin/env bash + +# Based on jitsi-meet letsencrypt certificate installer +# https://github.com/jitsi/jitsi-meet/blob/master/resources/install-letsencrypt-cert.sh + +DOMAIN="${1}" + +CERT_KEY="/etc/ssl/live/$DOMAIN/privkey.pem" +CERT_CRT="/etc/ssl/live/$DOMAIN/fullchain.pem" + +if [ -f /etc/nginx/sites-enabled/$DOMAIN.conf ] ; then + echo "Configuring nginx" + + CONF_FILE="/etc/nginx/sites-available/$DOMAIN.conf" + CERT_KEY_ESC=$(echo $CERT_KEY | sed 's/\./\\\./g') + CERT_KEY_ESC=$(echo $CERT_KEY_ESC | sed 's/\//\\\//g') + sed -i "s/ssl_certificate_key\ \/etc\/jitsi\/meet\/.*key/ssl_certificate_key\ $CERT_KEY_ESC/g" \ + $CONF_FILE + CERT_CRT_ESC=$(echo $CERT_CRT | sed 's/\./\\\./g') + CERT_CRT_ESC=$(echo $CERT_CRT_ESC | sed 's/\//\\\//g') + sed -i "s/ssl_certificate\ \/etc\/jitsi\/meet\/.*crt/ssl_certificate\ $CERT_CRT_ESC/g" \ + $CONF_FILE + + service nginx reload + + TURN_CONFIG="/etc/turnserver.conf" + if [ -f $TURN_CONFIG ] && grep -q "jitsi-meet coturn config" "$TURN_CONFIG" ; then + echo "Configuring turnserver" + sed -i "s/cert=\/etc\/jitsi\/meet\/.*crt/cert=$CERT_CRT_ESC/g" $TURN_CONFIG + sed -i "s/pkey=\/etc\/jitsi\/meet\/.*key/pkey=$CERT_KEY_ESC/g" $TURN_CONFIG + + service coturn restart + fi +elif [ -f /etc/apache2/sites-enabled/$DOMAIN.conf ] ; then + echo "Configuring apache2" + + CONF_FILE="/etc/apache2/sites-available/$DOMAIN.conf" + CERT_KEY_ESC=$(echo $CERT_KEY | sed 's/\./\\\./g') + CERT_KEY_ESC=$(echo $CERT_KEY_ESC | sed 's/\//\\\//g') + sed -i "s/SSLCertificateKeyFile\ \/etc\/jitsi\/meet\/.*key/SSLCertificateKeyFile\ $CERT_KEY_ESC/g" \ + $CONF_FILE + CERT_CRT_ESC=$(echo $CERT_CRT | sed 's/\./\\\./g') + CERT_CRT_ESC=$(echo $CERT_CRT_ESC | sed 's/\//\\\//g') + sed -i "s/SSLCertificateFile\ \/etc\/jitsi\/meet\/.*crt/SSLCertificateFile\ $CERT_CRT_ESC/g" \ + $CONF_FILE + + service apache2 reload +else + service jitsi-videobridge stop + + echo "Configuring jetty" + + CERT_P12="/etc/jitsi/videobridge/$DOMAIN.p12" + CERT_JKS="/etc/jitsi/videobridge/$DOMAIN.jks" + # create jks from certs + openssl pkcs12 -export \ + -in $CERT_CRT -inkey $CERT_KEY -passout pass:changeit > $CERT_P12 + keytool -importkeystore -destkeystore $CERT_JKS \ + -srckeystore $CERT_P12 -srcstoretype pkcs12 \ + -noprompt -storepass changeit -srcstorepass changeit + + service jitsi-videobridge start +fi diff --git a/installer/install-jitsi.sh b/installer/install-jitsi.sh new file mode 100755 index 0000000..07cacc3 --- /dev/null +++ b/installer/install-jitsi.sh @@ -0,0 +1,299 @@ +#!/usr/bin/env bash +set -e + +SCRIPT_DIR="$(dirname "$(readlink -e "${BASH_SOURCE[0]}")")" +INSTALL_ROOT="$(dirname "${SCRIPT_DIR}")" +JITSIRC_PATH="${INSTALL_ROOT}/jitsiinstallrc" +CONFIG_PATH="${INSTALL_ROOT}/configs" + +main() { + [ "$(id -u)" = "0" ] || die "This script must be executed as root!" + [ -s "${JITSIRC_PATH}" ] || die "jitsi installer config not found at '${JITSIRC_PATH}'!" + + # shellcheck source=./jitsirc + source "${JITSIRC_PATH}" + + set -u + + os_pkg_install curl + log "Setup hostname..."; setup_hostname + log "Setup firewall..."; setup_firewall + log "Update system..."; update_system + log "Configure systemd process limits..."; configure_systemd_process_limits + + log "Invoking pre install hooks..."; invoke_hook "pre_install" + + log "Install dependencies..."; install_dependencies + log "Install Jitsi..."; install_jitsi + log "Setup Jitsi SIP dialin..."; install_phone_dialin + log "Install ssl certificate..."; install_ssl_certificate + log "Enable Jitsi user authentication..."; enable_authentication + log "Tweaking Jitsi config..."; tweak_config + + log "Ensure Jitsi started..."; ensure_jitsi_started + log "Validate Jitsi install..."; validate_jitsi_install + + log "Invoking post install hooks..."; invoke_hook "post_install" + log "Persisting post install hooks..."; persist_hook "post_install" "reboot" +} + +setup_hostname() { + hostnamectl set-hostname "$(get_fully_qualified_hostname)" + echo "127.0.1.1 $(get_fully_qualified_hostname) $(get_hostname)" >> /etc/hosts +} + +setup_firewall() { + os_firewall allow http + os_firewall allow https + os_firewall allow in 10000:20000/udp + + [ "${FIREWALL_ENABLE_SSH:-}" != "true" ] || os_firewall allow OpenSSH + os_firewall --force enable + os_svc restart ufw +} + +configure_systemd_process_limits() { + ensure_key_value_present /etc/systemd/system.conf "DefaultLimitNOFILE" "65000" + ensure_key_value_present /etc/systemd/system.conf "DefaultLimitNPROC" "65000" + ensure_key_value_present /etc/systemd/system.conf "DefaultTasksMax" "65000" + os_svc_init daemon-reload +} + +update_system() { + os_pkg_repo_update + os_pkg_system_update +} + +install_dependencies() { + install_java + install_nginx + + os_pkg_install debconf-utils curl +} + +install_java() { + os_pkg_install openjdk-8-jre-headless + define_persisted_env_variable "JAVA_HOME" "$(dirname "$(dirname "$(readlink -e "$(command -v java)")")")" +} + +install_nginx() { + os_pkg_install nginx + os_svc_init enable nginx + os_svc start nginx +} + +install_jitsi() { + echo 'deb https://download.jitsi.org stable/' >> /etc/apt/sources.list.d/jitsi-stable.list + curl -s https://download.jitsi.org/jitsi-key.gpg.key | apt-key add - + os_pkg_install apt-transport-https + os_pkg_repo_update + + echo "jitsi-videobridge2 jitsi-videobridge/jvb-hostname string $(get_fully_qualified_hostname)" | debconf-set-selections + echo "jitsi-videobridge2 jitsi-videobridge/jvbsecret password $(generate_passwd)" | debconf-set-selections + os_pkg_install jitsi-meet +} + +install_ssl_certificate() { + if [ -n "${SSL_LETSENCRYPT_EMAIL:-}" ]; then + echo -e "${SSL_LETSENCRYPT_EMAIL}\n" | /usr/share/jitsi-meet/scripts/install-letsencrypt-cert.sh + return $? + fi + + local fqdn; fqdn=$(get_fully_qualified_hostname) + local certificate_dir="/etc/ssl/live/${fqdn}" + mkdir -p "${certificate_dir}" + + local cert_key="${certificate_dir}/privkey.pem" + local cert_crt="${certificate_dir}/fullchain.pem" + if [ -n "${SSL_CERTIFICATE_KEY:-}" ] && [ -n "${SSL_CERTIFICATE_CRT:-}" ]; then + write_file_from_encoded_var "SSL_CERTIFICATE_KEY" "${cert_key}" + write_file_from_encoded_var "SSL_CERTIFICATE_CRT" "${cert_crt}" + else + generate_self_signed_cert "${fqdn}" "${cert_key}" "${cert_crt}" + fi + + /bin/bash "${CONFIG_PATH}/ssl/configure-certificate.sh" "${fqdn}" +} + +write_file_from_encoded_var() { + local var_name="${1}" + local file_name="${2}" + echo "${!var_name}" | base64 -d > "${file_name}" +} + +generate_self_signed_cert() { + local fqdn="${1}" + local key="${2}" + local cert="${3}" + + is_command_present openssl || os_pkg_install openssl + openssl req -newkey rsa:4096 \ + -x509 -sha256 -days 3650 -nodes \ + -out "${cert}" \ + -keyout "${key}" \ + -subj "/C=NA/ST=None/L=None/O=None/OU=None/CN=${fqdn}" +} + +install_phone_dialin() { + [ "${DIALIN_SIP_ACCOUNT_UID:-}" ] && [ -n "${DIALIN_SIP_PASSWORD:-}" ] && [ -n "${DIALIN_PSTN_NUMBERS:-}" ] || return 0 + + echo "jigasi jigasi/sip-account string ${DIALIN_SIP_ACCOUNT_UID}" | debconf-set-selections + echo "jigasi jigasi/sip-password password ${DIALIN_SIP_PASSWORD}" | debconf-set-selections + + os_pkg_install jigasi + # TODO: Configure dial in url and conference mapper + echo '{ + "message":"Phone numbers available.", + "numbers": {"Worldwide":[' "\"${DIALIN_PSTN_NUMBERS//,/\",\"}\"" ']}, + "numbersEnabled":true + }' > /usr/share/jitsi-meet/static/phoneNumberList.json +} + +ensure_jitsi_started() { + local services=(jicofo jitsi-videobridge2 prosody jigasi nginx) + for service in "${services[@]}"; do + os_svc restart "${service}" && log "restarted '${service}'" || log "did not restart '${service}'" + done +} + +enable_authentication() { + [ -n "${JITSI_MEETING_CREATOR_USERNAME:-}" ] && [ -n "${JITSI_MEETING_CREATOR_PASSWORD:-}" ] || return 0 + + local fqdn; fqdn=$(get_fully_qualified_hostname) + local jicofo_secret; jicofo_secret=$(grep -e '^JICOFO_SECRET=.*' /etc/jitsi/jicofo/config | cut -d '=' -f2) + local turn_secret; turn_secret=$(generate_passwd) + sed "s/JITSI_DOMAIN_NAME/${fqdn}/g; + s/JICOFO_SECRET/${jicofo_secret}/g; + s/TURN_SECRET/${turn_secret}/g" \ + "${CONFIG_PATH}/auth/prosody.cfg.lua" > "/etc/prosody/conf.avail/${fqdn}.cfg.lua" + + sed "s/JITSI_DOMAIN_NAME/${fqdn}/g" \ + "${CONFIG_PATH}/auth/meet-config.js" > "/etc/jitsi/meet/${fqdn}-config.js" + + ensure_key_value_present /etc/jitsi/jicofo/sip-communicator.properties \ + "org.jitsi.jicofo.auth.URL" "XMPP:${fqdn}" + + prosodyctl register "${JITSI_MEETING_CREATOR_USERNAME}" "${fqdn}" "${JITSI_MEETING_CREATOR_PASSWORD}" +} + +tweak_config() { + [ -z "${JITSI_WATERMARK_IMAGE_URL:-}" ] || \ + curl -Ls "${JITSI_WATERMARK_IMAGE_URL:-}" -o /usr/share/jitsi-meet/images/watermark.png + + local fqdn; fqdn=$(get_fully_qualified_hostname) + local js_config_file="/etc/jitsi/meet/${fqdn}-config.js" + [ -z "${JITSI_ENABLE_WELCOME_PAGE:-}" ] || \ + update_colon_separated_value "${js_config_file}" \ + "enableWelcomePage" "${JITSI_ENABLE_WELCOME_PAGE}" + + [ -z "${JITSI_START_AUDIO_MUTED:-}" ] || \ + update_colon_separated_value "${js_config_file}" \ + "startAudioMuted" "${JITSI_START_AUDIO_MUTED}" + + [ -z "${JITSI_START_VIDEO_MUTED:-}" ] || \ + update_colon_separated_value "${js_config_file}" \ + "startVideoMuted" "${JITSI_START_VIDEO_MUTED}" + + [ -z "${JITSI_REQUIRE_DISPLAY_NAME:-}" ] || \ + update_colon_separated_value "${js_config_file}" \ + "requireDisplayName" "${JITSI_REQUIRE_DISPLAY_NAME}" +} + +update_colon_separated_value() { + local file="${1}" + local key="${2}" + local value="${3}" + + sed -Ei "s/^(\s*)([\/#]*\s*)(${key}\s*:\s*)(.+?)(\s*),$/\1\3${value}\5,/" "${file}" +} + +ensure_key_value_present() { + local file="${1}"; shift + local key="${1}"; shift + local value="${*}" + + local tmp_file; tmp_file="$(mktemp)" + if [ -f "${file}" ]; then + grep -v "^${key}=" "${file}" > "${tmp_file}" + cp "${file}"{,"$(date +%s).bak"} + cp -f "${tmp_file}" "${file}" + else + mkdir -p "$(dirname "${file}")" + fi + + echo "${key}=${value}" >> "${file}" +} + +define_persisted_env_variable() { + local name="${1}"; shift + local value="${*}" + + ensure_key_value_present /etc/profile "export ${name}" "${value}" + eval "export ${name}='${value}'" +} + +validate_jitsi_install() { + if ! curl -sfL "https://$(get_fully_qualified_hostname)"; then + log "Cannot establish secure connection (is the certificate trusted?)" + curl -sfLk "https://$(get_fully_qualified_hostname)" + fi +} + +is_hook_provided() { + local name="${1}" + [ -n "$(hook_content "${name}")" ] +} + +invoke_hook() { + local name="${1}" + + is_hook_provided "${name}" || return 0 + + is_command_present curl || os_pkg_install curl + curl -sL "$(hook_content "${name}")" + log "Waiting for ${HOOKS_WAIT_TIME} seconds..." + sleep ${HOOKS_WAIT_TIME:-0} +} + +hook_content() { + local name="${1}" + local var_name="HOOKS_${name^^}_URL" + echo "${!var_name:-}" +} + +persist_hook() { + local name="${1}" + local event="${2}" + + is_hook_provided "${name}" || return 0 + { + crontab -l 2>/dev/null; echo "@${event} $(command -v curl) -sL '$(hook_content "${name}")'" + } | crontab - +} + +get_fully_qualified_hostname() { + local ec2_hostname; ec2_hostname=$(curl --connect-timeout 1 -s http://169.254.169.254/2019-10-01/meta-data/public-hostname || true) + ec2_hostname=${ec2_hostname:-$(hostname)} + + echo "${FULLY_QUALIFIED_HOSTNAME:-${ec2_hostname}}" +} + +get_hostname() { + local fqdn; fqdn=$(get_fully_qualified_hostname) + echo "${fqdn%%.*}" +} + +generate_passwd() { < /dev/urandom tr -dc '_A-Z-a-z-0-9@#' | head -c8; } +is_command_present() { command -v "${1}" >/dev/null 2>&1; } + +os_pkg_install() { DEBIAN_FRONTEND=noninteractive apt install -y "${@}"; } +os_pkg_repo_update() { apt update; } +os_pkg_system_update() { apt upgrade -y; } +os_firewall() { ufw "${@}"; } +os_svc() { service "${2}" "${1}"; } +os_svc_init() { systemctl "${@}"; } + +log() { echo "# [$(date)] $*"; } +die() { log "$*" >&2; exit 1; } + +main "${@}" diff --git a/jitsiinstallrc.example b/jitsiinstallrc.example new file mode 100644 index 0000000..fcdcfe7 --- /dev/null +++ b/jitsiinstallrc.example @@ -0,0 +1,49 @@ +# Hostname for this meet installation +FULLY_QUALIFIED_HOSTNAME=meet.example.com + +# Set to true to keep SSH port open in firewall +FIREWALL_ENABLE_SSH=true + +# +# Leave values for OPTIONAL attributes empty or comment out if not needed +# + +# OPTIONAL: +# Hooks to execute pre or post install; post install hook will also run on boot +#HOOKS_POST_INSTALL_URL='https://api.dynu.com/nic/update?hostname=meet.example.com&password=PASSWORD' +#HOOKS_PRE_INSTALL_URL='https://api.dynu.com/nic/update?hostname=meet.example.com&password=PASSWORD' +#HOOKS_WAIT_TIME=121 + + +# OPTIONAL: +# If none of the following SSL config is provided, +# a self-signed certificate will be created +# Provide email for letsencrypt (requires correct DNS entry) +#SSL_LETSENCRYPT_EMAIL=admin@example.com +# Alternatively, provide base64 encoded keys and certificates (without linebreaks) +#SSL_CERTIFICATE_CRT='base64 encoded key without linebreaks' +#SSL_CERTIFICATE_KEY='base64 encoded key without linebreaks' + + +# OPTIONAL: +# When a username and password provided here, only that user will be able to +# create or start a meeting. +#JITSI_MEETING_CREATOR_USERNAME=host +#JITSI_MEETING_CREATOR_PASSWORD=pass123 + + +# OPTIONAL: +# Original dimensions 612x272 px +#JITSI_WATERMARK_IMAGE_URL=https://github.com/jitsi/jitsi-meet/raw/master/images/watermark.png +# Various jitsi meet options (see https://meet.jit.si/config.js) +#JITSI_ENABLE_WELCOME_PAGE=false +#JITSI_START_AUDIO_MUTED=4 +#JITSI_START_VIDEO_MUTED=10 +#JITSI_REQUIRE_DISPLAY_NAME=true + + +# OPTIONAL: +# Phone dial in configuration (not implemented yet) +#DIALIN_SIP_ACCOUNT_UID=12345@sip-provider.net +#DIALIN_SIP_PASSWORD=PASSWORD +#DIALIN_PSTN_NUMBERS='+1.212.345.6789,+49.30.4567890'