Skip to content
Snippets Groups Projects
Commit a00938dc authored by PICHOU Kyâne's avatar PICHOU Kyâne
Browse files

Merge branch 'murmur' into 'master'

Add murmur server

See merge request !49
parents 27c591e1 09d8f2b5
No related branches found
No related tags found
1 merge request!49Add murmur server
Pipeline #64655 passed
README.md
docker-compose.yml
secrets/
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"
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
<!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&middot;e&middot;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 -->
&nbsp;(<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>
README.md
docker-compose.yml
secrets/
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"]
# 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.
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: ¯\_(ツ)_/¯
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
#!/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
# 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)
zeroc-ice==3.7.3
prometheus_client==0.7.1
......@@ -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
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment