diff --git a/.githooks/pre-commit b/.githooks/pre-commit deleted file mode 100755 index 043729d..0000000 --- a/.githooks/pre-commit +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash - -go test diff --git a/.gitignore b/.gitignore index f85659c..612bf58 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .shellspy-*.txt shellspy-*.txt +shellspy.txt dist/ diff --git a/README.md b/README.md index 700963f..81cc49e 100644 --- a/README.md +++ b/README.md @@ -24,4 +24,8 @@ OR ``` > go run cmd/main.go --mode local shellspy is running locally -``` \ No newline at end of file +``` + +TODO +- per session readmefile +- authentication \ No newline at end of file diff --git a/cmd/shellspy/main.go b/cmd/shellspy/main.go index 7201e58..0714726 100644 --- a/cmd/shellspy/main.go +++ b/cmd/shellspy/main.go @@ -8,6 +8,5 @@ import ( func main() { - cliArgs := os.Args - shellspy.RunCLI(cliArgs, os.Stdout) + shellspy.RunCLI(os.Args[1:], os.Stdout) } diff --git a/shellspy.go b/shellspy.go index cdc88b1..3e0d2e6 100644 --- a/shellspy.go +++ b/shellspy.go @@ -2,7 +2,6 @@ package shellspy import ( "bufio" - "bytes" "flag" "fmt" "io" @@ -11,225 +10,144 @@ import ( "os/exec" "os/signal" "strings" - "time" + "syscall" ) -type session struct { - Input io.Reader - Output io.Writer - TranscriptOutput io.Writer - File *os.File - Port string +type Session struct { + Input io.Reader + Output io.Writer + Terminal io.Writer + Transcript io.Writer } -type Option func(*session) - -func WithOutput(output io.Writer) Option { - return func(s *session) { - s.Output = output - } +type Server struct { + Port int + C net.Conn } -func WithTranscriptOutput(TranscriptOutput io.Writer) Option { - return func(s *session) { - s.TranscriptOutput = TranscriptOutput +func RunCLI(cliArgs []string, output io.Writer) { + + if len(cliArgs) == 0 { + RunLocally(output) } -} -func NewSession(opts ...Option) (*session, error) { + if len(cliArgs) == 2 { + fs := flag.NewFlagSet("cmd", flag.ExitOnError) + portFlag := fs.Int("port", 2000, "-port 3000") - session := &session{} + fs.Parse(cliArgs) - for _, o := range opts { - o(session) - } - - file, err := CreateTranscriptFile() - if err != nil { - return session, err + if portFlag != nil { + RunRemotely(*portFlag) + } } - session.File = file - - return session, nil } -func RunCLI(cliArgs []string, w io.Writer) { +func NewSession(output io.Writer) (*Session, error) { - s, err := NewSession( - WithOutput(w), - ) + s := &Session{} + file, err := CreateTranscriptFile() if err != nil { - fmt.Printf("%v", err) - os.Exit(1) - } - - fmt.Println(len(cliArgs)) - fmt.Println(cliArgs) - if len(cliArgs) == 1 { - RunLocally(s, w) - } - - fs := flag.NewFlagSet("cmd", flag.ContinueOnError) - fs.Parse(os.Args[1:]) - - switch os.Args[1] { - case "port": - args := fs.Args() - s.Port = args[1] - RunRemotely(s, w) + return nil, err } + s.Transcript = file + s.Input = os.Stdin + s.Terminal = output + s.Output = io.MultiWriter(s.Terminal, s.Transcript) + return s, nil } -func RunLocally(s *session, w io.Writer) { +func CreateTranscriptFile() (*os.File, error) { - buf := &bytes.Buffer{} - buf.WriteString("shellspy is running locally\n") - fmt.Fprint(w, buf) - s.Output = w - input := io.Reader(os.Stdin) - Input(input, s) + file, err := os.OpenFile("shellspy.txt", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + return nil, err + } + return file, nil } -func RunRemotely(s *session, w io.Writer) error { +func RunRemotely(port int) error { - port := s.Port - buf := &bytes.Buffer{} - buf.WriteString("shellspy is running remotely " + port + "\n") - fmt.Fprint(w, buf) - s.TranscriptOutput = buf - - address := "localhost:" + port + fmt.Printf("shellspy is running remotely on port %v and the output file is shellspy.txt\n", port) + address := fmt.Sprintf("localhost:%d", port) listener, err := net.Listen("tcp", address) if err != nil { return err } - killSignal := make(chan os.Signal, 1) - signal.Notify(killSignal, os.Interrupt) + for { + + killSignal := make(chan os.Signal, 1) + signal.Notify(killSignal, syscall.SIGINT, syscall.SIGTERM) + + go func() { + <-killSignal + os.Exit(0) + }() + conn, err := listener.Accept() if err != nil { return err } - go handleConn(conn, s) - <-killSignal - fmt.Println("\nconnection terminated by server!") - listener.Close() + go handleConn(conn) + } } -func handleConn(c net.Conn, s *session) { - - fmt.Fprintf(c, "hello, welcome to shellspy"+"\n") - input := io.Reader(c) - exitStatus := Input(input, s) - if exitStatus == "0" { +func handleConn(c net.Conn) { + s, err := NewSession(c) + if err != nil { + fmt.Fprint(c, err) c.Close() + fmt.Printf("connection is closed due to: %v", err) } - c.Close() -} - -func Input(input io.Reader, s *session) string { - scanner := bufio.NewScanner(input) - for scanner.Scan() { - s.Input = strings.NewReader(scanner.Text()) - exitStatus := s.Run() - if exitStatus == "0" { - return "0" - } - } - return "" + fmt.Printf("new connection established %s and the file is shellspy.txt ", c.RemoteAddr()) + s.Input = c + s.Start() } -func (s *session) Run() string { - - writer := &bytes.Buffer{} - twriter := &bytes.Buffer{} - iReader := &bytes.Buffer{} - fmt.Fprint(iReader, s.Input) - file := s.File - input := iReader.String() - input = strings.TrimPrefix(input, "&{") - input = strings.TrimSuffix(input, " 0 -1}") - stdOut, exitStatus := RunServer(input, file) - if exitStatus == "0" { - return "0" +func RunLocally(output io.Writer) { + s, err := NewSession(output) + if err != nil { + fmt.Println("cannot create transcript file") + os.Exit(1) } - s.Output = writer - s.TranscriptOutput = twriter - fmt.Fprint(writer, stdOut) - fmt.Fprint(twriter, stdOut) - return "" + s.Start() } -func RunServer(line string, file *os.File) (string, string) { +func (s *Session) Start() { - cmd := CommandFromString(line) + fmt.Fprintln(s.Output, "welcome to shellspy") + fmt.Fprintf(s.Output, "$ ") + scanner := bufio.NewScanner(s.Input) - if strings.HasPrefix(line, "exit") { - return "", "0" + for scanner.Scan() { + cmd := CommandFromString(scanner.Text()) + input := scanner.Text() + "\n" + cmd.Stdout = s.Output + cmd.Stderr = s.Output + fmt.Fprint(s.Transcript, input) + if scanner.Text() == "exit" { + os.Exit(0) + } + err := cmd.Run() + if err != nil { + fmt.Fprint(s.Output, err) + } + fmt.Fprintf(s.Output, "$ ") } - - stdOut, stdErr := RunFromCmd(cmd) - WriteTranscript(stdOut, stdErr, cmd, file) - return stdOut, "" } func CommandFromString(line string) *exec.Cmd { + trim := strings.TrimSuffix(line, "\n") name := strings.Fields(trim) args := name[1:] - join := strings.Join(args, " ") - cmd := exec.Command(name[0], join) + cmd := exec.Command(name[0], args...) return cmd } - -func RunFromCmd(cmd *exec.Cmd) (string, string) { - var outb bytes.Buffer - var errb bytes.Buffer - cmd.Stdout = &outb - cmd.Stderr = &errb - - cmd.Run() - - stdOut := outb.String() - stdErr := errb.String() - - return stdOut, stdErr -} - -func CreateTranscriptFile() (*os.File, error) { - now := time.Now() - filename := ".shellspy-" + now.Format("2006-01-02-15:04:05") + ".txt" - file, err := os.OpenFile(filename, - os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) - if err != nil { - return nil, err - } - return file, nil -} - -func WriteTranscript(stdOut, stdErr string, cmd *exec.Cmd, file *os.File) os.File { - - if _, err := file.WriteString(cmd.String()); err != nil { - err = fmt.Errorf("unable to write cmd to disk due to error; %w", err) - file.WriteString(err.Error()) - } - - file.WriteString("\n") - - if stdErr != "" { - file.WriteString(stdErr) - } - - if _, err := file.WriteString(stdOut); err != nil { - err = fmt.Errorf("unable to write stdOut to disk due to error; %w", err) - file.WriteString(err.Error()) - } - - return *file -} diff --git a/shellspy_test.go b/shellspy_test.go index c467ccf..8fd20e4 100644 --- a/shellspy_test.go +++ b/shellspy_test.go @@ -1,20 +1,22 @@ package shellspy_test import ( + "bufio" "bytes" - "fmt" + "io" + "net" "os" "os/exec" "strings" "testing" "time" - "github.com/redscaresu/shellspy" - "github.com/google/go-cmp/cmp" + "github.com/redscaresu/shellspy" ) func TestCommandFromString(t *testing.T) { + t.Parallel() cmdWant := &exec.Cmd{} cmdWant.Args = []string{"/bin/echo", "hello", "world"} @@ -26,37 +28,26 @@ func TestCommandFromString(t *testing.T) { t.Error(cmp.Diff(want, got)) } } +func TestCommandFromStringArgs(t *testing.T) { + t.Parallel() -func TestRunCommand(t *testing.T) { - - cmd := exec.Command("echo", "hello world") - want := "hello world\n" - got, _ := shellspy.RunFromCmd(cmd) - + input := "ls" + want := []string{"ls"} + got := shellspy.CommandFromString(input).Args if !cmp.Equal(want, got) { t.Error(cmp.Diff(want, got)) } - - cmd = exec.Command("pwd", "-x") - want = "pwd: illegal option -- x\nusage: pwd [-L | -P]\n" - - _, got = shellspy.RunFromCmd(cmd) - if !cmp.Equal(want, got) { - t.Error(cmp.Diff(want, got)) - } - } -func TestWriteShellScript(t *testing.T) { +func TestRunCommand(t *testing.T) { + t.Parallel() - wantBuf := &bytes.Buffer{} gotBuf := &bytes.Buffer{} - wantBuf.WriteString("hello world\n") - session, _ := shellspy.NewSession( - shellspy.WithTranscriptOutput(wantBuf), - ) - - session.Input = strings.NewReader("echo hello world") + s, err := shellspy.NewSession(gotBuf) + s.Input = strings.NewReader("echo hello world") + if err != nil { + t.Fatal("unable to create file") + } tempDir := t.TempDir() now := time.Now() @@ -66,13 +57,12 @@ func TestWriteShellScript(t *testing.T) { if err != nil { t.Fatal("unable to create file") } + s.Transcript = file - session.File = file - session.Run() - fmt.Fprint(gotBuf, session.TranscriptOutput) - - want := wantBuf.String() + s.Start() + // fmt.Fprint(wantBuf, s.Terminal) got := gotBuf.String() + want := "welcome to shellspy\n$ hello world\n$ " if !cmp.Equal(want, got) { t.Error(cmp.Diff(want, got)) @@ -80,24 +70,50 @@ func TestWriteShellScript(t *testing.T) { } func TestRunWithoutPortFlagRunInteractively(t *testing.T) { + t.Parallel() + var flagArgs []string buf := &bytes.Buffer{} - flagArgs := []string{"/var/folders/1v/4mmgcg8s51362djr4g9s9sfw0000gn/T/go-build3590226918/b001/exe/main"} + go shellspy.RunCLI(flagArgs, buf) + + for buf.String() == "" { + time.Sleep(5 * time.Millisecond) + } - shellspy.RunCLI(flagArgs, buf) got := buf.String() - want := "shellspy is running locally\n" + want := "welcome to shellspy\n$ " if !cmp.Equal(want, got) { t.Error(cmp.Diff(want, got)) } - } -// func TestPortFlagStartsNetListener(t *testing.T) { +func TestPortFlagListensOnPort(t *testing.T) { + t.Parallel() + + flagArgs := []string{"-port", "6666"} + + go shellspy.RunCLI(flagArgs, io.Discard) + + conn, err := net.Dial("tcp", "127.0.0.1:6666") + for err != nil { + t.Log("retrying network connection") + time.Sleep(10 * time.Millisecond) + conn, err = net.Dial("tcp", "127.0.0.1:6666") + } + + scanner := bufio.NewScanner(conn) + if !scanner.Scan() { + t.Fatal("nothing has been returned by the scanner") + } + + got := scanner.Text() + + want := "welcome to shellspy" -// flagArgs := []string{"port", "9999"} -// shellspy.RunCLI(flagArgs) -// } + if !cmp.Equal(want, got) { + t.Error(cmp.Diff(want, got)) + } +}