Golang SSH服务器:如何使用scp处理文件传输?

huangapple go评论90阅读模式
英文:

Golang SSH-Server: How to handle file transfer with scp?

问题

我用golang和crypto/ssh包编写了一个小型的SSH服务器。

它支持返回交互式shell和立即执行命令。

这是服务器的一个最简示例:

package main

import (
	"fmt"
	"io/ioutil"
	"log"
	"net"
	"os/exec"

	"golang.org/x/crypto/ssh"
)

func main() {
	c := &ssh.ServerConfig{
		PasswordCallback: func(c ssh.ConnMetadata, pass []byte) (*ssh.Permissions, error) {
			if c.User() == "foo" && string(pass) == "bar" {
				return nil, nil
			}
			return nil, fmt.Errorf("password rejected for %q", c.User())
		},
	}

	keyBytes, _ := ioutil.ReadFile("key")
	key, _ := ssh.ParsePrivateKey(keyBytes)
	c.AddHostKey(key)

	listener, _ := net.Listen("tcp", "0.0.0.0:2200")
	for {
		tcpConn, _ := listener.Accept()
		_, chans, reqs, _ := ssh.NewServerConn(tcpConn, c)
		go ssh.DiscardRequests(reqs)
		go handleChannels(chans)
	}
}

func handleChannels(chans <-chan ssh.NewChannel) {
	for newChannel := range chans {
		go handleChannel(newChannel)
	}
}

func handleChannel(newChannel ssh.NewChannel) {
	channel, requests, _ := newChannel.Accept()
	for req := range requests {
		switch req.Type {
		case "shell":
			go handleShell(channel)
		case "exec":
			go handleExec(channel, req)
		}
	}
}

func handleShell(c ssh.Channel) {}
func handleExec(c ssh.Channel, r *ssh.Request) {
	cmdString, args, _ := parseCommand(r.Payload)
	log.Printf("exec: %s\n", cmdString)
	for i := range args {
		log.Printf("arg %d: %s\n", i, args[i])
	}
	cmd := exec.Command(cmdString, args...)
	cmd.Run()
}

func parseCommand(b []byte) (string, []string, error) {
	cmdString := strings.TrimSpace(string(b))
	cmdArray := strings.Split(cmdString, " ")

	cmd := strings.Trim(cmdArray[0], " ")
	args := cmdArray[1:]

	return cmd, args, nil
}

如果我运行服务器并执行以下scp命令:

scp -P 2200 test.file foo@localhost:~/

将调用handleExec函数。

cmdString的输出如下:

2015/11/22 17:49:14 exec: scp
2015/11/22 17:49:14 arg 0: -t
2015/11/22 17:49:14 arg 1: ~/

但是,我该如何实现handleExec函数来实际保存通过scp传递的文件/目录呢?

英文:

I have written a small SSH-Server in golang with the crypto/ssh package.

It supports returning an interactive shell and immediate command execution.

Here is a minimal example of the server:

package main
import (
&quot;fmt&quot;
&quot;io/ioutil&quot;
&quot;log&quot;
&quot;net&quot;
&quot;os/exec&quot;
&quot;golang.org/x/crypto/ssh&quot;
)
func main() {
c := &amp;ssh.ServerConfig{
PasswordCallback: func(c ssh.ConnMetadata, pass []byte) (*ssh.Permissions, error) {
if c.User() == &quot;foo&quot; &amp;&amp; string(pass) == &quot;bar&quot; {
return nil, nil
}
return nil, fmt.Errorf(&quot;password rejected for %q&quot;, c.User())
},
}
keyBytes, _ := ioutil.ReadFile(&quot;key&quot;)
key, _ := ssh.ParsePrivateKey(keyBytes)
c.AddHostKey(key)
listener, _ := net.Listen(&quot;tcp&quot;, &quot;0.0.0.0:2200&quot;)
for {
tcpConn, _ := listener.Accept()
_, chans, reqs, _ := ssh.NewServerConn(tcpConn, c)
go ssh.DiscardRequests(reqs)
go handleChannels(chans)
}
}
func handleChannels(chans &lt;-chan ssh.NewChannel) {
for newChannel := range chans {
go handleChannel(newChannel)
}
}
func handleChannel(newChannel ssh.NewChannel) {
channel, requests, _ := newChannel.Accept()
for req := range requests {
switch req.Type {
case &quot;shell&quot;:
go handleShell(channel)
case &quot;exec&quot;:
go handleExec(channel, req)
}
}
}
func handleShell(c ssh.Channel) {}
func handleExec(c ssh.Channel, r *ssh.Request) {
cmdString, args, _ := parseCommand(r.Payload)
log.Printf(&quot;exec: %s\n&quot;, cmdString)
for i := range args {
log.Printf(&quot;arg %d: %s\n&quot;, i, args[i])
}
cmd := exec.Command(cmdString, args...)
cmd.Run()
}
func parseCommand(b []byte) (string, []string, error) {
cmdString := strings.TrimSpace(string(b))
cmdArray := strings.Split(cmdString, &quot; &quot;)
cmd := strings.Trim(cmdArray[0], &quot; &quot;)
args := cmdArray[1:]
return cmd, args, nil
}

If I run the server and execute scp as follows:

scp -P 2200 test.file foo@localhost:~/

the handleExec function is called.

The output of the cmdString shows:

2015/11/22 17:49:14 exec: scp
2015/11/22 17:49:14 arg 0: -t
2015/11/22 17:49:14 arg 1: ~/

But how can I implement the handleExec function to actually save the file/dir I passed via scp?

答案1

得分: 2

我刚刚遇到了在我的SSH服务器上执行scp和自定义命令的问题,由于没有文档说明如何做到这一点,所以我从crypto.ssh的测试代码中拼凑了一些代码(https://github.com/golang/crypto/blob/master/ssh/session.go和https://github.com/golang/crypto/blob/master/ssh/session_test.go)。它适用于OpenSSH和crypto.ssh客户端。例如,您可以在客户端上调用session.Run()并使用它处理scp或自定义命令。

type exitStatusMsg struct {
    Status uint32
}

// RFC 4254 Section 6.5.
type execMsg struct {
    Command string
}

go func(in <-chan *ssh.Request, channel ssh.Channel) {
    for req := range in {
        if req.Type == "exec" {
            var msg execMsg
            if err := ssh.Unmarshal(req.Payload, &msg); err != nil {
                log.Printf("error parsing ssh execMsg: %s\n", err)
                req.Reply(false, nil)
                return
            }
            go func(msg execMsg, ch ssh.Channel) {
                // 如果需要交互,可以将ch用作ReadWriteCloser
                runYourCommand(msg.Command, ch)
                ex := exitStatusMsg{
                    Status: 0,
                }
                // 返回状态码
                if _, err := ch.SendRequest("exit-status", false, ssh.Marshal(&ex)); err != nil {
                    log.Printf("unable to send status: %v", err)
                }
                ch.Close()
            }(msg, channel)
            req.Reply(true, nil) // 告诉另一端我们可以运行该请求
        } else {
            req.Reply(req.Type == "shell", nil)
        }
    }
}(requests, channel)

您需要将runYourCommand替换为执行您的命令的函数,并将退出代码设置为您的命令/进程返回的值。

英文:

I just ran into the problem of executing scp and custom commands over my ssh server and as it is undocumented how to do this I pieced together some code from the tests in crypto.ssh (https://github.com/golang/crypto/blob/master/ssh/session.go and https://github.com/golang/crypto/blob/master/ssh/session_test.go) It works with OpenSSH and the crypto.ssh client. You can for instance call session.Run() on your client and handle e.g. scp or custom commands with it.

type exitStatusMsg struct {
Status uint32
}
// RFC 4254 Section 6.5.
type execMsg struct {
Command string
}
go func(in &lt;-chan *ssh.Request, channel ssh.Channel) {
for req := range in {
if req.Type == &quot;exec&quot; {
var msg execMsg
if err := ssh.Unmarshal(req.Payload, &amp;msg); err != nil {
log.Printf(&quot;error parsing ssh execMsg: %s\n&quot;, err)
req.Reply(false, nil)
return
}
go func(msg execMsg, ch ssh.Channel) {
// ch can be used as a ReadWriteCloser if there should be interactivity
runYourCommand(msg.Command, ch)
ex := exitStatusMsg{
Status: 0,
}
// return the status code
if _, err := ch.SendRequest(&quot;exit-status&quot;, false, ssh.Marshal(&amp;ex)); err != nil {
log.Printf(&quot;unable to send status: %v&quot;, err)
}
ch.Close()
}(msg, channel)
req.Reply(true, nil) // tell the other end that we can run the request
} else {
req.Reply(req.Type == &quot;shell&quot;, nil)
}
}
}(requests, channel)

You need to replace runYourCommand with whatever function then executes your command and set the exit code to whatever your command/process returns.

huangapple
  • 本文由 发表于 2015年11月22日 02:22:28
  • 转载请务必保留本文链接:https://go.coder-hub.com/33846959.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定