Compare commits

..

No commits in common. "f02ec71a3b1c5db12938294352c4f73b7d3e22b9" and "2b07a5ee429092e85adab4ec677b3e00da820d1c" have entirely different histories.

2 changed files with 154 additions and 216 deletions

View File

@ -4,7 +4,6 @@
### Dependencies
* zenity (unless called with option `--no-gui`)
* jq
* Audio subsystem:
- PulseAudio or

View File

@ -17,144 +17,81 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
##
# Arguments
operation=$1
##
# Configuration
config_dir="$HOME"/.config/pulseaudio-tcp
config="$config_dir"/config.inc.sh
##
# Functions
usage() {
cat >&2 <<EOF
Usage: $0 [OPTIONS] OPERATION
Options:
--debug Enable additional debug output for start and stop operations.
--no-gui Do not display GUI dialogs, use terminal for input and output.
Operations:
setup Interactively gather settings.
start Start redirection to remote host.
stop Stop redirection to remote host.
restart Stop and then start redirection.
status Check if redirection is set up and enabled.
EOF
}
log() {
level=$1
shift
msg=$*
echo "$level: $msg" >&2
if [[ -t 1 ]] && $gui && [[ -n $(type -p zenity) ]] ; then
case "$level" in
ERROR)
zenity --error --text="$msg"
;;
*)
zenity --info --text="$msg"
;;
esac
fi
}
error() {
log ERROR "$@"
}
info() {
log INFO "$@"
}
debug() {
if "$debug" ; then
log DEBUG "$@"
else
gui=false log DEBUG "$@"
fi
}
_ssh() {
if ssh -S "$USER"-pulseaudio "$remote_user"@"$remote_ip" "$@" ; then
return 0
else
error "SSH remote_ip=$remote_ip failed."
echo "ERROR: SSH remote_ip=$remote_ip failed." >&2
return 1
fi
}
question_yesno() {
question=$*
if "$gui" ; then
if zenity --question --text="$question" ; then
return 0
else
return 1
fi
else
while read -r -p "$question [y|n] " answer ; do
case $answer in
[yY])
return 0
;;
[nN])
return 1
;;
esac
done
fi
}
question_input() {
title=$1
question=$2
value=$3
if $gui ; then
zenity --entry --title="$title" --text="$question" --entry-text="$value"
else
echo "$title" >&2
read -r -p "$question ($value)" answer
[[ -z $answer ]] && answer=$value
echo "$answer"
fi
}
# Perform setup operation
do_setup() {
if test -e "$config" ; then
if question_yesno "$config already exists, overwrite it?"; then
if ! test -w "$config" ; then
error "$config is not writable."
return 1 ;
elif ! test -r "$config" ; then
error "$config is not readable."
return 1 ;
else
. "$config"
fi
else
error "Setup aborted."
read -r -p "config=$config already exists, overwrite it? (Y|n) " answer
case "$answer" in
y|Y|"")
if ! test -w "$config" ; then
echo "ERROR: config=$config is not writable." >&2 ;
return 1 ;
elif ! test -r "$config" ; then
echo "ERROR: config=$config is not readable." >&2 ;
return 1 ;
else
. "$config"
fi
;;
*)
echo "ERROR: Setup aborted." >&2
return 1
fi
esac
elif ! test -e "$config_dir" ; then
mkdir -p "$config_dir" || {
error "Could not create $config_dir"
echo "ERROR: Could not create config_dir=$config_dir." >&2 ;
return 1 ;
}
elif ! test -d "$config_dir" ; then
error "$config_dir is not a directory"
echo "ERROR: config_dir=$config_dir is not a directory." >&2
return 1
elif ! test -w "$config_dir" ; then
error "$config_dir is not writable"
echo "ERROR: config_dir=$config_dir is not writable." >&2
return 1
fi
while true ; do
if remote_ip_in=$(question_input "Set remote host" "Enter name or IP address of remote host:" "$remote_ip") ; then
read -r -p "Enter IP address of remote host ($remote_ip): " remote_ip_in
if test -n "$remote_ip_in" ; then
break
elif test -n "$remote_ip" ; then
remote_ip_in=$remote_ip
break
fi
done
while true ; do
if remote_user_in=$(question_input "Set remote user" "Enter username on remote host:" "$remote_user") ; then
read -r -p "Enter username on remote host $remote_ip ($remote_user): " remote_user_in
if test -n "$remote_user_in" ; then
break
elif test -n "$remote_user" ; then
remote_user_in=$remote_user
break
fi
done
@ -162,25 +99,65 @@ do_setup() {
inbound=${inbound:-true}
while true ; do
if question_yesno "Enable inbound audio?"; then
inbound_in=true
break
if "$inbound" ; then
prompt="Y/n"
else
inbound_in=false
break
prompt="y/N"
fi
read -r -p "Enable inbound audio ($prompt): " inbound_in
case "$inbound_in" in
"")
if test -n "$inbound" ; then
inbound_in=$inbound
break 2
fi
;;
y|Y)
inbound_in=true
break 2
;;
n|N)
inbound_in=false
break 2
;;
*)
echo "ERROR: Please type \"y\" or \"n\"." >&2
;;
esac
done
outbound=${outbound:-true}
while true ; do
if question_yesno "Enable outbound audio?"; then
outbound_in=true
break
if "$outbound" ; then
prompt="Y/n"
else
outbound_in=false
break
prompt="y/N"
fi
read -r -p "Enable outbound audio ($prompt): " outbound_in
case "$outbound_in" in
"")
if test -n "$outbound" ; then
outbound_in=$outbound
break 2
fi
;;
y|Y)
outbound_in=true
break 2
;;
n|N)
outbound_in=false
break 2
;;
*)
echo "ERROR: Please type \"y\" or \"n\"." >&2
;;
esac
done
cat > "$config" << EOF
@ -215,40 +192,37 @@ check_pa_ssh() {
# Perform status operation
do_status() {
rv=0
errors=()
if ! check_pa_ssh ; then
rv=1
fi
if ! _ssh pactl list modules | grep -Fq "Name: module-native-protocol-tcp" ; then
errors+=("PulseAudio module \"module-native-protocol-tcp\" is not loaded on remote host $remote_ip.")
echo "ERROR: PulseAudio module \"module-native-protocol-tcp\" is not loaded on remote_ip=$remote_ip." >&2
rv=1
fi
if "$outbound" ; then
if ! pactl list modules | grep -Fq "Name: module-tunnel-sink" ; then
errors+=("PulseAudio module \"module-tunnel-sink\" is not loaded.")
echo "ERROR: PulseAudio module \"module-tunnel-sink\" is not loaded." >&2
rv=1
fi
if ! pactl get-default-sink | grep -Fq "tunnel-sink.tcp:127.0.0.1" ; then
errors+=("\"tunnel-sink.tcp:127.0.0.1\" is not the default PulseAudio sink.")
echo "ERROR: \"tunnel-sink.tcp:127.0.0.1\" is not the default PulseAudio sink." >&2
rv=1
fi
fi
if "$inbound" ; then
if ! pactl list modules | grep -Fq "Name: module-tunnel-source" ; then
errors+=("PulseAudio module \"module-tunnel-source\" is not loaded.")
echo "ERROR: PulseAudio module \"module-tunnel-source\" is not loaded." >&2
rv=1
fi
fi
if [[ $rv -eq 0 ]] ; then
info "All checks passed; pulseaudio-tcp status is okay."
else
error "pulseaudio-tcp status is not okay: ${errors[*]}"
if test "$rv" -eq 0 ; then
echo "INFO: All checks passed; pulseaudio-tcp status is okay." >&2
fi
return "$rv"
@ -257,10 +231,10 @@ do_status() {
# Acquire PulseAudio cookie from remote host
sync_pa_cookie() {
if scp -q "$remote_user"@"$remote_ip":.config/pulse/cookie ~/.config/pulse/cookie ; then
debug "Synced PulseAudio cookie from remote host $remote_ip."
echo "INFO: Synced PulseAudio cookie from remote_ip=$remote_ip." >&2
return 0
else
error "Unable to sync PulseAudio cookie from remote host $remote_ip."
echo "ERROR: Unable to sync PulseAudio cookie from remote_ip=$remote_ip." >&2
return 1
fi
}
@ -273,13 +247,13 @@ establish_ssh_portforward() {
# Enable PulseAudio TCP tunnel server on remote host
enable_remote_pa_tunnel_server() {
if _ssh pactl list modules | grep -Fq "Name: module-native-protocol-tcp" ; then
debug "PulseAudio module \"module-native-protocol-tcp\" already loaded on remote host $remote_ip."
echo "INFO: PulseAudio module \"module-native-protocol-tcp\" already loaded on remote_ip=$remote_ip." >&2
return 0
elif _ssh pactl load-module module-native-protocol-tcp listen=127.0.0.1 auth-ip-acl=127.0.0.1 ; then
debug "Loaded PulseAudio module \"module-native-protocol-tcp\" on remote host $remote_ip."
echo "INFO: Loaded PulseAudio module \"module-native-protocol-tcp\" on remote_ip=$remote_ip." >&2
return 0
else
error "Unable to load PulseAudio module \"module-native-protocol-tcp\" on remote host $remote_ip."
echo "ERROR: Unable to load PulseAudio module \"module-native-protocol-tcp\" on remote_ip=$remote_ip." >&2
return 1
fi
}
@ -287,13 +261,13 @@ enable_remote_pa_tunnel_server() {
# Enable tunnel sink on local host
enable_local_pa_tunnel_sink() {
if pactl list modules | grep -Fq "Name: module-tunnel-sink" ; then
debug "PulseAudio module \"module-tunnel-sink\" already loaded."
echo "INFO: PulseAudio module \"module-tunnel-sink\" already loaded." >&2
return 0
elif pactl load-module module-tunnel-sink server=tcp:127.0.0.1 ; then
debug "Loaded PulseAudio module \"module-tunnel-sink\"."
echo "INFO: Loaded PulseAudio module \"module-tunnel-sink\"." >&2
return 0
else
error "Unable to load PulseAudio module \"module-tunnel-sink\"."
echo "ERROR: Unable to load PulseAudio module \"module-tunnel-sink\"." >&2
return 1
fi
}
@ -301,13 +275,13 @@ enable_local_pa_tunnel_sink() {
# Enable tunnel source on local host
enable_local_pa_tunnel_source() {
if pactl list modules | grep -Fq "Name: module-tunnel-source" ; then
debug "PulseAudio module \"module-tunnel-source\" already loaded."
echo "INFO: PulseAudio module \"module-tunnel-source\" already loaded." >&2
return 0
elif pactl load-module module-tunnel-source server=tcp:127.0.0.1 ; then
debug "Loaded PulseAudio module \"module-tunnel-source\"."
echo "INFO: Loaded PulseAudio module \"module-tunnel-source\"." >&2
return 0
else
error "Unable to load PulseAudio module \"module-tunnel-source\"."
echo "ERROR: Unable to load PulseAudio module \"module-tunnel-source\"." >&2
return 1
fi
}
@ -315,10 +289,10 @@ enable_local_pa_tunnel_source() {
# Set tunnel sink as default sink on local host
set_local_pa_tunnel_sink_as_default() {
if pactl set-default-sink tunnel-sink.tcp:127.0.0.1 ; then
debug "Set \"tunnel-sink.tcp:127.0.0.1\" as default PulseAudio sink."
echo "INFO: Set \"tunnel-sink.tcp:127.0.0.1\" as default PulseAudio sink." >&2
return 0
else
error "Failed to set \"tunnel-sink.tcp:127.0.0.1\" as default PulseAudio sink."
echo "ERROR: Failed to set \"tunnel-sink.tcp:127.0.0.1\" as default PulseAudio sink." >&2
return 1
fi
}
@ -331,7 +305,6 @@ do_start() {
if "$outbound" ; then
enable_local_pa_tunnel_sink || return 1
# workaround, local tunnel is not available immediately
sleep 1
set_local_pa_tunnel_sink_as_default || return 1
fi
@ -346,10 +319,10 @@ do_start() {
# Remove PulseAudio TCP tunnel sink on local host
remove_local_pa_tunnel_sink() {
if ! pactl list modules | grep -Fq "Name: module-tunnel-sink" ; then
debug " PulseAudio module \"module-tunnel-sink\" is not loaded."
echo "INFO: PulseAudio module \"module-tunnel-sink\" is not loaded." >&2
return 0
elif ! pactl list sinks | grep -Fq "tunnel-sink.tcp:127.0.0.1" ; then
debug "No PulseAudio tunnel sink to 127.0.0.1 exists."
echo "INFO: No PulseAudio tunnel sink to 127.0.0.1 exists." >&2
return 0
else
owner_module=$(
@ -358,10 +331,10 @@ remove_local_pa_tunnel_sink() {
)
if ! pactl unload-module "$owner_module" ; then
error "Could not unload owner module $owner_module of PulseAudio sink \"tunnel-sink.tcp:127.0.0.1\"."
echo "ERROR: Could not unload owner module $owner_module of PulseAudio sink \"tunnel-sink.tcp:127.0.0.1\"." >&2
return 1
else
debug "Unloaded owner module $owner_module of PulseAudio sink \"tunnel-sink.tcp:127.0.0.1\"."
echo "INFO: Unloaded owner module $owner_module of PulseAudio sink \"tunnel-sink.tcp:127.0.0.1\"." >&2
return 0
fi
fi
@ -370,10 +343,10 @@ remove_local_pa_tunnel_sink() {
# Remove PulseAudio TCP tunnel source on local host
remove_local_pa_tunnel_source() {
if ! pactl list modules | grep -Fq "Name: module-tunnel-source" ; then
debug "PulseAudio module \"module-tunnel-source\" is not loaded."
echo "INFO: PulseAudio module \"module-tunnel-source\" is not loaded." >&2
return 0
elif ! pactl list sources | grep -Fq "tunnel-source.tcp:127.0.0.1" ; then
debug "No PulseAudio tunnel source from 127.0.0.1 exists."
echo "INFO: No PulseAudio tunnel source from 127.0.0.1 exists." >&2
return 0
else
owner_module=$(
@ -382,10 +355,10 @@ remove_local_pa_tunnel_source() {
)
if ! pactl unload-module "$owner_module" ; then
error "Could not unload owner module $owner_module of PulseAudio source \"tunnel-source.tcp:127.0.0.1\"."
echo "ERROR: Could not unload owner module $owner_module of PulseAudio source \"tunnel-source.tcp:127.0.0.1\"." >&2
return 1
else
debug "Unloaded owner module $owner_module of PulseAudio source \"tunnel-source.tcp:127.0.0.1\"."
echo "INFO: Unloaded owner module $owner_module of PulseAudio source \"tunnel-source.tcp:127.0.0.1\"." >&2
return 0
fi
fi
@ -394,13 +367,13 @@ remove_local_pa_tunnel_source() {
# Stop PulseAudio TCP tunnel server on remote host.
disable_remote_pa_tunnel_server() {
if ! _ssh pactl list modules | grep -Fq "Name: module-native-protocol-tcp" ; then
debug "PulseAudio module \"module-native-protocol-tcp\" not loaded on remote_ip=$remote_ip."
echo "INFO: PulseAudio module \"module-native-protocol-tcp\" not loaded on remote_ip=$remote_ip." >&2
return 0
elif ! _ssh pactl unload-module module-native-protocol-tcp ; then
error "Could not unload PulseAudio module \"module-native-protocol-tcp\" on remote host $remote_ip."
echo "ERROR: Could not unload PulseAudio module \"module-native-protocol-tcp\" on remote_ip=$remote_ip." >&2
return 1
else
debug "Unloaded PulseAudio module \"module-native-protocol-tcp\" on remote host $remote_ip."
echo "INFO: Unloaded PulseAudio module \"module-native-protocol-tcp\" on remote_ip=$remote_ip." >&2
return 0
fi
}
@ -430,93 +403,58 @@ do_stop() {
return 0
}
##
# Arguments
for arg in "$@" ; do
case "$arg" in
--debug)
debug_cmdline=true
;;
--no-gui)
gui_cmdline=false
;;
--help)
help_cmdline=true
;;
*)
operation=$arg
;;
esac
done
##
# Configuration
config_dir="$HOME"/.config/pulseaudio-tcp
config=$config_dir/config.inc.sh
if [[ $help_cmdline = true ]] ; then
usage
exit 0
fi
if [[ $debug_cmdline = true ]] ; then
debug=true
else
debug=false
fi
if [[ $gui_cmdline = false ]] ; then
gui=false
else
gui=true
fi
##
# Main Program
rv=0
if test "$operation" = setup ; then
info "Entering setup mode ..."
echo "Entering setup mode ..."
else
if ! test -e "$config" ; then
error "Configfile $config does not exist (use \"$0 setup\" first)."
echo "ERROR: Configfile $config does not exist (use \"$0 setup\" first)." >&2
rv=1
elif ! test -r "$config" ; then
error "Configfile $config is not readable."
echo "ERROR: Configfile $config is not readable." >&2
rv=1
elif ! test -f "$config" ; then
error "Configfile $config is not a regular file."
echo "ERROR: Configfile $config is not a regular file." >&2
rv=1
else
. "$config"
if [[ -z $remote_ip ]] ; then
error "\"remote_ip=<IP address>\" not set in configfile $config."
if test -z "$remote_ip" ; then
echo "ERROR: \"remote_ip=<IP address>\" not set in configfile $config." >&2
rv=1
elif [[ -z $remote_user ]] ; then
error "\"remote_user=<username>\" not set in configfile $config."
elif test -z "$remote_user" ; then
echo "ERROR: \"remote_user=<username>\" not set in configfile $config." >&2
rv=1
fi
fi
required_cmds=( jq pactl ssh )
"$gui" && required_cmds+=( zenity )
for exe in "${required_cmds[@]}" ; do
if [[ -z $(type -p "$exe") ]] ; then
error "Required executable \"$exe\" not found."
rv=1
fi
done
if test -z "$(which jq)" ; then
echo "ERROR: Required executable \"jq\" not found." >&2
rv=1
elif test -z "$(which pactl)" ; then
echo "ERROR: Required executable \"pactl\" not found." >&2
rv=1
elif test -z "$(which ssh)" ; then
echo "ERROR: Required executable \"ssh\" not found." >&2
rv=1
fi
fi
if [[ $rv -ne 0 ]] ; then
error "Preliminary checks failed, skipping operation."
if test "$rv" -ne 0 ; then
echo "ERROR: Preliminary checks failed, skipping operation." >&2
else
case "$operation" in
""|-h|--help)
cat << EOF
Setup and run encrypted connection to remote PulseAudio/Pipewire server
Usage: $0 restart|setup|start|status|stop
EOF
rv=0
;;
setup)
do_setup
rv=$?
@ -539,10 +477,11 @@ else
rv=$?
;;
*)
usage
echo "ERROR: Usage: $0 restart|setup|start|status|stop" >&2
rv=1
;;
esac
fi
exit "$rv"