diff --git a/pica-mumble-web/.dockerignore b/pica-mumble-web/.dockerignore new file mode 100644 index 0000000000000000000000000000000000000000..8d3188d51af5570cae953804a4db830dc67f7799 --- /dev/null +++ b/pica-mumble-web/.dockerignore @@ -0,0 +1,3 @@ +README.md +docker-compose.yml +secrets/ diff --git a/pica-mumble-web/Dockerfile b/pica-mumble-web/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..4640db03748866b90d1d70569a0fd7f06f47f84d --- /dev/null +++ b/pica-mumble-web/Dockerfile @@ -0,0 +1,33 @@ +FROM alpine:3.12 + +RUN echo http://nl.alpinelinux.org/alpine/edge/testing >> /etc/apk/repositories && \ + apk add --update --no-cache nodejs npm tini websockify git && \ + git clone --branch features/fabien/framasoft-theme https://framagit.org/fabien4vo/mumble-web.git /home/node && \ + adduser -D -g 1001 -u 1001 -h /home/node node && \ + mkdir -p /home/node && \ + mkdir -p /home/node/.npm-global && \ + mkdir -p /home/node/app && \ + chown -R node: /home/node + +USER node + +ENV PATH=/home/node/.npm-global/bin:$PATH +ENV NPM_CONFIG_PREFIX=/home/node/.npm-global + +RUN cd /home/node && \ + npm install && \ + npm run build + +USER root + +RUN apk del gcc git + +USER node + +COPY index.html /home/node/app/index.html + +EXPOSE 8080 +ENV MUMBLE_SERVER=murmur:64738 + +ENTRYPOINT ["/sbin/tini", "--"] +CMD websockify --ssl-target --web=/home/node/dist 8080 "$MUMBLE_SERVER" diff --git a/pica-mumble-web/clair-whitelist.yml b/pica-mumble-web/clair-whitelist.yml new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/pica-mumble-web/docker-compose.yml b/pica-mumble-web/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..d2aa8e74bb1bc48c8df4ce8e79e5813deddb7314 --- /dev/null +++ b/pica-mumble-web/docker-compose.yml @@ -0,0 +1,22 @@ +version: "2.4" +networks: + docker_default: + external: true + name: "docker_default" + +services: + mumble-web: + image: registry.picasoft.net/pica-mumble-web:1.3.0 + container_name: mumble-web + environment: + MUMBLE_SERVER: "murmur:64738" + networks: + - docker_default + volumes: + - /DATA/docker/volumes/murmur/mumble-web/config.js:/home/node/dist/config.local.js + labels: + - "traefik.frontend.rule=Host:voice.picasoft.net" + - "traefik.port=8080" + - "traefik.frontend.passHostHeader=true" + - "traefik.enable=true" + restart: unless-stopped diff --git a/pica-mumble-web/index.html b/pica-mumble-web/index.html new file mode 100644 index 0000000000000000000000000000000000000000..70409982e1268cf4aa89d4f7b25f1b852a22399f --- /dev/null +++ b/pica-mumble-web/index.html @@ -0,0 +1,652 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8"> + <!-- Favicon as generated by realfavicongenerator.net (slightly modified for webpack) --> + <link rel="apple-touch-icon" sizes="180x180" href="favicon/apple-touch-icon.png"> + <link rel="icon" type="image/png" href="favicon/favicon-32x32.png" sizes="32x32"> + <link rel="icon" type="image/png" href="favicon/favicon-16x16.png" sizes="16x16"> + <link rel="manifest" href="favicon/manifest.json"> + <link rel="mask-icon" href="favicon/safari-pinned-tab.svg" color="#5bbad5"> + <link rel="shortcut icon" href="favicon/favicon.ico"> + <meta name="apple-mobile-web-app-title" content="Mumble"> + <meta name="application-name" content="Mumble"> + <meta name="msapplication-config" content="${require('./favicon/browserconfig.xml')}"> + <meta name="theme-color" content="#ffffff"> + + <script src="config.js"></script> + <script src="config.local.js"></script> + <script src="theme.js"></script> + <script src="matrix.js"></script> + </head> + <body> + <div class="loading-container" data-bind="css: { loaded: true }"> + <div class="loading-circle" data-bind="css: { loaded: true }"><div></div><div><div></div></div></div> + </div> + <div id="container" style="display: none" data-bind="visible: true, + css: { minimal: minimalView }"> + + <div class="dialog-overlay" data-bind="visible: connectDialog.visible() && config.defaults.easyMode"> + <div class="startup-dialog dialog"> + <div class="dialog-header" data-bind="text: connected() ? 'Connecté' : 'Préparation'"></div> + <div style="text-align: center">Bienvenue sur le Mumble de Picasoft !</div> + <div style="text-align: center">Ceci est l'interface web, elle permet d'utiliser le service, mais offre beaucoup moins de fonctionnalités que le client lourd. Vous pouvez consulter la <a href="https://voice.picasoft.net/doc">Documentation</a> pour plus d'informations.</div> + <form data-bind="submit: connectDialog.connect"> + <!-- ko if: !connected() --> + <fieldset> + <legend>Rejoindre le salon <span data-bind="text: connectDialog.channelName() == '' ? 'principal' : connectDialog.channelName()"></span></legend> + <label data-bind="visible: !connectDialog.loading()"> + Quel est votre nom ? + <input type="text" data-bind="textInput: connectDialog.username" required autofocus> + </label> + <div data-bind="visible: connectDialog.loading()"> + Connexion en tant que <span data-bind="text: connectDialog.username"></span>, + merci de patienter ... + <div class="small"><div class="loading-container"> + <div class="loading-circle"><div></div><div><div></div></div></div> + </div></div> + </div> + <input class="dialog-submit" type="submit" data-bind="{ + value: !connected() && !connectDialog.loading() ? 'Participer' : 'Connexion...', + disable: !connectDialog.username() || connectDialog.loading() + }"> + </fieldset> + <!-- /ko --> + <!-- ko if: connected() --> + <fieldset> + Bonjour <span data-bind="text: thisUser().name()"></span>, + vous voilà connecté sur le salon <span data-bind="text: thisUser().channel().name()"></span>. + <!-- ko if: thisUser().channel() --> + <p>Il y a <span data-bind="text: thisUser().channel().users().length"></span> participant·e·s à ce salon</p> + <!-- /ko --> + <ul data-bind="foreach: thisUser().channel().users()" style="text-align: left"> + <li> + <span data-bind="{text: name, style: {fontWeight: name() == $parent.thisUser().name() ? 'bold': 'normal'}}"></span> + <div data-bind="class: talking" class="talking"><div><div data-bind="text: talking"></div><div></div></div></div> + </li> + </ul> + <button class="dialog-submit" data-bind="click: connectDialog.hide">Interface complète</button> + </fieldset> + <!-- /ko --> + </form> + </div> + </div> + + <!-- ko with: connectDialog --> + <div class="connect-dialog dialog" data-bind="visible: visible() && !joinOnly() && !config.defaults.easyMode"> + <div class="dialog-header"> + Connect to Server + </div> + <form data-bind="submit: connect"> + <table> + <tr data-bind="if: $root.config.connectDialog.address"> + <td>Address</td> + <td><input id="address" type="text" data-bind="value: address" required></td> + </tr> + <tr data-bind="if: $root.config.connectDialog.port"> + <td>Port</td> + <td><input id="port" type="text" data-bind="value: port" required></td> + </tr> + <tr data-bind="if: $root.config.connectDialog.username"> + <td>Username</td> + <td><input id="username" type="text" data-bind="value: username" required></td> + </tr> + <tr data-bind="if: $root.config.connectDialog.password"> + <td>Password</td> + <td><input id="password" type="password" data-bind="value: password"></td> + </tr> + <tr data-bind="if: $root.config.connectDialog.token"> + <td>Tokens</td> + <td> + <input type="text" data-bind='value: tokenToAdd, valueUpdate: "afterkeydown"'> + </td> + </tr> + <tr data-bind="if: $root.config.connectDialog.token"> + <td></td> + <td> + <button class="dialog-submit" type="button" data-bind="enable: selectedTokens().length > 0, click: removeSelectedTokens()">Remove</button> + <button class="dialog-submit" type="button" data-bind="enable: tokenToAdd().length > 0, click: addToken()">Add</button> + </td> + </tr> + <tr data-bind="if: $root.config.connectDialog.token, visible: tokens().length > 0"> + <td></td> + <td><select id="token" multiple="multiple" height="5" data-bind="options:tokens, selectedOptions:selectedTokens"></select></td> + </tr> + <tr data-bind="if: $root.config.connectDialog.channelName"> + <td>Channel</td> + <td><input id="channelName" type="text" data-bind="value: channelName"></td> + </tr> + </table> + <div class="dialog-footer"> + <input class="dialog-close" type="button" data-bind="click: hide" value="Cancel"> + <input class="dialog-submit" type="submit" value="Connect"> + </div> + </form> + </div> + <!-- /ko --> + <!-- ko with: connectDialog --> + <div class="join-dialog dialog" data-bind="visible: visible() && joinOnly()"> + <div class="dialog-header"> + Mumble Voice Conference + </div> + <form data-bind="submit: connect"> + <input class="dialog-submit" type="submit" value="Join Conference"> + </form> + </div> + <!-- /ko --> + <!-- ko with: connectErrorDialog --> + <div class="connect-dialog error-dialog dialog" data-bind="visible: visible()"> + <div class="dialog-header"> + Failed to connect + </div> + <form data-bind="submit: connect"> + <table> + <tr> + <td colspan=2> + <!-- ko if: type() == 0 || type() == 8 --> + The connection has been refused. + <!-- /ko --> + <!-- ko if: type() == 1 --> + The server uses an incompatible version. + <!-- /ko --> + <!-- ko if: type() == 2 --> + Your user name was rejected. Maybe try a different one? + <!-- /ko --> + <!-- ko if: type() == 3 --> + The given password is incorrect. + The user name you have chosen requires a special one. + <!-- /ko --> + <!-- ko if: type() == 4 --> + The given password is incorrect. + <!-- /ko --> + <!-- ko if: type() == 5 --> + The user name you have chosen is already in use. + <!-- /ko --> + <!-- ko if: type() == 6 --> + The server is full. + <!-- /ko --> + <!-- ko if: type() == 7 --> + The server requires you to provide a client certificate + which is not supported by this web application. + <!-- /ko --> + <br> + The server reports: + <br> + "<span class="connect-error-reason" data-bind="text: reason"></span>" + </td> + </tr> + <tr data-bind="if: type() == 2 || type() == 3 || type() == 5"> + <td>Username</td> + <td><input id="username" type="text" data-bind="value: username" required></td> + </tr> + <tr data-bind="if: type() == 3 || type() == 4"> + <td>Password</td> + <td><input id="password" type="password" data-bind="value: password" required></td> + </tr> + </table> + <div class="dialog-footer"> + <input class="dialog-close" type="button" value="Cancel" + data-bind="click: hide, visible: !joinOnly()"> + <input class="dialog-submit" type="submit" value="Retry"> + </div> + </form> + </div> + <!-- /ko --> + <!-- ko with: connectionInfo --> + <div class="connection-info-dialog dialog" data-bind="visible: visible"> + <div class="dialog-header"> + Connection Information + </div> + <div class="dialog-content"> + <h3>Version</h3> + <!-- ko with: serverVersion --> + Protocol + <span data-bind="text: major + '.' + minor + '.' + patch"></span>. + <br> + <br> + <span data-bind="text: release"></span> + <br> + <span data-bind="text: os"></span> + <span data-bind="text: osVersion"></span> + <br> + <!-- /ko --> + <!-- ko if: !serverVersion() --> + Unknown + <!-- /ko --> + + <h3>Control channel</h3> + <span data-bind="text: latencyMs().toFixed(2)"></span> ms average latency + (<span data-bind="text: latencyDeviation().toFixed(2)"></span> deviation) + <br> + <br> + Remote host <span data-bind="text: remoteHost"></span> + (port <span data-bind="text: remotePort"></span>) + <br> + + <h3>Audio bandwidth</h3> + Maximum <span data-bind="text: (maxBitrate()/1000).toFixed(1)"></span> kbits/s + (<span data-bind="text: (maxBandwidth()/1000).toFixed(1)"></span> kbits/s with overhead) + <br> + Current <span data-bind="text: (currentBitrate()/1000).toFixed(1)"></span> kbits/s + (<span data-bind="text: (currentBandwidth()/1000).toFixed(1)"></span> kbits/s with overhead) + <br> + Codec: <span data-bind="text: codec"></span> + </div> + <div class="dialog-footer"> + <input class="dialog-close" type="button" data-bind="click: hide" value="OK"> + </div> + </div> + <!-- /ko --> + <!-- ko with: settingsDialog --> + <div class="settings-dialog dialog" data-bind="visible: $data"> + <div class="dialog-header"> + Settings + </div> + <form data-bind="submit: $root.applySettings"> + <table> + <tr> + <td>Transmission</td> + <td> + <select data-bind='value: voiceMode'> + <option value="cont">Continuous</option> + <option value="vad">Voice Activity</option> + <option value="ptt">Push To Talk</option> + </td> + </tr> + <tr data-bind="visible: voiceMode() == 'vad'"> + <td colspan="2"> + <div class="mic-volume-container"> + <div class="mic-volume" data-bind="style: { + width: testVadLevel()*100 + '%', + background: testVadActive() ? 'green' : 'red' + }"></div> + </div> + <input type="range" min="0" max="1" step="0.01" + data-bind="value: vadLevel"> + </td> + </tr> + <tr data-bind="visible: voiceMode() == 'ptt'"> + <td>PTT Key</td> + <td> + <input type="button" data-bind="value: pttKeyDisplay, click: recordPttKey"> + </td> + </tr> + <tr> + <td>Audio Quality</td> + <td><span data-bind="text: (audioBitrate()/1000).toFixed(1)"></span> kbit/s</td> + </tr> + <tr> + <td colspan="2"> + <input type="range" min="8000" max="96000" step="8" + data-bind="value: audioBitrate, valueUpdate: 'input'"> + </td> + </tr> + <tr> + <td>Audio per packet</td> + <td><span data-bind="text: msPerPacket"></span> ms</td> + </tr> + <tr> + <td colspan="2"> + <input type="range" min="10" max="60" step="10" + data-bind="value: msPerPacket, valueUpdate: 'input'"> + </td> + </tr> + <tr> + <td colspan="2" class="bandwidth-info"> + <span data-bind="text: (totalBandwidth()/1000).toFixed(1)"></span> + kbit/s + (Audio + <span data-bind="text: (audioBitrate()/1000).toFixed(1)"></span>, + Position + <span data-bind="text: (positionBandwidth()/1000).toFixed(1)"></span>, + Overhead + <span data-bind="text: (overheadBandwidth()/1000).toFixed(1)"></span>) + </td> + </tr> + <tr> + <td>Show Avatars</td> + <td> + <select data-bind='value: showAvatars'> + <option value="always">Always</option> + <option value="own_channel">Same Channel</option> + <option value="linked_channel">Linked Channels</option> + <option value="minimal_only">Minimal View</option> + <option value="never">Never</option> + </td> + </tr> + <tr> + <td colspan="2"> + <input type="checkbox" data-bind="checked: userCountInChannelName"> + Show user count after channel name + </td> + </tr> + </table> + <div class="dialog-footer"> + <input class="dialog-close" type="button" data-bind="click: $root.closeSettings" value="Cancel"> + <input class="dialog-submit" type="submit" value="Apply"> + </div> + </form> + </div> + <!-- /ko --> + <img class="avatar-view" data-bind="visible: avatarView, attr: { src: avatarView }, + click: function () { avatarView(null) }"></img> + <!-- ko with: userContextMenu --> + <ul class="context-menu" data-bind="if: target, + style: { left: posX() + 'px', + top: posY() + 'px' }"> + + <!-- ko with: target --> + <li data-bind="css: { disabled: !canChangeMute() }"> + Mute + </li> + <li data-bind="css: { disabled: !canChangeDeafen() }"> + Deafen + </li> + <li data-bind="css: { disabled: !canChangePrioritySpeaker() }"> + Priority Speaker + </li> + + <li data-bind="css: { disabled: !canLocalMute() }"> + Local Mute + </li> + <li data-bind="css: { disabled: !canIgnoreMessages() }"> + Ignore Messages + </li> + + <li data-bind="css: { disabled: !canChangeComment() }, visible: comment"> + View Comment + </li> + <!-- ko if: $data === $root.thisUser() --> + <li data-bind="css: { disabled: !canChangeComment() }, visible: true"> + Change Comment + </li> + <!-- /ko --> + <!-- ko if: $data !== $root.thisUser() --> + <li data-bind="css: { disabled: !canChangeComment() }, visible: comment"> + Reset Comment + </li> + <!-- /ko --> + + <li data-bind="css: { disabled: !canChangeAvatar() }, visible: texture, + click: viewAvatar"> + View Avatar + </li> + <!-- ko if: $data === $root.thisUser() --> + <li data-bind="css: { disabled: !canChangeAvatar() }, visible: true, + click: changeAvatar"> + Change Avatar + </li> + <!-- /ko --> + <li data-bind="css: { disabled: !canChangeAvatar() }, visible: texture, + click: removeAvatar"> + Reset Avatar + </li> + + <li data-bind="css: { disabled: true }, visible: true"> + Send Message + </li> + <li data-bind="css: { disabled: true }, visible: true"> + Information + </li> + + <li data-bind="visible: $data === $root.thisUser(), + css: { checked: selfMute }, + click: toggleMute"> + Self Mute + </li> + <li data-bind="visible: $data === $root.thisUser(), + css: { checked: selfDeaf }, + click: toggleDeaf"> + Self Deafen + </li> + + <!-- ko if: $data !== $root.thisUser() --> + <li data-bind="css: { disabled: true }, visible: true"> + Add Friend + </li> + <li data-bind="css: { disabled: true }, visible: false"> + Remove Friend + </li> + <!-- /ko --> + + <!-- /ko --> + </ul> + <!-- /ko --> + <!-- ko with: channelContextMenu --> + <ul class="context-menu" data-bind="if: target, + style: { left: posX() + 'px', + top: posY() + 'px' }"> + + <!-- ko with: target --> + <li data-bind="visible: users.indexOf($root.thisUser()) === -1, + css: { disabled: !canJoin() }, + click: $root.requestMove.bind($root, $root.thisUser())"> + Join Channel + </li> + <li data-bind="css: { disabled: !canAdd() }"> + Add + </li> + <li data-bind="css: { disabled: !canEdit() }"> + Edit + </li> + <li data-bind="css: { disabled: !canRemove() }"> + Remove + </li> + + <li data-bind="css: { disabled: !canLink() }"> + Link + </li> + <li data-bind="css: { disabled: !canUnlink() }"> + Unlink + </li> + <li data-bind="css: { disabled: !canUnlink() }"> + Unlink All + </li> + + <li data-bind="css: { disabled: true }"> + Copy Mumble URL + </li> + <li data-bind="css: { disabled: true }"> + Copy Mumble-Web URL + </li> + <li data-bind="css: { disabled: !canSendMessage() }"> + Send Message + </li> + + <!-- /ko --> + </ul> + <!-- /ko --> + <script type="text/html" id="user-tag"> + <span class="user-tag" data-bind="text: name"></span> + </script> + <script type="text/html" id="channel-tag"> + <span class="channel-tag" data-bind="text: name"></span> + </script> + <div class="toolbar" data-bind="css: { 'toolbar-horizontal': toolbarHorizontal(), + 'toolbar-vertical': !toolbarHorizontal() }"> + <img class="handle-horizontal" src="/svg/handle_horizontal.svg" + data-bind="click: toggleToolbarOrientation"> + <img class="handle-vertical" src="/svg/handle_vertical.svg" + data-bind="click: toggleToolbarOrientation"> + <img class="tb-connect" data-bind="visible: !connectDialog.joinOnly(), + click: connectDialog.show" + rel="connect" src="/svg/applications-internet.svg"> + <img class="tb-information" rel="information" src="/svg/information_icon.svg" + data-bind="click: connectionInfo.show, + css: { disabled: !thisUser() }"> + <div class="divider"></div> + <img class="tb-mute" data-bind="visible: !selfMute(), + click: function () { requestMute(thisUser()) }" + rel="mute" src="/svg/audio-input-microphone.svg"> + <img class="tb-unmute tb-active" data-bind="visible: selfMute, + click: function () { requestUnmute(thisUser()) }" + rel="unmute" src="/svg/audio-input-microphone-muted.svg"> + <img class="tb-deaf" data-bind="visible: !selfDeaf(), + click: function () { requestDeaf(thisUser()) }" + rel="deaf" src="/svg/audio-output.svg"> + <img class="tb-undeaf tb-active" data-bind="visible: selfDeaf, + click: function () { requestUndeaf(thisUser()) }" + rel="undeaf" src="/svg/audio-output-deafened.svg"> + <img class="tb-record" data-bind="click: function(){}" + rel="record" src="/svg/media-record.svg"> + <div class="divider"></div> + <img class="tb-comment" data-bind="click: commentDialog.show" + rel="comment" src="/svg/toolbar-comment.svg"> + <div class="divider"></div> + <img class="tb-settings" data-bind="click: openSettings" + rel="settings" src="/svg/config_basic.svg"> + <div class="divider"></div> + <img class="tb-sourcecode" data-bind="click: openSourceCode" + rel="Source Code" src="/svg/source-code.svg"> + </div> + <div class="chat"> + <script type="text/html" id="log-generic"> + <span data-bind="text: value"></span> + </script> + <script type="text/html" id="log-welcome-message"> + Welcome message: <span data-bind="html: message"></span> + </script> + <script type="text/html" id="log-chat-message"> + <span data-bind="visible: channel"> + (Channel) + </span> + <span data-bind="template: { name: 'user-tag', data: user }"></span>: + <span class="message-content" data-bind="html: message"></span> + </script> + <script type="text/html" id="log-chat-message-self"> + To + <span data-bind="template: { if: $data.channel, name: 'channel-tag', data: $data.channel }"> + </span><span data-bind="template: { if: $data.user, name: 'user-tag', data: $data.user }"> + </span>: + <span class="message-content" data-bind="html: message"></span> + </script> + <script type="text/html" id="log-disconnect"> + </script> + <div class="log" data-bind="foreach: { + data: log, + afterRender: function (e) { + [].forEach.call(e[1].getElementsByTagName('a'), function(e){e.target = '_blank'}) + } + }"> + <div class="log-entry"> + <span class="log-timestamp" data-bind="text: $root.getTimeString()"></span> + <!-- ko template: { data: $data, name: function(l) { return 'log-' + l.type; } } --> + <!-- /ko --> + </div> + </div> + <form data-bind="submit: submitMessageBox"> + <input id="message-box" type="text" data-bind=" + attr: { placeholder: messageBoxHint }, textInput: messageBox"> + </form> + </div> + <script type="text/html" id="channel"> + <div class="channel" data-bind=" + click: $root.select, + event: { + contextmenu: openContextMenu, + dblclick: $root.requestMove.bind($root, $root.thisUser()) + }, + css: { + selected: $root.selected() === $data, + currentChannel: users.indexOf($root.thisUser()) !== -1 + }"> + <div class="channel-status"> + <img class="channel-description" data-bind="visible: description" + alt="description" src="/svg/comment.svg"> + </div> + <div data-bind="if: description"> + <div class="channel-description tooltip" data-bind="html: description"></div> + </div> + <img class="channel-icon" src="/svg/channel.svg" + data-bind="visible: !linked() && $root.thisUser().channel() !== $data"> + <img class="channel-icon-active" src="/svg/channel_active.svg" + data-bind="visible: $root.thisUser().channel() === $data"> + <img class="channel-icon-linked" src="/svg/channel_linked.svg" + data-bind="visible: linked() && $root.thisUser().channel() !== $data"> + <div class="channel-name"> + <span data-bind="text: name"></span> + <!-- ko if: $root.settings.userCountInChannelName() && userCount() !== 0 --> + (<span data-bind="text: userCount()"></span>) + <!-- /ko --> + </div> + </div> + <!-- ko if: expanded --> + <!-- ko foreach: users --> + <div class="user-wrapper"> + <div class="user-tree"></div> + <div class="user" data-bind=" + click: $root.select, + event: { + contextmenu: openContextMenu + }, + css: { + thisClient: $root.thisUser() === $data, + selected: $root.selected() === $data + }"> + <div class="user-status" data-bind="attr: { title: state }"> + <img class="user-comment" data-bind="visible: comment" + alt="comment" src="/svg/comment.svg"> + <img class="user-server-mute" data-bind="visible: mute" + alt="server mute" src="/svg/muted_server.svg"> + <img class="user-suppress-mute" data-bind="visible: suppress" + alt="suppressed" src="/svg/muted_suppressed.svg"> + <img class="user-self-mute" data-bind="visible: selfMute" + alt="self mute" src="/svg/muted_self.svg"> + <img class="user-server-deaf" data-bind="visible: deaf" + alt="server deaf" src="/svg/deafened_server.svg"> + <img class="user-self-deaf" data-bind="visible: selfDeaf" + alt="self deaf" src="/svg/deafened_self.svg"> + <img class="user-authenticated" data-bind="visible: uid" + alt="authenticated" src="/svg/authenticated.svg"> + </div> + <div data-bind="if: comment"> + <div class="user-comment tooltip" data-bind="html: comment"></div> + </div> + <!-- ko if: show_avatar() --> + <img class="user-avatar" alt="avatar" + data-bind="attr: { src: texture }, + css: { 'user-avatar-talk-off': talking() == 'off', + 'user-avatar-talk-on': talking() == 'on', + 'user-avatar-talk-whisper': talking() == 'whisper', + 'user-avatar-talk-shout': talking() == 'shout' }"> + <!-- /ko --> + <!-- ko ifnot: show_avatar() --> + <img class="user-talk user-talk-off" data-bind="visible: talking() == 'off'" + alt="talk off" src="/svg/talking_off.svg"> + <img class="user-talk user-talk-on" data-bind="visible: talking() == 'on'" + alt="talk on" src="/svg/talking_on.svg"> + <img class="user-talk user-talk-whisper" data-bind="visible: talking() == 'whisper'" + alt="whisper" src="/svg/talking_whisper.svg"> + <img class="user-talk user-talk-shout" data-bind="visible: talking() == 'shout'" + alt="shout" src="/svg/talking_alt.svg"> + <!-- /ko --> + <div class="user-name" data-bind="text: name"></div> + </div> + </div> + <!-- /ko --> + <!-- ko foreach: channels --> + <div class="channel-wrapper"> + <!-- ko ifnot: users().length || channels().length --> + <div class="channel-tree"></div> + <!-- /ko --> + <div class="branch" data-bind="if: users().length || channels().length"> + <img class="branch-open" src="/svg/branch_open.svg" + data-bind="click: expanded.bind($data, false), visible: expanded()"> + <img class="branch-closed" src="/svg/branch_closed.svg" + data-bind="click: expanded.bind($data, true), visible: !expanded()"> + </div> + <div class="channel-sub" data-bind="template: {name: 'channel', data: $data}"></div> + </div> + <!-- /ko --> + <!-- /ko --> + </script> + <div class="channel-root-container" data-bind="if: root, visible: !minimalView()"> + <div class="channel-root" data-bind="template: {name: 'channel', data: root}"></div> + </div> + <div class="channel-root-container" data-bind="if: thisUser, visible: minimalView()"> + <div class="channel-root" data-bind="template: {name: 'channel', data: thisUser().channel}"></div> + </div> + </div> + </body> + <script src="index.js"></script> +</html> diff --git a/pica-murmur/.dockerignore b/pica-murmur/.dockerignore new file mode 100644 index 0000000000000000000000000000000000000000..8d3188d51af5570cae953804a4db830dc67f7799 --- /dev/null +++ b/pica-murmur/.dockerignore @@ -0,0 +1,3 @@ +README.md +docker-compose.yml +secrets/ diff --git a/pica-murmur/Dockerfile b/pica-murmur/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..0cfd9bd253034b1d9c1f146e33d336c03f95dc6f --- /dev/null +++ b/pica-murmur/Dockerfile @@ -0,0 +1,28 @@ +FROM debian:buster-slim + +# Install Mumble server +RUN apt-get update -y \ + && apt-get dist-upgrade -y \ + && apt-get install -y \ + libssl-dev \ + libbz2-dev \ + mumble-server=1.3.0~git20190125.440b173+dfsg-2 \ + pcproc \ + python3 \ + python3-pip \ + zeroc-ice-slice \ + && rm -rf /var/lib/apt/lists/* + +# Install prometheus exporter and Python dependencies +COPY requirements.txt / +RUN pip3 install --no-cache-dir -r requirements.txt +COPY exporter.py / + +EXPOSE 64738/tcp 64738/udp 8000/tcp + +# Volume for persistent storage (config file and database) +VOLUME ["/data"] + +# Add entrypoint +COPY entrypoint.sh / +ENTRYPOINT ["/entrypoint.sh"] diff --git a/pica-murmur/README.md b/pica-murmur/README.md new file mode 100644 index 0000000000000000000000000000000000000000..94e274429f16a12b15ccbdbde70fe1b1d100d00e --- /dev/null +++ b/pica-murmur/README.md @@ -0,0 +1,15 @@ +# Murmur + +Docker image for a simple Mumble server (Murmur) + +## Configuration +Some environment variables allow to configure Murmur at startup : +- `MAX_BANDWIDTH` : integer, maximum bandwidth clients are allowed speech at (default is `128000` bps) +- `MAX_USERS` : integer, maximum number of users on the server (default is `100`) +- `METRICS_SERVER_LABEL` : Name of this Murmur server to add as metrics label + +## Mounted volumes +Murmur store its server database and configuration files under `/data/` folder. You should mount this directory on your host. + +## Network +Murmur is listening both TCP and UDP on port 64738. You should bind this container port to your host. diff --git a/pica-murmur/clair-whitelist.yml b/pica-murmur/clair-whitelist.yml new file mode 100644 index 0000000000000000000000000000000000000000..82e705c5a9dda07792d93b2b24af4eb1e201b693 --- /dev/null +++ b/pica-murmur/clair-whitelist.yml @@ -0,0 +1,12 @@ +generalwhitelist: + CVE-2019-19816: On utilise pas btrfs + CVE-2019-19814: On utilise pas f2fs + CVE-2019-19074: ¯\_(ツ)_/¯ + CVE-2020-10543: ¯\_(ツ)_/¯ + CVE-2020-10878: ¯\_(ツ)_/¯ + CVE-2019-19813: On utilise pas btrfs + CVE-2019-19815: On utilise pas f2fs + CVE-2020-8492: ¯\_(ツ)_/¯ + CVE-2013-7445: ¯\_(ツ)_/¯ + CVE-2020-13974: DISPUTED + CVE-2020-14155: ¯\_(ツ)_/¯ diff --git a/pica-murmur/docker-compose.yml b/pica-murmur/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..b05c0c11dccb70e3fc805157d8de3cca311dbd5e --- /dev/null +++ b/pica-murmur/docker-compose.yml @@ -0,0 +1,33 @@ +version: "2.4" +networks: + docker_default: + external: true + name: "docker_default" + +volumes: + murmur-data: + name: murmur-data + +services: + murmur: + image: registry.picasoft.net/pica-murmur:1.3.0 + container_name: murmur + environment: + MAX_USERS: 2000 + METRICS_SERVER_LABEL: voice.picasoft.net + ports: + - "64738:64738" + - "64738:64738/udp" + volumes: + - murmur-data:/data + - /DATA/docker/certs/voice.picasoft.net/:/certs + networks: + - docker_default + labels: + - "traefik.enable=true" + - "traefik.port=8000" + - "traefik.frontend.rule=Host:voice.picasoft.net;Path:/metrics" + - "tls-certs-monitor.enable=true" + - "tls-certs-monitor.action=kill:SIGUSR1" + - "tls-certs-monitor.owner=103" + restart: unless-stopped diff --git a/pica-murmur/entrypoint.sh b/pica-murmur/entrypoint.sh new file mode 100755 index 0000000000000000000000000000000000000000..3b28d616911f3a78a3207181db55bcb0034164e1 --- /dev/null +++ b/pica-murmur/entrypoint.sh @@ -0,0 +1,36 @@ +#!/bin/bash + +CONFIG_FILE="/data/mumble-server.ini" +MAX_BANDWIDTH=${MAX_BANDWIDTH:-128000} +MAX_USERS=${MAX_USERS:-100} + +# Copy configuration file if not exists +if [ ! -f $CONFIG_FILE ] +then + cp /etc/mumble-server.ini $CONFIG_FILE +fi + +# Change configuration +sed -i -E "s/^(\/\/ )?database( )?=.*/database=\/data\/murmur.sqlite/g" $CONFIG_FILE +sed -i -E "s/^(\/\/ )?logfile( )?=.*/logfile=/g" $CONFIG_FILE +sed -i -E "s/^(\/\/ )?bandwidth( )?=.*/bandwidth=$MAX_BANDWIDTH/g" $CONFIG_FILE +sed -i -E "s/^(\/\/ )?users( )?=.*/users=$MAX_USERS/g" $CONFIG_FILE + +# Set correct rights on murmur files +chown -R mumble-server:mumble-server /data + +# Run exporter if variable set +if [ -n "$METRICS_SERVER_LABEL" ] +then + python3 /exporter.py & +fi + +# Trap SIGUSR1 signal to reload certificates without restarting +_reload() { + echo "Caught SIGUSR1 signal!" + /usr/bin/pkill -USR1 murmurd +} +trap _term SIGUSR1 + +# Run murmur +murmurd -fg -v -ini $CONFIG_FILE diff --git a/pica-murmur/exporter.py b/pica-murmur/exporter.py new file mode 100644 index 0000000000000000000000000000000000000000..0c806b73c143f13ba8e0e000b80c65d030e5b569 --- /dev/null +++ b/pica-murmur/exporter.py @@ -0,0 +1,151 @@ +# This script is adapted from another script developped by +# Stefan Hacker <dd0t@users.sourceforge.net> : https://github.com/Natenom/munin-plugins/tree/master/murmur + +# Imports +import tempfile +import os +import sys +import signal +import time +import IcePy +import Ice +from prometheus_client import start_http_server, Counter, Gauge, REGISTRY, PROCESS_COLLECTOR, PLATFORM_COLLECTOR + +# Path to Murmur.ice; the script tries first to retrieve this file dynamically from Murmur itself; if this fails it tries this file. +SLICE_FILE = "/usr/share/slice/Murmur.ice" +# Ice.MessageSizeMax from Murmur server +ICE_MESSAGE_SIZE_MAX = "65535" +# ICE connection variable +ICE_HOST = "127.0.0.1" +ICE_PORT = 6502 +PROXY_STRING = "Meta -e 1.0:tcp -h %s -p %d -t 1000" % (ICE_HOST, ICE_PORT) + +# Number of seconds between 2 metrics collection +COLLECT_INTERVAL = os.getenv('EXPORTER_COLLECT_INTERVAL', 10) + +# Get server name +METRICS_SERVER_LABEL = os.getenv('METRICS_SERVER_LABEL') +if not METRICS_SERVER_LABEL: + print('Must define METRICS_SERVER_LABEL environment variable !') + sys.exit(1) + +# Prepare ICE properties +ice_props = Ice.createProperties() +ice_props.setProperty("Ice.ImplicitContext", "Shared") +ice_props.setProperty("Ice.MessageSizeMax", str(ICE_MESSAGE_SIZE_MAX)) +# Initialize ICE +idata = Ice.InitializationData() +idata.properties = ice_props +ice = Ice.initialize(idata) + +########################################################################################### +###### This part is (almost) an entire copy of the original script to connect to ICE ###### +####################### To be clear : I HAVE NO IDEA WHAT I'M DOING ####################### +########################################################################################### + +connection_done = False +while not connection_done: + try: + ice_proxy = ice.stringToProxy(PROXY_STRING) + + # Get slice directory + slice_dir = Ice.getSliceDir() + slice_dir = ['-I' + slice_dir] + + try: + op = IcePy.Operation('getSlice', Ice.OperationMode.Idempotent, Ice.OperationMode.Idempotent, + True, None, (), (), (), ((), IcePy._t_string, False, 0), ()) + + slice = op.invoke(ice_proxy, ((), None)) + (dynslicefiledesc, dynslicefilepath) = tempfile.mkstemp(suffix='.ice') + dynslicefile = os.fdopen(dynslicefiledesc, 'w') + dynslicefile.write(slice) + dynslicefile.flush() + Ice.loadSlice('', slice_dir + [dynslicefilepath]) + dynslicefile.close() + os.remove(dynslicefilepath) + except Exception as e: + try: + Ice.loadSlice('', slice_dir + [SLICE_FILE]) + except: + raise Ice.ConnectionRefusedException + + import Murmur + + # Check connection is working + Murmur.MetaPrx.checkedCast(ice_proxy) + connection_done = True + + except Ice.ConnectionRefusedException: + print('Cannot connect exporter to ICE, retry in 5 seconds') + time.sleep(5) + +########################################################################################### +######################## End of the "NO IDEA WHAT I'M DOING PART" ######################### +########################################################################################### + +# Remove unwanted Prometheus metrics +[REGISTRY.unregister(c) for c in [PROCESS_COLLECTOR, PLATFORM_COLLECTOR, + REGISTRY._names_to_collectors['python_gc_objects_collected_total']]] + +# Start Prometheus exporter server +start_http_server(8000) + +# Register metrics +users_all_gauge = Gauge('murmur_online_users_all', 'Number of online users', ['server']) +users_unregistered_gauge = Gauge('murmur_online_users_unregistered', + 'Number of online unregistered users', ['server']) +users_registered_gauge = Gauge('murmur_online_users_registered', 'Number of online registered users', ['server']) +users_muted_gauge = Gauge('murmur_online_users_muted', 'Number of online muted users', ['server']) +users_banned_gauge = Gauge('murmur_users_banned', 'Number of banned users', ['server']) +chan_count_gauge = Gauge('murmur_channels', 'Number of channels', ['server']) +uptime_gauge = Gauge('murmur_uptime', 'Number of seconds the server is uptime', ['server']) + + +def exit_handler(sig, frame): + # Define handler for stop signals + print('Terminating...') + ice.destroy() + sys.exit(0) + + +# Catch several signals +signal.signal(signal.SIGINT, exit_handler) +signal.signal(signal.SIGTERM, exit_handler) + + +# Loop forever +while True: + # Get data from Murmur server + meta = Murmur.MetaPrx.checkedCast(ice_proxy) + server = meta.getServer(1) + + # Initialize metrics counters + users_muted_count = 0 + users_unregistered_count = 0 + users_registered_count = 0 + + # Collect and count users + onlineusers = server.getUsers() + for key in onlineusers.keys(): + # Count user as registered + if onlineusers[key].userid == -1: + users_unregistered_count += 1 + # Count user as not registered + if onlineusers[key].userid > 0: + users_registered_count += 1 + # Count muted users + if onlineusers[key].mute or onlineusers[key].selfMute or onlineusers[key].suppress: + users_muted_count += 1 + + # Set metrics + users_all_gauge.labels(server=METRICS_SERVER_LABEL).set(len(onlineusers)) + users_muted_gauge.labels(server=METRICS_SERVER_LABEL).set(users_muted_count) + users_unregistered_gauge.labels(server=METRICS_SERVER_LABEL).set(users_unregistered_count) + users_registered_gauge.labels(server=METRICS_SERVER_LABEL).set(users_registered_count) + users_banned_gauge.labels(server=METRICS_SERVER_LABEL).set(len(server.getBans())) + chan_count_gauge.labels(server=METRICS_SERVER_LABEL).set(len(server.getChannels())) + uptime_gauge.labels(server=METRICS_SERVER_LABEL).set(meta.getUptime()) + + # Wait beforce next metrics collection + time.sleep(COLLECT_INTERVAL) diff --git a/pica-murmur/requirements.txt b/pica-murmur/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..5e01e40a21dddfc329f183c6d662849f8004482c --- /dev/null +++ b/pica-murmur/requirements.txt @@ -0,0 +1,3 @@ +zeroc-ice==3.7.3 +prometheus_client==0.7.1 + diff --git a/pica-tls-certs-monitor/docker-compose.yml b/pica-tls-certs-monitor/docker-compose.yml index 5fbdd3cea8c48f3d6ead228ec27c866866baf49e..63f9ed8fe32f25fb20a53b41079d6e6337d9aa8b 100644 --- a/pica-tls-certs-monitor/docker-compose.yml +++ b/pica-tls-certs-monitor/docker-compose.yml @@ -2,7 +2,7 @@ version: '3.7' services: tls-certs-monitor: - image: registry.picasoft.net/pica-tls-certs-monitor:v1.4 + image: registry.picasoft.net/pica-tls-certs-monitor:v1.5 container_name: tls-certs-monitor volumes: - /DATA/docker/traefik/certs/acme.json:/certs/acme.json