You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

325 lines
6.7 KiB
Go

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
}