From ba1ad8f13a470c0ee848d615b16e5a41ab63e003 Mon Sep 17 00:00:00 2001 From: Hugo Thunnissen Date: Sat, 7 May 2022 22:37:44 +0200 Subject: [PATCH] The big work in progress nothing works commit --- commands/constants.go | 9 + commands/echo.go | 45 ++++ commands/env.go | 26 ++ commands/getconf.go | 15 ++ commands/handler.go | 123 ++++++++++ commands/mkdir.go | 34 +++ commands/shell.go | 363 ++++++++++++++++++++++++++++ commands/shell/block.go | 60 +++++ commands/shell/builtins/echo.go | 22 ++ commands/shell/command_executor.go | 8 + commands/shell/dollar.go | 132 ++++++++++ commands/shell/dollar_test.go | 54 +++++ commands/shell/line.go | 55 +++++ commands/shell/parser.go | 143 +++++++++++ commands/shell/parser_parameters.go | 20 ++ commands/shell/parser_test.go | 44 ++++ commands/shell/pipe.go | 30 +++ commands/shell/quotes.go | 92 +++++++ commands/shell/quotes_test.go | 61 +++++ shell.go => commands/shell/shell.go | 2 +- commands/shell/state.go | 112 +++++++++ commands/shell/statement.go | 49 ++++ commands/shell/subshell.go | 63 +++++ commands/shell/token.go | 41 ++++ commands/shell/util.go | 19 ++ commands/shell/util_test.go | 26 ++ commands/shell/whitespace.go | 45 ++++ commands/shell/word.go | 128 ++++++++++ commands/shell/word_test.go | 86 +++++++ commands/stty.go | 150 ++++++++++++ commands/test.go | 71 ++++++ go.mod | 24 +- go.sum | 67 +---- handler.go | 324 ------------------------- local.log | 289 ---------------------- main.go | 4 +- sshd.go | 19 +- storage/interface.go | 18 ++ filestore.go => storage/ipfs.go | 51 ++-- 39 files changed, 2214 insertions(+), 710 deletions(-) create mode 100644 commands/constants.go create mode 100644 commands/echo.go create mode 100644 commands/env.go create mode 100644 commands/getconf.go create mode 100644 commands/handler.go create mode 100644 commands/mkdir.go create mode 100644 commands/shell.go create mode 100644 commands/shell/block.go create mode 100644 commands/shell/builtins/echo.go create mode 100644 commands/shell/command_executor.go create mode 100644 commands/shell/dollar.go create mode 100644 commands/shell/dollar_test.go create mode 100644 commands/shell/line.go create mode 100644 commands/shell/parser.go create mode 100644 commands/shell/parser_parameters.go create mode 100644 commands/shell/parser_test.go create mode 100644 commands/shell/pipe.go create mode 100644 commands/shell/quotes.go create mode 100644 commands/shell/quotes_test.go rename shell.go => commands/shell/shell.go (99%) create mode 100644 commands/shell/state.go create mode 100644 commands/shell/statement.go create mode 100644 commands/shell/subshell.go create mode 100644 commands/shell/token.go create mode 100644 commands/shell/util.go create mode 100644 commands/shell/util_test.go create mode 100644 commands/shell/whitespace.go create mode 100644 commands/shell/word.go create mode 100644 commands/shell/word_test.go create mode 100644 commands/stty.go create mode 100644 commands/test.go delete mode 100644 handler.go delete mode 100644 local.log create mode 100644 storage/interface.go rename filestore.go => storage/ipfs.go (71%) diff --git a/commands/constants.go b/commands/constants.go new file mode 100644 index 0000000..8e96fa2 --- /dev/null +++ b/commands/constants.go @@ -0,0 +1,9 @@ +package commands + +const fakePath = "/bin:/usr/bin" +const fakePipeBuf = "4096" +const trampSuccess = "tramp_exit_status 0\n" +const trampFailure = "tramp_exit_status 1\n" +const lsExecutable = "/bin/ls" +const testExecutable = "/bin/test" +const statExecutable = "/bin/stat" diff --git a/commands/echo.go b/commands/echo.go new file mode 100644 index 0000000..b4c29c1 --- /dev/null +++ b/commands/echo.go @@ -0,0 +1,45 @@ +package commands + +import ( + "fmt" + "log" + "strings" +) + +type EchoCommand struct{} + +func (c *EchoCommand) Execute(shell *Shell, args []string) error { + if len(args) > 1 { + if "\"`uname -sr`\"" == args[0]+" "+args[1] { + log.Println("Tramp requested uname, returning a fake one: " + fakeUname) + _, err := shell.WriteOutput( + []byte( + fmt.Sprintf( + "\"%s\"\n%s\n", + fakeUname, + "tramp_exit_status 0", + ), + ), + ) + return err + } + + if "\"`tty`\"" == args[0] { + log.Println("Telling tramp that it's dealing with a tty") + _, err := shell.WriteOutputString("\"/dev/pts/0\"\n" + trampSuccess) + + return err + } + } + + if len(args) > 0 { + if args[0] == "~root" { + log.Println("Telling tramp root's home directory") + _, err := shell.WriteOutputString("/root") + return err + } + } + + _, err := shell.WriteOutput([]byte(strings.Join(args, " ") + "\n")) + return err +} diff --git a/commands/env.go b/commands/env.go new file mode 100644 index 0000000..3fb67bf --- /dev/null +++ b/commands/env.go @@ -0,0 +1,26 @@ +package commands + +import "strings" + +func AssignmentsToMap(args []string) map[string]string { + var assignments = make(map[string]string) + for _, arg := range args { + split := strings.Split(arg, "=") + if len(split) > 1 { + assignments[split[0]] = split[1] + } + } + + return assignments +} + +func ApplyEnvIfPresent(shell *Shell, comm *Command) { + var args []string + args = append(args, comm.Name) + args = append(args, comm.Arguments...) + + assignments := AssignmentsToMap(args) + if ps1, ok := assignments["PS1"]; ok { + shell.SetPrompt(ps1) + } +} diff --git a/commands/getconf.go b/commands/getconf.go new file mode 100644 index 0000000..99f2a47 --- /dev/null +++ b/commands/getconf.go @@ -0,0 +1,15 @@ +package commands + +type GetConfCommand struct{} + +func (c *GetConfCommand) Execute(shell *Shell, args []string) error { + if len(args) > 0 { + switch args[0] { + case "PATH": + shell.WriteOutputString(fakePath + "\n" + trampSuccess) + case "PIPE_BUF": + shell.WriteOutputString(fakePipeBuf + "\n" + trampSuccess) + } + } + return nil +} diff --git a/commands/handler.go b/commands/handler.go new file mode 100644 index 0000000..69db4b0 --- /dev/null +++ b/commands/handler.go @@ -0,0 +1,123 @@ +package commands + +import ( + "fmt" + "log" + "strings" + + "git.snorba.art/hugo/nssh/storage" +) + +type CommandHandler struct { + shell *Shell + filestore storage.Filestore +} + +func NewCommandHandler(shell *Shell, filestore storage.Filestore) *CommandHandler { + return &CommandHandler{ + shell: shell, + filestore: filestore, + } +} + +func (h *CommandHandler) Handle(comm *Command) error { + if strings.Contains(comm.Name, "=") { + ApplyEnvIfPresent(h.shell, comm) + return nil + } + + switch comm.Name { + case "exec": + ApplyEnvIfPresent(h.shell, comm) + case "(cd": + h.shell.WriteOutput([]byte("tramp_exit_status 0\n")) + case "echo": + (&EchoCommand{}).Execute(h.shell, comm.Arguments) + case "(echo": + if strings.Join(comm.Arguments, " ") == "foo ; echo bar)" { + log.Println("Handling tramp's foobar test") + h.shell.WriteOutputString("foo\nbar\n") + } + case "stty": + return (&SttyCommand{}).Execute(h.shell, comm.Arguments) + case "set": + log.Println("Ignoring \"set\" command") + case "locale": + if len(comm.Arguments) > 0 { + if comm.Arguments[0] == "-a" { + locales := []string{"C", "C.UTF-8", "POSIX", "en_US.utf8"} + log.Println("Tramp requested locale, returning fake values: ", locales) + h.shell.WriteOutputString( + strings.Join(locales, "\n") + "\n", + ) + return nil + } + } + + log.Println("Ignoring \"locale\" command with unsupported parameters: ", comm.Arguments) + case "getconf": + return (&GetConfCommand{}).Execute(h.shell, comm.Arguments) + case "test", "/bin/test": + return (&TestCommand{filestore: h.filestore}).Execute(h.shell, comm.Arguments) + case "while": + if len(comm.Arguments) > 6 { + var err error + + log.Printf("Pointing tramp to executable for \"%s\"", comm.Arguments[6]) + + switch comm.Arguments[6] { + case "$d/ls": + _, err = h.shell.WriteOutputString("tramp_executable " + lsExecutable) + case "$d/test": + _, err = h.shell.WriteOutputString("tramp_executable " + testExecutable) + case "$d/stat": + _, err = h.shell.WriteOutputString("tramp_executable " + statExecutable) + } + return err + } + case "ls", "/bin/ls": + if len(comm.Arguments) > 2 && + comm.Arguments[1] == "-al" && + comm.Arguments[2] == "/dev/null" { + _, err := h.shell.WriteOutputString( + fmt.Sprintf( + "crw-rw-rw- 1 root root 1, 3 May 5 00:05 /dev/null\n%s", + trampSuccess, + ), + ) + + return err + } + + if len(comm.Arguments) > 1 && comm.Arguments[0] == "-lnd" { + _, err := h.shell.WriteOutputString( + fmt.Sprintf( + "drwxrwxr-x 3 1000 1000 28 Apr 28 14:04 %s\n%s", + comm.Arguments[1], + trampSuccess, + ), + ) + + return err + } + + // should handle "--color=never --help 2>&1 | grep -iq busybox" so that we + // can pretend to have busybox ls. The assumption is that this is easier + // to emulate than the --dired flag of ls + if len(comm.Arguments) > 7 && + comm.Arguments[4] == "grep" && + comm.Arguments[6] == "busybox" { + _, err := h.shell.WriteOutputString(trampSuccess) + return err + } + + _, err := h.shell.WriteOutputString(trampFailure) + return err + case "mkdir": + return (&MkdirCommand{filestore: h.filestore}).Execute(comm.Arguments) + default: + log.Printf("Error: Received unexpected command %s", comm.Name) + } + + return nil +} diff --git a/commands/mkdir.go b/commands/mkdir.go new file mode 100644 index 0000000..c69c2b6 --- /dev/null +++ b/commands/mkdir.go @@ -0,0 +1,34 @@ +package commands + +import ( + "strings" + + "git.snorba.art/hugo/nssh/storage" +) + +type MkdirCommand struct { + filestore storage.Filestore +} + +func (c *MkdirCommand) Execute(arguments []string) error { + for _, dir := range arguments { + if len(dir) > 0 && dir[0] != '-' { + var currentPath string + for _, part := range strings.Split(dir, "/") { + if part == "" { + continue + } + + currentPath += "/" + part + if exists, _ := c.filestore.DirectoryExists(currentPath); !exists { + err := c.filestore.MakeDirectory(currentPath) + if err != nil { + return err + } + } + } + } + } + + return nil +} diff --git a/commands/shell.go b/commands/shell.go new file mode 100644 index 0000000..979dadb --- /dev/null +++ b/commands/shell.go @@ -0,0 +1,363 @@ +package commands + +import ( + "bufio" + "errors" + "io" + "log" + "os" + "strings" + "unsafe" + + "github.com/google/shlex" + "github.com/pkg/term/termios" + "golang.org/x/sys/unix" +) + +type Shell struct { + pty *os.File + tty *os.File + prompt []byte + + readData []byte + readIndex int + winsize *Winsize +} + +type Command struct { + Name string + Arguments []string +} + +func (s *Shell) GetPty() *os.File { + return s.pty +} + +func (s *Shell) Attach(conn io.ReadWriteCloser) error { + pty, tty, err := termios.Pty() + if err != nil { + return err + } + + s.pty = pty + s.tty = tty + + if s.winsize != nil { + SetWinsize(s.pty.Fd(), s.winsize) + } + + go func() { + defer s.pty.Close() + io.Copy(s.pty, conn) + }() + + go func() { + defer conn.Close() + io.Copy(conn, s.pty) + }() + + _, err = s.tty.Write(s.prompt) + return err +} + +func NewShell(prompt string) *Shell { + return &Shell{ + prompt: []byte(prompt), + } +} + +func (s *Shell) SetWinsize(w, h uint32) { + s.winsize = &Winsize{Width: uint16(w), Height: uint16(h)} + + if s.pty != nil { + SetWinsize(s.pty.Fd(), s.winsize) + } +} + +func (s *Shell) Close() (error, error) { + return s.pty.Close(), s.tty.Close() +} + +var ErrEmptyCommand error = errors.New("Empty command line") + +type State struct { + Variables map[string]string + Commands map[string]CommandExecutor +} + +type Statement interface { + Words() []Word + Statements() []Statement + AddStatement(Statement) + Evaluate(state State, output io.Writer) (exitcode uint8) +} + +type BaseStatement struct { + words []Word + statements []Statement +} + +func (s *BaseStatement) Words() []Word { + return s.words +} + +func (s *BaseStatement) Statements() []Statement { + return s.statements +} + +func (s *BaseStatement) AddStatement(statement Statement) { + s.statements = append(s.statements, statement) +} + +func (s *BaseStatement) AddWord(word Word) { + s.words = append(s.words, word) +} + +type Word string + +func (w Word) Statements() []Statement { + return []Statement{} +} + +func (w Word) AddStatement(statement Statement) {} + +func (w Word) AddWord(word Word) {} + +type ParserParameters interface { + MakeStatement() Statement + ShouldLeave(Word, rune) bool + Supports(rune) bool + SubParsers() []Parser +} + +type Parser interface { + Parse(*bufio.Reader) (Statement, error) + Parameters() ParserParameters +} + +type BaseParser struct { + parameters ParserParameters +} + +func (p *BaseParser) Parameters() ParserParameters { + return p.parameters +} + +type BaseParameters struct { + EntryMarkers []rune + subParsers []Parser +} + +func (p *BaseParameters) SubParsers() []Parser { + return p.subParsers +} + +func (p *BaseParameters) Supports(r rune) bool { + for _, char := range p.EntryMarkers { + if char == r { + return true + } + } + + return false +} + +func (p *BaseParameters) MakeStatement() Statement { + return &BaseStatement{} +} + +func (p *BaseParameters) ShouldLeave(w Word, r rune) bool { + if r == '\n' || r == ';' { + return true + } +} + +func (p *BaseParser) Parse(r *bufio.Reader) (Statement, error) { + var currentWord Word = "" + statement := p.Parameters().MakeStatement() + for { + char, _, err := r.ReadRune() + if err != nil { + return statement, err + } + + if char == ' ' || char == '\t' { + statement.AddWord(currentWord) + currentWord = "" + } + + if p.Parameters().ShouldLeave(currentWord, char) { + return statement, nil + } + + var matchedParser bool + + for _, parser := range p.Parameters().SubParsers() { + if parser.Parameters().Supports(char) { + matchedParser = true + nestedStatement, err := parser.Parse(r) + + if word, ok := nestedStatement.(Word); ok { + statement.AddWord(word) + } else { + statement.AddStatement(nestedStatement) + } + + if err != nil { + return statement, err + } + } + } + + if !matchedParser { + currentWord += Word(char) + } + } +} + +func (s *Shell) ReadCommand() (*Command, error) { + var currentLine string + var backslash bool + var insideDoubleQuote bool + var insideSingleQuote bool + var maybeHeredoc bool + var insideHeredoc bool + var heredocMarker string + var heredocMarkerComplete bool + var heredocCurrentWord string + + reader := bufio.NewReader(s.tty) + +GatherCommand: + for { + char, _, err := reader.ReadRune() + if err != nil { + return nil, err + } + + if insideHeredoc { + currentLine += string(char) + + if !heredocMarkerComplete && char == '<' { + insideHeredoc = false + continue + } + + if char == '\n' { + log.Printf("Heredoc current word: \"%s\"", heredocCurrentWord) + if heredocCurrentWord == heredocMarker { + log.Println("exiting heredoc") + insideHeredoc = false + heredocMarkerComplete = false + break GatherCommand + } + + if !heredocMarkerComplete { + log.Println("Encountered heredoc marker: " + heredocCurrentWord) + heredocMarker = strings.Trim(heredocCurrentWord, "'") + heredocMarkerComplete = true + } + + heredocCurrentWord = "" + continue + } + + heredocCurrentWord += string(char) + continue + } + + if backslash { + if !insideDoubleQuote && char == '\n' { + currentLine += string(" ") + continue + } + + currentLine += "\\" + string(char) + backslash = false + continue + } + + switch rune(char) { + case '<': + currentLine += string(char) + if maybeHeredoc { + maybeHeredoc = false + insideHeredoc = true + continue + } + + maybeHeredoc = true + continue + case '\\': + backslash = true + continue + case '"': + insideDoubleQuote = !insideDoubleQuote + currentLine += string(char) + case '\'': + insideSingleQuote = !insideSingleQuote + currentLine += string(char) + case '\n': + if insideSingleQuote || insideDoubleQuote { + currentLine += string(char) + continue + } + + break GatherCommand + default: + currentLine += string(char) + } + } + + commandLine, err := shlex.Split(currentLine) + if err != nil { + return nil, err + } + + if len(commandLine) == 0 { + return nil, ErrEmptyCommand + } + + comm := &Command{Name: commandLine[0]} + if len(commandLine) > 1 { + comm.Arguments = commandLine[1:] + } + + return comm, nil +} + +func (s *Shell) SetPrompt(prompt string) { + log.Printf("Changing prompt to \"%s\"", prompt) + s.prompt = []byte(prompt) +} + +func (s *Shell) WritePrompt() error { + _, err := s.tty.Write(s.prompt) + return err +} + +func (s *Shell) WriteOutput(output []byte) (int, error) { + return s.tty.Write(output) +} + +func (s *Shell) WriteOutputString(output string) (int, error) { + return s.WriteOutput([]byte(output)) +} + +// Winsize stores the Height and Width of a terminal. +type Winsize struct { + Height uint16 + Width uint16 + x uint16 // unused + y uint16 // unused +} + +// SetWinsize sets the size of the given pty. +func SetWinsize(fd uintptr, winsize *Winsize) { + log.Printf("window resize %dx%d", winsize.Width, winsize.Height) + unix.Syscall( + unix.SYS_IOCTL, + fd, + uintptr(unix.TIOCSWINSZ), uintptr(unsafe.Pointer(winsize)), + ) +} diff --git a/commands/shell/block.go b/commands/shell/block.go new file mode 100644 index 0000000..ccb0f3e --- /dev/null +++ b/commands/shell/block.go @@ -0,0 +1,60 @@ +package shell + +type Block struct { + BaseToken +} + +type DollarBlock struct { + BaseToken +} + +type BlockParameters struct{} +type DollarBlockParameters struct { + BlockParameters +} + +func (p *BlockParameters) Enter(i *CharIterator) error { + return i.Next() +} + +func (p *DollarBlockParameters) SubParsers() []Parser { + return []Parser{ + &BaseParser{ + parameters: &WordParameters{}, + }, + } +} + +func (p *DollarBlockParameters) MakeToken() Token { + return &DollarBlock{} +} + +func (p *BlockParameters) SubParsers() []Parser { + return []Parser{ + &BaseParser{ + parameters: &StatementParameters{}, + }, + } +} + +func (p *BlockParameters) Supports(charsBefore []rune, r rune) bool { + if r == '{' { + return len(charsBefore) == 0 || + countBackslashSuffixes(charsBefore)%2 == 0 + } + + return false +} + +func (p *BlockParameters) ShouldLeave(charsBefore []rune, r rune) bool { + if r == '}' { + return len(charsBefore) == 0 || + countBackslashSuffixes(charsBefore)%2 == 0 + } + + return false +} + +func (p *BlockParameters) MakeToken() Token { + return &Block{} +} diff --git a/commands/shell/builtins/echo.go b/commands/shell/builtins/echo.go new file mode 100644 index 0000000..df8dcb4 --- /dev/null +++ b/commands/shell/builtins/echo.go @@ -0,0 +1,22 @@ +package builtins + +import ( + "io" + "strings" +) + +type Echo struct{} + +func (e *Echo) Name() string { + return "echo" +} + +func (e *Echo) Execute(arguments []string, _ io.Reader, stdout io.Writer, stderr io.Writer) uint8 { + _, err := stdout.Write([]byte(strings.Join(arguments, " "))) + if err != nil { + stderr.Write([]byte(err.Error())) + return 1 + } + + return 0 +} diff --git a/commands/shell/command_executor.go b/commands/shell/command_executor.go new file mode 100644 index 0000000..13393d3 --- /dev/null +++ b/commands/shell/command_executor.go @@ -0,0 +1,8 @@ +package shell + +import "io" + +type CommandExecutor interface { + Name() string + Execute(arguments []string, stdin io.Reader, stdout io.Writer, stderr io.Writer) (exitcode uint8) +} diff --git a/commands/shell/dollar.go b/commands/shell/dollar.go new file mode 100644 index 0000000..bca3afa --- /dev/null +++ b/commands/shell/dollar.go @@ -0,0 +1,132 @@ +package shell + +import ( + "io" + "log" +) + +type DollarParameters struct { + ignoreParams *IgnoreFirstCharParameters +} + +func (p *DollarParameters) SubParsers() []Parser { + if p.ignoreParams == nil { + p.ignoreParams = &IgnoreFirstCharParameters{} + } + + return []Parser{ + &BaseParser{ + parameters: p.ignoreParams, + }, + &BaseParser{ + parameters: &WordParameters{}, + }, + &BaseParser{ + parameters: &SubshellParameters{}, + }, + &BaseParser{ + parameters: &DollarBlockParameters{}, + }, + } +} + +func (p *DollarParameters) Enter(_ *CharIterator) error { + if p.ignoreParams != nil { + p.ignoreParams.Reset() + } + + return nil +} + +func (p *DollarParameters) Supports(charsBefore []rune, r rune) bool { + if r == '$' { + return len(charsBefore) == 0 || + countBackslashSuffixes(charsBefore)%2 == 0 + } + + return false +} + +func (p *DollarParameters) ShouldLeave(charsBefore []rune, r rune) bool { + if isWhitespace(r) || r == ';' { + return true + } + + return false +} + +func (p *DollarParameters) Leave(i *CharIterator) error { + return i.Previous() +} + +func (p *DollarParameters) MakeToken() Token { + return &Dollar{} +} + +type Dollar struct { + BaseToken +} + +func (d *Dollar) Expand(state State, stdin io.Reader, stderr io.Writer) ([]string, uint8) { + if len(d.tokens) > 1 { + logstr := "Unexpected tokens after dollar sign: " + d.tokens[1].String() + log.Println(logstr) + stderr.Write([]byte(logstr)) + return []string{""}, 2 + } + + if word, ok := d.tokens[0].(*Word); ok { + return []string{state.Variable(word.String())}, 0 + } else if block, ok := d.tokens[0].(*DollarBlock); ok && len(block.Tokens()) > 0 { + + if word, ok := block.Tokens()[0].(*Word); ok { + return []string{state.Variable(word.String())}, 0 + } else { + log.Println("Whaat") + } + } else { + logstr := "Unexpected tokens after dollar sign: " + d.String() + log.Println(logstr) + stderr.Write([]byte(logstr)) + return []string{""}, 2 + } + + return []string{""}, 1 +} + +type IgnoreFirstCharParameters struct { + entered bool +} + +func (p *IgnoreFirstCharParameters) Reset() { + p.entered = false +} + +func (p *IgnoreFirstCharParameters) Enter(_ *CharIterator) error { + p.entered = true + return nil +} + +func (p *IgnoreFirstCharParameters) SubParsers() []Parser { + return []Parser{ + &BaseParser{ + parameters: &CharParserParameters{}, + }, + } +} + +func (p *IgnoreFirstCharParameters) MakeToken() Token { + return nil +} + +func (p *IgnoreFirstCharParameters) Supports(_ []rune, r rune) bool { + if !p.entered { + return true + } + + return false +} + +func (p *IgnoreFirstCharParameters) ShouldLeave(_ []rune, r rune) bool { + return true +} diff --git a/commands/shell/dollar_test.go b/commands/shell/dollar_test.go new file mode 100644 index 0000000..2ac89f0 --- /dev/null +++ b/commands/shell/dollar_test.go @@ -0,0 +1,54 @@ +package shell + +import ( + "bufio" + "io" + "strings" + "testing" + + "github.com/stretchr/testify/suite" +) + +type DollarTest struct { + suite.Suite +} + +func (t *DollarTest) TestParse() { + parser := &BaseParser{&DollarParameters{}} + reader := bufio.NewReader(strings.NewReader("$variable")) + + iterator, err := NewCharIterator(reader) + t.NoError(err) + + token, err := parser.Parse(iterator, &CharCollection{}) + t.ErrorIs(err, io.EOF) + + t.IsType(&Dollar{}, token) + t.IsType(&Word{}, token.Tokens()[0]) + + t.Equal(token.Tokens()[0].String(), "variable") +} + +func (t *DollarTest) TestExpand() { + parser := &BaseParser{&DollarParameters{}} + reader := bufio.NewReader(strings.NewReader("$variable $hey")) + + iterator, err := NewCharIterator(reader) + t.NoError(err) + + token, err := parser.Parse(iterator, &CharCollection{}) + + t.IsType(&Dollar{}, token) + t.IsType(&Word{}, token.Tokens()[0]) + + state := NewShellState() + state.SetVariable("variable", "Test") + + expanded, exit := token.(Expandable).Expand(state, nil, nil) + t.Equal(uint8(0), exit) + t.Equal(expanded[0], "Test") +} + +func TestDollarTest(t *testing.T) { + suite.Run(t, new(DollarTest)) +} diff --git a/commands/shell/line.go b/commands/shell/line.go new file mode 100644 index 0000000..5618dff --- /dev/null +++ b/commands/shell/line.go @@ -0,0 +1,55 @@ +package shell + +import ( + "fmt" + "io" +) + +type LineParameters struct{} + +func (p *LineParameters) SubParsers() []Parser { + return []Parser{ + &BaseParser{ + parameters: &StatementParameters{}, + }, + } +} + +func (p *LineParameters) Supports(charsBefore []rune, r rune) bool { + return true +} + +func (p *LineParameters) ShouldLeave(charsBefore []rune, r rune) bool { + if r == '\n' { + return countBackslashSuffixes(charsBefore)%2 == 0 + } + + return false +} + +func (p *LineParameters) MakeToken() Token { + return &Line{} +} + +type Line struct { + BaseToken +} + +func (l *Line) Evaluate(state State, stdin io.Reader, stdout io.Writer, stderr io.Writer) uint8 { + var retval uint8 = 0 + + for _, token := range l.Tokens() { + if eval, ok := token.(Evalable); ok { + retval = eval.Evaluate(state, stdin, stdout, stderr) + } else { + stderr.Write([]byte( + fmt.Sprintf( + "shell: Syntax error near unexpected token: %s", + token.String(), + ), + )) + } + } + + return retval +} diff --git a/commands/shell/parser.go b/commands/shell/parser.go new file mode 100644 index 0000000..fe9383f --- /dev/null +++ b/commands/shell/parser.go @@ -0,0 +1,143 @@ +package shell + +import ( + "bufio" + "errors" + "fmt" + "reflect" +) + +type Parser interface { + Parse(*CharIterator, *CharCollection) (Token, error) + Parameters() ParserParameters +} + +type BaseParser struct { + parameters ParserParameters +} + +func (p *BaseParser) Parameters() ParserParameters { + return p.parameters +} + +type CharIterator struct { + reader *bufio.Reader + currentChar rune + lastChar rune +} + +func NewCharIterator(reader *bufio.Reader) (*CharIterator, error) { + char, _, err := reader.ReadRune() + if err != nil { + return nil, err + } + + return &CharIterator{ + reader: reader, + currentChar: char, + }, nil +} + +func (i *CharIterator) Next() error { + var err error + + i.lastChar = i.currentChar + i.currentChar, _, err = i.reader.ReadRune() + + return err +} + +func (i *CharIterator) Previous() error { + if i.lastChar == -1 { + return errors.New("Chariterator can only go back once") + } + + err := i.reader.UnreadRune() + if err != nil { + return err + } + + i.currentChar = i.lastChar + i.lastChar = -1 + + return err +} + +type CharCollection struct { + chars []rune +} + +func (c *CharCollection) Append(r rune) { + c.chars = append(c.chars, r) +} + +func (c *CharCollection) Chars() []rune { + return c.chars +} + +func (i CharIterator) Current() rune { + return i.currentChar +} + +func (p *BaseParser) Parse(i *CharIterator, charsBefore *CharCollection) (Token, error) { + token := p.Parameters().MakeToken() + if enter, ok := p.parameters.(Enterable); ok { + err := enter.Enter(i) + if err != nil { + return nil, err + } + + } + parsers := p.Parameters().SubParsers() + +ParseLoop: + for { + char := i.Current() + + if p.Parameters().ShouldLeave(charsBefore.Chars(), char) { + if leave, ok := p.Parameters().(Leavable); ok { + err := leave.Leave(i) + if err != nil { + return token, err + } + } + + return token, nil + } + + var matchedParser bool + for _, parser := range parsers { + if parser.Parameters().Supports(charsBefore.Chars(), char) { + matchedParser = true + + nestedToken, err := parser.Parse(i, charsBefore) + + if token != nil { + token.AddToken(nestedToken) + } + + if err != nil { + return token, err + } + + charsBefore.Append(char) + err = i.Next() + if err != nil { + return token, err + } + + continue ParseLoop + } + } + + if !matchedParser { + return token, fmt.Errorf( + "Parser encountered unsupported token or char. Parser: %s full text: %s -\n Char: %s", + reflect.TypeOf(*&p.parameters).String(), + string(charsBefore.Chars()), + string(char), + ) + } + + } +} diff --git a/commands/shell/parser_parameters.go b/commands/shell/parser_parameters.go new file mode 100644 index 0000000..f7ed667 --- /dev/null +++ b/commands/shell/parser_parameters.go @@ -0,0 +1,20 @@ +package shell + +type ParserParameters interface { + MakeToken() Token + ShouldLeave(charsBefore []rune, r rune) bool + Supports(charsBefore []rune, r rune) bool + SubParsers() []Parser +} + +type Enterable interface { + Enter(*CharIterator) error +} + +type Resetable interface { + Reset() +} + +type Leavable interface { + Leave(*CharIterator) error +} diff --git a/commands/shell/parser_test.go b/commands/shell/parser_test.go new file mode 100644 index 0000000..57fdee0 --- /dev/null +++ b/commands/shell/parser_test.go @@ -0,0 +1,44 @@ +package shell + +import ( + "bufio" + "strings" + "testing" + + "github.com/stretchr/testify/suite" +) + +type CharIteratorTest struct { + suite.Suite +} + +func (t *CharIteratorTest) TestNextAndBacktrack() { + charIterator, err := NewCharIterator(bufio.NewReader(strings.NewReader("abcdefg"))) + t.NoError(err) + + t.Equal(string('a'), string(charIterator.Current())) + + err = charIterator.Next() + t.NoError(err) + + t.Equal(string('b'), string(charIterator.Current())) + + err = charIterator.Previous() + t.NoError(err) + + t.Equal(string('a'), string(charIterator.Current())) + + err = charIterator.Previous() + t.Error(err) + t.Equal("Chariterator can only go back once", err.Error()) + + err = charIterator.Next() + t.NoError(err) + + t.Equal(string('b'), string(charIterator.Current())) + +} + +func TestCharIteratorTest(t *testing.T) { + suite.Run(t, new(CharIteratorTest)) +} diff --git a/commands/shell/pipe.go b/commands/shell/pipe.go new file mode 100644 index 0000000..69d4b36 --- /dev/null +++ b/commands/shell/pipe.go @@ -0,0 +1,30 @@ +package shell + +type Pipe struct { + BaseToken +} + +type PipeParameters struct { + StatementParameters +} + +func (p *PipeParameters) SubParsers() []Parser { + return []Parser{ + &BaseParser{ + parameters: &StatementParameters{}, + }, + } +} + +func (p *PipeParameters) Supports(charsBefore []rune, r rune) bool { + if r == '|' { + return len(charsBefore) == 0 || + countBackslashSuffixes(charsBefore)%2 == 0 + } + + return false +} + +func (p *PipeParameters) MakeToken() Token { + return &Pipe{} +} diff --git a/commands/shell/quotes.go b/commands/shell/quotes.go new file mode 100644 index 0000000..fe5ee0a --- /dev/null +++ b/commands/shell/quotes.go @@ -0,0 +1,92 @@ +package shell + +import ( + "io" + "log" + "strings" +) + +type QuoteParameters struct{} + +func (p *QuoteParameters) SubParsers() []Parser { + return []Parser{ + &BaseParser{ + parameters: &DollarParameters{}, + }, + &BaseParser{ + parameters: &CharParserParameters{}, + }, + } +} + +func (p *QuoteParameters) Supports(charsBefore []rune, r rune) bool { + if r == '"' { + return len(charsBefore) == 0 || + countBackslashSuffixes(charsBefore)%2 == 0 + } + + return false +} + +func (p *QuoteParameters) ShouldLeave(charsBefore []rune, r rune) bool { + if r == '"' { + return len(charsBefore) == 0 || + countBackslashSuffixes(charsBefore)%2 == 0 + } + + return false +} + +func (p *QuoteParameters) MakeToken() Token { + return &Quote{} +} + +type Quote struct { + BaseToken +} + +func (q *Quote) Expand(state State, stdin io.Reader, stderr io.Writer) ([]string, uint8) { + var retcode uint8 + var str string + + for _, token := range q.Tokens() { + if expand, ok := token.(Expandable); ok { + log.Println(expand) + var expansion []string + expansion, retcode = expand.Expand(state, stdin, stderr) + + str += strings.Join(expansion, " ") + continue + } + + str += token.String() + } + + return []string{strings.Trim(str, "'")}, retcode +} + +type SingleQuoteParameters struct { + QuoteParameters +} + +type SingleQuote struct { + BaseToken +} + +func (p *SingleQuoteParameters) Supports(charsBefore []rune, r rune) bool { + if r == '\'' { + return len(charsBefore) == 0 || + countBackslashSuffixes(charsBefore)%2 == 0 + } + + return false +} + +func (p *SingleQuoteParameters) ShouldLeave(charsBefore []rune, r rune) bool { + if r == '\'' { + return len(charsBefore) == 0 || + countBackslashSuffixes(charsBefore)%2 == 0 + } + + return false +} diff --git a/commands/shell/quotes_test.go b/commands/shell/quotes_test.go new file mode 100644 index 0000000..2aa556a --- /dev/null +++ b/commands/shell/quotes_test.go @@ -0,0 +1,61 @@ +package shell + +import ( + "bufio" + "fmt" + "io" + "strings" + "testing" + + "git.snorba.art/hugo/nssh/commands/shell/builtins" + "github.com/stretchr/testify/suite" +) + +type QuotesTest struct { + suite.Suite +} + +func (t *QuotesTest) TestParse() { + expectedContents := fmt.Sprintf("'%s'", "word1 word2 word3\n word4") + + parser := &BaseParser{&QuoteParameters{}} + reader := bufio.NewReader(strings.NewReader(expectedContents)) + + iterator, err := NewCharIterator(reader) + t.NoError(err) + + token, err := parser.Parse(iterator, &CharCollection{}) + t.ErrorIs(err, io.EOF) + + t.Equal(expectedContents, token.String()) + + expanded, _ := token.(Expandable).Expand(nil, nil, nil) + t.Equal("word1 word2 word3\n word4", strings.Join(expanded, " ")) +} + +func (t *QuotesTest) TestExpandVariables() { + parser := &BaseParser{&QuoteParameters{}} + reader := bufio.NewReader( + strings.NewReader("This is a $variable ${variable2} $variable"), + ) + + state := NewShellState() + state.SetVariable("variable", "supergood") + state.SetVariable("variable2", "thing") + + echo := &builtins.Echo{} + state.SetCommand(echo.Name(), echo) + + iterator, err := NewCharIterator(reader) + t.NoError(err) + + token, err := parser.Parse(iterator, &CharCollection{}) + t.ErrorIs(err, io.EOF) + + expanded, _ := token.(Expandable).Expand(state, nil, nil) + t.Equal("This is a supergood thing supergood", expanded[0]) +} + +func TestQuotesTest(t *testing.T) { + suite.Run(t, new(QuotesTest)) +} diff --git a/shell.go b/commands/shell/shell.go similarity index 99% rename from shell.go rename to commands/shell/shell.go index 3dda0aa..5ac4659 100644 --- a/shell.go +++ b/commands/shell/shell.go @@ -1,4 +1,4 @@ -package main +package shell import ( "bufio" diff --git a/commands/shell/state.go b/commands/shell/state.go new file mode 100644 index 0000000..64e2d67 --- /dev/null +++ b/commands/shell/state.go @@ -0,0 +1,112 @@ +package shell + +type State interface { + Variable(string) string + Command(string) CommandExecutor + Env(string) string + + Pushd(string) error + Popd() error + Cwd() string + + SetVariable(string, string) + SetCommand(string, CommandExecutor) + SetEnv(string, string) + + Clone() State +} + +func NewShellState() *ShellState { + return &ShellState{ + variables: map[string]string{}, + commands: map[string]CommandExecutor{}, + env: map[string]string{}, + dirstack: []string{}, + } +} + +type ShellState struct { + variables map[string]string + commands map[string]CommandExecutor + env map[string]string + dirstack []string +} + +func (s *ShellState) Variable(n string) string { + if v, ok := s.variables[n]; ok { + return v + } + + return "" +} + +func (s *ShellState) Command(n string) CommandExecutor { + if v, ok := s.commands[n]; ok { + return v + } + + return nil +} + +func (s *ShellState) Env(n string) string { + if v, ok := s.env[n]; ok { + return v + } + + return "" +} + +func (s *ShellState) SetVariable(n string, v string) { + s.variables[n] = v +} + +func (s *ShellState) SetCommand(n string, e CommandExecutor) { + s.commands[n] = e +} + +func (s *ShellState) SetEnv(n string, v string) { + s.env[n] = v +} + +func (s *ShellState) Pushd(dir string) error { + s.dirstack = append([]string{dir}, s.dirstack...) + return nil +} + +func (s *ShellState) Popd() error { + if len(s.dirstack) == 0 { + return nil + } + + s.dirstack = s.dirstack[1:] + return nil +} + +func (s *ShellState) Cwd() string { + if len(s.dirstack) == 0 { + return "" + } + + return s.dirstack[0] +} + +func (s *ShellState) Clone() State { + state := NewShellState() + + for k, v := range s.commands { + state.SetCommand(k, v) + } + + for k, v := range s.variables { + state.SetVariable(k, v) + } + + for k, v := range s.env { + state.SetEnv(k, v) + } + + state.dirstack = make([]string, len(s.dirstack)) + copy(s.dirstack, state.dirstack) + + return state +} diff --git a/commands/shell/statement.go b/commands/shell/statement.go new file mode 100644 index 0000000..5603b83 --- /dev/null +++ b/commands/shell/statement.go @@ -0,0 +1,49 @@ +package shell + +type Statement struct { + BaseToken +} + +type StatementParameters struct{} + +func (p *StatementParameters) SubParsers() []Parser { + return []Parser{ + &BaseParser{ + parameters: &QuoteParameters{}, + }, + &BaseParser{ + parameters: &SingleQuoteParameters{}, + }, + &BaseParser{ + parameters: &WordParameters{}, + }, + &BaseParser{ + parameters: &DollarParameters{}, + }, + &BaseParser{ + parameters: &PipeParameters{}, + }, + &BaseParser{ + parameters: &SubshellParameters{}, + }, + &BaseParser{ + parameters: &BlockParameters{}, + }, + } +} + +func (p *StatementParameters) Supports(charsBefore []rune, r rune) bool { + return true +} + +func (p *StatementParameters) MakeToken() Token { + return &Statement{} +} + +func (p *StatementParameters) ShouldLeave(charsBefore []rune, r rune) bool { + if r == '\n' || r == ';' { + return countBackslashSuffixes(charsBefore)%2 == 0 + } + + return false +} diff --git a/commands/shell/subshell.go b/commands/shell/subshell.go new file mode 100644 index 0000000..1e9f4be --- /dev/null +++ b/commands/shell/subshell.go @@ -0,0 +1,63 @@ +package shell + +type SubshellParameters struct{} + +func (p *SubshellParameters) SubParsers() []Parser { + return []Parser{ + &BaseParser{ + parameters: &StatementParameters{}, + }, + } +} + +func (p *SubshellParameters) Supports(charsBefore []rune, r rune) bool { + if r == '(' { + return len(charsBefore) == 0 || + countBackslashSuffixes(charsBefore)%2 == 0 + } + + return false +} + +func (p *SubshellParameters) ShouldLeave(charsBefore []rune, r rune) bool { + if r == ')' { + return len(charsBefore) == 0 || + countBackslashSuffixes(charsBefore)%2 == 0 + } + + return false +} + +func (p *SubshellParameters) MakeToken() Token { + return &Subshell{} +} + +type Subshell struct { + BaseToken +} + +type Backtick struct { + Subshell +} + +type BacktickParameters struct { + SubshellParameters +} + +func (p *BacktickParameters) Supports(charsBefore []rune, r rune) bool { + if r == '`' { + return len(charsBefore) == 0 || + countBackslashSuffixes(charsBefore)%2 == 0 + } + + return false +} + +func (p *BacktickParameters) ShouldLeave(charsBefore []rune, r rune) bool { + if r == '`' { + return len(charsBefore) == 0 || + countBackslashSuffixes(charsBefore)%2 == 0 + } + + return false +} diff --git a/commands/shell/token.go b/commands/shell/token.go new file mode 100644 index 0000000..9ed3e2f --- /dev/null +++ b/commands/shell/token.go @@ -0,0 +1,41 @@ +package shell + +import "io" + +type Token interface { + Tokens() []Token + AddToken(Token) + String() string +} + +type Expandable interface { + Expand(state State, stdin io.Reader, stderr io.Writer) ([]string, uint8) +} + +type Evalable interface { + Evaluate(state State, stdin io.Reader, stdout io.Writer, stderr io.Writer) uint8 +} + +type BaseToken struct { + tokens []Token +} + +func (t *BaseToken) Tokens() []Token { + return t.tokens +} + +func (t *BaseToken) AddToken(token Token) { + if token != nil { + t.tokens = append(t.tokens, token) + } +} + +func (t *BaseToken) String() string { + var str string + + for _, token := range t.Tokens() { + str += token.String() + } + + return str +} diff --git a/commands/shell/util.go b/commands/shell/util.go new file mode 100644 index 0000000..9b20394 --- /dev/null +++ b/commands/shell/util.go @@ -0,0 +1,19 @@ +package shell + +func countBackslashSuffixes(chars []rune) int { + backslashCount := 0 + for i := len(chars) - 1; i > 0; i-- { + if chars[i] == '\\' { + backslashCount += 1 + continue + } + + break + } + + return backslashCount +} + +func isWhitespace(r rune) bool { + return r == ' ' || r == '\t' || r == '\n' +} diff --git a/commands/shell/util_test.go b/commands/shell/util_test.go new file mode 100644 index 0000000..f23a7db --- /dev/null +++ b/commands/shell/util_test.go @@ -0,0 +1,26 @@ +package shell + +import ( + "testing" + + "github.com/stretchr/testify/suite" +) + +type UtilTest struct { + suite.Suite +} + +func (t *UtilTest) TestCountBackslashSuffixes() { + three := []rune{'a', '\\', 'b', '\\', '\\', '\\'} + t.Equal(3, countBackslashSuffixes(three)) + + six := []rune{'a', '\\', 'b', '\\', '\\', '\\', '\\', '\\', '\\'} + t.Equal(6, countBackslashSuffixes(six)) + + zero := []rune{'a', '\\', 'b', '\\', '\\', '\\', 'c'} + t.Equal(0, countBackslashSuffixes(zero)) +} + +func TestUtilTest(t *testing.T) { + suite.Run(t, new(UtilTest)) +} diff --git a/commands/shell/whitespace.go b/commands/shell/whitespace.go new file mode 100644 index 0000000..cf4072b --- /dev/null +++ b/commands/shell/whitespace.go @@ -0,0 +1,45 @@ +package shell + +type WhitespaceParameters struct{} + +func (p *WhitespaceParameters) SubParsers() []Parser { + return []Parser{ + &BaseParser{ + parameters: &WhitespaceParameters{}, + }, + } +} + +func (p *WhitespaceParameters) Supports(charsBefore []rune, r rune) bool { + return isWhitespace(r) +} + +func (p *WhitespaceParameters) ShouldLeave(charsBefore []rune, r rune) bool { + return !p.Supports(charsBefore, r) +} + +func (p *WhitespaceParameters) MakeToken() Token { + return &Whitespace{} +} + +type Whitespace struct { + tokens []Token +} + +func (q *Whitespace) AddToken(s Token) { + q.tokens = append(q.tokens, s) +} + +func (q *Whitespace) Tokens() []Token { + return q.tokens +} + +func (q *Whitespace) String() string { + var str string + + for _, t := range q.Tokens() { + str += t.String() + } + + return str +} diff --git a/commands/shell/word.go b/commands/shell/word.go new file mode 100644 index 0000000..a3e657d --- /dev/null +++ b/commands/shell/word.go @@ -0,0 +1,128 @@ +package shell + +import ( + "io" + "log" + "regexp" + "strings" +) + +func NewWord(wordS string) *Word { + word := new(Word) + + for _, c := range wordS { + word.AddToken(Char(c)) + } + + return word +} + +type Word struct { + chars []Char + consecutiveBackslashes int +} + +func (w *Word) Tokens() []Token { + return []Token{} +} + +func (w *Word) AddToken(token Token) { + if char, ok := token.(Char); ok { + if rune(char) == '\\' { + w.consecutiveBackslashes += 1 + } else if w.consecutiveBackslashes%2 == 1 { + if len(w.chars) > 0 { + w.chars = w.chars[:len(w.chars)-1] + } else { + log.Println( + "Parse error: funky shit is happening, a word with 0" + + " chars was expected to contain a backslash", + ) + } + + w.consecutiveBackslashes = 0 + } + + w.chars = append(w.chars, char) + } +} + +func (w *Word) Expand(_ State, _ io.Reader, _ io.Writer) ([]string, uint8) { + var str string + for _, char := range w.chars { + str += string(char) + } + + return []string{str}, 0 +} + +func (w *Word) String() string { + str, _ := w.Expand(nil, nil, nil) + + return strings.Join(str, "") +} + +type WordParameters struct{} + +func (p *WordParameters) MakeToken() Token { + return new(Word) +} + +var wordRegexp = regexp.MustCompile("[0-9a-zA-Z_.~/:\\\\=-]") + +func (p *WordParameters) Supports(charsBefore []rune, r rune) bool { + return wordRegexp.MatchString(string(r)) +} + +func (p *WordParameters) ShouldLeave(charsBefore []rune, r rune) bool { + if !p.Supports(charsBefore, r) { + return countBackslashSuffixes(charsBefore)%2 == 0 + } + + return false +} + +func (p *WordParameters) Leave(i *CharIterator) error { + return i.Previous() +} + +func (p *WordParameters) SubParsers() []Parser { + return []Parser{ + &BaseParser{ + parameters: &CharParserParameters{}, + }, + } +} + +type Char rune + +func (c Char) Tokens() []Token { + return nil +} + +func (c Char) AddToken(_ Token) {} + +func (c Char) String() string { + return string(c) +} + +type CharParserParameters struct { + currentRune rune +} + +func (p *CharParserParameters) MakeToken() Token { + return Char(p.currentRune) +} + +func (p *CharParserParameters) Supports(charsBefore []rune, r rune) bool { + p.currentRune = r + return true +} + +func (p *CharParserParameters) ShouldLeave(charsBefore []rune, r rune) bool { + return true +} + +func (p *CharParserParameters) SubParsers() []Parser { + return nil +} diff --git a/commands/shell/word_test.go b/commands/shell/word_test.go new file mode 100644 index 0000000..4164072 --- /dev/null +++ b/commands/shell/word_test.go @@ -0,0 +1,86 @@ +package shell + +import ( + "bufio" + "io" + "strings" + "testing" + + "github.com/stretchr/testify/suite" +) + +type WordTest struct { + suite.Suite +} + +func (t *WordTest) TestSupports() { + p := &WordParameters{} + + noChars := []rune{} + t.True(p.Supports(noChars, 'a')) + t.True(p.Supports(noChars, '-')) + t.True(p.Supports(noChars, 'A')) + t.True(p.Supports(noChars, '0')) + t.True(p.Supports(noChars, '.')) +} + +func (t *WordTest) TestShouldLeave() { + p := &WordParameters{} + + noChars := []rune{} + t.True(p.ShouldLeave(noChars, ' ')) + t.True(p.ShouldLeave(noChars, '\t')) + t.True(p.ShouldLeave(noChars, '\n')) +} +func (t *WordTest) TestAddToken() { + word := NewWord("") + + word.AddToken(Char('a')) + + t.Equal("a", word.String()) +} + +func (t *WordTest) TestParse() { + parser := &BaseParser{&WordParameters{}} + reader := bufio.NewReader(strings.NewReader("word1 word2 word\\ 3")) + + iterator, err := NewCharIterator(reader) + t.NoError(err) + + token, err := parser.Parse(iterator, &CharCollection{}) + t.NoError(err) + t.Equal("word1", token.String()) + + iterator.Next() + token, err = parser.Parse(iterator, &CharCollection{}) + t.NoError(err) + t.Equal("", token.String()) + + iterator.Next() + iterator.Next() + token, err = parser.Parse(iterator, &CharCollection{}) + t.NoError(err) + t.Equal("word2", token.String()) + + iterator.Next() + iterator.Next() + token, err = parser.Parse(iterator, &CharCollection{}) + t.ErrorIs(err, io.EOF) + t.Equal("word 3", token.String()) +} + +func (t *WordTest) TestParseBackslash() { + parser := &BaseParser{&WordParameters{}} + reader := bufio.NewReader(strings.NewReader("w\\\\\\\\\\o=rd1\\ \\$wo\\\\rd2\\\\\\ word\\ 3")) + + iterator, err := NewCharIterator(reader) + t.NoError(err) + + token, err := parser.Parse(iterator, &CharCollection{}) + t.ErrorIs(err, io.EOF) + t.Equal("w\\\\\\\\o=rd1 $wo\\\\rd2\\\\ word 3", token.String()) +} + +func TestWordTest(t *testing.T) { + suite.Run(t, new(WordTest)) +} diff --git a/commands/stty.go b/commands/stty.go new file mode 100644 index 0000000..904f6b9 --- /dev/null +++ b/commands/stty.go @@ -0,0 +1,150 @@ +package commands + +import ( + "errors" + "log" + "strconv" + + "golang.org/x/sys/unix" +) + +func Control(c rune) rune { + return c & 0x1f +} + +func sttyCharFromDesc(c string) (rune, error) { + if len(c) > 2 || len(c) < 1 { + return 'i', errors.New( + "stty char description has unsupported length: " + + strconv.Itoa(len(c)), + ) + } + + if rune(c[0]) == '^' { + return Control(rune(c[1])), nil + } + + return rune(c[0]), nil +} + +const fakeUname = "Linux 5.16.19-76051619-generic" + +type CommandExecutor interface { + Execute(shell *Shell, args []string) error +} + +type SttyCommand struct{} + +func (c *SttyCommand) Execute(shell *Shell, args []string) error { + // Tramp uses: + // -inlcr: don't translate newline to carriage return + // -onlcr: don't translate newline to carriage return-newline + // -echo: echo input characters + // kill CHAR: CHAR will erase the current line + // erase CHAR: CHAR will erase the last character typed + // icanon: enable special charactersq + + fd := int(shell.GetPty().Fd()) + const ioctlReadTermios = unix.TCGETS // Linux + const ioctlWriteTermios = unix.TCSETS // Linux + + termios, err := unix.IoctlGetTermios(fd, ioctlReadTermios) + if err != nil { + panic(err) + } + + newState := *termios + + var skipNext bool + for i, arg := range args { + if skipNext { + skipNext = false + continue + } + + var disable bool + if arg[0] == '-' { + disable = true + arg = arg[1:] + } + + switch arg { + case "onlcr": + if disable { + newState.Iflag &^= unix.ONLCR + continue + } + + newState.Iflag |= unix.ONLCR + case "inlcr": + if disable { + newState.Iflag &^= unix.INLCR + continue + } + + newState.Iflag |= unix.INLCR + case "echo": + if disable { + newState.Lflag &^= unix.ECHO + continue + } + + newState.Lflag |= unix.ECHO + case "kill": + skipNext = true + + char, err := sttyCharFromDesc(args[i+1]) + if err != nil { + log.Printf( + "Warning: Not applying unsupported character description for stty kill: %s", + args[i+1], + ) + continue + } + + newState.Cc[unix.VKILL] = uint8(char) + case "erase": + skipNext = true + + char, err := sttyCharFromDesc(args[i+1]) + if err != nil { + log.Printf( + "Warning: Not applying unsupported character description for stty erase: %s", + args[i+1], + ) + continue + } + + newState.Cc[unix.VERASE] = uint8(char) + case "icanon": + if disable { + newState.Lflag &^= unix.ICANON + continue + } + + newState.Lflag |= unix.ICANON + + case "tab0": + newState.Oflag = (newState.Oflag & ^uint32(unix.TABDLY)) | unix.TAB0 + case "iutf8": + if disable { + newState.Iflag &^= unix.IUTF8 + continue + } + + newState.Iflag |= unix.IUTF8 + + default: + log.Printf( + "Warning, tramp requested unexpected stty option %s", + arg, + ) + } + } + + if err := unix.IoctlSetTermios(fd, ioctlWriteTermios, &newState); err != nil { + return err + } + + return nil +} diff --git a/commands/test.go b/commands/test.go new file mode 100644 index 0000000..aa22936 --- /dev/null +++ b/commands/test.go @@ -0,0 +1,71 @@ +package commands + +import ( + "log" + + "git.snorba.art/hugo/nssh/storage" +) + +type TestCommand struct { + filestore storage.Filestore +} + +var fakeDirs = map[string]bool{ + "/bin": true, + "/usr/bin": true, + "/sbin": true, +} + +func (c *TestCommand) Execute(shell *Shell, args []string) error { + if len(args) > 0 && args[0] == "0" { + _, err := shell.WriteOutputString(trampSuccess) + return err + } + + if len(args) > 1 && args[0] == "-d" { + log.Printf("Checking for existance of directory %s", args[1]) + var exists bool + var err error + + if answer, ok := fakeDirs[args[1]]; ok { + exists = answer + } else { + exists, err = c.filestore.DirectoryExists(args[1]) + if err != nil { + return err + } + } + + if exists { + log.Printf("Directory %s exists", args[1]) + _, err := shell.WriteOutputString(trampSuccess) + return err + } + + _, err = shell.WriteOutputString(trampFailure) + return err + } + + if len(args) > 1 && (args[0] == "-e" || args[0] == "-r") { + log.Printf("Checking for existance of file %s", args[1]) + // Tramp sometimes throws in a sentence "this file does not exist", my + // guess is it does this to check if the test command is working + // properly. This catches that and makes the test command fail + // appropriately. + if len(args) > 5 && args[2] == "this" && args[5] == "not" { + _, err := shell.WriteOutputString(trampFailure) + return err + } + + if exists, _ := c.filestore.Exists(args[1]); exists { + log.Printf("File %s exists", args[1]) + _, err := shell.WriteOutputString(trampSuccess) + return err + } + + _, err := shell.WriteOutputString(trampFailure) + return err + } + + return nil +} diff --git a/go.mod b/go.mod index 828ff05..2b5a4e7 100644 --- a/go.mod +++ b/go.mod @@ -3,14 +3,21 @@ module git.snorba.art/hugo/nssh go 1.18 require ( - github.com/SkynetLabs/go-skynet/v2 v2.1.0 // indirect + github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 + github.com/ipfs/go-ipfs-api v0.3.0 + github.com/pkg/term v1.1.0 + github.com/stretchr/testify v1.7.0 + golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f + golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4 + golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6 +) + +require ( github.com/btcsuite/btcd v0.22.1 // indirect github.com/crackcomm/go-gitignore v0.0.0-20170627025303-887ab5e44cc3 // indirect - github.com/creack/pty v1.1.18 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/ipfs/go-cid v0.2.0 // indirect - github.com/ipfs/go-ipfs-api v0.3.0 // indirect github.com/ipfs/go-ipfs-files v0.1.1 // indirect github.com/klauspost/cpuid/v2 v2.0.12 // indirect github.com/libp2p/go-buffer-pool v0.0.2 // indirect @@ -28,15 +35,10 @@ require ( github.com/multiformats/go-multicodec v0.4.1 // indirect github.com/multiformats/go-multihash v0.1.0 // indirect github.com/multiformats/go-varint v0.0.6 // indirect - github.com/pkg/term v1.1.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/spacemonkeygo/spacelog v0.0.0-20180420211403-2296661a0572 // indirect github.com/spaolacci/murmur3 v1.1.0 // indirect github.com/whyrusleeping/tar-utils v0.0.0-20201201191210-20a61371de5b // indirect - gitlab.com/NebulousLabs/errors v0.0.0-20200929122200-06c536cf6975 // indirect - go.opencensus.io v0.23.0 // indirect - golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f // indirect - golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4 // indirect - golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6 // indirect - golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect + gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect lukechampine.com/blake3 v1.1.7 // indirect ) diff --git a/go.sum b/go.sum index 1114029..e826ad0 100644 --- a/go.sum +++ b/go.sum @@ -1,11 +1,10 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/SkynetLabs/go-skynet/v2 v2.1.0 h1:fYUoe2Lu8epLBkd+B2jaKbS82KQ2Vv+eqLG0R8zMwZM= -github.com/SkynetLabs/go-skynet/v2 v2.1.0/go.mod h1:XOk0zwGlXeGjHQgmhXTEk7qTD6FVv3dXPW38Wh3XsIc= github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= github.com/btcsuite/btcd v0.22.1 h1:CnwP9LM/M9xuRrGSCGeMVs9iv09uMqwsVX7EeIpgV2c= github.com/btcsuite/btcd v0.22.1/go.mod h1:wqgTSL29+50LRkmOVknEdmt8ZojIzhuWvgu/iptuN7Y= +github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 h1:q0rUy8C/TYNBQS1+CGKw68tLOFYSNEs0TFnxxnS9+4U= github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg= @@ -13,49 +12,28 @@ github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVa github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cheekybits/is v0.0.0-20150225183255-68e9c0620927 h1:SKI1/fuSdodxmNNyVBR8d7X/HuLnRpvvFO0AgyQk764= github.com/cheekybits/is v0.0.0-20150225183255-68e9c0620927/go.mod h1:h/aW8ynjgkuj+NQRlZcDbAbM1ORAbXjXX77sX7T289U= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/crackcomm/go-gitignore v0.0.0-20170627025303-887ab5e44cc3 h1:HVTnpeuvF6Owjd5mniCL8DEXo7uYXdQEmOP4FJbV5tg= github.com/crackcomm/go-gitignore v0.0.0-20170627025303-887ab5e44cc3/go.mod h1:p1d6YEZWvFzEh4KLyvBcVSnrfNDDvK2zfK/4x2v/4pE= -github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= -github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= -github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/ipfs/go-cid v0.0.7/go.mod h1:6Ux9z5e+HpkQdckYoX1PG/6xqKspzlEIR5SDmgqgC/I= github.com/ipfs/go-cid v0.2.0 h1:01JTiihFq9en9Vz0lc0VDWvZe/uBonGpzo4THP0vcQ0= @@ -120,33 +98,27 @@ github.com/multiformats/go-multihash v0.1.0/go.mod h1:RJlXsxt6vHGaia+S8We0Erjhoj github.com/multiformats/go-varint v0.0.5/go.mod h1:3Ls8CIEsrijN6+B7PbrXRPxHRPuXSrVKRY101jdMZYE= github.com/multiformats/go-varint v0.0.6 h1:gk85QWKxh3TazbLxED/NlDVv8+q+ReFJk7Y2W/KhfNY= github.com/multiformats/go-varint v0.0.6/go.mod h1:3Ls8CIEsrijN6+B7PbrXRPxHRPuXSrVKRY101jdMZYE= -github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/pkg/term v1.1.0 h1:xIAAdCMh3QIAy+5FrE8Ad8XoDhEU4ufwbaSozViP9kk= github.com/pkg/term v1.1.0/go.mod h1:E25nymQcrSllhX42Ok8MRm1+hyBdHY0dCeiKZ9jpNGw= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/spacemonkeygo/spacelog v0.0.0-20180420211403-2296661a0572 h1:RC6RW7j+1+HkWaX/Yh71Ee5ZHaHYt7ZP4sQgUrm6cDU= github.com/spacemonkeygo/spacelog v0.0.0-20180420211403-2296661a0572/go.mod h1:w0SWMsp6j9O/dk4/ZpIhL+3CkG8ofA2vuv7k+ltqUMc= github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/whyrusleeping/tar-utils v0.0.0-20180509141711-8c6c8ba81d5c/go.mod h1:xxcJeBb7SIUl/Wzkz1eVKJE/CB34YNrqX2TQI6jY9zs= github.com/whyrusleeping/tar-utils v0.0.0-20201201191210-20a61371de5b h1:wA3QeTsaAXybLL2kb2cKhCAQTHgYTMwuI8lBlJSv5V8= github.com/whyrusleeping/tar-utils v0.0.0-20201201191210-20a61371de5b/go.mod h1:xT1Y5p2JR2PfSZihE0s4mjdJaRGp1waCTf5JzhQLBck= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -gitlab.com/NebulousLabs/errors v0.0.0-20171229012116-7ead97ef90b8/go.mod h1:ZkMZ0dpQyWwlENaeZVBiQRjhMEZvk6VTXquzl3FOFP8= -gitlab.com/NebulousLabs/errors v0.0.0-20200929122200-06c536cf6975 h1:L/ENs/Ar1bFzUeKx6m3XjlmBgIUlykX9dzvp5k9NGxc= -gitlab.com/NebulousLabs/errors v0.0.0-20200929122200-06c536cf6975/go.mod h1:ZkMZ0dpQyWwlENaeZVBiQRjhMEZvk6VTXquzl3FOFP8= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M= -go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -170,7 +142,6 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4 h1:HVyaeDAYux4pnY+D/SiwmLOR36ewZ4iGQIIrtnuCjFA= golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -190,17 +161,11 @@ golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210309074719-68d13333faf2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6 h1:nonptSpoQ4vQjyraW20DXPAglgQfVnM9ZC6MmNLMR60= golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -209,7 +174,6 @@ golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= @@ -221,32 +185,17 @@ google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9Ywl google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= -google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= -google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= -google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= -gopkg.in/h2non/gock.v1 v1.0.15/go.mod h1:sX4zAkdYX1TRGJ2JY156cFspQn4yRWn6p9EMdODlynE= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= lukechampine.com/blake3 v1.1.6/go.mod h1:tkKEOtDkNtklkXtLNEOGNq5tcV90tJiA1vAA12R78LA= lukechampine.com/blake3 v1.1.7 h1:GgRMhmdsuK8+ii6UZFDL8Nb+VyMwadAgcJyfYHxG6n0= lukechampine.com/blake3 v1.1.7/go.mod h1:tkKEOtDkNtklkXtLNEOGNq5tcV90tJiA1vAA12R78LA= diff --git a/handler.go b/handler.go deleted file mode 100644 index e921e15..0000000 --- a/handler.go +++ /dev/null @@ -1,324 +0,0 @@ -package main - -import ( - "errors" - "fmt" - "log" - "strconv" - "strings" - - "golang.org/x/sys/unix" -) - -type CommandHandler struct { - shell *Shell - filestore Filestore -} - -func NewCommandHandler(shell *Shell, filestore Filestore) *CommandHandler { - return &CommandHandler{ - shell: shell, - filestore: filestore, - } -} - -func Control(c rune) rune { - return c & 0x1f -} - -func sttyCharFromDesc(c string) (rune, error) { - if len(c) > 2 || len(c) < 1 { - return 'i', errors.New( - "stty char description has unsupported length: " + - strconv.Itoa(len(c)), - ) - } - - if rune(c[0]) == '^' { - return Control(rune(c[1])), nil - } - - return rune(c[0]), nil -} - -const fakeUname = "Linux 5.16.19-76051619-generic" - -type CommandExecutor interface { - Execute(shell *Shell, args []string) error -} - -type SttyCommand struct{} - -func (c *SttyCommand) Execute(shell *Shell, args []string) error { - // Tramp uses: - // -inlcr: don't translate newline to carriage return - // -onlcr: don't translate newline to carriage return-newline - // -echo: echo input characters - // kill CHAR: CHAR will erase the current line - // erase CHAR: CHAR will erase the last character typed - // icanon: enable special charactersq - - fd := int(shell.GetPty().Fd()) - const ioctlReadTermios = unix.TCGETS // Linux - const ioctlWriteTermios = unix.TCSETS // Linux - - termios, err := unix.IoctlGetTermios(fd, ioctlReadTermios) - if err != nil { - panic(err) - } - - newState := *termios - - var skipNext bool - for i, arg := range args { - if skipNext { - skipNext = false - continue - } - - var disable bool - if arg[0] == '-' { - disable = true - arg = arg[1:] - } - - switch arg { - case "onlcr": - if disable { - newState.Iflag &^= unix.ONLCR - continue - } - - newState.Iflag |= unix.ONLCR - case "inlcr": - if disable { - newState.Iflag &^= unix.INLCR - continue - } - - newState.Iflag |= unix.INLCR - case "echo": - if disable { - newState.Lflag &^= unix.ECHO - continue - } - - newState.Lflag |= unix.ECHO - case "kill": - skipNext = true - - char, err := sttyCharFromDesc(args[i+1]) - if err != nil { - log.Printf( - "Warning: Not applying unsupported character description for stty kill: %s", - args[i+1], - ) - continue - } - - newState.Cc[unix.VKILL] = uint8(char) - case "erase": - skipNext = true - - char, err := sttyCharFromDesc(args[i+1]) - if err != nil { - log.Printf( - "Warning: Not applying unsupported character description for stty erase: %s", - args[i+1], - ) - continue - } - - newState.Cc[unix.VERASE] = uint8(char) - case "icanon": - if disable { - newState.Lflag &^= unix.ICANON - continue - } - - newState.Lflag |= unix.ICANON - - case "tab0": - newState.Oflag = (newState.Oflag & ^uint32(unix.TABDLY)) | unix.TAB0 - case "iutf8": - if disable { - newState.Iflag &^= unix.IUTF8 - continue - } - - newState.Iflag |= unix.IUTF8 - - default: - log.Printf( - "Warning, tramp requested unexpected stty option %s", - arg, - ) - } - } - - if err := unix.IoctlSetTermios(fd, ioctlWriteTermios, &newState); err != nil { - return err - } - - return nil -} - -type EchoCommand struct{} - -func (c *EchoCommand) Execute(shell *Shell, args []string) error { - if len(args) > 1 { - if "\"`uname -sr`\"" == args[0]+" "+args[1] { - log.Println("Tramp requested uname, returning a fake one: " + fakeUname) - _, err := shell.WriteOutput( - []byte( - fmt.Sprintf( - "\"%s\"\n%s\n", - fakeUname, - "tramp_exit_status 0", - ), - ), - ) - return err - } - - if "\"`tty`\"" == args[0] { - log.Println("Telling tramp that it's dealing with a tty") - _, err := shell.WriteOutputString("\"/dev/pts/0\"\n" + trampSuccess) - - return err - } - } - - if len(args) > 0 { - if args[0] == "~root" { - log.Println("Telling tramp root's home directory") - _, err := shell.WriteOutputString("/root") - return err - } - } - - _, err := shell.WriteOutput([]byte(strings.Join(args, " ") + "\n")) - return err -} - -func AssignmentsToMap(args []string) map[string]string { - var assignments = make(map[string]string) - for _, arg := range args { - split := strings.Split(arg, "=") - if len(split) > 1 { - assignments[split[0]] = split[1] - } - } - - return assignments -} - -func ApplyEnvIfPresent(shell *Shell, comm *Command) { - var args []string - args = append(args, comm.Name) - args = append(args, comm.Arguments...) - - assignments := AssignmentsToMap(args) - if ps1, ok := assignments["PS1"]; ok { - shell.SetPrompt(ps1) - } -} - -type TestCommand struct { - filestore Filestore -} - -func (c *TestCommand) Execute(shell *Shell, args []string) error { - if len(args) > 0 && args[0] == "0" { - _, err := shell.WriteOutputString(trampSuccess) - return err - } - - if len(args) > 1 && args[0] == "-d" { - log.Printf("Checking for existance of directory %s", args[1]) - exists, err := c.filestore.Exists(args[1]) - if err != nil { - return err - } - - if exists { - _, err := shell.WriteOutputString(trampSuccess) - return err - } - - _, err = shell.WriteOutputString(trampFailure) - return err - } - - if len(args) > 1 && args[0] == "-r" { - _, err := shell.WriteOutputString(trampSuccess) - return err - } - - return nil -} - -const fakePath = "/bin:/usr/bin" -const fakePipeBuf = "4096" -const trampSuccess = "tramp_exit_status 0\n" -const trampFailure = "tramp_exit_status 1\n" - -type GetConfCommand struct{} - -func (c *GetConfCommand) Execute(shell *Shell, args []string) error { - if len(args) > 0 { - switch args[0] { - case "PATH": - shell.WriteOutputString(fakePath + "\n" + trampSuccess) - case "PIPE_BUF": - shell.WriteOutputString(fakePipeBuf + "\n" + trampSuccess) - } - } - return nil -} - -func (h *CommandHandler) Handle(comm *Command) error { - if strings.Contains(comm.Name, "=") { - ApplyEnvIfPresent(h.shell, comm) - return nil - } - - switch comm.Name { - case "exec": - ApplyEnvIfPresent(h.shell, comm) - case "(cd": - h.shell.WriteOutput([]byte("tramp_exit_status 0\n")) - case "echo": - (&EchoCommand{}).Execute(h.shell, comm.Arguments) - case "(echo": - if strings.Join(comm.Arguments, " ") == "foo ; echo bar)" { - log.Println("Handling tramp's foobar test") - h.shell.WriteOutputString("foo\nbar\n") - } - case "stty": - return (&SttyCommand{}).Execute(h.shell, comm.Arguments) - case "set": - log.Println("Ignoring \"set\" command") - case "locale": - if len(comm.Arguments) > 0 { - if comm.Arguments[0] == "-a" { - locales := []string{"C", "C.UTF-8", "POSIX", "en_US.utf8"} - log.Println("Tramp requested locale, returning fake values: ", locales) - h.shell.WriteOutputString( - strings.Join(locales, "\n") + "\n", - ) - return nil - } - } - - log.Println("Ignoring \"locale\" command with unsupported parameters: ", comm.Arguments) - case "getconf": - return (&GetConfCommand{}).Execute(h.shell, comm.Arguments) - case "test": - return (&TestCommand{filestore: h.filestore}).Execute(h.shell, comm.Arguments) - default: - log.Printf("Error: Received unexpected command %s", comm.Name) - } - - return nil -} diff --git a/local.log b/local.log deleted file mode 100644 index d1cc44c..0000000 --- a/local.log +++ /dev/null @@ -1,289 +0,0 @@ -2022/05/05 11:04:59 Private Key generated -2022/05/05 11:05:06 logged in with key SHA256:e+GGc8xMdIe6nXdohM3LR8s2H4moA8zffv8CP5WjB8M -2022/05/05 11:05:06 received request of type "pty-req" -2022/05/05 11:05:06 window resize 106x42 -2022/05/05 11:05:06 pty-req 'xterm-256color' -2022/05/05 11:05:06 received request of type "env" -2022/05/05 11:05:06 received request of type "env" -2022/05/05 11:05:06 received request of type "env" -2022/05/05 11:05:06 received request of type "env" -2022/05/05 11:05:06 received request of type "env" -2022/05/05 11:05:06 received request of type "env" -2022/05/05 11:05:06 received request of type "env" -2022/05/05 11:05:06 received request of type "env" -2022/05/05 11:05:06 received request of type "env" -2022/05/05 11:05:06 received request of type "env" -2022/05/05 11:05:06 received request of type "env" -2022/05/05 11:05:06 received request of type "env" -2022/05/05 11:05:06 received request of type "env" -2022/05/05 11:05:06 received request of type "shell" -2022/05/05 11:05:09 command: "ls" [] -2022/05/05 11:05:09 Error: Received unexpected command ls -2022/05/05 11:05:26 command: "test" [-d /var/www] -2022/05/05 11:05:26 Checking for existance of directory /var/www -2022/05/05 11:06:36 command: "locale" [] -2022/05/05 11:06:36 Ignoring "locale" command with unsupported parameters: [] -2022/05/05 11:06:39 command: "locale" [-a] -2022/05/05 11:06:39 Tramp requested locale, returning fake values: [C C.UTF-8 POSIX en_US.utf8] -2022/05/05 11:07:01 command: "exec" [] -2022/05/05 11:07:08 command: "PS1=hallo" [] -2022/05/05 11:07:08 Changing prompt to "hallo" -2022/05/05 11:07:21 command: "exec" [PS1=heey] -2022/05/05 11:07:21 Changing prompt to "heey" -2022/05/05 11:07:35 command: "exec" [HAAi=DOEI PS1=heeaaaa] -2022/05/05 11:07:35 Changing prompt to "heeaaaa" -2022/05/05 11:08:57 command: "echo" [heeeeey] -2022/05/05 11:09:06 Received EOF, closing TTY -2022/05/05 11:09:06 logged in with key SHA256:e+GGc8xMdIe6nXdohM3LR8s2H4moA8zffv8CP5WjB8M -2022/05/05 11:09:06 received request of type "pty-req" -2022/05/05 11:09:06 window resize 106x42 -2022/05/05 11:09:06 pty-req 'xterm-256color' -2022/05/05 11:09:06 received request of type "env" -2022/05/05 11:09:06 received request of type "env" -2022/05/05 11:09:06 received request of type "env" -2022/05/05 11:09:06 received request of type "env" -2022/05/05 11:09:06 received request of type "env" -2022/05/05 11:09:06 received request of type "env" -2022/05/05 11:09:06 received request of type "env" -2022/05/05 11:09:06 received request of type "env" -2022/05/05 11:09:06 received request of type "env" -2022/05/05 11:09:06 received request of type "env" -2022/05/05 11:09:06 received request of type "env" -2022/05/05 11:09:06 received request of type "env" -2022/05/05 11:09:06 received request of type "env" -2022/05/05 11:09:06 received request of type "shell" -2022/05/05 11:09:11 command: "echo" [heey] -2022/05/05 11:09:16 command: "echo" [ - - -faiwiefjawiejfwae -ajeifwajef - - -] -2022/05/05 11:09:38 Heredoc current word: "HAAAAAA" -2022/05/05 11:09:38 Encountered heredoc marker: HAAAAAA -2022/05/05 11:09:40 Heredoc current word: "fijwoiajfawoie" -2022/05/05 11:09:40 Heredoc current word: "afjwaoeifj" -2022/05/05 11:09:41 Heredoc current word: "awfjoiwjfe" -2022/05/05 11:09:41 Heredoc current word: "" -2022/05/05 11:09:42 Heredoc current word: "jawif;eijaw" -2022/05/05 11:09:42 Heredoc current word: "" -2022/05/05 11:09:49 Heredoc current word: "HAAAAAAA" -2022/05/05 11:09:59 Heredoc current word: "HAAAAAA" -2022/05/05 11:09:59 exiting heredoc -2022/05/05 11:09:59 command: "while" [read -r heey; do hahahaha; done </dev/null; echo tramp_exit_status $?] -2022/05/05 12:18:04 command: "set" [+o vi +o emacs] -2022/05/05 12:18:04 Ignoring "set" command -2022/05/05 12:18:04 command: "stty" [-inlcr -onlcr -echo kill ^U erase ^H] -2022/05/05 12:18:04 command: "echo" [foo] -2022/05/05 12:18:04 command: "PS1=///eac57010bddba5dae79f08f3cd43067c#$" [PS2= PS3= PROMPT_COMMAND=] -2022/05/05 12:18:04 Changing prompt to "///eac57010bddba5dae79f08f3cd43067c#$" -2022/05/05 12:18:04 command: "echo" ["`uname -sr`" 2>/dev/null; echo tramp_exit_status $?] -2022/05/05 12:18:04 Tramp requested uname, returning a fake one: Linux 5.16.19-76051619-generic -2022/05/05 12:18:04 command: "(echo" [foo ; echo bar)] -2022/05/05 12:18:04 Handling tramp's foobar test -2022/05/05 12:18:04 command: "PATH=/bin:/usr/bin:/sbin:/usr/sbin:/usr/local/bin:/usr/local/sbin:/local/bin:/local/freeware/bin:/local/gnu/bin:/usr/freeware/bin:/usr/pkg/bin:/usr/contrib/bin:/opt/bin:/opt/sbin:/opt/local/bin" [&& export PATH] -2022/05/05 12:18:04 command: "mesg" [n 2>/dev/null; biff n 2>/dev/null] -2022/05/05 12:18:04 Error: Received unexpected command mesg -2022/05/05 12:18:04 command: "stty" [tab0] -2022/05/05 12:18:04 command: "stty" [iutf8 2>/dev/null] -2022/05/05 12:18:04 Warning, tramp requested unexpected stty option 2>/dev/null -2022/05/05 12:18:04 command: "echo" ["`tty`" 2>/dev/null; echo tramp_exit_status $?] -2022/05/05 12:18:04 Telling tramp that it's dealing with a tty -2022/05/05 12:18:04 Heredoc current word: "'cf8110e8e028188d140944ad469ceb8f'" -2022/05/05 12:18:04 Encountered heredoc marker: 'cf8110e8e028188d140944ad469ceb8f' -2022/05/05 12:18:04 Heredoc current word: "LC_ALL en_US.utf8" -2022/05/05 12:18:04 Heredoc current word: "ENV ''" -2022/05/05 12:18:04 Heredoc current word: "TMOUT 0" -2022/05/05 12:18:04 Heredoc current word: "LC_CTYPE ''" -2022/05/05 12:18:04 Heredoc current word: "PAGER cat" -2022/05/05 12:18:04 Heredoc current word: "cf8110e8e028188d140944ad469ceb8f" -2022/05/05 12:18:04 exiting heredoc -2022/05/05 12:18:04 command: "while" [read var val; do export $var=$val; done </dev/null; echo tramp_exit_status $?] -2022/05/05 12:18:04 command: "/bin/test" [-e / 2>/dev/null; echo tramp_exit_status $?] -2022/05/05 12:18:04 Error: Received unexpected command /bin/test -2022/05/05 12:18:04 command: "/usr/bin/test" [-e / 2>/dev/null; echo tramp_exit_status $?] -2022/05/05 12:18:04 Error: Received unexpected command /usr/bin/test -2022/05/05 12:18:04 Heredoc current word: "'cf8110e8e028188d140944ad469ceb8f'" -2022/05/05 12:18:04 Encountered heredoc marker: 'cf8110e8e028188d140944ad469ceb8f' -2022/05/05 12:18:04 Heredoc current word: "/bin" -2022/05/05 12:18:04 Heredoc current word: "/usr/bin" -2022/05/05 12:18:04 Heredoc current word: "/sbin" -2022/05/05 12:18:04 Heredoc current word: "/usr/sbin" -2022/05/05 12:18:04 Heredoc current word: "/usr/local/bin" -2022/05/05 12:18:04 Heredoc current word: "/usr/local/sbin" -2022/05/05 12:18:04 Heredoc current word: "/local/bin" -2022/05/05 12:18:04 Heredoc current word: "/local/freeware/bin" -2022/05/05 12:18:04 Heredoc current word: "/local/gnu/bin" -2022/05/05 12:18:04 Heredoc current word: "/usr/freeware/bin" -2022/05/05 12:18:04 Heredoc current word: "/usr/pkg/bin" -2022/05/05 12:18:04 Heredoc current word: "/usr/contrib/bin" -2022/05/05 12:18:04 Heredoc current word: "/opt/bin" -2022/05/05 12:18:04 Heredoc current word: "/opt/sbin" -2022/05/05 12:18:04 Heredoc current word: "/opt/local/bin" -2022/05/05 12:18:04 Heredoc current word: "cf8110e8e028188d140944ad469ceb8f" -2022/05/05 12:18:04 exiting heredoc -2022/05/05 12:18:04 command: "while" [read d; do if test -x $d/ls && test -f $d/ls; then echo tramp_executable $d/ls; break; fi; done </dev/null; echo tramp_exit_status $?] -2022/05/05 12:18:04 command: "/bin/test" [-e / 2>/dev/null; echo tramp_exit_status $?] -2022/05/05 12:18:04 Error: Received unexpected command /bin/test -2022/05/05 12:18:04 command: "/usr/bin/test" [-e / 2>/dev/null; echo tramp_exit_status $?] -2022/05/05 12:18:04 Error: Received unexpected command /usr/bin/test -2022/05/05 12:18:04 Heredoc current word: "'cf8110e8e028188d140944ad469ceb8f'" -2022/05/05 12:18:04 Encountered heredoc marker: 'cf8110e8e028188d140944ad469ceb8f' -2022/05/05 12:18:04 Heredoc current word: "/bin" -2022/05/05 12:18:04 Heredoc current word: "/usr/bin" -2022/05/05 12:18:04 Heredoc current word: "/sbin" -2022/05/05 12:18:04 Heredoc current word: "/usr/sbin" -2022/05/05 12:18:04 Heredoc current word: "/usr/local/bin" -2022/05/05 12:18:04 Heredoc current word: "/usr/local/sbin" -2022/05/05 12:18:04 Heredoc current word: "/local/bin" -2022/05/05 12:18:04 Heredoc current word: "/local/freeware/bin" -2022/05/05 12:18:04 Heredoc current word: "/local/gnu/bin" -2022/05/05 12:18:04 Heredoc current word: "/usr/freeware/bin" -2022/05/05 12:18:04 Heredoc current word: "/usr/pkg/bin" -2022/05/05 12:18:04 Heredoc current word: "/usr/contrib/bin" -2022/05/05 12:18:04 Heredoc current word: "/opt/bin" -2022/05/05 12:18:04 Heredoc current word: "/opt/sbin" -2022/05/05 12:18:04 Heredoc current word: "/opt/local/bin" -2022/05/05 12:18:04 Heredoc current word: "cf8110e8e028188d140944ad469ceb8f" -2022/05/05 12:18:04 exiting heredoc -2022/05/05 12:18:04 command: "while" [read d; do if test -x $d/ls && test -f $d/ls; then echo tramp_executable $d/ls; break; fi; done </dev/null; echo tramp_exit_status $?] diff --git a/main.go b/main.go index b15af90..73bf06d 100644 --- a/main.go +++ b/main.go @@ -4,6 +4,7 @@ import ( "io/ioutil" "log" + "git.snorba.art/hugo/nssh/storage" ipfsapi "github.com/ipfs/go-ipfs-api" "golang.org/x/crypto/ssh" ) @@ -30,8 +31,7 @@ func main() { authorizedKeysBytes = rest } - // shell := - filestore, err := NewIPFSFilestore(ipfsapi.NewShell("127.0.0.1:5001"), WebDir) + filestore, err := storage.NewIPFSFilestore(ipfsapi.NewShell("127.0.0.1:5001"), WebDir) if err != nil { log.Fatal(err) } diff --git a/sshd.go b/sshd.go index f4214fd..ee4c37b 100644 --- a/sshd.go +++ b/sshd.go @@ -12,6 +12,8 @@ import ( "log" "net" + "git.snorba.art/hugo/nssh/commands" + "git.snorba.art/hugo/nssh/storage" "golang.org/x/crypto/ssh" ) @@ -58,7 +60,7 @@ func parseDims(b []byte) (uint32, uint32) { type Sshd struct { AuthorizedKeysMap map[string]bool - Filestore Filestore + Filestore storage.Filestore } func (s *Sshd) Listen(iface string) error { @@ -140,7 +142,7 @@ func (s *Sshd) HandleRequests(listener net.Listener, config *ssh.ServerConfig) e } session := &SshSession{ - shell: NewShell("user@host:/# "), + shell: commands.NewShell("user@host:/# "), filestore: s.Filestore, } @@ -155,8 +157,8 @@ func (s *Sshd) HandleRequests(listener net.Listener, config *ssh.ServerConfig) e } type SshSession struct { - shell *Shell - filestore Filestore + shell *commands.Shell + filestore storage.Filestore } func (s *SshSession) HandleRequests(in <-chan *ssh.Request) { @@ -185,7 +187,7 @@ func (s *SshSession) HandleRequests(in <-chan *ssh.Request) { func (s *SshSession) RunShell(channel io.ReadWriteCloser) { s.shell.Attach(channel) - handler := NewCommandHandler(s.shell, s.filestore) + handler := commands.NewCommandHandler(s.shell, s.filestore) go func() { for { @@ -197,14 +199,17 @@ func (s *SshSession) RunShell(channel io.ReadWriteCloser) { log.Println("Error reading command: ", err) } - if !errors.Is(err, ErrEmptyCommand) { + if !errors.Is(err, commands.ErrEmptyCommand) { s.shell.Close() return } } else { log.Printf("command: \"%s\" %s", comm.Name, comm.Arguments) - handler.Handle(comm) + err := handler.Handle(comm) + if err != nil { + log.Println("Error handling command: " + err.Error()) + } } err = s.shell.WritePrompt() diff --git a/storage/interface.go b/storage/interface.go new file mode 100644 index 0000000..8f5e26f --- /dev/null +++ b/storage/interface.go @@ -0,0 +1,18 @@ +package storage + +import "io" + +type File struct { + Name string `json:"name"` + AbsolutePath string `json:"absolute_path"` + Contents io.Reader `json:"-"` +} + +type Filestore interface { + Get(name string) (*File, error) + Set(file *File) error + Delete(file *File) error + Exists(name string) (bool, error) + MakeDirectory(name string) error + DirectoryExists(name string) (bool, error) +} diff --git a/filestore.go b/storage/ipfs.go similarity index 71% rename from filestore.go rename to storage/ipfs.go index 9ac1c82..0ff0a4c 100644 --- a/filestore.go +++ b/storage/ipfs.go @@ -1,27 +1,16 @@ -package main +package storage import ( "errors" "io" + "log" + "strings" "time" ipfsapi "github.com/ipfs/go-ipfs-api" "golang.org/x/net/context" ) -type File struct { - Name string `json:"name"` - AbsolutePath string `json:"absolute_path"` - Contents io.Reader `json:"-"` -} - -type Filestore interface { - Get(name string) (*File, error) - Set(file *File) error - Delete(file *File) error - Exists(name string) (bool, error) -} - func NewIPFSFilestore(ipfs *ipfsapi.Shell, rootDir string) (*IPFSFilestore, error) { store := &IPFSFilestore{ ipfs: ipfs, @@ -75,7 +64,21 @@ func newContext() context.Context { func (s *IPFSFilestore) Exists(name string) (bool, error) { ctx := newContext() - _, err := s.ipfs.FilesStat(ctx, s.rootDir+"/"+name) + _, err := s.ipfs.FilesStat(ctx, s.filepath(name)) + if err != nil { + if err.Error() == "files/stat: file does not exist" { + return false, nil + } + + return false, &ErrFailedFileExistsCheck{parent: err} + } + + return true, nil +} + +func (s *IPFSFilestore) DirectoryExists(name string) (bool, error) { + ctx := newContext() + st, err := s.ipfs.FilesStat(ctx, s.filepath(name)) if err != nil { if err.Error() == "files/stat: file does not exist" { return false, nil @@ -84,9 +87,23 @@ func (s *IPFSFilestore) Exists(name string) (bool, error) { return false, &ErrFailedFileExistsCheck{parent: err} } + if st.Type != "directory" { + return false, nil + } + return true, nil } +func (s *IPFSFilestore) filepath(name string) string { + return s.rootDir + "/" + strings.TrimPrefix(name, "/") +} + +func (s *IPFSFilestore) MakeDirectory(name string) error { + filepath := s.filepath(name) + log.Printf("Making directory \"%s\"", filepath) + return s.ipfs.FilesMkdir(newContext(), filepath) +} + type FileReader struct { ipfs *ipfsapi.Shell file *File @@ -125,7 +142,7 @@ func (s *IPFSFilestore) Get(name string) (*File, error) { file := &File{ Name: name, - AbsolutePath: s.rootDir + "/" + name, + AbsolutePath: s.filepath(name), } file.Contents = &FileReader{ @@ -137,7 +154,7 @@ func (s *IPFSFilestore) Get(name string) (*File, error) { } func (s *IPFSFilestore) Set(file *File) error { - return s.ipfs.FilesWrite(newContext(), s.rootDir+"/"+file.Name, file.Contents) + return s.ipfs.FilesWrite(newContext(), s.filepath(file.Name), file.Contents) } func (s *IPFSFilestore) Delete(file *File) error {