bashsocat

How do I redirect socat's stdin and stdout to its parent process which is a bash script?


What I want to do SHOULD be fairly straight forward but a whole day lost to this already I'm reworking the question a bit to help focus:

The overall goal is to have a bash based "server" that, using either socat (preferred) or ncat (should work well also), receives connections from one or more "clients", processes data from the clients and, when appropriate, returns a response to clients. This script also needs to be able to handle a few sundry "system management" tasks that relate to all this, and this is only mentioned because redirecting the scripts in and out would be problematic.

The overall, quite persistent problem is getting bidirectional data flow to and from the clients and this bash script simultaneously. One or the other at any one time appears to be relatively easy, but both at the same time I have not achieved yet, and when I do, this question is solved.

Early Research:

This question seems to ask the right question, but it garnered no answers that would help me.

The closest I've found so far was in this answer, but it doesn't do what I want to do. That one hands both / all IO to the connecting client, thus making it the server... What I like about it is that it actually DOES set up ncat's input and output in a useful way. Thus, the ideas contained therein were interesting enough to try and I did. However, they use a pipe for input and an > based redirect of output and in retrospect perhaps I should pursue this strategy a bit more.

However, I also tried this answer which also seems very promising, but also doesn't work.

In the comments and proposed answer it became clear that there's confusion over the terms stdin and stdout, mistaking these to mean stdin and stdout of the bash script itself. It didn't help that I was using stdin to get user input from the keyboard to help show sending to clients as well as receiving.

Of course, each program has their own view of what is standard in or out and in my writing I have only ever meant stdout and stdin from the point of view of socat or ncat.

In "pseudo code," here's an example of what the finished code might look like:

while read line
do
   # ...Process line based on it's contents.
   #
   # When a response is needed, stored in a variable
   # called reply, then this might happen:

   if [ -n "$reply" ] ;
   then
      echo "$reply" > socat_stdin
   fi
done < socat_stdout

Along the way I've tried many versions of this:

while read line
do
   echo "$line"
   echo "Back at you: $line" >&3

done < $( exec 3> >(ncat -l -k $port) )

By now I've tried all manner of redirecting to socat's stdin using < and in some configurations of that it then won't start saying it doesn't have two connections specified. Obviously I haven't hit the right combination yet.

Here's where I left off last night:

#!/bin/bash
#
port=7654
inFile=/tmp/in
outFile=/tmp/out
logFile=/tmp/log
exec /bin/socat -U TCP-LISTEN:$port,fork EXEC:"tail -f $inFile" > $outFile 2>$logFile &

while read line
do
   if [ -z "$line" ] ;
   then
      echo "EMPTY LINE!"
   else
      echo "user: $line"
      echo "Back at you: $line" > "$inFile"
   fi
done < $(tail -f "$outFile")

If I could figure out how to get the exec (not EXEC!) to perform a compound command I'd like to try putting a tail -f in front of it and pipe in the responses from the server to the client(s).

In diagnosing that, lsof shows, among many other things:

socat   3588549 root    0r   CHR                1,3      0t0         4 /dev/null

...YES, it persistently has an open file descriptor to /dev/null?! I think this is a clear sign "I'm doing it wrong!" And, indeed, in this above scenario, clients are muted. Hmmm...

Any questions about this, just ask, etc... Thanks for the help...

...For now the question remains; how is this done?


In working on this I found reference in the literature to this file: /usr/share/doc/socat/mail.sh

For anyone curious about this file it points the way BUT requires TWO programs to do this, or a second iteration; it'd be GREAT to eliminate one and I'm looking to perhaps adapt Phillipe's named pipe suggestion.

Here are that file's contents - it's fairly short:

#! /bin/sh
# source: mail.sh
# Copyright Gerhard Rieger and contributors (see file CHANGES)
# Published under the GNU General Public License V.2, see file COPYING

#set -vx

# This is an example for a shell script that can be fed to socat with exec.
# Its clue is that it does not use stdin/stdout for communication with socat,
# so you may feed the mail message via stdin to the script. The message should
# contain appropriate mail headers without continuation lines.
# socat establishes the connection to the SMTP server; the script performs the
# SMTP dialog and afterwards transfers the message body to the server.
# Lines with only a dot are not permitted - use two dots as escape.
# This script supports multiline answers from server, but not much more yet.

# Usage:  cat message.txt |socat exec:"mail.sh target@domain.com",fdin=3,fdout=4 tcp:mail.relay.org:25,crlf

while [ "$1" ]; do
    case "$1" in
    -f) shift; mailfrom="$1"; shift;;
    *) break;;
    esac
done

rcptto="$1"
[ -z "$1" ] && rcptto="root@loopback"
#server=$(expr "$rcptto" : '[^@]*@\(.*\)')
[ -z "$mailfrom" ] && mailfrom="$USER@$(hostname)"

# this function waits for a complete server message, checks if its status
# is in the permitted range (terminates session if not), and returns.
mail_chat () {
    local cmd="$1"
    local errlevel="$2";  [ -z "$errlevel" ] && errlevel=300

    if [ "$cmd" ]; then  echo "> $cmd" >&2;  fi
    if [ -n "$cmd" ]; then echo "$cmd" >&4; fi
    while read status message <&3;
        (
            case "$status" in
            [0-9][0-9][0-9]-*) exit 0;;
            [0-9][0-9][0-9]*) exit 1;;
            *) exit 1;;
            esac
        )
    do :; done
    if [ -z "$status" ]; then  echo smtp connection failed >&2; exit;  fi
    echo "< $status $message" >&2
    if [ "$status" -ge "$errlevel" ]; then
        echo $message >&2
        echo "QUIT" >&4; exit 1
    fi
}


# expect server greeting
mail_chat
#...more calls to the mail_chat function...

Solution

  • Indeed, the socat developers provided a major clue - hard to find, though! - with their file provided with my distribution:

    /usr/share/doc/socat/mail.sh
    

    In digging in and trying it, I got it working with something like this:

    ONE script, uses functions thus:

    function startSocat()
    {
       # ... check for free port, etc
       socat exec:"${BASH_SOURCE[0]} -SD",fdin=3,fdout=4 TCP-LISTEN:$port,fork
       # -SD indicates, "socat-daemon"
       # 
       # The above tells it to start this same code, pass it -SD and connect to it
       # using file descriptors 3 and 4 and, of course where to listen, etc.
    }
    
    function socatLoop
    {
       while read line <&3;
       do
          # Process the read in line - in variable line!
       done
    }
    
    function replyToClient()
    {
       echo "$@" >&4
    }
    

    And that pretty much covers all you really need do. ... I trust the reader can fairly easily figure out how to call these but to be explicit:

    1. Any decision process that works for you can call startSocat(). Once called, we don't expect it to return control to us since socat isn't backgrounded. If you did, then, before exiting, call wait.\
    2. Once the daemon starts, it needs to know it started and that's what the -SD flag is all about. A clever programmer can instead determine if the process is running interactively or not, etc, an exercise left to the reader. However, once it's known this is a deamon, call socatLoop.
    3. From here, socatLoop needs to determine how to behave and when appropriate, call replyToClient, which goes to all connected clients.

    Of course, if you want to orchestrate private dialogues with an individual client, that's more work on your part, but not that hard now that you know the basics!