Skip to content

Commit 0dfff0f

Browse files
committed
Base implementation of a Gocrypt SFTP proxy server
Right now still a proof-of-concept to show that adding a caching and decryption layer on top of SFTP greatly improves syncingperformance over mounting a FUSE decryption layer on top of a FUSE SFTP layer. Actual modifications to the underlying FS not supported yet.
1 parent 4e74257 commit 0dfff0f

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

76 files changed

+6625
-0
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
.vscode

backend/pool.go

+80
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package backend
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
7+
"github.com/pkg/sftp"
8+
"golang.org/x/crypto/ssh"
9+
)
10+
11+
type conn struct {
12+
sshConn *ssh.Client
13+
sftpConn *sftp.Client
14+
}
15+
16+
func newConn(remoteAddr string, clientConfig *ssh.ClientConfig) (*conn, error) {
17+
sshClient, err := ssh.Dial("tcp", remoteAddr, clientConfig)
18+
if err != nil {
19+
return nil, fmt.Errorf("failed to connect to remote server %s", err)
20+
}
21+
sftpClient, err := sftp.NewClient(sshClient)
22+
if err != nil {
23+
sshClient.Close()
24+
return nil, fmt.Errorf("could not open sftp session to remote %s", err)
25+
}
26+
c := &conn{
27+
sshConn: sshClient,
28+
sftpConn: sftpClient,
29+
}
30+
31+
return c, nil
32+
}
33+
34+
func (c *conn) Close() {
35+
if c.sshConn != nil {
36+
_ = c.sshConn.Close()
37+
}
38+
if c.sftpConn != nil {
39+
_ = c.sftpConn.Close()
40+
}
41+
}
42+
43+
type pool struct {
44+
conns chan *conn
45+
46+
remoteAddr string
47+
clientConfig *ssh.ClientConfig
48+
}
49+
50+
func newPool(capacity uint32, remoteAddr string, clientConfig *ssh.ClientConfig) *pool {
51+
return &pool{
52+
conns: make(chan *conn, capacity),
53+
54+
remoteAddr: remoteAddr,
55+
clientConfig: clientConfig,
56+
}
57+
}
58+
59+
func (p *pool) Get() (*conn, error) {
60+
select {
61+
case c := <-p.conns:
62+
if c == nil {
63+
return nil, errors.New("pool is closed")
64+
}
65+
return c, nil
66+
default:
67+
return newConn(p.remoteAddr, p.clientConfig)
68+
}
69+
}
70+
71+
func (p *pool) Put(c *conn) {
72+
if c == nil {
73+
return
74+
}
75+
select {
76+
case p.conns <- c:
77+
default:
78+
c.Close()
79+
}
80+
}

backend/provider.go

+98
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
package backend
2+
3+
import (
4+
"bytes"
5+
"log"
6+
"os"
7+
8+
"golang.org/x/crypto/ssh"
9+
)
10+
11+
// Provider provides the ability to make requests to a backend through a pool
12+
// of connections. This package does not provide goroutines on top of
13+
// connections; if you make multiple calls to ReadFile in a single goroutine,
14+
// it's basically the same thing as calling ReadFile on a connection in serial.
15+
// What this package does provide is a guarantee that if you call ReadFile from
16+
// multiple goroutines, no two concurrent calls will share the same connection.
17+
type Provider struct {
18+
p *pool
19+
}
20+
21+
// NewProvider creates a new instance of a Provider
22+
func NewProvider(remoteAddr string, clientConfig *ssh.ClientConfig, logger *log.Logger) *Provider {
23+
return &Provider{
24+
p: newPool(32, remoteAddr, clientConfig),
25+
}
26+
}
27+
28+
// ReadFile acquires a connection from the connection pool and calls ReadFile
29+
// on the acquired SFTP connection.
30+
func (p *Provider) ReadFile(path string) ([]byte, error) {
31+
c, err := p.p.Get()
32+
if err != nil {
33+
return nil, err
34+
}
35+
file, err := c.sftpConn.Open(path)
36+
if err != nil {
37+
// Test the underlying ssh connection to see if it's still okay.
38+
_, _, sshErr := c.sshConn.SendRequest("ping", true, nil)
39+
if sshErr != nil {
40+
c.Close()
41+
} else {
42+
p.p.Put(c)
43+
}
44+
return nil, err
45+
}
46+
defer p.p.Put(c)
47+
defer func() { _ = file.Close() }()
48+
buf := new(bytes.Buffer)
49+
_, err = file.WriteTo(buf)
50+
if err != nil {
51+
return nil, err
52+
}
53+
return buf.Bytes(), nil
54+
}
55+
56+
// ReadDir acquires a connection from the connection pool and calls ReadDir
57+
// on the acquired SFTP connection.
58+
func (p *Provider) ReadDir(path string) ([]os.FileInfo, error) {
59+
c, err := p.p.Get()
60+
if err != nil {
61+
return nil, err
62+
}
63+
listing, err := c.sftpConn.ReadDir(path)
64+
if err != nil {
65+
// Test the underlying ssh connection to see if it's still okay.
66+
_, _, sshErr := c.sshConn.SendRequest("ping", true, nil)
67+
if sshErr != nil {
68+
c.Close()
69+
} else {
70+
p.p.Put(c)
71+
}
72+
return nil, err
73+
}
74+
p.p.Put(c)
75+
return listing, nil
76+
}
77+
78+
// Stat acquires a connection from the connection pool and calls Stat
79+
// on the acquired SFTP connection.
80+
func (p *Provider) Stat(path string) (os.FileInfo, error) {
81+
c, err := p.p.Get()
82+
if err != nil {
83+
return nil, err
84+
}
85+
stat, err := c.sftpConn.Stat(path)
86+
if err != nil {
87+
// Test the underlying ssh connection to see if it's still okay.
88+
_, _, sshErr := c.sshConn.SendRequest("ping", true, nil)
89+
if sshErr != nil {
90+
c.Close()
91+
} else {
92+
p.p.Put(c)
93+
}
94+
return nil, err
95+
}
96+
p.p.Put(c)
97+
return stat, nil
98+
}

config/config.go

+98
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
package config
2+
3+
import (
4+
"crypto/x509"
5+
"encoding/json"
6+
"encoding/pem"
7+
"fmt"
8+
"io/ioutil"
9+
"syscall"
10+
11+
"gopkg.in/go-playground/validator.v9"
12+
13+
"golang.org/x/crypto/ssh"
14+
"golang.org/x/crypto/ssh/terminal"
15+
)
16+
17+
type RemoteConfig struct {
18+
Addr string `validate:"required"`
19+
FileRoot string `validate:"required"`
20+
User string `validate:"required"`
21+
PrivateKeyPath string `validate:"required,file"`
22+
}
23+
24+
type Config struct {
25+
ProxyUser string `validate:"required"`
26+
ProxyPassword string `validate:"required"`
27+
KnownHostsPath string `validate:"required,file"`
28+
29+
Remote RemoteConfig
30+
}
31+
32+
type PasswordReader interface {
33+
ReadPassword(prompt string) ([]byte, error)
34+
}
35+
36+
var pwReader PasswordReader = stdinPasswordReader{}
37+
38+
type stdinPasswordReader struct{}
39+
40+
func (s stdinPasswordReader) ReadPassword(prompt string) ([]byte, error) {
41+
fmt.Print(prompt)
42+
pass, err := terminal.ReadPassword(int(syscall.Stdin))
43+
fmt.Println("")
44+
return pass, err
45+
}
46+
47+
func LoadConfig(path string) (*Config, error) {
48+
configBytes, err := ioutil.ReadFile(path)
49+
if err != nil {
50+
return nil, err
51+
}
52+
cfg := Config{}
53+
err = json.Unmarshal(configBytes, &cfg)
54+
if err != nil {
55+
return nil, err
56+
}
57+
return &cfg, nil
58+
}
59+
60+
func (c *Config) Validate() error {
61+
validate := validator.New()
62+
return validate.Struct(c)
63+
}
64+
65+
func (c *Config) LoadSSHKey() (ssh.Signer, error) {
66+
privateBytes, err := ioutil.ReadFile(c.Remote.PrivateKeyPath)
67+
if err != nil {
68+
return nil, fmt.Errorf("failed to load private key: %s", err)
69+
}
70+
if isPrivateKeyEncrypted(privateBytes) {
71+
for i := 0; i < 3; i++ {
72+
prompt := fmt.Sprintf("Enter passphrase for key '%s': ", c.Remote.PrivateKeyPath)
73+
passphrase, err := pwReader.ReadPassword(prompt)
74+
if err != nil {
75+
return nil, err
76+
}
77+
signer, err := ssh.ParsePrivateKeyWithPassphrase(privateBytes, []byte(passphrase))
78+
if err == nil {
79+
return signer, nil
80+
} else if err != x509.IncorrectPasswordError {
81+
return nil, err
82+
}
83+
}
84+
return nil, x509.IncorrectPasswordError
85+
}
86+
87+
return ssh.ParsePrivateKey(privateBytes)
88+
}
89+
90+
func isPrivateKeyEncrypted(key []byte) bool {
91+
block, _ := pem.Decode(key)
92+
return x509.IsEncryptedPEMBlock(block)
93+
}
94+
95+
func (c *Config) GetDecrpytionPassphrase() ([]byte, error) {
96+
prompt := fmt.Sprintf("Enter passphrase for gocryptfs root at '%s': ", c.Remote.FileRoot)
97+
return pwReader.ReadPassword(prompt)
98+
}

config/config_suite_test.go

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package config_test
2+
3+
import (
4+
"testing"
5+
6+
. "github.com/onsi/ginkgo"
7+
. "github.com/onsi/gomega"
8+
)
9+
10+
func TestConfig(t *testing.T) {
11+
RegisterFailHandler(Fail)
12+
RunSpecs(t, "Config Suite")
13+
}

0 commit comments

Comments
 (0)