1 // Copyright © 2015 Jerry Jacobs <jerry.jacobs@xor-gate.org>.
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 // http://www.apache.org/licenses/LICENSE-2.0
8 // Unless required by applicable law or agreed to in writing, software
9 // distributed under the License is distributed on an "AS IS" BASIS,
10 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 // See the License for the specific language governing permissions and
12 // limitations under the License.
30 "golang.org/x/crypto/ssh"
34 type SftpFsContext struct {
36 sshcfg *ssh.ClientConfig
40 // TODO we only connect with hardcoded user+pass for now
41 // it should be possible to use $HOME/.ssh/id_rsa to login into the stub sftp server
42 func SftpConnect(user, password, host string) (*SftpFsContext, error) {
44 pemBytes, err := ioutil.ReadFile(os.Getenv("HOME") + "/.ssh/id_rsa")
49 signer, err := ssh.ParsePrivateKey(pemBytes)
54 sshcfg := &ssh.ClientConfig{
56 Auth: []ssh.AuthMethod{
57 ssh.Password(password),
58 ssh.PublicKeys(signer),
63 sshcfg := &ssh.ClientConfig{
65 Auth: []ssh.AuthMethod{
66 ssh.Password(password),
70 sshc, err := ssh.Dial("tcp", host, sshcfg)
75 sftpc, err := sftp.NewClient(sshc)
80 ctx := &SftpFsContext{
89 func (ctx *SftpFsContext) Disconnect() error {
95 // TODO for such a weird reason rootpath is "." when writing "file1" with afero sftp backend
96 func RunSftpServer(rootpath string) {
105 flag.BoolVar(&readOnly, "R", false, "read-only server")
106 flag.BoolVar(&debugStderr, "e", true, "debug to stderr")
107 flag.StringVar(&debugLevelStr, "l", "none", "debug level")
108 flag.StringVar(&rootDir, "root", rootpath, "root directory")
111 debugStream := ioutil.Discard
113 debugStream = os.Stderr
117 // An SSH server is represented by a ServerConfig, which holds
118 // certificate details and handles authentication of ServerConns.
119 config := &ssh.ServerConfig{
120 PasswordCallback: func(c ssh.ConnMetadata, pass []byte) (*ssh.Permissions, error) {
121 // Should use constant-time compare (or better, salt+hash) in
122 // a production setting.
123 fmt.Fprintf(debugStream, "Login: %s\n", c.User())
124 if c.User() == "test" && string(pass) == "test" {
127 return nil, fmt.Errorf("password rejected for %q", c.User())
131 privateBytes, err := ioutil.ReadFile("./test/id_rsa")
133 log.Fatal("Failed to load private key", err)
136 private, err := ssh.ParsePrivateKey(privateBytes)
138 log.Fatal("Failed to parse private key", err)
141 config.AddHostKey(private)
143 // Once a ServerConfig has been configured, connections can be
145 listener, err := net.Listen("tcp", "0.0.0.0:2022")
147 log.Fatal("failed to listen for connection", err)
149 fmt.Printf("Listening on %v\n", listener.Addr())
151 nConn, err := listener.Accept()
153 log.Fatal("failed to accept incoming connection", err)
156 // Before use, a handshake must be performed on the incoming
158 _, chans, reqs, err := ssh.NewServerConn(nConn, config)
160 log.Fatal("failed to handshake", err)
162 fmt.Fprintf(debugStream, "SSH server established\n")
164 // The incoming Request channel must be serviced.
165 go ssh.DiscardRequests(reqs)
167 // Service the incoming Channel channel.
168 for newChannel := range chans {
169 // Channels have a type, depending on the application level
170 // protocol intended. In the case of an SFTP session, this is "subsystem"
171 // with a payload string of "<length=4>sftp"
172 fmt.Fprintf(debugStream, "Incoming channel: %s\n", newChannel.ChannelType())
173 if newChannel.ChannelType() != "session" {
174 newChannel.Reject(ssh.UnknownChannelType, "unknown channel type")
175 fmt.Fprintf(debugStream, "Unknown channel type: %s\n", newChannel.ChannelType())
178 channel, requests, err := newChannel.Accept()
180 log.Fatal("could not accept channel.", err)
182 fmt.Fprintf(debugStream, "Channel accepted\n")
184 // Sessions have out-of-band requests such as "shell",
185 // "pty-req" and "env". Here we handle only the
186 // "subsystem" request.
187 go func(in <-chan *ssh.Request) {
188 for req := range in {
189 fmt.Fprintf(debugStream, "Request: %v\n", req.Type)
193 fmt.Fprintf(debugStream, "Subsystem: %s\n", req.Payload[4:])
194 if string(req.Payload[4:]) == "sftp" {
198 fmt.Fprintf(debugStream, " - accepted: %v\n", ok)
203 server, err := sftp.NewServer(channel, channel, debugStream, debugLevel, readOnly, rootpath)
207 if err := server.Serve(); err != nil {
208 log.Fatal("sftp server completed with error:", err)
213 // MakeSSHKeyPair make a pair of public and private keys for SSH access.
214 // Public key is encoded in the format for inclusion in an OpenSSH authorized_keys file.
215 // Private Key generated is PEM encoded
216 func MakeSSHKeyPair(bits int, pubKeyPath, privateKeyPath string) error {
217 privateKey, err := rsa.GenerateKey(_rand.Reader, bits)
222 // generate and write private key as PEM
223 privateKeyFile, err := os.Create(privateKeyPath)
224 defer privateKeyFile.Close()
229 privateKeyPEM := &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(privateKey)}
230 if err := pem.Encode(privateKeyFile, privateKeyPEM); err != nil {
234 // generate and write public key
235 pub, err := ssh.NewPublicKey(&privateKey.PublicKey)
240 return ioutil.WriteFile(pubKeyPath, ssh.MarshalAuthorizedKey(pub), 0655)
243 func TestSftpCreate(t *testing.T) {
244 os.Mkdir("./test", 0777)
245 MakeSSHKeyPair(1024, "./test/id_rsa.pub", "./test/id_rsa")
247 go RunSftpServer("./test/")
248 time.Sleep(5 * time.Second)
250 ctx, err := SftpConnect("test", "test", "localhost:2022")
254 defer ctx.Disconnect()
256 var AppFs Fs = SftpFs{
257 SftpClient: ctx.sftpc,
260 AppFs.MkdirAll("test/dir1/dir2/dir3", os.FileMode(0777))
261 AppFs.Mkdir("test/foo", os.FileMode(0000))
262 AppFs.Chmod("test/foo", os.FileMode(0700))
263 AppFs.Mkdir("test/bar", os.FileMode(0777))
265 file, err := AppFs.Create("file1")
271 file.Write([]byte("hello\t"))
272 file.WriteString("world!\n")
274 f1, err := AppFs.Open("file1")
276 log.Fatalf("open: %v", err)
280 b := make([]byte, 100)
283 fmt.Println(string(b))
285 // TODO check here if "hello\tworld\n" is in buffer b