Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,4 @@
ftpserver
.idea
.vscode
debug
__debug_bin
*.toml
*.json
83 changes: 83 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package config

import (
"encoding/json"
"errors"
"github.com/fclairamb/ftpserver/config/confpar"
"github.com/fclairamb/ftpserver/fs"
"github.com/fclairamb/ftpserverlib/log"
"os"
)

// Config provides the general server config
type Config struct {
fileName string
logger log.Logger
Content *confpar.Content
}

func NewConfig(fileName string, logger log.Logger) (*Config, error) {
if fileName == "" {
fileName = "ftpserver.json"
}
config := &Config{
fileName: fileName,
logger: logger,
}
if err := config.Load(); err != nil {
return nil, err
}
return config, nil
}

func (c *Config) Load() error {
file, errOpen := os.Open(c.fileName)
if errOpen != nil {
return errOpen
}
defer func() {
if errClose := file.Close(); errClose != nil {
c.logger.Error("Cannot close config file", errClose)
}
}()
decoder := json.NewDecoder(file)

// We parse and then copy to allow hot-reload in the future
var content confpar.Content
if errDecode := decoder.Decode(&content); errDecode != nil {
c.logger.Error("Cannot decode file", errDecode)
return errDecode
}
c.Content = &content
return c.Prepare()
}

func (c *Config) Prepare() error {
ct := c.Content
if ct.ListenAddress == "" {
ct.ListenAddress = "0.0.0.0:2121"
}

return nil
// return c.CheckAccesses()
}

func (c *Config) CheckAccesses() error {
for _, access := range c.Content.Accesses {
_, errAccess := fs.LoadFs(&access)
if errAccess != nil {
c.logger.Error("Config: Invalid access !", errAccess, "username", access.User, "fs", access.Fs)
return errAccess
}
}
return nil
}

func (c *Config) GetAccess(user string, pass string) (*confpar.Access, error) {
for _, a := range c.Content.Accesses {
if a.User == user && a.Pass == pass {
return &a, nil
}
}
return nil, errors.New("unknown user")
}
17 changes: 17 additions & 0 deletions config/confpar/confpar.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package confpar

// Access provides rules around any access
type Access struct {
User string `json:"user"` // User authenticating
Pass string `json:"pass"` // Password used for authentication
Fs string `json:"fs"` // Backend used for accessing file
Params map[string]string `json:"params"` // Backend parameters
}

// Content defines the content of the config file
type Content struct {
Version int `json:"version"` // File format version
ListenAddress string `json:"listen_address"` // Address to listen on
MaxClients int `json:"max_clients"` // Maximum clients who can connect at any given time
Accesses []Access `json:"accesses"` // Accesses offered to users
}
15 changes: 15 additions & 0 deletions fs/afero_os/afero_os.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package afero_os

import (
"errors"
"github.com/fclairamb/ftpserver/config/confpar"
"github.com/spf13/afero"
)

func LoadFs(access *confpar.Access) (afero.Fs, error) {
basePath := access.Params["basePath"]
if basePath == "" {
return nil, errors.New("basePath must be specified")
}
return afero.NewBasePathFs(afero.NewOsFs(), basePath), nil
}
37 changes: 37 additions & 0 deletions fs/afero_sftp/afero_sftp.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package afero_sftp

import (
"fmt"
"github.com/fclairamb/ftpserver/config/confpar"
"github.com/pkg/sftp"
"github.com/spf13/afero"
"github.com/spf13/afero/sftpfs"
"golang.org/x/crypto/ssh"
"net"
)

func LoadFs(access *confpar.Access) (afero.Fs, error) {
par := access.Params
config := &ssh.ClientConfig{
User: par["usename"],
Auth: []ssh.AuthMethod{
ssh.Password(par["password"]),
},
HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
return nil
},
}

// Dial your ssh server.
conn, errSsh := ssh.Dial("tcp", par["hostname"], config)
if errSsh != nil {
return nil, fmt.Errorf("unable to connect: %s", errSsh)
}

client, errSftp := sftp.NewClient(conn)
if errSftp != nil {
return nil, fmt.Errorf("unable to setup sftp: %s", errSftp)
}

return sftpfs.New(client), nil
}
23 changes: 23 additions & 0 deletions fs/fs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package fs

import (
"fmt"
"github.com/fclairamb/ftpserver/config/confpar"
"github.com/fclairamb/ftpserver/fs/afero_os"
"github.com/fclairamb/ftpserver/fs/afero_sftp"
afero_s3 "github.com/fclairamb/ftpserver/fs/s3"
"github.com/spf13/afero"
)

func LoadFs(access *confpar.Access) (afero.Fs, error) {
switch access.Fs {
case "os":
return afero_os.LoadFs(access)
case "s3":
return afero_s3.LoadFs(access)
case "sftp":
return afero_sftp.LoadFs(access)
default:
return nil, fmt.Errorf("Fs not supported: %s", access.Fs)
}
}
34 changes: 34 additions & 0 deletions fs/s3/afero_s3.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package afero_s3

import (
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/s3"
afero_s3 "github.com/fclairamb/afero-s3"
// "github.com/chonthu/aferoS3"
"github.com/fclairamb/ftpserver/config/confpar"
"github.com/spf13/afero"
)

func LoadFs(access *confpar.Access) (afero.Fs, error) {
region := access.Params["region"]
bucket := access.Params["bucket"]
keyId := access.Params["access_key_id"]
secretAccessKey := access.Params["secret_access_key"]

sess, errSession := session.NewSession(&aws.Config{
Region: &region,
Credentials: credentials.NewStaticCredentials(keyId, secretAccessKey, ""),
})

if errSession != nil {
return nil, errSession
}

s3Int := s3.New(sess)
s3Fs := afero_s3.NewFs(bucket, s3Int)
// withoutTrailingSlash := stripslash.NewStripSlashPathFs(s3Fs, 1)
return s3Fs, nil
// return aferoS3.NewS3Fs(sess, bucket), nil
}
144 changes: 144 additions & 0 deletions fs/stripslash/stripslash.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
package stripslash

import (
"fmt"
"github.com/spf13/afero"
"os"
"time"
)

type StripSlashPathFs struct {
source afero.Fs
start int
}

type StripSlashPathFile struct {
afero.File
start int
}

func (f *StripSlashPathFile) Name() string {
return f.File.Name()[f.start:]
}

func NewStripSlashPathFs(source afero.Fs, start int) afero.Fs {
return &StripSlashPathFs{source: source, start: start}
}

// on a afero.File outside the base path it returns the given afero.File name and an error,
// else the given afero.File with the base path prepended
func (b *StripSlashPathFs) RealPath(name string) (path string, err error) {
if len(name) > b.start {
return "", fmt.Errorf("path needs to at least %d chars long", b.start)
}

return name[b.start:], nil
}

func (b *StripSlashPathFs) Chtimes(name string, atime, mtime time.Time) (err error) {
if name, err = b.RealPath(name); err != nil {
return &os.PathError{Op: "chtimes", Path: name, Err: err}
}
return b.source.Chtimes(name, atime, mtime)
}

func (b *StripSlashPathFs) Chmod(name string, mode os.FileMode) (err error) {
if name, err = b.RealPath(name); err != nil {
return &os.PathError{Op: "chmod", Path: name, Err: err}
}
return b.source.Chmod(name, mode)
}

func (b *StripSlashPathFs) Name() string {
return "StripSlashPathFs"
}

func (b *StripSlashPathFs) Stat(name string) (fi os.FileInfo, err error) {
if name, err = b.RealPath(name); err != nil {
return nil, &os.PathError{Op: "stat", Path: name, Err: err}
}
return b.source.Stat(name)
}

func (b *StripSlashPathFs) Rename(oldname, newname string) (err error) {
if oldname, err = b.RealPath(oldname); err != nil {
return &os.PathError{Op: "rename", Path: oldname, Err: err}
}
if newname, err = b.RealPath(newname); err != nil {
return &os.PathError{Op: "rename", Path: newname, Err: err}
}
return b.source.Rename(oldname, newname)
}

func (b *StripSlashPathFs) RemoveAll(name string) (err error) {
if name, err = b.RealPath(name); err != nil {
return &os.PathError{Op: "remove_all", Path: name, Err: err}
}
return b.source.RemoveAll(name)
}

func (b *StripSlashPathFs) Remove(name string) (err error) {
if name, err = b.RealPath(name); err != nil {
return &os.PathError{Op: "remove", Path: name, Err: err}
}
return b.source.Remove(name)
}

func (b *StripSlashPathFs) OpenFile(name string, flag int, mode os.FileMode) (f afero.File, err error) {
if name, err = b.RealPath(name); err != nil {
return nil, &os.PathError{Op: "openfile", Path: name, Err: err}
}
sourcef, err := b.source.OpenFile(name, flag, mode)
if err != nil {
return nil, err
}
return &StripSlashPathFile{File: sourcef, start: b.start}, nil
}

func (b *StripSlashPathFs) Open(name string) (f afero.File, err error) {
if name, err = b.RealPath(name); err != nil {
return nil, &os.PathError{Op: "open", Path: name, Err: err}
}
sourcef, err := b.source.Open(name)
if err != nil {
return nil, err
}
return &StripSlashPathFile{File: sourcef, start: b.start}, nil
}

func (b *StripSlashPathFs) Mkdir(name string, mode os.FileMode) (err error) {
if name, err = b.RealPath(name); err != nil {
return &os.PathError{Op: "mkdir", Path: name, Err: err}
}
return b.source.Mkdir(name, mode)
}

func (b *StripSlashPathFs) MkdirAll(name string, mode os.FileMode) (err error) {
if name, err = b.RealPath(name); err != nil {
return &os.PathError{Op: "mkdir", Path: name, Err: err}
}
return b.source.MkdirAll(name, mode)
}

func (b *StripSlashPathFs) Create(name string) (f afero.File, err error) {
if name, err = b.RealPath(name); err != nil {
return nil, &os.PathError{Op: "create", Path: name, Err: err}
}
sourcef, err := b.source.Create(name)
if err != nil {
return nil, err
}
return &StripSlashPathFile{File: sourcef, start: b.start}, nil
}

func (b *StripSlashPathFs) LstatIfPossible(name string) (os.FileInfo, bool, error) {
name, err := b.RealPath(name)
if err != nil {
return nil, false, &os.PathError{Op: "lstat", Path: name, Err: err}
}
if lstater, ok := b.source.(afero.Lstater); ok {
return lstater.LstatIfPossible(name)
}
fi, err := b.source.Stat(name)
return fi, false, err
}
16 changes: 10 additions & 6 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
module github.com/fclairamb/ftpserver

go 1.14

require (
github.com/aws/aws-sdk-go v1.30.29
github.com/fclairamb/afero-s3 v0.0.0
github.com/fclairamb/ftpserverlib v0.6.1-0.20200510182933-a33aa704d7c2
github.com/go-kit/kit v0.10.0
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/naoina/go-stringutil v0.1.0 // indirect
github.com/naoina/toml v0.1.1
github.com/secsy/goftp v0.0.0-20190720192957-f31499d7c79a
github.com/pkg/sftp v1.11.0
github.com/spf13/afero v1.2.2
gopkg.in/dutchcoders/goftp.v1 v1.0.0-20170301105846-ed59a591ce14
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550
)

go 1.13
replace github.com/fclairamb/ftpserverlib => /Users/florent/go/src/github.com/fclairamb/ftpserverlib

replace github.com/fclairamb/afero-s3 => /Users/florent/go/src/github.com/fclairamb/afero-s3
Loading