18/10/2021 - GO
Bu örnekte, SSH bağlantısı üzerinden dosya yüklemek/indirmek için bir SFTP istemcisi ve sunucusu kullanacağız. Sunucu simülasyonu ve sunucu ile etkileşime geçmek için temel işlevsellik sağlayan bir Docker kapsayıcı oluşturacağız.
Önemli: Bir sonraki sürümü, bellek açısından daha verimli olup, dosyanın belleğe parçalar halinde okunduğu için bu sürüme tercih edin.
├── docker
│ └── docker-compose.yaml
├── main.go
├── sftp
│ └── sftp.go
├── ssh
│ └── id_rsa
└── tmp
Özel anahtar kimlik doğrulaması, SSH anahtarlarının kopyalanmasını gerektirir. SSH kurulumunuza bağlı olarak, docker komutunu çalıştırmadan önce $ cp ~/.ssh/[id_rsa|id_ed25519] ssh/
komutunu kullanın. id_rsa
dosyasına sahibim, bu nedenle onu kullanıyorum.
version: "3.4"
services:
sftp-server:
image: "atmoz/sftp"
container_name: "sftp-server"
ports:
- "2022:22"
volumes:
- "../tmp:/home/inanzzz/tmp"
- "$HOME/.ssh/id_rsa:/etc/ssh/ssh_host_rsa_key:ro"
- "$HOME/.ssh/id_rsa.pub:/home/inanzzz/.ssh/keys/id_rsa.pub:ro"
# - "$HOME/.ssh/id_ed25519:/etc/ssh/ssh_host_ed25519_key:ro"
# - "$HOME/.ssh/id_ed25519.pub:/home/inanzzz/.ssh/keys/id_ed25519.pub:ro"
command: "inanzzz:password:1001"
Sahne arkasında, SFTP istemcisine güç sağlayan uzak bir sunucuyla yeni bir SSH bağlantısı kurar. Yerleşik "keepalive" ve otomatik yeniden bağlanma seçeneklerinin olmaması nedeniyle, Upload
, Download
ve Info
işlevlerine yapılan her çağrı, yalnızca kapatıldığında yeni bir SSH bağlantısı açar, aksi takdirde canlı olanı yeniden kullanır. Şu anda, Config
türüyle yapılandırılmış "parola" ve "özel anahtar" kimlik doğrulama yöntemlerinden yararlanır. Her ikisi de aynı anda etkinleştirilirse, özel anahtar kimlik doğrulaması önceliklidir.
Bu paketin iki özel optimizasyona ihtiyacı var. Örneğin, Upload
ve Download
, io arabirimini veya bayt akışlarını kabul edebilir/döndürebilir.
package sftp
import (
"fmt"
"io/ioutil"
"net"
"os"
"time"
"github.com/pkg/sftp"
"golang.org/x/crypto/ssh"
)
// Config represents SSH connection parameters.
type Config struct {
Username string
Password string
PrivateKey string
Server string
KeyExchanges []string
Timeout time.Duration
}
// Client provides basic functionality to interact with a SFTP server.
type Client struct {
config Config
sshClient *ssh.Client
sftpClient *sftp.Client
}
// New initialises SSH and SFTP clients and returns Client type to use.
func New(config Config) (*Client, error) {
c := &Client{
config: config,
}
if err := c.connect(); err != nil {
return nil, err
}
return c, nil
}
// Upload writes a file to a remote location.
func (c *Client) Upload(filePath string, fileContent []byte) error {
if err := c.connect(); err != nil {
return fmt.Errorf("connect: %w", err)
}
file, err := c.sftpClient.Create(filePath)
if err != nil {
return fmt.Errorf("file create: %w", err)
}
defer file.Close()
if _, err := file.Write(fileContent); err != nil {
return fmt.Errorf("file write: %w", err)
}
return nil
}
// Download returns a remote file.
func (c *Client) Download(filePath string) ([]byte, error) {
if err := c.connect(); err != nil {
return nil, fmt.Errorf("connect: %w", err)
}
file, err := c.sftpClient.Open(filePath)
if err != nil {
return nil, fmt.Errorf("file open: %w", err)
}
defer file.Close()
return ioutil.ReadAll(file)
}
// Info gets the details of a file. If the file was not found, an error is returned.
func (c *Client) Info(filePath string) (os.FileInfo, error) {
if err := c.connect(); err != nil {
return nil, fmt.Errorf("connect: %w", err)
}
info, err := c.sftpClient.Lstat(filePath)
if err != nil {
return nil, fmt.Errorf("file stats: %w", err)
}
return info, nil
}
// Close closes open connections.
func (c *Client) Close() {
if c.sftpClient != nil {
c.sftpClient.Close()
}
if c.sshClient != nil {
c.sshClient.Close()
}
}
// connect initialises a new SSH and SFTP client only if they were not
// initialised before at all and, they were initialised but the SSH
// connection was lost for any reason.
func (c *Client) connect() error {
if c.sshClient != nil {
_, _, err := c.sshClient.SendRequest("keepalive", false, nil)
if err == nil {
return nil
}
}
auth := ssh.Password(c.config.Password)
if c.config.PrivateKey != "" {
signer, err := ssh.ParsePrivateKey([]byte(c.config.PrivateKey))
if err != nil {
return fmt.Errorf("ssh parse private key: %w", err)
}
auth = ssh.PublicKeys(signer)
}
cfg := &ssh.ClientConfig{
User: c.config.Username,
Auth: []ssh.AuthMethod{
auth,
},
HostKeyCallback: func(string, net.Addr, ssh.PublicKey) error { return nil },
Timeout: c.config.Timeout,
Config: ssh.Config{
KeyExchanges: c.config.KeyExchanges,
},
}
sshClient, err := ssh.Dial("tcp", c.config.Server, cfg)
if err != nil {
return fmt.Errorf("ssh dial: %w", err)
}
c.sshClient = sshClient
sftpClient, err := sftp.NewClient(sshClient)
if err != nil {
return fmt.Errorf("sftp new client: %w", err)
}
c.sftpClient = sftpClient
return nil
}
package main
import (
"fmt"
"io/ioutil"
"log"
"time"
"github.com/you/client/sftp"
)
func main() {
pk, err := ioutil.ReadFile("./ssh/id_rsa") // required only if private key authentication is to be used
if err != nil {
log.Fatalln(err)
}
config := sftp.Config{
Username: "inanzzz",
Password: "password", // required only if password authentication is to be used
PrivateKey: string(pk), // required only if private key authentication is to be used
Server: "0.0.0.0:2022",
KeyExchanges: []string{"diffie-hellman-group-exchange-sha256", "diffie-hellman-group14-sha256"}, // optional
Timeout: time.Second * 30, // 0 for not timeout
}
client, err := sftp.New(config)
if err != nil {
log.Fatalln(err)
}
defer client.Close()
if err := client.Upload("tmp/file.txt", []byte(`File Content`)); err != nil {
log.Fatalln(err)
}
file, err := client.Download("tmp/file.txt")
if err != nil {
log.Fatalln(err)
}
fmt.Println(string(file))
info, err := client.Info("tmp/file.txt")
if err != nil {
log.Fatalln(err)
}
fmt.Printf("%+v", info)
}
$ docker-compose up
Recreating sftp-server ... done
Attaching to sftp-server
sftp-server | [/usr/local/bin/create-sftp-user] Parsing user data: "inanzzz:password:1001"
sftp-server | Generating public/private ed25519 key pair.
sftp-server | Your identification has been saved in /etc/ssh/ssh_host_ed25519_key.
sftp-server | Your public key has been saved in /etc/ssh/ssh_host_ed25519_key.pub.
sftp-server | The key fingerprint is:
sftp-server | SHA256:ggGrRmCvYias/aso5Lsad3POMerwIzys9fseI+4Zweghh root@111f66e033b4
sftp-server | The keys randomart image is:
sftp-server | +--[ED25519 256]--+
sftp-server | | o |
sftp-server | | . o |
sftp-server | |o o o . |
sftp-server | |=++o+o o + |
sftp-server | |+=*=+.. + |
sftp-server | |O*=o..o . |
sftp-server | +----[SHA256]-----+
sftp-server | chmod: changing permissions of '/etc/ssh/ssh_host_rsa_key': Read-only file system
sftp-server | [/entrypoint] Executing sshd
sftp-server | Server listening on 0.0.0.0 port 22.
sftp-server | Server listening on :: port 22.
$ go run -race main.go
File Content
&{name:file.txt stat:0xc0001d01c0}
// When using private key auth
sftp-server | Accepted publickey for inanzzz from 172.21.0.1 port 57818 ssh2: RSA SHA256:saboQipddaHVp67cUsadTSwS1ORslJioasasCA6NIDs
// When using password auth
sftp-server | Accepted password for inanzzz from 172.21.0.1 port 57822 ssh2