#!/bin/bash
# program version
_BC_VER="1.0.0"

cat <<EOT
Bash Broadcast version ${_BC_VER}
Broadcast bash shell session over a TCP network socket
Copyright 2016 <tj@squoo.sh> <hacker@iam.tj>
Licensed on the terms of the GNU General Public License version 3.

Type 'broadcast help' for assistance.
EOT

# Do not directly execute this script; instead, 'source' it into an active terminal
# and then call its functions when required. E.g:
# 
# $ BC_PORT=12345 # optional; default port used is 10007
# $ . /usr/local/lib/broadcast.bash
# $ broadcast_init
# $ # ... do some stuff before starting ...
# $ broadcast_start
# $ echo "Hello and welcome to all the viewers..."
# $ broadcast_pause
# $ # .. do some stuff without broadcasting it
# $ broadcast_continue
# $ cat /etc/issue
# $ broadcast_stop
#


broadcast_configure()
{
  # These variables may be over-ridden in the calling environment

  # TCP port
  BC_PORT=${BC_PORT:-10007}
  # default is bind to all addresses on all interfaces
  BC_ADDR=${BC_ADDR:-}
  # First-In First-Out pipe
  BC_PIPE=${BC_PIPE:-/tmp/broadcast}
  # firewall rules
  BC_IP4TABLES_RULE=${BC_IP4TABLES_RULE:--p tcp -m tcp --dport ${BC_PORT} -j ACCEPT -m comment --comment 'Terminal Broadcast server'}
  BC_IP6TABLES_RULE=${BC_IP6TABLES_RULE:--p tcp -m tcp --dport ${BC_PORT} -j ACCEPT -m comment --commect 'Terminal Broadcast server'}
  # server program and command-line (must accept input on stdin)
  BC_SERVER=${BC_SERVER:-}
  BC_SERVER_OPTS=${BC_SERVER_OPTS:-}
  # proxy (relay) server to forward the connection to
  BC_PROXY_ADDR=${BC_PROXY_ADDR:-}
  BC_PROXY_PORT=${BC_PROXY_PORT:-10006}

  # End of over-ridden variables

  # initial state (UNDEFINED, STOPPED, PAUSED, BROADCAST)
  _BC_STATE=UNDEFINED
  _BC_IP4TABLES=UNSET
  _BC_IP6TABLES=UNSET
  _BC_SERVER_PID=

  if [ -n ${_BC_CONFIGURED} ]; then
    # in cases where this function is called twice the original BC_SERVER value needs to be restored
    BC_SERVER=${_BC_SERVER:-}
  else
    _BC_SERVER=
  fi

  if [ -z "${BC_SERVER}" ]; then
    # locate the  netcat type clients
    _BC_NCAT="$(readlink -e $(which ncat))"
    _BC_NC="$(readlink -e $(which nc))"
    if [ -n "${_BC_NCAT}" ]; then
      if [ -z "${BC_PROXY_ADDR}" ]; then
        # multi-client server (defaults to 100 simultaneous client connections)
        BC_SERVER="ncat ${BC_SERVER_OPTS} -k -l ${BC_ADDR} ${BC_PORT}"
        _BC_STATE=STOPPED
        echo "The Broadcast server is 'ncat'; it can accept simultaneous connections"
      else
        BC_SERVER="ncat ${BC_SERVER_OPTS} ${BC_PROXY_ADDR} ${BC_PROXY_PORT}"
        _BC_STATE=STOPPED
        echo "The Broadcast proxy server is 'ncat' forwarding to ${BC_PROXY_ADDR}:${BC_PROXY_PORT}"
      fi
    elif [ -n "${_BC_NC}" ]; then
      # these servers only handle 1 client at a time
      if nc -h |& grep -q cruft ; then
        # netcat-traditional
        if [ -z "${BC_PROXY_ADDR}" ]; then
          BC_SERVER="nc ${BC_SERVER_OPTS} -w 99999 -p ${BC_PORT} -l ${BC_ADDR}"
          echo "The Broadcast server is 'nc'; it can only accept 1 connection at a time. Install 'ncat' for multiple connections."
        else
          BC_SERVER="nc ${BC_SERVER_OPTS} ${BC_PROXY_ADDR} ${BC_PROXY_PORT}"
          echo "The Broadcast proxy server is traditional 'nc' forwarding to ${BC_PROXY_ADDR}:${BC_PROXY_PORT}"
        fi
      else
        # netcat-openbsd
        if [ -z "${BC_PROXY_ADDR}" ]; then
          BC_SERVER="nc ${BC_SERVER_OPTS} -k -l ${BC_ADDR} ${BC_PORT}" # netcat-openbsd
          echo "The Broadcast server is 'nc'; it can only accept 1 connection at a time. Install 'ncat' for multiple connections."
        else
          BC_SERVER="nc ${BC_SERVER_OPTS} ${BC_PROXY_ADDR} ${BC_PROXY_PORT}"
          echo "The Broadcast proxy server is openbsd 'nc' forwarding to ${BC_PROXY_ADDR}:${BC_PROXY_PORT}"
        fi
      fi
      _BC_STATE=STOPPED
    else
      echo "Error: Broadcast: unable to function; cannot find 'ncat' or 'nc' tools" >&2
    fi
    if [ -n "${BC_PROXY_ADDR}" ]; then
      cat <<EOT
Please run this command on the proxy server before init/start of the broadcast:

  ncat -m 1 -k -l ${BC_PROXY_ADDR} ${BC_PROXY_PORT} | ncat -k -l ${BC_PROXY_ADDR} ${BC_PORT} &

Your viewers should connnect using:

  bash
  exec {TV}</dev/tcp/${BC_PROXY_ADDR}/${BC_PORT}
  cat <&\${TV}

and disconnect by pressing Ctrl+C (interrupt signal) then close the socket with:

  exec {TV}<&-
EOT
    fi
  else
    _BC_SERVER=${BC_SERVER}
  fi
  _BC_CONFIGURED=YES
}

# report some status change
broadcast_report()
{
  echo "$(date +%F-%T): Broadcasting $1"
}

# Prepare for broadcasting. Starts the TCP listening server but does not
# connect the terminal to the server.
broadcast_init()
{
  if [ -z ${_BC_CONFIGURED} ]; then
    broadcast_configure
  fi
  if [ -n "${BC_PIPE}" ]; then
    if [ ! -e "${BC_PIPE}" ]; then
      mkfifo "${BC_PIPE}"
    fi
    if [ -r "${BC_PIPE}" ]; then
      # start the TCP server with stdin connected to the FIFO
      ${BC_SERVER} <${BC_PIPE}&
      _BC_SERVER_PID=$!
      { coproc _BC_BROADCAST { tee ${BC_PIPE}; } >&${_BC_FD}; } {_BC_FD}>&1
      _BC_STATE=PAUSED
      broadcast_report "ready but paused"
      return 1
    else
      echo "Error: unable to broadcast; unable to access pipe ${BC_PIPE}" >&2
      return 99
    fi
  fi
}

# starts broadcasting the terminal's stdout and stderr.
broadcast_start()
{
  if [ "${_BC_STATE}" = "UNDEFINED" -o "${_BC_STATE}" = "STOPPED" ]; then
    broadcast_init
  fi
  if [ "${_BC_STATE}" = "PAUSED" ]; then
    # copy of current stdout and stderr if they don't already exist
    [ -z ${_BC_STDOUT} ] && exec {_BC_STDOUT}>&1
    [ -z ${_BC_STDERR} ] && exec {_BC_STDERR}>&2
    # redirect terminal's stdout and stderr to co-process
    exec >&${_BC_BROADCAST[1]} 2>&1
    broadcast_report "started"
    _BC_STATE=BROADCAST
    return 2
  fi
}

# pause/unpause (toggle) an ongoing broadcast by disconnection/reconnection of the terminal
# to the server
broadcast_pause()
{
  if [ "${_BC_STATE}" = "BROADCAST" ]; then
    broadcast_report "${1:-paused}"
    # connect stdout and stderr back to their original file descriptors   
    exec 2>&${_BC_STDERR}
    exec 1>&${_BC_STDOUT}
    _BC_STATE=PAUSED
    return 1
  elif [ "${_BC_STATE}" = "PAUSED" ]; then
    broadcast_start
    return 2
  elif [ "${_BC_STATE}" = "STOPPED" ]; then
    broadcast_init
    return 1
  fi
}

# unpause; reconnect terminal to server
broadcast_continue()
{
  if [ "${_BC_STATE}" = "PAUSED" ]; then
    broadcast_pause
  fi
}

# stop broadcasting and shut down the server
broadcast_stop()
{
  if [ "${_BC_STATE}" = "BROADCAST" ]; then
    broadcast_pause "stopped"
  fi
  if [ "${_BC_STATE}" = "PAUSED" ]; then
    # stop the co-process
    kill -TERM ${_BC_BROADCAST_PID}
    # stop the server
    kill -TERM ${_BC_SERVER_PID}
    _BC_SERVER_PID=""
    _BC_STATE=STOPPED
    return 0
  fi
}

# toggle state of IPv4 firewall INPUT port rule
broadcast_ip4tables()
{
  if [ "${_BC_IP4TABLES}" = "UNSET" ]; then
    # add rule
    sudo iptables -I INPUT 1 ${BC_IP4TABLES_RULE}
    _BC_IP4TABLES=SET
  else
    # remove rule
    sudo iptables -D INPUT ${BC_IP4TABLES_RULE}
    _BC_IP4TABLES=UNSET
  fi
}

# toggle state of IPv6 firewall INPUT port rule
broadcast_ip6tables()
{
  if [ "${_BC_IP6TABLES}" = "UNSET" ]; then
    # add rule
    sudo ip6tables -I INPUT 1 ${BC_IP6TABLES_RULE}
    _BC_IP6TABLES=SET
  else
    # remove rule
    sudo ip6tables -D INPUT ${BC_IP6TABLES_RULE}
    _BC_IP6TABLES=UNSET
  fi
}

# toggle state of IPv4 and IPv6 firewall INPUT port rules
broadcast_iptables()
{
  broadcast_ip4tables
  broadcast_ip6tables
}

# report state
broadcast_status()
{
  if [ -z ${BC_PORT} ]; then
    echo "Not yet configured. Please do 'broadcast config'"
  else
    cat <<EOT
Bash Broadcast: ${_BC_VER}
State:  ${_BC_STATE}
Port:   $([ -z ${BC_PROXY_ADDR} ] && echo ${BC_PORT})
$([ ! -z ${BC_PROXY_ADDR} ] && echo "Proxy:  ${BC_PROXY_ADDR}:${BC_PROXY_PORT}")
Server: ${BC_SERVER}  PID: ${_BC_SERVER_PID}
Firewall IPv4: ${_BC_IP4TABLES} : ${BC_IP4TABLES_RULE}
Firewall IPv6: ${_BC_IP6TABLES} : ${BC_IP6TABLES_RULE}
EOT
  fi
}

# configuration values
broadcast_options()
{
  cat <<EOT
These environment variables have default values but may be redefined *before* 'broadcast init': ${!BC_*}

Variable={Default Value}

BC_PORT=10007           # TCP port for server to listen for viewers on
BC_ADDR=                # IP address for server to listen on; default is bind to all addresses on all interfaces
BC_PIPE=/tmp/broadcast  # First-In First-Out pipe links to server from terminal capture

BC_SERVER=              # alternate server program and command-line (must accept input on stdin)
BC_SERVER_OPTS=         # optional additional command-line arguments for the server program

# proxy (relay) server to forward the terminal to. Useful if terminal is behind a firewall or NAT
BC_PROXY_ADDR=          # no proxy is used if this is not defined
BC_PROXY_PORT=10006     # proxy server port to forward to

# firewall rules for IPv4 and IPv6 when accepting direct connections
# two functions 'broadcast ip4tables' and 'broadcast ip6tables' toggle these rules on/off
# the functions  must be called manually if required
BC_IP4TABLES_RULE="-p tcp -m tcp --dport ${BC_PORT} -j ACCEPT -m comment --comment 'Terminal Broadcast server' "
BC_IP6TABLES_RULE="-p tcp -m tcp --dport ${BC_PORT} -j ACCEPT -m comment --commect 'Terminal Broadcast server' "
EOT
}

# describe usage
broadcast_examples()
{
  cat <<EOT
1. 'source' the file into a bash shell (do NOT execute it directly!)
2. if using a proxy start the remote proxy listening
3. set any of the optional user-defined variables
4. initialise the TCP server
5. start broadcasting (will perform 'init' if not already done)
6. pause the broadcast
7. check status
8. continue the broadcast
9. stop broadcasting

= Example 1: direct broadcast; viewers connect to host on port 10007

bash
. /usr/local/lib/broadcast.bash        #1
broadcast init                         #4
broadcast start                        #5
# terminal now being echoed to viewers
broadcast pause                        #6
# viewers no longer receiving terminal content
broadcast status                       #7
broadcast continue                     #8
# terminal being echoed to viewers again
broadcast stop                         #9
exit

On the viewer:

a. socket open
b. view
c. stop viewing
d. socket close

bash
exec {TV}</dev/tcp/${BC_ADDR:-my.host.com}/${BC_PORT:-10007}  #a
cat <&\${TV}                             #b
# terminal now being echoed to viewers
^C                                     #c
exec {TV}<&-                              #d
exit

= Example 2: proxy broadcast; viewers connect to proxy on port 10007

bash
. /usr/local/lib/broadcast.bash        #1

On the proxy server (my.proxy.com) start the forwarding as a background job (or use service control):

ncat -k -l ${BC_PROXY_ADDR:-my.proxy.com} ${BC_PROXY_PORT:-10006} | ncat -k -l ${BC_PROXY_ADDR:-my.proxy.com} ${BC_PORT:-10007} &    #2

Back on the broadcaster:

BC_PROXY_ADDR=my.proxy.com             #3
broadcast init                         #4
broadcast start                        #5
# terminal now being echoed to viewers
broadcast pause                        #6
# viewers no longer receiving terminal content
broadcast status                       #7
broadcast continue                     #8
# terminal being echoed to viewers again
broadcast stop                         #9
exit

On the viewer:

bash
exec {TV}</dev/tcp/${BC_PROXY_ADDR:-my.proxy.com}/${BC_PORT:-10007}  #a
cat <&\${TV}                             #b
# terminal now being echoed to viewers
^C                                     #c
exec {TV}<&-                           #d
exit
EOT
}

# single-word access to command functions
broadcast()
{
  case $1 in
    config)
      broadcast_configure
      ;;
    init)
      broadcast_init
      ;;
    start)
      broadcast_start
      ;;
    pause)
      broadcast_pause
      ;;
    continue)
      broadcast_continue
      ;;
    stop)
      broadcast_stop
      ;;
    iptables)
      broadcast_iptables
      ;;
    iptables4|ip4tables)
      broadcast_ip4tables
      ;;
    iptables6|ip6tables)
      broadcast_ip6tables
      ;;
    status)
      broadcast_status
      ;;
    options)
      broadcast_options
      ;;
    examples)
      broadcast_examples
      ;;
    help|*)
      cat <<EOT
usage: broadcast [command]
  config     load configuration variables
  init       run the server but do not begin broadcasting
  start      begin broadcasting the terminal
  pause      pause broadcasting (toggles pause state)
  continue   continue broadcasting
  stop       stop broadcasting; stop the server
  iptables   toggle IPv4 and IPv6 firewall rules
  ip4tables  toggle IPv4 firewall rules
  ip6tables  toggle IPv6 firewall rules
  status     report current state
  options    describe configuration variables
  examples   how to use broadcasts
EOT
      ;;
  esac
}

