From 19bcae4b1a639537f447460df76219459f33caaa Mon Sep 17 00:00:00 2001 From: Georg Pfuetzenreuter Date: Sun, 31 Jul 2022 19:52:23 +0200 Subject: Init SyncPlay Signed-off-by: Georg Pfuetzenreuter --- README.md | 9 ++ stunnel/syncplay.conf | 6 + syncplay-bridge/syncplay-bridge | 189 ++++++++++++++++++++++++++++++++ syncplay-bridge/syncplay-bridge.service | 25 +++++ syncplay-proxy/syncplay-proxy | 62 +++++++++++ syncplay-proxy/syncplay-proxy.service | 23 ++++ syncplay/motd | 1 + syncplay/syncplay.service | 23 ++++ 8 files changed, 338 insertions(+) create mode 100644 README.md create mode 100644 stunnel/syncplay.conf create mode 100755 syncplay-bridge/syncplay-bridge create mode 100644 syncplay-bridge/syncplay-bridge.service create mode 100755 syncplay-proxy/syncplay-proxy create mode 100644 syncplay-proxy/syncplay-proxy.service create mode 100644 syncplay/motd create mode 100644 syncplay/syncplay.service diff --git a/README.md b/README.md new file mode 100644 index 0000000..fd1c0cd --- /dev/null +++ b/README.md @@ -0,0 +1,9 @@ +Resources powering the `lysergic.media` SyncPlay instance. + +#### Proxy: +The proxy removes the superfluous STARTTLS message in order for stunnel to terminate the TLS connection. + +SyncPlay Client -> syncplay-proxy (lysergic.media:8999) -> stunnel (TLS [::1]:8997) -> syncplay (PLAIN [::1]:8998) + +#### Bridge: +The bridge relays chat messages between SyncPlay and IRC. diff --git a/stunnel/syncplay.conf b/stunnel/syncplay.conf new file mode 100644 index 0000000..d029bea --- /dev/null +++ b/stunnel/syncplay.conf @@ -0,0 +1,6 @@ +[syncplay] +accept = ::1:8997 +connect = ::1:8998 +cert = /etc/ssl/lysergic.media/crt +key = /etc/ssl/lysergic.media/key +sslVersion = TLSv1.3 diff --git a/syncplay-bridge/syncplay-bridge b/syncplay-bridge/syncplay-bridge new file mode 100755 index 0000000..20d8c5f --- /dev/null +++ b/syncplay-bridge/syncplay-bridge @@ -0,0 +1,189 @@ +#!/usr/bin/env bash +# dependencies: bash 4+, jq + +# original: https://gist.github.com/ncfavier/55c20a77a3fd72e00407f8889fc6e852 +# forked for LibertaCasa by Georg Pfuetzenreuter +# notable changes: use of IRCv3 draft/relaymsg and NickServ authentication using PASS + +set -Cu + +#. ./config.sh +#: "${verbose:=0}" +#: "${syncplay_host:=syncplay.pl}" +#: "${syncplay_port:=8999}" +#: "${syncplay_nick:=syncplaybridge}" +#: "${syncplay_room:?"can't be empty"}" +#: "${irc_host:=chat.freenode.net}" +#: "${irc_port:=6667}" +#: "${irc_nick:=syncplaybridge}" +#: "${irc_channel:?"can't be empty"}" + +verbose=0 +syncplay_host='::1' +syncplay_port=8998 +syncplay_password=NOT_IMPLEMENTED +syncplay_nick=irc +syncplay_room=%%SP_ROOM%% +irc_host='::1' +irc_port=6667 +irc_nick=syncplay +irc_nickserv_nick=syncplay +irc_nickserv_pass='%%IRC_NS_PASS%%' +irc_channel='%%IRC_CHANNEL%%' #including channel prefix such as # or ! +#irc_channel='#dev' + +irc_send() { + local args=("$@") + #echo "DEBUG: ARGS ARE - $args" + args[0]=${args[0]^^} + #echo "DEBUG: ARGS ARE - $args" + if (( ${#args[@]} > 1 )); then + args[${#args[@]}-1]=:${args[${#args[@]}-1]} + fi + printf '%s\r\n' "${args[*]}" >&"$irc_fd" + (( verbose )) && printf 'IRC %s==>%s %s\n' "$(tput setaf 1)" "$(tput sgr0)" "${args[*]}" >&2 +} + +irc_getnick() { + local arg=$1 + echo "$arg" | grep -Po "(?<=\<).*(?=\>)" +} + +irc_zwspnick() { + local arg=$1 + local nick1="${arg:0:${#arg}/2}" + local nick2="${arg:${#arg}/2}" + printf "$nick1\u200b$nick2" +} + +irc_say() { + #echo "DEBUG: irc_say $irc_channel - $*" + local arg="$*" + #echo "DEBUG: irc_say ARG: $arg" + local msg="$(echo $arg | cut -d '>' -f 2 | sed -e 's/^[[:space:]]*//')" + local nick="$(irc_getnick $arg)" + if [ -n "$nick" ] + then + local zwspnick="$(irc_zwspnick $nick)" + #irc_send relaymsg "$irc_channel" "$zwspnick/sp" "$msg" + irc_send relaymsg "$irc_channel" "$nick/sp" "$msg" + fi + #echo "DEBUG: irc_say NICK $zwspnick" + #echo "DEBUG: irc_say MSG $msg" + if [ -z "$nick" ] + then + irc_send privmsg "$irc_channel" "$msg" + fi +} + +irc_ctcp() { + local args=("$@") + args[0]=${args[0]^^} + irc_send notice "$sender_nick" $'\1'"${args[*]}"$'\1' +} + +syncplay_send() { + local msg=$(jq -nc "$@") + printf '%s\r\n' "$msg" >&"$syncplay_fd" + (( verbose )) && printf 'syncplay %s==>%s %s\n' "$(tput setaf 1)" "$(tput sgr0)" "$msg" >&2 +} + +syncplay_say() { + syncplay_send --arg message "$*" '{Chat: $message}' +} + +on_exit() { + irc_send quit + kill $(jobs -p) +} + +exec {irc_fd}<> /dev/tcp/"$irc_host"/"$irc_port" || exit +exec {syncplay_fd}<> /dev/tcp/"$syncplay_host"/"$syncplay_port" || exit + +trap on_exit exit + +irc_send pass "$irc_nickserv_nick:$irc_nickserv_pass" +irc_send nick "$irc_nick" +irc_send user "$irc_nick" 0 '*' 'Syncplay<->IRC' + +# patched version with server password attempt +#syncplay_send --arg username "$syncplay_nick" --arg room "$syncplay_room" --arg password "$syncplay_password" \ +# '{Hello: {$username, password: $password, room: {name: $room}, realversion: "1.6.5"}}' +syncplay_send --arg username "$syncplay_nick" --arg room "$syncplay_room" \ + '{Hello: {$username, room: {name: $room}, realversion: "1.6.5"}}' + +while read -ru "$irc_fd" line; do + line=${line%$'\r'} + (( verbose )) && printf 'IRC %s<==%s %s\n' "$(tput setaf 2)" "$(tput sgr0)" "$line" >&2 + sender= sender_nick= + args=() + if [[ $line == :* ]]; then + sender=${line%% *} + sender=${sender#:} + sender_nick=${sender%%!*} + line=${line#* } + fi + while [[ $line == *' '* && $line != :* ]]; do + args+=("${line%% *}") + line=${line#* } + done + args+=("${line#:}") + cmd=${args[0]} + set -- "${args[@]:1}" + case ${cmd,,} in + 001) irc_send join "$irc_channel";; + ping) irc_send pong "$1";; + privmsg) + target=$1 + message=$2 + #echo "DEBUG: $target" + #echo "DEBUG: $message" + if [[ $target == "$irc_nick" ]] && [[ $message =~ ^$'\1'(.*)$'\1'$ ]]; then + #echo "DEBUG: privmsg IF 1" + message=${BASH_REMATCH[1]} + #echo "DEBUG: BASH REMATCH: $message" + read -r ctcp_cmd _ <<< "$message" + case ${ctcp_cmd,,} in + version) irc_ctcp version "syncplaybridge";; + esac + elif [[ "$target" == "$irc_channel" ]]; then + #echo "DEBUG: sender_nick IS $sender_nick" + if [[ ! "$sender_nick" == */sp ]]; then + #echo "DEBUG: privmsg IF 2" + if [[ $message =~ ^$'\1ACTION '(.*)$'\1'$ ]]; then + #echo "DEBUG: privmsg IF 2 $message MATCHES" + syncplay_say "* $sender_nick ${BASH_REMATCH[1]}" + else + #echo "DEBUG: privmsg IF 2 DOES NOT MATCH" + syncplay_say "<$sender_nick> $2" + fi + fi + fi + esac +done & + +while read -ru "$syncplay_fd" line; do + line=${line%$'\r'} + (( verbose )) && printf 'syncplay %s<==%s %s\n' "$(tput setaf 2)" "$(tput sgr0)" "$line" >&2 + . <(jq <<< "$line" -r --arg self "$syncplay_nick" ' + def irc($msg): @sh "irc_say \($msg)"; + (.State // empty + | @sh "syncplay_send \"{State: {}}\""), + (.Chat // empty + | select(.username != $self) + | irc("<\(.username)> \(.message)")), + (.Set // empty + | .user // empty + | keys[0] as $nick + | .[$nick] + | ( + (.event // empty + | irc("\($nick) \(if .joined then "joined the room" else "left the room" end)")), + (.file // empty + | irc("\($nick) is now playing \(.name)")) + ) + ) + ') +done & + +wait diff --git a/syncplay-bridge/syncplay-bridge.service b/syncplay-bridge/syncplay-bridge.service new file mode 100644 index 0000000..a8d4e97 --- /dev/null +++ b/syncplay-bridge/syncplay-bridge.service @@ -0,0 +1,25 @@ +[Unit] +Description=Syncplay IRC Bridge +Wants=network.target +After=syncplay.service stunnel.service +BindsTo=syncplay.service stunnel.service + +[Service] +User=syncplay +Group=syncplay +ExecStart=/bin/bash /usr/local/bin/syncplay-bridge +ProtectSystem=strict +ProtectHome=yes +PrivateDevices=yes +PrivateTmp=yes +PrivateUsers=yes +ProtectKernelTunables=yes +ProtectKernelLogs=yes +ProtectControlGroups=yes +ReadWritePaths=/var/lib/syncplay +RestrictAddressFamilies=AF_INET6 +SystemCallArchitectures=native +SystemCallFilter=@system-service + +[Install] +WantedBy=multi-user.target diff --git a/syncplay-proxy/syncplay-proxy b/syncplay-proxy/syncplay-proxy new file mode 100755 index 0000000..5ed7a4e --- /dev/null +++ b/syncplay-proxy/syncplay-proxy @@ -0,0 +1,62 @@ +#!/usr/bin/python3 + +# See https://github.com/Syncplay/syncplay/issues/346 for this script's raison-d'etre. + +# original: https://github.com/Syncplay/syncplay/issues/346#issuecomment-698134786 +# forked for LibertaCasa by Georg Pfuetzenreuter +# notable changes: utilize IPv6 + +import select +import socket +import socketserver +import sys + +listen_port = int(sys.argv[1]) +forward_port = int(sys.argv[2]) + +class SyncplayRequestHandler(socketserver.BaseRequestHandler): + def handle(self): + print('Handling connection from:', self.client_address) + data = self.request.recv(1024) + if data.strip() != b'{"TLS": {"startTLS": "send"}}': + print('Bad connection header from:', self.client_address) + try: + self.request.close() + except: + pass + return + print('Opening forwarding connection on behalf of:', self.client_address) + forwarded_conn = socket.socket(socket.AF_INET6, socket.SOCK_STREAM) + forwarded_conn.connect(('localhost', forward_port)) + # Acknowledge to the client that we are ready. + self.request.sendall(b'{"TLS": {"startTLS": "true"}}\r\n') + # Start proxying. + print('Proxying started for:', self.client_address) + sockets = (self.request, forwarded_conn) + while True: + rlist, _, xlist = select.select(sockets, (), sockets) + if len(xlist): + break + for s in rlist: + if s is self.request: + forwarded_conn.sendall(self.request.recv(1024)) + elif s is forwarded_conn: + self.request.sendall(forwarded_conn.recv(1024)) + print('Proxying stopped for:', self.client_address) + try: + forwarded_conn.close() + except: + pass + try: + self.request.close() + except: + pass + +class SyncplayServer(socketserver.ThreadingTCPServer): + address_family = socket.AF_INET6 + def server_bind(self): + self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + socketserver.ThreadingTCPServer.server_bind(self) + +with SyncplayServer(('', listen_port), SyncplayRequestHandler) as server: + server.serve_forever() diff --git a/syncplay-proxy/syncplay-proxy.service b/syncplay-proxy/syncplay-proxy.service new file mode 100644 index 0000000..6d11d46 --- /dev/null +++ b/syncplay-proxy/syncplay-proxy.service @@ -0,0 +1,23 @@ +[Unit] +Description=Syncplay TLS Fixer +Wants=network.target + +[Service] +User=syncplay +Group=syncplay +ExecStart=/usr/bin/python3.10 /usr/local/bin/syncplay-proxy 8999 8997 +ProtectSystem=strict +ProtectHome=yes +PrivateDevices=yes +PrivateTmp=yes +PrivateUsers=yes +ProtectKernelTunables=yes +ProtectKernelLogs=yes +ProtectControlGroups=yes +ReadWritePaths=/var/lib/syncplay +RestrictAddressFamilies=AF_INET6 AF_INET +SystemCallArchitectures=native +SystemCallFilter=@system-service + +[Install] +WantedBy=multi-user.target diff --git a/syncplay/motd b/syncplay/motd new file mode 100644 index 0000000..196c138 --- /dev/null +++ b/syncplay/motd @@ -0,0 +1 @@ +The LYSERGIC SyncPlay server hugs you for a warm welcome!!! <3 diff --git a/syncplay/syncplay.service b/syncplay/syncplay.service new file mode 100644 index 0000000..286ac0d --- /dev/null +++ b/syncplay/syncplay.service @@ -0,0 +1,23 @@ +[Unit] +Description=Syncplay Server +Wants=network.target + +[Service] +User=syncplay +Group=syncplay +ExecStart=/usr/bin/syncplay-server --salt %%SALT%% --stats-db-file /var/lib/syncplay/db.sqlite --port 8998 --motd-file /etc/syncplay/motd +ProtectSystem=strict +ProtectHome=yes +PrivateDevices=yes +PrivateTmp=yes +PrivateUsers=yes +ProtectKernelTunables=yes +ProtectKernelLogs=yes +ProtectControlGroups=yes +ReadWritePaths=/var/lib/syncplay +RestrictAddressFamilies=AF_INET6 AF_INET +SystemCallArchitectures=native +SystemCallFilter=@system-service + +[Install] +WantedBy=multi-user.target -- cgit v1.2.3