Skip to content

Commit

Permalink
Initial Commit
Browse files Browse the repository at this point in the history
  • Loading branch information
iamacarpet committed May 28, 2019
0 parents commit 08f2ad2
Show file tree
Hide file tree
Showing 10 changed files with 491 additions and 0 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
build
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
## SFTP Server with Google Cloud Storage backend

Runs an isolated, sandboxed SFTP server that only interacts with virtual backing storage on Google Cloud Storage (GCS).


Set the following environment variables, e.g.

```
SFTP_USERNAME=user123
SFTP_PASSWORD=kl5dfqpw3NXCZX0
SFTP_PORT=2022
SFTP_SERVER_KEY_PATH=/id_rsa
GCS_CREDENTIALS_FILE=/credentials.json
GCS_BUCKET=my-sftp-bucket
```

GET, PUT, STAT, LIST & MKDIR are currently the only methods implemented.
14 changes: 14 additions & 0 deletions functions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package main

import (
"os"
"log"
)

func mustGetenv(k string) string {
v := os.Getenv(k)
if v == "" {
log.Fatalf("%s environment variable not set.", k)
}
return v
}
28 changes: 28 additions & 0 deletions handler/defines.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package gsftp

import (
"io"
"os"

"cloud.google.com/go/storage"
)

type gcsHandler struct {
client *storage.Client
bucket *storage.BucketHandle
}

type listerat []os.FileInfo

// Modeled after strings.Reader's ReadAt() implementation
func (f listerat) ListAt(ls []os.FileInfo, offset int64) (int, error) {
var n int
if offset >= int64(len(f)) {
return 0, io.EOF
}
n = copy(ls, f[offset:])
if n < len(ls) {
return n, io.EOF
}
return n, nil
}
49 changes: 49 additions & 0 deletions handler/defines_fileinfo.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package gsftp

import (
"os"
"time"

"cloud.google.com/go/storage"
)

type SyntheticFileInfo struct {
objAttr *storage.ObjectAttrs
prefix string
}

func (f *SyntheticFileInfo) Name() string { // base name of the file
if f.objAttr.Prefix == "" {
return f.objAttr.Name[len(f.prefix):]
} else {
return f.objAttr.Prefix[len(f.prefix) : len(f.objAttr.Prefix)-1]
}
}

func (f *SyntheticFileInfo) Size() int64 { // length in bytes for regular files; system-dependent for others
return f.objAttr.Size
}

func (f *SyntheticFileInfo) Mode() os.FileMode { // file mode bits
if f.objAttr.Prefix == "" {
return 0777
} else {
return os.ModeDir | 0777
}
}

func (f *SyntheticFileInfo) ModTime() time.Time { // modification time
if f.objAttr.Prefix == "" {
return f.objAttr.Updated
} else {
return time.Now()
}
}

func (f *SyntheticFileInfo) IsDir() bool { // abbreviation for Mode().IsDir()
return f.objAttr.Prefix == ""
}

func (f *SyntheticFileInfo) Sys() interface{} { // underlying data source (can return nil)
return nil
}
21 changes: 21 additions & 0 deletions handler/defines_readat.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package gsftp

import (
"bytes"
"io"
"io/ioutil"
)

func NewReadAtBuffer(r io.ReadCloser) (io.ReaderAt, error) {
buf, err := ioutil.ReadAll(r)
if err != nil {
return nil, err
}

err = r.Close()
if err != nil {
return nil, err
}

return bytes.NewReader(buf), nil
}
70 changes: 70 additions & 0 deletions handler/defines_writeat.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package gsftp

import (
"io"
"sync"
)

type WriteAtBuffer struct {
buf []byte
m sync.Mutex

// GrowthCoeff defines the growth rate of the internal buffer. By
// default, the growth rate is 1, where expanding the internal
// buffer will allocate only enough capacity to fit the new expected
// length.
GrowthCoeff float64

Writer io.WriteCloser
}

// NewWriteAtBuffer creates a WriteAtBuffer with an internal buffer
// provided by buf.
func NewWriteAtBuffer(w io.WriteCloser, buf []byte) *WriteAtBuffer {
return &WriteAtBuffer{
Writer: w,
buf: buf,
}
}

// WriteAt writes a slice of bytes to a buffer starting at the position provided
// The number of bytes written will be returned, or error. Can overwrite previous
// written slices if the write ats overlap.
func (b *WriteAtBuffer) WriteAt(p []byte, pos int64) (n int, err error) {
pLen := len(p)
expLen := pos + int64(pLen)
b.m.Lock()
defer b.m.Unlock()
if int64(len(b.buf)) < expLen {
if int64(cap(b.buf)) < expLen {
if b.GrowthCoeff < 1 {
b.GrowthCoeff = 1
}
newBuf := make([]byte, expLen, int64(b.GrowthCoeff*float64(expLen)))
copy(newBuf, b.buf)
b.buf = newBuf
}
b.buf = b.buf[:expLen]
}
copy(b.buf[pos:], p)
return pLen, nil
}

// Bytes returns a slice of bytes written to the buffer.
func (b *WriteAtBuffer) Bytes() []byte {
b.m.Lock()
defer b.m.Unlock()
return b.buf
}

func (b *WriteAtBuffer) Close() error {
b.m.Lock()
defer b.m.Unlock()

_, err := b.Writer.Write(b.buf)
if err != nil {
return err
}

return b.Writer.Close()
}
136 changes: 136 additions & 0 deletions handler/handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
package gsftp

import (
"fmt"
"io"
"log"
"os"

"cloud.google.com/go/storage"
"github.com/pkg/sftp"
"google.golang.org/api/iterator"
)

// Example Handlers
func (fs *gcsHandler) Fileread(r *sftp.Request) (io.ReaderAt, error) {
object := fs.bucket.Object(r.Filepath[1:])

log.Printf("Reading file %s", r.Filepath)

reader, err := object.NewReader(r.Context())
if err != nil {
return nil, err
}

return NewReadAtBuffer(reader)
}

func (fs *gcsHandler) Filewrite(r *sftp.Request) (io.WriterAt, error) {
object := fs.bucket.Object(r.Filepath[1:])

log.Printf("Writing file %s", r.Filepath)

writer := object.NewWriter(r.Context())

return NewWriteAtBuffer(writer, []byte{}), nil
}

func (fs *gcsHandler) Filecmd(r *sftp.Request) error {
switch r.Method {
case "Setstat":
return nil
case "Rename":
return fmt.Errorf("not implemented")
case "Rmdir", "Remove":
return fmt.Errorf("not implemented")
case "Mkdir":
object := fs.bucket.Object(r.Filepath[1:] + "/")

log.Printf("Creating directory %s", r.Filepath)

writer := object.NewWriter(r.Context())

err := writer.Close()
return err
case "Symlink":
return fmt.Errorf("not implemented")
}
return nil
}

func (fs *gcsHandler) Filelist(r *sftp.Request) (sftp.ListerAt, error) {
switch r.Method {
case "List":
log.Printf("Listing directory for path %s", r.Filepath)

prefix := r.Filepath[1:]
if prefix != "" {
prefix += "/"
}

objects := fs.bucket.Objects(r.Context(), &storage.Query{
Delimiter: "/",
Prefix: prefix,
})

list := []os.FileInfo{}

for {
objAttrs, err := objects.Next()
if err == iterator.Done {
break
}
if err != nil {
log.Printf("Error iterating directory %s: %s", r.Filepath, err)

return nil, err
}

// Don't include self.
if ((prefix != "") && (objAttrs.Prefix == prefix)) || ((objAttrs.Prefix == "") && (objAttrs.Name == prefix)) {
continue
}

list = append(list, &SyntheticFileInfo{
prefix: prefix,
objAttr: objAttrs,
})
}

return listerat(list), nil
case "Stat":
if r.Filepath == "/" {
return listerat([]os.FileInfo{
&SyntheticFileInfo{
objAttr: &storage.ObjectAttrs{
Prefix: "/",
},
},
}), nil
}

object := fs.bucket.Object(r.Filepath[1:])

log.Printf("Getting file info for %s", r.Filepath)

attrs, err := object.Attrs(r.Context())
if err == storage.ErrObjectNotExist {
object := fs.bucket.Object(r.Filepath[1:] + "/")

log.Printf("Retrying file info for %s", r.Filepath+"/")

attrs, err = object.Attrs(r.Context())
}
if err != nil {
return nil, err
}

file := &SyntheticFileInfo{
objAttr: attrs,
}
return listerat([]os.FileInfo{file}), nil
case "Readlink":
return nil, fmt.Errorf("not implemented")
}
return nil, nil
}
26 changes: 26 additions & 0 deletions handler/new.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package gsftp

import (
"context"
"fmt"

"cloud.google.com/go/storage"
"github.com/pkg/sftp"
"google.golang.org/api/option"
)

func GoogleCloudStorageHandler(ctx context.Context, credentialsFile string, bucketName string) (*sftp.Handlers, error) {
client, err := storage.NewClient(ctx, option.WithCredentialsFile(credentialsFile))
if err != nil {
return nil, fmt.Errorf("Storage Client Error: %s", err)
}

bucket := client.Bucket(bucketName)

handler := &gcsHandler{
client: client,
bucket: bucket,
}

return &sftp.Handlers{handler, handler, handler, handler}, nil
}
Loading

0 comments on commit 08f2ad2

Please sign in to comment.