| // Copyright © 2015 Jerry Jacobs <jerry.jacobs@xor-gate.org>. |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| package afero |
| |
| import ( |
| "testing" |
| "os" |
| "log" |
| "fmt" |
| "net" |
| "flag" |
| "time" |
| "io/ioutil" |
| "crypto/rsa" |
| _rand "crypto/rand" |
| "encoding/pem" |
| "crypto/x509" |
| |
| "golang.org/x/crypto/ssh" |
| "github.com/pkg/sftp" |
| ) |
| |
| type SftpFsContext struct { |
| sshc *ssh.Client |
| sshcfg *ssh.ClientConfig |
| sftpc *sftp.Client |
| } |
| |
| // TODO we only connect with hardcoded user+pass for now |
| // it should be possible to use $HOME/.ssh/id_rsa to login into the stub sftp server |
| func SftpConnect(user, password, host string) (*SftpFsContext, error) { |
| /* |
| pemBytes, err := ioutil.ReadFile(os.Getenv("HOME") + "/.ssh/id_rsa") |
| if err != nil { |
| return nil,err |
| } |
| |
| signer, err := ssh.ParsePrivateKey(pemBytes) |
| if err != nil { |
| return nil,err |
| } |
| |
| sshcfg := &ssh.ClientConfig{ |
| User: user, |
| Auth: []ssh.AuthMethod{ |
| ssh.Password(password), |
| ssh.PublicKeys(signer), |
| }, |
| } |
| */ |
| |
| sshcfg := &ssh.ClientConfig{ |
| User: user, |
| Auth: []ssh.AuthMethod{ |
| ssh.Password(password), |
| }, |
| } |
| |
| sshc, err := ssh.Dial("tcp", host, sshcfg) |
| if err != nil { |
| return nil,err |
| } |
| |
| sftpc, err := sftp.NewClient(sshc) |
| if err != nil { |
| return nil,err |
| } |
| |
| ctx := &SftpFsContext{ |
| sshc: sshc, |
| sshcfg: sshcfg, |
| sftpc: sftpc, |
| } |
| |
| return ctx,nil |
| } |
| |
| func (ctx *SftpFsContext) Disconnect() error { |
| ctx.sftpc.Close() |
| ctx.sshc.Close() |
| return nil |
| } |
| |
| // TODO for such a weird reason rootpath is "." when writing "file1" with afero sftp backend |
| func RunSftpServer(rootpath string) { |
| var ( |
| readOnly bool |
| debugLevelStr string |
| debugLevel int |
| debugStderr bool |
| rootDir string |
| ) |
| |
| flag.BoolVar(&readOnly, "R", false, "read-only server") |
| flag.BoolVar(&debugStderr, "e", true, "debug to stderr") |
| flag.StringVar(&debugLevelStr, "l", "none", "debug level") |
| flag.StringVar(&rootDir, "root", rootpath, "root directory") |
| flag.Parse() |
| |
| debugStream := ioutil.Discard |
| if debugStderr { |
| debugStream = os.Stderr |
| debugLevel = 1 |
| } |
| |
| // An SSH server is represented by a ServerConfig, which holds |
| // certificate details and handles authentication of ServerConns. |
| config := &ssh.ServerConfig{ |
| PasswordCallback: func(c ssh.ConnMetadata, pass []byte) (*ssh.Permissions, error) { |
| // Should use constant-time compare (or better, salt+hash) in |
| // a production setting. |
| fmt.Fprintf(debugStream, "Login: %s\n", c.User()) |
| if c.User() == "test" && string(pass) == "test" { |
| return nil, nil |
| } |
| return nil, fmt.Errorf("password rejected for %q", c.User()) |
| }, |
| } |
| |
| privateBytes, err := ioutil.ReadFile("./test/id_rsa") |
| if err != nil { |
| log.Fatal("Failed to load private key", err) |
| } |
| |
| private, err := ssh.ParsePrivateKey(privateBytes) |
| if err != nil { |
| log.Fatal("Failed to parse private key", err) |
| } |
| |
| config.AddHostKey(private) |
| |
| // Once a ServerConfig has been configured, connections can be |
| // accepted. |
| listener, err := net.Listen("tcp", "0.0.0.0:2022") |
| if err != nil { |
| log.Fatal("failed to listen for connection", err) |
| } |
| fmt.Printf("Listening on %v\n", listener.Addr()) |
| |
| nConn, err := listener.Accept() |
| if err != nil { |
| log.Fatal("failed to accept incoming connection", err) |
| } |
| |
| // Before use, a handshake must be performed on the incoming |
| // net.Conn. |
| _, chans, reqs, err := ssh.NewServerConn(nConn, config) |
| if err != nil { |
| log.Fatal("failed to handshake", err) |
| } |
| fmt.Fprintf(debugStream, "SSH server established\n") |
| |
| // The incoming Request channel must be serviced. |
| go ssh.DiscardRequests(reqs) |
| |
| // Service the incoming Channel channel. |
| for newChannel := range chans { |
| // Channels have a type, depending on the application level |
| // protocol intended. In the case of an SFTP session, this is "subsystem" |
| // with a payload string of "<length=4>sftp" |
| fmt.Fprintf(debugStream, "Incoming channel: %s\n", newChannel.ChannelType()) |
| if newChannel.ChannelType() != "session" { |
| newChannel.Reject(ssh.UnknownChannelType, "unknown channel type") |
| fmt.Fprintf(debugStream, "Unknown channel type: %s\n", newChannel.ChannelType()) |
| continue |
| } |
| channel, requests, err := newChannel.Accept() |
| if err != nil { |
| log.Fatal("could not accept channel.", err) |
| } |
| fmt.Fprintf(debugStream, "Channel accepted\n") |
| |
| // Sessions have out-of-band requests such as "shell", |
| // "pty-req" and "env". Here we handle only the |
| // "subsystem" request. |
| go func(in <-chan *ssh.Request) { |
| for req := range in { |
| fmt.Fprintf(debugStream, "Request: %v\n", req.Type) |
| ok := false |
| switch req.Type { |
| case "subsystem": |
| fmt.Fprintf(debugStream, "Subsystem: %s\n", req.Payload[4:]) |
| if string(req.Payload[4:]) == "sftp" { |
| ok = true |
| } |
| } |
| fmt.Fprintf(debugStream, " - accepted: %v\n", ok) |
| req.Reply(ok, nil) |
| } |
| }(requests) |
| |
| server, err := sftp.NewServer(channel, channel, debugStream, debugLevel, readOnly, rootpath) |
| if err != nil { |
| log.Fatal(err) |
| } |
| if err := server.Serve(); err != nil { |
| log.Fatal("sftp server completed with error:", err) |
| } |
| } |
| } |
| |
| // MakeSSHKeyPair make a pair of public and private keys for SSH access. |
| // Public key is encoded in the format for inclusion in an OpenSSH authorized_keys file. |
| // Private Key generated is PEM encoded |
| func MakeSSHKeyPair(bits int, pubKeyPath, privateKeyPath string) error { |
| privateKey, err := rsa.GenerateKey(_rand.Reader, bits) |
| if err != nil { |
| return err |
| } |
| |
| // generate and write private key as PEM |
| privateKeyFile, err := os.Create(privateKeyPath) |
| defer privateKeyFile.Close() |
| if err != nil { |
| return err |
| } |
| |
| privateKeyPEM := &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(privateKey)} |
| if err := pem.Encode(privateKeyFile, privateKeyPEM); err != nil { |
| return err |
| } |
| |
| // generate and write public key |
| pub, err := ssh.NewPublicKey(&privateKey.PublicKey) |
| if err != nil { |
| return err |
| } |
| |
| return ioutil.WriteFile(pubKeyPath, ssh.MarshalAuthorizedKey(pub), 0655) |
| } |
| |
| func TestSftpCreate(t *testing.T) { |
| os.Mkdir("./test", 0777) |
| MakeSSHKeyPair(1024, "./test/id_rsa.pub", "./test/id_rsa") |
| |
| go RunSftpServer("./test/") |
| time.Sleep(5 * time.Second) |
| |
| ctx, err := SftpConnect("test", "test", "localhost:2022") |
| if err != nil { |
| t.Fatal(err) |
| } |
| defer ctx.Disconnect() |
| |
| var AppFs Fs = SftpFs{ |
| SftpClient: ctx.sftpc, |
| } |
| |
| AppFs.MkdirAll("test/dir1/dir2/dir3", os.FileMode(0777)) |
| AppFs.Mkdir("test/foo", os.FileMode(0000)) |
| AppFs.Chmod("test/foo", os.FileMode(0700)) |
| AppFs.Mkdir("test/bar", os.FileMode(0777)) |
| |
| file, err := AppFs.Create("file1") |
| if err != nil { |
| t.Error(err) |
| } |
| defer file.Close() |
| |
| file.Write([]byte("hello\t")) |
| file.WriteString("world!\n") |
| |
| f1, err := AppFs.Open("file1") |
| if err != nil { |
| log.Fatalf("open: %v", err) |
| } |
| defer f1.Close() |
| |
| b := make([]byte, 100) |
| |
| _, err = f1.Read(b) |
| fmt.Println(string(b)) |
| |
| // TODO check here if "hello\tworld\n" is in buffer b |
| } |