Socket issue - what should I do?

The problem: I can’t manage to get the bytes from an interactive session ( interactive shell ), capture them, work with them, and send them to the server.

The goal: capture all the bytes of cmd.Stdout, split them with this function https://gist.github.com/xlab/6e204ef96b4433a697b3#file-bytes_split-go ( the first one ), and just then send the chunks to the server.

Original lines:

	cmd := exec.Command(binPath, "-i") // /bin/sh -i    
	cmd.Stdin = conn
	cmd.Stdout = conn
	cmd.Stderr = conn
	cmd.Run()

Once the client connects to the server, if I run any command I get the output. If I run any non-existing commands, I get the stderr. So, everything works fine.

To achieve the goal I want, I tried different things, but after some tests and debugging I think I finally understood why everything I tried did not work: because its an interactive shell rathen than a simple command… but I don’t know how to do the same thing in my case. In practice:

	cmd := exec.Command(binPath, "-i") // /bin/sh -i    
	cmd.Stdin = conn
	//cmd.Stdout = conn
            *****
	stdout := new(bytes.Buffer)
	cmd.Stdout = stdout
	gotthem := stdout.Bytes()
    conn.Write(gotthem)
            *****
	cmd.Stderr = conn
	cmd.Run()

I get the shell on the server, I can type any non-existing stuff to see if Stderr works fine ( which does), but I don’t get any output of the existing commands.
This code should in theory work, but it does not because gotthem its always empty. So I won’t get any output on the server.
I tried also to move cmd.Run() before cmd.Stdout, load the buffer, and so on, but it breaks everything.
Just to make a better idea of what my goal is, I paste a non-working sample code:

	cmd := exec.Command(binPath, "-i") // /bin/sh -i    
	cmd.Stdin = conn
	//cmd.Stdout = conn
            *****
	stdout := new(bytes.Buffer)
	cmd.Stdout = stdout

	gotthem := stdout.Bytes()
	trans := split(gotthem, 100)

	for _, s := range trans {
		    conn.Write(s)
	}
            ********
	cmd.Stderr = conn
	cmd.Run()

Hi @GoNE, just a quick guess after skimming over the code (I might be wrong):

The code calls stdout.Bytes() before calling cmd.Run(). The buffer certainly is still empty at that point.

What type is conn by the way? What is the goal behind connecting all of cmd.Stdin, cmd.Stdout, and cmd.Stderr to conn?

Hi! Thanks for your answer!(:

" The code calls stdout.Bytes() before calling cmd.Run() . The buffer certainly is still empty at that point"

This is correct, but in my case it does not work because it is an interactive shell! And this is why it makes me crazy! ahah
I will explain as better as I can, what I tried and what I understood.
This code https://play.golang.org/p/yfEcqzrNNP5 is a backup code of some versions ago of my actual tool, but trust me, its really really similar to the one I have ( the function handleConn is the same for example; the one that connects the client to the server is almost the same ). This will explain in general everything you asked(:

In my first try, I do get the output, but just of the interactive shell itself. I mean, on the server I get the shell because I get the $ prompted, and I get the errors if a command does not exist. The fact is that I don’t get the output of the commands ran INSIDE the interactive shell. So I get just a part of it. Its like if stdout.Bytes() has just the bytes if /bin/sh -i rather than everything also launched inside /bin/sh -i ( for example: ls, whoami, cat /etc/passwd, etc etc… )

Of course I tried also what you said, but it did not work. It did not even spawn a shell on the server.

cmd := exec.Command(binPath, "-i") // /bin/sh -i    
cmd.Stdin = conn
//cmd.Stdout = conn
        *****
cmd.Run()
stdout := new(bytes.Buffer)
cmd.Stdout = stdout
gotthem := stdout.Bytes()
conn.Write(gotthem)
        *****
......

If I move cmd.Run() from its actual position, it just breaks everything because of the interactivity ahah aegbjkagbkeagavbhegh

This is not so easy as it seems. I guess that buffering the data is in conflict with that.
I hope I explained it well. If not, tell me, I will try again(:

Thanks for explaining and for the playground code.

Based on the playground code, I see that

  • The code opens a TCP connection to a server.
  • The code creates a local command running a shell.
  • The shell’s stdin gets connected to the TCP connection.

Below the point where stdin is set to the TCP connection, there is code that I don’t understand - see the inline comments:

		var stdout bytes.Buffer // an empty buffer
		cmd.Stdout = &stdout // cmd.Stdout is now a pointer to an empty buffer with type io.Writer
		outStr := stdout.Bytes() // outStr is now an empty byte slice
		z := bytes.NewBuffer(outStr) // z is now a new empty buffer
		kk := io.Writer(z) // kk is now z with type io.Writer but gets overwritten in the next line
		kk = conn // kk is now conn with type io.Writer. kk.Write(...) will write to conn
		if kk != nil {
			fmt.Println(kk) // print an io.Writer
		}

The initial description of your goal indicates that you want to achieve the following:

  • Spawn an interactive shell
  • Execute commands via that shell
  • Read the shell’s stdout stream
  • Process that stream
  • Send the results to a server

I took my own try on this (out of curiosity), and I found that using Cmd.StdinPipe() and CmdStdoutPipe() are convenient for connecting the shell’s input and output to my own code.

package main

import (
	"errors"
	"fmt"
	"io"
	"os"
	"os/exec"
)

func processStream(in io.Reader, out io.Writer) error {
	if in == nil {
		return errors.New("in is nil")
	}
	if out == nil {
		return errors.New("out is nil")
	}

	buf := make([]byte, 1024)
	for {

		n, err := in.Read(buf)
		if err != nil {
			if errors.Is(err, io.EOF) {
				break
			}
			return fmt.Errorf("error reading shell output (%d bytes read): %w", n, err)
		}

		n, err = out.Write(buf[:n])
		if err != nil {
			return fmt.Errorf("error writing shell output (%d bytes written): %w", n, err)
		}
	}
	return nil
}

func main() {
	shell := exec.Command("/bin/sh", "-i")

	shellStdin, err := shell.StdinPipe()
	if err != nil {
		fmt.Fprintf(os.Stderr, "error creating shell stdin pipe: %s\n", err)
		os.Exit(1)
	}
	defer shellStdin.Close()

	shellStdout, err := shell.StdoutPipe()
	if err != nil {
		fmt.Fprintf(os.Stderr, "error creating shell stdout pipe: %s\n", err)
		os.Exit(1)
	}

	err = shell.Start() // does not block
	if err != nil {
		fmt.Fprintf(os.Stderr, "error starting shell: %s\n", err)
		os.Exit(1)
	}

	serverConnection := os.Stdout // fake server connection

	// Send commands to the shell
	go func(pipe io.WriteCloser) {
		cmds := []string{
			"echo hello",
			"ls -l",
		}

		for _, cmd := range cmds {
			fmt.Fprintln(pipe, cmd)
		}
		pipe.Close()
	}(shellStdin)

	// Read the shell output, process it, and send it to the server
	err = processStream(shellStdout, serverConnection)
	if err != nil {
		fmt.Fprintf(os.Stderr, "error processing stream: %s\n", err)
		os.Exit(1)
	}
}

AHHHHH I’m so so SO sorry! ahah I pasted a kind of imaginary example to keep in my mind some stuff. Please just ignore the previous link, this is the ( rude ) example code: https://play.golang.org/p/079lwPe63k4

By the way you got the goals! I will add just some more info:

I could not implement everything because I got stuck at the first part: get the bytes of the commands ran inside of the interactive shell.
Thanks for you kind help!(: This is harder than I could even imagine! ahah

No problem, and thanks for posting the intended sample code.

I guess that if you use the StdinPipe() and StdoutPipe() functions as seen in my code, you can work quite comfortably with the data streams going to and coming from the shell.

And when all works fine locally, you can add the server communication part and you should be ready to go. Good luck!