diff --git a/auth.go b/auth.go index c1d4931..318a322 100644 --- a/auth.go +++ b/auth.go @@ -37,6 +37,17 @@ func Key(prvFile string, passphrase string) (Auth, error) { }, nil } +func RawKey(privateKey string, passphrase string) (Auth, error) { + signer, err := GetSignerForRawKey([]byte(privateKey), passphrase) + if err != nil { + return nil, err + } + + return Auth{ + ssh.PublicKeys(signer), + }, nil +} + // HasAgent checks if ssh agent exists. func HasAgent() bool { return os.Getenv("SSH_AUTH_SOCK") != "" @@ -78,3 +89,27 @@ func GetSigner(prvFile string, passphrase string) (ssh.Signer, error) { return signer, err } + +// GetSigner returns ssh signer from private key file. +func GetSignerForRawKey(privateKey []byte, passphrase string) (ssh.Signer, error) { + + var ( + err error + signer ssh.Signer + ) + + if err != nil { + + return nil, err + + } else if passphrase != "" { + + signer, err = ssh.ParsePrivateKeyWithPassphrase(privateKey, []byte(passphrase)) + + } else { + + signer, err = ssh.ParsePrivateKey(privateKey) + } + + return signer, err +} diff --git a/client.go b/client.go index a1165b2..47bbaee 100644 --- a/client.go +++ b/client.go @@ -4,9 +4,11 @@ package goph import ( + "bytes" "context" "fmt" "io" + "io/ioutil" "net" "os" "time" @@ -109,6 +111,36 @@ func (c Client) Run(cmd string) ([]byte, error) { return sess.CombinedOutput(cmd) } +// Run starts a new SSH session and runs the cmd, it returns CombinedOutput and err if any. +func (c Client) Script(script string) (*Cmd, error) { + + var ( + err error + sess *ssh.Session + ) + + if sess, err = c.NewSession(); err != nil { + return nil, err + } + + return &Cmd{ + Path: "", + Args: []string{}, + Session: sess, + Context: context.Background(), + script: bytes.NewBufferString(script + "\n"), + _type: rawScript, + }, nil +} + +func (c Client) ScriptFile(filePath string) (*Cmd, error) { + file, err := ioutil.ReadFile(filePath) + if err != nil { + return nil, err + } + return c.Script(string(file)) +} + // Run starts a new SSH session with context and runs the cmd. It returns CombinedOutput and err if any. func (c Client) RunContext(ctx context.Context, name string) ([]byte, error) { cmd, err := c.CommandContext(ctx, name) @@ -136,6 +168,8 @@ func (c Client) Command(name string, args ...string) (*Cmd, error) { Args: args, Session: sess, Context: context.Background(), + _type: cmdLine, + script: bytes.NewBufferString(name + "\n"), }, nil } diff --git a/cmd.go b/cmd.go index 3a4608f..2d4e9c2 100644 --- a/cmd.go +++ b/cmd.go @@ -4,11 +4,22 @@ package goph import ( + "bytes" "context" "fmt" + "io" + "strings" + "github.com/pkg/errors" "golang.org/x/crypto/ssh" - "strings" +) + +type remoteScriptType byte + +const ( + cmdLine remoteScriptType = iota + rawScript + scriptFile ) // Cmd it's like os/exec.Cmd but for ssh session. @@ -28,6 +39,21 @@ type Cmd struct { // Context for cancellation Context context.Context + + _type remoteScriptType + + script *bytes.Buffer + + // scriptFile string + + stdout io.Writer + stderr io.Writer +} + +func appendStringToBuffer(str string, buf bytes.Buffer) *bytes.Buffer { + parentStr := buf.String() + parentStr = fmt.Sprintf("%v \n %v", str, parentStr) + return bytes.NewBufferString(parentStr) } // CombinedOutput runs cmd on the remote host and returns its combined stdout and stderr. @@ -52,6 +78,82 @@ func (c *Cmd) Output() ([]byte, error) { }) } +// Output runs cmd on the remote host and returns its stdout. +func (c *Cmd) ScriptOutput() ([]byte, error) { + if err := c.init(); err != nil { + return nil, errors.Wrap(err, "cmd init") + } + var ( + stdout bytes.Buffer + stderr bytes.Buffer + ) + c.stdout = &stdout + c.stderr = &stderr + return c.runWithContext(func() ([]byte, error) { + err := c.run() + if err != nil { + return stderr.Bytes(), err + } + return stdout.Bytes(), err + }) +} + +func (c *Cmd) run() error { + if c._type == cmdLine { + return c.runCmds() + } else if c._type == rawScript { + return c.runScript() + } else if c._type == scriptFile { + return nil + // return c.runScriptFile() + } else { + return errors.New("Not supported RemoteScript type") + } +} + +func (c *Cmd) runCmd(cmd string) error { + c.Session.Stdout = c.stdout + c.Session.Stderr = c.stderr + + if err := c.Session.Run(cmd); err != nil { + return err + } + return nil +} + +func (c *Cmd) runCmds() error { + for { + statment, err := c.script.ReadString('\n') + if err == io.EOF { + break + } + if err != nil { + return err + } + + if err := c.runCmd(statment); err != nil { + return err + } + } + + return nil +} + +func (c *Cmd) runScript() error { + envs := strings.Join(c.Env, "\n") + c.script = appendStringToBuffer(fmt.Sprintf("%v \n", envs), *c.script) + c.Session.Stdin = c.script + c.Session.Stdout = c.stdout + c.Session.Stderr = c.stderr + if err := c.Session.Shell(); err != nil { + return err + } + if err := c.Session.Wait(); err != nil { + return err + } + return nil +} + // Run runs cmd on the remote host. func (c *Cmd) Run() error { if err := c.init(); err != nil { @@ -81,6 +183,9 @@ func (c *Cmd) String() string { // Init inits and sets session env vars. func (c *Cmd) init() (err error) { + if c._type == rawScript { + return + } // Set session env vars var env []string for _, value := range c.Env { diff --git a/examples/goph/main.go b/examples/goph/main.go index 97778b5..fca9d85 100644 --- a/examples/goph/main.go +++ b/examples/goph/main.go @@ -14,8 +14,8 @@ import ( "strings" "time" - "github.com/melbahja/goph" "github.com/pkg/sftp" + "github.com/tareksalem/goph" "golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh/terminal" ) diff --git a/go.mod b/go.mod index f7e98dc..035a18b 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/melbahja/goph +module github.com/tareksalem/goph go 1.13 @@ -6,4 +6,5 @@ require ( github.com/pkg/errors v0.9.1 github.com/pkg/sftp v1.13.5 golang.org/x/crypto v0.6.0 + golang.org/x/term v0.7.0 ) diff --git a/go.sum b/go.sum index eea2896..ee8f3df 100644 --- a/go.sum +++ b/go.sum @@ -34,10 +34,14 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU= +golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0 h1:n2a8QNdAb0sZNpU9R1ALUXBbY+w51fCQDN+7EdxNBsY= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.7.0 h1:BEvjmm5fURWqcfbSKTdpkDXYBrUS1c0m8agp14W48vQ= +golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= diff --git a/goph_test.go b/goph_test.go index b99e061..1e9715a 100644 --- a/goph_test.go +++ b/goph_test.go @@ -6,9 +6,10 @@ import ( "net" "testing" - "github.com/melbahja/goph" "golang.org/x/crypto/ssh" - "golang.org/x/crypto/ssh/terminal" + "golang.org/x/term" + + "github.com/tareksalem/goph" ) var privateBytes = []byte(` @@ -183,7 +184,7 @@ func newServer(port string) { } }(requests) - term := terminal.NewTerminal(channel, "> ") + term := term.NewTerminal(channel, "> ") go func() { defer channel.Close()